mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5f31b643e | |||
| 07862c32cb | |||
| e04f876df9 | |||
| 78d707de83 | |||
| 058153292f | |||
| 53b5e08804 | |||
| 7cee7dca64 | |||
| e12a62d006 | |||
| 77a0c5e073 | |||
| e5d21e003d | |||
| c6a4324bda | |||
| 9ac8f011fe | |||
| d84befe28f | |||
| 4eef22e04e | |||
| 93e6811efc | |||
| 3ebe3c2140 | |||
| d6d5499416 | |||
| 0221f21c12 | |||
| 617445d5ca | |||
| 5730db327a | |||
| 24a03738a8 | |||
| 3a093c13b8 | |||
| e367ced954 | |||
| 4c2c1bd8eb | |||
| 20b1f37fbd | |||
| 2bae39ef30 | |||
| 9f8bcf8729 | |||
| 137eec4dd1 |
+22
-4
@@ -1,5 +1,23 @@
|
||||
# NodeJS
|
||||
frontend/node_modules
|
||||
config.toml
|
||||
.envrc
|
||||
.env
|
||||
.git
|
||||
|
||||
# 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
|
||||
|
||||
@@ -33,6 +33,7 @@ body:
|
||||
- Docker command in README
|
||||
- GitHub resolver
|
||||
- Development workflow
|
||||
- CLI
|
||||
- app.all-hands.dev
|
||||
- Other
|
||||
default: 0
|
||||
|
||||
@@ -16,7 +16,6 @@ updates:
|
||||
mcp-packages:
|
||||
patterns:
|
||||
- "mcp"
|
||||
- "mcpm"
|
||||
security-all:
|
||||
applies-to: "security-updates"
|
||||
patterns:
|
||||
@@ -73,3 +72,9 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directories:
|
||||
- "containers/*"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
@@ -2,8 +2,6 @@ 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.
|
||||
|
||||
@@ -21,13 +19,91 @@ 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
|
||||
@@ -50,6 +126,13 @@ 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.
|
||||
@@ -80,4 +163,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)
|
||||
+11
-15
@@ -1,16 +1,16 @@
|
||||
ARG OPENHANDS_BUILD_VERSION=dev
|
||||
FROM node:21.7.2-bookworm-slim AS frontend-builder
|
||||
FROM node:22.16.0-bookworm-slim AS frontend-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm install -g npm@10.5.1
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY ./frontend ./
|
||||
COPY frontend ./
|
||||
RUN npm run build
|
||||
|
||||
FROM python:3.12.3-slim AS backend-builder
|
||||
FROM python:3.12.10-slim AS base
|
||||
FROM base AS backend-builder
|
||||
|
||||
WORKDIR /app
|
||||
ENV PYTHONPATH='/app'
|
||||
@@ -22,17 +22,18 @@ ENV POETRY_NO_INTERACTION=1 \
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt-get install -y curl make git build-essential \
|
||||
&& python3 -m pip install poetry==1.8.2 --break-system-packages
|
||||
&& python3 -m pip install poetry --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 python:3.12.3-slim AS openhands-app
|
||||
FROM base AS openhands-app
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ARG OPENHANDS_BUILD_VERSION #re-declare for this section
|
||||
# re-declare for this section
|
||||
ARG OPENHANDS_BUILD_VERSION
|
||||
|
||||
ENV RUN_AS_OPENHANDS=true
|
||||
# A random number--we need this to be different from the user's UID on the host machine
|
||||
@@ -74,12 +75,7 @@ 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 --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
|
||||
COPY --chown=openhands:app pyproject.toml poetry.lock README.md MANIFEST.in 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
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Running OpenHands Locally",
|
||||
"group": "Running OpenHands on Your Own",
|
||||
"pages": [
|
||||
"usage/local-setup",
|
||||
"usage/how-to/gui-mode",
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
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,24 +1,39 @@
|
||||
---
|
||||
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
|
||||
|
||||
### Running with Python
|
||||
|
||||
1. Ensure you have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
1. Install OpenHands using pip:
|
||||
|
||||
```bash
|
||||
pip install openhands-ai
|
||||
```
|
||||
|
||||
2. Set your model, API key, and other preferences using environment variables or with the [`config.toml`](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) file.
|
||||
3. Launch an interactive OpenHands conversation from the command line:
|
||||
|
||||
```bash
|
||||
poetry run python -m openhands.cli.main
|
||||
openhands
|
||||
```
|
||||
|
||||
This command opens an interactive prompt where you can type tasks or commands and get responses from OpenHands.
|
||||
|
||||
#### For Developers
|
||||
|
||||
If you have cloned the repository, you can run the CLI directly using Poetry:
|
||||
|
||||
```bash
|
||||
poetry run python -m openhands.cli.main
|
||||
```
|
||||
|
||||
### Running with Docker
|
||||
|
||||
1. Set the following environment variables in your terminal:
|
||||
|
||||
@@ -46,7 +46,7 @@ This will produce a new image called `custom-image`, which will be available in
|
||||
|
||||
## Using the Docker Command
|
||||
|
||||
When running OpenHands using [the docker command](/usage/installation#start-the-app), replace
|
||||
When running OpenHands using [the docker command](/usage/local-setup#start-the-app), replace
|
||||
`-e SANDBOX_RUNTIME_CONTAINER_IMAGE=...` with `-e SANDBOX_BASE_CONTAINER_IMAGE=<custom image name>`:
|
||||
|
||||
```commandline
|
||||
|
||||
@@ -48,6 +48,6 @@ The customization options you can set are:
|
||||
| `LLM_MODEL` | Variable | Set the LLM to use with OpenHands | `LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"` |
|
||||
| `OPENHANDS_MAX_ITER` | Variable | Set max limit for agent iterations | `OPENHANDS_MAX_ITER=10` |
|
||||
| `OPENHANDS_MACRO` | Variable | Customize default macro for invoking the resolver | `OPENHANDS_MACRO=@resolveit` |
|
||||
| `OPENHANDS_BASE_CONTAINER_IMAGE` | Variable | Custom Sandbox ([learn more](https://docs.all-hands.dev/modules/usage/how-to/custom-sandbox-guide)) | `OPENHANDS_BASE_CONTAINER_IMAGE="custom_image"` |
|
||||
| `OPENHANDS_BASE_CONTAINER_IMAGE` | Variable | Custom Sandbox ([learn more](/usage/how-to/custom-sandbox-guide)) | `OPENHANDS_BASE_CONTAINER_IMAGE="custom_image"` |
|
||||
| `TARGET_BRANCH` | Variable | Merge to branch other than `main` | `TARGET_BRANCH="dev"` |
|
||||
| `TARGET_RUNNER` | Variable | Target runner to execute the agent workflow (default ubuntu-latest) | `TARGET_RUNNER="custom-runner"` |
|
||||
|
||||
@@ -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,8 +12,8 @@ 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.
|
||||
|
||||
For more information see [running OpenHands locally.](/usage/local-setup)
|
||||
For more information see [running OpenHands on your own.](/usage/local-setup)
|
||||
|
||||
@@ -48,7 +48,7 @@ We recommend using [LMStudio](https://lmstudio.ai/) for serving these models loc
|
||||
|
||||
### Start OpenHands with locally served model
|
||||
|
||||
Check [the installation guide](https://docs.all-hands.dev/modules/usage/installation) to make sure you have all the prerequisites for running OpenHands.
|
||||
Check [the installation guide](/usage/local-setup) to make sure you have all the prerequisites for running OpenHands.
|
||||
|
||||
```bash
|
||||
export LMSTUDIO_MODEL_NAME="imported-models/uncategorized/devstralq4_k_m.gguf" # <- Replace this with the model name you copied from LMStudio
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
@@ -19,7 +18,7 @@ describe("Empty state", () => {
|
||||
const { useWsClient: useWsClientMock } = vi.hoisted(() => ({
|
||||
useWsClient: vi.fn(() => ({
|
||||
send: sendMock,
|
||||
status: WsClientProviderStatus.CONNECTED,
|
||||
status: "CONNECTED",
|
||||
isLoadingMessages: false,
|
||||
})),
|
||||
}));
|
||||
@@ -64,7 +63,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: WsClientProviderStatus.CONNECTED,
|
||||
status: "CONNECTED",
|
||||
isLoadingMessages: false,
|
||||
}));
|
||||
const user = userEvent.setup();
|
||||
@@ -87,7 +86,7 @@ describe("Empty state", () => {
|
||||
async () => {
|
||||
useWsClientMock.mockImplementation(() => ({
|
||||
send: sendMock,
|
||||
status: WsClientProviderStatus.CONNECTED,
|
||||
status: "CONNECTED",
|
||||
isLoadingMessages: false,
|
||||
}));
|
||||
const user = userEvent.setup();
|
||||
@@ -101,7 +100,7 @@ describe("Empty state", () => {
|
||||
|
||||
useWsClientMock.mockImplementation(() => ({
|
||||
send: sendMock,
|
||||
status: WsClientProviderStatus.CONNECTED,
|
||||
status: "CONNECTED",
|
||||
isLoadingMessages: false,
|
||||
}));
|
||||
rerender(<ChatInterface />);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -9,10 +9,7 @@ import {
|
||||
AGENT_STATUS_MAP,
|
||||
IndicatorColor,
|
||||
} from "../../agent-status-map.constant";
|
||||
import {
|
||||
useWsClient,
|
||||
WsClientProviderStatus,
|
||||
} from "#/context/ws-client-provider";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useNotification } from "#/hooks/useNotification";
|
||||
import { browserTab } from "#/utils/browser-tab";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
@@ -80,10 +77,13 @@ export function AgentStatusBar() {
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (conversation?.status === "STARTING") {
|
||||
if (conversation?.status === "CONNECTING") {
|
||||
setStatusMessage(t(I18nKey.STATUS$CONNECTING_TO_RUNTIME));
|
||||
setIndicatorColor(IndicatorColor.YELLOW);
|
||||
} else if (conversation?.status === "STARTING") {
|
||||
setStatusMessage(t(I18nKey.STATUS$STARTING_RUNTIME));
|
||||
setIndicatorColor(IndicatorColor.RED);
|
||||
} else if (status === WsClientProviderStatus.DISCONNECTED) {
|
||||
} else if (status === "DISCONNECTED") {
|
||||
setStatusMessage(t(I18nKey.STATUS$WEBSOCKET_CLOSED));
|
||||
setIndicatorColor(IndicatorColor.RED);
|
||||
} else {
|
||||
|
||||
+14
-2
@@ -2,9 +2,20 @@ 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";
|
||||
export type ProjectStatus =
|
||||
| "RUNNING"
|
||||
| "STOPPED"
|
||||
| "STARTING"
|
||||
| "CONNECTING"
|
||||
| "CONNECTED"
|
||||
| "DISCONNECTED";
|
||||
|
||||
const INDICATORS: Record<ProjectStatus, SVGIcon> = {
|
||||
type ProjectStatusWithIcon = Exclude<
|
||||
ProjectStatus,
|
||||
"CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||
>;
|
||||
|
||||
const INDICATORS: Record<ProjectStatusWithIcon, SVGIcon> = {
|
||||
STOPPED: ColdIcon,
|
||||
RUNNING: RunningIcon,
|
||||
STARTING: ColdIcon,
|
||||
@@ -17,6 +28,7 @@ interface ConversationStateIndicatorProps {
|
||||
export function ConversationStateIndicator({
|
||||
status,
|
||||
}: ConversationStateIndicatorProps) {
|
||||
// @ts-expect-error - Type 'ProjectStatus' is not assignable to type 'ProjectStatusWithIcon'.
|
||||
const StateIcon = INDICATORS[status];
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,6 +9,7 @@ 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();
|
||||
@@ -79,6 +80,7 @@ export function PaymentForm() {
|
||||
{t(I18nKey.PAYMENT$ADD_CREDIT)}
|
||||
</BrandButton>
|
||||
{isPending && <LoadingSpinner size="small" />}
|
||||
<PoweredByStripeTag />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -96,7 +96,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
testId="llm-api-key-help-anchor"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/usage/installation#getting-an-api-key"
|
||||
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ 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" &&
|
||||
@@ -67,14 +68,8 @@ const isMessageAction = (
|
||||
): event is UserMessageAction | AssistantMessageAction =>
|
||||
isUserMessage(event) || isAssistantMessage(event);
|
||||
|
||||
export enum WsClientProviderStatus {
|
||||
CONNECTED,
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
}
|
||||
|
||||
interface UseWsClient {
|
||||
status: WsClientProviderStatus;
|
||||
status: ProjectStatus;
|
||||
isLoadingMessages: boolean;
|
||||
events: Record<string, unknown>[];
|
||||
parsedEvents: (OpenHandsAction | OpenHandsObservation)[];
|
||||
@@ -82,7 +77,7 @@ interface UseWsClient {
|
||||
}
|
||||
|
||||
const WsClientContext = React.createContext<UseWsClient>({
|
||||
status: WsClientProviderStatus.DISCONNECTED,
|
||||
status: "DISCONNECTED",
|
||||
isLoadingMessages: true,
|
||||
events: [],
|
||||
parsedEvents: [],
|
||||
@@ -139,9 +134,7 @@ export function WsClientProvider({
|
||||
const { setErrorMessage, removeErrorMessage } = useWSErrorMessage();
|
||||
const queryClient = useQueryClient();
|
||||
const sioRef = React.useRef<Socket | null>(null);
|
||||
const [status, setStatus] = React.useState(
|
||||
WsClientProviderStatus.DISCONNECTED,
|
||||
);
|
||||
const [status, setStatus] = React.useState<ProjectStatus>("CONNECTING");
|
||||
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
|
||||
const [parsedEvents, setParsedEvents] = React.useState<
|
||||
(OpenHandsAction | OpenHandsObservation)[]
|
||||
@@ -162,7 +155,7 @@ export function WsClientProvider({
|
||||
}
|
||||
|
||||
function handleConnect() {
|
||||
setStatus(WsClientProviderStatus.CONNECTED);
|
||||
setStatus("CONNECTED");
|
||||
removeErrorMessage();
|
||||
}
|
||||
|
||||
@@ -261,7 +254,7 @@ export function WsClientProvider({
|
||||
}
|
||||
|
||||
function handleDisconnect(data: unknown) {
|
||||
setStatus(WsClientProviderStatus.DISCONNECTED);
|
||||
setStatus("DISCONNECTED");
|
||||
const sio = sioRef.current;
|
||||
if (!sio) {
|
||||
return;
|
||||
@@ -275,7 +268,7 @@ export function WsClientProvider({
|
||||
|
||||
function handleError(data: unknown) {
|
||||
// set status
|
||||
setStatus(WsClientProviderStatus.DISCONNECTED);
|
||||
setStatus("DISCONNECTED");
|
||||
updateStatusWhenErrorMessagePresent(data);
|
||||
|
||||
setErrorMessage(
|
||||
@@ -294,7 +287,7 @@ export function WsClientProvider({
|
||||
// reset events when conversationId changes
|
||||
setEvents([]);
|
||||
setParsedEvents([]);
|
||||
setStatus(WsClientProviderStatus.DISCONNECTED);
|
||||
setStatus("CONNECTING");
|
||||
}, [conversationId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import {
|
||||
useWsClient,
|
||||
WsClientProviderStatus,
|
||||
} from "#/context/ws-client-provider";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
@@ -17,7 +14,7 @@ export const useConversationConfig = () => {
|
||||
if (!conversationId) throw new Error("No conversation ID");
|
||||
return OpenHands.getRuntimeId(conversationId);
|
||||
},
|
||||
enabled: status !== WsClientProviderStatus.DISCONNECTED && !!conversationId,
|
||||
enabled: status !== "DISCONNECTED" && !!conversationId,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
// 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",
|
||||
@@ -470,6 +480,7 @@ 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,6 +5855,22 @@
|
||||
"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": "启动运行时...",
|
||||
@@ -7519,6 +7535,22 @@
|
||||
"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": "請求情報を追加",
|
||||
|
||||
@@ -304,7 +304,7 @@ function LlmSettingsScreen() {
|
||||
testId="llm-api-key-help-anchor"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/usage/installation#getting-an-api-key"
|
||||
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
|
||||
/>
|
||||
|
||||
<SettingsInput
|
||||
@@ -379,7 +379,7 @@ function LlmSettingsScreen() {
|
||||
testId="llm-api-key-help-anchor-advanced"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/usage/installation#getting-an-api-key"
|
||||
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
|
||||
/>
|
||||
|
||||
<SettingsInput
|
||||
|
||||
@@ -12,7 +12,7 @@ export const TIPS: Tip[] = [
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$SETUP_SCRIPT,
|
||||
link: "https://docs.all-hands.dev/usage/customization/repository",
|
||||
link: "https://docs.all-hands.dev/usage/prompting/repository#setup-script",
|
||||
},
|
||||
{ key: I18nKey.TIPS$VSCODE_INSTANCE },
|
||||
{ key: I18nKey.TIPS$SAVE_WORK },
|
||||
@@ -38,7 +38,7 @@ export const TIPS: Tip[] = [
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$API_USAGE,
|
||||
link: "https://docs.all-hands.dev/swagger-ui/",
|
||||
link: "https://docs.all-hands.dev/api-reference/health-check",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -37,5 +37,4 @@ When creating a new microagent:
|
||||
For detailed information, see:
|
||||
|
||||
- [Microagents Overview](https://docs.all-hands.dev/usage/prompting/microagents-overview)
|
||||
- [Microagents Syntax](https://docs.all-hands.dev/usage/prompting/microagents-syntax)
|
||||
- [Example GitHub Microagent](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/github.md)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Iterable
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx # type: ignore
|
||||
from fastapi import status
|
||||
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.event_filter import EventFilter
|
||||
@@ -42,6 +43,9 @@ class NestedEventStore(EventStoreABC):
|
||||
if self.session_api_key:
|
||||
headers['X-Session-API-Key'] = self.session_api_key
|
||||
response = httpx.get(url, headers=headers)
|
||||
if response.status_code == status.HTTP_404_NOT_FOUND:
|
||||
# Follow pattern of event store not throwing errors on not found
|
||||
return
|
||||
result_set = response.json()
|
||||
for result in result_set['events']:
|
||||
event = event_from_dict(result)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -9,7 +9,6 @@ import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import shutil
|
||||
@@ -26,8 +25,6 @@ from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.security import APIKeyHeader
|
||||
from mcpm import MCPRouter, RouterConfig
|
||||
from mcpm.router.router import logger as mcp_router_logger
|
||||
from openhands_aci.editor.editor import OHEditor
|
||||
from openhands_aci.editor.exceptions import ToolError
|
||||
from openhands_aci.editor.results import ToolResult
|
||||
@@ -37,6 +34,7 @@ from starlette.background import BackgroundTask
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from uvicorn import run
|
||||
|
||||
from openhands.core.config.mcp_config import MCPStdioServerConfig
|
||||
from openhands.core.exceptions import BrowserUnavailableException
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action import (
|
||||
@@ -63,20 +61,18 @@ from openhands.events.serialization import event_from_dict, event_to_dict
|
||||
from openhands.runtime.browser import browse
|
||||
from openhands.runtime.browser.browser_env import BrowserEnv
|
||||
from openhands.runtime.file_viewer_server import start_file_viewer_server
|
||||
|
||||
# Import our custom MCP Proxy Manager
|
||||
from openhands.runtime.mcp.proxy import MCPProxyManager
|
||||
from openhands.runtime.plugins import ALL_PLUGINS, JupyterPlugin, Plugin, VSCodePlugin
|
||||
from openhands.runtime.utils import find_available_tcp_port
|
||||
from openhands.runtime.utils.bash import BashSession
|
||||
from openhands.runtime.utils.files import insert_lines, read_lines
|
||||
from openhands.runtime.utils.log_capture import capture_logs
|
||||
from openhands.runtime.utils.memory_monitor import MemoryMonitor
|
||||
from openhands.runtime.utils.runtime_init import init_user_and_working_directory
|
||||
from openhands.runtime.utils.system_stats import get_system_stats
|
||||
from openhands.utils.async_utils import call_sync_from_async, wait_all
|
||||
|
||||
# Set MCP router logger to the same level as the main logger
|
||||
mcp_router_logger.setLevel(logger.getEffectiveLevel())
|
||||
|
||||
|
||||
if sys.platform == 'win32':
|
||||
from openhands.runtime.utils.windows_bash import WindowsPowershellSession
|
||||
|
||||
@@ -471,7 +467,7 @@ class ActionExecutor:
|
||||
filepath = self._resolve_path(action.path, working_dir)
|
||||
try:
|
||||
if filepath.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):
|
||||
with open(filepath, 'rb') as file: # noqa: ASYNC101
|
||||
with open(filepath, 'rb') as file:
|
||||
image_data = file.read()
|
||||
encoded_image = base64.b64encode(image_data).decode('utf-8')
|
||||
mime_type, _ = mimetypes.guess_type(filepath)
|
||||
@@ -481,13 +477,13 @@ class ActionExecutor:
|
||||
|
||||
return FileReadObservation(path=filepath, content=encoded_image)
|
||||
elif filepath.lower().endswith('.pdf'):
|
||||
with open(filepath, 'rb') as file: # noqa: ASYNC101
|
||||
with open(filepath, 'rb') as file:
|
||||
pdf_data = file.read()
|
||||
encoded_pdf = base64.b64encode(pdf_data).decode('utf-8')
|
||||
encoded_pdf = f'data:application/pdf;base64,{encoded_pdf}'
|
||||
return FileReadObservation(path=filepath, content=encoded_pdf)
|
||||
elif filepath.lower().endswith(('.mp4', '.webm', '.ogg')):
|
||||
with open(filepath, 'rb') as file: # noqa: ASYNC101
|
||||
with open(filepath, 'rb') as file:
|
||||
video_data = file.read()
|
||||
encoded_video = base64.b64encode(video_data).decode('utf-8')
|
||||
mime_type, _ = mimetypes.guess_type(filepath)
|
||||
@@ -497,7 +493,7 @@ class ActionExecutor:
|
||||
|
||||
return FileReadObservation(path=filepath, content=encoded_video)
|
||||
|
||||
with open(filepath, 'r', encoding='utf-8') as file: # noqa: ASYNC101
|
||||
with open(filepath, 'r', encoding='utf-8') as file:
|
||||
lines = read_lines(file.readlines(), action.start, action.end)
|
||||
except FileNotFoundError:
|
||||
return ErrorObservation(
|
||||
@@ -530,7 +526,7 @@ class ActionExecutor:
|
||||
|
||||
mode = 'w' if not file_exists else 'r+'
|
||||
try:
|
||||
with open(filepath, mode, encoding='utf-8') as file: # noqa: ASYNC101
|
||||
with open(filepath, mode, encoding='utf-8') as file:
|
||||
if mode != 'w':
|
||||
all_lines = file.readlines()
|
||||
new_file = insert_lines(insert, all_lines, action.start, action.end)
|
||||
@@ -654,14 +650,11 @@ if __name__ == '__main__':
|
||||
plugins_to_load.append(ALL_PLUGINS[plugin]()) # type: ignore
|
||||
|
||||
client: ActionExecutor | None = None
|
||||
mcp_router: MCPRouter | None = None
|
||||
MCP_ROUTER_PROFILE_PATH = os.path.join(
|
||||
os.path.dirname(__file__), 'mcp', 'config.json'
|
||||
)
|
||||
mcp_proxy_manager: MCPProxyManager | None = None
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
global client, mcp_router
|
||||
global client, mcp_proxy_manager
|
||||
logger.info('Initializing ActionExecutor...')
|
||||
client = ActionExecutor(
|
||||
plugins_to_load,
|
||||
@@ -676,63 +669,36 @@ if __name__ == '__main__':
|
||||
# Check if we're on Windows
|
||||
is_windows = sys.platform == 'win32'
|
||||
|
||||
# Initialize and mount MCP Router (skip on Windows)
|
||||
# Initialize and mount MCP Proxy Manager (skip on Windows)
|
||||
if is_windows:
|
||||
logger.info('Skipping MCP Router initialization on Windows')
|
||||
mcp_router = None
|
||||
logger.info('Skipping MCP Proxy initialization on Windows')
|
||||
mcp_proxy_manager = None
|
||||
else:
|
||||
logger.info('Initializing MCP Router...')
|
||||
mcp_router = MCPRouter(
|
||||
profile_path=MCP_ROUTER_PROFILE_PATH,
|
||||
router_config=RouterConfig(
|
||||
api_key=SESSION_API_KEY,
|
||||
auth_enabled=bool(SESSION_API_KEY),
|
||||
),
|
||||
logger.info('Initializing MCP Proxy Manager...')
|
||||
# Create a MCP Proxy Manager
|
||||
mcp_proxy_manager = MCPProxyManager(
|
||||
auth_enabled=bool(SESSION_API_KEY),
|
||||
api_key=SESSION_API_KEY,
|
||||
logger_level=logger.getEffectiveLevel(),
|
||||
)
|
||||
mcp_proxy_manager.initialize()
|
||||
# Mount the proxy to the app
|
||||
allowed_origins = ['*']
|
||||
sse_app = await mcp_router.get_sse_server_app(
|
||||
allow_origins=allowed_origins, include_lifespan=False
|
||||
)
|
||||
|
||||
# Only mount SSE app if MCP Router is initialized (not on Windows)
|
||||
if mcp_router is not None:
|
||||
# Check for route conflicts before mounting
|
||||
main_app_routes = {route.path for route in app.routes}
|
||||
sse_app_routes = {route.path for route in sse_app.routes}
|
||||
conflicting_routes = main_app_routes.intersection(sse_app_routes)
|
||||
|
||||
if conflicting_routes:
|
||||
logger.error(f'Route conflicts detected: {conflicting_routes}')
|
||||
raise RuntimeError(
|
||||
f'Cannot mount SSE app - conflicting routes found: {conflicting_routes}'
|
||||
)
|
||||
|
||||
app.mount('/', sse_app)
|
||||
logger.info(
|
||||
f'Mounted MCP Router SSE app at root path with allowed origins: {allowed_origins}'
|
||||
)
|
||||
|
||||
# Additional debug logging
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug('Main app routes:')
|
||||
for route in main_app_routes:
|
||||
logger.debug(f' {route}')
|
||||
logger.debug('MCP SSE server app routes:')
|
||||
for route in sse_app_routes:
|
||||
logger.debug(f' {route}')
|
||||
try:
|
||||
await mcp_proxy_manager.mount_to_app(app, allowed_origins)
|
||||
except Exception as e:
|
||||
logger.error(f'Error mounting MCP Proxy: {e}', exc_info=True)
|
||||
raise RuntimeError(f'Cannot mount MCP Proxy: {e}')
|
||||
|
||||
yield
|
||||
|
||||
# Clean up & release the resources
|
||||
logger.info('Shutting down MCP Router...')
|
||||
if mcp_router:
|
||||
try:
|
||||
await mcp_router.shutdown()
|
||||
logger.info('MCP Router shutdown successfully.')
|
||||
except Exception as e:
|
||||
logger.error(f'Error shutting down MCP Router: {e}', exc_info=True)
|
||||
logger.info('Shutting down MCP Proxy Manager...')
|
||||
if mcp_proxy_manager:
|
||||
del mcp_proxy_manager
|
||||
mcp_proxy_manager = None
|
||||
else:
|
||||
logger.info('MCP Router instance not found for shutdown.')
|
||||
logger.info('MCP Proxy Manager instance not found for shutdown.')
|
||||
|
||||
logger.info('Closing ActionExecutor...')
|
||||
if client:
|
||||
@@ -824,6 +790,9 @@ if __name__ == '__main__':
|
||||
# Check if we're on Windows
|
||||
is_windows = sys.platform == 'win32'
|
||||
|
||||
# Access the global mcp_proxy_manager variable
|
||||
global mcp_proxy_manager
|
||||
|
||||
if is_windows:
|
||||
# On Windows, just return a success response without doing anything
|
||||
logger.info(
|
||||
@@ -838,17 +807,10 @@ if __name__ == '__main__':
|
||||
)
|
||||
|
||||
# Non-Windows implementation
|
||||
assert mcp_router is not None
|
||||
assert os.path.exists(MCP_ROUTER_PROFILE_PATH)
|
||||
|
||||
# Use synchronous file operations outside of async function
|
||||
def read_profile():
|
||||
with open(MCP_ROUTER_PROFILE_PATH, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
current_profile = read_profile()
|
||||
assert 'default' in current_profile
|
||||
assert isinstance(current_profile['default'], list)
|
||||
if mcp_proxy_manager is None:
|
||||
raise HTTPException(
|
||||
status_code=500, detail='MCP Proxy Manager is not initialized'
|
||||
)
|
||||
|
||||
# Get the request body
|
||||
mcp_tools_to_sync = await request.json()
|
||||
@@ -856,31 +818,17 @@ if __name__ == '__main__':
|
||||
raise HTTPException(
|
||||
status_code=400, detail='Request must be a list of MCP tools to sync'
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'Updating MCP server to: {json.dumps(mcp_tools_to_sync, indent=2)}.\nPrevious profile: {json.dumps(current_profile, indent=2)}'
|
||||
f'Updating MCP server with tools: {json.dumps(mcp_tools_to_sync, indent=2)}'
|
||||
)
|
||||
current_profile['default'] = mcp_tools_to_sync
|
||||
|
||||
# Use synchronous file operations outside of async function
|
||||
def write_profile(profile):
|
||||
with open(MCP_ROUTER_PROFILE_PATH, 'w') as f:
|
||||
json.dump(profile, f)
|
||||
|
||||
write_profile(current_profile)
|
||||
|
||||
# Manually reload the profile and update the servers
|
||||
mcp_router.profile_manager.reload()
|
||||
servers_wait_for_update = mcp_router.get_unique_servers()
|
||||
async with capture_logs('mcpm.router.router') as log_capture:
|
||||
await mcp_router.update_servers(servers_wait_for_update)
|
||||
router_error_log = log_capture.getvalue()
|
||||
|
||||
logger.info(
|
||||
f'MCP router updated successfully with unique servers: {servers_wait_for_update}'
|
||||
)
|
||||
if router_error_log:
|
||||
logger.warning(f'Some MCP servers failed to be added: {router_error_log}')
|
||||
mcp_tools_to_sync = [MCPStdioServerConfig(**tool) for tool in mcp_tools_to_sync]
|
||||
try:
|
||||
await mcp_proxy_manager.update_and_remount(app, mcp_tools_to_sync, ['*'])
|
||||
logger.info('MCP Proxy Manager updated and remounted successfully')
|
||||
router_error_log = ''
|
||||
except Exception as e:
|
||||
logger.error(f'Error updating MCP Proxy Manager: {e}', exc_info=True)
|
||||
router_error_log = str(e)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=200,
|
||||
@@ -915,7 +863,7 @@ if __name__ == '__main__':
|
||||
)
|
||||
|
||||
zip_path = os.path.join(full_dest_path, file.filename)
|
||||
with open(zip_path, 'wb') as buffer: # noqa: ASYNC101
|
||||
with open(zip_path, 'wb') as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Extract the zip file
|
||||
@@ -928,7 +876,7 @@ if __name__ == '__main__':
|
||||
else:
|
||||
# For single file uploads
|
||||
file_path = os.path.join(full_dest_path, file.filename)
|
||||
with open(file_path, 'wb') as buffer: # noqa: ASYNC101
|
||||
with open(file_path, 'wb') as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
logger.debug(f'Uploaded file {file.filename} to {destination}')
|
||||
|
||||
|
||||
@@ -435,7 +435,7 @@ class ActionExecutionClient(Runtime):
|
||||
# We should always include the runtime as an MCP server whenever there's > 0 stdio servers
|
||||
updated_mcp_config.sse_servers.append(
|
||||
MCPSSEServerConfig(
|
||||
url=self.action_execution_server_url.rstrip('/') + '/sse',
|
||||
url=self.action_execution_server_url.rstrip('/') + '/mcp/sse',
|
||||
api_key=self.session_api_key,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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})'
|
||||
),
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"default": []
|
||||
"mcpServers": {
|
||||
"default": {}
|
||||
},
|
||||
"tools": []
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# MCP Proxy Manager
|
||||
|
||||
This module provides a manager class for handling FastMCP proxy instances in OpenHands, including initialization, configuration, and mounting to FastAPI applications.
|
||||
|
||||
## Overview
|
||||
|
||||
The `MCPProxyManager` class encapsulates all the functionality related to creating, configuring, and managing FastMCP proxy instances. It simplifies the process of:
|
||||
|
||||
1. Initializing a FastMCP proxy
|
||||
2. Configuring the proxy with tools
|
||||
3. Mounting the proxy to a FastAPI application
|
||||
4. Updating the proxy configuration
|
||||
5. Shutting down the proxy
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from openhands.runtime.mcp.proxy import MCPProxyManager
|
||||
from fastapi import FastAPI
|
||||
|
||||
# Create a FastAPI app
|
||||
app = FastAPI()
|
||||
|
||||
# Create a proxy manager
|
||||
proxy_manager = MCPProxyManager(
|
||||
name="MyProxyServer",
|
||||
auth_enabled=True,
|
||||
api_key="my-api-key"
|
||||
)
|
||||
|
||||
# Initialize the proxy
|
||||
proxy_manager.initialize()
|
||||
|
||||
# Mount the proxy to the app
|
||||
await proxy_manager.mount_to_app(app, allow_origins=["*"])
|
||||
|
||||
# Update the tools configuration
|
||||
tools = [
|
||||
{
|
||||
"name": "my_tool",
|
||||
"description": "My tool description",
|
||||
"parameters": {...}
|
||||
}
|
||||
]
|
||||
proxy_manager.update_tools(tools)
|
||||
|
||||
# Update and remount the proxy
|
||||
await proxy_manager.update_and_remount(app, tools, allow_origins=["*"])
|
||||
|
||||
# Shutdown the proxy
|
||||
await proxy_manager.shutdown()
|
||||
```
|
||||
|
||||
### In-Memory Configuration
|
||||
|
||||
The `MCPProxyManager` maintains the configuration in-memory, eliminating the need for file-based configuration. This makes it easier to update the configuration and reduces the complexity of the code.
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Simplified API**: The `MCPProxyManager` provides a simple and intuitive API for managing FastMCP proxies.
|
||||
2. **In-Memory Configuration**: Configuration is maintained in-memory, eliminating the need for file I/O operations.
|
||||
3. **Improved Error Handling**: The manager provides better error handling and logging for proxy operations.
|
||||
4. **Cleaner Code**: By encapsulating proxy-related functionality in a dedicated class, the code is more maintainable and easier to understand.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The `MCPProxyManager` uses the `FastMCP.as_proxy()` method to create a proxy server. It manages the lifecycle of the proxy, including initialization, configuration updates, and shutdown.
|
||||
|
||||
When updating the tools configuration, the manager creates a new proxy with the updated configuration and remounts it to the FastAPI application, ensuring that the proxy is always up-to-date with the latest configuration.
|
||||
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
MCP Proxy module for OpenHands.
|
||||
"""
|
||||
|
||||
from openhands.runtime.mcp.proxy.manager import MCPProxyManager
|
||||
|
||||
__all__ = ['MCPProxyManager']
|
||||
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
MCP Proxy Manager for OpenHands.
|
||||
|
||||
This module provides a manager class for handling FastMCP proxy instances,
|
||||
including initialization, configuration, and mounting to FastAPI applications.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.utilities.logging import get_logger as fastmcp_get_logger
|
||||
|
||||
from openhands.core.config.mcp_config import MCPStdioServerConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
fastmcp_logger = fastmcp_get_logger('fastmcp')
|
||||
|
||||
|
||||
class MCPProxyManager:
|
||||
"""
|
||||
Manager for FastMCP proxy instances.
|
||||
|
||||
This class encapsulates all the functionality related to creating, configuring,
|
||||
and managing FastMCP proxy instances, including mounting them to FastAPI applications.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
auth_enabled: bool = False,
|
||||
api_key: Optional[str] = None,
|
||||
logger_level: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the MCP Proxy Manager.
|
||||
|
||||
Args:
|
||||
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
|
||||
"""
|
||||
self.auth_enabled = auth_enabled
|
||||
self.api_key = api_key
|
||||
self.proxy: Optional[FastMCP] = None
|
||||
# Initialize with a valid configuration format for FastMCP
|
||||
self.config: dict[str, Any] = {
|
||||
'mcpServers': {},
|
||||
}
|
||||
|
||||
# Configure FastMCP logger
|
||||
if logger_level is not None:
|
||||
fastmcp_logger.setLevel(logger_level)
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""
|
||||
Initialize the FastMCP proxy with the current configuration.
|
||||
"""
|
||||
if len(self.config['mcpServers']) == 0:
|
||||
logger.info(
|
||||
'No MCP servers configured for FastMCP Proxy, skipping initialization.'
|
||||
)
|
||||
return None
|
||||
|
||||
# Create a new proxy with the current configuration
|
||||
self.proxy = FastMCP.as_proxy(
|
||||
self.config,
|
||||
auth_enabled=self.auth_enabled,
|
||||
api_key=self.api_key,
|
||||
)
|
||||
|
||||
logger.info('FastMCP Proxy initialized successfully')
|
||||
|
||||
async def mount_to_app(
|
||||
self, app: FastAPI, allow_origins: Optional[list[str]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Mount the SSE server app to a FastAPI application.
|
||||
|
||||
Args:
|
||||
app: FastAPI application to mount to
|
||||
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.')
|
||||
return
|
||||
|
||||
if not self.proxy:
|
||||
raise ValueError('FastMCP Proxy is not initialized')
|
||||
|
||||
# Get the SSE app
|
||||
# mcp_app = self.proxy.http_app(path='/shttp')
|
||||
mcp_app = self.proxy.http_app(path='/sse', transport='sse')
|
||||
app.mount('/mcp', mcp_app)
|
||||
|
||||
# Remove any existing mounts at root path
|
||||
if '/mcp' in app.routes:
|
||||
app.routes.remove('/mcp')
|
||||
|
||||
app.mount('/', mcp_app)
|
||||
logger.info('Mounted FastMCP Proxy app at /mcp')
|
||||
|
||||
async def update_and_remount(
|
||||
self,
|
||||
app: FastAPI,
|
||||
stdio_servers: list[MCPStdioServerConfig],
|
||||
allow_origins: Optional[list[str]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Update the tools configuration and remount the proxy to the app.
|
||||
|
||||
This is a convenience method that combines updating the tools,
|
||||
shutting down the existing proxy, initializing a new one, and
|
||||
mounting it to the app.
|
||||
|
||||
Args:
|
||||
app: FastAPI application to mount to
|
||||
stdio_servers: List of stdio server configurations
|
||||
allow_origins: List of allowed origins for CORS
|
||||
"""
|
||||
tools = {t.name: t.model_dump() for t in stdio_servers}
|
||||
self.config['mcpServers'] = tools
|
||||
|
||||
del self.proxy
|
||||
self.proxy = None
|
||||
|
||||
# Initialize a new proxy
|
||||
self.initialize()
|
||||
|
||||
# Mount the new proxy to the app
|
||||
await self.mount_to_app(app, allow_origins)
|
||||
@@ -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)
|
||||
|
||||
@@ -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,17 +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()
|
||||
except Exception:
|
||||
logger.exception("error_stopping_container")
|
||||
|
||||
# Check up to 3 times that client has closed
|
||||
for _ in range(3):
|
||||
response = await client.get(f'{nested_url}/api/conversations/{sid}')
|
||||
response.raise_for_status()
|
||||
if response.json().get('status') == 'STOPPED':
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
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:
|
||||
@@ -381,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(
|
||||
@@ -423,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(
|
||||
@@ -455,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
|
||||
@@ -501,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
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ async def get_conversation(
|
||||
conversation_id, user_id
|
||||
)
|
||||
if not conversation:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
f'get_conversation: conversation {conversation_id} not found, attach_to_conversation returned None',
|
||||
extra={'session_id': conversation_id, 'user_id': user_id},
|
||||
)
|
||||
|
||||
Generated
+299
-393
File diff suppressed because one or more lines are too long
+5
-6
@@ -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 = "*"
|
||||
@@ -67,7 +67,6 @@ poetry = "^2.1.2"
|
||||
anyio = "4.9.0"
|
||||
pythonnet = "*"
|
||||
fastmcp = "^2.5.2"
|
||||
mcpm = "1.12.0"
|
||||
python-frontmatter = "^1.1.0"
|
||||
# TODO: Should these go into the runtime group?
|
||||
ipywidgets = "^8.1.5"
|
||||
@@ -81,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 = "*" }
|
||||
@@ -91,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 = "*"
|
||||
|
||||
@@ -114,9 +114,11 @@ def test_default_activated_tools():
|
||||
)
|
||||
with open(mcp_config_path, 'r') as f:
|
||||
mcp_config = json.load(f)
|
||||
assert 'default' in mcp_config
|
||||
assert 'mcpServers' in mcp_config
|
||||
assert 'default' in mcp_config['mcpServers']
|
||||
assert 'tools' in mcp_config
|
||||
# no tools are always activated yet
|
||||
assert len(mcp_config['default']) == 0
|
||||
assert len(mcp_config['tools']) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -249,7 +251,11 @@ async def test_both_stdio_and_sse_mcp(
|
||||
assert obs_cat.exit_code == 0
|
||||
|
||||
mcp_action_fetch = MCPAction(
|
||||
name='fetch', arguments={'url': 'http://localhost:8000'}
|
||||
# NOTE: the tool name is `fetch_fetch` because the tool name is `fetch`
|
||||
# And FastMCP Proxy will pre-pend the server name (in this case, `fetch`)
|
||||
# to the tool name, so the full tool name becomes `fetch_fetch`
|
||||
name='fetch',
|
||||
arguments={'url': 'http://localhost:8000'},
|
||||
)
|
||||
obs_fetch = await runtime.call_tool_mcp(mcp_action_fetch)
|
||||
logger.info(obs_fetch, extra={'msg_type': 'OBSERVATION'})
|
||||
@@ -304,7 +310,9 @@ async def test_microagent_and_one_stdio_mcp_in_config(
|
||||
logger.info(f'updated_config: {updated_config}')
|
||||
|
||||
# ======= Test the stdio server in the config =======
|
||||
mcp_action_sse = MCPAction(name='list_directory', arguments={'path': '/'})
|
||||
mcp_action_sse = MCPAction(
|
||||
name='filesystem_list_directory', arguments={'path': '/'}
|
||||
)
|
||||
obs_sse = await runtime.call_tool_mcp(mcp_action_sse)
|
||||
logger.info(obs_sse, extra={'msg_type': 'OBSERVATION'})
|
||||
assert isinstance(obs_sse, MCPObservation), (
|
||||
@@ -332,7 +340,7 @@ async def test_microagent_and_one_stdio_mcp_in_config(
|
||||
assert obs_cat.exit_code == 0
|
||||
|
||||
mcp_action_fetch = MCPAction(
|
||||
name='fetch', arguments={'url': 'http://localhost:8000'}
|
||||
name='fetch_fetch', arguments={'url': 'http://localhost:8000'}
|
||||
)
|
||||
obs_fetch = await runtime.call_tool_mcp(mcp_action_fetch)
|
||||
logger.info(obs_fetch, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
Reference in New Issue
Block a user