Compare commits

..

28 Commits

Author SHA1 Message Date
openhands
68ca0b6a9b Add evaluation changes without disabling repository memory 2025-04-15 15:06:58 +00:00
Shotaro Sano
e0fcd7a61e Fix issue #6098: Prevent duplicate error message display in chat interface (#7858) 2025-04-15 16:21:23 +04:00
Ryan H. Tran
e9989d1085 Upgrade openhands-aci to 0.2.10 (#7810) 2025-04-15 18:43:44 +07:00
Xingyao Wang
49c515b252 frontend: Display think action as action rather than text (#7852)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-15 09:09:16 +08:00
Robert Brennan
2d05578c21 Fix links in readme (#7854) 2025-04-15 02:27:25 +04:00
Engel Nyst
d05a6f30e1 [Refactor] Rename codeact_* agent options to simple name (#7853) 2025-04-15 00:14:13 +02:00
Calvin Smith
10c81c39fb Fix export conversation button in Safari (#7662)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-04-14 15:10:20 -06:00
sumeetkumar1701
2d599349ef fix:Transmitting accurate head parameter in cross-repository pull requests. (#7788) 2025-04-14 17:57:15 +00:00
mamoodi
33caf5c6ca Update feature template to add note about adding reaction (#7847) 2025-04-14 13:56:04 -04:00
Ciocanel Razvan
a9850766a7 Allow input for pr_type openhands-resolver.yml (#7619)
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-04-14 17:53:58 +00:00
OpenHands
77e2416def Fix issue #7826: [Bug]: Chat input box is too small (#7827) 2025-04-14 12:19:38 -05:00
蔡政特
02af9865ec fix: Runtime local docker environment HTTPStatusError (#7648)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-04-14 15:41:54 +00:00
dependabot[bot]
75ca2aa6b1 chore(deps): bump the version-all group with 10 updates (#7846)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 17:16:27 +02:00
mamoodi
d820661592 Update docs on why we use sandbox user (#7845) 2025-04-14 11:01:35 -04:00
Robert Brennan
1ff351a4f1 Add OpenHands Cloud to README, other minor tweaks (#7844) 2025-04-14 14:01:52 +00:00
OpenHands
78b8e58561 Fix issue #7837: [Bug]: Unit tests for tool use support (#7838)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-04-14 15:45:37 +02:00
tofarr
fddbfce51a Fix for race condition in cache (#7812) 2025-04-12 07:43:34 -06:00
Rohit Malhotra
20d3766451 [Fix]: Use better auth header for GitLab microagent (#7828) 2025-04-11 20:09:28 -04:00
sp.wack
72b5e18898 fix(backend): Return 400 if trying to open a binary file (#7825) 2025-04-11 22:47:57 +00:00
Rohit Malhotra
03b8b8c19a (Chore): Rm single provider legacy code (#7821)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-11 18:21:35 +00:00
Panduka Muditha
7c2f1b075e feat: CLI enhancements to support /init, /help and /exit (#7801)
Co-authored-by: Bashwara Undupitiya <bashwarau@verdentra.com>
2025-04-11 14:13:41 -04:00
Graham Neubig
883da1b28c Add extensive typing to openhands/runtime/plugins directory (#7726)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-12 02:02:53 +08:00
Engel Nyst
bb98d94b35 [evaluation] fix missing metadata (#7819) 2025-04-11 16:58:59 +00:00
sp.wack
d114c45135 chore: Improve pre-commit (#7818) 2025-04-11 20:55:26 +04:00
Calvin Smith
36e092e0ac fix: Disable prompt caching in default condenser (#7781)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-04-11 10:09:23 -06:00
Ray Myers
e2bb69908a chore - Rebuild docker image in fork CI instead of using artifacts (#7809) 2025-04-11 11:06:46 -05:00
Ray Myers
cd33c5eac7 Revert "chore - User docker cache mount for vscode server archive (#7… (#7817) 2025-04-11 16:04:50 +00:00
dependabot[bot]
0f8a139fb5 chore(deps): bump the version-all group with 5 updates (#7814)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 17:06:59 +02:00
99 changed files with 2232 additions and 937 deletions

View File

@@ -12,3 +12,6 @@ assignees: ''
**Describe the UX or technical implementation you have in mind**
**Additional context**
### If you find this feature request or enhancement useful, make sure to add a 👍 to the issue

View File

@@ -174,20 +174,19 @@ jobs:
build-args: ${{ env.DOCKER_BUILD_ARGS }}
context: containers/runtime
provenance: false
# Forked repos can't push to GHCR, so we need to upload the image as an artifact
# Forked repos can't push to GHCR, so we just build in order to populate the cache for rebuilding
- name: Build runtime image ${{ matrix.base_image.image }} for fork
if: github.event.pull_request.head.repo.fork
uses: useblacksmith/build-push-action@v1
with:
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
outputs: type=docker,dest=/tmp/runtime-${{ matrix.base_image.tag }}.tar
context: containers/runtime
- name: Upload runtime image for fork
- name: Upload runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/upload-artifact@v4
with:
name: runtime-${{ matrix.base_image.tag }}
path: /tmp/runtime-${{ matrix.base_image.tag }}.tar
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
verify_hash_equivalence_in_runtime_and_app:
name: Verify Hash Equivalence in Runtime and Docker images
@@ -258,17 +257,23 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
# Forked repos can't push to GHCR, so we need to download the image as an artifact
- name: Download runtime image for fork
- name: Download runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/download-artifact@v4
with:
name: runtime-${{ matrix.base_image.tag }}
path: /tmp
- name: Load runtime image for fork
if: github.event.pull_request.head.repo.fork
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
- name: Lowercase Repository Owner
run: |
docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
# Forked repos can't push to GHCR, so we need to rebuild using cache
- name: Build runtime image ${{ matrix.base_image.image }} for fork
if: github.event.pull_request.head.repo.fork
uses: useblacksmith/build-push-action@v1
with:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
@@ -287,9 +292,6 @@ jobs:
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Run docker runtime tests
run: |
# We install pytest-xdist in order to run tests across CPUs
@@ -327,17 +329,23 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
# Forked repos can't push to GHCR, so we need to download the image as an artifact
- name: Download runtime image for fork
- name: Download runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/download-artifact@v4
with:
name: runtime-${{ matrix.base_image.tag }}
path: /tmp
- name: Load runtime image for fork
if: github.event.pull_request.head.repo.fork
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
- name: Lowercase Repository Owner
run: |
docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
# Forked repos can't push to GHCR, so we need to rebuild using cache
- name: Build runtime image ${{ matrix.base_image.image }} for fork
if: github.event.pull_request.head.repo.fork
uses: useblacksmith/build-push-action@v1
with:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
@@ -356,9 +364,6 @@ jobs:
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
- name: Run runtime tests
run: |
# We install pytest-xdist in order to run tests across CPUs

View File

@@ -7,7 +7,7 @@ name: Lint
on:
push:
branches:
- main
- main
pull_request:
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
@@ -33,23 +33,9 @@ jobs:
- name: Lint and TypeScript compilation
run: |
cd frontend
npm run lint:fix
npm run lint
npm run make-i18n && tsc
# Commit and push changes if any as a result of lint:fix
- name: Check for changes
id: git-check
run: |
git diff --quiet || echo "changes=true" >> $GITHUB_OUTPUT
- name: Commit and push if there are changes
if: steps.git-check.outputs.changes == 'true'
run: |
git config --local user.email "openhands@all-hands.dev"
git config --local user.name "OpenHands Bot"
git add -A
git commit -m "🤖 Auto-fix frontend linting issues"
git push
# Run lint on the python code
lint-python:
name: Lint python

View File

@@ -16,6 +16,11 @@ on:
type: string
default: "main"
description: "Target branch to pull and create PR against"
pr_type:
required: false
type: string
default: "draft"
description: "The PR type that is going to be created (draft, ready)"
LLM_MODEL:
required: false
type: string
@@ -280,9 +285,9 @@ jobs:
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--target-branch ${{ env.TARGET_BRANCH }} \
--pr-type draft \
--pr-type ${{ inputs.pr_type || 'draft' }} \
--reviewer ${{ github.actor }} | tee pr_result.txt && \
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
grep "PR created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
else
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \

View File

@@ -27,7 +27,7 @@ Welcome to OpenHands (formerly OpenDevin), a platform for software development a
OpenHands agents can do anything a human developer can: modify code, run commands, browse the web,
call APIs, and yes—even copy code snippets from StackOverflow.
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [Quick Start](#-quick-start).
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for OpenHands Cloud](https://app.all-hands.dev) to get started.
> [!IMPORTANT]
> Using OpenHands for work? We'd love to chat! Fill out
@@ -36,12 +36,21 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [
![App screenshot](./docs/static/img/screenshot.png)
## ⚡ Quick Start
## ☁️ OpenHands Cloud
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
which comes with $50 in free credits for new users.
The easiest way to run OpenHands is in Docker.
## 💻 Running OpenHands Locally
OpenHands can also run on your local system using Docker.
See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) guide for
system requirements and more information.
> [!WARNING]
> On a public network? See our [Hardened Docker Installation Guide](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation)
> to secure your deployment by restricting network binding and implementing additional security measures.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
@@ -56,17 +65,21 @@ docker run -it --rm --pull=always \
docker.all-hands.dev/all-hands-ai/openhands:0.32
```
> [!WARNING]
> On a public network? See our [Hardened Docker Installation](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation) guide
> to secure your deployment by restricting network binding and implementing additional security measures.
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
Finally, you'll need a model provider and API key.
When you open the application, you'll be asked to choose an LLM provider and add an API key.
[Anthropic's Claude 3.5 Sonnet](https://www.anthropic.com/api) (`anthropic/claude-3-5-sonnet-20241022`)
works best, but you have [many options](https://docs.all-hands.dev/modules/usage/llms).
---
## 💡 Other ways to run OpenHands
> [!CAUTION]
> OpenHands is meant to be run by a single user on their local workstation.
> It is not appropriate for multi-tenant deployments where multiple users share the same instance. There is no built-in authentication, isolation, or scalability.
>
> If you're interested in running OpenHands in a multi-tenant environment, please
> [get in touch with us](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> for advanced deployment options.
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes/docker#connecting-to-your-filesystem),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
@@ -75,14 +88,6 @@ or run it on tagged issues with [a github action](https://docs.all-hands.dev/mod
Visit [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
> [!CAUTION]
> OpenHands is meant to be run by a single user on their local workstation.
> It is not appropriate for multi-tenant deployments where multiple users share the same instance. There is no built-in isolation or scalability.
>
> If you're interested in running OpenHands in a multi-tenant environment, please
> [get in touch with us](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> for advanced deployment options.
If you want to modify the OpenHands source code, check out [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
Having issues? The [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting) can help.

View File

@@ -216,13 +216,13 @@ model = "gpt-4o"
[agent]
# Whether the browsing tool is enabled
codeact_enable_browsing = true
enable_browsing = true
# Whether the LLM draft editor is enabled
codeact_enable_llm_editor = false
enable_llm_editor = false
# Whether the IPython tool is enabled
codeact_enable_jupyter = true
enable_jupyter = true
# LLM config group to use
#llm_config = 'your-llm-config-group'

View File

@@ -354,12 +354,12 @@ Les options de configuration de l'agent sont définies dans les sections `[agent
- Valeur par défaut : `true`
- Description : Si l'appel de fonction est activé
- `codeact_enable_browsing`
- `enable_browsing`
- Type : `bool`
- Valeur par défaut : `false`
- Description : Si le délégué de navigation est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- Type : `bool`
- Valeur par défaut : `false`
- Description : Si l'éditeur LLM est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)

View File

@@ -348,12 +348,12 @@ dockerコマンドで使用する場合は、`-e LLM_<option>`として渡しま
- デフォルト値: `true`
- 説明: 関数呼び出しが有効かどうか
- `codeact_enable_browsing`
- `enable_browsing`
- 型: `bool`
- デフォルト値: `false`
- 説明: アクションスペースでブラウジングデリゲートが有効かどうか(関数呼び出しでのみ機能)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- 型: `bool`
- デフォルト値: `false`
- 説明: アクションスペースでLLMエディタが有効かどうか関数呼び出しでのみ機能

View File

@@ -292,17 +292,17 @@ As opções de configuração do agente são definidas nas seções `[agent]` e
- Padrão: `true`
- Descrição: Se a chamada de função está habilitada
- `codeact_enable_browsing`
- `enable_browsing`
- Tipo: `bool`
- Padrão: `false`
- Descrição: Se o delegado de navegação está habilitado no espaço de ação (funciona apenas com chamada de função)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- Tipo: `bool`
- Padrão: `false`
- Descrição: Se o editor LLM está habilitado no espaço de ação (funciona apenas com chamada de função)
- `codeact_enable_jupyter`
- `enable_jupyter`
- Tipo: `bool`
- Padrão: `false`
- Descrição: Se o Jupyter está habilitado no espaço de ação

View File

@@ -349,17 +349,17 @@ Agent 配置选项在 `config.toml` 文件的 `[agent]` 和 `[agent.<agent_name>
- 默认值: `true`
- 描述: 是否启用函数调用
- `codeact_enable_browsing`
- `enable_browsing`
- 类型: `bool`
- 默认值: `false`
- 描述: 是否在 action space 中启用浏览代理(仅适用于函数调用)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- 类型: `bool`
- 默认值: `false`
- 描述: 是否在 action space 中启用 LLM 编辑器(仅适用于函数调用)
- `codeact_enable_jupyter`
- `enable_jupyter`
- 类型: `bool`
- 默认值: `false`
- 描述: 是否在 action space 中启用 Jupyter

View File

@@ -291,17 +291,17 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Default: `true`
- Description: Whether function calling is enabled
- `codeact_enable_browsing`
- `enable_browsing`
- Type: `bool`
- Default: `false`
- Description: Whether browsing delegate is enabled in the action space (only works with function calling)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- Type: `bool`
- Default: `false`
- Description: Whether LLM editor is enabled in the action space (only works with function calling)
- `codeact_enable_jupyter`
- `enable_jupyter`
- Type: `bool`
- Default: `false`
- Description: Whether Jupyter is enabled in the action space

View File

@@ -50,3 +50,6 @@ docker run -it \
```
This command will start an interactive session in Docker where you can input tasks and receive responses from OpenHands.
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host users
permissions. This prevents the agent from creating root-owned files in the mounted workspace.

View File

@@ -47,6 +47,9 @@ docker run -it \
python -m openhands.core.main -t "write a bash script that prints hi"
```
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host users
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
## Advanced Headless Configurations
To view all available configuration options for headless mode, run the Python command with the `--help` flag.

View File

@@ -35,8 +35,8 @@ A useful feature is the ability to connect to your local filesystem. To mount yo
Be careful! There's nothing stopping the OpenHands agent from deleting or modifying
any files that are mounted into its workspace.
This setup can cause some issues with file permissions (hence the `SANDBOX_USER_ID` variable)
but seems to work well on most systems.
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host users
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
## Hardened Docker Installation

View File

@@ -20,3 +20,18 @@ Try these in order:
* If using Docker Desktop, ensure `Settings > Advanced > Allow the default Docker socket to be used` is enabled.
* Depending on your configuration you may need `Settings > Resources > Network > Enable host networking` enabled in Docker Desktop.
* Reinstall Docker Desktop.
### Permission Error
**Description**
On initial prompt, an error is seen with `Permission Denied` or `PermissionError`.
**Resolution**
* Check if the `~/.openhands-state` is owned by `root`. If so, you can:
* Change the directory's ownership: `sudo chown <user>:<user> ~/.openhands-state`.
* or update permissions on the directory: `sudo chmod 777 ~/.openhands-state`
* or delete it if you dont need previous data. OpenHands will recreate it. You'll need to re-enter LLM settings.
* If mounting a local directory, ensure your `WORKSPACE_BASE` has the necessary permissions for the user running
OpenHands.

View File

@@ -14,6 +14,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -74,6 +75,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -22,6 +22,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -55,6 +56,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -21,6 +21,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -61,6 +62,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
# copy 'draft_editor' config if exists

View File

@@ -19,6 +19,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -72,6 +73,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -22,6 +22,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -86,6 +87,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -15,6 +15,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -50,6 +51,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -20,6 +20,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
@@ -131,10 +132,11 @@ def get_config(
)
)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=False,
)
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
return config

View File

@@ -21,6 +21,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -76,11 +77,13 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
agent_config = AgentConfig(
function_calling=False,
codeact_enable_jupyter=True,
codeact_enable_browsing_delegate=True,
enable_jupyter=True,
enable_browsing=True,
)
config.set_agent_config(agent_config)
return config

View File

@@ -18,6 +18,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -66,6 +67,8 @@ def get_config(
else:
logger.info('Agent config not provided, using default settings')
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -16,6 +16,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -54,6 +55,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -34,6 +34,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -75,6 +76,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -27,6 +27,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -96,6 +97,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -14,6 +14,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -63,6 +64,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -19,6 +19,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -121,6 +122,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -30,6 +30,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -91,6 +92,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -30,6 +30,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
@@ -96,7 +97,7 @@ Phase 1. READING: read the problem and reword it in clearer terms
Phase 2. RUNNING: install and run the tests on the repository
2.1 Follow the readme
2.2 Install the environment and anything needed
2.2 Iterate and figure out how to run the tests
2.2 Iterate and figure out how to run the tests
Phase 3. EXPLORATION: find the files that are related to the problem and possible solutions
3.1 Use `grep` to search for relevant methods, classes, keywords and error messages.
@@ -225,12 +226,13 @@ def get_config(
)
)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
return config
@@ -238,6 +240,7 @@ def get_config(
def initialize_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required
metadata: EvalMetadata,
):
"""Initialize the runtime for the agent.
@@ -577,7 +580,7 @@ def process_instance(
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance)
initialize_runtime(runtime, instance, metadata)
message_action = get_instruction(instance, metadata)

View File

@@ -30,6 +30,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
@@ -158,12 +159,13 @@ def get_config(
)
)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
return config

View File

@@ -13,7 +13,10 @@ from typing import List
import yaml
from browsing import pre_login
from evaluation.utils.shared import get_default_sandbox_config_for_eval
from evaluation.utils.shared import (
get_default_sandbox_config_for_eval,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
@@ -58,12 +61,14 @@ def get_config(
)
config.set_llm_config(llm_config)
if agent_config:
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
else:
logger.info('Agent config not provided, using default settings')
agent_config = AgentConfig(
enable_prompt_extensions=False,
)
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
return config

View File

@@ -15,6 +15,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -55,6 +56,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -16,6 +16,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -76,6 +77,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -62,9 +62,9 @@ def get_config(
)
)
agent_config = AgentConfig(
codeact_enable_jupyter=True,
codeact_enable_browsing=True,
codeact_enable_llm_editor=False,
enable_jupyter=True,
enable_browsing=True,
enable_llm_editor=False,
)
config.set_agent_config(agent_config)
return config

View File

@@ -160,6 +160,26 @@ def cleanup():
process.join()
def update_agent_config_for_eval(
agent_config: AgentConfig | None = None,
) -> AgentConfig:
"""Update agent config with evaluation-specific settings.
Args:
agent_config: The agent config to update. If None, a new AgentConfig will be created.
Returns:
The updated agent config.
"""
if agent_config is None:
agent_config = AgentConfig()
# Note: We're not disabling repository memory here as requested
# This function can be used for other evaluation-specific settings
return agent_config
def make_metadata(
llm_config: LLMConfig,
dataset_name: str,
@@ -172,6 +192,8 @@ def make_metadata(
agent_config: AgentConfig | None = None,
condenser_config: CondenserConfig | None = None,
) -> EvalMetadata:
# Update agent config with evaluation-specific settings
agent_config = update_agent_config_for_eval(agent_config)
model_name = llm_config.model.split('/')[-1]
model_path = model_name.replace(':', '_').replace('@', '-')
eval_note = f'_N_{eval_note}' if eval_note else ''

View File

@@ -1,9 +1,5 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithProviders } from "../../test-utils";
import { BrowserPanel } from "#/components/features/browser/browser";
// Mock useParams before importing components
vi.mock("react-router", async () => {
const actual = await vi.importActual("react-router");
@@ -27,6 +23,10 @@ vi.mock("react-i18next", async () => {
};
});
import { screen } from "@testing-library/react";
import { renderWithProviders } from "../../test-utils";
import { BrowserPanel } from "#/components/features/browser/browser";
describe("Browser", () => {
afterEach(() => {
vi.clearAllMocks();

View File

@@ -17,7 +17,7 @@ describe("CopyToClipboardButton", () => {
isDisabled={false}
onClick={() => {}}
mode="copy"
/>,
/>
);
const button = screen.getByTestId("copy-to-clipboard");
@@ -31,7 +31,7 @@ describe("CopyToClipboardButton", () => {
isDisabled={false}
onClick={() => {}}
mode="copied"
/>,
/>
);
const button = screen.getByTestId("copy-to-clipboard");

View File

@@ -1,8 +1,8 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { useSelector } from "react-redux";
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", () => ({
@@ -24,9 +24,9 @@ vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
ACTION$PUSH_TO_BRANCH: "Push to Branch",
ACTION$PUSH_CREATE_PR: "Push & Create PR",
ACTION$PUSH_CHANGES_TO_PR: "Push Changes to PR",
"ACTION$PUSH_TO_BRANCH": "Push to Branch",
"ACTION$PUSH_CREATE_PR": "Push & Create PR",
"ACTION$PUSH_CHANGES_TO_PR": "Push Changes to PR"
};
return translations[key] || key;
},

View File

@@ -217,6 +217,17 @@ describe("ChatInput", () => {
expect(onImagePaste).toHaveBeenCalledWith([file]);
});
it("should use the default maxRows value", () => {
// We can't directly test the maxRows prop as it's not exposed in the DOM
// Instead, we'll verify the component renders with the default props
render(<ChatInput onSubmit={onSubmitMock} />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
// The actual verification of maxRows=16 is handled internally by the TextareaAutosize component
// and affects how many rows the textarea can expand to
});
it("should not submit when Enter is pressed during IME composition", async () => {
const user = userEvent.setup();
render(<ChatInput onSubmit={onSubmitMock} />);

View File

@@ -1,8 +1,8 @@
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import type { Message } from "#/message";
import { act, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import type { Message } from "#/message";
import { addUserMessage } from "#/state/chat-slice";
import { SUGGESTIONS } from "#/utils/suggestions";
import * as ChatSlice from "#/state/chat-slice";

View File

@@ -24,9 +24,7 @@ describe("AuthModal", () => {
const user = userEvent.setup();
render(<AuthModal githubAuthUrl={null} />);
const checkbox = screen.getByRole("checkbox");
const button = screen.getByRole("button", {
name: "GITHUB$CONNECT_TO_GITHUB",
});
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
expect(button).toBeDisabled();
@@ -47,9 +45,7 @@ describe("AuthModal", () => {
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const button = screen.getByRole("button", {
name: "GITHUB$CONNECT_TO_GITHUB",
});
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
await user.click(button);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);

View File

@@ -16,6 +16,8 @@ import { ConversationCard } from "#/components/features/conversation-panel/conve
import { clickOnEditButton } from "./utils";
// We'll use the actual i18next implementation but override the translation function
import { I18nextProvider } from "react-i18next";
import i18n from "i18next";
// Mock the t function to return our custom translations
vi.mock("react-i18next", async () => {
@@ -25,9 +27,9 @@ vi.mock("react-i18next", async () => {
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
CONVERSATION$CREATED: "Created",
CONVERSATION$AGO: "ago",
CONVERSATION$UPDATED: "Updated",
"CONVERSATION$CREATED": "Created",
"CONVERSATION$AGO": "ago",
"CONVERSATION$UPDATED": "Updated"
};
return translations[key] || key;
},
@@ -80,9 +82,7 @@ describe("ConversationCard", () => {
expect(card).toHaveTextContent("ago");
// Use a regex to match the time part since it might have whitespace
const timeRegex = new RegExp(
formatTimeDelta(new Date("2021-10-01T12:00:00Z")),
);
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z")));
expect(card).toHaveTextContent(timeRegex);
});

View File

@@ -1,13 +1,19 @@
import { screen, waitFor, within } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { QueryClientConfig } from "@tanstack/react-query";
import {
QueryClientProvider,
QueryClient,
QueryClientConfig,
} from "@tanstack/react-query";
import userEvent from "@testing-library/user-event";
import { createRoutesStub } from "react-router";
import React from "react";
import { renderWithProviders } from "test-utils";
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
import OpenHands from "#/api/open-hands";
import { AuthProvider } from "#/context/auth-context";
import { clickOnEditButton } from "./utils";
import { queryClientConfig } from "#/query-client-config";
import { renderWithProviders } from "test-utils";
describe("ConversationPanel", () => {
const onCloseMock = vi.fn();
@@ -23,9 +29,9 @@ describe("ConversationPanel", () => {
preloadedState: {
metrics: {
cost: null,
usage: null,
},
},
usage: null
}
}
});
const { endSessionMock } = vi.hoisted(() => ({
@@ -78,9 +84,7 @@ describe("ConversationPanel", () => {
vi.clearAllMocks();
vi.restoreAllMocks();
// Setup default mock for getUserConversations
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([
...mockConversations,
]);
vi.spyOn(OpenHands, "getUserConversations").mockResolvedValue([...mockConversations]);
});
it("should render the conversations", async () => {
@@ -134,9 +138,7 @@ describe("ConversationPanel", () => {
const cancelButton = screen.getByRole("button", { name: /cancel/i });
await user.click(cancelButton);
expect(
screen.queryByRole("button", { name: /cancel/i }),
).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /cancel/i })).not.toBeInTheDocument();
// Ensure the conversation is not deleted
cards = await screen.findAllByTestId("conversation-card");
@@ -149,22 +151,19 @@ describe("ConversationPanel", () => {
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockImplementation(async () => mockData);
const deleteUserConversationSpy = vi.spyOn(
OpenHands,
"deleteUserConversation",
);
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
deleteUserConversationSpy.mockImplementation(async (id: string) => {
const index = mockData.findIndex((conv) => conv.conversation_id === id);
const index = mockData.findIndex(conv => conv.conversation_id === id);
if (index !== -1) {
mockData.splice(index, 1);
}
// Wait for React Query to update its cache
await new Promise((resolve) => setTimeout(resolve, 0));
await new Promise(resolve => setTimeout(resolve, 0));
});
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
let cards = await screen.findAllByTestId("conversation-card");
const ellipsisButton = within(cards[1]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const deleteButton = screen.getByTestId("delete-button");
@@ -176,18 +175,13 @@ describe("ConversationPanel", () => {
const confirmButton = screen.getByRole("button", { name: /confirm/i });
await user.click(confirmButton);
expect(
screen.queryByRole("button", { name: /confirm/i }),
).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
// Wait for the cards to update with a longer timeout
await waitFor(
() => {
const updatedCards = screen.getAllByTestId("conversation-card");
expect(updatedCards).toHaveLength(2);
},
{ timeout: 2000 },
);
await waitFor(() => {
const updatedCards = screen.getAllByTestId("conversation-card");
expect(updatedCards).toHaveLength(2);
}, { timeout: 2000 });
expect(endSessionMock).toHaveBeenCalledOnce();
});
@@ -224,12 +218,9 @@ describe("ConversationPanel", () => {
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockImplementation(async () => mockData);
const deleteUserConversationSpy = vi.spyOn(
OpenHands,
"deleteUserConversation",
);
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
deleteUserConversationSpy.mockImplementation(async (id: string) => {
const index = mockData.findIndex((conv) => conv.conversation_id === id);
const index = mockData.findIndex(conv => conv.conversation_id === id);
if (index !== -1) {
mockData.splice(index, 1);
}
@@ -237,7 +228,7 @@ describe("ConversationPanel", () => {
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
let cards = await screen.findAllByTestId("conversation-card");
expect(cards).toHaveLength(3);
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
@@ -251,9 +242,7 @@ describe("ConversationPanel", () => {
const confirmButton = screen.getByRole("button", { name: /confirm/i });
await user.click(confirmButton);
expect(
screen.queryByRole("button", { name: /confirm/i }),
).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
// Wait for the cards to update
await waitFor(() => {
@@ -359,9 +348,9 @@ describe("ConversationPanel", () => {
preloadedState: {
metrics: {
cost: null,
usage: null,
},
},
usage: null
}
}
});
const toggleButton = screen.getByText("Toggle");

View File

@@ -1,11 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { FeedbackForm } from "#/components/features/feedback/feedback-form";
import { I18nKey } from "#/i18n/declaration";
// Mock useParams before importing components
vi.mock("react-router", async () => {
const actual = await vi.importActual("react-router");
@@ -15,6 +9,12 @@ vi.mock("react-router", async () => {
};
});
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { FeedbackForm } from "#/components/features/feedback/feedback-form";
import { I18nKey } from "#/i18n/declaration";
describe("FeedbackForm", () => {
const user = userEvent.setup();
const onCloseMock = vi.fn();

View File

@@ -1,8 +1,8 @@
import { screen } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { renderWithProviders } from "test-utils";
import { Messages } from "#/components/features/chat/messages";
import type { Message } from "#/message";
import { renderWithProviders } from "test-utils";
describe("File Operations Messages", () => {
it("should show success indicator for successful file read operation", () => {
@@ -17,9 +17,7 @@ describe("File Operations Messages", () => {
},
];
renderWithProviders(
<Messages messages={messages} isAwaitingUserConfirmation={false} />,
);
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
const statusIcon = screen.getByTestId("status-icon");
expect(statusIcon).toBeInTheDocument();
@@ -38,9 +36,7 @@ describe("File Operations Messages", () => {
},
];
renderWithProviders(
<Messages messages={messages} isAwaitingUserConfirmation={false} />,
);
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
const statusIcon = screen.getByTestId("status-icon");
expect(statusIcon).toBeInTheDocument();
@@ -59,9 +55,7 @@ describe("File Operations Messages", () => {
},
];
renderWithProviders(
<Messages messages={messages} isAwaitingUserConfirmation={false} />,
);
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
const statusIcon = screen.getByTestId("status-icon");
expect(statusIcon).toBeInTheDocument();
@@ -80,9 +74,7 @@ describe("File Operations Messages", () => {
},
];
renderWithProviders(
<Messages messages={messages} isAwaitingUserConfirmation={false} />,
);
renderWithProviders(<Messages messages={messages} isAwaitingUserConfirmation={false} />);
const statusIcon = screen.getByTestId("status-icon");
expect(statusIcon).toBeInTheDocument();

View File

@@ -1,7 +1,7 @@
import { ImagePreview } from "#/components/features/images/image-preview";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { ImagePreview } from "#/components/features/images/image-preview";
describe("ImagePreview", () => {
it("should render an image", () => {

View File

@@ -1,4 +1,4 @@
import { render, screen, within } from "@testing-library/react";
import { render, screen, within, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
@@ -144,7 +144,7 @@ describe("InteractiveChatBox", () => {
onStop={onStop}
onChange={onChange}
value="test message"
/>,
/>
);
// Upload an image via the upload button - this should NOT clear the text input
@@ -173,7 +173,7 @@ describe("InteractiveChatBox", () => {
onStop={onStop}
onChange={onChange}
value=""
/>,
/>
);
// Verify the text input was cleared

View File

@@ -1,9 +1,9 @@
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import { describe, it, expect } from "vitest";
import { JupyterEditor } from "#/components/features/jupyter/jupyter";
import { jupyterReducer } from "#/state/jupyter-slice";
import { vi, describe, it, expect } from "vitest";
describe("JupyterEditor", () => {
const mockStore = configureStore({
@@ -36,7 +36,7 @@ describe("JupyterEditor", () => {
<div style={{ height: "100vh" }}>
<JupyterEditor maxWidth={800} />
</div>
</Provider>,
</Provider>
);
const container = screen.getByTestId("jupyter-container");

View File

@@ -5,13 +5,7 @@ import translations from "../../src/i18n/translation.json";
import { UserAvatar } from "../../src/components/features/sidebar/user-avatar";
vi.mock("@heroui/react", () => ({
Tooltip: ({
content,
children,
}: {
content: string;
children: React.ReactNode;
}) => (
Tooltip: ({ content, children }: { content: string; children: React.ReactNode }) => (
<div>
{children}
<div>{content}</div>
@@ -19,33 +13,15 @@ vi.mock("@heroui/react", () => ({
),
}));
const supportedLanguages = [
"en",
"ja",
"zh-CN",
"zh-TW",
"ko-KR",
"de",
"no",
"it",
"pt",
"es",
"ar",
"fr",
"tr",
];
const supportedLanguages = ['en', 'ja', 'zh-CN', 'zh-TW', 'ko-KR', 'de', 'no', 'it', 'pt', 'es', 'ar', 'fr', 'tr'];
// Helper function to check if a translation exists for all supported languages
function checkTranslationExists(key: string) {
const missingTranslations: string[] = [];
const translationEntry = (
translations as Record<string, Record<string, string>>
)[key];
const translationEntry = (translations as Record<string, Record<string, string>>)[key];
if (!translationEntry) {
throw new Error(
`Translation key "${key}" does not exist in translation.json`,
);
throw new Error(`Translation key "${key}" does not exist in translation.json`);
}
for (const lang of supportedLanguages) {
@@ -77,9 +53,7 @@ function findDuplicateKeys(obj: Record<string, any>) {
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translationEntry = (
translations as Record<string, Record<string, string>>
)[key];
const translationEntry = (translations as Record<string, Record<string, string>>)[key];
return translationEntry?.ja || key;
},
}),
@@ -88,7 +62,7 @@ vi.mock("react-i18next", () => ({
describe("Landing page translations", () => {
test("should render Japanese translations correctly", () => {
// Mock a simple component that uses the translations
function TestComponent() {
const TestComponent = () => {
const { t } = useTranslation();
return (
<div>
@@ -121,16 +95,14 @@ describe("Landing page translations", () => {
</div>
</div>
);
}
};
render(<TestComponent />);
// Check main content translations
expect(screen.getByText("開発を始めましょう!")).toBeInTheDocument();
expect(screen.getByText("VS Codeで開く")).toBeInTheDocument();
expect(
screen.getByText("テストカバレッジを向上させる"),
).toBeInTheDocument();
expect(screen.getByText("テストカバレッジを向上させる")).toBeInTheDocument();
expect(screen.getByText("Dependabot PRを自動マージ")).toBeInTheDocument();
expect(screen.getByText("READMEを改善")).toBeInTheDocument();
expect(screen.getByText("依存関係を整理")).toBeInTheDocument();
@@ -148,12 +120,8 @@ describe("Landing page translations", () => {
expect(tabs).toHaveTextContent("コードエディタ");
// Check workspace label and new project button
expect(screen.getByTestId("workspace-label")).toHaveTextContent(
"ワークスペース",
);
expect(screen.getByTestId("new-project")).toHaveTextContent(
"新規プロジェクト",
);
expect(screen.getByTestId("workspace-label")).toHaveTextContent("ワークスペース");
expect(screen.getByTestId("new-project")).toHaveTextContent("新規プロジェクト");
// Check status messages
const status = screen.getByTestId("status");
@@ -191,12 +159,12 @@ describe("Landing page translations", () => {
"STATUS$CONNECTED_TO_SERVER",
"TIME$MINUTES_AGO",
"TIME$HOURS_AGO",
"TIME$DAYS_AGO",
"TIME$DAYS_AGO"
];
// Check all keys and collect missing translations
const missingTranslationsMap = new Map<string, string[]>();
translationKeys.forEach((key) => {
translationKeys.forEach(key => {
const missing = checkTranslationExists(key);
if (missing.length > 0) {
missingTranslationsMap.set(key, missing);
@@ -206,11 +174,8 @@ describe("Landing page translations", () => {
// If any translations are missing, throw an error with all missing translations
if (missingTranslationsMap.size > 0) {
const errorMessage = Array.from(missingTranslationsMap.entries())
.map(
([key, langs]) =>
`\n- "${key}" is missing translations for: ${langs.join(", ")}`,
)
.join("");
.map(([key, langs]) => `\n- "${key}" is missing translations for: ${langs.join(', ')}`)
.join('');
throw new Error(`Missing translations:${errorMessage}`);
}
});
@@ -219,9 +184,7 @@ describe("Landing page translations", () => {
const duplicates = findDuplicateKeys(translations);
if (duplicates.length > 0) {
throw new Error(
`Found duplicate translation keys: ${duplicates.join(", ")}`,
);
throw new Error(`Found duplicate translation keys: ${duplicates.join(', ')}`);
}
});
});

View File

@@ -1,15 +1,12 @@
import { describe, expect, it } from "vitest";
import fs from "fs";
import path from "path";
import { describe, expect, it } from 'vitest';
import fs from 'fs';
import path from 'path';
describe("translation.json", () => {
it("should not have duplicate translation keys", () => {
describe('translation.json', () => {
it('should not have duplicate translation keys', () => {
// Read the translation.json file
const translationPath = path.join(
__dirname,
"../../src/i18n/translation.json",
);
const translationContent = fs.readFileSync(translationPath, "utf-8");
const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
const translationContent = fs.readFileSync(translationPath, 'utf-8');
// First, let's check for exact string matches of key definitions
const keyRegex = /"([^"]+)": {/g;
@@ -33,7 +30,7 @@ describe("translation.json", () => {
if (uniqueDuplicates.length > 0) {
const errorMessage = `Found duplicate translation keys:\n${uniqueDuplicates
.map((key) => ` - "${key}" appears ${keyOccurrences.get(key)} times`)
.join("\n")}`;
.join('\n')}`;
throw new Error(errorMessage);
}
@@ -41,13 +38,10 @@ describe("translation.json", () => {
expect(uniqueDuplicates).toHaveLength(0);
});
it("should have consistent translations for each key", () => {
it('should have consistent translations for each key', () => {
// Read the translation.json file
const translationPath = path.join(
__dirname,
"../../src/i18n/translation.json",
);
const translationContent = fs.readFileSync(translationPath, "utf-8");
const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
const translationContent = fs.readFileSync(translationPath, 'utf-8');
const translations = JSON.parse(translationContent);
// Create a map to store English translations for each key
@@ -56,7 +50,7 @@ describe("translation.json", () => {
// Check each key's English translation
Object.entries(translations).forEach(([key, value]: [string, any]) => {
if (typeof value === "object" && value.en !== undefined) {
if (typeof value === 'object' && value.en !== undefined) {
const currentEn = value.en.toLowerCase();
const existingEn = englishTranslations.get(key)?.toLowerCase();
@@ -71,10 +65,8 @@ describe("translation.json", () => {
// If there are inconsistencies, create a helpful error message
if (inconsistentKeys.length > 0) {
const errorMessage = `Found inconsistent translations for keys:\n${inconsistentKeys
.map(
(key) => ` - "${key}" has multiple different English translations`,
)
.join("\n")}`;
.map((key) => ` - "${key}" has multiple different English translations`)
.join('\n')}`;
throw new Error(errorMessage);
}

View File

@@ -65,9 +65,7 @@ describe("Settings Screen", () => {
await waitFor(() => {
// Use queryAllByText to handle multiple elements with the same text
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(
0,
);
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(0);
screen.getByText("ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS");
screen.getByText("BUTTON$RESET_TO_DEFAULTS");
screen.getByText("BUTTON$SAVE");

View File

@@ -32,11 +32,9 @@ describe("Actions Service", () => {
handleStatusMessage(message);
expect(store.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
payload: message,
}),
);
expect(store.dispatch).toHaveBeenCalledWith(expect.objectContaining({
payload: message,
}));
});
it("should log error messages and display them in chat", () => {
@@ -55,11 +53,9 @@ describe("Actions Service", () => {
metadata: { msgId: "runtime.connection.failed" },
});
expect(store.dispatch).toHaveBeenCalledWith(
expect.objectContaining({
payload: message,
}),
);
expect(store.dispatch).toHaveBeenCalledWith(expect.objectContaining({
payload: message,
}));
});
});
@@ -76,27 +72,21 @@ describe("Actions Service", () => {
final_thought: "",
task_completed: "partial",
outputs: "",
thought: "",
},
thought: ""
}
};
// Mock implementation to capture the message
let capturedPartialMessage = "";
(store.dispatch as any).mockImplementation((action: any) => {
if (
action.type === "chat/addAssistantMessage" &&
action.payload.includes(
"believe that the task was **completed partially**",
)
) {
if (action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **completed partially**")) {
capturedPartialMessage = action.payload;
}
});
handleActionMessage(messagePartial);
expect(capturedPartialMessage).toContain(
"I believe that the task was **completed partially**",
);
expect(capturedPartialMessage).toContain("I believe that the task was **completed partially**");
// Test not completed
const messageNotCompleted: ActionMessage = {
@@ -109,25 +99,21 @@ describe("Actions Service", () => {
final_thought: "",
task_completed: "false",
outputs: "",
thought: "",
},
thought: ""
}
};
// Mock implementation to capture the message
let capturedNotCompletedMessage = "";
(store.dispatch as any).mockImplementation((action: any) => {
if (
action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **not completed**")
) {
if (action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **not completed**")) {
capturedNotCompletedMessage = action.payload;
}
});
handleActionMessage(messageNotCompleted);
expect(capturedNotCompletedMessage).toContain(
"I believe that the task was **not completed**",
);
expect(capturedNotCompletedMessage).toContain("I believe that the task was **not completed**");
// Test completed successfully
const messageCompleted: ActionMessage = {
@@ -140,27 +126,21 @@ describe("Actions Service", () => {
final_thought: "",
task_completed: "true",
outputs: "",
thought: "",
},
thought: ""
}
};
// Mock implementation to capture the message
let capturedCompletedMessage = "";
(store.dispatch as any).mockImplementation((action: any) => {
if (
action.type === "chat/addAssistantMessage" &&
action.payload.includes(
"believe that the task was **completed successfully**",
)
) {
if (action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **completed successfully**")) {
capturedCompletedMessage = action.payload;
}
});
handleActionMessage(messageCompleted);
expect(capturedCompletedMessage).toContain(
"I believe that the task was **completed successfully**",
);
expect(capturedCompletedMessage).toContain("I believe that the task was **completed successfully**");
});
});
});

View File

@@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { handleObservationMessage } from "#/services/observations";
import store from "#/store";
import { ObservationMessage } from "#/types/message";
// Mock dependencies
vi.mock("#/store", () => ({
default: {
dispatch: vi.fn(),
},
}));
describe("Observations Service", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("handleObservationMessage", () => {
const createErrorMessage = (): ObservationMessage => ({
id: 14,
timestamp: "2025-04-14T13:37:54.451843",
message: "The action has not been executed.",
cause: 12,
observation: "error",
content: "The action has not been executed.",
extras: {
error_id: "",
metadata: {},
},
});
it("should dispatch error messages exactly once", () => {
const errorMessage = createErrorMessage();
handleObservationMessage(errorMessage);
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith({
type: "chat/addAssistantObservation",
payload: expect.objectContaining({
observation: "error",
content: "The action has not been executed.",
source: "user",
extras: {
error_id: "",
},
}),
});
});
});
});

View File

@@ -5,16 +5,15 @@ const mockI18n = {
language: "ja",
t: (key: string) => {
const translations: Record<string, string> = {
SUGGESTIONS$TODO_APP: "ToDoリストアプリを開発する",
LANDING$BUILD_APP_BUTTON: "プルリクエストを表示するアプリを開発する",
SUGGESTIONS$HACKER_NEWS:
"Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
LANDING$TITLE: "一緒に開発を始めましょう!",
OPEN_IN_VSCODE: "VS Codeで開く",
INCREASE_TEST_COVERAGE: "テストカバレッジを向上",
AUTO_MERGE_PRS: "PRを自動マージ",
FIX_README: "READMEを修正",
CLEAN_DEPENDENCIES: "依存関係を整理",
"SUGGESTIONS$TODO_APP": "ToDoリストアプリを開発する",
"LANDING$BUILD_APP_BUTTON": "プルリクエストを表示するアプリを開発する",
"SUGGESTIONS$HACKER_NEWS": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
"LANDING$TITLE": "一緒に開発を始めましょう!",
"OPEN_IN_VSCODE": "VS Codeで開く",
"INCREASE_TEST_COVERAGE": "テストカバレッジを向上",
"AUTO_MERGE_PRS": "PRを自動マージ",
"FIX_README": "READMEを修正",
"CLEAN_DEPENDENCIES": "依存関係を整理"
};
return translations[key] || key;
},
@@ -24,5 +23,7 @@ const mockI18n = {
};
export function I18nTestProvider({ children }: { children: ReactNode }) {
return <I18nextProvider i18n={mockI18n as any}>{children}</I18nextProvider>;
return (
<I18nextProvider i18n={mockI18n as any}>{children}</I18nextProvider>
);
}

View File

@@ -29,7 +29,7 @@ export function ChatInput({
disabled,
showButton = true,
value,
maxRows = 4,
maxRows = 16,
onSubmit,
onStop,
onChange,

View File

@@ -1,7 +1,6 @@
import { NavLink } from "react-router";
import { cn } from "#/utils/utils";
import { BetaBadge } from "./beta-badge";
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
interface NavTabProps {
to: string;

View File

@@ -104,7 +104,12 @@ export function handleActionMessage(message: ActionMessage) {
}
if (message.source === "agent") {
if (message.args && message.args.thought) {
// Only add thought as a message if it's not a "think" action
if (
message.args &&
message.args.thought &&
message.action !== ActionType.THINK
) {
store.dispatch(addAssistantMessage(message.args.thought));
}
// Need to convert ActionMessage to RejectAction

View File

@@ -52,6 +52,7 @@ export function handleObservationMessage(message: ObservationMessage) {
case ObservationType.THINK:
case ObservationType.NULL:
case ObservationType.RECALL:
case ObservationType.ERROR:
break; // We don't display the default message for these observations
default:
store.dispatch(addAssistantMessage(message.message));

View File

@@ -24,6 +24,7 @@ const HANDLED_ACTIONS: OpenHandsEventType[] = [
"browse_interactive",
"edit",
"recall",
"think",
];
function getRiskText(risk: ActionSecurityRisk) {

View File

@@ -32,6 +32,9 @@ enum ObservationType {
// An observation that shows agent's context extension
RECALL = "recall",
// An error observation
ERROR = "error",
// A no-op observation
NULL = "null",
}

View File

@@ -1,3 +1,5 @@
import { downloadJSON } from "./download-json";
function isSaveFilePickerSupported(): boolean {
return typeof window !== "undefined" && "showSaveFilePicker" in window;
}
@@ -6,25 +8,37 @@ export async function downloadTrajectory(
conversationId: string,
data: unknown[] | null,
): Promise<void> {
if (!isSaveFilePickerSupported()) {
throw new Error(
"Your browser doesn't support downloading files. Please use Chrome, Edge, or another browser that supports the File System Access API.",
);
}
const options: SaveFilePickerOptions = {
suggestedName: `trajectory-${conversationId}.json`,
types: [
{
description: "JSON File", // This is a file type description, not user-facing text
accept: {
"application/json": [".json"],
},
},
],
};
// Ensure data is an object for downloadJSON
const jsonData = data || {};
const fileHandle = await window.showSaveFilePicker(options);
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
if (!isSaveFilePickerSupported()) {
// Fallback for browsers that don't support File System Access API (Safari, Firefox)
// This method dumps the JSON data right into the download folder
downloadJSON(jsonData as object, `trajectory-${conversationId}.json`);
return;
}
try {
const options: SaveFilePickerOptions = {
suggestedName: `trajectory-${conversationId}.json`,
types: [
{
description: "JSON File", // This is a file type description, not user-facing text
accept: {
"application/json": [".json"],
},
},
],
};
const fileHandle = await window.showSaveFilePicker(options);
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
} catch (error) {
// If an error occurs, fall back to the downloadJSON method
if (error instanceof Error && error.name !== "AbortError") {
downloadJSON(jsonData as object, `trajectory-${conversationId}.json`);
}
}
}

View File

@@ -52,7 +52,7 @@ export default defineConfig(({ mode }) => {
},
},
watch: {
ignored: ["**/node_modules/**", "**/.git/**"],
ignored: ['**/node_modules/**', '**/.git/**'],
},
},
ssr: {

View File

@@ -30,6 +30,6 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
git remote -v && git branch # to find the current org, repo and branch
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
curl -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/merge_requests" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-H "Authorization: Bearer $GITLAB_TOKEN" \
-d '{"source_branch": "create-widget", "target_branch": "openhands-workspace", "title": "Create widget"}'
```

View File

@@ -53,9 +53,9 @@ The agent provides several built-in tools:
## Configuration
Tools can be enabled/disabled through configuration parameters:
- `codeact_enable_browsing`: Enable browser interaction tools
- `codeact_enable_jupyter`: Enable IPython code execution
- `codeact_enable_llm_editor`: Enable LLM-based file editing (falls back to string replacement editor if disabled)
- `enable_browsing`: Enable browser interaction tools
- `enable_jupyter`: Enable IPython code execution
- `enable_llm_editor`: Enable LLM-based file editing (falls back to string replacement editor if disabled)
## Micro-agents

View File

@@ -69,9 +69,9 @@ class CodeActAgent(Agent):
self.reset()
built_in_tools = codeact_function_calling.get_tools(
codeact_enable_browsing=self.config.codeact_enable_browsing,
codeact_enable_jupyter=self.config.codeact_enable_jupyter,
codeact_enable_llm_editor=self.config.codeact_enable_llm_editor,
enable_browsing=self.config.enable_browsing,
enable_jupyter=self.config.enable_jupyter,
enable_llm_editor=self.config.enable_llm_editor,
llm=self.llm,
)

View File

@@ -240,9 +240,9 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
def get_tools(
codeact_enable_browsing: bool = False,
codeact_enable_llm_editor: bool = False,
codeact_enable_jupyter: bool = False,
enable_browsing: bool = False,
enable_llm_editor: bool = False,
enable_jupyter: bool = False,
llm: LLM | None = None,
) -> list[ChatCompletionToolParam]:
SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-', 'o3', 'o1']
@@ -259,12 +259,12 @@ def get_tools(
ThinkTool,
FinishTool,
]
if codeact_enable_browsing:
if enable_browsing:
tools.append(WebReadTool)
tools.append(BrowserTool)
if codeact_enable_jupyter:
if enable_jupyter:
tools.append(IPythonTool)
if codeact_enable_llm_editor:
if enable_llm_editor:
tools.append(LLMBasedFileEditTool)
else:
tools.append(

View File

@@ -1,13 +1,27 @@
import asyncio
import logging
import sys
import time
from pathlib import Path
from typing import List, Optional
from uuid import uuid4
import toml
from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.application import Application
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.formatted_text import HTML, FormattedText
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import HSplit, Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.shortcuts import clear, print_container
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Frame, TextArea
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
from openhands import __version__
from openhands.core.config import (
AppConfig,
parse_arguments,
@@ -37,89 +51,176 @@ from openhands.events.observation import (
AgentStateChangedObservation,
CmdOutputObservation,
FileEditObservation,
FileReadObservation,
)
from openhands.io import read_task
from openhands.llm.metrics import Metrics
from openhands.mcp import fetch_mcp_tools_from_config
from openhands.microagent.microagent import BaseMicroagent
prompt_session = PromptSession()
# Color and styling constants
COLOR_GOLD = '#FFD700'
COLOR_GREY = '#808080'
DEFAULT_STYLE = Style.from_dict(
{
'gold': COLOR_GOLD,
'grey': COLOR_GREY,
'prompt': f'{COLOR_GOLD} bold',
}
)
COMMANDS = {
'/exit': 'Exit the application',
'/help': 'Display available commands',
'/init': 'Initialize a new repository',
}
REPO_MD_CREATE_PROMPT = """
Please explore this repository. Create the file .openhands/microagents/repo.md with:
- A description of the project
- An overview of the file structure
- Any information on how to run tests or other relevant commands
- Any other information that would be helpful to a brand new developer
Keep it short--just a few paragraphs will do.
"""
def display_message(message: str) -> None:
class CommandCompleter(Completer):
"""Custom completer for commands."""
def get_completions(self, document, complete_event):
text = document.text
# Only show completions if the user has typed '/'
if text.startswith('/'):
# If just '/' is typed, show all commands
if text == '/':
for command, description in COMMANDS.items():
yield Completion(
command[1:], # Remove the leading '/' as it's already typed
start_position=0,
display=f'{command} - {description}',
)
# Otherwise show matching commands
else:
for command, description in COMMANDS.items():
if command.startswith(text):
yield Completion(
command[len(text) :], # Complete the remaining part
start_position=0,
display=f'{command} - {description}',
)
class UsageMetrics:
def __init__(self):
self.total_cost: float = 0.00
self.total_input_tokens: int = 0
self.total_output_tokens: int = 0
self.total_cache_read: int = 0
self.total_cache_write: int = 0
prompt_session = PromptSession(style=DEFAULT_STYLE, completer=CommandCompleter())
def display_message(message: str):
message = message.strip()
if message:
print_formatted_text(f'\n{message}\n')
def display_command(command: str):
container = Frame(
TextArea(
text=command,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Command Run',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_confirmation(confirmation_state: ActionConfirmationStatus):
status_map = {
ActionConfirmationStatus.CONFIRMED: ('ansigreen', ''),
ActionConfirmationStatus.REJECTED: ('ansired', ''),
ActionConfirmationStatus.AWAITING_CONFIRMATION: ('ansiyellow', ''),
}
color, icon = status_map.get(confirmation_state, ('ansiyellow', ''))
print_formatted_text(
FormattedText(
[
('ansiyellow', '🤖 '),
('ansiyellow', message),
(color, f'{icon} '),
(color, str(confirmation_state)),
('', '\n'),
]
)
)
def display_command(command: str) -> None:
print_formatted_text(
FormattedText(
[
('', ' '),
('ansigreen', command),
('', '\n'),
]
)
)
def display_confirmation(confirmation_state: ActionConfirmationStatus) -> None:
if confirmation_state == ActionConfirmationStatus.CONFIRMED:
print_formatted_text(
FormattedText(
[
('ansigreen', ''),
('ansigreen', str(confirmation_state)),
('', '\n'),
]
)
)
elif confirmation_state == ActionConfirmationStatus.REJECTED:
print_formatted_text(
FormattedText(
[
('ansired', ''),
('ansired', str(confirmation_state)),
('', '\n'),
]
)
)
else:
print_formatted_text(
FormattedText(
[
('ansiyellow', ''),
('ansiyellow', str(confirmation_state)),
('', '\n'),
]
)
)
def display_command_output(output: str) -> None:
def display_command_output(output: str):
lines = output.split('\n')
formatted_lines = []
for line in lines:
if line.startswith('[Python Interpreter') or line.startswith('openhands@'):
# TODO: clean this up once we clean up terminal output
continue
print_formatted_text(FormattedText([('ansiblue', line)]))
formatted_lines.append(line)
formatted_lines.append('\n')
# Remove the last newline if it exists
if formatted_lines:
formatted_lines.pop()
container = Frame(
TextArea(
text=''.join(formatted_lines),
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Command Output',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_file_edit(event: FileEditAction | FileEditObservation) -> None:
print_formatted_text(
FormattedText(
[
('ansigreen', str(event)),
('', '\n'),
]
)
def display_file_edit(event: FileEditAction | FileEditObservation):
container = Frame(
TextArea(
text=f'{event}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='File Edit',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_file_read(event: FileReadObservation):
container = Frame(
TextArea(
text=f'{event}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='File Read',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_event(event: Event, config: AppConfig) -> None:
@@ -137,10 +238,115 @@ def display_event(event: Event, config: AppConfig) -> None:
display_file_edit(event)
if isinstance(event, FileEditObservation):
display_file_edit(event)
if isinstance(event, FileReadObservation):
display_file_read(event)
if hasattr(event, 'confirmation_state') and config.security.confirmation_mode:
display_confirmation(event.confirmation_state)
def display_help(style=DEFAULT_STYLE):
print_formatted_text(
HTML(f'\n<grey>OpenHands CLI v{__version__}</grey>\n'), style=style
)
print_formatted_text(
HTML(
'<gold>OpenHands CLI lets you interact with the OpenHands agent from the command line.</gold>'
)
)
print_formatted_text('')
print_formatted_text('Things that you can try:')
print_formatted_text(
HTML('• Ask questions about the codebase <grey>> How does main.py work?</grey>')
)
print_formatted_text(
HTML(
'• Edit files or add new features <grey>> Add a new function to ...</grey>'
)
)
print_formatted_text(
HTML('• Find and fix issues <grey>> Fix the type error in ...</grey>')
)
print_formatted_text('')
print_formatted_text('Some tips to get the most out of OpenHands:')
print_formatted_text(
'• Be as specific as possible about the desired outcome or the problem to be solved.'
)
print_formatted_text(
'• Provide context, including relevant file paths and line numbers if available.'
)
print_formatted_text('• Break large tasks into smaller, manageable prompts.')
print_formatted_text('• Include relevant error messages or logs.')
print_formatted_text(
'• Specify the programming language or framework, if not obvious.'
)
print_formatted_text('')
print_formatted_text(HTML('Interactive commands:'), style=style)
for command, description in COMMANDS.items():
print_formatted_text(
HTML(f'<gold><b>{command}</b></gold> - <grey>{description}</grey>'),
style=style,
)
print_formatted_text('')
print_formatted_text(
HTML(
'<grey>Learn more at: https://docs.all-hands.dev/modules/usage/getting-started</grey>'
)
)
print_formatted_text('')
def display_banner(session_id: str, is_loaded: asyncio.Event):
print_formatted_text(
HTML(r"""<gold>
___ _ _ _
/ _ \ _ __ ___ _ __ | | | | __ _ _ __ __| |___
| | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
| |_| | |_) | __/ | | | _ | (_| | | | | (_| \__ \
\___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
|_|
</gold>"""),
style=DEFAULT_STYLE,
)
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
banner_text = (
'Initialized session' if is_loaded.is_set() else 'Initializing session'
)
print_formatted_text(HTML(f'\n<grey>{banner_text} {session_id}</grey>\n'))
def display_welcome_message():
print_formatted_text(
HTML("<gold>Let's start building!</gold>\n"), style=DEFAULT_STYLE
)
print_formatted_text(
HTML('What do you want to build? <grey>Type /help for help</grey>\n'),
style=DEFAULT_STYLE,
)
def display_initialization_animation(text, is_loaded: asyncio.Event):
ANIMATION_FRAMES = ['', '', '', '', '', '', '', '', '', '']
i = 0
while not is_loaded.is_set():
sys.stdout.write('\n')
sys.stdout.write(
f'\033[s\033[J\033[38;2;255;215;0m[{ANIMATION_FRAMES[i % len(ANIMATION_FRAMES)]}] {text}\033[0m\033[u\033[1A'
)
sys.stdout.flush()
time.sleep(0.1)
i += 1
sys.stdout.write('\r' + ' ' * (len(text) + 10) + '\r')
sys.stdout.flush()
async def read_prompt_input(multiline=False):
try:
if multiline:
@@ -150,35 +356,292 @@ async def read_prompt_input(multiline=False):
def _(event):
event.current_buffer.validate_and_handle()
message = await prompt_session.prompt_async(
'Enter your message and press Ctrl+D to finish:\n',
multiline=True,
key_bindings=kb,
)
with patch_stdout():
message = await prompt_session.prompt_async(
'Enter your message and press Ctrl+D to finish:\n',
multiline=True,
key_bindings=kb,
)
else:
message = await prompt_session.prompt_async(
'>> ',
)
with patch_stdout():
message = await prompt_session.prompt_async(
'> ',
)
return message
except KeyboardInterrupt:
return 'exit'
return '/exit'
except EOFError:
return 'exit'
return '/exit'
async def read_confirmation_input():
try:
confirmation = await prompt_session.prompt_async(
'Confirm action (possible security risk)? (y/n) >> ',
'Confirm action (possible security risk)? (y/n) > ',
)
return confirmation.lower() == 'y'
except (KeyboardInterrupt, EOFError):
return False
async def main(loop: asyncio.AbstractEventLoop) -> None:
async def init_repository(current_dir: str) -> bool:
repo_file_path = Path(current_dir) / '.openhands' / 'microagents' / 'repo.md'
init_repo = False
if repo_file_path.exists():
try:
content = await asyncio.get_event_loop().run_in_executor(
None, read_file, repo_file_path
)
print_formatted_text(
'Repository instructions file (repo.md) already exists.\n'
)
container = Frame(
TextArea(
text=content,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Repository Instructions (repo.md)',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('') # Add a newline after the frame
init_repo = cli_confirm(
'Do you want to re-initialize?',
['Yes, re-initialize', 'No, dismiss'],
)
if init_repo:
write_to_file(repo_file_path, '')
except Exception:
print_formatted_text('Error reading repository instructions file (repo.md)')
init_repo = False
else:
print_formatted_text(
'\nRepository instructions file will be created by exploring the repository.\n'
)
init_repo = cli_confirm(
'Do you want to proceed?',
['Yes, create', 'No, dismiss'],
)
return init_repo
def read_file(file_path):
with open(file_path, 'r') as f:
return f.read()
def write_to_file(file_path, content):
with open(file_path, 'w') as f:
f.write(content)
def cli_confirm(question: str = 'Are you sure?', choices: Optional[List[str]] = None):
if choices is None:
choices = ['Yes', 'No']
selected = [0] # Using list to allow modification in closure
def get_choice_text():
return [
('class:question', f'{question}\n\n'),
] + [
(
'class:selected' if i == selected[0] else 'class:unselected',
f"{'> ' if i == selected[0] else ' '}{choice}\n",
)
for i, choice in enumerate(choices)
]
kb = KeyBindings()
@kb.add('up')
def _(event):
selected[0] = (selected[0] - 1) % len(choices)
@kb.add('down')
def _(event):
selected[0] = (selected[0] + 1) % len(choices)
@kb.add('enter')
def _(event):
event.app.exit(result=selected[0] == 0)
style = Style.from_dict({'selected': COLOR_GOLD, 'unselected': ''})
layout = Layout(
HSplit(
[
Window(
FormattedTextControl(get_choice_text),
always_hide_cursor=True,
)
]
)
)
app = Application(
layout=layout,
key_bindings=kb,
style=style,
mouse_support=True,
full_screen=False,
)
return app.run(in_thread=True)
def update_usage_metrics(event: Event, usage_metrics: UsageMetrics):
"""Updates the UsageMetrics object with data from an event's llm_metrics."""
if hasattr(event, 'llm_metrics'):
llm_metrics: Metrics | None = getattr(event, 'llm_metrics', None)
if llm_metrics:
# Safely get accumulated_cost
cost = getattr(llm_metrics, 'accumulated_cost', 0)
# Ensure cost is a number before adding
usage_metrics.total_cost += cost if isinstance(cost, float) else 0
# Safely get token usage details object/dict
token_usage = getattr(llm_metrics, 'accumulated_token_usage', None)
if token_usage:
# Assume object access using getattr, providing defaults
prompt_tokens = getattr(token_usage, 'prompt_tokens', 0)
completion_tokens = getattr(token_usage, 'completion_tokens', 0)
cache_read = getattr(token_usage, 'cache_read_tokens', 0)
cache_write = getattr(token_usage, 'cache_write_tokens', 0)
# Ensure tokens are numbers before adding
usage_metrics.total_input_tokens += (
prompt_tokens if isinstance(prompt_tokens, int) else 0
)
usage_metrics.total_output_tokens += (
completion_tokens if isinstance(completion_tokens, int) else 0
)
usage_metrics.total_cache_read += (
cache_read if isinstance(cache_read, int) else 0
)
usage_metrics.total_cache_write += (
cache_write if isinstance(cache_write, int) else 0
)
def shutdown(usage_metrics: UsageMetrics, session_id: str):
cost_str = f'${usage_metrics.total_cost:.6f}'
input_tokens_str = f'{usage_metrics.total_input_tokens:,}'
cache_read_str = f'{usage_metrics.total_cache_read:,}'
cache_write_str = f'{usage_metrics.total_cache_write:,}'
output_tokens_str = f'{usage_metrics.total_output_tokens:,}'
total_tokens_str = (
f'{usage_metrics.total_input_tokens + usage_metrics.total_output_tokens:,}'
)
labels_and_values = [
(' Total Cost (USD):', cost_str),
(' Total Input Tokens:', input_tokens_str),
(' Cache Hits:', cache_read_str),
(' Cache Writes:', cache_write_str),
(' Total Output Tokens:', output_tokens_str),
(' Total Tokens:', total_tokens_str),
]
# Calculate max widths for alignment
max_label_width = max(len(label) for label, _ in labels_and_values)
max_value_width = max(len(value) for _, value in labels_and_values)
# Construct the summary text with aligned columns
summary_lines = [
f'{label:<{max_label_width}} {value:>{max_value_width}}'
for label, value in labels_and_values
]
summary_text = '\n'.join(summary_lines)
container = Frame(
TextArea(
text=summary_text,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Session Summary',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text(HTML(f'\n<grey>Closed session {session_id}</grey>\n'))
def manage_openhands_file(folder_path=None, add_to_trusted=False):
openhands_file = Path.home() / '.openhands.toml'
default_content: dict = {'trusted_dirs': []}
if not openhands_file.exists():
with open(openhands_file, 'w') as f:
toml.dump(default_content, f)
if folder_path:
with open(openhands_file, 'r') as f:
try:
config = toml.load(f)
except Exception:
config = default_content
if 'trusted_dirs' not in config:
config['trusted_dirs'] = []
if folder_path in config['trusted_dirs']:
return True
if add_to_trusted:
config['trusted_dirs'].append(folder_path)
with open(openhands_file, 'w') as f:
toml.dump(config, f)
return False
return False
def check_folder_security_agreement(current_dir):
is_trusted = manage_openhands_file(current_dir)
if not is_trusted:
security_frame = Frame(
TextArea(
text=(
f'Do you trust the files in this folder?\n\n'
f'{current_dir}\n\n'
'OpenHands may read and execute files in this folder with your permission.'
),
style='#ffffff',
read_only=True,
wrap_lines=True,
)
)
clear()
print_container(security_frame)
confirm = cli_confirm('Do you wish to continue?', ['Yes, proceed', 'No, exit'])
if confirm:
manage_openhands_file(current_dir, add_to_trusted=True)
return confirm
return True
async def main(loop: asyncio.AbstractEventLoop):
"""Runs the agent in CLI mode."""
reload_microagents = False
args = parse_arguments()
logger.setLevel(logging.WARNING)
@@ -186,6 +649,12 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
# Load config from toml and override with command line arguments
config: AppConfig = setup_config_from_args(args)
# TODO: Set working directory from config or use current working directory?
current_dir = config.workspace_base
if not current_dir:
raise ValueError('Workspace base directory not specified')
# Read task from file, CLI args, or stdin
task_str = read_task(args, config.cli_multiline_input)
@@ -193,7 +662,15 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
initial_user_action = MessageAction(content=task_str) if task_str else None
sid = str(uuid4())
display_message(f'Session ID: {sid}')
is_loaded = asyncio.Event()
# Show OpenHands banner and session ID
display_banner(session_id=sid, is_loaded=is_loaded)
# Show Initialization loader
loop.run_in_executor(
None, display_initialization_animation, 'Initializing...', is_loaded
)
agent = create_agent(config)
mcp_tools = await fetch_mcp_tools_from_config(config.mcp)
@@ -209,26 +686,64 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
event_stream = runtime.event_stream
async def prompt_for_next_task() -> None:
next_message = await read_prompt_input(config.cli_multiline_input)
if not next_message.strip():
await prompt_for_next_task()
if next_message == 'exit':
event_stream.add_event(
ChangeAgentStateAction(AgentState.STOPPED), EventSource.ENVIRONMENT
)
usage_metrics = UsageMetrics()
async def prompt_for_next_task():
nonlocal reload_microagents
while True:
next_message = await read_prompt_input(config.cli_multiline_input)
if not next_message.strip():
continue
if next_message == '/exit':
event_stream.add_event(
ChangeAgentStateAction(AgentState.STOPPED), EventSource.ENVIRONMENT
)
shutdown(usage_metrics, sid)
return
elif next_message == '/help':
display_help()
continue
elif next_message == '/init':
if config.runtime == 'local':
init_repo = await init_repository(current_dir)
if init_repo:
event_stream.add_event(
MessageAction(content=REPO_MD_CREATE_PROMPT),
EventSource.USER,
)
reload_microagents = True
return
else:
print_formatted_text(
'\nRepository initialization through the CLI is only supported for local runtime.\n'
)
continue
action = MessageAction(content=next_message)
event_stream.add_event(action, EventSource.USER)
return
action = MessageAction(content=next_message)
event_stream.add_event(action, EventSource.USER)
async def on_event_async(event: Event) -> None:
nonlocal reload_microagents
display_event(event, config)
update_usage_metrics(event, usage_metrics)
if isinstance(event, AgentStateChangedObservation):
if event.agent_state in [
AgentState.AWAITING_USER_INPUT,
AgentState.FINISHED,
]:
# Reload microagents after initialization of repo.md
if reload_microagents:
microagents: list[BaseMicroagent] = (
runtime.get_microagents_from_selected_repo(None)
)
memory.load_user_workspace_microagents(microagents)
reload_microagents = False
await prompt_for_next_task()
if event.agent_state == AgentState.AWAITING_USER_CONFIRMATION:
user_confirmed = await read_confirmation_input()
if user_confirmed:
@@ -266,6 +781,25 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
repo_directory=repo_directory,
)
# Clear loading animation
is_loaded.set()
if not check_folder_security_agreement(current_dir):
# User rejected, exit application
event_stream.add_event(
ChangeAgentStateAction(AgentState.STOPPED), EventSource.ENVIRONMENT
)
return
# Clear the terminal
clear()
# Show OpenHands banner and session ID
display_banner(session_id=sid, is_loaded=is_loaded)
# Show OpenHands welcome
display_welcome_message()
if initial_user_action:
# If there's an initial user action, enqueue it and do not prompt again
event_stream.add_event(initial_user_action, EventSource.USER)

View File

@@ -10,10 +10,9 @@ class AgentConfig(BaseModel):
"""Configuration for the agent.
Attributes:
function_calling: Whether function calling is enabled. Default is True.
codeact_enable_browsing: Whether browsing delegate is enabled in the action space. Default is False. Only works with function calling.
codeact_enable_llm_editor: Whether LLM editor is enabled in the action space. Default is False. Only works with function calling.
codeact_enable_jupyter: Whether Jupyter is enabled in the action space. Default is False.
enable_browsing: Whether browsing delegate is enabled in the action space. Default is False. Only works with function calling.
enable_llm_editor: Whether LLM editor is enabled in the action space. Default is False. Only works with function calling.
enable_jupyter: Whether Jupyter is enabled in the action space. Default is False.
llm_config: The name of the llm config to use. If specified, this will override global llm config.
enable_prompt_extensions: Whether to use prompt extensions (e.g., microagents, inject runtime info). Default is True.
disabled_microagents: A list of microagents to disable (by name, without .py extension, e.g. ["github", "lint"]). Default is None.
@@ -23,9 +22,9 @@ class AgentConfig(BaseModel):
"""
llm_config: str | None = Field(default=None)
codeact_enable_browsing: bool = Field(default=True)
codeact_enable_llm_editor: bool = Field(default=False)
codeact_enable_jupyter: bool = Field(default=True)
enable_browsing: bool = Field(default=True)
enable_llm_editor: bool = Field(default=False)
enable_jupyter: bool = Field(default=True)
enable_prompt_extensions: bool = Field(default=True)
disabled_microagents: list[str] = Field(default_factory=list)
enable_history_truncation: bool = Field(default=True)

View File

@@ -160,30 +160,38 @@ class EventStream(EventStore):
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.'
)
event._timestamp = datetime.now().isoformat()
event._source = source # type: ignore [attr-defined]
with self._lock:
event._id = self.cur_id # type: ignore [attr-defined]
self.cur_id += 1
logger.debug(f'Adding {type(event).__name__} id={event.id} from {source.name}')
event._timestamp = datetime.now().isoformat()
event._source = source # type: ignore [attr-defined]
logger.debug(f'Event to add: {event}')
data = event_to_dict(event)
data = self._replace_secrets(data)
event = event_from_dict(data)
# Take a copy of the current write page
current_write_page = self._write_page_cache
data = event_to_dict(event)
data = self._replace_secrets(data)
event = event_from_dict(data)
current_write_page.append(data)
# If the page is full, create a new page for future events / other threads to use
if len(current_write_page) == self.cache_size:
self._write_page_cache = []
if event.id is not None:
# Write the event to the store - this can take some time
self.file_store.write(
self._get_filename_for_id(event.id, self.user_id), json.dumps(data)
)
self._write_page_cache.append(data)
self._store_cache_page()
# Store the cache page last - if it is not present during reads then it will simply be bypassed.
self._store_cache_page(current_write_page)
self._queue.put(event)
def _store_cache_page(self):
def _store_cache_page(self, current_write_page: list[dict]):
"""Store a page in the cache. Reading individual events is slow when there are a lot of them, so we use pages."""
current_write_page = self._write_page_cache
if len(current_write_page) < self.cache_size:
return
self._write_page_cache = []
start = current_write_page[0]['id']
end = start + self.cache_size
contents = json.dumps(current_write_page)

View File

@@ -114,8 +114,14 @@ class LLMAttentionCondenser(RollingCondenser):
@classmethod
def from_config(cls, config: LLMAttentionCondenserConfig) -> LLMAttentionCondenser:
# This condenser cannot take advantage of prompt caching. If it happens
# to be set, we'll pay for the cache writes but never get a chance to
# save on a read.
llm_config = config.llm_config.model_copy()
llm_config.caching_prompt = False
return LLMAttentionCondenser(
llm=LLM(config=config.llm_config),
llm=LLM(config=llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
)

View File

@@ -155,8 +155,14 @@ CURRENT_STATE: Last flip: Heads, Haiku count: 15/20"""
def from_config(
cls, config: LLMSummarizingCondenserConfig
) -> LLMSummarizingCondenser:
# This condenser cannot take advantage of prompt caching. If it happens
# to be set, we'll pay for the cache writes but never get a chance to
# save on a read.
llm_config = config.llm_config.model_copy()
llm_config.caching_prompt = False
return LLMSummarizingCondenser(
llm=LLM(config=config.llm_config),
llm=LLM(config=llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
max_event_length=config.max_event_length,

View File

@@ -311,8 +311,14 @@ Capture all relevant information, especially:
def from_config(
cls, config: StructuredSummaryCondenserConfig
) -> StructuredSummaryCondenser:
# This condenser cannot take advantage of prompt caching. If it happens
# to be set, we'll pay for the cache writes but never get a chance to
# save on a read.
llm_config = config.llm_config.model_copy()
llm_config.caching_prompt = False
return StructuredSummaryCondenser(
llm=LLM(config=config.llm_config),
llm=LLM(config=llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
max_event_length=config.max_event_length,

View File

@@ -319,6 +319,12 @@ def send_pull_request(
pr_body += f'\n\n{additional_message}'
pr_body += '\n\nAutomatic fix generated by [OpenHands](https://github.com/All-Hands-AI/OpenHands/) 🙌'
# For cross repo pull request, we need to send head parameter like fork_owner:branch as per git documentation here : https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#create-a-pull-request
# head parameter usage : The name of the branch where your changes are implemented. For cross-repository pull requests in the same network, namespace head with a user like this: username:branch.
if fork_owner and platform == Platform.GITHUB:
head_branch = f'{fork_owner}:{branch_name}'
else:
head_branch = branch_name
# If we are not sending a PR, we can finish early and return the
# URL for the user to open a PR manually
if pr_type == 'branch':
@@ -328,7 +334,7 @@ def send_pull_request(
data = {
'title': final_pr_title,
('body' if platform == Platform.GITHUB else 'description'): pr_body,
('head' if platform == Platform.GITHUB else 'source_branch'): branch_name,
('head' if platform == Platform.GITHUB else 'source_branch'): head_branch,
('base' if platform == Platform.GITHUB else 'target_branch'): base_branch,
'draft': pr_type == 'draft',
}

View File

@@ -18,6 +18,7 @@ from contextlib import asynccontextmanager
from pathlib import Path
from zipfile import ZipFile
from binaryornot.check import is_binary
from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile
from fastapi.exceptions import RequestValidationError
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
@@ -355,6 +356,11 @@ class ActionExecutor:
async def read(self, action: FileReadAction) -> Observation:
assert self.bash_session is not None
# Cannot read binary files
if is_binary(action.path):
return ErrorObservation('ERROR_BINARY_FILE')
if action.impl_source == FileReadSource.OH_ACI:
result_str, _ = _execute_file_editor(
self.file_editor,

View File

@@ -14,7 +14,6 @@ from typing import Callable, cast
from zipfile import ZipFile
import httpx
from pydantic import SecretStr
from openhands.core.config import AppConfig, SandboxConfig
from openhands.core.exceptions import AgentRuntimeDisconnectedError
@@ -151,15 +150,12 @@ class Runtime(FileEditRuntimeMixin):
# Load mixins
FileEditRuntimeMixin.__init__(
self, enable_llm_editor=config.get_agent_config().codeact_enable_llm_editor
self, enable_llm_editor=config.get_agent_config().enable_llm_editor
)
self.user_id = user_id
self.git_provider_tokens = git_provider_tokens
# TODO: remove once done debugging expired github token
self.prev_token: SecretStr | None = None
def setup_initial_env(self) -> None:
if self.attach_to_existing:
return
@@ -257,31 +253,14 @@ class Runtime(FileEditRuntimeMixin):
if not providers_called:
return
logger.info(f'Fetching latest github token for runtime: {self.sid}')
logger.info(f'Fetching latest provider tokens for runtime: {self.sid}')
env_vars = await self.provider_handler.get_env_vars(
providers=providers_called, expose_secrets=False, get_latest=True
)
# This statement is to debug expired github tokens, and will be removed later
if ProviderType.GITHUB not in env_vars:
logger.error(f'Failed to refresh github token for runtime: {self.sid}')
return
if len(env_vars) == 0:
return
raw_token = env_vars[ProviderType.GITHUB].get_secret_value()
if not self.prev_token:
logger.info(
f'Setting github token in runtime: {self.sid}\nToken value: {raw_token[0:5]}; length: {len(raw_token)}'
)
elif self.prev_token.get_secret_value() != raw_token:
logger.info(
f'Setting new github token in runtime {self.sid}\nToken value: {raw_token[0:5]}; length: {len(raw_token)}'
)
self.prev_token = SecretStr(raw_token)
try:
await self.provider_handler.set_event_stream_secrets(
self.event_stream, env_vars=env_vars

View File

@@ -39,13 +39,14 @@ from openhands.events.observation import (
from openhands.events.serialization import event_to_dict, observation_from_dict
from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
from openhands.mcp import call_tool_mcp as call_tool_mcp_handler, create_mcp_clients, MCPClient
from openhands.mcp import MCPClient, create_mcp_clients
from openhands.mcp import call_tool_mcp as call_tool_mcp_handler
from openhands.runtime.base import Runtime
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.utils.request import send_request
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.http_session import HttpSession
from openhands.utils.tenacity_stop import stop_if_should_exit
from openhands.utils.async_utils import call_async_from_sync
def _is_retryable_error(exception):
@@ -325,10 +326,11 @@ class ActionExecutionClient(Runtime):
async def call_tool_mcp(self, action: McpAction) -> Observation:
if self.mcp_clients is None:
self.log('debug', f'Creating MCP clients with servers: {self.config.mcp.sse.mcp_servers}')
self.mcp_clients = await create_mcp_clients(
self.config.mcp.sse.mcp_servers
self.log(
'debug',
f'Creating MCP clients with servers: {self.config.mcp.sse.mcp_servers}',
)
self.mcp_clients = await create_mcp_clients(self.config.mcp.sse.mcp_servers)
return await call_tool_mcp_handler(self.mcp_clients, action)
async def aclose(self) -> None:

View File

@@ -44,7 +44,13 @@ def _is_retryable_wait_until_alive_error(exception):
return _is_retryable_wait_until_alive_error(cause)
return isinstance(
exception, (ConnectionError, httpx.NetworkError, httpx.RemoteProtocolError)
exception,
(
ConnectionError,
httpx.NetworkError,
httpx.RemoteProtocolError,
httpx.HTTPStatusError,
),
)

View File

@@ -1,5 +1,7 @@
from dataclasses import dataclass
from openhands.events.action import Action
from openhands.events.observation import Observation
from openhands.runtime.plugins.agent_skills import agentskills
from openhands.runtime.plugins.requirement import Plugin, PluginRequirement
@@ -12,3 +14,11 @@ class AgentSkillsRequirement(PluginRequirement):
class AgentSkillsPlugin(Plugin):
name: str = 'agent_skills'
async def initialize(self, username: str) -> None:
"""Initialize the plugin."""
pass
async def run(self, action: Action) -> Observation:
"""Run the plugin for a given action."""
raise NotImplementedError('AgentSkillsPlugin does not support run method')

View File

@@ -1,6 +1,5 @@
import asyncio
import os
import subprocess
import time
from dataclasses import dataclass
from openhands.core.logger import openhands_logger as logger
@@ -19,8 +18,14 @@ class JupyterRequirement(PluginRequirement):
class JupyterPlugin(Plugin):
name: str = 'jupyter'
kernel_gateway_port: int
kernel_id: str
gateway_process: asyncio.subprocess.Process
python_interpreter_path: str
async def initialize(self, username: str, kernel_id: str = 'openhands-default'):
async def initialize(
self, username: str, kernel_id: str = 'openhands-default'
) -> None:
self.kernel_gateway_port = find_available_tcp_port(40000, 49999)
self.kernel_id = kernel_id
if username in ['root', 'openhands']:
@@ -61,19 +66,22 @@ class JupyterPlugin(Plugin):
)
logger.debug(f'Jupyter launch command: {jupyter_launch_command}')
self.gateway_process = subprocess.Popen(
# Using asyncio.create_subprocess_shell instead of subprocess.Popen
# to avoid ASYNC101 linting error
self.gateway_process = await asyncio.create_subprocess_shell(
jupyter_launch_command,
stderr=subprocess.STDOUT,
shell=True,
stderr=asyncio.subprocess.STDOUT,
stdout=asyncio.subprocess.PIPE,
)
# read stdout until the kernel gateway is ready
output = ''
while should_continue() and self.gateway_process.stdout is not None:
line = self.gateway_process.stdout.readline().decode('utf-8')
line_bytes = await self.gateway_process.stdout.readline()
line = line_bytes.decode('utf-8')
output += line
if 'at' in line:
break
time.sleep(1)
await asyncio.sleep(1)
logger.debug('Waiting for jupyter kernel gateway to start...')
logger.debug(

View File

@@ -50,22 +50,22 @@ def strip_ansi(o: str) -> str:
class JupyterKernel:
def __init__(self, url_suffix, convid, lang='python'):
def __init__(self, url_suffix: str, convid: str, lang: str = 'python') -> None:
self.base_url = f'http://{url_suffix}'
self.base_ws_url = f'ws://{url_suffix}'
self.lang = lang
self.kernel_id = None
self.ws = None
self.kernel_id: str | None = None
self.ws: tornado.websocket.WebSocketClientConnection | None = None
self.convid = convid
logging.info(
f'Jupyter kernel created for conversation {convid} at {url_suffix}'
)
self.heartbeat_interval = 10000 # 10 seconds
self.heartbeat_callback = None
self.heartbeat_callback: PeriodicCallback | None = None
self.initialized = False
async def initialize(self):
async def initialize(self) -> None:
await self.execute(r'%colors nocolor')
# pre-defined tools
self.tools_to_run: list[str] = [
@@ -76,7 +76,7 @@ class JupyterKernel:
logging.info(f'Tool [{tool}] initialized:\n{res}')
self.initialized = True
async def _send_heartbeat(self):
async def _send_heartbeat(self) -> None:
if not self.ws:
return
try:
@@ -91,7 +91,7 @@ class JupyterKernel:
'ConnectionRefusedError: Failed to reconnect to kernel websocket - Is the kernel still running?'
)
async def _connect(self):
async def _connect(self) -> None:
if self.ws:
self.ws.close()
self.ws = None
@@ -138,7 +138,7 @@ class JupyterKernel:
stop=stop_after_attempt(3),
wait=wait_fixed(2),
)
async def execute(self, code, timeout=120):
async def execute(self, code: str, timeout: int = 120) -> str:
if not self.ws:
await self._connect()
@@ -170,45 +170,49 @@ class JupyterKernel:
)
logging.info(f'Executed code in jupyter kernel:\n{res}')
outputs = []
outputs: list[str] = []
async def wait_for_messages():
async def wait_for_messages() -> bool:
execution_done = False
while not execution_done:
assert self.ws is not None
msg = await self.ws.read_message()
msg = json_decode(msg)
msg_type = msg['msg_type']
parent_msg_id = msg['parent_header'].get('msg_id', None)
if msg is None:
continue
msg_dict = json_decode(msg)
msg_type = msg_dict['msg_type']
parent_msg_id = msg_dict['parent_header'].get('msg_id', None)
if parent_msg_id != msg_id:
continue
if os.environ.get('DEBUG'):
logging.info(
f"MSG TYPE: {msg_type.upper()} DONE:{execution_done}\nCONTENT: {msg['content']}"
f"MSG TYPE: {msg_type.upper()} DONE:{execution_done}\nCONTENT: {msg_dict['content']}"
)
if msg_type == 'error':
traceback = '\n'.join(msg['content']['traceback'])
traceback = '\n'.join(msg_dict['content']['traceback'])
outputs.append(traceback)
execution_done = True
elif msg_type == 'stream':
outputs.append(msg['content']['text'])
outputs.append(msg_dict['content']['text'])
elif msg_type in ['execute_result', 'display_data']:
outputs.append(msg['content']['data']['text/plain'])
if 'image/png' in msg['content']['data']:
outputs.append(msg_dict['content']['data']['text/plain'])
if 'image/png' in msg_dict['content']['data']:
# use markdone to display image (in case of large image)
outputs.append(
f"\n![image](data:image/png;base64,{msg['content']['data']['image/png']})\n"
f"\n![image](data:image/png;base64,{msg_dict['content']['data']['image/png']})\n"
)
elif msg_type == 'execute_reply':
execution_done = True
return execution_done
async def interrupt_kernel():
async def interrupt_kernel() -> None:
client = AsyncHTTPClient()
if self.kernel_id is None:
return
interrupt_response = await client.fetch(
f'{self.base_url}/api/kernels/{self.kernel_id}/interrupt',
method='POST',
@@ -234,7 +238,7 @@ class JupyterKernel:
logging.info(f'OUTPUT:\n{ret}')
return ret
async def shutdown_async(self):
async def shutdown_async(self) -> None:
if self.kernel_id:
client = AsyncHTTPClient()
await client.fetch(
@@ -248,10 +252,10 @@ class JupyterKernel:
class ExecuteHandler(tornado.web.RequestHandler):
def initialize(self, jupyter_kernel):
def initialize(self, jupyter_kernel: JupyterKernel) -> None:
self.jupyter_kernel = jupyter_kernel
async def post(self):
async def post(self) -> None:
data = json_decode(self.request.body)
code = data.get('code')
@@ -265,10 +269,10 @@ class ExecuteHandler(tornado.web.RequestHandler):
self.write(output)
def make_app():
def make_app() -> tornado.web.Application:
jupyter_kernel = JupyterKernel(
f"localhost:{os.environ.get('JUPYTER_GATEWAY_PORT')}",
os.environ.get('JUPYTER_GATEWAY_KERNEL_ID'),
f"localhost:{os.environ.get('JUPYTER_GATEWAY_PORT', '8888')}",
os.environ.get('JUPYTER_GATEWAY_KERNEL_ID', 'default'),
)
asyncio.get_event_loop().run_until_complete(jupyter_kernel.initialize())

View File

@@ -14,7 +14,7 @@ class Plugin:
name: str
@abstractmethod
async def initialize(self, username: str):
async def initialize(self, username: str) -> None:
"""Initialize the plugin."""
pass

View File

@@ -1,10 +1,12 @@
import asyncio
import os
import subprocess
import time
import uuid
from dataclasses import dataclass
from typing import Optional
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import Action
from openhands.events.observation import Observation
from openhands.runtime.plugins.requirement import Plugin, PluginRequirement
from openhands.runtime.utils.system import check_port_available
from openhands.utils.shutdown_listener import should_continue
@@ -17,10 +19,11 @@ class VSCodeRequirement(PluginRequirement):
class VSCodePlugin(Plugin):
name: str = 'vscode'
vscode_port: int | None = None
vscode_connection_token: str | None = None
vscode_port: Optional[int] = None
vscode_connection_token: Optional[str] = None
gateway_process: asyncio.subprocess.Process
async def initialize(self, username: str):
async def initialize(self, username: str) -> None:
if username not in ['root', 'openhands']:
self.vscode_port = None
self.vscode_connection_token = None
@@ -41,22 +44,29 @@ class VSCodePlugin(Plugin):
'EOF'
)
self.gateway_process = subprocess.Popen(
# Using asyncio.create_subprocess_shell instead of subprocess.Popen
# to avoid ASYNC101 linting error
self.gateway_process = await asyncio.create_subprocess_shell(
cmd,
stderr=subprocess.STDOUT,
shell=True,
stderr=asyncio.subprocess.STDOUT,
stdout=asyncio.subprocess.PIPE,
)
# read stdout until the kernel gateway is ready
output = ''
while should_continue() and self.gateway_process.stdout is not None:
line = self.gateway_process.stdout.readline().decode('utf-8')
line_bytes = await self.gateway_process.stdout.readline()
line = line_bytes.decode('utf-8')
print(line)
output += line
if 'at' in line:
break
time.sleep(1)
await asyncio.sleep(1)
logger.debug('Waiting for VSCode server to start...')
logger.debug(
f'VSCode server started at port {self.vscode_port}. Output: {output}'
)
async def run(self, action: Action) -> Observation:
"""Run the plugin for a given action."""
raise NotImplementedError('VSCodePlugin does not support run method')

View File

@@ -62,8 +62,7 @@ ARG RELEASE_ORG="gitpod-io"
# ARG USER_UID=1000
# ARG USER_GID=1000
RUN --mount=type=cache,target=/var/cache/wget \
if [ -z "${RELEASE_TAG}" ]; then \
RUN if [ -z "${RELEASE_TAG}" ]; then \
echo "The RELEASE_TAG build arg must be set." >&2 && \
exit 1; \
fi && \
@@ -75,15 +74,12 @@ RUN --mount=type=cache,target=/var/cache/wget \
elif [ "${arch}" = "armv7l" ]; then \
arch="armhf"; \
fi && \
cd /var/cache/wget && \
if [ ! -f "${RELEASE_TAG}-linux-${arch}.tar.gz" ]; then \
wget https://github.com/${RELEASE_ORG}/openvscode-server/releases/download/${RELEASE_TAG}/${RELEASE_TAG}-linux-${arch}.tar.gz ; \
fi && \
cd - && \
tar -xzf /var/cache/wget/${RELEASE_TAG}-linux-${arch}.tar.gz && \
wget https://github.com/${RELEASE_ORG}/openvscode-server/releases/download/${RELEASE_TAG}/${RELEASE_TAG}-linux-${arch}.tar.gz && \
tar -xzf ${RELEASE_TAG}-linux-${arch}.tar.gz && \
if [ -d "${OPENVSCODE_SERVER_ROOT}" ]; then rm -rf "${OPENVSCODE_SERVER_ROOT}"; fi && \
mv ${RELEASE_TAG}-linux-${arch} ${OPENVSCODE_SERVER_ROOT} && \
cp ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/openvscode-server ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/code
cp ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/openvscode-server ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/code && \
rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz

View File

@@ -135,6 +135,13 @@ async def select_file(file: str, request: Request):
return {'code': content}
elif isinstance(observation, ErrorObservation):
logger.error(f'Error opening file {file}: {observation}')
if 'ERROR_BINARY_FILE' in observation.message:
return JSONResponse(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
content={'error': f'Unable to open binary file: {file}'},
)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'error': f'Error opening file: {observation}'},

369
poetry.lock generated
View File

@@ -496,18 +496,18 @@ files = [
[[package]]
name = "boto3"
version = "1.37.31"
version = "1.37.33"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "boto3-1.37.31-py3-none-any.whl", hash = "sha256:cf8997be0742a5cab9d33a138ef56e423a8ebd8881f6f73e95076b26656b36dc"},
{file = "boto3-1.37.31.tar.gz", hash = "sha256:dfee02b2f8f632a239a2f4ba6a2d568e2edd7f7464e9afd8a487fdb3fa9a0ad3"},
{file = "boto3-1.37.33-py3-none-any.whl", hash = "sha256:7b1b1bc69762975824e5a5d570880abebf634f7594f88b3dc175e8800f35be1a"},
{file = "boto3-1.37.33.tar.gz", hash = "sha256:4390317a1578af73f1514651bd180ba25802dcbe0a23deafa13851d54d3c3203"},
]
[package.dependencies]
botocore = ">=1.37.31,<1.38.0"
botocore = ">=1.37.33,<1.38.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.11.0,<0.12.0"
@@ -516,14 +516,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "boto3-stubs"
version = "1.37.31"
description = "Type annotations for boto3 1.37.31 generated with mypy-boto3-builder 8.10.1"
version = "1.37.33"
description = "Type annotations for boto3 1.37.33 generated with mypy-boto3-builder 8.10.1"
optional = false
python-versions = ">=3.8"
groups = ["evaluation"]
files = [
{file = "boto3_stubs-1.37.31-py3-none-any.whl", hash = "sha256:356d5af1e3d8aba5d8068492f11426ee9f41d0a799c099ec548be13462337632"},
{file = "boto3_stubs-1.37.31.tar.gz", hash = "sha256:ab2e8f7877fddb97f50f4712f3e6ffafa0b20fbc6a1ac7019400d3b2261936cb"},
{file = "boto3_stubs-1.37.33-py3-none-any.whl", hash = "sha256:b811b9c99bb47300efead119af7cfc4496545b9e571dde62e7447901966e74f8"},
{file = "boto3_stubs-1.37.33.tar.gz", hash = "sha256:d0561aadb98fa4ecffd34f34c3b8be3abd0a0e5de1367f7d3e985eb4de904c5b"},
]
[package.dependencies]
@@ -579,7 +579,7 @@ bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (
bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.37.0,<1.38.0)"]
billing = ["mypy-boto3-billing (>=1.37.0,<1.38.0)"]
billingconductor = ["mypy-boto3-billingconductor (>=1.37.0,<1.38.0)"]
boto3 = ["boto3 (==1.37.31)"]
boto3 = ["boto3 (==1.37.33)"]
braket = ["mypy-boto3-braket (>=1.37.0,<1.38.0)"]
budgets = ["mypy-boto3-budgets (>=1.37.0,<1.38.0)"]
ce = ["mypy-boto3-ce (>=1.37.0,<1.38.0)"]
@@ -943,14 +943,14 @@ xray = ["mypy-boto3-xray (>=1.37.0,<1.38.0)"]
[[package]]
name = "botocore"
version = "1.37.31"
version = "1.37.33"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "botocore-1.37.31-py3-none-any.whl", hash = "sha256:598a33a7a0e5a014bd1416c999a0b9c634fbbba3d1363e2368e6a92da4544df4"},
{file = "botocore-1.37.31.tar.gz", hash = "sha256:eb3dfa44a87187bd82c3b493d568d8436270d4d000f237b49b669a01fcd8a21c"},
{file = "botocore-1.37.33-py3-none-any.whl", hash = "sha256:4a167dfecae51e9140de24067de1c339acde5ade3dad524a4600ac2c72055e23"},
{file = "botocore-1.37.33.tar.gz", hash = "sha256:09b213b0d0500040f85c7daee912ea767c724e43ed61909e624c803ff6925222"},
]
[package.dependencies]
@@ -1781,14 +1781,14 @@ urllib3 = ">=1.25.3,<3.0.0"
[[package]]
name = "daytona-sdk"
version = "0.12.1"
version = "0.13.1"
description = "Python SDK for Daytona"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "daytona_sdk-0.12.1-py3-none-any.whl", hash = "sha256:2f7b884a3dbeb237f7d1aa7ec2ad6715eb1389e43ffc69e917ffadbd7eb09c30"},
{file = "daytona_sdk-0.12.1.tar.gz", hash = "sha256:133ca80ad192702d01934b44377b21fe11e8262748445a47cebbf3b9a2605307"},
{file = "daytona_sdk-0.13.1-py3-none-any.whl", hash = "sha256:9c51429d6f7a22310d5ab79629e8ac6ef85a9263aa496c99f78ae118c9fd3085"},
{file = "daytona_sdk-0.13.1.tar.gz", hash = "sha256:42d84e3b134f857e117c5d54a85d3eeae45043e9e136015f45a432689e1c3604"},
]
[package.dependencies]
@@ -2662,7 +2662,7 @@ grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_versi
grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
proto-plus = [
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
{version = ">=1.22.3,<2.0.0dev"},
]
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0"
requests = ">=2.18.0,<3.0.0.dev0"
@@ -2675,14 +2675,14 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
version = "2.166.0"
version = "2.167.0"
description = "Google API Client Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_api_python_client-2.166.0-py2.py3-none-any.whl", hash = "sha256:dd8cc74d9fc18538ab05cbd2e93cb4f82382f910c5f6945db06c91f1deae6e45"},
{file = "google_api_python_client-2.166.0.tar.gz", hash = "sha256:b8cf843bd9d736c134aef76cf1dc7a47c9283a2ef24267b97207b9dd43b30ef7"},
{file = "google_api_python_client-2.167.0-py2.py3-none-any.whl", hash = "sha256:ce25290cc229505d770ca5c8d03850e0ae87d8e998fc6dd743ecece018baa396"},
{file = "google_api_python_client-2.167.0.tar.gz", hash = "sha256:a458d402572e1c2caf9db090d8e7b270f43ff326bd9349c731a86b19910e3995"},
]
[package.dependencies]
@@ -2876,7 +2876,7 @@ google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev"
grpc-google-iam-v1 = ">=0.14.0,<1.0.0dev"
proto-plus = [
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0dev"},
{version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
@@ -3809,14 +3809,14 @@ files = [
[[package]]
name = "json-repair"
version = "0.41.0"
version = "0.41.1"
description = "A package to repair broken json strings"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "json_repair-0.41.0-py3-none-any.whl", hash = "sha256:e3be820988fa210fe046e54c8475eafa9e069f1b85d67725f842eb00d1b61f21"},
{file = "json_repair-0.41.0.tar.gz", hash = "sha256:c9ee227d949d5da463024942b715e0e331f9f96cb7377e2f85a661b21596c50b"},
{file = "json_repair-0.41.1-py3-none-any.whl", hash = "sha256:0e181fd43a696887881fe19fed23422a54b3e4c558b6ff27a86a8c3ddde9ae79"},
{file = "json_repair-0.41.1.tar.gz", hash = "sha256:bba404b0888c84a6b86ecc02ec43b71b673cfee463baf6da94e079c55b136565"},
]
[[package]]
@@ -4428,14 +4428,14 @@ types-tqdm = "*"
[[package]]
name = "litellm"
version = "1.65.5"
version = "1.66.0"
description = "Library to easily interface with LLM API providers"
optional = false
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
groups = ["main"]
files = [
{file = "litellm-1.65.5-py3-none-any.whl", hash = "sha256:ed954ec575cc1c468418e705fe922c827591dc01c1204abd4caf9c7572c73b5c"},
{file = "litellm-1.65.5.tar.gz", hash = "sha256:f8b1e0800ad66a2029c21a3e37456dedf4d1391da2ef5860f0e22268f1557c7c"},
{file = "litellm-1.66.0-py3-none-any.whl", hash = "sha256:1e4a2a9e023b12a385e4283b2f7922f7bd9c15b93ee9e64e203333cf8ddbc895"},
{file = "litellm-1.66.0.tar.gz", hash = "sha256:15f592bab604233083dc8b79e1e510e7e234f06525efe4c4255732bfc7ceb219"},
]
[package.dependencies]
@@ -4453,7 +4453,7 @@ tokenizers = "*"
[package.extras]
extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"]
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-proxy-extras (==0.1.3)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"]
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-proxy-extras (==0.1.7)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"]
[[package]]
name = "lxml"
@@ -4898,14 +4898,14 @@ files = [
[[package]]
name = "modal"
version = "0.73.159"
version = "0.73.170"
description = "Python client library for Modal"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
files = [
{file = "modal-0.73.159-py3-none-any.whl", hash = "sha256:2363729caaaa4475eba6e0dce90b759fa8a37c82845de5333e9e18eabc52c5c7"},
{file = "modal-0.73.159.tar.gz", hash = "sha256:4c9a5f7d37a343420334d4b8436034c75ee47d0976f6677fe52f7c68ea9d32b6"},
{file = "modal-0.73.170-py3-none-any.whl", hash = "sha256:d3b9eab82c823c69604fdfd18e970c4b65c76b3aa82f60019e608d4d3166224f"},
{file = "modal-0.73.170.tar.gz", hash = "sha256:4df2405efa1a6d41fa42da675516bfdfc14fedb26f1ea905f3e7c49addc355f5"},
]
[package.dependencies]
@@ -5276,7 +5276,7 @@ version = "3.4.2"
description = "Python package for creating and manipulating graphs and networks"
optional = false
python-versions = ">=3.10"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"},
{file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"},
@@ -5622,14 +5622,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "openai"
version = "1.72.0"
version = "1.73.0"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.8"
groups = ["main", "evaluation", "test"]
files = [
{file = "openai-1.72.0-py3-none-any.whl", hash = "sha256:34f5496ba5c8cb06c592831d69e847e2d164526a2fb92afdc3b5cf2891c328c3"},
{file = "openai-1.72.0.tar.gz", hash = "sha256:f51de971448905cc90ed5175a5b19e92fd94e31f68cde4025762f9f5257150db"},
{file = "openai-1.73.0-py3-none-any.whl", hash = "sha256:f52d1f673fb4ce6069a40d544a80fcb062eba1b3f489004fac4f9923a074c425"},
{file = "openai-1.73.0.tar.gz", hash = "sha256:b58ea39ba589de07db85c9905557ac12d2fc77600dcd2b92a08b99c9a3dce9e0"},
]
[package.dependencies]
@@ -5649,14 +5649,14 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
[[package]]
name = "openhands-aci"
version = "0.2.8"
version = "0.2.10"
description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands."
optional = false
python-versions = "<4.0,>=3.12"
groups = ["main"]
files = [
{file = "openhands_aci-0.2.8-py3-none-any.whl", hash = "sha256:20df3a846f663bd86d6c73253ef990aa701f8db4595b75fda19a2d7861cf0514"},
{file = "openhands_aci-0.2.8.tar.gz", hash = "sha256:557b81ccabbe3df4bb163ad8e941796bd5d61aeb07833ed02931197ab472e71e"},
{file = "openhands_aci-0.2.10-py3-none-any.whl", hash = "sha256:0703eb117e24326d80d990b81a0c71e28a364f56999095b95c146c934e40fc55"},
{file = "openhands_aci-0.2.10.tar.gz", hash = "sha256:a5e6bf46cbd9a99c5c592548419a9a3b3091c4e82f0227e8aaf470b18a261cc9"},
]
[package.dependencies]
@@ -5666,11 +5666,7 @@ charset-normalizer = ">=3.4.1,<4.0.0"
flake8 = "*"
gitpython = "*"
grep-ast = "0.3.3"
litellm = "*"
networkx = "*"
numpy = "*"
pandas = "*"
scipy = "*"
pydantic = ">=2.11.3,<3.0.0"
tree-sitter = ">=0.24.0,<0.25.0"
tree-sitter-javascript = ">=0.23.1,<0.24.0"
tree-sitter-python = ">=0.23.6,<0.24.0"
@@ -5807,7 +5803,7 @@ version = "2.2.3"
description = "Powerful data structures for data analysis, time series, and statistics"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation", "test"]
groups = ["evaluation", "test"]
files = [
{file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"},
{file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"},
@@ -6527,20 +6523,21 @@ files = [
[[package]]
name = "pydantic"
version = "2.10.6"
version = "2.11.3"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["main", "evaluation", "test"]
files = [
{file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
{file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
{file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"},
{file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.27.2"
pydantic-core = "2.33.1"
typing-extensions = ">=4.12.2"
typing-inspection = ">=0.4.0"
[package.extras]
email = ["email-validator (>=2.0.0)"]
@@ -6548,112 +6545,111 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows
[[package]]
name = "pydantic-core"
version = "2.27.2"
version = "2.33.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["main", "evaluation", "test"]
files = [
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
{file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
{file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
{file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
{file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
{file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
{file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
{file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"},
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89"},
{file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde"},
{file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65"},
{file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc"},
{file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091"},
{file = "pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383"},
{file = "pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504"},
{file = "pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24"},
{file = "pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f"},
{file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77"},
{file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961"},
{file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1"},
{file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c"},
{file = "pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896"},
{file = "pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83"},
{file = "pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89"},
{file = "pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8"},
{file = "pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d"},
{file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b"},
{file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39"},
{file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a"},
{file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db"},
{file = "pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda"},
{file = "pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4"},
{file = "pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea"},
{file = "pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a"},
{file = "pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d"},
{file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4"},
{file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde"},
{file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e"},
{file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd"},
{file = "pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f"},
{file = "pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40"},
{file = "pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523"},
{file = "pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d"},
{file = "pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c"},
{file = "pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18"},
{file = "pydantic_core-2.33.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5ab77f45d33d264de66e1884fca158bc920cb5e27fd0764a72f72f5756ae8bdb"},
{file = "pydantic_core-2.33.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7aaba1b4b03aaea7bb59e1b5856d734be011d3e6d98f5bcaa98cb30f375f2ad"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fb66263e9ba8fea2aa85e1e5578980d127fb37d7f2e292773e7bc3a38fb0c7b"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f2648b9262607a7fb41d782cc263b48032ff7a03a835581abbf7a3bec62bcf5"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:723c5630c4259400818b4ad096735a829074601805d07f8cafc366d95786d331"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d100e3ae783d2167782391e0c1c7a20a31f55f8015f3293647544df3f9c67824"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177d50460bc976a0369920b6c744d927b0ecb8606fb56858ff542560251b19e5"},
{file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3edde68d1a1f9af1273b2fe798997b33f90308fb6d44d8550c89fc6a3647cf6"},
{file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a62c3c3ef6a7e2c45f7853b10b5bc4ddefd6ee3cd31024754a1a5842da7d598d"},
{file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:c91dbb0ab683fa0cd64a6e81907c8ff41d6497c346890e26b23de7ee55353f96"},
{file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f466e8bf0a62dc43e068c12166281c2eca72121dd2adc1040f3aa1e21ef8599"},
{file = "pydantic_core-2.33.1-cp39-cp39-win32.whl", hash = "sha256:ab0277cedb698749caada82e5d099dc9fed3f906a30d4c382d1a21725777a1e5"},
{file = "pydantic_core-2.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:5773da0ee2d17136b1f1c6fbde543398d452a6ad2a7b54ea1033e2daa739b8d2"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add"},
{file = "pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544"},
{file = "pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7edbc454a29fc6aeae1e1eecba4f07b63b8d76e76a748532233c4c167b4cb9ea"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad05b683963f69a1d5d2c2bdab1274a31221ca737dbbceaa32bcb67359453cdd"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df6a94bf9452c6da9b5d76ed229a5683d0306ccb91cca8e1eea883189780d568"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7965c13b3967909a09ecc91f21d09cfc4576bf78140b988904e94f130f188396"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f1fdb790440a34f6ecf7679e1863b825cb5ffde858a9197f851168ed08371e5"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5277aec8d879f8d05168fdd17ae811dd313b8ff894aeeaf7cd34ad28b4d77e33"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8ab581d3530611897d863d1a649fb0644b860286b4718db919bfd51ece41f10b"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0483847fa9ad5e3412265c1bd72aad35235512d9ce9d27d81a56d935ef489672"},
{file = "pydantic_core-2.33.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de9e06abe3cc5ec6a2d5f75bc99b0bdca4f5c719a5b34026f8c57efbdecd2ee3"},
{file = "pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df"},
]
[package.dependencies]
@@ -7147,14 +7143,14 @@ XlsxWriter = ">=0.5.7"
[[package]]
name = "python-socketio"
version = "5.12.1"
version = "5.13.0"
description = "Socket.IO server and client for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "python_socketio-5.12.1-py3-none-any.whl", hash = "sha256:24a0ea7cfff0e021eb28c68edbf7914ee4111bdf030b95e4d250c4dc9af7a386"},
{file = "python_socketio-5.12.1.tar.gz", hash = "sha256:0299ff1f470b676c09c1bfab1dead25405077d227b2c13cf217a34dadc68ba9c"},
{file = "python_socketio-5.13.0-py3-none-any.whl", hash = "sha256:51f68d6499f2df8524668c24bcec13ba1414117cfb3a90115c559b601ab10caf"},
{file = "python_socketio-5.13.0.tar.gz", hash = "sha256:ac4e19a0302ae812e23b712ec8b6427ca0521f7c582d6abb096e36e24a263029"},
]
[package.dependencies]
@@ -7172,7 +7168,7 @@ version = "2025.1"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
groups = ["main", "evaluation", "test"]
groups = ["evaluation", "test"]
files = [
{file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"},
{file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"},
@@ -7967,30 +7963,30 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.11.4"
version = "0.11.5"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev", "evaluation"]
files = [
{file = "ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2"},
{file = "ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc"},
{file = "ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906"},
{file = "ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f"},
{file = "ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e"},
{file = "ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223"},
{file = "ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e"},
{file = "ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d"},
{file = "ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99"},
{file = "ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222"},
{file = "ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304"},
{file = "ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019"},
{file = "ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896"},
{file = "ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751"},
{file = "ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270"},
{file = "ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb"},
{file = "ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc"},
{file = "ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407"},
{file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"},
{file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"},
{file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"},
{file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"},
{file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"},
{file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"},
{file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"},
{file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"},
{file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"},
{file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"},
{file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"},
{file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"},
{file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"},
{file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"},
{file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"},
{file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"},
{file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"},
{file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"},
]
[[package]]
@@ -8166,7 +8162,7 @@ version = "1.15.2"
description = "Fundamental algorithms for scientific computing in Python"
optional = false
python-versions = ">=3.10"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "scipy-1.15.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2ec871edaa863e8213ea5df811cd600734f6400b4af272e1c011e69401218e9"},
{file = "scipy-1.15.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6f223753c6ea76983af380787611ae1291e3ceb23917393079dcc746ba60cfb5"},
@@ -9430,13 +9426,28 @@ files = [
mypy-extensions = ">=0.3.0"
typing-extensions = ">=3.7.4"
[[package]]
name = "typing-inspection"
version = "0.4.0"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation", "test"]
files = [
{file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"},
{file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"},
]
[package.dependencies]
typing-extensions = ">=4.12.0"
[[package]]
name = "tzdata"
version = "2025.1"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["main", "evaluation", "test"]
groups = ["evaluation", "test"]
files = [
{file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"},
{file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"},
@@ -9501,14 +9512,14 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
version = "0.34.0"
version = "0.34.1"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"},
{file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"},
{file = "uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065"},
{file = "uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc"},
]
[package.dependencies]
@@ -10258,4 +10269,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "535e002070d94b63adb76e6faad5c1966d177b8b482ea82212555514b95c623c"
content-hash = "e103d6d899af073c222deec407e7d0f58d445d10b2cfb9b0579243790eefd5be"

View File

@@ -62,7 +62,7 @@ runloop-api-client = "0.29.0"
libtmux = ">=0.37,<0.40"
pygithub = "^2.5.0"
joblib = "*"
openhands-aci = "^0.2.8"
openhands-aci = "^0.2.10"
python-socketio = "^5.11.4"
redis = "^5.2.0"
sse-starlette = "^2.1.3"
@@ -71,14 +71,14 @@ stripe = ">=11.5,<13.0"
ipywidgets = "^8.1.5"
qtconsole = "^5.6.1"
memory-profiler = "^0.61.0"
daytona-sdk = "0.12.1"
daytona-sdk = "0.13.1"
mcp = "1.6.0"
python-json-logger = "^3.2.1"
playwright = "^1.51.0"
prompt-toolkit = "^3.0.50"
[tool.poetry.group.dev.dependencies]
ruff = "0.11.4"
ruff = "0.11.5"
mypy = "1.15.0"
pre-commit = "4.2.0"
build = "*"
@@ -97,6 +97,7 @@ gevent = "^24.2.1"
[tool.coverage.run]
concurrency = ["gevent"]
[tool.poetry.group.runtime.dependencies]
jupyterlab = "*"
notebook = "*"
@@ -125,6 +126,7 @@ ignore = ["D1"]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.poetry.group.evaluation.dependencies]
streamlit = "*"
whatthepatch = "*"

View File

@@ -200,9 +200,10 @@ def test_str_replace_multi_line_with_tabs(temp_dir, runtime_cls, run_as_openhand
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
test_file = os.path.join(config.workspace_mount_path_in_sandbox, 'test.txt')
action = FileWriteAction(
content='def test():\n\tprint("Hello, World!")',
action = FileEditAction(
command='create',
path=test_file,
file_text='def test():\n\tprint("Hello, World!")',
)
runtime.run_action(action)
@@ -220,7 +221,6 @@ def test_str_replace_multi_line_with_tabs(temp_dir, runtime_cls, run_as_openhand
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}:
1\tdef test():
2\t{'\t'.expandtabs()}print("Hello, Universe!")
3\t
Review the changes and make sure they are as expected. Edit the file again if necessary."""
)

View File

@@ -82,9 +82,9 @@ def get_config() -> AppConfig:
workspace_mount_path=None,
)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing=False,
codeact_enable_llm_editor=False,
enable_jupyter=False,
enable_browsing=False,
enable_llm_editor=False,
)
config.set_agent_config(agent_config)
return config

View File

@@ -558,7 +558,7 @@ def test_send_pull_request_target_branch_with_fork(
post_data = mock_post.call_args[1]['json']
assert post_data['base'] == target_branch # PR should target the specified branch
assert (
post_data['head'] == 'openhands-fix-issue-42'
post_data['head'] == 'fork-owner:openhands-fix-issue-42'
) # Branch name should be standard
# Check that push was to fork

View File

@@ -10,10 +10,8 @@ from prompt_toolkit.output import create_output
from openhands.core.cli import main
from openhands.core.config import AppConfig
from openhands.core.schema import AgentState
from openhands.events.action import MessageAction
from openhands.events.event import EventSource
from openhands.events.observation import AgentStateChangedObservation
class MockEventStream:
@@ -88,6 +86,7 @@ def mock_config():
mock_config.security.confirmation_mode = False
mock_config.sandbox = Mock()
mock_config.sandbox.selected_repo = None
mock_config.workspace_base = '/test'
mock_setup_config.return_value = mock_config
yield mock_config
@@ -126,44 +125,32 @@ def mock_runtime():
@pytest.mark.asyncio
async def test_cli_greeting(
async def test_cli_basic_prompt(
mock_runtime, mock_controller, mock_config, mock_agent, mock_memory, mock_read_task
):
buffer = StringIO()
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
mock_controller.status_callback = None
with patch('openhands.core.cli.manage_openhands_file', return_value=True):
with patch('openhands.core.cli.cli_confirm', return_value=True):
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
mock_controller.status_callback = None
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
await asyncio.sleep(0.1)
await asyncio.sleep(0.1)
hello_response = MessageAction(content='Ping')
hello_response._source = EventSource.AGENT
mock_runtime.event_stream.add_event(hello_response, EventSource.AGENT)
hello_response = MessageAction(content='Ping')
hello_response._source = EventSource.AGENT
mock_runtime.event_stream.add_event(hello_response, EventSource.AGENT)
state_change = AgentStateChangedObservation(
content='Awaiting user input', agent_state=AgentState.AWAITING_USER_INPUT
)
state_change._source = EventSource.AGENT
mock_runtime.event_stream.add_event(state_change, EventSource.AGENT)
try:
await asyncio.wait_for(main_task, timeout=1.0)
except asyncio.TimeoutError:
main_task.cancel()
stop_event = AgentStateChangedObservation(
content='Stop', agent_state=AgentState.STOPPED
)
stop_event._source = EventSource.AGENT
mock_runtime.event_stream.add_event(stop_event, EventSource.AGENT)
buffer.seek(0)
output = buffer.read()
mock_controller.state.agent_state = AgentState.STOPPED
try:
await asyncio.wait_for(main_task, timeout=1.0)
except asyncio.TimeoutError:
main_task.cancel()
buffer.seek(0)
output = buffer.read()
assert 'Ping' in output
assert 'Ping' in output

View File

@@ -0,0 +1,368 @@
import asyncio
from io import StringIO
from unittest.mock import AsyncMock, Mock, patch
import pytest
from prompt_toolkit.application import create_app_session
from prompt_toolkit.input import create_pipe_input
from prompt_toolkit.output import create_output
from openhands.core.cli import main
from openhands.core.config import AppConfig
from openhands.core.schema import AgentState
from openhands.events.action import ChangeAgentStateAction, MessageAction
from openhands.events.event import EventSource
from openhands.events.observation import AgentStateChangedObservation
class MockEventStream:
def __init__(self):
self._subscribers = {}
self.cur_id = 0
self.events = []
def subscribe(self, subscriber_id, callback, callback_id=None):
if subscriber_id not in self._subscribers:
self._subscribers[subscriber_id] = {}
self._subscribers[subscriber_id][callback_id] = callback
return callback_id
def unsubscribe(self, subscriber_id, callback_id):
if (
subscriber_id in self._subscribers
and callback_id in self._subscribers[subscriber_id]
):
del self._subscribers[subscriber_id][callback_id]
def add_event(self, event, source):
event._id = self.cur_id
self.cur_id += 1
event._source = source
event._timestamp = '2023-01-01T00:00:00'
self.events.append((event, source))
for subscriber_id in self._subscribers:
for callback_id, callback in self._subscribers[subscriber_id].items():
if asyncio.iscoroutinefunction(callback):
asyncio.create_task(callback(event))
else:
callback(event)
@pytest.fixture
def mock_agent():
with patch('openhands.core.cli.create_agent') as mock_create_agent:
mock_agent_instance = AsyncMock()
mock_agent_instance.name = 'test-agent'
mock_agent_instance.llm = AsyncMock()
mock_agent_instance.llm.config = AsyncMock()
mock_agent_instance.llm.config.model = 'test-model'
mock_agent_instance.llm.config.base_url = 'http://test'
mock_agent_instance.llm.config.max_message_chars = 1000
mock_agent_instance.config = AsyncMock()
mock_agent_instance.config.disabled_microagents = []
mock_agent_instance.sandbox_plugins = []
mock_agent_instance.prompt_manager = AsyncMock()
mock_create_agent.return_value = mock_agent_instance
yield mock_agent_instance
@pytest.fixture
def mock_controller():
with patch('openhands.core.cli.create_controller') as mock_create_controller:
mock_controller_instance = AsyncMock()
mock_controller_instance.state.agent_state = None
# Mock run_until_done to finish immediately
mock_controller_instance.run_until_done = AsyncMock(return_value=None)
mock_create_controller.return_value = (mock_controller_instance, None)
yield mock_controller_instance
@pytest.fixture
def mock_config():
with patch('openhands.core.cli.parse_arguments') as mock_parse_args:
args = Mock()
args.file = None
args.task = None
args.directory = None
mock_parse_args.return_value = args
with patch('openhands.core.cli.setup_config_from_args') as mock_setup_config:
mock_config = AppConfig()
mock_config.cli_multiline_input = False
mock_config.security = Mock()
mock_config.security.confirmation_mode = False
mock_config.sandbox = Mock()
mock_config.sandbox.selected_repo = None
mock_config.workspace_base = '/test'
mock_config.runtime = 'local' # Important for /init test
mock_setup_config.return_value = mock_config
yield mock_config
@pytest.fixture
def mock_memory():
with patch('openhands.core.cli.create_memory') as mock_create_memory:
mock_memory_instance = AsyncMock()
mock_create_memory.return_value = mock_memory_instance
yield mock_memory_instance
@pytest.fixture
def mock_read_task():
with patch('openhands.core.cli.read_task') as mock_read_task:
mock_read_task.return_value = None
yield mock_read_task
@pytest.fixture
def mock_runtime():
with patch('openhands.core.cli.create_runtime') as mock_create_runtime:
mock_runtime_instance = AsyncMock()
mock_event_stream = MockEventStream()
mock_runtime_instance.event_stream = mock_event_stream
mock_runtime_instance.connect = AsyncMock()
# Ensure status_callback is None
mock_runtime_instance.status_callback = None
# Mock get_microagents_from_selected_repo
mock_runtime_instance.get_microagents_from_selected_repo = Mock(return_value=[])
mock_create_runtime.return_value = mock_runtime_instance
yield mock_runtime_instance
@pytest.mark.asyncio
async def test_help_command(
mock_runtime, mock_controller, mock_config, mock_agent, mock_memory, mock_read_task
):
buffer = StringIO()
with patch('openhands.core.cli.manage_openhands_file', return_value=True):
with patch(
'openhands.core.cli.check_folder_security_agreement', return_value=True
):
with patch('openhands.core.cli.read_prompt_input') as mock_prompt:
# Setup to return /help first, then simulate an exit
mock_prompt.side_effect = ['/help', '/exit']
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
mock_controller.status_callback = None
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
agent_ready_event = AgentStateChangedObservation(
agent_state=AgentState.AWAITING_USER_INPUT,
content='Agent is ready for user input',
)
mock_runtime.event_stream.add_event(
agent_ready_event, EventSource.AGENT
)
await asyncio.sleep(0.1)
try:
await asyncio.wait_for(main_task, timeout=0.5)
except asyncio.TimeoutError:
main_task.cancel()
try:
await main_task
except asyncio.CancelledError:
pass
buffer.seek(0)
output = buffer.read()
# Verify help output was displayed
assert 'OpenHands CLI' in output
assert 'Things that you can try' in output
assert 'Interactive commands' in output
assert '/help' in output
assert '/exit' in output
# Verify the help command didn't add a MessageAction to the event stream
message_actions = [
event
for event, _ in mock_runtime.event_stream.events
if isinstance(event, MessageAction)
]
assert len(message_actions) == 0
@pytest.mark.asyncio
async def test_exit_command(
mock_runtime, mock_controller, mock_config, mock_agent, mock_memory, mock_read_task
):
buffer = StringIO()
with patch('openhands.core.cli.manage_openhands_file', return_value=True):
with patch(
'openhands.core.cli.check_folder_security_agreement', return_value=True
):
with patch('openhands.core.cli.read_prompt_input') as mock_prompt:
# First prompt call returns /exit
mock_prompt.side_effect = ['/exit']
with patch('openhands.core.cli.shutdown') as mock_shutdown:
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
mock_controller.status_callback = None
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
agent_ready_event = AgentStateChangedObservation(
agent_state=AgentState.AWAITING_USER_INPUT,
content='Agent is ready for user input',
)
mock_runtime.event_stream.add_event(
agent_ready_event, EventSource.AGENT
)
await asyncio.sleep(0.1)
try:
await asyncio.wait_for(main_task, timeout=0.5)
except asyncio.TimeoutError:
main_task.cancel()
try:
await main_task
except asyncio.CancelledError:
pass
# Verify that the exit command sent a STOPPED state change event
state_change_events = [
event
for event, source in mock_runtime.event_stream.events
if isinstance(event, ChangeAgentStateAction)
and event.agent_state == AgentState.STOPPED
and source == EventSource.ENVIRONMENT
]
assert len(state_change_events) == 1
# Verify shutdown was called
mock_shutdown.assert_called_once()
@pytest.mark.asyncio
async def test_init_command(
mock_runtime, mock_controller, mock_config, mock_agent, mock_memory, mock_read_task
):
buffer = StringIO()
with patch('openhands.core.cli.manage_openhands_file', return_value=True):
with patch(
'openhands.core.cli.check_folder_security_agreement', return_value=True
):
with patch('openhands.core.cli.read_prompt_input') as mock_prompt:
# First prompt call returns /init, second call returns /exit
mock_prompt.side_effect = ['/init', '/exit']
with patch('openhands.core.cli.init_repository') as mock_init_repo:
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
mock_controller.status_callback = None
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
agent_ready_event = AgentStateChangedObservation(
agent_state=AgentState.AWAITING_USER_INPUT,
content='Agent is ready for user input',
)
mock_runtime.event_stream.add_event(
agent_ready_event, EventSource.AGENT
)
await asyncio.sleep(0.1)
try:
await asyncio.wait_for(main_task, timeout=0.5)
except asyncio.TimeoutError:
main_task.cancel()
try:
await main_task
except asyncio.CancelledError:
pass
# Verify init_repository was called with the correct directory
mock_init_repo.assert_called_once_with('/test')
# Verify that a MessageAction was sent with the repository creation prompt
message_events = [
event
for event, source in mock_runtime.event_stream.events
if isinstance(event, MessageAction)
and 'Please explore this repository' in event.content
and source == EventSource.USER
]
assert len(message_events) == 1
@pytest.mark.asyncio
async def test_init_command_non_local_runtime(
mock_runtime, mock_controller, mock_config, mock_agent, mock_memory, mock_read_task
):
buffer = StringIO()
# Set runtime to non-local for this test
mock_config.runtime = 'remote'
with patch('openhands.core.cli.manage_openhands_file', return_value=True):
with patch(
'openhands.core.cli.check_folder_security_agreement', return_value=True
):
with patch('openhands.core.cli.read_prompt_input') as mock_prompt:
# First prompt call returns /init, second call returns /exit
mock_prompt.side_effect = ['/init', '/exit']
with patch('openhands.core.cli.init_repository') as mock_init_repo:
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
mock_controller.status_callback = None
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
# Send AgentStateChangedObservation to trigger prompt
agent_ready_event = AgentStateChangedObservation(
agent_state=AgentState.AWAITING_USER_INPUT,
content='Agent is ready for user input',
)
mock_runtime.event_stream.add_event(
agent_ready_event, EventSource.AGENT
)
await asyncio.sleep(0.1)
try:
await asyncio.wait_for(main_task, timeout=0.5)
except asyncio.TimeoutError:
main_task.cancel()
try:
await main_task
except asyncio.CancelledError:
pass
buffer.seek(0)
output = buffer.read()
# Verify error message was displayed
assert (
'Repository initialization through the CLI is only supported for local runtime'
in output
)
# Verify init_repository was not called
mock_init_repo.assert_not_called()
# Verify no MessageAction was sent for repository creation
message_events = [
event
for event, _ in mock_runtime.event_stream.events
if isinstance(event, MessageAction)
and 'Please explore this repository' in event.content
]
assert len(message_events) == 0

View File

@@ -1,127 +0,0 @@
import asyncio
from argparse import Namespace
from io import StringIO
from pathlib import Path
from unittest.mock import AsyncMock, Mock, patch
import pytest
from prompt_toolkit.application import create_app_session
from prompt_toolkit.input import create_pipe_input
from prompt_toolkit.output import create_output
from openhands.core.cli import main
from openhands.core.config import AppConfig
from openhands.core.schema import AgentState
from openhands.events.event import EventSource
from openhands.events.observation import AgentStateChangedObservation
@pytest.fixture
def mock_runtime():
with patch('openhands.core.cli.create_runtime') as mock_create_runtime:
mock_runtime_instance = AsyncMock()
# Mock the event stream with proper async methods
mock_event_stream = AsyncMock()
mock_event_stream.subscribe = AsyncMock()
mock_event_stream.add_event = AsyncMock()
mock_event_stream.get_events = AsyncMock(return_value=[])
mock_event_stream.get_latest_event_id = AsyncMock(return_value=0)
mock_runtime_instance.event_stream = mock_event_stream
# Mock connect method to return immediately
mock_runtime_instance.connect = AsyncMock()
# Ensure status_callback is None
mock_runtime_instance.status_callback = None
# Mock get_microagents_from_selected_repo
mock_runtime_instance.get_microagents_from_selected_repo = Mock(return_value=[])
mock_create_runtime.return_value = mock_runtime_instance
yield mock_runtime_instance
@pytest.fixture
def mock_agent():
with patch('openhands.core.cli.create_agent') as mock_create_agent:
mock_agent_instance = AsyncMock()
mock_agent_instance.name = 'test-agent'
mock_agent_instance.llm = AsyncMock()
mock_agent_instance.llm.config = AsyncMock()
mock_agent_instance.llm.config.model = 'test-model'
mock_agent_instance.llm.config.base_url = 'http://test'
mock_agent_instance.llm.config.max_message_chars = 1000
mock_agent_instance.config = AsyncMock()
mock_agent_instance.config.disabled_microagents = []
mock_agent_instance.sandbox_plugins = []
mock_agent_instance.prompt_manager = AsyncMock()
mock_create_agent.return_value = mock_agent_instance
yield mock_agent_instance
@pytest.fixture
def mock_controller():
with patch('openhands.core.cli.create_controller') as mock_create_controller:
mock_controller_instance = AsyncMock()
# Mock run_until_done to finish immediately
mock_controller_instance.run_until_done = AsyncMock(return_value=None)
mock_create_controller.return_value = (mock_controller_instance, None)
yield mock_controller_instance
@pytest.fixture
def task_file(tmp_path: Path) -> Path:
# Create a temporary file with our task
task_file = tmp_path / 'task.txt'
task_file.write_text('Ask me what your task is')
return task_file
@pytest.fixture
def mock_config(task_file: Path):
with patch('openhands.core.cli.parse_arguments') as mock_parse_args:
# Create a proper Namespace with our temporary task file
args = Namespace(file=str(task_file), task=None, directory=None)
mock_parse_args.return_value = args
with patch('openhands.core.cli.setup_config_from_args') as mock_setup_config:
mock_config = AppConfig()
mock_setup_config.return_value = mock_config
yield mock_config
@pytest.mark.asyncio
async def test_cli_session_id_output(
mock_runtime, mock_agent, mock_controller, mock_config
):
# status_callback is set when initializing the runtime
mock_controller.status_callback = None
buffer = StringIO()
# Use input patch just for the exit command
with patch('builtins.input', return_value='exit'):
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
# Create a task for main
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
# Give it a moment to display the session ID
await asyncio.sleep(0.1)
# Trigger agent state change to STOPPED to end the main loop
event = AgentStateChangedObservation(
content='Stop', agent_state=AgentState.STOPPED
)
event._source = EventSource.AGENT
await mock_runtime.event_stream.add_event(event)
# Wait for main to finish with a timeout
try:
await asyncio.wait_for(main_task, timeout=1.0)
except asyncio.TimeoutError:
main_task.cancel()
buffer.seek(0)
output = buffer.read()
# Check the output
assert 'Session ID:' in output
# Also verify that our task message was processed
assert 'Ask me what your task is' in str(mock_runtime.mock_calls)

View File

@@ -0,0 +1,301 @@
import asyncio
from datetime import datetime
from io import StringIO
from unittest.mock import AsyncMock, Mock, patch
import pytest
from prompt_toolkit.application import create_app_session
from prompt_toolkit.input import create_pipe_input
from prompt_toolkit.output import create_output
from openhands.core.cli import main
from openhands.core.config import AppConfig
class MockEventStream:
def __init__(self):
self._subscribers = {}
self.cur_id = 0
def subscribe(self, subscriber_id, callback, callback_id):
if subscriber_id not in self._subscribers:
self._subscribers[subscriber_id] = {}
self._subscribers[subscriber_id][callback_id] = callback
def unsubscribe(self, subscriber_id, callback_id):
if (
subscriber_id in self._subscribers
and callback_id in self._subscribers[subscriber_id]
):
del self._subscribers[subscriber_id][callback_id]
def add_event(self, event, source):
event._id = self.cur_id
self.cur_id += 1
event._source = source
event._timestamp = datetime.now().isoformat()
for subscriber_id in self._subscribers:
for callback_id, callback in self._subscribers[subscriber_id].items():
callback(event)
@pytest.fixture
def mock_agent():
with patch('openhands.core.cli.create_agent') as mock_create_agent:
mock_agent_instance = AsyncMock()
mock_agent_instance.name = 'test-agent'
mock_agent_instance.llm = AsyncMock()
mock_agent_instance.llm.config = AsyncMock()
mock_agent_instance.llm.config.model = 'test-model'
mock_agent_instance.llm.config.base_url = 'http://test'
mock_agent_instance.llm.config.max_message_chars = 1000
mock_agent_instance.config = AsyncMock()
mock_agent_instance.config.disabled_microagents = []
mock_agent_instance.sandbox_plugins = []
mock_agent_instance.prompt_manager = AsyncMock()
mock_create_agent.return_value = mock_agent_instance
yield mock_agent_instance
@pytest.fixture
def mock_controller():
with patch('openhands.core.cli.create_controller') as mock_create_controller:
mock_controller_instance = AsyncMock()
mock_controller_instance.state.agent_state = None
# Mock run_until_done to finish immediately
mock_controller_instance.run_until_done = AsyncMock(return_value=None)
mock_create_controller.return_value = (mock_controller_instance, None)
yield mock_controller_instance
@pytest.fixture
def mock_config():
with patch('openhands.core.cli.parse_arguments') as mock_parse_args:
args = Mock()
args.file = None
args.task = None
args.directory = None
mock_parse_args.return_value = args
with patch('openhands.core.cli.setup_config_from_args') as mock_setup_config:
mock_config = AppConfig()
mock_config.cli_multiline_input = False
mock_config.security = Mock()
mock_config.security.confirmation_mode = False
mock_config.sandbox = Mock()
mock_config.sandbox.selected_repo = None
mock_config.workspace_base = '/test'
mock_setup_config.return_value = mock_config
yield mock_config
@pytest.fixture
def mock_memory():
with patch('openhands.core.cli.create_memory') as mock_create_memory:
mock_memory_instance = AsyncMock()
mock_create_memory.return_value = mock_memory_instance
yield mock_memory_instance
@pytest.fixture
def mock_read_task():
with patch('openhands.core.cli.read_task') as mock_read_task:
mock_read_task.return_value = None
yield mock_read_task
@pytest.fixture
def mock_runtime():
with patch('openhands.core.cli.create_runtime') as mock_create_runtime:
mock_runtime_instance = AsyncMock()
mock_event_stream = MockEventStream()
mock_runtime_instance.event_stream = mock_event_stream
mock_runtime_instance.connect = AsyncMock()
# Ensure status_callback is None
mock_runtime_instance.status_callback = None
# Mock get_microagents_from_selected_repo
mock_runtime_instance.get_microagents_from_selected_repo = Mock(return_value=[])
mock_create_runtime.return_value = mock_runtime_instance
yield mock_runtime_instance
@pytest.mark.asyncio
async def test_cli_startup_folder_security_confirmation_agree(
mock_runtime, mock_controller, mock_config, mock_agent, mock_memory, mock_read_task
):
buffer = StringIO()
with patch(
'openhands.core.cli.manage_openhands_file', return_value=False
) as mock_manage_openhands_file:
with patch(
'openhands.core.cli.cli_confirm', return_value=True
) as mock_cli_confirm:
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
mock_controller.status_callback = None
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
await asyncio.sleep(0.1)
try:
await asyncio.wait_for(main_task, timeout=0.1)
except Exception:
main_task.cancel()
buffer.seek(0)
output = buffer.read()
# ASCII art banner
assert '___' in output
# Version information
assert 'OpenHands CLI v' in output
# Session initialization
assert 'Initializing session' in output
# Folder security confirmation
assert 'Do you trust the files in this folder?' in output
assert '/test' in output
assert (
'OpenHands may read and execute files in this folder with your permission.'
in output
)
# Confirmation prompt
mock_manage_openhands_file.assert_any_call('/test')
mock_cli_confirm.assert_called_once_with(
'Do you wish to continue?', ['Yes, proceed', 'No, exit']
)
mock_manage_openhands_file.assert_any_call('/test', add_to_trusted=True)
# Session initialization complete
assert 'Initialized session' in output
# Welcome message
assert "Let's start building!" in output
assert 'What do you want to build?' in output
assert 'Type /help for help' in output
@pytest.mark.asyncio
async def test_cli_startup_folder_security_confirmation_disagree(
mock_runtime, mock_controller, mock_config, mock_agent, mock_memory, mock_read_task
):
buffer = StringIO()
with patch(
'openhands.core.cli.manage_openhands_file', return_value=False
) as mock_manage_openhands_file:
with patch(
'openhands.core.cli.cli_confirm', return_value=False
) as mock_cli_confirm:
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
mock_controller.status_callback = None
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
await asyncio.sleep(0.1)
try:
await asyncio.wait_for(main_task, timeout=0.1)
except Exception:
main_task.cancel()
buffer.seek(0)
output = buffer.read()
# ASCII art banner
assert '___' in output
# Version information
assert 'OpenHands CLI v' in output
# Session initialization
assert 'Initializing session' in output
# Folder security confirmation
assert 'Do you trust the files in this folder?' in output
assert '/test' in output
assert (
'OpenHands may read and execute files in this folder with your permission.'
in output
)
# Confirmation prompt
mock_manage_openhands_file.assert_called_once_with('/test')
mock_cli_confirm.assert_called_once_with(
'Do you wish to continue?', ['Yes, proceed', 'No, exit']
)
# Session initialization complete
assert 'Initialized session' not in output
# Welcome message
assert "Let's start building!" not in output
assert 'What do you want to build?' not in output
assert 'Type /help for help' not in output
@pytest.mark.asyncio
async def test_cli_startup_trusted_folder(
mock_runtime, mock_controller, mock_config, mock_agent, mock_memory, mock_read_task
):
buffer = StringIO()
with patch('openhands.core.cli.manage_openhands_file', return_value=True):
with patch(
'openhands.core.cli.cli_confirm', return_value=True
) as mock_cli_confirm:
with create_app_session(
input=create_pipe_input(), output=create_output(stdout=buffer)
):
mock_controller.status_callback = None
main_task = asyncio.create_task(main(asyncio.get_event_loop()))
await asyncio.sleep(0.1)
try:
await asyncio.wait_for(main_task, timeout=0.1)
except Exception:
main_task.cancel()
buffer.seek(0)
output = buffer.read()
# ASCII art banner
assert '___' in output
# Version information
assert 'OpenHands CLI v' in output
# Session initialization
assert 'Initializing session' in output
# Folder security confirmation should not be shown
assert 'Do you trust the files in this folder?' not in output
assert '/test' not in output
assert (
'OpenHands may read and execute files in this folder with your permission.'
not in output
)
# Confirmation prompt should not be shown
mock_cli_confirm.assert_not_called()
# Session initialization
assert 'Initialized session' in output
# Welcome message
assert "Let's start building!" in output
assert 'What do you want to build?' in output
assert 'Type /help for help' in output

View File

@@ -80,9 +80,9 @@ def test_step_with_pending_actions(agent: CodeActAgent):
def test_get_tools_default():
tools = get_tools(
codeact_enable_jupyter=True,
codeact_enable_llm_editor=True,
codeact_enable_browsing=True,
enable_jupyter=True,
enable_llm_editor=True,
enable_browsing=True,
)
assert len(tools) > 0
@@ -97,9 +97,9 @@ def test_get_tools_default():
def test_get_tools_with_options():
# Test with all options enabled
tools = get_tools(
codeact_enable_browsing=True,
codeact_enable_jupyter=True,
codeact_enable_llm_editor=True,
enable_browsing=True,
enable_jupyter=True,
enable_llm_editor=True,
)
tool_names = [tool['function']['name'] for tool in tools]
assert 'browser' in tool_names
@@ -108,9 +108,9 @@ def test_get_tools_with_options():
# Test with all options disabled
tools = get_tools(
codeact_enable_browsing=False,
codeact_enable_jupyter=False,
codeact_enable_llm_editor=False,
enable_browsing=False,
enable_jupyter=False,
enable_llm_editor=False,
)
tool_names = [tool['function']['name'] for tool in tools]
assert 'browser' not in tool_names

View File

@@ -331,10 +331,7 @@ def test_llm_summarizing_condenser_from_config():
config = LLMSummarizingCondenserConfig(
max_size=50,
keep_first=10,
llm_config=LLMConfig(
model='gpt-4o',
api_key='test_key',
),
llm_config=LLMConfig(model='gpt-4o', api_key='test_key', caching_prompt=True),
)
condenser = Condenser.from_config(config)
@@ -344,6 +341,10 @@ def test_llm_summarizing_condenser_from_config():
assert condenser.max_size == 50
assert condenser.keep_first == 10
# Since this condenser can't take advantage of caching, we intercept the
# passed config and manually flip the caching prompt to False.
assert not condenser.llm.config.caching_prompt
def test_llm_summarizing_condenser_invalid_config():
"""Test that LLMSummarizingCondenser raises error when keep_first > max_size."""
@@ -474,6 +475,7 @@ def test_llm_attention_condenser_from_config():
llm_config=LLMConfig(
model='gpt-4o',
api_key='test_key',
caching_prompt=True,
),
)
condenser = Condenser.from_config(config)
@@ -484,6 +486,10 @@ def test_llm_attention_condenser_from_config():
assert condenser.max_size == 50
assert condenser.keep_first == 10
# Since this condenser can't take advantage of caching, we intercept the
# passed config and manually flip the caching prompt to False.
assert not condenser.llm.config.caching_prompt
def test_llm_attention_condenser_invalid_config():
"""Test that LLMAttentionCondenser raises an error if the configured LLM doesn't support response schema."""
@@ -614,6 +620,7 @@ def test_structured_summary_condenser_from_config():
llm_config=LLMConfig(
model='gpt-4o',
api_key='test_key',
caching_prompt=True,
),
)
condenser = Condenser.from_config(config)
@@ -624,6 +631,10 @@ def test_structured_summary_condenser_from_config():
assert condenser.max_size == 50
assert condenser.keep_first == 10
# Since this condenser can't take advantage of caching, we intercept the
# passed config and manually flip the caching prompt to False.
assert not condenser.llm.config.caching_prompt
def test_structured_summary_condenser_invalid_config():
"""Test that StructuredSummaryCondenser raises error when keep_first > max_size."""

View File

@@ -834,9 +834,7 @@ def test_api_keys_repr_str():
# Test AgentConfig
# No attrs in AgentConfig have 'key' or 'token' in their name
agent_config = AgentConfig(
enable_prompt_extensions=True, codeact_enable_browsing=False
)
agent_config = AgentConfig(enable_prompt_extensions=True, enable_browsing=False)
for attr_name in AgentConfig.model_fields.keys():
if not attr_name.startswith('__'):
assert (
@@ -941,7 +939,7 @@ max_budget_per_task = 4.0
enable_prompt_extensions = true
[agent.BrowsingAgent]
codeact_enable_jupyter = false
enable_jupyter = false
"""
with open(temp_toml_file, 'w') as f:
@@ -952,7 +950,7 @@ codeact_enable_jupyter = false
codeact_config = default_config.get_agent_configs().get('CodeActAgent')
assert codeact_config.enable_prompt_extensions is True
browsing_config = default_config.get_agent_configs().get('BrowsingAgent')
assert browsing_config.codeact_enable_jupyter is False
assert browsing_config.enable_jupyter is False
def test_get_agent_config_arg(temp_toml_file):
@@ -963,11 +961,11 @@ max_budget_per_task = 4.0
[agent.CodeActAgent]
enable_prompt_extensions = false
codeact_enable_browsing = false
enable_browsing = false
[agent.BrowsingAgent]
enable_prompt_extensions = true
codeact_enable_jupyter = false
enable_jupyter = false
"""
with open(temp_toml_file, 'w') as f:
@@ -975,11 +973,11 @@ codeact_enable_jupyter = false
agent_config = get_agent_config_arg('CodeActAgent', temp_toml_file)
assert not agent_config.enable_prompt_extensions
assert not agent_config.codeact_enable_browsing
assert not agent_config.enable_browsing
agent_config2 = get_agent_config_arg('BrowsingAgent', temp_toml_file)
assert agent_config2.enable_prompt_extensions
assert not agent_config2.codeact_enable_jupyter
assert not agent_config2.enable_jupyter
def test_agent_config_custom_group_name(temp_toml_file):
@@ -1015,8 +1013,8 @@ def test_agent_config_from_toml_section():
# Test with base config and custom configs
agent_section = {
'enable_prompt_extensions': True,
'codeact_enable_browsing': True,
'CustomAgent1': {'codeact_enable_browsing': False},
'enable_browsing': True,
'CustomAgent1': {'enable_browsing': False},
'CustomAgent2': {'enable_prompt_extensions': False},
'InvalidAgent': {
'invalid_field': 'some_value' # This should be skipped but not affect others
@@ -1029,15 +1027,15 @@ def test_agent_config_from_toml_section():
# Verify the base config was correctly parsed
assert 'agent' in result
assert result['agent'].enable_prompt_extensions is True
assert result['agent'].codeact_enable_browsing is True
assert result['agent'].enable_browsing is True
# Verify custom configs were correctly parsed and inherit from base
assert 'CustomAgent1' in result
assert result['CustomAgent1'].codeact_enable_browsing is False # Overridden
assert result['CustomAgent1'].enable_browsing is False # Overridden
assert result['CustomAgent1'].enable_prompt_extensions is True # Inherited
assert 'CustomAgent2' in result
assert result['CustomAgent2'].codeact_enable_browsing is True # Inherited
assert result['CustomAgent2'].enable_browsing is True # Inherited
assert result['CustomAgent2'].enable_prompt_extensions is False # Overridden
# Verify the invalid config was skipped
@@ -1051,10 +1049,10 @@ def test_agent_config_from_toml_section_with_invalid_base():
# Test with invalid base config but valid custom configs
agent_section = {
'invalid_field': 'some_value', # This should be ignored in base config
'codeact_enable_jupyter': 'not_a_bool', # This should cause validation error
'enable_jupyter': 'not_a_bool', # This should cause validation error
'CustomAgent': {
'codeact_enable_browsing': False,
'codeact_enable_jupyter': True,
'enable_browsing': False,
'enable_jupyter': True,
},
}
@@ -1063,10 +1061,10 @@ def test_agent_config_from_toml_section_with_invalid_base():
# Verify a default base config was created despite the invalid fields
assert 'agent' in result
assert result['agent'].codeact_enable_browsing is True # Default value
assert result['agent'].codeact_enable_jupyter is True # Default value
assert result['agent'].enable_browsing is True # Default value
assert result['agent'].enable_jupyter is True # Default value
# Verify custom config was still processed correctly
assert 'CustomAgent' in result
assert result['CustomAgent'].codeact_enable_browsing is False
assert result['CustomAgent'].codeact_enable_jupyter is True
assert result['CustomAgent'].enable_browsing is False
assert result['CustomAgent'].enable_jupyter is True

View File

@@ -449,35 +449,84 @@ def test_completion_retry_with_llm_no_response_error_nonzero_temp(
This test verifies that when LLMNoResponseError is raised with a non-zero temperature:
1. The retry mechanism is triggered
2. The temperature remains unchanged (not set to 0.2)
3. After all retries are exhausted, the exception is propagated
3. After all retries are exhausted, the error is raised
"""
# Define a side effect function that always raises LLMNoResponseError
mock_litellm_completion.side_effect = LLMNoResponseError(
'LLM did not return a response'
)
# Create LLM instance and make a completion call with non-zero temperature
llm = LLM(config=default_config)
# We expect this to raise an exception after all retries are exhausted
with pytest.raises(LLMNoResponseError):
llm.completion(
messages=[{'role': 'user', 'content': 'Hello!'}],
stream=False,
temperature=0.7, # Initial temperature is non-zero
temperature=0.7, # Non-zero temperature
)
# Verify that litellm_completion was called multiple times (retries happened)
# The default_config has num_retries=2, so it should be called 2 times
assert mock_litellm_completion.call_count == 2
# Verify that litellm_completion was called the expected number of times
assert mock_litellm_completion.call_count == default_config.num_retries
# Check the temperature in the first call (should be 0.7)
first_call_kwargs = mock_litellm_completion.call_args_list[0][1]
assert first_call_kwargs.get('temperature') == 0.7
# Check that all calls used the original temperature
for call in mock_litellm_completion.call_args_list:
assert call[1].get('temperature') == 0.7
# Check the temperature in the second call (should remain 0.7, not changed to 0.2)
second_call_kwargs = mock_litellm_completion.call_args_list[1][1]
assert second_call_kwargs.get('temperature') == 0.7
@patch('openhands.llm.llm.litellm.get_model_info')
@patch('openhands.llm.llm.httpx.get')
def test_gemini_25_pro_function_calling(mock_httpx_get, mock_get_model_info):
"""
Test that Gemini 2.5 Pro models have function calling enabled by default.
This includes testing various model name formats with different prefixes.
"""
# Mock the model info response
mock_get_model_info.return_value = {
'max_input_tokens': 8000,
'max_output_tokens': 2000,
}
# Mock the httpx response for litellm proxy
mock_response = MagicMock()
mock_response.json.return_value = {
'data': [
{
'model_name': 'gemini-2.5-pro-preview-03-25',
'model_info': {
'max_input_tokens': 8000,
'max_output_tokens': 2000,
},
}
]
}
mock_httpx_get.return_value = mock_response
# Test cases with model names and expected function calling support
test_cases = [
# Base model names
('gemini-2.5-pro-preview-03-25', True),
('gemini-2.5-pro-exp-03-25', True),
# With gemini/ prefix
('gemini/gemini-2.5-pro-preview-03-25', True),
('gemini/gemini-2.5-pro-exp-03-25', True),
# With litellm_proxy/ prefix
('litellm_proxy/gemini-2.5-pro-preview-03-25', True),
('litellm_proxy/gemini-2.5-pro-exp-03-25', True),
# With openrouter/gemini/ prefix
('openrouter/gemini/gemini-2.5-pro-preview-03-25', True),
('openrouter/gemini/gemini-2.5-pro-exp-03-25', True),
# With litellm_proxy/gemini/ prefix
('litellm_proxy/gemini/gemini-2.5-pro-preview-03-25', True),
('litellm_proxy/gemini/gemini-2.5-pro-exp-03-25', True),
# Control case - a model that shouldn't have function calling
('gemini-1.0-pro', False),
]
for model_name, expected_support in test_cases:
config = LLMConfig(model=model_name, api_key='test_key')
llm = LLM(config)
assert (
llm.is_function_calling_active() == expected_support
), f'Expected function calling support to be {expected_support} for model {model_name}'
@patch('openhands.llm.llm.litellm_completion')