Compare commits

..

41 Commits

Author SHA1 Message Date
rohitvinodmalhotra@gmail.com 51bf321a01 create defaults 2025-03-27 17:21:41 -04:00
Robert Brennan 2d66939b42 fix error message (#7550) 2025-03-27 13:35:41 -07:00
Carlos Freund a0c79f7388 fix(Runtime): Wait for container to start up (#7548)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: tofarr <tofarr@gmail.com>
2025-03-27 13:55:06 -06:00
tofarr a44cdae36e Fix for broken OpenAPI Schema (#7558) 2025-03-27 13:28:53 -06:00
Engel Nyst 7aa7eb2399 Fix uninitialized accumulated tokens (#7553) 2025-03-27 20:19:10 +01:00
Calvin Smith 42712a44d8 (fix): Condensation events to reconstruct contexts added to event stream (#7353)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-03-27 13:16:31 -06:00
Rohit Malhotra 76c992e2df [Feat]: Trigger microagents on agent keywords (#7516) 2025-03-27 13:58:37 -04:00
mamoodi 26b420a01d Release 0.30.1 (#7545) 2025-03-27 13:26:28 -04:00
dependabot[bot] e707be429e chore(deps): bump the version-all group across 1 directory with 27 updates (#7475)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-03-27 16:14:21 +00:00
Xingyao Wang 24773e15c5 Fix Push & Create PR button prompt to request meaningful branch names (#7529)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-27 23:21:52 +08:00
Caique 5dba964281 docs: Improve the Microagents usage documentation (#7542) 2025-03-27 15:11:14 +00:00
VS 1596a6cc62 fix: correct string concatenation in llm_summarizing_condenser.py (#7541) 2025-03-27 07:28:45 -07:00
Rohit Malhotra 0df87bfacc [Feat]: Tell the agent the current date (#7509) 2025-03-27 01:23:12 -04:00
Engel Nyst 8e9eb7d07d Reduce max iterations by default (#7535) 2025-03-27 01:00:14 +00:00
Rohit Malhotra 60196d2eca (Hotfix): Github token fails to refresh on cloud openhands (#7532) 2025-03-27 00:05:38 +00:00
tofarr b9af0188fe Feat vscode startup (#7518)
Co-authored-by: OpenHands Bot <openhands@all-hands.dev>
2025-03-26 16:01:23 -06:00
Engel Nyst 9850f1767a Use response_id to track token usage for MessageActions (#6913)
Co-authored-by: Calvin Smith <email@cjsmith.io>
2025-03-26 21:07:01 +01:00
tofarr c5491e87aa Run runtime.close in background thread (#7524) 2025-03-26 12:12:59 -06:00
dependabot[bot] 400afeb70e chore(deps): bump the version-all group across 1 directory with 15 updates (#7520)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 17:11:30 +01:00
Xingyao Wang c63d52d5e6 (llm): Track accumulated token usage instead of per-request token usage (#7511)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>
2025-03-26 16:05:36 +00:00
tofarr 1230b229b5 Replace use of requests with httpx (#7354) 2025-03-26 13:37:10 +00:00
Rohit Malhotra 72d5f1fe53 [Fix]: Add min amount to funds placeholder (#7517) 2025-03-25 21:14:11 -07:00
Boxuan Li e3a5df514e (chore) Remove unused grep-ast dependency (#7414)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-25 20:45:00 -07:00
Graham Neubig aaf65ebf0f Update translation updater to use claude-3-7-sonnet-20250219 (#7500)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-25 15:56:44 -07:00
mamoodi 844d84d7bb Release 0.30.0 (#7467)
Co-authored-by: மனோஜ்குமார் பழனிச்சாமி <smartmanoj42857@gmail.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-03-25 18:39:36 -04:00
Rohit Malhotra 5a3eca2a2a [Refactor]: Create dedicated reset settings route (#7472)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-25 18:34:55 -04:00
tawago efcf30a23d Update the resolver workflow once a new release includes #7287 (#7343) 2025-03-25 18:25:33 -04:00
Xingyao Wang 951cb1c880 Fix TypeError in bash parsing with unclosed backticks (#7392)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-25 21:38:01 +00:00
Calvin Smith 78b67bc9d9 Update event schema types to use enum pattern (#7498)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-25 15:36:13 -06:00
sp.wack 500e09f12b hotfix(frontend): Wrap WS tests in query provider for migration changes (#7504) 2025-03-25 22:45:04 +04:00
Caique 2a5e17d548 docs(frontend): add environment variables in .env.sample and README (#7401) 2025-03-25 22:08:20 +04:00
mamoodi 6541eab43b Remove empty lines in pyproject (#7501) 2025-03-25 17:02:09 +00:00
Robert Brennan 2e72ef151e Add API key instructions and update documentation links (#7434)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-25 16:25:55 +00:00
Robert Brennan 032eb152bf Fix WebSocket timeout messages in chat window (#7405)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-25 09:17:18 -07:00
sp.wack 63ebd9e338 hotfix(frontend): Show background color for logout button (#7495) 2025-03-25 11:59:28 -04:00
Zach 1064939013 Fix incorrect file position descriptions in script comments (#7492) 2025-03-25 16:48:48 +01:00
Engel Nyst ff6312ab02 Remove unused AgentSummarizeAction and SUMMARIZE action type (#7283)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-25 16:46:41 +01:00
Graham Neubig 036fa5dccf Add type hints to storage directory (#7110)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-25 08:01:04 -07:00
Graham Neubig 86c6feafcc Fix mypy errors in runtime/utils directory (#6902)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-25 08:00:03 -07:00
Graham Neubig 0efe4feb2a Fix mypy errors in core/config directory (#7113)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-25 05:57:00 -07:00
Graham Neubig 8b473397d1 Fix mypy errors in events directory (#6810)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-25 06:30:58 +00:00
173 changed files with 5529 additions and 4797 deletions
+1 -1
View File
@@ -238,7 +238,7 @@ jobs:
PYTHONPATH: ""
run: |
cd /tmp && python -m openhands.resolver.resolve_issue \
--repo ${{ github.repository }} \
--selected-repo ${{ github.repository }} \
--issue-number ${{ env.ISSUE_NUMBER }} \
--issue-type ${{ env.ISSUE_TYPE }} \
--max-iterations ${{ env.MAX_ITERATIONS }} \
+1 -1
View File
@@ -118,7 +118,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.29-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.30-nikolaik`
## Develop inside Docker container
+3 -3
View File
@@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio
system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
> [!WARNING]
+2 -2
View File
@@ -24,7 +24,7 @@
#daytona_target = ""
# Base path for the workspace
workspace_base = "./workspace"
#workspace_base = "./workspace"
# Cache directory path
#cache_dir = "/tmp/cache"
@@ -64,7 +64,7 @@ workspace_base = "./workspace"
#max_budget_per_task = 0.0
# Maximum number of iterations
#max_iterations = 100
#max_iterations = 250
# Path to mount the workspace in the sandbox
#workspace_mount_path_in_sandbox = "/workspace"
+1 -1
View File
@@ -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.29-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.30-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1
View File
@@ -47,6 +47,7 @@ docker run -it \
...
```
### Referring to UI Elements
When referencing UI elements, use ``.
+1 -1
View File
@@ -57,4 +57,4 @@ $ poetry run python docs/translation_updater.py
# ...
```
This process uses `claude-3-opus-20240229` as base model and each language consumes at least ~30k input tokens and ~35k output tokens.
This process uses `claude-3-7-sonnet-20250219` as base model and each language consumes at least ~30k input tokens and ~35k output tokens.
@@ -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.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
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.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
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.29-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
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.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
@@ -35,7 +35,7 @@ Para executar o OpenHands no modo CLI com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,7 @@ docker run -it \
-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.29 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.cli
```
@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-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.29 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.main -t "escreva um script bash que imprima oi"
```
@@ -58,17 +58,17 @@
A maneira mais fácil de executar o OpenHands é no Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
Você encontrará o OpenHands em execução em http://localhost:3000!
@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.cli
```
@@ -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.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
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.29-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
你也可以在可脚本化的[无头模式](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.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
+2 -2
View File
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,7 @@ docker run -it \
-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.29 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.cli
```
+2 -2
View File
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-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.29 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+21 -3
View File
@@ -58,17 +58,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-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.29
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
You'll find OpenHands running at http://localhost:3000!
@@ -87,6 +87,24 @@ If the required model does not exist in the list, you can toggle `Advanced` opti
in the `Custom Model` text box.
The `Advanced` options also allow you to specify a `Base URL` if required.
### Getting an API Key
OpenHands requires an API key to access most language models. Here's how to get an API key from the recommended providers:
#### Anthropic (Claude)
1. [Create an Anthropic account](https://console.anthropic.com/)
2. [Generate an API key](https://console.anthropic.com/settings/keys)
3. [Set up billing(https://console.anthropic.com/settings/billing)
Consider setting usage limits to control costs.
#### OpenAI
1. [Create an OpenAI account](https://platform.openai.com/)
2. [Generate an API key](https://platform.openai.com/api-keys)
3. [Set up billing](https://platform.openai.com/account/billing/overview)
Now you're ready to [get started with OpenHands](./getting-started).
## Versions
@@ -4,33 +4,28 @@ Microagents are specialized prompts that enhance OpenHands with domain-specific
and task-specific workflows. They help by providing expert guidance, automating common tasks, and ensuring
consistent practices across projects.
## Microagent Types
## Microagent Categories
Currently OpenHands supports the following types of microagents:
Currently OpenHands supports two categories of microagents:
* [Repository Microagents](./microagents-repo): Repository-specific context and guidelines for OpenHands.
* [Public Microagents](./microagents-public): General guidelines triggered by keywords for all OpenHands users.
- [Repository-specific Microagents](./microagents-repo): Repository-specific context and guidelines for OpenHands.
- [Public Microagents](./microagents-public): General guidelines triggered by keywords for all OpenHands users.
A microagent is classified as repository-specific or public depending on its location:
- Repository-specific microagents are located in a repository's `.openhands/microagents/` directory
- Public microagents are located in the official OpenHands repository inside the `/microagents` folder
When OpenHands works with a repository, it:
1. Loads repository-specific instructions from `.openhands/microagents/` if present in the repository.
2. Loads general guidelines triggered by keywords in conversations.
See current [Public Microagents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
1. Loads **repository-specific** microagents from `.openhands/microagents/` if present in the repository.
2. Loads **public knowledge** microagents triggered by keywords in conversations
3. Loads **public tasks** microagents when explicitly requested by the user
You can check out the existing public microagents at the [official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/).
## Microagent Format
All microagents use markdown files with YAML frontmatter that have special instructions to help OpenHands accomplish
tasks:
```
---
name: <Name of the microagent>
type: <MicroAgent type>
version: <MicroAgent version>
agent: <The agent type (Typically CodeActAgent)>
triggers:
- <Optional keywords triggering the microagent. If triggers are removed, it will always be included>
---
All microagents use markdown files with YAML frontmatter that have special instructions to help OpenHands activate them.
<Markdown with any special guidelines, instructions, and prompts that OpenHands should follow.
Check out the specific documentation for each microagent on best practices for more information.>
```
Check out the [syntax documentation](./microagents-syntax) for a comprehensive guide on how to configure your microagents.
@@ -2,64 +2,32 @@
## Overview
Public microagents are specialized guidelines triggered by keywords for all OpenHands users.
They are defined in markdown files under the
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) directory.
Public microagents provide specialized context and capabilities for all OpenHands users, regardless of their repository configuration. Unlike repository-specific microagents, public microagents are globally available across all repositories.
Public microagents come in two types:
- **Knowledge microagents**: Automatically activated when keywords in conversations match their triggers
- **Task microagents**: Explicitly invoked by users to guide through specific workflows
Both types follow the same syntax and structure as repository-specific microagents, using markdown files with YAML frontmatter that define their behavior and capabilities. They are located in the official OpenHands repository under:
- [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for knowledge microagents
- [`microagents/tasks/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks) for task microagents
Public microagents:
- Monitor incoming commands for their trigger words.
- Activate when relevant triggers are detected.
- Apply their specialized knowledge and capabilities.
- Follow their specific guidelines and restrictions.
## Current Public Microagents
For more information about specific microagents, refer to their individual documentation files in
the [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/) directory.
### GitHub Agent
**File**: `github.md`
**Triggers**: `github`, `git`
The GitHub agent specializes in GitHub API interactions and repository management. It:
- Has access to a `GITHUB_TOKEN` for API authentication.
- Follows strict guidelines for repository interactions.
- Handles branch management and pull requests.
- Uses the GitHub API instead of web browser interactions.
Key features:
- Branch protection (prevents direct pushes to main/master)
- Automated PR creation
- Git configuration management
- API-first approach for GitHub operations
Usage Example:
```bash
git checkout -b feature-branch
git commit -m "Add new feature"
git push origin feature-branch
```
### NPM Agent
**File**: `npm.md`
**Triggers**: `npm`
Specializes in handling npm package management with specific focus on:
- Non-interactive shell operations.
- Automated confirmation handling using Unix 'yes' command.
- Package installation automation.
Usage Example:
```bash
yes | npm install package-name
```
When loading public microagents, OpenHands scans the official repository's microagents directories recursively, processing all markdown files except README.md. The system categorizes each microagent based on its `type` field in the YAML frontmatter, regardless of its exact file location within the knowledge or tasks directories.
## Contributing a Public Microagent
You can create your own public microagents by adding new markdown files to the
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/) directory.
You can create public microagents and share with the community by opening a pull request to the official repository.
See the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) for specific instructions on how to contribute to OpenHands.
### Public Microagents Best Practices
@@ -74,6 +42,7 @@ You can create your own public microagents by adding new markdown files to the
#### 1. Plan the Public Microagent
Before creating a public microagent, consider:
- What specific problem or use case will it address?
- What unique capabilities or knowledge should it have?
- What trigger words make sense for activating it?
@@ -81,73 +50,24 @@ Before creating a public microagent, consider:
#### 2. Create File
Create a new markdown file in [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/)
with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
Create a new Markdown file with a descriptive name in the appropriate directory:
Update the file with the required frontmatter [according to the required format](./microagents-overview#microagent-format)
and the required specialized guidelines while following the [best practices above](#public-microagents-best-practices).
- [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for knowledge microagents
- [`microagents/tasks/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks) for task microagents
Ensure it follows the correct [syntax](./microagents-syntax.md) and [best practices](./microagents-syntax.md#markdown-content-best-practices).
#### 3. Testing the Public Microagent
- Test the agent with various prompts.
- Verify trigger words activate the agent correctly.
- Ensure instructions are clear and comprehensive.
- Check for potential conflicts with existing agents.
- Test the agent with various prompts
- Verify trigger words activate the agent correctly
- Ensure instructions are clear and comprehensive
- Check for potential conflicts and overlaps with existing agents
#### 4. Submission Process
Submit a pull request with:
- The new microagent file.
- Updated documentation if needed.
- Description of the agent's purpose and capabilities.
### Example Public Microagent Implementation
Here's a template for a new microagent:
```markdown
---
name: docker
agent: CodeActAgent
triggers:
- docker
- container
---
You are responsible for Docker container management and Dockerfile creation.
Key responsibilities:
1. Create and modify Dockerfiles
2. Manage container lifecycle
3. Handle Docker Compose configurations
Guidelines:
- Always use official base images when possible
- Include necessary security considerations
- Follow Docker best practices for layer optimization
Examples:
1. Creating a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
2. Docker Compose usage:
version: '3'
services:
web:
build: .
ports:
- "3000:3000"
Remember to:
- Validate Dockerfile syntax
- Check for security vulnerabilities
- Optimize for build time and image size
```
See the [current public micro-agents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for
more examples.
- The new microagent file
- Updated documentation if needed
- Description of the agent's purpose and capabilities
+100 -51
View File
@@ -1,68 +1,117 @@
# Repository Microagents
# Repository-specific Microagents
## Overview
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context
and guidelines. This section explains how to optimize OpenHands for your project.
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context and guidelines.
## Creating a Repository Micro-Agent
This section explains how to optimize OpenHands for your project.
## Creating Repository Microagents
You can customize OpenHands' behavior for your repository by creating a `.openhands/microagents/` directory in your repository's root.
At minimum it should contain the file
`.openhands/microagents/repo.md`, which includes instructions that will
be given to the agent every time it works with this repository.
### Repository Microagents Best Practices
You can enhance OpenHands' performance by adding custom microagents to your repository:
- **Keep Instructions Updated**: Regularly update your `.openhands/microagents/` directory as your project evolves.
- **Be Specific**: Include specific paths, patterns, and requirements unique to your project.
- **Document Dependencies**: List all tools and dependencies required for development.
- **Include Examples**: Provide examples of good code patterns from your project.
- **Specify Conventions**: Document naming conventions, file organization, and code style preferences.
1. For overall repository-specific instructions, create a `.openhands/microagents/repo.md` file
2. For reusable domain knowledge triggered by keywords, add multiple `.md` files to `.openhands/microagents/knowledge/`
3. For common workflows and tasks, create multiple `.md` files to `.openhands/microagents/tasks/`
### Steps to Create a Repository Microagent
Check out the [best practices](./microagents-syntax.md#markdown-content-best-practices) for formatting the content of your custom microagent.
#### 1. Plan the Repository Microagent
When creating a repository-specific micro-agent, we suggest including the following information:
- **Repository Overview**: A brief description of your project's purpose and architecture.
- **Directory Structure**: Key directories and their purposes.
- **Development Guidelines**: Project-specific coding standards and practices.
- **Testing Requirements**: How to run tests and what types of tests are required.
- **Setup Instructions**: Steps needed to build and run the project.
Keep in mind that loaded microagents take up space in the context window. It's crucial to strike a balance between the additional context provided by microagents and the instructions provided in the user's inputs.
#### 2. Create File
Note that you can use OpenHands to create new microagents. The public microagent [`add_agent`](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/knowledge/add_agent.md) is loaded to all OpenHands instance and can support you on this.
Create a file in your repository under `.openhands/microagents/` (Example: `.openhands/microagents/repo.md`)
## Types of Microagents
Update the file with the required frontmatter [according to the required format](./microagents-overview#microagent-format)
and the required specialized guidelines for your repository.
OpenHands supports three primary types of microagents, each with specific purposes and features to enhance agent performance:
### Example Repository Microagent
- [repository](#repository-microagents)
- [knowledge](#knowledge-microagents)
- [tasks](#tasks-microagents)
The standard directory structure within a repository is:
- One main `repo.md` file containing repository-specific instructions
- Additional `Knowledge` agents in `.openhands/microagents/knowledge/` directory
- Additional `Task` agents in `.openhands/microagents/tasks/` directory
When processing the `.openhands/microagents/` directory, OpenHands will recursively scan all subfolders and process any `.md` files (except `README.md`) it finds. The system determines the microagent type based on the `type` field in the YAML frontmatter, not by the file's location. However, for organizational clarity, it's recommended to follow the standard directory structure.
### Repository Microagents
The `Repository` microagent is loaded specifically from `.openhands/microagents/repo.md` and serves as the main
repository-specific instruction file. This single file is automatically loaded whenever OpenHands works with that repository
without requiring any keyword matching or explicit call from the user.
OpenHands does not support multiple `repo.md` files in different locations or multiple microagents with type `repo`.
If you need to organize different types of repository information, the recommended approach is to use a single `repo.md` file with well-structured sections rather than trying to create multiple microagents with the type `repo`.
The best practice is to include project-specific instructions, team practices, coding standards, and architectural guidelines that are relevant for **all** prompts in that repository.
Example structure:
```
---
name: repo
type: repo
agent: CodeActAgent
---
Repository: MyProject
Description: A web application for task management
Directory Structure:
- src/: Main application code
- tests/: Test files
- docs/: Documentation
Setup:
- Run `npm install` to install dependencies
- Use `npm run dev` for development
- Run `npm test` for testing
Guidelines:
- Follow ESLint configuration
- Write tests for all new features
- Use TypeScript for new code
If adding a new component in src/components, always add appropriate unit tests in tests/components/.
your-repository/
└── .openhands/
└── microagents/
└── repo.md # Repository-specific instructions
```
[See the example in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands/microagents/repo.md?plain=1)
### Knowledge Microagents
Knowledge microagents provide specialized domain expertise:
- Recommended to be located in `.openhands/microagents/knowledge/`
- Triggered by specific keywords in conversations
- Contain expertise on tools, languages, frameworks, and common practices
Use knowledge microagents to trigger additional context relevant to specific technologies, tools, or workflows. For example, mentioning "git" in your conversation will automatically trigger git-related expertise to help with Git operations.
Examples structure:
```
your-repository/
└── .openhands/
└── microagents/
└── knowledge/
└── git.md
└── docker.md
└── python.md
└── ...
└── repo.md
```
You can find several real examples of `Knowledge` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
### Tasks Microagents
Task microagents guide users through interactive workflows:
- Recommended to be located in `.openhands/microagents/tasks/`
- Provide step-by-step processes for common development tasks
- Accept inputs and adapt to different scenarios
- Ensure consistent outcomes for complex operations
Task microagents are a convenient way to store multi-step processes you perform regularly. For instance, you can create a `update_pr_description.md` microagent to automatically generate better pull request descriptions based on code changes.
Examples structure:
```
your-repository/
└── .openhands/
└── microagents/
└── tasks/
└── update_pr_description.md
└── address_pr_comments.md
└── get_test_to_pass.md
└── ...
└── knowledge/
└── ...
└── repo.md
```
You can find several real examples of `Tasks` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks)
@@ -0,0 +1,128 @@
# Microagents Syntax
Microagents are defined using markdown files with YAML frontmatter that specify their behavior, triggers, and capabilities.
Find below a comprehensive description of the frontmatter syntax and other details about how to use each type of microagent available at OpenHands.
## Frontmatter Schema
Every microagent requires a YAML frontmatter section at the beginning of the file, enclosed by triple dashes (`---`). The fields are:
| Field | Description | Required | Used By |
| ---------- | -------------------------------------------------- | ------------------------ | ---------------- |
| `name` | Unique identifier for the microagent | Yes | All types |
| `type` | Type of microagent: `repo`, `knowledge`, or `task` | Yes | All types |
| `version` | Version number (Semantic versioning recommended) | Yes | All types |
| `agent` | The agent type (typically `CodeActAgent`) | Yes | All types |
| `author` | Creator of the microagent | No | All types |
| `triggers` | List of keywords that activate the microagent | Yes for knowledge agents | Knowledge agents |
| `inputs` | Defines required user inputs for task execution | Yes for task agents | Task agents |
## Core Fields
### `agent`
**Purpose**: Specifies which agent implementation processes the microagent (typically `CodeActAgent`).
- Defines a single agent responsible for processing the microagent
- Must be available in the OpenHands system (see the [agent hub](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/agenthub))
- If the specified agent is not active, the microagent will not be used
### `triggers`
**Purpose**: Defines keywords that activate the `knowledge` microagent.
**Example**:
```yaml
triggers:
- kubernetes
- k8s
- docker
- security
- containers cluster
```
**Key points**:
- Can include both single words and multi-word phrases
- Case-insensitive matching is typically used
- More specific triggers (like "docker compose") prevent false activations
- Multiple triggers increase the chance of activation in relevant contexts
- Unique triggers like "flarglebargle" can be used for testing or special functionality
- Triggers should be carefully chosen to avoid unwanted activations or conflicts with other microagents
- Common terms used in many conversations may cause the microagent to be activated too frequently
When using multiple triggers, the microagent will be activated if any of the trigger words or phrases appear in the
conversation.
### `inputs`
**Purpose**: Defines parameters required from the user when a `task` microagent is activated.
**Schema**:
```yaml
inputs:
- name: INPUT_NAME # Used with {{ INPUT_NAME }}
description: 'Description of what this input is for'
required: true # Optional, defaults to true
```
**Key points**:
- The `name` and `description` properties are required for each input
- The `required` property is optional and defaults to `true`
- Input values are referenced in the microagent body using double curly braces (e.g., `{{ INPUT_NAME }}`)
- All inputs defined will be collected from the user before the task microagent executes
**Variable Usage**: Reference input values using double curly braces `{{ INPUT_NAME }}`.
## Example Formats
### Repository Microagent
Repository microagents provide context and guidelines for a specific repository.
- Located at: `.openhands/microagents/repo.md`
- Automatically loaded when working with the repository
- Only one per repository
The `Repository` microagent is loaded specifically from `.openhands/microagents/repo.md` and serves as the main
repository-specific instruction file. This single file is automatically loaded whenever OpenHands works with that repository
without requiring any keyword matching or explicit call from the user.
[See the example in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands/microagents/repo.md?plain=1)
### Knowledge Microagent
Provides specialized domain expertise triggered by keywords.
You can find several real examples of `Knowledge` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
### Task Microagent
When explicitly asked by the user, will guide through interactive workflows with specific inputs.
You can find several real examples of `Tasks` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks)
## Markdown Content Best Practices
After the frontmatter, compose the microagent body using Markdown syntax. Examples of elements you can include are:
- Clear, concise instructions outlining the microagent's purpose and responsibilities
- Specific guidelines and constraints the microagent should adhere to
- Relevant code snippets and practical examples to illustrate key points
- Step-by-step procedures for task agents, guiding users through workflows
**Design Tips**:
- Keep microagents focused with a clear purpose
- Provide specific guidelines rather than general advice
- Use distinctive triggers for knowledge agents
- Keep content concise to minimize context window usage
- Break large microagents into smaller, focused ones
Aim for clarity, brevity, and practicality in your writing. Use formatting like bullet points, code blocks, and emphasis to enhance readability and comprehension.
Remember that balancing microagents details with user input space is important for maintaining effective interactions.
+8 -3
View File
@@ -1,7 +1,7 @@
import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
const sidebars: SidebarsConfig = {
apiSidebar: [require("./modules/python/sidebar.json")],
apiSidebar: [require('./modules/python/sidebar.json')],
docsSidebar: [
{
type: 'doc',
@@ -38,7 +38,7 @@ const sidebars: SidebarsConfig = {
},
{
type: 'doc',
label: 'Repository',
label: 'Repository-specific',
id: 'usage/prompting/microagents-repo',
},
{
@@ -46,6 +46,11 @@ const sidebars: SidebarsConfig = {
label: 'Public',
id: 'usage/prompting/microagents-public',
},
{
type: 'doc',
label: 'Syntax',
id: 'usage/prompting/microagents-syntax',
},
],
},
],
+1 -1
View File
@@ -56,7 +56,7 @@ def translate_content(content, target_lang):
system_prompt = f'You are a professional translator. Translate the following content into {target_lang}. Preserve all Markdown formatting, code blocks, and front matter. Keep any {{% jsx %}} tags and similar intact. Do not translate code examples, URLs, or technical terms.'
message = client.messages.create(
model='claude-3-opus-20240229',
model='claude-3-7-sonnet-20250219',
max_tokens=4096,
temperature=0,
system=system_prompt,
+3 -3
View File
@@ -1,8 +1,8 @@
import logging
import re
import httpx
import openai
import requests.exceptions
from openai import OpenAI
from retry import retry
@@ -101,7 +101,7 @@ class Q20Game:
@retry(
(
openai.Timeout,
requests.exceptions.ReadTimeout,
httpx.TimeoutException,
openai.RateLimitError,
openai.APIError,
openai.APIConnectionError,
@@ -161,7 +161,7 @@ class Q20GameCelebrity(Q20Game):
@retry(
(
openai.Timeout,
requests.exceptions.ReadTimeout,
httpx.TimeoutException,
openai.RateLimitError,
openai.APIError,
openai.APIConnectionError,
+2 -2
View File
@@ -2,8 +2,8 @@ import asyncio
import json
import os
import httpx
import pandas as pd
import requests
from evaluation.benchmarks.gorilla.utils import encode_question, get_data_for_hub
from evaluation.utils.shared import (
@@ -182,7 +182,7 @@ if __name__ == '__main__':
# Check if the file exists
if not os.path.exists(file_path):
url = 'https://raw.githubusercontent.com/ShishirPatil/gorilla/main/eval/eval-scripts/codebleu/parser/my-languages.so'
response = requests.get(url)
response = httpx.get(url)
with open(file_path, 'wb') as f:
f.write(response.content)
else:
+2 -2
View File
@@ -2,8 +2,8 @@ import json
import os
from functools import partial
import httpx
import pandas as pd
import requests
from ast_eval_hf import ast_eval_hf, ast_parse
from ast_eval_tf import ast_eval_tf
from ast_eval_th import ast_eval_th
@@ -60,7 +60,7 @@ def fetch_data(url, filename):
with open(cache_path, 'r') as f:
return f.read()
else:
response = requests.get(url)
response = httpx.get(url)
if response.status_code == 200:
with open(cache_path, 'w') as f:
f.write(response.text)
+3 -3
View File
@@ -4,7 +4,7 @@ import re
import string
import zipfile
import requests
import httpx
def download_data(dir):
@@ -40,7 +40,7 @@ def download_tools(dir, wolfram_alpha_appid='YOUR_WOLFRAMALPHA_APPID'):
]
for tool in tools:
url = f'https://raw.githubusercontent.com/night-chen/ToolQA/main/benchmark/ReAct/code/tools/{tool}'
response = requests.get(url)
response = httpx.get(url)
output_file = os.path.join(tool_path, tool.split('/')[1])
with open(output_file, 'wb') as f:
f.write(response.content)
@@ -82,7 +82,7 @@ def get_data(dataset, hardness):
)
data = []
url = f'https://raw.githubusercontent.com/night-chen/ToolQA/main/data/questions/{hardness}/{dataset}-{hardness}.jsonl'
url = requests.get(url)
url = httpx.get(url)
if url.status_code == 200:
lines = url.text.splitlines()
for line in lines:
+7 -1
View File
@@ -1,2 +1,8 @@
VITE_BACKEND_BASE_URL="localhost:3000" # Backend URL without protocol (e.g. localhost:3000)
VITE_MOCK_API="false" # true or false
VITE_BACKEND_HOST="127.0.0.1:3000" # Backend host with port for API connections
VITE_MOCK_API="false" # Enable/disable API mocking with MSW (true or false)
VITE_MOCK_SAAS="false" # Simulate SaaS mode in development (true or false)
VITE_USE_TLS="false" # Use HTTPS/WSS for backend connections (true or false)
VITE_FRONTEND_PORT="3001" # Port to run the frontend application
VITE_INSECURE_SKIP_VERIFY="false" # Skip TLS certificate verification (true or false)
# VITE_GITHUB_TOKEN="" # GitHub token for repository access (used in some tests)
+14 -1
View File
@@ -79,7 +79,20 @@ npm run dev:mock or npm run dev:mock:saas
### Environment Variables
TODO
The frontend application uses the following environment variables:
| Variable | Description | Default Value |
| --------------------------- | ---------------------------------------------------------------------- | ---------------- |
| `VITE_BACKEND_BASE_URL` | The backend hostname without protocol (used for WebSocket connections) | `localhost:3000` |
| `VITE_BACKEND_HOST` | The backend host with port for API connections | `127.0.0.1:3000` |
| `VITE_MOCK_API` | Enable/disable API mocking with MSW | `false` |
| `VITE_MOCK_SAAS` | Simulate SaaS mode in development | `false` |
| `VITE_USE_TLS` | Use HTTPS/WSS for backend connections | `false` |
| `VITE_FRONTEND_PORT` | Port to run the frontend application | `3001` |
| `VITE_INSECURE_SKIP_VERIFY` | Skip TLS certificate verification | `false` |
| `VITE_GITHUB_TOKEN` | GitHub token for repository access (used in some tests) | - |
You can create a `.env` file in the frontend directory with these variables based on the `.env.sample` file.
### Project Structure
@@ -0,0 +1,81 @@
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { ActionSuggestions } from "#/components/features/chat/action-suggestions";
import { useAuth } from "#/context/auth-context";
import { useSelector } from "react-redux";
// Mock dependencies
vi.mock("posthog-js", () => ({
default: {
capture: vi.fn(),
},
}));
vi.mock("react-redux", () => ({
useSelector: vi.fn(),
}));
vi.mock("#/context/auth-context", () => ({
useAuth: vi.fn(),
}));
describe("ActionSuggestions", () => {
// Setup mocks for each test
vi.clearAllMocks();
(useAuth as any).mockReturnValue({
githubTokenIsSet: true,
});
(useSelector as any).mockReturnValue({
selectedRepository: "test-repo",
});
it("should render both GitHub buttons when GitHub token is set and repository is selected", () => {
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
const pushButton = screen.getByRole("button", { name: "Push to Branch" });
const prButton = screen.getByRole("button", { name: "Push & Create PR" });
expect(pushButton).toBeInTheDocument();
expect(prButton).toBeInTheDocument();
});
it("should not render buttons when GitHub token is not set", () => {
(useAuth as any).mockReturnValue({
githubTokenIsSet: false,
});
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
expect(screen.queryByRole("button", { name: "Push to Branch" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Push & Create PR" })).not.toBeInTheDocument();
});
it("should not render buttons when no repository is selected", () => {
(useSelector as any).mockReturnValue({
selectedRepository: null,
});
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
expect(screen.queryByRole("button", { name: "Push to Branch" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Push & Create PR" })).not.toBeInTheDocument();
});
it("should have different prompts for 'Push to Branch' and 'Push & Create PR' buttons", () => {
// This test verifies that the prompts are different in the component
const component = render(<ActionSuggestions onSuggestionsClick={() => {}} />);
// Get the component instance to access the internal values
const pushBranchPrompt = "Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.";
const createPRPrompt = "Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes.";
// Verify the prompts are different
expect(pushBranchPrompt).not.toEqual(createPRPrompt);
// Verify the PR prompt mentions creating a meaningful branch name
expect(createPRPrompt).toContain("meaningful branch name");
expect(createPRPrompt).not.toContain("SAME branch name");
});
});
@@ -1,47 +1,51 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { render, waitFor } from "@testing-library/react";
import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import * as ChatSlice from "#/state/chat-slice";
import {
updateStatusWhenErrorMessagePresent,
WsClientProvider,
useWsClient,
} from "#/context/ws-client-provider";
import React from "react";
describe("Propagate error message", () => {
it("should do nothing when no message was passed from server", () => {
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage");
updateStatusWhenErrorMessagePresent(null)
updateStatusWhenErrorMessagePresent(undefined)
updateStatusWhenErrorMessagePresent({})
updateStatusWhenErrorMessagePresent({message: null})
updateStatusWhenErrorMessagePresent(null);
updateStatusWhenErrorMessagePresent(undefined);
updateStatusWhenErrorMessagePresent({});
updateStatusWhenErrorMessagePresent({ message: null });
expect(addErrorMessageSpy).not.toHaveBeenCalled();
});
it("should display error to user when present", () => {
const message = "We have a problem!"
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage")
updateStatusWhenErrorMessagePresent({message})
const message = "We have a problem!";
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage");
updateStatusWhenErrorMessagePresent({ message });
expect(addErrorMessageSpy).toHaveBeenCalledWith({
message,
status_update: true,
type: 'error'
});
type: "error",
});
});
it("should display error including translation id when present", () => {
const message = "We have a problem!"
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage")
updateStatusWhenErrorMessagePresent({message, data: {msg_id: '..id..'}})
const message = "We have a problem!";
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage");
updateStatusWhenErrorMessagePresent({
message,
data: { msg_id: "..id.." },
});
expect(addErrorMessageSpy).toHaveBeenCalledWith({
message,
id: '..id..',
id: "..id..",
status_update: true,
type: 'error'
});
type: "error",
});
});
});
@@ -51,24 +55,22 @@ const mockOn = vi.fn();
const mockOff = vi.fn();
const mockDisconnect = vi.fn();
vi.mock("socket.io-client", () => {
return {
io: vi.fn(() => ({
emit: mockEmit,
on: mockOn,
off: mockOff,
disconnect: mockDisconnect,
io: {
opts: {
query: {},
},
vi.mock("socket.io-client", () => ({
io: vi.fn(() => ({
emit: mockEmit,
on: mockOn,
off: mockOff,
disconnect: mockDisconnect,
io: {
opts: {
query: {},
},
})),
};
});
},
})),
}));
// Mock component to test the hook
const TestComponent = () => {
function TestComponent() {
const { send } = useWsClient();
React.useEffect(() => {
@@ -77,7 +79,7 @@ const TestComponent = () => {
}, [send]);
return <div>Test Component</div>;
};
}
describe("WsClientProvider", () => {
beforeEach(() => {
@@ -85,18 +87,27 @@ describe("WsClientProvider", () => {
});
it("should emit oh_user_action event when send is called", async () => {
const { getByText } = render(
<WsClientProvider conversationId="test-conversation-id">
<TestComponent />
</WsClientProvider>
);
const { getByText } = render(<TestComponent />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
<WsClientProvider conversationId="test-conversation-id">
{children}
</WsClientProvider>
</QueryClientProvider>
),
});
// Assert
expect(getByText("Test Component")).toBeInTheDocument();
// Wait for the emit call to happen (useEffect needs time to run)
await waitFor(() => {
expect(mockEmit).toHaveBeenCalledWith("oh_user_action", { type: "test_event" });
}, { timeout: 1000 });
await waitFor(
() => {
expect(mockEmit).toHaveBeenCalledWith("oh_user_action", {
type: "test_event",
});
},
{ timeout: 1000 },
);
});
});
+37 -47
View File
@@ -8,7 +8,6 @@ import { AuthProvider } from "#/context/auth-context";
import SettingsScreen from "#/routes/settings";
import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
import { PostApiSettings } from "#/types/settings";
import * as ConsentHandlers from "#/utils/handle-capture-consent";
import AccountSettings from "#/routes/account-settings";
@@ -20,6 +19,7 @@ const toggleAdvancedSettings = async (user: UserEvent) => {
describe("Settings Screen", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
const resetSettingsSpy = vi.spyOn(OpenHands, "resetSettings");
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const { handleLogoutMock } = vi.hoisted(() => ({
@@ -583,6 +583,11 @@ describe("Settings Screen", () => {
test("resetting settings with no changes but having advanced enabled should hide the advanced items", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValueOnce({
...MOCK_DEFAULT_USER_SETTINGS,
});
renderSettingsScreen();
await toggleAdvancedSettings(user);
@@ -594,6 +599,15 @@ describe("Settings Screen", () => {
const modal = await screen.findByTestId("reset-modal");
expect(modal).toBeInTheDocument();
// Mock the settings that will be returned after reset
// This should be the default settings with no advanced settings enabled
getSettingsSpy.mockResolvedValueOnce({
...MOCK_DEFAULT_USER_SETTINGS,
llm_base_url: "",
confirmation_mode: false,
security_analyzer: "",
});
// confirm reset
const confirmButton = within(modal).getByText("Reset");
await user.click(confirmButton);
@@ -817,19 +831,27 @@ describe("Settings Screen", () => {
const confirmButton = within(modal).getByText("Reset");
await user.click(confirmButton);
const mockCopy: Partial<PostApiSettings> = {
...MOCK_DEFAULT_USER_SETTINGS,
};
delete mockCopy.github_token_is_set;
delete mockCopy.unset_github_token;
delete mockCopy.user_consents_to_analytics;
expect(saveSettingsSpy).toHaveBeenCalledWith({
...mockCopy,
provider_tokens: undefined, // not set
llm_api_key: "", // reset as well
await waitFor(() => {
expect(resetSettingsSpy).toHaveBeenCalled();
});
// Mock the settings response after reset
getSettingsSpy.mockResolvedValueOnce({
...MOCK_DEFAULT_USER_SETTINGS,
llm_base_url: "",
confirmation_mode: false,
security_analyzer: "",
});
// Wait for the mutation to complete and the modal to be removed
await waitFor(() => {
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
expect(screen.queryByTestId("llm-custom-model-input")).not.toBeInTheDocument();
expect(screen.queryByTestId("base-url-input")).not.toBeInTheDocument();
expect(screen.queryByTestId("agent-input")).not.toBeInTheDocument();
expect(screen.queryByTestId("security-analyzer-input")).not.toBeInTheDocument();
expect(screen.queryByTestId("enable-confirmation-mode-switch")).not.toBeInTheDocument();
});
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
});
it("should cancel the reset when the 'Cancel' button is clicked", async () => {
@@ -887,32 +909,6 @@ describe("Settings Screen", () => {
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(false);
});
it("should not reset analytics consent when resetting to defaults", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
user_consents_to_analytics: true,
});
renderSettingsScreen();
const analyticsConsentInput = await screen.findByTestId(
"enable-analytics-switch",
);
expect(analyticsConsentInput).toBeChecked();
const resetButton = await screen.findByText("Reset to defaults");
await user.click(resetButton);
const modal = await screen.findByTestId("reset-modal");
const confirmButton = within(modal).getByText("Reset");
await user.click(confirmButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({ user_consents_to_analytics: undefined }),
);
});
it("should render the security analyzer input if the confirmation mode is enabled", async () => {
const user = userEvent.setup();
renderSettingsScreen();
@@ -1094,14 +1090,8 @@ describe("Settings Screen", () => {
const modal = await screen.findByTestId("reset-modal");
const confirmButton = within(modal).getByText("Reset");
await user.click(confirmButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_api_key: undefined,
llm_base_url: undefined,
llm_model: undefined,
}),
);
expect(saveSettingsSpy).not.toHaveBeenCalled();
expect(resetSettingsSpy).toHaveBeenCalled();
});
});
});
+1856 -2282
View File
File diff suppressed because it is too large Load Diff
+26 -26
View File
@@ -1,35 +1,35 @@
{
"name": "openhands-frontend",
"version": "0.29.1",
"version": "0.30.1",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@heroui/react": "2.7.4",
"@heroui/react": "2.7.5",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.3.0",
"@react-router/serve": "^7.3.0",
"@react-router/node": "^7.4.0",
"@react-router/serve": "^7.4.0",
"@react-types/shared": "^3.28.0",
"@reduxjs/toolkit": "^2.6.0",
"@stripe/react-stripe-js": "^3.3.0",
"@stripe/stripe-js": "^5.10.0",
"@tanstack/react-query": "^5.67.2",
"@reduxjs/toolkit": "^2.6.1",
"@stripe/react-stripe-js": "^3.5.1",
"@stripe/stripe-js": "^6.1.0",
"@tanstack/react-query": "^5.69.0",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.8.2",
"axios": "^1.8.4",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.4.10",
"i18next": "^24.2.2",
"framer-motion": "^12.6.2",
"i18next": "^24.2.3",
"i18next-browser-languagedetector": "^8.0.4",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.23",
"jose": "^6.0.8",
"isbot": "^5.1.25",
"jose": "^6.0.10",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.229.3",
"posthog-js": "^1.233.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-highlight": "^0.15.0",
@@ -38,14 +38,14 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.3.0",
"react-router": "^7.4.0",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.7",
"react-textarea-autosize": "^8.5.8",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.0.2",
"vite": "^6.2.1",
"vite": "^6.2.3",
"web-vitals": "^3.5.2",
"ws": "^8.18.1"
},
@@ -80,24 +80,24 @@
},
"devDependencies": {
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.51.0",
"@react-router/dev": "^7.3.0",
"@playwright/test": "^1.51.1",
"@react-router/dev": "^7.4.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.67.2",
"@tanstack/eslint-plugin-query": "^5.68.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.13.9",
"@types/react": "^19.0.8",
"@types/node": "^22.13.14",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.3",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/ws": "^8.18.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^3.0.8",
"autoprefixer": "^10.4.20",
"@vitest/coverage-v8": "^3.0.9",
"autoprefixer": "^10.4.21",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
@@ -105,12 +105,12 @@
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-prettier": "^5.2.5",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^26.0.0",
"lint-staged": "^15.4.3",
"lint-staged": "^15.5.0",
"msw": "^2.6.6",
"postcss": "^8.5.2",
"prettier": "^3.5.3",
+12 -2
View File
@@ -273,6 +273,14 @@ class OpenHands {
return data.status === 200;
}
/**
* Reset user settings in server
*/
static async resetSettings(): Promise<boolean> {
const response = await openHands.post("/api/reset-settings");
return response.status === 200;
}
static async createCheckoutSession(amount: number): Promise<string> {
const { data } = await openHands.post(
"/api/billing/create-checkout-session",
@@ -345,8 +353,10 @@ class OpenHands {
return data;
}
static async logout(): Promise<void> {
await openHands.post("/api/logout");
static async logout(appMode: GetConfigResponse["APP_MODE"]): Promise<void> {
const endpoint =
appMode === "saas" ? "/api/logout" : "/api/unset-settings-tokens";
await openHands.post(endpoint);
}
}
@@ -1,7 +1,7 @@
import { I18nKey } from "#/i18n/declaration";
import { AgentState } from "#/types/agent-state";
enum IndicatorColor {
export enum IndicatorColor {
BLUE = "bg-blue-500",
GREEN = "bg-green-500",
ORANGE = "bg-orange-500",
@@ -40,7 +40,7 @@ export function ActionSuggestions({
suggestion={{
label: "Push & Create PR",
value:
"Please push the changes to GitHub and open a pull request. Please use the exact SAME branch name as the one you are currently on.",
"Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes.",
}}
onClick={(value) => {
posthog.capture("create_pr_button_clicked");
@@ -10,7 +10,6 @@ import { addUserMessage } from "#/state/chat-slice";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { getStopProcessesCommand } from "#/services/terminal-service";
import { FeedbackModal } from "../feedback/feedback-modal";
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
import { TypingIndicator } from "./typing-indicator";
@@ -83,8 +82,7 @@ export function ChatInterface() {
const handleStop = () => {
posthog.capture("stop_button_clicked");
send(getStopProcessesCommand()); // First kill all processes
send(generateAgentStateChangeEvent(AgentState.STOPPED)); // Then change agent state
send(generateAgentStateChangeEvent(AgentState.STOPPED));
};
const onClickShareFeedbackActionButton = async (
@@ -22,7 +22,7 @@ export function AccountSettingsContextMenu({
<ContextMenu
testId="account-settings-context-menu"
ref={ref}
className="absolute right-full md:left-full -top-1 z-10"
className="absolute right-full md:left-full -top-1 z-10 w-fit"
>
<ContextMenuListItem onClick={onLogout} isDisabled={!isLoggedIn}>
{t(I18nKey.ACCOUNT_SETTINGS$LOGOUT)}
@@ -4,7 +4,10 @@ import { useSelector } from "react-redux";
import { showErrorToast } from "#/utils/error-handler";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { AGENT_STATUS_MAP } from "../../agent-status-map.constant";
import {
AGENT_STATUS_MAP,
IndicatorColor,
} from "../../agent-status-map.constant";
import {
useWsClient,
WsClientProviderStatus,
@@ -69,11 +72,17 @@ export function AgentStatusBar() {
};
}, []);
const [indicatorColor, setIndicatorColor] = React.useState<string>(
AGENT_STATUS_MAP[curAgentState].indicator,
);
React.useEffect(() => {
if (status === WsClientProviderStatus.DISCONNECTED) {
setStatusMessage("Connecting...");
setIndicatorColor(IndicatorColor.RED);
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
setIndicatorColor(AGENT_STATUS_MAP[curAgentState].indicator);
if (notificationStates.includes(curAgentState)) {
const message = t(AGENT_STATUS_MAP[curAgentState].message);
notify(t(AGENT_STATUS_MAP[curAgentState].message), {
@@ -87,13 +96,13 @@ export function AgentStatusBar() {
}
}
}
}, [curAgentState, notify, t]);
}, [curAgentState, status, notify, t]);
return (
<div className="flex flex-col items-center">
<div className="flex items-center bg-base-secondary px-2 py-1 text-gray-400 rounded-[100px] text-sm gap-[6px]">
<div
className={`w-2 h-2 rounded-full animate-pulse ${AGENT_STATUS_MAP[curAgentState].indicator}`}
className={`w-2 h-2 rounded-full animate-pulse ${indicatorColor}`}
/>
<span className="text-sm text-stone-400">{t(statusMessage)}</span>
</div>
@@ -240,22 +240,67 @@ export function ConversationCard({
title="Metrics Information"
testID="metrics-modal"
>
<div className="space-y-2">
{metrics?.cost !== null && (
<p>Total Cost: ${metrics.cost.toFixed(4)}</p>
)}
{metrics?.usage !== null && (
<>
<p>Tokens Used:</p>
<ul className="list-inside space-y-1 ml-2">
<li>- Input: {metrics.usage.prompt_tokens}</li>
<li>- Output: {metrics.usage.completion_tokens}</li>
<li>- Total: {metrics.usage.total_tokens}</li>
</ul>
</>
<div className="space-y-4">
{(metrics?.cost !== null || metrics?.usage !== null) && (
<div className="rounded-md p-3">
<div className="grid gap-3">
{metrics?.cost !== null && (
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
<span className="text-lg font-semibold">
Total Cost (USD):
</span>
<span className="font-semibold">
${metrics.cost.toFixed(4)}
</span>
</div>
)}
{metrics?.usage !== null && (
<>
<div className="flex justify-between items-center pb-2">
<span>Total Input Tokens:</span>
<span className="font-semibold">
{metrics.usage.prompt_tokens.toLocaleString()}
</span>
</div>
<div className="grid grid-cols-2 gap-2 pl-4 text-sm">
<span className="text-neutral-400">Cache Hit:</span>
<span className="text-right">
{metrics.usage.cache_read_tokens.toLocaleString()}
</span>
<span className="text-neutral-400">Cache Write:</span>
<span className="text-right">
{metrics.usage.cache_write_tokens.toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center border-b border-neutral-700 pb-2">
<span>Total Output Tokens:</span>
<span className="font-semibold">
{metrics.usage.completion_tokens.toLocaleString()}
</span>
</div>
<div className="flex justify-between items-center pt-1">
<span className="font-semibold">Total Tokens:</span>
<span className="font-bold">
{(
metrics.usage.prompt_tokens +
metrics.usage.completion_tokens
).toLocaleString()}
</span>
</div>
</>
)}
</div>
</div>
)}
{!metrics?.cost && !metrics?.usage && (
<p className="text-neutral-400">No metrics data available</p>
<div className="rounded-md p-4 text-center">
<p className="text-neutral-400">No metrics data available</p>
</div>
)}
</div>
</BaseModal>
@@ -64,7 +64,7 @@ export function PaymentForm() {
onChange={handleTopUpInputChange}
type="text"
label="Add funds"
placeholder="Specify an amount (USD) to add to your account"
placeholder="Specify an amount in USD to add - min $10"
className="w-[680px]"
/>
@@ -21,7 +21,6 @@ import { useLogout } from "#/hooks/mutation/use-logout";
import { useConfig } from "#/hooks/query/use-config";
import { cn } from "#/utils/utils";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
export function Sidebar() {
const location = useLocation();
@@ -36,7 +35,6 @@ export function Sidebar() {
isFetching: isFetchingSettings,
} = useSettings();
const { mutateAsync: logout } = useLogout();
const { mutate: saveUserSettings } = useSaveSettings();
const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
@@ -78,8 +76,7 @@ export function Sidebar() {
};
const handleLogout = async () => {
if (config?.APP_MODE === "saas") await logout();
else saveUserSettings({ unset_github_token: true });
await logout();
posthog.reset();
};
@@ -104,7 +104,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
testId="llm-api-key-help-anchor"
text="Don't know your API key?"
linkText="Click here for instructions"
href="https://docs.all-hands.dev/modules/usage/llms"
href="https://docs.all-hands.dev/modules/usage/installation#getting-an-api-key"
/>
</div>
+1 -1
View File
@@ -78,7 +78,7 @@ export function updateStatusWhenErrorMessagePresent(data: ErrorArg | unknown) {
!!val && typeof val === "object";
const isString = (val: unknown): val is string => typeof val === "string";
if (isObject(data) && "message" in data && isString(data.message)) {
if (data.message === "websocket error") {
if (data.message === "websocket error" || data.message === "timeout") {
return;
}
let msgId: string | undefined;
+13 -3
View File
@@ -1,16 +1,26 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context";
import { useConfig } from "../query/use-config";
export const useLogout = () => {
const { setGitHubTokenIsSet } = useAuth();
const queryClient = useQueryClient();
const { data: config } = useConfig();
return useMutation({
mutationFn: OpenHands.logout,
onSuccess: async () => {
mutationFn: async () => {
// Pause all queries that depend on githubTokenIsSet
queryClient.setQueryData(["user"], null);
// Call logout endpoint
await OpenHands.logout(config?.APP_MODE ?? "oss");
// Remove settings from cache so it will be refetched with new token state
queryClient.removeQueries({ queryKey: ["settings"] });
// Update token state - this will trigger a settings refetch since it's part of the query key
setGitHubTokenIsSet(false);
await queryClient.invalidateQueries();
},
});
};
@@ -4,8 +4,14 @@ import OpenHands from "#/api/open-hands";
import { PostSettings, PostApiSettings } from "#/types/settings";
import { useSettings } from "../query/use-settings";
const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
const resetLlmApiKey = settings.LLM_API_KEY === "";
const saveSettingsMutationFn = async (
settings: Partial<PostSettings> | null,
) => {
// If settings is null, we're resetting
if (settings === null) {
await OpenHands.resetSettings();
return;
}
const apiSettings: Partial<PostApiSettings> = {
llm_model: settings.LLM_MODEL,
@@ -14,12 +20,12 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
language: settings.LANGUAGE || DEFAULT_SETTINGS.LANGUAGE,
confirmation_mode: settings.CONFIRMATION_MODE,
security_analyzer: settings.SECURITY_ANALYZER,
llm_api_key: resetLlmApiKey
? ""
: settings.LLM_API_KEY?.trim() || undefined,
llm_api_key:
settings.LLM_API_KEY === ""
? ""
: settings.LLM_API_KEY?.trim() || undefined,
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
provider_tokens: settings.provider_tokens,
unset_github_token: settings.unset_github_token,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: settings.user_consents_to_analytics,
@@ -33,20 +39,13 @@ export const useSaveSettings = () => {
const { data: currentSettings } = useSettings();
return useMutation({
mutationFn: async (settings: Partial<PostSettings>) => {
const newSettings = { ...currentSettings, ...settings };
// Temp hack for reset logic
if (
settings.LLM_API_KEY === undefined &&
settings.LLM_BASE_URL === undefined &&
settings.LLM_MODEL === undefined
) {
delete newSettings.LLM_API_KEY;
delete newSettings.LLM_BASE_URL;
delete newSettings.LLM_MODEL;
mutationFn: async (settings: Partial<PostSettings> | null) => {
if (settings === null) {
await saveSettingsMutationFn(null);
return;
}
const newSettings = { ...currentSettings, ...settings };
await saveSettingsMutationFn(newSettings);
},
onSuccess: async () => {
+2 -8
View File
@@ -5,13 +5,11 @@ import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context";
import { useLogout } from "../mutation/use-logout";
import { useSaveSettings } from "../mutation/use-save-settings";
export const useGitHubUser = () => {
const { githubTokenIsSet } = useAuth();
const { setGitHubTokenIsSet } = useAuth();
const { mutateAsync: logout } = useLogout();
const { mutate: saveUserSettings } = useSaveSettings();
const { data: config } = useConfig();
const user = useQuery({
@@ -36,11 +34,7 @@ export const useGitHubUser = () => {
}, [user.data]);
const handleLogout = async () => {
if (config?.APP_MODE === "saas") await logout();
else {
saveUserSettings({ unset_github_token: true });
setGitHubTokenIsSet(false);
}
await logout();
posthog.reset();
};
+1 -6
View File
@@ -1,15 +1,10 @@
import { useLogout } from "./mutation/use-logout";
import { useSaveSettings } from "./mutation/use-save-settings";
import { useConfig } from "./query/use-config";
export const useAppLogout = () => {
const { data: config } = useConfig();
const { mutateAsync: logout } = useLogout();
const { mutate: saveUserSettings } = useSaveSettings();
const handleLogout = async () => {
if (config?.APP_MODE === "saas") await logout();
else saveUserSettings({ unset_github_token: true });
await logout();
};
return { handleLogout };
+6 -5
View File
@@ -206,11 +206,6 @@ export const handlers = [
let newSettings: Partial<PostApiSettings> = {};
if (typeof body === "object") {
newSettings = { ...body };
if (newSettings.unset_github_token) {
newSettings.provider_tokens = { github: "", gitlab: "" };
newSettings.github_token_is_set = false;
delete newSettings.unset_github_token;
}
}
const fullSettings = {
@@ -306,4 +301,10 @@ export const handlers = [
}),
http.post("/api/logout", () => HttpResponse.json(null, { status: 200 })),
http.post("/api/reset-settings", async () => {
await delay();
MOCK_USER_PREFERENCES.settings = { ...MOCK_DEFAULT_USER_SETTINGS };
return HttpResponse.json(null, { status: 200 });
}),
];
+3 -17
View File
@@ -25,7 +25,6 @@ import {
displayErrorToast,
displaySuccessToast,
} from "#/utils/custom-toast-handlers";
import { PostSettings } from "#/types/settings";
const REMOTE_RUNTIME_OPTIONS = [
{ key: 1, label: "1x (2 core, 8G)" },
@@ -170,24 +169,11 @@ function AccountSettings() {
};
const handleReset = () => {
const newSettings: Partial<PostSettings> = {
...DEFAULT_SETTINGS,
LLM_API_KEY: "", // reset LLM API key
};
// we don't want the user to be able to modify these settings in SaaS
// and we should make sure they aren't included in the reset
if (shouldHandleSpecialSaasCase) {
delete newSettings.LLM_API_KEY;
delete newSettings.LLM_BASE_URL;
delete newSettings.LLM_MODEL;
}
saveSettings(newSettings, {
saveSettings(null, {
onSuccess: () => {
displaySuccessToast("Settings reset");
setResetSettingsModalIsOpen(false);
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
setLlmConfigMode("basic");
},
});
};
@@ -297,7 +283,7 @@ function AccountSettings() {
testId="llm-api-key-help-anchor"
text="Don't know your API key?"
linkText="Click here for instructions"
href="https://docs.all-hands.dev/modules/usage/llms"
href="https://docs.all-hands.dev/modules/usage/installation#getting-an-api-key"
/>
)}
+2 -5
View File
@@ -87,13 +87,10 @@ export function handleActionMessage(message: ActionMessage) {
}
// Update metrics if available
if (
message.llm_metrics ||
message.tool_call_metadata?.model_response?.usage
) {
if (message.llm_metrics) {
const metrics = {
cost: message.llm_metrics?.accumulated_cost ?? null,
usage: message.tool_call_metadata?.model_response?.usage ?? null,
usage: message.llm_metrics?.accumulated_token_usage ?? null,
};
store.dispatch(setMetrics(metrics));
}
@@ -4,8 +4,3 @@ export function getTerminalCommand(command: string, hidden: boolean = false) {
const event = { action: ActionType.RUN, args: { command, hidden } };
return event;
}
export function getStopProcessesCommand() {
const event = { action: ActionType.RUN, args: { command: "pkill -P $$" } };
return event;
}
+2 -1
View File
@@ -5,7 +5,8 @@ interface MetricsState {
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
cache_read_tokens: number;
cache_write_tokens: number;
} | null;
}
+6
View File
@@ -19,6 +19,12 @@ export interface ActionMessage {
// LLM metrics information
llm_metrics?: {
accumulated_cost: number;
accumulated_token_usage: {
prompt_tokens: number;
completion_tokens: number;
cache_read_tokens: number;
cache_write_tokens: number;
};
};
// Tool call metadata
-2
View File
@@ -35,12 +35,10 @@ export type ApiSettings = {
export type PostSettings = Settings & {
provider_tokens: Record<Provider, string>;
unset_github_token: boolean;
user_consents_to_analytics: boolean | null;
};
export type PostApiSettings = ApiSettings & {
provider_tokens: Record<Provider, string>;
unset_github_token: boolean;
user_consents_to_analytics: boolean | null;
};
+26 -23
View File
@@ -4,17 +4,17 @@ type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- new agent
- new microagent
- create agent
- create an agent
- create microagent
- create a microagent
- add agent
- add an agent
- add microagent
- add a microagent
- microagent template
- new agent
- new microagent
- create agent
- create an agent
- create microagent
- create a microagent
- add agent
- add an agent
- add microagent
- add a microagent
- microagent template
---
This agent helps create new microagents in the `.openhands/microagents` directory by providing guidance and templates.
@@ -22,17 +22,20 @@ This agent helps create new microagents in the `.openhands/microagents` director
Microagents are specialized prompts that provide context and capabilities for specific domains or tasks. They are activated by trigger words in the conversation and help the AI assistant understand what capabilities are available, how to use specific APIs or tools, what limitations exist, and how to handle common scenarios.
When creating a new microagent:
* Create a markdown file in `.openhands/microagents/` with an appropriate name (e.g., `github.md`, `google_workspace.md`)
* Include YAML frontmatter with metadata (name, type, version, agent, triggers)
* type is by DEFAULT knowledge
* version is DEFAULT 1.0.0
* agent is by DEFAULT CodeActAgent
* Document any credentials, environment variables, or API access needed
* Keep trigger words specific to avoid false activations
* Include error handling guidance and limitations
* Provide clear usage examples
* Keep the prompt focused and concise
- Create a markdown file in `.openhands/microagents/` with an appropriate name (e.g., `github.md`, `google_workspace.md`)
- Include YAML frontmatter with metadata (name, type, version, agent, triggers)
- type is by DEFAULT knowledge
- version is DEFAULT 1.0.0
- agent is by DEFAULT CodeActAgent
- Document any credentials, environment variables, or API access needed
- Keep trigger words specific to avoid false activations
- Include error handling guidance and limitations
- Provide clear usage examples
- Keep the prompt focused and concise
For detailed information, see:
* [Microagents Overview](https://docs.all-hands.dev/modules/usage/prompting/microagents-overview)
* [Example GitHub Microagent](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/knowledge/github.md)
- [Microagents Overview](https://docs.all-hands.dev/modules/usage/prompting/microagents-overview)
- [Microagents Syntax](https://docs.all-hands.dev/modules/usage/prompting/microagents-syntax)
- [Example GitHub Microagent](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/knowledge/github.md)
@@ -11,8 +11,10 @@ from openhands.events.action import (
Action,
AgentFinishAction,
)
from openhands.events.event import Event
from openhands.llm.llm import LLM
from openhands.memory.condenser import Condenser
from openhands.memory.condenser.condenser import Condensation, View
from openhands.memory.conversation_memory import ConversationMemory
from openhands.runtime.plugins import (
AgentSkillsRequirement,
@@ -92,6 +94,7 @@ class CodeActAgent(Agent):
def step(self, state: State) -> Action:
"""Performs one step using the CodeAct Agent.
This includes gathering info on previous steps and prompting the model to make a command to execute.
Parameters:
@@ -113,8 +116,23 @@ class CodeActAgent(Agent):
if latest_user_message and latest_user_message.content.strip() == '/exit':
return AgentFinishAction()
# prepare what we want to send to the LLM
messages = self._get_messages(state)
# Condense the events from the state. If we get a view we'll pass those
# to the conversation manager for processing, but if we get a condensation
# event we'll just return that instead of an action. The controller will
# immediately ask the agent to step again with the new view.
condensed_history: list[Event] = []
match self.condenser.condensed_history(state):
case View(events=events):
condensed_history = events
case Condensation(action=condensation_action):
return condensation_action
logger.debug(
f'Processing {len(condensed_history)} events from a total of {len(state.history)} events'
)
messages = self._get_messages(condensed_history)
params: dict = {
'messages': self.llm.format_messages_for_llm(messages),
}
@@ -127,7 +145,7 @@ class CodeActAgent(Agent):
self.pending_actions.append(action)
return self.pending_actions.popleft()
def _get_messages(self, state: State) -> list[Message]:
def _get_messages(self, events: list[Event]) -> list[Message]:
"""Constructs the message history for the LLM conversation.
This method builds a structured conversation history by processing events from the state
@@ -143,7 +161,7 @@ class CodeActAgent(Agent):
6. Adds environment reminders for non-function-calling mode
Args:
state (State): The current state object containing conversation history and other metadata
events: The list of events to convert to messages
Returns:
list[Message]: A list of formatted messages ready for LLM consumption, including:
@@ -167,13 +185,6 @@ class CodeActAgent(Agent):
with_caching=self.llm.is_caching_prompt_active()
)
# Condense the events from the state.
events = self.condenser.condensed_history(state)
logger.debug(
f'Processing {len(events)} events from a total of {len(state.history)} events'
)
# Use ConversationMemory to process events
messages = self.conversation_memory.process_events(
condensed_history=events,
@@ -215,6 +215,13 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
)
)
# Add response id to actions
# This will ensure we can match both actions without tool calls (e.g. MessageAction)
# and actions with tool calls (e.g. CmdRunAction, IPythonRunCellAction, etc.)
# with the token usage data
for action in actions:
action.response_id = response.id
assert len(actions) >= 1
return actions
@@ -24,5 +24,8 @@ For example, if you are using vite.config.js, you should set server.host and ser
{% if runtime_info.additional_agent_instructions %}
{{ runtime_info.additional_agent_instructions }}
{% endif %}
{% if runtime_info.date %}
Today's date is {{ runtime_info.date }} (UTC).
{% endif %}
</RUNTIME_INFORMATION>
{% endif %}
@@ -9,7 +9,7 @@ _FILE_EDIT_DESCRIPTION = """Edit a file in plain-text format.
**Example 1: general edit for short files**
For example, given an existing file `/path/to/file.py` that looks like this:
(this is the end of the file)
(this is the beginning of the file)
1|class MyClass:
2| def __init__(self):
3| self.x = 1
@@ -21,7 +21,7 @@ For example, given an existing file `/path/to/file.py` that looks like this:
(this is the end of the file)
The assistant wants to edit the file to look like this:
(this is the end of the file)
(this is the beginning of the file)
1|class MyClass:
2| def __init__(self):
3| self.x = 1
@@ -45,7 +45,7 @@ print(MyClass().y)
**Example 2: append to file for short files**
For example, given an existing file `/path/to/file.py` that looks like this:
(this is the end of the file)
(this is the beginning of the file)
1|class MyClass:
2| def __init__(self):
3| self.x = 1
+15 -3
View File
@@ -54,7 +54,7 @@ from openhands.events.action import (
MessageAction,
NullAction,
)
from openhands.events.action.agent import RecallAction
from openhands.events.action.agent import CondensationAction, RecallAction
from openhands.events.event import Event
from openhands.events.observation import (
AgentCondensationObservation,
@@ -305,6 +305,8 @@ class AgentController:
return True
if isinstance(event, AgentDelegateAction):
return True
if isinstance(event, CondensationAction):
return True
return False
if isinstance(event, Observation):
if (
@@ -479,8 +481,18 @@ class AgentController:
if self.get_agent_state() != AgentState.RUNNING:
await self.set_agent_state_to(AgentState.RUNNING)
elif action.source == EventSource.AGENT and action.wait_for_response:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
elif action.source == EventSource.AGENT:
# Check if we need to trigger microagents based on agent message content
recall_action = RecallAction(
query=action.content, recall_type=RecallType.KNOWLEDGE
)
self._pending_action = recall_action
# This is source=AGENT because the agent message is the trigger for the microagent retrieval
self.event_stream.add_event(recall_action, EventSource.AGENT)
# If the agent is waiting for a response, set the appropriate state
if action.wait_for_response:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
def _reset(self) -> None:
"""Resets the agent controller."""
+3 -1
View File
@@ -30,7 +30,9 @@ class AgentConfig(BaseModel):
disabled_microagents: list[str] = Field(default_factory=list)
enable_history_truncation: bool = Field(default=True)
enable_som_visual_browsing: bool = Field(default=False)
condenser: CondenserConfig = Field(default_factory=NoOpCondenserConfig)
condenser: CondenserConfig = Field(
default_factory=lambda: NoOpCondenserConfig(type='noop')
)
model_config = {'extra': 'forbid'}
+2 -1
View File
@@ -134,4 +134,5 @@ class AppConfig(BaseModel):
def model_post_init(self, __context):
"""Post-initialization hook, called when the instance is created with only default values."""
super().model_post_init(__context)
AppConfig.defaults_dict = model_defaults_to_dict(self)
if not AppConfig.defaults_dict: # Only set defaults_dict if it's empty
AppConfig.defaults_dict = model_defaults_to_dict(self)
+1 -1
View File
@@ -200,7 +200,7 @@ def condenser_config_from_toml_section(
f'Invalid condenser configuration: {e}. Using NoOpCondenserConfig.'
)
# Default to NoOpCondenserConfig if config fails
config = NoOpCondenserConfig()
config = NoOpCondenserConfig(type='noop')
condenser_mapping['condenser'] = config
return condenser_mapping
+1 -1
View File
@@ -5,7 +5,7 @@ from pydantic import BaseModel
from pydantic.fields import FieldInfo
OH_DEFAULT_AGENT = 'CodeActAgent'
OH_MAX_ITERATIONS = 500
OH_MAX_ITERATIONS = 250
def get_field_info(field: FieldInfo) -> dict[str, Any]:
+13 -12
View File
@@ -1,7 +1,9 @@
from typing import Any
from pydantic import RootModel
class ExtendedConfig(RootModel[dict]):
class ExtendedConfig(RootModel[dict[str, Any]]):
"""Configuration for extended functionalities.
This is implemented as a root model so that the entire input is stored
@@ -9,31 +11,30 @@ class ExtendedConfig(RootModel[dict]):
accessed via attribute or dictionary-style access.
"""
@property
def root(self) -> dict: # type annotation to help mypy
return super().root
def __str__(self) -> str:
# Use the root dict to build a string representation.
attr_str = [f'{k}={repr(v)}' for k, v in self.root.items()]
return f"ExtendedConfig({', '.join(attr_str)})"
root_dict: dict[str, Any] = self.model_dump()
attr_str = [f'{k}={repr(v)}' for k, v in root_dict.items()]
return f'ExtendedConfig({", ".join(attr_str)})'
def __repr__(self) -> str:
return self.__str__()
@classmethod
def from_dict(cls, data: dict) -> 'ExtendedConfig':
def from_dict(cls, data: dict[str, Any]) -> 'ExtendedConfig':
# Create an instance directly by wrapping the input dict.
return cls(data)
def __getitem__(self, key: str) -> object:
def __getitem__(self, key: str) -> Any:
# Provide dictionary-like access via the root dict.
return self.root[key]
root_dict: dict[str, Any] = self.model_dump()
return root_dict[key]
def __getattr__(self, key: str) -> object:
def __getattr__(self, key: str) -> Any:
# Fallback for attribute access using the root dict.
try:
return self.root[key]
root_dict: dict[str, Any] = self.model_dump()
return root_dict[key]
except KeyError as e:
raise AttributeError(
f"'ExtendedConfig' object has no attribute '{key}'"
+13 -5
View File
@@ -5,7 +5,7 @@ import platform
import sys
from ast import literal_eval
from types import UnionType
from typing import Any, MutableMapping, get_args, get_origin
from typing import MutableMapping, get_args, get_origin
from uuid import uuid4
import toml
@@ -46,10 +46,16 @@ def load_from_env(
env_or_toml_dict: The environment variables or a config.toml dict.
"""
def get_optional_type(union_type: UnionType) -> Any:
def get_optional_type(union_type: UnionType | type | None) -> type | None:
"""Returns the non-None type from a Union."""
types = get_args(union_type)
return next((t for t in types if t is not type(None)), None)
if union_type is None:
return None
if get_origin(union_type) is UnionType:
types = get_args(union_type)
return next((t for t in types if t is not type(None)), None)
if isinstance(union_type, type):
return union_type
return None
# helper function to set attributes based on env vars
def set_attr_from_env(sub_config: BaseModel, prefix='') -> None:
@@ -85,7 +91,8 @@ def load_from_env(
elif get_origin(field_type) is dict:
cast_value = literal_eval(value)
else:
cast_value = field_type(value)
if field_type is not None:
cast_value = field_type(value)
setattr(sub_config, field_name, cast_value)
except (ValueError, TypeError):
logger.openhands_logger.error(
@@ -225,6 +232,7 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
# Create default LLM summarizing condenser config
default_condenser = LLMSummarizingCondenserConfig(
llm_config=cfg.get_llm_config(), # Use default LLM config
type='llm',
)
# Set as default condenser
+24 -16
View File
@@ -4,22 +4,30 @@ from openhands.llm.metrics import Metrics, TokenUsage
def get_token_usage_for_event(event: Event, metrics: Metrics) -> TokenUsage | None:
"""
Returns at most one token usage record for the `model_response.id` in this event's
`tool_call_metadata`.
Returns at most one token usage record for either:
- `tool_call_metadata.model_response.id`, if possible
- otherwise event.response_id, if set
If no response_id is found, or none match in metrics.token_usages, returns None.
If neither exist or none matches in metrics.token_usages, returns None.
"""
# 1) Use the tool_call_metadata's response.id if present
if event.tool_call_metadata and event.tool_call_metadata.model_response:
response_id = event.tool_call_metadata.model_response.get('id')
if response_id:
return next(
(
usage
for usage in metrics.token_usages
if usage.response_id == response_id
),
tool_response_id = event.tool_call_metadata.model_response.get('id')
if tool_response_id:
usage_rec = next(
(u for u in metrics.token_usages if u.response_id == tool_response_id),
None,
)
if usage_rec is not None:
return usage_rec
# 2) Fallback to the top-level event.response_id if present
if event.response_id:
return next(
(u for u in metrics.token_usages if u.response_id == event.response_id),
None,
)
return None
@@ -28,17 +36,17 @@ def get_token_usage_for_event_id(
) -> TokenUsage | None:
"""
Starting from the event with .id == event_id and moving backwards in `events`,
find the first TokenUsage record (if any) associated with a response_id from
tool_call_metadata.model_response.id.
find the first TokenUsage record (if any) associated either with:
- tool_call_metadata.model_response.id, or
- event.response_id
Returns the first match found, or None if none is found.
"""
# find the index of the event with the given id
# Find the index of the event with the given id
idx = next((i for i, e in enumerate(events) if e.id == event_id), None)
if idx is None:
return None
# search backward from idx down to 0
# Search backward from idx down to 0
for i in range(idx, -1, -1):
usage = get_token_usage_for_event(events[i], metrics)
if usage is not None:
+25 -31
View File
@@ -1,91 +1,85 @@
from pydantic import BaseModel, Field
__all__ = ['ActionType']
from enum import Enum
class ActionTypeSchema(BaseModel):
MESSAGE: str = Field(default='message')
class ActionType(str, Enum):
MESSAGE = 'message'
"""Represents a message.
"""
START: str = Field(default='start')
START = 'start'
"""Starts a new development task OR send chat from the user. Only sent by the client.
"""
READ: str = Field(default='read')
READ = 'read'
"""Reads the content of a file.
"""
WRITE: str = Field(default='write')
WRITE = 'write'
"""Writes the content to a file.
"""
EDIT: str = Field(default='edit')
EDIT = 'edit'
"""Edits a file by providing a draft.
"""
RUN: str = Field(default='run')
RUN = 'run'
"""Runs a command.
"""
RUN_IPYTHON: str = Field(default='run_ipython')
RUN_IPYTHON = 'run_ipython'
"""Runs a IPython cell.
"""
BROWSE: str = Field(default='browse')
BROWSE = 'browse'
"""Opens a web page.
"""
BROWSE_INTERACTIVE: str = Field(default='browse_interactive')
BROWSE_INTERACTIVE = 'browse_interactive'
"""Interact with the browser instance.
"""
DELEGATE: str = Field(default='delegate')
DELEGATE = 'delegate'
"""Delegates a task to another agent.
"""
THINK: str = Field(default='think')
THINK = 'think'
"""Logs a thought.
"""
FINISH: str = Field(default='finish')
FINISH = 'finish'
"""If you're absolutely certain that you've completed your task and have tested your work,
use the finish action to stop working.
"""
REJECT: str = Field(default='reject')
REJECT = 'reject'
"""If you're absolutely certain that you cannot complete the task with given requirements,
use the reject action to stop working.
"""
NULL: str = Field(default='null')
NULL = 'null'
SUMMARIZE: str = Field(default='summarize')
PAUSE: str = Field(default='pause')
PAUSE = 'pause'
"""Pauses the task.
"""
RESUME: str = Field(default='resume')
RESUME = 'resume'
"""Resumes the task.
"""
STOP: str = Field(default='stop')
STOP = 'stop'
"""Stops the task. Must send a start action to restart a new task.
"""
CHANGE_AGENT_STATE: str = Field(default='change_agent_state')
CHANGE_AGENT_STATE = 'change_agent_state'
PUSH: str = Field(default='push')
PUSH = 'push'
"""Push a branch to github."""
SEND_PR: str = Field(default='send_pr')
SEND_PR = 'send_pr'
"""Send a PR to github."""
RECALL: str = Field(default='recall')
RECALL = 'recall'
"""Retrieves content from a user workspace, microagent, or other source."""
ActionType = ActionTypeSchema()
CONDENSATION = 'condensation'
"""Condenses a list of events into a summary."""
+19 -24
View File
@@ -1,56 +1,51 @@
from pydantic import BaseModel, Field
__all__ = ['ObservationType']
from enum import Enum
class ObservationTypeSchema(BaseModel):
READ: str = Field(default='read')
class ObservationType(str, Enum):
READ = 'read'
"""The content of a file
"""
WRITE: str = Field(default='write')
WRITE = 'write'
EDIT: str = Field(default='edit')
EDIT = 'edit'
BROWSE: str = Field(default='browse')
BROWSE = 'browse'
"""The HTML content of a URL
"""
RUN: str = Field(default='run')
RUN = 'run'
"""The output of a command
"""
RUN_IPYTHON: str = Field(default='run_ipython')
RUN_IPYTHON = 'run_ipython'
"""Runs a IPython cell.
"""
CHAT: str = Field(default='chat')
CHAT = 'chat'
"""A message from the user
"""
DELEGATE: str = Field(default='delegate')
DELEGATE = 'delegate'
"""The result of a task delegated to another agent
"""
MESSAGE: str = Field(default='message')
MESSAGE = 'message'
ERROR: str = Field(default='error')
ERROR = 'error'
SUCCESS: str = Field(default='success')
SUCCESS = 'success'
NULL: str = Field(default='null')
NULL = 'null'
THINK: str = Field(default='think')
THINK = 'think'
AGENT_STATE_CHANGED: str = Field(default='agent_state_changed')
AGENT_STATE_CHANGED = 'agent_state_changed'
USER_REJECTED: str = Field(default='user_rejected')
USER_REJECTED = 'user_rejected'
CONDENSE: str = Field(default='condense')
CONDENSE = 'condense'
"""Result of a condensation operation."""
RECALL: str = Field(default='recall')
RECALL = 'recall'
"""Result of a recall operation. This can be the workspace context, a microagent, or other types of information."""
ObservationType = ObservationTypeSchema()
+1 -3
View File
@@ -3,7 +3,6 @@ from openhands.events.action.agent import (
AgentDelegateAction,
AgentFinishAction,
AgentRejectAction,
AgentSummarizeAction,
AgentThinkAction,
ChangeAgentStateAction,
RecallAction,
@@ -30,11 +29,10 @@ __all__ = [
'AgentFinishAction',
'AgentRejectAction',
'AgentDelegateAction',
'AgentSummarizeAction',
'ChangeAgentStateAction',
'IPythonRunCellAction',
'MessageAction',
'ActionConfirmationStatus',
'AgentThinkAction',
'RecallAction',
]
+84 -15
View File
@@ -20,21 +20,6 @@ class ChangeAgentStateAction(Action):
return f'Agent state changed to {self.agent_state}'
@dataclass
class AgentSummarizeAction(Action):
summary: str
action: str = ActionType.SUMMARIZE
@property
def message(self) -> str:
return self.summary
def __str__(self) -> str:
ret = '**AgentSummarizeAction**\n'
ret += f'SUMMARY: {self.summary}'
return ret
class AgentFinishTaskCompleted(Enum):
FALSE = 'false'
PARTIAL = 'partial'
@@ -126,3 +111,87 @@ class RecallAction(Action):
ret = '**RecallAction**\n'
ret += f'QUERY: {self.query[:50]}'
return ret
@dataclass
class CondensationAction(Action):
"""This action indicates a condensation of the conversation history is happening.
There are two ways to specify the events to be forgotten:
1. By providing a list of event IDs.
2. By providing the start and end IDs of a range of events.
In the second case, we assume that event IDs are monotonically increasing, and that _all_ events between the start and end IDs are to be forgotten.
Raises:
ValueError: If the optional fields are not instantiated in a valid configuration.
"""
action: str = ActionType.CONDENSATION
forgotten_event_ids: list[int] | None = None
"""The IDs of the events that are being forgotten (removed from the `View` given to the LLM)."""
forgotten_events_start_id: int | None = None
"""The ID of the first event to be forgotten in a range of events."""
forgotten_events_end_id: int | None = None
"""The ID of the last event to be forgotten in a range of events."""
summary: str | None = None
"""An optional summary of the events being forgotten."""
summary_offset: int | None = None
"""An optional offset to the start of the resulting view indicating where the summary should be inserted."""
def _validate_field_polymorphism(self) -> bool:
"""Check if the optional fields are instantiated in a valid configuration."""
# For the forgotton events, there are only two valid configurations:
# 1. We're forgetting events based on the list of provided IDs, or
using_event_ids = self.forgotten_event_ids is not None
# 2. We're forgetting events based on the range of IDs.
using_event_range = (
self.forgotten_events_start_id is not None
and self.forgotten_events_end_id is not None
)
# Either way, we can only have one of the two valid configurations.
forgotten_event_configuration = using_event_ids ^ using_event_range
# We also need to check that if the summary is provided, so is the
# offset (and vice versa).
summary_configuration = (
self.summary is None and self.summary_offset is None
) or (self.summary is not None and self.summary_offset is not None)
return forgotten_event_configuration and summary_configuration
def __post_init__(self):
if not self._validate_field_polymorphism():
raise ValueError('Invalid configuration of the optional fields.')
@property
def forgotten(self) -> list[int]:
"""The list of event IDs that should be forgotten."""
# Start by making sure the fields are instantiated in a valid
# configuration. We check this whenever the event is initialized, but we
# can't make the dataclass immutable so we need to check it again here
# to make sure the configuration is still valid.
if not self._validate_field_polymorphism():
raise ValueError('Invalid configuration of the optional fields.')
if self.forgotten_event_ids is not None:
return self.forgotten_event_ids
# If we've gotten this far, the start/end IDs are not None.
assert self.forgotten_events_start_id is not None
assert self.forgotten_events_end_id is not None
return list(
range(self.forgotten_events_start_id, self.forgotten_events_end_id + 1)
)
@property
def message(self) -> str:
if self.summary:
return f'Summary: {self.summary}'
return f'Condenser is dropping the events: {self.forgotten}.'
-3
View File
@@ -60,6 +60,3 @@ class IPythonRunCellAction(Action):
@property
def message(self) -> str:
return f'Running Python code interactively: {self.code}'
+2 -2
View File
@@ -17,12 +17,12 @@ class MessageAction(Action):
return self.content
@property
def images_urls(self):
def images_urls(self) -> list[str] | None:
# Deprecated alias for backward compatibility
return self.image_urls
@images_urls.setter
def images_urls(self, value):
def images_urls(self, value: list[str] | None) -> None:
self.image_urls = value
def __str__(self) -> str:
+32 -12
View File
@@ -39,19 +39,23 @@ class Event:
@property
def message(self) -> str | None:
if hasattr(self, '_message'):
return self._message # type: ignore[attr-defined]
msg = getattr(self, '_message')
return str(msg) if msg is not None else None
return ''
@property
def id(self) -> int:
if hasattr(self, '_id'):
return self._id # type: ignore[attr-defined]
id_val = getattr(self, '_id')
return int(id_val) if id_val is not None else Event.INVALID_ID
return Event.INVALID_ID
@property
def timestamp(self):
def timestamp(self) -> str | None:
if hasattr(self, '_timestamp') and isinstance(self._timestamp, str):
return self._timestamp
ts = getattr(self, '_timestamp')
return str(ts) if ts is not None else None
return None
@timestamp.setter
def timestamp(self, value: datetime) -> None:
@@ -61,22 +65,25 @@ class Event:
@property
def source(self) -> EventSource | None:
if hasattr(self, '_source'):
return self._source # type: ignore[attr-defined]
src = getattr(self, '_source')
return EventSource(src) if src is not None else None
return None
@property
def cause(self) -> int | None:
if hasattr(self, '_cause'):
return self._cause # type: ignore[attr-defined]
cause_val = getattr(self, '_cause')
return int(cause_val) if cause_val is not None else None
return None
@property
def timeout(self) -> int | None:
def timeout(self) -> float | None:
if hasattr(self, '_timeout'):
return self._timeout # type: ignore[attr-defined]
timeout_val = getattr(self, '_timeout')
return float(timeout_val) if timeout_val is not None else None
return None
def set_hard_timeout(self, value: int | None, blocking: bool = True) -> None:
def set_hard_timeout(self, value: float | None, blocking: bool = True) -> None:
"""Set the timeout for the event.
NOTE, this is a hard timeout, meaning that the event will be blocked
@@ -100,20 +107,33 @@ class Event:
@property
def llm_metrics(self) -> Metrics | None:
if hasattr(self, '_llm_metrics'):
return self._llm_metrics # type: ignore[attr-defined]
metrics = getattr(self, '_llm_metrics')
return metrics if isinstance(metrics, Metrics) else None
return None
@llm_metrics.setter
def llm_metrics(self, value: Metrics) -> None:
self._llm_metrics = value
# optional field
# optional field, metadata about the tool call, if the event has a tool call
@property
def tool_call_metadata(self) -> ToolCallMetadata | None:
if hasattr(self, '_tool_call_metadata'):
return self._tool_call_metadata # type: ignore[attr-defined]
metadata = getattr(self, '_tool_call_metadata')
return metadata if isinstance(metadata, ToolCallMetadata) else None
return None
@tool_call_metadata.setter
def tool_call_metadata(self, value: ToolCallMetadata) -> None:
self._tool_call_metadata = value
# optional field, the id of the response from the LLM
@property
def response_id(self) -> str | None:
if hasattr(self, '_response_id'):
return self._response_id # type: ignore[attr-defined]
return None
@response_id.setter
def response_id(self, value: str) -> None:
self._response_id = value
+2
View File
@@ -72,6 +72,7 @@ class RecallObservation(Observation):
repo_instructions: str = ''
runtime_hosts: dict[str, int] = field(default_factory=dict)
additional_agent_instructions: str = ''
date: str = ''
# knowledge
microagent_knowledge: list[MicroagentKnowledge] = field(default_factory=list)
@@ -112,6 +113,7 @@ class RecallObservation(Observation):
f'repo_instructions={self.repo_instructions[:20]}...',
f'runtime_hosts={self.runtime_hosts}',
f'additional_agent_instructions={self.additional_agent_instructions[:20]}...',
f'date={self.date}'
]
)
else:
+11 -6
View File
@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from typing import Any
from browsergym.utils.obs import flatten_axtree_to_str
@@ -16,13 +17,17 @@ class BrowserOutputObservation(Observation):
set_of_marks: str = field(default='', repr=False) # don't show in repr
error: bool = False
observation: str = ObservationType.BROWSE
goal_image_urls: list = field(default_factory=list)
goal_image_urls: list[str] = field(default_factory=list)
# do not include in the memory
open_pages_urls: list = field(default_factory=list)
open_pages_urls: list[str] = field(default_factory=list)
active_page_index: int = -1
dom_object: dict = field(default_factory=dict, repr=False) # don't show in repr
axtree_object: dict = field(default_factory=dict, repr=False) # don't show in repr
extra_element_properties: dict = field(
dom_object: dict[str, Any] = field(
default_factory=dict, repr=False
) # don't show in repr
axtree_object: dict[str, Any] = field(
default_factory=dict, repr=False
) # don't show in repr
extra_element_properties: dict[str, Any] = field(
default_factory=dict, repr=False
) # don't show in repr
last_browser_action: str = ''
@@ -102,4 +107,4 @@ class BrowserOutputObservation(Observation):
skip_generic=False,
filter_visible_only=filter_visible_only,
)
return cur_axtree_txt
return str(cur_axtree_txt)
+4 -4
View File
@@ -2,7 +2,7 @@ import json
import re
import traceback
from dataclasses import dataclass, field
from typing import Self
from typing import Any, Self
from pydantic import BaseModel
@@ -105,10 +105,10 @@ class CmdOutputObservation(Observation):
content: str,
command: str,
observation: str = ObservationType.RUN,
metadata: dict | CmdOutputMetadata | None = None,
metadata: dict[str, Any] | CmdOutputMetadata | None = None,
hidden: bool = False,
**kwargs,
):
**kwargs: Any,
) -> None:
super().__init__(content)
self.command = command
self.observation = observation
+6 -1
View File
@@ -1,3 +1,5 @@
from typing import Any
from openhands.core.exceptions import LLMMalformedActionError
from openhands.events.action.action import Action
from openhands.events.action.agent import (
@@ -6,6 +8,7 @@ from openhands.events.action.agent import (
AgentRejectAction,
AgentThinkAction,
ChangeAgentStateAction,
CondensationAction,
RecallAction,
)
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
@@ -37,12 +40,13 @@ actions = (
RecallAction,
ChangeAgentStateAction,
MessageAction,
CondensationAction,
)
ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]
def handle_action_deprecated_args(args: dict) -> dict:
def handle_action_deprecated_args(args: dict[str, Any]) -> dict[str, Any]:
# keep_prompt has been deprecated in https://github.com/All-Hands-AI/OpenHands/pull/4881
if 'keep_prompt' in args:
args.pop('keep_prompt')
@@ -126,4 +130,5 @@ def action_from_dict(action: dict) -> Action:
raise LLMMalformedActionError(
f'action={action} has the wrong arguments: {str(e)}'
)
assert isinstance(decoded_action, Action)
return decoded_action
+8 -2
View File
@@ -1,6 +1,7 @@
from dataclasses import asdict
from datetime import datetime
from enum import Enum
from typing import Any
from pydantic import BaseModel
@@ -48,14 +49,14 @@ DELETE_FROM_TRAJECTORY_EXTRAS_AND_SCREENSHOTS = DELETE_FROM_TRAJECTORY_EXTRAS |
}
def event_from_dict(data) -> 'Event':
def event_from_dict(data: dict[str, Any]) -> 'Event':
evt: Event
if 'action' in data:
evt = action_from_dict(data)
elif 'observation' in data:
evt = observation_from_dict(data)
else:
raise ValueError('Unknown event type: ' + data)
raise ValueError(f'Unknown event type: {data}')
for key in UNDERSCORE_KEYS:
if key in data:
value = data[key]
@@ -78,6 +79,11 @@ def event_from_dict(data) -> 'Event':
metrics.token_usages = [
TokenUsage(**usage) for usage in value.get('token_usages', [])
]
# Set accumulated token usage if available
if 'accumulated_token_usage' in value:
metrics._accumulated_token_usage = TokenUsage(
**value.get('accumulated_token_usage', {})
)
value = metrics
setattr(evt, '_' + key, value)
return evt
@@ -1,4 +1,5 @@
import copy
from typing import Any
from openhands.events.event import RecallType
from openhands.events.observation.agent import (
@@ -53,8 +54,8 @@ OBSERVATION_TYPE_TO_CLASS = {
def _update_cmd_output_metadata(
metadata: dict | CmdOutputMetadata | None, **kwargs
) -> dict | CmdOutputMetadata:
metadata: dict[str, Any] | CmdOutputMetadata | None, **kwargs: Any
) -> dict[str, Any] | CmdOutputMetadata:
"""Update the metadata of a CmdOutputObservation.
If metadata is None, create a new CmdOutputMetadata instance.
@@ -128,4 +129,6 @@ def observation_from_dict(observation: dict) -> Observation:
for item in extras['microagent_knowledge']
]
return observation_class(content=content, **extras)
obs = observation_class(content=content, **extras)
assert isinstance(obs, Observation)
return obs
+2 -2
View File
@@ -1,4 +1,4 @@
def remove_fields(obj, fields: set[str]):
def remove_fields(obj: dict | list | tuple, fields: set[str]) -> None:
"""Remove fields from an object.
Parameters:
@@ -14,7 +14,7 @@ def remove_fields(obj, fields: set[str]):
elif isinstance(obj, (list, tuple)):
for item in obj:
remove_fields(item, fields)
elif hasattr(obj, '__dataclass_fields__'):
if hasattr(obj, '__dataclass_fields__'):
raise ValueError(
'Object must not contain dataclass, consider converting to dict first'
)
+40 -29
View File
@@ -5,7 +5,7 @@ from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from enum import Enum
from functools import partial
from typing import Callable, Iterable
from typing import Any, AsyncIterator, Callable, Iterable
from openhands.core.logger import openhands_logger as logger
from openhands.events.event import Event, EventSource
@@ -43,18 +43,21 @@ async def session_exists(
class AsyncEventStreamWrapper:
def __init__(self, event_stream, *args, **kwargs):
def __init__(self, event_stream: 'EventStream', *args: Any, **kwargs: Any) -> None:
self.event_stream = event_stream
self.args = args
self.kwargs = kwargs
async def __aiter__(self):
async def __aiter__(self) -> AsyncIterator[Event]:
loop = asyncio.get_running_loop()
# Create an async generator that yields events
for event in self.event_stream.get_events(*self.args, **self.kwargs):
# Run the blocking get_events() in a thread pool
yield await loop.run_in_executor(None, lambda e=event: e) # type: ignore
def get_event(e: Event = event) -> Event:
return e
yield await loop.run_in_executor(None, get_event)
class EventStream:
@@ -121,14 +124,14 @@ class EventStream:
if id >= self._cur_id:
self._cur_id = id + 1
def _init_thread_loop(self, subscriber_id: str, callback_id: str):
def _init_thread_loop(self, subscriber_id: str, callback_id: str) -> None:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if subscriber_id not in self._thread_loops:
self._thread_loops[subscriber_id] = {}
self._thread_loops[subscriber_id][callback_id] = loop
def close(self):
def close(self) -> None:
self._stop_flag.set()
if self._queue_thread.is_alive():
self._queue_thread.join()
@@ -143,7 +146,7 @@ class EventStream:
while not self._queue.empty():
self._queue.get()
def _clean_up_subscriber(self, subscriber_id: str, callback_id: str):
def _clean_up_subscriber(self, subscriber_id: str, callback_id: str) -> None:
if subscriber_id not in self._subscribers:
logger.warning(f'Subscriber not found during cleanup: {subscriber_id}')
return
@@ -191,7 +194,7 @@ class EventStream:
end_id: int | None = None,
reverse: bool = False,
filter_out_type: tuple[type[Event], ...] | None = None,
filter_hidden=False,
filter_hidden: bool = False,
) -> Iterable[Event]:
"""
Retrieve events from the event stream, optionally filtering out events of a given type
@@ -208,7 +211,7 @@ class EventStream:
Events from the stream that match the criteria.
"""
def should_filter(event: Event):
def should_filter(event: Event) -> bool:
if filter_hidden and hasattr(event, 'hidden') and event.hidden:
return True
if filter_out_type is not None and isinstance(event, filter_out_type):
@@ -263,8 +266,11 @@ class EventStream:
return self._cur_id - 1
def subscribe(
self, subscriber_id: EventStreamSubscriber, callback: Callable, callback_id: str
):
self,
subscriber_id: EventStreamSubscriber,
callback: Callable[[Event], None],
callback_id: str,
) -> None:
initializer = partial(self._init_thread_loop, subscriber_id, callback_id)
pool = ThreadPoolExecutor(max_workers=1, initializer=initializer)
if subscriber_id not in self._subscribers:
@@ -279,7 +285,9 @@ class EventStream:
self._subscribers[subscriber_id][callback_id] = callback
self._thread_pools[subscriber_id][callback_id] = pool
def unsubscribe(self, subscriber_id: EventStreamSubscriber, callback_id: str):
def unsubscribe(
self, subscriber_id: EventStreamSubscriber, callback_id: str
) -> None:
if subscriber_id not in self._subscribers:
logger.warning(f'Subscriber not found during unsubscribe: {subscriber_id}')
return
@@ -290,8 +298,8 @@ class EventStream:
self._clean_up_subscriber(subscriber_id, callback_id)
def add_event(self, event: Event, source: EventSource):
if hasattr(event, '_id') and event.id is not None:
def add_event(self, event: Event, source: EventSource) -> None:
if event.id != Event.INVALID_ID:
raise ValueError(
f'Event already has an ID:{event.id}. It was probably added back to the EventStream from inside a handler, triggering a loop.'
)
@@ -310,13 +318,13 @@ class EventStream:
)
self._queue.put(event)
def set_secrets(self, secrets: dict[str, str]):
def set_secrets(self, secrets: dict[str, str]) -> None:
self.secrets = secrets.copy()
def update_secrets(self, secrets: dict[str, str]):
def update_secrets(self, secrets: dict[str, str]) -> None:
self.secrets.update(secrets)
def _replace_secrets(self, data: dict) -> dict:
def _replace_secrets(self, data: dict[str, Any]) -> dict[str, Any]:
for key in data:
if isinstance(data[key], dict):
data[key] = self._replace_secrets(data[key])
@@ -325,7 +333,7 @@ class EventStream:
data[key] = data[key].replace(secret, '<secret_hidden>')
return data
def _run_queue_loop(self):
def _run_queue_loop(self) -> None:
self._queue_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._queue_loop)
try:
@@ -333,7 +341,7 @@ class EventStream:
finally:
self._queue_loop.close()
async def _process_queue(self):
async def _process_queue(self) -> None:
while should_continue() and not self._stop_flag.is_set():
event = None
try:
@@ -350,8 +358,10 @@ class EventStream:
future = pool.submit(callback, event)
future.add_done_callback(self._make_error_handler(callback_id, key))
def _make_error_handler(self, callback_id: str, subscriber_id: str):
def _handle_callback_error(fut):
def _make_error_handler(
self, callback_id: str, subscriber_id: str
) -> Callable[[Any], None]:
def _handle_callback_error(fut: Any) -> None:
try:
# This will raise any exception that occurred during callback execution
fut.result()
@@ -364,14 +374,14 @@ class EventStream:
return _handle_callback_error
def filtered_events_by_source(self, source: EventSource):
def filtered_events_by_source(self, source: EventSource) -> Iterable[Event]:
for event in self.get_events():
if event.source == source:
yield event
def _should_filter_event(
self,
event,
event: Event,
query: str | None = None,
event_types: tuple[type[Event], ...] | None = None,
source: str | None = None,
@@ -394,13 +404,14 @@ class EventStream:
if event_types and not isinstance(event, event_types):
return True
if source and not event.source.value == source:
if source:
if event.source is None or event.source.value != source:
return True
if start_date and event.timestamp is not None and event.timestamp < start_date:
return True
if start_date and event.timestamp < start_date:
return True
if end_date and event.timestamp > end_date:
if end_date and event.timestamp is not None and event.timestamp > end_date:
return True
# Text search in event content if query provided
@@ -422,7 +433,7 @@ class EventStream:
start_id: int = 0,
limit: int = 100,
reverse: bool = False,
) -> list[type[Event]]:
) -> list[Event]:
"""Get matching events from the event stream based on filters.
Args:
+25 -3
View File
@@ -2,18 +2,20 @@ from __future__ import annotations
from enum import Enum
from types import MappingProxyType
from typing import Any, Coroutine, Literal, overload
from typing import Annotated, Any, Coroutine, Literal, overload
from pydantic import (
BaseModel,
Field,
SecretStr,
SerializationInfo,
WithJsonSchema,
field_serializer,
model_validator,
)
from pydantic.json import pydantic_encoder
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.action import Action
from openhands.events.action.commands import CmdRunAction
from openhands.events.stream import EventStream
@@ -57,10 +59,14 @@ class ProviderToken(BaseModel):
PROVIDER_TOKEN_TYPE = MappingProxyType[ProviderType, ProviderToken]
CUSTOM_SECRETS_TYPE = MappingProxyType[str, SecretStr]
PROVIDER_TOKEN_TYPE_WITH_JSON_SCHEMA = Annotated[
PROVIDER_TOKEN_TYPE,
WithJsonSchema({'type': 'object', 'additionalProperties': {'type': 'string'}}),
]
class SecretStore(BaseModel):
provider_tokens: PROVIDER_TOKEN_TYPE = Field(
provider_tokens: PROVIDER_TOKEN_TYPE_WITH_JSON_SCHEMA = Field(
default_factory=lambda: MappingProxyType({})
)
@@ -268,7 +274,9 @@ class ProviderHandler:
get_latest: Get the latest working token for the providers if True, otherwise get the existing ones
"""
if not self.provider_tokens:
# TODO: We should remove `not get_latest` in the future. More
# details about the error this fixes is in the next comment below
if not self.provider_tokens and not get_latest:
return {}
env_vars: dict[ProviderType, SecretStr] = {}
@@ -289,6 +297,20 @@ class ProviderHandler:
if token:
env_vars[provider] = token
# TODO: we have an error where reinitializing the runtime doesn't happen with
# the provider tokens; thus the code above believes that github isn't a provider
# when it really is. We need to share information about current providers set
# for the user when the socket event for connect is sent
if ProviderType.GITHUB not in env_vars and get_latest:
logger.info(
f'Force refresh runtime token for user: {self.external_auth_id}'
)
service = GithubServiceImpl(
external_auth_id=self.external_auth_id,
external_token_manager=self.external_token_manager,
)
env_vars[ProviderType.GITHUB] = await service.get_latest_token()
if not expose_secrets:
return env_vars
+2 -2
View File
@@ -5,7 +5,7 @@ import warnings
from functools import partial
from typing import Any, Callable
import requests
import httpx
from openhands.core.config import LLMConfig
@@ -347,7 +347,7 @@ class LLM(RetryMixin, DebugMixin):
if self.config.model.startswith('litellm_proxy/'):
# IF we are using LiteLLM proxy, get model info from LiteLLM proxy
# GET {base_url}/v1/model/info with litellm_model_id as path param
response = requests.get(
response = httpx.get(
f'{self.config.base_url}/v1/model/info',
headers={
'Authorization': f'Bearer {self.config.api_key.get_secret_value() if self.config.api_key else None}'
+72 -15
View File
@@ -20,12 +20,23 @@ class ResponseLatency(BaseModel):
class TokenUsage(BaseModel):
"""Metric tracking detailed token usage per completion call."""
model: str
prompt_tokens: int
completion_tokens: int
cache_read_tokens: int
cache_write_tokens: int
response_id: str
model: str = Field(default='')
prompt_tokens: int = Field(default=0)
completion_tokens: int = Field(default=0)
cache_read_tokens: int = Field(default=0)
cache_write_tokens: int = Field(default=0)
response_id: str = Field(default='')
def __add__(self, other: 'TokenUsage') -> 'TokenUsage':
"""Add two TokenUsage instances together."""
return TokenUsage(
model=self.model,
prompt_tokens=self.prompt_tokens + other.prompt_tokens,
completion_tokens=self.completion_tokens + other.completion_tokens,
cache_read_tokens=self.cache_read_tokens + other.cache_read_tokens,
cache_write_tokens=self.cache_write_tokens + other.cache_write_tokens,
response_id=self.response_id,
)
class Metrics:
@@ -42,6 +53,14 @@ class Metrics:
self._response_latencies: list[ResponseLatency] = []
self.model_name = model_name
self._token_usages: list[TokenUsage] = []
self._accumulated_token_usage: TokenUsage = TokenUsage(
model=model_name,
prompt_tokens=0,
completion_tokens=0,
cache_read_tokens=0,
cache_write_tokens=0,
response_id='',
)
@property
def accumulated_cost(self) -> float:
@@ -77,6 +96,20 @@ class Metrics:
def token_usages(self, value: list[TokenUsage]) -> None:
self._token_usages = value
@property
def accumulated_token_usage(self) -> TokenUsage:
"""Get the accumulated token usage, initializing it if it doesn't exist."""
if not hasattr(self, '_accumulated_token_usage'):
self._accumulated_token_usage = TokenUsage(
model=self.model_name,
prompt_tokens=0,
completion_tokens=0,
cache_read_tokens=0,
cache_write_tokens=0,
response_id='',
)
return self._accumulated_token_usage
def add_cost(self, value: float) -> None:
if value < 0:
raise ValueError('Added cost cannot be negative.')
@@ -99,15 +132,24 @@ class Metrics:
response_id: str,
) -> None:
"""Add a single usage record."""
self._token_usages.append(
TokenUsage(
model=self.model_name,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
cache_read_tokens=cache_read_tokens,
cache_write_tokens=cache_write_tokens,
response_id=response_id,
)
usage = TokenUsage(
model=self.model_name,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
cache_read_tokens=cache_read_tokens,
cache_write_tokens=cache_write_tokens,
response_id=response_id,
)
self._token_usages.append(usage)
# Update accumulated token usage using the __add__ operator
self._accumulated_token_usage = self.accumulated_token_usage + TokenUsage(
model=self.model_name,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
cache_read_tokens=cache_read_tokens,
cache_write_tokens=cache_write_tokens,
response_id='',
)
def merge(self, other: 'Metrics') -> None:
@@ -118,10 +160,16 @@ class Metrics:
self.token_usages += other.token_usages
self.response_latencies += other.response_latencies
# Merge accumulated token usage using the __add__ operator
self._accumulated_token_usage = (
self.accumulated_token_usage + other.accumulated_token_usage
)
def get(self) -> dict:
"""Return the metrics in a dictionary."""
return {
'accumulated_cost': self._accumulated_cost,
'accumulated_token_usage': self.accumulated_token_usage.model_dump(),
'costs': [cost.model_dump() for cost in self._costs],
'response_latencies': [
latency.model_dump() for latency in self._response_latencies
@@ -134,6 +182,15 @@ class Metrics:
self._costs = []
self._response_latencies = []
self._token_usages = []
# Reset accumulated token usage with a new instance
self._accumulated_token_usage = TokenUsage(
model=self.model_name,
prompt_tokens=0,
completion_tokens=0,
cache_read_tokens=0,
cache_write_tokens=0,
response_id='',
)
def log(self):
"""Log the metrics."""
+13 -2
View File
@@ -1,4 +1,15 @@
import openhands.memory.condenser.impl # noqa F401 (we import this to get the condensers registered)
from openhands.memory.condenser.condenser import Condenser, get_condensation_metadata
from openhands.memory.condenser.condenser import (
Condenser,
get_condensation_metadata,
View,
Condensation,
)
__all__ = ['Condenser', 'get_condensation_metadata', 'CONDENSER_REGISTRY']
__all__ = [
'Condenser',
'get_condensation_metadata',
'CONDENSER_REGISTRY',
'View',
'Condensation',
]
+95 -38
View File
@@ -2,13 +2,15 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from contextlib import contextmanager
from typing import Any
from typing import Any, overload
from typing_extensions import override
from pydantic import BaseModel
from openhands.controller.state.state import State
from openhands.core.config.condenser_config import CondenserConfig
from openhands.events.action.agent import CondensationAction
from openhands.events.event import Event
from openhands.events.observation.agent import AgentCondensationObservation
CONDENSER_METADATA_KEY = 'condenser_meta'
"""Key identifying where metadata is stored in a `State` object's `extra_data` field."""
@@ -32,6 +34,75 @@ CONDENSER_REGISTRY: dict[type[CondenserConfig], type[Condenser]] = {}
"""Registry of condenser configurations to their corresponding condenser classes."""
class View(BaseModel):
"""Linearly ordered view of events.
Produced by a condenser to indicate the included events are ready to process as LLM input.
"""
events: list[Event]
def __len__(self) -> int:
return len(self.events)
def __iter__(self):
return iter(self.events)
# To preserve list-like indexing, we ideally support slicing and position-based indexing.
# The only challenge with that is switching the return type based on the input type -- we
# can mark the different signatures for MyPy with `@overload` decorators.
@overload
def __getitem__(self, key: slice) -> list[Event]: ...
@overload
def __getitem__(self, key: int) -> Event: ...
def __getitem__(self, key: int | slice) -> Event | list[Event]:
if isinstance(key, slice):
start, stop, step = key.indices(len(self))
return [self[i] for i in range(start, stop, step)]
elif isinstance(key, int):
return self.events[key]
else:
raise ValueError(f'Invalid key type: {type(key)}')
@staticmethod
def from_events(events: list[Event]) -> View:
"""Create a view from a list of events, respecting the semantics of any condensation events."""
forgotten_event_ids: set[int] = set()
for event in events:
if isinstance(event, CondensationAction):
forgotten_event_ids.update(event.forgotten)
kept_events = [event for event in events if event.id not in forgotten_event_ids]
# If we have a summary, insert it at the specified offset.
summary: str | None = None
summary_offset: int | None = None
# The relevant summary is always in the last condensation event (i.e., the most recent one).
for event in reversed(events):
if isinstance(event, CondensationAction):
if event.summary is not None and event.summary_offset is not None:
summary = event.summary
summary_offset = event.summary_offset
break
if summary is not None and summary_offset is not None:
kept_events.insert(
summary_offset, AgentCondensationObservation(content=summary)
)
return View(events=kept_events)
class Condensation(BaseModel):
"""Produced by a condenser to indicate the history has been condensed."""
action: CondensationAction
class Condenser(ABC):
"""Abstract condenser interface.
@@ -39,10 +110,7 @@ class Condenser(ABC):
Agents can use condensers to reduce the amount of events they need to consider when deciding which action to take. To use a condenser, agents can call the `condensed_history` method on the current `State` being considered and use the results instead of the full history.
Example usage::
condenser = Condenser.from_config(condenser_config)
events = condenser.condensed_history(state)
If the condenser returns a `Condensation` instead of a `View`, the agent should return `Condensation.action` instead of producing its own action. On the next agent step the condenser will use that condensation event to produce a new `View`.
"""
def __init__(self):
@@ -82,7 +150,7 @@ class Condenser(ABC):
self.write_metadata(state)
@abstractmethod
def condense(self, events: list[Event]) -> list[Event]:
def condense(self, events: list[Event]) -> View | Condensation:
"""Condense a sequence of events into a potentially smaller list.
New condenser strategies should override this method to implement their own condensation logic. Call `self.add_metadata` in the implementation to record any relevant per-condensation diagnostic information.
@@ -91,10 +159,10 @@ class Condenser(ABC):
events: A list of events representing the entire history of the agent.
Returns:
list[Event]: An event sequence representing a condensed history of the agent.
View | Condensation: A condensed view of the events or an event indicating the history has been condensed.
"""
def condensed_history(self, state: State) -> list[Event]:
def condensed_history(self, state: State) -> View | Condensation:
"""Condense the state's history."""
with self.metadata_batch(state):
return self.condense(state.history)
@@ -140,39 +208,28 @@ class Condenser(ABC):
class RollingCondenser(Condenser, ABC):
"""Base class for a specialized condenser strategy that applies condensation to a rolling history.
The rolling history is computed by appending new events to the most recent condensation. For example, the sequence of calls::
The rolling history is generated by `View.from_events`, which analyzes all events in the history and produces a `View` object representing what will be sent to the LLM.
assert state.history == [event1, event2, event3]
condensation = condenser.condensed_history(state)
# ...new events are added to the state...
assert state.history == [event1, event2, event3, event4, event5]
condenser.condensed_history(state)
will result in second call to `condensed_history` passing `condensation + [event4, event5]` to the `condense` method.
If `should_condense` says so, the condenser is then responsible for generating a `Condensation` object from the `View` object. This will be added to the event history which should -- when given to `get_view` -- produce the condensed `View` to be passed to the LLM.
"""
def __init__(self) -> None:
self._condensation: list[Event] = []
self._last_history_length: int = 0
@abstractmethod
def should_condense(self, view: View) -> bool:
"""Determine if a view should be condensed."""
super().__init__()
@abstractmethod
def get_condensation(self, view: View) -> Condensation:
"""Get the condensation from a view."""
@override
def condensed_history(self, state: State) -> list[Event]:
# The history should grow monotonically -- if it doesn't, something has
# truncated the history and we need to reset our tracking.
if len(state.history) < self._last_history_length:
self._condensation = []
self._last_history_length = 0
def condense(self, events: list[Event]) -> View | Condensation:
# Convert the state to a view. This might require some condenser-specific logic.
view = View.from_events(events)
new_events = state.history[self._last_history_length :]
# If we trigger the condenser-specific condensation threshold, compute and return
# the condensation.
if self.should_condense(view):
return self.get_condensation(view)
with self.metadata_batch(state):
results = self.condense(self._condensation + new_events)
self._condensation = results
self._last_history_length = len(state.history)
return results
# Otherwise we're safe to just return the view.
else:
return view
@@ -1,8 +1,12 @@
from __future__ import annotations
from openhands.core.config.condenser_config import AmortizedForgettingCondenserConfig
from openhands.events.event import Event
from openhands.memory.condenser.condenser import RollingCondenser
from openhands.events.action.agent import CondensationAction
from openhands.memory.condenser.condenser import (
Condensation,
RollingCondenser,
View,
)
class AmortizedForgettingCondenser(RollingCondenser):
@@ -32,18 +36,25 @@ class AmortizedForgettingCondenser(RollingCondenser):
super().__init__()
def condense(self, events: list[Event]) -> list[Event]:
"""Apply the amortized forgetting strategy to the given list of events."""
if len(events) <= self.max_size:
return events
def get_condensation(self, view: View) -> Condensation:
target_size = self.max_size // 2
head = events[: self.keep_first]
head = view[: self.keep_first]
events_from_tail = target_size - len(head)
tail = events[-events_from_tail:]
tail = view[-events_from_tail:]
return head + tail
event_ids_to_keep = {event.id for event in head + tail}
event_ids_to_forget = {event.id for event in view} - event_ids_to_keep
event = CondensationAction(
forgotten_events_start_id=min(event_ids_to_forget),
forgotten_events_end_id=max(event_ids_to_forget),
)
return Condensation(action=event)
def should_condense(self, view: View) -> bool:
return len(view) > self.max_size
@classmethod
def from_config(
@@ -4,7 +4,7 @@ from openhands.core.config.condenser_config import BrowserOutputCondenserConfig
from openhands.events.event import Event
from openhands.events.observation import BrowserOutputObservation
from openhands.events.observation.agent import AgentCondensationObservation
from openhands.memory.condenser.condenser import Condenser
from openhands.memory.condenser.condenser import Condensation, Condenser, View
class BrowserOutputCondenser(Condenser):
@@ -17,7 +17,7 @@ class BrowserOutputCondenser(Condenser):
self.attention_window = attention_window
super().__init__()
def condense(self, events: list[Event]) -> list[Event]:
def condense(self, events: list[Event]) -> View | Condensation:
"""Replace the content of browser observations outside of the attention window with a placeholder."""
results: list[Event] = []
cnt: int = 0
@@ -36,7 +36,7 @@ class BrowserOutputCondenser(Condenser):
if isinstance(event, BrowserOutputObservation):
cnt += 1
return list(reversed(results))
return View(events=list(reversed(results)))
@classmethod
def from_config(
@@ -4,9 +4,13 @@ from litellm import supports_response_schema
from pydantic import BaseModel
from openhands.core.config.condenser_config import LLMAttentionCondenserConfig
from openhands.events.event import Event
from openhands.events.action.agent import CondensationAction
from openhands.llm.llm import LLM
from openhands.memory.condenser.condenser import RollingCondenser
from openhands.memory.condenser.condenser import (
Condensation,
RollingCondenser,
View,
)
class ImportantEventSelection(BaseModel):
@@ -43,15 +47,11 @@ class LLMAttentionCondenser(RollingCondenser):
super().__init__()
def condense(self, events: list[Event]) -> list[Event]:
"""If the history is too long, use an LLM to select the most important events."""
if len(events) <= self.max_size:
return events
def get_condensation(self, view: View) -> Condensation:
target_size = self.max_size // 2
head = events[: self.keep_first]
head_event_ids = [event.id for event in view.events[: self.keep_first]]
events_from_tail = target_size - len(head)
events_from_tail = target_size - len(head_event_ids)
message: str = """You will be given a list of actions, observations, and thoughts from a coding agent.
Each item in the list has an identifier. Please sort the identifiers in order of how important the
@@ -66,7 +66,7 @@ class LLMAttentionCondenser(RollingCondenser):
'content': f'<ID>{e.id}</ID>\n<CONTENT>{e.message}</CONTENT>',
'role': 'user',
}
for e in events
for e in view
],
],
response_format={
@@ -82,27 +82,35 @@ class LLMAttentionCondenser(RollingCondenser):
response.choices[0].message.content
).ids
self.add_metadata('all_event_ids', [event.id for event in events])
self.add_metadata('response_ids', response_ids)
self.add_metadata('metrics', self.llm.metrics.get())
# Filter out any IDs from the head and trim the results down
head_ids = [event.id for event in head]
response_ids = [
response_id for response_id in response_ids if response_id not in head_ids
response_id
for response_id in response_ids
if response_id not in head_event_ids
][:events_from_tail]
# If the response IDs aren't _long_ enough, iterate backwards through the events and add any unfound IDs to the list.
for event in reversed(events):
for event in reversed(view):
if len(response_ids) >= events_from_tail:
break
if event.id not in response_ids:
response_ids.append(event.id)
# Grab the events associated with the response IDs
tail = [event for event in events if event.id in response_ids]
# Now that we've found the right number of events to keep, convert this into a list of events to forget.
event = CondensationAction(
forgotten_event_ids=[
event.id
for event in view
if event.id not in response_ids and event.id not in head_event_ids
],
)
return head + tail
return Condensation(action=event)
def should_condense(self, view: View) -> bool:
return len(view) > self.max_size
@classmethod
def from_config(cls, config: LLMAttentionCondenserConfig) -> LLMAttentionCondenser:
@@ -2,10 +2,14 @@ from __future__ import annotations
from openhands.core.config.condenser_config import LLMSummarizingCondenserConfig
from openhands.core.message import Message, TextContent
from openhands.events.event import Event
from openhands.events.action.agent import CondensationAction
from openhands.events.observation.agent import AgentCondensationObservation
from openhands.llm import LLM
from openhands.memory.condenser.condenser import RollingCondenser
from openhands.memory.condenser.condenser import (
Condensation,
RollingCondenser,
View,
)
class LLMSummarizingCondenser(RollingCondenser):
@@ -32,26 +36,22 @@ class LLMSummarizingCondenser(RollingCondenser):
super().__init__()
def condense(self, events: list[Event]) -> list[Event]:
"""Apply the amortized forgetting strategy with LLM summarization to the given list of events."""
if len(events) <= self.max_size:
return events
head = events[: self.keep_first]
def get_condensation(self, view: View) -> Condensation:
head = view[: self.keep_first]
target_size = self.max_size // 2
events_from_tail = target_size - len(head)
tail = events[-events_from_tail:]
# Number of events to keep from the tail -- target size, minus however many
# prefix events from the head, minus one for the summarization event
events_from_tail = target_size - len(head) - 1
summary_event = (
events[self.keep_first]
if isinstance(events[self.keep_first], AgentCondensationObservation)
view[self.keep_first]
if isinstance(view[self.keep_first], AgentCondensationObservation)
else AgentCondensationObservation('No events summarized')
)
# Identify events to be forgotten (those not in head or tail)
forgotten_events = []
for event in events[self.keep_first : -events_from_tail]:
for event in view[self.keep_first : -events_from_tail]:
if not isinstance(event, AgentCondensationObservation):
forgotten_events.append(event)
@@ -82,11 +82,11 @@ CHANGES: str(val) replaces f"{val:.16G}"
DEPS: None modified
INTENT: Fix precision while maintaining FITS compliance"""
prompt + '\n\n'
prompt += '\n\n'
prompt += ('\n' + summary_event.message + '\n') if summary_event.message else ''
prompt + '\n\n'
prompt += '\n\n'
for forgotten_event in forgotten_events:
prompt += str(forgotten_event) + '\n\n'
@@ -101,7 +101,17 @@ INTENT: Fix precision while maintaining FITS compliance"""
self.add_metadata('response', response.model_dump())
self.add_metadata('metrics', self.llm.metrics.get())
return head + [AgentCondensationObservation(summary)] + tail
return Condensation(
action=CondensationAction(
forgotten_events_start_id=min(event.id for event in forgotten_events),
forgotten_events_end_id=max(event.id for event in forgotten_events),
summary=summary,
summary_offset=self.keep_first,
)
)
def should_condense(self, view: View) -> bool:
return len(view) > self.max_size
@classmethod
def from_config(
@@ -2,15 +2,15 @@ from __future__ import annotations
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.events.event import Event
from openhands.memory.condenser.condenser import Condenser
from openhands.memory.condenser.condenser import Condensation, Condenser, View
class NoOpCondenser(Condenser):
"""A condenser that does nothing to the event sequence."""
def condense(self, events: list[Event]) -> list[Event]:
def condense(self, events: list[Event]) -> View | Condensation:
"""Returns the list of events unchanged."""
return events
return View(events=events)
@classmethod
def from_config(cls, config: NoOpCondenserConfig) -> NoOpCondenser:

Some files were not shown because too many files have changed in this diff Show More