Compare commits

..

21 Commits

Author SHA1 Message Date
openhands
1968734c4d Parallelize action execution server startup 2025-05-27 22:17:18 +00:00
Xingyao Wang
35f7efb9d7 Fix: Remove strip() from parameter value extraction to preserve indentation (#8739)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-27 20:24:00 +00:00
Xuhui Zhou
14498c5e25 Feature/swe run interact (#8714)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-05-27 19:35:21 +00:00
sp.wack
cdb9aeb9ba fix(frontend): Don't show terminal commands in chat interface that are from the user (#8729) 2025-05-27 18:59:32 +00:00
Robert Brennan
318883e5e0 Fix VS Code tab and other runtime-dependent features showing null (#8734)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-27 18:41:31 +00:00
Rohit Malhotra
767b6ce600 [Refactor]: separate args setup logic for restarting conversations (#8679)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-05-27 13:16:33 -04:00
Xingyao Wang
3ccc96d794 Fix(docs): volumes configuration under [sandbox] in config.toml (#8724)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-28 00:30:07 +08:00
Yunfeng Yu
6f1effba5b Fix Feedback Submission Retry (#8693)
Co-authored-by: Test User <test@example.com>
Co-authored-by: Auroral <1596588744@163.com>
2025-05-27 14:48:30 +00:00
dependabot[bot]
bc223885a0 chore(deps): bump the version-all group across 1 directory with 9 updates (#8709)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-05-27 14:33:27 +00:00
sp.wack
0dcd5e9d30 hotfix(frontend): Clear error message on WS connect (#8725) 2025-05-27 10:12:56 -04:00
Engel Nyst
8ee85a45a2 Reduce more logs (#8712) 2025-05-27 16:05:04 +02:00
Marco Dalalba
342563d113 fix: url repo encode (#8713) 2025-05-27 17:39:13 +04:00
KianoshArian
af037b3a8a fix(frontend): fix mobile view of SettingsModal (#8711) 2025-05-27 17:38:31 +04:00
sp.wack
33b714e0a0 fix(frontend): Consider agent state errors (#8672) 2025-05-27 09:30:53 -04:00
Kent Johnson
35d2281717 feat: Add dev container (#8589) 2025-05-26 21:35:27 -04:00
dependabot[bot]
83bfbc7045 chore(deps): bump the version-all group across 1 directory with 16 updates (#8710)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 23:35:15 +02:00
CoreJa
11e6d40c7a bug: fix fn_call error during API response (#8695)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-05-26 14:10:10 +02:00
mamoodi
41d84ee8cd Update the versions to align with already released 0.39.2 (#8673) 2025-05-25 12:09:19 -04:00
CoreJa
0c2924453f bug: fix cache_control missing during convertion (#8692)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-05-25 15:35:34 +02:00
Graham Neubig
77cd05c33b Fix setup.sh error logging (#8678) 2025-05-24 17:23:26 -04:00
sp.wack
ff22712686 fix(frontend): Use uppercase event type for generic events without translation keys (#8671)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-24 18:20:08 +00:00
63 changed files with 2128 additions and 2006 deletions

View File

@@ -0,0 +1,15 @@
// For format details, see: https://aka.ms/devcontainer.json
{
"name": "Python 3",
// Documentation for this image:
// - https://github.com/devcontainers/templates/tree/main/src/python
// - https://github.com/microsoft/vscode-remote-try-python
// - https://hub.docker.com/r/microsoft/devcontainers-python
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers-extra/features/poetry:2": {},
"ghcr.io/devcontainers/features/node:1": {},
},
"postCreateCommand": ".devcontainer/setup.sh",
}

7
.devcontainer/setup.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
# Install `nc`
sudo apt update && sudo apt install netcat -y
# Do common setup tasks
source .openhands/setup.sh

5
.editorconfig Normal file
View File

@@ -0,0 +1,5 @@
[*]
# force *nix line endings so files don't look modified in container run from Windows clone
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

6
.gitattributes vendored
View File

@@ -1 +1,7 @@
*.ipynb linguist-vendored
# force *nix line endings so files don't look modified in container run from Windows clone
* text eol=lf
# Git incorrectly thinks some media is text
*.png -text
*.mp4 -text

9
.gitignore vendored
View File

@@ -161,9 +161,16 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.vscode/
.cursorignore
# VS Code: Ignore all but certain files that specify repo-specific settings.
# https://stackoverflow.com/questions/32964920/should-i-commit-the-vscode-folder-to-source-control
.vscode/**/*
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/settings.json
!.vscode/tasks.json
# evaluation
evaluation/evaluation_outputs
evaluation/outputs

View File

@@ -9,4 +9,5 @@ python -m pip install pre-commit
if [ -d ".git" ]; then
echo "Installing pre-commit hooks..."
pre-commit install
make install-pre-commit-hooks
fi

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
// force *nix line endings so files don't look modified in container run from Windows clone
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
}

146
README_CN.md Normal file
View File

@@ -0,0 +1,146 @@
<a name="readme-top"></a>
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: 少写代码,多做事</h1>
</div>
<div align="center">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="加入我们的Discord社区"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="致谢"></a>
<br/>
<a href="https://docs.all-hands.dev/modules/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="查看文档"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv论文"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="评估基准分数"></a>
<hr>
</div>
欢迎使用OpenHands前身为OpenDevin这是一个由AI驱动的软件开发代理平台。
OpenHands代理可以完成人类开发者能做的任何事情修改代码、运行命令、浏览网页、调用API甚至从StackOverflow复制代码片段。
在[docs.all-hands.dev](https://docs.all-hands.dev)了解更多信息,或[注册OpenHands Cloud](https://app.all-hands.dev)开始使用。
> [!IMPORTANT]
> 在工作中使用OpenHands我们很想与您交流填写
> [这份简短表格](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> 加入我们的设计合作伙伴计划,您将获得商业功能的早期访问权限,并有机会对我们的产品路线图提供意见。
![应用截图](./docs/static/img/screenshot.png)
## ☁️ OpenHands Cloud
开始使用OpenHands的最简单方式是在[OpenHands Cloud](https://app.all-hands.dev)上,
新用户可获得$50的免费额度。
## 💻 在本地运行OpenHands
OpenHands也可以使用Docker在本地系统上运行。
查看[运行OpenHands](https://docs.all-hands.dev/modules/usage/installation)指南了解
系统要求和更多信息。
> [!WARNING]
> 在公共网络上?请参阅我们的[强化Docker安装指南](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation)
> 通过限制网络绑定和实施其他安全措施来保护您的部署。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.39-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.39-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.39
```
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands
打开应用程序时您将被要求选择一个LLM提供商并添加API密钥。
[Anthropic的Claude Sonnet 4](https://www.anthropic.com/api)`anthropic/claude-sonnet-4-20250514`
效果最佳,但您还有[许多选择](https://docs.all-hands.dev/modules/usage/llms)。
## 💡 运行OpenHands的其他方式
> [!CAUTION]
> OpenHands旨在由单个用户在其本地工作站上运行。
> 它不适合多租户部署,即多个用户共享同一实例。没有内置的身份验证、隔离或可扩展性。
>
> 如果您有兴趣在多租户环境中运行OpenHands
> [与我们联系](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> 了解高级部署选项。
您还可以[将OpenHands连接到本地文件系统](https://docs.all-hands.dev/modules/usage/runtimes/docker#connecting-to-your-filesystem)
以可编程的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)运行OpenHands
通过[友好的CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode)与其交互,
或使用[GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)在标记的问题上运行它。
访问[运行OpenHands](https://docs.all-hands.dev/modules/usage/installation)获取更多信息和设置说明。
如果您想修改OpenHands源代码请查看[Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)。
遇到问题?[故障排除指南](https://docs.all-hands.dev/modules/usage/troubleshooting)可以提供帮助。
## 📖 文档
<a href="https://deepwiki.com/All-Hands-AI/OpenHands"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" title="DeepWiki自动生成文档"></a>
要了解有关项目的更多信息以及使用OpenHands的技巧
请查看我们的[文档](https://docs.all-hands.dev/modules/usage/getting-started)。
在那里您将找到有关如何使用不同LLM提供商、
故障排除资源和高级配置选项的资源。
## 🤝 如何加入社区
OpenHands是一个社区驱动的项目我们欢迎每个人的贡献。我们大部分沟通
通过Slack进行因此这是开始的最佳场所但我们也很乐意您通过Discord或Github与我们联系
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - 这里我们讨论研究、架构和未来发展。
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。
在[COMMUNITY.md](./COMMUNITY.md)中了解更多关于社区的信息,或在[CONTRIBUTING.md](./CONTRIBUTING.md)中找到有关贡献的详细信息。
## 📈 进展
在[这里](https://github.com/orgs/All-Hands-AI/projects/1)查看OpenHands月度路线图每月月底在维护者会议上更新
<p align="center">
<a href="https://star-history.com/#All-Hands-AI/OpenHands&Date">
<img src="https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date" width="500" alt="Star History Chart">
</a>
</p>
## 📜 许可证
根据MIT许可证分发。有关更多信息请参阅[`LICENSE`](./LICENSE)。
## 🙏 致谢
OpenHands由大量贡献者构建每一份贡献都备受感谢我们还借鉴了其他开源项目对他们的工作深表感谢。
有关OpenHands中使用的开源项目和许可证列表请参阅我们的[CREDITS.md](./CREDITS.md)文件。
## 📚 引用
```
@misc{openhands,
title={{OpenHands: An Open Platform for AI Software Developers as Generalist Agents}},
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
year={2024},
eprint={2407.16741},
archivePrefix={arXiv},
primaryClass={cs.SE},
url={https://arxiv.org/abs/2407.16741},
}
```

View File

@@ -325,6 +325,15 @@ classpath = "my_package.my_module.MyCustomAgent"
# Useful when deploying OpenHands in a remote machine where you need to expose a specific port.
#vscode_port = 41234
# Volume mounts in the format 'host_path:container_path[:mode]'
# e.g. '/my/host/dir:/workspace:rw'
# Multiple mounts can be specified using commas
# e.g. '/path1:/workspace/path1,/path2:/workspace/path2:ro'
# Configure volumes under the [sandbox] section:
# [sandbox]
# volumes = "/my/host/dir:/workspace:rw,/path2:/workspace/path2:ro"
#################################### Security ###################################
# Configuration for security features
##############################################################################

View File

@@ -1,273 +0,0 @@
# Runtime Building Procedure Design
## Architecture Overview
The OpenHands Docker Runtime is a core component that enables secure and flexible execution of AI agent actions. It creates a sandboxed environment using Docker, where arbitrary code can be run safely without risking the host system.
### Traditional Building Process
The traditional runtime building procedure follows these steps:
1. **Base Image Selection**: Takes a base image (e.g., `nikolaik/python-nodejs:python3.12-nodejs22`)
2. **Image Building**: Builds a new Docker image on top of it with OpenHands-specific code and dependencies
3. **Tagging System**: Uses a sophisticated tagging system to optimize rebuilds:
- **Source Tag** (`oh_v{version}_{lock_hash}_{source_hash}`): Most specific, includes source code hash
- **Lock Tag** (`oh_v{version}_{lock_hash}`): Based on dependencies and base image
- **Versioned Tag** (`oh_v{version}_{base_image}`): Most generic, based on OpenHands version and base image
4. **Dependency Installation**:
- System dependencies via apt-get (including tmux, git, etc.)
- Python dependencies via poetry and micromamba
- Chromium via playwright install
- VSCode server
- Other tools and configurations
5. **Optimization Strategies**:
- Reusing existing images when possible
- Caching dependencies
- Building in layers
### Key Components
1. **Action Execution Server**: Runs inside the Docker container and executes actions received from the OpenHands backend
2. **Browser Environment**: Uses Chromium installed via Playwright
3. **Bash Session**: Uses tmux for persistent terminal sessions
4. **Plugin System**: Supports extensions like Jupyter notebooks
## Two-Stage Building Approach
### Overview
The two-stage approach simplifies the runtime building procedure by:
1. Building all dependencies into an intermediate Docker image with everything in `/openhands` folder
2. For any arbitrary base image, simply copying the `/openhands` folder to form the final image
### Benefits
1. **Faster Builds**: Significantly reduces build time for new base images
2. **Flexibility**: Makes it easier to use arbitrary base images
3. **Cleaner Separation**: Clear distinction between OpenHands dependencies and the base image
4. **Simplified Maintenance**: Easier to update dependencies independently of the base image
5. **Reduced Duplication**: Avoids rebuilding the same dependencies for different base images
### Challenges and Solutions
#### 1. Binary Compatibility
**Challenge**: Binaries compiled in one environment might not work in another due to different system libraries.
**Solutions**:
- Include all necessary shared libraries in the `/openhands` folder
- Use wrapper scripts that set up the correct environment (LD_LIBRARY_PATH, etc.)
- For critical components like Chromium, include all dependencies in a self-contained manner
#### 2. Chromium Considerations
**Challenge**: Chromium has extensive system dependencies and might not work when simply copied.
**Solutions**:
- Use Playwright's self-contained Chromium distribution
- Include all Chromium dependencies in the `/openhands` folder
- Create a wrapper script that sets up the correct environment variables before launching Chromium
- Consider using container-in-container approach for Chromium if necessary
#### 3. tmux Considerations
**Challenge**: tmux depends on system libraries like libevent and ncurses.
**Solutions**:
- Include tmux and its dependencies in the `/openhands` folder
- Create a wrapper script that sets LD_LIBRARY_PATH to find the included libraries
- Consider statically linking tmux to reduce dependencies
#### 4. Path and Configuration Issues
**Challenge**: Hardcoded paths and configurations might break when moved.
**Solutions**:
- Use relative paths where possible
- Create configuration files at runtime based on the actual environment
- Use environment variables to specify paths instead of hardcoding them
#### 5. Permission Issues
**Challenge**: Copying files might not preserve permissions correctly.
**Solutions**:
- Explicitly set permissions after copying
- Use archive mode when copying to preserve permissions
- Handle user/group IDs consistently across images
## PyInstaller Approach
### Overview
The PyInstaller approach further simplifies the runtime building procedure by:
1. Using PyInstaller to bundle the action_execution_server and all its dependencies into a standalone binary
2. Packaging Playwright's Chromium browser in a portable way
3. Copying only the binary and browser components to the target runtime image
4. Eliminating the need to install Python and other dependencies in the target image
### Benefits
1. **Smaller Image Size**: Only the binary and browser components are needed, not all Python dependencies
2. **Faster Builds**: No need to install Python and dependencies in the target image
3. **Better Compatibility**: The binary should work on any Linux distribution with compatible glibc
4. **Simplified Maintenance**: Easier to update the binary independently of the base image
### Challenges and Solutions
#### 1. Binary Compatibility
**Challenge**: Binaries compiled in one environment might not work in another due to different system libraries.
**Solutions**:
- Build the binary in a minimal environment (e.g., Ubuntu 20.04) for maximum compatibility
- Include all necessary shared libraries in the binary
- Use static linking where possible
#### 2. Browser Integration
**Challenge**: Playwright requires Chromium and its dependencies.
**Solutions**:
- Extract Chromium from Playwright's cache
- Create a portable browser package
- Use wrapper scripts to set up the correct environment
#### 3. Plugin System
**Challenge**: The current plugin system might not work with a bundled binary.
**Solutions**:
- Modify the plugin system to work with the binary
- Include all plugins in the binary
- Implement a mechanism to load plugins at runtime
### Implementation
The implementation consists of three main components:
1. **PyInstaller Binary Builder**: Uses PyInstaller to bundle the action_execution_server and its dependencies
2. **Browser Packager**: Extracts and packages the Playwright browser for use with the binary
3. **Docker Image Builder**: Creates a minimal Docker image with the binary and browser components
## Implementation Plan
### Phase 1: Create PyInstaller Binary
1. Create a PyInstaller spec file for action_execution_server.py
2. Build the binary using PyInstaller
3. Extract and package Playwright's Chromium browser
4. Create a new Dockerfile template for the PyInstaller approach
### Phase 2: Update Runtime Builder
1. Update `runtime_build.py` to implement the PyInstaller approach
2. Add option to build using PyInstaller
3. Implement the copying mechanism for the binary and browser components
4. Simplify the tagging system for better clarity
### Phase 3: Integration and Testing
1. Test with various base images to ensure compatibility
2. Benchmark performance improvements
3. Verify all components work correctly (browser, bash, plugins, etc.)
4. Update documentation
### Phase 4: Optimization
1. Analyze and optimize the size of the binary
2. Implement selective features based on requirements
3. Further improve build performance
## Technical Details
### PyInstaller Binary Structure
```
/openhands/
├── action-execution-server/ # PyInstaller binary
│ ├── action-execution-server # Main executable
│ ├── _internal/ # PyInstaller bundled dependencies
│ └── ...
├── browser/ # Packaged Chromium browser
│ ├── ms-playwright/ # Playwright browser files
│ └── chromium-wrapper.sh # Wrapper script for Chromium
└── lib/ # Additional shared libraries if needed
```
### Dockerfile Template for PyInstaller Approach
```dockerfile
FROM {{ base_image }}
# Install minimal dependencies required by the base system
RUN if command -v apt-get > /dev/null; then \
apt-get update && apt-get install -y --no-install-recommends ca-certificates bash && \
apt-get clean && rm -rf /var/lib/apt/lists/*; \
elif command -v apk > /dev/null; then \
apk add --no-cache ca-certificates bash gcompat libstdc++; \
elif command -v yum > /dev/null; then \
yum install -y ca-certificates bash; \
yum clean all; \
fi
# Create the openhands user if it doesn't exist
RUN if ! id -u openhands > /dev/null 2>&1; then \
if command -v useradd > /dev/null 2>&1; then \
groupadd -g 1000 openhands 2>/dev/null || true; \
useradd -u 1000 -g 1000 -m -s /bin/bash openhands 2>/dev/null || true; \
elif command -v adduser > /dev/null 2>&1; then \
addgroup -g 1000 openhands 2>/dev/null || true; \
adduser -D -u 1000 -G openhands openhands 2>/dev/null || true; \
fi; \
fi
# Create directories
RUN mkdir -p /openhands/bin /openhands/lib /workspace && \
chown -R openhands:openhands /workspace /openhands 2>/dev/null || true
# Copy the bundled action execution server
COPY ./dist/pyinstaller/action-execution-server /openhands/action-execution-server
# Copy Playwright browser
COPY ./browser /openhands/browser
# Set environment variables
ENV PATH=/openhands/bin:$PATH \
LD_LIBRARY_PATH=/openhands/lib:$LD_LIBRARY_PATH \
PLAYWRIGHT_BROWSERS_PATH=/openhands/browser \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8
# Set the working directory
WORKDIR /workspace
# Switch to the openhands user
USER openhands
# Command to start the action execution server
CMD ["/openhands/action-execution-server/action-execution-server", "8000", "/workspace"]
```
### Usage
To build a runtime image using the PyInstaller approach:
```bash
python runtime_build_pyinstaller.py --base-image ubuntu:22.04
```
This will:
1. Build the PyInstaller binary
2. Package the Playwright browser
3. Create a Docker image with the binary and browser components
## Conclusion
Both the two-stage building approach and the PyInstaller approach offer significant benefits in terms of build speed, flexibility, and maintainability. The PyInstaller approach provides additional advantages in terms of image size and build simplicity, but may have challenges with plugin support and binary compatibility.
By bundling the action_execution_server and its dependencies into a standalone binary, we can achieve an even more efficient runtime building process that supports a wider range of base images while maintaining the security and isolation properties of the traditional approach.

View File

@@ -67,40 +67,55 @@ OpenHands' approach to building and managing runtime images ensures efficiency,
Check out the [relevant code](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/utils/runtime_build.py) if you are interested in more details.
OpenHands uses a two-stage build process for runtime images. See [Runtime Building Approach](runtime_build.md) for details.
### Image Tagging System
OpenHands uses a simple tagging system for its runtime images:
OpenHands uses a three-tag system for its runtime images to balance reproducibility with flexibility.
Tags may be in one of 2 formats:
- **Dependencies Image**: `oh_deps_v{openhands_version}` (e.g.: `oh_deps_v0.9.9`)
- **Runtime Image**: `oh_v{openhands_version}_image_{base_image}_tag_{tag}_{source_hash}`
(e.g.: `oh_v0.9.9_image_nikolaik_s_python-nodejs_tag_python3.12-nodejs22_1234abcd`)
- **Versioned Tag**: `oh_v{openhands_version}_{base_image}` (e.g.: `oh_v0.9.9_nikolaik_s_python-nodejs_t_python3.12-nodejs22`)
- **Lock Tag**: `oh_v{openhands_version}_{16_digit_lock_hash}` (e.g.: `oh_v0.9.9_1234567890abcdef`)
- **Source Tag**: `oh_v{openhands_version}_{16_digit_lock_hash}_{16_digit_source_hash}`
(e.g.: `oh_v0.9.9_1234567890abcdef_1234567890abcdef`)
#### Dependencies Image
#### Source Tag - Most Specific
This image contains all the dependencies needed by OpenHands, installed in the `/openhands` folder. It's built once per OpenHands version and can be reused for multiple runtime images.
This is the first 16 digits of the MD5 of the directory hash for the source directory. This gives a hash
for only the openhands source
#### Runtime Image
#### Lock Tag
This image is built by copying the `/openhands` folder from the dependencies image into any base image. The tag includes:
- The OpenHands version
- The base image name (transformed to fit in tag standard)
- A hash of the OpenHands source code
This hash is built from the first 16 digits of the MD5 of:
- The name of the base image upon which the image was built (e.g.: `nikolaik/python-nodejs:python3.12-nodejs22`)
- The content of the `pyproject.toml` included in the image.
- The content of the `poetry.lock` included in the image.
This effectively gives a hash for the dependencies of Openhands independent of the source code.
#### Versioned Tag - Most Generic
This tag is a concatenation of openhands version and the base image name (transformed to fit in tag standard).
#### Build Process
When generating an image:
When generating an image...
1. **Dependencies Image**: If the dependencies image doesn't exist, it's built first
2. **Runtime Image**: The runtime image is built by copying from the dependencies image
3. **Caching**: If a runtime image with the same tag already exists, it's reused unless force_rebuild is specified
- **No re-build**: OpenHands first checks whether an image with the same **most specific source tag** exists. If there is such an image,
no build is performed - the existing image is used.
- **Fastest re-build**: OpenHands next checks whether an image with the **generic lock tag** exists. If there is such an image,
OpenHands builds a new image based upon it, bypassing all installation steps (like `poetry install` and
`apt-get`) except a final operation to copy the current source code. The new image is tagged with a
**source** tag only.
- **Ok-ish re-build**: If neither a **source** nor **lock** tag exists, an image will be built based upon the **versioned** tag image.
In versioned tag image, most dependencies should already been installed hence saving time.
- **Slowest re-build**: If all of the three tags don't exists, a brand new image is built based upon the base
image (Which is a slower operation). This new image is tagged with all the **source**, **lock**, and **versioned** tags.
This approach offers several advantages:
- Faster build times for new base images
- Smaller final images (no duplicate dependencies)
- Better compatibility with different base images
- Easier maintenance and updates
This tagging approach allows OpenHands to efficiently manage both development and production environments.
1. Identical source code and Dockerfile always produce the same image (via hash-based tags)
2. The system can quickly rebuild images when minor changes occur (by leveraging recent compatible images)
3. The **lock** tag (e.g., `runtime:oh_v0.9.3_1234567890abcdef`) always points to the latest build for a particular base image, dependency, and OpenHands version combination
## Runtime Plugin System

View File

@@ -1,97 +0,0 @@
# Runtime Building Approach
This document describes the approach to building OpenHands runtime images.
## Overview
The OpenHands runtime building approach uses a two-stage process:
1. **Dependencies Image**: Build a single image containing all dependencies in the `/openhands` folder
2. **Runtime Image**: For any base image, copy the `/openhands` folder from the dependencies image
This approach offers several advantages:
- Faster build times for new base images
- Smaller final images (no duplicate dependencies)
- Better compatibility with different base images
- Easier maintenance and updates
## How It Works
### Dependencies Image
The dependencies image is built once and contains:
- All Python dependencies installed via Poetry
- Playwright and Chromium
- VSCode Server
- Tmux and other utilities
- Wrapper scripts for compatibility
Everything is installed into the `/openhands` folder, which is self-contained and can be copied to any base image.
### Runtime Image
The runtime image is built by:
1. Starting from any base image
2. Copying the `/openhands` folder from the dependencies image
3. Setting up environment variables to use the tools in `/openhands/bin`
4. Installing minimal dependencies required by the base system
## Wrapper Scripts
To ensure compatibility across different base images, wrapper scripts are provided in `/openhands/bin`:
- `oh-tmux`: Wrapper for tmux with proper library paths
- `oh-chromium`: Wrapper for Chromium with proper library paths
- `oh-playwright`: Wrapper for Playwright
- `oh-python`: Wrapper for Python with proper environment
- `oh-action-execution-server`: Wrapper for the action execution server
## Usage
To build a runtime image:
```bash
# Build the dependencies image (only needed once)
python -m openhands.runtime.utils.runtime_build --build_deps_only
# Build a runtime image using the dependencies image
python -m openhands.runtime.utils.runtime_build --base_image <base_image>
```
You can also specify a custom dependencies image:
```bash
python -m openhands.runtime.utils.runtime_build --base_image <base_image> --deps_image <deps_image>
```
## Compatibility Considerations
### Library Dependencies
The wrapper scripts ensure that the correct library paths are set, so tools like tmux and Chromium can find their dependencies in `/openhands/lib`.
### Base Image Requirements
The base image must have:
- Basic shell utilities (bash)
- CA certificates for HTTPS connections
- Compatible architecture (same as the dependencies image)
Most minimal base images (Alpine, Debian, Ubuntu) already meet these requirements.
## Implementation Details
The implementation consists of:
1. New Dockerfile templates:
- `Dockerfile.deps.j2`: Template for building the dependencies image
- `Dockerfile.runtime.j2`: Template for building the runtime image
2. Wrapper scripts in `/openhands/bin`:
- Ensure proper environment variables and library paths
- Handle compatibility issues across different base images
3. Build process:
- `BuildFromImageType.DEPS` option for the two-stage build
- Functions to build and use the dependencies image
- CLI options to control the build process

View File

@@ -331,6 +331,8 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
The sandbox configuration options are defined in the `[sandbox]` section of the `config.toml` file.
To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-e SANDBOX_TIMEOUT`.
### Execution

View File

@@ -2,6 +2,8 @@
This folder contains the evaluation harness that we built on top of the original [SWE-Bench benchmark](https://www.swebench.com/) ([paper](https://arxiv.org/abs/2310.06770)).
**UPDATE (5/26/2025): We now support running interactive SWE-Bench evaluation (see the paper [here](https://arxiv.org/abs/2502.13069))! For how to run it, checkout [this README](./SWE-Interact.md).**
**UPDATE (4/8/2025): We now support running SWT-Bench evaluation! For more details, checkout [the corresponding section](#SWT-Bench-Evaluation).**
**UPDATE (03/27/2025): We now support SWE-Bench multimodal evaluation! Simply use "princeton-nlp/SWE-bench_Multimodal" as the dataset name in the `run_infer.sh` script to evaluate on multimodal instances.**

View File

@@ -0,0 +1,92 @@
# SWE-Interact Benchmark
This document explains how to use the [Interactive SWE-Bench](https://arxiv.org/abs/2502.13069) benchmark scripts for running and evaluating interactive software engineering tasks.
## Setting things up
After following the [README](./README.md) to set up the environment, you would need to additionally add LLM configurations for simulated human users. In the original [paper](https://arxiv.org/abs/2502.13069), we use gpt-4o as the simulated human user. You can add the following to your `config.toml` file:
```toml
[llm.fake_user]
model="litellm_proxy/gpt-4o-2024-08-06"
api_key="<your-api-key>"
temperature = 0.0
base_url = "https://llm-proxy.eval.all-hands.dev"
```
## Running the Benchmark
The main script for running the benchmark is `run_infer_interact.sh`. Here's how to use it:
```bash
bash ./evaluation/benchmarks/swe_bench/scripts/run_infer_interact.sh <model_config> <commit_hash> <agent> <eval_limit> <max_iter> <num_workers> <split>
```
### Parameters:
- `model_config`: Path to the LLM configuration file (e.g., `llm.claude-3-7-sonnet`)
- `commit_hash`: Git commit hash to use (e.g., `HEAD`)
- `agent`: The agent class to use (e.g., `CodeActAgent`)
- `eval_limit`: Number of examples to evaluate (e.g., `500`)
- `max_iter`: Maximum number of iterations per task (e.g., `100`)
- `num_workers`: Number of parallel workers (e.g., `1`)
- `split`: Dataset split to use (e.g., `test`)
### Example:
```bash
bash ./evaluation/benchmarks/swe_bench/scripts/run_infer_interact.sh llm.claude-3-7-sonnet HEAD CodeActAgent 500 100 1 test
```
### Additional Environment Variables:
You can customize the behavior using these environment variables:
- `RUN_WITH_BROWSING`: Enable/disable web browsing (default: false)
- `USE_HINT_TEXT`: Enable/disable hint text (default: false)
- `EVAL_CONDENSER`: Specify a condenser configuration
- `EXP_NAME`: Add a custom experiment name to the output
- `N_RUNS`: Number of runs to perform (default: 1)
- `SKIP_RUNS`: Comma-separated list of run numbers to skip
## Evaluating Results
After running the benchmark, you can evaluate the results using `eval_infer.sh`:
```bash
./evaluation/benchmarks/swe_bench/scripts/eval_infer.sh <output_file> <instance_id> <dataset> <split>
```
### Parameters:
- `output_file`: Path to the output JSONL file
- `instance_id`: The specific instance ID to evaluate
- `dataset`: Dataset name (e.g., `cmu-lti/interactive-swe`)
- `split`: Dataset split (e.g., `test`)
### Example:
```bash
./evaluation/benchmarks/swe_bench/scripts/eval_infer.sh evaluation/evaluation_outputs/outputs/cmu-lti__interactive-swe-test/CodeActAgent/claude-3-7-sonnet-20250219_maxiter_100_N_v0.39.0-no-hint-run_1/output.jsonl sphinx-doc__sphinx-8721 cmu-lti/interactive-swe test
```
## Output Structure
The benchmark outputs are stored in the `evaluation/evaluation_outputs/outputs/` directory with the following structure:
```
evaluation/evaluation_outputs/outputs/
└── cmu-lti__interactive-swe-{split}/
└── {agent}/
└── {model}-{date}_maxiter_{max_iter}_N_{version}-{options}-run_{run_number}/
└── output.jsonl
```
Where:
- `{split}` is the dataset split (e.g., test)
- `{agent}` is the agent class name
- `{model}` is the model name
- `{date}` is the run date
- `{max_iter}` is the maximum iterations
- `{version}` is the OpenHands version
- `{options}` includes any additional options (e.g., no-hint, with-browsing)
- `{run_number}` is the run number

View File

@@ -0,0 +1,411 @@
import asyncio
import json
import os
import pandas as pd
from datasets import load_dataset
from litellm import completion as litellm_completion
import openhands.agenthub
from evaluation.benchmarks.swe_bench.run_infer import (
AgentFinishedCritic,
complete_runtime,
filter_dataset,
get_config,
initialize_runtime,
)
from evaluation.benchmarks.swe_bench.run_infer import (
get_instruction as base_get_instruction,
)
from evaluation.utils.shared import (
EvalException,
EvalMetadata,
EvalOutput,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
)
from openhands.controller.state.state import State
from openhands.core.config import (
get_llm_config_arg,
get_parser,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import MessageAction
from openhands.events.serialization.event import event_from_dict, event_to_dict
from openhands.utils.async_utils import call_async_from_sync
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false').lower() == 'true'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'false'
class FakeUser:
def __init__(self, issue, hints, files):
self.system_message = f"""
You are a GitHub user reporting an issue. Here are the details of your issue and environment:
Issue: {issue}
Hints: {hints}
Files relative to your current directory: {files}
Your task is to respond to questions from a coder who is trying to solve your issue. The coder has a summarized version of the issue you have. Follow these rules:
1. If the coder asks a question that is directly related to the information in the issue you have, provide that information.
2. Always stay in character as a user reporting an issue, not as an AI assistant.
3. Keep your responses concise and to the point.
4. The coder has limited turns to solve the issue. Do not interact with the coder beyond 3 turns.
Respond with "I don't have that information" if the question is unrelated or you're unsure.
"""
self.chat_history = [{'role': 'system', 'content': self.system_message}]
self.turns = 0
# Get LLM config from config.toml
self.llm_config = get_llm_config_arg(
'llm.fake_user'
) # You can change 'fake_user' to any config name you want
def generate_reply(self, question):
if self.turns > 3:
return 'Please continue working on the task. Do NOT ask for more help.'
self.chat_history.append({'role': 'user', 'content': question.content})
response = litellm_completion(
model=self.llm_config.model,
messages=self.chat_history,
api_key=self.llm_config.api_key.get_secret_value(),
temperature=self.llm_config.temperature,
base_url=self.llm_config.base_url,
)
reply = response.choices[0].message.content
self.chat_history.append({'role': 'assistant', 'content': reply})
self.turns += 1
return reply
# Global variable for fake user
fake_user = None
def get_fake_user_response(state: State) -> str:
global fake_user
if not fake_user:
return 'Please continue working on the task.'
last_agent_message = state.get_last_agent_message()
if last_agent_message:
return fake_user.generate_reply(last_agent_message)
return 'Please continue working on the task.'
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': get_fake_user_response,
}
def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageAction:
instance_copy = instance.copy()
instance_copy.problem_statement = f'{instance.problem_statement}\n\nHints:\nThe user has not provided all the necessary details about the issue, and there are some hidden details that are helpful. Please ask the user specific questions using non-code commands to gather the relevant information that the user has to help you solve the issue. Ensure you have all the details you require to solve the issue.'
return base_get_instruction(instance_copy, metadata)
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
) -> EvalOutput:
config = get_config(instance, metadata)
global fake_user
original_issue = instance.original_issue
issue = str(original_issue)
fake_user = FakeUser(issue=issue, hints=instance.hints_text, files=instance.files)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance, metadata)
message_action = get_instruction(instance, metadata)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=message_action,
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
metadata.agent_class
],
)
)
# if fatal error, throw EvalError to trigger re-run
if (
state
and state.last_error
and 'fatal error during agent execution' in state.last_error
and 'stuck in a loop' not in state.last_error
):
raise EvalException('Fatal error detected: ' + state.last_error)
# Get git patch
return_val = complete_runtime(runtime, instance)
git_patch = return_val['git_patch']
logger.info(
f'Got git diff for instance {instance.instance_id}:\n--------\n{git_patch}\n--------'
)
finally:
runtime.close()
# Prepare test result
test_result = {
'git_patch': git_patch,
}
if state is None:
raise ValueError('State should not be None.')
histories = [event_to_dict(event) for event in state.history]
metrics = state.metrics.get() if state.metrics else None
# Save the output
instruction = message_action.content
if message_action.image_urls:
instruction += (
'\n\n<image_urls>' + '\n'.join(message_action.image_urls) + '</image_urls>'
)
output = EvalOutput(
instance_id=instance.instance_id,
instruction=instruction,
instance=instance.to_dict(),
test_result=test_result,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
)
return output
if __name__ == '__main__':
parser = get_parser()
parser.add_argument(
'--dataset',
type=str,
default='cmu-lti/interactive-swe',
help='dataset to evaluate on',
)
parser.add_argument(
'--split',
type=str,
default='test',
help='split to evaluate on',
)
args, _ = parser.parse_known_args()
# Load dataset from huggingface datasets
dataset = load_dataset(args.dataset, split=args.split)
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
logger.info(
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_bench_tests)} tasks'
)
llm_config = None
if args.llm_config:
llm_config = get_llm_config_arg(args.llm_config)
llm_config.log_completions = True
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
llm_config.modify_params = False
if llm_config is None:
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
# Get condenser config from environment variable
condenser_name = os.environ.get('EVAL_CONDENSER')
if condenser_name:
condenser_config = get_condenser_config_arg(condenser_name)
if condenser_config is None:
raise ValueError(
f'Could not find Condenser config: EVAL_CONDENSER={condenser_name}'
)
else:
# If no specific condenser config is provided via env var, default to NoOpCondenser
condenser_config = NoOpCondenserConfig()
logger.debug(
'No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.'
)
details = {'mode': 'interact'}
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
dataset_descrption = (
args.dataset.replace('/', '__') + '-' + args.split.replace('/', '__')
)
metadata = make_metadata(
llm_config,
dataset_descrption,
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
details=details,
condenser_config=condenser_config,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
print(f'### OUTPUT FILE: {output_file} ###')
# Run evaluation in iterative mode:
# If a rollout fails to output AgentFinishAction, we will try again until it succeeds OR total 3 attempts have been made.
ITERATIVE_EVAL_MODE = (
os.environ.get('ITERATIVE_EVAL_MODE', 'false').lower() == 'true'
)
ITERATIVE_EVAL_MODE_MAX_ATTEMPTS = int(
os.environ.get('ITERATIVE_EVAL_MODE_MAX_ATTEMPTS', '3')
)
if not ITERATIVE_EVAL_MODE:
# load the dataset
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
if len(instances) > 0 and not isinstance(
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
):
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
instances[col] = instances[col].apply(lambda x: str(x))
run_evaluation(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
else:
critic = AgentFinishedCritic()
def get_cur_output_file_path(attempt: int) -> str:
return (
f'{output_file.removesuffix(".jsonl")}.critic_attempt_{attempt}.jsonl'
)
eval_ids = None
for attempt in range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1):
cur_output_file = get_cur_output_file_path(attempt)
logger.info(
f'Running evaluation with critic {critic.__class__.__name__} for attempt {attempt} of {ITERATIVE_EVAL_MODE_MAX_ATTEMPTS}.'
)
# For deterministic eval, we set temperature to 0.1 for (>1) attempt
# so hopefully we get slightly different results
if attempt > 1 and metadata.llm_config.temperature == 0:
logger.info(
f'Detected temperature is 0 for (>1) attempt {attempt}. Setting temperature to 0.1...'
)
metadata.llm_config.temperature = 0.1
# Load instances - at first attempt, we evaluate all instances
# On subsequent attempts, we only evaluate the instances that failed the previous attempt determined by critic
instances = prepare_dataset(
swe_bench_tests, cur_output_file, args.eval_n_limit, eval_ids=eval_ids
)
if len(instances) > 0 and not isinstance(
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
):
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
instances[col] = instances[col].apply(lambda x: str(x))
# Run evaluation - but save them to cur_output_file
logger.info(
f'Evaluating {len(instances)} instances for attempt {attempt}...'
)
run_evaluation(
instances,
metadata,
cur_output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
# When eval is done, we update eval_ids to the instances that failed the current attempt
instances_failed = []
logger.info(
f'Use critic {critic.__class__.__name__} to check {len(instances)} instances for attempt {attempt}...'
)
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
try:
history = [
event_from_dict(event) for event in instance['history']
]
critic_result = critic.evaluate(
history, instance['test_result'].get('git_patch', '')
)
if not critic_result.success:
instances_failed.append(instance['instance_id'])
except Exception as e:
logger.error(
f'Error loading history for instance {instance["instance_id"]}: {e}'
)
instances_failed.append(instance['instance_id'])
logger.info(
f'{len(instances_failed)} instances failed the current attempt {attempt}: {instances_failed}'
)
eval_ids = instances_failed
# If no instances failed, we break
if len(instances_failed) == 0:
break
# Then we should aggregate the results from all attempts into the original output file
# and remove the intermediate files
logger.info(
'Aggregating results from all attempts into the original output file...'
)
fout = open(output_file, 'w')
added_instance_ids = set()
for attempt in reversed(range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1)):
cur_output_file = get_cur_output_file_path(attempt)
if not os.path.exists(cur_output_file):
logger.warning(
f'Intermediate output file {cur_output_file} does not exist. Skipping...'
)
continue
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
# Also make sure git_patch is not empty - otherwise we fall back to previous attempt (empty patch is worse than anything else)
if (
instance['instance_id'] not in added_instance_ids
and instance['test_result'].get('git_patch', '').strip()
):
fout.write(line)
added_instance_ids.add(instance['instance_id'])
logger.info(
f'Aggregated instances from {cur_output_file}. Total instances added so far: {len(added_instance_ids)}'
)
fout.close()
logger.info(
f'Done! Total {len(added_instance_ids)} instances added to {output_file}'
)

View File

@@ -0,0 +1,131 @@
#!/usr/bin/env bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
MODEL_CONFIG=$1
COMMIT_HASH=$2
AGENT=$3
EVAL_LIMIT=$4
MAX_ITER=$5
NUM_WORKERS=$6
SPLIT=$8
N_RUNS=$9
if [ -z "$NUM_WORKERS" ]; then
NUM_WORKERS=1
echo "Number of workers not specified, use default $NUM_WORKERS"
fi
checkout_eval_branch
if [ -z "$AGENT" ]; then
echo "Agent not specified, use default CodeActAgent"
AGENT="CodeActAgent"
fi
if [ -z "$MAX_ITER" ]; then
echo "MAX_ITER not specified, use default 100"
MAX_ITER=100
fi
if [ -z "$RUN_WITH_BROWSING" ]; then
echo "RUN_WITH_BROWSING not specified, use default false"
RUN_WITH_BROWSING=false
fi
if [ -z "$DATASET" ]; then
echo "DATASET not specified, use default cmu-lti/interactive-swe"
DATASET="cmu-lti/interactive-swe"
fi
if [ -z "$SPLIT" ]; then
echo "SPLIT not specified, use default test"
SPLIT="test"
fi
if [ -n "$EVAL_CONDENSER" ]; then
echo "Using Condenser Config: $EVAL_CONDENSER"
else
echo "No Condenser Config provided via EVAL_CONDENSER, use default (NoOpCondenser)."
fi
export RUN_WITH_BROWSING=$RUN_WITH_BROWSING
echo "RUN_WITH_BROWSING: $RUN_WITH_BROWSING"
get_openhands_version
echo "AGENT: $AGENT"
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
echo "DATASET: $DATASET"
echo "SPLIT: $SPLIT"
echo "MAX_ITER: $MAX_ITER"
echo "NUM_WORKERS: $NUM_WORKERS"
echo "COMMIT_HASH: $COMMIT_HASH"
echo "EVAL_CONDENSER: $EVAL_CONDENSER"
# Default to NOT use Hint
if [ -z "$USE_HINT_TEXT" ]; then
export USE_HINT_TEXT=false
fi
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
EVAL_NOTE="$OPENHANDS_VERSION"
# if not using Hint, add -no-hint to the eval note
if [ "$USE_HINT_TEXT" = false ]; then
EVAL_NOTE="$EVAL_NOTE-no-hint"
fi
if [ "$RUN_WITH_BROWSING" = true ]; then
EVAL_NOTE="$EVAL_NOTE-with-browsing"
fi
if [ -n "$EXP_NAME" ]; then
EVAL_NOTE="$EVAL_NOTE-$EXP_NAME"
fi
# Add condenser config to eval note if provided
if [ -n "$EVAL_CONDENSER" ]; then
EVAL_NOTE="${EVAL_NOTE}-${EVAL_CONDENSER}"
fi
function run_eval() {
local eval_note="${1}"
COMMAND="poetry run python evaluation/benchmarks/swe_bench/run_infer_interact.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations $MAX_ITER \
--eval-num-workers $NUM_WORKERS \
--eval-note $eval_note \
--dataset $DATASET \
--split $SPLIT"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
# Run the command
eval $COMMAND
}
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
if [ -z "$N_RUNS" ]; then
N_RUNS=1
echo "N_RUNS not specified, use default $N_RUNS"
fi
# Skip runs if the run number is in the SKIP_RUNS list
# read from env variable SKIP_RUNS as a comma separated list of run numbers
SKIP_RUNS=(${SKIP_RUNS//,/ })
for i in $(seq 1 $N_RUNS); do
if [[ " ${SKIP_RUNS[@]} " =~ " $i " ]]; then
echo "Skipping run $i"
continue
fi
current_eval_note="$EVAL_NOTE-run_$i"
echo "EVAL_NOTE: $current_eval_note"
run_eval $current_eval_note
done
checkout_original_branch

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.39.1",
"version": "0.39.2",
"private": true,
"type": "module",
"engines": {
@@ -10,21 +10,21 @@
"@heroui/react": "2.7.8",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.6.0",
"@react-router/serve": "^7.6.0",
"@react-router/node": "^7.6.1",
"@react-router/serve": "^7.6.1",
"@react-types/shared": "^3.29.1",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.3.0",
"@tanstack/react-query": "^5.76.1",
"@tanstack/react-query": "^5.77.2",
"@vitejs/plugin-react": "^4.4.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.9.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.12.1",
"i18next": "^25.1.3",
"framer-motion": "^12.14.0",
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.28",
@@ -40,7 +40,7 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.6.0",
"react-router": "^7.6.1",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1",
@@ -83,9 +83,9 @@
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.52.0",
"@react-router/dev": "^7.6.0",
"@react-router/dev": "^7.6.1",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.74.7",
"@tanstack/eslint-plugin-query": "^5.78.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.3.0",

View File

@@ -28,41 +28,58 @@ export const getEventContent = (
let details: string = "";
if (isOpenHandsAction(event)) {
title = (
<Trans
i18nKey={`ACTION_MESSAGE$${event.action.toUpperCase()}`}
values={{
path: hasPathProperty(event.args) && event.args.path,
command:
hasCommandProperty(event.args) && trimText(event.args.command, 80),
mcp_tool_name: event.action === "call_tool_mcp" && event.args.name,
}}
components={{
path: <PathComponent />,
cmd: <MonoComponent />,
}}
/>
);
const actionKey = `ACTION_MESSAGE$${event.action.toUpperCase()}`;
// If translation key exists, use Trans component
if (i18n.exists(actionKey)) {
title = (
<Trans
i18nKey={actionKey}
values={{
path: hasPathProperty(event.args) && event.args.path,
command:
hasCommandProperty(event.args) &&
trimText(event.args.command, 80),
mcp_tool_name: event.action === "call_tool_mcp" && event.args.name,
}}
components={{
path: <PathComponent />,
cmd: <MonoComponent />,
}}
/>
);
} else {
// For generic actions, just use the uppercase type
title = event.action.toUpperCase();
}
details = getActionContent(event);
}
if (isOpenHandsObservation(event)) {
title = (
<Trans
i18nKey={`OBSERVATION_MESSAGE$${event.observation.toUpperCase()}`}
values={{
path: hasPathProperty(event.extras) && event.extras.path,
command:
hasCommandProperty(event.extras) &&
trimText(event.extras.command, 80),
mcp_tool_name: event.observation === "mcp" && event.extras.name,
}}
components={{
path: <PathComponent />,
cmd: <MonoComponent />,
}}
/>
);
const observationKey = `OBSERVATION_MESSAGE$${event.observation.toUpperCase()}`;
// If translation key exists, use Trans component
if (i18n.exists(observationKey)) {
title = (
<Trans
i18nKey={observationKey}
values={{
path: hasPathProperty(event.extras) && event.extras.path,
command:
hasCommandProperty(event.extras) &&
trimText(event.extras.command, 80),
mcp_tool_name: event.observation === "mcp" && event.extras.name,
}}
components={{
path: <PathComponent />,
cmd: <MonoComponent />,
}}
/>
);
} else {
// For generic observations, just use the uppercase type
title = event.observation.toUpperCase();
}
details = getObservationContent(event);
}

View File

@@ -1,6 +1,11 @@
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsEventType } from "#/types/core/base";
import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
import {
isCommandAction,
isCommandObservation,
isOpenHandsAction,
isOpenHandsObservation,
} from "#/types/core/guards";
import { OpenHandsObservation } from "#/types/core/observations";
const COMMON_NO_RENDER_LIST: OpenHandsEventType[] = [
@@ -15,11 +20,21 @@ export const shouldRenderEvent = (
event: OpenHandsAction | OpenHandsObservation,
) => {
if (isOpenHandsAction(event)) {
if (isCommandAction(event) && event.source === "user") {
// For user commands, we always hide them from the chat interface
return false;
}
const noRenderList = COMMON_NO_RENDER_LIST.concat(ACTION_NO_RENDER_LIST);
return !noRenderList.includes(event.action);
}
if (isOpenHandsObservation(event)) {
if (isCommandObservation(event) && event.source === "user") {
// For user commands, we always hide them from the chat interface
return false;
}
return !COMMON_NO_RENDER_LIST.includes(event.observation);
}

View File

@@ -2,32 +2,10 @@ import React from "react";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsObservation } from "#/types/core/observations";
import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
import { OpenHandsEventType } from "#/types/core/base";
import { EventMessage } from "./event-message";
import { ChatMessage } from "./chat-message";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
const COMMON_NO_RENDER_LIST: OpenHandsEventType[] = [
"system",
"agent_state_changed",
"change_agent_state",
];
const ACTION_NO_RENDER_LIST: OpenHandsEventType[] = ["recall"];
const shouldRenderEvent = (event: OpenHandsAction | OpenHandsObservation) => {
if (isOpenHandsAction(event)) {
const noRenderList = COMMON_NO_RENDER_LIST.concat(ACTION_NO_RENDER_LIST);
return !noRenderList.includes(event.action);
}
if (isOpenHandsObservation(event)) {
return !COMMON_NO_RENDER_LIST.includes(event.observation);
}
return true;
};
interface MessagesProps {
messages: (OpenHandsAction | OpenHandsObservation)[];
isAwaitingUserConfirmation: boolean;
@@ -54,7 +32,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
return (
<>
{messages.filter(shouldRenderEvent).map((message, index) => (
{messages.map((message, index) => (
<EventMessage
key={index}
event={message}

View File

@@ -16,6 +16,7 @@ interface FeedbackFormProps {
export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
const { t } = useTranslation();
const copiedToClipboardToast = () => {
hotToast(t(I18nKey.FEEDBACK$PASSWORD_COPIED_MESSAGE), {
icon: "📋",
@@ -127,7 +128,9 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
className="grow"
isDisabled={isPending}
>
{t(I18nKey.FEEDBACK$SHARE_LABEL)}
{isPending
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL) || "Submitting..."
: t(I18nKey.FEEDBACK$SHARE_LABEL)}
</BrandButton>
<BrandButton
type="button"
@@ -139,6 +142,12 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
{t(I18nKey.FEEDBACK$CANCEL_LABEL)}
</BrandButton>
</div>
{isPending && (
<p className="text-sm text-center text-neutral-400">
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE) ||
"Submitting your feedback, please wait..."}
</p>
)}
</form>
);
}

View File

@@ -84,7 +84,7 @@ export function RepositorySelectionForm({
const allRepositories = repositories?.concat(searchedRepos || []);
const repositoriesItems = allRepositories?.map((repo) => ({
key: repo.id,
label: repo.full_name,
label: decodeURIComponent(repo.full_name),
}));
const branchesItems = branches?.map((branch) => ({

View File

@@ -28,7 +28,7 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
{suggestedTasks?.map((taskGroup, index) => (
<TaskGroup
key={index}
title={taskGroup.title}
title={decodeURIComponent(taskGroup.title)}
tasks={taskGroup.tasks}
/>
))}

View File

@@ -68,7 +68,7 @@ export function ModelSelector({
const { t } = useTranslation();
return (
<div className="flex w-[680px] justify-between gap-[46px]">
<div className="flex flex-col md:flex-row w-[full] md:w-[680px] justify-between gap-4 md:gap-[46px]">
<fieldset className="flex flex-col gap-2.5 w-full">
<label className="text-sm">{t(I18nKey.LLM$PROVIDER)}</label>
<Autocomplete

View File

@@ -87,7 +87,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
name="llm-api-key-input"
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
type="password"
className="w-[680px]"
className="w-full"
placeholder={isLLMKeySet ? "<hidden>" : ""}
startContent={isLLMKeySet && <KeyStatusIcon isSet={isLLMKeySet} />}
/>

View File

@@ -21,7 +21,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
<ModalBackdrop>
<div
data-testid="ai-config-modal"
className="bg-base-secondary min-w-[384px] p-6 rounded-xl flex flex-col gap-2 border border-tertiary"
className="bg-base-secondary min-w-[384px] m-4 p-6 rounded-xl flex flex-col gap-2 border border-tertiary"
>
{aiConfigOptions.error && (
<p className="text-danger text-xs">{aiConfigOptions.error.message}</p>

View File

@@ -19,9 +19,11 @@ import { useUserProviders } from "#/hooks/use-user-providers";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { OpenHandsObservation } from "#/types/core/observations";
import {
isAgentStateChangeObservation,
isErrorObservation,
isOpenHandsAction,
isOpenHandsObservation,
isStatusUpdate,
isUserMessage,
} from "#/types/core/guards";
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
@@ -160,10 +162,33 @@ export function WsClientProvider({
function handleConnect() {
setStatus(WsClientProviderStatus.CONNECTED);
removeErrorMessage();
}
function handleMessage(event: Record<string, unknown>) {
if (isOpenHandsEvent(event)) {
const isStatusUpdateError =
isStatusUpdate(event) && event.type === "error";
const isAgentStateChangeError =
isAgentStateChangeObservation(event) &&
event.extras.agent_state === "error";
if (isStatusUpdateError || isAgentStateChangeError) {
const errorMessage = isStatusUpdate(event)
? event.message
: event.extras.reason || "Unknown error";
trackError({
message: errorMessage,
source: "chat",
metadata: { msgId: event.id },
});
setErrorMessage(errorMessage);
return;
}
if (isOpenHandsAction(event) || isOpenHandsObservation(event)) {
setParsedEvents((prevEvents) => [...prevEvents, event]);
}

View File

@@ -16,5 +16,7 @@ export const useSubmitFeedback = () => {
onError: (error) => {
displayErrorToast(error.message);
},
retry: 2,
retryDelay: 500,
});
};

View File

@@ -1,21 +1,14 @@
import { useQueries, useQuery } from "@tanstack/react-query";
import axios from "axios";
import React from "react";
import { useSelector } from "react-redux";
import OpenHands from "#/api/open-hands";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { RootState } from "#/store";
import { useConversationId } from "#/hooks/use-conversation-id";
import { useActiveConversation } from "./use-active-conversation";
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
export const useActiveHost = () => {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const [activeHost, setActiveHost] = React.useState<string | null>(null);
const { conversationId } = useConversationId();
const { data: conversation } = useActiveConversation();
const enabled =
conversation?.status === "RUNNING" &&
RUNTIME_INACTIVE_STATES.includes(curAgentState);
const runtimeIsReady = useRuntimeIsReady();
const { data } = useQuery({
queryKey: [conversationId, "hosts"],
@@ -23,7 +16,7 @@ export const useActiveHost = () => {
const hosts = await OpenHands.getWebHosts(conversationId);
return { hosts };
},
enabled,
enabled: runtimeIsReady && !!conversationId,
initialData: { hosts: [] },
meta: {
disableToast: true,

View File

@@ -1,23 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import { useSelector } from "react-redux";
import OpenHands from "#/api/open-hands";
import { useConversationId } from "#/hooks/use-conversation-id";
import { GitChange } from "#/api/open-hands.types";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { useActiveConversation } from "./use-active-conversation";
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
export const useGetGitChanges = () => {
const { conversationId } = useConversationId();
const { data: conversation } = useActiveConversation();
const [orderedChanges, setOrderedChanges] = React.useState<GitChange[]>([]);
const previousDataRef = React.useRef<GitChange[]>(null);
const { curAgentState } = useSelector((state: RootState) => state.agent);
const enabled =
conversation?.status === "RUNNING" &&
RUNTIME_INACTIVE_STATES.includes(curAgentState);
const runtimeIsReady = useRuntimeIsReady();
const result = useQuery({
queryKey: ["file_changes", conversationId],
@@ -25,7 +17,7 @@ export const useGetGitChanges = () => {
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
enabled,
enabled: runtimeIsReady && !!conversationId,
meta: {
disableToast: true,
},

View File

@@ -1,13 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import OpenHands from "#/api/open-hands";
import { useConversationId } from "#/hooks/use-conversation-id";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import { useActiveConversation } from "./use-active-conversation";
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
// Define the return type for the VS Code URL query
interface VSCodeUrlResult {
@@ -18,11 +15,7 @@ interface VSCodeUrlResult {
export const useVSCodeUrl = () => {
const { t } = useTranslation();
const { conversationId } = useConversationId();
const { data: conversation } = useActiveConversation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const enabled =
conversation?.status === "RUNNING" &&
RUNTIME_INACTIVE_STATES.includes(curAgentState);
const runtimeIsReady = useRuntimeIsReady();
return useQuery<VSCodeUrlResult>({
queryKey: ["vscode_url", conversationId],
@@ -40,7 +33,7 @@ export const useVSCodeUrl = () => {
error: t(I18nKey.VSCODE$URL_NOT_AVAILABLE),
};
},
enabled,
enabled: runtimeIsReady && !!conversationId,
refetchOnMount: true,
retry: 3,
});

View File

@@ -0,0 +1,19 @@
import { useSelector } from "react-redux";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { useActiveConversation } from "./query/use-active-conversation";
/**
* Hook to determine if the runtime is ready for operations
*
* @returns boolean indicating if the runtime is ready
*/
export const useRuntimeIsReady = (): boolean => {
const { data: conversation } = useActiveConversation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
return (
conversation?.status === "RUNNING" &&
!RUNTIME_INACTIVE_STATES.includes(curAgentState)
);
};

View File

@@ -228,6 +228,8 @@ export enum I18nKey {
FEEDBACK$FAILED_TO_SHARE = "FEEDBACK$FAILED_TO_SHARE",
FEEDBACK$COPY_LABEL = "FEEDBACK$COPY_LABEL",
FEEDBACK$SHARING_SETTINGS_LABEL = "FEEDBACK$SHARING_SETTINGS_LABEL",
FEEDBACK$SUBMITTING_LABEL = "FEEDBACK$SUBMITTING_LABEL",
FEEDBACK$SUBMITTING_MESSAGE = "FEEDBACK$SUBMITTING_MESSAGE",
SECURITY$UNKNOWN_ANALYZER_LABEL = "SECURITY$UNKNOWN_ANALYZER_LABEL",
INVARIANT$UPDATE_POLICY_LABEL = "INVARIANT$UPDATE_POLICY_LABEL",
INVARIANT$UPDATE_SETTINGS_LABEL = "INVARIANT$UPDATE_SETTINGS_LABEL",

View File

@@ -8766,5 +8766,37 @@
"tr": "Uzman ipucu",
"de": "Profi-Tipp",
"uk": "Порада професіонала"
},
"FEEDBACK$SUBMITTING_LABEL": {
"en": "Submitting...",
"ja": "送信中...",
"zh-CN": "提交中...",
"zh-TW": "提交中...",
"ko-KR": "제출 중...",
"no": "Sender...",
"it": "Inviando...",
"pt": "Enviando...",
"es": "Enviando...",
"ar": "إرسال...",
"fr": "Envoi...",
"tr": "Gönderiliyor...",
"de": "Senden...",
"uk": "Відправляємо..."
},
"FEEDBACK$SUBMITTING_MESSAGE": {
"en": "Submitting feedback, please wait...",
"ja": "フィードバックを送信中です。しばらくお待ちください...",
"zh-CN": "正在提交反馈,请稍候...",
"zh-TW": "正在提交回饋,請稍候...",
"ko-KR": "피드백을 제출하고 있습니다. 잠시만 기다려주세요...",
"no": "Sender inn feedback, vennligst vent...",
"it": "Invio feedback, attendi...",
"pt": "Enviando feedback, por favor aguarde...",
"es": "Enviando comentarios, por favor espere...",
"ar": "إرسال التغذية الرجعية، يرجى الإنتظار...",
"fr": "Envoi de commentaires, veuillez patienter...",
"tr": "Geri bildirim gönderiliyor, lütfen bekleyin...",
"de": "Feedback senden, bitte warten...",
"uk": "Відправляємо відгук, будь ласка, почекайте..."
}
}

View File

@@ -20,7 +20,7 @@ export interface SystemMessageAction extends OpenHandsActionEvent<"system"> {
}
export interface CommandAction extends OpenHandsActionEvent<"run"> {
source: "agent";
source: "agent" | "user";
args: {
command: string;
security_risk: ActionSecurityRisk;

View File

@@ -4,13 +4,16 @@ import {
AssistantMessageAction,
OpenHandsAction,
SystemMessageAction,
CommandAction,
} from "./actions";
import {
AgentStateChangeObservation,
CommandObservation,
ErrorObservation,
MCPObservation,
OpenHandsObservation,
} from "./observations";
import { StatusUpdate } from "./variances";
export const isOpenHandsAction = (
event: OpenHandsParsedEvent,
@@ -39,6 +42,15 @@ export const isErrorObservation = (
): event is ErrorObservation =>
isOpenHandsObservation(event) && event.observation === "error";
export const isCommandAction = (
event: OpenHandsParsedEvent,
): event is CommandAction => isOpenHandsAction(event) && event.action === "run";
export const isAgentStateChangeObservation = (
event: OpenHandsParsedEvent,
): event is AgentStateChangeObservation =>
isOpenHandsObservation(event) && event.observation === "agent_state_changed";
export const isCommandObservation = (
event: OpenHandsParsedEvent,
): event is CommandObservation =>
@@ -63,3 +75,8 @@ export const isMcpObservation = (
event: OpenHandsParsedEvent,
): event is MCPObservation =>
isOpenHandsObservation(event) && event.observation === "mcp";
export const isStatusUpdate = (
event: OpenHandsParsedEvent,
): event is StatusUpdate =>
"status_update" in event && "type" in event && "id" in event;

View File

@@ -6,11 +6,12 @@ export interface AgentStateChangeObservation
source: "agent";
extras: {
agent_state: AgentState;
reason?: string;
};
}
export interface CommandObservation extends OpenHandsObservationEvent<"run"> {
source: "agent";
source: "agent" | "user";
extras: {
command: string;
hidden?: boolean;

View File

@@ -33,7 +33,15 @@ interface LocalUserMessageAction {
};
}
export interface StatusUpdate {
status_update: true;
type: "error";
id: string;
message: string;
}
export type OpenHandsVariance =
| TokenConfig
| InitConfig
| LocalUserMessageAction;
| LocalUserMessageAction
| StatusUpdate;

View File

@@ -189,10 +189,14 @@ class AgentController:
# Add the system message to the event stream
# This should be done for all agents, including delegates
system_message = self.agent.get_system_message()
logger.debug(f'System message got from agent: {system_message}')
if system_message:
if system_message and system_message.content:
preview = (
system_message.content[:50] + '...'
if len(system_message.content) > 50
else system_message.content
)
logger.debug(f'System message: {preview}')
self.event_stream.add_event(system_message, EventSource.AGENT)
logger.debug(f'System message added to event stream: {system_message}')
async def close(self, set_stop_state: bool = True) -> None:
"""Closes the agent controller, canceling any ongoing tasks and unsubscribing from the event stream.

View File

@@ -536,14 +536,20 @@ def convert_fncall_messages_to_non_fncall_messages(
if isinstance(content, str):
content = prefix + content
elif isinstance(content, list):
if content and content[-1]['type'] == 'text':
content[-1]['text'] = prefix + content[-1]['text']
if content and (
first_text_content := next(
(c for c in content if c['type'] == 'text'), None
)
):
first_text_content['text'] = prefix + first_text_content['text']
else:
content = [{'type': 'text', 'text': prefix}] + content
else:
raise FunctionCallConversionError(
f'Unexpected content type {type(content)}. Expected str or list. Content: {content}'
)
if 'cache_control' in message:
content[-1]['cache_control'] = {'type': 'ephemeral'}
converted_messages.append({'role': 'user', 'content': content})
else:
raise FunctionCallConversionError(
@@ -576,7 +582,7 @@ def _extract_and_validate_params(
found_params = set()
for param_match in param_matches:
param_name = param_match.group(1)
param_value = param_match.group(2).strip()
param_value = param_match.group(2)
# Validate parameter is allowed
if allowed_params and param_name not in allowed_params:

View File

@@ -450,7 +450,9 @@ class LLM(RetryMixin, DebugMixin):
pass
from openhands.io import json
logger.debug(f'Model info: {json.dumps(self.model_info, indent=2)}')
logger.debug(
f'Model info: {json.dumps({"model": self.config.model, "base_url": self.config.base_url}, indent=2)}'
)
if self.config.model.startswith('huggingface'):
# HF doesn't support the OpenAI default value for top_p (1)

View File

@@ -194,7 +194,6 @@ def load_microagents_from_dir(
logger.debug(f'Loading agents from {microagent_dir}')
if microagent_dir.exists():
for file in microagent_dir.rglob('*.md'):
logger.debug(f'Checking file {file}...')
# skip README.md
if file.name == 'README.md':
continue
@@ -204,9 +203,6 @@ def load_microagents_from_dir(
repo_agents[agent.name] = agent
elif isinstance(agent, KnowledgeMicroagent):
knowledge_agents[agent.name] = agent
logger.debug(
f'Loaded agent {agent.name} from {file}. Type: {type(agent)}'
)
except MicroagentValidationError as e:
# For validation errors, include the original exception
error_msg = f'Error loading microagent from {file}: {str(e)}'
@@ -216,4 +212,8 @@ def load_microagents_from_dir(
error_msg = f'Error loading microagent from {file}: {str(e)}'
raise ValueError(error_msg) from e
logger.debug(
f'Loaded {len(repo_agents) + len(knowledge_agents)} microagents: '
f'{[*repo_agents.keys(), *knowledge_agents.keys()]}'
)
return repo_agents, knowledge_agents

View File

@@ -72,7 +72,7 @@ from openhands.runtime.utils.log_capture import capture_logs
from openhands.runtime.utils.memory_monitor import MemoryMonitor
from openhands.runtime.utils.runtime_init import init_user_and_working_directory
from openhands.runtime.utils.system_stats import get_system_stats
from openhands.utils.async_utils import call_sync_from_async, wait_all
from openhands.utils.async_utils import call_sync_from_async
# Set MCP router logger to the same level as the main logger
mcp_router_logger.setLevel(logger.getEffectiveLevel())
@@ -255,7 +255,7 @@ class ActionExecutor:
logger.debug('Browser is ready')
async def ainit(self):
# bash needs to be initialized first
# Initialize bash session first as it's required by other components
logger.debug('Initializing bash session')
if sys.platform == 'win32':
self.bash_session = WindowsPowershellSession( # type: ignore[name-defined]
@@ -278,30 +278,74 @@ class ActionExecutor:
self.bash_session.initialize()
logger.debug('Bash session initialized')
# Start browser initialization in the background
self.browser_init_task = asyncio.create_task(self._init_browser_async())
# Start all initializations concurrently
init_tasks = []
# Start browser initialization
browser_task = asyncio.create_task(self._init_browser_async())
init_tasks.append(browser_task)
self.browser_init_task = browser_task
logger.debug('Browser initialization started in background')
await wait_all(
(self._init_plugin(plugin) for plugin in self.plugins_to_load),
timeout=60,
)
logger.debug('All plugins initialized')
# Start plugin initializations concurrently
plugin_tasks = [
asyncio.create_task(self._init_plugin(plugin))
for plugin in self.plugins_to_load
]
init_tasks.extend(plugin_tasks)
logger.debug(f'Started {len(plugin_tasks)} plugin initialization tasks')
# Wait for all plugin initializations to complete first
# This is necessary because other initializations may depend on plugins
try:
await asyncio.gather(*plugin_tasks)
logger.debug('All plugins initialized')
except Exception as e:
logger.error(f'Error initializing plugins: {e}')
# Re-raise to prevent further initialization if plugins fail
raise
# Start remaining initializations concurrently now that plugins are ready
remaining_tasks = []
# Start bash commands initialization
bash_commands_task = asyncio.create_task(self._init_bash_commands())
remaining_tasks.append(bash_commands_task)
logger.debug('Bash commands initialization started in background')
# This is a temporary workaround
# TODO: refactor AgentSkills to be part of JupyterPlugin
# AFTER ServerRuntime is deprecated
logger.debug('Initializing AgentSkills')
if 'agent_skills' in self.plugins and 'jupyter' in self.plugins:
obs = await self.run_ipython(
IPythonRunCellAction(
code='from openhands.runtime.plugins.agent_skills.agentskills import *\n'
logger.debug('Initializing AgentSkills in background')
agent_skills_task = asyncio.create_task(
self.run_ipython(
IPythonRunCellAction(
code='from openhands.runtime.plugins.agent_skills.agentskills import *\n'
)
)
)
logger.debug(f'AgentSkills initialized: {obs}')
remaining_tasks.append(agent_skills_task)
logger.debug('Initializing bash commands')
await self._init_bash_commands()
# Wait for all remaining initialization tasks to complete
if remaining_tasks:
try:
await asyncio.gather(*remaining_tasks)
logger.debug('All remaining initialization tasks completed')
except Exception as e:
logger.error(f'Error in remaining initialization tasks: {e}')
# Continue execution even if these tasks fail
# as they're not critical for basic functionality
# Wait for browser initialization to complete if it hasn't already
if not browser_task.done():
try:
await browser_task
logger.debug('Browser initialization completed')
except Exception as e:
logger.error(f'Error completing browser initialization: {e}')
# Browser is not critical for basic functionality
logger.debug('All initialization tasks completed')
logger.debug('Runtime client initialized.')
self._initialized = True

View File

@@ -431,7 +431,7 @@ class Runtime(FileEditRuntimeMixin):
)
action.set_hard_timeout(600)
obs = self.run_action(action)
if isinstance(obs, CmdOutputObservation) and obs.exit_code != 0:
if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0:
self.log('error', f'Setup script failed: {obs.content}')
@property

View File

@@ -19,37 +19,26 @@ from openhands.runtime.builder import DockerRuntimeBuilder, RuntimeBuilder
class BuildFromImageType(Enum):
DEPS = 'deps' # Two-stage build: Use a pre-built dependencies image and copy /openhands folder
SCRATCH = 'scratch' # Slowest: Build from base image (no dependencies are reused)
VERSIONED = 'versioned' # Medium speed: Reuse the most recent image with the same base image & OH version (a lot of dependencies are already installed)
LOCK = 'lock' # Fastest: Reuse the most recent image with the exact SAME dependencies (lock files)
def get_runtime_image_repo() -> str:
return os.getenv('OH_RUNTIME_RUNTIME_IMAGE_REPO', 'ghcr.io/all-hands-ai/runtime')
def get_deps_image_name() -> str:
"""Get the name of the dependencies image.
Returns:
str: The name of the dependencies image
"""
repo = get_runtime_image_repo()
deps_tag = f'oh_deps_v{oh_version}'
return f'{repo}:{deps_tag}'
def _generate_dockerfile(
base_image: str,
build_from: BuildFromImageType = BuildFromImageType.DEPS,
build_from: BuildFromImageType = BuildFromImageType.SCRATCH,
extra_deps: str | None = None,
deps_image: str | None = None,
) -> str:
"""Generate the Dockerfile content for the runtime image based on the base image.
Parameters:
- base_image (str): The base image provided for the runtime image
- build_from (BuildFromImageType): The build method for the runtime image.
- extra_deps (str): Extra dependencies to install
- deps_image (str): The dependencies image to use (only for DEPS build method)
- extra_deps (str):
Returns:
- str: The resulting Dockerfile content
@@ -59,14 +48,14 @@ def _generate_dockerfile(
searchpath=os.path.join(os.path.dirname(__file__), 'runtime_templates')
)
)
template = env.get_template('Dockerfile.runtime.j2')
template = env.get_template('Dockerfile.j2')
dockerfile_content = template.render(
base_image=base_image,
deps_image=deps_image or get_deps_image_name(),
build_from_scratch=build_from == BuildFromImageType.SCRATCH,
build_from_versioned=build_from == BuildFromImageType.VERSIONED,
extra_deps=extra_deps if extra_deps is not None else '',
)
return dockerfile_content
@@ -113,7 +102,8 @@ def get_runtime_image_repo_and_tag(base_image: str) -> tuple[str, str]:
return get_runtime_image_repo(), new_tag
def build_deps_image(
def build_runtime_image(
base_image: str,
runtime_builder: RuntimeBuilder,
platform: str | None = None,
extra_deps: str | None = None,
@@ -122,92 +112,152 @@ def build_deps_image(
force_rebuild: bool = False,
extra_build_args: list[str] | None = None,
) -> str:
"""Build the dependencies image containing all OpenHands dependencies.
"""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.
Parameters:
- base_image (str): The name of the base Docker image to use
- runtime_builder (RuntimeBuilder): The runtime builder to use
- platform (str): The target platform for the build (e.g. linux/amd64, linux/arm64)
- extra_deps (str): Extra dependencies to install
- extra_deps (str):
- 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: The name of the dependencies image
- str: <image_repo>:<MD5 hash>. Where MD5 hash is the hash of the docker build folder
See https://docs.all-hands.dev/modules/usage/architecture/runtime for more details.
"""
if build_folder is None:
with tempfile.TemporaryDirectory() as temp_dir:
result = build_deps_image_in_folder(
result = build_runtime_image_in_folder(
base_image=base_image,
runtime_builder=runtime_builder,
build_folder=Path(temp_dir),
extra_deps=extra_deps,
dry_run=dry_run,
force_rebuild=force_rebuild,
platform=platform,
extra_build_args=extra_build_args,
)
return result
result = build_deps_image_in_folder(
result = build_runtime_image_in_folder(
base_image=base_image,
runtime_builder=runtime_builder,
build_folder=Path(build_folder),
extra_deps=extra_deps,
dry_run=dry_run,
force_rebuild=force_rebuild,
platform=platform,
extra_build_args=extra_build_args,
)
return result
def build_deps_image_in_folder(
def build_runtime_image_in_folder(
base_image: str,
runtime_builder: RuntimeBuilder,
build_folder: Path,
extra_deps: str | None,
dry_run: bool,
force_rebuild: bool,
platform: str | None = None,
extra_build_args: list[str] | None = None,
) -> str:
"""Prepares the build folder and builds the dependencies image.
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)}'
versioned_tag = (
# truncate the base image to 96 characters to fit in the tag max length (128 characters)
f'oh_v{oh_version}_{get_tag_for_versioned_image(base_image)}'
)
versioned_image_name = f'{runtime_image_repo}:{versioned_tag}'
source_tag = f'{lock_tag}_{get_hash_for_source_files()}'
hash_image_name = f'{runtime_image_repo}:{source_tag}'
Parameters:
- runtime_builder (RuntimeBuilder): The runtime builder to use
- build_folder (Path): The directory to use for the build
- extra_deps (str): Extra dependencies to install
- dry_run (bool): if True, it will only ready the build folder. It will not actually build the Docker image
- platform (str): The target platform for the build (e.g. linux/amd64, linux/arm64)
- extra_build_args (List[str]): Additional build arguments to pass to the builder
Returns:
- str: The name of the dependencies image
"""
deps_image_name = get_deps_image_name()
logger.info(f'Building dependencies image: {deps_image_name}')
# Create a Dockerfile for the dependencies image
env = Environment(
loader=FileSystemLoader(
searchpath=os.path.join(os.path.dirname(__file__), 'runtime_templates')
logger.info(f'Building image: {hash_image_name}')
if force_rebuild:
logger.debug(
f'Force rebuild: [{runtime_image_repo}:{source_tag}] from scratch.'
)
)
template = env.get_template('Dockerfile.deps.j2')
dockerfile_content = template.render(
extra_deps=extra_deps if extra_deps is not None else '',
)
prep_build_folder(
build_folder,
base_image,
build_from=BuildFromImageType.SCRATCH,
extra_deps=extra_deps,
)
if not dry_run:
_build_sandbox_image(
build_folder,
runtime_builder,
runtime_image_repo,
source_tag,
lock_tag,
versioned_tag,
platform,
extra_build_args=extra_build_args,
)
return hash_image_name
with open(Path(build_folder, 'Dockerfile'), 'w') as file:
file.write(dockerfile_content)
lock_image_name = f'{runtime_image_repo}:{lock_tag}'
build_from = BuildFromImageType.SCRATCH
# Copy wrapper scripts
wrappers_dir = os.path.join(os.path.dirname(__file__), 'wrappers')
target_dir = os.path.join(build_folder, 'code', 'openhands', 'runtime', 'utils', 'wrappers')
os.makedirs(target_dir, exist_ok=True)
for file in os.listdir(wrappers_dir):
shutil.copy(os.path.join(wrappers_dir, file), os.path.join(target_dir, file))
# If the exact image already exists, we do not need to build it
if runtime_builder.image_exists(hash_image_name, False):
logger.debug(f'Reusing Image [{hash_image_name}]')
return hash_image_name
# Copy project files
# We look for an existing image that shares the same lock_tag. If such an image exists, we
# can use it as the base image for the build and just copy source files. This makes the build
# much faster.
if runtime_builder.image_exists(lock_image_name):
logger.debug(f'Build [{hash_image_name}] from lock image [{lock_image_name}]')
build_from = BuildFromImageType.LOCK
base_image = lock_image_name
elif runtime_builder.image_exists(versioned_image_name):
logger.info(
f'Build [{hash_image_name}] from versioned image [{versioned_image_name}]'
)
build_from = BuildFromImageType.VERSIONED
base_image = versioned_image_name
else:
logger.debug(f'Build [{hash_image_name}] from scratch')
prep_build_folder(build_folder, base_image, build_from, extra_deps)
if not dry_run:
_build_sandbox_image(
build_folder,
runtime_builder,
runtime_image_repo,
source_tag=source_tag,
lock_tag=lock_tag,
# Only tag the versioned image if we are building from scratch.
# This avoids too much layers when you lay one image on top of another multiple times
versioned_tag=versioned_tag
if build_from == BuildFromImageType.SCRATCH
else None,
platform=platform,
extra_build_args=extra_build_args,
)
return hash_image_name
def prep_build_folder(
build_folder: Path,
base_image: str,
build_from: BuildFromImageType,
extra_deps: str | None,
) -> None:
# Copy the source code to directory. It will end up in build_folder/code
# If package is not found, build from source code
openhands_source_dir = Path(openhands.__file__).parent
project_root = openhands_source_dir.parent
logger.debug(f'Building source distribution using project root: {project_root}')
# Copy the 'openhands' directory (Source code)
shutil.copytree(
openhands_source_dir,
@@ -218,7 +268,6 @@ def build_deps_image_in_folder(
'*.pyc',
'*.md',
),
dirs_exist_ok=True,
)
# Copy pyproject.toml and poetry.lock files
@@ -226,163 +275,13 @@ def build_deps_image_in_folder(
src = Path(openhands_source_dir, file)
if not src.exists():
src = Path(project_root, file)
if src.exists():
shutil.copy2(src, Path(build_folder, 'code', file))
if not dry_run:
# Build the dependencies image
runtime_builder.build_image(
path=str(build_folder),
tag=deps_image_name,
platform=platform,
extra_build_args=extra_build_args,
)
return deps_image_name
def build_runtime_image(
base_image: str,
runtime_builder: RuntimeBuilder,
platform: str | None = None,
extra_deps: str | None = None,
build_folder: str | None = None,
dry_run: bool = False,
force_rebuild: bool = False,
extra_build_args: List[str] | None = None,
deps_image: 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.
Parameters:
- base_image (str): The name of the base Docker image to use
- runtime_builder (RuntimeBuilder): The runtime builder to use
- platform (str): The target platform for the build (e.g. linux/amd64, linux/arm64)
- extra_deps (str): Extra dependencies to install
- 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 force rebuilding even if the image already exists
- extra_build_args (List[str]): Additional build arguments to pass to the builder
- deps_image (str): The dependencies image to use (if None, will use the default)
Returns:
- str: <image_repo>:<MD5 hash>. Where MD5 hash is the hash of the docker build folder
See https://docs.all-hands.dev/modules/usage/architecture/runtime_build for more details.
"""
# If using the dependencies image approach, first ensure the dependencies image exists
if deps_image is None:
deps_image = get_deps_image_name()
# Check if the dependencies image exists
try:
runtime_builder.get_image(deps_image)
logger.info(f'Using existing dependencies image: {deps_image}')
except Exception:
# Dependencies image doesn't exist, build it
logger.info(f'Dependencies image {deps_image} not found. Building it...')
deps_image = build_deps_image(
runtime_builder=runtime_builder,
platform=platform,
extra_deps=extra_deps,
build_folder=build_folder,
dry_run=dry_run,
extra_build_args=extra_build_args,
)
if build_folder is None:
with tempfile.TemporaryDirectory() as temp_dir:
result = build_runtime_image_from_deps(
base_image=base_image,
runtime_builder=runtime_builder,
deps_image=deps_image,
build_folder=Path(temp_dir),
extra_deps=extra_deps,
dry_run=dry_run,
platform=platform,
extra_build_args=extra_build_args,
force_rebuild=force_rebuild,
)
return result
result = build_runtime_image_from_deps(
base_image=base_image,
runtime_builder=runtime_builder,
deps_image=deps_image,
build_folder=Path(build_folder),
extra_deps=extra_deps,
dry_run=dry_run,
platform=platform,
extra_build_args=extra_build_args,
force_rebuild=force_rebuild,
)
return result
def prep_build_folder(
build_folder: Path,
base_image: str,
build_from: BuildFromImageType,
extra_deps: str | None,
deps_image: str | None = None,
) -> None:
"""Prepare the build folder with necessary files.
Parameters:
- build_folder (Path): The directory to use for the build
- base_image (str): The base image to use
- build_from (BuildFromImageType): The build method to use
- extra_deps (str): Extra dependencies to install
- deps_image (str): The dependencies image to use (only for DEPS build method)
"""
# Copy the source code to directory. It will end up in build_folder/code
openhands_source_dir = Path(openhands.__file__).parent
project_root = openhands_source_dir.parent
logger.debug(f'Building source distribution using project root: {project_root}')
# For DEPS build method, we only need to copy the wrapper scripts
if build_from == BuildFromImageType.DEPS:
# Copy the 'openhands' directory (Source code)
os.makedirs(os.path.join(build_folder, 'code', 'openhands'), exist_ok=True)
shutil.copytree(
openhands_source_dir,
Path(build_folder, 'code', 'openhands'),
ignore=shutil.ignore_patterns(
'.*/',
'__pycache__/',
'*.pyc',
'*.md',
),
dirs_exist_ok=True,
)
else:
# Copy the 'openhands' directory (Source code)
shutil.copytree(
openhands_source_dir,
Path(build_folder, 'code', 'openhands'),
ignore=shutil.ignore_patterns(
'.*/',
'__pycache__/',
'*.pyc',
'*.md',
),
)
# Copy pyproject.toml and poetry.lock files
for file in ['pyproject.toml', 'poetry.lock']:
src = Path(openhands_source_dir, file)
if not src.exists():
src = Path(project_root, file)
shutil.copy2(src, Path(build_folder, 'code', file))
shutil.copy2(src, Path(build_folder, 'code', file))
# Create a Dockerfile and write it to build_folder
dockerfile_content = _generate_dockerfile(
base_image,
build_from=build_from,
extra_deps=extra_deps,
deps_image=deps_image,
)
dockerfile_path = Path(build_folder, 'Dockerfile')
with open(str(dockerfile_path), 'w') as f:
@@ -424,83 +323,51 @@ def get_tag_for_versioned_image(base_image: str) -> str:
def get_hash_for_source_files() -> str:
"""Get a hash of the source files.
Returns:
- str: The hash of the source files
"""
openhands_source_dir = Path(openhands.__file__).parent
source_hash = dirhash(
dir_hash = dirhash(
openhands_source_dir,
'md5',
ignore_hidden=True,
excluded_extensions=['.pyc', '.md'],
ignore=[
'.*/', # hidden directories
'__pycache__/',
'*.pyc',
],
)
return source_hash[:8]
# We get away with truncation because we want something that is unique
# rather than something that is cryptographically secure
result = truncate_hash(dir_hash)
return result
def build_runtime_image_from_deps(
base_image: str,
runtime_builder: RuntimeBuilder,
deps_image: str,
def _build_sandbox_image(
build_folder: Path,
extra_deps: str | None = None,
dry_run: bool = False,
runtime_builder: RuntimeBuilder,
runtime_image_repo: str,
source_tag: str,
lock_tag: str,
versioned_tag: str | None,
platform: str | None = None,
extra_build_args: list[str] | None = None,
force_rebuild: bool = False,
) -> str:
"""Build a runtime image using the dependencies image.
"""Build and tag the sandbox image. The image will be tagged with all tags that do not yet exist."""
names = [
f'{runtime_image_repo}:{source_tag}',
f'{runtime_image_repo}:{lock_tag}',
]
if versioned_tag is not None:
names.append(f'{runtime_image_repo}:{versioned_tag}')
names = [name for name in names if not runtime_builder.image_exists(name, False)]
Parameters:
- base_image (str): The base image to use
- runtime_builder (RuntimeBuilder): The runtime builder to use
- deps_image (str): The dependencies image to use
- build_folder (Path): The directory to use for the build
- extra_deps (str): Extra dependencies to install
- dry_run (bool): if True, it will only ready the build folder. It will not actually build the Docker image
- platform (str): The target platform for the build (e.g. linux/amd64, linux/arm64)
- extra_build_args (List[str]): Additional build arguments to pass to the builder
- force_rebuild (bool): if True, it will force rebuilding even if the image already exists
Returns:
- str: The name of the runtime image
"""
runtime_image_repo, runtime_image_tag = get_runtime_image_repo_and_tag(base_image)
source_tag = f'{runtime_image_tag}_{get_hash_for_source_files()}'
runtime_image_name = f'{runtime_image_repo}:{source_tag}'
logger.info(f'Building runtime image: {runtime_image_name}')
# Check if the image already exists
if not force_rebuild:
try:
runtime_builder.get_image(runtime_image_name)
logger.info(f'Runtime image {runtime_image_name} already exists. Reusing it.')
return runtime_image_name
except Exception:
logger.info(f'Runtime image {runtime_image_name} not found. Building it...')
# Create a Dockerfile for the runtime image
dockerfile_content = _generate_dockerfile(
base_image=base_image,
deps_image=deps_image,
extra_deps=extra_deps,
image_name = runtime_builder.build(
path=str(build_folder),
tags=names,
platform=platform,
extra_build_args=extra_build_args,
)
if not image_name:
raise AgentRuntimeBuildError(f'Build failed for image {names}')
with open(Path(build_folder, 'Dockerfile'), 'w') as file:
file.write(dockerfile_content)
if not dry_run:
# Build the runtime image
runtime_builder.build_image(
path=str(build_folder),
tag=runtime_image_name,
platform=platform,
extra_build_args=extra_build_args,
)
return runtime_image_name
return image_name
if __name__ == '__main__':
@@ -511,22 +378,8 @@ if __name__ == '__main__':
parser.add_argument('--build_folder', type=str, default=None)
parser.add_argument('--force_rebuild', action='store_true', default=False)
parser.add_argument('--platform', type=str, default=None)
parser.add_argument('--deps_image', type=str, default=None,
help='The dependencies image to use')
parser.add_argument('--build_deps_only', action='store_true', default=False,
help='Only build the dependencies image')
args = parser.parse_args()
# If only building the dependencies image
if args.build_deps_only:
deps_image = build_deps_image(
runtime_builder=DockerRuntimeBuilder(docker.from_env()),
build_folder=args.build_folder,
platform=args.platform,
)
logger.info(f'Dependencies image built: {deps_image}')
exit(0)
if args.build_folder is not None:
# If a build_folder is provided, we do not actually build the Docker image. We copy the necessary source code
# and create a Dockerfile dynamically and place it in the build_folder only. This allows the Docker image to
@@ -556,7 +409,6 @@ if __name__ == '__main__':
dry_run=True,
force_rebuild=args.force_rebuild,
platform=args.platform,
deps_image=args.deps_image,
)
_runtime_image_repo, runtime_image_source_tag = (
@@ -592,10 +444,6 @@ if __name__ == '__main__':
logger.debug('Building image in a temporary folder')
docker_builder = DockerRuntimeBuilder(docker.from_env())
image_name = build_runtime_image(
args.base_image,
docker_builder,
platform=args.platform,
force_rebuild=args.force_rebuild,
deps_image=args.deps_image,
args.base_image, docker_builder, platform=args.platform
)
logger.debug(f'\nBuilt image: {image_name}\n')
logger.debug(f'\nBuilt image: {image_name}\n')

View File

@@ -0,0 +1,189 @@
FROM {{ base_image }}
# Shared environment variables
ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry \
MAMBA_ROOT_PREFIX=/openhands/micromamba \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
EDITOR=code \
VISUAL=code \
GIT_EDITOR="code --wait" \
OPENVSCODE_SERVER_ROOT=/openhands/.openvscode-server
{% macro setup_base_system() %}
# Install base system dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
wget curl ca-certificates sudo apt-utils git jq tmux build-essential ripgrep \
{%- if 'ubuntu' in base_image and (base_image.endswith(':latest') or base_image.endswith(':24.04')) -%}
libgl1 \
{%- else %}
libgl1-mesa-glx \
{% endif -%}
libasound2-plugins libatomic1 && \
{%- if 'ubuntu' in base_image -%}
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
TZ=Etc/UTC DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends nodejs python3.12 python-is-python3 python3-pip python3.12-venv && \
corepack enable yarn && \
{% endif -%}
apt-get clean && \
rm -rf /var/lib/apt/lists/*
{% if 'ubuntu' in base_image %}
RUN ln -s "$(dirname $(which node))/corepack" /usr/local/bin/corepack && \
npm install -g corepack && corepack enable yarn && \
curl -fsSL --compressed https://install.python-poetry.org | python -
{% endif %}
# Install uv (required by MCP)
RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/openhands/bin" sh
# Add /openhands/bin to PATH
ENV PATH="/openhands/bin:${PATH}"
# Remove UID 1000 named pn or ubuntu, so the 'openhands' user can be created from ubuntu hosts
RUN (if getent passwd 1000 | grep -q pn; then userdel pn; fi) && \
(if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi)
# Create necessary directories
RUN mkdir -p /openhands && \
mkdir -p /openhands/logs && \
mkdir -p /openhands/poetry
{% endmacro %}
{% macro setup_vscode_server() %}
# Reference:
# 1. https://github.com/gitpod-io/openvscode-server
# 2. https://github.com/gitpod-io/openvscode-releases
# Setup VSCode Server
ARG RELEASE_TAG="openvscode-server-v1.98.2"
ARG RELEASE_ORG="gitpod-io"
# ARG USERNAME=openvscode-server
# ARG USER_UID=1000
# ARG USER_GID=1000
RUN if [ -z "${RELEASE_TAG}" ]; then \
echo "The RELEASE_TAG build arg must be set." >&2 && \
exit 1; \
fi && \
arch=$(uname -m) && \
if [ "${arch}" = "x86_64" ]; then \
arch="x64"; \
elif [ "${arch}" = "aarch64" ]; then \
arch="arm64"; \
elif [ "${arch}" = "armv7l" ]; then \
arch="armhf"; \
fi && \
wget https://github.com/${RELEASE_ORG}/openvscode-server/releases/download/${RELEASE_TAG}/${RELEASE_TAG}-linux-${arch}.tar.gz && \
tar -xzf ${RELEASE_TAG}-linux-${arch}.tar.gz && \
if [ -d "${OPENVSCODE_SERVER_ROOT}" ]; then rm -rf "${OPENVSCODE_SERVER_ROOT}"; fi && \
mv ${RELEASE_TAG}-linux-${arch} ${OPENVSCODE_SERVER_ROOT} && \
cp ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/openvscode-server ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/code && \
rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz
{% endmacro %}
{% macro install_vscode_extensions() %}
# Install our custom extension
RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world && \
cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/hello-world/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world/
RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor && \
cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/memory-monitor/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor/
# Some extension dirs are removed because they trigger false positives in vulnerability scans.
RUN rm -rf ${OPENVSCODE_SERVER_ROOT}/extensions/{handlebars,pug,json,diff,grunt,ini,npm}
{% endmacro %}
{% macro install_dependencies() %}
# Install all dependencies
WORKDIR /openhands/code
# Configure micromamba and poetry
RUN /openhands/micromamba/bin/micromamba config set changeps1 False && \
/openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && \
/openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.12
# Install project dependencies in smaller chunks
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry install --only main --no-interaction --no-root
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry install --only runtime --no-interaction --no-root
# Install playwright and its dependencies
RUN apt-get update && \
/openhands/micromamba/bin/micromamba run -n openhands poetry run pip install playwright && \
/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium
# Set environment variables and permissions
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \
chmod -R g+rws /openhands/poetry && \
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace
# Clear caches
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . -n && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
/openhands/micromamba/bin/micromamba clean --all
{% endmacro %}
{% if build_from_scratch %}
# ================================================================
# START: Build Runtime Image from Scratch
# ================================================================
# This is used in cases where the base image is something more generic like nikolaik/python-nodejs
# rather than the current OpenHands release
{{ setup_base_system() }}
# Install micromamba
RUN mkdir -p /openhands/micromamba/bin && \
/bin/bash -c "PREFIX_LOCATION=/openhands/micromamba BIN_FOLDER=/openhands/micromamba/bin INIT_YES=no CONDA_FORGE_YES=yes $(curl -L https://micro.mamba.pm/install.sh)" && \
/openhands/micromamba/bin/micromamba config remove channels defaults && \
/openhands/micromamba/bin/micromamba config list
# Create the openhands virtual environment and install poetry and python
RUN /openhands/micromamba/bin/micromamba create -n openhands -y && \
/openhands/micromamba/bin/micromamba install -n openhands -c conda-forge poetry python=3.12 -y
# Create a clean openhands directory including only the pyproject.toml, poetry.lock and openhands/__init__.py
RUN \
if [ -d /openhands/code ]; then rm -rf /openhands/code; fi && \
mkdir -p /openhands/code/openhands && \
touch /openhands/code/openhands/__init__.py
COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
{{ install_dependencies() }}
# ================================================================
# END: Build Runtime Image from Scratch
# ================================================================
{% endif %}
# ================================================================
# Copy Project source files
# ================================================================
RUN if [ -d /openhands/code/openhands ]; then rm -rf /openhands/code/openhands; fi
COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
COPY ./code/openhands /openhands/code/openhands
RUN chmod a+rwx /openhands/code/openhands/__init__.py
{{ setup_vscode_server() }}
# ================================================================
# END: Build from versioned image
# ================================================================
{% if build_from_versioned %}
{{ install_dependencies() }}
{{ install_vscode_extensions() }}
{% endif %}
# Install extra dependencies if specified
{% if extra_deps %}RUN {{ extra_deps }} {% endif %}

View File

@@ -1,49 +0,0 @@
FROM {{ base_image }}
# Install minimal dependencies required by the base system
RUN if command -v apt-get > /dev/null; then \
apt-get update && apt-get install -y --no-install-recommends ca-certificates bash && \
apt-get clean && rm -rf /var/lib/apt/lists/*; \
elif command -v apk > /dev/null; then \
apk add --no-cache ca-certificates bash gcompat libstdc++; \
elif command -v yum > /dev/null; then \
yum install -y ca-certificates bash; \
yum clean all; \
fi
# Create the openhands user if it doesn't exist
RUN if ! id -u openhands > /dev/null 2>&1; then \
if command -v useradd > /dev/null 2>&1; then \
groupadd -g 1000 openhands 2>/dev/null || true; \
useradd -u 1000 -g 1000 -m -s /bin/bash openhands 2>/dev/null || true; \
elif command -v adduser > /dev/null 2>&1; then \
addgroup -g 1000 openhands 2>/dev/null || true; \
adduser -D -u 1000 -G openhands openhands 2>/dev/null || true; \
fi; \
fi
# Create directories
RUN mkdir -p /openhands/bin /openhands/lib /workspace && \
chown -R openhands:openhands /workspace /openhands 2>/dev/null || true
# Copy the bundled action execution server
COPY ./dist/pyinstaller/action-execution-server /openhands/action-execution-server
# Copy Playwright browser
COPY ./browser /openhands/browser
# Set environment variables
ENV PATH=/openhands/bin:$PATH \
LD_LIBRARY_PATH=/openhands/lib:$LD_LIBRARY_PATH \
PLAYWRIGHT_BROWSERS_PATH=/openhands/browser \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8
# Set the working directory
WORKDIR /workspace
# Switch to the openhands user
USER openhands
# Command to start the action execution server
CMD ["/openhands/action-execution-server/action-execution-server", "8000", "/workspace"]

View File

@@ -1,58 +0,0 @@
ARG DEPS_IMAGE
FROM {{ deps_image }} as deps
FROM {{ base_image }}
# Copy the /openhands folder from the deps image
COPY --from=deps /openhands /openhands
# Set up environment variables
ENV PATH=/openhands/bin:$PATH \
LD_LIBRARY_PATH=/openhands/lib:$LD_LIBRARY_PATH \
POETRY_VIRTUALENVS_PATH=/openhands/poetry \
MAMBA_ROOT_PREFIX=/openhands/micromamba \
PLAYWRIGHT_BROWSERS_PATH=/openhands/browser/ms-playwright \
OPENVSCODE_SERVER_ROOT=/openhands/.openvscode-server \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
EDITOR=code \
VISUAL=code \
GIT_EDITOR="code --wait"
# Install minimal dependencies required by the base system
RUN if command -v apt-get > /dev/null; then \
apt-get update && apt-get install -y --no-install-recommends ca-certificates bash && \
apt-get clean && rm -rf /var/lib/apt/lists/*; \
elif command -v apk > /dev/null; then \
apk add --no-cache ca-certificates bash gcompat libstdc++; \
elif command -v yum > /dev/null; then \
yum install -y ca-certificates bash; \
yum clean all; \
fi
# Create the openhands user if it doesn't exist
RUN if ! id -u openhands > /dev/null 2>&1; then \
if command -v useradd > /dev/null 2>&1; then \
groupadd -g 1000 openhands 2>/dev/null || true; \
useradd -u 1000 -g 1000 -m -s /bin/bash openhands 2>/dev/null || true; \
elif command -v adduser > /dev/null 2>&1; then \
addgroup -g 1000 openhands 2>/dev/null || true; \
adduser -D -u 1000 -G openhands openhands 2>/dev/null || true; \
fi; \
fi
# Create and set permissions for workspace directory
RUN mkdir -p /workspace && \
chown -R openhands:openhands /workspace /openhands 2>/dev/null || true
# Copy OpenHands source code
COPY ./code/openhands /openhands/code/openhands
RUN chmod a+rwx /openhands/code/openhands/__init__.py
# Set the working directory
WORKDIR /workspace
# Switch to the openhands user
USER openhands
# Command to start the action execution server
CMD ["/openhands/bin/oh-action-execution-server", "8000", "/workspace"]

View File

@@ -48,6 +48,34 @@ def create_provider_tokens_object(
return MappingProxyType(provider_information)
async def setup_init_convo_settings(
user_id: str | None, providers_set: list[ProviderType]
) -> ConversationInitData:
settings_store = await SettingsStoreImpl.get_instance(config, user_id)
settings = await settings_store.load()
secrets_store = await SecretsStoreImpl.get_instance(config, user_id)
user_secrets: UserSecrets | None = await secrets_store.load()
if not settings:
raise ConnectionRefusedError(
'Settings not found', {'msg_id': 'CONFIGURATION$SETTINGS_NOT_FOUND'}
)
session_init_args: dict = {}
session_init_args = {**settings.__dict__, **session_init_args}
git_provider_tokens = create_provider_tokens_object(providers_set)
if server_config.app_mode != AppMode.SAAS and user_secrets:
git_provider_tokens = user_secrets.provider_tokens
session_init_args['git_provider_tokens'] = git_provider_tokens
if user_secrets:
session_init_args['custom_secrets'] = user_secrets.custom_secrets
return ConversationInitData(**session_init_args)
@sio.event
async def connect(connection_id: str, environ: dict) -> None:
try:
@@ -85,30 +113,7 @@ async def connect(connection_id: str, environ: dict) -> None:
conversation_id, cookies_str, authorization_header
)
settings_store = await SettingsStoreImpl.get_instance(config, user_id)
settings = await settings_store.load()
secrets_store = await SecretsStoreImpl.get_instance(config, user_id)
user_secrets: UserSecrets | None = await secrets_store.load()
if not settings:
raise ConnectionRefusedError(
'Settings not found', {'msg_id': 'CONFIGURATION$SETTINGS_NOT_FOUND'}
)
session_init_args: dict = {}
if settings:
session_init_args = {**settings.__dict__, **session_init_args}
git_provider_tokens = create_provider_tokens_object(providers_set)
if server_config.app_mode != AppMode.SAAS and user_secrets:
git_provider_tokens = user_secrets.provider_tokens
session_init_args['git_provider_tokens'] = git_provider_tokens
if user_secrets:
session_init_args['custom_secrets'] = user_secrets.custom_secrets
conversation_init_data = ConversationInitData(**session_init_args)
conversation_init_data = await setup_init_convo_settings(user_id, providers_set)
agent_loop_info = await conversation_manager.join_conversation(
conversation_id,
connection_id,

View File

@@ -411,12 +411,9 @@ class AgentSession:
'\n--------------------------------- OpenHands Configuration ---------------------------------\n'
f'LLM: {agent.llm.config.model}\n'
f'Base URL: {agent.llm.config.base_url}\n'
)
msg += (
f'Agent: {agent.name}\n'
f'Runtime: {self.runtime.__class__.__name__}\n'
f'Plugins: {agent.sandbox_plugins}\n'
f'Plugins: {[p.name for p in agent.sandbox_plugins] if agent.sandbox_plugins else "None"}\n'
'-------------------------------------------------------------------------------------------'
)
self.logger.debug(msg)

View File

@@ -154,10 +154,15 @@ class Session:
),
]
)
self.logger.info(f'Enabling default condenser: {default_condenser_config}')
self.logger.info(
f'Enabling pipeline condenser with:'
f' browser_output_masking(attention_window=2), '
f' llm(model="{llm.config.model}", '
f' base_url="{llm.config.base_url}", '
f' keep_first=4, max_size=80)'
)
agent_config.condenser = default_condenser_config
agent = Agent.get_cls(agent_cls)(llm, agent_config)
git_provider_tokens = None

View File

@@ -89,8 +89,6 @@ async def auto_generate_title(
Returns:
A generated title string
"""
logger.info(f'Auto-generating title for conversation {conversation_id}')
try:
# Create an event stream for the conversation
event_stream = EventStream(conversation_id, file_store, user_id)

View File

@@ -1,106 +0,0 @@
#!/usr/bin/env python3
"""
Script to extract and package the Playwright browser for use with the PyInstaller binary.
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
def find_playwright_browser_path():
"""Find the Playwright browser path in the current environment."""
try:
# Try to get the path from the PLAYWRIGHT_BROWSERS_PATH environment variable
if "PLAYWRIGHT_BROWSERS_PATH" in os.environ:
browser_path = Path(os.environ["PLAYWRIGHT_BROWSERS_PATH"])
if browser_path.exists():
return browser_path
# Try to find it in the user's home directory
home_dir = Path.home()
playwright_path = home_dir / ".cache" / "ms-playwright"
if playwright_path.exists():
return playwright_path
# Try to find it in the root user's home directory
root_playwright_path = Path("/root/.cache/ms-playwright")
if root_playwright_path.exists():
return root_playwright_path
# If not found, install Playwright and get the path
print("Playwright browser not found. Installing...")
subprocess.run([sys.executable, "-m", "pip", "install", "playwright"], check=True)
subprocess.run([sys.executable, "-m", "playwright", "install", "chromium"], check=True)
# Try again to find the path
if "PLAYWRIGHT_BROWSERS_PATH" in os.environ:
browser_path = Path(os.environ["PLAYWRIGHT_BROWSERS_PATH"])
if browser_path.exists():
return browser_path
playwright_path = home_dir / ".cache" / "ms-playwright"
if playwright_path.exists():
return playwright_path
root_playwright_path = Path("/root/.cache/ms-playwright")
if root_playwright_path.exists():
return root_playwright_path
raise FileNotFoundError("Could not find Playwright browser path")
except Exception as e:
print(f"Error finding Playwright browser path: {e}")
raise
def package_browser(output_dir):
"""Package the Playwright browser for use with the PyInstaller binary."""
try:
# Find the Playwright browser path
browser_path = find_playwright_browser_path()
print(f"Found Playwright browser at: {browser_path}")
# Create the output directory
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# Copy the browser files
print(f"Copying browser files to {output_path}...")
shutil.copytree(browser_path, output_path / "ms-playwright", dirs_exist_ok=True)
# Create a wrapper script for the browser
wrapper_script = output_path / "chromium-wrapper.sh"
with open(wrapper_script, "w") as f:
f.write("""#!/bin/bash
# Wrapper script for Chromium
# Set up environment
export PLAYWRIGHT_BROWSERS_PATH="$(dirname "$0")/ms-playwright"
# Find the Chromium executable
CHROMIUM_PATH=$(find "$PLAYWRIGHT_BROWSERS_PATH" -name "chrome" -type f | head -n 1)
if [ -z "$CHROMIUM_PATH" ]; then
echo "Error: Chromium executable not found in $PLAYWRIGHT_BROWSERS_PATH"
exit 1
fi
# Execute Chromium with all arguments passed to this script
exec "$CHROMIUM_PATH" "$@"
""")
# Make the wrapper script executable
wrapper_script.chmod(0o755)
print(f"Browser packaged successfully to {output_path}")
return output_path
except Exception as e:
print(f"Error packaging browser: {e}")
raise
if __name__ == "__main__":
output_dir = sys.argv[1] if len(sys.argv) > 1 else "./browser"
package_browser(output_dir)

211
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -200,14 +200,14 @@ files = [
[[package]]
name = "anthropic"
version = "0.51.0"
version = "0.52.0"
description = "The official Python library for the anthropic API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "anthropic-0.51.0-py3-none-any.whl", hash = "sha256:b8b47d482c9aa1f81b923555cebb687c2730309a20d01be554730c8302e0f62a"},
{file = "anthropic-0.51.0.tar.gz", hash = "sha256:6f824451277992af079554430d5b2c8ff5bc059cc2c968cdc3f06824437da201"},
{file = "anthropic-0.52.0-py3-none-any.whl", hash = "sha256:c026daa164f0e3bde36ce9cbdd27f5f1419fff03306be1e138726f42e6a7810f"},
{file = "anthropic-0.52.0.tar.gz", hash = "sha256:f06bc924d7eb85f8a43fe587b875ff58b410d60251b7dc5f1387b322a35bd67b"},
]
[package.dependencies]
@@ -562,34 +562,34 @@ stem = ["PyStemmer"]
[[package]]
name = "boto3"
version = "1.38.20"
version = "1.38.23"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "boto3-1.38.20-py3-none-any.whl", hash = "sha256:0494bafa771561c02ae5926143ce69b6ee4017f11ced22d0293a8372acb7472a"},
{file = "boto3-1.38.20.tar.gz", hash = "sha256:aa1424213678a249fe828fb9345deac5e33f9a2266fd1b23ec72e02857b018a2"},
{file = "boto3-1.38.23-py3-none-any.whl", hash = "sha256:70ab8364f1f6f0a7e0eaf97f62fbdacf9c1e4cc1de330faf1c146ef9ab01e7d0"},
{file = "boto3-1.38.23.tar.gz", hash = "sha256:bcf73aca469add09e165b8793be18e7578db8d2604d82505ab13dc2495bad982"},
]
[package.dependencies]
botocore = ">=1.38.20,<1.39.0"
botocore = ">=1.38.23,<1.39.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.12.0,<0.13.0"
s3transfer = ">=0.13.0,<0.14.0"
[package.extras]
crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "boto3-stubs"
version = "1.38.20"
description = "Type annotations for boto3 1.38.20 generated with mypy-boto3-builder 8.11.0"
version = "1.38.23"
description = "Type annotations for boto3 1.38.23 generated with mypy-boto3-builder 8.11.0"
optional = false
python-versions = ">=3.8"
groups = ["evaluation"]
files = [
{file = "boto3_stubs-1.38.20-py3-none-any.whl", hash = "sha256:5406da868980a3854cc9b57db150c6f2e39a4fe4a58f2872e61ac5a3d46f734e"},
{file = "boto3_stubs-1.38.20.tar.gz", hash = "sha256:7f1d7bfff7355eb4d17e7984fbf27f44709cd8484abb54bd6ba34ec73a552605"},
{file = "boto3_stubs-1.38.23-py3-none-any.whl", hash = "sha256:fb6f97862fa67f8c3052a936ef4e012880a6c0719fce5b94b24e205c300c24dd"},
{file = "boto3_stubs-1.38.23.tar.gz", hash = "sha256:f7632c193f06828b984d7e2bcfbc8c5eca8066ed390a235ad9f35f72307512bc"},
]
[package.dependencies]
@@ -645,7 +645,7 @@ bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (
bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)"]
billing = ["mypy-boto3-billing (>=1.38.0,<1.39.0)"]
billingconductor = ["mypy-boto3-billingconductor (>=1.38.0,<1.39.0)"]
boto3 = ["boto3 (==1.38.20)"]
boto3 = ["boto3 (==1.38.23)"]
braket = ["mypy-boto3-braket (>=1.38.0,<1.39.0)"]
budgets = ["mypy-boto3-budgets (>=1.38.0,<1.39.0)"]
ce = ["mypy-boto3-ce (>=1.38.0,<1.39.0)"]
@@ -1008,14 +1008,14 @@ xray = ["mypy-boto3-xray (>=1.38.0,<1.39.0)"]
[[package]]
name = "botocore"
version = "1.38.20"
version = "1.38.23"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "botocore-1.38.20-py3-none-any.whl", hash = "sha256:70feba9b3f73946a9739d0c16703190d79379f065cf6e29883b5d7f791b247b8"},
{file = "botocore-1.38.20.tar.gz", hash = "sha256:03a5027a207fc66cd0bf8cd1abb98db41fd4d23e6bd5f43f68586af9736240fc"},
{file = "botocore-1.38.23-py3-none-any.whl", hash = "sha256:a7f818672f10d7a080c2c4558428011c3e0abc1039a047d27ac76ec846158457"},
{file = "botocore-1.38.23.tar.gz", hash = "sha256:29685c91050a870c3809238dc5da1ac65a48a3a20b4bca46b6057dcb6b39c72a"},
]
[package.dependencies]
@@ -1898,14 +1898,14 @@ vision = ["Pillow (>=9.4.0)"]
[[package]]
name = "daytona-api-client"
version = "0.18.1"
version = "0.19.1"
description = "Daytona"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "daytona_api_client-0.18.1-py3-none-any.whl", hash = "sha256:6bf44fc38433381266faf66f961ae3773ecc90905075f730400deec5d0f62556"},
{file = "daytona_api_client-0.18.1.tar.gz", hash = "sha256:f95be8642630657468444303022526588014ed82ae3e1759b0fdceb5e6c7565f"},
{file = "daytona_api_client-0.19.1-py3-none-any.whl", hash = "sha256:7c16be29a2bcc21109e14fc314b348063390cec018823c6ff77d5c464b6b3484"},
{file = "daytona_api_client-0.19.1.tar.gz", hash = "sha256:855fb6e569021c5d1d13ea6236837a4c780a1efec9d06dec085520cce790c0a9"},
]
[package.dependencies]
@@ -1916,24 +1916,25 @@ urllib3 = ">=1.25.3,<3.0.0"
[[package]]
name = "daytona-sdk"
version = "0.16.1"
version = "0.18.1"
description = "Python SDK for Daytona"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "daytona_sdk-0.16.1-py3-none-any.whl", hash = "sha256:f3ab5c92688d1b767bb12c3ef38c56bdea9e77b2be7e7dd57df785a14489a425"},
{file = "daytona_sdk-0.16.1.tar.gz", hash = "sha256:97fa381e8363a74ce99bbdd62508709c9083f657d3816e7f24c0349b739784b2"},
{file = "daytona_sdk-0.18.1-py3-none-any.whl", hash = "sha256:75b81afbdf25c3aa24c62af120e22a10e6a2f99cd42c1c5f78a423d9e9e9d2fd"},
{file = "daytona_sdk-0.18.1.tar.gz", hash = "sha256:959d4d07f8342cd27e64eac3550f2a0b2fba5c4850e2b8fe582d00ed150e3a46"},
]
[package.dependencies]
daytona_api_client = ">=0.17.1,<1.0.0"
daytona_api_client = ">=0.19.1,<1.0.0"
Deprecated = ">=1.2.18,<2.0.0"
environs = ">=9.5.0,<10.0.0"
httpx = ">=0.28.0,<0.29.0"
marshmallow = ">=3.19.0,<4.0.0"
pydantic = ">=2.4.2,<3.0.0"
python-dateutil = ">=2.8.2,<3.0.0"
requests_toolbelt = ">=1.0.0,<1.1.0"
urllib3 = ">=2.0.7,<3.0.0"
[package.extras]
@@ -2468,14 +2469,14 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc
[[package]]
name = "fastmcp"
version = "2.3.5"
version = "2.5.1"
description = "The fast, Pythonic way to build MCP servers."
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "fastmcp-2.3.5-py3-none-any.whl", hash = "sha256:193e35a8d35a5c6a4af07e764873d8592aadc2f1e32dd8827b57869a83956088"},
{file = "fastmcp-2.3.5.tar.gz", hash = "sha256:09e11723c6588d8c13562d5eb04d42b13b91eb32f53cef77cc8c0ee121b2f907"},
{file = "fastmcp-2.5.1-py3-none-any.whl", hash = "sha256:a6fe50693954a6aed89fc6e43f227dcd66e112e3d3a1d633ee22b4f435ee8aed"},
{file = "fastmcp-2.5.1.tar.gz", hash = "sha256:0d10ec65a362ae4f78bdf3b639faf35b36cc0a1c8f5461a54fac906fe821b84d"},
]
[package.dependencies]
@@ -3010,7 +3011,7 @@ grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_versi
grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
proto-plus = [
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0"},
{version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""},
]
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
requests = ">=2.18.0,<3.0.0"
@@ -3023,14 +3024,14 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
version = "2.169.0"
version = "2.170.0"
description = "Google API Client Library for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "google_api_python_client-2.169.0-py3-none-any.whl", hash = "sha256:dae3e882dc0e6f28e60cf09c1f13fedfd881db84f824dd418aa9e44def2fe00d"},
{file = "google_api_python_client-2.169.0.tar.gz", hash = "sha256:0585bb97bd5f5bf3ed8d4bf624593e4c5a14d06c811d1952b07a1f94b4d12c51"},
{file = "google_api_python_client-2.170.0-py3-none-any.whl", hash = "sha256:7bf518a0527ad23322f070fa69f4f24053170d5c766821dc970ff0571ec22748"},
{file = "google_api_python_client-2.170.0.tar.gz", hash = "sha256:75f3a1856f11418ea3723214e0abc59d9b217fd7ed43dcf743aab7f06ab9e2b1"},
]
[package.dependencies]
@@ -3105,14 +3106,14 @@ tool = ["click (>=6.0.0)"]
[[package]]
name = "google-cloud-aiplatform"
version = "1.93.1"
version = "1.94.0"
description = "Vertex AI API client library"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "google_cloud_aiplatform-1.93.1-py2.py3-none-any.whl", hash = "sha256:d46004d77c7eb950957ea541620d7191bc7bf3475d6cca228e85eadf239fd26e"},
{file = "google_cloud_aiplatform-1.93.1.tar.gz", hash = "sha256:b15dda9163d4c5d967290b5d23146888552340567cd8057cde11f8508b392f50"},
{file = "google_cloud_aiplatform-1.94.0-py2.py3-none-any.whl", hash = "sha256:cfb198d322133559c076677fbff5b51568ca7759f228c6fc5273e091c2752a41"},
{file = "google_cloud_aiplatform-1.94.0.tar.gz", hash = "sha256:5429dfaa953eef90e48e16fb2a6be82f26c5d4a9a0e53178fcf6f837b528373e"},
]
[package.dependencies]
@@ -3131,7 +3132,7 @@ shapely = "<3.0.0"
typing-extensions = "*"
[package.extras]
adk = ["google-adk (>=0.0.2)"]
adk = ["google-adk (>=0.0.2,<1.0.0)"]
ag2 = ["ag2[gemini]", "openinference-instrumentation-autogen (>=0.1.6,<0.2)"]
ag2-testing = ["absl-py", "ag2[gemini]", "cloudpickle (>=3.0,<4.0)", "google-cloud-trace (<2)", "openinference-instrumentation-autogen (>=0.1.6,<0.2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "pydantic (>=2.11.1,<3)", "pytest-xdist", "typing-extensions"]
agent-engines = ["cloudpickle (>=3.0,<4.0)", "google-cloud-logging (<4)", "google-cloud-trace (<2)", "opentelemetry-exporter-gcp-trace (<2)", "opentelemetry-sdk (<2)", "packaging (>=24.0)", "pydantic (>=2.11.1,<3)", "typing-extensions"]
@@ -3749,6 +3750,22 @@ http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "httpx-aiohttp"
version = "0.1.4"
description = "Aiohttp transport for HTTPX"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "httpx_aiohttp-0.1.4-py3-none-any.whl", hash = "sha256:740a8725af7b7a03d12f21ccd48a83457baa037304646589b87595746c05c87e"},
{file = "httpx_aiohttp-0.1.4.tar.gz", hash = "sha256:61030eed28deeac26286d2e872b7c167f5450b7b0eec5a617ae7d3f7da9c8684"},
]
[package.dependencies]
aiohttp = ">=3,<4"
httpx = ">=0.28.1,<1"
[[package]]
name = "httpx-sse"
version = "0.4.0"
@@ -4261,26 +4278,26 @@ files = [
[[package]]
name = "joblib"
version = "1.5.0"
version = "1.5.1"
description = "Lightweight pipelining with Python functions"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
files = [
{file = "joblib-1.5.0-py3-none-any.whl", hash = "sha256:206144b320246485b712fc8cc51f017de58225fa8b414a1fe1764a7231aca491"},
{file = "joblib-1.5.0.tar.gz", hash = "sha256:d8757f955389a3dd7a23152e43bc297c2e0c2d3060056dad0feefc88a06939b5"},
{file = "joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a"},
{file = "joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444"},
]
[[package]]
name = "json-repair"
version = "0.45.1"
version = "0.46.0"
description = "A package to repair broken json strings"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "json_repair-0.45.1-py3-none-any.whl", hash = "sha256:a5e53cf65e875ddc36c804aa6cf5899a1472319ab832578ebf5efc47f8dbc336"},
{file = "json_repair-0.45.1.tar.gz", hash = "sha256:7ee439d044e4525aebc57f05666ddfbbd7230c68a1fae94ad036f05c0307943f"},
{file = "json_repair-0.46.0-py3-none-any.whl", hash = "sha256:54d6a9889fba0846b80befb2b1aca619103ad3ed74612fb3fedd965a4a3b1653"},
{file = "json_repair-0.46.0.tar.gz", hash = "sha256:abc751162baf8e384685558acba978478e833c1207be31468d9babfaf8029ab6"},
]
[[package]]
@@ -4524,14 +4541,14 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>
[[package]]
name = "jupyterlab"
version = "4.4.2"
version = "4.4.3"
description = "JupyterLab computational environment"
optional = false
python-versions = ">=3.9"
groups = ["runtime"]
files = [
{file = "jupyterlab-4.4.2-py3-none-any.whl", hash = "sha256:857111a50bed68542bf55dca784522fe728f9f88b4fe69e8c585db5c50900419"},
{file = "jupyterlab-4.4.2.tar.gz", hash = "sha256:afa9caf28c0cb966488be18e5e8daba9f018a1c4273a406b7d5006344cbc6d16"},
{file = "jupyterlab-4.4.3-py3-none-any.whl", hash = "sha256:164302f6d4b6c44773dfc38d585665a4db401a16e5296c37df5cba63904fbdea"},
{file = "jupyterlab-4.4.3.tar.gz", hash = "sha256:a94c32fd7f8b93e82a49dc70a6ec45a5c18281ca2a7228d12765e4e210e5bca2"},
]
[package.dependencies]
@@ -4968,24 +4985,25 @@ types-tqdm = "*"
[[package]]
name = "litellm"
version = "1.70.2"
version = "1.71.1"
description = "Library to easily interface with LLM API providers"
optional = false
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
groups = ["main"]
files = [
{file = "litellm-1.70.2-py3-none-any.whl", hash = "sha256:765bb4314e0f764735cb036dcfabfddfec84320831df17275a47d3bb48b577a3"},
{file = "litellm-1.70.2.tar.gz", hash = "sha256:d2e45076f76d668f2b420c98067c9a992dfaa7fea3031a02d0ed89589a2f8841"},
{file = "litellm-1.71.1-py3-none-any.whl", hash = "sha256:9b94e250c58fba3c87c6ebb77e33c1cc8aa9110cee99dfdc37b368a11cec57c7"},
{file = "litellm-1.71.1.tar.gz", hash = "sha256:c20e5917fdbe771ba4b6d1862b3d38d6e89cfba53e85bb337013f848256566eb"},
]
[package.dependencies]
aiohttp = "*"
click = "*"
httpx = ">=0.23.0"
httpx-aiohttp = {version = ">=0.1.4", markers = "python_version >= \"3.9\""}
importlib-metadata = ">=6.8.0"
jinja2 = ">=3.1.2,<4.0.0"
jsonschema = ">=4.22.0,<5.0.0"
openai = ">=1.68.2,<1.76.0"
openai = ">=1.68.2"
pydantic = ">=2.0.0,<3.0.0"
python-dotenv = ">=0.2.0"
tiktoken = ">=0.7.0"
@@ -4993,7 +5011,7 @@ tokenizers = "*"
[package.extras]
extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"]
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.5)", "litellm-proxy-extras (==0.1.21)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"]
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.6)", "litellm-proxy-extras (==0.2.0)", "mcp (==1.5.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"]
utils = ["numpydoc"]
[[package]]
@@ -6285,19 +6303,19 @@ files = [
[[package]]
name = "notebook"
version = "7.4.2"
version = "7.4.3"
description = "Jupyter Notebook - A web-based notebook environment for interactive computing"
optional = false
python-versions = ">=3.8"
groups = ["runtime"]
files = [
{file = "notebook-7.4.2-py3-none-any.whl", hash = "sha256:9ccef602721aaa5530852e3064710b8ae5415c4e2ce26f8896d0433222755259"},
{file = "notebook-7.4.2.tar.gz", hash = "sha256:e739defd28c3f615a6bfb0a2564bd75018a9cc6613aa00bbd9c15e68eed2de1b"},
{file = "notebook-7.4.3-py3-none-any.whl", hash = "sha256:9cdeee954e04101cadb195d90e2ab62b7c9286c1d4f858bf3bb54e40df16c0c3"},
{file = "notebook-7.4.3.tar.gz", hash = "sha256:a1567481cd3853f2610ee0ecf5dfa12bb508e878ee8f92152c134ef7f0568a76"},
]
[package.dependencies]
jupyter-server = ">=2.4.0,<3"
jupyterlab = ">=4.4.0,<4.5"
jupyterlab = ">=4.4.3,<4.5"
jupyterlab-server = ">=2.27.1,<3"
notebook-shim = ">=0.2,<0.3"
tornado = ">=6.2.0"
@@ -6631,14 +6649,14 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
[[package]]
name = "openai"
version = "1.75.0"
version = "1.82.0"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.8"
groups = ["main", "evaluation", "test"]
files = [
{file = "openai-1.75.0-py3-none-any.whl", hash = "sha256:fe6f932d2ded3b429ff67cc9ad118c71327db32eb9d32dd723de3acfca337125"},
{file = "openai-1.75.0.tar.gz", hash = "sha256:fb3ea907efbdb1bcfd0c44507ad9c961afd7dce3147292b54505ecfd17be8fd1"},
{file = "openai-1.82.0-py3-none-any.whl", hash = "sha256:8c40647fea1816516cb3de5189775b30b5f4812777e40b8768f361f232b61b30"},
{file = "openai-1.82.0.tar.gz", hash = "sha256:b0a009b9a58662d598d07e91e4219ab4b1e3d8ba2db3f173896a92b9b874d1a7"},
]
[package.dependencies]
@@ -9289,7 +9307,7 @@ description = "C version of reader, parser and emitter for ruamel.yaml derived f
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "platform_python_implementation == \"CPython\" and python_version == \"3.12\""
markers = "python_version < \"3.13\" and platform_python_implementation == \"CPython\""
files = [
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"},
{file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"},
@@ -9341,42 +9359,42 @@ files = [
[[package]]
name = "ruff"
version = "0.11.10"
version = "0.11.11"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev", "evaluation"]
files = [
{file = "ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58"},
{file = "ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed"},
{file = "ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca"},
{file = "ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2"},
{file = "ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5"},
{file = "ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641"},
{file = "ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947"},
{file = "ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4"},
{file = "ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f"},
{file = "ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b"},
{file = "ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2"},
{file = "ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523"},
{file = "ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125"},
{file = "ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad"},
{file = "ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19"},
{file = "ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224"},
{file = "ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1"},
{file = "ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6"},
{file = "ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092"},
{file = "ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4"},
{file = "ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd"},
{file = "ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6"},
{file = "ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4"},
{file = "ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac"},
{file = "ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709"},
{file = "ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8"},
{file = "ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b"},
{file = "ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875"},
{file = "ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1"},
{file = "ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81"},
{file = "ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639"},
{file = "ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345"},
{file = "ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112"},
{file = "ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f"},
{file = "ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b"},
{file = "ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d"},
]
[[package]]
name = "runloop-api-client"
version = "0.32.0"
version = "0.33.0"
description = "The official Python library for the runloop API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "runloop_api_client-0.32.0-py3-none-any.whl", hash = "sha256:37f156f711b1aa4cef86c0f779cc27afa43ce3d3f6b1976d7f68667466317a6d"},
{file = "runloop_api_client-0.32.0.tar.gz", hash = "sha256:735a967d96b5c3e8a08b89072722adcbe2b10ed904268d3f45785b7cfd5420d1"},
{file = "runloop_api_client-0.33.0-py3-none-any.whl", hash = "sha256:073450f499e01d9b25a2136b217229fbcae45f4d2ba3c9d863b12304e4af04aa"},
{file = "runloop_api_client-0.33.0.tar.gz", hash = "sha256:83f7a18dfe5539d1a005d1aa0988fd79fcc51c150a6aa853fe5b7d979ac3fe3d"},
]
[package.dependencies]
@@ -9389,14 +9407,14 @@ typing-extensions = ">=4.10,<5"
[[package]]
name = "s3transfer"
version = "0.12.0"
version = "0.13.0"
description = "An Amazon S3 Transfer Manager"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "s3transfer-0.12.0-py3-none-any.whl", hash = "sha256:35b314d7d82865756edab59f7baebc6b477189e6ab4c53050e28c1de4d9cce18"},
{file = "s3transfer-0.12.0.tar.gz", hash = "sha256:8ac58bc1989a3fdb7c7f3ee0918a66b160d038a147c7b5db1500930a607e9a1c"},
{file = "s3transfer-0.13.0-py3-none-any.whl", hash = "sha256:0148ef34d6dd964d0d8cf4311b2b21c474693e57c2e069ec708ce043d2b527be"},
{file = "s3transfer-0.13.0.tar.gz", hash = "sha256:f5e6db74eb7776a37208001113ea7aa97695368242b364d73e91c981ac522177"},
]
[package.dependencies]
@@ -9665,6 +9683,7 @@ files = [
{file = "setuptools-80.8.0-py3-none-any.whl", hash = "sha256:95a60484590d24103af13b686121328cc2736bee85de8936383111e421b9edc0"},
{file = "setuptools-80.8.0.tar.gz", hash = "sha256:49f7af965996f26d43c8ae34539c8d99c5042fbff34302ea151eaa9c207cd257"},
]
markers = {evaluation = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
@@ -10462,24 +10481,24 @@ optree = ["optree (>=0.13.0)"]
[[package]]
name = "tornado"
version = "6.5"
version = "6.5.1"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation", "runtime"]
files = [
{file = "tornado-6.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:f81067dad2e4443b015368b24e802d0083fecada4f0a4572fdb72fc06e54a9a6"},
{file = "tornado-6.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9ac1cbe1db860b3cbb251e795c701c41d343f06a96049d6274e7c77559117e41"},
{file = "tornado-6.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c625b9d03f1fb4d64149c47d0135227f0434ebb803e2008040eb92906b0105a"},
{file = "tornado-6.5-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a0d8d2309faf015903080fb5bdd969ecf9aa5ff893290845cf3fd5b2dd101bc"},
{file = "tornado-6.5-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03576ab51e9b1677e4cdaae620d6700d9823568b7939277e4690fe4085886c55"},
{file = "tornado-6.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab75fe43d0e1b3a5e3ceddb2a611cb40090dd116a84fc216a07a298d9e000471"},
{file = "tornado-6.5-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:119c03f440a832128820e87add8a175d211b7f36e7ee161c631780877c28f4fb"},
{file = "tornado-6.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:231f2193bb4c28db2bdee9e57bc6ca0cd491f345cd307c57d79613b058e807e0"},
{file = "tornado-6.5-cp39-abi3-win32.whl", hash = "sha256:fd20c816e31be1bbff1f7681f970bbbd0bb241c364220140228ba24242bcdc59"},
{file = "tornado-6.5-cp39-abi3-win_amd64.whl", hash = "sha256:007f036f7b661e899bd9ef3fa5f87eb2cb4d1b2e7d67368e778e140a2f101a7a"},
{file = "tornado-6.5-cp39-abi3-win_arm64.whl", hash = "sha256:542e380658dcec911215c4820654662810c06ad872eefe10def6a5e9b20e9633"},
{file = "tornado-6.5.tar.gz", hash = "sha256:c70c0a26d5b2d85440e4debd14a8d0b463a0cf35d92d3af05f5f1ffa8675c826"},
{file = "tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7"},
{file = "tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6"},
{file = "tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888"},
{file = "tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331"},
{file = "tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e"},
{file = "tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401"},
{file = "tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692"},
{file = "tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a"},
{file = "tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365"},
{file = "tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b"},
{file = "tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7"},
{file = "tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c"},
]
[[package]]
@@ -12174,4 +12193,4 @@ cffi = ["cffi (>=1.11)"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "992d589aba26102b85b8bd2568bac47617a9409c643e6b77151341fd9c2e5ea3"
content-hash = "509493c2d53d5923054d7521079a212c6e67323389b75886b6e1dccc9869127c"

View File

@@ -1,88 +0,0 @@
# PyInstaller Runtime for OpenHands
This directory contains the implementation of a PyInstaller-based approach for the OpenHands runtime. This approach bundles all required dependencies of the action_execution_server into a binary, which can then be copied into any base image to make it OpenHands compatible.
## Overview
The traditional runtime building procedure involves installing all dependencies (Python, Node.js, Playwright, etc.) in the target image, which can be time-consuming and may lead to compatibility issues. The PyInstaller approach simplifies this by:
1. Building a standalone binary with PyInstaller that includes all Python dependencies
2. Copying only the binary and necessary browser components to the target runtime image
3. Eliminating the need to install Python and other dependencies in the target image
## How It Works
### 1. Building the Binary
We use PyInstaller to bundle the action_execution_server and all its dependencies into a standalone binary. This can be done in two ways:
#### Option A: Using poetry-pyinstaller-plugin
```bash
# Install the plugin
pip install poetry-pyinstaller-plugin
# Add configuration to pyproject.toml
# [tool.poetry-pyinstaller-plugin]
# version = "6.13.0"
#
# [tool.poetry-pyinstaller-plugin.scripts]
# action-execution-server = { source = "openhands/runtime/action_execution_server.py", type = "onedir", bundle = false }
# Build the binary
poetry build --format pyinstaller
```
#### Option B: Direct PyInstaller Usage
```bash
# Install PyInstaller
pip install pyinstaller
# Build the binary
pyinstaller --onedir openhands/runtime/action_execution_server.py
```
### 2. Packaging Browser Components
We extract Playwright's Chromium browser and package it for use with the binary:
```bash
# Package the browser
python package_browser.py browser
```
### 3. Building the Runtime Image
We use a modified version of the runtime_build.py script to build the runtime image:
```bash
# Build the runtime image
python runtime_build_pyinstaller.py --base-image ubuntu:22.04
```
## Files
- `pyinstaller_runtime_plan.md`: The implementation plan for the PyInstaller approach
- `package_browser.py`: Script to extract and package the Playwright browser
- `runtime_build_pyinstaller.py`: Modified runtime_build.py that implements the PyInstaller approach
- `openhands/runtime/utils/runtime_templates/Dockerfile.pyinstaller.j2`: Dockerfile template for the PyInstaller approach
## Advantages
1. **Smaller Image Size**: Only the binary and browser components are needed, not all Python dependencies
2. **Faster Builds**: No need to install Python and dependencies in the target image
3. **Better Compatibility**: The binary should work on any Linux distribution with compatible glibc
4. **Simplified Maintenance**: Easier to update the binary independently of the base image
## Limitations
1. **Binary Compatibility**: The binary may not work on all Linux distributions due to glibc version differences
2. **Browser Integration**: Playwright requires Chromium and its dependencies, which may not be available on all images
3. **Plugin System**: The current plugin system might not work with a bundled binary
## Future Work
1. **Improve Binary Compatibility**: Build the binary in a minimal environment for maximum compatibility
2. **Enhance Browser Integration**: Create a more portable browser package
3. **Modify Plugin System**: Update the plugin system to work with the bundled binary

View File

@@ -1,102 +0,0 @@
# PyInstaller Runtime Implementation Plan
## Overview
This plan outlines how to implement a PyInstaller-based approach for the OpenHands runtime, which will bundle all required dependencies of the action_execution_server into a binary. This approach will simplify the runtime building procedure by:
1. Building a standalone binary with PyInstaller that includes all Python dependencies
2. Copying only the binary and necessary browser components to the target runtime image
3. Eliminating the need to install Python and other dependencies in the target image
## Implementation Steps
### 1. Build the PyInstaller Binary
#### Option A: Using poetry-pyinstaller-plugin
- Add configuration to pyproject.toml:
```toml
[tool.poetry-pyinstaller-plugin]
version = "6.13.0"
[tool.poetry-pyinstaller-plugin.scripts]
action-execution-server = { source = "openhands/runtime/action_execution_server.py", type = "onedir", bundle = false }
```
- Run `poetry build` to create the binary
#### Option B: Direct PyInstaller Usage
- Create a spec file for action_execution_server.py
- Run PyInstaller with the spec file
- Ensure all dependencies are included
### 2. Package Browser Components
- Extract Playwright's Chromium browser from the cache
- Create a portable browser package that can be copied to the target image
- Include necessary wrapper scripts for browser execution
### 3. Update Runtime Builder
- Modify `runtime_build.py` to implement the PyInstaller approach
- Add a new build method that uses the PyInstaller binary
- Create a new Dockerfile template for the PyInstaller approach
- Implement the copying mechanism for the binary and browser components
### 4. Create Wrapper Scripts
- Create wrapper scripts for the action_execution_server binary
- Create wrapper scripts for browser execution
- Ensure proper environment variables are set
### 5. Testing
- Test with various base images to ensure compatibility
- Verify all components work correctly (browser, bash, plugins, etc.)
- Benchmark performance improvements
## Advantages
1. **Smaller Image Size**: Only the binary and browser components are needed, not all Python dependencies
2. **Faster Builds**: No need to install Python and dependencies in the target image
3. **Better Compatibility**: The binary should work on any Linux distribution with compatible glibc
4. **Simplified Maintenance**: Easier to update the binary independently of the base image
## Challenges and Solutions
### 1. Binary Compatibility
**Challenge**: Binaries compiled in one environment might not work in another due to different system libraries.
**Solutions**:
- Build the binary in a minimal environment (e.g., Ubuntu 20.04) for maximum compatibility
- Include all necessary shared libraries in the binary
- Use static linking where possible
### 2. Browser Integration
**Challenge**: Playwright requires Chromium and its dependencies.
**Solutions**:
- Extract Chromium from Playwright's cache
- Create a portable browser package
- Use wrapper scripts to set up the correct environment
### 3. Plugin System
**Challenge**: The current plugin system might not work with a bundled binary.
**Solutions**:
- Modify the plugin system to work with the binary
- Include all plugins in the binary
- Implement a mechanism to load plugins at runtime
## Implementation Timeline
1. **Phase 1**: Create and test the PyInstaller binary (1-2 days)
2. **Phase 2**: Package browser components (1 day)
3. **Phase 3**: Update runtime builder (1-2 days)
4. **Phase 4**: Create wrapper scripts (1 day)
5. **Phase 5**: Testing and optimization (2-3 days)
## Conclusion
The PyInstaller approach offers significant benefits in terms of build speed, image size, and compatibility. While there are challenges related to binary compatibility and browser integration, these can be addressed with careful implementation of wrapper scripts and proper environment setup.

View File

@@ -6,7 +6,7 @@ requires = [
[tool.poetry]
name = "openhands-ai"
version = "0.39.1"
version = "0.39.2"
description = "OpenHands: Code Less, Make More"
authors = [ "OpenHands" ]
license = "MIT"
@@ -64,7 +64,7 @@ protobuf = "^4.21.6,<5.0.0"
opentelemetry-api = "1.25.0"
opentelemetry-exporter-otlp-proto-grpc = "1.25.0"
modal = ">=0.66.26,<0.78.0"
runloop-api-client = "0.32.0"
runloop-api-client = "0.33.0"
libtmux = ">=0.37,<0.40"
pygithub = "^2.5.0"
joblib = "*"
@@ -77,7 +77,7 @@ stripe = ">=11.5,<13.0"
ipywidgets = "^8.1.5"
qtconsole = "^5.6.1"
memory-profiler = "^0.61.0"
daytona-sdk = "0.16.1"
daytona-sdk = "0.18.1"
python-json-logger = "^3.2.1"
prompt-toolkit = "^3.0.50"
poetry = "^2.1.2"
@@ -87,7 +87,7 @@ fastmcp = "^2.3.3"
mcpm = "1.12.0"
[tool.poetry.group.dev.dependencies]
ruff = "0.11.10"
ruff = "0.11.11"
mypy = "1.15.0"
pre-commit = "4.2.0"
build = "*"
@@ -110,12 +110,6 @@ notebook = "*"
jupyter_kernel_gateway = "*"
flake8 = "*"
[tool.poetry-pyinstaller-plugin]
version = "6.13.0"
[tool.poetry-pyinstaller-plugin.scripts]
action-execution-server = { source = "openhands/runtime/action_execution_server.py", type = "onedir", bundle = false }
[tool.poetry.group.evaluation.dependencies]
streamlit = "*"
whatthepatch = "*"
@@ -168,10 +162,3 @@ lint.pydocstyle.convention = "google"
[tool.coverage.run]
concurrency = [ "gevent" ]
[tool.poetry-pyinstaller-plugin]
entry-point = "openhands.runtime.action_execution_server:main"
name = "action-execution-server"
strip = true
onedir = true
console = true

View File

@@ -1,198 +0,0 @@
#!/usr/bin/env python3
"""
Modified runtime_build.py that implements the PyInstaller approach.
"""
import argparse
import hashlib
import os
import shutil
import subprocess
import tempfile
from enum import Enum
from pathlib import Path
import docker
from jinja2 import Environment, FileSystemLoader
import openhands
from openhands import __version__ as oh_version
from openhands.core.exceptions import AgentRuntimeBuildError
from openhands.core.logger import openhands_logger as logger
class BuildMethod(Enum):
PYINSTALLER = 'pyinstaller' # Use PyInstaller to bundle the action_execution_server
def get_runtime_image_repo() -> str:
return os.getenv('OH_RUNTIME_RUNTIME_IMAGE_REPO', 'ghcr.io/all-hands-ai/runtime')
def get_runtime_image_repo_and_tag(base_image: str) -> tuple[str, str]:
"""Retrieves the Docker repo and tag associated with the Docker image.
Parameters:
- base_image (str): The name of the base Docker image
Returns:
- tuple[str, str]: The Docker repo and tag of the Docker image
"""
if get_runtime_image_repo() in base_image:
logger.debug(
f'The provided image [{base_image}] is already a valid runtime image.\n'
f'Will try to reuse it as is.'
)
if ':' not in base_image:
base_image = base_image + ':latest'
repo, tag = base_image.split(':')
return repo, tag
else:
if ':' not in base_image:
base_image = base_image + ':latest'
[repo, tag] = base_image.split(':')
# Hash the repo if it's too long
if len(repo) > 32:
repo_hash = hashlib.md5(repo[:-24].encode()).hexdigest()[:8]
repo = f"{repo[-24:]}_{repo_hash}"
runtime_repo = get_runtime_image_repo()
runtime_tag = f'oh_v{oh_version}_{tag}'
return runtime_repo, runtime_tag
def _generate_dockerfile(
base_image: str,
build_method: BuildMethod = BuildMethod.PYINSTALLER,
) -> str:
"""Generate the Dockerfile content for the runtime image based on the base image.
Parameters:
- base_image (str): The base image provided for the runtime image
- build_method (BuildMethod): The build method for the runtime image.
Returns:
- str: The resulting Dockerfile content
"""
env = Environment(
loader=FileSystemLoader(
searchpath=os.path.join(os.path.dirname(__file__), 'openhands/runtime/utils/runtime_templates')
)
)
template = env.get_template('Dockerfile.pyinstaller.j2')
dockerfile_content = template.render(
base_image=base_image,
)
return dockerfile_content
def build_pyinstaller_binary():
"""Build the PyInstaller binary for the action_execution_server."""
logger.info("Building PyInstaller binary for action_execution_server...")
# Check if poetry-pyinstaller-plugin is installed
try:
subprocess.run(["pip", "show", "poetry-pyinstaller-plugin"], check=True, capture_output=True)
logger.info("Using poetry-pyinstaller-plugin to build the binary")
# Build the binary using poetry
subprocess.run(["poetry", "build", "--format", "pyinstaller"], check=True)
except subprocess.CalledProcessError:
logger.info("poetry-pyinstaller-plugin not found, using direct PyInstaller approach")
# Build the binary using PyInstaller directly
subprocess.run(["pyinstaller", "--onedir", "openhands/runtime/action_execution_server.py"], check=True)
# Move the binary to the expected location
os.makedirs("dist/pyinstaller", exist_ok=True)
shutil.move("dist/action_execution_server", "dist/pyinstaller/action-execution-server")
logger.info("PyInstaller binary built successfully")
def package_browser():
"""Package the Playwright browser for use with the PyInstaller binary."""
logger.info("Packaging Playwright browser...")
# Run the package_browser.py script
subprocess.run(["python", "package_browser.py", "browser"], check=True)
logger.info("Playwright browser packaged successfully")
def build_runtime_image(
base_image: str,
build_method: BuildMethod = BuildMethod.PYINSTALLER,
no_cache: bool = False,
) -> str:
"""Build a runtime image based on the base image.
Parameters:
- base_image (str): The base image provided for the runtime image
- build_method (BuildMethod): The build method for the runtime image.
- no_cache (bool): Whether to use Docker cache when building the image
Returns:
- str: The name of the built runtime image
"""
logger.info(f"Building runtime image with {build_method.value} method...")
# Build the PyInstaller binary
build_pyinstaller_binary()
# Package the browser
package_browser()
# Generate the Dockerfile
dockerfile_content = _generate_dockerfile(base_image, build_method)
# Create a temporary directory for the Docker build context
with tempfile.TemporaryDirectory() as tmpdir:
# Write the Dockerfile to the temporary directory
dockerfile_path = os.path.join(tmpdir, 'Dockerfile')
with open(dockerfile_path, 'w') as f:
f.write(dockerfile_content)
# Copy the PyInstaller binary to the temporary directory
shutil.copytree("dist/pyinstaller/action-execution-server", os.path.join(tmpdir, "dist/pyinstaller/action-execution-server"))
# Copy the browser to the temporary directory
shutil.copytree("browser", os.path.join(tmpdir, "browser"))
# Get the runtime image name
runtime_repo, runtime_tag = get_runtime_image_repo_and_tag(base_image)
runtime_image = f"{runtime_repo}:{runtime_tag}"
# Build the Docker image
logger.info(f"Building Docker image {runtime_image}...")
client = docker.from_env()
try:
client.images.build(
path=tmpdir,
tag=runtime_image,
nocache=no_cache,
)
logger.info(f"Docker image {runtime_image} built successfully")
return runtime_image
except docker.errors.BuildError as e:
logger.error(f"Error building Docker image: {e}")
raise AgentRuntimeBuildError(f"Error building Docker image: {e}")
def main():
"""Main function for the runtime_build_pyinstaller.py script."""
parser = argparse.ArgumentParser(description='Build a runtime image with PyInstaller')
parser.add_argument('--base-image', type=str, default='ubuntu:22.04', help='Base image for the runtime')
parser.add_argument('--no-cache', action='store_true', help='Do not use Docker cache when building the image')
args = parser.parse_args()
runtime_image = build_runtime_image(args.base_image, BuildMethod.PYINSTALLER, args.no_cache)
print(f"Runtime image built: {runtime_image}")
if __name__ == '__main__':
main()

View File

@@ -683,6 +683,29 @@ def test_agent_config_condenser_with_no_enabled():
assert isinstance(agent_config.condenser, NoOpCondenserConfig)
def test_sandbox_volumes_toml(default_config, temp_toml_file):
"""Test that volumes configuration under [sandbox] works correctly."""
with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:
toml_file.write("""
[sandbox]
volumes = "/home/user/mydir:/workspace:rw,/data:/data:ro"
timeout = 1
""")
load_from_toml(default_config, temp_toml_file)
finalize_config(default_config)
# Check that sandbox.volumes is set correctly
assert (
default_config.sandbox.volumes
== '/home/user/mydir:/workspace:rw,/data:/data:ro'
)
assert default_config.workspace_mount_path == '/home/user/mydir'
assert default_config.workspace_mount_path_in_sandbox == '/workspace'
assert default_config.workspace_base == '/home/user/mydir'
assert default_config.sandbox.timeout == 1
def test_condenser_config_from_toml_basic(default_config, temp_toml_file):
"""Test loading basic condenser configuration from TOML."""
with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:

View File

@@ -652,6 +652,34 @@ NON_FNCALL_RESPONSE_MESSAGE = {
<parameter=command>view</parameter>
<parameter=path>/test/file.py</parameter>
<parameter=view_range>[1, 10]</parameter>
</function>""",
),
# Test case with indented code block to verify indentation is preserved
(
[
{
'index': 1,
'function': {
'arguments': '{"command": "str_replace", "path": "/test/file.py", "old_str": "def example():\\n pass", "new_str": "def example():\\n # This is indented\\n print(\\"hello\\")\\n return True"}',
'name': 'str_replace_editor',
},
'id': 'test_id',
'type': 'function',
}
],
"""<function=str_replace_editor>
<parameter=command>str_replace</parameter>
<parameter=path>/test/file.py</parameter>
<parameter=old_str>
def example():
pass
</parameter>
<parameter=new_str>
def example():
# This is indented
print("hello")
return True
</parameter>
</function>""",
),
],
@@ -944,3 +972,81 @@ def test_convert_from_multiple_tool_calls_no_tool_calls():
input_messages
)
assert result == input_messages
def test_convert_fncall_messages_with_cache_control():
"""Test that cache_control is properly handled in tool messages."""
# Prepare test data
messages = [
{
'role': 'tool',
'name': 'test_tool',
'content': [{'type': 'text', 'text': 'test content'}],
'cache_control': {'type': 'ephemeral'},
}
]
# Call the function
result = convert_fncall_messages_to_non_fncall_messages(messages, [])
# Verify the result
assert len(result) == 1
assert result[0]['role'] == 'user'
assert 'cache_control' in result[0]['content'][-1]
assert result[0]['content'][-1]['cache_control'] == {'type': 'ephemeral'}
assert (
result[0]['content'][0]['text']
== 'EXECUTION RESULT of [test_tool]:\ntest content'
)
def test_convert_fncall_messages_without_cache_control():
"""Test that tool messages without cache_control work as expected."""
# Prepare test data
messages = [
{
'role': 'tool',
'name': 'test_tool',
'content': [{'type': 'text', 'text': 'test content'}],
}
]
# Call the function
result = convert_fncall_messages_to_non_fncall_messages(messages, [])
# Verify the result
assert len(result) == 1
assert result[0]['role'] == 'user'
assert 'cache_control' not in result[0]['content'][-1]
assert (
result[0]['content'][0]['text']
== 'EXECUTION RESULT of [test_tool]:\ntest content'
)
def test_convert_fncall_messages_with_image_url():
"""Test that convert_fncall_messages_to_non_fncall_messages handles image URLs correctly."""
messages = [
{
'role': 'tool',
'name': 'browser',
'content': [
{
'type': 'text',
'text': 'some browser tool results',
},
{
'type': 'image_url',
'image_url': {'url': 'data:image/gif;base64,R0lGODlhAQABAAAAACw='},
},
],
}
]
converted_messages = convert_fncall_messages_to_non_fncall_messages(messages, [])
assert len(converted_messages) == 1
assert converted_messages[0]['role'] == 'user'
assert len(converted_messages[0]['content']) == len(messages[0]['content'])
assert (
next(c for c in converted_messages[0]['content'] if c['type'] == 'text')['text']
== f'EXECUTION RESULT of [{messages[0]["name"]}]:\n{messages[0]["content"][0]["text"]}'
)

View File

@@ -1,98 +0,0 @@
import os
import tempfile
from pathlib import Path
from unittest import mock
import pytest
from openhands.runtime.utils.runtime_build import (
BuildFromImageType,
_generate_dockerfile,
build_deps_image,
build_runtime_image,
build_runtime_image_from_deps,
get_deps_image_name,
)
class TestRuntimeRefactored:
def test_get_deps_image_name(self):
"""Test that get_deps_image_name returns the expected name."""
deps_image = get_deps_image_name()
assert "oh_deps_v" in deps_image
assert deps_image.startswith("ghcr.io/all-hands-ai/runtime:")
def test_generate_dockerfile_deps(self):
"""Test that _generate_dockerfile generates the expected Dockerfile for DEPS build method."""
dockerfile = _generate_dockerfile(
base_image="ubuntu:22.04",
build_from=BuildFromImageType.DEPS,
deps_image="ghcr.io/all-hands-ai/runtime:oh_deps_v0.1.0",
)
assert "FROM ghcr.io/all-hands-ai/runtime:oh_deps_v0.1.0 as deps" in dockerfile
assert "FROM ubuntu:22.04" in dockerfile
assert "COPY --from=deps /openhands /openhands" in dockerfile
@mock.patch("openhands.runtime.utils.runtime_build.RuntimeBuilder")
def test_build_deps_image(self, mock_runtime_builder):
"""Test that build_deps_image calls the right methods."""
mock_runtime_builder.build_image.return_value = "test_image"
mock_runtime_builder.get_image.return_value = None
with tempfile.TemporaryDirectory() as temp_dir:
# Mock the build process
with mock.patch(
"openhands.runtime.utils.runtime_build.build_deps_image_in_folder",
return_value="test_deps_image",
):
result = build_deps_image(
runtime_builder=mock_runtime_builder,
build_folder=temp_dir,
dry_run=True,
)
assert result == "test_deps_image"
@mock.patch("openhands.runtime.utils.runtime_build.RuntimeBuilder")
def test_build_runtime_image_from_deps(self, mock_runtime_builder):
"""Test that build_runtime_image_from_deps calls the right methods."""
mock_runtime_builder.image_exists.return_value = False
mock_runtime_builder.build_image.return_value = "test_image"
with tempfile.TemporaryDirectory() as temp_dir:
# Create necessary directories
os.makedirs(os.path.join(temp_dir, "code", "openhands"), exist_ok=True)
# Mock the build process
with mock.patch(
"openhands.runtime.utils.runtime_build._build_sandbox_image",
return_value="test_runtime_image",
):
result = build_runtime_image_from_deps(
base_image="ubuntu:22.04",
runtime_builder=mock_runtime_builder,
deps_image="test_deps_image",
build_folder=Path(temp_dir),
dry_run=True,
)
assert "oh_deps_" in result
@mock.patch("openhands.runtime.utils.runtime_build.RuntimeBuilder")
def test_build_runtime_image_with_deps(self, mock_runtime_builder):
"""Test that build_runtime_image with use_deps_image=True calls the right methods."""
mock_runtime_builder.get_image.return_value = "test_deps_image"
with tempfile.TemporaryDirectory() as temp_dir:
# Mock the build process
with mock.patch(
"openhands.runtime.utils.runtime_build.build_runtime_image_from_deps",
return_value="test_runtime_image",
):
result = build_runtime_image(
base_image="ubuntu:22.04",
runtime_builder=mock_runtime_builder,
build_folder=temp_dir,
dry_run=True,
use_deps_image=True,
deps_image="test_deps_image",
)
assert result == "test_runtime_image"