Compare commits

..

6 Commits

Author SHA1 Message Date
openhands 367e82c69c Remove feedback module imports and router inclusion 2025-06-09 22:37:44 +00:00
openhands 1fa03176de Update package-lock.json to remove @remix-run/router dependency 2025-06-09 22:34:14 +00:00
openhands e57efa1ef5 Delete feedback.py files instead of keeping them empty 2025-06-09 22:33:00 +00:00
openhands 1601ed5ed8 Remove unintended package dependency 2025-06-09 22:30:09 +00:00
openhands 6d82e446cd Remove comments about feedback functionality 2025-06-09 22:28:15 +00:00
openhands aaf1718406 Remove feedback functionality and thumbs up/down buttons 2025-06-09 22:23:18 +00:00
30 changed files with 74 additions and 787 deletions
+4 -22
View File
@@ -1,23 +1,5 @@
# NodeJS
frontend/node_modules
# Configuration (except pyproject.toml)
*.ini
*.toml
!pyproject.toml
*.yml
# Documentation (except README.md)
*.md
!README.md
# Hidden files and directories
.*
__pycache__
# Unneded files and directories
/dev_config/
/docs/
/evaluation/
/tests/
CITATION.cff
config.toml
.envrc
.env
.git
-6
View File
@@ -72,9 +72,3 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directories:
- "containers/*"
schedule:
interval: "weekly"
+7 -90
View File
@@ -2,6 +2,8 @@ This repository contains the code for OpenHands, an automated AI software engine
(in the `openhands` directory) and React frontend (in the `frontend` directory).
## General Setup:
To set up the entire repo, including frontend and backend, run `make build`.
You don't need to do this unless the user asks you to, or if you're trying to run the entire application.
IMPORTANT: Before making any changes to the codebase, ALWAYS run `make install-pre-commit-hooks` to ensure pre-commit hooks are properly installed.
@@ -19,91 +21,13 @@ then re-run the command to ensure it passes. Common issues include:
- Trailing whitespace
- Missing newlines at end of files
## Testing and Debugging
### Environment Setup for Testing
- Run `make build` to install all dependencies (only necessary for running tests):
```bash
make build
```
**IMPORTANT**: When using `execute_bash` to run `make build` or similar long-running commands, set the `timeout` parameter to a high value (e.g., 600 seconds):
```
execute_bash(command="make build", timeout=600)
```
#### Docker Installation
**NOTE: Docker installation is ONLY required for running runtime tests with the Docker runtime.**
- Install Docker on Debian-based systems:
```bash
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
```
- Start Docker daemon (in container environments without systemd):
```bash
sudo dockerd > /tmp/docker.log 2>&1 & sleep 5
```
- Verify Docker installation:
```bash
sudo docker run hello-world
```
#### Development Environment Setup
- Before running `make run`, ensure netcat is installed:
```bash
sudo apt-get install -y netcat-openbsd
```
### Unit Tests
- All unit tests are in `tests/unit/test_*.py`
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
- Write all tests with pytest
### Runtime Tests
- Runtime tests are in `tests/runtime/test_*.py`
- Run tests with different runtime implementations by setting the `TEST_RUNTIME` environment variable:
```bash
# Use Docker runtime (default)
DEBUG=1 poetry run pytest -vvxss tests/runtime/test_bash.py
# Use CLI runtime (more reliable in some environments)
DEBUG=1 TEST_RUNTIME=cli poetry run pytest -vvxss tests/runtime/test_bash.py
# Run a specific test
DEBUG=1 TEST_RUNTIME=cli poetry run pytest -vvxss tests/runtime/test_bash.py::test_bash_server
```
- **IMPORTANT**: Runtime tests can take a long time to run, especially when building Docker images. Set a high timeout value:
```
execute_bash(command="DEBUG=1 poetry run pytest -vvxss tests/runtime/test_bash.py", timeout=600)
```
- The `DEBUG=1` flag enables more verbose logging
- The `-vvxss` flags make the test output more verbose and stop after the first failure
### Debugging Docker Issues
- Check Docker container status:
```bash
sudo docker ps -a
```
- View Docker logs:
```bash
sudo docker logs <container_id>
```
- Check Docker daemon logs:
```bash
sudo cat /tmp/docker.log | tail -n 100
```
- Check OpenHands logs:
```bash
cat logs/openhands_*.log | grep -i error | tail -n 20
```
## Repository Structure
Backend:
- Located in the `openhands` directory
- Testing:
- All tests are in `tests/unit/test_*.py`
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
- Write all tests with pytest
Frontend:
- Located in the `frontend` directory
@@ -126,13 +50,6 @@ Frontend:
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
## Runtime Architecture
- OpenHands uses a Docker-based runtime for secure execution of agent actions
- The runtime builds a custom Docker image based on a specified base image
- The image includes OpenHands-specific code and the runtime client
- The runtime client executes actions in the sandboxed environment and returns observations
- More details in the [runtime architecture documentation](https://docs.all-hands.dev/usage/architecture/runtime)
## Implementation Details
These details may or may not be useful for your current task.
@@ -163,4 +80,4 @@ These details may or may not be useful for your current task.
- Add the translation key to `frontend/src/i18n/declaration.ts`
2. Add the setting to the backend:
- Add the setting to the `Settings` model in `openhands/storage/data_models/settings.py`
- Update any relevant backend code to apply the setting (e.g., in session creation)
- Update any relevant backend code to apply the setting (e.g., in session creation)
+15 -11
View File
@@ -1,16 +1,16 @@
ARG OPENHANDS_BUILD_VERSION=dev
FROM node:22.16.0-bookworm-slim AS frontend-builder
FROM node:21.7.2-bookworm-slim AS frontend-builder
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json ./
COPY ./frontend/package.json frontend/package-lock.json ./
RUN npm install -g npm@10.5.1
RUN npm ci
COPY frontend ./
COPY ./frontend ./
RUN npm run build
FROM python:3.12.10-slim AS base
FROM base AS backend-builder
FROM python:3.12.3-slim AS backend-builder
WORKDIR /app
ENV PYTHONPATH='/app'
@@ -22,18 +22,17 @@ ENV POETRY_NO_INTERACTION=1 \
RUN apt-get update -y \
&& apt-get install -y curl make git build-essential \
&& python3 -m pip install poetry --break-system-packages
&& python3 -m pip install poetry==1.8.2 --break-system-packages
COPY pyproject.toml poetry.lock ./
COPY ./pyproject.toml ./poetry.lock ./
RUN touch README.md
RUN export POETRY_CACHE_DIR && poetry install --no-root && rm -rf $POETRY_CACHE_DIR
FROM base AS openhands-app
FROM python:3.12.3-slim AS openhands-app
WORKDIR /app
# re-declare for this section
ARG OPENHANDS_BUILD_VERSION
ARG OPENHANDS_BUILD_VERSION #re-declare for this section
ENV RUN_AS_OPENHANDS=true
# A random number--we need this to be different from the user's UID on the host machine
@@ -75,7 +74,12 @@ COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${V
COPY --chown=openhands:app --chmod=770 ./microagents ./microagents
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
COPY --chown=openhands:app pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./
COPY --chown=openhands:app --chmod=770 ./openhands/agenthub ./openhands/agenthub
COPY --chown=openhands:app ./pyproject.toml ./pyproject.toml
COPY --chown=openhands:app ./poetry.lock ./poetry.lock
COPY --chown=openhands:app ./README.md ./README.md
COPY --chown=openhands:app ./MANIFEST.in ./MANIFEST.in
COPY --chown=openhands:app ./LICENSE ./LICENSE
# This is run as "openhands" user, and will create __pycache__ with openhands:openhands ownership
RUN python openhands/core/download.py # No-op to download assets
@@ -4,6 +4,7 @@ import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import type { Message } from "#/message";
import { SUGGESTIONS } from "#/utils/suggestions";
import { WsClientProviderStatus } from "#/context/ws-client-provider";
import { ChatInterface } from "#/components/features/chat/chat-interface";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -18,7 +19,7 @@ describe("Empty state", () => {
const { useWsClient: useWsClientMock } = vi.hoisted(() => ({
useWsClient: vi.fn(() => ({
send: sendMock,
status: "CONNECTED",
status: WsClientProviderStatus.CONNECTED,
isLoadingMessages: false,
})),
}));
@@ -63,7 +64,7 @@ describe("Empty state", () => {
// this is to test that the message is in the UI before the socket is called
useWsClientMock.mockImplementation(() => ({
send: sendMock,
status: "CONNECTED",
status: WsClientProviderStatus.CONNECTED,
isLoadingMessages: false,
}));
const user = userEvent.setup();
@@ -86,7 +87,7 @@ describe("Empty state", () => {
async () => {
useWsClientMock.mockImplementation(() => ({
send: sendMock,
status: "CONNECTED",
status: WsClientProviderStatus.CONNECTED,
isLoadingMessages: false,
}));
const user = userEvent.setup();
@@ -100,7 +101,7 @@ describe("Empty state", () => {
useWsClientMock.mockImplementation(() => ({
send: sendMock,
status: "CONNECTED",
status: WsClientProviderStatus.CONNECTED,
isLoadingMessages: false,
}));
rerender(<ChatInterface />);
@@ -1,76 +0,0 @@
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { TrajectoryActions } from "#/components/features/trajectory/trajectory-actions";
describe("TrajectoryActions", () => {
const user = userEvent.setup();
const onPositiveFeedback = vi.fn();
const onNegativeFeedback = vi.fn();
const onExportTrajectory = vi.fn();
afterEach(() => {
vi.clearAllMocks();
});
it("should render correctly", () => {
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
/>,
);
const actions = screen.getByTestId("feedback-actions");
within(actions).getByTestId("positive-feedback");
within(actions).getByTestId("negative-feedback");
within(actions).getByTestId("export-trajectory");
});
it("should call onPositiveFeedback when positive feedback is clicked", async () => {
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
/>,
);
const positiveFeedback = screen.getByTestId("positive-feedback");
await user.click(positiveFeedback);
expect(onPositiveFeedback).toHaveBeenCalled();
});
it("should call onNegativeFeedback when negative feedback is clicked", async () => {
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
/>,
);
const negativeFeedback = screen.getByTestId("negative-feedback");
await user.click(negativeFeedback);
expect(onNegativeFeedback).toHaveBeenCalled();
});
it("should call onExportTrajectory when export button is clicked", async () => {
renderWithProviders(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
/>,
);
const exportButton = screen.getByTestId("export-trajectory");
await user.click(exportButton);
expect(onExportTrajectory).toHaveBeenCalled();
});
});
@@ -1,68 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
// Mock useParams before importing components
vi.mock("react-router", async () => {
const actual = await vi.importActual("react-router");
return {
...(actual as object),
useParams: () => ({ conversationId: "test-conversation-id" }),
};
});
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { FeedbackForm } from "#/components/features/feedback/feedback-form";
import { I18nKey } from "#/i18n/declaration";
describe("FeedbackForm", () => {
const user = userEvent.setup();
const onCloseMock = vi.fn();
afterEach(() => {
vi.clearAllMocks();
});
it("should render correctly", () => {
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
screen.getByLabelText(I18nKey.FEEDBACK$EMAIL_LABEL);
screen.getByLabelText(I18nKey.FEEDBACK$PRIVATE_LABEL);
screen.getByLabelText(I18nKey.FEEDBACK$PUBLIC_LABEL);
screen.getByRole("button", { name: I18nKey.FEEDBACK$SHARE_LABEL });
screen.getByRole("button", { name: I18nKey.FEEDBACK$CANCEL_LABEL });
});
it("should switch between private and public permissions", async () => {
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
const privateRadio = screen.getByLabelText(I18nKey.FEEDBACK$PRIVATE_LABEL);
const publicRadio = screen.getByLabelText(I18nKey.FEEDBACK$PUBLIC_LABEL);
expect(privateRadio).toBeChecked(); // private is the default value
expect(publicRadio).not.toBeChecked();
await user.click(publicRadio);
expect(publicRadio).toBeChecked();
expect(privateRadio).not.toBeChecked();
await user.click(privateRadio);
expect(privateRadio).toBeChecked();
expect(publicRadio).not.toBeChecked();
});
it("should call onClose when the close button is clicked", async () => {
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
await user.click(
screen.getByRole("button", { name: I18nKey.FEEDBACK$CANCEL_LABEL }),
);
expect(onCloseMock).toHaveBeenCalled();
});
});
-16
View File
@@ -1,7 +1,5 @@
import { AxiosHeaders } from "axios";
import {
Feedback,
FeedbackResponse,
GitHubAccessTokenResponse,
GetConfigResponse,
GetVSCodeUrlResponse,
@@ -95,20 +93,6 @@ class OpenHands {
return headers;
}
/**
* Send feedback to the server
* @param data Feedback data
* @returns The stored feedback data
*/
static async submitFeedback(
conversationId: string,
feedback: Feedback,
): Promise<FeedbackResponse> {
const url = `/api/conversations/${conversationId}/submit-feedback`;
const { data } = await openHands.post<FeedbackResponse>(url, feedback);
return data;
}
/**
* Authenticate with GitHub token
* @returns Response with authentication status and user info if successful
-20
View File
@@ -14,17 +14,6 @@ export interface FileUploadSuccessResponse {
skipped_files: { name: string; reason: string }[];
}
export interface FeedbackBodyResponse {
message: string;
feedback_id: string;
password: string;
}
export interface FeedbackResponse {
statusCode: number;
body: FeedbackBodyResponse;
}
export interface GitHubAccessTokenResponse {
access_token: string;
}
@@ -34,15 +23,6 @@ export interface AuthenticationResponse {
login?: string; // Only present when allow list is enabled
}
export interface Feedback {
version: string;
email: string;
token: string;
polarity: "positive" | "negative";
permissions: "public" | "private";
trajectory: unknown[];
}
export interface GetConfigResponse {
APP_MODE: "saas" | "oss";
APP_SLUG?: string;
-6
View File
@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0" style="enable-background:new 0 0 468 222.5" version="1.1" viewBox="0 0 468 222.5">
<style>
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#635bff}
</style>
<path d="M414 113.4c0-25.6-12.4-45.8-36.1-45.8-23.8 0-38.2 20.2-38.2 45.6 0 30.1 17 45.3 41.4 45.3 11.9 0 20.9-2.7 27.7-6.5v-20c-6.8 3.4-14.6 5.5-24.5 5.5-9.7 0-18.3-3.4-19.4-15.2h48.9c0-1.3.2-6.5.2-8.9zm-49.4-9.5c0-11.3 6.9-16 13.2-16 6.1 0 12.6 4.7 12.6 16h-25.8zM301.1 67.6c-9.8 0-16.1 4.6-19.6 7.8l-1.3-6.2h-22v116.6l25-5.3.1-28.3c3.6 2.6 8.9 6.3 17.7 6.3 17.9 0 34.2-14.4 34.2-46.1-.1-29-16.6-44.8-34.1-44.8zm-6 68.9c-5.9 0-9.4-2.1-11.8-4.7l-.1-37.1c2.6-2.9 6.2-4.9 11.9-4.9 9.1 0 15.4 10.2 15.4 23.3 0 13.4-6.2 23.4-15.4 23.4zM223.8 61.7l25.1-5.4V36l-25.1 5.3zM223.8 69.3h25.1v87.5h-25.1zM196.9 76.7l-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7 15.9-6.3 19-5.2v-23c-3.2-1.2-14.9-3.4-20.8 7.4zM146.9 47.6l-24.4 5.2-.1 80.1c0 14.8 11.1 25.7 25.9 25.7 8.2 0 14.2-1.5 17.5-3.3V135c-3.2 1.3-19 5.9-19-8.9V90.6h19V69.3h-19l.1-21.7zM79.3 94.7c0-3.9 3.2-5.4 8.5-5.4 7.6 0 17.2 2.3 24.8 6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6C67.5 67.6 54 78.2 54 95.9c0 27.6 38 23.2 38 35.1 0 4.6-4 6.1-9.6 6.1-8.3 0-18.9-3.4-27.3-8v23.8c9.3 4 18.7 5.7 27.3 5.7 20.8 0 35.1-10.3 35.1-28.2-.1-29.8-38.2-24.5-38.2-35.7z" class="st0"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -11,7 +11,6 @@ import { InteractiveChatBox } from "./interactive-chat-box";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { FeedbackModal } from "../feedback/feedback-modal";
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
import { TypingIndicator } from "./typing-indicator";
import { useWsClient } from "#/context/ws-client-provider";
@@ -50,10 +49,6 @@ export function ChatInterface() {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const [feedbackPolarity, setFeedbackPolarity] = React.useState<
"positive" | "negative"
>("positive");
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
const [messageToSend, setMessageToSend] = React.useState<string | null>(null);
const { selectedRepository, replayJson } = useSelector(
(state: RootState) => state.initialQuery,
@@ -96,13 +91,6 @@ export function ChatInterface() {
send(generateAgentStateChangeEvent(AgentState.STOPPED));
};
const onClickShareFeedbackActionButton = async (
polarity: "positive" | "negative",
) => {
setFeedbackModalIsOpen(true);
setFeedbackPolarity(polarity);
};
const onClickExportTrajectoryButton = () => {
if (!params.conversationId) {
displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR));
@@ -164,12 +152,6 @@ export function ChatInterface() {
<div className="flex flex-col gap-[6px] px-4 pb-4">
<div className="flex justify-between relative">
<TrajectoryActions
onPositiveFeedback={() =>
onClickShareFeedbackActionButton("positive")
}
onNegativeFeedback={() =>
onClickShareFeedbackActionButton("negative")
}
onExportTrajectory={() => onClickExportTrajectoryButton()}
/>
@@ -194,12 +176,6 @@ export function ChatInterface() {
onChange={setMessageToSend}
/>
</div>
<FeedbackModal
isOpen={feedbackModalIsOpen}
onClose={() => setFeedbackModalIsOpen(false)}
polarity={feedbackPolarity}
/>
</div>
);
}
@@ -9,7 +9,10 @@ import {
AGENT_STATUS_MAP,
IndicatorColor,
} from "../../agent-status-map.constant";
import { useWsClient } from "#/context/ws-client-provider";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useNotification } from "#/hooks/useNotification";
import { browserTab } from "#/utils/browser-tab";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
@@ -77,13 +80,10 @@ export function AgentStatusBar() {
);
React.useEffect(() => {
if (conversation?.status === "CONNECTING") {
setStatusMessage(t(I18nKey.STATUS$CONNECTING_TO_RUNTIME));
setIndicatorColor(IndicatorColor.YELLOW);
} else if (conversation?.status === "STARTING") {
if (conversation?.status === "STARTING") {
setStatusMessage(t(I18nKey.STATUS$STARTING_RUNTIME));
setIndicatorColor(IndicatorColor.RED);
} else if (status === "DISCONNECTED") {
} else if (status === WsClientProviderStatus.DISCONNECTED) {
setStatusMessage(t(I18nKey.STATUS$WEBSOCKET_CLOSED));
setIndicatorColor(IndicatorColor.RED);
} else {
@@ -2,20 +2,9 @@ import ColdIcon from "./state-indicators/cold.svg?react";
import RunningIcon from "./state-indicators/running.svg?react";
type SVGIcon = React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
export type ProjectStatus =
| "RUNNING"
| "STOPPED"
| "STARTING"
| "CONNECTING"
| "CONNECTED"
| "DISCONNECTED";
export type ProjectStatus = "RUNNING" | "STOPPED" | "STARTING";
type ProjectStatusWithIcon = Exclude<
ProjectStatus,
"CONNECTING" | "CONNECTED" | "DISCONNECTED"
>;
const INDICATORS: Record<ProjectStatusWithIcon, SVGIcon> = {
const INDICATORS: Record<ProjectStatus, SVGIcon> = {
STOPPED: ColdIcon,
RUNNING: RunningIcon,
STARTING: ColdIcon,
@@ -28,7 +17,6 @@ interface ConversationStateIndicatorProps {
export function ConversationStateIndicator({
status,
}: ConversationStateIndicatorProps) {
// @ts-expect-error - Type 'ProjectStatus' is not assignable to type 'ProjectStatusWithIcon'.
const StateIcon = INDICATORS[status];
return (
@@ -1,152 +0,0 @@
import React from "react";
import hotToast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Feedback } from "#/api/open-hands.types";
import { useSubmitFeedback } from "#/hooks/mutation/use-submit-feedback";
import { BrandButton } from "../settings/brand-button";
const FEEDBACK_VERSION = "1.0";
const VIEWER_PAGE = "https://www.all-hands.dev/share";
interface FeedbackFormProps {
onClose: () => void;
polarity: "positive" | "negative";
}
export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
const { t } = useTranslation();
const copiedToClipboardToast = () => {
hotToast(t(I18nKey.FEEDBACK$PASSWORD_COPIED_MESSAGE), {
icon: "📋",
position: "bottom-right",
});
};
const onPressToast = (password: string) => {
navigator.clipboard.writeText(password);
copiedToClipboardToast();
};
const shareFeedbackToast = (
message: string,
link: string,
password: string,
) => {
hotToast(
<div className="flex flex-col gap-1">
<span>{message}</span>
<a
data-testid="toast-share-url"
className="text-blue-500 underline"
onClick={() => onPressToast(password)}
href={link}
target="_blank"
rel="noreferrer"
>
{t(I18nKey.FEEDBACK$GO_TO_FEEDBACK)}
</a>
<span onClick={() => onPressToast(password)} className="cursor-pointer">
{t(I18nKey.FEEDBACK$PASSWORD)}: {password}{" "}
<span className="text-gray-500">
({t(I18nKey.FEEDBACK$COPY_LABEL)})
</span>
</span>
</div>,
{ duration: 10000 },
);
};
const { mutate: submitFeedback, isPending } = useSubmitFeedback();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event?.preventDefault();
const formData = new FormData(event.currentTarget);
const email = formData.get("email")?.toString() || "";
const permissions = (formData.get("permissions")?.toString() ||
"private") as "private" | "public";
const feedback: Feedback = {
version: FEEDBACK_VERSION,
email,
polarity,
permissions,
trajectory: [],
token: "",
};
submitFeedback(
{ feedback },
{
onSuccess: (data) => {
const { message, feedback_id, password } = data.body; // eslint-disable-line
const link = `${VIEWER_PAGE}?share_id=${feedback_id}`;
shareFeedbackToast(message, link, password);
onClose();
},
},
);
};
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-6 w-full">
<label className="flex flex-col gap-2">
<span className="text-xs text-neutral-400">
{t(I18nKey.FEEDBACK$EMAIL_LABEL)}
</span>
<input
required
name="email"
type="email"
placeholder={t(I18nKey.FEEDBACK$EMAIL_PLACEHOLDER)}
className="bg-[#27272A] px-3 py-[10px] rounded"
/>
</label>
<div className="flex gap-4 text-neutral-400">
<label className="flex gap-2 cursor-pointer">
<input
name="permissions"
value="private"
type="radio"
defaultChecked
/>
{t(I18nKey.FEEDBACK$PRIVATE_LABEL)}
</label>
<label className="flex gap-2 cursor-pointer">
<input name="permissions" value="public" type="radio" />
{t(I18nKey.FEEDBACK$PUBLIC_LABEL)}
</label>
</div>
<div className="flex gap-2">
<BrandButton
type="submit"
variant="primary"
className="grow"
isDisabled={isPending}
>
{isPending
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL)
: t(I18nKey.FEEDBACK$SHARE_LABEL)}
</BrandButton>
<BrandButton
type="button"
variant="secondary"
className="grow"
onClick={onClose}
isDisabled={isPending}
>
{t(I18nKey.FEEDBACK$CANCEL_LABEL)}
</BrandButton>
</div>
{isPending && (
<p className="text-sm text-center text-neutral-400">
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE)}
</p>
)}
</form>
);
}
@@ -1,34 +0,0 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import {
BaseModalTitle,
BaseModalDescription,
} from "#/components/shared/modals/confirmation-modals/base-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { FeedbackForm } from "./feedback-form";
interface FeedbackModalProps {
onClose: () => void;
isOpen: boolean;
polarity: "positive" | "negative";
}
export function FeedbackModal({
onClose,
isOpen,
polarity,
}: FeedbackModalProps) {
const { t } = useTranslation();
if (!isOpen) return null;
return (
<ModalBackdrop onClose={onClose}>
<ModalBody className="border border-tertiary">
<BaseModalTitle title={t(I18nKey.FEEDBACK$TITLE)} />
<BaseModalDescription description={t(I18nKey.FEEDBACK$DESCRIPTION)} />
<FeedbackForm onClose={onClose} polarity={polarity} />
</ModalBody>
</ModalBackdrop>
);
}
@@ -9,7 +9,6 @@ import { BrandButton } from "../settings/brand-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { amountIsValid } from "#/utils/amount-is-valid";
import { I18nKey } from "#/i18n/declaration";
import { PoweredByStripeTag } from "./powered-by-stripe-tag";
export function PaymentForm() {
const { t } = useTranslation();
@@ -80,7 +79,6 @@ export function PaymentForm() {
{t(I18nKey.PAYMENT$ADD_CREDIT)}
</BrandButton>
{isPending && <LoadingSpinner size="small" />}
<PoweredByStripeTag />
</div>
</div>
</form>
@@ -1,16 +0,0 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import stripeLogo from "#/assets/stripe.svg";
export function PoweredByStripeTag() {
const { t } = useTranslation();
return (
<div className="flex flex-row items-center">
<span className="text-medium font-semi-bold">
{t(I18nKey.BILLING$POWERED_BY)}
</span>
<img src={stripeLogo} alt="Stripe" className="h-8" />
</div>
);
}
@@ -1,37 +1,19 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import ThumbsUpIcon from "#/icons/thumbs-up.svg?react";
import ThumbDownIcon from "#/icons/thumbs-down.svg?react";
import ExportIcon from "#/icons/export.svg?react";
import { TrajectoryActionButton } from "#/components/shared/buttons/trajectory-action-button";
interface TrajectoryActionsProps {
onPositiveFeedback: () => void;
onNegativeFeedback: () => void;
onExportTrajectory: () => void;
}
export function TrajectoryActions({
onPositiveFeedback,
onNegativeFeedback,
onExportTrajectory,
}: TrajectoryActionsProps) {
const { t } = useTranslation();
return (
<div data-testid="feedback-actions" className="flex gap-1">
<TrajectoryActionButton
testId="positive-feedback"
onClick={onPositiveFeedback}
icon={<ThumbsUpIcon width={15} height={15} />}
tooltip={t(I18nKey.BUTTON$MARK_HELPFUL)}
/>
<TrajectoryActionButton
testId="negative-feedback"
onClick={onNegativeFeedback}
icon={<ThumbDownIcon width={15} height={15} />}
tooltip={t(I18nKey.BUTTON$MARK_NOT_HELPFUL)}
/>
<div data-testid="trajectory-actions" className="flex gap-1">
<TrajectoryActionButton
testId="export-trajectory"
onClick={onExportTrajectory}
+15 -8
View File
@@ -28,7 +28,6 @@ import {
} from "#/types/core/guards";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
const hasValidMessageProperty = (obj: unknown): obj is { message: string } =>
typeof obj === "object" &&
@@ -68,8 +67,14 @@ const isMessageAction = (
): event is UserMessageAction | AssistantMessageAction =>
isUserMessage(event) || isAssistantMessage(event);
export enum WsClientProviderStatus {
CONNECTED,
DISCONNECTED,
CONNECTING,
}
interface UseWsClient {
status: ProjectStatus;
status: WsClientProviderStatus;
isLoadingMessages: boolean;
events: Record<string, unknown>[];
parsedEvents: (OpenHandsAction | OpenHandsObservation)[];
@@ -77,7 +82,7 @@ interface UseWsClient {
}
const WsClientContext = React.createContext<UseWsClient>({
status: "DISCONNECTED",
status: WsClientProviderStatus.DISCONNECTED,
isLoadingMessages: true,
events: [],
parsedEvents: [],
@@ -134,7 +139,9 @@ export function WsClientProvider({
const { setErrorMessage, removeErrorMessage } = useWSErrorMessage();
const queryClient = useQueryClient();
const sioRef = React.useRef<Socket | null>(null);
const [status, setStatus] = React.useState<ProjectStatus>("CONNECTING");
const [status, setStatus] = React.useState(
WsClientProviderStatus.DISCONNECTED,
);
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
const [parsedEvents, setParsedEvents] = React.useState<
(OpenHandsAction | OpenHandsObservation)[]
@@ -155,7 +162,7 @@ export function WsClientProvider({
}
function handleConnect() {
setStatus("CONNECTED");
setStatus(WsClientProviderStatus.CONNECTED);
removeErrorMessage();
}
@@ -254,7 +261,7 @@ export function WsClientProvider({
}
function handleDisconnect(data: unknown) {
setStatus("DISCONNECTED");
setStatus(WsClientProviderStatus.DISCONNECTED);
const sio = sioRef.current;
if (!sio) {
return;
@@ -268,7 +275,7 @@ export function WsClientProvider({
function handleError(data: unknown) {
// set status
setStatus("DISCONNECTED");
setStatus(WsClientProviderStatus.DISCONNECTED);
updateStatusWhenErrorMessagePresent(data);
setErrorMessage(
@@ -287,7 +294,7 @@ export function WsClientProvider({
// reset events when conversationId changes
setEvents([]);
setParsedEvents([]);
setStatus("CONNECTING");
setStatus(WsClientProviderStatus.DISCONNECTED);
}, [conversationId]);
React.useEffect(() => {
@@ -1,22 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import { Feedback } from "#/api/open-hands.types";
import OpenHands from "#/api/open-hands";
import { useConversationId } from "#/hooks/use-conversation-id";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
type SubmitFeedbackArgs = {
feedback: Feedback;
};
export const useSubmitFeedback = () => {
const { conversationId } = useConversationId();
return useMutation({
mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
OpenHands.submitFeedback(conversationId, feedback),
onError: (error) => {
displayErrorToast(error.message);
},
retry: 2,
retryDelay: 500,
});
};
@@ -1,6 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import { useWsClient } from "#/context/ws-client-provider";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useConversationId } from "#/hooks/use-conversation-id";
import OpenHands from "#/api/open-hands";
@@ -14,7 +17,7 @@ export const useConversationConfig = () => {
if (!conversationId) throw new Error("No conversation ID");
return OpenHands.getRuntimeId(conversationId);
},
enabled: status !== "DISCONNECTED" && !!conversationId,
enabled: status !== WsClientProviderStatus.DISCONNECTED && !!conversationId,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
-11
View File
@@ -1,15 +1,5 @@
// this file generate by script, don't modify it manually!!!
export enum I18nKey {
MICROAGENT$NO_REPOSITORY_FOUND = "MICROAGENT$NO_REPOSITORY_FOUND",
MICROAGENT$ADD_TO_MICROAGENT = "MICROAGENT$ADD_TO_MICROAGENT",
MICROAGENT$WHAT_TO_ADD = "MICROAGENT$WHAT_TO_ADD",
MICROAGENT$WHERE_TO_PUT = "MICROAGENT$WHERE_TO_PUT",
MICROAGENT$ADD_TRIGGER = "MICROAGENT$ADD_TRIGGER",
MICROAGENT$WAIT_FOR_RUNTIME = "MICROAGENT$WAIT_FOR_RUNTIME",
MICROAGENT$ADDING_CONTEXT = "MICROAGENT$ADDING_CONTEXT",
MICROAGENT$VIEW_CONVERSATION = "MICROAGENT$VIEW_CONVERSATION",
MICROAGENT$SUCCESS_PR_READY = "MICROAGENT$SUCCESS_PR_READY",
STATUS$CONNECTING_TO_RUNTIME = "STATUS$CONNECTING_TO_RUNTIME",
STATUS$WEBSOCKET_CLOSED = "STATUS$WEBSOCKET_CLOSED",
HOME$LAUNCH_FROM_SCRATCH = "HOME$LAUNCH_FROM_SCRATCH",
HOME$READ_THIS = "HOME$READ_THIS",
@@ -480,7 +470,6 @@ export enum I18nKey {
BILLING$YOUVE_GOT_50 = "BILLING$YOUVE_GOT_50",
BILLING$ERROR_WHILE_CREATING_SESSION = "BILLING$ERROR_WHILE_CREATING_SESSION",
BILLING$CLAIM_YOUR_50 = "BILLING$CLAIM_YOUR_50",
BILLING$POWERED_BY = "BILLING$POWERED_BY",
BILLING$PROCEED_TO_STRIPE = "BILLING$PROCEED_TO_STRIPE",
BILLING$YOURE_IN = "BILLING$YOURE_IN",
PAYMENT$ADD_FUNDS = "PAYMENT$ADD_FUNDS",
-32
View File
@@ -5855,22 +5855,6 @@
"tr": "Konuşmalar",
"uk": "Розмови"
},
"STATUS$CONNECTING_TO_RUNTIME": {
"en": "Connecting to runtime...",
"zh-CN": "正在连接到运行时...",
"zh-TW": "正在連接到執行時...",
"de": "Verbinde mit der Laufzeitumgebung...",
"ko-KR": "런타임에 연결 중...",
"no": "Kobler til kjøretidsmiljø...",
"it": "Connessione all'ambiente di esecuzione in corso...",
"pt": "Conectando ao ambiente de execução...",
"es": "Conectando al entorno de ejecución...",
"ar": "جارٍ الاتصال ببيئة التشغيل...",
"fr": "Connexion à l'environnement d'exécution en cours...",
"tr": "Çalışma zamanı ortamına bağlanılıyor...",
"ja": "ランタイムに接続中",
"uk": "Підключення до середовища виконання..."
},
"STATUS$STARTING_RUNTIME": {
"en": "Starting runtime...",
"zh-CN": "启动运行时...",
@@ -7535,22 +7519,6 @@
"de": "Fügen Sie eine Kreditkarte mit Stripe hinzu, um $50 zu erhalten. <b>Wir belasten Sie nicht ohne vorherige Zustimmung!</b>",
"uk": "Додайте кредитну картку до Stripe, щоб отримати свої 50 доларів. <b>Ми не стягуватимемо з вас плату без попереднього запиту!</b>"
},
"BILLING$POWERED_BY": {
"en": "Powered by",
"ja": "提供:",
"zh-CN": "技术支持:",
"zh-TW": "技術支援:",
"ko-KR": "제공: ",
"no": "Drevet av",
"it": "Offerto da",
"pt": "Oferecido por",
"es": "Ofrecido por",
"ar": "مشغل بواسطة",
"fr": "Propulsé par",
"tr": "Tarafından desteklenmektedir",
"de": "Bereitgestellt von",
"uk": "Працює на базі"
},
"BILLING$PROCEED_TO_STRIPE": {
"en": "Add Billing Info",
"ja": "請求情報を追加",
-5
View File
@@ -1,5 +0,0 @@
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11.8749 1.75H3.91861C3.47998 1.75015 3.05528 1.90407 2.71841 2.18499C2.38154 2.4659 2.15382 2.85603 2.07486 3.2875L1.28111 7.6625C1.23166 7.93277 1.2422 8.21062 1.31201 8.47636C1.38182 8.74211 1.50917 8.98927 1.68507 9.20035C1.86097 9.41142 2.08111 9.58126 2.32991 9.69785C2.57872 9.81443 2.8501 9.87491 3.12486 9.875H5.97486L5.62486 10.7688C5.47928 11.1601 5.4308 11.5809 5.48357 11.995C5.53635 12.4092 5.68881 12.8044 5.92787 13.1467C6.16694 13.489 6.48547 13.7683 6.85615 13.9604C7.22683 14.1526 7.63859 14.2519 8.05611 14.25C8.17634 14.2497 8.29394 14.2148 8.39482 14.1494C8.4957 14.084 8.57557 13.9909 8.62486 13.8813L10.4061 9.875H11.8749C12.3721 9.875 12.8491 9.67746 13.2007 9.32583C13.5523 8.97419 13.7499 8.49728 13.7499 8V3.625C13.7499 3.12772 13.5523 2.65081 13.2007 2.29917C12.8491 1.94754 12.3721 1.75 11.8749 1.75ZM9.37486 9.11875L7.67486 12.9438C7.50092 12.8911 7.3396 12.8034 7.20083 12.6861C7.06206 12.5688 6.94878 12.4242 6.86798 12.2615C6.78717 12.0987 6.74055 11.9211 6.73099 11.7396C6.72143 11.5581 6.74912 11.3766 6.81236 11.2062L7.14361 10.3125C7.2142 10.1236 7.23803 9.92041 7.21307 9.72029C7.18811 9.52018 7.1151 9.32907 7.00028 9.16329C6.88546 8.9975 6.73223 8.86196 6.55367 8.76823C6.37511 8.67449 6.17653 8.62535 5.97486 8.625H3.12486C3.03304 8.62515 2.94232 8.60507 2.85914 8.56618C2.77597 8.52729 2.70238 8.47055 2.64361 8.4C2.58341 8.33042 2.5393 8.24841 2.51445 8.15982C2.4896 8.07123 2.48462 7.97824 2.49986 7.8875L3.29361 3.5125C3.32024 3.3669 3.39767 3.23548 3.51212 3.14162C3.62657 3.04777 3.77062 2.99759 3.91861 3H9.37486V9.11875ZM12.4999 8C12.4999 8.16576 12.434 8.32473 12.3168 8.44194C12.1996 8.55915 12.0406 8.625 11.8749 8.625H10.6249V3H11.8749C12.0406 3 12.1996 3.06585 12.3168 3.18306C12.434 3.30027 12.4999 3.45924 12.4999 3.625V8Z"
fill="white" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

-5
View File
@@ -1,5 +0,0 @@
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M13.3125 6.80003C13.1369 6.58918 12.9171 6.41945 12.6687 6.30282C12.4204 6.18619 12.1494 6.1255 11.875 6.12503H9.025L9.375 5.23128C9.52058 4.83995 9.56907 4.41916 9.51629 4.00498C9.46351 3.5908 9.31106 3.19561 9.07199 2.8533C8.83293 2.51099 8.51439 2.23178 8.14371 2.03962C7.77303 1.84746 7.36127 1.74809 6.94375 1.75003C6.82352 1.75028 6.70592 1.7852 6.60504 1.8506C6.50417 1.91601 6.42429 2.00912 6.375 2.11878L4.59375 6.12503H3.125C2.62772 6.12503 2.15081 6.32257 1.79917 6.6742C1.44754 7.02583 1.25 7.50275 1.25 8.00003V12.375C1.25 12.8723 1.44754 13.3492 1.79917 13.7009C2.15081 14.0525 2.62772 14.25 3.125 14.25H11.0812C11.5199 14.2499 11.9446 14.096 12.2815 13.815C12.6183 13.5341 12.846 13.144 12.925 12.7125L13.7188 8.33753C13.7678 8.06714 13.7569 7.78927 13.6867 7.52358C13.6165 7.25788 13.4887 7.01087 13.3125 6.80003ZM4.375 13H3.125C2.95924 13 2.80027 12.9342 2.68306 12.817C2.56585 12.6998 2.5 12.5408 2.5 12.375V8.00003C2.5 7.83427 2.56585 7.6753 2.68306 7.55809C2.80027 7.44088 2.95924 7.37503 3.125 7.37503H4.375V13ZM12.5 8.11253L11.7062 12.4875C11.6796 12.6331 11.6022 12.7646 11.4877 12.8584C11.3733 12.9523 11.2292 13.0024 11.0812 13H5.625V6.88128L7.325 3.05628C7.49999 3.10729 7.6625 3.19403 7.80229 3.31102C7.94207 3.428 8.05608 3.57269 8.13712 3.73596C8.21817 3.89923 8.26449 4.07752 8.27316 4.25959C8.28183 4.44166 8.25266 4.62355 8.1875 4.79378L7.85625 5.68753C7.78567 5.87644 7.76184 6.07962 7.7868 6.27973C7.81176 6.47985 7.88476 6.67095 7.99958 6.83674C8.11441 7.00253 8.26763 7.13807 8.44619 7.2318C8.62475 7.32554 8.82333 7.37468 9.025 7.37503H11.875C11.9668 7.37488 12.0575 7.39496 12.1407 7.43385C12.2239 7.47274 12.2975 7.52948 12.3563 7.60003C12.4165 7.66961 12.4606 7.75162 12.4854 7.84021C12.5103 7.9288 12.5152 8.02179 12.5 8.11253Z"
fill="white" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

+13 -6
View File
@@ -36,6 +36,7 @@ class MCPProxyManager:
Initialize the MCP Proxy Manager.
Args:
name: Name of the proxy server
auth_enabled: Whether authentication is enabled
api_key: API key for authentication (required if auth_enabled is True)
logger_level: Logging level for the FastMCP logger
@@ -58,7 +59,7 @@ class MCPProxyManager:
"""
if len(self.config['mcpServers']) == 0:
logger.info(
'No MCP servers configured for FastMCP Proxy, skipping initialization.'
f"No MCP servers configured for FastMCP Proxy, skipping initialization."
)
return None
@@ -69,7 +70,7 @@ class MCPProxyManager:
api_key=self.api_key,
)
logger.info('FastMCP Proxy initialized successfully')
logger.info(f"FastMCP Proxy initialized successfully")
async def mount_to_app(
self, app: FastAPI, allow_origins: Optional[list[str]] = None
@@ -82,7 +83,9 @@ class MCPProxyManager:
allow_origins: List of allowed origins for CORS
"""
if len(self.config['mcpServers']) == 0:
logger.info('No MCP servers configured for FastMCP Proxy, skipping mount.')
logger.info(
f"No MCP servers configured for FastMCP Proxy, skipping mount."
)
return
if not self.proxy:
@@ -98,7 +101,8 @@ class MCPProxyManager:
app.routes.remove('/mcp')
app.mount('/', mcp_app)
logger.info('Mounted FastMCP Proxy app at /mcp')
logger.info(f"Mounted FastMCP Proxy app at /mcp")
async def update_and_remount(
self,
@@ -115,10 +119,13 @@ class MCPProxyManager:
Args:
app: FastAPI application to mount to
stdio_servers: List of stdio server configurations
tools: List of tool configurations
allow_origins: List of allowed origins for CORS
"""
tools = {t.name: t.model_dump() for t in stdio_servers}
tools = {
t.name: t.model_dump()
for t in stdio_servers
}
self.config['mcpServers'] = tools
del self.proxy
-2
View File
@@ -15,7 +15,6 @@ from fastapi import (
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
from openhands import __version__
from openhands.server.routes.conversation import app as conversation_api_router
from openhands.server.routes.feedback import app as feedback_api_router
from openhands.server.routes.files import app as files_api_router
from openhands.server.routes.git import app as git_api_router
from openhands.server.routes.health import add_health_endpoints
@@ -63,7 +62,6 @@ app = FastAPI(
app.include_router(public_api_router)
app.include_router(files_api_router)
app.include_router(security_api_router)
app.include_router(feedback_api_router)
app.include_router(conversation_api_router)
app.include_router(manage_conversation_api_router)
app.include_router(settings_router)
-45
View File
@@ -1,45 +0,0 @@
import json
from typing import Any, Literal
import httpx
from pydantic import BaseModel
from openhands.core.logger import openhands_logger as logger
class FeedbackDataModel(BaseModel):
version: str
email: str
polarity: Literal['positive', 'negative']
feedback: Literal[
'positive', 'negative'
] # TODO: remove this, its here for backward compatibility
permissions: Literal['public', 'private']
trajectory: list[dict[str, Any]] | None
FEEDBACK_URL = 'https://share-od-trajectory-3u9bw9tx.uc.gateway.dev/share_od_trajectory'
def store_feedback(feedback: FeedbackDataModel) -> dict[str, str]:
# Start logging
feedback.feedback = feedback.polarity
display_feedback = feedback.model_dump()
if 'trajectory' in display_feedback:
display_feedback['trajectory'] = (
f'elided [length: {len(display_feedback["trajectory"])}'
)
if 'token' in display_feedback:
display_feedback['token'] = 'elided'
logger.debug(f'Got feedback: {display_feedback}')
# Start actual request
response = httpx.post(
FEEDBACK_URL,
headers={'Content-Type': 'application/json'},
json=feedback.model_dump(),
)
if response.status_code != 200:
raise ValueError(f'Failed to store feedback: {response.text}')
response_data: dict[str, str] = json.loads(response.text)
logger.debug(f'Stored feedback: {response.text}')
return response_data
-62
View File
@@ -1,62 +0,0 @@
from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import JSONResponse
from openhands.core.logger import openhands_logger as logger
from openhands.events.async_event_store_wrapper import AsyncEventStoreWrapper
from openhands.events.serialization import event_to_dict
from openhands.server.data_models.feedback import FeedbackDataModel, store_feedback
from openhands.server.dependencies import get_dependencies
from openhands.server.utils import get_conversation
from openhands.utils.async_utils import call_sync_from_async
from openhands.server.session.conversation import ServerConversation
app = APIRouter(prefix='/api/conversations/{conversation_id}', dependencies=get_dependencies())
@app.post('/submit-feedback')
async def submit_feedback(request: Request, conversation: ServerConversation = Depends(get_conversation)) -> JSONResponse:
"""Submit user feedback.
This function stores the provided feedback data.
To submit feedback:
```sh
curl -X POST -d '{"email": "test@example.com"}' -H "Authorization:"
```
Args:
request (Request): The incoming request object.
feedback (FeedbackDataModel): The feedback data to be stored.
Returns:
dict: The stored feedback data.
Raises:
HTTPException: If there's an error submitting the feedback.
"""
# Assuming the storage service is already configured in the backend
# and there is a function to handle the storage.
body = await request.json()
async_store = AsyncEventStoreWrapper(
conversation.event_stream, filter_hidden=True
)
trajectory = []
async for event in async_store:
trajectory.append(event_to_dict(event))
feedback = FeedbackDataModel(
email=body.get('email', ''),
version=body.get('version', ''),
permissions=body.get('permissions', 'private'),
polarity=body.get('polarity', ''),
feedback=body.get('polarity', ''),
trajectory=trajectory,
)
try:
feedback_data = await call_sync_from_async(store_feedback, feedback)
return JSONResponse(status_code=status.HTTP_200_OK, content=feedback_data)
except Exception as e:
logger.error(f'Error submitting feedback: {e}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'error': 'Failed to submit feedback'},
)
+1 -1
View File
@@ -26,7 +26,7 @@ async def get_conversation(
conversation_id, user_id
)
if not conversation:
logger.warning(
logger.warn(
f'get_conversation: conversation {conversation_id} not found, attach_to_conversation returned None',
extra={'session_id': conversation_id, 'user_id': user_id},
)