mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 367e82c69c | |||
| 1fa03176de | |||
| e57efa1ef5 | |||
| 1601ed5ed8 | |||
| 6d82e446cd | |||
| aaf1718406 |
+4
-22
@@ -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
|
||||
|
||||
@@ -72,9 +72,3 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directories:
|
||||
- "containers/*"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
-14
@@ -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}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "請求情報を追加",
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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'},
|
||||
)
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user