Compare commits

..

35 Commits

Author SHA1 Message Date
amanape da500222d5 Change how changes tab loads items 2025-05-27 20:05:11 +04:00
Yunfeng Yu 6f1effba5b Fix Feedback Submission Retry (#8693)
Co-authored-by: Test User <test@example.com>
Co-authored-by: Auroral <1596588744@163.com>
2025-05-27 14:48:30 +00:00
dependabot[bot] bc223885a0 chore(deps): bump the version-all group across 1 directory with 9 updates (#8709)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-05-27 14:33:27 +00:00
sp.wack 0dcd5e9d30 hotfix(frontend): Clear error message on WS connect (#8725) 2025-05-27 10:12:56 -04:00
Engel Nyst 8ee85a45a2 Reduce more logs (#8712) 2025-05-27 16:05:04 +02:00
Marco Dalalba 342563d113 fix: url repo encode (#8713) 2025-05-27 17:39:13 +04:00
KianoshArian af037b3a8a fix(frontend): fix mobile view of SettingsModal (#8711) 2025-05-27 17:38:31 +04:00
sp.wack 33b714e0a0 fix(frontend): Consider agent state errors (#8672) 2025-05-27 09:30:53 -04:00
Kent Johnson 35d2281717 feat: Add dev container (#8589) 2025-05-26 21:35:27 -04:00
dependabot[bot] 83bfbc7045 chore(deps): bump the version-all group across 1 directory with 16 updates (#8710)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 23:35:15 +02:00
CoreJa 11e6d40c7a bug: fix fn_call error during API response (#8695)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-05-26 14:10:10 +02:00
mamoodi 41d84ee8cd Update the versions to align with already released 0.39.2 (#8673) 2025-05-25 12:09:19 -04:00
CoreJa 0c2924453f bug: fix cache_control missing during convertion (#8692)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-05-25 15:35:34 +02:00
Graham Neubig 77cd05c33b Fix setup.sh error logging (#8678) 2025-05-24 17:23:26 -04:00
sp.wack ff22712686 fix(frontend): Use uppercase event type for generic events without translation keys (#8671)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-24 18:20:08 +00:00
sp.wack edc43ca60d fix(frontend): Fix actionHasObservationPair function to correctly identify pairs (#8661)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-05-24 09:39:37 +00:00
sp.wack b838ea0427 hotfix(frontend): Prevent too many re-renders (#8670) 2025-05-24 09:38:51 +00:00
tofarr 9a5e5956fc Added ability to read specify permitted origins in env (#8675)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-23 19:18:20 -06:00
Robert Brennan 35426a04d8 fix warning for workspace_base (#8667) 2025-05-23 15:11:53 -04:00
Rohit Malhotra 598d19cd5d [Feat]: slack conversation instruction template (#8648) 2025-05-23 14:39:53 -04:00
Rohit Malhotra dd3b3d1253 [Fix]: conversation ID attachment in create_new_conversation (#8669) 2025-05-23 14:37:10 -04:00
tofarr 693d912361 Fix conversation initial state (#8647) 2025-05-23 17:36:58 +00:00
Xingyao Wang 31ad7fc175 chore: add claude 4 to verified mode & global replace 3.7 to claude 4 (#8665)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-23 17:35:30 +00:00
Xingyao Wang 5e43dbadcb feat(frontend): Display MCP tool name and arguments as JSON in MCPObservation when visualized in frontend (#8644)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-23 17:21:22 +00:00
Ryan H. Tran b0b5a6c2a1 Expose partial MCP server addition failure logs (#8655) 2025-05-24 00:17:31 +07:00
chuckbutkus 92f8061558 Fix so redirect URL is in OAuth state (#8653) 2025-05-23 17:09:47 +00:00
mamoodi 2bb1b7b7aa Fix tip link (#8666) 2025-05-24 00:56:35 +08:00
Howie Zhou ca9fe7c5c8 docs: fix debug command (#8654) 2025-05-23 12:43:27 -04:00
Xingyao Wang a40443f5f4 feat(agent): First-class Search API support via MCP (#8638)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-24 00:07:48 +08:00
sp.wack 20983a2128 refactor(frontend): Simplify useConversation hook by removing context dependency (#8659)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-23 19:56:00 +04:00
Zhaoling Chen efe287ce34 integrate LocAgent into OpenHands (#7371)
Co-authored-by: czlll <gangda@huaihe.usc.edu>
Co-authored-by: Hoang Tran <descience.thh10@gmail.com>
2025-05-23 22:42:58 +07:00
sp.wack fa5b52298e fix(frontend): Fix action buttons display bugs in chat interface (#8660)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-23 15:35:41 +00:00
dependabot[bot] 57bfef3735 chore(deps): bump the version-all group in /frontend with 2 updates (#8635)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-05-23 13:55:42 +00:00
Graham Neubig f3a127a17f Fix unlocalized elements in frontend (#8650)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-05-23 08:40:10 -04:00
Robert Brennan 8cc039997f Downgrade info to debug log (#8643) 2025-05-23 13:43:40 +02:00
135 changed files with 4002 additions and 1010 deletions
+15
View File
@@ -0,0 +1,15 @@
// For format details, see: https://aka.ms/devcontainer.json
{
"name": "Python 3",
// Documentation for this image:
// - https://github.com/devcontainers/templates/tree/main/src/python
// - https://github.com/microsoft/vscode-remote-try-python
// - https://hub.docker.com/r/microsoft/devcontainers-python
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
"ghcr.io/devcontainers-extra/features/poetry:2": {},
"ghcr.io/devcontainers/features/node:1": {},
},
"postCreateCommand": ".devcontainer/setup.sh",
}
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# Install `nc`
sudo apt update && sudo apt install netcat -y
# Do common setup tasks
source .openhands/setup.sh
+5
View File
@@ -0,0 +1,5 @@
[*]
# force *nix line endings so files don't look modified in container run from Windows clone
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
+6
View File
@@ -1 +1,7 @@
*.ipynb linguist-vendored
# force *nix line endings so files don't look modified in container run from Windows clone
* text eol=lf
# Git incorrectly thinks some media is text
*.png -text
*.mp4 -text
+1 -1
View File
@@ -24,7 +24,7 @@ on:
LLM_MODEL:
required: false
type: string
default: "anthropic/claude-3-7-sonnet-20250219"
default: "anthropic/claude-sonnet-4-20250514"
LLM_API_VERSION:
required: false
type: string
+8 -1
View File
@@ -161,9 +161,16 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.vscode/
.cursorignore
# VS Code: Ignore all but certain files that specify repo-specific settings.
# https://stackoverflow.com/questions/32964920/should-i-commit-the-vscode-folder-to-source-control
.vscode/**/*
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/settings.json
!.vscode/tasks.json
# evaluation
evaluation/evaluation_outputs
evaluation/outputs
+1
View File
@@ -9,4 +9,5 @@ python -m pip install pre-commit
if [ -d ".git" ]; then
echo "Installing pre-commit hooks..."
pre-commit install
make install-pre-commit-hooks
fi
+6
View File
@@ -0,0 +1,6 @@
{
// force *nix line endings so files don't look modified in container run from Windows clone
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
}
+1 -1
View File
@@ -67,7 +67,7 @@ docker run -it --rm --pull=always \
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
When you open the application, you'll be asked to choose an LLM provider and add an API key.
[Anthropic's Claude 3.7 Sonnet](https://www.anthropic.com/api) (`anthropic/claude-3-7-sonnet-20250219`)
[Anthropic's Claude Sonnet 4](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-20250514`)
works best, but you have [many options](https://docs.all-hands.dev/modules/usage/llms).
## 💡 Other ways to run OpenHands
+146
View File
@@ -0,0 +1,146 @@
<a name="readme-top"></a>
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: 少写代码,多做事</h1>
</div>
<div align="center">
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="加入我们的Discord社区"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="致谢"></a>
<br/>
<a href="https://docs.all-hands.dev/modules/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="查看文档"></a>
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv论文"></a>
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="评估基准分数"></a>
<hr>
</div>
欢迎使用OpenHands(前身为OpenDevin),这是一个由AI驱动的软件开发代理平台。
OpenHands代理可以完成人类开发者能做的任何事情:修改代码、运行命令、浏览网页、调用API,甚至从StackOverflow复制代码片段。
在[docs.all-hands.dev](https://docs.all-hands.dev)了解更多信息,或[注册OpenHands Cloud](https://app.all-hands.dev)开始使用。
> [!IMPORTANT]
> 在工作中使用OpenHands?我们很想与您交流!填写
> [这份简短表格](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> 加入我们的设计合作伙伴计划,您将获得商业功能的早期访问权限,并有机会对我们的产品路线图提供意见。
![应用截图](./docs/static/img/screenshot.png)
## ☁️ OpenHands Cloud
开始使用OpenHands的最简单方式是在[OpenHands Cloud](https://app.all-hands.dev)上,
新用户可获得$50的免费额度。
## 💻 在本地运行OpenHands
OpenHands也可以使用Docker在本地系统上运行。
查看[运行OpenHands](https://docs.all-hands.dev/modules/usage/installation)指南了解
系统要求和更多信息。
> [!WARNING]
> 在公共网络上?请参阅我们的[强化Docker安装指南](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation)
> 通过限制网络绑定和实施其他安全措施来保护您的部署。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.39-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.39-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.39
```
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands
打开应用程序时,您将被要求选择一个LLM提供商并添加API密钥。
[Anthropic的Claude Sonnet 4](https://www.anthropic.com/api)`anthropic/claude-sonnet-4-20250514`
效果最佳,但您还有[许多选择](https://docs.all-hands.dev/modules/usage/llms)。
## 💡 运行OpenHands的其他方式
> [!CAUTION]
> OpenHands旨在由单个用户在其本地工作站上运行。
> 它不适合多租户部署,即多个用户共享同一实例。没有内置的身份验证、隔离或可扩展性。
>
> 如果您有兴趣在多租户环境中运行OpenHands,请
> [与我们联系](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> 了解高级部署选项。
您还可以[将OpenHands连接到本地文件系统](https://docs.all-hands.dev/modules/usage/runtimes/docker#connecting-to-your-filesystem)
以可编程的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)运行OpenHands
通过[友好的CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode)与其交互,
或使用[GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)在标记的问题上运行它。
访问[运行OpenHands](https://docs.all-hands.dev/modules/usage/installation)获取更多信息和设置说明。
如果您想修改OpenHands源代码,请查看[Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)。
遇到问题?[故障排除指南](https://docs.all-hands.dev/modules/usage/troubleshooting)可以提供帮助。
## 📖 文档
<a href="https://deepwiki.com/All-Hands-AI/OpenHands"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" title="DeepWiki自动生成文档"></a>
要了解有关项目的更多信息,以及使用OpenHands的技巧,
请查看我们的[文档](https://docs.all-hands.dev/modules/usage/getting-started)。
在那里,您将找到有关如何使用不同LLM提供商、
故障排除资源和高级配置选项的资源。
## 🤝 如何加入社区
OpenHands是一个社区驱动的项目,我们欢迎每个人的贡献。我们大部分沟通
通过Slack进行,因此这是开始的最佳场所,但我们也很乐意您通过Discord或Github与我们联系:
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - 这里我们讨论研究、架构和未来发展。
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。
在[COMMUNITY.md](./COMMUNITY.md)中了解更多关于社区的信息,或在[CONTRIBUTING.md](./CONTRIBUTING.md)中找到有关贡献的详细信息。
## 📈 进展
在[这里](https://github.com/orgs/All-Hands-AI/projects/1)查看OpenHands月度路线图(每月月底在维护者会议上更新)。
<p align="center">
<a href="https://star-history.com/#All-Hands-AI/OpenHands&Date">
<img src="https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date" width="500" alt="Star History Chart">
</a>
</p>
## 📜 许可证
根据MIT许可证分发。有关更多信息,请参阅[`LICENSE`](./LICENSE)。
## 🙏 致谢
OpenHands由大量贡献者构建,每一份贡献都备受感谢!我们还借鉴了其他开源项目,对他们的工作深表感谢。
有关OpenHands中使用的开源项目和许可证列表,请参阅我们的[CREDITS.md](./CREDITS.md)文件。
## 📚 引用
```
@misc{openhands,
title={{OpenHands: An Open Platform for AI Software Developers as Generalist Agents}},
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
year={2024},
eprint={2407.16741},
archivePrefix={arXiv},
primaryClass={cs.SE},
url={https://arxiv.org/abs/2407.16741},
}
```
+1 -1
View File
@@ -52,4 +52,4 @@ $ poetry run python docs/translation_updater.py
# ...
```
This process uses `claude-3-7-sonnet-20250219` as base model and each language consumes at least ~30k input tokens and ~35k output tokens.
This process uses `claude-sonnet-4-20250514` as base model and each language consumes at least ~30k input tokens and ~35k output tokens.
@@ -13,7 +13,7 @@ recommandations pour la sélection de modèles. Nos derniers résultats d'évalu
Sur la base de ces résultats et des retours de la communauté, les modèles suivants ont été vérifiés comme fonctionnant raisonnablement bien avec OpenHands :
- [anthropic/claude-3-7-sonnet-20250219](https://www.anthropic.com/api) (recommandé)
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommandé)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [openai/o3-mini](https://openai.com/index/openai-o3-mini/)
@@ -13,7 +13,7 @@ OpenHandsはLiteLLMでサポートされているあらゆるLLMに接続でき
これらの調査結果とコミュニティからのフィードバックに基づき、以下のモデルはOpenHandsでうまく動作することが確認されています:
- [anthropic/claude-3-7-sonnet-20250219](https://www.anthropic.com/api) (推奨)
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (推奨)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [openai/o3-mini](https://openai.com/index/openai-o3-mini/)
@@ -13,7 +13,7 @@ recomendações para seleção de modelos. Nossos resultados de benchmarking mai
Com base nessas descobertas e feedback da comunidade, os seguintes modelos foram verificados e funcionam razoavelmente bem com o OpenHands:
- [anthropic/claude-3-7-sonnet-20250219](https://www.anthropic.com/api) (recomendado)
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recomendado)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [openai/o3-mini](https://openai.com/index/openai-o3-mini/)
@@ -12,7 +12,7 @@ OpenHands 可以连接到任何 LiteLLM 支持的 LLM。但是,它需要一个
基于这些发现和社区反馈,以下模型已被验证可以与 OpenHands 合理地配合使用:
- [anthropic/claude-3-7-sonnet-20250219](https://www.anthropic.com/api)(推荐)
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api)(推荐)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [openai/o3-mini](https://openai.com/index/openai-o3-mini/)
+1 -1
View File
@@ -23,7 +23,7 @@ This command opens an interactive prompt where you can type tasks or commands an
1. Set the following environment variables in your terminal:
- `SANDBOX_VOLUMES` to specify the directory you want OpenHands to access ([See using SANDBOX_VOLUMES for more info](../runtimes/docker#using-sandbox_volumes))
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-3-7-sonnet-20250219"`)
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-sonnet-4-20250514"`)
- `LLM_API_KEY` - your API key (e.g. `export LLM_API_KEY="sk_test_12345"`)
2. Run the following command:
+1 -1
View File
@@ -14,7 +14,7 @@ The following `launch.json` will allow debugging the agent, controller and serve
"name": "OpenHands CLI",
"type": "debugpy",
"request": "launch",
"module": "openhands.core.cli",
"module": "openhands.cli.main",
"justMyCode": false
},
{
+1 -1
View File
@@ -23,7 +23,7 @@ To run OpenHands in Headless mode with Docker:
1. Set the following environment variables in your terminal:
- `SANDBOX_VOLUMES` to specify the directory you want OpenHands to access ([See using SANDBOX_VOLUMES for more info](../runtimes/docker#using-sandbox_volumes))
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-3-7-sonnet-20250219"`)
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-sonnet-4-20250514"`)
- `LLM_API_KEY` - your API key (e.g. `export LLM_API_KEY="sk_test_12345"`)
2. Run the following Docker command:
+1 -1
View File
@@ -13,7 +13,7 @@ recommendations for model selection. Our latest benchmarking results can be foun
Based on these findings and community feedback, these are the latest models that have been verified to work reasonably well with OpenHands:
- [anthropic/claude-3-7-sonnet-20250219](https://www.anthropic.com/api) (recommended)
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
- [openai/o4-mini](https://openai.com/index/introducing-o3-and-o4-mini/)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
+1 -1
View File
@@ -57,7 +57,7 @@ def translate_content(content, target_lang):
system_prompt = f'You are a professional translator. Translate the following content into {target_lang}. Preserve all Markdown formatting, code blocks, and front matter. Keep any {{% jsx %}} tags and similar intact. Do not translate code examples, URLs, or technical terms.'
message = client.messages.create(
model='claude-3-7-sonnet-20250219',
model='claude-sonnet-4-20250514',
max_tokens=4096,
temperature=0,
system=system_prompt,
@@ -0,0 +1,69 @@
TASK_INSTRUECTION="""
Given the following GitHub problem description, your objective is to localize the specific files, classes or functions, and lines of code that need modification or contain key information to resolve the issue.
Follow these steps to localize the issue:
## Step 1: Categorize and Extract Key Problem Information
- Classify the problem statement into the following categories:
Problem description, error trace, code to reproduce the bug, and additional context.
- Identify modules in the '{package_name}' package mentioned in each category.
- Use extracted keywords and line numbers to search for relevant code references for additional context.
## Step 2: Locate Referenced Modules
- Accurately determine specific modules
- Explore the repo to familiarize yourself with its structure.
- Analyze the described execution flow to identify specific modules or components being referenced.
- Pay special attention to distinguishing between modules with similar names using context and described execution flow.
- Output Format for collected relevant modules:
- Use the format: 'file_path:QualifiedName'
- E.g., for a function `calculate_sum` in the `MathUtils` class located in `src/helpers/math_helpers.py`, represent it as: 'src/helpers/math_helpers.py:MathUtils.calculate_sum'.
## Step 3: Analyze and Reproducing the Problem
- Clarify the Purpose of the Issue
- If expanding capabilities: Identify where and how to incorporate new behavior, fields, or modules.
- If addressing unexpected behavior: Focus on localizing modules containing potential bugs.
- Reconstruct the execution flow
- Identify main entry points triggering the issue.
- Trace function calls, class interactions, and sequences of events.
- Identify potential breakpoints causing the issue.
Important: Keep the reconstructed flow focused on the problem, avoiding irrelevant details.
## Step 4: Locate Areas for Modification
- Locate specific files, functions, or lines of code requiring changes or containing critical information for resolving the issue.
- Consider upstream and downstream dependencies that may affect or be affected by the issue.
- If applicable, identify where to introduce new fields, functions, or variables.
- Think Thoroughly: List multiple potential solutions and consider edge cases that could impact the resolution.
## Output Format for Final Results:
Your final output should list the locations requiring modification, wrapped with triple backticks ```
Each location should include the file path, class name (if applicable), function name, or line numbers, ordered by importance.
Your answer would better include about 5 files.
### Examples:
```
full_path1/file1.py
line: 10
class: MyClass1
function: my_function1
full_path2/file2.py
line: 76
function: MyClass2.my_function2
full_path3/file3.py
line: 24
line: 156
function: my_function3
```
Return just the location(s)
Note: Your thinking should be thorough and so it's fine if it's very long.
"""
FAKE_USER_MSG_FOR_LOC = (
'Verify if the found locations contain all the necessary information to address the issue, and check for any relevant references in other parts of the codebase that may not have appeared in the search results. '
'If not, continue searching for additional locations related to the issue.\n'
'Verify that you have carefully analyzed the impact of the found locations on the repository, especially their dependencies. '
'If you think you have solved the task, please send your final answer (including the former answer and reranking) to user through message and then call `finish` to finish.\n'
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP.\n'
)
@@ -0,0 +1,713 @@
import asyncio
import json
import os
import tempfile
from typing import Any
import pandas as pd
import toml
from datasets import load_dataset
import openhands.agenthub
from evaluation.benchmarks.swe_bench.resource.mapping import (
get_instance_resource_factor,
)
from evaluation.utils.shared import (
EvalException,
EvalMetadata,
EvalOutput,
assert_and_raise,
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
AppConfig,
get_llm_config_arg,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation, ErrorObservation
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
INDEX_BASE_DIR = os.environ.get('INDEX_BASE_DIR', '')
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
'LocAgent': codeact_user_response,
}
def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
return f'{instance.repo}__{instance.version}'.replace('/', '__')
def get_instruction(instance: pd.Series, metadata: EvalMetadata):
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
instruction = f"""
Consider the following issue description:
<issue_description>
{instance.problem_statement}
</issue_description>
Your objective is to localize the specific files, classes or functions, and lines of code that need modification or contain key information to resolve the issue.
Follow these steps to localize the issue:
## Step 1: Categorize and Extract Key Problem Information
- Classify the problem statement into the following categories:
Problem description, error trace, code to reproduce the bug, and additional context.
- Identify modules in the "{instance.instance_id.split('_')[0]}" package mentioned in each category.
- Use extracted keywords and line numbers to search for relevant code references for additional context.
## Step 2: Locate Referenced Modules
- Accurately determine specific modules
- Explore the repo to familiarize yourself with its structure.
- Analyze the described execution flow to identify specific modules or components being referenced.
- Pay special attention to distinguishing between modules with similar names using context and described execution flow.
- Output Format for collected relevant modules:
- Use the format: 'file_path:QualifiedName'
- E.g., for a function `calculate_sum` in the `MathUtils` class located in `src/helpers/math_helpers.py`, represent it as: 'src/helpers/math_helpers.py:MathUtils.calculate_sum'.
## Step 3: Analyze and Reproducing the Problem
- Clarify the Purpose of the Issue
- If expanding capabilities: Identify where and how to incorporate new behavior, fields, or modules.
- If addressing unexpected behavior: Focus on localizing modules containing potential bugs.
- Reconstruct the execution flow
- Identify main entry points triggering the issue.
- Trace function calls, class interactions, and sequences of events.
- Identify potential breakpoints causing the issue.
Important: Keep the reconstructed flow focused on the problem, avoiding irrelevant details.
## Step 4: Locate Areas for Modification
- Locate specific files, functions, or lines of code requiring changes or containing critical information for resolving the issue.
- Consider upstream and downstream dependencies that may affect or be affected by the issue.
- If applicable, identify where to introduce new fields, functions, or variables.
- Think Thoroughly: List multiple potential solutions and consider edge cases that could impact the resolution.
## Output Format for Final Results:
Your final output should list the locations requiring modification, wrapped with triple backticks ```
Each location should include the file path, class name (if applicable), function name, or line numbers, ordered by importance.
Your answer would better include about 5 files.
### Examples:
```
full_path1/file1.py
line: 10
class: MyClass1
function: my_function1
full_path2/file2.py
line: 76
function: MyClass2.my_function2
full_path3/file3.py
line: 24
line: 156
function: my_function3
```
Return just the location(s)
Note: Your thinking should be thorough and so it's fine if it's very long.
"""
instruction += (
'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
"Don't include any lambda functions!\n"
'You should NOT modify any files!\n'
)
if RUN_WITH_BROWSING:
instruction += """
<IMPORTANT!>
You SHOULD NEVER attempt to browse the web.
</IMPORTANT!>
"""
return instruction
# TODO: migrate all swe-bench docker to ghcr.io/openhands
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get(
'EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/'
)
logger.info(f'Default docker image prefix: {DEFAULT_DOCKER_IMAGE_PREFIX}')
def get_instance_docker_image(instance_id: str, official_image: bool = False) -> str:
if official_image:
# Official SWE-Bench image
# swebench/sweb.eval.x86_64.django_1776_django-11333:v1
docker_image_prefix = 'docker.io/swebench/'
repo, name = instance_id.split('__')
image_name = f'sweb.eval.x86_64.{repo}_1776_{name}:latest'
logger.warning(f'Using official SWE-Bench image: {image_name}')
else:
# OpenHands version of the image
docker_image_prefix = DEFAULT_DOCKER_IMAGE_PREFIX
image_name = 'sweb.eval.x86_64.' + instance_id
image_name = image_name.replace(
'__', '_s_'
) # to comply with docker image naming convention
return (docker_image_prefix.rstrip('/') + '/' + image_name).lower()
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> AppConfig:
# We use a different instance image for the each instance of swe-bench eval
use_official_image = bool(
'verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower()
)
base_container_image = get_instance_docker_image(
instance['instance_id'], use_official_image
)
logger.info(
f'Using instance container image: {base_container_image}. '
f'Please make sure this image exists. '
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
sandbox_config.enable_auto_lint = True
sandbox_config.use_host_network = False
# Add platform to the sandbox config to solve issue 4401
sandbox_config.platform = 'linux/amd64'
sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor(
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
sandbox_config.runtime_startup_env_vars = {
'REPO_PATH': f'/workspace/{workspace_dir_name}/',
}
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
)
)
agent_config = AgentConfig(
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
config.set_agent_config(agent_config)
return config
def initialize_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required
):
"""Initialize the runtime for the agent.
This function is called before the runtime is used to run the agent.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Initialization Fn')
logger.info('-' * 30)
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
obs: CmdOutputObservation
# Set instance id
action = CmdRunAction(
command=f"""echo 'export SWE_INSTANCE_ID={instance['instance_id']}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo "alias git='git --no-pager'" >> ~/.bashrc"""
)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0, f'Failed to export SWE_INSTANCE_ID: {str(obs)}'
)
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to export USER: {str(obs)}')
# inject the init script
script_dir = os.path.dirname(__file__)
# inject the instance info
action = CmdRunAction(command='mkdir -p /swe_util/eval_data/instances')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to create /swe_util/eval_data/instances: {str(obs)}',
)
swe_instance_json_name = 'swe-bench-instance.json'
with tempfile.TemporaryDirectory() as temp_dir:
# Construct the full path for the desired file name within the temporary directory
temp_file_path = os.path.join(temp_dir, swe_instance_json_name)
# Write to the file with the desired name within the temporary directory
with open(temp_file_path, 'w') as f:
if not isinstance(instance, dict):
json.dump([instance.to_dict()], f)
else:
json.dump([instance], f)
# Copy the file to the desired location
runtime.copy_to(temp_file_path, '/swe_util/eval_data/instances/')
# inject the instance swe entry
runtime.copy_to(
str(os.path.join(script_dir, 'scripts/setup/instance_swe_entry.sh')),
'/swe_util/',
)
action = CmdRunAction(command='cat ~/.bashrc')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to cat ~/.bashrc: {str(obs)}')
action = CmdRunAction(command='source ~/.bashrc')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if isinstance(obs, ErrorObservation):
logger.error(f'Failed to source ~/.bashrc: {str(obs)}')
assert_and_raise(obs.exit_code == 0, f'Failed to source ~/.bashrc: {str(obs)}')
action = CmdRunAction(command='source /swe_util/instance_swe_entry.sh')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to source /swe_util/instance_swe_entry.sh: {str(obs)}',
)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0,
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
)
action = CmdRunAction(command='git reset --hard')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to git reset --hard: {str(obs)}')
action = CmdRunAction(
command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
# Copy the processed indexes if available
action = CmdRunAction(command='mkdir _index_data/graph_index_v2.3')
obs = runtime.run_action(action)
# Check if an existing graph index file is available
graph_index_file_path = os.path.join(
INDEX_BASE_DIR, 'graph_index_v2.3', f"{instance['instance_id']}.pkl"
)
if INDEX_BASE_DIR and os.path.exists(graph_index_file_path):
logger.info(
f"Copying graph index from {graph_index_file_path} to /workspace/{workspace_dir_name}/_index_data/graph_index_v2.3"
)
runtime.copy_to(
graph_index_file_path,
f'/workspace/{workspace_dir_name}/_index_data/graph_index_v2.3',
)
action = CmdRunAction(
command=f'mv _index_data/graph_index_v2.3/{instance["instance_id"]}.pkl _index_data/graph_index_v2.3/code_graph.pkl'
)
obs = runtime.run_action(action)
bm25_index_dir = os.path.join(INDEX_BASE_DIR, 'BM25_index', instance['instance_id'])
runtime.copy_to(
bm25_index_dir, f'/workspace/{workspace_dir_name}/_index_data', recursive=True
)
action = CmdRunAction(
command=f'mv _index_data/{instance["instance_id"]} _index_data/bm25_index'
)
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to mv file: {str(obs)}')
action = CmdRunAction(command='which python')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
obs.exit_code == 0 and 'testbed' in obs.content,
f'Expected to find python interpreter from testbed, but got: {str(obs)}',
)
logger.info('-' * 30)
logger.info('END Runtime Initialization Fn')
logger.info('-' * 30)
def complete_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
) -> dict[str, Any]:
"""Complete the runtime for the agent.
This function is called before the runtime is used to run the agent.
If you need to do something in the sandbox to get the correctness metric after
the agent has run, modify this function.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Completion Fn')
logger.info('-' * 30)
obs: CmdOutputObservation
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if obs.exit_code == -1:
# The previous command is still running
# We need to kill previous command
logger.info('The previous command is still running, trying to kill it...')
action = CmdRunAction(command='C-c')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
# Then run the command again
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
)
action = CmdRunAction(command='git config --global core.pager ""')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to git config --global core.pager "": {str(obs)}',
)
# First check for any git repositories in subdirectories
action = CmdRunAction(command='find . -type d -name .git -not -path "./.git"')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to find git repositories: {str(obs)}',
)
git_dirs = [p for p in obs.content.strip().split('\n') if p]
if git_dirs:
# Remove all .git directories in subdirectories
for git_dir in git_dirs:
action = CmdRunAction(command=f'rm -rf "{git_dir}"')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to remove git directory {git_dir}: {str(obs)}',
)
# add all files
action = CmdRunAction(command='git add -A')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to git add -A: {str(obs)}',
)
n_retries = 0
git_patch = None
while n_retries < 5:
action = CmdRunAction(
command=f'git diff --no-color --cached {instance["base_commit"]}'
)
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
n_retries += 1
if isinstance(obs, CmdOutputObservation):
if obs.exit_code == 0:
git_patch = obs.content.strip()
break
else:
logger.info('Failed to get git diff, retrying...')
sleep_if_should_continue(10)
elif isinstance(obs, ErrorObservation):
logger.error(f'Error occurred: {obs.content}. Retrying...')
sleep_if_should_continue(10)
else:
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
assert_and_raise(git_patch is not None, 'Failed to get git diff (None)')
logger.info('-' * 30)
logger.info('END Runtime Completion Fn')
logger.info('-' * 30)
return {'git_patch': git_patch}
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
runtime_failure_count: int = 0,
) -> EvalOutput:
config = get_config(instance, metadata)
# 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}.')
# Increase resource_factor with increasing attempt_id
if runtime_failure_count > 0:
config.sandbox.remote_runtime_resource_factor = min(
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
8,
)
logger.warning(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance)
instruction = 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=MessageAction(content=instruction),
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 is_fatal_evaluation_error(state.last_error):
raise EvalException('Fatal error detected: ' + state.last_error)
# ======= THIS IS SWE-Bench specific =======
# 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()
# ==========================================
# ======= Attempt to evaluate the agent's edits =======
# we use eval_infer.sh to evaluate the agent's edits, not here
# because the agent may alter the environment / testcases
test_result = {
'git_patch': git_patch,
}
# If you are working on some simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
if state is None:
raise ValueError('State should not be None.')
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
histories = [event_to_dict(event) for event in state.history]
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
instance_id=instance.instance_id,
instruction=instruction,
instance=instance.to_dict(), # SWE Bench specific
test_result=test_result,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
)
return output
def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.toml')
if os.path.exists(file_path):
with open(file_path, 'r') as file:
data = toml.load(file)
if 'selected_ids' in data:
selected_ids = data['selected_ids']
logger.info(
f'Filtering {len(selected_ids)} tasks from "selected_ids"...'
)
subset = dataset[dataset[filter_column].isin(selected_ids)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
if len(skip_ids) > 0:
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')
return dataset[~dataset[filter_column].isin(skip_ids)]
return dataset
# A list of instances that are known to be tricky to infer
# (will cause runtime failure even with resource factor = 8)
SWEGYM_EXCLUDE_IDS = [
'dask__dask-10422',
'pandas-dev__pandas-50548',
'pandas-dev__pandas-53672',
'pandas-dev__pandas-54174',
'pandas-dev__pandas-55518',
'pandas-dev__pandas-58383',
'pydata__xarray-6721',
'pytest-dev__pytest-10081',
'pytest-dev__pytest-7236',
]
if __name__ == '__main__':
parser = get_parser()
parser.add_argument(
'--dataset',
type=str,
default='princeton-nlp/SWE-bench',
help='data set to evaluate on, either full-test or lite-test',
)
parser.add_argument(
'--split',
type=str,
default='test',
help='split to evaluate on',
)
args, _ = parser.parse_known_args()
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
# so we don't need to manage file uploading to OpenHands's repo
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'
)
if 'SWE-Gym' in args.dataset:
swe_bench_tests = swe_bench_tests[
~swe_bench_tests['instance_id'].isin(SWEGYM_EXCLUDE_IDS)
]
logger.info(
f'{len(swe_bench_tests)} tasks left after excluding SWE-Gym excluded 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}')
details = {}
_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,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
print(f'### OUTPUT FILE: {output_file} ###')
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,
)
+117
View File
@@ -0,0 +1,117 @@
#!/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
DATASET=$7
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 princeton-nlp/SWE-bench_Lite"
DATASET="princeton-nlp/SWE-bench_Lite"
fi
if [ -z "$SPLIT" ]; then
echo "SPLIT not specified, use default test"
SPLIT="test"
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"
# 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
function run_eval() {
local eval_note=$1
COMMAND="poetry run python evaluation/benchmarks/swe_bench/run_localize.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
@@ -4,7 +4,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ActionSuggestions } from "#/components/features/chat/action-suggestions";
import OpenHands from "#/api/open-hands";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
import { ConversationProvider } from "#/context/conversation-context";
// Mock dependencies
vi.mock("posthog-js", () => ({
@@ -48,11 +47,9 @@ vi.mock("react-router", () => ({
const renderActionSuggestions = () =>
render(<ActionSuggestions onSuggestionsClick={() => {}} />, {
wrapper: ({ children }) => (
<ConversationProvider>
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
</ConversationProvider>
<QueryClientProvider client={new QueryClient()}>
{children}
</QueryClientProvider>
),
});
@@ -56,15 +56,15 @@ function TestComponent() {
describe("WsClientProvider", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mock("#/hooks/query/use-user-conversation", () => ({
useUserConversation: () => {
vi.mock("#/hooks/query/use-active-conversation", () => ({
useActiveConversation: () => {
return { data: {
conversation_id: "1",
title: "Conversation 1",
selected_repository: null,
last_updated_at: "2021-10-01T12:00:00Z",
created_at: "2021-10-01T12:00:00Z",
status: "STOPPED" as const,
status: "RUNNING" as const,
url: null,
session_api_key: null,
}}},
@@ -48,7 +48,7 @@ describe("Content", () => {
await waitFor(() => {
expect(provider).toHaveValue("Anthropic");
expect(model).toHaveValue("claude-3-7-sonnet-20250219");
expect(model).toHaveValue("claude-sonnet-4-20250514");
expect(apiKey).toHaveValue("");
expect(apiKey).toHaveProperty("placeholder", "");
@@ -105,7 +105,7 @@ describe("Content", () => {
within(advancedForm).getByTestId("llm-custom-model-input");
within(advancedForm).getByTestId("base-url-input");
within(advancedForm).getByTestId("llm-api-key-input");
within(advancedForm).getByTestId("llm-api-key-help-anchor");
within(advancedForm).getByTestId("llm-api-key-help-anchor-advanced");
within(advancedForm).getByTestId("agent-input");
within(advancedForm).getByTestId("enable-confirmation-mode-switch");
within(advancedForm).getByTestId("enable-memory-condenser-switch");
@@ -135,7 +135,7 @@ describe("Content", () => {
);
const condensor = screen.getByTestId("enable-memory-condenser-switch");
expect(model).toHaveValue("anthropic/claude-3-7-sonnet-20250219");
expect(model).toHaveValue("anthropic/claude-sonnet-4-20250514");
expect(baseUrl).toHaveValue("");
expect(apiKey).toHaveValue("");
expect(apiKey).toHaveProperty("placeholder", "");
@@ -542,7 +542,7 @@ describe("Form submission", () => {
// select model
await userEvent.click(model);
const modelOption = screen.getByText("claude-3-7-sonnet-20250219");
const modelOption = screen.getByText("claude-sonnet-4-20250514");
await userEvent.click(modelOption);
const submitButton = screen.getByTestId("submit-button");
@@ -550,7 +550,7 @@ describe("Form submission", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_model: "anthropic/claude-3-7-sonnet-20250219",
llm_model: "anthropic/claude-sonnet-4-20250514",
llm_base_url: "",
confirmation_mode: false,
}),
@@ -71,6 +71,18 @@ describe("extractModelAndProvider", () => {
separator: "/",
});
expect(extractModelAndProvider("claude-sonnet-4-20250514")).toEqual({
provider: "anthropic",
model: "claude-sonnet-4-20250514",
separator: "/",
});
expect(extractModelAndProvider("claude-opus-4-20250514")).toEqual({
provider: "anthropic",
model: "claude-opus-4-20250514",
separator: "/",
});
expect(extractModelAndProvider("claude-3-haiku-20240307")).toEqual({
provider: "anthropic",
model: "claude-3-haiku-20240307",
+363 -356
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.39.1",
"version": "0.39.2",
"private": true,
"type": "module",
"engines": {
@@ -10,28 +10,28 @@
"@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",
"jose": "^6.0.11",
"lucide-react": "^0.511.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.245.1",
"posthog-js": "^1.245.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-highlight": "^0.15.0",
@@ -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",
+1 -1
View File
@@ -8,7 +8,7 @@
* - Please do NOT serve this file on production.
*/
const PACKAGE_VERSION = '2.7.6'
const PACKAGE_VERSION = '2.8.4'
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
@@ -113,6 +113,7 @@ const EXCLUDED_TECHNICAL_STRINGS = [
"GitHub API", // Git provider specific terminology
"add-secret-form", // Test ID for secret form
"edit-secret-form", // Test ID for secret form
"search-api-key-input", // Input name for search API key
];
function isExcludedTechnicalString(str) {
@@ -3,7 +3,7 @@ import { useSelector, useDispatch } from "react-redux";
import { RootState } from "#/store";
import { BrowserSnapshot } from "./browser-snapshot";
import { EmptyBrowserMessage } from "./empty-browser-message";
import { useConversation } from "#/context/conversation-context";
import { useConversationId } from "#/hooks/use-conversation-id";
import {
initialState as browserInitialState,
setUrl,
@@ -14,7 +14,7 @@ export function BrowserPanel() {
const { url, screenshotSrc } = useSelector(
(state: RootState) => state.browser,
);
const { conversationId } = useConversation();
const { conversationId } = useConversationId();
const dispatch = useDispatch();
useEffect(() => {
@@ -4,8 +4,7 @@ import { useTranslation } from "react-i18next";
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
import { I18nKey } from "#/i18n/declaration";
import { useUserProviders } from "#/hooks/use-user-providers";
import { useConversation } from "#/context/conversation-context";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
interface ActionSuggestionsProps {
onSuggestionsClick: (value: string) => void;
@@ -16,9 +15,7 @@ export function ActionSuggestions({
}: ActionSuggestionsProps) {
const { t } = useTranslation();
const { providers } = useUserProviders();
const { conversationId } = useConversation();
const { data: conversation } = useUserConversation(conversationId);
const { data: conversation } = useActiveConversation();
const [hasPullRequest, setHasPullRequest] = React.useState(false);
const providersAreSet = providers.length > 0;
@@ -153,11 +153,13 @@ export function ChatInterface() {
/>
)}
{isWaitingForUserInput && (
<ActionSuggestions
onSuggestionsClick={(value) => handleSendMessage(value, [])}
/>
)}
{isWaitingForUserInput &&
events.length > 0 &&
!optimisticUserMessage && (
<ActionSuggestions
onSuggestionsClick={(value) => handleSendMessage(value, [])}
/>
)}
</div>
<div className="flex flex-col gap-[6px] px-4 pb-4">
@@ -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);
}
@@ -46,14 +46,6 @@ const getBrowseObservationContent = (event: BrowseObservation) => {
return contentDetails;
};
const getMcpObservationContent = (event: OpenHandsObservation): string => {
let { content } = event;
if (content.length > MAX_CONTENT_LENGTH) {
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
}
return `**Output:**\n\`\`\`\n${content.trim() || i18n.t("OBSERVATION$MCP_NO_OUTPUT")}\n\`\`\``;
};
const getRecallObservationContent = (event: RecallObservation): string => {
let content = "";
@@ -124,8 +116,6 @@ export const getObservationContent = (event: OpenHandsObservation): string => {
return getCommandObservationContent(event);
case "browse":
return getBrowseObservationContent(event);
case "mcp":
return getMcpObservationContent(event);
case "recall":
return getRecallObservationContent(event);
default:
@@ -8,11 +8,13 @@ import {
isOpenHandsObservation,
isFinishAction,
isRejectObservation,
isMcpObservation,
} from "#/types/core/guards";
import { OpenHandsObservation } from "#/types/core/observations";
import { ImageCarousel } from "../images/image-carousel";
import { ChatMessage } from "./chat-message";
import { ErrorMessage } from "./error-message";
import { MCPObservationContent } from "./mcp-observation-content";
import { getObservationResult } from "./event-content-helpers/get-observation-result";
import { getEventContent } from "./event-content-helpers/get-event-content";
import { GenericEventMessage } from "./generic-event-message";
@@ -46,12 +48,11 @@ export function EventMessage({
);
}
if (
hasObservationPair &&
isOpenHandsAction(event) &&
hasThoughtProperty(event.args)
) {
return <ChatMessage type="agent" message={event.args.thought} />;
if (hasObservationPair && isOpenHandsAction(event)) {
if (hasThoughtProperty(event.args)) {
return <ChatMessage type="agent" message={event.args.thought} />;
}
return null;
}
if (isFinishAction(event)) {
@@ -78,6 +79,19 @@ export function EventMessage({
return <ChatMessage type="agent" message={event.content} />;
}
if (isMcpObservation(event)) {
return (
<div>
<GenericEventMessage
title={getEventContent(event).title}
details={<MCPObservationContent event={event} />}
success={getObservationResult(event)}
/>
{shouldShowConfirmationButtons && <ConfirmationButtons />}
</div>
);
}
return (
<div>
{isOpenHandsAction(event) && hasThoughtProperty(event.args) && (
@@ -10,7 +10,7 @@ import { ObservationResultStatus } from "./event-content-helpers/get-observation
interface GenericEventMessageProps {
title: React.ReactNode;
details: string;
details: string | React.ReactNode;
success?: ObservationResultStatus;
}
@@ -44,18 +44,21 @@ export function GenericEventMessage({
{success && <SuccessIndicator status={success} />}
</div>
{showDetails && (
<Markdown
components={{
code,
ul,
ol,
}}
remarkPlugins={[remarkGfm]}
>
{details}
</Markdown>
)}
{showDetails &&
(typeof details === "string" ? (
<Markdown
components={{
code,
ul,
ol,
}}
remarkPlugins={[remarkGfm]}
>
{details}
</Markdown>
) : (
details
))}
</div>
);
}
@@ -0,0 +1,73 @@
import React from "react";
import ReactJsonView from "@microlink/react-json-view";
import { useTranslation } from "react-i18next";
import { MCPObservation } from "#/types/core/observations";
import { JSON_VIEW_THEME } from "#/utils/constants";
interface MCPObservationContentProps {
event: MCPObservation;
}
export function MCPObservationContent({ event }: MCPObservationContentProps) {
const { t } = useTranslation();
// Parse the content as JSON if possible
let outputData: unknown;
try {
outputData = JSON.parse(event.content);
} catch (e) {
// If parsing fails, use the raw content
outputData = event.content;
}
const hasArguments =
event.extras.arguments && Object.keys(event.extras.arguments).length > 0;
return (
<div className="flex flex-col gap-4">
{/* Arguments section */}
{hasArguments && (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-300">
{t("MCP_OBSERVATION$ARGUMENTS")}
</h3>
</div>
<div className="p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[200px] shadow-inner">
<ReactJsonView
name={false}
src={event.extras.arguments}
theme={JSON_VIEW_THEME}
collapsed={1}
displayDataTypes={false}
/>
</div>
</div>
)}
{/* Output section */}
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-gray-300">
{t("MCP_OBSERVATION$OUTPUT")}
</h3>
</div>
<div className="p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[300px] shadow-inner">
{typeof outputData === "object" && outputData !== null ? (
<ReactJsonView
name={false}
src={outputData}
theme={JSON_VIEW_THEME}
collapsed={1}
displayDataTypes={false}
/>
) : (
<pre className="whitespace-pre-wrap">
{event.content.trim() || t("OBSERVATION$MCP_NO_OUTPUT")}
</pre>
)}
</div>
</div>
</div>
);
}
@@ -49,7 +49,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
return false;
},
[messages],
[],
);
return (
@@ -70,6 +70,14 @@ export const Messages: React.FC<MessagesProps> = React.memo(
</>
);
},
(prevProps, nextProps) => {
// Prevent re-renders if messages are the same length
if (prevProps.messages.length !== nextProps.messages.length) {
return false;
}
return true;
},
);
Messages.displayName = "Messages";
@@ -15,6 +15,7 @@ import {
} from "#/context/ws-client-provider";
import { useNotification } from "#/hooks/useNotification";
import { browserTab } from "#/utils/browser-tab";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
const notificationStates = [
AgentState.AWAITING_USER_INPUT,
@@ -28,6 +29,7 @@ export function AgentStatusBar() {
const { curStatusMessage } = useSelector((state: RootState) => state.status);
const { status } = useWsClient();
const { notify } = useNotification();
const { data: conversation } = useActiveConversation();
const [statusMessage, setStatusMessage] = React.useState<string>("");
@@ -78,7 +80,10 @@ export function AgentStatusBar() {
);
React.useEffect(() => {
if (status === WsClientProviderStatus.DISCONNECTED) {
if (conversation?.status === "STARTING") {
setStatusMessage(t(I18nKey.STATUS$STARTING_RUNTIME));
setIndicatorColor(IndicatorColor.RED);
} else if (status === WsClientProviderStatus.DISCONNECTED) {
setStatusMessage(t(I18nKey.STATUS$CONNECTED)); // Using STATUS$CONNECTED instead of STATUS$CONNECTING
setIndicatorColor(IndicatorColor.RED);
} else {
@@ -97,7 +102,7 @@ export function AgentStatusBar() {
}
}
}
}, [curAgentState, status, notify, t]);
}, [curAgentState, status, notify, t, conversation?.status]);
return (
<div className="flex flex-col items-center">
@@ -1,9 +1,8 @@
import { useParams } from "react-router";
import React from "react";
import { AgentControlBar } from "./agent-control-bar";
import { AgentStatusBar } from "./agent-status-bar";
import { SecurityLock } from "./security-lock";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { ConversationCard } from "../conversation-panel/conversation-card";
interface ControlsProps {
@@ -12,10 +11,7 @@ interface ControlsProps {
}
export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
const params = useParams();
const { data: conversation } = useUserConversation(
params.conversationId ?? null,
);
const { data: conversation } = useActiveConversation();
return (
<div className="flex flex-col gap-2 md:items-center md:justify-between md:flex-row">
@@ -6,26 +6,7 @@ import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/b
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { cn } from "#/utils/utils";
// Custom JSON viewer theme that matches our application theme
const jsonViewTheme = {
base00: "transparent", // background
base01: "#2d2d2d", // lighter background
base02: "#4e4e4e", // selection background
base03: "#6c6c6c", // comments, invisibles
base04: "#969896", // dark foreground
base05: "#d9d9d9", // default foreground
base06: "#e8e8e8", // light foreground
base07: "#ffffff", // light background
base08: "#ff5370", // variables, red
base09: "#f78c6c", // integers, orange
base0A: "#ffcb6b", // booleans, yellow
base0B: "#c3e88d", // strings, green
base0C: "#89ddff", // support, cyan
base0D: "#82aaff", // functions, blue
base0E: "#c792ea", // keywords, purple
base0F: "#ff5370", // deprecated, red
};
import { JSON_VIEW_THEME } from "#/utils/constants";
interface SystemMessageModalProps {
isOpen: boolean;
@@ -207,8 +188,9 @@ export function SystemMessageModal({
</h4>
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
<ReactJsonView
name={false}
src={parameters}
theme={jsonViewTheme}
theme={JSON_VIEW_THEME}
/>
</div>
</div>
@@ -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}
/>
))}
@@ -29,7 +29,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
// Store the login method in local storage (only in SAAS mode)
if (appMode === "saas") {
setLoginMethod(LoginMethod.GITHUB);
// Set the "just logged in" flag to true
}
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = githubAuthUrl;
@@ -41,7 +40,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
// Store the login method in local storage (only in SAAS mode)
if (appMode === "saas") {
setLoginMethod(LoginMethod.GITLAB);
// Set the "just logged in" flag to true
}
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = gitlabAuthUrl;
@@ -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
@@ -42,6 +42,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
posthog.capture("settings_saved", {
LLM_MODEL: newSettings.LLM_MODEL,
LLM_API_KEY_SET: newSettings.LLM_API_KEY_SET ? "SET" : "UNSET",
SEARCH_API_KEY_SET: newSettings.SEARCH_API_KEY ? "SET" : "UNSET",
REMOTE_RUNTIME_RESOURCE_FACTOR:
newSettings.REMOTE_RUNTIME_RESOURCE_FACTOR,
});
@@ -86,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>
@@ -1,42 +0,0 @@
import React, { useMemo } from "react";
import { useParams } from "react-router";
interface ConversationContextType {
conversationId: string;
}
const ConversationContext = React.createContext<ConversationContextType | null>(
null,
);
export function ConversationProvider({
children,
}: {
children: React.ReactNode;
}) {
const { conversationId } = useParams<{ conversationId: string }>();
if (!conversationId) {
throw new Error(
"ConversationProvider must be used within a route that has a conversationId parameter",
);
}
const value = useMemo(() => ({ conversationId }), [conversationId]);
return (
<ConversationContext.Provider value={value}>
{children}
</ConversationContext.Provider>
);
}
export function useConversation() {
const context = React.useContext(ConversationContext);
if (!context) {
throw new Error(
"useConversation must be used within a ConversationProvider",
);
}
return context;
}
+33 -6
View File
@@ -16,12 +16,14 @@ import {
} from "#/types/core/actions";
import { Conversation } from "#/api/open-hands.types";
import { useUserProviders } from "#/hooks/use-user-providers";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
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";
@@ -68,6 +70,7 @@ const isMessageAction = (
export enum WsClientProviderStatus {
CONNECTED,
DISCONNECTED,
CONNECTING,
}
interface UseWsClient {
@@ -147,7 +150,7 @@ export function WsClientProvider({
const { providers } = useUserProviders();
const messageRateHandler = useRate({ threshold: 250 });
const { data: conversation } = useUserConversation(conversationId);
const { data: conversation } = useActiveConversation();
function send(event: Record<string, unknown>) {
if (!sioRef.current) {
@@ -159,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]);
}
@@ -187,11 +213,12 @@ export function WsClientProvider({
// Invalidate diffs cache when a file is edited or written
if (
isFileEditAction(event) ||
isFileWriteAction(event) ||
isCommandAction(event)
!messageRateHandler.isUnderThreshold &&
(isFileEditAction(event) ||
isFileWriteAction(event) ||
isCommandAction(event))
) {
queryClient.removeQueries({
queryClient.invalidateQueries({
queryKey: ["file_changes", conversationId],
});
@@ -25,6 +25,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
mcp_config: settings.MCP_CONFIG,
enable_proactive_conversation_starters:
settings.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
search_api_key: settings.SEARCH_API_KEY?.trim() || "",
};
await OpenHands.saveSettings(apiSettings);
@@ -1,7 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import { Feedback } from "#/api/open-hands.types";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
import { useConversationId } from "#/hooks/use-conversation-id";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
type SubmitFeedbackArgs = {
@@ -9,12 +9,14 @@ type SubmitFeedbackArgs = {
};
export const useSubmitFeedback = () => {
const { conversationId } = useConversation();
const { conversationId } = useConversationId();
return useMutation({
mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
OpenHands.submitFeedback(conversationId, feedback),
onError: (error) => {
displayErrorToast(error.message);
},
retry: 2,
retryDelay: 500,
});
};
@@ -0,0 +1,14 @@
import { useConversationId } from "#/hooks/use-conversation-id";
import { useUserConversation } from "./use-user-conversation";
const FIVE_MINUTES = 1000 * 60 * 5;
export const useActiveConversation = () => {
const { conversationId } = useConversationId();
return useUserConversation(conversationId, (query) => {
if (query.state.data?.status === "STARTING") {
return 2000; // 2 seconds
}
return FIVE_MINUTES;
});
};
+9 -5
View File
@@ -5,13 +5,17 @@ import { useSelector } from "react-redux";
import OpenHands from "#/api/open-hands";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { RootState } from "#/store";
import { useConversation } from "#/context/conversation-context";
import { useConversationId } from "#/hooks/use-conversation-id";
import { useActiveConversation } from "./use-active-conversation";
export const useActiveHost = () => {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const [activeHost, setActiveHost] = React.useState<string | null>(null);
const { conversationId } = useConversation();
const { conversationId } = useConversationId();
const { data: conversation } = useActiveConversation();
const enabled =
conversation?.status === "RUNNING" &&
RUNTIME_INACTIVE_STATES.includes(curAgentState);
const { data } = useQuery({
queryKey: [conversationId, "hosts"],
@@ -19,7 +23,7 @@ export const useActiveHost = () => {
const hosts = await OpenHands.getWebHosts(conversationId);
return { hosts };
},
enabled: !RUNTIME_INACTIVE_STATES.includes(curAgentState),
enabled,
initialData: { hosts: [] },
meta: {
disableToast: true,
@@ -37,7 +41,7 @@ export const useActiveHost = () => {
return "";
}
},
refetchInterval: 3000,
// refetchInterval: 3000,
meta: {
disableToast: true,
},
@@ -4,12 +4,12 @@ import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useConversation } from "#/context/conversation-context";
import { useConversationId } from "#/hooks/use-conversation-id";
import OpenHands from "#/api/open-hands";
export const useConversationConfig = () => {
const { status } = useWsClient();
const { conversationId } = useConversation();
const { conversationId } = useConversationId();
const query = useQuery({
queryKey: ["conversation_config", conversationId],
+2 -2
View File
@@ -1,7 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { GitChangeStatus } from "#/api/open-hands.types";
import { useConversation } from "#/context/conversation-context";
import { useConversationId } from "#/hooks/use-conversation-id";
type UseGetDiffConfig = {
filePath: string;
@@ -10,7 +10,7 @@ type UseGetDiffConfig = {
};
export const useGitDiff = (config: UseGetDiffConfig) => {
const { conversationId } = useConversation();
const { conversationId } = useConversationId();
return useQuery({
queryKey: ["file_diff", conversationId, config.filePath, config.type],
@@ -2,18 +2,22 @@ import { useQuery } from "@tanstack/react-query";
import React from "react";
import { useSelector } from "react-redux";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
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";
export const useGetGitChanges = () => {
const { conversationId } = useConversation();
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 runtimeIsActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
const enabled =
conversation?.status === "RUNNING" &&
RUNTIME_INACTIVE_STATES.includes(curAgentState);
const result = useQuery({
queryKey: ["file_changes", conversationId],
@@ -21,7 +25,7 @@ export const useGetGitChanges = () => {
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
enabled: runtimeIsActive,
enabled,
meta: {
disableToast: true,
},
+2
View File
@@ -18,6 +18,7 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
CONFIRMATION_MODE: apiSettings.confirmation_mode,
SECURITY_ANALYZER: apiSettings.security_analyzer,
LLM_API_KEY_SET: apiSettings.llm_api_key_set,
SEARCH_API_KEY_SET: apiSettings.search_api_key_set,
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
@@ -25,6 +26,7 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
ENABLE_PROACTIVE_CONVERSATION_STARTERS:
apiSettings.enable_proactive_conversation_starters,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
SEARCH_API_KEY: apiSettings.search_api_key || "",
MCP_CONFIG: apiSettings.mcp_config,
IS_NEW_USER: false,
@@ -1,10 +1,24 @@
import { useQuery } from "@tanstack/react-query";
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Query, useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";
import OpenHands from "#/api/open-hands";
import { Conversation } from "#/api/open-hands.types";
const FIVE_MINUTES = 1000 * 60 * 5;
const FIFTEEN_MINUTES = 1000 * 60 * 15;
type RefetchInterval = (
query: Query<
Conversation | null,
AxiosError<unknown, any>,
Conversation | null,
(string | null)[]
>,
) => number;
export const useUserConversation = (cid: string | null) =>
export const useUserConversation = (
cid: string | null,
refetchInterval?: RefetchInterval,
) =>
useQuery({
queryKey: ["user", "conversation", cid],
queryFn: async () => {
@@ -14,12 +28,7 @@ export const useUserConversation = (cid: string | null) =>
},
enabled: !!cid,
retry: false,
refetchInterval: (query) => {
if (query.state.data?.status === "STARTING") {
return 2000; // 2 seconds
}
return FIVE_MINUTES;
},
refetchInterval,
staleTime: FIVE_MINUTES,
gcTime: FIFTEEN_MINUTES,
});
+8 -4
View File
@@ -2,11 +2,12 @@ import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useSelector } from "react-redux";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
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";
// Define the return type for the VS Code URL query
interface VSCodeUrlResult {
@@ -16,9 +17,12 @@ interface VSCodeUrlResult {
export const useVSCodeUrl = () => {
const { t } = useTranslation();
const { conversationId } = useConversation();
const { conversationId } = useConversationId();
const { data: conversation } = useActiveConversation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState);
const enabled =
conversation?.status === "RUNNING" &&
RUNTIME_INACTIVE_STATES.includes(curAgentState);
return useQuery<VSCodeUrlResult>({
queryKey: ["vscode_url", conversationId],
@@ -36,7 +40,7 @@ export const useVSCodeUrl = () => {
error: t(I18nKey.VSCODE$URL_NOT_AVAILABLE),
};
},
enabled: !!conversationId && !isRuntimeInactive,
enabled,
refetchOnMount: true,
retry: 3,
});
+1 -51
View File
@@ -1,14 +1,7 @@
import { useEffect } from "react";
import { useNavigate } from "react-router";
import { useConfig } from "./query/use-config";
import { useIsAuthed } from "./query/use-is-authed";
import {
getLoginMethod,
getLastPage,
getJustLoggedIn,
setJustLoggedIn,
LoginMethod,
} from "#/utils/local-storage";
import { getLoginMethod, LoginMethod } from "#/utils/local-storage";
import { useAuthUrl } from "./use-auth-url";
/**
@@ -16,7 +9,6 @@ import { useAuthUrl } from "./use-auth-url";
* Only works in SAAS mode and when the user is not already logged in
*/
export const useAutoLogin = () => {
const navigate = useNavigate();
const { data: config, isLoading: isConfigLoading } = useConfig();
const { data: isAuthed, isLoading: isAuthLoading } = useIsAuthed();
@@ -61,9 +53,6 @@ export const useAutoLogin = () => {
// If we have an auth URL, redirect to it
if (authUrl) {
// Set the "just logged in" flag to true
setJustLoggedIn(true);
// After successful login, the user will be redirected back and can navigate to the last page
window.location.href = authUrl;
}
@@ -76,43 +65,4 @@ export const useAutoLogin = () => {
githubAuthUrl,
gitlabAuthUrl,
]);
// Handle navigation to last page after login
useEffect(() => {
// Only navigate in SAAS mode
if (config?.APP_MODE !== "saas") {
return;
}
// Wait for auth to load
if (isAuthLoading) {
return;
}
// Only navigate if authenticated
if (!isAuthed) {
return;
}
// Get the last page from local storage
const lastPage = getLastPage();
// Get the current pathname
const currentPath = window.location.pathname;
// Check if the user just logged in
// Only navigate to the last page if:
// 1. Last page exists in local storage
// 2. We're on the home page (/) - this prevents redirecting when a user
// explicitly navigates to a specific page or opens a link in a new tab
// 3. The user just logged in (new condition)
if (lastPage && currentPath === "/" && getJustLoggedIn()) {
// Clear the "just logged in" flag
setJustLoggedIn(false);
// Navigate to the last page
navigate(lastPage);
}
}, [config?.APP_MODE, isAuthed, isAuthLoading, navigate]);
};
+13
View File
@@ -0,0 +1,13 @@
import { useParams } from "react-router";
export function useConversationId() {
const { conversationId } = useParams<{ conversationId: string }>();
if (!conversationId) {
throw new Error(
"useConversationId must be used within a route that has a conversationId parameter",
);
}
return { conversationId };
}
@@ -1,6 +1,5 @@
import { useParams } from "react-router";
import { useEffect, useRef } from "react";
import { useUserConversation } from "./query/use-user-conversation";
import { useActiveConversation } from "./query/use-active-conversation";
/**
* Hook that updates the document title based on the current conversation.
@@ -9,10 +8,7 @@ import { useUserConversation } from "./query/use-user-conversation";
* @param suffix Optional suffix to append to the title (default: "OpenHands")
*/
export function useDocumentTitleFromState(suffix = "OpenHands") {
const params = useParams();
const { data: conversation } = useUserConversation(
params.conversationId ?? null,
);
const { data: conversation } = useActiveConversation();
const lastValidTitleRef = useRef<string | null>(null);
useEffect(() => {
-33
View File
@@ -1,33 +0,0 @@
import { useEffect } from "react";
import { useLocation } from "react-router";
import { useConfig } from "./query/use-config";
import { setLastPage, shouldExcludePath } from "#/utils/local-storage";
import { useIsAuthed } from "./query/use-is-authed";
/**
* Hook to track the last visited page in local storage
* Only tracks pages in SAAS mode and excludes certain paths
*/
export const useTrackLastPage = () => {
const location = useLocation();
const { data: config } = useConfig();
const { data: isAuthed, isLoading: isAuthLoading } = useIsAuthed();
useEffect(() => {
// Only track pages in SAAS mode when authenticated
if (config?.APP_MODE !== "saas" || !isAuthed || isAuthLoading) {
return;
}
const { pathname } = location;
// Don't track excluded paths
if (shouldExcludePath(pathname)) {
// leave code block for now as we may decide not to track certain pages.
// return;
}
// Store the current path as the last visited page
setLastPage(pathname);
}, [location, config?.APP_MODE]);
};
+9
View File
@@ -11,6 +11,8 @@ export enum I18nKey {
EVENT$UNKNOWN_EVENT = "EVENT$UNKNOWN_EVENT",
OBSERVATION$COMMAND_NO_OUTPUT = "OBSERVATION$COMMAND_NO_OUTPUT",
OBSERVATION$MCP_NO_OUTPUT = "OBSERVATION$MCP_NO_OUTPUT",
MCP_OBSERVATION$ARGUMENTS = "MCP_OBSERVATION$ARGUMENTS",
MCP_OBSERVATION$OUTPUT = "MCP_OBSERVATION$OUTPUT",
OBSERVATION$ERROR_PREFIX = "OBSERVATION$ERROR_PREFIX",
TASK$ADDRESSING_TASK = "TASK$ADDRESSING_TASK",
SECRETS$SECRET_VALUE_REQUIRED = "SECRETS$SECRET_VALUE_REQUIRED",
@@ -117,6 +119,9 @@ export enum I18nKey {
SETTINGS$GIT_SETTINGS = "SETTINGS$GIT_SETTINGS",
SETTINGS$SOUND_NOTIFICATIONS = "SETTINGS$SOUND_NOTIFICATIONS",
SETTINGS$PROACTIVE_CONVERSATION_STARTERS = "SETTINGS$PROACTIVE_CONVERSATION_STARTERS",
SETTINGS$SEARCH_API_KEY = "SETTINGS$SEARCH_API_KEY",
SETTINGS$SEARCH_API_KEY_OPTIONAL = "SETTINGS$SEARCH_API_KEY_OPTIONAL",
SETTINGS$SEARCH_API_KEY_INSTRUCTIONS = "SETTINGS$SEARCH_API_KEY_INSTRUCTIONS",
SETTINGS$CUSTOM_MODEL = "SETTINGS$CUSTOM_MODEL",
GITHUB$CODE_NOT_IN_GITHUB = "GITHUB$CODE_NOT_IN_GITHUB",
GITHUB$START_FROM_SCRATCH = "GITHUB$START_FROM_SCRATCH",
@@ -421,6 +426,7 @@ export enum I18nKey {
ACTION_MESSAGE$BROWSE_INTERACTIVE = "ACTION_MESSAGE$BROWSE_INTERACTIVE",
ACTION_MESSAGE$THINK = "ACTION_MESSAGE$THINK",
ACTION_MESSAGE$SYSTEM = "ACTION_MESSAGE$SYSTEM",
ACTION_MESSAGE$CONDENSATION = "ACTION_MESSAGE$CONDENSATION",
OBSERVATION_MESSAGE$RUN = "OBSERVATION_MESSAGE$RUN",
OBSERVATION_MESSAGE$RUN_IPYTHON = "OBSERVATION_MESSAGE$RUN_IPYTHON",
OBSERVATION_MESSAGE$READ = "OBSERVATION_MESSAGE$READ",
@@ -429,6 +435,7 @@ export enum I18nKey {
OBSERVATION_MESSAGE$BROWSE = "OBSERVATION_MESSAGE$BROWSE",
OBSERVATION_MESSAGE$MCP = "OBSERVATION_MESSAGE$MCP",
OBSERVATION_MESSAGE$RECALL = "OBSERVATION_MESSAGE$RECALL",
OBSERVATION_MESSAGE$THINK = "OBSERVATION_MESSAGE$THINK",
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
EXPANDABLE_MESSAGE$HIDE_DETAILS = "EXPANDABLE_MESSAGE$HIDE_DETAILS",
AI_SETTINGS$TITLE = "AI_SETTINGS$TITLE",
@@ -541,4 +548,6 @@ export enum I18nKey {
TIPS$API_USAGE = "TIPS$API_USAGE",
TIPS$LEARN_MORE = "TIPS$LEARN_MORE",
TIPS$PROTIP = "TIPS$PROTIP",
FEEDBACK$SUBMITTING_LABEL = "FEEDBACK$SUBMITTING_LABEL",
FEEDBACK$SUBMITTING_MESSAGE = "FEEDBACK$SUBMITTING_MESSAGE",
}
+144
View File
@@ -175,6 +175,38 @@
"de": "[MCP-Tool wurde ohne Ausgabe ausgeführt]",
"uk": "[Інструмент MCP завершив виконання без виводу]"
},
"MCP_OBSERVATION$ARGUMENTS": {
"en": "Arguments",
"ja": "引数",
"zh-CN": "参数",
"zh-TW": "參數",
"ko-KR": "인수",
"no": "Argumenter",
"it": "Argomenti",
"pt": "Argumentos",
"es": "Argumentos",
"ar": "المعاملات",
"fr": "Arguments",
"tr": "Argümanlar",
"de": "Argumente",
"uk": "Аргументи"
},
"MCP_OBSERVATION$OUTPUT": {
"en": "Output",
"ja": "出力",
"zh-CN": "输出",
"zh-TW": "輸出",
"ko-KR": "출력",
"no": "Utdata",
"it": "Output",
"pt": "Saída",
"es": "Salida",
"ar": "المخرجات",
"fr": "Sortie",
"tr": "Çıktı",
"de": "Ausgabe",
"uk": "Вивід"
},
"OBSERVATION$ERROR_PREFIX": {
"en": "error:",
"ja": "エラー:",
@@ -1871,6 +1903,54 @@
"tr": "GitHub'da Görevler Öner",
"uk": "Запропонувати завдання на GitHub"
},
"SETTINGS$SEARCH_API_KEY": {
"en": "Search API Key (Tavily)",
"ja": "検索APIキー (Tavily)",
"zh-CN": "搜索API密钥 (Tavily)",
"zh-TW": "搜索API密鑰 (Tavily)",
"ko-KR": "검색 API 키 (Tavily)",
"de": "Such-API-Schlüssel (Tavily)",
"no": "Søk API-nøkkel (Tavily)",
"it": "Chiave API di ricerca (Tavily)",
"pt": "Chave de API de pesquisa (Tavily)",
"es": "Clave API de búsqueda (Tavily)",
"ar": "مفتاح API للبحث (Tavily)",
"fr": "Clé API de recherche (Tavily)",
"tr": "Arama API Anahtarı (Tavily)",
"uk": "Ключ API пошуку (Tavily)"
},
"SETTINGS$SEARCH_API_KEY_OPTIONAL": {
"en": "This field is optional. We use Tavily as our default search engine provider.",
"ja": "このフィールドは任意です。デフォルトの検索エンジンプロバイダーとしてTavilyを使用しています。",
"zh-CN": "此字段为可选项。我们使用Tavily作为默认搜索引擎提供商。",
"zh-TW": "此字段為可選項。我們使用Tavily作為默認搜索引擎提供商。",
"ko-KR": "이 필드는 선택 사항입니다. 기본 검색 엔진 제공업체로 Tavily를 사용합니다.",
"de": "Dieses Feld ist optional. Wir verwenden Tavily als unseren Standard-Suchmaschinenanbieter.",
"no": "Dette feltet er valgfritt. Vi bruker Tavily som vår standard søkemotorleverandør.",
"it": "Questo campo è opzionale. Utilizziamo Tavily come nostro fornitore di motori di ricerca predefinito.",
"pt": "Este campo é opcional. Usamos o Tavily como nosso provedor de mecanismo de pesquisa padrão.",
"es": "Este campo es opcional. Utilizamos Tavily como nuestro proveedor de motor de búsqueda predeterminado.",
"ar": "هذا الحقل اختياري. نستخدم Tavily كمزود محرك البحث الافتراضي.",
"fr": "Ce champ est facultatif. Nous utilisons Tavily comme fournisseur de moteur de recherche par défaut.",
"tr": "Bu alan isteğe bağlıdır. Varsayılan arama motoru sağlayıcısı olarak Tavily'yi kullanıyoruz.",
"uk": "Це поле є необов'язковим. Ми використовуємо Tavily як нашого типового постачальника пошукової системи."
},
"SETTINGS$SEARCH_API_KEY_INSTRUCTIONS": {
"en": "Get your API key from Tavily",
"ja": "TavilyからAPIキーを取得する",
"zh-CN": "从Tavily获取您的API密钥",
"zh-TW": "從Tavily獲取您的API密鑰",
"ko-KR": "Tavily에서 API 키 받기",
"de": "Holen Sie sich Ihren API-Schlüssel von Tavily",
"no": "Få API-nøkkelen din fra Tavily",
"it": "Ottieni la tua chiave API da Tavily",
"pt": "Obtenha sua chave de API do Tavily",
"es": "Obtenga su clave API de Tavily",
"ar": "احصل على مفتاح API الخاص بك من Tavily",
"fr": "Obtenez votre clé API de Tavily",
"tr": "API anahtarınızı Tavily'den alın",
"uk": "Отримайте свій ключ API від Tavily"
},
"SETTINGS$CUSTOM_MODEL": {
"en": "Custom Model",
"ja": "カスタムモデル",
@@ -6735,6 +6815,22 @@
"tr": "Sistem Mesajı",
"uk": "Системне повідомлення"
},
"ACTION_MESSAGE$CONDENSATION": {
"en": "Condensation",
"zh-CN": "浓缩",
"zh-TW": "濃縮",
"ko-KR": "응축",
"ja": "凝縮",
"no": "Kondensering",
"ar": "تكثيف",
"de": "Kondensation",
"fr": "Condensation",
"it": "Condensazione",
"pt": "Condensação",
"es": "Condensación",
"tr": "Yoğunlaşma",
"uk": "Конденсація"
},
"OBSERVATION_MESSAGE$RUN": {
"en": "Ran <cmd>{{command}}</cmd>",
"zh-CN": "运行 <cmd>{{command}}</cmd>",
@@ -6863,6 +6959,22 @@
"de": "Microagent bereit",
"uk": "Мікроагент готовий"
},
"OBSERVATION_MESSAGE$THINK": {
"en": "Thought",
"ja": "思考",
"zh-CN": "思考",
"zh-TW": "思考",
"ko-KR": "생각",
"no": "Tanke",
"it": "Pensiero",
"pt": "Pensamento",
"es": "Pensamiento",
"ar": "فكرة",
"fr": "Pensée",
"tr": "Düşünce",
"de": "Gedanke",
"uk": "Думка"
},
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
"en": "Show details",
"zh-CN": "显示详情",
@@ -8654,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": "Відправляємо відгук, будь ласка, почекайте..."
}
}
+2 -1
View File
@@ -17,6 +17,7 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
llm_base_url: DEFAULT_SETTINGS.LLM_BASE_URL,
llm_api_key: null,
llm_api_key_set: DEFAULT_SETTINGS.LLM_API_KEY_SET,
search_api_key_set: DEFAULT_SETTINGS.SEARCH_API_KEY_SET,
agent: DEFAULT_SETTINGS.AGENT,
language: DEFAULT_SETTINGS.LANGUAGE,
confirmation_mode: DEFAULT_SETTINGS.CONFIRMATION_MODE,
@@ -99,7 +100,7 @@ const openHandsHandlers = [
"gpt-4o",
"gpt-4o-mini",
"anthropic/claude-3.5",
"anthropic/claude-3-7-sonnet-20250219",
"anthropic/claude-sonnet-4-20250514",
]),
),
+1
View File
@@ -73,6 +73,7 @@ function AppSettingsScreen() {
setLanguageInputHasChanged(false);
setAnalyticsSwitchHasChanged(false);
setSoundNotificationsSwitchHasChanged(false);
setProactiveConversationsSwitchHasChanged(false);
},
},
);
+5 -14
View File
@@ -8,10 +8,7 @@ import { DiGit } from "react-icons/di";
import { VscCode } from "react-icons/vsc";
import { I18nKey } from "#/i18n/declaration";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import {
ConversationProvider,
useConversation,
} from "#/context/conversation-context";
import { useConversationId } from "#/hooks/use-conversation-id";
import { Controls } from "#/components/features/controls/controls";
import { clearTerminal } from "#/state/command-slice";
import { useEffectOnce } from "#/hooks/use-effect-once";
@@ -30,7 +27,7 @@ import {
ResizablePanel,
} from "#/components/layout/resizable-panel";
import Security from "#/components/shared/modals/security/security";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { ServedAppLabel } from "#/components/layout/served-app-label";
import { useSettings } from "#/hooks/query/use-settings";
import { RootState } from "#/store";
@@ -44,10 +41,8 @@ function AppContent() {
useConversationConfig();
const { t } = useTranslation();
const { data: settings } = useSettings();
const { conversationId } = useConversation();
const { data: conversation, isFetched } = useUserConversation(
conversationId || null,
);
const { conversationId } = useConversationId();
const { data: conversation, isFetched } = useActiveConversation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const dispatch = useDispatch();
@@ -213,11 +208,7 @@ function AppContent() {
}
function App() {
return (
<ConversationProvider>
<AppContent />
</ConversationProvider>
);
return <AppContent />;
}
export default App;
+65 -4
View File
@@ -40,6 +40,7 @@ function LlmSettingsScreen() {
const [dirtyInputs, setDirtyInputs] = React.useState({
model: false,
apiKey: false,
searchApiKey: false,
baseUrl: false,
agent: false,
confirmationMode: false,
@@ -77,6 +78,7 @@ function LlmSettingsScreen() {
setDirtyInputs({
model: false,
apiKey: false,
searchApiKey: false,
baseUrl: false,
agent: false,
confirmationMode: false,
@@ -94,6 +96,7 @@ function LlmSettingsScreen() {
const provider = formData.get("llm-provider-input")?.toString();
const model = formData.get("llm-model-input")?.toString();
const apiKey = formData.get("llm-api-key-input")?.toString();
const searchApiKey = formData.get("search-api-key-input")?.toString();
const fullLlmModel =
provider && model && `${provider}/${model}`.toLowerCase();
@@ -102,6 +105,7 @@ function LlmSettingsScreen() {
{
LLM_MODEL: fullLlmModel,
llm_api_key: apiKey || null,
SEARCH_API_KEY: searchApiKey || "",
// reset advanced settings
LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL,
@@ -121,6 +125,7 @@ function LlmSettingsScreen() {
const model = formData.get("llm-custom-model-input")?.toString();
const baseUrl = formData.get("base-url-input")?.toString();
const apiKey = formData.get("llm-api-key-input")?.toString();
const searchApiKey = formData.get("search-api-key-input")?.toString();
const agent = formData.get("agent-input")?.toString();
const confirmationMode =
formData.get("enable-confirmation-mode-switch")?.toString() === "on";
@@ -135,6 +140,7 @@ function LlmSettingsScreen() {
LLM_MODEL: model,
LLM_BASE_URL: baseUrl,
llm_api_key: apiKey || null,
SEARCH_API_KEY: searchApiKey || "",
AGENT: agent,
CONFIRMATION_MODE: confirmationMode,
ENABLE_DEFAULT_CONDENSER: enableDefaultCondenser,
@@ -158,6 +164,7 @@ function LlmSettingsScreen() {
setDirtyInputs({
model: false,
apiKey: false,
searchApiKey: false,
baseUrl: false,
agent: false,
confirmationMode: false,
@@ -184,6 +191,14 @@ function LlmSettingsScreen() {
}));
};
const handleSearchApiKeyIsDirty = (searchApiKey: string) => {
const searchApiKeyIsDirty = searchApiKey !== settings?.SEARCH_API_KEY;
setDirtyInputs((prev) => ({
...prev,
searchApiKey: searchApiKeyIsDirty,
}));
};
const handleCustomModelIsDirty = (model: string) => {
const modelIsDirty = model !== settings?.LLM_MODEL && model !== "";
setDirtyInputs((prev) => ({
@@ -264,7 +279,7 @@ function LlmSettingsScreen() {
<ModelSelector
models={modelsAndProviders}
currentModel={
settings.LLM_MODEL || "anthropic/claude-3-5-sonnet-20241022"
settings.LLM_MODEL || "anthropic/claude-sonnet-4-20250514"
}
onChange={handleModelIsDirty}
/>
@@ -291,6 +306,29 @@ function LlmSettingsScreen() {
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
href="https://docs.all-hands.dev/modules/usage/installation#getting-an-api-key"
/>
<SettingsInput
testId="search-api-key-input"
name="search-api-key-input"
label={t(I18nKey.SETTINGS$SEARCH_API_KEY)}
type="password"
className="w-[680px]"
defaultValue={settings.SEARCH_API_KEY || ""}
onChange={handleSearchApiKeyIsDirty}
placeholder="sk-tavily-..."
startContent={
settings.SEARCH_API_KEY_SET && (
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />
)
}
/>
<HelpLink
testId="search-api-key-help-anchor"
text={t(I18nKey.SETTINGS$SEARCH_API_KEY_OPTIONAL)}
linkText={t(I18nKey.SETTINGS$SEARCH_API_KEY_INSTRUCTIONS)}
href="https://tavily.com/"
/>
</div>
)}
@@ -304,9 +342,9 @@ function LlmSettingsScreen() {
name="llm-custom-model-input"
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
defaultValue={
settings.LLM_MODEL || "anthropic/claude-3-7-sonnet-20250219"
settings.LLM_MODEL || "anthropic/claude-sonnet-4-20250514"
}
placeholder="anthropic/claude-3-7-sonnet-20250219"
placeholder="anthropic/claude-sonnet-4-20250514"
type="text"
className="w-[680px]"
onChange={handleCustomModelIsDirty}
@@ -338,12 +376,35 @@ function LlmSettingsScreen() {
}
/>
<HelpLink
testId="llm-api-key-help-anchor"
testId="llm-api-key-help-anchor-advanced"
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
href="https://docs.all-hands.dev/modules/usage/installation#getting-an-api-key"
/>
<SettingsInput
testId="search-api-key-input"
name="search-api-key-input"
label={t(I18nKey.SETTINGS$SEARCH_API_KEY)}
type="password"
className="w-[680px]"
defaultValue={settings.SEARCH_API_KEY || ""}
onChange={handleSearchApiKeyIsDirty}
placeholder="tvly-..."
startContent={
settings.SEARCH_API_KEY_SET && (
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />
)
}
/>
<HelpLink
testId="search-api-key-help-anchor"
text={t(I18nKey.SETTINGS$SEARCH_API_KEY_OPTIONAL)}
linkText={t(I18nKey.SETTINGS$SEARCH_API_KEY_INSTRUCTIONS)}
href="https://tavily.com/"
/>
<SettingsDropdownInput
testId="agent-input"
name="agent-input"
+1 -5
View File
@@ -22,7 +22,6 @@ import { useBalance } from "#/hooks/query/use-balance";
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { useTrackLastPage } from "#/hooks/use-track-last-page";
import { useAutoLogin } from "#/hooks/use-auto-login";
import { LOCAL_STORAGE_KEYS } from "#/utils/local-storage";
@@ -86,9 +85,6 @@ export default function MainApp() {
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
// Track the last visited page
useTrackLastPage();
// Auto-login if login method is stored in local storage
useAutoLogin();
@@ -130,7 +126,7 @@ export default function MainApp() {
React.useEffect(() => {
// Don't do any redirects when on TOS page
// Don't allow users to use the app if it 402s
if (!isOnTosPage && error?.status === 402) {
if (!isOnTosPage && error?.status === 402 && pathname !== "/") {
navigate("/");
}
}, [error?.status, pathname, isOnTosPage]);
+3 -1
View File
@@ -3,11 +3,12 @@ import { Settings } from "#/types/settings";
export const LATEST_SETTINGS_VERSION = 5;
export const DEFAULT_SETTINGS: Settings = {
LLM_MODEL: "anthropic/claude-3-7-sonnet-20250219",
LLM_MODEL: "anthropic/claude-sonnet-4-20250514",
LLM_BASE_URL: "",
AGENT: "CodeActAgent",
LANGUAGE: "en",
LLM_API_KEY_SET: false,
SEARCH_API_KEY_SET: false,
CONFIRMATION_MODE: false,
SECURITY_ANALYZER: "",
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
@@ -16,6 +17,7 @@ export const DEFAULT_SETTINGS: Settings = {
ENABLE_SOUND_NOTIFICATIONS: false,
USER_CONSENTS_TO_ANALYTICS: false,
ENABLE_PROACTIVE_CONVERSATION_STARTERS: false,
SEARCH_API_KEY: "",
IS_NEW_USER: true,
MCP_CONFIG: {
sse_servers: [],
+18
View File
@@ -6,10 +6,13 @@ import {
SystemMessageAction,
} from "./actions";
import {
AgentStateChangeObservation,
CommandObservation,
ErrorObservation,
MCPObservation,
OpenHandsObservation,
} from "./observations";
import { StatusUpdate } from "./variances";
export const isOpenHandsAction = (
event: OpenHandsParsedEvent,
@@ -38,6 +41,11 @@ export const isErrorObservation = (
): event is ErrorObservation =>
isOpenHandsObservation(event) && event.observation === "error";
export const isAgentStateChangeObservation = (
event: OpenHandsParsedEvent,
): event is AgentStateChangeObservation =>
isOpenHandsObservation(event) && event.observation === "agent_state_changed";
export const isCommandObservation = (
event: OpenHandsParsedEvent,
): event is CommandObservation =>
@@ -57,3 +65,13 @@ export const isRejectObservation = (
event: OpenHandsParsedEvent,
): event is OpenHandsObservation =>
isOpenHandsObservation(event) && event.observation === "user_rejected";
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;
+2
View File
@@ -6,6 +6,7 @@ export interface AgentStateChangeObservation
source: "agent";
extras: {
agent_state: AgentState;
reason?: string;
};
}
@@ -135,6 +136,7 @@ export interface MCPObservation extends OpenHandsObservationEvent<"mcp"> {
source: "agent";
extras: {
name: string;
arguments: Record<string, unknown>;
};
}
+9 -1
View File
@@ -33,7 +33,15 @@ interface LocalUserMessageAction {
};
}
export interface StatusUpdate {
status_update: true;
type: "error";
id: string;
message: string;
}
export type OpenHandsVariance =
| TokenConfig
| InitConfig
| LocalUserMessageAction;
| LocalUserMessageAction
| StatusUpdate;
+6
View File
@@ -33,6 +33,7 @@ export type Settings = {
AGENT: string;
LANGUAGE: string;
LLM_API_KEY_SET: boolean;
SEARCH_API_KEY_SET: boolean;
CONFIRMATION_MODE: boolean;
SECURITY_ANALYZER: string;
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
@@ -41,6 +42,7 @@ export type Settings = {
ENABLE_SOUND_NOTIFICATIONS: boolean;
ENABLE_PROACTIVE_CONVERSATION_STARTERS: boolean;
USER_CONSENTS_TO_ANALYTICS: boolean | null;
SEARCH_API_KEY?: string;
IS_NEW_USER?: boolean;
MCP_CONFIG?: MCPConfig;
};
@@ -52,6 +54,7 @@ export type ApiSettings = {
language: string;
llm_api_key: string | null;
llm_api_key_set: boolean;
search_api_key_set: boolean;
confirmation_mode: boolean;
security_analyzer: string;
remote_runtime_resource_factor: number | null;
@@ -59,6 +62,7 @@ export type ApiSettings = {
enable_sound_notifications: boolean;
enable_proactive_conversation_starters: boolean;
user_consents_to_analytics: boolean | null;
search_api_key?: string;
provider_tokens_set: Partial<Record<Provider, string | null>>;
mcp_config?: {
sse_servers: (string | MCPSSEServer)[];
@@ -69,10 +73,12 @@ export type ApiSettings = {
export type PostSettings = Settings & {
user_consents_to_analytics: boolean | null;
llm_api_key?: string | null;
search_api_key?: string;
mcp_config?: MCPConfig;
};
export type PostApiSettings = ApiSettings & {
user_consents_to_analytics: boolean | null;
search_api_key?: string;
mcp_config?: MCPConfig;
};
+19
View File
@@ -9,3 +9,22 @@ export const ASSET_FILE_TYPES = [
".webm",
".ogg",
];
export const JSON_VIEW_THEME = {
base00: "transparent", // background
base01: "#2d2d2d", // lighter background
base02: "#4e4e4e", // selection background
base03: "#6c6c6c", // comments, invisibles
base04: "#969896", // dark foreground
base05: "#d9d9d9", // default foreground
base06: "#e8e8e8", // light foreground
base07: "#ffffff", // light background
base08: "#ff5370", // variables, red
base09: "#f78c6c", // integers, orange
base0A: "#ffcb6b", // booleans, yellow
base0B: "#c3e88d", // strings, green
base0C: "#89ddff", // support, cyan
base0D: "#82aaff", // functions, blue
base0E: "#c792ea", // keywords, purple
base0F: "#ff5370", // deprecated, red
};
+1 -1
View File
@@ -16,5 +16,5 @@ export const generateAuthUrl = (identityProvider: string, requestUrl: URL) => {
authUrl = `auth.${requestUrl.hostname}`;
}
const scope = "openid email profile"; // OAuth scope - not user-facing
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=allhands&kc_idp_hint=${identityProvider}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=allhands&kc_idp_hint=${identityProvider}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(requestUrl.href)}`;
};
+1 -45
View File
@@ -1,8 +1,6 @@
// Local storage keys
export const LOCAL_STORAGE_KEYS = {
LOGIN_METHOD: "openhands_login_method",
LAST_PAGE: "openhands_last_page",
JUST_LOGGED_IN: "openhands_just_logged_in",
};
// Login methods
@@ -29,50 +27,8 @@ export const getLoginMethod = (): LoginMethod | null => {
};
/**
* Set the last visited page in local storage
* @param path The path of the last visited page
*/
export const setLastPage = (path: string): void => {
localStorage.setItem(LOCAL_STORAGE_KEYS.LAST_PAGE, path);
};
/**
* Get the last visited page from local storage
* @returns The last visited page or null if not set
*/
export const getLastPage = (): string | null =>
localStorage.getItem(LOCAL_STORAGE_KEYS.LAST_PAGE);
/**
* Clear login method, last page, and just logged in flag from local storage
* Clear login method and last page from local storage
*/
export const clearLoginData = (): void => {
localStorage.removeItem(LOCAL_STORAGE_KEYS.LOGIN_METHOD);
localStorage.removeItem(LOCAL_STORAGE_KEYS.LAST_PAGE);
localStorage.removeItem(LOCAL_STORAGE_KEYS.JUST_LOGGED_IN);
};
/**
* Check if the given path should be excluded from being saved as the last page
* @param path The path to check
* @returns True if the path should be excluded, false otherwise
*/
export const shouldExcludePath = (path: string): boolean =>
path.startsWith("/settings");
/**
* Set the "just logged in" flag in local storage
* @param value True if the user just logged in, false otherwise
*/
export const setJustLoggedIn = (value: boolean): void => {
localStorage.setItem(LOCAL_STORAGE_KEYS.JUST_LOGGED_IN, value.toString());
};
/**
* Get the "just logged in" flag from local storage
* @returns True if the user just logged in, false otherwise
*/
export const getJustLoggedIn = (): boolean => {
const value = localStorage.getItem(LOCAL_STORAGE_KEYS.JUST_LOGGED_IN);
return value === "true";
};
+1 -1
View File
@@ -30,7 +30,7 @@ export const TIPS: Tip[] = [
},
{
key: I18nKey.TIPS$GITHUB_HOOK,
link: "https://docs.all-hands.dev/modules/usage/cloud/cloud-github-resolver",
link: "https://docs.all-hands.dev/modules/usage/cloud/cloud-issue-resolver",
},
{
key: I18nKey.TIPS$BLOG_SIGNUP,
+4
View File
@@ -6,6 +6,8 @@ export const VERIFIED_MODELS = [
"o4-mini-2025-04-16",
"claude-3-5-sonnet-20241022",
"claude-3-7-sonnet-20250219",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
"deepseek-chat",
];
@@ -39,4 +41,6 @@ export const VERIFIED_ANTHROPIC_MODELS = [
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-7-sonnet-20250219",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
];
+1 -4
View File
@@ -10,7 +10,6 @@ import i18n from "i18next";
import { vi } from "vitest";
import { AxiosError } from "axios";
import { AppStore, RootState, rootReducer } from "./src/store";
import { ConversationProvider } from "#/context/conversation-context";
// Mock useParams before importing components
vi.mock("react-router", async () => {
@@ -72,9 +71,7 @@ export function renderWithProviders(
})
}
>
<ConversationProvider>
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
</ConversationProvider>
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
</QueryClientProvider>
</Provider>
);
+2
View File
@@ -7,6 +7,7 @@ from openhands.agenthub import ( # noqa: E402
browsing_agent,
codeact_agent,
dummy_agent,
loc_agent,
readonly_agent,
visualbrowsing_agent,
)
@@ -19,4 +20,5 @@ __all__ = [
'browsing_agent',
'visualbrowsing_agent',
'readonly_agent',
'loc_agent',
]
@@ -266,5 +266,5 @@ class CodeActAgent(Agent):
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
return codeact_function_calling.response_to_actions(
response, mcp_tool_names=list(self.mcp_tools.keys())
response, mcp_tool_names=list(self.mcp_tools.keys()),
)
+14
View File
@@ -0,0 +1,14 @@
# LocAgent Framework
This folder is an implementation of Locagent. It is based on ([LocAgent](https://arxiv.org/abs/2503.09089), [tweet](https://x.com/XiangruTang/status/1900392655009333338)), a framework that addresses code localization through graph-based representation. By parsing codebases into directed heterogeneous graphs, LocAgent creates a lightweight representation that captures code structures and their dependencies, enabling LLM agents to effectively search and locate relevant entities through powerful multi-hop reasoning.
<!-- ## Overview -->
## Built-in Tools
The agent provides several built-in tools:
1. `search_code_snippets`
2. `get_entity_contents`
3. `explore_tree_structure`
+4
View File
@@ -0,0 +1,4 @@
from openhands.agenthub.loc_agent.loc_agent import LocAgent
from openhands.controller.agent import Agent
Agent.register('LocAgent', LocAgent)
@@ -0,0 +1,126 @@
"""This file contains the function calling implementation for different actions.
This is similar to the functionality of `CodeActResponseParser`.
"""
import json
from litellm import (
ChatCompletionToolParam,
ModelResponse,
)
from openhands.agenthub.codeact_agent.tools import FinishTool
from openhands.agenthub.codeact_agent.function_calling import combine_thought
from openhands.agenthub.loc_agent.tools import (
SearchEntityTool,
SearchRepoTool,
create_explore_tree_structure_tool,
)
from openhands.core.exceptions import (
FunctionCallNotExistsError,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
Action,
AgentFinishAction,
IPythonRunCellAction,
MessageAction,
)
from openhands.events.tool import ToolCallMetadata
def response_to_actions(
response: ModelResponse, mcp_tool_names: list[str] | None = None,
) -> list[Action]:
actions: list[Action] = []
assert len(response.choices) == 1, 'Only one choice is supported for now'
choice = response.choices[0]
assistant_msg = choice.message
if hasattr(assistant_msg, 'tool_calls') and assistant_msg.tool_calls:
# Check if there's assistant_msg.content. If so, add it to the thought
thought = ''
if isinstance(assistant_msg.content, str):
thought = assistant_msg.content
elif isinstance(assistant_msg.content, list):
for msg in assistant_msg.content:
if msg['type'] == 'text':
thought += msg['text']
# Process each tool call to OpenHands action
for i, tool_call in enumerate(assistant_msg.tool_calls):
action: Action
logger.debug(f'Tool call in function_calling.py: {tool_call}')
try:
arguments = json.loads(tool_call.function.arguments)
except json.decoder.JSONDecodeError as e:
raise RuntimeError(
f'Failed to parse tool call arguments: {tool_call.function.arguments}'
) from e
# ================================================
# LocAgent's Tools
# ================================================
ALL_FUNCTIONS = [
'explore_tree_structure',
'search_code_snippets',
'get_entity_contents',
]
if tool_call.function.name in ALL_FUNCTIONS:
# We implement this in agent_skills, which can be used via Jupyter
func_name = tool_call.function.name
code = f'print({func_name}(**{arguments}))'
logger.debug(f'TOOL CALL: {func_name} with code: {code}')
action = IPythonRunCellAction(code=code)
# ================================================
# AgentFinishAction
# ================================================
elif tool_call.function.name == FinishTool['function']['name']:
action = AgentFinishAction(
final_thought=arguments.get('message', ''),
task_completed=arguments.get('task_completed', None),
)
else:
raise FunctionCallNotExistsError(
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
)
# We only add thought to the first action
if i == 0:
action = combine_thought(action, thought)
# Add metadata for tool calling
action.tool_call_metadata = ToolCallMetadata(
tool_call_id=tool_call.id,
function_name=tool_call.function.name,
model_response=response,
total_calls_in_response=len(assistant_msg.tool_calls),
)
actions.append(action)
else:
actions.append(
MessageAction(
content=str(assistant_msg.content) if assistant_msg.content else '',
wait_for_response=True,
)
)
# Add response id to actions
# This will ensure we can match both actions without tool calls (e.g. MessageAction)
# and actions with tool calls (e.g. CmdRunAction, IPythonRunCellAction, etc.)
# with the token usage data
for action in actions:
action.response_id = response.id
assert len(actions) >= 1
return actions
def get_tools() -> list[ChatCompletionToolParam]:
tools = [FinishTool]
tools.append(SearchRepoTool)
tools.append(SearchEntityTool)
tools.append(create_explore_tree_structure_tool(use_simplified_description=True))
return tools
+39
View File
@@ -0,0 +1,39 @@
from openhands.agenthub.codeact_agent import CodeActAgent
import openhands.agenthub.loc_agent.function_calling as locagent_function_calling
from openhands.core.config import AgentConfig
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from openhands.events.action import Action
from openhands.llm.llm import ModelResponse
class LocAgent(CodeActAgent):
VERSION = '1.0'
def __init__(
self,
llm: LLM,
config: AgentConfig,
) -> None:
"""Initializes a new instance of the LocAgent class.
Parameters:
- llm (LLM): The llm to be used by this agent
- config (AgentConfig): The configuration for the agent
"""
super().__init__(llm, config)
self.tools = locagent_function_calling.get_tools()
logger.debug(
f'TOOLS loaded for LocAgent: {", ".join([tool.get("function").get("name") for tool in self.tools])}'
)
def response_to_actions(self, response: 'ModelResponse') -> list['Action']:
return locagent_function_calling.response_to_actions(
response, mcp_tool_names=list(self.mcp_tools.keys()),
)
@@ -0,0 +1,8 @@
from .explore_structure import create_explore_tree_structure_tool
from .search_content import SearchEntityTool, SearchRepoTool
__all__ = [
'SearchEntityTool',
'SearchRepoTool',
'create_explore_tree_structure_tool',
]
@@ -0,0 +1,185 @@
from litellm import (
ChatCompletionToolParam,
ChatCompletionToolParamFunctionChunk,
)
_SIMPLIFIED_STRUCTURE_EXPLORER_DESCRIPTION = """
A unified tool that traverses a pre-built code graph to retrieve dependency structure around specified entities,
with options to explore upstream or downstream, and control traversal depth and filters for entity and dependency types.
"""
_SIMPLIFIED_TREE_EXAMPLE = """
Example Usage:
1. Exploring Downstream Dependencies:
```
explore_tree_structure(
start_entities=['src/module_a.py:ClassA'],
direction='downstream',
traversal_depth=2,
dependency_type_filter=['invokes', 'imports']
)
```
2. Exploring the repository structure from the root directory (/) up to two levels deep:
```
explore_tree_structure(
start_entities=['/'],
traversal_depth=2,
dependency_type_filter=['contains']
)
```
3. Generate Class Diagrams:
```
explore_tree_structure(
start_entities=selected_entity_ids,
direction='both',
traverse_depth=-1,
dependency_type_filter=['inherits']
)
```
"""
_DETAILED_STRUCTURE_EXPLORER_DESCRIPTION = """
Unified repository exploring tool that traverses a pre-built code graph to retrieve dependency structure around specified entities.
The search can be controlled to traverse upstream (exploring dependencies that entities rely on) or downstream (exploring how entities impact others), with optional limits on traversal depth and filters for entity and dependency types.
Code Graph Definition:
* Entity Types: 'directory', 'file', 'class', 'function'.
* Dependency Types: 'contains', 'imports', 'invokes', 'inherits'.
* Hierarchy:
- Directories contain files and subdirectories.
- Files contain classes and functions.
- Classes contain inner classes and methods.
- Functions can contain inner functions.
* Interactions:
- Files/classes/functions can import classes and functions.
- Classes can inherit from other classes.
- Classes and functions can invoke others (invocations in a class's `__init__` are attributed to the class).
Entity ID:
* Unique identifier including file path and module path.
* Here's an example of an Entity ID: `"interface/C.py:C.method_a.inner_func"` identifies function `inner_func` within `method_a` of class `C` in `"interface/C.py"`.
Notes:
* Traversal Control: The `traversal_depth` parameter specifies how deep the function should explore the graph starting from the input entities.
* Filtering: Use `entity_type_filter` and `dependency_type_filter` to narrow down the scope of the search, focusing on specific entity types and relationships.
"""
_DETAILED_TREE_EXAMPLE = """
Example Usage:
1. Exploring Outward Dependencies:
```
explore_tree_structure(
start_entities=['src/module_a.py:ClassA'],
direction='downstream',
traversal_depth=2,
dependency_type_filter=['invokes', 'imports']
)
```
This retrieves the dependencies of `ClassA` up to 2 levels deep, focusing only on classes and functions with 'invokes' and 'imports' relationships.
2. Exploring Inward Dependencies:
```
explore_tree_structure(
start_entities=['src/module_b.py:FunctionY'],
direction='upstream',
traversal_depth=-1
)
```
This finds all entities that depend on `FunctionY` without restricting the traversal depth.
3. Exploring Repository Structure:
```
explore_tree_structure(
start_entities=['/'],
traversal_depth=2,
dependency_type_filter=['contains']
)
```
This retrieves the tree repository structure from the root directory (/), traversing up to two levels deep and focusing only on 'contains' relationship.
4. Generate Class Diagrams:
```
explore_tree_structure(
start_entities=selected_entity_ids,
direction='both',
traverse_depth=-1,
dependency_type_filter=['inherits']
)
```
"""
_STRUCTURE_EXPLORER_PARAMETERS = {
'type': 'object',
'properties': {
'start_entities': {
'description': (
'List of entities (e.g., class, function, file, or directory paths) to begin the search from.\n'
'Entities representing classes or functions must be formatted as "file_path:QualifiedName" (e.g., `interface/C.py:C.method_a.inner_func`).\n'
'For files or directories, provide only the file or directory path (e.g., `src/module_a.py` or `src/`).'
),
'type': 'array',
'items': {'type': 'string'},
},
'direction': {
'description': (
'Direction of traversal in the code graph; allowed options are: `upstream`, `downstream`, `both`.\n'
"- 'upstream': Traversal to explore dependencies that the specified entities rely on (how they depend on others).\n"
"- 'downstream': Traversal to explore the effects or interactions of the specified entities on others (how others depend on them).\n"
"- 'both': Traversal on both direction."
),
'type': 'string',
'enum': ['upstream', 'downstream', 'both'],
'default': 'downstream',
},
'traversal_depth': {
'description': (
'Maximum depth of traversal. A value of -1 indicates unlimited depth (subject to a maximum limit).'
'Must be either `-1` or a non-negative integer (≥ 0).'
),
'type': 'integer',
'default': 2,
},
'entity_type_filter': {
'description': (
"List of entity types (e.g., 'class', 'function', 'file', 'directory') to include in the traversal. If None, all entity types are included."
),
'type': ['array', 'null'],
'items': {'type': 'string'},
'default': None,
},
'dependency_type_filter': {
'description': (
"List of dependency types (e.g., 'contains', 'imports', 'invokes', 'inherits') to include in the traversal. If None, all dependency types are included."
),
'type': ['array', 'null'],
'items': {'type': 'string'},
'default': None,
},
},
'required': ['start_entities'],
}
def create_explore_tree_structure_tool(
use_simplified_description: bool = False,
) -> ChatCompletionToolParam:
description = (
_SIMPLIFIED_STRUCTURE_EXPLORER_DESCRIPTION
if use_simplified_description
else _DETAILED_STRUCTURE_EXPLORER_DESCRIPTION
)
example = (
_SIMPLIFIED_TREE_EXAMPLE
if use_simplified_description
else _DETAILED_TREE_EXAMPLE
)
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='explore_tree_structure',
description=description + example,
parameters=_STRUCTURE_EXPLORER_PARAMETERS,
),
)
@@ -0,0 +1,98 @@
from litellm import (
ChatCompletionToolParam,
ChatCompletionToolParamFunctionChunk,
)
_SEARCH_ENTITY_DESCRIPTION = """
Searches the codebase to retrieve the complete implementations of specified entities based on the provided entity names.
The tool can handle specific entity queries such as function names, class names, or file paths.
**Usage Example:**
# Search for a specific function implementation
get_entity_contents(['src/my_file.py:MyClass.func_name'])
# Search for a file's complete content
get_entity_contents(['src/my_file.py'])
**Entity Name Format:**
- To specify a function or class, use the format: `file_path:QualifiedName`
(e.g., 'src/helpers/math_helpers.py:MathUtils.calculate_sum').
- To search for a file's content, use only the file path (e.g., 'src/my_file.py').
"""
SearchEntityTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='get_entity_contents',
description=_SEARCH_ENTITY_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'entity_names': {
'type': 'array',
'items': {'type': 'string'},
'description': (
'A list of entity names to query. Each entity name can represent a function, class, or file. '
"For functions or classes, the format should be 'file_path:QualifiedName' "
"(e.g., 'src/helpers/math_helpers.py:MathUtils.calculate_sum'). "
"For files, use just the file path (e.g., 'src/my_file.py')."
),
}
},
'required': ['entity_names'],
},
),
)
_SEARCH_REPO_DESCRIPTION = """Searches the codebase to retrieve relevant code snippets based on given queries(terms or line numbers).
** Note:
- Either `search_terms` or `line_nums` must be provided to perform a search.
- If `search_terms` are provided, it searches for code snippets based on each term:
- If `line_nums` is provided, it searches for code snippets around the specified lines within the file defined by `file_path_or_pattern`.
** Example Usage:
# Search for code content contain keyword `order`, `bill`
search_code_snippets(search_terms=["order", "bill"])
# Search for a class
search_code_snippets(search_terms=["MyClass"])
# Search for context around specific lines (10 and 15) within a file
search_code_snippets(line_nums=[10, 15], file_path_or_pattern='src/example.py')
"""
SearchRepoTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='search_code_snippets',
description=_SEARCH_REPO_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'search_terms': {
'type': 'array',
'items': {'type': 'string'},
'description': 'A list of names, keywords, or code snippets to search for within the codebase. '
'This can include potential function names, class names, or general code fragments. '
'Either `search_terms` or `line_nums` must be provided to perform a search.',
},
'line_nums': {
'type': 'array',
'items': {'type': 'integer'},
'description': 'Specific line numbers to locate code snippets within a specified file. '
'Must be used alongside a valid `file_path_or_pattern`. '
'Either `line_nums` or `search_terms` must be provided to perform a search.',
},
'file_path_or_pattern': {
'type': 'string',
'description': 'A glob pattern or specific file path used to filter search results '
'to particular files or directories. Defaults to "**/*.py", meaning all Python files are searched by default. '
'If `line_nums` are provided, this must specify a specific file path.',
'default': '**/*.py',
},
},
'required': [],
},
),
)
+13 -1
View File
@@ -35,6 +35,7 @@ from openhands.core.config import (
setup_config_from_args,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.mcp_config import OpenHandsMCPConfigImpl
from openhands.core.logger import openhands_logger as logger
from openhands.core.loop import run_agent_until_done
from openhands.core.schema import AgentState
@@ -261,7 +262,18 @@ async def run_session(
# Add MCP tools to the agent
if agent.config.enable_mcp:
await add_mcp_tools_to_agent(agent, runtime, memory, config.mcp)
# Add OpenHands' MCP server by default
openhands_mcp_server, openhands_mcp_stdio_servers = (
OpenHandsMCPConfigImpl.create_default_mcp_server_config(
config.mcp_host, config, None
)
)
# FIXME: OpenHands' SSE server may not be running when CLI mode is started
# if openhands_mcp_server:
# config.mcp.sse_servers.append(openhands_mcp_server)
config.mcp.stdio_servers.extend(openhands_mcp_stdio_servers)
await add_mcp_tools_to_agent(agent, runtime, memory, config)
# Clear loading animation
is_loaded.set()
+2
View File
@@ -167,6 +167,8 @@ VERIFIED_ANTHROPIC_MODELS = [
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-7-sonnet-20250219',
'claude-sonnet-4-20250514',
'claude-opus-4-20250514',
]
+7 -3
View File
@@ -189,10 +189,14 @@ class AgentController:
# Add the system message to the event stream
# This should be done for all agents, including delegates
system_message = self.agent.get_system_message()
logger.debug(f'System message got from agent: {system_message}')
if system_message:
if system_message and system_message.content:
preview = (
system_message.content[:50] + '...'
if len(system_message.content) > 50
else system_message.content
)
logger.debug(f'System message: {preview}')
self.event_stream.add_event(system_message, EventSource.AGENT)
logger.info(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.
+8 -4
View File
@@ -32,10 +32,11 @@ class AppConfig(BaseModel):
save_trajectory_path: Either a folder path to store trajectories with auto-generated filenames, or a designated trajectory file path.
save_screenshots_in_trajectory: Whether to save screenshots in trajectory (in encoded image format).
replay_trajectory_path: Path to load trajectory and replay. If provided, trajectory would be replayed first before user's instruction.
workspace_base: Base path for the workspace. Defaults to `./workspace` as absolute path.
workspace_mount_path: Path to mount the workspace. Defaults to `workspace_base`.
workspace_mount_path_in_sandbox: Path to mount the workspace in sandbox. Defaults to `/workspace`.
workspace_mount_rewrite: Path to rewrite the workspace mount path.
search_api_key: API key for Tavily search engine (https://tavily.com/).
workspace_base (deprecated): Base path for the workspace. Defaults to `./workspace` as absolute path.
workspace_mount_path (deprecated): Path to mount the workspace. Defaults to `workspace_base`.
workspace_mount_path_in_sandbox (deprecated): Path to mount the workspace in sandbox. Defaults to `/workspace`.
workspace_mount_rewrite (deprecated): Path to rewrite the workspace mount path.
cache_dir: Path to cache directory. Defaults to `/tmp/cache`.
run_as_openhands: Whether to run as openhands.
max_iterations: Maximum number of iterations allowed.
@@ -64,12 +65,15 @@ class AppConfig(BaseModel):
save_trajectory_path: str | None = Field(default=None)
save_screenshots_in_trajectory: bool = Field(default=False)
replay_trajectory_path: str | None = Field(default=None)
search_api_key: SecretStr | None = Field(default=None, description="API key for Tavily search engine (https://tavily.com/). Required for search functionality.")
# Deprecated parameters - will be removed in a future version
workspace_base: str | None = Field(default=None, deprecated=True)
workspace_mount_path: str | None = Field(default=None, deprecated=True)
workspace_mount_path_in_sandbox: str = Field(default='/workspace', deprecated=True)
workspace_mount_rewrite: str | None = Field(default=None, deprecated=True)
# End of deprecated parameters
cache_dir: str = Field(default='/tmp/cache')
run_as_openhands: bool = Field(default=True)
max_iterations: int = Field(default=OH_MAX_ITERATIONS)
+1 -1
View File
@@ -47,7 +47,7 @@ class LLMConfig(BaseModel):
seed: The seed to use for the LLM.
"""
model: str = Field(default='claude-3-7-sonnet-20250219')
model: str = Field(default='claude-sonnet-4-20250514')
api_key: SecretStr | None = Field(default=None)
base_url: str | None = Field(default=None)
api_version: str | None = Field(default=None)
+35 -7
View File
@@ -1,8 +1,12 @@
import os
from urllib.parse import urlparse
from typing import TYPE_CHECKING
from pydantic import BaseModel, Field, ValidationError, model_validator
if TYPE_CHECKING:
from openhands.core.config.app_config import AppConfig
from openhands.core.logger import openhands_logger as logger
from openhands.utils.import_utils import get_impl
@@ -142,21 +146,45 @@ class MCPConfig(BaseModel):
class OpenHandsMCPConfig:
@staticmethod
def add_search_engine(app_config: "AppConfig") -> MCPStdioServerConfig | None:
"""Add search engine to the MCP config"""
if (
app_config.search_api_key
and app_config.search_api_key.get_secret_value().startswith('tvly-')
):
logger.info('Adding search engine to MCP config')
return MCPStdioServerConfig(
name='tavily',
command='npx',
args=['-y', 'tavily-mcp@0.1.4'],
env={'TAVILY_API_KEY': app_config.search_api_key.get_secret_value()},
)
else:
logger.warning('No search engine API key found, skipping search engine')
# Do not add search engine to MCP config in SaaS mode since it will be added by the OpenHands server
return None
@staticmethod
def create_default_mcp_server_config(
host: str, user_id: str | None = None
) -> MCPSSEServerConfig | None:
host: str, config: "AppConfig", user_id: str | None = None
) -> tuple[MCPSSEServerConfig, list[MCPStdioServerConfig]]:
"""
Create a default MCP server configuration.
Args:
host: Host string
config: AppConfig
Returns:
MCPSSEServerConfig: A default SSE server configuration
tuple[MCPSSEServerConfig, list[MCPStdioServerConfig]]: A tuple containing the default SSE server configuration and a list of MCP stdio server configurations
"""
return MCPSSEServerConfig(url=f'http://{host}/mcp/sse', api_key=None)
sse_server = MCPSSEServerConfig(url=f'http://{host}/mcp/sse', api_key=None)
stdio_servers = []
search_engine_stdio_server = OpenHandsMCPConfig.add_search_engine(config)
if search_engine_stdio_server:
stdio_servers.append(search_engine_stdio_server)
return sse_server, stdio_servers
openhands_mcp_config_cls = os.environ.get(
+5 -5
View File
@@ -305,6 +305,11 @@ def get_or_create_jwt_secret(file_store: FileStore) -> str:
def finalize_config(cfg: AppConfig) -> None:
"""More tweaks to the config after it's been loaded."""
# Handle the sandbox.volumes parameter
if cfg.workspace_base is not None or cfg.workspace_mount_path is not None:
logger.openhands_logger.warning(
'DEPRECATED: The WORKSPACE_BASE and WORKSPACE_MOUNT_PATH environment variables are deprecated. '
"Please use RUNTIME_MOUNT instead, e.g. 'RUNTIME_MOUNT=/my/host/dir:/workspace:rw'"
)
if cfg.sandbox.volumes is not None:
# Split by commas to handle multiple mounts
mounts = cfg.sandbox.volumes.split(',')
@@ -348,11 +353,6 @@ def finalize_config(cfg: AppConfig) -> None:
# Handle the deprecated workspace_* parameters
elif cfg.workspace_base is not None or cfg.workspace_mount_path is not None:
logger.openhands_logger.warning(
'DEPRECATED: The WORKSPACE_BASE and WORKSPACE_MOUNT_PATH environment variables are deprecated. '
"Please use RUNTIME_MOUNT instead, e.g. 'RUNTIME_MOUNT=/my/host/dir:/workspace:rw'"
)
if cfg.workspace_base is not None:
cfg.workspace_base = os.path.abspath(cfg.workspace_base)
if cfg.workspace_mount_path is None:

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