mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
28 Commits
ALL-1769/f
...
xw/evaluat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68ca0b6a9b | ||
|
|
e0fcd7a61e | ||
|
|
e9989d1085 | ||
|
|
49c515b252 | ||
|
|
2d05578c21 | ||
|
|
d05a6f30e1 | ||
|
|
10c81c39fb | ||
|
|
2d599349ef | ||
|
|
33caf5c6ca | ||
|
|
a9850766a7 | ||
|
|
77e2416def | ||
|
|
02af9865ec | ||
|
|
75ca2aa6b1 | ||
|
|
d820661592 | ||
|
|
1ff351a4f1 | ||
|
|
78b8e58561 | ||
|
|
fddbfce51a | ||
|
|
20d3766451 | ||
|
|
72b5e18898 | ||
|
|
03b8b8c19a | ||
|
|
7c2f1b075e | ||
|
|
883da1b28c | ||
|
|
bb98d94b35 | ||
|
|
d114c45135 | ||
|
|
36e092e0ac | ||
|
|
e2bb69908a | ||
|
|
cd33c5eac7 | ||
|
|
0f8a139fb5 |
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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
|
||||
|
||||
55
.github/workflows/ghcr-build.yml
vendored
55
.github/workflows/ghcr-build.yml
vendored
@@ -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
|
||||
|
||||
18
.github/workflows/lint.yml
vendored
18
.github/workflows/lint.yml
vendored
@@ -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
|
||||
|
||||
9
.github/workflows/openhands-resolver.yml
vendored
9
.github/workflows/openhands-resolver.yml
vendored
@@ -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 }} \
|
||||
|
||||
39
README.md
39
README.md
@@ -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 [
|
||||
|
||||

|
||||
|
||||
## ⚡ 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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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エディタが有効かどうか(関数呼び出しでのみ機能)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 user’s
|
||||
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
|
||||
|
||||
@@ -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 user’s
|
||||
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.
|
||||
|
||||
@@ -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 user’s
|
||||
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
|
||||
|
||||
## Hardened Docker Installation
|
||||
|
||||
|
||||
@@ -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 don’t 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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} />);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(', ')}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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**");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
51
frontend/__tests__/services/observations.test.ts
Normal file
51
frontend/__tests__/services/observations.test.ts
Normal 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: "",
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function ChatInput({
|
||||
disabled,
|
||||
showButton = true,
|
||||
value,
|
||||
maxRows = 4,
|
||||
maxRows = 16,
|
||||
onSubmit,
|
||||
onStop,
|
||||
onChange,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -24,6 +24,7 @@ const HANDLED_ACTIONS: OpenHandsEventType[] = [
|
||||
"browse_interactive",
|
||||
"edit",
|
||||
"recall",
|
||||
"think",
|
||||
];
|
||||
|
||||
function getRiskText(risk: ActionSecurityRisk) {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
ignored: ["**/node_modules/**", "**/.git/**"],
|
||||
ignored: ['**/node_modules/**', '**/.git/**'],
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
|
||||
@@ -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"}'
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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\n"
|
||||
f"\n\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())
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
369
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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 = "*"
|
||||
|
||||
@@ -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."""
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
368
tests/unit/test_cli_commands.py
Normal file
368
tests/unit/test_cli_commands.py
Normal 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
|
||||
@@ -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)
|
||||
301
tests/unit/test_cli_startup.py
Normal file
301
tests/unit/test_cli_startup.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user