Compare commits

..

53 Commits

Author SHA1 Message Date
OpenHands Bot
3d67fd8b10 🤖 Auto-fix Python linting issues 2024-12-15 17:26:36 +00:00
openhands
da943214b6 Fix issue #5612: [Bug]: Github Resolver outputs "The workflow to fix this issue encountered an error." despite the "Overview of Changes" has been successfully commented 2024-12-15 17:07:22 +00:00
Engel Nyst
590ebb6e47 Small fix and addition for token counting (#5550)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-15 15:12:05 +01:00
Engel Nyst
4716955960 Remove unused codeact-SWE agent (#5600)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-14 20:49:44 +01:00
Engel Nyst
f0257c793b fix: allow to continue when the agent is stuck in interactive mode (#5597)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-14 20:49:04 +01:00
Calvin Smith
7ef6fa666d feat(eval): Response Latency Tracking (#5588)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2024-12-13 22:51:13 +01:00
Robert Brennan
e0626a5741 Remove docker layer caching (#5587)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-13 16:13:26 -05:00
Robert Brennan
deb2d330b6 Reset iteration budget and update default max_iterations to 500 (#5590)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-13 20:46:40 +00:00
Engel Nyst
d733bc6bdd fix: serialize tool calls (#5553)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-13 20:51:03 +01:00
tofarr
d782bdf691 Persist jwt_secret in config file (#5353)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-13 11:05:19 -07:00
OpenHands
9c950f499e [Resolver] Reorder and comment workflow conditions (#5446)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2024-12-13 11:07:37 -05:00
Ryan H. Tran
8ae2fb636e Remove symlink use for swebench setup (#5549) 2024-12-13 22:18:14 +08:00
sp.wack
de75bd0690 fix(frontend): Prevent VSCode from opening when remounting (#5544) 2024-12-13 09:35:34 +04:00
tofarr
2fb45d410d Fix: Making the logs quieter (#5525) 2024-12-12 19:36:13 -07:00
mamoodi
8300cf0436 Specify unsupported paths for installing OpenHands (#5540) 2024-12-12 16:26:18 -05:00
mamoodi
7dd2bc569f Restart troubleshooting documentation. (#5317) 2024-12-12 15:49:18 -05:00
Robert Brennan
6e1fae29c9 Add note about design partner program to README (#5570) 2024-12-12 20:13:07 +00:00
sp.wack
19525a487c fix(frontend): Trim settings data when setting to storage (#5567) 2024-12-12 22:36:17 +04:00
Engel Nyst
7d0405282a Apply context window truncation for certain bad requests (#5566) 2024-12-12 18:11:59 +00:00
OpenHands
92c166551f Fix issue #5563: [Bug]: Prompt is not deleted when the user sends a message (#5564) 2024-12-12 10:06:40 -08:00
Xingyao Wang
ebb68b33db Fix issue #5527: Document repository customization and micro-agents (#5528)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-12 09:47:28 -08:00
sp.wack
37c46f1ed8 fix(frontend): Prevent push message from being rendered twice (#5546) 2024-12-12 09:19:48 -08:00
Engel Nyst
ac5190c283 Add voyage ai embeddings (#5547) 2024-12-12 09:19:05 -08:00
dependabot[bot]
ed3916b79b chore(deps-dev): bump @tanstack/eslint-plugin-query from 5.61.4 to 5.62.1 in /frontend in the eslint group (#5556)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 15:30:02 +00:00
mamoodi
27a647cd3e Release 0.15.2 (#5552) 2024-12-12 10:09:47 -05:00
sp.wack
42a536d450 Revert "chore(deps): bump the version-all group across 1 directory with 30 updates" (#5548) 2024-12-12 13:48:57 +04:00
dependabot[bot]
41e564dc41 chore(deps): bump the version-all group across 1 directory with 30 updates (#5522)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2024-12-12 12:18:26 +04:00
Graham Neubig
e979f51ea5 Fix chat input not clearing after image paste/drop (#5342)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-11 22:18:38 -08:00
Engel Nyst
425ccb0fbb Clean up empty content fix (revert #4935) (#5539) 2024-12-12 02:48:06 +00:00
Cheng Yang
7e4c1c733b feat(sandbox): add support for extra Docker build arguments (#5447) 2024-12-12 10:21:46 +08:00
Engel Nyst
ffd472d6b8 Update litellm (#5520) 2024-12-12 03:12:50 +01:00
mamoodi
2f2ea9ec91 Update the doc for headless to include no continue (#5537) 2024-12-12 02:03:06 +00:00
OpenHands
6a6ce5f3ee Fix issue #5478: Add color to the line next to "Ran a XXX Command" based on return value (#5483)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-12-11 23:20:29 +00:00
Xingyao Wang
907c65cc00 chore: add back accidentally removed repo info (#5532) 2024-12-12 05:51:05 +08:00
tofarr
a6d1a4c98f Fix: Redis listener attached at startup (#5516) 2024-12-11 09:39:57 -05:00
Robert Brennan
a60ee09881 Add docker layer caching to ghcr build (#5517) 2024-12-11 09:39:09 -05:00
Graham Neubig
246107c618 Parallize Python Unit tests (#5499) 2024-12-11 01:05:29 -08:00
Robert Brennan
5fa18511b3 minor fixes for when commands time out (#5518) 2024-12-10 21:55:02 +00:00
Rohit Malhotra
a482182a9e Remove Beta label from Browser tab (#5484)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-10 15:10:00 -05:00
tofarr
58d22a1905 Fix for issue where double scroll hides save button (#5488) 2024-12-10 19:50:49 +00:00
dependabot[bot]
17bbfa29a1 chore(deps): bump react-use from 17.5.1 to 17.6.0 in /docs in the version-all group (#5505)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 23:40:20 +04:00
tofarr
5fe116cfb1 Make layout responsive for mobile devices (#5475)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-11 03:31:20 +08:00
Xingyao Wang
e9637d40b9 Add browser observations to chat interface (#5514)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-11 03:30:44 +08:00
OpenHands
6de177521f Fix issue #5450: In openhands-resolver.yml, request code review from the person who initiated the workflow (#5451)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-12-10 12:19:55 -05:00
Xingyao Wang
9d36b80b96 Fix duplicate search messages in web browsing actions (#5511)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-10 15:51:03 +00:00
Engel Nyst
b11e905988 Verify costs script (#5469) 2024-12-10 14:20:53 +01:00
dependabot[bot]
39e5311233 chore(deps-dev): bump the llama group across 1 directory with 3 updates (#5503)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 07:00:45 +00:00
Engel Nyst
651ed1c3c8 Dependabot config for any browsergym-* package (#5501) 2024-12-10 01:27:11 -05:00
tofarr
e27c2e9c99 Fix: Auto-refresh file content when selected file changes (#5476)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-09 21:17:41 -05:00
Rohit Malhotra
cfe222e1d5 Fix issue #5162: docs: Improve GitHub token setup documentation in UI… (#5491)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-12-09 21:14:55 -05:00
tofarr
c872af4658 Doc: Added troubleshooting section for Nebulous docker errors (#5482) 2024-12-09 22:04:23 +00:00
OpenHands
99fa6c6a4a Fix issue #5186: [Bug]: Fix up inline code styles in chat window (#5226)
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2024-12-09 16:33:25 -05:00
OpenHands
3946f813a4 Fix issue #5471: Resolver: LLM_MODEL should use "variable" instead of "secret" (#5477) 2024-12-09 16:08:45 -05:00
118 changed files with 2047 additions and 1849 deletions

View File

@@ -1 +0,0 @@
The files in this directory configure a development container for GitHub Codespaces.

View File

@@ -1,15 +0,0 @@
{
"name": "OpenHands Codespaces",
"image": "mcr.microsoft.com/devcontainers/universal",
"customizations":{
"vscode":{
"extensions": [
"ms-python.python"
]
}
},
"onCreateCommand": "sh ./.devcontainer/on_create.sh",
"postCreateCommand": "make build",
"postStartCommand": "USE_HOST_NETWORK=True nohup bash -c 'make run &'"
}

View File

@@ -1,6 +0,0 @@
#!/usr/bin/env bash
sudo apt update
sudo apt install -y netcat
sudo add-apt-repository -y ppa:deadsnakes/ppa
sudo apt install -y python3.12
curl -sSL https://install.python-poetry.org | python3.12 -

View File

@@ -18,7 +18,7 @@ updates:
- "chromadb"
browsergym:
patterns:
- "browsergym"
- "browsergym*"
security-all:
applies-to: "security-updates"
patterns:

View File

@@ -59,7 +59,6 @@ jobs:
github.event_name == 'workflow_call' ||
github.event.label.name == 'fix-me' ||
github.event.label.name == 'fix-me-experimental' ||
(
((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
contains(github.event.comment.body, inputs.macro || '@openhands-agent') &&
@@ -140,15 +139,19 @@ jobs:
- name: Set environment variables
run: |
if [ -n "${{ github.event.review.body }}" ]; then
# Handle pull request events first
if [ -n "${{ github.event.pull_request.number }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
# Handle pull request review events
elif [ -n "${{ github.event.review.body }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
# Handle issue comment events that reference a PR
elif [ -n "${{ github.event.issue.pull_request }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
elif [ -n "${{ github.event.pull_request.number }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
# Handle regular issue events
else
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=issue" >> $GITHUB_ENV
@@ -239,7 +242,8 @@ jobs:
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type draft | tee pr_result.txt && \
--pr-type draft \
--reviewer ${{ github.actor }} | tee pr_result.txt && \
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
else
cd /tmp && python -m openhands.resolver.send_pull_request \

View File

@@ -42,7 +42,7 @@ jobs:
- name: Build Environment
run: make build
- name: Run Tests
run: poetry run pytest --forked --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_memory.py
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_memory.py
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
env:

View File

@@ -29,6 +29,11 @@ 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).
> [!IMPORTANT]
> Using OpenHands for work? We'd love to chat! Fill out
> [this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> to join our Design Partner program, where you'll get early access to commercial features and the opportunity to provide input on our product roadmap.
![App screenshot](./docs/static/img/screenshot.png)
## ⚡ Quick Start
@@ -40,10 +45,11 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.15-nikolaik
docker run -it --pull=always \
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.15-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/home/openhands/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \

View File

@@ -172,6 +172,10 @@ model = "gpt-4o"
# If model is vision capable, this option allows to disable image processing (useful for cost reduction).
#disable_vision = true
# Custom tokenizer to use for token counting
# https://docs.litellm.ai/docs/completion/token_usage
#custom_tokenizer = ""
[llm.gpt4o-mini]
api_key = "your-api-key"
model = "gpt-4o"
@@ -217,6 +221,9 @@ llm_config = 'gpt3'
# Use host network
#use_host_network = false
# runtime extra build args
#runtime_extra_build_args = ["--network=host", "--add-host=host.docker.internal:host-gateway"]
# Enable auto linting after editing
#enable_auto_lint = false

View File

@@ -42,6 +42,8 @@ ENV USE_HOST_NETWORK=false
ENV WORKSPACE_BASE=/opt/workspace_base
ENV OPENHANDS_BUILD_VERSION=$OPENHANDS_BUILD_VERSION
ENV SANDBOX_USER_ID=0
ENV FILE_STORE=local
ENV FILE_STORE_PATH=~/.openhands
RUN mkdir -p $WORKSPACE_BASE
RUN apt-get update -y \

View File

@@ -1,5 +1,8 @@
# Develop in Docker
> [!WARNING]
> This is not officially supported and may not work.
Install [Docker](https://docs.docker.com/engine/install/) on your host machine and run:
```bash

View File

@@ -9,7 +9,6 @@ Si vous trouvez plus d'informations ou une solution de contournement pour l'un d
:::tip
OpenHands ne prend en charge Windows que via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
Veuillez vous assurer d'exécuter toutes les commandes à l'intérieur de votre terminal WSL.
Consultez les [Notes pour les utilisateurs de WSL sur Windows](troubleshooting/windows) pour des guides de dépannage.
:::
## Problèmes courants

View File

@@ -1,66 +0,0 @@
# Notes pour les utilisateurs de WSL sur Windows
OpenHands ne prend en charge Windows que via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
Veuillez vous assurer d'exécuter toutes les commandes dans votre terminal WSL.
## Dépannage
### Recommandation : Ne pas exécuter en tant qu'utilisateur root
Pour des raisons de sécurité, il est fortement recommandé de ne pas exécuter OpenHands en tant qu'utilisateur root, mais en tant qu'utilisateur avec un UID non nul.
Références :
* [Pourquoi il est mauvais de se connecter en tant que root](https://askubuntu.com/questions/16178/why-is-it-bad-to-log-in-as-root)
* [Définir l'utilisateur par défaut dans WSL](https://www.tenforums.com/tutorials/128152-set-default-user-windows-subsystem-linux-distro-windows-10-a.html#option2)
Astuce concernant la 2ème référence : pour les utilisateurs d'Ubuntu, la commande pourrait en fait être "ubuntupreview" au lieu de "ubuntu".
---
### Erreur : 'docker' n'a pas pu être trouvé dans cette distribution WSL 2.
Si vous utilisez Docker Desktop, assurez-vous de le démarrer avant d'appeler toute commande docker depuis WSL.
Docker doit également avoir l'option d'intégration WSL activée.
---
### Installation de Poetry
* Si vous rencontrez des problèmes pour exécuter Poetry même après l'avoir installé pendant le processus de build, vous devrez peut-être ajouter son chemin binaire à votre environnement :
```sh
export PATH="$HOME/.local/bin:$PATH"
```
* Si make build s'arrête sur une erreur comme celle-ci :
```sh
ModuleNotFoundError: no module named <module-name>
```
Cela pourrait être un problème avec le cache de Poetry.
Essayez d'exécuter ces 2 commandes l'une après l'autre :
```sh
rm -r ~/.cache/pypoetry
make build
```
---
### L'objet NoneType n'a pas d'attribut 'request'
Si vous rencontrez des problèmes liés au réseau, tels que `NoneType object has no attribute 'request'` lors de l'exécution de `make run`, vous devrez peut-être configurer les paramètres réseau de WSL2. Suivez ces étapes :
* Ouvrez ou créez le fichier `.wslconfig` situé à `C:\Users\%username%\.wslconfig` sur votre machine hôte Windows.
* Ajoutez la configuration suivante au fichier `.wslconfig` :
```sh
[wsl2]
networkingMode=mirrored
localhostForwarding=true
```
* Enregistrez le fichier `.wslconfig`.
* Redémarrez complètement WSL2 en quittant toutes les instances WSL2 en cours d'exécution et en exécutant la commande `wsl --shutdown` dans votre invite de commande ou terminal.
* Après avoir redémarré WSL, essayez d'exécuter à nouveau `make run`.
Le problème de réseau devrait être résolu.

View File

@@ -7,7 +7,6 @@
:::tip
OpenHands 仅通过 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) 支持 Windows。
请确保在您的 WSL 终端内运行所有命令。
查看 [Windows 用户的 WSL 注意事项](troubleshooting/windows) 以获取一些故障排除指南。
:::
## 常见问题

View File

@@ -1,66 +0,0 @@
以下是翻译后的内容:
# 针对 Windows 上 WSL 用户的注意事项
OpenHands 仅通过 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) 支持 Windows。
请确保在您的 WSL 终端内运行所有命令。
## 故障排除
### 建议: 不要以 root 用户身份运行
出于安全原因,强烈建议不要以 root 用户身份运行 OpenHands,而是以具有非零 UID 的用户身份运行。
参考:
* [为什么以 root 身份登录不好](https://askubuntu.com/questions/16178/why-is-it-bad-to-log-in-as-root)
* [在 WSL 中设置默认用户](https://www.tenforums.com/tutorials/128152-set-default-user-windows-subsystem-linux-distro-windows-10-a.html#option2)
关于第二个参考的提示:对于 Ubuntu 用户,命令实际上可能是 "ubuntupreview" 而不是 "ubuntu"。
---
### 错误: 在此 WSL 2 发行版中找不到 'docker'。
如果您正在使用 Docker Desktop,请确保在从 WSL 内部调用任何 docker 命令之前启动它。
Docker 还需要激活 WSL 集成选项。
---
### Poetry 安装
* 如果您在构建过程中安装 Poetry 后仍然面临运行 Poetry 的问题,您可能需要将其二进制路径添加到环境中:
```sh
export PATH="$HOME/.local/bin:$PATH"
```
* 如果 make build 在如下错误上停止:
```sh
ModuleNotFoundError: no module named <module-name>
```
这可能是 Poetry 缓存的问题。
尝试依次运行这两个命令:
```sh
rm -r ~/.cache/pypoetry
make build
```
---
### NoneType 对象没有属性 'request'
如果您在执行 `make run` 时遇到与网络相关的问题,例如 `NoneType 对象没有属性 'request'`,您可能需要配置 WSL2 网络设置。请按照以下步骤操作:
* 在 Windows 主机上打开或创建位于 `C:\Users\%username%\.wslconfig``.wslconfig` 文件。
* 将以下配置添加到 `.wslconfig` 文件中:
```sh
[wsl2]
networkingMode=mirrored
localhostForwarding=true
```
* 保存 `.wslconfig` 文件。
* 通过退出任何正在运行的 WSL2 实例并在命令提示符或终端中执行 `wsl --shutdown` 命令来完全重启 WSL2。
* 重新启动 WSL 后,再次尝试执行 `make run`
网络问题应该得到解决。

View File

@@ -23,10 +23,75 @@ OpenHands provides a user-friendly Graphical User Interface (GUI) mode for inter
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
1. Locally (OSS): The user directly inputs their GitHub token.
2. Online (SaaS): The token is obtained through GitHub OAuth authentication.
1. **Locally (OSS)**: The user directly inputs their GitHub token
2. **Online (SaaS)**: The token is obtained through GitHub OAuth authentication
When you reach the `/app` route, the app checks if a token is present. If it finds one, it sets it in the environment for the agent to use.
#### Setting Up a Local GitHub Token
1. **Generate a Personal Access Token (PAT)**:
- Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic)
- Click "Generate new token (classic)"
- Required scopes:
- `repo` (Full control of private repositories)
- `workflow` (Update GitHub Action workflows)
- `read:org` (Read organization data)
2. **Enter Token in OpenHands**:
- Click the Settings button (gear icon) in the top right
- Navigate to the "GitHub" section
- Paste your token in the "GitHub Token" field
- Click "Save" to apply the changes
#### Organizational Token Policies
If you're working with organizational repositories, additional setup may be required:
1. **Check Organization Requirements**:
- Organization admins may enforce specific token policies
- Some organizations require tokens to be created with SSO enabled
- Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization)
2. **Verify Organization Access**:
- Go to your token settings on GitHub
- Look for the organization under "Organization access"
- If required, click "Enable SSO" next to your organization
- Complete the SSO authorization process
#### OAuth Authentication (Online Mode)
When using OpenHands in online mode, the GitHub OAuth flow:
1. Requests the following permissions:
- Repository access (read/write)
- Workflow management
- Organization read access
2. Authentication steps:
- Click "Sign in with GitHub" when prompted
- Review the requested permissions
- Authorize OpenHands to access your GitHub account
- If using an organization, authorize organization access if prompted
#### Troubleshooting
Common issues and solutions:
1. **Token Not Recognized**:
- Ensure the token is properly saved in settings
- Check that the token hasn't expired
- Verify the token has the required scopes
- Try regenerating the token
2. **Organization Access Denied**:
- Check if SSO is required but not enabled
- Verify organization membership
- Contact organization admin if token policies are blocking access
3. **Verifying Token Works**:
- The app will show a green checkmark if the token is valid
- Try accessing a repository to confirm permissions
- Check the browser console for any error messages
- Use the "Test Connection" button in settings if available
### Advanced Settings

View File

@@ -55,5 +55,5 @@ docker run -it \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.15 \
python -m openhands.core.main -t "write a bash script that prints hi"
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```

View File

@@ -0,0 +1,16 @@
# Persisting Session Data
Using the standard installation, the session data is stored in memory. Currently, if OpenHands' service is restarted,
previous sessions become invalid (a new secret is generated) and thus not recoverable.
## How to Persist Session Data
### Development Workflow
In the `config.toml` file, specify the following:
```
[core]
...
file_store="local"
file_store_path="/absolute/path/to/openhands/cache/directory"
jwt_secret="secretpass"
```

View File

@@ -0,0 +1,213 @@
# Micro-Agents
OpenHands uses specialized micro-agents to handle specific tasks and contexts efficiently. These micro-agents are small, focused components that provide specialized behavior and knowledge for particular scenarios.
## Overview
Micro-agents are defined in markdown files under the `openhands/agenthub/codeact_agent/micro/` directory. Each micro-agent is configured with:
- A unique name
- The agent type (typically CodeActAgent)
- Trigger keywords that activate the agent
- Specific instructions and capabilities
## Available Micro-Agents
### GitHub Agent
**File**: `github.md`
**Triggers**: `github`, `git`
The GitHub agent specializes in GitHub API interactions and repository management. It:
- Has access to a `GITHUB_TOKEN` for API authentication
- Follows strict guidelines for repository interactions
- Handles branch management and pull requests
- Uses the GitHub API instead of web browser interactions
Key features:
- Branch protection (prevents direct pushes to main/master)
- Automated PR creation
- Git configuration management
- API-first approach for GitHub operations
### NPM Agent
**File**: `npm.md`
**Triggers**: `npm`
Specializes in handling npm package management with specific focus on:
- Non-interactive shell operations
- Automated confirmation handling using Unix 'yes' command
- Package installation automation
### Custom Micro-Agents
You can create your own micro-agents by adding new markdown files to the micro-agents directory. Each file should follow this structure:
```markdown
---
name: agent_name
agent: CodeActAgent
triggers:
- trigger_word1
- trigger_word2
---
Instructions and capabilities for the micro-agent...
```
## Best Practices
When working with micro-agents:
1. **Use Appropriate Triggers**: Ensure your commands include the relevant trigger words to activate the correct micro-agent
2. **Follow Agent Guidelines**: Each agent has specific instructions and limitations - respect these for optimal results
3. **API-First Approach**: When available, use API endpoints rather than web interfaces
4. **Automation Friendly**: Design commands that work well in non-interactive environments
## Integration
Micro-agents are automatically integrated into OpenHands' workflow. They:
- Monitor incoming commands for their trigger words
- Activate when relevant triggers are detected
- Apply their specialized knowledge and capabilities
- Follow their specific guidelines and restrictions
## Example Usage
```bash
# GitHub agent example
git checkout -b feature-branch
git commit -m "Add new feature"
git push origin feature-branch
# NPM agent example
yes | npm install package-name
```
For more information about specific agents, refer to their individual documentation files in the micro-agents directory.
## Contributing a Micro-Agent
To contribute a new micro-agent to OpenHands, follow these guidelines:
### 1. Planning Your Micro-Agent
Before creating a micro-agent, consider:
- What specific problem or use case will it address?
- What unique capabilities or knowledge should it have?
- What trigger words make sense for activating it?
- What constraints or guidelines should it follow?
### 2. File Structure
Create a new markdown file in `openhands/agenthub/codeact_agent/micro/` with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
### 3. Required Components
Your micro-agent file must include:
1. **Front Matter**: YAML metadata at the start of the file:
```markdown
---
name: your_agent_name
agent: CodeActAgent
triggers:
- trigger_word1
- trigger_word2
---
```
2. **Instructions**: Clear, specific guidelines for the agent's behavior:
```markdown
You are responsible for [specific task/domain].
Key responsibilities:
1. [Responsibility 1]
2. [Responsibility 2]
Guidelines:
- [Guideline 1]
- [Guideline 2]
Examples of usage:
[Example 1]
[Example 2]
```
### 4. Best Practices for Micro-Agent Development
1. **Clear Scope**: Keep the agent focused on a specific domain or task
2. **Explicit Instructions**: Provide clear, unambiguous guidelines
3. **Useful Examples**: Include practical examples of common use cases
4. **Safety First**: Include necessary warnings and constraints
5. **Integration Awareness**: Consider how the agent interacts with other components
### 5. Testing Your Micro-Agent
Before submitting:
1. Test the agent with various prompts
2. Verify trigger words activate the agent correctly
3. Ensure instructions are clear and comprehensive
4. Check for potential conflicts with existing agents
### 6. Example Implementation
Here's a template for a new micro-agent:
```markdown
---
name: docker
agent: CodeActAgent
triggers:
- docker
- container
---
You are responsible for Docker container management and Dockerfile creation.
Key responsibilities:
1. Create and modify Dockerfiles
2. Manage container lifecycle
3. Handle Docker Compose configurations
Guidelines:
- Always use official base images when possible
- Include necessary security considerations
- Follow Docker best practices for layer optimization
Examples:
1. Creating a Dockerfile:
```dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
```
2. Docker Compose usage:
```yaml
version: '3'
services:
web:
build: .
ports:
- "3000:3000"
```
Remember to:
- Validate Dockerfile syntax
- Check for security vulnerabilities
- Optimize for build time and image size
```
### 7. Submission Process
1. Create your micro-agent file in the correct directory
2. Test thoroughly
3. Submit a pull request with:
- The new micro-agent file
- Updated documentation if needed
- Description of the agent's purpose and capabilities
Remember that micro-agents are a powerful way to extend OpenHands' capabilities in specific domains. Well-designed agents can significantly improve the system's ability to handle specialized tasks.

View File

@@ -2,6 +2,11 @@
When working with OpenHands AI software developer, it's crucial to provide clear and effective prompts. This guide outlines best practices for creating prompts that will yield the most accurate and useful responses.
## Table of Contents
- [Characteristics of Good Prompts](#characteristics-of-good-prompts)
- [Customizing Prompts for your Project](#customizing-prompts-for-your-project)
## Characteristics of Good Prompts
Good prompts are:
@@ -39,3 +44,63 @@ Good prompts are:
Remember, the more precise and informative your prompt is, the better the AI can assist you in developing or modifying the OpenHands software.
See [Getting Started with OpenHands](./getting-started) for more examples of helpful prompts.
## Customizing Prompts for your Project
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context and guidelines. This section explains how to optimize OpenHands for your project.
### Repository Configuration
You can customize OpenHands' behavior for your repository by creating a `.openhands_instructions` file in your repository's root directory. This file should contain:
1. **Repository Overview**: A brief description of your project's purpose and architecture
2. **Directory Structure**: Key directories and their purposes
3. **Development Guidelines**: Project-specific coding standards and practices
4. **Testing Requirements**: How to run tests and what types of tests are required
5. **Setup Instructions**: Steps needed to build and run the project
Example `.openhands_instructions` file:
```
Repository: MyProject
Description: A web application for task management
Directory Structure:
- src/: Main application code
- tests/: Test files
- docs/: Documentation
Setup:
- Run `npm install` to install dependencies
- Use `npm run dev` for development
- Run `npm test` for testing
Guidelines:
- Follow ESLint configuration
- Write tests for all new features
- Use TypeScript for new code
```
### Customizing Prompts
When working with a customized repository:
1. **Reference Project Standards**: Mention specific coding standards or patterns used in your project
2. **Include Context**: Reference relevant documentation or existing implementations
3. **Specify Testing Requirements**: Include project-specific testing requirements in your prompts
Example customized prompt:
```
Add a new task completion feature to src/components/TaskList.tsx following our existing component patterns.
Include unit tests in tests/components/ and update the documentation in docs/features/.
The component should use our shared styling from src/styles/components.
```
### Best Practices for Repository Customization
1. **Keep Instructions Updated**: Regularly update your `.openhands_instructions` file as your project evolves
2. **Be Specific**: Include specific paths, patterns, and requirements unique to your project
3. **Document Dependencies**: List all tools and dependencies required for development
4. **Include Examples**: Provide examples of good code patterns from your project
5. **Specify Conventions**: Document naming conventions, file organization, and code style preferences
By customizing OpenHands for your repository, you'll get more accurate and consistent results that align with your project's standards and requirements.

View File

@@ -1,180 +1,44 @@
# 🚧 Troubleshooting
There are some error messages that frequently get reported by users.
We'll try to make the install process easier, but for now you can look for your error message below and see if there are any workarounds.
If you find more information or a workaround for one of these issues, please open a *PR* to add details to this file.
:::tip
OpenHands only supports Windows via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
Please be sure to run all commands inside your WSL terminal.
Check out [Notes for WSL on Windows Users](troubleshooting/windows) for some troubleshooting guides.
OpenHands only supports Windows via WSL. Please be sure to run all commands inside your WSL terminal.
:::
## Common Issues
### Launch docker client failed
* [Unable to connect to Docker](#unable-to-connect-to-docker)
* [404 Resource not found](#404-resource-not-found)
* [`make build` getting stuck on package installations](#make-build-getting-stuck-on-package-installations)
* [Sessions are not restored](#sessions-are-not-restored)
* [Connection to host.docker.internal timed out](#connection-to-host-docker-internal-timed-out)
**Description**
### Unable to connect to Docker
[GitHub Issue](https://github.com/All-Hands-AI/OpenHands/issues/1226)
**Symptoms**
```bash
Error creating controller. Please check Docker is running and visit `https://docs.all-hands.dev/modules/usage/troubleshooting` for more debugging information.
When running OpenHands, the following error is seen:
```
Launch docker client failed. Please make sure you have installed docker and started docker desktop/daemon.
```
```bash
docker.errors.DockerException: Error while fetching server API version: ('Connection aborted.', FileNotFoundError(2, 'No such file or directory'))
```
**Details**
OpenHands uses a Docker container to do its work safely, without potentially breaking your machine.
**Workarounds**
* Run `docker ps` to ensure that docker is running
* Make sure you don't need `sudo` to run docker [see here](https://www.baeldung.com/linux/docker-run-without-sudo)
* If you are on a Mac, check the [permissions requirements](https://docs.docker.com/desktop/mac/permission-requirements/) and in particular consider enabling the `Allow the default Docker socket to be used` under `Settings > Advanced` in Docker Desktop.
* In addition, upgrade your Docker to the latest version under `Check for Updates`
**Resolution**
Try these in order:
* Confirm `docker` is running on your system. You should be able to run `docker ps` in the terminal successfully.
* 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.
---
### `404 Resource not found`
**Symptoms**
# Development Workflow Specific
### Error building runtime docker image
```python
Traceback (most recent call last):
File "/app/.venv/lib/python3.12/site-packages/litellm/llms/openai.py", line 414, in completion
raise e
File "/app/.venv/lib/python3.12/site-packages/litellm/llms/openai.py", line 373, in completion
response = openai_client.chat.completions.create(**data, timeout=timeout) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/_utils/_utils.py", line 277, in wrapper
return func(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/resources/chat/completions.py", line 579, in create
return self._post(
^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1232, in post
return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 921, in request
return self._request(
^^^^^^^^^^^^^^
File "/app/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1012, in _request
raise self._make_status_error_from_response(err.response) from None
openai.NotFoundError: Error code: 404 - {'error': {'code': '404', 'message': 'Resource not found'}}
**Description**
Attempts to start a new session fail, and errors with terms like the following appear in the logs:
```
debian-security bookworm-security
InRelease At least one invalid signature was encountered.
```
**Details**
This seems to happen when the hash of an existing external library changes and your local docker instance has
cached a previous version. To work around this, please try the following:
This happens when LiteLLM (our library for connecting to different LLM providers) can't find
the API endpoint you're trying to connect to. Most often this happens for Azure or ollama users.
**Workarounds**
* Check that you've set `LLM_BASE_URL` properly
* Check that the model is set properly, based on the [LiteLLM docs](https://docs.litellm.ai/docs/providers)
* If you're running inside the UI, be sure to set the `model` in the settings modal
* If you're running headless (via main.py) be sure to set `LLM_MODEL` in your env/config
* Make sure you've followed any special instructions for your LLM provider
* [Azure](/modules/usage/llms/azure-llms)
* [Google](/modules/usage/llms/google-llms)
* Make sure your API key is correct
* See if you can connect to the LLM using `curl`
* Try [connecting via LiteLLM directly](https://github.com/BerriAI/litellm) to test your setup
---
### `make build` getting stuck on package installations
**Symptoms**
Package installation stuck on `Pending...` without any error message:
```bash
Package operations: 286 installs, 0 updates, 0 removals
- Installing certifi (2024.2.2): Pending...
- Installing h11 (0.14.0): Pending...
- Installing idna (3.7): Pending...
- Installing sniffio (1.3.1): Pending...
- Installing typing-extensions (4.11.0): Pending...
```
**Details**
In rare cases, `make build` can seemingly get stuck on package installations
without any error message.
**Workarounds**
The package installer Poetry may miss a configuration setting for where credentials are to be looked up (keyring).
First check with `env` if a value for `PYTHON_KEYRING_BACKEND` exists.
If not, run the below command to set it to a known value and retry the build:
```bash
export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
```
---
### Sessions are not restored
**Symptoms**
OpenHands usually asks whether to resume or start a new session when opening the UI.
But clicking "Resume" still starts a fresh new chat.
**Details**
With a standard installation as of today session data is stored in memory.
Currently, if OpenHands's service is restarted, previous sessions become
invalid (a new secret is generated) and thus not recoverable.
**Workarounds**
* Change configuration to make sessions persistent by editing the `config.toml`
file (in OpenHands's root folder) by specifying a `file_store` and an
absolute `file_store_path`:
```toml
file_store="local"
file_store_path="/absolute/path/to/openhands/cache/directory"
```
* Add a fixed jwt secret in your .bashrc, like below, so that previous session id's
should stay accepted.
```bash
EXPORT JWT_SECRET=A_CONST_VALUE
```
---
### Connection to host docker internal timed out
**Symptoms**
When you start the server using the docker command from the main [README](https://github.com/All-Hands-AI/OpenHands/README.md), you get a long timeout
followed by the a stack trace containing messages like:
* `Connection to host.docker.internal timed out. (connect timeout=310)`
* `Max retries exceeded with url: /alive`
**Details**
If Docker Engine is installed rather than Docker Desktop, the main command will not work as expected.
Docker Desktop includes easy DNS configuration for connecting processes running in different containers
which OpenHands makes use of when the main server is running inside a docker container.
(Further details: https://forums.docker.com/t/difference-between-docker-desktop-and-docker-engine/124612)
**Workarounds**
* [Install Docker Desktop](https://www.docker.com/products/docker-desktop/)
* Run OpenHands in [Development Mode](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md),
So that the main server is not run inside a container, but still creates dockerized runtime sandboxes.
* Stop any containers where the name has the prefix `openhands-runtime-` :
`docker ps --filter name=openhands-runtime- --filter status=running -aq | xargs docker stop`
* Remove any containers where the name has the prefix `openhands-runtime-` :
`docker rmi $(docker images --filter name=openhands-runtime- -q --no-trunc)`
* Stop and Remove any containers / images where the name has the prefix `openhands-runtime-`
* Prune containers / images : `docker container prune -f && docker image prune -f`

View File

@@ -1,64 +0,0 @@
# Notes for WSL on Windows Users
OpenHands only supports Windows via [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
Please be sure to run all commands inside your WSL terminal.
## Troubleshooting
### Recommendation: Do not run as root user
For security reasons, it is highly recommended to not run OpenHands as the root user, but a user with a non-zero UID.
References:
* [Why it is bad to login as root](https://askubuntu.com/questions/16178/why-is-it-bad-to-log-in-as-root)
* [Set default user in WSL](https://www.tenforums.com/tutorials/128152-set-default-user-windows-subsystem-linux-distro-windows-10-a.html#option2)
Hint about the 2nd reference: for Ubuntu users, the command could actually be "ubuntupreview" instead of "ubuntu".
---
### Error: 'docker' could not be found in this WSL 2 distro.
If you are using Docker Desktop, make sure to start it before calling any docker command from inside WSL.
Docker also needs to have the WSL integration option activated.
---
### Poetry Installation
* If you face issues running Poetry even after installing it during the build process, you may need to add its binary path to your environment:
```sh
export PATH="$HOME/.local/bin:$PATH"
```
* If make build stops on an error like this:
```sh
ModuleNotFoundError: no module named <module-name>
```
This could be an issue with Poetry's cache.
Try to run these 2 commands after another:
```sh
rm -r ~/.cache/pypoetry
make build
```
---
### NoneType object has no attribute 'request'
If you are experiencing issues related to networking, such as `NoneType object has no attribute 'request'` when executing `make run`, you may need to configure your WSL2 networking settings. Follow these steps:
* Open or create the `.wslconfig` file located at `C:\Users\%username%\.wslconfig` on your Windows host machine.
* Add the following configuration to the `.wslconfig` file:
```sh
[wsl2]
networkingMode=mirrored
localhostForwarding=true
```
* Save the `.wslconfig` file.
* Restart WSL2 completely by exiting any running WSL2 instances and executing the command `wsl --shutdown` in your command prompt or terminal.
* After restarting WSL, attempt to execute `make run` again.
The networking issue should be resolved.

View File

@@ -1,71 +0,0 @@
# ⬆️ Upgrade Guide
## 0.8.0 (2024-07-13)
### Config breaking changes
In this release we introduced a few breaking changes to backend configurations.
If you have only been using OpenHands via frontend (web GUI), nothing needs
to be taken care of.
Here's a list of breaking changes in configs. They only apply to users who
use OpenHands CLI via `main.py`. For more detail, see [#2756](https://github.com/All-Hands-AI/OpenHands/pull/2756).
#### Removal of --model-name option from main.py
Please note that `--model-name`, or `-m` option, no longer exists. You should set up the LLM
configs in `config.toml` or via environmental variables.
#### LLM config groups must be subgroups of 'llm'
Prior to release 0.8, you can use arbitrary name for llm config in `config.toml`, e.g.
```toml
[gpt-4o]
model="gpt-4o"
api_key="<your_api_key>"
```
and then use `--llm-config` CLI argument to specify the desired LLM config group
by name. This no longer works. Instead, the config group must be under `llm` group,
e.g.:
```toml
[llm.gpt-4o]
model="gpt-4o"
api_key="<your_api_key>"
```
If you have a config group named `llm`, no need to change it, it will be used
as the default LLM config group.
#### 'agent' group no longer contains 'name' field
Prior to release 0.8, you may or may not have a config group named `agent` that
looks like this:
```toml
[agent]
name="CodeActAgent"
memory_max_threads=2
```
Note the `name` field is now removed. Instead, you should put `default_agent` field
under `core` group, e.g.
```toml
[core]
# other configs
default_agent='CodeActAgent'
[agent]
llm_config='llm'
memory_max_threads=2
[agent.CodeActAgent]
llm_config='gpt-4o'
```
Note that similar to `llm` subgroups, you can also define `agent` subgroups.
Moreover, an agent can be associated with a specific LLM config group. For more
detail, see the examples in `config.template.toml`.

View File

@@ -18,7 +18,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"react-use": "^17.5.1"
"react-use": "^17.6.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.5.1",
@@ -15264,9 +15264,9 @@
}
},
"node_modules/react-use": {
"version": "17.5.1",
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.1.tgz",
"integrity": "sha512-LG/uPEVRflLWMwi3j/sZqR00nF6JGqTTDblkXK2nzXsIvij06hXl1V/MZIlwj1OKIQUtlh1l9jK8gLsRyCQxMg==",
"version": "17.6.0",
"resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz",
"integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==",
"dependencies": {
"@types/js-cookie": "^2.2.6",
"@xobotyi/scrollbar-width": "^1.9.5",

View File

@@ -25,7 +25,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.4.0",
"react-use": "^17.5.1"
"react-use": "^17.6.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.5.1",

View File

@@ -14,9 +14,20 @@ const sidebars: SidebarsConfig = {
id: 'usage/getting-started',
},
{
type: 'doc',
label: 'Prompting Best Practices',
id: 'usage/prompting-best-practices',
type: 'category',
label: 'Prompting',
items: [
{
type: 'doc',
label: 'Best Practices',
id: 'usage/prompting-best-practices',
},
{
type: 'doc',
label: 'Micro-Agents',
id: 'usage/micro-agents',
},
],
},
{
type: 'category',
@@ -110,6 +121,11 @@ const sidebars: SidebarsConfig = {
label: 'Custom Sandbox',
id: 'usage/how-to/custom-sandbox-guide',
},
{
type: 'doc',
label: 'Persist Session Data',
id: 'usage/how-to/persist-session-data',
},
],
},
{

View File

@@ -1,28 +0,0 @@
CODEACT_SWE_PROMPT = """Now, you're going to solve this issue on your own. Your terminal session has started and you're in the repository's root directory. You can use any bash commands or the special interface to help you. Edit all the files you need to and run any checks or tests that you want.
Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command.
When you're satisfied with all of the changes you've made, you can use the "finish" tool to finish the interaction.
Note however that you cannot use any interactive session commands (e.g. vim) in this environment, but you can write scripts and run them. E.g. you can write a python script and then run it with `python <script_name>.py`.
NOTE ABOUT THE EDIT COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line!
IMPORTANT TIPS:
1. Always start by trying to replicate the bug that the issues discusses.
If the issue includes code for reproducing the bug, we recommend that you re-implement that in your environment, and run it to make sure you can reproduce the bug.
Then start trying to fix it.
When you think you've fixed the bug, re-run the bug reproduction script to make sure that the bug has indeed been fixed.
If the bug reproduction script does not print anything when it successfully runs, we recommend adding a print("Script completed successfully, no errors.") command at the end of the file,
so that you can be sure that the script indeed ran fine all the way through.
2. If you run a command and it doesn't work, try running a different command. A command that did not work once will not work the second time unless you modify it!
3. If you open a file and need to get to an area around a specific line that is not in the first 100 lines, say line 583, don't just use the scroll_down command multiple times. Instead, use the goto 583 command. It's much quicker.
4. If the bug reproduction script requires inputting/reading a specific file, such as buggy-input.png, and you'd like to understand how to input that file, conduct a search in the existing repo code, to see whether someone else has already done that. Do this by running the command: find_file("buggy-input.png") If that doesn't work, use the linux 'find' command.
5. Always make sure to look at the currently open file and the current working directory (which appears right after the currently open file). The currently open file might be in a different directory than the working directory! Note that some commands, such as 'create', open files, so they might change the current open file.
6. When editing files, it is easy to accidentally specify a wrong line number or to write code with incorrect indentation. Always check the code after you issue an edit to make sure that it reflects what you wanted to accomplish. If it didn't, issue another command to fix it.
[Current directory: /workspace/{workspace_dir_name}]
"""

View File

@@ -9,7 +9,7 @@ import toml
from datasets import load_dataset
import openhands.agenthub
from evaluation.benchmarks.swe_bench.prompt import CODEACT_SWE_PROMPT
from evaluation.utils.shared import (
EvalException,
EvalMetadata,
@@ -45,7 +45,6 @@ RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'tru
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
'CodeActSWEAgent': codeact_user_response,
}
@@ -56,39 +55,27 @@ def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
def get_instruction(instance: pd.Series, metadata: EvalMetadata):
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
# Prepare instruction
if metadata.agent_class == 'CodeActSWEAgent':
instruction = (
'We are currently solving the following issue within our repository. Here is the issue text:\n'
'--- BEGIN ISSUE ---\n'
f'{instance.problem_statement}\n'
'--- END ISSUE ---\n\n'
)
if USE_HINT_TEXT and instance.hints_text:
instruction += (
f'--- BEGIN HINTS ---\n{instance.hints_text}\n--- END HINTS ---\n'
)
instruction += CODEACT_SWE_PROMPT.format(workspace_dir_name=workspace_dir_name)
else:
# Instruction based on Anthropic's official trajectory
# https://github.com/eschluntz/swe-bench-experiments/tree/main/evaluation/verified/20241022_tools_claude-3-5-sonnet-updated/trajs
instruction = (
'<uploaded_files>\n'
f'/workspace/{workspace_dir_name}\n'
'</uploaded_files>\n'
f"I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following PR description:\n\n"
f'<pr_description>\n'
f'{instance.problem_statement}\n'
'</pr_description>\n\n'
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <pr_description> are met?\n'
"I've already taken care of all changes to any of the test files described in the <pr_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!\n"
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied.\n'
'Follow these steps to resolve the issue:\n'
'1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.\n'
'2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
'3. Edit the sourcecode of the repo to resolve the issue\n'
'4. Rerun your reproduce script and confirm that the error is fixed!\n'
'5. Think about edgecases and make sure your fix handles them as well\n'
"Your thinking should be thorough and so it's fine if it's very long.\n"
# Instruction based on Anthropic's official trajectory
# https://github.com/eschluntz/swe-bench-experiments/tree/main/evaluation/verified/20241022_tools_claude-3-5-sonnet-updated/trajs
instruction = (
'<uploaded_files>\n'
f'/workspace/{workspace_dir_name}\n'
'</uploaded_files>\n'
f"I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following PR description:\n\n"
f'<pr_description>\n'
f'{instance.problem_statement}\n'
'</pr_description>\n\n'
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <pr_description> are met?\n'
"I've already taken care of all changes to any of the test files described in the <pr_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!\n"
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied.\n'
'Follow these steps to resolve the issue:\n'
'1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.\n'
'2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
'3. Edit the sourcecode of the repo to resolve the issue\n'
'4. Rerun your reproduce script and confirm that the error is fixed!\n'
'5. Think about edgecases and make sure your fix handles them as well\n'
"Your thinking should be thorough and so it's fine if it's very long.\n"
)
if RUN_WITH_BROWSING:

View File

@@ -0,0 +1,104 @@
import argparse
import pandas as pd
from openhands.core.logger import openhands_logger as logger
def verify_instance_costs(row: pd.Series) -> float:
"""
Verifies that the accumulated_cost matches the sum of individual costs in metrics.
Also checks for duplicate consecutive costs which might indicate buggy counting.
If the consecutive costs are identical, the file is affected by this bug:
https://github.com/All-Hands-AI/OpenHands/issues/5383
Args:
row: DataFrame row containing instance data with metrics
Returns:
float: The verified total cost for this instance (corrected if needed)
"""
try:
metrics = row.get('metrics')
if not metrics:
logger.warning(f"Instance {row['instance_id']}: No metrics found")
return 0.0
accumulated = metrics.get('accumulated_cost')
costs = metrics.get('costs', [])
if accumulated is None:
logger.warning(
f"Instance {row['instance_id']}: No accumulated_cost in metrics"
)
return 0.0
# Check for duplicate consecutive costs and systematic even-odd pairs
has_duplicate = False
all_pairs_match = True
# Check each even-odd pair (0-1, 2-3, etc.)
for i in range(0, len(costs) - 1, 2):
if abs(costs[i]['cost'] - costs[i + 1]['cost']) < 1e-6:
has_duplicate = True
logger.debug(
f"Instance {row['instance_id']}: Possible buggy double-counting detected! "
f"Steps {i} and {i+1} have identical costs: {costs[i]['cost']:.2f}"
)
else:
all_pairs_match = False
break
# Calculate total cost, accounting for buggy double counting if detected
if len(costs) >= 2 and has_duplicate and all_pairs_match:
paired_steps_cost = sum(
cost_entry['cost']
for cost_entry in costs[: -1 if len(costs) % 2 else None]
)
real_paired_cost = paired_steps_cost / 2
unpaired_cost = costs[-1]['cost'] if len(costs) % 2 else 0
total_cost = real_paired_cost + unpaired_cost
else:
total_cost = sum(cost_entry['cost'] for cost_entry in costs)
if not abs(total_cost - accumulated) < 1e-6:
logger.warning(
f"Instance {row['instance_id']}: Cost mismatch: "
f"accumulated: {accumulated:.2f}, sum of costs: {total_cost:.2f}, "
)
return total_cost
except Exception as e:
logger.error(
f"Error verifying costs for instance {row.get('instance_id', 'UNKNOWN')}: {e}"
)
return 0.0
def main():
parser = argparse.ArgumentParser(
description='Verify costs in SWE-bench output file'
)
parser.add_argument(
'input_filepath', type=str, help='Path to the output.jsonl file'
)
args = parser.parse_args()
try:
# Load and verify the JSONL file
df = pd.read_json(args.input_filepath, lines=True)
logger.info(f'Loaded {len(df)} instances from {args.input_filepath}')
# Verify costs for each instance and sum up total
total_cost = df.apply(verify_instance_costs, axis=1).sum()
logger.info(f'Total verified cost across all instances: ${total_cost:.2f}')
except Exception as e:
logger.error(f'Failed to process file: {e}')
raise
if __name__ == '__main__':
main()

View File

@@ -33,7 +33,7 @@ if [ -d /workspace/$WORKSPACE_NAME ]; then
rm -rf /workspace/$WORKSPACE_NAME
fi
mkdir -p /workspace
ln -s /testbed /workspace/$WORKSPACE_NAME
mv /testbed /workspace/$WORKSPACE_NAME
# Activate instance-specific environment
. /opt/miniconda3/etc/profile.d/conda.sh

View File

@@ -11,6 +11,7 @@ describe("Browser", () => {
browser: {
url: "https://example.com",
screenshotSrc: "",
updateCount: 0,
},
},
});
@@ -26,6 +27,7 @@ describe("Browser", () => {
url: "https://example.com",
screenshotSrc:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
updateCount: 0,
},
},
});

View File

@@ -70,4 +70,12 @@ describe("ChatMessage", () => {
);
expect(screen.getByTestId("custom-component")).toBeInTheDocument();
});
it("should apply correct styles to inline code", () => {
render(<ChatMessage type="assistant" message="Here is some `inline code` text" />);
const codeElement = screen.getByText("inline code");
expect(codeElement.tagName.toLowerCase()).toBe("code");
expect(codeElement.closest("article")).not.toBeNull();
});
});

View File

@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import { ExpandableMessage } from "#/components/features/chat/expandable-message";
describe("ExpandableMessage", () => {
it("should render with neutral border for non-action messages", () => {
renderWithProviders(<ExpandableMessage message="Hello" type="thought" />);
const element = screen.getByText("Hello");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
});
it("should render with neutral border for error messages", () => {
renderWithProviders(<ExpandableMessage message="Error occurred" type="error" />);
const element = screen.getByText("Error occurred");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
});
it("should render with success icon for successful action messages", () => {
renderWithProviders(
<ExpandableMessage
message="Command executed successfully"
type="action"
success={true}
/>
);
const element = screen.getByText("Command executed successfully");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
const icon = screen.getByTestId("status-icon");
expect(icon).toHaveClass("fill-success");
});
it("should render with error icon for failed action messages", () => {
renderWithProviders(
<ExpandableMessage
message="Command failed"
type="action"
success={false}
/>
);
const element = screen.getByText("Command failed");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
const icon = screen.getByTestId("status-icon");
expect(icon).toHaveClass("fill-danger");
});
it("should render with neutral border and no icon for action messages without success prop", () => {
renderWithProviders(<ExpandableMessage message="Running command" type="action" />);
const element = screen.getByText("Running command");
const container = element.closest("div.flex.gap-2.items-center.justify-between");
expect(container).toHaveClass("border-neutral-300");
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
});
});

View File

@@ -1,4 +1,4 @@
import { render, screen, within } from "@testing-library/react";
import { render, screen, within, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
@@ -131,4 +131,60 @@ describe("InteractiveChatBox", () => {
await user.click(stopButton);
expect(onStopMock).toHaveBeenCalledOnce();
});
it("should handle image upload and message submission correctly", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
const onStop = vi.fn();
const onChange = vi.fn();
const { rerender } = render(
<InteractiveChatBox
onSubmit={onSubmit}
onStop={onStop}
onChange={onChange}
value="test message"
/>
);
// Upload an image via the upload button - this should NOT clear the text input
const file = new File(["dummy content"], "test.png", { type: "image/png" });
const input = screen.getByTestId("upload-image-input");
await user.upload(input, file);
// Verify text input was not cleared
expect(screen.getByRole("textbox")).toHaveValue("test message");
expect(onChange).not.toHaveBeenCalledWith("");
// Submit the message with image
const submitButton = screen.getByRole("button", { name: "Send" });
await user.click(submitButton);
// Verify onSubmit was called with the message and image
expect(onSubmit).toHaveBeenCalledWith("test message", [file]);
// Verify onChange was called to clear the text input
expect(onChange).toHaveBeenCalledWith("");
// Simulate parent component updating the value prop
rerender(
<InteractiveChatBox
onSubmit={onSubmit}
onStop={onStop}
onChange={onChange}
value=""
/>
);
// Verify the text input was cleared
expect(screen.getByRole("textbox")).toHaveValue("");
// Upload another image - this should NOT clear the text input
onChange.mockClear();
await user.upload(input, file);
// Verify text input is still empty and onChange was not called
expect(screen.getByRole("textbox")).toHaveValue("");
expect(onChange).not.toHaveBeenCalled();
});
});

View File

@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.15.1",
"version": "0.15.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.15.1",
"version": "0.15.2",
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@nextui-org/react": "^2.4.8",
@@ -51,7 +51,7 @@
"@playwright/test": "^1.48.2",
"@react-router/dev": "^7.0.1",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/eslint-plugin-query": "^5.60.1",
"@tanstack/eslint-plugin-query": "^5.62.1",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
@@ -5503,9 +5503,9 @@
}
},
"node_modules/@tanstack/eslint-plugin-query": {
"version": "5.61.4",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.61.4.tgz",
"integrity": "sha512-QVVsY8hwrX9r6c8lLV48oY682SU2GeVlo0hWMSaOKkI05Yi4bXhw5jv7E2qkbjGrgA6DcVl3o/F0dT4wpT+/SQ==",
"version": "5.62.1",
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.1.tgz",
"integrity": "sha512-1886D5U+re1TW0wSH4/kUGG36yIoW5Wkz4twVEzlk3ZWmjF3XkRSWgB+Sc7n+Lyzt8usNV8ZqkZE6DA7IC47fQ==",
"dev": true,
"dependencies": {
"@typescript-eslint/utils": "^8.15.0"

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.15.1",
"version": "0.15.2",
"private": true,
"type": "module",
"engines": {
@@ -78,7 +78,7 @@
"@playwright/test": "^1.48.2",
"@react-router/dev": "^7.0.1",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/eslint-plugin-query": "^5.60.1",
"@tanstack/eslint-plugin-query": "^5.62.1",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",

View File

@@ -83,9 +83,13 @@ export function ChatInput({
};
const handleSubmitMessage = () => {
if (textareaRef.current?.value) {
onSubmit(textareaRef.current.value);
textareaRef.current.value = "";
const message = value || textareaRef.current?.value || "";
if (message) {
onSubmit(message);
onChange?.("");
if (textareaRef.current) {
textareaRef.current.value = "";
}
}
};

View File

@@ -6,17 +6,21 @@ import { code } from "../markdown/code";
import { ol, ul } from "../markdown/list";
import ArrowUp from "#/icons/angle-up-solid.svg?react";
import ArrowDown from "#/icons/angle-down-solid.svg?react";
import CheckCircle from "#/icons/check-circle-solid.svg?react";
import XCircle from "#/icons/x-circle-solid.svg?react";
interface ExpandableMessageProps {
id?: string;
message: string;
type: string;
success?: boolean;
}
export function ExpandableMessage({
id,
message,
type,
success,
}: ExpandableMessageProps) {
const { t, i18n } = useTranslation();
const [showDetails, setShowDetails] = useState(true);
@@ -31,22 +35,14 @@ export function ExpandableMessage({
}
}, [id, message, i18n.language]);
const border = type === "error" ? "border-danger" : "border-neutral-300";
const textColor = type === "error" ? "text-danger" : "text-neutral-300";
let arrowClasses = "h-4 w-4 ml-2 inline";
if (type === "error") {
arrowClasses += " fill-danger";
} else {
arrowClasses += " fill-neutral-300";
}
const arrowClasses = "h-4 w-4 ml-2 inline fill-neutral-300";
const statusIconClasses = "h-4 w-4 ml-2 inline";
return (
<div
className={`flex gap-2 items-center justify-start border-l-2 pl-2 my-2 py-2 ${border}`}
>
<div className="flex gap-2 items-center justify-between border-l-2 border-neutral-300 pl-2 my-2 py-2">
<div className="text-sm leading-4 flex flex-col gap-2 max-w-full">
{headline && (
<p className={`${textColor} font-bold`}>
<p className="text-neutral-300 font-bold">
{headline}
<button
type="button"
@@ -75,6 +71,21 @@ export function ExpandableMessage({
</Markdown>
)}
</div>
{type === "action" && success !== undefined && (
<div className="flex-shrink-0">
{success ? (
<CheckCircle
data-testid="status-icon"
className={`${statusIconClasses} fill-success`}
/>
) : (
<XCircle
data-testid="status-icon"
className={`${statusIconClasses} fill-danger`}
/>
)}
</div>
)}
</div>
);
}

View File

@@ -38,6 +38,9 @@ export function InteractiveChatBox({
const handleSubmit = (message: string) => {
onSubmit(message, images);
setImages([]);
if (message) {
onChange?.("");
}
};
return (

View File

@@ -20,6 +20,7 @@ export function Messages({
type={message.type}
id={message.translationID}
message={message.content}
success={message.success}
/>
);
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import AgentState from "#/types/agent-state";
import { ExplorerTree } from "#/components/features/file-explorer/explorer-tree";
@@ -14,6 +14,7 @@ import { Dropzone } from "./dropzone";
import { FileExplorerHeader } from "./file-explorer-header";
import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
import { OpenVSCodeButton } from "#/components/shared/buttons/open-vscode-button";
import { addAssistantMessage } from "#/state/chat-slice";
interface FileExplorerProps {
isOpen: boolean;
@@ -22,15 +23,37 @@ interface FileExplorerProps {
export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
const { t } = useTranslation();
const dispatch = useDispatch();
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
const [isDragging, setIsDragging] = React.useState(false);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const agentIsReady =
curAgentState !== AgentState.INIT && curAgentState !== AgentState.LOADING;
const { data: paths, refetch, error } = useListFiles();
const { mutate: uploadFiles } = useUploadFiles();
const { refetch: getVSCodeUrl } = useVSCodeUrl();
const { data: vscodeUrl } = useVSCodeUrl({ enabled: agentIsReady });
const handleOpenVSCode = () => {
if (vscodeUrl?.vscode_url) {
dispatch(
addAssistantMessage(
"You opened VS Code. Please inform the agent of any changes you made to the workspace or environment. To avoid conflicts, it's best to pause the agent before making any changes.",
),
);
window.open(vscodeUrl.vscode_url, "_blank");
} else if (vscodeUrl?.error) {
toast.error(
`open-vscode-error-${new Date().getTime()}`,
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
error: vscodeUrl.error,
}),
);
}
};
const selectFileInput = () => {
fileInputRef.current?.click(); // Trigger the file browser
@@ -142,11 +165,8 @@ export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
)}
{isOpen && (
<OpenVSCodeButton
onClick={getVSCodeUrl}
isDisabled={
curAgentState === AgentState.INIT ||
curAgentState === AgentState.LOADING
}
onClick={handleOpenVSCode}
isDisabled={!agentIsReady}
/>
)}
</div>

View File

@@ -1,10 +1,12 @@
import React from "react";
import { useSelector } from "react-redux";
import { useFiles } from "#/context/files";
import { cn } from "#/utils/utils";
import { useListFiles } from "#/hooks/query/use-list-files";
import { useListFile } from "#/hooks/query/use-list-file";
import { Filename } from "./filename";
import { RootState } from "#/store";
interface TreeNodeProps {
path: string;
@@ -20,6 +22,7 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
selectedPath,
} = useFiles();
const [isOpen, setIsOpen] = React.useState(defaultOpen);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const isDirectory = path.endsWith("/");
@@ -39,6 +42,12 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
}
}, [fileContent, path]);
React.useEffect(() => {
if (selectedPath === path && !isDirectory) {
refetch();
}
}, [curAgentState, selectedPath, path, isDirectory]);
const fileParts = path.split("/");
const filename =
fileParts[fileParts.length - 1] || fileParts[fileParts.length - 2];

View File

@@ -17,7 +17,20 @@ export function code({
const match = /language-(\w+)/.exec(className || ""); // get the language
if (!match) {
return <code className={className}>{children}</code>;
return (
<code
className={className}
style={{
backgroundColor: "#2a3038",
padding: "0.2em 0.4em",
borderRadius: "4px",
color: "#e6edf3",
border: "1px solid #30363d",
}}
>
{children}
</code>
);
}
return (

View File

@@ -1,9 +1,7 @@
import React from "react";
import { useDispatch } from "react-redux";
import toast from "react-hot-toast";
import posthog from "posthog-js";
import EllipsisH from "#/icons/ellipsis-h.svg?react";
import { addUserMessage } from "#/state/chat-slice";
import { createChatMessage } from "#/services/chat-service";
import { ProjectMenuCardContextMenu } from "./project.menu-card-context-menu";
import { ProjectMenuDetailsPlaceholder } from "./project-menu-details-placeholder";
@@ -28,7 +26,6 @@ export function ProjectMenuCard({
githubData,
}: ProjectMenuCardProps) {
const { send } = useWsClient();
const dispatch = useDispatch();
const [contextMenuIsOpen, setContextMenuIsOpen] = React.useState(false);
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
@@ -56,7 +53,6 @@ Please push the changes to GitHub and open a pull request.
);
send(event); // send to socket
dispatch(addUserMessage(rawEvent)); // display in chat interface
setContextMenuIsOpen(false);
};

View File

@@ -54,13 +54,13 @@ export function Sidebar() {
return (
<>
<aside className="px-1 flex flex-col gap-1">
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1">
<div className="w-[34px] h-[34px] flex items-center justify-center">
{user.isLoading && <LoadingSpinner size="small" />}
{!user.isLoading && <AllHandsLogoButton onClick={handleClickLogo} />}
</div>
<nav className="py-[18px] flex flex-col items-center gap-[18px]">
<nav className="md:py-[18px] flex flex-row md:flex-col items-center gap-[18px]">
<UserActions
user={user.data ? { avatar_url: user.data.avatar_url } : undefined}
onLogout={logout}

View File

@@ -5,7 +5,7 @@ import { NavTab } from "./nav-tab";
interface ContainerProps {
label?: string;
labels?: {
label: string;
label: string | React.ReactNode;
to: string;
icon?: React.ReactNode;
isBeta?: boolean;
@@ -39,7 +39,7 @@ export function Container({
{label}
</div>
)}
<div className="overflow-scroll h-full rounded-b-xl">{children}</div>
<div className="overflow-hidden h-full rounded-b-xl">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export function CountBadge({ count }: { count: number }) {
return (
<span className="text-[11px] leading-5 text-root-primary bg-neutral-400 px-1 rounded-xl">
{count}
</span>
);
}

View File

@@ -4,7 +4,7 @@ import { BetaBadge } from "./beta-badge";
interface NavTabProps {
to: string;
label: string;
label: string | React.ReactNode;
icon: React.ReactNode;
isBeta?: boolean;
}

View File

@@ -1,43 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import toast from "#/utils/toast";
import { addAssistantMessage } from "#/state/chat-slice";
import { I18nKey } from "#/i18n/declaration";
import OpenHands from "#/api/open-hands";
export const useVSCodeUrl = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
export const useVSCodeUrl = (config: { enabled: boolean }) => {
const data = useQuery({
queryKey: ["vscode_url"],
queryFn: OpenHands.getVSCodeUrl,
enabled: false,
enabled: config.enabled,
refetchOnMount: false,
});
const { data: vscodeUrlObject, isFetching } = data;
React.useEffect(() => {
if (isFetching) return;
if (vscodeUrlObject?.vscode_url) {
dispatch(
addAssistantMessage(
"You opened VS Code. Please inform the agent of any changes you made to the workspace or environment. To avoid conflicts, it's best to pause the agent before making any changes.",
),
);
window.open(vscodeUrlObject.vscode_url, "_blank");
} else if (vscodeUrlObject?.error) {
toast.error(
`open-vscode-error-${new Date().getTime()}`,
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
error: vscodeUrlObject.error,
}),
);
}
}, [vscodeUrlObject, isFetching]);
return data;
};

View File

@@ -2017,6 +2017,9 @@
"ACTION_MESSAGE$WRITE": {
"en": "Writing to a file"
},
"ACTION_MESSAGE$BROWSE": {
"en": "Browsing the web"
},
"OBSERVATION_MESSAGE$RUN": {
"en": "Ran a bash command"
},
@@ -2029,6 +2032,9 @@
"OBSERVATION_MESSAGE$WRITE": {
"en": "Wrote to a file"
},
"OBSERVATION_MESSAGE$BROWSE": {
"en": "Browsing completed"
},
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
"en": "Show details"
},

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c9.4-9.4 24.6-9.4 33.9 0l47 47 47-47c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9l-47 47 47 47c9.4 9.4 9.4 24.6 0 33.9s-24.6 9.4-33.9 0l-47-47-47 47c-9.4 9.4-24.6 9.4-33.9 0s-9.4-24.6 0-33.9l47-47-47-47c-9.4-9.4-9.4-24.6 0-33.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 404 B

View File

@@ -32,8 +32,11 @@ code {
margin: 0;
font-size: 85%;
white-space: break-spaces;
background-color: var(--bg-neutral-muted);
border-radius: 6px;
background-color: #2a3038;
border-radius: 4px;
color: #e6edf3;
border: 1px solid #30363d;
letter-spacing: -0.2px;
}
.markdown-body pre code {

View File

@@ -4,6 +4,7 @@ type Message = {
timestamp: string;
imageUrls?: string[];
type?: "thought" | "error" | "action";
success?: boolean;
pending?: boolean;
translationID?: string;
eventID?: number;

View File

@@ -52,6 +52,7 @@ export const useWSStatusChange = () => {
if (gitHubToken && selectedRepository) {
dispatch(clearSelectedRepository());
additionalInfo = `Repository ${selectedRepository} has been cloned to /workspace. Please check the /workspace for files.`;
} else if (importedProjectZip) {
// if there's an uploaded project zip, add it to the chat
additionalInfo =

View File

@@ -21,6 +21,7 @@ import { useUserPrefs } from "#/context/user-prefs-context";
import { useConversationConfig } from "#/hooks/query/use-conversation-config";
import { Container } from "#/components/layout/container";
import Security from "#/components/shared/modals/security/security";
import { CountBadge } from "#/components/layout/count-badge";
function App() {
const { token, gitHubToken } = useAuth();
@@ -33,6 +34,8 @@ function App() {
(state: RootState) => state.initalQuery,
);
const { updateCount } = useSelector((state: RootState) => state.browser);
const { data: latestGitHubCommit } = useLatestRepoCommit({
repository: selectedRepository,
});
@@ -70,21 +73,25 @@ function App() {
<EventHandler>
<div className="flex flex-col h-full gap-3">
<div className="flex h-full overflow-auto gap-3">
<Container className="w-[390px] max-h-full relative">
<Container className="w-full md:w-[390px] max-h-full relative">
<ChatInterface />
</Container>
<div className="flex flex-col grow gap-3">
<div className="hidden md:flex flex-col grow gap-3">
<Container
className="h-2/3"
labels={[
{ label: "Workspace", to: "", icon: <CodeIcon /> },
{ label: "Jupyter", to: "jupyter", icon: <ListIcon /> },
{
label: "Browser",
label: (
<div className="flex items-center gap-1">
Browser
{updateCount > 0 && <CountBadge count={updateCount} />}
</div>
),
to: "browser",
icon: <GlobeIcon />,
isBeta: true,
},
]}
>

View File

@@ -79,11 +79,11 @@ export default function MainApp() {
return (
<div
data-testid="root-layout"
className="bg-root-primary p-3 h-screen min-w-[1024px] overflow-x-hidden flex gap-3"
className="bg-root-primary p-3 h-screen md:min-w-[1024px] overflow-x-hidden flex flex-col md:flex-row gap-3"
>
<Sidebar />
<div className="h-full w-full relative">
<div className="h-[calc(100%-50px)] md:h-full w-full relative">
<Outlet />
</div>

View File

@@ -15,21 +15,16 @@ import {
ObservationMessage,
StatusMessage,
} from "#/types/message";
import EventLogger from "#/utils/event-logger";
import { handleObservationMessage } from "./observations";
const messageActions = {
[ActionType.BROWSE]: (message: ActionMessage) => {
if (message.args.thought) {
store.dispatch(addAssistantMessage(message.args.thought));
} else {
if (!message.args.thought && message.message) {
store.dispatch(addAssistantMessage(message.message));
}
},
[ActionType.BROWSE_INTERACTIVE]: (message: ActionMessage) => {
if (message.args.thought) {
store.dispatch(addAssistantMessage(message.args.thought));
} else {
if (!message.args.thought && message.message) {
store.dispatch(addAssistantMessage(message.message));
}
},
@@ -107,6 +102,10 @@ export function handleAssistantMessage(message: Record<string, unknown>) {
} else if (message.status_update) {
handleStatusMessage(message as unknown as StatusMessage);
} else {
EventLogger.error(`Unknown message type ${message}`);
store.dispatch(
addErrorMessage({
message: "Unknown message type received",
}),
);
}
}

View File

@@ -93,7 +93,7 @@ export const saveSettings = (settings: Partial<Settings>) => {
if (!isValid) return;
let value = settings[key as keyof Settings];
if (value === undefined || value === null) value = "";
localStorage.setItem(key, value.toString());
localStorage.setItem(key, value.toString().trim());
});
localStorage.setItem("SETTINGS_VERSION", LATEST_SETTINGS_VERSION.toString());
};

View File

@@ -5,6 +5,8 @@ export const initialState = {
url: "https://github.com/All-Hands-AI/OpenHands",
// Base64-encoded screenshot of browser window (placeholder for now, will be replaced with the actual screenshot later)
screenshotSrc: "",
// Counter for browser updates
updateCount: 0,
};
export const browserSlice = createSlice({
@@ -16,6 +18,7 @@ export const browserSlice = createSlice({
},
setScreenshotSrc: (state, action) => {
state.screenshotSrc = action.payload;
state.updateCount += 1;
},
},
});

View File

@@ -1,14 +1,25 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
import { OpenHandsObservation } from "#/types/core/observations";
import {
OpenHandsObservation,
CommandObservation,
IPythonObservation,
} from "#/types/core/observations";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsEventType } from "#/types/core/base";
type SliceState = { messages: Message[] };
const MAX_CONTENT_LENGTH = 1000;
const HANDLED_ACTIONS = ["run", "run_ipython", "write", "read"];
const HANDLED_ACTIONS: OpenHandsEventType[] = [
"run",
"run_ipython",
"write",
"read",
"browse",
];
function getRiskText(risk: ActionSecurityRisk) {
switch (risk) {
@@ -92,6 +103,8 @@ export const chatSlice = createSlice({
text = `${action.payload.args.path}\n${content}`;
} else if (actionID === "read") {
text = action.payload.args.path;
} else if (actionID === "browse") {
text = `Browsing ${action.payload.args.url}`;
}
if (actionID === "run" || actionID === "run_ipython") {
if (
@@ -129,6 +142,18 @@ export const chatSlice = createSlice({
return;
}
causeMessage.translationID = translationID;
// Set success property based on observation type
if (observationID === "run") {
const commandObs = observation.payload as CommandObservation;
causeMessage.success = commandObs.extras.exit_code === 0;
} else if (observationID === "run_ipython") {
// For IPython, we consider it successful if there's no error message
const ipythonObs = observation.payload as IPythonObservation;
causeMessage.success = !ipythonObs.message
.toLowerCase()
.includes("error");
}
if (observationID === "run" || observationID === "run_ipython") {
let { content } = observation.payload;
if (content.length > MAX_CONTENT_LENGTH) {
@@ -136,6 +161,16 @@ export const chatSlice = createSlice({
}
content = `\`\`\`\n${content}\n\`\`\``;
causeMessage.content = content; // Observation content includes the action
} else if (observationID === "browse") {
let content = `**URL:** ${observation.payload.extras.url}\n`;
if (observation.payload.extras.error) {
content += `**Error:**\n${observation.payload.extras.error}\n`;
}
content += `**Output:**\n${observation.payload.content}`;
if (content.length > MAX_CONTENT_LENGTH) {
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
}
causeMessage.content = content;
}
},

View File

@@ -52,6 +52,21 @@ export interface BrowseObservation extends OpenHandsObservationEvent<"browse"> {
};
}
export interface WriteObservation extends OpenHandsObservationEvent<"write"> {
source: "agent";
extras: {
path: string;
content: string;
};
}
export interface ReadObservation extends OpenHandsObservationEvent<"read"> {
source: "agent";
extras: {
path: string;
};
}
export interface ErrorObservation extends OpenHandsObservationEvent<"error"> {
source: "user";
extras: {
@@ -65,4 +80,6 @@ export type OpenHandsObservation =
| IPythonObservation
| DelegateObservation
| BrowseObservation
| WriteObservation
| ReadObservation
| ErrorObservation;

View File

@@ -14,6 +14,7 @@ export default {
'root-secondary': '#262626',
'hyperlink': '#007AFF',
'danger': '#EF3744',
'success': '#4CAF50',
},
},
},

View File

@@ -6,10 +6,31 @@ import { configureStore } from "@reduxjs/toolkit";
// eslint-disable-next-line import/no-extraneous-dependencies
import { RenderOptions, render } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nextProvider } from "react-i18next";
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { AppStore, RootState, rootReducer } from "./src/store";
import { AuthProvider } from "#/context/auth-context";
import { UserPrefsProvider } from "#/context/user-prefs-context";
// Initialize i18n for tests
i18n
.use(initReactI18next)
.init({
lng: "en",
fallbackLng: "en",
ns: ["translation"],
defaultNS: "translation",
resources: {
en: {
translation: {},
},
},
interpolation: {
escapeValue: false,
},
});
const setupStore = (preloadedState?: Partial<RootState>): AppStore =>
configureStore({
reducer: rootReducer,
@@ -40,7 +61,9 @@ export function renderWithProviders(
<UserPrefsProvider>
<AuthProvider>
<QueryClientProvider client={new QueryClient()}>
{children}
<I18nextProvider i18n={i18n}>
{children}
</I18nextProvider>
</QueryClientProvider>
</AuthProvider>
</UserPrefsProvider>

View File

@@ -12,7 +12,13 @@ HTMLElement.prototype.scrollTo = vi.fn();
// Mock the i18n provider
vi.mock("react-i18next", async (importOriginal) => ({
...(await importOriginal<typeof import("react-i18next")>()),
useTranslation: () => ({ t: (key: string) => key }),
useTranslation: () => ({
t: (key: string) => key,
i18n: {
language: "en",
exists: () => false,
},
}),
}));
// Mock requests during tests

View File

@@ -10,7 +10,6 @@ load_dotenv()
from openhands.agenthub import ( # noqa: E402
browsing_agent,
codeact_agent,
codeact_swe_agent,
delegator_agent,
dummy_agent,
planner_agent,
@@ -18,7 +17,6 @@ from openhands.agenthub import ( # noqa: E402
__all__ = [
'codeact_agent',
'codeact_swe_agent',
'planner_agent',
'delegator_agent',
'dummy_agent',

View File

@@ -166,6 +166,7 @@ class CodeActAgent(Agent):
# Add the LLM message (assistant) that initiated the tool calls
# (overwrites any previous message with the same response_id)
logger.debug(f'Tool calls type: {type(assistant_msg.tool_calls)}, value: {assistant_msg.tool_calls}')
pending_tool_call_action_messages[llm_response.id] = Message(
role=assistant_msg.role,
# tool call content SHOULD BE a string

View File

@@ -1,7 +0,0 @@
# CodeAct (SWE Edit Specialized)
This agent is an adaptation of the original [SWE Agent](https://swe-agent.com/) based on CodeAct using the `agentskills` library of OpenHands.
Its intended use is **solving GitHub issues**.
It removes web-browsing and GitHub capability from the original CodeAct agent to avoid confusion to the agent.

View File

@@ -1,4 +0,0 @@
from openhands.agenthub.codeact_swe_agent.codeact_swe_agent import CodeActSWEAgent
from openhands.controller.agent import Agent
Agent.register('CodeActSWEAgent', CodeActSWEAgent)

View File

@@ -1,110 +0,0 @@
import re
from openhands.controller.action_parser import ActionParser
from openhands.events.action import (
Action,
AgentFinishAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
)
class CodeActSWEActionParserFinish(ActionParser):
"""Parser action:
- AgentFinishAction() - end the interaction
"""
def __init__(
self,
):
self.finish_command = None
def check_condition(self, action_str: str) -> bool:
self.finish_command = re.search(r'<finish>.*</finish>', action_str, re.DOTALL)
return self.finish_command is not None
def parse(self, action_str: str) -> Action:
assert (
self.finish_command is not None
), 'self.finish_command should not be None when parse is called'
thought = action_str.replace(self.finish_command.group(0), '').strip()
return AgentFinishAction(thought=thought)
class CodeActSWEActionParserCmdRun(ActionParser):
"""Parser action:
- CmdRunAction(command) - bash command to run
- AgentFinishAction() - end the interaction
"""
def __init__(
self,
):
self.bash_command = None
def check_condition(self, action_str: str) -> bool:
self.bash_command = re.search(
r'<execute_bash>(.*?)</execute_bash>', action_str, re.DOTALL
)
return self.bash_command is not None
def parse(self, action_str: str) -> Action:
assert (
self.bash_command is not None
), 'self.bash_command should not be None when parse is called'
thought = action_str.replace(self.bash_command.group(0), '').strip()
# a command was found
command_group = self.bash_command.group(1).strip()
if command_group.strip() == 'exit':
return AgentFinishAction()
return CmdRunAction(command=command_group, thought=thought)
class CodeActSWEActionParserIPythonRunCell(ActionParser):
"""Parser action:
- IPythonRunCellAction(code) - IPython code to run
"""
def __init__(
self,
):
self.python_code = None
self.jupyter_kernel_init_code: str = 'from agentskills import *'
def check_condition(self, action_str: str) -> bool:
self.python_code = re.search(
r'<execute_ipython>(.*?)</execute_ipython>', action_str, re.DOTALL
)
return self.python_code is not None
def parse(self, action_str: str) -> Action:
assert (
self.python_code is not None
), 'self.python_code should not be None when parse is called'
code_group = self.python_code.group(1).strip()
thought = action_str.replace(self.python_code.group(0), '').strip()
return IPythonRunCellAction(
code=code_group,
thought=thought,
kernel_init_code=self.jupyter_kernel_init_code,
)
class CodeActSWEActionParserMessage(ActionParser):
"""Parser action:
- MessageAction(content) - Message action to run (e.g. ask for clarification)
"""
def __init__(
self,
):
pass
def check_condition(self, action_str: str) -> bool:
# We assume the LLM is GOOD enough that when it returns pure natural language
# it wants to talk to the user
return True
def parse(self, action_str: str) -> Action:
return MessageAction(content=action_str, wait_for_response=True)

View File

@@ -1,224 +0,0 @@
from openhands.agenthub.codeact_swe_agent.prompt import (
COMMAND_DOCS,
SWE_EXAMPLE,
SYSTEM_PREFIX,
SYSTEM_SUFFIX,
)
from openhands.agenthub.codeact_swe_agent.response_parser import (
CodeActSWEResponseParser,
)
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.core.message import ImageContent, Message, TextContent
from openhands.events.action import (
Action,
AgentFinishAction,
CmdRunAction,
IPythonRunCellAction,
MessageAction,
)
from openhands.events.observation import (
CmdOutputObservation,
IPythonRunCellObservation,
)
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.observation import Observation
from openhands.events.serialization.event import truncate_content
from openhands.llm.llm import LLM
from openhands.runtime.plugins import (
AgentSkillsRequirement,
JupyterRequirement,
PluginRequirement,
)
def get_system_message() -> str:
return f'{SYSTEM_PREFIX}\n\n{COMMAND_DOCS}\n\n{SYSTEM_SUFFIX}'
def get_in_context_example() -> str:
return SWE_EXAMPLE
class CodeActSWEAgent(Agent):
VERSION = '1.6'
"""
This agent is an adaptation of the original [SWE Agent](https://swe-agent.com/) based on CodeAct 1.5 using the `agentskills` library of OpenHands.
It is intended use is **solving Github issues**.
It removes web-browsing and Github capability from the original CodeAct agent to avoid confusion to the agent.
"""
sandbox_plugins: list[PluginRequirement] = [
# NOTE: AgentSkillsRequirement need to go before JupyterRequirement, since
# AgentSkillsRequirement provides a lot of Python functions,
# and it needs to be initialized before Jupyter for Jupyter to use those functions.
AgentSkillsRequirement(),
JupyterRequirement(),
]
system_message: str = get_system_message()
in_context_example: str = f"Here is an example of how you can interact with the environment for task solving:\n{get_in_context_example()}\n\nNOW, LET'S START!"
response_parser = CodeActSWEResponseParser()
def __init__(
self,
llm: LLM,
config: AgentConfig,
) -> None:
"""Initializes a new instance of the CodeActSWEAgent class.
Parameters:
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm, config)
self.reset()
def action_to_str(self, action: Action) -> str:
if isinstance(action, CmdRunAction):
return (
f'{action.thought}\n<execute_bash>\n{action.command}\n</execute_bash>'
)
elif isinstance(action, IPythonRunCellAction):
return f'{action.thought}\n<execute_ipython>\n{action.code}\n</execute_ipython>'
elif isinstance(action, MessageAction):
return action.content
return ''
def get_action_message(self, action: Action) -> Message | None:
if isinstance(action, (CmdRunAction, IPythonRunCellAction, MessageAction)):
content = [TextContent(text=self.action_to_str(action))]
if (
self.llm.vision_is_active()
and isinstance(action, MessageAction)
and action.image_urls
):
content.append(ImageContent(image_urls=action.image_urls))
return Message(
role='user' if action.source == 'user' else 'assistant', content=content
)
return None
def get_observation_message(self, obs: Observation) -> Message | None:
max_message_chars = self.llm.config.max_message_chars
if isinstance(obs, CmdOutputObservation):
text = 'OBSERVATION:\n' + truncate_content(
obs.content + obs.interpreter_details, max_message_chars
)
text += (
f'\n[Command {obs.command_id} finished with exit code {obs.exit_code}]'
)
return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, IPythonRunCellObservation):
text = 'OBSERVATION:\n' + obs.content
# replace base64 images with a placeholder
splitted = text.split('\n')
for i, line in enumerate(splitted):
if '![image](data:image/png;base64,' in line:
splitted[i] = (
'![image](data:image/png;base64, ...) already displayed to user'
)
text = '\n'.join(splitted)
text = truncate_content(text, max_message_chars)
return Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, ErrorObservation):
text = 'OBSERVATION:\n' + truncate_content(obs.content, max_message_chars)
text += '\n[Error occurred in processing last action]'
return Message(role='user', content=[TextContent(text=text)])
else:
# If an observation message is not returned, it will cause an error
# when the LLM tries to return the next message
raise ValueError(f'Unknown observation type: {type(obs)}')
def reset(self) -> None:
"""Resets the CodeAct Agent."""
super().reset()
def step(self, state: State) -> Action:
"""Performs one step using the CodeAct Agent.
This includes gathering info on previous steps and prompting the model to make a command to execute.
Parameters:
- state (State): used to get updated info and background commands
Returns:
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
# if we're done, go back
last_user_message = state.get_last_user_message()
if last_user_message and last_user_message.content.strip() == '/exit':
return AgentFinishAction()
# prepare what we want to send to the LLM
messages: list[Message] = self._get_messages(state)
response = self.llm.completion(
messages=self.llm.format_messages_for_llm(messages),
stop=[
'</execute_ipython>',
'</execute_bash>',
],
)
return self.response_parser.parse(response)
def _get_messages(self, state: State) -> list[Message]:
messages: list[Message] = [
Message(role='system', content=[TextContent(text=self.system_message)]),
Message(role='user', content=[TextContent(text=self.in_context_example)]),
]
for event in state.history:
# create a regular message from an event
if isinstance(event, Action):
message = self.get_action_message(event)
elif isinstance(event, Observation):
message = self.get_observation_message(event)
else:
raise ValueError(f'Unknown event type: {type(event)}')
# add regular message
if message:
# handle error if the message is the SAME role as the previous message
# litellm.exceptions.BadRequestError: litellm.BadRequestError: OpenAIException - Error code: 400 - {'detail': 'Only supports u/a/u/a/u...'}
# there should not have two consecutive messages from the same role
if messages and messages[-1].role == message.role:
messages[-1].content.extend(message.content)
else:
messages.append(message)
# the latest user message is important:
# we want to remind the agent of the environment constraints
latest_user_message = next(
(m for m in reversed(messages) if m.role == 'user'), None
)
# Get the last user text inside content
if latest_user_message:
latest_user_message_text = next(
(
t
for t in reversed(latest_user_message.content)
if isinstance(t, TextContent)
)
)
# add a reminder to the prompt
reminder_text = f'\n\nENVIRONMENT REMINDER: You have {state.max_iterations - state.iteration} turns left to complete the task. When finished reply with <finish></finish>.'
if latest_user_message_text:
latest_user_message_text.text = (
latest_user_message_text.text + reminder_text
)
else:
latest_user_message_text = TextContent(text=reminder_text)
latest_user_message.content.append(latest_user_message_text)
return messages

View File

@@ -1,455 +0,0 @@
from openhands.runtime.plugins import AgentSkillsRequirement
_AGENT_SKILLS_DOCS = AgentSkillsRequirement.documentation
COMMAND_DOCS = (
'\nApart from the standard Python library, the assistant can also use the following functions (already imported) in <execute_ipython> environment:\n'
f'{_AGENT_SKILLS_DOCS}'
"Please note that THE `edit_file` FUNCTION REQUIRES PROPER INDENTATION. If the assistant would like to add the line ' print(x)', it must fully write that out, with all those spaces before the code! Indentation is important and code that is not indented correctly will fail and require fixing before it can be run."
)
# ======= SYSTEM MESSAGE =======
MINIMAL_SYSTEM_PREFIX = """A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
The assistant can interact with an interactive Python (Jupyter Notebook) environment and receive the corresponding output when needed. The code should be enclosed using "<execute_ipython>" tag, for example:
<execute_ipython>
print("Hello World!")
</execute_ipython>
The assistant can execute bash commands on behalf of the user by wrapping them with <execute_bash> and </execute_bash>.
For example, you can list the files in the current directory by <execute_bash> ls </execute_bash>.
"""
PIP_INSTALL_PREFIX = """The assistant can install Python packages using the %pip magic command in an IPython environment by using the following syntax: <execute_ipython> %pip install [package needed] </execute_ipython> and should always import packages and define variables before starting to use them."""
SYSTEM_PREFIX = MINIMAL_SYSTEM_PREFIX + PIP_INSTALL_PREFIX
SYSTEM_SUFFIX = """The assistant's response should be concise.
The assistant should include ONLY ONE <execute_ipython> or <execute_bash> in every one of the responses, unless the assistant is finished with the task or need more input or action from the user in order to proceed.
IMPORTANT: Whenever possible, execute the code for the user using <execute_ipython> or <execute_bash> instead of providing it.
"""
SWE_EXAMPLE = """
--- START OF EXAMPLE ---
USER: We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
TimeDelta serialization precision
Hi there!
I just found quite strange behaviour of `TimeDelta` field serialization
```python3
from marshmallow.fields import TimeDelta
from datetime import timedelta
td_field = TimeDelta(precision="milliseconds")
obj = dict()
obj["td_field"] = timedelta(milliseconds=345)
print(td_field.serialize("td_field", obj))
```
Output of this snippet is `344`, but it seems that `345` is correct.
Looks like a rounding issue here: https://github.com/marshmallow-code/marshmallow/blob/dev/src/marshmallow/fields.py#L1474
--- END ISSUE ---
[Current directory: /workspace/marshmallow-code__marshmallow]
ASSISTANT:
Let's first start by reproducing the results of the issue. The issue includes some example code for reproduction, which we can use. We'll create a new file called `reproduce.py` and paste the example code into it.
<execute_ipython>
create_file('/workspace/marshmallow-code__marshmallow/reproduce.py')
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/marshmallow-code__marshmallow/reproduce.py (1 lines total)]
1|
[File /workspace/marshmallow-code__marshmallow/reproduce.py created.]
ASSISTANT:
Now let's paste in the example code from the issue.
<execute_ipython>
EDITED_CODE = \"\"\"from marshmallow.fields import TimeDelta
from datetime import timedelta
td_field = TimeDelta(precision="milliseconds")
obj = dict()
obj["td_field"] = timedelta(milliseconds=345)
print(td_field.serialize("td_field", obj))
\"\"\"
edit_file(start=1, end=1, content=EDITED_CODE)
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/marshmallow-code__marshmallow/reproduce.py (9 lines total)]
1|from marshmallow.fields import TimeDelta
2|from datetime import timedelta
3|
4|td_field = TimeDelta(precision="milliseconds")
5|
6|obj = dict()
7|obj["td_field"] = timedelta(milliseconds=345)
8|
9|print(td_field.serialize("td_field", obj))
[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
Now let's run the code to see if we see the same output as the issue.
<execute_bash>
python reproduce.py
</execute_bash>
USER:
OBSERVATION:
344
ASSISTANT:
We are indeed seeing the same output as the issue. The issue suggests that we should look at line 1474 of the `fields.py` file to see if there is a rounding issue. Let's open the `fields.py` file and navigate to line 1474 to see the relevant code. First we should find the `fields.py` file in the repository. It is likely to be in the `src/marshmallow` directory, but we should check to be sure.
<execute_bash>
ls -F
</execute_bash>
USER:
OBSERVATION:
AUTHORS.rst
CHANGELOG.rst
CODE_OF_CONDUCT.md
CONTRIBUTING.rst
LICENSE
MANIFEST.in
NOTICE
README.rst
RELEASING.md
azure-pipelines.yml
docs/
examples/
performance/
pyproject.toml
reproduce.py
setup.cfg
setup.py
src/
tests/
tox.ini
ASSISTANT:
It looks like the `src` directory is present, which suggests that the `fields.py` file is likely to be in the `src` directory. Let's use find_file to see where it is.
<execute_ipython>
find_file("fields.py", "/workspace/marshmallow-code__marshmallow/src")
</execute_ipython>
USER:
OBSERVATION:
[Found 1 matches for "fields.py" in /workspace/marshmallow-code__marshmallow/src]
/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py
ASSISTANT:
It looks like the `fields.py` file is present in the `./src/marshmallow/` directory. The issue also points to a specific URL with line number 1474. We should navigate to that line in fields.py to see the relevant code for the `TimeDelta` serialization.
<execute_ipython>
open_file("/workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py", 1474)
</execute_ipython>
USER:
OBSERVATION:
[File: /workspace/marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
(1458 more lines above)
1459| self.WEEKS,
1460| )
1461|
1462| if precision not in units:
1463| msg = 'The precision must be {} or "{}".'.format(
1464| ", ".join([f'"{each}"' for each in units[:-1]]), units[-1]
1465| )
1466| raise ValueError(msg)
1467|
1468| self.precision = precision
1469| super().__init__(**kwargs)
1470|
1471| def _serialize(self, value, attr, obj, **kwargs):
1472| if value is None:
1473| return None
1474| base_unit = dt.timedelta(**{self.precision: 1})
1475| return int(value.total_seconds() / base_unit.total_seconds())
1476|
1477| def _deserialize(self, value, attr, data, **kwargs):
1478| try:
1479| value = int(value)
1480| except (TypeError, ValueError) as error:
1481| raise self.make_error("invalid") from error
1482|
1483| kwargs = {self.precision: value}
1484|
1485| try:
1486| return dt.timedelta(**kwargs)
1487| except OverflowError as error:
1488| raise self.make_error("invalid") from error
1489|
1490|
1491|class Mapping(Field):
1492| \"\"\"An abstract class for objects with key-value pairs.
1493|
1494| :param keys: A field class or instance for dict keys.
1495| :param values: A field class or instance for dict values.
1496| :param kwargs: The same keyword arguments that :class:`Field` receives.
1497|
1498| .. note::
1499| When the structure of nested data is not known, you may omit the
1500| `keys` and `values` arguments to prevent content validation.
1501|
1502| .. versionadded:: 3.0.0rc4
1503| \"\"\"
1504|
1505| mapping_type = dict
1506|
1507| #: Default error messages.
1508| default_error_messages = {"invalid": "Not a valid mapping type."}
1509|
1510| def __init__(
1511| self,
1512| keys: typing.Optional[typing.Union[Field, type]] = None,
1513| values: typing.Optional[typing.Union[Field, type]] = None,
1514| **kwargs,
1515| ):
1516| super().__init__(**kwargs)
1517| if keys is None:
1518| self.key_field = None
1519| else:
1520| try:
1521| self.key_field = resolve_field_instance(keys)
1522| except FieldInstanceResolutionError as error:
1523| raise ValueError(
1524| '"keys" must be a subclass or instance of '
1525| "marshmallow.base.FieldABC."
1526| ) from error
1527|
1528| if values is None:
1529| self.value_field = None
1530| else:
1531| try:
1532| self.value_field = resolve_field_instance(values)
1533| except FieldInstanceResolutionError as error:
1534| raise ValueError(
1535| '"values" must be a subclass or instance of '
1536| "marshmallow.base.FieldABC."
1537| ) from error
1538| if isinstance(self.value_field, Nested):
1539| self.only = self.value_field.only
1540| self.exclude = self.value_field.exclude
1541|
1542| def _bind_to_schema(self, field_name, schema):
1543| super()._bind_to_schema(field_name, schema)
1544| if self.value_field:
1545| self.value_field = copy.deepcopy(self.value_field)
1546| self.value_field._bind_to_schema(field_name, self)
1547| if isinstance(self.value_field, Nested):
1548| self.value_field.only = self.only
1549| self.value_field.exclude = self.exclude
1550| if self.key_field:
1551| self.key_field = copy.deepcopy(self.key_field)
1552| self.key_field._bind_to_schema(field_name, self)
1553|
1554| def _serialize(self, value, attr, obj, **kwargs):
1555| if value is None:
1556| return None
1557| if not self.value_field and not self.key_field:
1558| return self.mapping_type(value)
(439 more lines below)
ASSISTANT:
We are now looking at the relevant section of the `fields.py` file where the `TimeDelta` serialization occurs. The issue suggests that there is a rounding problem near line 1474. The current implementation uses integer division, which truncates the decimal part and can lead to incorrect rounding on line 1475. To fix this issue, we should ensure that the division results in a float, and then round the result to the nearest integer before converting it to an int. This should preserve the milliseconds precision as expected. Let's make the necessary edit to the code.
<execute_ipython>
edit_file(start=1475, end=1475, content="return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int")
</execute_ipython>
USER:
OBSERVATION:
[Your proposed edit has introduced new syntax error(s). Please understand the errors and retry your edit command.]
ERRORS:
- E999 IndentationError: unexpected indent
[This is how your edit would have looked if applied]
-------------------------------------------------
[File: /marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
(1468 more lines above)
1469| super().__init__(**kwargs)
1470|
1471| def _serialize(self, value, attr, obj, **kwargs):
1472| if value is None:
1473| return None
1474| base_unit = dt.timedelta(**{self.precision: 1})
1475|return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int
1476|
1477| def _deserialize(self, value, attr, data, **kwargs):
1478| try:
1479| value = int(value)
(518 more lines below)
-------------------------------------------------
[This is the original code before your edit]
-------------------------------------------------
[File: /marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
(1469 more lines above)
1470|
1471| def _serialize(self, value, attr, obj, **kwargs):
1472| if value is None:
1473| return None
1474| base_unit = dt.timedelta(**{self.precision: 1})
1475| return int(value.total_seconds() / base_unit.total_seconds())
1476|
1477| def _deserialize(self, value, attr, data, **kwargs):
1478| try:
1479| value = int(value)
1480| except (TypeError, ValueError) as error:
(517 more lines below)
-------------------------------------------------
Your changes have NOT been applied. Please fix your edit command and try again.
You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.
DO NOT re-run the same failed edit command. Running it again will lead to the same error.
ASSISTANT:
My edit command did not use the proper indentation, I will fix my syntax in this follow up edit command.
<execute_ipython>
EDITED_CODE=" return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int"
edit_file(start=1475, end=1475, content=EDITED_CODE)
</execute_ipython>
USER:
OBSERVATION:
[File: /marshmallow-code__marshmallow/src/marshmallow/fields.py (1997 lines total)]
(1424 more lines above)
1425|
1426| :param precision: Influences how the integer is interpreted during
1427| (de)serialization. Must be 'days', 'seconds', 'microseconds',
1428| 'milliseconds', 'minutes', 'hours' or 'weeks'.
1429| :param kwargs: The same keyword arguments that :class:`Field` receives.
1430|
1431| .. versionchanged:: 2.0.0
1432| Always serializes to an integer value to avoid rounding errors.
1433| Add `precision` parameter.
1434| \"\"\"
1435|
1436| DAYS = "days"
1437| SECONDS = "seconds"
1438| MICROSECONDS = "microseconds"
1439| MILLISECONDS = "milliseconds"
1440| MINUTES = "minutes"
1441| HOURS = "hours"
1442| WEEKS = "weeks"
1443|
1444| #: Default error messages.
1445| default_error_messages = {
1446| "invalid": "Not a valid period of time.",
1447| "format": "{input!r} cannot be formatted as a timedelta.",
1448| }
1449|
1450| def __init__(self, precision: str = SECONDS, **kwargs):
1451| precision = precision.lower()
1452| units = (
1453| self.DAYS,
1454| self.SECONDS,
1455| self.MICROSECONDS,
1456| self.MILLISECONDS,
1457| self.MINUTES,
1458| self.HOURS,
1459| self.WEEKS,
1460| )
1461|
1462| if precision not in units:
1463| msg = 'The precision must be {} or "{}".'.format(
1464| ", ".join([f'"{each}"' for each in units[:-1]]), units[-1]
1465| )
1466| raise ValueError(msg)
1467|
1468| self.precision = precision
1469| super().__init__(**kwargs)
1470|
1471| def _serialize(self, value, attr, obj, **kwargs):
1472| if value is None:
1473| return None
1474| base_unit = dt.timedelta(**{self.precision: 1})
1475| return int(round(value.total_seconds() / base_unit.total_seconds())) # round to nearest int
1476|
1477| def _deserialize(self, value, attr, data, **kwargs):
1478| try:
1479| value = int(value)
1480| except (TypeError, ValueError) as error:
1481| raise self.make_error("invalid") from error
1482|
1483| kwargs = {self.precision: value}
1484|
1485| try:
1486| return dt.timedelta(**kwargs)
1487| except OverflowError as error:
1488| raise self.make_error("invalid") from error
1489|
1490|
1491|class Mapping(Field):
1492| \"\"\"An abstract class for objects with key-value pairs.
1493|
1494| :param keys: A field class or instance for dict keys.
1495| :param values: A field class or instance for dict values.
1496| :param kwargs: The same keyword arguments that :class:`Field` receives.
1497|
1498| .. note::
1499| When the structure of nested data is not known, you may omit the
1500| `keys` and `values` arguments to prevent content validation.
1501|
1502| .. versionadded:: 3.0.0rc4
1503| \"\"\"
1504|
1505| mapping_type = dict
1506|
1507| #: Default error messages.
1508| default_error_messages = {"invalid": "Not a valid mapping type."}
1509|
1510| def __init__(
1511| self,
1512| keys: typing.Optional[typing.Union[Field, type]] = None,
1513| values: typing.Optional[typing.Union[Field, type]] = None,
1514| **kwargs,
1515| ):
1516| super().__init__(**kwargs)
1517| if keys is None:
1518| self.key_field = None
1519| else:
1520| try:
1521| self.key_field = resolve_field_instance(keys)
1522| except FieldInstanceResolutionError as error:
1523| raise ValueError(
1524| '"keys" must be a subclass or instance of '
(473 more lines below)
[File updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
ASSISTANT:
The code has been updated to use the `round` function, which should fix the rounding issue. Before submitting the changes, it would be prudent to run the reproduce.py code again to ensure that this change has the desired effect. I see that "Current directory" is still `/marshmallow-code__marshmallow`, so we can just run `reproduce.py` directly.
<execute_bash>
python reproduce.py
</execute_bash>
USER:
OBSERVATION:
345
ASSISTANT:
The output has changed from 344 to 345, which suggests that the rounding issue has been fixed. Let's remove the reproduce.py file since it is no longer needed.
<execute_bash>
rm reproduce.py
</execute_bash>
USER:
OBSERVATION:
ASSISTANT:
rm doesn't have any output when it deletes successfully, so that must have worked. Now that we have fixed the issue, we can exit the current task.
<execute_bash>
exit
</execute_bash>
--- END OF EXAMPLE ---
"""

View File

@@ -1,46 +0,0 @@
from openhands.agenthub.codeact_swe_agent.action_parser import (
CodeActSWEActionParserCmdRun,
CodeActSWEActionParserFinish,
CodeActSWEActionParserIPythonRunCell,
CodeActSWEActionParserMessage,
)
from openhands.controller.action_parser import ResponseParser
from openhands.events.action import Action
class CodeActSWEResponseParser(ResponseParser):
"""Parser action:
- CmdRunAction(command) - bash command to run
- IPythonRunCellAction(code) - IPython code to run
- MessageAction(content) - Message action to run (e.g. ask for clarification)
- AgentFinishAction() - end the interaction
"""
def __init__(self):
# Need pay attention to the item order in self.action_parsers
super().__init__()
self.action_parsers = [
CodeActSWEActionParserFinish(),
CodeActSWEActionParserCmdRun(),
CodeActSWEActionParserIPythonRunCell(),
]
self.default_parser = CodeActSWEActionParserMessage()
def parse(self, response: str) -> Action:
action_str = self.parse_response(response)
return self.parse_action(action_str)
def parse_response(self, response) -> str:
action = response.choices[0].message.content
if action is None:
return ''
for lang in ['bash', 'ipython']:
if f'<execute_{lang}>' in action and f'</execute_{lang}>' not in action:
action += f'</execute_{lang}>'
return action
def parse_action(self, action_str: str) -> Action:
for action_parser in self.action_parsers:
if action_parser.check_condition(action_str):
return action_parser.parse(action_str)
return self.default_parser.parse(action_str)

View File

@@ -5,7 +5,7 @@ import traceback
from typing import Callable, ClassVar, Type
import litellm
from litellm.exceptions import ContextWindowExceededError
from litellm.exceptions import BadRequestError, ContextWindowExceededError
from openhands.controller.agent import Agent
from openhands.controller.state.state import State, TrafficControlState
@@ -184,26 +184,18 @@ class AgentController:
self.state.local_iteration += 1
async def update_state_after_step(self):
# update metrics especially for cost. Use deepcopy to avoid it being modified by agent.reset()
# update metrics especially for cost. Use deepcopy to avoid it being modified by agent._reset()
self.state.local_metrics = copy.deepcopy(self.agent.llm.metrics)
async def _react_to_exception(
self,
e: Exception,
):
# Reset state before changing to error state to ensure proper cleanup
if isinstance(e, RuntimeError) and str(e) == 'Agent got stuck in a loop':
# Reset the agent's history to allow for new messages
self.state.history = []
self._stuck_detector = StuckDetector(self.state)
await self.set_agent_state_to(AgentState.ERROR)
if self.status_callback is not None:
err_id = ''
if isinstance(e, litellm.AuthenticationError):
err_id = 'STATUS$ERROR_LLM_AUTHENTICATION'
elif isinstance(e, RuntimeError) and str(e) == 'Agent got stuck in a loop':
err_id = 'STATUS$ERROR_AGENT_STUCK'
self.status_callback('error', err_id, str(e))
async def start_step_loop(self):
@@ -321,20 +313,15 @@ class AgentController:
extra={'msg_type': 'ACTION', 'event_source': EventSource.USER},
)
if self.get_agent_state() != AgentState.RUNNING:
# Allow transitioning from ERROR state to RUNNING state when receiving a new message
if self.get_agent_state() == AgentState.ERROR:
self.reset_task()
await self.set_agent_state_to(AgentState.RUNNING)
elif action.source == EventSource.AGENT and action.wait_for_response:
await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
def reset_task(self) -> None:
"""Resets the agent's task and state."""
self.almost_stuck = 0
def _reset(self) -> None:
"""Resets the agent controller"""
self._pending_action = None
self.agent.reset()
# Reset the stuck detector state
if hasattr(self, '_stuck_detector'):
self._stuck_detector = StuckDetector(self.state)
async def set_agent_state_to(self, new_state: AgentState) -> None:
"""Updates the agent's state and handles side effects. Can emit events to the event stream.
@@ -351,7 +338,7 @@ class AgentController:
return
if new_state in (AgentState.STOPPED, AgentState.ERROR):
self.reset_task()
self._reset()
elif (
new_state == AgentState.RUNNING
and self.state.agent_state == AgentState.PAUSED
@@ -522,15 +509,24 @@ class AgentController:
EventSource.AGENT,
)
return
except ContextWindowExceededError:
# When context window is exceeded, keep roughly half of agent interactions
self.state.history = self._apply_conversation_window(self.state.history)
except (ContextWindowExceededError, BadRequestError) as e:
# FIXME: this is a hack until a litellm fix is confirmed
# Check if this is a nested context window error
error_str = str(e).lower()
if (
'contextwindowexceedederror' in error_str
or 'prompt is too long' in error_str
or isinstance(e, ContextWindowExceededError)
):
# When context window is exceeded, keep roughly half of agent interactions
self.state.history = self._apply_conversation_window(self.state.history)
# Save the ID of the first event in our truncated history for future reloading
if self.state.history:
self.state.start_id = self.state.history[0].id
# Don't add error event - let the agent retry with reduced context
return
# Save the ID of the first event in our truncated history for future reloading
if self.state.history:
self.state.start_id = self.state.history[0].id
# Don't add error event - let the agent retry with reduced context
return
raise
if action.runnable:
if self.state.confirmation_mode and (
@@ -916,7 +912,7 @@ class AgentController:
if self.delegate and self.delegate._is_stuck():
return True
return self._stuck_detector.is_stuck()
return self._stuck_detector.is_stuck(self.headless_mode)
def __repr__(self):
return (

View File

@@ -94,7 +94,7 @@ class State:
end_id: int = -1
# truncation_id tracks where to load history after context window truncation
truncation_id: int = -1
almost_stuck: int = 0
delegates: dict[tuple[int, int], tuple[str, str]] = field(default_factory=dict)
# NOTE: This will never be used by the controller, but it can be used by different
# evaluation tasks to store extra data needed to track the progress/state of the task.

View File

@@ -24,16 +24,44 @@ class StuckDetector:
def __init__(self, state: State):
self.state = state
def is_stuck(self):
# filter out MessageAction with source='user' from history
def is_stuck(self, headless_mode: bool = True):
"""Checks if the agent is stuck in a loop.
Args:
headless_mode: Matches AgentController's headless_mode.
If True: Consider all history (automated/testing)
If False: Consider only history after last user message (interactive)
Returns:
bool: True if the agent is stuck in a loop, False otherwise.
"""
if not headless_mode:
# In interactive mode, only look at history after the last user message
last_user_msg_idx = -1
for i, event in enumerate(reversed(self.state.history)):
if (
isinstance(event, MessageAction)
and event.source == EventSource.USER
):
last_user_msg_idx = len(self.state.history) - i - 1
break
history_to_check = self.state.history[last_user_msg_idx + 1 :]
else:
# In headless mode, look at all history
history_to_check = self.state.history
# Filter out user messages and null events
filtered_history = [
event
for event in self.state.history
for event in history_to_check
if not (
# Filter works elegantly in both modes:
# - In headless: actively filters out user messages from full history
# - In non-headless: no-op since we already sliced after last user message
(isinstance(event, MessageAction) and event.source == EventSource.USER)
or
# there might be some NullAction or NullObservation in the history at least for now
isinstance(event, (NullAction, NullObservation))
or isinstance(event, (NullAction, NullObservation))
)
]
@@ -81,43 +109,19 @@ class StuckDetector:
# it takes 4 actions and 4 observations to detect a loop
# assert len(last_actions) == 4 and len(last_observations) == 4
# reset almost_stuck reminder
self.state.almost_stuck = 0
# almost stuck? if two actions, obs are the same, we're almost stuck
if len(last_actions) >= 2 and len(last_observations) >= 2:
# Check for a loop of 4 identical action-observation pairs
if len(last_actions) == 4 and len(last_observations) == 4:
actions_equal = all(
self._eq_no_pid(last_actions[0], action) for action in last_actions[:2]
self._eq_no_pid(last_actions[0], action) for action in last_actions
)
observations_equal = all(
self._eq_no_pid(last_observations[0], observation)
for observation in last_observations[:2]
for observation in last_observations
)
# the last two actions and obs are the same?
if actions_equal and observations_equal:
self.state.almost_stuck = 2
# the last three actions and observations are the same?
if len(last_actions) >= 3 and len(last_observations) >= 3:
if (
actions_equal
and observations_equal
and self._eq_no_pid(last_actions[0], last_actions[2])
and self._eq_no_pid(last_observations[0], last_observations[2])
):
self.state.almost_stuck = 1
if len(last_actions) == 4 and len(last_observations) == 4:
if (
actions_equal
and observations_equal
and self._eq_no_pid(last_actions[0], last_actions[3])
and self._eq_no_pid(last_observations[0], last_observations[3])
):
logger.warning('Action, Observation loop detected')
self.state.almost_stuck = 0
return True
logger.warning('Action, Observation loop detected')
return True
return False

View File

@@ -1,4 +1,3 @@
import uuid
from dataclasses import dataclass, field, fields, is_dataclass
from typing import ClassVar
@@ -66,7 +65,7 @@ class AppConfig:
modal_api_token_id: str = ''
modal_api_token_secret: str = ''
disable_color: bool = False
jwt_secret: str = uuid.uuid4().hex
jwt_secret: str = ''
attach_session_middleware_class: str = (
'openhands.server.middleware.AttachSessionMiddleware'
)

View File

@@ -2,7 +2,7 @@ from types import UnionType
from typing import get_args, get_origin
OH_DEFAULT_AGENT = 'CodeActAgent'
OH_MAX_ITERATIONS = 100
OH_MAX_ITERATIONS = 500
def get_field_info(f):

View File

@@ -43,6 +43,7 @@ class LLMConfig:
log_completions: Whether to log LLM completions to the state.
log_completions_folder: The folder to log LLM completions to. Required if log_completions is True.
draft_editor: A more efficient LLM to use for file editing. Introduced in [PR 3985](https://github.com/All-Hands-AI/OpenHands/pull/3985).
custom_tokenizer: A custom tokenizer to use for token counting.
"""
model: str = 'claude-3-5-sonnet-20241022'
@@ -77,6 +78,7 @@ class LLMConfig:
log_completions: bool = False
log_completions_folder: str = os.path.join(LOG_DIR, 'completions')
draft_editor: Optional['LLMConfig'] = None
custom_tokenizer: str | None = None
def defaults_to_dict(self) -> dict:
"""Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional."""

View File

@@ -48,6 +48,7 @@ class SandboxConfig:
False # once enabled, OpenHands would lint files after editing
)
use_host_network: bool = False
runtime_extra_build_args: list[str] | None = None
initialize_plugins: bool = True
force_rebuild_runtime: bool = False
runtime_extra_deps: str | None = None

View File

@@ -5,6 +5,7 @@ import platform
from dataclasses import is_dataclass
from types import UnionType
from typing import Any, MutableMapping, get_args, get_origin
from uuid import uuid4
import toml
from dotenv import load_dotenv
@@ -19,7 +20,10 @@ from openhands.core.config.config_utils import (
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
from openhands.storage import get_file_store
from openhands.storage.files import FileStore
JWT_SECRET = '.jwt_secret'
load_dotenv()
@@ -195,6 +199,16 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
)
def get_or_create_jwt_secret(file_store: FileStore) -> str:
try:
jwt_secret = file_store.read(JWT_SECRET)
return jwt_secret
except FileNotFoundError:
new_secret = uuid4().hex
file_store.write(JWT_SECRET, new_secret)
return new_secret
def finalize_config(cfg: AppConfig):
"""More tweaks to the config after it's been loaded."""
if cfg.workspace_base is not None:
@@ -223,6 +237,11 @@ def finalize_config(cfg: AppConfig):
if cfg.cache_dir:
pathlib.Path(cfg.cache_dir).mkdir(parents=True, exist_ok=True)
if not cfg.jwt_secret:
cfg.jwt_secret = get_or_create_jwt_secret(
get_file_store(cfg.file_store, cfg.file_store_path)
)
# Utility function for command line --group argument
def get_llm_config_arg(

View File

@@ -105,14 +105,6 @@ class Message(BaseModel):
message_dict: dict = {'content': content, 'role': self.role}
# pop content if it's empty
if not content or (
len(content) == 1
and content[0]['type'] == 'text'
and content[0]['text'] == ''
):
message_dict.pop('content')
if role_tool_with_prompt_caching:
message_dict['cache_control'] = {'type': 'ephemeral'}
@@ -122,11 +114,21 @@ class Message(BaseModel):
def _add_tool_call_keys(self, message_dict: dict) -> dict:
"""Add tool call keys if we have a tool call or response.
NOTE: this is necessary for both native and non-native tool calling"""
NOTE: this is necessary for both native and non-native tool calling."""
# an assistant message calling a tool
if self.tool_calls is not None:
message_dict['tool_calls'] = self.tool_calls
message_dict['tool_calls'] = [
{
'id': tool_call.id,
'type': 'function',
'function': {
'name': tool_call.function.name,
'arguments': tool_call.function.arguments,
},
}
for tool_call in self.tool_calls
]
# an observation message with tool response
if self.tool_call_id is not None:

View File

@@ -0,0 +1,89 @@
# OpenHands Message Format and litellm Integration
## Overview
OpenHands uses its own `Message` class (`openhands/core/message.py`) which provides rich content support while maintaining compatibility with litellm's message handling system.
## Class Structure
Our `Message` class (`openhands/core/message.py`):
```python
class Message(BaseModel):
role: Literal['user', 'system', 'assistant', 'tool']
content: list[TextContent | ImageContent] = Field(default_factory=list)
cache_enabled: bool = False
vision_enabled: bool = False
condensable: bool = True
function_calling_enabled: bool = False
tool_calls: list[ChatCompletionMessageToolCall] | None = None
tool_call_id: str | None = None
name: str | None = None
event_id: int = -1
```
litellm's `Message` class (`litellm/types/utils.py`):
```python
class Message(OpenAIObject):
content: Optional[str]
role: Literal["assistant", "user", "system", "tool", "function"]
tool_calls: Optional[List[ChatCompletionMessageToolCall]]
function_call: Optional[FunctionCall]
audio: Optional[ChatCompletionAudioResponse] = None
```
## How It Works
1. **Message Creation**: Our `Message` class is a Pydantic model that supports rich content (text and images) through its `content` field.
2. **Serialization**: The class uses Pydantic's `@model_serializer` to convert messages into dictionaries that litellm can understand. We have two serialization methods:
```python
def _string_serializer(self) -> dict:
# convert content to a single string
content = '\n'.join(item.text for item in self.content if isinstance(item, TextContent))
message_dict: dict = {'content': content, 'role': self.role}
return self._add_tool_call_keys(message_dict)
def _list_serializer(self) -> dict:
content: list[dict] = []
for item in self.content:
d = item.model_dump()
if isinstance(item, TextContent):
content.append(d)
elif isinstance(item, ImageContent) and self.vision_enabled:
content.extend(d)
return {'content': content, 'role': self.role}
```
The appropriate serializer is chosen based on the message's capabilities:
```python
@model_serializer
def serialize_model(self) -> dict:
if self.cache_enabled or self.vision_enabled or self.function_calling_enabled:
return self._list_serializer()
return self._string_serializer()
```
3. **Tool Call Handling**: Tool calls require special attention in serialization because:
- They need to work with litellm's API calls (which accept both dicts and objects)
- They need to be properly serialized for token counting
- They need to maintain compatibility with different LLM providers' formats
4. **litellm Integration**: When we pass our messages to `litellm.completion()`, litellm doesn't care about the message class type - it works with the dictionary representation. This works because:
- litellm's transformation code (e.g., `litellm/llms/anthropic/chat/transformation.py`) processes messages based on their structure, not their type
- our serialization produces dictionaries that match litellm's expected format
- litellm handles rich content by looking at the message structure, supporting both simple string content and lists of content items
5. **Provider-Specific Handling**: litellm then transforms these messages into provider-specific formats (e.g., Anthropic, OpenAI) through its transformation layers, which know how to handle both simple and rich content structures.
### Token Counting
To use litellm's token counter, we need to make sure that all message components (including tool calls) are properly serialized to dictionaries. This is because:
- litellm's token counter expects dictionary structures
- Tool calls need to be included in the token count
- Different providers may count tokens differently for structured content
## Note
- We don't need to inherit from litellm's `Message` class because litellm works with dictionary representations, not class types
- Our rich content model is more sophisticated than litellm's basic string content, but litellm handles it correctly through its transformation layers
- The compatibility is maintained through proper serialization rather than inheritance

View File

@@ -23,6 +23,10 @@ class CmdOutputObservation(Observation):
def message(self) -> str:
return f'Command `{self.command}` executed with exit code {self.exit_code}.'
@property
def success(self) -> bool:
return not self.error
def __str__(self) -> str:
return f'**CmdOutputObservation (source={self.source}, exit code={self.exit_code})**\n{self.content}'
@@ -42,5 +46,9 @@ class IPythonRunCellObservation(Observation):
def message(self) -> str:
return 'Code executed in IPython cell.'
@property
def success(self) -> bool:
return True # IPython cells are always considered successful
def __str__(self) -> str:
return f'**IPythonRunCellObservation**\n{self.content}'

View File

@@ -83,6 +83,9 @@ def event_to_dict(event: 'Event') -> dict:
elif 'observation' in d:
d['content'] = props.pop('content', '')
d['extras'] = props
# Include success field for CmdOutputObservation
if hasattr(event, 'success'):
d['success'] = event.success
else:
raise ValueError('Event must be either action or observation')
return d

View File

@@ -50,4 +50,5 @@ def observation_from_dict(observation: dict) -> Observation:
observation.pop('message', None)
content = observation.pop('content', '')
extras = observation.pop('extras', {})
return observation_class(content=content, **extras)

View File

@@ -16,7 +16,7 @@ class DebugMixin:
debug_message = MESSAGE_SEPARATOR.join(
self._format_message_content(msg)
for msg in messages
if msg.get('content', None)
if msg['content'] is not None
)
if debug_message:

View File

@@ -321,7 +321,7 @@ def convert_fncall_messages_to_non_fncall_messages(
first_user_message_encountered = False
for message in messages:
role = message['role']
content = message.get('content', '')
content = message['content']
# 1. SYSTEM MESSAGES
# append system prompt suffix to content

View File

@@ -25,6 +25,7 @@ from litellm.exceptions import (
ServiceUnavailableError,
)
from litellm.types.utils import CostPerToken, ModelResponse, Usage
from litellm.utils import create_pretrained_tokenizer
from openhands.core.exceptions import CloudFlareBlockageError
from openhands.core.logger import openhands_logger as logger
@@ -122,6 +123,13 @@ class LLM(RetryMixin, DebugMixin):
if self.is_function_calling_active():
logger.debug('LLM: model supports function calling')
# if using a custom tokenizer, make sure it's loaded and accessible in the format expected by litellm
if self.config.custom_tokenizer is not None:
self.tokenizer = create_pretrained_tokenizer(self.config.custom_tokenizer)
else:
self.tokenizer = None
# set up the completion function
self._completion = partial(
litellm_completion,
model=self.config.model,
@@ -199,9 +207,17 @@ class LLM(RetryMixin, DebugMixin):
}
try:
# Record start time for latency measurement
start_time = time.time()
# we don't support streaming here, thus we get a ModelResponse
resp: ModelResponse = self._completion_unwrapped(*args, **kwargs)
# Calculate and record latency
latency = time.time() - start_time
response_id = resp.get('id', 'unknown')
self.metrics.add_response_latency(latency, response_id)
non_fncall_response = copy.deepcopy(resp)
if mock_function_calling:
assert len(resp.choices) == 1
@@ -436,6 +452,11 @@ class LLM(RetryMixin, DebugMixin):
self.metrics.accumulated_cost,
)
# Add latency to stats if available
if self.metrics.response_latencies:
latest_latency = self.metrics.response_latencies[-1]
stats += 'Response Latency: %.3f seconds\n' % latest_latency.latency
usage: Usage | None = response.get('usage')
if usage:
@@ -478,19 +499,43 @@ class LLM(RetryMixin, DebugMixin):
return cur_cost
def get_token_count(self, messages) -> int:
"""Get the number of tokens in a list of messages.
def get_token_count(self, messages: list[dict] | list[Message]) -> int:
"""Get the number of tokens in a list of messages. Use dicts for better token counting.
Args:
messages (list): A list of messages.
messages (list): A list of messages, either as a list of dicts or as a list of Message objects.
Returns:
int: The number of tokens.
"""
# attempt to convert Message objects to dicts, litellm expects dicts
if (
isinstance(messages, list)
and len(messages) > 0
and isinstance(messages[0], Message)
):
logger.info(
'Message objects now include serialized tool calls in token counting'
)
messages = self.format_messages_for_llm(messages) # type: ignore
# try to get the token count with the default litellm tokenizers
# or the custom tokenizer if set for this LLM configuration
try:
return litellm.token_counter(model=self.config.model, messages=messages)
except Exception:
# TODO: this is to limit logspam in case token count is not supported
return litellm.token_counter(
model=self.config.model,
messages=messages,
custom_tokenizer=self.tokenizer,
)
except Exception as e:
# limit logspam in case token count is not supported
logger.error(
f'Error getting token count for\n model {self.config.model}\n{e}'
+ (
f'\ncustom_tokenizer: {self.config.custom_tokenizer}'
if self.config.custom_tokenizer is not None
else ''
)
)
return 0
def _is_local(self) -> bool:

View File

@@ -9,15 +9,25 @@ class Cost(BaseModel):
timestamp: float = Field(default_factory=time.time)
class ResponseLatency(BaseModel):
"""Metric tracking the round-trip time per completion call."""
model: str
latency: float
response_id: str
class Metrics:
"""Metrics class can record various metrics during running and evaluation.
Currently, we define the following metrics:
accumulated_cost: the total cost (USD $) of the current LLM.
response_latency: the time taken for each LLM completion call.
"""
def __init__(self, model_name: str = 'default') -> None:
self._accumulated_cost: float = 0.0
self._costs: list[Cost] = []
self._response_latencies: list[ResponseLatency] = []
self.model_name = model_name
@property
@@ -34,26 +44,44 @@ class Metrics:
def costs(self) -> list[Cost]:
return self._costs
@property
def response_latencies(self) -> list[ResponseLatency]:
return self._response_latencies
def add_cost(self, value: float) -> None:
if value < 0:
raise ValueError('Added cost cannot be negative.')
self._accumulated_cost += value
self._costs.append(Cost(cost=value, model=self.model_name))
def add_response_latency(self, value: float, response_id: str) -> None:
if value < 0:
raise ValueError('Response latency cannot be negative.')
self._response_latencies.append(
ResponseLatency(
latency=value, model=self.model_name, response_id=response_id
)
)
def merge(self, other: 'Metrics') -> None:
self._accumulated_cost += other.accumulated_cost
self._costs += other._costs
self._response_latencies += other._response_latencies
def get(self) -> dict:
"""Return the metrics in a dictionary."""
return {
'accumulated_cost': self._accumulated_cost,
'costs': [cost.model_dump() for cost in self._costs],
'response_latencies': [
latency.model_dump() for latency in self._response_latencies
],
}
def reset(self):
self._accumulated_cost = 0.0
self._costs = []
self._response_latencies = []
def log(self):
"""Log the metrics."""

View File

@@ -51,6 +51,7 @@ class LongTermMemory:
self.embed_model = EmbeddingsLoader.get_embedding_model(
embedding_strategy, llm_config
)
logger.debug(f'Using embedding model: {self.embed_model}')
# instantiate the index
self.index = VectorStoreIndex.from_vector_store(vector_store, self.embed_model)

View File

@@ -24,9 +24,9 @@ jobs:
macro: ${{ vars.OPENHANDS_MACRO || '@openhands-agent' }}
max_iterations: ${{ fromJson(vars.OPENHANDS_MAX_ITER || 50) }}
base_container_image: ${{ vars.OPENHANDS_BASE_CONTAINER_IMAGE || '' }}
LLM_MODEL: ${{ vars.LLM_MODEL }}
secrets:
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
LLM_MODEL: ${{ secrets.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}

View File

@@ -285,7 +285,7 @@ async def process_issue(
success=success,
comment_success=comment_success,
success_explanation=success_explanation,
error=last_error,
error=last_error if not (comment_success and any(comment_success)) else None,
)
return output

View File

@@ -18,3 +18,7 @@ class ResolverOutput(BaseModel):
comment_success: list[bool] | None
success_explanation: str
error: str | None
@property
def has_partial_success(self) -> bool:
return bool(self.comment_success and any(self.comment_success))

View File

@@ -238,6 +238,7 @@ def send_pull_request(
fork_owner: str | None = None,
additional_message: str | None = None,
target_branch: str | None = None,
reviewer: str | None = None,
) -> str:
"""Send a pull request to a GitHub repository.
@@ -350,6 +351,19 @@ def send_pull_request(
response.raise_for_status()
pr_data = response.json()
# Request review if a reviewer was specified
if reviewer and pr_type != 'branch':
review_data = {'reviewers': [reviewer]}
review_response = requests.post(
f'{base_url}/pulls/{pr_data["number"]}/requested_reviewers',
headers=headers,
json=review_data,
)
if review_response.status_code != 201:
print(
f'Warning: Failed to request review from {reviewer}: {review_response.text}'
)
url = pr_data['html_url']
print(f'{pr_type} created: {url}\n\n--- Title: {pr_title}\n\n--- Body:\n{pr_body}')
@@ -520,6 +534,7 @@ def process_single_issue(
fork_owner: str | None,
send_on_failure: bool,
target_branch: str | None = None,
reviewer: str | None = None,
) -> None:
if not resolver_output.success and not send_on_failure:
print(
@@ -569,6 +584,7 @@ def process_single_issue(
fork_owner=fork_owner,
additional_message=resolver_output.success_explanation,
target_branch=target_branch,
reviewer=reviewer,
)
@@ -665,6 +681,12 @@ def main():
default=None,
help='Target branch to create the pull request against (defaults to repository default branch)',
)
parser.add_argument(
'--reviewer',
type=str,
help='GitHub username of the person to request review from',
default=None,
)
my_args = parser.parse_args()
github_token = (
@@ -718,6 +740,7 @@ def main():
my_args.fork_owner,
my_args.send_on_failure,
my_args.target_branch,
my_args.reviewer,
)

View File

@@ -8,6 +8,7 @@ class RuntimeBuilder(abc.ABC):
path: str,
tags: list[str],
platform: str | None = None,
extra_build_args: list[str] | None = None,
) -> str:
"""Build the runtime image.
@@ -15,6 +16,7 @@ class RuntimeBuilder(abc.ABC):
path (str): The path to the runtime image's build directory.
tags (list[str]): The tags to apply to the runtime image (e.g., ["repo:my-repo", "sha:my-sha"]).
platform (str, optional): The target platform for the build. Defaults to None.
extra_build_args (list[str], optional): Additional build arguments to pass to the builder. Defaults to None.
Returns:
str: The name:tag of the runtime image after build (e.g., "repo:sha").

View File

@@ -28,8 +28,8 @@ class DockerRuntimeBuilder(RuntimeBuilder):
path: str,
tags: list[str],
platform: str | None = None,
use_local_cache: bool = False,
extra_build_args: list[str] | None = None,
use_local_cache: bool = False,
) -> str:
"""Builds a Docker image using BuildKit and handles the build logs appropriately.

View File

@@ -23,7 +23,13 @@ class RemoteRuntimeBuilder(RuntimeBuilder):
self.session = requests.Session()
self.session.headers.update({'X-API-Key': self.api_key})
def build(self, path: str, tags: list[str], platform: str | None = None) -> str:
def build(
self,
path: str,
tags: list[str],
platform: str | None = None,
extra_build_args: list[str] | None = None,
) -> str:
"""Builds a Docker image using the Runtime API's /build endpoint."""
# Create a tar archive of the build context
tar_buffer = io.BytesIO()

View File

@@ -229,6 +229,7 @@ class EventStreamRuntime(Runtime):
platform=self.config.sandbox.platform,
extra_deps=self.config.sandbox.runtime_extra_deps,
force_rebuild=self.config.sandbox.force_rebuild_runtime,
extra_build_args=self.config.sandbox.runtime_extra_build_args,
)
self.log(

View File

@@ -24,7 +24,7 @@ def send_request(
timeout: int = 10,
**kwargs: Any,
) -> requests.Response:
response = session.request(method, url, **kwargs)
response = session.request(method, url, timeout=timeout, **kwargs)
try:
response.raise_for_status()
except requests.HTTPError as e:

View File

@@ -111,6 +111,7 @@ def build_runtime_image(
build_folder: str | None = None,
dry_run: bool = False,
force_rebuild: bool = False,
extra_build_args: List[str] | None = None,
) -> str:
"""Prepares the final docker build folder.
If dry_run is False, it will also build the OpenHands runtime Docker image using the docker build folder.
@@ -123,6 +124,7 @@ def build_runtime_image(
- build_folder (str): The directory to use for the build. If not provided a temporary directory will be used
- dry_run (bool): if True, it will only ready the build folder. It will not actually build the Docker image
- force_rebuild (bool): if True, it will create the Dockerfile which uses the base_image
- extra_build_args (List[str]): Additional build arguments to pass to the builder
Returns:
- str: <image_repo>:<MD5 hash>. Where MD5 hash is the hash of the docker build folder
@@ -139,6 +141,7 @@ def build_runtime_image(
dry_run=dry_run,
force_rebuild=force_rebuild,
platform=platform,
extra_build_args=extra_build_args,
)
return result
@@ -150,6 +153,7 @@ def build_runtime_image(
dry_run=dry_run,
force_rebuild=force_rebuild,
platform=platform,
extra_build_args=extra_build_args,
)
return result
@@ -162,6 +166,7 @@ def build_runtime_image_in_folder(
dry_run: bool,
force_rebuild: bool,
platform: str | None = None,
extra_build_args: List[str] | None = None,
) -> str:
runtime_image_repo, _ = get_runtime_image_repo_and_tag(base_image)
lock_tag = f'oh_v{oh_version}_{get_hash_for_lock_files(base_image)}'
@@ -193,6 +198,7 @@ def build_runtime_image_in_folder(
lock_tag,
versioned_tag,
platform,
extra_build_args=extra_build_args,
)
return hash_image_name
@@ -234,6 +240,7 @@ def build_runtime_image_in_folder(
if build_from == BuildFromImageType.SCRATCH
else None,
platform=platform,
extra_build_args=extra_build_args,
)
return hash_image_name
@@ -339,6 +346,7 @@ def _build_sandbox_image(
lock_tag: str,
versioned_tag: str | None,
platform: str | None = None,
extra_build_args: List[str] | None = None,
):
"""Build and tag the sandbox image. The image will be tagged with all tags that do not yet exist"""
names = [
@@ -350,7 +358,10 @@ def _build_sandbox_image(
names = [name for name in names if not runtime_builder.image_exists(name, False)]
image_name = runtime_builder.build(
path=str(build_folder), tags=names, platform=platform
path=str(build_folder),
tags=names,
platform=platform,
extra_build_args=extra_build_args,
)
if not image_name:
raise RuntimeError(f'Build failed for image {names}')

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