mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
14 Commits
fix-cli-te
...
remove-fee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
367e82c69c | ||
|
|
1fa03176de | ||
|
|
e57efa1ef5 | ||
|
|
1601ed5ed8 | ||
|
|
6d82e446cd | ||
|
|
aaf1718406 | ||
|
|
53b5e08804 | ||
|
|
7cee7dca64 | ||
|
|
e12a62d006 | ||
|
|
77a0c5e073 | ||
|
|
e5d21e003d | ||
|
|
c6a4324bda | ||
|
|
9ac8f011fe | ||
|
|
d84befe28f |
BIN
docs/static/img/api-key-generation.png
vendored
Normal file
BIN
docs/static/img/api-key-generation.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/static/img/docs/api-key-generation.png
vendored
BIN
docs/static/img/docs/api-key-generation.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,9 +1,11 @@
|
||||
---
|
||||
title: Cloud API
|
||||
description: OpenHands Cloud provides a REST API that allows you to programmatically interact with the service. This guide explains how to obtain an API key and use the API to start conversations.
|
||||
description: OpenHands Cloud provides a REST API that allows you to programmatically interact with OpenHands.
|
||||
This guide explains how to obtain an API key and use the API to start conversations and retrieve their status.
|
||||
---
|
||||
|
||||
For more detailed information about the API, refer to the [OpenHands API Reference](https://docs.all-hands.dev/swagger-ui/).
|
||||
For the available API endpoints, refer to the
|
||||
[OpenHands API Reference](https://docs.all-hands.dev/api-reference).
|
||||
|
||||
## Obtaining an API Key
|
||||
|
||||
@@ -16,7 +18,7 @@ To use the OpenHands Cloud API, you'll need to generate an API key:
|
||||
5. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
|
||||
6. Copy the generated API key and store it securely. It will only be shown once.
|
||||
|
||||

|
||||

|
||||
|
||||
## API Usage
|
||||
|
||||
@@ -33,87 +35,81 @@ To start a new conversation with OpenHands to perform a task, you'll need to mak
|
||||
|
||||
#### Examples
|
||||
|
||||
<details>
|
||||
<summary>cURL</summary>
|
||||
|
||||
```bash
|
||||
curl -X POST "https://app.all-hands.dev/api/conversations" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
|
||||
"repository": "yourusername/your-repo"
|
||||
}'
|
||||
```
|
||||
</details>
|
||||
<Accordion title="cURL">
|
||||
```bash
|
||||
curl -X POST "https://app.all-hands.dev/api/conversations" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
|
||||
"repository": "yourusername/your-repo"
|
||||
}'
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<details>
|
||||
<summary>Python (with requests)</summary>
|
||||
<Accordion title="Python (with requests)">
|
||||
```python
|
||||
import requests
|
||||
|
||||
```python
|
||||
import requests
|
||||
api_key = "YOUR_API_KEY"
|
||||
url = "https://app.all-hands.dev/api/conversations"
|
||||
|
||||
api_key = "YOUR_API_KEY"
|
||||
url = "https://app.all-hands.dev/api/conversations"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
|
||||
"repository": "yourusername/your-repo"
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
conversation = response.json()
|
||||
|
||||
print(f"Conversation Link: https://app.all-hands.dev/conversations/{conversation['conversation_id']}")
|
||||
print(f"Status: {conversation['status']}")
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>TypeScript/JavaScript (with fetch)</summary>
|
||||
|
||||
```typescript
|
||||
const apiKey = "YOUR_API_KEY";
|
||||
const url = "https://app.all-hands.dev/api/conversations";
|
||||
|
||||
const headers = {
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
const data = {
|
||||
initial_user_msg: "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
|
||||
repository: "yourusername/your-repo"
|
||||
};
|
||||
|
||||
async function startConversation() {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const conversation = await response.json();
|
||||
|
||||
console.log(`Conversation Link: https://app.all-hands.dev/conversations/${conversation.id}`);
|
||||
console.log(`Status: ${conversation.status}`);
|
||||
|
||||
return conversation;
|
||||
} catch (error) {
|
||||
console.error("Error starting conversation:", error);
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
startConversation();
|
||||
```
|
||||
data = {
|
||||
"initial_user_msg": "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
|
||||
"repository": "yourusername/your-repo"
|
||||
}
|
||||
|
||||
</details>
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
conversation = response.json()
|
||||
|
||||
print(f"Conversation Link: https://app.all-hands.dev/conversations/{conversation['conversation_id']}")
|
||||
print(f"Status: {conversation['status']}")
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="TypeScript/JavaScript (with fetch)">
|
||||
```typescript
|
||||
const apiKey = "YOUR_API_KEY";
|
||||
const url = "https://app.all-hands.dev/api/conversations";
|
||||
|
||||
const headers = {
|
||||
"Authorization": `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json"
|
||||
};
|
||||
|
||||
const data = {
|
||||
initial_user_msg: "Check whether there is any incorrect information in the README.md file and send a PR to fix it if so.",
|
||||
repository: "yourusername/your-repo"
|
||||
};
|
||||
|
||||
async function startConversation() {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const conversation = await response.json();
|
||||
|
||||
console.log(`Conversation Link: https://app.all-hands.dev/conversations/${conversation.id}`);
|
||||
console.log(`Status: ${conversation.status}`);
|
||||
|
||||
return conversation;
|
||||
} catch (error) {
|
||||
console.error("Error starting conversation:", error);
|
||||
}
|
||||
}
|
||||
|
||||
startConversation();
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
#### Response
|
||||
|
||||
@@ -145,14 +141,12 @@ GET https://app.all-hands.dev/api/conversations/{conversation_id}
|
||||
|
||||
#### Example
|
||||
|
||||
<details>
|
||||
<summary>cURL</summary>
|
||||
|
||||
```bash
|
||||
curl -X GET "https://app.all-hands.dev/api/conversations/{conversation_id}" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
```
|
||||
</details>
|
||||
<Accordion title="cURL">
|
||||
```bash
|
||||
curl -X GET "https://app.all-hands.dev/api/conversations/{conversation_id}" \
|
||||
-H "Authorization: Bearer YOUR_API_KEY"
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
#### Response
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ The Settings page allows you to:
|
||||
|
||||
## Key Features
|
||||
|
||||
For an overview of the key features available inside a conversation, please refer to the [Key Features](../key-features)
|
||||
For an overview of the key features available inside a conversation, please refer to the [Key Features](/usage/key-features)
|
||||
section of the documentation.
|
||||
|
||||
## Next Steps
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
---
|
||||
title: CLI Mode
|
||||
description: CLI mode provides a powerful interactive Command-Line Interface (CLI) that lets you engage with OpenHands directly from your terminal.
|
||||
title: CLI
|
||||
description: The Command-Line Interface (CLI) provides a powerful interface that lets you engage with OpenHands
|
||||
directly from your terminal.
|
||||
---
|
||||
|
||||
This mode is different from the [headless mode](./headless-mode), which is non-interactive and better for scripting.
|
||||
This mode is different from the [headless mode](/usage/how-to/headless-mode), which is non-interactive and better
|
||||
for scripting.
|
||||
|
||||
## Getting Started
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
---
|
||||
title: GUI Mode
|
||||
description: OpenHands provides a Graphical User Interface (GUI) mode for interacting with the AI assistant.
|
||||
title: GUI
|
||||
description: High level overview of the Graphical User Interface (GUI) in OpenHands.
|
||||
---
|
||||
|
||||
## Installation and Setup
|
||||
## Prerequisites
|
||||
|
||||
1. Follow the installation instructions to install OpenHands.
|
||||
2. After running the command, access OpenHands at [http://localhost:3000](http://localhost:3000).
|
||||
- [OpenHands is running](/usage/local-setup)
|
||||
|
||||
## Interacting with the GUI
|
||||
## Overview
|
||||
|
||||
### Initial Setup
|
||||
|
||||
@@ -19,16 +18,23 @@ description: OpenHands provides a Graphical User Interface (GUI) mode for intera
|
||||
3. Enter the corresponding `API Key` for your chosen provider.
|
||||
4. Click `Save Changes` to apply the settings.
|
||||
|
||||
### Version Control Tokens
|
||||
### Settings
|
||||
|
||||
OpenHands supports multiple version control providers. You can configure tokens for multiple providers simultaneously.
|
||||
You can use the Settings page at any time to:
|
||||
|
||||
#### GitHub Token Setup
|
||||
- Setup the LLM provider and model for OpenHands.
|
||||
- [Setup the search engine](/usage/search-engine-setup).
|
||||
- [Configure MCP servers](/usage/mcp).
|
||||
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup) and [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup)
|
||||
- Set application settings like your preferred language, notifications and other preferences.
|
||||
- Generate custom secrets.
|
||||
|
||||
#### GitHub Setup
|
||||
|
||||
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if provided:
|
||||
|
||||
<details>
|
||||
<summary>Setting Up a GitHub Token</summary>
|
||||
<AccordionGroup>
|
||||
<Accordion title="Setting Up a GitHub Token">
|
||||
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- On GitHub, go to Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
|
||||
@@ -37,16 +43,11 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if pro
|
||||
- `repo` (Full control of private repositories)
|
||||
- **Fine-Grained Tokens**
|
||||
- All Repositories (You can select specific repositories, but this will impact what returns in repo search)
|
||||
- Minimal Permissions ( Select `Meta Data = Read-only` read for search, `Pull Requests = Read and Write` and `Content = Read and Write` for branch creation)
|
||||
- Minimal Permissions (Select `Meta Data = Read-only` read for search, `Pull Requests = Read and Write` and `Content = Read and Write` for branch creation)
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon).
|
||||
- Navigate to the `Git` tab.
|
||||
- In the Settings page, navigate to the `Git` tab.
|
||||
- Paste your token in the `GitHub Token` field.
|
||||
- Click `Save Changes` to apply the changes.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Organizational Token Policies</summary>
|
||||
|
||||
If you're working with organizational repositories, additional setup may be required:
|
||||
|
||||
@@ -59,15 +60,12 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if pro
|
||||
- Look for the organization under `Organization access`.
|
||||
- If required, click `Enable SSO` next to your organization.
|
||||
- Complete the SSO authorization process.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Troubleshooting</summary>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Troubleshooting">
|
||||
Common issues and solutions:
|
||||
|
||||
- **Token Not Recognized**:
|
||||
- Ensure the token is properly saved in settings.
|
||||
- Check that the token hasn't expired.
|
||||
- Verify the token has the required scopes.
|
||||
- Try regenerating the token.
|
||||
@@ -81,15 +79,15 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if pro
|
||||
- The app will show a green checkmark if the token is valid.
|
||||
- Try accessing a repository to confirm permissions.
|
||||
- Check the browser console for any error messages.
|
||||
</details>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
#### GitLab Token Setup
|
||||
#### GitLab Setup
|
||||
|
||||
OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if provided:
|
||||
|
||||
<details>
|
||||
<summary>Setting Up a GitLab Token</summary>
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Setting Up a GitLab Token">
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- On GitLab, go to User Settings > Access Tokens.
|
||||
- Create a new token with the following scopes:
|
||||
@@ -99,15 +97,12 @@ OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if pro
|
||||
- `write_repository` (Write repository)
|
||||
- Set an expiration date or leave it blank for a non-expiring token.
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon).
|
||||
- Navigate to the `Git` tab.
|
||||
- In the Settings page, navigate to the `Git` tab.
|
||||
- Paste your token in the `GitLab Token` field.
|
||||
- Click `Save Changes` to apply the changes.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Troubleshooting</summary>
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Troubleshooting">
|
||||
Common issues and solutions:
|
||||
|
||||
- **Token Not Recognized**:
|
||||
@@ -119,25 +114,30 @@ OpenHands automatically exports a `GITLAB_TOKEN` to the shell environment if pro
|
||||
- Verify project access permissions.
|
||||
- Check if the token has the necessary scopes.
|
||||
- For group/organization repositories, ensure you have proper access.
|
||||
</details>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Advanced Settings
|
||||
#### Advanced Settings
|
||||
|
||||
1. Inside the Settings page, under the `LLM` tab, toggle `Advanced` options to access additional settings.
|
||||
2. Use the `Custom Model` text box to manually enter a model if it's not in the list.
|
||||
3. Specify a `Base URL` if required by your LLM provider.
|
||||
The `Advanced` settings allows configuration of additional LLM settings. Inside the Settings page, under the `LLM` tab,
|
||||
toggle `Advanced` options to access additional settings.
|
||||
|
||||
### Interacting with the AI
|
||||
- Custom Model: Use the `Custom Model` text box to manually enter a model. Make sure to use the correct prefix based on litellm docs.
|
||||
- Base URL: Specify a `Base URL` if required by your LLM provider.
|
||||
- Memory Condensation: The memory condenser manages the LLM's context by ensuring only the most important and relevant information is presented.
|
||||
- Confirmation Mode: Enabling this mode will cause OpenHands to confirm an action with the user before performing it.
|
||||
|
||||
1. Type your prompt in the input box.
|
||||
2. Click the send button or press Enter to submit your message.
|
||||
3. The AI will process your input and provide a response in the chat window.
|
||||
4. You can continue the conversation by asking follow-up questions or providing additional information.
|
||||
### Key Features
|
||||
|
||||
For an overview of the key features available inside a conversation, please refer to the [Key Features](/usage/key-features)
|
||||
section of the documentation.
|
||||
|
||||
## Tips for Effective Use
|
||||
|
||||
- Be specific in your requests to get the most accurate and helpful responses, as described in the [prompting best practices](../prompting/prompting-best-practices).
|
||||
- Use one of the recommended models, as described in the [LLMs section](usage/llms/llms.md).
|
||||
|
||||
Remember, the GUI mode of OpenHands is designed to make your interaction with the AI assistant as smooth and intuitive
|
||||
as possible. Don't hesitate to explore its features to maximize your productivity.
|
||||
## Other Ways to Run Openhands
|
||||
- [Run OpenHands in a scriptable headless mode.](/usage/how-to/headless-mode)
|
||||
- [Run OpenHands with a friendly CLI.](/usage/how-to/cli-mode)
|
||||
- [Run OpenHands on GitHub issues with a GitHub action.](/usage/how-to/github-action)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
title: Headless Mode
|
||||
description: You can run OpenHands with a single command, without starting the web application. This makes it easy to write scripts and automate tasks with OpenHands.
|
||||
title: Headless
|
||||
description: You can run OpenHands with a single command, without starting the web application. This makes it easy to
|
||||
write scripts and automate tasks with OpenHands.
|
||||
---
|
||||
|
||||
This is different from [CLI Mode](./cli-mode), which is interactive, and better for active development.
|
||||
This is different from [the CLI](./cli-mode), which is interactive, and better for active development.
|
||||
|
||||
## With Python
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ To get started with OpenHands Cloud, visit [app.all-hands.dev](https://app.all-h
|
||||
|
||||
For more information see [getting started with OpenHands Cloud.](/usage/cloud/openhands-cloud)
|
||||
|
||||
## Running OpenHands Locally
|
||||
## Running OpenHands on Your Own
|
||||
|
||||
Run OpenHands on your local system and bring your own LLM and API key.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: Getting started with running OpenHands locally.
|
||||
description: Getting started with running OpenHands on your own.
|
||||
---
|
||||
|
||||
## Recommended Methods for Running Openhands on Your Local System
|
||||
@@ -62,17 +62,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
### Start the App
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.40-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.41-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.40
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.41
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
@@ -132,7 +132,6 @@ To enable search functionality in OpenHands:
|
||||
|
||||
For more details, see the [Search Engine Setup](/usage/search-engine-setup) guide.
|
||||
|
||||
|
||||
Now you're ready to [get started with OpenHands](/usage/getting-started).
|
||||
|
||||
### Versions
|
||||
|
||||
@@ -74,6 +74,24 @@ If no condenser configuration is specified, the 'noop' condenser will be used by
|
||||
|
||||
For other configurations specific to evaluation, such as `save_trajectory_path`, these are typically set in the `get_config` function of the respective `run_infer.py` file for each benchmark.
|
||||
|
||||
### Enabling LLM-Based Editor Tools
|
||||
|
||||
The LLM-Based Editor tool (currently supported only for SWE-Bench) can be enabled by setting:
|
||||
```bash
|
||||
export ENABLE_LLM_EDITOR=true
|
||||
```
|
||||
|
||||
You can set the config for the Editor LLM as:
|
||||
```toml
|
||||
[llm.draft_editor]
|
||||
base_url = "http://localhost:9002/v1"
|
||||
model = "hosted_vllm/lite_coder_qwen_editor_3B"
|
||||
api_key = ""
|
||||
temperature = 0.7
|
||||
max_input_tokens = 10500
|
||||
max_output_tokens = 10500
|
||||
```
|
||||
|
||||
## Supported Benchmarks
|
||||
|
||||
The OpenHands evaluation harness supports a wide variety of benchmarks across [software engineering](#software-engineering), [web browsing](#web-browsing), [miscellaneous assistance](#misc-assistance), and [real-world](#real-world) tasks.
|
||||
|
||||
@@ -42,7 +42,7 @@ from openhands.core.config import (
|
||||
AgentConfig,
|
||||
OpenHandsConfig,
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
get_parser
|
||||
)
|
||||
from openhands.core.config.condenser_config import NoOpCondenserConfig
|
||||
from openhands.core.config.utils import get_condenser_config_arg
|
||||
@@ -62,6 +62,7 @@ from openhands.utils.shutdown_listener import sleep_if_should_continue
|
||||
|
||||
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
|
||||
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
|
||||
ENABLE_LLM_EDITOR = os.environ.get('ENABLE_LLM_EDITOR', 'false').lower() == 'true'
|
||||
BenchMode = Literal['swe', 'swt', 'swt-ci']
|
||||
|
||||
|
||||
@@ -254,15 +255,19 @@ def get_config(
|
||||
workspace_base=None,
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
|
||||
config.set_llm_config(
|
||||
update_llm_config_for_completions_logging(
|
||||
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
|
||||
)
|
||||
)
|
||||
# get 'draft_editor' config if exists
|
||||
config.set_llm_config(get_llm_config_arg('draft_editor'), 'draft_editor')
|
||||
|
||||
agent_config = AgentConfig(
|
||||
enable_jupyter=False,
|
||||
enable_browsing=RUN_WITH_BROWSING,
|
||||
enable_llm_editor=False,
|
||||
enable_llm_editor=ENABLE_LLM_EDITOR,
|
||||
enable_mcp=False,
|
||||
condenser=metadata.condenser_config,
|
||||
enable_prompt_extensions=False,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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 |
@@ -141,6 +141,9 @@ def response_to_actions(
|
||||
content=arguments['content'],
|
||||
start=arguments.get('start', 1),
|
||||
end=arguments.get('end', -1),
|
||||
impl_source=arguments.get(
|
||||
'impl_source', FileEditSource.LLM_BASED_EDIT
|
||||
),
|
||||
)
|
||||
elif (
|
||||
tool_call.function.name
|
||||
|
||||
@@ -2,10 +2,18 @@ from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChun
|
||||
|
||||
_FILE_EDIT_DESCRIPTION = """Edit a file in plain-text format.
|
||||
* The assistant can edit files by specifying the file path and providing a draft of the new file content.
|
||||
* The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# unchanged` to indicate unchanged sections.
|
||||
* The draft content doesn't need to be exactly the same as the existing file; the assistant may skip unchanged lines using comments like `# ... existing code ...` to indicate unchanged sections.
|
||||
* IMPORTANT: For large files (e.g., > 300 lines), specify the range of lines to edit using `start` and `end` (1-indexed, inclusive). The range should be smaller than 300 lines.
|
||||
* -1 indicates the last line of the file when used as the `start` or `end` value.
|
||||
* Keep at least one unchanged line before the changed section and after the changed section wherever possible.
|
||||
* Make sure to set the `start` and `end` to include all the lines in the original file referred to in the draft of the new file content. Failure to do so will result in bad edits.
|
||||
* To append to a file, set both `start` and `end` to `-1`.
|
||||
* If the file doesn't exist, a new file will be created with the provided content.
|
||||
* IMPORTANT: Make sure you include all the required indentations for each line of code in the draft, otherwise the edited code will be incorrectly indented.
|
||||
* IMPORTANT: Make sure that the first line of the draft is also properly indented and has the required whitespaces.
|
||||
* IMPORTANT: NEVER include or make references to lines from outside the `start` and `end` range in the draft.
|
||||
* IMPORTANT: Start the content with a comment in the format: #EDIT: Reason for edit
|
||||
* IMPORTANT: If you are not appending to the file, avoid setting `start` and `end` to the same value.
|
||||
|
||||
**Example 1: general edit for short files**
|
||||
For example, given an existing file `/path/to/file.py` that looks like this:
|
||||
@@ -33,13 +41,12 @@ The assistant wants to edit the file to look like this:
|
||||
The assistant may produce an edit action like this:
|
||||
path="/path/to/file.txt" start=1 end=-1
|
||||
content=```
|
||||
#EDIT: I want to change the value of y to 2
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
# no changes before
|
||||
# ... existing code ...
|
||||
self.y = 2
|
||||
# self.z is removed
|
||||
|
||||
# MyClass().z is removed
|
||||
print(MyClass().y)
|
||||
```
|
||||
|
||||
@@ -58,6 +65,7 @@ For example, given an existing file `/path/to/file.py` that looks like this:
|
||||
|
||||
To append the following lines to the file:
|
||||
```python
|
||||
#EDIT: I want to print the value of y
|
||||
print(MyClass().y)
|
||||
```
|
||||
|
||||
@@ -93,9 +101,9 @@ The assistant wants to edit the file to look like this:
|
||||
(2000 more lines below)
|
||||
|
||||
The assistant may produce an edit action like this:
|
||||
path="/path/to/file.txt" start=1001 end=1008
|
||||
path="/path/to/file.txt" start=1002 end=1008
|
||||
content=```
|
||||
class MyClass:
|
||||
#EDIT: I want to change the value of y to 2
|
||||
def __init__(self):
|
||||
# no changes before
|
||||
self.y = 2
|
||||
|
||||
@@ -186,9 +186,18 @@ class EventStream(EventStore):
|
||||
|
||||
if event.id is not None:
|
||||
# Write the event to the store - this can take some time
|
||||
self.file_store.write(
|
||||
self._get_filename_for_id(event.id, self.user_id), json.dumps(data)
|
||||
)
|
||||
event_json = json.dumps(data)
|
||||
filename = self._get_filename_for_id(event.id, self.user_id)
|
||||
if len(event_json) > 1_000_000: # Roughly 1MB in bytes, ignoring encoding
|
||||
logger.warning(
|
||||
f'Saving event JSON over 1MB: {len(event_json):,} bytes, filename: {filename}',
|
||||
extra={
|
||||
'user_id': self.user_id,
|
||||
'session_id': self.sid,
|
||||
'size': len(event_json),
|
||||
},
|
||||
)
|
||||
self.file_store.write(filename, event_json)
|
||||
|
||||
# Store the cache page last - if it is not present during reads then it will simply be bypassed.
|
||||
self._store_cache_page(current_write_page)
|
||||
|
||||
@@ -22,6 +22,7 @@ from openhands.llm.tool_names import (
|
||||
BROWSER_TOOL_NAME,
|
||||
EXECUTE_BASH_TOOL_NAME,
|
||||
FINISH_TOOL_NAME,
|
||||
LLM_BASED_EDIT_TOOL_NAME,
|
||||
STR_REPLACE_EDITOR_TOOL_NAME,
|
||||
)
|
||||
|
||||
@@ -251,6 +252,58 @@ noop(1000) # Wait for page to load
|
||||
USER: EXECUTION RESULT of [browser]:
|
||||
[Browser shows the numbers in a table format]
|
||||
"""
|
||||
},
|
||||
'edit_file': {
|
||||
'create_file': """
|
||||
ASSISTANT: There is no `app.py` file in the current directory. Let me create a Python file `app.py`:
|
||||
<function=edit_file>
|
||||
<parameter=path>/workspace/app.py</parameter>
|
||||
<parameter=start>1</parameter>
|
||||
<parameter=end>-1</parameter>
|
||||
<parameter=content>
|
||||
from flask import Flask
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
numbers = list(range(1, 11))
|
||||
return str(numbers)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(port=5000)
|
||||
</parameter>
|
||||
</function>
|
||||
|
||||
USER: EXECUTION RESULT of [edit_file]:
|
||||
File created successfully at: /workspace/app.py
|
||||
""",
|
||||
'edit_file': """
|
||||
ASSISTANT:
|
||||
Now let me display the numbers in a table format:
|
||||
<function=edit_file>
|
||||
<parameter=path>/workspace/app.py</parameter>
|
||||
<parameter=start>6</parameter>
|
||||
<parameter=end>9</parameter>
|
||||
<parameter=content>
|
||||
numbers = list(range(1, 11))
|
||||
return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
# ... existing code ...
|
||||
if __name__ == '__main__':
|
||||
</parameter>
|
||||
</function>
|
||||
|
||||
USER: EXECUTION RESULT of [edit_file]:
|
||||
The file /workspace/app.py has been edited. Here's the result of running `cat -n` on a snippet of /workspace/app.py:
|
||||
3
|
||||
4 @app.route('/')
|
||||
5 def index():
|
||||
6 numbers = list(range(1, 11))
|
||||
7 return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
|
||||
8
|
||||
9 if __name__ == '__main__':
|
||||
10 app.run(port=5000)
|
||||
Review the changes and make sure they are as expected. Edit the file again if necessary.
|
||||
""",
|
||||
},
|
||||
'finish': {
|
||||
'task_completed': """
|
||||
@@ -279,6 +332,8 @@ def get_example_for_tools(tools: list[dict]) -> str:
|
||||
available_tools.add('browser')
|
||||
elif name == FINISH_TOOL_NAME:
|
||||
available_tools.add('finish')
|
||||
elif name == LLM_BASED_EDIT_TOOL_NAME:
|
||||
available_tools.add('edit_file')
|
||||
|
||||
if not available_tools:
|
||||
return ''
|
||||
@@ -297,6 +352,8 @@ USER: Create a list of numbers from 1 to 10, and display them in a web page at p
|
||||
|
||||
if 'str_replace_editor' in available_tools:
|
||||
example += TOOL_EXAMPLES['str_replace_editor']['create_file']
|
||||
elif 'edit_file' in available_tools:
|
||||
example += TOOL_EXAMPLES['edit_file']['create_file']
|
||||
|
||||
if 'execute_bash' in available_tools:
|
||||
example += TOOL_EXAMPLES['execute_bash']['run_server']
|
||||
@@ -309,6 +366,8 @@ USER: Create a list of numbers from 1 to 10, and display them in a web page at p
|
||||
|
||||
if 'str_replace_editor' in available_tools:
|
||||
example += TOOL_EXAMPLES['str_replace_editor']['edit_file']
|
||||
elif 'edit_file' in available_tools:
|
||||
example += TOOL_EXAMPLES['edit_file']['edit_file']
|
||||
|
||||
if 'execute_bash' in available_tools:
|
||||
example += TOOL_EXAMPLES['execute_bash']['run_server_again']
|
||||
|
||||
@@ -4,3 +4,4 @@ EXECUTE_BASH_TOOL_NAME = 'execute_bash'
|
||||
STR_REPLACE_EDITOR_TOOL_NAME = 'str_replace_editor'
|
||||
BROWSER_TOOL_NAME = 'browser'
|
||||
FINISH_TOOL_NAME = 'finish'
|
||||
LLM_BASED_EDIT_TOOL_NAME = 'edit_file'
|
||||
|
||||
@@ -307,8 +307,10 @@ class LocalRuntime(ActionExecutionClient):
|
||||
env['PATH'] = f'{python_bin_path}{os.pathsep}{env.get("PATH", "")}'
|
||||
logger.debug(f'Updated PATH for subprocesses: {env["PATH"]}')
|
||||
|
||||
# Check dependencies using the derived env_root_path
|
||||
check_dependencies(code_repo_path, env_root_path)
|
||||
# Check dependencies using the derived env_root_path if not skipped
|
||||
if os.getenv('SKIP_DEPENDENCY_CHECK', '') != '1':
|
||||
check_dependencies(code_repo_path, env_root_path)
|
||||
|
||||
self.server_process = subprocess.Popen( # noqa: S603
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -406,8 +408,8 @@ class LocalRuntime(ActionExecutionClient):
|
||||
return port
|
||||
|
||||
@tenacity.retry(
|
||||
wait=tenacity.wait_exponential(min=1, max=10),
|
||||
stop=tenacity.stop_after_attempt(10) | stop_if_should_exit(),
|
||||
wait=tenacity.wait_fixed(2),
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
before_sleep=lambda retry_state: logger.debug(
|
||||
f'Waiting for server to be ready... (attempt {retry_state.attempt_number})'
|
||||
),
|
||||
|
||||
@@ -4,7 +4,7 @@ import tempfile
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from openhands_aci.utils.diff import get_diff
|
||||
from openhands_aci.utils.diff import get_diff # type: ignore
|
||||
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
@@ -26,39 +26,31 @@ from openhands.llm.llm import LLM
|
||||
from openhands.llm.metrics import Metrics
|
||||
from openhands.utils.chunk_localizer import Chunk, get_top_k_chunk_matches
|
||||
|
||||
SYS_MSG = """Your job is to produce a new version of the file based on the old version and the
|
||||
provided draft of the new version. The provided draft may be incomplete (it may skip lines) and/or incorrectly indented. You should try to apply the changes present in the draft to the old version, and output a new version of the file.
|
||||
NOTE:
|
||||
- The output file should be COMPLETE and CORRECTLY INDENTED. Do not omit any lines, and do not change any lines that are not part of the changes.
|
||||
- You should output the new version of the file by wrapping the new version of the file content in a ``` block.
|
||||
- If there's no explicit comment to remove the existing code, we should keep them and append the new code to the end of the file.
|
||||
- If there's placeholder comments like `# no changes before` or `# no changes here`, we should replace these comments with the original code near the placeholder comments.
|
||||
"""
|
||||
|
||||
USER_MSG = """
|
||||
HERE IS THE OLD VERSION OF THE FILE:
|
||||
```
|
||||
{old_contents}
|
||||
```
|
||||
Code changes will be provided in the form of a draft. You will need to apply the draft to the original code.
|
||||
The original code will be enclosed within `<original_code>` tags.
|
||||
The draft will be enclosed within `<update_snippet>` tags.
|
||||
You need to output the update code within `<updated_code>` tags.
|
||||
|
||||
HERE IS THE DRAFT OF THE NEW VERSION OF THE FILE:
|
||||
```
|
||||
{draft_changes}
|
||||
```
|
||||
Within the `<updated_code>` tag, include only the final code after updation. Do not include any explanations or other content within these tags.
|
||||
|
||||
GIVE ME THE NEW VERSION OF THE FILE.
|
||||
IMPORTANT:
|
||||
- There should be NO placeholder comments like `# no changes before` or `# no changes here`. They should be replaced with the original code near the placeholder comments.
|
||||
- The output file should be COMPLETE and CORRECTLY INDENTED. Do not omit any lines, and do not change any lines that are not part of the changes.
|
||||
""".strip()
|
||||
<original_code>{old_contents}</original_code>
|
||||
|
||||
<update_snippet>{draft_changes}</update_snippet>
|
||||
"""
|
||||
|
||||
|
||||
def _extract_code(string: str) -> str | None:
|
||||
pattern = r'```(?:\w*\n)?(.*?)```'
|
||||
pattern = r'<updated_code>(.*?)</updated_code>'
|
||||
matches = re.findall(pattern, string, re.DOTALL)
|
||||
if not matches:
|
||||
return None
|
||||
return str(matches[0])
|
||||
|
||||
content = str(matches[0])
|
||||
if content.startswith('#EDIT:'):
|
||||
#Remove first line
|
||||
content = content[content.find('\n') + 1:]
|
||||
return content
|
||||
|
||||
|
||||
def get_new_file_contents(
|
||||
@@ -66,7 +58,6 @@ def get_new_file_contents(
|
||||
) -> str | None:
|
||||
while num_retries > 0:
|
||||
messages = [
|
||||
{'role': 'system', 'content': SYS_MSG},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': USER_MSG.format(
|
||||
|
||||
@@ -38,7 +38,7 @@ def send_request(
|
||||
session: HttpSession,
|
||||
method: str,
|
||||
url: str,
|
||||
timeout: int = 10,
|
||||
timeout: int = 60,
|
||||
**kwargs: Any,
|
||||
) -> httpx.Response:
|
||||
response = session.request(method, url, timeout=timeout, **kwargs)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -22,7 +22,6 @@ from openhands.events.nested_event_store import NestedEventStore
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.runtime.impl.docker.containers import stop_all_containers
|
||||
from openhands.runtime.impl.docker.docker_runtime import DockerRuntime
|
||||
from openhands.server.config.server_config import ServerConfig
|
||||
from openhands.server.conversation_manager.conversation_manager import (
|
||||
@@ -90,7 +89,7 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
"""
|
||||
Get the running agent loops directly from docker.
|
||||
"""
|
||||
containers : list[Container] = self.docker_client.containers.list()
|
||||
containers: list[Container] = self.docker_client.containers.list()
|
||||
names = (container.name or '' for container in containers)
|
||||
conversation_ids = {
|
||||
name[len('openhands-runtime-') :]
|
||||
@@ -284,7 +283,7 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
# First try to graceful stop server.
|
||||
try:
|
||||
container = self.docker_client.containers.get(f'openhands-runtime-{sid}')
|
||||
except docker.errors.NotFound as e:
|
||||
except docker.errors.NotFound:
|
||||
return
|
||||
try:
|
||||
nested_url = self.get_nested_url_for_container(container)
|
||||
@@ -293,25 +292,33 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
'X-Session-API-Key': self._get_session_api_key_for_conversation(sid)
|
||||
}
|
||||
) as client:
|
||||
response = await client.post(f'{nested_url}/api/conversations/{sid}/stop')
|
||||
# Stop conversation
|
||||
response = await client.post(
|
||||
f'{nested_url}/api/conversations/{sid}/stop'
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# Check up to 3 times that client has closed
|
||||
for _ in range(3):
|
||||
response = await client.get(f'{nested_url}/api/conversations/{sid}')
|
||||
if response.status_code == status.HTTP_200_OK and response.json().get('status') == "STOPPED":
|
||||
response.raise_for_status()
|
||||
if response.json().get('status') == 'STOPPED':
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception:
|
||||
logger.exception("error_stopping_container")
|
||||
except Exception as e:
|
||||
logger.warning('error_stopping_container', extra={"sid": sid, "error": str(e)})
|
||||
container.stop()
|
||||
|
||||
async def get_agent_loop_info(self, user_id: str | None = None, filter_to_sids: set[str] | None = None) -> list[AgentLoopInfo]:
|
||||
async def get_agent_loop_info(
|
||||
self, user_id: str | None = None, filter_to_sids: set[str] | None = None
|
||||
) -> list[AgentLoopInfo]:
|
||||
results = []
|
||||
containers : list[Container] = self.docker_client.containers.list()
|
||||
containers: list[Container] = self.docker_client.containers.list()
|
||||
for container in containers:
|
||||
if not container.name or not container.name.startswith('openhands-runtime-'):
|
||||
if not container.name or not container.name.startswith(
|
||||
'openhands-runtime-'
|
||||
):
|
||||
continue
|
||||
conversation_id = container.name[len('openhands-runtime-') :]
|
||||
if filter_to_sids is not None and conversation_id not in filter_to_sids:
|
||||
@@ -389,7 +396,9 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
)
|
||||
return session_api_key
|
||||
|
||||
async def ensure_num_conversations_below_limit(self, sid: str, user_id: str | None) -> None:
|
||||
async def ensure_num_conversations_below_limit(
|
||||
self, sid: str, user_id: str | None
|
||||
) -> None:
|
||||
response_ids = await self.get_running_agent_loops(user_id)
|
||||
if len(response_ids) >= self.config.max_concurrent_conversations:
|
||||
logger.info(
|
||||
@@ -431,7 +440,9 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
)
|
||||
return provider_handler
|
||||
|
||||
async def _create_runtime(self, sid: str, user_id: str | None, settings: Settings) -> DockerRuntime:
|
||||
async def _create_runtime(
|
||||
self, sid: str, user_id: str | None, settings: Settings
|
||||
) -> DockerRuntime:
|
||||
# This session is created here only because it is the easiest way to get a runtime, which
|
||||
# is the easiest way to create the needed docker container
|
||||
session = Session(
|
||||
@@ -463,8 +474,9 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
env_vars['SESSION_API_KEY'] = self._get_session_api_key_for_conversation(sid)
|
||||
# We need to be able to specify the nested conversation id within the nested runtime
|
||||
env_vars['ALLOW_SET_CONVERSATION_ID'] = '1'
|
||||
env_vars['WORKSPACE_BASE'] = f'/workspace'
|
||||
env_vars['WORKSPACE_BASE'] = '/workspace'
|
||||
env_vars['SANDBOX_CLOSE_DELAY'] = '0'
|
||||
env_vars['SKIP_DEPENDENCY_CHECK'] = '1'
|
||||
|
||||
# Set up mounted volume for conversation directory within workspace
|
||||
# TODO: Check if we are using the standard event store and file store
|
||||
@@ -509,7 +521,7 @@ class DockerNestedConversationManager(ConversationManager):
|
||||
await call_sync_from_async(container.start)
|
||||
return True
|
||||
return False
|
||||
except docker.errors.NotFound as e:
|
||||
except docker.errors.NotFound:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -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'},
|
||||
)
|
||||
554
poetry.lock
generated
554
poetry.lock
generated
File diff suppressed because one or more lines are too long
@@ -36,7 +36,7 @@ numpy = "*"
|
||||
json-repair = "*"
|
||||
browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface
|
||||
html2text = "*"
|
||||
e2b = ">=1.0.5,<1.4.0"
|
||||
e2b = ">=1.0.5,<1.6.0"
|
||||
pexpect = "*"
|
||||
jinja2 = "^3.1.3"
|
||||
python-multipart = "*"
|
||||
@@ -53,7 +53,7 @@ protobuf = "^5.0.0,<6.0.0" # Updated to support newer op
|
||||
opentelemetry-api = "^1.33.1"
|
||||
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
|
||||
modal = ">=0.66.26,<0.78.0"
|
||||
runloop-api-client = "0.33.0"
|
||||
runloop-api-client = "0.39.0"
|
||||
libtmux = ">=0.37,<0.40"
|
||||
pygithub = "^2.5.0"
|
||||
joblib = "*"
|
||||
@@ -80,7 +80,7 @@ bashlex = "^0.18"
|
||||
# TODO: These are integrations that should probably be optional
|
||||
redis = ">=5.2,<7.0"
|
||||
minio = "^7.2.8"
|
||||
daytona-sdk = "0.18.1"
|
||||
daytona-sdk = "0.20.0"
|
||||
stripe = ">=11.5,<13.0"
|
||||
google-cloud-aiplatform = "*"
|
||||
anthropic = { extras = [ "vertex" ], version = "*" }
|
||||
@@ -90,8 +90,8 @@ boto3 = "*"
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "0.11.11"
|
||||
mypy = "1.15.0"
|
||||
ruff = "0.11.13"
|
||||
mypy = "1.16.0"
|
||||
pre-commit = "4.2.0"
|
||||
build = "*"
|
||||
types-setuptools = "*"
|
||||
|
||||
Reference in New Issue
Block a user