mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
21 Commits
openhands-
...
paralleliz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1968734c4d | ||
|
|
35f7efb9d7 | ||
|
|
14498c5e25 | ||
|
|
cdb9aeb9ba | ||
|
|
318883e5e0 | ||
|
|
767b6ce600 | ||
|
|
3ccc96d794 | ||
|
|
6f1effba5b | ||
|
|
bc223885a0 | ||
|
|
0dcd5e9d30 | ||
|
|
8ee85a45a2 | ||
|
|
342563d113 | ||
|
|
af037b3a8a | ||
|
|
33b714e0a0 | ||
|
|
35d2281717 | ||
|
|
83bfbc7045 | ||
|
|
11e6d40c7a | ||
|
|
41d84ee8cd | ||
|
|
0c2924453f | ||
|
|
77cd05c33b | ||
|
|
ff22712686 |
15
.devcontainer/devcontainer.json
Normal file
15
.devcontainer/devcontainer.json
Normal 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
7
.devcontainer/setup.sh
Normal 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
5
.editorconfig
Normal 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
6
.gitattributes
vendored
@@ -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
9
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
6
.vscode/settings.json
vendored
Normal 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
146
README_CN.md
Normal 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)
|
||||
> 加入我们的设计合作伙伴计划,您将获得商业功能的早期访问权限,并有机会对我们的产品路线图提供意见。
|
||||
|
||||

|
||||
|
||||
## ☁️ 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},
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
##############################################################################
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.**
|
||||
|
||||
92
evaluation/benchmarks/swe_bench/SWE-Interact.md
Normal file
92
evaluation/benchmarks/swe_bench/SWE-Interact.md
Normal 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
|
||||
411
evaluation/benchmarks/swe_bench/run_infer_interact.py
Executable file
411
evaluation/benchmarks/swe_bench/run_infer_interact.py
Executable 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}'
|
||||
)
|
||||
131
evaluation/benchmarks/swe_bench/scripts/run_infer_interact.sh
Normal file
131
evaluation/benchmarks/swe_bench/scripts/run_infer_interact.sh
Normal 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
|
||||
676
frontend/package-lock.json
generated
676
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -16,5 +16,7 @@ export const useSubmitFeedback = () => {
|
||||
onError: (error) => {
|
||||
displayErrorToast(error.message);
|
||||
},
|
||||
retry: 2,
|
||||
retryDelay: 500,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
19
frontend/src/hooks/use-runtime-is-ready.ts
Normal file
19
frontend/src/hooks/use-runtime-is-ready.ts
Normal 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)
|
||||
);
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Відправляємо відгук, будь ласка, почекайте..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
189
openhands/runtime/utils/runtime_templates/Dockerfile.j2
Normal file
189
openhands/runtime/utils/runtime_templates/Dockerfile.j2
Normal 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 %}
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
211
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]}'
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user