mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c3fbd6e6c |
+1
-1
@@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
|
||||
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.18-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.17-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -43,17 +43,17 @@ See the [Installation](https://docs.all-hands.dev/modules/usage/installation) gu
|
||||
system requirements and more information.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-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.18
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17
|
||||
```
|
||||
|
||||
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
|
||||
@@ -64,16 +64,16 @@ works best, but you have [many options](https://docs.all-hands.dev/modules/usage
|
||||
|
||||
---
|
||||
|
||||
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes#connecting-to-your-filesystem),
|
||||
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes),
|
||||
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
|
||||
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
|
||||
or run it on tagged issues with [a github action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
or run it on tagged issues with [a github action](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md).
|
||||
|
||||
Visit [Installation](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
|
||||
|
||||
> [!CAUTION]
|
||||
> OpenHands is meant to be run by a single user on their local workstation.
|
||||
> It is not appropriate for multi-tenant deployments where multiple users share the same instance. There is no built-in isolation or scalability.
|
||||
> It is not appropriate for multi-tenant deployments, where multiple users share the same instance--there is no built-in isolation or scalability.
|
||||
>
|
||||
> If you're interested in running OpenHands in a multi-tenant environment, please
|
||||
> [get in touch with us](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
@@ -86,7 +86,7 @@ Having issues? The [Troubleshooting Guide](https://docs.all-hands.dev/modules/us
|
||||
## 📖 Documentation
|
||||
|
||||
To learn more about the project, and for tips on using OpenHands,
|
||||
check out our [documentation](https://docs.all-hands.dev/modules/usage/getting-started).
|
||||
**check out our [documentation](https://docs.all-hands.dev/modules/usage/getting-started)**.
|
||||
|
||||
There you'll find resources on how to use different LLM providers,
|
||||
troubleshooting resources, and advanced configuration options.
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.18-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.17-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.18-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.17-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -61,7 +61,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -56,6 +56,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
La façon la plus simple d'exécuter OpenHands est avec Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.18
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17
|
||||
```
|
||||
|
||||
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -59,7 +59,7 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -57,6 +57,6 @@ docker run -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
在 Docker 中运行 OpenHands 是最简单的方式。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.18
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17
|
||||
```
|
||||
|
||||
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
@@ -6,9 +6,10 @@ This mode is different from the [headless mode](headless-mode), which is non-int
|
||||
|
||||
## With Python
|
||||
|
||||
To start an interactive OpenHands session via the command line:
|
||||
To start an interactive OpenHands session via the command line, follow these steps:
|
||||
|
||||
1. Ensure you have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
|
||||
2. Run the following command:
|
||||
|
||||
```bash
|
||||
@@ -20,32 +21,45 @@ This command will start an interactive session where you can input tasks and rec
|
||||
You'll need to be sure to set your model, API key, and other settings via environment variables
|
||||
[or the `config.toml` file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml).
|
||||
|
||||
|
||||
## With Docker
|
||||
|
||||
To run OpenHands in CLI mode with Docker:
|
||||
To run OpenHands in CLI mode with Docker, follow these steps:
|
||||
|
||||
1. Set the following environmental variables in your terminal:
|
||||
1. Set `WORKSPACE_BASE` to the directory you want OpenHands to edit:
|
||||
|
||||
* `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
|
||||
* `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
|
||||
* `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
|
||||
```bash
|
||||
WORKSPACE_BASE=$(pwd)/workspace
|
||||
```
|
||||
|
||||
2. Run the following Docker command:
|
||||
2. Set `LLM_MODEL` to the model you want to use:
|
||||
|
||||
```bash
|
||||
LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"
|
||||
|
||||
```
|
||||
|
||||
3. Set `LLM_API_KEY` to your API key:
|
||||
|
||||
```bash
|
||||
LLM_API_KEY="sk_test_12345"
|
||||
```
|
||||
|
||||
4. Run the following Docker command:
|
||||
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
-e LLM_MODEL=$LLM_MODEL \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.cli
|
||||
```
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@ This is different from [CLI Mode](cli-mode), which is interactive, and better fo
|
||||
|
||||
## With Python
|
||||
|
||||
To run OpenHands in headless mode with Python:
|
||||
1. Ensure you have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
|
||||
2. Run the following command:
|
||||
To run OpenHands in headless mode with Python,
|
||||
[follow the Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md),
|
||||
and then run:
|
||||
|
||||
```bash
|
||||
poetry run python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
poetry run python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
You'll need to be sure to set your model, API key, and other settings via environment variables
|
||||
@@ -19,20 +20,31 @@ You'll need to be sure to set your model, API key, and other settings via enviro
|
||||
|
||||
## With Docker
|
||||
|
||||
To run OpenHands in Headless mode with Docker:
|
||||
1. Set `WORKSPACE_BASE` to the directory you want OpenHands to edit:
|
||||
|
||||
1. Set the following environmental variables in your terminal:
|
||||
```bash
|
||||
WORKSPACE_BASE=$(pwd)/workspace
|
||||
```
|
||||
|
||||
* `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
|
||||
* `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
|
||||
* `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
|
||||
2. Set `LLM_MODEL` to the model you want to use:
|
||||
|
||||
2. Run the following Docker command:
|
||||
```bash
|
||||
LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"
|
||||
|
||||
```
|
||||
|
||||
3. Set `LLM_API_KEY` to your API key:
|
||||
|
||||
```bash
|
||||
LLM_API_KEY="sk_test_12345"
|
||||
```
|
||||
|
||||
4. Run the following Docker command:
|
||||
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -40,17 +52,8 @@ docker run -it \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.18 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
|
||||
```
|
||||
|
||||
## Advanced Headless Configurations
|
||||
|
||||
To view all available configuration options for headless mode, run the Python command with the `--help` flag.
|
||||
|
||||
### Additional Logs
|
||||
|
||||
For the headless mode to log all the agent actions, in your terminal run: `export LOG_ALL_EVENTS=true`
|
||||
|
||||
@@ -11,25 +11,20 @@
|
||||
The easiest way to run OpenHands is in Docker.
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-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.18
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.17
|
||||
```
|
||||
|
||||
You'll find OpenHands running at http://localhost:3000!
|
||||
|
||||
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes#connecting-to-your-filesystem),
|
||||
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
|
||||
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
|
||||
or run it on tagged issues with [a github action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or using the [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ some flags being passed to `docker run` that make this possible:
|
||||
|
||||
```
|
||||
docker run # ...
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.18-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.17-nikolaik \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ...
|
||||
```
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.18.0",
|
||||
"version": "0.17.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.18.0",
|
||||
"version": "0.17.0",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@nextui-org/react": "^2.6.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.18.0",
|
||||
"version": "0.17.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -33,6 +33,12 @@ enum ActionType {
|
||||
// Reject a request from user or another agent.
|
||||
REJECT = "reject",
|
||||
|
||||
// Adds a task to the plan.
|
||||
ADD_TASK = "add_task",
|
||||
|
||||
// Updates a task in the plan.
|
||||
MODIFY_TASK = "modify_task",
|
||||
|
||||
// Changes the state of the agent, e.g. to paused or running
|
||||
CHANGE_AGENT_STATE = "change_agent_state",
|
||||
}
|
||||
|
||||
@@ -78,6 +78,27 @@ export interface BrowseInteractiveAction
|
||||
};
|
||||
}
|
||||
|
||||
export interface AddTaskAction extends OpenHandsActionEvent<"add_task"> {
|
||||
source: "agent";
|
||||
timeout: number;
|
||||
args: {
|
||||
parent: string;
|
||||
goal: string;
|
||||
subtasks: unknown[];
|
||||
thought: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ModifyTaskAction extends OpenHandsActionEvent<"modify_task"> {
|
||||
source: "agent";
|
||||
timeout: number;
|
||||
args: {
|
||||
task_id: string;
|
||||
state: string;
|
||||
thought: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FileReadAction extends OpenHandsActionEvent<"read"> {
|
||||
source: "agent";
|
||||
args: {
|
||||
@@ -123,4 +144,6 @@ export type OpenHandsAction =
|
||||
| FileReadAction
|
||||
| FileEditAction
|
||||
| FileWriteAction
|
||||
| AddTaskAction
|
||||
| ModifyTaskAction
|
||||
| RejectAction;
|
||||
|
||||
@@ -10,6 +10,8 @@ export type OpenHandsEventType =
|
||||
| "browse"
|
||||
| "browse_interactive"
|
||||
| "reject"
|
||||
| "add_task"
|
||||
| "modify_task"
|
||||
| "finish"
|
||||
| "error";
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ from openhands.agenthub import ( # noqa: E402
|
||||
codeact_agent,
|
||||
delegator_agent,
|
||||
dummy_agent,
|
||||
planner_agent,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'codeact_agent',
|
||||
'planner_agent',
|
||||
'delegator_agent',
|
||||
'dummy_agent',
|
||||
'browsing_agent',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import TypedDict
|
||||
from typing import TypedDict, Union
|
||||
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
@@ -6,6 +6,7 @@ from openhands.core.config import AgentConfig
|
||||
from openhands.core.schema import AgentState
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
AddTaskAction,
|
||||
AgentFinishAction,
|
||||
AgentRejectAction,
|
||||
BrowseInteractiveAction,
|
||||
@@ -14,10 +15,10 @@ from openhands.events.action import (
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
MessageAction,
|
||||
ModifyTaskAction,
|
||||
)
|
||||
from openhands.events.observation import (
|
||||
AgentStateChangedObservation,
|
||||
BrowserOutputObservation,
|
||||
CmdOutputObservation,
|
||||
FileReadObservation,
|
||||
FileWriteObservation,
|
||||
@@ -48,6 +49,20 @@ class DummyAgent(Agent):
|
||||
def __init__(self, llm: LLM, config: AgentConfig):
|
||||
super().__init__(llm, config)
|
||||
self.steps: list[ActionObs] = [
|
||||
{
|
||||
'action': AddTaskAction(
|
||||
parent='None', goal='check the current directory'
|
||||
),
|
||||
'observations': [],
|
||||
},
|
||||
{
|
||||
'action': AddTaskAction(parent='0', goal='run ls'),
|
||||
'observations': [],
|
||||
},
|
||||
{
|
||||
'action': ModifyTaskAction(task_id='0', state='in_progress'),
|
||||
'observations': [],
|
||||
},
|
||||
{
|
||||
'action': MessageAction('Time to get started!'),
|
||||
'observations': [],
|
||||
@@ -90,12 +105,7 @@ class DummyAgent(Agent):
|
||||
{
|
||||
'action': BrowseURLAction(url='https://google.com'),
|
||||
'observations': [
|
||||
BrowserOutputObservation(
|
||||
'<html><body>Simulated Google page</body></html>',
|
||||
url='https://google.com',
|
||||
screenshot='',
|
||||
trigger_by_action='',
|
||||
),
|
||||
# BrowserOutputObservation('<html><body>Simulated Google page</body></html>',url='https://google.com',screenshot=''),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -103,12 +113,7 @@ class DummyAgent(Agent):
|
||||
browser_actions='goto("https://google.com")'
|
||||
),
|
||||
'observations': [
|
||||
BrowserOutputObservation(
|
||||
'<html><body>Simulated Google page after interaction</body></html>',
|
||||
url='https://google.com',
|
||||
screenshot='',
|
||||
trigger_by_action='',
|
||||
),
|
||||
# BrowserOutputObservation('<html><body>Simulated Google page after interaction</body></html>',url='https://google.com',screenshot=''),
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -130,6 +135,30 @@ class DummyAgent(Agent):
|
||||
current_step = self.steps[state.iteration]
|
||||
action = current_step['action']
|
||||
|
||||
# If the action is AddTaskAction or ModifyTaskAction, update the parent ID or task_id
|
||||
if isinstance(action, AddTaskAction):
|
||||
if action.parent == 'None':
|
||||
action.parent = '' # Root task has no parent
|
||||
elif action.parent == '0':
|
||||
action.parent = state.root_task.id
|
||||
elif action.parent.startswith('0.'):
|
||||
action.parent = f'{state.root_task.id}{action.parent[1:]}'
|
||||
elif isinstance(action, ModifyTaskAction):
|
||||
if action.task_id == '0':
|
||||
action.task_id = state.root_task.id
|
||||
elif action.task_id.startswith('0.'):
|
||||
action.task_id = f'{state.root_task.id}{action.task_id[1:]}'
|
||||
# Ensure the task_id doesn't start with a dot
|
||||
if action.task_id.startswith('.'):
|
||||
action.task_id = action.task_id[1:]
|
||||
elif isinstance(action, (BrowseURLAction, BrowseInteractiveAction)):
|
||||
try:
|
||||
return self.simulate_browser_action(action)
|
||||
except (
|
||||
Exception
|
||||
): # This could be a specific exception for browser unavailability
|
||||
return self.handle_browser_unavailable(action)
|
||||
|
||||
if state.iteration > 0:
|
||||
prev_step = self.steps[state.iteration - 1]
|
||||
|
||||
@@ -161,3 +190,22 @@ class DummyAgent(Agent):
|
||||
)
|
||||
|
||||
return action
|
||||
|
||||
def simulate_browser_action(
|
||||
self, action: Union[BrowseURLAction, BrowseInteractiveAction]
|
||||
) -> Action:
|
||||
# Instead of simulating, we'll reject the browser action
|
||||
return self.handle_browser_unavailable(action)
|
||||
|
||||
def handle_browser_unavailable(
|
||||
self, action: Union[BrowseURLAction, BrowseInteractiveAction]
|
||||
) -> Action:
|
||||
# Create a message action to inform that browsing is not available
|
||||
message = 'Browser actions are not available in the DummyAgent environment.'
|
||||
if isinstance(action, BrowseURLAction):
|
||||
message += f' Unable to browse URL: {action.url}'
|
||||
elif isinstance(action, BrowseInteractiveAction):
|
||||
message += (
|
||||
f' Unable to perform interactive browsing: {action.browser_actions}'
|
||||
)
|
||||
return MessageAction(content=message)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from openhands.agenthub.planner_agent.agent import PlannerAgent
|
||||
from openhands.controller.agent import Agent
|
||||
|
||||
Agent.register('PlannerAgent', PlannerAgent)
|
||||
@@ -0,0 +1,53 @@
|
||||
from openhands.agenthub.planner_agent.prompt import get_prompt_and_images
|
||||
from openhands.agenthub.planner_agent.response_parser import PlannerResponseParser
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import AgentConfig
|
||||
from openhands.core.message import ImageContent, Message, TextContent
|
||||
from openhands.events.action import Action, AgentFinishAction
|
||||
from openhands.llm.llm import LLM
|
||||
|
||||
|
||||
class PlannerAgent(Agent):
|
||||
VERSION = '1.0'
|
||||
"""
|
||||
The planner agent utilizes a special prompting strategy to create long term plans for solving problems.
|
||||
The agent is given its previous action-observation pairs, current task, and hint based on last action taken at every step.
|
||||
"""
|
||||
response_parser = PlannerResponseParser()
|
||||
|
||||
def __init__(self, llm: LLM, config: AgentConfig):
|
||||
"""Initialize the Planner Agent with an LLM
|
||||
|
||||
Parameters:
|
||||
- llm (LLM): The llm to be used by this agent
|
||||
"""
|
||||
super().__init__(llm, config)
|
||||
|
||||
def step(self, state: State) -> Action:
|
||||
"""Checks to see if current step is completed, returns AgentFinishAction if True.
|
||||
Otherwise, creates a plan prompt and sends to model for inference, returning the result as the next action.
|
||||
|
||||
Parameters:
|
||||
- state (State): The current state given the previous actions and observations
|
||||
|
||||
Returns:
|
||||
- AgentFinishAction: If the last state was 'completed', 'verified', or 'abandoned'
|
||||
- Action: The next action to take based on llm response
|
||||
"""
|
||||
if state.root_task.state in [
|
||||
'completed',
|
||||
'verified',
|
||||
'abandoned',
|
||||
]:
|
||||
return AgentFinishAction()
|
||||
|
||||
prompt, image_urls = get_prompt_and_images(
|
||||
state, self.llm.config.max_message_chars
|
||||
)
|
||||
content = [TextContent(text=prompt)]
|
||||
if self.llm.vision_is_active() and image_urls:
|
||||
content.append(ImageContent(image_urls=image_urls))
|
||||
message = Message(role='user', content=content)
|
||||
resp = self.llm.completion(messages=self.llm.format_messages_for_llm(message))
|
||||
return self.response_parser.parse(resp)
|
||||
@@ -0,0 +1,191 @@
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.schema import ActionType
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
NullAction,
|
||||
)
|
||||
from openhands.events.serialization.action import action_from_dict
|
||||
from openhands.events.serialization.event import event_to_memory
|
||||
|
||||
HISTORY_SIZE = 20
|
||||
|
||||
prompt = """
|
||||
# Task
|
||||
You're a diligent software engineer AI. You can't see, draw, or interact with a
|
||||
browser, but you can read and write files, and you can run commands, and you can think.
|
||||
|
||||
You've been given the following task:
|
||||
|
||||
%(task)s
|
||||
|
||||
## Plan
|
||||
As you complete this task, you're building a plan and keeping
|
||||
track of your progress. Here's a JSON representation of your plan:
|
||||
|
||||
%(plan)s
|
||||
|
||||
|
||||
%(plan_status)s
|
||||
|
||||
You're responsible for managing this plan and the status of tasks in
|
||||
it, by using the `add_task` and `modify_task` actions described below.
|
||||
|
||||
If the History below contradicts the state of any of these tasks, you
|
||||
MUST modify the task using the `modify_task` action described below.
|
||||
|
||||
Be sure NOT to duplicate any tasks. Do NOT use the `add_task` action for
|
||||
a task that's already represented. Every task must be represented only once.
|
||||
|
||||
Tasks that are sequential MUST be siblings. They must be added in order
|
||||
to their parent task.
|
||||
|
||||
If you mark a task as 'completed', 'verified', or 'abandoned',
|
||||
all non-abandoned subtasks will be marked the same way.
|
||||
So before closing a task this way, you MUST not only be sure that it has
|
||||
been completed successfully--you must ALSO be sure that all its subtasks
|
||||
are ready to be marked the same way.
|
||||
|
||||
If, and only if, ALL tasks have already been marked verified,
|
||||
you MUST respond with the `finish` action.
|
||||
|
||||
## History
|
||||
Here is a recent history of actions you've taken in service of this plan,
|
||||
as well as observations you've made. This only includes the MOST RECENT
|
||||
ten actions--more happened before that.
|
||||
|
||||
%(history)s
|
||||
|
||||
|
||||
Your most recent action is at the bottom of that history.
|
||||
|
||||
## Action
|
||||
What is your next thought or action? Your response must be in JSON format.
|
||||
|
||||
It must be an object, and it must contain two fields:
|
||||
* `action`, which is one of the actions below
|
||||
* `args`, which is a map of key-value pairs, specifying the arguments for that action
|
||||
|
||||
* `read` - reads the content of a file. Arguments:
|
||||
* `path` - the path of the file to read
|
||||
* `write` - writes the content to a file. Arguments:
|
||||
* `path` - the path of the file to write
|
||||
* `content` - the content to write to the file
|
||||
* `run` - runs a command on the command line in a Linux shell. Arguments:
|
||||
* `command` - the command to run
|
||||
* `browse` - opens a web page. Arguments:
|
||||
* `url` - the URL to open
|
||||
* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
|
||||
* `content` - the message to record
|
||||
* `wait_for_response` - set to `true` to wait for the user to respond before proceeding
|
||||
* `add_task` - add a task to your plan. Arguments:
|
||||
* `parent` - the ID of the parent task (leave empty if it should go at the top level)
|
||||
* `goal` - the goal of the task
|
||||
* `subtasks` - a list of subtasks, each of which is a map with a `goal` key.
|
||||
* `modify_task` - close a task. Arguments:
|
||||
* `task_id` - the ID of the task to close
|
||||
* `state` - set to 'in_progress' to start the task, 'completed' to finish it, 'verified' to assert that it was successful, 'abandoned' to give up on it permanently, or `open` to stop working on it for now.
|
||||
* `finish` - if ALL of your tasks and subtasks have been verified or abandoned, and you're absolutely certain that you've completed your task and have tested your work, use the finish action to stop working.
|
||||
|
||||
You MUST take time to think in between read, write, run, and browse actions--do this with the `message` action.
|
||||
You should never act twice in a row without thinking. But if your last several
|
||||
actions are all `message` actions, you should consider taking a different action.
|
||||
|
||||
What is your next thought or action? Again, you must reply with JSON, and only with JSON.
|
||||
|
||||
%(hint)s
|
||||
"""
|
||||
|
||||
|
||||
def get_hint(latest_action_id: str) -> str:
|
||||
"""Returns action type hint based on given action_id"""
|
||||
hints = {
|
||||
'': "You haven't taken any actions yet. Start by using `ls` to check out what files you're working with.",
|
||||
ActionType.RUN: 'You should think about the command you just ran, what output it gave, and how that affects your plan.',
|
||||
ActionType.READ: 'You should think about the file you just read, what you learned from it, and how that affects your plan.',
|
||||
ActionType.WRITE: 'You just changed a file. You should think about how it affects your plan.',
|
||||
ActionType.BROWSE: 'You should think about the page you just visited, and what you learned from it.',
|
||||
ActionType.MESSAGE: "Look at your last thought in the history above. What does it suggest? Don't think anymore--take action.",
|
||||
ActionType.ADD_TASK: 'You should think about the next action to take.',
|
||||
ActionType.MODIFY_TASK: 'You should think about the next action to take.',
|
||||
ActionType.SUMMARIZE: '',
|
||||
ActionType.FINISH: '',
|
||||
}
|
||||
return hints.get(latest_action_id, '')
|
||||
|
||||
|
||||
def get_prompt_and_images(
|
||||
state: State, max_message_chars: int
|
||||
) -> tuple[str, list[str] | None]:
|
||||
"""Gets the prompt for the planner agent.
|
||||
|
||||
Formatted with the most recent action-observation pairs, current task, and hint based on last action
|
||||
|
||||
Parameters:
|
||||
- state (State): The state of the current agent
|
||||
|
||||
Returns:
|
||||
- str: The formatted string prompt with historical values
|
||||
"""
|
||||
# the plan
|
||||
plan_str = json.dumps(state.root_task.to_dict(), indent=2)
|
||||
|
||||
# the history
|
||||
history_dicts = []
|
||||
latest_action: Action = NullAction()
|
||||
|
||||
# retrieve the latest HISTORY_SIZE events
|
||||
for event_count, event in enumerate(reversed(state.history)):
|
||||
if event_count >= HISTORY_SIZE:
|
||||
break
|
||||
if latest_action == NullAction() and isinstance(event, Action):
|
||||
latest_action = event
|
||||
history_dicts.append(event_to_memory(event, max_message_chars))
|
||||
|
||||
# history_dicts is in reverse order, lets fix it
|
||||
history_dicts.reverse()
|
||||
|
||||
# and get it as a JSON string
|
||||
history_str = json.dumps(history_dicts, indent=2)
|
||||
|
||||
# the plan status
|
||||
current_task = state.root_task.get_current_task()
|
||||
if current_task is not None:
|
||||
plan_status = f"You're currently working on this task:\n{current_task.goal}."
|
||||
if len(current_task.subtasks) == 0:
|
||||
plan_status += "\nIf it's not achievable AND verifiable with a SINGLE action, you MUST break it down into subtasks NOW."
|
||||
else:
|
||||
plan_status = "You're not currently working on any tasks. Your next action MUST be to mark a task as in_progress."
|
||||
|
||||
# the hint, based on the last action
|
||||
hint = get_hint(event_to_memory(latest_action, max_message_chars).get('action', ''))
|
||||
logger.debug('HINT:\n' + hint, extra={'msg_type': 'DETAIL'})
|
||||
|
||||
# the last relevant user message (the task)
|
||||
message, image_urls = state.get_current_user_intent()
|
||||
|
||||
# finally, fill in the prompt
|
||||
return prompt % {
|
||||
'task': message,
|
||||
'plan': plan_str,
|
||||
'history': history_str,
|
||||
'hint': hint,
|
||||
'plan_status': plan_status,
|
||||
}, image_urls
|
||||
|
||||
|
||||
def parse_response(response: str) -> Action:
|
||||
"""Parses the model output to find a valid action to take
|
||||
Parameters:
|
||||
- response (str): A response from the model that potentially contains an Action.
|
||||
|
||||
Returns:
|
||||
- Action: A valid next action to perform from model output
|
||||
"""
|
||||
action_dict = json.loads(response)
|
||||
if 'contents' in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
action_dict['content'] = action_dict.pop('contents')
|
||||
action = action_from_dict(action_dict)
|
||||
return action
|
||||
@@ -0,0 +1,37 @@
|
||||
from openhands.controller.action_parser import ResponseParser
|
||||
from openhands.core.utils import json
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
)
|
||||
from openhands.events.serialization.action import action_from_dict
|
||||
|
||||
|
||||
class PlannerResponseParser(ResponseParser):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def parse(self, response: str) -> Action:
|
||||
action_str = self.parse_response(response)
|
||||
return self.parse_action(action_str)
|
||||
|
||||
def parse_response(self, response) -> str:
|
||||
# get the next action from the response
|
||||
return response['choices'][0]['message']['content']
|
||||
|
||||
def parse_action(self, action_str: str) -> Action:
|
||||
"""Parses a string to find an action within it
|
||||
|
||||
Parameters:
|
||||
- response (str): The string to be parsed
|
||||
|
||||
Returns:
|
||||
- Action: The action that was found in the response string
|
||||
"""
|
||||
# attempt to load the JSON dict from the response
|
||||
action_dict = json.loads(action_str)
|
||||
|
||||
if 'content' in action_dict:
|
||||
# The LLM gets confused here. Might as well be robust
|
||||
action_dict['contents'] = action_dict.pop('content')
|
||||
|
||||
return action_from_dict(action_dict)
|
||||
@@ -26,6 +26,7 @@ from openhands.events import EventSource, EventStream, EventStreamSubscriber
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
ActionConfirmationStatus,
|
||||
AddTaskAction,
|
||||
AgentDelegateAction,
|
||||
AgentFinishAction,
|
||||
AgentRejectAction,
|
||||
@@ -33,6 +34,7 @@ from openhands.events.action import (
|
||||
CmdRunAction,
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
ModifyTaskAction,
|
||||
NullAction,
|
||||
)
|
||||
from openhands.events.event import Event
|
||||
@@ -206,12 +208,11 @@ class AgentController:
|
||||
reported = RuntimeError(
|
||||
'There was an unexpected error while running the agent.'
|
||||
)
|
||||
if isinstance(e, litellm.AuthenticationError):
|
||||
if isinstance(e, litellm.LLMError):
|
||||
reported = e
|
||||
await self._react_to_exception(reported)
|
||||
|
||||
def should_step(self, event: Event) -> bool:
|
||||
print('should step?', event)
|
||||
if isinstance(event, Action):
|
||||
if isinstance(event, MessageAction) and event.source == EventSource.USER:
|
||||
return True
|
||||
@@ -260,7 +261,12 @@ class AgentController:
|
||||
await self._handle_message_action(action)
|
||||
elif isinstance(action, AgentDelegateAction):
|
||||
await self.start_delegate(action)
|
||||
|
||||
elif isinstance(action, AddTaskAction):
|
||||
self.state.root_task.add_subtask(
|
||||
action.parent, action.goal, action.subtasks
|
||||
)
|
||||
elif isinstance(action, ModifyTaskAction):
|
||||
self.state.root_task.set_subtask_state(action.task_id, action.state)
|
||||
elif isinstance(action, AgentFinishAction):
|
||||
self.state.outputs = action.outputs
|
||||
self.state.metrics.merge(self.state.local_metrics)
|
||||
@@ -536,9 +542,7 @@ class AgentController:
|
||||
self.update_state_before_step()
|
||||
action: Action = NullAction()
|
||||
try:
|
||||
print('STEP AGENT')
|
||||
action = self.agent.step(self.state)
|
||||
print('GOT ACTION', action)
|
||||
if action is None:
|
||||
raise LLMNoActionError('No action was returned')
|
||||
except (
|
||||
@@ -726,20 +730,12 @@ class AgentController:
|
||||
# - the previous session, in which case it has history
|
||||
# - from a parent agent, in which case it has no history
|
||||
# - None / a new state
|
||||
|
||||
# If state is None, we create a brand new state and still load the event stream so we can restore the history
|
||||
if state is None:
|
||||
self.state = State(
|
||||
inputs={},
|
||||
max_iterations=max_iterations,
|
||||
confirmation_mode=confirmation_mode,
|
||||
)
|
||||
self.state.start_id = 0
|
||||
|
||||
self.log(
|
||||
'debug',
|
||||
f'AgentController {self.id} - created new state. start_id: {self.state.start_id}',
|
||||
)
|
||||
else:
|
||||
self.state = state
|
||||
|
||||
@@ -751,8 +747,7 @@ class AgentController:
|
||||
f'AgentController {self.id} initializing history from event {self.state.start_id}',
|
||||
)
|
||||
|
||||
# Always load from the event stream to avoid losing history
|
||||
self._init_history()
|
||||
self._init_history()
|
||||
|
||||
def _init_history(self) -> None:
|
||||
"""Initializes the agent's history from the event stream.
|
||||
|
||||
@@ -91,7 +91,7 @@ def display_event(event: Event, config: AppConfig):
|
||||
display_confirmation(event.confirmation_state)
|
||||
|
||||
|
||||
async def main(loop):
|
||||
async def main():
|
||||
"""Runs the agent in CLI mode"""
|
||||
|
||||
parser = get_parser()
|
||||
@@ -112,7 +112,7 @@ async def main(loop):
|
||||
|
||||
logger.setLevel(logging.WARNING)
|
||||
config = load_app_config(config_file=args.config_file)
|
||||
sid = str(uuid4())
|
||||
sid = 'cli'
|
||||
|
||||
agent_cls: Type[Agent] = Agent.get_cls(config.default_agent)
|
||||
agent_config = config.get_agent_config(config.default_agent)
|
||||
@@ -150,6 +150,7 @@ async def main(loop):
|
||||
|
||||
async def prompt_for_next_task():
|
||||
# Run input() in a thread pool to avoid blocking the event loop
|
||||
loop = asyncio.get_event_loop()
|
||||
next_message = await loop.run_in_executor(
|
||||
None, lambda: input('How can I help? >> ')
|
||||
)
|
||||
@@ -164,12 +165,13 @@ async def main(loop):
|
||||
event_stream.add_event(action, EventSource.USER)
|
||||
|
||||
async def prompt_for_user_confirmation():
|
||||
loop = asyncio.get_event_loop()
|
||||
user_confirmation = await loop.run_in_executor(
|
||||
None, lambda: input('Confirm action (possible security risk)? (y/n) >> ')
|
||||
)
|
||||
return user_confirmation.lower() == 'y'
|
||||
|
||||
async def on_event_async(event: Event):
|
||||
async def on_event(event: Event):
|
||||
display_event(event, config)
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
if event.agent_state in [
|
||||
@@ -191,9 +193,6 @@ async def main(loop):
|
||||
ChangeAgentStateAction(AgentState.USER_REJECTED), EventSource.USER
|
||||
)
|
||||
|
||||
def on_event(event: Event) -> None:
|
||||
loop.create_task(on_event_async(event))
|
||||
|
||||
event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, str(uuid4()))
|
||||
|
||||
await runtime.connect()
|
||||
@@ -209,7 +208,7 @@ if __name__ == '__main__':
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
loop.run_until_complete(main(loop))
|
||||
loop.run_until_complete(main())
|
||||
except KeyboardInterrupt:
|
||||
print('Received keyboard interrupt, shutting down...')
|
||||
except ConnectionRefusedError as e:
|
||||
|
||||
@@ -385,7 +385,7 @@ def get_parser() -> argparse.ArgumentParser:
|
||||
parser.add_argument(
|
||||
'-n',
|
||||
'--name',
|
||||
default='',
|
||||
default='default',
|
||||
type=str,
|
||||
help='Name for the session',
|
||||
)
|
||||
|
||||
@@ -182,7 +182,7 @@ async def run_controller(
|
||||
# init with the provided actions
|
||||
event_stream.add_event(initial_user_action, EventSource.USER)
|
||||
|
||||
def on_event(event: Event):
|
||||
async def on_event(event: Event):
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
if event.agent_state == AgentState.AWAITING_USER_INPUT:
|
||||
if exit_on_message:
|
||||
|
||||
@@ -62,6 +62,10 @@ class ActionTypeSchema(BaseModel):
|
||||
|
||||
SUMMARIZE: str = Field(default='summarize')
|
||||
|
||||
ADD_TASK: str = Field(default='add_task')
|
||||
|
||||
MODIFY_TASK: str = Field(default='modify_task')
|
||||
|
||||
PAUSE: str = Field(default='pause')
|
||||
"""Pauses the task.
|
||||
"""
|
||||
|
||||
@@ -15,6 +15,7 @@ from openhands.events.action.files import (
|
||||
FileWriteAction,
|
||||
)
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
|
||||
|
||||
__all__ = [
|
||||
'Action',
|
||||
@@ -29,6 +30,8 @@ __all__ = [
|
||||
'AgentRejectAction',
|
||||
'AgentDelegateAction',
|
||||
'AgentSummarizeAction',
|
||||
'AddTaskAction',
|
||||
'ModifyTaskAction',
|
||||
'ChangeAgentStateAction',
|
||||
'IPythonRunCellAction',
|
||||
'MessageAction',
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from openhands.core.schema import ActionType
|
||||
from openhands.events.action.action import Action
|
||||
|
||||
|
||||
@dataclass
|
||||
class AddTaskAction(Action):
|
||||
parent: str
|
||||
goal: str
|
||||
subtasks: list = field(default_factory=list)
|
||||
thought: str = ''
|
||||
action: str = ActionType.ADD_TASK
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f'Added task: {self.goal}'
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModifyTaskAction(Action):
|
||||
task_id: str
|
||||
state: str
|
||||
thought: str = ''
|
||||
action: str = ActionType.MODIFY_TASK
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return f'Set task {self.task_id} to {self.state}'
|
||||
@@ -18,6 +18,7 @@ from openhands.events.action.files import (
|
||||
FileWriteAction,
|
||||
)
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.events.action.tasks import AddTaskAction, ModifyTaskAction
|
||||
|
||||
actions = (
|
||||
NullAction,
|
||||
@@ -31,6 +32,8 @@ actions = (
|
||||
AgentFinishAction,
|
||||
AgentRejectAction,
|
||||
AgentDelegateAction,
|
||||
AddTaskAction,
|
||||
ModifyTaskAction,
|
||||
ChangeAgentStateAction,
|
||||
MessageAction,
|
||||
)
|
||||
|
||||
@@ -74,9 +74,6 @@ class EventStream:
|
||||
self._lock = threading.Lock()
|
||||
self._cur_id = 0
|
||||
|
||||
# load the stream
|
||||
self.__post_init__()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
try:
|
||||
events = self.file_store.list(get_conversation_events_dir(self.sid))
|
||||
@@ -229,24 +226,10 @@ class EventStream:
|
||||
for callback_id in callbacks:
|
||||
callback = callbacks[callback_id]
|
||||
pool = self._thread_pools[key][callback_id]
|
||||
future = pool.submit(callback, event)
|
||||
future.add_done_callback(self._make_error_handler(callback_id, key))
|
||||
pool.submit(callback, event)
|
||||
|
||||
def _make_error_handler(self, callback_id: str, subscriber_id: str):
|
||||
def _handle_callback_error(fut):
|
||||
try:
|
||||
# This will raise any exception that occurred during callback execution
|
||||
fut.result()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Error in event callback {callback_id} for subscriber {subscriber_id}: {str(e)}',
|
||||
exc_info=True,
|
||||
stack_info=True,
|
||||
)
|
||||
# Re-raise in the main thread so the error is not swallowed
|
||||
raise e
|
||||
|
||||
return _handle_callback_error
|
||||
def _callback(self, callback: Callable, event: Event):
|
||||
asyncio.run(callback(event))
|
||||
|
||||
def filtered_events_by_source(self, source: EventSource):
|
||||
for event in self.get_events():
|
||||
|
||||
@@ -202,7 +202,7 @@ async def process_issue(
|
||||
runtime = create_runtime(config)
|
||||
await runtime.connect()
|
||||
|
||||
def on_event(evt):
|
||||
async def on_event(evt):
|
||||
logger.info(evt)
|
||||
|
||||
runtime.event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, str(uuid4()))
|
||||
|
||||
@@ -219,24 +219,6 @@ class Runtime(FileEditRuntimeMixin):
|
||||
self.log('info', f'Cloning repo: {selected_repository}')
|
||||
self.run_action(action)
|
||||
|
||||
def maybe_run_setup_script(self, selected_repository: str | None):
|
||||
"""Run .openhands/setup.sh if it exists in the workspace or repository."""
|
||||
setup_script = '.openhands/setup.sh'
|
||||
if selected_repository:
|
||||
repo_name = selected_repository.split('/')[1]
|
||||
setup_script = f'{repo_name}/.openhands/setup.sh'
|
||||
|
||||
# Try to read the setup script
|
||||
read_obs = self.read(FileReadAction(path=setup_script))
|
||||
if isinstance(read_obs, ErrorObservation):
|
||||
return
|
||||
|
||||
# Execute the script
|
||||
action = CmdRunAction(f'chmod +x {setup_script} && {setup_script}')
|
||||
obs = self.run_action(action)
|
||||
if isinstance(obs, CmdOutputObservation) and obs.exit_code != 0:
|
||||
self.log('error', f'Setup script failed: {obs.content}')
|
||||
|
||||
def get_custom_microagents(self, selected_repository: str | None) -> list[str]:
|
||||
custom_microagents_content = []
|
||||
custom_microagents_dir = Path('.openhands') / 'microagents'
|
||||
|
||||
@@ -5,11 +5,6 @@ class E2BFileStore(FileStore):
|
||||
def __init__(self, filesystem):
|
||||
self.filesystem = filesystem
|
||||
|
||||
def get_full_path(self, path: str) -> str:
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
return path
|
||||
|
||||
def write(self, path: str, contents: str) -> None:
|
||||
self.filesystem.write(path, contents)
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ def read_llm_models():
|
||||
def read_llm_agents():
|
||||
return [
|
||||
'CodeActAgent',
|
||||
'PlannerAgent',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -202,7 +202,6 @@ class AgentSession:
|
||||
return
|
||||
|
||||
self.runtime.clone_repo(github_token, selected_repository)
|
||||
self.runtime.maybe_run_setup_script(selected_repository)
|
||||
if agent.prompt_manager:
|
||||
microagents = await call_sync_from_async(
|
||||
self.runtime.get_custom_microagents, selected_repository
|
||||
@@ -270,26 +269,11 @@ class AgentSession:
|
||||
headless_mode=False,
|
||||
status_callback=self._status_callback,
|
||||
)
|
||||
|
||||
# Note: We now attempt to restore the state from session here,
|
||||
# but if it fails, we fall back to None and still initialize the controller
|
||||
# with a fresh state. That way, the controller will always load events from the event stream
|
||||
# even if the state file was corrupt.
|
||||
|
||||
restored_state = None
|
||||
try:
|
||||
restored_state = State.restore_from_session(self.sid, self.file_store)
|
||||
except Exception as e:
|
||||
if self.event_stream.get_latest_event_id() > 0:
|
||||
# if we have events, we should have a state
|
||||
logger.warning(f'State could not be restored: {e}')
|
||||
|
||||
# Set the initial state through the controller.
|
||||
controller.set_initial_state(restored_state, max_iterations, confirmation_mode)
|
||||
if restored_state:
|
||||
agent_state = State.restore_from_session(self.sid, self.file_store)
|
||||
controller.set_initial_state(agent_state, max_iterations, confirmation_mode)
|
||||
logger.debug(f'Restored agent state from session, sid: {self.sid}')
|
||||
else:
|
||||
logger.debug('New session state created.')
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f'State could not be restored: {e}')
|
||||
logger.debug('Agent controller initialized.')
|
||||
return controller
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from uuid import uuid4
|
||||
@@ -44,7 +45,8 @@ class SessionManager:
|
||||
_local_agent_loops_by_sid: dict[str, Session] = field(default_factory=dict)
|
||||
local_connection_id_to_session_id: dict[str, str] = field(default_factory=dict)
|
||||
_last_alive_timestamps: dict[str, float] = field(default_factory=dict)
|
||||
_redis_listen_task: asyncio.Task | None = None
|
||||
_redis_listen_thread: threading.Thread | None = None
|
||||
_redis_listen_stop_event: threading.Event = field(default_factory=threading.Event)
|
||||
_session_is_running_checks: dict[str, _SessionIsRunningCheck] = field(
|
||||
default_factory=dict
|
||||
)
|
||||
@@ -63,14 +65,18 @@ class SessionManager:
|
||||
async def __aenter__(self):
|
||||
redis_client = self._get_redis_client()
|
||||
if redis_client:
|
||||
self._redis_listen_task = asyncio.create_task(self._redis_subscribe())
|
||||
self._redis_listen_stop_event.clear()
|
||||
self._redis_listen_thread = threading.Thread(target=self._run_redis_subscribe)
|
||||
self._redis_listen_thread.daemon = True
|
||||
self._redis_listen_thread.start()
|
||||
self._cleanup_task = asyncio.create_task(self._cleanup_detached_conversations())
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
if self._redis_listen_task:
|
||||
self._redis_listen_task.cancel()
|
||||
self._redis_listen_task = None
|
||||
if self._redis_listen_thread:
|
||||
self._redis_listen_stop_event.set()
|
||||
self._redis_listen_thread.join()
|
||||
self._redis_listen_thread = None
|
||||
if self._cleanup_task:
|
||||
self._cleanup_task.cancel()
|
||||
self._cleanup_task = None
|
||||
@@ -79,31 +85,32 @@ class SessionManager:
|
||||
redis_client = getattr(self.sio.manager, 'redis', None)
|
||||
return redis_client
|
||||
|
||||
async def _redis_subscribe(self):
|
||||
def _run_redis_subscribe(self):
|
||||
"""
|
||||
We use a redis backchannel to send actions between server nodes
|
||||
We use a redis backchannel to send actions between server nodes.
|
||||
This method runs in a separate thread.
|
||||
"""
|
||||
logger.debug('_redis_subscribe')
|
||||
redis_client = self._get_redis_client()
|
||||
pubsub = redis_client.pubsub()
|
||||
await pubsub.subscribe('oh_event')
|
||||
while should_continue():
|
||||
pubsub.subscribe('oh_event')
|
||||
|
||||
while not self._redis_listen_stop_event.is_set() and should_continue():
|
||||
try:
|
||||
message = await pubsub.get_message(
|
||||
message = pubsub.get_message(
|
||||
ignore_subscribe_messages=True, timeout=5
|
||||
)
|
||||
if message:
|
||||
await self._process_message(message)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except Exception:
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
logger.warning(
|
||||
'error_reading_from_redis', exc_info=True, stack_info=True
|
||||
# Schedule the message processing in the event loop
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._process_message(message),
|
||||
asyncio.get_event_loop()
|
||||
)
|
||||
except RuntimeError:
|
||||
return # Loop has been shut down
|
||||
except Exception:
|
||||
logger.warning(
|
||||
'error_reading_from_redis', exc_info=True, stack_info=True
|
||||
)
|
||||
time.sleep(1) # Avoid tight loop on error
|
||||
|
||||
async def _process_message(self, message: dict):
|
||||
data = json.loads(message['data'])
|
||||
|
||||
@@ -112,9 +112,6 @@ class Session:
|
||||
github_token=github_token,
|
||||
selected_repository=selected_repository,
|
||||
)
|
||||
|
||||
# Run setup script if it exists
|
||||
await self._run_setup_script()
|
||||
except Exception as e:
|
||||
logger.exception(f'Error creating controller: {e}')
|
||||
await self.send_error(
|
||||
@@ -209,4 +206,4 @@ class Session:
|
||||
"""Queues a status message to be sent asynchronously."""
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._send_status_message(msg_type, id, message), self.loop
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
from openhands.core.config.app_config import AppConfig
|
||||
from openhands.server.data_models.conversation_metadata import ConversationMetadata
|
||||
from openhands.storage import get_file_store
|
||||
from openhands.storage.conversation.conversation_store import ConversationStore
|
||||
from openhands.server.data_models.conversation_metadata import ConversationMetadata
|
||||
from openhands.storage.files import FileStore
|
||||
from openhands.storage.locations import get_conversation_metadata_filename
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
@@ -2,18 +2,6 @@ from abc import abstractmethod
|
||||
|
||||
|
||||
class FileStore:
|
||||
@abstractmethod
|
||||
def get_full_path(self, path: str) -> str:
|
||||
"""Get the full path for a given relative path.
|
||||
|
||||
Args:
|
||||
path: The relative path.
|
||||
|
||||
Returns:
|
||||
The full path.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def write(self, path: str, contents: str) -> None:
|
||||
pass
|
||||
|
||||
@@ -19,11 +19,6 @@ class GoogleCloudFileStore(FileStore):
|
||||
self.storage_client = storage.Client()
|
||||
self.bucket = self.storage_client.bucket(bucket_name)
|
||||
|
||||
def get_full_path(self, path: str) -> str:
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
return path
|
||||
|
||||
def write(self, path: str, contents: str | bytes) -> None:
|
||||
blob = self.bucket.blob(path)
|
||||
with blob.open('w') as f:
|
||||
|
||||
@@ -12,11 +12,6 @@ class InMemoryFileStore(FileStore):
|
||||
def __init__(self, files: dict[str, str] = IN_MEMORY_FILES):
|
||||
self.files = files
|
||||
|
||||
def get_full_path(self, path: str) -> str:
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
return path
|
||||
|
||||
def write(self, path: str, contents: str) -> None:
|
||||
self.files[path] = contents
|
||||
|
||||
|
||||
@@ -15,11 +15,6 @@ class S3FileStore(FileStore):
|
||||
self.bucket = os.getenv('AWS_S3_BUCKET')
|
||||
self.client = Minio(endpoint, access_key, secret_key, secure=secure)
|
||||
|
||||
def get_full_path(self, path: str) -> str:
|
||||
if path.startswith('/'):
|
||||
path = path[1:]
|
||||
return path
|
||||
|
||||
def write(self, path: str, contents: str) -> None:
|
||||
as_bytes = contents.encode('utf-8')
|
||||
stream = io.BytesIO(as_bytes)
|
||||
|
||||
Generated
+7
-7
@@ -3782,19 +3782,19 @@ pydantic = ">=1.10"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index"
|
||||
version = "0.12.9"
|
||||
version = "0.12.8"
|
||||
description = "Interface between LLMs and your data"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index-0.12.9-py3-none-any.whl", hash = "sha256:95c39d8055c7d19bd5f099560b53c0971ae9997ebe46f7438766189ed48e4456"},
|
||||
{file = "llama_index-0.12.9.tar.gz", hash = "sha256:2f8d671e6ca7e5b33b0f5cbddef8c0a11eb1e39781f1be65e9bd0c4a7a0deb5b"},
|
||||
{file = "llama_index-0.12.8-py3-none-any.whl", hash = "sha256:6b98ea44c225c7d230fd7f552dfcc2911ef327e3be352dc239011118242e4a28"},
|
||||
{file = "llama_index-0.12.8.tar.gz", hash = "sha256:f1578bb6873fa4f90a8645a80f4f997d184770e63bd7a2b45a98ab6e5c70fb59"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
llama-index-agent-openai = ">=0.4.0,<0.5.0"
|
||||
llama-index-cli = ">=0.4.0,<0.5.0"
|
||||
llama-index-core = ">=0.12.9,<0.13.0"
|
||||
llama-index-core = ">=0.12.8,<0.13.0"
|
||||
llama-index-embeddings-openai = ">=0.3.0,<0.4.0"
|
||||
llama-index-indices-managed-llama-cloud = ">=0.4.0"
|
||||
llama-index-llms-openai = ">=0.3.0,<0.4.0"
|
||||
@@ -3839,13 +3839,13 @@ llama-index-llms-openai = ">=0.3.0,<0.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "llama-index-core"
|
||||
version = "0.12.9"
|
||||
version = "0.12.8"
|
||||
description = "Interface between LLMs and your data"
|
||||
optional = false
|
||||
python-versions = "<4.0,>=3.9"
|
||||
files = [
|
||||
{file = "llama_index_core-0.12.9-py3-none-any.whl", hash = "sha256:75bfdece8e1eb37faba43345cfbd9a8004859c177c1b5b358fc77620908c0f3f"},
|
||||
{file = "llama_index_core-0.12.9.tar.gz", hash = "sha256:a6a702af13f8a840ff2a459024d21280e5b04d37f22c73efdc52def60e047af6"},
|
||||
{file = "llama_index_core-0.12.8-py3-none-any.whl", hash = "sha256:7ebecbdaa1d5b6a320c050bf90525605ac03b242d26ad55f0e00a0e1df69e070"},
|
||||
{file = "llama_index_core-0.12.8.tar.gz", hash = "sha256:3b360437b4ae47b7bd1733f6492a95126e6739c7a2fd2b649ebe8bb3afea7143"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "openhands-ai"
|
||||
version = "0.18.0"
|
||||
version = "0.17.0"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
authors = ["OpenHands"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from openhands.events.action import (
|
||||
Action,
|
||||
AddTaskAction,
|
||||
AgentFinishAction,
|
||||
AgentRejectAction,
|
||||
BrowseInteractiveAction,
|
||||
@@ -8,6 +9,7 @@ from openhands.events.action import (
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
MessageAction,
|
||||
ModifyTaskAction,
|
||||
)
|
||||
from openhands.events.action.action import ActionConfirmationStatus
|
||||
from openhands.events.serialization import (
|
||||
@@ -154,3 +156,24 @@ def test_file_write_action_serialization_deserialization():
|
||||
},
|
||||
}
|
||||
serialization_deserialization(original_action_dict, FileWriteAction)
|
||||
|
||||
|
||||
def test_add_task_action_serialization_deserialization():
|
||||
original_action_dict = {
|
||||
'action': 'add_task',
|
||||
'args': {
|
||||
'parent': 'Test parent',
|
||||
'goal': 'Test goal',
|
||||
'subtasks': [],
|
||||
'thought': '',
|
||||
},
|
||||
}
|
||||
serialization_deserialization(original_action_dict, AddTaskAction)
|
||||
|
||||
|
||||
def test_modify_task_action_serialization_deserialization():
|
||||
original_action_dict = {
|
||||
'action': 'modify_task',
|
||||
'args': {'task_id': 1, 'state': 'Test state.', 'thought': ''},
|
||||
}
|
||||
serialization_deserialization(original_action_dict, ModifyTaskAction)
|
||||
|
||||
@@ -18,7 +18,7 @@ def test_parser_default_values():
|
||||
assert args.eval_num_workers == 4
|
||||
assert args.eval_note is None
|
||||
assert args.llm_config is None
|
||||
assert args.name == ''
|
||||
assert args.name == 'default'
|
||||
assert not args.no_auto_continue
|
||||
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ def test_load_from_old_style_env(monkeypatch, default_config):
|
||||
# Test loading configuration from old-style environment variables using monkeypatch
|
||||
monkeypatch.setenv('LLM_API_KEY', 'test-api-key')
|
||||
monkeypatch.setenv('AGENT_MEMORY_ENABLED', 'True')
|
||||
monkeypatch.setenv('DEFAULT_AGENT', 'BrowsingAgent')
|
||||
monkeypatch.setenv('DEFAULT_AGENT', 'PlannerAgent')
|
||||
monkeypatch.setenv('WORKSPACE_BASE', '/opt/files/workspace')
|
||||
monkeypatch.setenv('SANDBOX_BASE_CONTAINER_IMAGE', 'custom_image')
|
||||
|
||||
@@ -79,7 +79,7 @@ def test_load_from_old_style_env(monkeypatch, default_config):
|
||||
|
||||
assert default_config.get_llm_config().api_key == 'test-api-key'
|
||||
assert default_config.get_agent_config().memory_enabled is True
|
||||
assert default_config.default_agent == 'BrowsingAgent'
|
||||
assert default_config.default_agent == 'PlannerAgent'
|
||||
assert default_config.workspace_base == '/opt/files/workspace'
|
||||
assert default_config.workspace_mount_path is None # before finalize_config
|
||||
assert default_config.workspace_mount_path_in_sandbox is not None
|
||||
@@ -333,10 +333,8 @@ def test_defaults_dict_after_updates(default_config):
|
||||
updated_config.get_llm_config().api_key = 'updated-api-key'
|
||||
updated_config.get_llm_config('llm').api_key = 'updated-api-key'
|
||||
updated_config.get_llm_config_from_agent('agent').api_key = 'updated-api-key'
|
||||
updated_config.get_llm_config_from_agent(
|
||||
'BrowsingAgent'
|
||||
).api_key = 'updated-api-key'
|
||||
updated_config.default_agent = 'BrowsingAgent'
|
||||
updated_config.get_llm_config_from_agent('PlannerAgent').api_key = 'updated-api-key'
|
||||
updated_config.default_agent = 'PlannerAgent'
|
||||
|
||||
defaults_after_updates = updated_config.defaults_dict
|
||||
assert defaults_after_updates['default_agent']['default'] == 'CodeActAgent'
|
||||
@@ -549,7 +547,7 @@ max_budget_per_task = 4.0
|
||||
[agent.CodeActAgent]
|
||||
memory_enabled = true
|
||||
|
||||
[agent.BrowsingAgent]
|
||||
[agent.PlannerAgent]
|
||||
memory_max_threads = 10
|
||||
"""
|
||||
|
||||
@@ -560,5 +558,5 @@ memory_max_threads = 10
|
||||
|
||||
codeact_config = default_config.get_agent_configs().get('CodeActAgent')
|
||||
assert codeact_config.memory_enabled is True
|
||||
browsing_config = default_config.get_agent_configs().get('BrowsingAgent')
|
||||
assert browsing_config.memory_max_threads == 10
|
||||
planner_config = default_config.get_agent_configs().get('PlannerAgent')
|
||||
assert planner_config.memory_max_threads == 10
|
||||
|
||||
@@ -31,7 +31,7 @@ def event_stream(temp_dir):
|
||||
def agent_configs():
|
||||
return {
|
||||
'CoderAgent': AgentConfig(memory_enabled=True),
|
||||
'BrowsingAgent': AgentConfig(memory_enabled=True),
|
||||
'PlannerAgent': AgentConfig(memory_enabled=True),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from openhands.agenthub.micro.agent import parse_response as parse_response_micro
|
||||
from openhands.agenthub.planner_agent.prompt import (
|
||||
parse_response as parse_response_planner,
|
||||
)
|
||||
from openhands.core.exceptions import LLMResponseError
|
||||
from openhands.core.utils.json import loads as custom_loads
|
||||
from openhands.events.action import (
|
||||
@@ -11,7 +14,7 @@ from openhands.events.action import (
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'parse_response_module',
|
||||
[parse_response_micro],
|
||||
[parse_response_micro, parse_response_planner],
|
||||
)
|
||||
def test_parse_single_complete_json(parse_response_module):
|
||||
input_response = """
|
||||
@@ -31,7 +34,7 @@ def test_parse_single_complete_json(parse_response_module):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'parse_response_module',
|
||||
[parse_response_micro],
|
||||
[parse_response_micro, parse_response_planner],
|
||||
)
|
||||
def test_parse_json_with_surrounding_text(parse_response_module):
|
||||
input_response = """
|
||||
@@ -54,7 +57,7 @@ def test_parse_json_with_surrounding_text(parse_response_module):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'parse_response_module',
|
||||
[parse_response_micro],
|
||||
[parse_response_micro, parse_response_planner],
|
||||
)
|
||||
def test_parse_first_of_multiple_jsons(parse_response_module):
|
||||
input_response = """
|
||||
|
||||
@@ -13,8 +13,6 @@ def mock_event_stream():
|
||||
stream = MagicMock()
|
||||
# Mock get_events to return an empty list by default
|
||||
stream.get_events.return_value = []
|
||||
# Mock get_latest_event_id to return a valid integer
|
||||
stream.get_latest_event_id.return_value = 0
|
||||
return stream
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user