mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d882ff5f0 | |||
| f876bbe76a | |||
| 955e201e2c | |||
| 78c5f5578b | |||
| 815cc8e660 |
@@ -1,15 +0,0 @@
|
||||
// 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",
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Install `nc`
|
||||
sudo apt update && sudo apt install netcat -y
|
||||
|
||||
# Do common setup tasks
|
||||
source .openhands/setup.sh
|
||||
@@ -1,5 +0,0 @@
|
||||
[*]
|
||||
# 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
|
||||
@@ -1,7 +1 @@
|
||||
*.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
|
||||
|
||||
@@ -24,7 +24,7 @@ on:
|
||||
LLM_MODEL:
|
||||
required: false
|
||||
type: string
|
||||
default: "anthropic/claude-sonnet-4-20250514"
|
||||
default: "anthropic/claude-3-7-sonnet-20250219"
|
||||
LLM_API_VERSION:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
+1
-8
@@ -161,16 +161,9 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
.vscode/
|
||||
.cursorignore
|
||||
|
||||
# VS Code: Ignore all but certain files that specify repo-specific settings.
|
||||
# https://stackoverflow.com/questions/32964920/should-i-commit-the-vscode-folder-to-source-control
|
||||
.vscode/**/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
|
||||
# evaluation
|
||||
evaluation/evaluation_outputs
|
||||
evaluation/outputs
|
||||
|
||||
@@ -9,5 +9,4 @@ python -m pip install pre-commit
|
||||
if [ -d ".git" ]; then
|
||||
echo "Installing pre-commit hooks..."
|
||||
pre-commit install
|
||||
make install-pre-commit-hooks
|
||||
fi
|
||||
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
// 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,
|
||||
}
|
||||
+10
-23
@@ -1,10 +1,8 @@
|
||||
# Development Guide
|
||||
|
||||
This guide is for people working on OpenHands and editing the source code.
|
||||
If you wish to contribute your changes, check out the
|
||||
[CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md)
|
||||
on how to clone and setup the project initially before moving on. Otherwise,
|
||||
you can clone the OpenHands project directly.
|
||||
If you wish to contribute your changes, check out the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) on how to clone and setup the project
|
||||
initially before moving on. Otherwise, you can clone the OpenHands project directly.
|
||||
|
||||
## Start the Server for Development
|
||||
|
||||
@@ -21,20 +19,9 @@ you can clone the OpenHands project directly.
|
||||
|
||||
Make sure you have all these dependencies installed before moving on to `make build`.
|
||||
|
||||
#### Dev container
|
||||
|
||||
There is a [dev container](https://containers.dev/) available which provides a
|
||||
pre-configured environment with all the necessary dependencies installed if you
|
||||
are using a [supported editor or tool](https://containers.dev/supporting). For
|
||||
example, if you are using Visual Studio Code (VS Code) with the
|
||||
[Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
extension installed, you can open the project in a dev container by using the
|
||||
_Dev Container: Reopen in Container_ command from the Command Palette
|
||||
(Ctrl+Shift+P).
|
||||
|
||||
#### Develop without sudo access
|
||||
|
||||
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use
|
||||
If you want to develop without system admin/sudo access to upgrade/install `Python` and/or `NodeJs`, you can use
|
||||
`conda` or `mamba` to manage the packages for you:
|
||||
|
||||
```bash
|
||||
@@ -50,7 +37,7 @@ mamba install conda-forge::poetry
|
||||
|
||||
### 2. Build and Setup The Environment
|
||||
|
||||
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures
|
||||
Begin by building the project which includes setting up the environment and installing dependencies. This step ensures
|
||||
that OpenHands is ready to run on your system:
|
||||
|
||||
```bash
|
||||
@@ -67,11 +54,11 @@ To configure the LM of your choice, run:
|
||||
make setup-config
|
||||
```
|
||||
|
||||
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenHands is
|
||||
tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI,
|
||||
This command will prompt you to enter the LLM API key, model name, and other variables ensuring that OpenHands is
|
||||
tailored to your specific needs. Note that the model name will apply only when you run headless. If you use the UI,
|
||||
please set the model in the UI.
|
||||
|
||||
Note: If you have previously run OpenHands using the docker command, you may have already set some environmental
|
||||
Note: If you have previously run OpenHands using the docker command, you may have already set some environmental
|
||||
variables in your terminal. The final configurations are set from highest to lowest priority:
|
||||
Environment variables > config.toml variables > default variables
|
||||
|
||||
@@ -90,14 +77,14 @@ make run
|
||||
|
||||
#### Option B: Individual Server Startup
|
||||
|
||||
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on
|
||||
- **Start the Backend Server:** If you prefer, you can start the backend server independently to focus on
|
||||
backend-related tasks or configurations.
|
||||
|
||||
```bash
|
||||
make start-backend
|
||||
```
|
||||
|
||||
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related
|
||||
- **Start the Frontend Server:** Similarly, you can start the frontend server on its own to work on frontend-related
|
||||
components or interface enhancements.
|
||||
```bash
|
||||
make start-frontend
|
||||
@@ -133,7 +120,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
|
||||
### 9. Use existing Docker image
|
||||
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
|
||||
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.39-nikolaik`
|
||||
|
||||
@@ -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 Sonnet 4](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-20250514`)
|
||||
[Anthropic's Claude 3.7 Sonnet](https://www.anthropic.com/api) (`anthropic/claude-3-7-sonnet-20250219`)
|
||||
works best, but you have [many options](https://docs.all-hands.dev/modules/usage/llms).
|
||||
|
||||
## 💡 Other ways to run OpenHands
|
||||
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
|
||||
<h1 align="center">OpenHands: 少写代码,多做事</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
|
||||
<br/>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="加入我们的Discord社区"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="致谢"></a>
|
||||
<br/>
|
||||
<a href="https://docs.all-hands.dev/modules/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="查看文档"></a>
|
||||
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv论文"></a>
|
||||
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="评估基准分数"></a>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
欢迎使用OpenHands(前身为OpenDevin),这是一个由AI驱动的软件开发代理平台。
|
||||
|
||||
OpenHands代理可以完成人类开发者能做的任何事情:修改代码、运行命令、浏览网页、调用API,甚至从StackOverflow复制代码片段。
|
||||
|
||||
在[docs.all-hands.dev](https://docs.all-hands.dev)了解更多信息,或[注册OpenHands Cloud](https://app.all-hands.dev)开始使用。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 在工作中使用OpenHands?我们很想与您交流!填写
|
||||
> [这份简短表格](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
> 加入我们的设计合作伙伴计划,您将获得商业功能的早期访问权限,并有机会对我们的产品路线图提供意见。
|
||||
|
||||

|
||||
|
||||
## ☁️ OpenHands Cloud
|
||||
开始使用OpenHands的最简单方式是在[OpenHands Cloud](https://app.all-hands.dev)上,
|
||||
新用户可获得$50的免费额度。
|
||||
|
||||
## 💻 在本地运行OpenHands
|
||||
|
||||
OpenHands也可以使用Docker在本地系统上运行。
|
||||
查看[运行OpenHands](https://docs.all-hands.dev/modules/usage/installation)指南了解
|
||||
系统要求和更多信息。
|
||||
|
||||
> [!WARNING]
|
||||
> 在公共网络上?请参阅我们的[强化Docker安装指南](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation)
|
||||
> 通过限制网络绑定和实施其他安全措施来保护您的部署。
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.39-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.39-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands-state:/.openhands-state \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.39
|
||||
```
|
||||
|
||||
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands!
|
||||
|
||||
打开应用程序时,您将被要求选择一个LLM提供商并添加API密钥。
|
||||
[Anthropic的Claude Sonnet 4](https://www.anthropic.com/api)(`anthropic/claude-sonnet-4-20250514`)
|
||||
效果最佳,但您还有[许多选择](https://docs.all-hands.dev/modules/usage/llms)。
|
||||
|
||||
## 💡 运行OpenHands的其他方式
|
||||
|
||||
> [!CAUTION]
|
||||
> OpenHands旨在由单个用户在其本地工作站上运行。
|
||||
> 它不适合多租户部署,即多个用户共享同一实例。没有内置的身份验证、隔离或可扩展性。
|
||||
>
|
||||
> 如果您有兴趣在多租户环境中运行OpenHands,请
|
||||
> [与我们联系](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
> 了解高级部署选项。
|
||||
|
||||
您还可以[将OpenHands连接到本地文件系统](https://docs.all-hands.dev/modules/usage/runtimes/docker#connecting-to-your-filesystem),
|
||||
以可编程的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)运行OpenHands,
|
||||
通过[友好的CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode)与其交互,
|
||||
或使用[GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)在标记的问题上运行它。
|
||||
|
||||
访问[运行OpenHands](https://docs.all-hands.dev/modules/usage/installation)获取更多信息和设置说明。
|
||||
|
||||
如果您想修改OpenHands源代码,请查看[Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)。
|
||||
|
||||
遇到问题?[故障排除指南](https://docs.all-hands.dev/modules/usage/troubleshooting)可以提供帮助。
|
||||
|
||||
## 📖 文档
|
||||
<a href="https://deepwiki.com/All-Hands-AI/OpenHands"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" title="DeepWiki自动生成文档"></a>
|
||||
|
||||
要了解有关项目的更多信息,以及使用OpenHands的技巧,
|
||||
请查看我们的[文档](https://docs.all-hands.dev/modules/usage/getting-started)。
|
||||
|
||||
在那里,您将找到有关如何使用不同LLM提供商、
|
||||
故障排除资源和高级配置选项的资源。
|
||||
|
||||
## 🤝 如何加入社区
|
||||
|
||||
OpenHands是一个社区驱动的项目,我们欢迎每个人的贡献。我们大部分沟通
|
||||
通过Slack进行,因此这是开始的最佳场所,但我们也很乐意您通过Discord或Github与我们联系:
|
||||
|
||||
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - 这里我们讨论研究、架构和未来发展。
|
||||
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
|
||||
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。
|
||||
|
||||
在[COMMUNITY.md](./COMMUNITY.md)中了解更多关于社区的信息,或在[CONTRIBUTING.md](./CONTRIBUTING.md)中找到有关贡献的详细信息。
|
||||
|
||||
## 📈 进展
|
||||
|
||||
在[这里](https://github.com/orgs/All-Hands-AI/projects/1)查看OpenHands月度路线图(每月月底在维护者会议上更新)。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://star-history.com/#All-Hands-AI/OpenHands&Date">
|
||||
<img src="https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date" width="500" alt="Star History Chart">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 📜 许可证
|
||||
|
||||
根据MIT许可证分发。有关更多信息,请参阅[`LICENSE`](./LICENSE)。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
OpenHands由大量贡献者构建,每一份贡献都备受感谢!我们还借鉴了其他开源项目,对他们的工作深表感谢。
|
||||
|
||||
有关OpenHands中使用的开源项目和许可证列表,请参阅我们的[CREDITS.md](./CREDITS.md)文件。
|
||||
|
||||
## 📚 引用
|
||||
|
||||
```
|
||||
@misc{openhands,
|
||||
title={{OpenHands: An Open Platform for AI Software Developers as Generalist Agents}},
|
||||
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
|
||||
year={2024},
|
||||
eprint={2407.16741},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.SE},
|
||||
url={https://arxiv.org/abs/2407.16741},
|
||||
}
|
||||
```
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
[ ] Rename `Conversation` in openhands/server to `ServerConversation`
|
||||
[ ] Replace all instances of `sid` in openhands/* to `conversation_id`
|
||||
[ ] Make EventStream take in a `conversation_id` in its constructor.
|
||||
* remove `conversation_id` from all methods on EventStream and use self.conversation_id instead.
|
||||
* fix all callers of EventStream to pass in `conversation_id` in the constructor and remove it from the method calls.
|
||||
[ ] Rename AppConfig to OpenHandsConfig
|
||||
[ ] Create a new class `Conversation` in openhands/core/ that will be the main interface for conversations.
|
||||
* Its constructor will take in a:
|
||||
* conversation_id (string)
|
||||
* Runtime
|
||||
* LLM
|
||||
* EventStream
|
||||
* AgentController
|
||||
* No logic, it's just a dataclass
|
||||
[ ] Add a new OpenHands class to openhands/core/ which will take care of creating Conversations
|
||||
* Constructor is ONLY an OpenHandsConfig
|
||||
* Only one method: `create_conversation()`
|
||||
* This will create a Runtime, LLM, EventStream, and AgentController, and return a Conversation object.
|
||||
* These objects will be created according to the OpenHandsConfig passed in to the constructor.
|
||||
@@ -325,15 +325,6 @@ classpath = "my_package.my_module.MyCustomAgent"
|
||||
# Useful when deploying OpenHands in a remote machine where you need to expose a specific port.
|
||||
#vscode_port = 41234
|
||||
|
||||
# Volume mounts in the format 'host_path:container_path[:mode]'
|
||||
# e.g. '/my/host/dir:/workspace:rw'
|
||||
# Multiple mounts can be specified using commas
|
||||
# e.g. '/path1:/workspace/path1,/path2:/workspace/path2:ro'
|
||||
|
||||
# Configure volumes under the [sandbox] section:
|
||||
# [sandbox]
|
||||
# volumes = "/my/host/dir:/workspace:rw,/path2:/workspace/path2:ro"
|
||||
|
||||
#################################### Security ###################################
|
||||
# Configuration for security features
|
||||
##############################################################################
|
||||
|
||||
+1
-1
@@ -52,4 +52,4 @@ $ poetry run python docs/translation_updater.py
|
||||
# ...
|
||||
```
|
||||
|
||||
This process uses `claude-sonnet-4-20250514` as base model and each language consumes at least ~30k input tokens and ~35k output tokens.
|
||||
This process uses `claude-3-7-sonnet-20250219` 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-sonnet-4-20250514](https://www.anthropic.com/api) (recommandé)
|
||||
- [anthropic/claude-3-7-sonnet-20250219](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-sonnet-4-20250514](https://www.anthropic.com/api) (推奨)
|
||||
- [anthropic/claude-3-7-sonnet-20250219](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-sonnet-4-20250514](https://www.anthropic.com/api) (recomendado)
|
||||
- [anthropic/claude-3-7-sonnet-20250219](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-sonnet-4-20250514](https://www.anthropic.com/api)(推荐)
|
||||
- [anthropic/claude-3-7-sonnet-20250219](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,84 +0,0 @@
|
||||
# Using OpenHands as a library
|
||||
|
||||
|
||||
## Hello World
|
||||
```python
|
||||
import asyncio
|
||||
from openhands.core.config import OpenHandsConfig, LLMConfig, AgentConfig
|
||||
from openhands.core.setup import run_agent
|
||||
|
||||
async def run_openhands_agent():
|
||||
final_state = await run_agent(
|
||||
config=OpenHandsConfig(
|
||||
llm=LLMConfig(
|
||||
model="claude-sonnet-4-20250514",
|
||||
api_key="your_api_key_here", # Replace with your actual API key
|
||||
),
|
||||
),
|
||||
initial_user_message="Flip a coin",
|
||||
context_message="You build simple programs and run them.",
|
||||
)
|
||||
|
||||
return final_state
|
||||
|
||||
# Run the async function
|
||||
if __name__ == "__main__":
|
||||
final_state = asyncio.run(run_openhands_agent())
|
||||
print("Agent execution completed!")
|
||||
```
|
||||
|
||||
## Using the internals
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.core.config import OpenHandsConfig, LLMConfig, AgentConfig
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.core.setup import (
|
||||
create_runtime,
|
||||
create_memory,
|
||||
generate_sid,
|
||||
)
|
||||
from openhands.core.main import run_controller
|
||||
|
||||
async def run_openhands_agent():
|
||||
config = OpenHandsConfig(
|
||||
runtime="local",
|
||||
file_store="memory",
|
||||
llm=LLMConfig(
|
||||
model="claude-sonnet-4-20250514", # Choose your preferred model
|
||||
api_key="your_api_key_here", # Replace with your actual API key
|
||||
temperature=0.0, # Set temperature to 0 for deterministic output
|
||||
),
|
||||
agent=AgentConfig(
|
||||
enable_browsing=False,
|
||||
),
|
||||
)
|
||||
|
||||
oh = OpenHands(config=config)
|
||||
|
||||
conversation = oh.create_conversation(
|
||||
conversation_id='hello-world',
|
||||
)
|
||||
await conversation.runtime.connect()
|
||||
|
||||
def on_event(event: Event) -> None:
|
||||
print(f"Event received: {event}")
|
||||
conversation.event_stream.subscribe(EventStreamSubscriber.MAIN, on_event)
|
||||
|
||||
initial_user_action = MessageAction(content="Flip a coin")
|
||||
conversation.event_stream.add_event(initial_user_action, EventSource.USER)
|
||||
|
||||
while conversation.state.agent_state not in end_states:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await runtime.close()
|
||||
|
||||
return conversation.state
|
||||
|
||||
# Run the async function
|
||||
if __name__ == "__main__":
|
||||
final_state = asyncio.run(run_openhands_agent())
|
||||
print("Agent execution completed!")
|
||||
```
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"items": ["python/python", "python/using-openhands-as-library"],
|
||||
"items": ["python/python"],
|
||||
"label": "Backend",
|
||||
"type": "category"
|
||||
}
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
# Using OpenHands as a Library
|
||||
|
||||
OpenHands can be used as a Python library in your own applications. This guide will show you how to integrate OpenHands into your Python projects, allowing you to build custom applications that leverage OpenHands' powerful agent capabilities.
|
||||
|
||||
## Installation
|
||||
|
||||
First, install the OpenHands library from PyPI:
|
||||
|
||||
```bash
|
||||
pip install openhands-ai
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Here's a simple example of how to use OpenHands in your Python code:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.core.config import AppConfig, LLMConfig, AgentConfig
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.core.setup import (
|
||||
create_runtime,
|
||||
create_memory,
|
||||
generate_sid,
|
||||
)
|
||||
from openhands.core.main import run_controller
|
||||
|
||||
async def run_openhands_agent():
|
||||
# 1. Create configuration
|
||||
config = AppConfig(
|
||||
runtime="local", # Use local runtime
|
||||
file_store="memory", # Store events in memory
|
||||
)
|
||||
|
||||
# 2. Configure LLM
|
||||
llm_config = LLMConfig(
|
||||
model="claude-sonnet-4-20250514", # Choose your preferred model
|
||||
api_key="your_api_key_here", # Replace with your actual API key
|
||||
temperature=0.0,
|
||||
)
|
||||
config.set_llm_config(llm_config)
|
||||
|
||||
# 3. Configure Agent
|
||||
agent_config = AgentConfig(
|
||||
enable_browsing=False, # Disable browsing for this example
|
||||
)
|
||||
config.set_agent_config(agent_config)
|
||||
|
||||
# 4. Create Agent
|
||||
agent = Agent(
|
||||
llm=LLM(config=llm_config),
|
||||
config=agent_config,
|
||||
)
|
||||
|
||||
# 5. Generate a session ID
|
||||
sid = generate_sid(config)
|
||||
|
||||
# 6. Create Runtime
|
||||
runtime = create_runtime(
|
||||
config=config,
|
||||
sid=sid,
|
||||
headless_mode=True,
|
||||
agent=agent,
|
||||
)
|
||||
|
||||
# 7. Connect to the runtime
|
||||
await runtime.connect()
|
||||
|
||||
# 8. Create Memory
|
||||
memory = create_memory(
|
||||
runtime=runtime,
|
||||
event_stream=runtime.event_stream,
|
||||
sid=sid,
|
||||
)
|
||||
|
||||
# 9. Define the initial task
|
||||
initial_user_action = MessageAction(content="Write a Python function that calculates the factorial of a number")
|
||||
|
||||
# 10. Run the agent
|
||||
final_state = await run_controller(
|
||||
config=config,
|
||||
initial_user_action=initial_user_action,
|
||||
sid=sid,
|
||||
runtime=runtime,
|
||||
agent=agent,
|
||||
memory=memory,
|
||||
headless_mode=True,
|
||||
exit_on_message=True, # Exit when the agent asks for user input
|
||||
)
|
||||
|
||||
# 11. Close the runtime
|
||||
await runtime.close()
|
||||
|
||||
return final_state
|
||||
|
||||
# Run the async function
|
||||
if __name__ == "__main__":
|
||||
final_state = asyncio.run(run_openhands_agent())
|
||||
print("Agent execution completed!")
|
||||
```
|
||||
|
||||
## Components Overview
|
||||
|
||||
### AppConfig
|
||||
|
||||
The `AppConfig` class is the main configuration object for OpenHands. It contains settings for the runtime, agent, LLM, and more.
|
||||
|
||||
```python
|
||||
from openhands.core.config import AppConfig
|
||||
|
||||
config = AppConfig(
|
||||
runtime="local", # Options: "local", "docker", "e2b", "modal", etc.
|
||||
file_store="memory", # Options: "memory", "local", etc.
|
||||
file_store_path="/path/to/store", # Only needed for "local" file_store
|
||||
max_iterations=100, # Maximum number of agent iterations
|
||||
)
|
||||
```
|
||||
|
||||
### LLMConfig
|
||||
|
||||
The `LLMConfig` class configures the language model used by the agent.
|
||||
|
||||
```python
|
||||
from openhands.core.config import LLMConfig
|
||||
|
||||
llm_config = LLMConfig(
|
||||
model="claude-sonnet-4-20250514", # Model name
|
||||
api_key="your_api_key_here", # API key
|
||||
temperature=0.0, # Temperature for generation
|
||||
max_output_tokens=4096, # Maximum tokens in the response
|
||||
)
|
||||
```
|
||||
|
||||
### AgentConfig
|
||||
|
||||
The `AgentConfig` class configures the agent's behavior and available tools.
|
||||
|
||||
```python
|
||||
from openhands.core.config import AgentConfig
|
||||
|
||||
agent_config = AgentConfig(
|
||||
enable_browsing=True, # Enable web browsing
|
||||
enable_cmd=True, # Enable bash commands
|
||||
enable_editor=True, # Enable file editing
|
||||
enable_jupyter=True, # Enable Jupyter notebook
|
||||
enable_think=True, # Enable thinking tool
|
||||
enable_finish=True, # Enable finish tool
|
||||
)
|
||||
```
|
||||
|
||||
### Agent
|
||||
|
||||
The `Agent` class represents the AI agent that will perform tasks.
|
||||
|
||||
```python
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.llm.llm import LLM
|
||||
|
||||
agent = Agent(
|
||||
llm=LLM(config=llm_config),
|
||||
config=agent_config,
|
||||
)
|
||||
```
|
||||
|
||||
### Runtime
|
||||
|
||||
The runtime is the environment where the agent executes commands and interacts with the system.
|
||||
|
||||
```python
|
||||
from openhands.core.setup import create_runtime
|
||||
|
||||
runtime = create_runtime(
|
||||
config=config,
|
||||
sid=sid,
|
||||
headless_mode=True,
|
||||
agent=agent,
|
||||
)
|
||||
```
|
||||
|
||||
### Memory
|
||||
|
||||
The memory component manages the agent's context and conversation history.
|
||||
|
||||
```python
|
||||
from openhands.core.setup import create_memory
|
||||
|
||||
memory = create_memory(
|
||||
runtime=runtime,
|
||||
event_stream=runtime.event_stream,
|
||||
sid=sid,
|
||||
)
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Sandbox Configuration
|
||||
|
||||
You can customize the sandbox environment by configuring the `SandboxConfig`:
|
||||
|
||||
```python
|
||||
from openhands.core.config import SandboxConfig
|
||||
|
||||
sandbox_config = SandboxConfig(
|
||||
selected_repo="username/repo", # GitHub repository to clone
|
||||
base_image="ubuntu:22.04", # Base Docker image
|
||||
)
|
||||
config.sandbox = sandbox_config
|
||||
```
|
||||
|
||||
### Security Configuration
|
||||
|
||||
Configure security settings using the `SecurityConfig`:
|
||||
|
||||
```python
|
||||
from openhands.core.config import SecurityConfig
|
||||
|
||||
security_config = SecurityConfig(
|
||||
confirmation_mode=False, # Whether to require confirmation for actions
|
||||
security_analyzer="default", # Security analyzer to use
|
||||
)
|
||||
config.security = security_config
|
||||
```
|
||||
|
||||
### Custom Agent Response Handling
|
||||
|
||||
You can provide a custom function to handle agent responses:
|
||||
|
||||
```python
|
||||
def custom_response_handler(state):
|
||||
# Process the agent's state and generate a response
|
||||
return "Continue with your current approach"
|
||||
|
||||
final_state = await run_controller(
|
||||
config=config,
|
||||
initial_user_action=initial_user_action,
|
||||
fake_user_response_fn=custom_response_handler,
|
||||
)
|
||||
```
|
||||
|
||||
## Building a Complete Application
|
||||
|
||||
Here's an example of a more complete application that uses OpenHands to assist with code generation:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.core.config import AppConfig, LLMConfig, AgentConfig, SandboxConfig
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.core.setup import create_runtime, create_memory, generate_sid
|
||||
from openhands.core.main import run_controller
|
||||
from openhands.events import EventStreamSubscriber
|
||||
from openhands.events.observation import AgentStateChangedObservation
|
||||
from openhands.core.schema import AgentState
|
||||
|
||||
class CodeAssistant:
|
||||
def __init__(self, api_key, model="claude-sonnet-4-20250514"):
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.config = None
|
||||
self.agent = None
|
||||
self.runtime = None
|
||||
self.memory = None
|
||||
self.sid = None
|
||||
self.event_stream = None
|
||||
|
||||
async def initialize(self):
|
||||
# Create configuration
|
||||
self.config = AppConfig(
|
||||
runtime="docker",
|
||||
file_store="memory",
|
||||
)
|
||||
|
||||
# Configure LLM
|
||||
llm_config = LLMConfig(
|
||||
model=self.model,
|
||||
api_key=self.api_key,
|
||||
temperature=0.0,
|
||||
)
|
||||
self.config.set_llm_config(llm_config)
|
||||
|
||||
# Configure Agent
|
||||
agent_config = AgentConfig(
|
||||
enable_browsing=True,
|
||||
enable_cmd=True,
|
||||
enable_editor=True,
|
||||
enable_jupyter=True,
|
||||
)
|
||||
self.config.set_agent_config(agent_config)
|
||||
|
||||
# Configure Sandbox
|
||||
sandbox_config = SandboxConfig(
|
||||
base_image="ubuntu:22.04",
|
||||
)
|
||||
self.config.sandbox = sandbox_config
|
||||
|
||||
# Create Agent
|
||||
self.agent = Agent(
|
||||
llm=LLM(config=llm_config),
|
||||
config=agent_config,
|
||||
)
|
||||
|
||||
# Generate a session ID
|
||||
self.sid = generate_sid(self.config)
|
||||
|
||||
# Create Runtime
|
||||
self.runtime = create_runtime(
|
||||
config=self.config,
|
||||
sid=self.sid,
|
||||
headless_mode=True,
|
||||
agent=self.agent,
|
||||
)
|
||||
|
||||
# Connect to the runtime
|
||||
await self.runtime.connect()
|
||||
|
||||
# Create Memory
|
||||
self.memory = create_memory(
|
||||
runtime=self.runtime,
|
||||
event_stream=self.runtime.event_stream,
|
||||
sid=self.sid,
|
||||
)
|
||||
|
||||
self.event_stream = self.runtime.event_stream
|
||||
|
||||
async def run_task(self, task_description, callback=None):
|
||||
# Define the initial task
|
||||
initial_user_action = MessageAction(content=task_description)
|
||||
|
||||
# Set up event callback if provided
|
||||
if callback:
|
||||
def on_event(event):
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
callback(event)
|
||||
|
||||
self.event_stream.subscribe(
|
||||
EventStreamSubscriber.MAIN,
|
||||
on_event,
|
||||
self.sid
|
||||
)
|
||||
|
||||
# Run the agent
|
||||
final_state = await run_controller(
|
||||
config=self.config,
|
||||
initial_user_action=initial_user_action,
|
||||
sid=self.sid,
|
||||
runtime=self.runtime,
|
||||
agent=self.agent,
|
||||
memory=self.memory,
|
||||
headless_mode=True,
|
||||
exit_on_message=True,
|
||||
)
|
||||
|
||||
return final_state
|
||||
|
||||
async def close(self):
|
||||
if self.runtime:
|
||||
await self.runtime.close()
|
||||
|
||||
# Example usage
|
||||
async def main():
|
||||
# Initialize the code assistant
|
||||
assistant = CodeAssistant(api_key=os.environ.get("ANTHROPIC_API_KEY"))
|
||||
await assistant.initialize()
|
||||
|
||||
# Define a callback to process events
|
||||
def event_callback(event):
|
||||
if isinstance(event, AgentStateChangedObservation):
|
||||
print(f"Agent state changed to: {event.agent_state}")
|
||||
|
||||
# Run a task
|
||||
task = """
|
||||
Create a simple Flask API with the following endpoints:
|
||||
1. GET /users - Returns a list of users
|
||||
2. GET /users/{id} - Returns a specific user
|
||||
3. POST /users - Creates a new user
|
||||
|
||||
Use SQLite as the database and implement proper error handling.
|
||||
"""
|
||||
|
||||
final_state = await assistant.run_task(task, callback=event_callback)
|
||||
|
||||
# Close the assistant
|
||||
await assistant.close()
|
||||
|
||||
print("Task completed!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Using OpenHands as a library gives you the flexibility to integrate AI agents into your own applications. You can customize the agent's behavior, runtime environment, and how it interacts with your application.
|
||||
|
||||
For more advanced usage, refer to the OpenHands source code and API documentation. The library is highly customizable and can be adapted to a wide range of use cases.
|
||||
@@ -331,8 +331,6 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
|
||||
|
||||
The sandbox configuration options are defined in the `[sandbox]` section of the `config.toml` file.
|
||||
|
||||
|
||||
|
||||
To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-e SANDBOX_TIMEOUT`.
|
||||
|
||||
### Execution
|
||||
|
||||
@@ -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-sonnet-4-20250514"`)
|
||||
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-3-7-sonnet-20250219"`)
|
||||
- `LLM_API_KEY` - your API key (e.g. `export LLM_API_KEY="sk_test_12345"`)
|
||||
|
||||
2. Run the following command:
|
||||
|
||||
@@ -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.cli.main",
|
||||
"module": "openhands.core.cli",
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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-sonnet-4-20250514"`)
|
||||
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-3-7-sonnet-20250219"`)
|
||||
- `LLM_API_KEY` - your API key (e.g. `export LLM_API_KEY="sk_test_12345"`)
|
||||
|
||||
2. Run the following Docker command:
|
||||
|
||||
@@ -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-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
|
||||
- [anthropic/claude-3-7-sonnet-20250219](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/)
|
||||
|
||||
@@ -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-sonnet-4-20250514',
|
||||
model='claude-3-7-sonnet-20250219',
|
||||
max_tokens=4096,
|
||||
temperature=0,
|
||||
system=system_prompt,
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
This folder contains the evaluation harness that we built on top of the original [SWE-Bench benchmark](https://www.swebench.com/) ([paper](https://arxiv.org/abs/2310.06770)).
|
||||
|
||||
**UPDATE (5/26/2025): We now support running interactive SWE-Bench evaluation (see the paper [here](https://arxiv.org/abs/2502.13069))! For how to run it, checkout [this README](./SWE-Interact.md).**
|
||||
|
||||
**UPDATE (4/8/2025): We now support running SWT-Bench evaluation! For more details, checkout [the corresponding section](#SWT-Bench-Evaluation).**
|
||||
|
||||
**UPDATE (03/27/2025): We now support SWE-Bench multimodal evaluation! Simply use "princeton-nlp/SWE-bench_Multimodal" as the dataset name in the `run_infer.sh` script to evaluate on multimodal instances.**
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# SWE-Interact Benchmark
|
||||
|
||||
This document explains how to use the [Interactive SWE-Bench](https://arxiv.org/abs/2502.13069) benchmark scripts for running and evaluating interactive software engineering tasks.
|
||||
|
||||
## Setting things up
|
||||
After following the [README](./README.md) to set up the environment, you would need to additionally add LLM configurations for simulated human users. In the original [paper](https://arxiv.org/abs/2502.13069), we use gpt-4o as the simulated human user. You can add the following to your `config.toml` file:
|
||||
|
||||
```toml
|
||||
[llm.fake_user]
|
||||
model="litellm_proxy/gpt-4o-2024-08-06"
|
||||
api_key="<your-api-key>"
|
||||
temperature = 0.0
|
||||
base_url = "https://llm-proxy.eval.all-hands.dev"
|
||||
```
|
||||
|
||||
## Running the Benchmark
|
||||
|
||||
The main script for running the benchmark is `run_infer_interact.sh`. Here's how to use it:
|
||||
|
||||
```bash
|
||||
bash ./evaluation/benchmarks/swe_bench/scripts/run_infer_interact.sh <model_config> <commit_hash> <agent> <eval_limit> <max_iter> <num_workers> <split>
|
||||
```
|
||||
|
||||
### Parameters:
|
||||
|
||||
- `model_config`: Path to the LLM configuration file (e.g., `llm.claude-3-7-sonnet`)
|
||||
- `commit_hash`: Git commit hash to use (e.g., `HEAD`)
|
||||
- `agent`: The agent class to use (e.g., `CodeActAgent`)
|
||||
- `eval_limit`: Number of examples to evaluate (e.g., `500`)
|
||||
- `max_iter`: Maximum number of iterations per task (e.g., `100`)
|
||||
- `num_workers`: Number of parallel workers (e.g., `1`)
|
||||
- `split`: Dataset split to use (e.g., `test`)
|
||||
|
||||
### Example:
|
||||
|
||||
```bash
|
||||
bash ./evaluation/benchmarks/swe_bench/scripts/run_infer_interact.sh llm.claude-3-7-sonnet HEAD CodeActAgent 500 100 1 test
|
||||
```
|
||||
|
||||
### Additional Environment Variables:
|
||||
|
||||
You can customize the behavior using these environment variables:
|
||||
|
||||
- `RUN_WITH_BROWSING`: Enable/disable web browsing (default: false)
|
||||
- `USE_HINT_TEXT`: Enable/disable hint text (default: false)
|
||||
- `EVAL_CONDENSER`: Specify a condenser configuration
|
||||
- `EXP_NAME`: Add a custom experiment name to the output
|
||||
- `N_RUNS`: Number of runs to perform (default: 1)
|
||||
- `SKIP_RUNS`: Comma-separated list of run numbers to skip
|
||||
|
||||
## Evaluating Results
|
||||
|
||||
After running the benchmark, you can evaluate the results using `eval_infer.sh`:
|
||||
|
||||
```bash
|
||||
./evaluation/benchmarks/swe_bench/scripts/eval_infer.sh <output_file> <instance_id> <dataset> <split>
|
||||
```
|
||||
|
||||
### Parameters:
|
||||
|
||||
- `output_file`: Path to the output JSONL file
|
||||
- `instance_id`: The specific instance ID to evaluate
|
||||
- `dataset`: Dataset name (e.g., `cmu-lti/interactive-swe`)
|
||||
- `split`: Dataset split (e.g., `test`)
|
||||
|
||||
### Example:
|
||||
|
||||
```bash
|
||||
./evaluation/benchmarks/swe_bench/scripts/eval_infer.sh evaluation/evaluation_outputs/outputs/cmu-lti__interactive-swe-test/CodeActAgent/claude-3-7-sonnet-20250219_maxiter_100_N_v0.39.0-no-hint-run_1/output.jsonl sphinx-doc__sphinx-8721 cmu-lti/interactive-swe test
|
||||
```
|
||||
|
||||
## Output Structure
|
||||
|
||||
The benchmark outputs are stored in the `evaluation/evaluation_outputs/outputs/` directory with the following structure:
|
||||
|
||||
```
|
||||
evaluation/evaluation_outputs/outputs/
|
||||
└── cmu-lti__interactive-swe-{split}/
|
||||
└── {agent}/
|
||||
└── {model}-{date}_maxiter_{max_iter}_N_{version}-{options}-run_{run_number}/
|
||||
└── output.jsonl
|
||||
```
|
||||
|
||||
Where:
|
||||
- `{split}` is the dataset split (e.g., test)
|
||||
- `{agent}` is the agent class name
|
||||
- `{model}` is the model name
|
||||
- `{date}` is the run date
|
||||
- `{max_iter}` is the maximum iterations
|
||||
- `{version}` is the OpenHands version
|
||||
- `{options}` includes any additional options (e.g., no-hint, with-browsing)
|
||||
- `{run_number}` is the run number
|
||||
@@ -1,69 +0,0 @@
|
||||
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'
|
||||
)
|
||||
@@ -1,411 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
from datasets import load_dataset
|
||||
from litellm import completion as litellm_completion
|
||||
|
||||
import openhands.agenthub
|
||||
from evaluation.benchmarks.swe_bench.run_infer import (
|
||||
AgentFinishedCritic,
|
||||
complete_runtime,
|
||||
filter_dataset,
|
||||
get_config,
|
||||
initialize_runtime,
|
||||
)
|
||||
from evaluation.benchmarks.swe_bench.run_infer import (
|
||||
get_instruction as base_get_instruction,
|
||||
)
|
||||
from evaluation.utils.shared import (
|
||||
EvalException,
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
make_metadata,
|
||||
prepare_dataset,
|
||||
reset_logger_for_multiprocessing,
|
||||
run_evaluation,
|
||||
)
|
||||
from openhands.controller.state.state import State
|
||||
from openhands.core.config import (
|
||||
get_llm_config_arg,
|
||||
get_parser,
|
||||
)
|
||||
from openhands.core.config.condenser_config import NoOpCondenserConfig
|
||||
from openhands.core.config.utils import get_condenser_config_arg
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime, run_controller
|
||||
from openhands.events.action import MessageAction
|
||||
from openhands.events.serialization.event import event_from_dict, event_to_dict
|
||||
from openhands.utils.async_utils import call_async_from_sync
|
||||
|
||||
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
|
||||
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false').lower() == 'true'
|
||||
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'false'
|
||||
|
||||
|
||||
class FakeUser:
|
||||
def __init__(self, issue, hints, files):
|
||||
self.system_message = f"""
|
||||
You are a GitHub user reporting an issue. Here are the details of your issue and environment:
|
||||
|
||||
Issue: {issue}
|
||||
|
||||
Hints: {hints}
|
||||
|
||||
Files relative to your current directory: {files}
|
||||
|
||||
Your task is to respond to questions from a coder who is trying to solve your issue. The coder has a summarized version of the issue you have. Follow these rules:
|
||||
1. If the coder asks a question that is directly related to the information in the issue you have, provide that information.
|
||||
2. Always stay in character as a user reporting an issue, not as an AI assistant.
|
||||
3. Keep your responses concise and to the point.
|
||||
4. The coder has limited turns to solve the issue. Do not interact with the coder beyond 3 turns.
|
||||
|
||||
Respond with "I don't have that information" if the question is unrelated or you're unsure.
|
||||
"""
|
||||
self.chat_history = [{'role': 'system', 'content': self.system_message}]
|
||||
self.turns = 0
|
||||
# Get LLM config from config.toml
|
||||
self.llm_config = get_llm_config_arg(
|
||||
'llm.fake_user'
|
||||
) # You can change 'fake_user' to any config name you want
|
||||
|
||||
def generate_reply(self, question):
|
||||
if self.turns > 3:
|
||||
return 'Please continue working on the task. Do NOT ask for more help.'
|
||||
self.chat_history.append({'role': 'user', 'content': question.content})
|
||||
|
||||
response = litellm_completion(
|
||||
model=self.llm_config.model,
|
||||
messages=self.chat_history,
|
||||
api_key=self.llm_config.api_key.get_secret_value(),
|
||||
temperature=self.llm_config.temperature,
|
||||
base_url=self.llm_config.base_url,
|
||||
)
|
||||
|
||||
reply = response.choices[0].message.content
|
||||
self.chat_history.append({'role': 'assistant', 'content': reply})
|
||||
self.turns += 1
|
||||
return reply
|
||||
|
||||
|
||||
# Global variable for fake user
|
||||
fake_user = None
|
||||
|
||||
|
||||
def get_fake_user_response(state: State) -> str:
|
||||
global fake_user
|
||||
if not fake_user:
|
||||
return 'Please continue working on the task.'
|
||||
last_agent_message = state.get_last_agent_message()
|
||||
if last_agent_message:
|
||||
return fake_user.generate_reply(last_agent_message)
|
||||
return 'Please continue working on the task.'
|
||||
|
||||
|
||||
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
|
||||
'CodeActAgent': get_fake_user_response,
|
||||
}
|
||||
|
||||
|
||||
def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageAction:
|
||||
instance_copy = instance.copy()
|
||||
instance_copy.problem_statement = f'{instance.problem_statement}\n\nHints:\nThe user has not provided all the necessary details about the issue, and there are some hidden details that are helpful. Please ask the user specific questions using non-code commands to gather the relevant information that the user has to help you solve the issue. Ensure you have all the details you require to solve the issue.'
|
||||
return base_get_instruction(instance_copy, metadata)
|
||||
|
||||
|
||||
def process_instance(
|
||||
instance: pd.Series,
|
||||
metadata: EvalMetadata,
|
||||
reset_logger: bool = True,
|
||||
) -> EvalOutput:
|
||||
config = get_config(instance, metadata)
|
||||
global fake_user
|
||||
original_issue = instance.original_issue
|
||||
issue = str(original_issue)
|
||||
fake_user = FakeUser(issue=issue, hints=instance.hints_text, files=instance.files)
|
||||
|
||||
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
|
||||
if reset_logger:
|
||||
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
|
||||
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
|
||||
else:
|
||||
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
|
||||
|
||||
runtime = create_runtime(config)
|
||||
call_async_from_sync(runtime.connect)
|
||||
|
||||
try:
|
||||
initialize_runtime(runtime, instance, metadata)
|
||||
|
||||
message_action = get_instruction(instance, metadata)
|
||||
|
||||
# Here's how you can run the agent (similar to the `main` function) and get the final task state
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=message_action,
|
||||
runtime=runtime,
|
||||
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
|
||||
metadata.agent_class
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
# if fatal error, throw EvalError to trigger re-run
|
||||
if (
|
||||
state
|
||||
and state.last_error
|
||||
and 'fatal error during agent execution' in state.last_error
|
||||
and 'stuck in a loop' not in state.last_error
|
||||
):
|
||||
raise EvalException('Fatal error detected: ' + state.last_error)
|
||||
|
||||
# Get git patch
|
||||
return_val = complete_runtime(runtime, instance)
|
||||
git_patch = return_val['git_patch']
|
||||
logger.info(
|
||||
f'Got git diff for instance {instance.instance_id}:\n--------\n{git_patch}\n--------'
|
||||
)
|
||||
finally:
|
||||
runtime.close()
|
||||
|
||||
# Prepare test result
|
||||
test_result = {
|
||||
'git_patch': git_patch,
|
||||
}
|
||||
|
||||
if state is None:
|
||||
raise ValueError('State should not be None.')
|
||||
|
||||
histories = [event_to_dict(event) for event in state.history]
|
||||
metrics = state.metrics.get() if state.metrics else None
|
||||
|
||||
# Save the output
|
||||
instruction = message_action.content
|
||||
if message_action.image_urls:
|
||||
instruction += (
|
||||
'\n\n<image_urls>' + '\n'.join(message_action.image_urls) + '</image_urls>'
|
||||
)
|
||||
output = EvalOutput(
|
||||
instance_id=instance.instance_id,
|
||||
instruction=instruction,
|
||||
instance=instance.to_dict(),
|
||||
test_result=test_result,
|
||||
metadata=metadata,
|
||||
history=histories,
|
||||
metrics=metrics,
|
||||
error=state.last_error if state and state.last_error else None,
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = get_parser()
|
||||
parser.add_argument(
|
||||
'--dataset',
|
||||
type=str,
|
||||
default='cmu-lti/interactive-swe',
|
||||
help='dataset to evaluate on',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--split',
|
||||
type=str,
|
||||
default='test',
|
||||
help='split to evaluate on',
|
||||
)
|
||||
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
# Load dataset from huggingface datasets
|
||||
dataset = load_dataset(args.dataset, split=args.split)
|
||||
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
|
||||
logger.info(
|
||||
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_bench_tests)} tasks'
|
||||
)
|
||||
llm_config = None
|
||||
if args.llm_config:
|
||||
llm_config = get_llm_config_arg(args.llm_config)
|
||||
llm_config.log_completions = True
|
||||
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
|
||||
llm_config.modify_params = False
|
||||
|
||||
if llm_config is None:
|
||||
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
|
||||
|
||||
# Get condenser config from environment variable
|
||||
condenser_name = os.environ.get('EVAL_CONDENSER')
|
||||
if condenser_name:
|
||||
condenser_config = get_condenser_config_arg(condenser_name)
|
||||
if condenser_config is None:
|
||||
raise ValueError(
|
||||
f'Could not find Condenser config: EVAL_CONDENSER={condenser_name}'
|
||||
)
|
||||
else:
|
||||
# If no specific condenser config is provided via env var, default to NoOpCondenser
|
||||
condenser_config = NoOpCondenserConfig()
|
||||
logger.debug(
|
||||
'No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.'
|
||||
)
|
||||
|
||||
details = {'mode': 'interact'}
|
||||
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
|
||||
|
||||
dataset_descrption = (
|
||||
args.dataset.replace('/', '__') + '-' + args.split.replace('/', '__')
|
||||
)
|
||||
metadata = make_metadata(
|
||||
llm_config,
|
||||
dataset_descrption,
|
||||
args.agent_cls,
|
||||
args.max_iterations,
|
||||
args.eval_note,
|
||||
args.eval_output_dir,
|
||||
details=details,
|
||||
condenser_config=condenser_config,
|
||||
)
|
||||
|
||||
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
|
||||
print(f'### OUTPUT FILE: {output_file} ###')
|
||||
|
||||
# Run evaluation in iterative mode:
|
||||
# If a rollout fails to output AgentFinishAction, we will try again until it succeeds OR total 3 attempts have been made.
|
||||
ITERATIVE_EVAL_MODE = (
|
||||
os.environ.get('ITERATIVE_EVAL_MODE', 'false').lower() == 'true'
|
||||
)
|
||||
ITERATIVE_EVAL_MODE_MAX_ATTEMPTS = int(
|
||||
os.environ.get('ITERATIVE_EVAL_MODE_MAX_ATTEMPTS', '3')
|
||||
)
|
||||
|
||||
if not ITERATIVE_EVAL_MODE:
|
||||
# load the dataset
|
||||
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
|
||||
if len(instances) > 0 and not isinstance(
|
||||
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
|
||||
):
|
||||
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
|
||||
instances[col] = instances[col].apply(lambda x: str(x))
|
||||
run_evaluation(
|
||||
instances,
|
||||
metadata,
|
||||
output_file,
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=8
|
||||
* 60
|
||||
* 60, # 8 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
)
|
||||
else:
|
||||
critic = AgentFinishedCritic()
|
||||
|
||||
def get_cur_output_file_path(attempt: int) -> str:
|
||||
return (
|
||||
f'{output_file.removesuffix(".jsonl")}.critic_attempt_{attempt}.jsonl'
|
||||
)
|
||||
|
||||
eval_ids = None
|
||||
for attempt in range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1):
|
||||
cur_output_file = get_cur_output_file_path(attempt)
|
||||
logger.info(
|
||||
f'Running evaluation with critic {critic.__class__.__name__} for attempt {attempt} of {ITERATIVE_EVAL_MODE_MAX_ATTEMPTS}.'
|
||||
)
|
||||
|
||||
# For deterministic eval, we set temperature to 0.1 for (>1) attempt
|
||||
# so hopefully we get slightly different results
|
||||
if attempt > 1 and metadata.llm_config.temperature == 0:
|
||||
logger.info(
|
||||
f'Detected temperature is 0 for (>1) attempt {attempt}. Setting temperature to 0.1...'
|
||||
)
|
||||
metadata.llm_config.temperature = 0.1
|
||||
|
||||
# Load instances - at first attempt, we evaluate all instances
|
||||
# On subsequent attempts, we only evaluate the instances that failed the previous attempt determined by critic
|
||||
instances = prepare_dataset(
|
||||
swe_bench_tests, cur_output_file, args.eval_n_limit, eval_ids=eval_ids
|
||||
)
|
||||
if len(instances) > 0 and not isinstance(
|
||||
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
|
||||
):
|
||||
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
|
||||
instances[col] = instances[col].apply(lambda x: str(x))
|
||||
|
||||
# Run evaluation - but save them to cur_output_file
|
||||
logger.info(
|
||||
f'Evaluating {len(instances)} instances for attempt {attempt}...'
|
||||
)
|
||||
run_evaluation(
|
||||
instances,
|
||||
metadata,
|
||||
cur_output_file,
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=8
|
||||
* 60
|
||||
* 60, # 8 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
)
|
||||
|
||||
# When eval is done, we update eval_ids to the instances that failed the current attempt
|
||||
instances_failed = []
|
||||
logger.info(
|
||||
f'Use critic {critic.__class__.__name__} to check {len(instances)} instances for attempt {attempt}...'
|
||||
)
|
||||
with open(cur_output_file, 'r') as f:
|
||||
for line in f:
|
||||
instance = json.loads(line)
|
||||
try:
|
||||
history = [
|
||||
event_from_dict(event) for event in instance['history']
|
||||
]
|
||||
critic_result = critic.evaluate(
|
||||
history, instance['test_result'].get('git_patch', '')
|
||||
)
|
||||
if not critic_result.success:
|
||||
instances_failed.append(instance['instance_id'])
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f'Error loading history for instance {instance["instance_id"]}: {e}'
|
||||
)
|
||||
instances_failed.append(instance['instance_id'])
|
||||
logger.info(
|
||||
f'{len(instances_failed)} instances failed the current attempt {attempt}: {instances_failed}'
|
||||
)
|
||||
eval_ids = instances_failed
|
||||
|
||||
# If no instances failed, we break
|
||||
if len(instances_failed) == 0:
|
||||
break
|
||||
|
||||
# Then we should aggregate the results from all attempts into the original output file
|
||||
# and remove the intermediate files
|
||||
logger.info(
|
||||
'Aggregating results from all attempts into the original output file...'
|
||||
)
|
||||
fout = open(output_file, 'w')
|
||||
added_instance_ids = set()
|
||||
for attempt in reversed(range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1)):
|
||||
cur_output_file = get_cur_output_file_path(attempt)
|
||||
if not os.path.exists(cur_output_file):
|
||||
logger.warning(
|
||||
f'Intermediate output file {cur_output_file} does not exist. Skipping...'
|
||||
)
|
||||
continue
|
||||
|
||||
with open(cur_output_file, 'r') as f:
|
||||
for line in f:
|
||||
instance = json.loads(line)
|
||||
# Also make sure git_patch is not empty - otherwise we fall back to previous attempt (empty patch is worse than anything else)
|
||||
if (
|
||||
instance['instance_id'] not in added_instance_ids
|
||||
and instance['test_result'].get('git_patch', '').strip()
|
||||
):
|
||||
fout.write(line)
|
||||
added_instance_ids.add(instance['instance_id'])
|
||||
logger.info(
|
||||
f'Aggregated instances from {cur_output_file}. Total instances added so far: {len(added_instance_ids)}'
|
||||
)
|
||||
fout.close()
|
||||
logger.info(
|
||||
f'Done! Total {len(added_instance_ids)} instances added to {output_file}'
|
||||
)
|
||||
@@ -1,713 +0,0 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,131 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
source "evaluation/utils/version_control.sh"
|
||||
|
||||
MODEL_CONFIG=$1
|
||||
COMMIT_HASH=$2
|
||||
AGENT=$3
|
||||
EVAL_LIMIT=$4
|
||||
MAX_ITER=$5
|
||||
NUM_WORKERS=$6
|
||||
SPLIT=$8
|
||||
N_RUNS=$9
|
||||
|
||||
|
||||
if [ -z "$NUM_WORKERS" ]; then
|
||||
NUM_WORKERS=1
|
||||
echo "Number of workers not specified, use default $NUM_WORKERS"
|
||||
fi
|
||||
checkout_eval_branch
|
||||
|
||||
if [ -z "$AGENT" ]; then
|
||||
echo "Agent not specified, use default CodeActAgent"
|
||||
AGENT="CodeActAgent"
|
||||
fi
|
||||
|
||||
if [ -z "$MAX_ITER" ]; then
|
||||
echo "MAX_ITER not specified, use default 100"
|
||||
MAX_ITER=100
|
||||
fi
|
||||
|
||||
if [ -z "$RUN_WITH_BROWSING" ]; then
|
||||
echo "RUN_WITH_BROWSING not specified, use default false"
|
||||
RUN_WITH_BROWSING=false
|
||||
fi
|
||||
|
||||
|
||||
if [ -z "$DATASET" ]; then
|
||||
echo "DATASET not specified, use default cmu-lti/interactive-swe"
|
||||
DATASET="cmu-lti/interactive-swe"
|
||||
fi
|
||||
|
||||
if [ -z "$SPLIT" ]; then
|
||||
echo "SPLIT not specified, use default test"
|
||||
SPLIT="test"
|
||||
fi
|
||||
|
||||
if [ -n "$EVAL_CONDENSER" ]; then
|
||||
echo "Using Condenser Config: $EVAL_CONDENSER"
|
||||
else
|
||||
echo "No Condenser Config provided via EVAL_CONDENSER, use default (NoOpCondenser)."
|
||||
fi
|
||||
|
||||
export RUN_WITH_BROWSING=$RUN_WITH_BROWSING
|
||||
echo "RUN_WITH_BROWSING: $RUN_WITH_BROWSING"
|
||||
|
||||
get_openhands_version
|
||||
|
||||
echo "AGENT: $AGENT"
|
||||
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
|
||||
echo "MODEL_CONFIG: $MODEL_CONFIG"
|
||||
echo "DATASET: $DATASET"
|
||||
echo "SPLIT: $SPLIT"
|
||||
echo "MAX_ITER: $MAX_ITER"
|
||||
echo "NUM_WORKERS: $NUM_WORKERS"
|
||||
echo "COMMIT_HASH: $COMMIT_HASH"
|
||||
echo "EVAL_CONDENSER: $EVAL_CONDENSER"
|
||||
|
||||
# Default to NOT use Hint
|
||||
if [ -z "$USE_HINT_TEXT" ]; then
|
||||
export USE_HINT_TEXT=false
|
||||
fi
|
||||
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
|
||||
EVAL_NOTE="$OPENHANDS_VERSION"
|
||||
# if not using Hint, add -no-hint to the eval note
|
||||
if [ "$USE_HINT_TEXT" = false ]; then
|
||||
EVAL_NOTE="$EVAL_NOTE-no-hint"
|
||||
fi
|
||||
|
||||
if [ "$RUN_WITH_BROWSING" = true ]; then
|
||||
EVAL_NOTE="$EVAL_NOTE-with-browsing"
|
||||
fi
|
||||
|
||||
if [ -n "$EXP_NAME" ]; then
|
||||
EVAL_NOTE="$EVAL_NOTE-$EXP_NAME"
|
||||
fi
|
||||
# Add condenser config to eval note if provided
|
||||
if [ -n "$EVAL_CONDENSER" ]; then
|
||||
EVAL_NOTE="${EVAL_NOTE}-${EVAL_CONDENSER}"
|
||||
fi
|
||||
|
||||
function run_eval() {
|
||||
local eval_note="${1}"
|
||||
COMMAND="poetry run python evaluation/benchmarks/swe_bench/run_infer_interact.py \
|
||||
--agent-cls $AGENT \
|
||||
--llm-config $MODEL_CONFIG \
|
||||
--max-iterations $MAX_ITER \
|
||||
--eval-num-workers $NUM_WORKERS \
|
||||
--eval-note $eval_note \
|
||||
--dataset $DATASET \
|
||||
--split $SPLIT"
|
||||
|
||||
if [ -n "$EVAL_LIMIT" ]; then
|
||||
echo "EVAL_LIMIT: $EVAL_LIMIT"
|
||||
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
|
||||
fi
|
||||
|
||||
# Run the command
|
||||
eval $COMMAND
|
||||
}
|
||||
|
||||
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
|
||||
if [ -z "$N_RUNS" ]; then
|
||||
N_RUNS=1
|
||||
echo "N_RUNS not specified, use default $N_RUNS"
|
||||
fi
|
||||
|
||||
# Skip runs if the run number is in the SKIP_RUNS list
|
||||
# read from env variable SKIP_RUNS as a comma separated list of run numbers
|
||||
SKIP_RUNS=(${SKIP_RUNS//,/ })
|
||||
for i in $(seq 1 $N_RUNS); do
|
||||
if [[ " ${SKIP_RUNS[@]} " =~ " $i " ]]; then
|
||||
echo "Skipping run $i"
|
||||
continue
|
||||
fi
|
||||
current_eval_note="$EVAL_NOTE-run_$i"
|
||||
echo "EVAL_NOTE: $current_eval_note"
|
||||
run_eval $current_eval_note
|
||||
done
|
||||
|
||||
checkout_original_branch
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/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,6 +4,7 @@ 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", () => ({
|
||||
@@ -47,9 +48,11 @@ vi.mock("react-router", () => ({
|
||||
const renderActionSuggestions = () =>
|
||||
render(<ActionSuggestions onSuggestionsClick={() => {}} />, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
<ConversationProvider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</ConversationProvider>
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -56,15 +56,15 @@ function TestComponent() {
|
||||
describe("WsClientProvider", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mock("#/hooks/query/use-active-conversation", () => ({
|
||||
useActiveConversation: () => {
|
||||
vi.mock("#/hooks/query/use-user-conversation", () => ({
|
||||
useUserConversation: () => {
|
||||
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: "RUNNING" as const,
|
||||
status: "STOPPED" as const,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
}}},
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("Content", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(provider).toHaveValue("Anthropic");
|
||||
expect(model).toHaveValue("claude-sonnet-4-20250514");
|
||||
expect(model).toHaveValue("claude-3-7-sonnet-20250219");
|
||||
|
||||
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-advanced");
|
||||
within(advancedForm).getByTestId("llm-api-key-help-anchor");
|
||||
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-sonnet-4-20250514");
|
||||
expect(model).toHaveValue("anthropic/claude-3-7-sonnet-20250219");
|
||||
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-sonnet-4-20250514");
|
||||
const modelOption = screen.getByText("claude-3-7-sonnet-20250219");
|
||||
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-sonnet-4-20250514",
|
||||
llm_model: "anthropic/claude-3-7-sonnet-20250219",
|
||||
llm_base_url: "",
|
||||
confirmation_mode: false,
|
||||
}),
|
||||
|
||||
@@ -71,18 +71,6 @@ 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",
|
||||
|
||||
Generated
+356
-363
File diff suppressed because it is too large
Load Diff
+10
-10
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.39.2",
|
||||
"version": "0.39.1",
|
||||
"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.1",
|
||||
"@react-router/serve": "^7.6.1",
|
||||
"@react-router/node": "^7.6.0",
|
||||
"@react-router/serve": "^7.6.0",
|
||||
"@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.77.2",
|
||||
"@tanstack/react-query": "^5.76.1",
|
||||
"@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.14.0",
|
||||
"i18next": "^25.2.1",
|
||||
"framer-motion": "^12.12.1",
|
||||
"i18next": "^25.1.3",
|
||||
"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.2",
|
||||
"posthog-js": "^1.245.1",
|
||||
"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.1",
|
||||
"react-router": "^7.6.0",
|
||||
"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.1",
|
||||
"@react-router/dev": "^7.6.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.78.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.74.7",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.8.4'
|
||||
const PACKAGE_VERSION = '2.7.6'
|
||||
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
@@ -113,7 +113,6 @@ 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 { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import {
|
||||
initialState as browserInitialState,
|
||||
setUrl,
|
||||
@@ -14,7 +14,7 @@ export function BrowserPanel() {
|
||||
const { url, screenshotSrc } = useSelector(
|
||||
(state: RootState) => state.browser,
|
||||
);
|
||||
const { conversationId } = useConversationId();
|
||||
const { conversationId } = useConversation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -4,7 +4,8 @@ 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 { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import { useUserConversation } from "#/hooks/query/use-user-conversation";
|
||||
|
||||
interface ActionSuggestionsProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@@ -15,7 +16,9 @@ export function ActionSuggestions({
|
||||
}: ActionSuggestionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { providers } = useUserProviders();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const { conversationId } = useConversation();
|
||||
const { data: conversation } = useUserConversation(conversationId);
|
||||
|
||||
const [hasPullRequest, setHasPullRequest] = React.useState(false);
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
|
||||
@@ -153,13 +153,11 @@ export function ChatInterface() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isWaitingForUserInput &&
|
||||
events.length > 0 &&
|
||||
!optimisticUserMessage && (
|
||||
<ActionSuggestions
|
||||
onSuggestionsClick={(value) => handleSendMessage(value, [])}
|
||||
/>
|
||||
)}
|
||||
{isWaitingForUserInput && (
|
||||
<ActionSuggestions
|
||||
onSuggestionsClick={(value) => handleSendMessage(value, [])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-[6px] px-4 pb-4">
|
||||
|
||||
@@ -28,58 +28,41 @@ export const getEventContent = (
|
||||
let details: string = "";
|
||||
|
||||
if (isOpenHandsAction(event)) {
|
||||
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();
|
||||
}
|
||||
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 />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
details = getActionContent(event);
|
||||
}
|
||||
|
||||
if (isOpenHandsObservation(event)) {
|
||||
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();
|
||||
}
|
||||
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 />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
details = getObservationContent(event);
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -46,6 +46,14 @@ 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 = "";
|
||||
|
||||
@@ -116,6 +124,8 @@ 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:
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsEventType } from "#/types/core/base";
|
||||
import {
|
||||
isCommandAction,
|
||||
isCommandObservation,
|
||||
isOpenHandsAction,
|
||||
isOpenHandsObservation,
|
||||
} from "#/types/core/guards";
|
||||
import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
|
||||
const COMMON_NO_RENDER_LIST: OpenHandsEventType[] = [
|
||||
@@ -20,21 +15,11 @@ export const shouldRenderEvent = (
|
||||
event: OpenHandsAction | OpenHandsObservation,
|
||||
) => {
|
||||
if (isOpenHandsAction(event)) {
|
||||
if (isCommandAction(event) && event.source === "user") {
|
||||
// For user commands, we always hide them from the chat interface
|
||||
return false;
|
||||
}
|
||||
|
||||
const noRenderList = COMMON_NO_RENDER_LIST.concat(ACTION_NO_RENDER_LIST);
|
||||
return !noRenderList.includes(event.action);
|
||||
}
|
||||
|
||||
if (isOpenHandsObservation(event)) {
|
||||
if (isCommandObservation(event) && event.source === "user") {
|
||||
// For user commands, we always hide them from the chat interface
|
||||
return false;
|
||||
}
|
||||
|
||||
return !COMMON_NO_RENDER_LIST.includes(event.observation);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,11 @@ 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";
|
||||
@@ -48,11 +46,12 @@ export function EventMessage({
|
||||
);
|
||||
}
|
||||
|
||||
if (hasObservationPair && isOpenHandsAction(event)) {
|
||||
if (hasThoughtProperty(event.args)) {
|
||||
return <ChatMessage type="agent" message={event.args.thought} />;
|
||||
}
|
||||
return null;
|
||||
if (
|
||||
hasObservationPair &&
|
||||
isOpenHandsAction(event) &&
|
||||
hasThoughtProperty(event.args)
|
||||
) {
|
||||
return <ChatMessage type="agent" message={event.args.thought} />;
|
||||
}
|
||||
|
||||
if (isFinishAction(event)) {
|
||||
@@ -79,19 +78,6 @@ 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 | React.ReactNode;
|
||||
details: string;
|
||||
success?: ObservationResultStatus;
|
||||
}
|
||||
|
||||
@@ -44,21 +44,18 @@ export function GenericEventMessage({
|
||||
{success && <SuccessIndicator status={success} />}
|
||||
</div>
|
||||
|
||||
{showDetails &&
|
||||
(typeof details === "string" ? (
|
||||
<Markdown
|
||||
components={{
|
||||
code,
|
||||
ul,
|
||||
ol,
|
||||
}}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
>
|
||||
{details}
|
||||
</Markdown>
|
||||
) : (
|
||||
details
|
||||
))}
|
||||
{showDetails && (
|
||||
<Markdown
|
||||
components={{
|
||||
code,
|
||||
ul,
|
||||
ol,
|
||||
}}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
>
|
||||
{details}
|
||||
</Markdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -2,10 +2,32 @@ import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
|
||||
import { OpenHandsEventType } from "#/types/core/base";
|
||||
import { EventMessage } from "./event-message";
|
||||
import { ChatMessage } from "./chat-message";
|
||||
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
|
||||
|
||||
const COMMON_NO_RENDER_LIST: OpenHandsEventType[] = [
|
||||
"system",
|
||||
"agent_state_changed",
|
||||
"change_agent_state",
|
||||
];
|
||||
|
||||
const ACTION_NO_RENDER_LIST: OpenHandsEventType[] = ["recall"];
|
||||
|
||||
const shouldRenderEvent = (event: OpenHandsAction | OpenHandsObservation) => {
|
||||
if (isOpenHandsAction(event)) {
|
||||
const noRenderList = COMMON_NO_RENDER_LIST.concat(ACTION_NO_RENDER_LIST);
|
||||
return !noRenderList.includes(event.action);
|
||||
}
|
||||
|
||||
if (isOpenHandsObservation(event)) {
|
||||
return !COMMON_NO_RENDER_LIST.includes(event.observation);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
interface MessagesProps {
|
||||
messages: (OpenHandsAction | OpenHandsObservation)[];
|
||||
isAwaitingUserConfirmation: boolean;
|
||||
@@ -27,12 +49,12 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
|
||||
return false;
|
||||
},
|
||||
[],
|
||||
[messages],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{messages.map((message, index) => (
|
||||
{messages.filter(shouldRenderEvent).map((message, index) => (
|
||||
<EventMessage
|
||||
key={index}
|
||||
event={message}
|
||||
@@ -48,14 +70,6 @@ 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,7 +15,6 @@ 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,
|
||||
@@ -29,7 +28,6 @@ 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>("");
|
||||
|
||||
@@ -80,10 +78,7 @@ export function AgentStatusBar() {
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (conversation?.status === "STARTING") {
|
||||
setStatusMessage(t(I18nKey.STATUS$STARTING_RUNTIME));
|
||||
setIndicatorColor(IndicatorColor.RED);
|
||||
} else if (status === WsClientProviderStatus.DISCONNECTED) {
|
||||
if (status === WsClientProviderStatus.DISCONNECTED) {
|
||||
setStatusMessage(t(I18nKey.STATUS$CONNECTED)); // Using STATUS$CONNECTED instead of STATUS$CONNECTING
|
||||
setIndicatorColor(IndicatorColor.RED);
|
||||
} else {
|
||||
@@ -102,7 +97,7 @@ export function AgentStatusBar() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [curAgentState, status, notify, t, conversation?.status]);
|
||||
}, [curAgentState, status, notify, t]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useUserConversation } from "#/hooks/query/use-user-conversation";
|
||||
import { ConversationCard } from "../conversation-panel/conversation-card";
|
||||
|
||||
interface ControlsProps {
|
||||
@@ -11,7 +12,10 @@ interface ControlsProps {
|
||||
}
|
||||
|
||||
export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const params = useParams();
|
||||
const { data: conversation } = useUserConversation(
|
||||
params.conversationId ?? null,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 md:items-center md:justify-between md:flex-row">
|
||||
|
||||
@@ -6,7 +6,26 @@ 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";
|
||||
import { JSON_VIEW_THEME } from "#/utils/constants";
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
interface SystemMessageModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -188,9 +207,8 @@ 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={JSON_VIEW_THEME}
|
||||
theme={jsonViewTheme}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,6 @@ interface FeedbackFormProps {
|
||||
|
||||
export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const copiedToClipboardToast = () => {
|
||||
hotToast(t(I18nKey.FEEDBACK$PASSWORD_COPIED_MESSAGE), {
|
||||
icon: "📋",
|
||||
@@ -128,9 +127,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
|
||||
className="grow"
|
||||
isDisabled={isPending}
|
||||
>
|
||||
{isPending
|
||||
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL) || "Submitting..."
|
||||
: t(I18nKey.FEEDBACK$SHARE_LABEL)}
|
||||
{t(I18nKey.FEEDBACK$SHARE_LABEL)}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
type="button"
|
||||
@@ -142,12 +139,6 @@ 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: decodeURIComponent(repo.full_name),
|
||||
label: 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={decodeURIComponent(taskGroup.title)}
|
||||
title={taskGroup.title}
|
||||
tasks={taskGroup.tasks}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -29,6 +29,7 @@ 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;
|
||||
@@ -40,6 +41,7 @@ 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 flex-col md:flex-row w-[full] md:w-[680px] justify-between gap-4 md:gap-[46px]">
|
||||
<div className="flex w-[680px] justify-between gap-[46px]">
|
||||
<fieldset className="flex flex-col gap-2.5 w-full">
|
||||
<label className="text-sm">{t(I18nKey.LLM$PROVIDER)}</label>
|
||||
<Autocomplete
|
||||
|
||||
@@ -42,7 +42,6 @@ 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,
|
||||
});
|
||||
@@ -87,7 +86,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
|
||||
name="llm-api-key-input"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-full"
|
||||
className="w-[680px]"
|
||||
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] m-4 p-6 rounded-xl flex flex-col gap-2 border border-tertiary"
|
||||
className="bg-base-secondary min-w-[384px] p-6 rounded-xl flex flex-col gap-2 border border-tertiary"
|
||||
>
|
||||
{aiConfigOptions.error && (
|
||||
<p className="text-danger text-xs">{aiConfigOptions.error.message}</p>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
}
|
||||
@@ -16,14 +16,12 @@ import {
|
||||
} from "#/types/core/actions";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useUserConversation } from "#/hooks/query/use-user-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";
|
||||
@@ -70,7 +68,6 @@ const isMessageAction = (
|
||||
export enum WsClientProviderStatus {
|
||||
CONNECTED,
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
}
|
||||
|
||||
interface UseWsClient {
|
||||
@@ -150,7 +147,7 @@ export function WsClientProvider({
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const messageRateHandler = useRate({ threshold: 250 });
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const { data: conversation } = useUserConversation(conversationId);
|
||||
|
||||
function send(event: Record<string, unknown>) {
|
||||
if (!sioRef.current) {
|
||||
@@ -162,33 +159,10 @@ 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]);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ 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 { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
type SubmitFeedbackArgs = {
|
||||
@@ -9,14 +9,12 @@ type SubmitFeedbackArgs = {
|
||||
};
|
||||
|
||||
export const useSubmitFeedback = () => {
|
||||
const { conversationId } = useConversationId();
|
||||
const { conversationId } = useConversation();
|
||||
return useMutation({
|
||||
mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
|
||||
OpenHands.submitFeedback(conversationId, feedback),
|
||||
onError: (error) => {
|
||||
displayErrorToast(error.message);
|
||||
},
|
||||
retry: 2,
|
||||
retryDelay: 500,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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;
|
||||
});
|
||||
};
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useQueries, useQuery } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { RootState } from "#/store";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
|
||||
export const useActiveHost = () => {
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const [activeHost, setActiveHost] = React.useState<string | null>(null);
|
||||
const { conversationId } = useConversationId();
|
||||
const runtimeIsReady = useRuntimeIsReady();
|
||||
|
||||
const { conversationId } = useConversation();
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: [conversationId, "hosts"],
|
||||
@@ -16,7 +19,7 @@ export const useActiveHost = () => {
|
||||
const hosts = await OpenHands.getWebHosts(conversationId);
|
||||
return { hosts };
|
||||
},
|
||||
enabled: runtimeIsReady && !!conversationId,
|
||||
enabled: !RUNTIME_INACTIVE_STATES.includes(curAgentState),
|
||||
initialData: { hosts: [] },
|
||||
meta: {
|
||||
disableToast: true,
|
||||
@@ -34,7 +37,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 { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const useConversationConfig = () => {
|
||||
const { status } = useWsClient();
|
||||
const { conversationId } = useConversationId();
|
||||
const { conversationId } = useConversation();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["conversation_config", conversationId],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { GitChangeStatus } from "#/api/open-hands.types";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
|
||||
type UseGetDiffConfig = {
|
||||
filePath: string;
|
||||
@@ -10,7 +10,7 @@ type UseGetDiffConfig = {
|
||||
};
|
||||
|
||||
export const useGitDiff = (config: UseGetDiffConfig) => {
|
||||
const { conversationId } = useConversationId();
|
||||
const { conversationId } = useConversation();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["file_diff", conversationId, config.filePath, config.type],
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import { GitChange } from "#/api/open-hands.types";
|
||||
import { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
|
||||
import { RootState } from "#/store";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
|
||||
export const useGetGitChanges = () => {
|
||||
const { conversationId } = useConversationId();
|
||||
const { conversationId } = useConversation();
|
||||
const [orderedChanges, setOrderedChanges] = React.useState<GitChange[]>([]);
|
||||
const previousDataRef = React.useRef<GitChange[]>(null);
|
||||
const runtimeIsReady = useRuntimeIsReady();
|
||||
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const runtimeIsActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
|
||||
|
||||
const result = useQuery({
|
||||
queryKey: ["file_changes", conversationId],
|
||||
@@ -17,7 +21,7 @@ export const useGetGitChanges = () => {
|
||||
retry: false,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
enabled: runtimeIsReady && !!conversationId,
|
||||
enabled: runtimeIsActive,
|
||||
meta: {
|
||||
disableToast: true,
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@ 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,
|
||||
@@ -26,7 +25,6 @@ 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,24 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Query, useQuery } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
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,
|
||||
refetchInterval?: RefetchInterval,
|
||||
) =>
|
||||
export const useUserConversation = (cid: string | null) =>
|
||||
useQuery({
|
||||
queryKey: ["user", "conversation", cid],
|
||||
queryFn: async () => {
|
||||
@@ -28,7 +14,12 @@ export const useUserConversation = (
|
||||
},
|
||||
enabled: !!cid,
|
||||
retry: false,
|
||||
refetchInterval,
|
||||
refetchInterval: (query) => {
|
||||
if (query.state.data?.status === "STARTING") {
|
||||
return 2000; // 2 seconds
|
||||
}
|
||||
return FIVE_MINUTES;
|
||||
},
|
||||
staleTime: FIVE_MINUTES,
|
||||
gcTime: FIFTEEN_MINUTES,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
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 { useRuntimeIsReady } from "#/hooks/use-runtime-is-ready";
|
||||
|
||||
// Define the return type for the VS Code URL query
|
||||
interface VSCodeUrlResult {
|
||||
@@ -14,8 +16,9 @@ interface VSCodeUrlResult {
|
||||
|
||||
export const useVSCodeUrl = () => {
|
||||
const { t } = useTranslation();
|
||||
const { conversationId } = useConversationId();
|
||||
const runtimeIsReady = useRuntimeIsReady();
|
||||
const { conversationId } = useConversation();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const isRuntimeInactive = RUNTIME_INACTIVE_STATES.includes(curAgentState);
|
||||
|
||||
return useQuery<VSCodeUrlResult>({
|
||||
queryKey: ["vscode_url", conversationId],
|
||||
@@ -33,7 +36,7 @@ export const useVSCodeUrl = () => {
|
||||
error: t(I18nKey.VSCODE$URL_NOT_AVAILABLE),
|
||||
};
|
||||
},
|
||||
enabled: runtimeIsReady && !!conversationId,
|
||||
enabled: !!conversationId && !isRuntimeInactive,
|
||||
refetchOnMount: true,
|
||||
retry: 3,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useConfig } from "./query/use-config";
|
||||
import { useIsAuthed } from "./query/use-is-authed";
|
||||
import { getLoginMethod, LoginMethod } from "#/utils/local-storage";
|
||||
import {
|
||||
getLoginMethod,
|
||||
getLastPage,
|
||||
getJustLoggedIn,
|
||||
setJustLoggedIn,
|
||||
LoginMethod,
|
||||
} from "#/utils/local-storage";
|
||||
import { useAuthUrl } from "./use-auth-url";
|
||||
|
||||
/**
|
||||
@@ -9,6 +16,7 @@ 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();
|
||||
|
||||
@@ -53,6 +61,9 @@ 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;
|
||||
}
|
||||
@@ -65,4 +76,43 @@ 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]);
|
||||
};
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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,5 +1,6 @@
|
||||
import { useParams } from "react-router";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useActiveConversation } from "./query/use-active-conversation";
|
||||
import { useUserConversation } from "./query/use-user-conversation";
|
||||
|
||||
/**
|
||||
* Hook that updates the document title based on the current conversation.
|
||||
@@ -8,7 +9,10 @@ import { useActiveConversation } from "./query/use-active-conversation";
|
||||
* @param suffix Optional suffix to append to the title (default: "OpenHands")
|
||||
*/
|
||||
export function useDocumentTitleFromState(suffix = "OpenHands") {
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const params = useParams();
|
||||
const { data: conversation } = useUserConversation(
|
||||
params.conversationId ?? null,
|
||||
);
|
||||
const lastValidTitleRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { useActiveConversation } from "./query/use-active-conversation";
|
||||
|
||||
/**
|
||||
* Hook to determine if the runtime is ready for operations
|
||||
*
|
||||
* @returns boolean indicating if the runtime is ready
|
||||
*/
|
||||
export const useRuntimeIsReady = (): boolean => {
|
||||
const { data: conversation } = useActiveConversation();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
return (
|
||||
conversation?.status === "RUNNING" &&
|
||||
!RUNTIME_INACTIVE_STATES.includes(curAgentState)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
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]);
|
||||
};
|
||||
@@ -11,8 +11,6 @@ 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",
|
||||
@@ -119,9 +117,6 @@ 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",
|
||||
@@ -228,8 +223,6 @@ export enum I18nKey {
|
||||
FEEDBACK$FAILED_TO_SHARE = "FEEDBACK$FAILED_TO_SHARE",
|
||||
FEEDBACK$COPY_LABEL = "FEEDBACK$COPY_LABEL",
|
||||
FEEDBACK$SHARING_SETTINGS_LABEL = "FEEDBACK$SHARING_SETTINGS_LABEL",
|
||||
FEEDBACK$SUBMITTING_LABEL = "FEEDBACK$SUBMITTING_LABEL",
|
||||
FEEDBACK$SUBMITTING_MESSAGE = "FEEDBACK$SUBMITTING_MESSAGE",
|
||||
SECURITY$UNKNOWN_ANALYZER_LABEL = "SECURITY$UNKNOWN_ANALYZER_LABEL",
|
||||
INVARIANT$UPDATE_POLICY_LABEL = "INVARIANT$UPDATE_POLICY_LABEL",
|
||||
INVARIANT$UPDATE_SETTINGS_LABEL = "INVARIANT$UPDATE_SETTINGS_LABEL",
|
||||
@@ -428,7 +421,6 @@ 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",
|
||||
@@ -437,7 +429,6 @@ 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",
|
||||
|
||||
@@ -175,38 +175,6 @@
|
||||
"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": "エラー:",
|
||||
@@ -1903,54 +1871,6 @@
|
||||
"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": "カスタムモデル",
|
||||
@@ -6815,22 +6735,6 @@
|
||||
"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>",
|
||||
@@ -6959,22 +6863,6 @@
|
||||
"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": "显示详情",
|
||||
@@ -8766,37 +8654,5 @@
|
||||
"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": "Відправляємо відгук, будь ласка, почекайте..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ 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,
|
||||
@@ -100,7 +99,7 @@ const openHandsHandlers = [
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"anthropic/claude-3.5",
|
||||
"anthropic/claude-sonnet-4-20250514",
|
||||
"anthropic/claude-3-7-sonnet-20250219",
|
||||
]),
|
||||
),
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ function AppSettingsScreen() {
|
||||
setLanguageInputHasChanged(false);
|
||||
setAnalyticsSwitchHasChanged(false);
|
||||
setSoundNotificationsSwitchHasChanged(false);
|
||||
setProactiveConversationsSwitchHasChanged(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8,7 +8,10 @@ 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 { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import {
|
||||
ConversationProvider,
|
||||
useConversation,
|
||||
} from "#/context/conversation-context";
|
||||
import { Controls } from "#/components/features/controls/controls";
|
||||
import { clearTerminal } from "#/state/command-slice";
|
||||
import { useEffectOnce } from "#/hooks/use-effect-once";
|
||||
@@ -27,7 +30,7 @@ import {
|
||||
ResizablePanel,
|
||||
} from "#/components/layout/resizable-panel";
|
||||
import Security from "#/components/shared/modals/security/security";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useUserConversation } from "#/hooks/query/use-user-conversation";
|
||||
import { ServedAppLabel } from "#/components/layout/served-app-label";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { RootState } from "#/store";
|
||||
@@ -41,8 +44,10 @@ function AppContent() {
|
||||
useConversationConfig();
|
||||
const { t } = useTranslation();
|
||||
const { data: settings } = useSettings();
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: conversation, isFetched } = useActiveConversation();
|
||||
const { conversationId } = useConversation();
|
||||
const { data: conversation, isFetched } = useUserConversation(
|
||||
conversationId || null,
|
||||
);
|
||||
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const dispatch = useDispatch();
|
||||
@@ -208,7 +213,11 @@ function AppContent() {
|
||||
}
|
||||
|
||||
function App() {
|
||||
return <AppContent />;
|
||||
return (
|
||||
<ConversationProvider>
|
||||
<AppContent />
|
||||
</ConversationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -40,7 +40,6 @@ function LlmSettingsScreen() {
|
||||
const [dirtyInputs, setDirtyInputs] = React.useState({
|
||||
model: false,
|
||||
apiKey: false,
|
||||
searchApiKey: false,
|
||||
baseUrl: false,
|
||||
agent: false,
|
||||
confirmationMode: false,
|
||||
@@ -78,7 +77,6 @@ function LlmSettingsScreen() {
|
||||
setDirtyInputs({
|
||||
model: false,
|
||||
apiKey: false,
|
||||
searchApiKey: false,
|
||||
baseUrl: false,
|
||||
agent: false,
|
||||
confirmationMode: false,
|
||||
@@ -96,7 +94,6 @@ 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();
|
||||
@@ -105,7 +102,6 @@ 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,
|
||||
@@ -125,7 +121,6 @@ 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";
|
||||
@@ -140,7 +135,6 @@ 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,
|
||||
@@ -164,7 +158,6 @@ function LlmSettingsScreen() {
|
||||
setDirtyInputs({
|
||||
model: false,
|
||||
apiKey: false,
|
||||
searchApiKey: false,
|
||||
baseUrl: false,
|
||||
agent: false,
|
||||
confirmationMode: false,
|
||||
@@ -191,14 +184,6 @@ 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) => ({
|
||||
@@ -279,7 +264,7 @@ function LlmSettingsScreen() {
|
||||
<ModelSelector
|
||||
models={modelsAndProviders}
|
||||
currentModel={
|
||||
settings.LLM_MODEL || "anthropic/claude-sonnet-4-20250514"
|
||||
settings.LLM_MODEL || "anthropic/claude-3-5-sonnet-20241022"
|
||||
}
|
||||
onChange={handleModelIsDirty}
|
||||
/>
|
||||
@@ -306,29 +291,6 @@ 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>
|
||||
)}
|
||||
|
||||
@@ -342,9 +304,9 @@ function LlmSettingsScreen() {
|
||||
name="llm-custom-model-input"
|
||||
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
|
||||
defaultValue={
|
||||
settings.LLM_MODEL || "anthropic/claude-sonnet-4-20250514"
|
||||
settings.LLM_MODEL || "anthropic/claude-3-7-sonnet-20250219"
|
||||
}
|
||||
placeholder="anthropic/claude-sonnet-4-20250514"
|
||||
placeholder="anthropic/claude-3-7-sonnet-20250219"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
onChange={handleCustomModelIsDirty}
|
||||
@@ -376,35 +338,12 @@ function LlmSettingsScreen() {
|
||||
}
|
||||
/>
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor-advanced"
|
||||
testId="llm-api-key-help-anchor"
|
||||
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"
|
||||
|
||||
@@ -22,6 +22,7 @@ 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";
|
||||
|
||||
@@ -85,6 +86,9 @@ 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();
|
||||
|
||||
@@ -126,7 +130,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 && pathname !== "/") {
|
||||
if (!isOnTosPage && error?.status === 402) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [error?.status, pathname, isOnTosPage]);
|
||||
|
||||
@@ -3,12 +3,11 @@ import { Settings } from "#/types/settings";
|
||||
export const LATEST_SETTINGS_VERSION = 5;
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
LLM_MODEL: "anthropic/claude-sonnet-4-20250514",
|
||||
LLM_MODEL: "anthropic/claude-3-7-sonnet-20250219",
|
||||
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,
|
||||
@@ -17,7 +16,6 @@ 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: [],
|
||||
|
||||
@@ -20,7 +20,7 @@ export interface SystemMessageAction extends OpenHandsActionEvent<"system"> {
|
||||
}
|
||||
|
||||
export interface CommandAction extends OpenHandsActionEvent<"run"> {
|
||||
source: "agent" | "user";
|
||||
source: "agent";
|
||||
args: {
|
||||
command: string;
|
||||
security_risk: ActionSecurityRisk;
|
||||
|
||||
@@ -4,16 +4,12 @@ import {
|
||||
AssistantMessageAction,
|
||||
OpenHandsAction,
|
||||
SystemMessageAction,
|
||||
CommandAction,
|
||||
} from "./actions";
|
||||
import {
|
||||
AgentStateChangeObservation,
|
||||
CommandObservation,
|
||||
ErrorObservation,
|
||||
MCPObservation,
|
||||
OpenHandsObservation,
|
||||
} from "./observations";
|
||||
import { StatusUpdate } from "./variances";
|
||||
|
||||
export const isOpenHandsAction = (
|
||||
event: OpenHandsParsedEvent,
|
||||
@@ -42,15 +38,6 @@ export const isErrorObservation = (
|
||||
): event is ErrorObservation =>
|
||||
isOpenHandsObservation(event) && event.observation === "error";
|
||||
|
||||
export const isCommandAction = (
|
||||
event: OpenHandsParsedEvent,
|
||||
): event is CommandAction => isOpenHandsAction(event) && event.action === "run";
|
||||
|
||||
export const isAgentStateChangeObservation = (
|
||||
event: OpenHandsParsedEvent,
|
||||
): event is AgentStateChangeObservation =>
|
||||
isOpenHandsObservation(event) && event.observation === "agent_state_changed";
|
||||
|
||||
export const isCommandObservation = (
|
||||
event: OpenHandsParsedEvent,
|
||||
): event is CommandObservation =>
|
||||
@@ -70,13 +57,3 @@ 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;
|
||||
|
||||
@@ -6,12 +6,11 @@ export interface AgentStateChangeObservation
|
||||
source: "agent";
|
||||
extras: {
|
||||
agent_state: AgentState;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CommandObservation extends OpenHandsObservationEvent<"run"> {
|
||||
source: "agent" | "user";
|
||||
source: "agent";
|
||||
extras: {
|
||||
command: string;
|
||||
hidden?: boolean;
|
||||
@@ -136,7 +135,6 @@ export interface MCPObservation extends OpenHandsObservationEvent<"mcp"> {
|
||||
source: "agent";
|
||||
extras: {
|
||||
name: string;
|
||||
arguments: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,15 +33,7 @@ interface LocalUserMessageAction {
|
||||
};
|
||||
}
|
||||
|
||||
export interface StatusUpdate {
|
||||
status_update: true;
|
||||
type: "error";
|
||||
id: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type OpenHandsVariance =
|
||||
| TokenConfig
|
||||
| InitConfig
|
||||
| LocalUserMessageAction
|
||||
| StatusUpdate;
|
||||
| LocalUserMessageAction;
|
||||
|
||||
@@ -33,7 +33,6 @@ 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;
|
||||
@@ -42,7 +41,6 @@ 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;
|
||||
};
|
||||
@@ -54,7 +52,6 @@ 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;
|
||||
@@ -62,7 +59,6 @@ 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)[];
|
||||
@@ -73,12 +69,10 @@ 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;
|
||||
};
|
||||
|
||||
@@ -9,22 +9,3 @@ 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
|
||||
};
|
||||
|
||||
@@ -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)}&state=${encodeURIComponent(requestUrl.href)}`;
|
||||
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)}`;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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
|
||||
@@ -27,8 +29,50 @@ export const getLoginMethod = (): LoginMethod | null => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear login method and last page from local storage
|
||||
* 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
|
||||
*/
|
||||
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";
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export const TIPS: Tip[] = [
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$GITHUB_HOOK,
|
||||
link: "https://docs.all-hands.dev/modules/usage/cloud/cloud-issue-resolver",
|
||||
link: "https://docs.all-hands.dev/modules/usage/cloud/cloud-github-resolver",
|
||||
},
|
||||
{
|
||||
key: I18nKey.TIPS$BLOG_SIGNUP,
|
||||
|
||||
@@ -6,8 +6,6 @@ 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",
|
||||
];
|
||||
|
||||
@@ -41,6 +39,4 @@ 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",
|
||||
];
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 () => {
|
||||
@@ -71,7 +72,9 @@ export function renderWithProviders(
|
||||
})
|
||||
}
|
||||
>
|
||||
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
|
||||
<ConversationProvider>
|
||||
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
|
||||
</ConversationProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ from openhands.agenthub import ( # noqa: E402
|
||||
browsing_agent,
|
||||
codeact_agent,
|
||||
dummy_agent,
|
||||
loc_agent,
|
||||
readonly_agent,
|
||||
visualbrowsing_agent,
|
||||
)
|
||||
@@ -20,5 +19,4 @@ __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())
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user