mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
53 Commits
update-mic
...
resolver-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ffc2ff345 | ||
|
|
a7b234d1f6 | ||
|
|
2c2a721937 | ||
|
|
7abad5844a | ||
|
|
4781e9a424 | ||
|
|
a24d7e636e | ||
|
|
66b95adbc9 | ||
|
|
d617d6842a | ||
|
|
0eb7f956a9 | ||
|
|
d3154c4bae | ||
|
|
04a15b1467 | ||
|
|
b74da7d4c3 | ||
|
|
70ad469fb2 | ||
|
|
a85f6af9c2 | ||
|
|
5e213963dc | ||
|
|
051c579855 | ||
|
|
6d66b8503c | ||
|
|
0fb1a712d5 | ||
|
|
94fe052561 | ||
|
|
612bc3fa60 | ||
|
|
668906f079 | ||
|
|
c7dff3e4d2 | ||
|
|
6efb992bae | ||
|
|
fafbe81d51 | ||
|
|
dfe6f2d8cc | ||
|
|
743c814ee8 | ||
|
|
feb529b1d5 | ||
|
|
8f566a4247 | ||
|
|
0e4aeba47c | ||
|
|
d37e40caf8 | ||
|
|
8e4a8a65f8 | ||
|
|
e9027e2ae8 | ||
|
|
1fd0aefd20 | ||
|
|
722fabfa97 | ||
|
|
24f12eed12 | ||
|
|
dfa54673d2 | ||
|
|
76914e3c26 | ||
|
|
b0b820f8b2 | ||
|
|
5c8bdd364e | ||
|
|
0c1c570dac | ||
|
|
fa75b22cc0 | ||
|
|
8aeb4dd632 | ||
|
|
4c34a5f0f5 | ||
|
|
848f692033 | ||
|
|
2df4536420 | ||
|
|
d66bcf5021 | ||
|
|
4f5e146783 | ||
|
|
0c38fb0ceb | ||
|
|
7b0f880860 | ||
|
|
a156d5d243 | ||
|
|
c29b5e9757 | ||
|
|
5e5168ffd4 | ||
|
|
6aad23d35c |
4
.github/workflows/ghcr-build.yml
vendored
4
.github/workflows/ghcr-build.yml
vendored
@@ -40,9 +40,7 @@ jobs:
|
||||
# Only build nikolaik on PRs, otherwise build both nikolaik and ubuntu.
|
||||
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
|
||||
json=$(jq -n -c '[
|
||||
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
|
||||
{ image: "ubuntu:24.04", tag: "ubuntu" }
|
||||
|
||||
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" }
|
||||
]')
|
||||
else
|
||||
json=$(jq -n -c '[
|
||||
|
||||
@@ -121,7 +121,7 @@ A specialized prompt that enhances OpenHands with domain-specific knowledge, rep
|
||||
A central repository of available microagents and their configurations.
|
||||
|
||||
#### Public Microagent
|
||||
A general-purpose microagent available to all OpenHands users, triggered by specific keywords.
|
||||
A general-purpose microagent available to all OpenHands users, triggered by specific keywords. Located in `microagents/`.
|
||||
|
||||
#### Repository Microagent
|
||||
A type of microagent that provides repository-specific context and guidelines, stored in the `.openhands/microagents/` directory.
|
||||
|
||||
@@ -68,6 +68,29 @@ If you are starting a pull request (PR), please follow the template in `.github/
|
||||
|
||||
These details may or may not be useful for your current task.
|
||||
|
||||
### Microagents
|
||||
|
||||
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge and task-specific workflows. They are Markdown files that can include frontmatter for configuration.
|
||||
|
||||
#### Types:
|
||||
- **Public Microagents**: Located in `microagents/`, available to all users
|
||||
- **Repository Microagents**: Located in `.openhands/microagents/`, specific to this repository
|
||||
|
||||
#### Loading Behavior:
|
||||
- **Without frontmatter**: Always loaded into LLM context
|
||||
- **With triggers in frontmatter**: Only loaded when user's message matches the specified trigger keywords
|
||||
|
||||
#### Structure:
|
||||
```yaml
|
||||
---
|
||||
triggers:
|
||||
- keyword1
|
||||
- keyword2
|
||||
---
|
||||
# Microagent Content
|
||||
Your specialized knowledge and instructions here...
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
#### Action Handling:
|
||||
|
||||
@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
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.45-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.47-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
19
README.md
19
README.md
@@ -11,7 +11,7 @@
|
||||
<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="Join our Slack community"></a>
|
||||
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></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="Join our Discord community"></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="Credits"></a>
|
||||
<br/>
|
||||
@@ -62,17 +62,17 @@ system requirements and more information.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.47
|
||||
```
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
@@ -85,15 +85,14 @@ works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
|
||||
|
||||
## 💡 Other ways to run OpenHands
|
||||
|
||||
> [!CAUTION]
|
||||
> [!WARNING]
|
||||
> OpenHands is meant to be run by a single user on their local workstation.
|
||||
> It is not appropriate for multi-tenant deployments where multiple users share the same instance. There is no built-in authentication, isolation, or scalability.
|
||||
>
|
||||
> If you're interested in running OpenHands in a multi-tenant environment, please
|
||||
> [get in touch with us](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
> for advanced deployment options.
|
||||
> If you're interested in running OpenHands in a multi-tenant environment, check out the source-available, commercially-licensed
|
||||
> [OpenHands Cloud Helm Chart](https://github.com/all-Hands-AI/OpenHands-cloud)
|
||||
|
||||
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
|
||||
You can [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
|
||||
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/usage/how-to/headless-mode),
|
||||
interact with it via a [friendly CLI](https://docs.all-hands.dev/usage/how-to/cli-mode),
|
||||
or run it on tagged issues with [a github action](https://docs.all-hands.dev/usage/how-to/github-action).
|
||||
@@ -118,7 +117,7 @@ troubleshooting resources, and advanced configuration options.
|
||||
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
|
||||
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
|
||||
|
||||
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - Here we talk about research, architecture, and future development.
|
||||
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) - Here we talk about research, architecture, and future development.
|
||||
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
|
||||
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.
|
||||
|
||||
|
||||
10
README_CN.md
10
README_CN.md
@@ -12,7 +12,7 @@
|
||||
<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://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><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/>
|
||||
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.47
|
||||
```
|
||||
|
||||
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
|
||||
@@ -107,7 +107,7 @@ docker run -it --rm --pull=always \
|
||||
OpenHands是一个社区驱动的项目,我们欢迎每个人的贡献。我们大部分沟通
|
||||
通过Slack进行,因此这是开始的最佳场所,但我们也很乐意您通过Discord或Github与我们联系:
|
||||
|
||||
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - 这里我们讨论研究、架构和未来发展。
|
||||
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) - 这里我们讨论研究、架构和未来发展。
|
||||
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
|
||||
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<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://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><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/>
|
||||
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
|
||||
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.47
|
||||
```
|
||||
|
||||
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
|
||||
|
||||
156
REFACTOR_PLAN.md
Normal file
156
REFACTOR_PLAN.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Resolver Runtime Refactoring Plan
|
||||
|
||||
## Task Overview
|
||||
Refactor the resolver component to reuse setup.py functions for runtime initialization, connection, and completion instead of reinventing the wheel.
|
||||
|
||||
## Repository Cloning Patterns Analysis
|
||||
|
||||
### Repository Cloning Patterns Across OpenHands Entry Points
|
||||
|
||||
#### 1. **Resolver (issue_resolver.py)** - DIFFERENT PATTERN (Legacy)
|
||||
```python
|
||||
# Step 1: Clone to separate location
|
||||
subprocess.check_output(['git', 'clone', url, f'{output_dir}/repo'])
|
||||
|
||||
# Step 2: Later, copy repo to workspace
|
||||
shutil.copytree(os.path.join(self.output_dir, 'repo'), self.workspace_base)
|
||||
|
||||
# Step 3: Create and connect runtime
|
||||
runtime = create_runtime(config)
|
||||
await runtime.connect()
|
||||
|
||||
# Step 4: Initialize runtime (git config, setup scripts)
|
||||
self.initialize_runtime(runtime)
|
||||
```
|
||||
|
||||
#### 2. **Main.py** - STANDARD PATTERN
|
||||
```python
|
||||
# Step 1: Create and connect runtime
|
||||
runtime = create_runtime(config)
|
||||
await runtime.connect()
|
||||
|
||||
# Step 2: Clone directly into runtime workspace + setup
|
||||
repo_directory = initialize_repository_for_runtime(runtime, selected_repository)
|
||||
```
|
||||
|
||||
#### 3. **Server/Session** - STANDARD PATTERN
|
||||
```python
|
||||
# Step 1: Create and connect runtime
|
||||
# Step 2: Clone directly into runtime workspace
|
||||
await runtime.clone_or_init_repo(tokens, repo, branch)
|
||||
# Step 3: Run setup scripts
|
||||
await runtime.maybe_run_setup_script()
|
||||
await runtime.maybe_setup_git_hooks()
|
||||
```
|
||||
|
||||
#### 4. **Setup.py's initialize_repository_for_runtime()** - STANDARD PATTERN
|
||||
```python
|
||||
# Calls runtime.clone_or_init_repo() + setup scripts
|
||||
repo_directory = runtime.clone_or_init_repo(tokens, repo, branch)
|
||||
runtime.maybe_run_setup_script()
|
||||
runtime.maybe_setup_git_hooks()
|
||||
```
|
||||
|
||||
### The Issue
|
||||
The **resolver is the odd one out** - it uses a 2-step process (clone to temp location, then copy to workspace) due to **legacy reasons** (it was originally developed as a separate app built on OH, not a component of OH). All other entry points use the standard pattern (clone directly into runtime workspace).
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### ✅ What Resolver Already Does Right:
|
||||
- [x] Uses `create_runtime()` from setup.py for runtime creation
|
||||
|
||||
### ❌ What Needs to be Fixed:
|
||||
- [ ] **Resolver uses legacy 2-step cloning instead of standard runtime.clone_or_init_repo()**
|
||||
- [ ] Resolver has custom `initialize_runtime()` method that duplicates setup.py logic
|
||||
- [ ] Resolver has custom `complete_runtime()` method with no setup.py equivalent
|
||||
- [ ] Resolver doesn't follow proper runtime cleanup patterns like main.py
|
||||
- [ ] Runtime connection pattern is inconsistent across codebase
|
||||
|
||||
## Refactoring Steps
|
||||
|
||||
### Phase 1: Fix Repository Cloning Pattern (PRIORITY)
|
||||
**Goal**: Make resolver use the same repository cloning pattern as all other OpenHands entry points.
|
||||
|
||||
- [ ] **Step 1.1**: Replace resolver's legacy 2-step cloning with standard pattern
|
||||
- Remove `subprocess.check_output(['git', 'clone', ...])` from `resolve_issue()`
|
||||
- Remove `shutil.copytree()` from `process_issue()`
|
||||
- Use `initialize_repository_for_runtime()` instead
|
||||
- This will clone directly into runtime workspace AND run setup scripts
|
||||
|
||||
- [ ] **Step 1.2**: Update resolver workflow to match standard pattern
|
||||
- Create and connect runtime first
|
||||
- Then call `initialize_repository_for_runtime()` for cloning + setup
|
||||
- Remove the manual repo copying step entirely
|
||||
- Ensure base_commit is still captured correctly
|
||||
|
||||
### Phase 2: Refactor Runtime Initialization and Completion
|
||||
**Goal**: Remove code duplication between resolver and setup.py for runtime operations.
|
||||
|
||||
- [ ] **Step 2.1**: Create missing functions in setup.py
|
||||
- Create `setup_runtime_environment()` for git config and platform-specific setup
|
||||
- Create `complete_runtime_session()` for git patch generation
|
||||
- Create `cleanup_runtime()` for proper resource cleanup
|
||||
|
||||
- [ ] **Step 2.2**: Replace resolver's `initialize_runtime()`
|
||||
- Use setup.py's `setup_runtime_environment()` instead
|
||||
- Remove duplicate git configuration code
|
||||
- Maintain platform-specific behavior (GitLab CI)
|
||||
|
||||
- [ ] **Step 2.3**: Replace resolver's `complete_runtime()`
|
||||
- Use setup.py's `complete_runtime_session()` instead
|
||||
- Move git patch generation logic to setup.py
|
||||
- Ensure return values match resolver's expectations
|
||||
|
||||
- [ ] **Step 2.4**: Add proper runtime cleanup to resolver
|
||||
- Use setup.py's `cleanup_runtime()` function
|
||||
- Ensure resources are properly released in try/finally blocks
|
||||
|
||||
### Phase 3: Testing and Validation
|
||||
- [ ] **Step 3.1**: Test resolver functionality with refactored code
|
||||
- Verify git operations work correctly
|
||||
- Verify setup scripts are executed
|
||||
- Verify git hooks are set up
|
||||
|
||||
- [ ] **Step 3.2**: Test runtime lifecycle (create → connect → clone → initialize → complete → cleanup)
|
||||
- Ensure no resource leaks
|
||||
- Verify proper error handling
|
||||
|
||||
- [ ] **Step 3.3**: Verify resolver output remains consistent
|
||||
- Git patches are generated correctly
|
||||
- Issue resolution works as before
|
||||
- No regression in functionality
|
||||
|
||||
### Phase 4: Code Quality and Documentation
|
||||
- [ ] **Step 4.1**: Add proper documentation to new setup.py functions
|
||||
- Document parameters and return values
|
||||
- Add usage examples
|
||||
- Document platform-specific behavior
|
||||
|
||||
- [ ] **Step 4.2**: Remove obsolete code from resolver
|
||||
- Delete old `initialize_runtime()` method
|
||||
- Delete old `complete_runtime()` method
|
||||
- Clean up imports and unused code
|
||||
|
||||
- [ ] **Step 4.3**: Update any other components that might benefit from these functions
|
||||
- Check if other entry points could use the same patterns
|
||||
- Ensure consistency across the codebase
|
||||
|
||||
## Success Criteria
|
||||
- [ ] **Resolver uses standard repository cloning pattern (runtime.clone_or_init_repo)**
|
||||
- [ ] Resolver uses setup.py functions for all runtime operations
|
||||
- [ ] No code duplication between resolver and setup.py
|
||||
- [ ] Proper runtime lifecycle management (connect → initialize → complete → cleanup)
|
||||
- [ ] All existing resolver functionality preserved
|
||||
- [ ] Consistent patterns across all OpenHands entry points
|
||||
- [ ] Proper error handling and resource cleanup
|
||||
|
||||
## Files to Modify
|
||||
1. `/openhands/core/setup.py` - Add new runtime management functions
|
||||
2. `/openhands/resolver/issue_resolver.py` - Refactor to use setup.py functions
|
||||
3. Any tests related to resolver functionality
|
||||
|
||||
## Risk Mitigation
|
||||
- Maintain backward compatibility during refactoring
|
||||
- Test thoroughly before removing old code
|
||||
- Keep git patch generation logic identical to avoid breaking issue resolution
|
||||
- Ensure platform-specific behavior (GitLab CI) is preserved
|
||||
@@ -10,18 +10,7 @@
|
||||
# General core configurations
|
||||
##############################################################################
|
||||
[core]
|
||||
# API key for E2B
|
||||
#e2b_api_key = ""
|
||||
|
||||
# API key for Modal
|
||||
#modal_api_token_id = ""
|
||||
#modal_api_token_secret = ""
|
||||
|
||||
# API key for Daytona
|
||||
#daytona_api_key = ""
|
||||
|
||||
# Daytona Target
|
||||
#daytona_target = ""
|
||||
# API keys and configuration for core services
|
||||
|
||||
# Base path for the workspace
|
||||
#workspace_base = "./workspace"
|
||||
@@ -201,6 +190,27 @@ model = "gpt-4o"
|
||||
#native_tool_calling = None
|
||||
|
||||
|
||||
# Safety settings for models that support them (e.g., Mistral AI, Gemini)
|
||||
# Example for Mistral AI:
|
||||
# safety_settings = [
|
||||
# { "category" = "hate", "threshold" = "low" },
|
||||
# { "category" = "harassment", "threshold" = "low" },
|
||||
# { "category" = "sexual", "threshold" = "low" },
|
||||
# { "category" = "dangerous", "threshold" = "low" }
|
||||
# ]
|
||||
#
|
||||
# Example for Gemini:
|
||||
# safety_settings = [
|
||||
# { "category" = "HARM_CATEGORY_HARASSMENT", "threshold" = "BLOCK_NONE" },
|
||||
# { "category" = "HARM_CATEGORY_HATE_SPEECH", "threshold" = "BLOCK_NONE" },
|
||||
# { "category" = "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold" = "BLOCK_NONE" },
|
||||
# { "category" = "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold" = "BLOCK_NONE" }
|
||||
# ]
|
||||
#safety_settings = []
|
||||
|
||||
[llm.draft_editor]
|
||||
# The number of times llm_editor tries to fix an error when editing.
|
||||
correct_num = 5
|
||||
|
||||
[llm.gpt4o-mini]
|
||||
api_key = ""
|
||||
@@ -250,6 +260,9 @@ enable_finish = true
|
||||
# length limit
|
||||
enable_history_truncation = true
|
||||
|
||||
# Whether the condensation request tool is enabled
|
||||
enable_condensation_request = false
|
||||
|
||||
[agent.RepoExplorerAgent]
|
||||
# Example: use a cheaper model for RepoExplorerAgent to reduce cost, especially
|
||||
# useful when an agent doesn't demand high quality but uses a lot of tokens
|
||||
@@ -318,6 +331,9 @@ classpath = "my_package.my_module.MyCustomAgent"
|
||||
# Enable GPU support in the runtime
|
||||
#enable_gpu = false
|
||||
|
||||
# When there are multiple cards, you can specify the GPU by ID
|
||||
#cuda_visible_devices = ''
|
||||
|
||||
# Additional Docker runtime kwargs
|
||||
#docker_runtime_kwargs = {}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
- DOCKER_HOST_ADDR=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.45-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.47-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -3,9 +3,9 @@ repos:
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
exclude: docs/modules/python
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/)
|
||||
- id: end-of-file-fixer
|
||||
exclude: docs/modules/python
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/)
|
||||
- id: check-yaml
|
||||
args: ["--allow-multiple-documents"]
|
||||
- id: debug-statements
|
||||
@@ -28,17 +28,19 @@ repos:
|
||||
entry: ruff check --config dev_config/python/ruff.toml
|
||||
types_or: [python, pyi, jupyter]
|
||||
args: [--fix, --unsafe-fixes]
|
||||
exclude: third_party/
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
entry: ruff format --config dev_config/python/ruff.toml
|
||||
types_or: [python, pyi, jupyter]
|
||||
exclude: third_party/
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.15.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, lxml]
|
||||
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, pydantic, lxml]
|
||||
# To see gaps add `--html-report mypy-report/`
|
||||
entry: mypy --config-file dev_config/python/mypy.ini openhands/
|
||||
always_run: true
|
||||
|
||||
@@ -7,3 +7,9 @@ warn_unreachable = True
|
||||
warn_redundant_casts = True
|
||||
no_implicit_optional = True
|
||||
strict_optional = True
|
||||
|
||||
# Exclude third-party runtime directory from type checking
|
||||
exclude = third_party/
|
||||
|
||||
[mypy-openhands.memory.condenser.impl.*]
|
||||
disable_error_code = override
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Exclude third-party runtime directory from linting
|
||||
exclude = ["third_party/"]
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
"E",
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"slack": "https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A",
|
||||
"slack": "https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA",
|
||||
"github": "https://github.com/All-Hands-AI/OpenHands",
|
||||
"discord": "https://discord.gg/ESHStjSjD4"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,15 @@ title: Slack Integration (Beta)
|
||||
description: This guide walks you through installing the OpenHands Slack app.
|
||||
---
|
||||
|
||||
<iframe
|
||||
className="w-full aspect-video"
|
||||
src="https://www.youtube.com/embed/hbloGmfZsJ4"
|
||||
title="OpenHands Slack Integration Tutorial"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen>
|
||||
</iframe>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Access to OpenHands Cloud.
|
||||
|
||||
@@ -1,28 +1,17 @@
|
||||
---
|
||||
title: Configuration Options
|
||||
description: This page outlines all available configuration options for OpenHands, allowing you to customize its behavior and integrate it with other services. In GUI Mode, any settings applied through the Settings UI will take precedence.
|
||||
description: This page outlines all available configuration options for OpenHands, allowing you to customize its
|
||||
behavior and integrate it with other services.
|
||||
---
|
||||
|
||||
<Note>
|
||||
In GUI Mode, any settings applied through the Settings UI will take precedence.
|
||||
</Note>
|
||||
|
||||
## Core Configuration
|
||||
|
||||
The core configuration options are defined in the `[core]` section of the `config.toml` file.
|
||||
|
||||
### API Keys
|
||||
- `e2b_api_key`
|
||||
- Type: `str`
|
||||
- Default: `""`
|
||||
- Description: API key for E2B
|
||||
|
||||
- `modal_api_token_id`
|
||||
- Type: `str`
|
||||
- Default: `""`
|
||||
- Description: API token ID for Modal
|
||||
|
||||
- `modal_api_token_secret`
|
||||
- Type: `str`
|
||||
- Default: `""`
|
||||
- Description: API token secret for Modal
|
||||
|
||||
### Workspace
|
||||
- `workspace_base` **(Deprecated)**
|
||||
- Type: `str`
|
||||
|
||||
@@ -88,7 +88,7 @@ If you would like to set things up more systematically, you can:
|
||||
1. **Search existing issues**: Check our [GitHub issues](https://github.com/All-Hands-AI/OpenHands/issues) to see if
|
||||
others have encountered the same problem.
|
||||
2. **Join our community**: Get help from other users and developers:
|
||||
- [Slack community](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A)
|
||||
- [Slack community](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA)
|
||||
- [Discord server](https://discord.gg/ESHStjSjD4)
|
||||
3. **Check our troubleshooting guide**: Common issues and solutions are documented in
|
||||
[Troubleshooting](/usage/troubleshooting/troubleshooting).
|
||||
|
||||
@@ -7,6 +7,15 @@ description: The Command-Line Interface (CLI) provides a powerful interface that
|
||||
This mode is different from the [headless mode](/usage/how-to/headless-mode), which is non-interactive and better
|
||||
for scripting.
|
||||
|
||||
<iframe
|
||||
className="w-full aspect-video"
|
||||
src="https://www.youtube.com/embed/PfvIx4y8h7w"
|
||||
title="OpenHands CLI Tutorial"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen>
|
||||
</iframe>
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Running with Python
|
||||
@@ -14,34 +23,34 @@ for scripting.
|
||||
**Note** - OpenHands requires Python version 3.12 or higher (Python 3.14 is not currently supported)
|
||||
|
||||
1. Install OpenHands using pip:
|
||||
|
||||
```bash
|
||||
pip install openhands-ai
|
||||
```
|
||||
|
||||
Or if you prefer not to manage your own Python environment, you can use `uvx`:
|
||||
Or if you prefer not to manage your own Python environment, you can use `uvx`:
|
||||
|
||||
```bash
|
||||
uvx --python 3.12 --from openhands-ai openhands
|
||||
```
|
||||
|
||||
2. Launch an interactive OpenHands conversation from the command line:
|
||||
|
||||
```bash
|
||||
openhands
|
||||
```
|
||||
|
||||
<Note>
|
||||
If you have cloned the repository, you can also run the CLI directly using Poetry:
|
||||
|
||||
poetry run python -m openhands.cli.main
|
||||
</Note>
|
||||
|
||||
3. Set your model, API key, and other preferences using the UI (or alternatively environment variables, below).
|
||||
|
||||
This command opens an interactive prompt where you can type tasks or commands and get responses from OpenHands.
|
||||
The first time you run the CLI, it will take you through configuring the required LLM
|
||||
settings. These will be saved for future sessions.
|
||||
|
||||
#### For Developers
|
||||
|
||||
If you have cloned the repository, you can run the CLI directly using Poetry:
|
||||
|
||||
```bash
|
||||
poetry run python -m openhands.cli.main
|
||||
```
|
||||
The conversation history will be saved in `~/.openhands/sessions`.
|
||||
|
||||
### Running with Docker
|
||||
|
||||
@@ -55,7 +64,7 @@ poetry run python -m openhands.cli.main
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -64,16 +73,21 @@ docker run -it \
|
||||
-v ~/.openhands:/.openhands \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.47 \
|
||||
python -m openhands.cli.main --override-cli-mode true
|
||||
```
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
<Note>
|
||||
If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your
|
||||
conversation history to the new location.
|
||||
</Note>
|
||||
|
||||
This launches the CLI in Docker, allowing you to interact with OpenHands as described above.
|
||||
This launches the CLI in Docker, allowing you to interact with OpenHands.
|
||||
|
||||
The `-e SANDBOX_USER_ID=$(id -u)` ensures files created by the agent in your workspace have the correct permissions.
|
||||
|
||||
The conversation history will be saved in `~/.openhands/sessions`.
|
||||
|
||||
## Interactive CLI Overview
|
||||
|
||||
### What is CLI Mode?
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Custom Sandbox
|
||||
description: This guide is for users that would like to use their own custom Docker image for the runtime. For example, with certain tools or programming languages pre-installed.
|
||||
description: This guide is for users that would like to use their own custom Docker image for the runtime.
|
||||
For example, with certain tools or programming languages pre-installed.
|
||||
---
|
||||
|
||||
The sandbox is where the agent performs its tasks. Instead of running commands directly on your computer
|
||||
|
||||
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -42,7 +42,7 @@ docker run -it \
|
||||
-v ~/.openhands:/.openhands \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.47 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
|
||||
@@ -73,6 +73,15 @@ We have a few guides for running OpenHands with specific model providers:
|
||||
- [OpenAI](/usage/llms/openai-llms)
|
||||
- [OpenRouter](/usage/llms/openrouter)
|
||||
|
||||
## Model Customization
|
||||
|
||||
LLM providers have specific settings that can be customized to optimize their performance with OpenHands, such as:
|
||||
|
||||
- **Custom Tokenizers**: For specialized models, you can add a suitable tokenizer
|
||||
- **Native Tool Calling**: Toggle native function/tool calling capabilities
|
||||
|
||||
For detailed information about model customization, see [LLM Configuration Options](configuration-options#llm-customization).
|
||||
|
||||
### API retries and rate limits
|
||||
|
||||
LLM providers typically have rate limits, sometimes very low, and may require retries. OpenHands will automatically
|
||||
|
||||
@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
|
||||
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.47
|
||||
```
|
||||
|
||||
2. Wait until the server is running (see log below):
|
||||
```
|
||||
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.47
|
||||
Starting OpenHands...
|
||||
Running OpenHands as root
|
||||
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
|
||||
@@ -119,7 +119,7 @@ When started for the first time, OpenHands will prompt you to set up the LLM pro
|
||||
|
||||
That's it! You can now start using OpenHands with the local LLM server.
|
||||
|
||||
If you encounter any issues, let us know on [Slack](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) or [Discord](https://discord.gg/ESHStjSjD4).
|
||||
If you encounter any issues, let us know on [Slack](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) or [Discord](https://discord.gg/ESHStjSjD4).
|
||||
|
||||
## Advanced: Alternative LLM Backends
|
||||
|
||||
|
||||
@@ -67,17 +67,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
### Start the App
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.47
|
||||
```
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Model Context Protocol (MCP)
|
||||
description: This page outlines how to configure and use the Model Context Protocol (MCP) in OpenHands, allowing you to extend the agent's capabilities with custom tools.
|
||||
description: This page outlines how to configure and use the Model Context Protocol (MCP) in OpenHands, allowing you
|
||||
to extend the agent's capabilities with custom tools.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -11,6 +11,8 @@ accordingly. However, they are applied to all repositories belonging to the orga
|
||||
Add a `.openhands` repository under the organization or user and create a `microagents` directory and place the
|
||||
microagents in that directory.
|
||||
|
||||
For GitLab organizations, use `openhands-config` as the repository name instead of `.openhands`, since GitLab doesn't support repository names starting with non-alphanumeric characters.
|
||||
|
||||
## Example
|
||||
|
||||
General microagent file example for organization `Great-Co` located inside the `.openhands` repository:
|
||||
@@ -20,3 +22,5 @@ General microagent file example for organization `Great-Co` located inside the `
|
||||
* Document interfaces and public APIs; use implementation comments only for non-obvious logic.
|
||||
* Follow the same naming convention for variables, classes, constants, etc. already used in each repository.
|
||||
```
|
||||
|
||||
For GitLab organizations, the same microagent would be located inside the `openhands-config` repository.
|
||||
|
||||
@@ -3,7 +3,6 @@ title: Daytona Runtime
|
||||
description: You can use [Daytona](https://www.daytona.io/) as a runtime provider.
|
||||
---
|
||||
|
||||
|
||||
## Step 1: Retrieve Your Daytona API Key
|
||||
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
|
||||
2. Click **"Create Key"**.
|
||||
|
||||
@@ -3,8 +3,6 @@ title: Docker Runtime
|
||||
description: This is the default Runtime that's used when you start OpenHands.
|
||||
---
|
||||
|
||||
This is the default Runtime that's used when you start OpenHands.
|
||||
|
||||
## Image
|
||||
The `SANDBOX_RUNTIME_CONTAINER_IMAGE` from nikolaik is a pre-built runtime image
|
||||
that contains our Runtime server, as well as some basic utilities for Python and NodeJS.
|
||||
|
||||
@@ -3,7 +3,8 @@ title: E2B Runtime
|
||||
description: E2B is an open-source secure cloud environment (sandbox) made for running AI-generated code and agents.
|
||||
---
|
||||
|
||||
[E2B](https://e2b.dev) offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b) SDK to spawn and control these sandboxes.
|
||||
[E2B](https://e2b.dev) offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b)
|
||||
SDK to spawn and control these sandboxes.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -18,9 +19,13 @@ description: E2B is an open-source secure cloud environment (sandbox) made for r
|
||||
Full CLI API is [here](https://e2b.dev/docs/cli/installation).
|
||||
|
||||
## OpenHands sandbox
|
||||
You can use the E2B CLI to create a custom sandbox with a Dockerfile. Read the full guide [here](https://e2b.dev/docs/guide/custom-sandbox). The premade OpenHands sandbox for E2B is set up in the `containers` directory. and it's called `openhands`.
|
||||
|
||||
You can use the E2B CLI to create a custom sandbox with a Dockerfile. Read the full guide
|
||||
[here](https://e2b.dev/docs/guide/custom-sandbox). The premade OpenHands sandbox for E2B is set up in the `containers`
|
||||
directory. and it's called `openhands`.
|
||||
|
||||
## Debugging
|
||||
|
||||
You can connect to a running E2B sandbox with E2B CLI in your terminal.
|
||||
|
||||
- List all running sandboxes (based on your API key)
|
||||
@@ -34,5 +39,6 @@ You can connect to a running E2B sandbox with E2B CLI in your terminal.
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [E2B Docs](https://e2b.dev/docs)
|
||||
- [E2B GitHub](https://github.com/e2b-dev/e2b)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
title: Local Runtime
|
||||
description: The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios where Docker is not available.
|
||||
description: The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without
|
||||
using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios
|
||||
where Docker is not available.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
|
||||
@@ -9,8 +9,6 @@ commands.
|
||||
By default, OpenHands uses a [Docker-based runtime](/usage/runtimes/docker), running on your local computer.
|
||||
This means you only have to pay for the LLM you're using, and your code is only ever sent to the LLM.
|
||||
|
||||
We also support other runtimes, which are typically managed by third-parties.
|
||||
|
||||
Additionally, we provide a [Local Runtime](/usage/runtimes/local) that runs directly on your machine without Docker,
|
||||
which can be useful in controlled environments like CI pipelines.
|
||||
|
||||
@@ -21,6 +19,18 @@ OpenHands supports several different runtime environments:
|
||||
- [Docker Runtime](/usage/runtimes/docker) - The default runtime that uses Docker containers for isolation (recommended for most users).
|
||||
- [OpenHands Remote Runtime](/usage/runtimes/remote) - Cloud-based runtime for parallel execution (beta).
|
||||
- [Local Runtime](/usage/runtimes/local) - Direct execution on your local machine without Docker.
|
||||
- And more third-party runtimes:
|
||||
- [Modal Runtime](/usage/runtimes/modal) - Runtime provided by our partners at Modal.
|
||||
- [Daytona Runtime](/usage/runtimes/daytona) - Runtime provided by Daytona.
|
||||
|
||||
### Third-Party Runtimes
|
||||
|
||||
The following third-party runtimes are available when you install the `third_party_runtimes` extra:
|
||||
|
||||
```bash
|
||||
pip install openhands-ai[third_party_runtimes]
|
||||
```
|
||||
|
||||
- [E2B Runtime](/usage/runtimes/e2b) - Open source runtime using E2B sandboxes.
|
||||
- [Modal Runtime](/usage/runtimes/modal) - Serverless runtime using Modal infrastructure.
|
||||
- [Runloop Runtime](/usage/runtimes/runloop) - Cloud runtime using Runloop infrastructure.
|
||||
- [Daytona Runtime](/usage/runtimes/daytona) - Development environment runtime using Daytona.
|
||||
|
||||
**Note**: These third-party runtimes are supported by their respective developers, not by the OpenHands team. For issues specific to these runtimes, please refer to their documentation or contact their support teams.
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
title: Remote Runtime
|
||||
description: This runtime is specifically designed for agent evaluation purposes only through the [OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be used to launch production OpenHands applications.
|
||||
description: This runtime is specifically designed for agent evaluation purposes only through the
|
||||
[OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be
|
||||
used to launch production OpenHands applications.
|
||||
---
|
||||
|
||||
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes
|
||||
in parallel in the cloud. Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
|
||||
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details),
|
||||
it allows you to launch runtimes in parallel in the cloud. Fill out
|
||||
[this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to
|
||||
apply if you want to try this out!
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Runloop Runtime
|
||||
description: Runloop provides a fast, secure and scalable AI sandbox (Devbox). Check out the [runloop docs](https://docs.runloop.ai/overview/what-is-runloop) for more detail.
|
||||
description: Runloop provides a fast, secure and scalable AI sandbox (Devbox). Check out the
|
||||
[runloop docs](https://docs.runloop.ai/overview/what-is-runloop) for more detail.
|
||||
---
|
||||
|
||||
## Access
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Search Engine Setup
|
||||
description: Configure OpenHands to use Tavily as a search engine
|
||||
description: Configure OpenHands to use Tavily as a search engine.
|
||||
---
|
||||
|
||||
## Setting Up Search Engine in OpenHands
|
||||
@@ -11,10 +11,10 @@ OpenHands can be configured to use [Tavily](https://tavily.com/) as a search eng
|
||||
|
||||
To use the search functionality in OpenHands, you'll need to obtain a Tavily API key:
|
||||
|
||||
1. Visit [Tavily's website](https://tavily.com/) and sign up for an account
|
||||
2. Navigate to the API section in your dashboard
|
||||
3. Generate a new API key
|
||||
4. Copy the API key (it should start with `tvly-`)
|
||||
1. Visit [Tavily's website](https://tavily.com/) and sign up for an account.
|
||||
2. Navigate to the API section in your dashboard.
|
||||
3. Generate a new API key.
|
||||
4. Copy the API key (it should start with `tvly-`).
|
||||
|
||||
### Configuring Search in OpenHands
|
||||
|
||||
@@ -22,13 +22,12 @@ Once you have your Tavily API key, you can configure OpenHands to use it:
|
||||
|
||||
#### In the OpenHands UI
|
||||
|
||||
1. Open OpenHands and navigate to the Settings page by clicking the gear icon
|
||||
2. In the LLM settings tab, locate the "Search API Key (Tavily)" field
|
||||
3. Enter your Tavily API key (starting with `tvly-`)
|
||||
4. Click "Save" to apply the changes
|
||||
1. Open OpenHands and navigate to the Settings page.
|
||||
2. Under the `LLM` tab, enter your Tavily API key (starting with `tvly-`) in the `Search API Key (Tavily)` field.
|
||||
3. Click `Save` to apply the changes.
|
||||
|
||||
<Note>
|
||||
The search API key field is optional. If you don't provide a key, the search functionality will not be available to the agent.
|
||||
The search API key field is optional. If you don't provide a key, the search functionality will not be available to the agent.
|
||||
</Note>
|
||||
|
||||
#### Using Configuration Files
|
||||
@@ -45,22 +44,23 @@ search_api_key = "tvly-your-api-key-here"
|
||||
|
||||
When the search engine is configured:
|
||||
|
||||
1. The agent can decide to search the web when it needs external information
|
||||
2. Search queries are sent to Tavily's API via [Tavily's MCP server](https://github.com/tavily-ai/tavily-mcp) which includes a variety of [tools](https://docs.tavily.com/documentation/api-reference/introduction) (search, extract, crawl, map).
|
||||
3. Results are returned and incorporated into the agent's context
|
||||
4. The agent can use this information to provide more accurate and up-to-date responses
|
||||
- The agent can decide to search the web when it needs external information.
|
||||
- Search queries are sent to Tavily's API via [Tavily's MCP server](https://github.com/tavily-ai/tavily-mcp) which
|
||||
includes a variety of [tools](https://docs.tavily.com/documentation/api-reference/introduction) (search, extract, crawl, map).
|
||||
- Results are returned and incorporated into the agent's context.
|
||||
- The agent can use this information to provide more accurate and up-to-date responses.
|
||||
|
||||
### Limitations
|
||||
|
||||
- Search results depend on Tavily's coverage and freshness
|
||||
- Usage may be subject to Tavily's rate limits and pricing tiers
|
||||
- The agent will only search when it determines that external information is needed
|
||||
- Search results depend on Tavily's coverage and freshness.
|
||||
- Usage may be subject to Tavily's rate limits and pricing tiers.
|
||||
- The agent will only search when it determines that external information is needed.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you encounter issues with the search functionality:
|
||||
|
||||
- Verify that your API key is correct and active
|
||||
- Check that your API key starts with `tvly-`
|
||||
- Ensure you have an active internet connection
|
||||
- Check Tavily's status page for any service disruptions
|
||||
- Verify that your API key is correct and active.
|
||||
- Check that your API key starts with `tvly-`.
|
||||
- Ensure you have an active internet connection.
|
||||
- Check Tavily's status page for any service disruptions.
|
||||
|
||||
@@ -3,13 +3,20 @@ import copy
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
import huggingface_hub
|
||||
import pandas as pd
|
||||
from datasets import load_dataset
|
||||
from PIL import Image
|
||||
from pydantic import SecretStr
|
||||
|
||||
from evaluation.benchmarks.gaia.scorer import question_scorer
|
||||
from evaluation.benchmarks.gaia.utils import (
|
||||
image_to_jpg_base64_url,
|
||||
image_to_png_base64_url,
|
||||
)
|
||||
from evaluation.utils.shared import (
|
||||
EvalMetadata,
|
||||
EvalOutput,
|
||||
@@ -97,27 +104,44 @@ def initialize_runtime(
|
||||
if instance['file_name'] != '':
|
||||
# if this question comes with a file, we need to save it to the workspace
|
||||
assert metadata.data_split is not None
|
||||
extension_name = instance['file_name'].split('.')[-1]
|
||||
src_file = os.path.join(
|
||||
DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
|
||||
)
|
||||
assert os.path.exists(src_file)
|
||||
dest_file = os.path.join('/workspace', instance['file_name'])
|
||||
runtime.copy_to(src_file, dest_file)
|
||||
if extension_name == 'zip':
|
||||
temp_dir = os.path.join(
|
||||
DATASET_CACHE_DIR, '2023', metadata.data_split, 'tmp_file'
|
||||
)
|
||||
os.makedirs(temp_dir, exist_ok=True)
|
||||
with zipfile.ZipFile(src_file, 'r') as zip_ref:
|
||||
zip_ref.extractall(temp_dir)
|
||||
for root, dirs, files in os.walk(temp_dir):
|
||||
for file in files:
|
||||
dest_file = '/workspace'
|
||||
runtime.copy_to(os.path.join(root, file), dest_file)
|
||||
shutil.rmtree(temp_dir)
|
||||
elif extension_name not in ['jpg', 'png']:
|
||||
dest_file = '/workspace'
|
||||
runtime.copy_to(src_file, dest_file)
|
||||
|
||||
# rename to file.extension_name
|
||||
extension_name = instance['file_name'].split('.')[-1]
|
||||
action = CmdRunAction(
|
||||
command=f'mv /workspace/{instance["file_name"]} /workspace/file.{extension_name}'
|
||||
)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
assert obs.exit_code == 0
|
||||
# rename to file.extension_name
|
||||
action = CmdRunAction(
|
||||
command=f'mv /workspace/{instance["file_name"]} /workspace/file.{extension_name}'
|
||||
)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(command='cd /workspace')
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(
|
||||
command='apt-get update && apt-get install -y ffmpeg && apt-get install -y ffprobe'
|
||||
)
|
||||
runtime.run_action(action)
|
||||
logger.info(f'{"-" * 50} END Runtime Initialization Fn {"-" * 50}')
|
||||
|
||||
|
||||
@@ -151,8 +175,31 @@ Here is the task:
|
||||
task_question=instance['Question'],
|
||||
)
|
||||
logger.info(f'Instruction: {instruction}')
|
||||
image_urls = []
|
||||
if dest_file:
|
||||
instruction += f'\n\nThe mentioned file is provided in the workspace at: {dest_file.split("/")[-1]}'
|
||||
if extension_name not in ['jpg', 'png', 'zip']:
|
||||
instruction += f'To solve this task you will have to use the attached file provided in the workspace at location: {dest_file}\n\n'
|
||||
elif extension_name == 'zip':
|
||||
filenames = []
|
||||
src_file = os.path.join(
|
||||
DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
|
||||
)
|
||||
with zipfile.ZipFile(src_file, 'r') as zip_ref:
|
||||
filenames = zip_ref.namelist()
|
||||
|
||||
filenames = [f'/workspace/{file}' for file in filenames]
|
||||
filenames = ', '.join(filenames)
|
||||
instruction += f'To solve this task you will have to use the attached files provided in the workspace at locations: {filenames}\n\n'
|
||||
else: # Image files: jpg, png
|
||||
src_file = os.path.join(
|
||||
DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
|
||||
)
|
||||
instruction += 'Image: To solve this task you will have to use the image shown below.\n\n'
|
||||
image = Image.open(src_file)
|
||||
if extension_name == 'jpg':
|
||||
image_urls.append(image_to_jpg_base64_url(image))
|
||||
else:
|
||||
image_urls.append(image_to_png_base64_url(image))
|
||||
|
||||
instruction += """IMPORTANT: When seeking information from a website, REFRAIN from arbitrary URL navigation. You should utilize the designated search engine tool with precise keywords to obtain relevant URLs or use the specific website's search interface. DO NOT navigate directly to specific URLs as they may not exist.\n\nFor example: if you want to search for a research paper on Arxiv, either use the search engine tool with specific keywords or navigate to arxiv.org and then use its interface.\n"""
|
||||
instruction += 'IMPORTANT: You should NEVER ask for Human Help.\n'
|
||||
@@ -174,7 +221,9 @@ Here is the task:
|
||||
state: State | None = asyncio.run(
|
||||
run_controller(
|
||||
config=config,
|
||||
initial_user_action=MessageAction(content=instruction),
|
||||
initial_user_action=MessageAction(
|
||||
content=instruction, image_urls=image_urls
|
||||
),
|
||||
runtime=runtime,
|
||||
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
|
||||
metadata.agent_class
|
||||
|
||||
43
evaluation/benchmarks/gaia/utils.py
Normal file
43
evaluation/benchmarks/gaia/utils.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import base64
|
||||
import io
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def image_to_png_base64_url(
|
||||
image: np.ndarray | Image.Image, add_data_prefix: bool = True
|
||||
):
|
||||
"""Convert a numpy array to a base64 encoded png image url."""
|
||||
if isinstance(image, np.ndarray):
|
||||
image = Image.fromarray(image)
|
||||
if image.mode in ('RGBA', 'LA'):
|
||||
image = image.convert('RGB')
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format='PNG')
|
||||
|
||||
image_base64 = base64.b64encode(buffered.getvalue()).decode()
|
||||
return (
|
||||
f'data:image/png;base64,{image_base64}'
|
||||
if add_data_prefix
|
||||
else f'{image_base64}'
|
||||
)
|
||||
|
||||
|
||||
def image_to_jpg_base64_url(
|
||||
image: np.ndarray | Image.Image, add_data_prefix: bool = True
|
||||
):
|
||||
"""Convert a numpy array to a base64 encoded jpeg image url."""
|
||||
if isinstance(image, np.ndarray):
|
||||
image = Image.fromarray(image)
|
||||
if image.mode in ('RGBA', 'LA'):
|
||||
image = image.convert('RGB')
|
||||
buffered = io.BytesIO()
|
||||
image.save(buffered, format='JPEG')
|
||||
|
||||
image_base64 = base64.b64encode(buffered.getvalue()).decode()
|
||||
return (
|
||||
f'data:image/jpeg;base64,{image_base64}'
|
||||
if add_data_prefix
|
||||
else f'{image_base64}'
|
||||
)
|
||||
@@ -109,7 +109,7 @@ def codeact_user_response(
|
||||
) -> str:
|
||||
encaps_str = (
|
||||
(
|
||||
'Please encapsulate your final answer (answer ONLY) within <solution> and </solution>.\n'
|
||||
'Your final answer MUST be encapsulated within <solution> and </solution>.\n'
|
||||
'For example: The answer to the question is <solution> 42 </solution>.\n'
|
||||
)
|
||||
if encapsulate_solution
|
||||
@@ -117,7 +117,7 @@ def codeact_user_response(
|
||||
)
|
||||
msg = (
|
||||
'Please continue working on the task on whatever approach you think is suitable.\n'
|
||||
'If you think you have solved the task, please first send your answer to user through message and then finish the interaction.\n'
|
||||
'When you think you have solved the question, please use the finish tool and include your final answer in the message parameter of the finish tool.\n'
|
||||
f'{encaps_str}'
|
||||
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP.\n'
|
||||
)
|
||||
|
||||
194
frontend/__tests__/components/event-message.test.tsx
Normal file
194
frontend/__tests__/components/event-message.test.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { EventMessage } from "#/components/features/chat/event-message";
|
||||
|
||||
vi.mock("#/hooks/query/use-config", () => ({
|
||||
useConfig: () => ({
|
||||
data: { APP_MODE: "saas" },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/query/use-feedback-exists", () => ({
|
||||
useFeedbackExists: (eventId: number | undefined) => ({
|
||||
data: { exists: false },
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("EventMessage", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render LikertScale for finish action when it's the last message", () => {
|
||||
const finishEvent = {
|
||||
id: 123,
|
||||
source: "agent" as const,
|
||||
action: "finish" as const,
|
||||
args: {
|
||||
final_thought: "Task completed successfully",
|
||||
task_completed: "success" as const,
|
||||
outputs: {},
|
||||
thought: "Task completed successfully",
|
||||
},
|
||||
message: "Task completed successfully",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventMessage
|
||||
event={finishEvent}
|
||||
hasObservationPair={false}
|
||||
isAwaitingUserConfirmation={false}
|
||||
isLastMessage={true}
|
||||
isInLast10Actions={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render LikertScale for assistant message when it's the last message", () => {
|
||||
const assistantMessageEvent = {
|
||||
id: 456,
|
||||
source: "agent" as const,
|
||||
action: "message" as const,
|
||||
args: {
|
||||
thought: "I need more information to proceed.",
|
||||
image_urls: null,
|
||||
file_urls: [],
|
||||
wait_for_response: true,
|
||||
},
|
||||
message: "I need more information to proceed.",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventMessage
|
||||
event={assistantMessageEvent}
|
||||
hasObservationPair={false}
|
||||
isAwaitingUserConfirmation={false}
|
||||
isLastMessage={true}
|
||||
isInLast10Actions={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render LikertScale for error observation when it's the last message", () => {
|
||||
const errorEvent = {
|
||||
id: 789,
|
||||
source: "user" as const,
|
||||
observation: "error" as const,
|
||||
content: "An error occurred",
|
||||
extras: {
|
||||
error_id: "test-error-123",
|
||||
},
|
||||
message: "An error occurred",
|
||||
timestamp: new Date().toISOString(),
|
||||
cause: 123,
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventMessage
|
||||
event={errorEvent}
|
||||
hasObservationPair={false}
|
||||
isAwaitingUserConfirmation={false}
|
||||
isLastMessage={true}
|
||||
isInLast10Actions={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should NOT render LikertScale when not the last message", () => {
|
||||
const finishEvent = {
|
||||
id: 101,
|
||||
source: "agent" as const,
|
||||
action: "finish" as const,
|
||||
args: {
|
||||
final_thought: "Task completed successfully",
|
||||
task_completed: "success" as const,
|
||||
outputs: {},
|
||||
thought: "Task completed successfully",
|
||||
},
|
||||
message: "Task completed successfully",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventMessage
|
||||
event={finishEvent}
|
||||
hasObservationPair={false}
|
||||
isAwaitingUserConfirmation={false}
|
||||
isLastMessage={false}
|
||||
isInLast10Actions={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText("Rate 1 stars")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Rate 5 stars")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render LikertScale for error observation when in last 10 actions but not last message", () => {
|
||||
const errorEvent = {
|
||||
id: 999,
|
||||
source: "user" as const,
|
||||
observation: "error" as const,
|
||||
content: "An error occurred",
|
||||
extras: {
|
||||
error_id: "test-error-456",
|
||||
},
|
||||
message: "An error occurred",
|
||||
timestamp: new Date().toISOString(),
|
||||
cause: 123,
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventMessage
|
||||
event={errorEvent}
|
||||
hasObservationPair={false}
|
||||
isAwaitingUserConfirmation={false}
|
||||
isLastMessage={false}
|
||||
isInLast10Actions={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should NOT render LikertScale for error observation when not in last 10 actions", () => {
|
||||
const errorEvent = {
|
||||
id: 888,
|
||||
source: "user" as const,
|
||||
observation: "error" as const,
|
||||
content: "An error occurred",
|
||||
extras: {
|
||||
error_id: "test-error-789",
|
||||
},
|
||||
message: "An error occurred",
|
||||
timestamp: new Date().toISOString(),
|
||||
cause: 123,
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<EventMessage
|
||||
event={errorEvent}
|
||||
hasObservationPair={false}
|
||||
isAwaitingUserConfirmation={false}
|
||||
isLastMessage={false}
|
||||
isInLast10Actions={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText("Rate 1 stars")).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText("Rate 5 stars")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -128,7 +128,7 @@ describe("RepoConnector", () => {
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
await screen.findByText("Add GitHub repos");
|
||||
await screen.findByText("HOME$ADD_GITHUB_REPOS");
|
||||
});
|
||||
|
||||
it("should not render the 'add git(hub|lab) repos' links if oss mode", async () => {
|
||||
|
||||
@@ -53,7 +53,7 @@ describe("TaskSuggestions", () => {
|
||||
it("should render an empty message if there are no tasks", async () => {
|
||||
getSuggestedTasksSpy.mockResolvedValue([]);
|
||||
renderTaskSuggestions();
|
||||
await screen.findByText(/No tasks available/i);
|
||||
await screen.findByText("TASKS$NO_TASKS_AVAILABLE");
|
||||
});
|
||||
|
||||
it("should render the task groups with the correct titles", async () => {
|
||||
|
||||
@@ -473,7 +473,7 @@ describe("Secret actions", () => {
|
||||
|
||||
// make POST request
|
||||
expect(createSecretSpy).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, "My_Custom_Secret");
|
||||
@@ -557,7 +557,7 @@ describe("Secret actions", () => {
|
||||
|
||||
// make POST request
|
||||
expect(createSecretSpy).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
|
||||
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
|
||||
|
||||
expect(nameInput).toHaveValue(MOCK_GET_SECRETS_RESPONSE[0].name);
|
||||
expect(valueInput).toHaveValue("my-custom-secret-value");
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { extractSettings } from "#/utils/settings-utils";
|
||||
|
||||
describe("Model name case preservation", () => {
|
||||
it("should preserve the original case of model names in extractSettings", () => {
|
||||
// Create FormData with proper casing
|
||||
const formData = new FormData();
|
||||
formData.set("llm-provider-input", "SambaNova");
|
||||
formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct");
|
||||
formData.set("agent", "CodeActAgent");
|
||||
formData.set("language", "en");
|
||||
|
||||
const settings = extractSettings(formData);
|
||||
|
||||
// Test that model names maintain their original casing
|
||||
expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
|
||||
});
|
||||
|
||||
it("should preserve openai model case", () => {
|
||||
const formData = new FormData();
|
||||
formData.set("llm-provider-input", "openai");
|
||||
formData.set("llm-model-input", "gpt-4o");
|
||||
formData.set("agent", "CodeActAgent");
|
||||
formData.set("language", "en");
|
||||
|
||||
const settings = extractSettings(formData);
|
||||
expect(settings.LLM_MODEL).toBe("openai/gpt-4o");
|
||||
});
|
||||
|
||||
it("should preserve anthropic model case", () => {
|
||||
const formData = new FormData();
|
||||
formData.set("llm-provider-input", "anthropic");
|
||||
formData.set("llm-model-input", "claude-sonnet-4-20250514");
|
||||
formData.set("agent", "CodeActAgent");
|
||||
formData.set("language", "en");
|
||||
|
||||
const settings = extractSettings(formData);
|
||||
expect(settings.LLM_MODEL).toBe("anthropic/claude-sonnet-4-20250514");
|
||||
});
|
||||
|
||||
it("should not automatically lowercase model names", () => {
|
||||
const formData = new FormData();
|
||||
formData.set("llm-provider-input", "SambaNova");
|
||||
formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct");
|
||||
formData.set("agent", "CodeActAgent");
|
||||
formData.set("language", "en");
|
||||
|
||||
const settings = extractSettings(formData);
|
||||
|
||||
// Test that camelCase and PascalCase are preserved
|
||||
expect(settings.LLM_MODEL).not.toBe("sambanova/meta-llama-3.1-8b-instruct");
|
||||
expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
|
||||
});
|
||||
});
|
||||
1877
frontend/package-lock.json
generated
1877
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,39 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.45.0",
|
||||
"version": "0.47.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.0-beta.9",
|
||||
"@heroui/react": "^2.8.0-beta.10",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.6.2",
|
||||
"@react-router/serve": "^7.6.2",
|
||||
"@react-router/node": "^7.6.3",
|
||||
"@react-router/serve": "^7.6.3",
|
||||
"@react-types/shared": "^3.29.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.3.1",
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@tanstack/react-query": "^5.80.10",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.81.4",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.10.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.18.1",
|
||||
"framer-motion": "^12.19.2",
|
||||
"i18next": "^25.2.1",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.28",
|
||||
"jose": "^6.0.11",
|
||||
"lucide-react": "^0.519.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.255.0",
|
||||
"posthog-js": "^1.255.1",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
@@ -42,14 +42,14 @@
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.6.2",
|
||||
"react-router": "^7.6.3",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"vite": "^6.3.5",
|
||||
"vite": "^7.0.0",
|
||||
"web-vitals": "^5.0.3",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
@@ -80,19 +80,19 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/parser": "^7.27.7",
|
||||
"@babel/traverse": "^7.27.7",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.53.1",
|
||||
"@react-router/dev": "^7.6.2",
|
||||
"@react-router/dev": "^7.6.3",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.78.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.81.2",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.0.3",
|
||||
"@types/node": "^24.0.5",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
@@ -107,9 +107,9 @@
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "^5.5.0",
|
||||
"eslint-plugin-prettier": "^5.5.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
@@ -117,7 +117,7 @@
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.2",
|
||||
"msw": "^2.6.6",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier": "^3.6.2",
|
||||
"stripe": "^18.2.1",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.8.3",
|
||||
|
||||
@@ -56,8 +56,6 @@ const NON_TEXT_ATTRIBUTES = [
|
||||
"type",
|
||||
"href",
|
||||
"src",
|
||||
"alt",
|
||||
"placeholder",
|
||||
"rel",
|
||||
"target",
|
||||
"style",
|
||||
@@ -65,7 +63,6 @@ const NON_TEXT_ATTRIBUTES = [
|
||||
"onChange",
|
||||
"onSubmit",
|
||||
"data-testid",
|
||||
"aria-label",
|
||||
"aria-labelledby",
|
||||
"aria-describedby",
|
||||
"aria-hidden",
|
||||
@@ -139,6 +136,7 @@ function isLikelyCode(str) {
|
||||
}
|
||||
|
||||
function isCommonDevelopmentString(str) {
|
||||
|
||||
// Technical patterns that are definitely not UI strings
|
||||
const technicalPatterns = [
|
||||
// URLs and paths
|
||||
@@ -191,7 +189,7 @@ function isCommonDevelopmentString(str) {
|
||||
|
||||
// CSS units and values
|
||||
const cssUnitsPattern =
|
||||
/(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
|
||||
/\b\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$|^(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
|
||||
const cssValuesPattern =
|
||||
/(rgb|rgba|hsl|hsla|#[0-9a-fA-F]+|solid|absolute|relative|sticky|fixed|static|block|inline|flex|grid|none|auto|hidden|visible)/;
|
||||
|
||||
@@ -394,6 +392,7 @@ function isCommonDevelopmentString(str) {
|
||||
}
|
||||
|
||||
function isLikelyUserFacingText(str) {
|
||||
|
||||
// Basic validation - skip very short strings or strings without letters
|
||||
if (!str || str.length <= 2 || !/[a-zA-Z]/.test(str)) {
|
||||
return false;
|
||||
@@ -540,8 +539,8 @@ function isInTranslationContext(path) {
|
||||
}
|
||||
|
||||
function scanFileForUnlocalizedStrings(filePath) {
|
||||
// Skip all suggestion files as they contain special strings
|
||||
if (filePath.includes("suggestions")) {
|
||||
// Skip suggestion content files as they contain special strings that are already properly localized
|
||||
if (filePath.includes("utils/suggestions/") || filePath.includes("mocks/task-suggestions-handlers.ts")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function ActionSuggestions({
|
||||
const terms = {
|
||||
pr,
|
||||
prShort,
|
||||
pushToBranch: `Please push the changes to a remote branch on ${getProviderName()}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
|
||||
pushToBranch: `Please push the changes to a remote branch on ${getProviderName()}, but do NOT create a ${pr}. Check your current branch name first - if it's main, master, deploy, or another common default branch name, create a new branch with a descriptive name related to your changes. Otherwise, use the exact SAME branch name as the one you are currently on.`,
|
||||
createPR: `Please push the changes to ${getProviderName()} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
|
||||
pushToPR: `Please push the latest changes to the existing ${pr}.`,
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ interface EventMessageProps {
|
||||
hasObservationPair: boolean;
|
||||
isAwaitingUserConfirmation: boolean;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
}
|
||||
|
||||
export function EventMessage({
|
||||
@@ -42,24 +43,52 @@ export function EventMessage({
|
||||
hasObservationPair,
|
||||
isAwaitingUserConfirmation,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
}: EventMessageProps) {
|
||||
const shouldShowConfirmationButtons =
|
||||
isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
|
||||
|
||||
const { data: config } = useConfig();
|
||||
|
||||
// Use our query hook to check if feedback exists and get rating/reason
|
||||
const {
|
||||
data: feedbackData = { exists: false },
|
||||
isLoading: isCheckingFeedback,
|
||||
} = useFeedbackExists(isFinishAction(event) ? event.id : undefined);
|
||||
} = useFeedbackExists(event.id);
|
||||
|
||||
const renderLikertScale = () => {
|
||||
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For error observations, show if in last 10 actions
|
||||
// For other events, show only if it's the last message
|
||||
const shouldShow = isErrorObservation(event)
|
||||
? isInLast10Actions
|
||||
: isLastMessage;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (isErrorObservation(event)) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
<>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
{renderLikertScale()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,24 +99,11 @@ export function EventMessage({
|
||||
return null;
|
||||
}
|
||||
|
||||
const showLikertScale =
|
||||
config?.APP_MODE === "saas" &&
|
||||
isFinishAction(event) &&
|
||||
isLastMessage &&
|
||||
!isCheckingFeedback;
|
||||
|
||||
if (isFinishAction(event)) {
|
||||
return (
|
||||
<>
|
||||
<ChatMessage type="agent" message={getEventContent(event).details} />
|
||||
{showLikertScale && (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
)}
|
||||
{renderLikertScale()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -96,15 +112,20 @@ export function EventMessage({
|
||||
const message = parseMessageFromEvent(event);
|
||||
|
||||
return (
|
||||
<ChatMessage type={event.source} message={message}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
<>
|
||||
<ChatMessage type={event.source} message={message}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
{isAssistantMessage(event) &&
|
||||
event.action === "message" &&
|
||||
renderLikertScale()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
hasObservationPair={actionHasObservationPair(message)}
|
||||
isAwaitingUserConfirmation={isAwaitingUserConfirmation}
|
||||
isLastMessage={messages.length - 1 === index}
|
||||
isInLast10Actions={messages.length - 1 - index < 10}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@ export function TypingIndicator() {
|
||||
return (
|
||||
<div className="flex items-center space-x-1.5 bg-tertiary px-3 py-1.5 rounded-full">
|
||||
<span
|
||||
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[-2px]"
|
||||
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[1px]"
|
||||
style={{ animationDelay: "0ms" }}
|
||||
/>
|
||||
<span
|
||||
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[-2px]"
|
||||
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[1px]"
|
||||
style={{ animationDelay: "75ms" }}
|
||||
/>
|
||||
<span
|
||||
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[-2px]"
|
||||
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[1px]"
|
||||
style={{ animationDelay: "150ms" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -44,6 +44,7 @@ export function LikertScale({
|
||||
t(I18nKey.FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION),
|
||||
t(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT),
|
||||
t(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES),
|
||||
t(I18nKey.FEEDBACK$REASON_SHOULD_ASK_FIRST),
|
||||
t(I18nKey.FEEDBACK$REASON_OTHER),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function RepoProviderLinks() {
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const githubHref = config
|
||||
@@ -10,7 +13,7 @@ export function RepoProviderLinks() {
|
||||
return (
|
||||
<div className="flex flex-col text-sm underline underline-offset-2 text-content-2 gap-4 w-fit">
|
||||
<a href={githubHref} target="_blank" rel="noopener noreferrer">
|
||||
Add GitHub repos
|
||||
{t(I18nKey.HOME$ADD_GITHUB_REPOS)}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export interface BranchDropdownProps {
|
||||
items: { key: React.Key; label: string }[];
|
||||
@@ -16,11 +18,13 @@ export function BranchDropdown({
|
||||
isDisabled,
|
||||
selectedKey,
|
||||
}: BranchDropdownProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="branch-dropdown"
|
||||
name="branch-dropdown"
|
||||
placeholder="Select a branch"
|
||||
placeholder={t(I18nKey.REPOSITORY$SELECT_BRANCH)}
|
||||
items={items}
|
||||
wrapperClassName="max-w-[500px]"
|
||||
onSelectionChange={onSelectionChange}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export interface RepositoryDropdownProps {
|
||||
items: { key: React.Key; label: string }[];
|
||||
@@ -14,11 +16,13 @@ export function RepositoryDropdown({
|
||||
onInputChange,
|
||||
defaultFilter,
|
||||
}: RepositoryDropdownProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId="repo-dropdown"
|
||||
name="repo-dropdown"
|
||||
placeholder="Select a repo"
|
||||
placeholder={t(I18nKey.REPOSITORY$SELECT_REPO)}
|
||||
items={items}
|
||||
wrapperClassName="max-w-[500px]"
|
||||
onSelectionChange={onSelectionChange}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TaskGroup } from "./task-group";
|
||||
import { useSuggestedTasks } from "#/hooks/query/use-suggested-tasks";
|
||||
import { TaskSuggestionsSkeleton } from "./task-suggestions-skeleton";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface TaskSuggestionsProps {
|
||||
filterFor?: string | null;
|
||||
}
|
||||
|
||||
export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const { data: tasks, isLoading } = useSuggestedTasks();
|
||||
const suggestedTasks = filterFor
|
||||
? tasks?.filter((task) => task.title === filterFor)
|
||||
@@ -20,11 +23,13 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
|
||||
data-testid="task-suggestions"
|
||||
className={cn("flex flex-col w-full", !hasSuggestedTasks && "gap-6")}
|
||||
>
|
||||
<h2 className="heading">Suggested Tasks</h2>
|
||||
<h2 className="heading">{t(I18nKey.TASKS$SUGGESTED_TASKS)}</h2>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{isLoading && <TaskSuggestionsSkeleton />}
|
||||
{!hasSuggestedTasks && !isLoading && <p>No tasks available</p>}
|
||||
{!hasSuggestedTasks && !isLoading && (
|
||||
<p>{t(I18nKey.TASKS$NO_TASKS_AVAILABLE)}</p>
|
||||
)}
|
||||
{suggestedTasks?.map((taskGroup, index) => (
|
||||
<TaskGroup
|
||||
key={index}
|
||||
|
||||
@@ -64,7 +64,7 @@ export function PaymentForm() {
|
||||
onChange={handleTopUpInputChange}
|
||||
type="number"
|
||||
label={t(I18nKey.PAYMENT$ADD_FUNDS)}
|
||||
placeholder="Specify an amount in USD to add - min $10"
|
||||
placeholder={t(I18nKey.PAYMENT$SPECIFY_AMOUNT_USD)}
|
||||
className="w-[680px]"
|
||||
min={10}
|
||||
max={25000}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Trans } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function BitbucketTokenHelpAnchor() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<p data-testid="bitbucket-token-help-anchor" className="text-xs">
|
||||
<Trans
|
||||
@@ -9,7 +11,7 @@ export function BitbucketTokenHelpAnchor() {
|
||||
components={[
|
||||
<a
|
||||
key="bitbucket-token-help-anchor-link"
|
||||
aria-label="Bitbucket token help link"
|
||||
aria-label={t(I18nKey.GIT$BITBUCKET_TOKEN_HELP_LINK)}
|
||||
href="https://bitbucket.org/account/settings/app-passwords/new?scopes=repository:write,pullrequest:write,issue:write"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
@@ -17,7 +19,7 @@ export function BitbucketTokenHelpAnchor() {
|
||||
/>,
|
||||
<a
|
||||
key="bitbucket-token-help-anchor-link-2"
|
||||
aria-label="Bitbucket token see more link"
|
||||
aria-label={t(I18nKey.GIT$BITBUCKET_TOKEN_SEE_MORE_LINK)}
|
||||
href="https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Trans } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function GitHubTokenHelpAnchor() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<p data-testid="github-token-help-anchor" className="text-xs">
|
||||
<Trans
|
||||
@@ -9,7 +11,7 @@ export function GitHubTokenHelpAnchor() {
|
||||
components={[
|
||||
<a
|
||||
key="github-token-help-anchor-link"
|
||||
aria-label="GitHub token help link"
|
||||
aria-label={t(I18nKey.GIT$GITHUB_TOKEN_HELP_LINK)}
|
||||
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
@@ -17,7 +19,7 @@ export function GitHubTokenHelpAnchor() {
|
||||
/>,
|
||||
<a
|
||||
key="github-token-help-anchor-link-2"
|
||||
aria-label="GitHub token see more link"
|
||||
aria-label={t(I18nKey.GIT$GITHUB_TOKEN_SEE_MORE_LINK)}
|
||||
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Trans } from "react-i18next";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function GitLabTokenHelpAnchor() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<p data-testid="gitlab-token-help-anchor" className="text-xs">
|
||||
<Trans
|
||||
@@ -9,7 +11,7 @@ export function GitLabTokenHelpAnchor() {
|
||||
components={[
|
||||
<a
|
||||
key="gitlab-token-help-anchor-link"
|
||||
aria-label="Gitlab token help link"
|
||||
aria-label={t(I18nKey.GIT$GITLAB_TOKEN_HELP_LINK)}
|
||||
href="https://gitlab.com/-/user_settings/personal_access_tokens?name=openhands-app&scopes=api,read_user,read_repository,write_repository"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
@@ -17,7 +19,7 @@ export function GitLabTokenHelpAnchor() {
|
||||
/>,
|
||||
<a
|
||||
key="gitlab-token-help-anchor-link-2"
|
||||
aria-label="GitLab token see more link"
|
||||
aria-label={t(I18nKey.GIT$GITLAB_TOKEN_SEE_MORE_LINK)}
|
||||
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
|
||||
@@ -111,7 +111,7 @@ export function SecretForm({
|
||||
(secret) => secret.name === name && secret.name !== selectedSecret,
|
||||
);
|
||||
if (isNameAlreadyUsed) {
|
||||
setError("Secret already exists");
|
||||
setError(t("SECRETS$SECRET_ALREADY_EXISTS"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ export function SecretForm({
|
||||
className="w-full max-w-[350px]"
|
||||
required
|
||||
defaultValue={mode === "edit" && selectedSecret ? selectedSecret : ""}
|
||||
placeholder="e.g. OpenAI_API_Key"
|
||||
placeholder={t("SECRETS$API_KEY_EXAMPLE")}
|
||||
pattern="^\S*$"
|
||||
/>
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
||||
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
|
||||
import BitbucketLogo from "#/assets/branding/bitbucket-logo.svg?react";
|
||||
import { useAuthUrl } from "#/hooks/use-auth-url";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
|
||||
@@ -23,6 +24,11 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
identityProvider: "gitlab",
|
||||
});
|
||||
|
||||
const bitbucketAuthUrl = useAuthUrl({
|
||||
appMode: appMode || null,
|
||||
identityProvider: "bitbucket",
|
||||
});
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
if (githubAuthUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
@@ -37,6 +43,13 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBitbucketAuth = () => {
|
||||
if (bitbucketAuthUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
window.location.href = bitbucketAuthUrl;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<ModalBody className="border border-tertiary">
|
||||
@@ -67,6 +80,16 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
>
|
||||
{t(I18nKey.GITLAB$CONNECT_TO_GITLAB)}
|
||||
</BrandButton>
|
||||
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleBitbucketAuth}
|
||||
className="w-full"
|
||||
startContent={<BitbucketLogo width={20} height={20} />}
|
||||
>
|
||||
{t(I18nKey.BITBUCKET$CONNECT_TO_BITBUCKET)}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function CustomModelInput({
|
||||
id="custom-model"
|
||||
name="custom-model"
|
||||
defaultValue={defaultValue}
|
||||
aria-label="Custom Model"
|
||||
aria-label={t(I18nKey.MODEL$CUSTOM_MODEL)}
|
||||
classNames={{
|
||||
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
|
||||
}}
|
||||
|
||||
@@ -198,46 +198,46 @@ function SecurityInvariant() {
|
||||
{t(I18nKey.INVARIANT$ASK_CONFIRMATION_RISK_SEVERITY_LABEL)}
|
||||
</p>
|
||||
<Select
|
||||
placeholder="Select risk severity"
|
||||
placeholder={t(I18nKey.SECURITY$SELECT_RISK_SEVERITY)}
|
||||
value={selectedRisk}
|
||||
onChange={(e) =>
|
||||
setSelectedRisk(Number(e.target.value) as ActionSecurityRisk)
|
||||
}
|
||||
className={getRiskColor(selectedRisk)}
|
||||
selectedKeys={new Set([selectedRisk.toString()])}
|
||||
aria-label="Select risk severity"
|
||||
aria-label={t(I18nKey.SECURITY$SELECT_RISK_SEVERITY)}
|
||||
>
|
||||
<SelectItem
|
||||
key={ActionSecurityRisk.UNKNOWN}
|
||||
aria-label="Unknown Risk"
|
||||
aria-label={t(I18nKey.SECURITY$UNKNOWN_RISK)}
|
||||
className={getRiskColor(ActionSecurityRisk.UNKNOWN)}
|
||||
>
|
||||
{getRiskText(ActionSecurityRisk.UNKNOWN)}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
key={ActionSecurityRisk.LOW}
|
||||
aria-label="Low Risk"
|
||||
aria-label={t(I18nKey.SECURITY$LOW_RISK)}
|
||||
className={getRiskColor(ActionSecurityRisk.LOW)}
|
||||
>
|
||||
{getRiskText(ActionSecurityRisk.LOW)}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
key={ActionSecurityRisk.MEDIUM}
|
||||
aria-label="Medium Risk"
|
||||
aria-label={t(I18nKey.SECURITY$MEDIUM_RISK)}
|
||||
className={getRiskColor(ActionSecurityRisk.MEDIUM)}
|
||||
>
|
||||
{getRiskText(ActionSecurityRisk.MEDIUM)}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
key={ActionSecurityRisk.HIGH}
|
||||
aria-label="High Risk"
|
||||
aria-label={t(I18nKey.SECURITY$HIGH_RISK)}
|
||||
className={getRiskColor(ActionSecurityRisk.HIGH)}
|
||||
>
|
||||
{getRiskText(ActionSecurityRisk.HIGH)}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
key={ActionSecurityRisk.HIGH + 1}
|
||||
aria-label="Don't ask for confirmation"
|
||||
aria-label={t(I18nKey.SECURITY$DONT_ASK_CONFIRMATION)}
|
||||
>
|
||||
{t(I18nKey.INVARIANT$DONT_ASK_FOR_CONFIRMATION_LABEL)}
|
||||
</SelectItem>
|
||||
|
||||
@@ -34,10 +34,7 @@ export const useAuthCallback = () => {
|
||||
const loginMethod = searchParams.get("login_method");
|
||||
|
||||
// Set the login method if it's valid
|
||||
if (
|
||||
loginMethod === LoginMethod.GITHUB ||
|
||||
loginMethod === LoginMethod.GITLAB
|
||||
) {
|
||||
if (Object.values(LoginMethod).includes(loginMethod as LoginMethod)) {
|
||||
setLoginMethod(loginMethod as LoginMethod);
|
||||
|
||||
// Clean up the URL by removing the login_method parameter
|
||||
|
||||
@@ -602,7 +602,30 @@ export enum I18nKey {
|
||||
FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION = "FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION",
|
||||
FEEDBACK$REASON_FORGOT_CONTEXT = "FEEDBACK$REASON_FORGOT_CONTEXT",
|
||||
FEEDBACK$REASON_UNNECESSARY_CHANGES = "FEEDBACK$REASON_UNNECESSARY_CHANGES",
|
||||
FEEDBACK$REASON_SHOULD_ASK_FIRST = "FEEDBACK$REASON_SHOULD_ASK_FIRST",
|
||||
FEEDBACK$REASON_OTHER = "FEEDBACK$REASON_OTHER",
|
||||
FEEDBACK$THANK_YOU_FOR_FEEDBACK = "FEEDBACK$THANK_YOU_FOR_FEEDBACK",
|
||||
FEEDBACK$FAILED_TO_SUBMIT = "FEEDBACK$FAILED_TO_SUBMIT",
|
||||
HOME$ADD_GITHUB_REPOS = "HOME$ADD_GITHUB_REPOS",
|
||||
REPOSITORY$SELECT_BRANCH = "REPOSITORY$SELECT_BRANCH",
|
||||
REPOSITORY$SELECT_REPO = "REPOSITORY$SELECT_REPO",
|
||||
TASKS$SUGGESTED_TASKS = "TASKS$SUGGESTED_TASKS",
|
||||
TASKS$NO_TASKS_AVAILABLE = "TASKS$NO_TASKS_AVAILABLE",
|
||||
PAYMENT$SPECIFY_AMOUNT_USD = "PAYMENT$SPECIFY_AMOUNT_USD",
|
||||
GIT$BITBUCKET_TOKEN_HELP_LINK = "GIT$BITBUCKET_TOKEN_HELP_LINK",
|
||||
GIT$BITBUCKET_TOKEN_SEE_MORE_LINK = "GIT$BITBUCKET_TOKEN_SEE_MORE_LINK",
|
||||
GIT$GITHUB_TOKEN_HELP_LINK = "GIT$GITHUB_TOKEN_HELP_LINK",
|
||||
GIT$GITHUB_TOKEN_SEE_MORE_LINK = "GIT$GITHUB_TOKEN_SEE_MORE_LINK",
|
||||
GIT$GITLAB_TOKEN_HELP_LINK = "GIT$GITLAB_TOKEN_HELP_LINK",
|
||||
GIT$GITLAB_TOKEN_SEE_MORE_LINK = "GIT$GITLAB_TOKEN_SEE_MORE_LINK",
|
||||
SECRETS$SECRET_ALREADY_EXISTS = "SECRETS$SECRET_ALREADY_EXISTS",
|
||||
SECRETS$API_KEY_EXAMPLE = "SECRETS$API_KEY_EXAMPLE",
|
||||
MODEL$CUSTOM_MODEL = "MODEL$CUSTOM_MODEL",
|
||||
SECURITY$SELECT_RISK_SEVERITY = "SECURITY$SELECT_RISK_SEVERITY",
|
||||
SECURITY$DONT_ASK_CONFIRMATION = "SECURITY$DONT_ASK_CONFIRMATION",
|
||||
SETTINGS$MAXIMUM_BUDGET_USD = "SETTINGS$MAXIMUM_BUDGET_USD",
|
||||
GIT$DISCONNECT_TOKENS = "GIT$DISCONNECT_TOKENS",
|
||||
API$TAVILY_KEY_EXAMPLE = "API$TAVILY_KEY_EXAMPLE",
|
||||
API$TVLY_KEY_EXAMPLE = "API$TVLY_KEY_EXAMPLE",
|
||||
SECRETS$CONNECT_GIT_PROVIDER = "SECRETS$CONNECT_GIT_PROVIDER",
|
||||
}
|
||||
|
||||
@@ -833,10 +833,10 @@
|
||||
},
|
||||
"HOME$LETS_START_BUILDING": {
|
||||
"en": "Let's Start Building!",
|
||||
"ja": "構築を始めましょう!",
|
||||
"zh-CN": "让我们开始构建!",
|
||||
"zh-TW": "讓我們開始構建!",
|
||||
"ko-KR": "구축을 시작합시다!",
|
||||
"ja": "開発を始めましょう!",
|
||||
"zh-CN": "让我们开始开发!",
|
||||
"zh-TW": "讓我們開始開發!",
|
||||
"ko-KR": "개발을 시작합시다!",
|
||||
"no": "La oss begynne å bygge!",
|
||||
"it": "Iniziamo a costruire!",
|
||||
"pt": "Vamos começar a construir!",
|
||||
@@ -849,7 +849,7 @@
|
||||
},
|
||||
"HOME$OPENHANDS_DESCRIPTION": {
|
||||
"en": "OpenHands makes it easy to build and maintain software using AI-driven development.",
|
||||
"ja": "OpenHandsはAI駆動の開発を使用してソフトウェアの構築と維持を容易にします。",
|
||||
"ja": "OpenHandsはAI駆動の開発を使用してソフトウェアの開発と維持を容易にします。",
|
||||
"zh-CN": "OpenHands使用AI驱动的开发方式,轻松构建和维护软件。",
|
||||
"zh-TW": "OpenHands使用AI驅動的開發方式,輕鬆構建和維護軟件。",
|
||||
"ko-KR": "OpenHands는 AI 기반 개발을 사용하여 소프트웨어를 쉽게 구축하고 유지할 수 있게 합니다.",
|
||||
@@ -9631,6 +9631,22 @@
|
||||
"de": "Der Agent hat unnötige Änderungen vorgenommen",
|
||||
"uk": "Агент зробив непотрібні зміни"
|
||||
},
|
||||
"FEEDBACK$REASON_SHOULD_ASK_FIRST": {
|
||||
"en": "The agent should've asked me first before doing it!",
|
||||
"ja": "エージェントは実行する前に私に確認すべきでした!",
|
||||
"zh-CN": "代理应该先问我再做这件事!",
|
||||
"zh-TW": "代理應該先問我再做這件事!",
|
||||
"ko-KR": "에이전트는 먼저 나에게 물어봤어야 했습니다!",
|
||||
"no": "Agenten burde ha spurt meg først før den gjorde det!",
|
||||
"it": "L'agente avrebbe dovuto chiedermelo prima di farlo!",
|
||||
"pt": "O agente deveria ter me perguntado primeiro antes de fazer isso!",
|
||||
"es": "¡El agente debería haberme preguntado primero antes de hacerlo!",
|
||||
"ar": "كان يجب على الوكيل أن يسألني أولاً قبل القيام بذلك!",
|
||||
"fr": "L'agent aurait dû me demander d'abord avant de le faire !",
|
||||
"tr": "Ajan bunu yapmadan önce bana sormalıydı!",
|
||||
"de": "Der Agent hätte mich vorher fragen sollen!",
|
||||
"uk": "Агент повинен був спочатку запитати мене, перш ніж це робити!"
|
||||
},
|
||||
"FEEDBACK$REASON_OTHER": {
|
||||
"en": "Other",
|
||||
"ja": "その他",
|
||||
@@ -9678,5 +9694,357 @@
|
||||
"tr": "Geri bildirim gönderilemedi",
|
||||
"de": "Feedback konnte nicht gesendet werden",
|
||||
"uk": "Не вдалося надіслати відгук"
|
||||
},
|
||||
"HOME$ADD_GITHUB_REPOS": {
|
||||
"en": "Add GitHub repos",
|
||||
"ja": "GitHubリポジトリを追加",
|
||||
"zh-CN": "添加GitHub仓库",
|
||||
"zh-TW": "新增GitHub儲存庫",
|
||||
"ko-KR": "GitHub 저장소 추가",
|
||||
"no": "Legg til GitHub-repositorier",
|
||||
"it": "Aggiungi repository GitHub",
|
||||
"pt": "Adicionar repositórios GitHub",
|
||||
"es": "Agregar repositorios de GitHub",
|
||||
"ar": "إضافة مستودعات GitHub",
|
||||
"fr": "Ajouter des dépôts GitHub",
|
||||
"tr": "GitHub depoları ekle",
|
||||
"de": "GitHub-Repositories hinzufügen",
|
||||
"uk": "Додати репозиторії GitHub"
|
||||
},
|
||||
"REPOSITORY$SELECT_BRANCH": {
|
||||
"en": "Select a branch",
|
||||
"ja": "ブランチを選択",
|
||||
"zh-CN": "选择分支",
|
||||
"zh-TW": "選擇分支",
|
||||
"ko-KR": "브랜치 선택",
|
||||
"no": "Velg en gren",
|
||||
"it": "Seleziona un ramo",
|
||||
"pt": "Selecionar um branch",
|
||||
"es": "Seleccionar una rama",
|
||||
"ar": "اختر فرع",
|
||||
"fr": "Sélectionner une branche",
|
||||
"tr": "Bir dal seç",
|
||||
"de": "Einen Branch auswählen",
|
||||
"uk": "Вибрати гілку"
|
||||
},
|
||||
"REPOSITORY$SELECT_REPO": {
|
||||
"en": "Select a repo",
|
||||
"ja": "リポジトリを選択",
|
||||
"zh-CN": "选择仓库",
|
||||
"zh-TW": "選擇儲存庫",
|
||||
"ko-KR": "저장소 선택",
|
||||
"no": "Velg et repositorium",
|
||||
"it": "Seleziona un repository",
|
||||
"pt": "Selecionar um repositório",
|
||||
"es": "Seleccionar un repositorio",
|
||||
"ar": "اختر مستودع",
|
||||
"fr": "Sélectionner un dépôt",
|
||||
"tr": "Bir depo seç",
|
||||
"de": "Ein Repository auswählen",
|
||||
"uk": "Вибрати репозиторій"
|
||||
},
|
||||
"TASKS$SUGGESTED_TASKS": {
|
||||
"en": "Suggested Tasks",
|
||||
"ja": "推奨タスク",
|
||||
"zh-CN": "建议任务",
|
||||
"zh-TW": "建議任務",
|
||||
"ko-KR": "추천 작업",
|
||||
"no": "Foreslåtte oppgaver",
|
||||
"it": "Attività suggerite",
|
||||
"pt": "Tarefas sugeridas",
|
||||
"es": "Tareas sugeridas",
|
||||
"ar": "المهام المقترحة",
|
||||
"fr": "Tâches suggérées",
|
||||
"tr": "Önerilen görevler",
|
||||
"de": "Vorgeschlagene Aufgaben",
|
||||
"uk": "Запропоновані завдання"
|
||||
},
|
||||
"TASKS$NO_TASKS_AVAILABLE": {
|
||||
"en": "No tasks available",
|
||||
"ja": "利用可能なタスクがありません",
|
||||
"zh-CN": "没有可用任务",
|
||||
"zh-TW": "沒有可用任務",
|
||||
"ko-KR": "사용 가능한 작업이 없습니다",
|
||||
"no": "Ingen oppgaver tilgjengelig",
|
||||
"it": "Nessuna attività disponibile",
|
||||
"pt": "Nenhuma tarefa disponível",
|
||||
"es": "No hay tareas disponibles",
|
||||
"ar": "لا توجد مهام متاحة",
|
||||
"fr": "Aucune tâche disponible",
|
||||
"tr": "Mevcut görev yok",
|
||||
"de": "Keine Aufgaben verfügbar",
|
||||
"uk": "Немає доступних завдань"
|
||||
},
|
||||
"PAYMENT$SPECIFY_AMOUNT_USD": {
|
||||
"en": "Specify an amount in USD to add - min $10",
|
||||
"ja": "追加するUSD金額を指定してください - 最小$10",
|
||||
"zh-CN": "指定要添加的美元金额 - 最少$10",
|
||||
"zh-TW": "指定要新增的美元金額 - 最少$10",
|
||||
"ko-KR": "추가할 USD 금액을 지정하세요 - 최소 $10",
|
||||
"no": "Spesifiser et beløp i USD å legge til - min $10",
|
||||
"it": "Specifica un importo in USD da aggiungere - min $10",
|
||||
"pt": "Especifique um valor em USD para adicionar - mín $10",
|
||||
"es": "Especifique una cantidad en USD para agregar - mín $10",
|
||||
"ar": "حدد مبلغًا بالدولار الأمريكي لإضافته - الحد الأدنى 10 دولارات",
|
||||
"fr": "Spécifiez un montant en USD à ajouter - min 10 $",
|
||||
"tr": "Eklenecek USD tutarını belirtin - min $10",
|
||||
"de": "Geben Sie einen USD-Betrag zum Hinzufügen an - min $10",
|
||||
"uk": "Вкажіть суму в доларах США для додавання - мін $10"
|
||||
},
|
||||
"GIT$BITBUCKET_TOKEN_HELP_LINK": {
|
||||
"en": "Bitbucket token help link",
|
||||
"ja": "Bitbucketトークンヘルプリンク",
|
||||
"zh-CN": "Bitbucket令牌帮助链接",
|
||||
"zh-TW": "Bitbucket令牌幫助連結",
|
||||
"ko-KR": "Bitbucket 토큰 도움말 링크",
|
||||
"no": "Bitbucket token hjelpelenke",
|
||||
"it": "Link di aiuto per il token Bitbucket",
|
||||
"pt": "Link de ajuda do token Bitbucket",
|
||||
"es": "Enlace de ayuda del token de Bitbucket",
|
||||
"ar": "رابط مساعدة رمز Bitbucket",
|
||||
"fr": "Lien d'aide pour le jeton Bitbucket",
|
||||
"tr": "Bitbucket token yardım bağlantısı",
|
||||
"de": "Bitbucket-Token-Hilfe-Link",
|
||||
"uk": "Посилання на довідку токена Bitbucket"
|
||||
},
|
||||
"GIT$BITBUCKET_TOKEN_SEE_MORE_LINK": {
|
||||
"en": "Bitbucket token see more link",
|
||||
"ja": "Bitbucketトークン詳細リンク",
|
||||
"zh-CN": "Bitbucket令牌查看更多链接",
|
||||
"zh-TW": "Bitbucket令牌查看更多連結",
|
||||
"ko-KR": "Bitbucket 토큰 더 보기 링크",
|
||||
"no": "Bitbucket token se mer lenke",
|
||||
"it": "Link per vedere di più sul token Bitbucket",
|
||||
"pt": "Link para ver mais sobre o token Bitbucket",
|
||||
"es": "Enlace para ver más del token de Bitbucket",
|
||||
"ar": "رابط لرؤية المزيد حول رمز Bitbucket",
|
||||
"fr": "Lien pour en voir plus sur le jeton Bitbucket",
|
||||
"tr": "Bitbucket token daha fazla görme bağlantısı",
|
||||
"de": "Bitbucket-Token mehr sehen Link",
|
||||
"uk": "Посилання для перегляду більше про токен Bitbucket"
|
||||
},
|
||||
"GIT$GITHUB_TOKEN_HELP_LINK": {
|
||||
"en": "GitHub token help link",
|
||||
"ja": "GitHubトークンヘルプリンク",
|
||||
"zh-CN": "GitHub令牌帮助链接",
|
||||
"zh-TW": "GitHub令牌幫助連結",
|
||||
"ko-KR": "GitHub 토큰 도움말 링크",
|
||||
"no": "GitHub token hjelpelenke",
|
||||
"it": "Link di aiuto per il token GitHub",
|
||||
"pt": "Link de ajuda do token GitHub",
|
||||
"es": "Enlace de ayuda del token de GitHub",
|
||||
"ar": "رابط مساعدة رمز GitHub",
|
||||
"fr": "Lien d'aide pour le jeton GitHub",
|
||||
"tr": "GitHub token yardım bağlantısı",
|
||||
"de": "GitHub-Token-Hilfe-Link",
|
||||
"uk": "Посилання на довідку токена GitHub"
|
||||
},
|
||||
"GIT$GITHUB_TOKEN_SEE_MORE_LINK": {
|
||||
"en": "GitHub token see more link",
|
||||
"ja": "GitHubトークン詳細リンク",
|
||||
"zh-CN": "GitHub令牌查看更多链接",
|
||||
"zh-TW": "GitHub令牌查看更多連結",
|
||||
"ko-KR": "GitHub 토큰 더 보기 링크",
|
||||
"no": "GitHub token se mer lenke",
|
||||
"it": "Link per vedere di più sul token GitHub",
|
||||
"pt": "Link para ver mais sobre o token GitHub",
|
||||
"es": "Enlace para ver más del token de GitHub",
|
||||
"ar": "رابط لرؤية المزيد حول رمز GitHub",
|
||||
"fr": "Lien pour en voir plus sur le jeton GitHub",
|
||||
"tr": "GitHub token daha fazla görme bağlantısı",
|
||||
"de": "GitHub-Token mehr sehen Link",
|
||||
"uk": "Посилання для перегляду більше про токен GitHub"
|
||||
},
|
||||
"GIT$GITLAB_TOKEN_HELP_LINK": {
|
||||
"en": "Gitlab token help link",
|
||||
"ja": "GitLabトークンヘルプリンク",
|
||||
"zh-CN": "GitLab令牌帮助链接",
|
||||
"zh-TW": "GitLab令牌幫助連結",
|
||||
"ko-KR": "GitLab 토큰 도움말 링크",
|
||||
"no": "GitLab token hjelpelenke",
|
||||
"it": "Link di aiuto per il token GitLab",
|
||||
"pt": "Link de ajuda do token GitLab",
|
||||
"es": "Enlace de ayuda del token de GitLab",
|
||||
"ar": "رابط مساعدة رمز GitLab",
|
||||
"fr": "Lien d'aide pour le jeton GitLab",
|
||||
"tr": "GitLab token yardım bağlantısı",
|
||||
"de": "GitLab-Token-Hilfe-Link",
|
||||
"uk": "Посилання на довідку токена GitLab"
|
||||
},
|
||||
"GIT$GITLAB_TOKEN_SEE_MORE_LINK": {
|
||||
"en": "GitLab token see more link",
|
||||
"ja": "GitLabトークン詳細リンク",
|
||||
"zh-CN": "GitLab令牌查看更多链接",
|
||||
"zh-TW": "GitLab令牌查看更多連結",
|
||||
"ko-KR": "GitLab 토큰 더 보기 링크",
|
||||
"no": "GitLab token se mer lenke",
|
||||
"it": "Link per vedere di più sul token GitLab",
|
||||
"pt": "Link para ver mais sobre o token GitLab",
|
||||
"es": "Enlace para ver más del token de GitLab",
|
||||
"ar": "رابط لرؤية المزيد حول رمز GitLab",
|
||||
"fr": "Lien pour en voir plus sur le jeton GitLab",
|
||||
"tr": "GitLab token daha fazla görme bağlantısı",
|
||||
"de": "GitLab-Token mehr sehen Link",
|
||||
"uk": "Посилання для перегляду більше про токен GitLab"
|
||||
},
|
||||
"SECRETS$SECRET_ALREADY_EXISTS": {
|
||||
"en": "Secret already exists",
|
||||
"ja": "シークレットは既に存在します",
|
||||
"zh-CN": "密钥已存在",
|
||||
"zh-TW": "密鑰已存在",
|
||||
"ko-KR": "시크릿이 이미 존재합니다",
|
||||
"no": "Hemmelighet eksisterer allerede",
|
||||
"it": "Il segreto esiste già",
|
||||
"pt": "Segredo já existe",
|
||||
"es": "El secreto ya existe",
|
||||
"ar": "السر موجود بالفعل",
|
||||
"fr": "Le secret existe déjà",
|
||||
"tr": "Gizli anahtar zaten mevcut",
|
||||
"de": "Geheimnis existiert bereits",
|
||||
"uk": "Секрет вже існує"
|
||||
},
|
||||
"SECRETS$API_KEY_EXAMPLE": {
|
||||
"en": "e.g. OpenAI_API_Key",
|
||||
"ja": "例: OpenAI_API_Key",
|
||||
"zh-CN": "例如 OpenAI_API_Key",
|
||||
"zh-TW": "例如 OpenAI_API_Key",
|
||||
"ko-KR": "예: OpenAI_API_Key",
|
||||
"no": "f.eks. OpenAI_API_Key",
|
||||
"it": "es. OpenAI_API_Key",
|
||||
"pt": "ex. OpenAI_API_Key",
|
||||
"es": "ej. OpenAI_API_Key",
|
||||
"ar": "مثل OpenAI_API_Key",
|
||||
"fr": "ex. OpenAI_API_Key",
|
||||
"tr": "örn. OpenAI_API_Key",
|
||||
"de": "z.B. OpenAI_API_Key",
|
||||
"uk": "наприклад OpenAI_API_Key"
|
||||
},
|
||||
"MODEL$CUSTOM_MODEL": {
|
||||
"en": "Custom Model",
|
||||
"ja": "カスタムモデル",
|
||||
"zh-CN": "自定义模型",
|
||||
"zh-TW": "自訂模型",
|
||||
"ko-KR": "사용자 정의 모델",
|
||||
"no": "Tilpasset modell",
|
||||
"it": "Modello personalizzato",
|
||||
"pt": "Modelo personalizado",
|
||||
"es": "Modelo personalizado",
|
||||
"ar": "نموذج مخصص",
|
||||
"fr": "Modèle personnalisé",
|
||||
"tr": "Özel model",
|
||||
"de": "Benutzerdefiniertes Modell",
|
||||
"uk": "Користувацька модель"
|
||||
},
|
||||
"SECURITY$SELECT_RISK_SEVERITY": {
|
||||
"en": "Select risk severity",
|
||||
"ja": "リスクの重要度を選択",
|
||||
"zh-CN": "选择风险严重程度",
|
||||
"zh-TW": "選擇風險嚴重程度",
|
||||
"ko-KR": "위험 심각도 선택",
|
||||
"no": "Velg risikoalvorlighet",
|
||||
"it": "Seleziona gravità del rischio",
|
||||
"pt": "Selecionar gravidade do risco",
|
||||
"es": "Seleccionar gravedad del riesgo",
|
||||
"ar": "اختر شدة المخاطر",
|
||||
"fr": "Sélectionner la gravité du risque",
|
||||
"tr": "Risk ciddiyetini seç",
|
||||
"de": "Risikoschweregrad auswählen",
|
||||
"uk": "Вибрати ступінь ризику"
|
||||
},
|
||||
"SECURITY$DONT_ASK_CONFIRMATION": {
|
||||
"en": "Don't ask for confirmation",
|
||||
"ja": "確認を求めない",
|
||||
"zh-CN": "不要求确认",
|
||||
"zh-TW": "不要求確認",
|
||||
"ko-KR": "확인을 요청하지 않음",
|
||||
"no": "Ikke spør om bekreftelse",
|
||||
"it": "Non chiedere conferma",
|
||||
"pt": "Não pedir confirmação",
|
||||
"es": "No pedir confirmación",
|
||||
"ar": "لا تطلب التأكيد",
|
||||
"fr": "Ne pas demander de confirmation",
|
||||
"tr": "Onay isteme",
|
||||
"de": "Nicht nach Bestätigung fragen",
|
||||
"uk": "Не запитувати підтвердження"
|
||||
},
|
||||
"SETTINGS$MAXIMUM_BUDGET_USD": {
|
||||
"en": "Maximum budget per conversation in USD",
|
||||
"ja": "会話あたりの最大予算(USD)",
|
||||
"zh-CN": "每次对话的最大预算(美元)",
|
||||
"zh-TW": "每次對話的最大預算(美元)",
|
||||
"ko-KR": "대화당 최대 예산(USD)",
|
||||
"no": "Maksimalt budsjett per samtale i USD",
|
||||
"it": "Budget massimo per conversazione in USD",
|
||||
"pt": "Orçamento máximo por conversa em USD",
|
||||
"es": "Presupuesto máximo por conversación en USD",
|
||||
"ar": "الحد الأقصى للميزانية لكل محادثة بالدولار الأمريكي",
|
||||
"fr": "Budget maximum par conversation en USD",
|
||||
"tr": "Konuşma başına maksimum bütçe (USD)",
|
||||
"de": "Maximales Budget pro Gespräch in USD",
|
||||
"uk": "Максимальний бюджет на розмову в доларах США"
|
||||
},
|
||||
"GIT$DISCONNECT_TOKENS": {
|
||||
"en": "Disconnect Tokens",
|
||||
"ja": "トークンを切断",
|
||||
"zh-CN": "断开令牌连接",
|
||||
"zh-TW": "中斷令牌連接",
|
||||
"ko-KR": "토큰 연결 해제",
|
||||
"no": "Koble fra tokens",
|
||||
"it": "Disconnetti token",
|
||||
"pt": "Desconectar tokens",
|
||||
"es": "Desconectar tokens",
|
||||
"ar": "قطع اتصال الرموز",
|
||||
"fr": "Déconnecter les jetons",
|
||||
"tr": "Token bağlantısını kes",
|
||||
"de": "Token trennen",
|
||||
"uk": "Відключити токени"
|
||||
},
|
||||
"API$TAVILY_KEY_EXAMPLE": {
|
||||
"en": "sk-tavily-...",
|
||||
"ja": "sk-tavily-...",
|
||||
"zh-CN": "sk-tavily-...",
|
||||
"zh-TW": "sk-tavily-...",
|
||||
"ko-KR": "sk-tavily-...",
|
||||
"no": "sk-tavily-...",
|
||||
"it": "sk-tavily-...",
|
||||
"pt": "sk-tavily-...",
|
||||
"es": "sk-tavily-...",
|
||||
"ar": "sk-tavily-...",
|
||||
"fr": "sk-tavily-...",
|
||||
"tr": "sk-tavily-...",
|
||||
"de": "sk-tavily-...",
|
||||
"uk": "sk-tavily-..."
|
||||
},
|
||||
"API$TVLY_KEY_EXAMPLE": {
|
||||
"en": "tvly-...",
|
||||
"ja": "tvly-...",
|
||||
"zh-CN": "tvly-...",
|
||||
"zh-TW": "tvly-...",
|
||||
"ko-KR": "tvly-...",
|
||||
"no": "tvly-...",
|
||||
"it": "tvly-...",
|
||||
"pt": "tvly-...",
|
||||
"es": "tvly-...",
|
||||
"ar": "tvly-...",
|
||||
"fr": "tvly-...",
|
||||
"tr": "tvly-...",
|
||||
"de": "tvly-...",
|
||||
"uk": "tvly-..."
|
||||
},
|
||||
"SECRETS$CONNECT_GIT_PROVIDER": {
|
||||
"en": "Connect a Git provider to manage secrets",
|
||||
"ja": "シークレットを管理するためにGitプロバイダーに接続",
|
||||
"zh-CN": "连接Git提供商以管理密钥",
|
||||
"zh-TW": "連接Git提供商以管理密鑰",
|
||||
"ko-KR": "시크릿 관리를 위해 Git 제공자에 연결",
|
||||
"no": "Koble til en Git-leverandør for å administrere hemmeligheter",
|
||||
"it": "Connetti un provider Git per gestire i segreti",
|
||||
"pt": "Conectar um provedor Git para gerenciar segredos",
|
||||
"es": "Conectar un proveedor Git para gestionar secretos",
|
||||
"ar": "اتصل بمزود Git لإدارة الأسرار",
|
||||
"fr": "Connecter un fournisseur Git pour gérer les secrets",
|
||||
"tr": "Gizli anahtarları yönetmek için bir Git sağlayıcısına bağlan",
|
||||
"de": "Git-Anbieter verbinden, um Geheimnisse zu verwalten",
|
||||
"uk": "Підключити провайдера Git для управління секретами"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ const openHandsHandlers = [
|
||||
"gpt-4o-mini",
|
||||
"anthropic/claude-3.5",
|
||||
"anthropic/claude-sonnet-4-20250514",
|
||||
"sambanova/Meta-Llama-3.1-8B-Instruct",
|
||||
]),
|
||||
),
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ function AppSettingsScreen() {
|
||||
label={t(I18nKey.SETTINGS$MAX_BUDGET_PER_CONVERSATION)}
|
||||
defaultValue={settings.MAX_BUDGET_PER_TASK?.toString() || ""}
|
||||
onChange={checkIfMaxBudgetPerTaskHasChanged}
|
||||
placeholder="Maximum budget per conversation in USD"
|
||||
placeholder={t(I18nKey.SETTINGS$MAXIMUM_BUDGET_USD)}
|
||||
min={1}
|
||||
step={1}
|
||||
className="w-[680px]" // Match the width of the language field
|
||||
|
||||
@@ -185,7 +185,7 @@ function GitSettingsScreen() {
|
||||
!isGitHubTokenSet && !isGitLabTokenSet && !isBitbucketTokenSet
|
||||
}
|
||||
>
|
||||
Disconnect Tokens
|
||||
{t(I18nKey.GIT$DISCONNECT_TOKENS)}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
testId="submit-button"
|
||||
|
||||
@@ -23,6 +23,7 @@ import { isCustomModel } from "#/utils/is-custom-model";
|
||||
import { LlmSettingsInputsSkeleton } from "#/components/features/settings/llm-settings/llm-settings-inputs-skeleton";
|
||||
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { getProviderId } from "#/utils/map-provider";
|
||||
|
||||
function LlmSettingsScreen() {
|
||||
const { t } = useTranslation();
|
||||
@@ -93,13 +94,15 @@ function LlmSettingsScreen() {
|
||||
};
|
||||
|
||||
const basicFormAction = (formData: FormData) => {
|
||||
const provider = formData.get("llm-provider-input")?.toString();
|
||||
const providerDisplay = formData.get("llm-provider-input")?.toString();
|
||||
const provider = providerDisplay
|
||||
? getProviderId(providerDisplay)
|
||||
: undefined;
|
||||
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();
|
||||
const fullLlmModel = provider && model && `${provider}/${model}`;
|
||||
|
||||
saveSettings(
|
||||
{
|
||||
@@ -315,7 +318,7 @@ function LlmSettingsScreen() {
|
||||
className="w-full max-w-[680px]"
|
||||
defaultValue={settings.SEARCH_API_KEY || ""}
|
||||
onChange={handleSearchApiKeyIsDirty}
|
||||
placeholder="sk-tavily-..."
|
||||
placeholder={t(I18nKey.API$TAVILY_KEY_EXAMPLE)}
|
||||
startContent={
|
||||
settings.SEARCH_API_KEY_SET && (
|
||||
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />
|
||||
@@ -390,7 +393,7 @@ function LlmSettingsScreen() {
|
||||
className="w-full max-w-[680px]"
|
||||
defaultValue={settings.SEARCH_API_KEY || ""}
|
||||
onChange={handleSearchApiKeyIsDirty}
|
||||
placeholder="tvly-..."
|
||||
placeholder={t(I18nKey.API$TVLY_KEY_EXAMPLE)}
|
||||
startContent={
|
||||
settings.SEARCH_API_KEY_SET && (
|
||||
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />
|
||||
|
||||
@@ -13,6 +13,7 @@ import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { ConfirmationModal } from "#/components/shared/modals/confirmation-modal";
|
||||
import { GetSecretsResponse } from "#/api/secrets-service.types";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
|
||||
function SecretsSettingsScreen() {
|
||||
@@ -90,7 +91,7 @@ function SecretsSettingsScreen() {
|
||||
type="button"
|
||||
>
|
||||
<BrandButton type="button" variant="secondary">
|
||||
Connect a Git provider to manage secrets
|
||||
{t(I18nKey.SECRETS$CONNECT_GIT_PROVIDER)}
|
||||
</BrandButton>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -13,8 +13,10 @@ export function handleObservationMessage(message: ObservationMessage) {
|
||||
let { content } = message;
|
||||
|
||||
if (content.length > 5000) {
|
||||
const head = content.slice(0, 5000);
|
||||
content = `${head}\r\n\n... (truncated ${message.content.length - 5000} characters) ...`;
|
||||
const halfLength = 2500;
|
||||
const head = content.slice(0, halfLength);
|
||||
const tail = content.slice(content.length - halfLength);
|
||||
content = `${head}\r\n\n... (truncated ${message.content.length - 5000} characters) ...\r\n\n${tail}`;
|
||||
}
|
||||
|
||||
store.dispatch(appendOutput(content));
|
||||
|
||||
81
frontend/src/test/localization-fix.test.ts
Normal file
81
frontend/src/test/localization-fix.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { execSync } from "child_process";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
describe("Localization Fix Tests", () => {
|
||||
it("should not find any unlocalized strings in the frontend code", () => {
|
||||
const scriptPath = path.join(
|
||||
__dirname,
|
||||
"../../scripts/check-unlocalized-strings.cjs",
|
||||
);
|
||||
|
||||
// Run the localization check script
|
||||
const result = execSync(`node ${scriptPath}`, {
|
||||
cwd: path.join(__dirname, "../.."),
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
// The script should output success message and exit with code 0
|
||||
expect(result).toContain(
|
||||
"✅ No unlocalized strings found in frontend code.",
|
||||
);
|
||||
});
|
||||
|
||||
it("should properly detect user-facing attributes like placeholder, alt, and aria-label", () => {
|
||||
// This test verifies that our fix to include placeholder, alt, and aria-label
|
||||
// attributes in the localization check is working correctly by testing the regex patterns
|
||||
|
||||
const scriptPath = path.join(
|
||||
__dirname,
|
||||
"../../scripts/check-unlocalized-strings.cjs",
|
||||
);
|
||||
const scriptContent = fs.readFileSync(scriptPath, "utf8");
|
||||
|
||||
// Verify that these attributes are now being checked for localization
|
||||
// by ensuring they're not excluded from text extraction
|
||||
const nonTextAttributesMatch = scriptContent.match(
|
||||
/const NON_TEXT_ATTRIBUTES = \[(.*?)\]/s,
|
||||
);
|
||||
expect(nonTextAttributesMatch).toBeTruthy();
|
||||
|
||||
const nonTextAttributes = nonTextAttributesMatch![1];
|
||||
expect(nonTextAttributes).not.toContain('"placeholder"');
|
||||
expect(nonTextAttributes).not.toContain('"alt"');
|
||||
expect(nonTextAttributes).not.toContain('"aria-label"');
|
||||
|
||||
// Verify that the script contains the correct attributes that should be excluded
|
||||
expect(nonTextAttributes).toContain('"className"');
|
||||
expect(nonTextAttributes).toContain('"testId"');
|
||||
expect(nonTextAttributes).toContain('"href"');
|
||||
});
|
||||
|
||||
it("should not incorrectly flag CSS units as unlocalized strings", () => {
|
||||
// This test verifies that our fix to the CSS units regex pattern
|
||||
// prevents false positives like "Suggested Tasks" being flagged
|
||||
|
||||
const testStrings = [
|
||||
"Suggested Tasks",
|
||||
"No tasks available",
|
||||
"Select a branch",
|
||||
"Select a repo",
|
||||
"Custom Models",
|
||||
"API Keys",
|
||||
"Git Settings",
|
||||
];
|
||||
|
||||
// These strings should not be flagged as CSS units
|
||||
const cssUnitsPattern =
|
||||
/\b\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$|^(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
|
||||
|
||||
testStrings.forEach((str) => {
|
||||
expect(cssUnitsPattern.test(str)).toBe(false);
|
||||
});
|
||||
|
||||
// But actual CSS units should still be detected
|
||||
const actualCssUnits = ["10px", "2rem", "100vh", "px", "rem", "s"];
|
||||
actualCssUnits.forEach((unit) => {
|
||||
expect(cssUnitsPattern.test(unit)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseMaxBudgetPerTask } from "../settings-utils";
|
||||
import { parseMaxBudgetPerTask, extractSettings } from "../settings-utils";
|
||||
|
||||
describe("parseMaxBudgetPerTask", () => {
|
||||
it("should return null for empty string", () => {
|
||||
@@ -47,3 +47,45 @@ describe("parseMaxBudgetPerTask", () => {
|
||||
expect(parseMaxBudgetPerTask("5e-1")).toBeNull(); // 0.5, which is < 1
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractSettings", () => {
|
||||
it("should preserve model name case when extracting settings", () => {
|
||||
// Test cases with various model name formats
|
||||
const testCases = [
|
||||
{ provider: "sambanova", model: "Meta-Llama-3.1-8B-Instruct" },
|
||||
{ provider: "openai", model: "GPT-4o" },
|
||||
{ provider: "anthropic", model: "Claude-3-5-Sonnet" },
|
||||
{ provider: "openrouter", model: "CamelCaseModel" },
|
||||
];
|
||||
|
||||
testCases.forEach(({ provider, model }) => {
|
||||
const formData = new FormData();
|
||||
formData.set("llm-provider-input", provider);
|
||||
formData.set("llm-model-input", model);
|
||||
|
||||
const settings = extractSettings(formData);
|
||||
|
||||
// Verify that the model name case is preserved
|
||||
const expectedModel = `${provider}/${model}`;
|
||||
expect(settings.LLM_MODEL).toBe(expectedModel);
|
||||
// Only test that it's not lowercased if the original has uppercase letters
|
||||
if (expectedModel !== expectedModel.toLowerCase()) {
|
||||
expect(settings.LLM_MODEL).not.toBe(expectedModel.toLowerCase());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle custom model without lowercasing", () => {
|
||||
const formData = new FormData();
|
||||
formData.set("llm-provider-input", "sambanova");
|
||||
formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct");
|
||||
formData.set("use-advanced-options", "true");
|
||||
formData.set("custom-model", "Custom-Model-Name");
|
||||
|
||||
const settings = extractSettings(formData);
|
||||
|
||||
// Custom model should take precedence and preserve case
|
||||
expect(settings.LLM_MODEL).toBe("Custom-Model-Name");
|
||||
expect(settings.LLM_MODEL).not.toBe("custom-model-name");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,3 +29,10 @@ export const mapProvider = (provider: string) =>
|
||||
Object.keys(MAP_PROVIDER).includes(provider)
|
||||
? MAP_PROVIDER[provider as keyof typeof MAP_PROVIDER]
|
||||
: provider;
|
||||
|
||||
export const getProviderId = (displayName: string): string => {
|
||||
const entry = Object.entries(MAP_PROVIDER).find(
|
||||
([, value]) => value === displayName,
|
||||
);
|
||||
return entry ? entry[0] : displayName;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Settings } from "#/types/settings";
|
||||
import { getProviderId } from "#/utils/map-provider";
|
||||
|
||||
const extractBasicFormData = (formData: FormData) => {
|
||||
const provider = formData.get("llm-provider-input")?.toString();
|
||||
const providerDisplay = formData.get("llm-provider-input")?.toString();
|
||||
const provider = providerDisplay ? getProviderId(providerDisplay) : undefined;
|
||||
const model = formData.get("llm-model-input")?.toString();
|
||||
|
||||
const LLM_MODEL = `${provider}/${model}`.toLowerCase();
|
||||
const LLM_MODEL = `${provider}/${model}`;
|
||||
const LLM_API_KEY = formData.get("llm-api-key-input")?.toString();
|
||||
const AGENT = formData.get("agent")?.toString();
|
||||
const LANGUAGE = formData.get("language")?.toString();
|
||||
|
||||
34
openhands-ui/.gitignore
vendored
Normal file
34
openhands-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
15
openhands-ui/README.md
Normal file
15
openhands-ui/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# openhands-ui
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.16. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
25
openhands-ui/bun.lock
Normal file
25
openhands-ui/bun.lock
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "openhands-ui",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
}
|
||||
}
|
||||
1
openhands-ui/index.ts
Normal file
1
openhands-ui/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
console.log("Hello via Bun!");
|
||||
17
openhands-ui/package.json
Normal file
17
openhands-ui/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@openhands/ui",
|
||||
"version": "0.1.0",
|
||||
"description": "OpenHands UI Components",
|
||||
"keywords": [
|
||||
"openhands",
|
||||
"ui",
|
||||
"components"
|
||||
],
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
29
openhands-ui/tsconfig.json
Normal file
29
openhands-ui/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@ if TYPE_CHECKING:
|
||||
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
|
||||
from openhands.agenthub.codeact_agent.tools.bash import create_cmd_run_tool
|
||||
from openhands.agenthub.codeact_agent.tools.browser import BrowserTool
|
||||
from openhands.agenthub.codeact_agent.tools.condensation_request import (
|
||||
CondensationRequestTool,
|
||||
)
|
||||
from openhands.agenthub.codeact_agent.tools.finish import FinishTool
|
||||
from openhands.agenthub.codeact_agent.tools.ipython import IPythonTool
|
||||
from openhands.agenthub.codeact_agent.tools.llm_based_edit import LLMBasedFileEditTool
|
||||
@@ -119,6 +122,8 @@ class CodeActAgent(Agent):
|
||||
tools.append(ThinkTool)
|
||||
if self.config.enable_finish:
|
||||
tools.append(FinishTool)
|
||||
if self.config.enable_condensation_request:
|
||||
tools.append(CondensationRequestTool)
|
||||
if self.config.enable_browsing:
|
||||
if sys.platform == 'win32':
|
||||
logger.warning('Windows runtime does not support browsing yet')
|
||||
|
||||
@@ -11,6 +11,7 @@ from litellm import (
|
||||
|
||||
from openhands.agenthub.codeact_agent.tools import (
|
||||
BrowserTool,
|
||||
CondensationRequestTool,
|
||||
FinishTool,
|
||||
IPythonTool,
|
||||
LLMBasedFileEditTool,
|
||||
@@ -35,6 +36,7 @@ from openhands.events.action import (
|
||||
IPythonRunCellAction,
|
||||
MessageAction,
|
||||
)
|
||||
from openhands.events.action.agent import CondensationRequestAction
|
||||
from openhands.events.action.mcp import MCPAction
|
||||
from openhands.events.event import FileEditSource, FileReadSource
|
||||
from openhands.events.tool import ToolCallMetadata
|
||||
@@ -203,6 +205,12 @@ def response_to_actions(
|
||||
elif tool_call.function.name == ThinkTool['function']['name']:
|
||||
action = AgentThinkAction(thought=arguments.get('thought', ''))
|
||||
|
||||
# ================================================
|
||||
# CondensationRequestAction
|
||||
# ================================================
|
||||
elif tool_call.function.name == CondensationRequestTool['function']['name']:
|
||||
action = CondensationRequestAction()
|
||||
|
||||
# ================================================
|
||||
# BrowserTool
|
||||
# ================================================
|
||||
|
||||
@@ -32,6 +32,7 @@ Your primary role is to assist users by executing commands, modifying code, and
|
||||
</VERSION_CONTROL>
|
||||
|
||||
<PULL_REQUESTS>
|
||||
* **Important**: Do not push to the remote branch and/or start a pull request unless explicitly asked to do so.
|
||||
* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise.
|
||||
* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue.
|
||||
* When updating a PR, preserve the original PR title and purpose, updating description only when necessary.
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
|
||||
|
||||
<ROLE>
|
||||
Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed.
|
||||
* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question.
|
||||
</ROLE>
|
||||
|
||||
<EFFICIENCY>
|
||||
* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once.
|
||||
* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations.
|
||||
</EFFICIENCY>
|
||||
|
||||
<FILE_SYSTEM_GUIDELINES>
|
||||
* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it.
|
||||
* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename.
|
||||
* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times.
|
||||
</FILE_SYSTEM_GUIDELINES>
|
||||
|
||||
<CODE_QUALITY>
|
||||
* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself.
|
||||
* When implementing solutions, focus on making the minimal changes needed to solve the problem.
|
||||
* Before implementing any changes, first thoroughly understand the codebase through exploration.
|
||||
* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate.
|
||||
</CODE_QUALITY>
|
||||
|
||||
<VERSION_CONTROL>
|
||||
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
|
||||
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
|
||||
* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
|
||||
* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
|
||||
* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification.
|
||||
</VERSION_CONTROL>
|
||||
|
||||
<PULL_REQUESTS>
|
||||
* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise.
|
||||
* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue.
|
||||
* When updating a PR, preserve the original PR title and purpose, updating description only when necessary.
|
||||
</PULL_REQUESTS>
|
||||
|
||||
<PROBLEM_SOLVING_WORKFLOW>
|
||||
1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions
|
||||
2. ANALYSIS: Consider multiple approaches and select the most promising one
|
||||
3. TESTING:
|
||||
* For bug fixes: Create tests to verify issues before implementing fixes
|
||||
* For new features: Consider test-driven development when appropriate
|
||||
* If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure
|
||||
* If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies
|
||||
4. IMPLEMENTATION: Make focused, minimal changes to address the problem
|
||||
5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests.
|
||||
</PROBLEM_SOLVING_WORKFLOW>
|
||||
|
||||
<TASK_MANAGEMENT>
|
||||
* For complex, long-horizon tasks, create a TODO.md file to track progress:
|
||||
1. Start by creating a detailed plan in TODO.md with clear steps
|
||||
2. Check TODO.md before each new action to maintain context and track progress
|
||||
3. Update TODO.md as you complete steps or discover new requirements
|
||||
4. Mark completed items with ✓ or [x] to maintain a clear record of progress
|
||||
5. For each major step, add sub-tasks as needed to break down complex work
|
||||
6. If you discover the plan needs significant changes, propose updates and confirm with the user before proceeding and update TODO.md
|
||||
7. IMPORTANT: Do NOT add TODO.md to git commits or version control systems
|
||||
|
||||
* Example TODO.md format:
|
||||
```markdown
|
||||
# Task: [Brief description of the overall task]
|
||||
|
||||
## Plan
|
||||
- [ ] Step 1: [Description]
|
||||
- [ ] Sub-task 1.1
|
||||
- [ ] Sub-task 1.2
|
||||
- [ ] Step 2: [Description]
|
||||
- [x] Step 3: [Description] (Completed)
|
||||
|
||||
## Notes
|
||||
- Important discovery: [Details about something you learned]
|
||||
- Potential issue: [Description of a potential problem]
|
||||
```
|
||||
|
||||
* When working on a task:
|
||||
- Read the README to understand how the system works
|
||||
- Create TODO.md with every major step unchecked
|
||||
- Add TODO.md to .gitignore if it's not already ignored
|
||||
- Until every item in TODO.md is checked:
|
||||
a. Pick the next unchecked item and work on it
|
||||
b. Run appropriate tests to verify your work
|
||||
c. If issues arise, fix them until tests pass
|
||||
d. Once complete, check off the item in TODO.md
|
||||
e. Proceed to the next unchecked item
|
||||
</TASK_MANAGEMENT>
|
||||
|
||||
<SECURITY>
|
||||
* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect.
|
||||
* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing.
|
||||
</SECURITY>
|
||||
|
||||
<ENVIRONMENT_SETUP>
|
||||
* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again.
|
||||
* If you encounter missing dependencies:
|
||||
1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.)
|
||||
2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.)
|
||||
3. Only install individual packages directly if no dependency files are found or if only specific packages are needed
|
||||
* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible.
|
||||
</ENVIRONMENT_SETUP>
|
||||
|
||||
<TROUBLESHOOTING>
|
||||
* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken:
|
||||
1. Step back and reflect on 5-7 different possible sources of the problem
|
||||
2. Assess the likelihood of each possible cause
|
||||
3. Methodically address the most likely causes, starting with the highest probability
|
||||
4. Document your reasoning process
|
||||
* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding.
|
||||
</TROUBLESHOOTING>
|
||||
@@ -1,5 +1,6 @@
|
||||
from .bash import create_cmd_run_tool
|
||||
from .browser import BrowserTool
|
||||
from .condensation_request import CondensationRequestTool
|
||||
from .finish import FinishTool
|
||||
from .ipython import IPythonTool
|
||||
from .llm_based_edit import LLMBasedFileEditTool
|
||||
@@ -8,6 +9,7 @@ from .think import ThinkTool
|
||||
|
||||
__all__ = [
|
||||
'BrowserTool',
|
||||
'CondensationRequestTool',
|
||||
'create_cmd_run_tool',
|
||||
'FinishTool',
|
||||
'IPythonTool',
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
|
||||
|
||||
_CONDENSATION_REQUEST_DESCRIPTION = 'Request a condensation of the conversation history when the context becomes too long or when you need to focus on the most relevant information.'
|
||||
|
||||
CondensationRequestTool = ChatCompletionToolParam(
|
||||
type='function',
|
||||
function=ChatCompletionToolParamFunctionChunk(
|
||||
name='request_condensation',
|
||||
description=_CONDENSATION_REQUEST_DESCRIPTION,
|
||||
parameters={
|
||||
'type': 'object',
|
||||
'properties': {},
|
||||
'required': [],
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -77,7 +77,6 @@ async def cleanup_session(
|
||||
controller: AgentController,
|
||||
) -> None:
|
||||
"""Clean up all resources from the current session."""
|
||||
|
||||
event_stream = runtime.event_stream
|
||||
end_state = controller.get_state()
|
||||
end_state.save_to_session(
|
||||
@@ -121,6 +120,7 @@ async def run_session(
|
||||
sid = generate_sid(config, session_name)
|
||||
is_loaded = asyncio.Event()
|
||||
is_paused = asyncio.Event() # Event to track agent pause requests
|
||||
pause_task: asyncio.Task | None = None # No more than one pause task
|
||||
always_confirm_mode = False # Flag to enable always confirm mode
|
||||
|
||||
# Show runtime initialization message
|
||||
@@ -236,9 +236,11 @@ async def run_session(
|
||||
|
||||
if event.agent_state == AgentState.RUNNING:
|
||||
display_agent_running_message()
|
||||
loop.create_task(
|
||||
process_agent_pause(is_paused, event_stream)
|
||||
) # Create a task to track agent pause requests from the user
|
||||
nonlocal pause_task
|
||||
if pause_task is None or pause_task.done():
|
||||
pause_task = loop.create_task(
|
||||
process_agent_pause(is_paused, event_stream)
|
||||
) # Create a task to track agent pause requests from the user
|
||||
|
||||
def on_event(event: Event) -> None:
|
||||
loop.create_task(on_event_async(event))
|
||||
@@ -434,7 +436,23 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
|
||||
return
|
||||
|
||||
# Read task from file, CLI args, or stdin
|
||||
task_str = read_task(args, config.cli_multiline_input)
|
||||
if args.file:
|
||||
# For CLI usage, we want to enhance the file content with a prompt
|
||||
# that instructs the agent to read and understand the file first
|
||||
with open(args.file, 'r', encoding='utf-8') as file:
|
||||
file_content = file.read()
|
||||
|
||||
# Create a prompt that instructs the agent to read and understand the file first
|
||||
task_str = f"""The user has tagged a file '{args.file}'.
|
||||
Please read and understand the following file content first:
|
||||
|
||||
```
|
||||
{file_content}
|
||||
```
|
||||
|
||||
After reviewing the file, please ask the user what they would like to do with it."""
|
||||
else:
|
||||
task_str = read_task(args, config.cli_multiline_input)
|
||||
|
||||
# Run the first session
|
||||
new_session_requested = await run_session(
|
||||
|
||||
@@ -59,7 +59,11 @@ from openhands.events.action import (
|
||||
NullAction,
|
||||
SystemMessageAction,
|
||||
)
|
||||
from openhands.events.action.agent import CondensationAction, RecallAction
|
||||
from openhands.events.action.agent import (
|
||||
CondensationAction,
|
||||
CondensationRequestAction,
|
||||
RecallAction,
|
||||
)
|
||||
from openhands.events.event import Event
|
||||
from openhands.events.observation import (
|
||||
AgentDelegateObservation,
|
||||
@@ -70,8 +74,7 @@ from openhands.events.observation import (
|
||||
)
|
||||
from openhands.events.serialization.event import truncate_content
|
||||
from openhands.llm.llm import LLM
|
||||
from openhands.llm.metrics import Metrics, TokenUsage
|
||||
from openhands.memory.view import View
|
||||
from openhands.llm.metrics import Metrics
|
||||
from openhands.storage.files import FileStore
|
||||
|
||||
# note: RESUME is only available on web GUI
|
||||
@@ -336,6 +339,8 @@ class AgentController:
|
||||
return True
|
||||
if isinstance(event, CondensationAction):
|
||||
return True
|
||||
if isinstance(event, CondensationRequestAction):
|
||||
return True
|
||||
return False
|
||||
if isinstance(event, Observation):
|
||||
if (
|
||||
@@ -829,7 +834,9 @@ class AgentController:
|
||||
or isinstance(e, ContextWindowExceededError)
|
||||
):
|
||||
if self.agent.config.enable_history_truncation:
|
||||
self._handle_long_context_error()
|
||||
self.event_stream.add_event(
|
||||
CondensationRequestAction(), EventSource.AGENT
|
||||
)
|
||||
return
|
||||
else:
|
||||
raise LLMContextWindowExceedError()
|
||||
@@ -880,7 +887,7 @@ class AgentController:
|
||||
action_id = getattr(action, 'id', 'unknown')
|
||||
action_type = type(action).__name__
|
||||
self.log(
|
||||
'warning',
|
||||
'info',
|
||||
f'Pending action active for {elapsed_time:.2f}s: {action_type} (id={action_id})',
|
||||
extra={'msg_type': 'PENDING_ACTION_TIMEOUT'},
|
||||
)
|
||||
@@ -949,180 +956,6 @@ class AgentController:
|
||||
assert self._closed
|
||||
return self.state_tracker.get_trajectory(include_screenshots)
|
||||
|
||||
def _handle_long_context_error(self) -> None:
|
||||
# When context window is exceeded, keep roughly half of agent interactions
|
||||
current_view = View.from_events(self.state.history)
|
||||
kept_events = self._apply_conversation_window(current_view.events)
|
||||
kept_event_ids = {e.id for e in kept_events}
|
||||
|
||||
self.log(
|
||||
'info',
|
||||
f'Context window exceeded. Keeping events with IDs: {kept_event_ids}',
|
||||
)
|
||||
|
||||
# The events to forget are those that are not in the kept set
|
||||
forgotten_event_ids = {e.id for e in self.state.history} - kept_event_ids
|
||||
|
||||
if len(kept_event_ids) == 0:
|
||||
self.log(
|
||||
'warning',
|
||||
'No events kept after applying conversation window. This should not happen.',
|
||||
)
|
||||
|
||||
# verify that the first event id in kept_event_ids is the same as the start_id
|
||||
if len(kept_event_ids) > 0 and self.state.history[0].id not in kept_event_ids:
|
||||
self.log(
|
||||
'warning',
|
||||
f'First event after applying conversation window was not kept: {self.state.history[0].id} not in {kept_event_ids}',
|
||||
)
|
||||
|
||||
# Add an error event to trigger another step by the agent
|
||||
self.event_stream.add_event(
|
||||
CondensationAction(
|
||||
forgotten_events_start_id=min(forgotten_event_ids)
|
||||
if forgotten_event_ids
|
||||
else 0,
|
||||
forgotten_events_end_id=max(forgotten_event_ids)
|
||||
if forgotten_event_ids
|
||||
else 0,
|
||||
),
|
||||
EventSource.AGENT,
|
||||
)
|
||||
|
||||
def _apply_conversation_window(self, history: list[Event]) -> list[Event]:
|
||||
"""Cuts history roughly in half when context window is exceeded.
|
||||
|
||||
It preserves action-observation pairs and ensures that the system message,
|
||||
the first user message, and its associated recall observation are always included
|
||||
at the beginning of the context window.
|
||||
|
||||
The algorithm:
|
||||
1. Identify essential initial events: System Message, First User Message, Recall Observation.
|
||||
2. Determine the slice of recent events to potentially keep.
|
||||
3. Validate the start of the recent slice for dangling observations.
|
||||
4. Combine essential events and validated recent events, ensuring essentials come first.
|
||||
|
||||
Args:
|
||||
events: List of events to filter
|
||||
|
||||
Returns:
|
||||
Filtered list of events keeping newest half while preserving pairs and essential initial events.
|
||||
"""
|
||||
# Handle empty history
|
||||
if not history:
|
||||
return []
|
||||
# 1. Identify essential initial events
|
||||
system_message: SystemMessageAction | None = None
|
||||
first_user_msg: MessageAction | None = None
|
||||
recall_action: RecallAction | None = None
|
||||
recall_observation: Observation | None = None
|
||||
|
||||
# Find System Message (should be the first event, if it exists)
|
||||
system_message = next(
|
||||
(e for e in history if isinstance(e, SystemMessageAction)), None
|
||||
)
|
||||
assert (
|
||||
system_message is None
|
||||
or isinstance(system_message, SystemMessageAction)
|
||||
and system_message.id == history[0].id
|
||||
)
|
||||
|
||||
# Find First User Message in the history, which MUST exist
|
||||
first_user_msg = self._first_user_message(history)
|
||||
if first_user_msg is None:
|
||||
# If not found in history, try the event stream
|
||||
first_user_msg = self._first_user_message()
|
||||
if first_user_msg is None:
|
||||
raise RuntimeError('No first user message found in the event stream.')
|
||||
self.log(
|
||||
'warning',
|
||||
'First user message not found in history. Using cached version from event stream.',
|
||||
)
|
||||
|
||||
# Find the first user message index in the history
|
||||
first_user_msg_index = -1
|
||||
for i, event in enumerate(history):
|
||||
if isinstance(event, MessageAction) and event.source == EventSource.USER:
|
||||
first_user_msg_index = i
|
||||
break
|
||||
|
||||
# Find Recall Action and Observation related to the First User Message
|
||||
# Look for RecallAction after the first user message
|
||||
for i in range(first_user_msg_index + 1, len(history)):
|
||||
event = history[i]
|
||||
if (
|
||||
isinstance(event, RecallAction)
|
||||
and event.query == first_user_msg.content
|
||||
):
|
||||
# Found RecallAction, now look for its Observation
|
||||
recall_action = event
|
||||
for j in range(i + 1, len(history)):
|
||||
obs_event = history[j]
|
||||
# Check for Observation caused by this RecallAction
|
||||
if (
|
||||
isinstance(obs_event, Observation)
|
||||
and obs_event.cause == recall_action.id
|
||||
):
|
||||
recall_observation = obs_event
|
||||
break # Found the observation, stop inner loop
|
||||
break # Found the recall action (and maybe obs), stop outer loop
|
||||
|
||||
essential_events: list[Event] = []
|
||||
if system_message:
|
||||
essential_events.append(system_message)
|
||||
# Only include first user message if history is not empty
|
||||
if history:
|
||||
essential_events.append(first_user_msg)
|
||||
# Include recall action and observation if both exist
|
||||
if recall_action and recall_observation:
|
||||
essential_events.append(recall_action)
|
||||
essential_events.append(recall_observation)
|
||||
# Include recall action without observation for backward compatibility
|
||||
elif recall_action:
|
||||
essential_events.append(recall_action)
|
||||
|
||||
# 2. Determine the slice of recent events to potentially keep
|
||||
num_non_essential_events = len(history) - len(essential_events)
|
||||
# Keep roughly half of the non-essential events, minimum 1
|
||||
num_recent_to_keep = max(1, num_non_essential_events // 2)
|
||||
|
||||
# Calculate the starting index for the recent slice
|
||||
slice_start_index = len(history) - num_recent_to_keep
|
||||
slice_start_index = max(0, slice_start_index) # Ensure index is not negative
|
||||
recent_events_slice = history[slice_start_index:]
|
||||
|
||||
# 3. Validate the start of the recent slice for dangling observations
|
||||
# IMPORTANT: Most observations in history are tool call results, which cannot be without their action, or we get an LLM API error
|
||||
first_valid_event_index = 0
|
||||
for i, event in enumerate(recent_events_slice):
|
||||
if isinstance(event, Observation):
|
||||
first_valid_event_index += 1
|
||||
else:
|
||||
break
|
||||
# If all events in the slice are dangling observations, we need to keep at least one
|
||||
if first_valid_event_index == len(recent_events_slice):
|
||||
self.log(
|
||||
'warning',
|
||||
'All recent events are dangling observations, which we truncate. This means the agent has only the essential first events. This should not happen.',
|
||||
)
|
||||
|
||||
# Adjust the recent_events_slice if dangling observations were found at the start
|
||||
if first_valid_event_index < len(recent_events_slice):
|
||||
validated_recent_events = recent_events_slice[first_valid_event_index:]
|
||||
if first_valid_event_index > 0:
|
||||
self.log(
|
||||
'debug',
|
||||
f'Removed {first_valid_event_index} dangling observation(s) from the start of recent event slice.',
|
||||
)
|
||||
else:
|
||||
validated_recent_events = []
|
||||
|
||||
# 4. Combine essential events and validated recent events
|
||||
events_to_keep: list[Event] = essential_events + validated_recent_events
|
||||
self.log('debug', f'History truncated. Kept {len(events_to_keep)} events.')
|
||||
|
||||
return events_to_keep
|
||||
|
||||
def _is_stuck(self) -> bool:
|
||||
"""Checks if the agent or its delegate is stuck in a loop.
|
||||
|
||||
@@ -1152,7 +985,7 @@ class AgentController:
|
||||
agent_metrics = self.state.metrics
|
||||
|
||||
# Get metrics from condenser LLM if it exists
|
||||
condenser_metrics: TokenUsage | None = None
|
||||
condenser_metrics: Metrics | None = None
|
||||
if hasattr(self.agent, 'condenser') and hasattr(self.agent.condenser, 'llm'):
|
||||
condenser_metrics = self.agent.condenser.llm.metrics
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
from openhands.core.config.condenser_config import CondenserConfig, NoOpCondenserConfig
|
||||
from openhands.core.config.extended_config import ExtendedConfig
|
||||
@@ -31,6 +31,8 @@ class AgentConfig(BaseModel):
|
||||
"""Whether to enable think tool"""
|
||||
enable_finish: bool = Field(default=True)
|
||||
"""Whether to enable finish tool"""
|
||||
enable_condensation_request: bool = Field(default=False)
|
||||
"""Whether to enable condensation request tool"""
|
||||
enable_prompt_extensions: bool = Field(default=True)
|
||||
"""Whether to enable prompt extensions"""
|
||||
enable_mcp: bool = Field(default=True)
|
||||
@@ -47,12 +49,11 @@ class AgentConfig(BaseModel):
|
||||
extended: ExtendedConfig = Field(default_factory=lambda: ExtendedConfig({}))
|
||||
"""Extended configuration for the agent."""
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
@classmethod
|
||||
def from_toml_section(cls, data: dict) -> dict[str, AgentConfig]:
|
||||
"""
|
||||
Create a mapping of AgentConfig instances from a toml dictionary representing the [agent] section.
|
||||
"""Create a mapping of AgentConfig instances from a toml dictionary representing the [agent] section.
|
||||
|
||||
The default configuration is built from all non-dict keys in data.
|
||||
Then, each key with a dict value is treated as a custom agent configuration, and its values override
|
||||
@@ -70,7 +71,6 @@ class AgentConfig(BaseModel):
|
||||
dict[str, AgentConfig]: A mapping where the key "agent" corresponds to the default configuration
|
||||
and additional keys represent custom configurations.
|
||||
"""
|
||||
|
||||
# Initialize the result mapping
|
||||
agent_mapping: dict[str, AgentConfig] = {}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Literal, cast
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
from openhands.core import logger
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
@@ -11,28 +11,28 @@ from openhands.core.config.llm_config import LLMConfig
|
||||
class NoOpCondenserConfig(BaseModel):
|
||||
"""Configuration for NoOpCondenser."""
|
||||
|
||||
type: Literal['noop'] = Field('noop')
|
||||
type: Literal['noop'] = Field(default='noop')
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
class ObservationMaskingCondenserConfig(BaseModel):
|
||||
"""Configuration for ObservationMaskingCondenser."""
|
||||
|
||||
type: Literal['observation_masking'] = Field('observation_masking')
|
||||
type: Literal['observation_masking'] = Field(default='observation_masking')
|
||||
attention_window: int = Field(
|
||||
default=100,
|
||||
description='The number of most-recent events where observations will not be masked.',
|
||||
ge=1,
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
class BrowserOutputCondenserConfig(BaseModel):
|
||||
"""Configuration for the BrowserOutputCondenser."""
|
||||
|
||||
type: Literal['browser_output_masking'] = Field('browser_output_masking')
|
||||
type: Literal['browser_output_masking'] = Field(default='browser_output_masking')
|
||||
attention_window: int = Field(
|
||||
default=1,
|
||||
description='The number of most recent browser output observations that will not be masked.',
|
||||
@@ -43,7 +43,7 @@ class BrowserOutputCondenserConfig(BaseModel):
|
||||
class RecentEventsCondenserConfig(BaseModel):
|
||||
"""Configuration for RecentEventsCondenser."""
|
||||
|
||||
type: Literal['recent'] = Field('recent')
|
||||
type: Literal['recent'] = Field(default='recent')
|
||||
|
||||
# at least one event by default, because the best guess is that it is the user task
|
||||
keep_first: int = Field(
|
||||
@@ -55,13 +55,13 @@ class RecentEventsCondenserConfig(BaseModel):
|
||||
default=100, description='Maximum number of events to keep.', ge=1
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
class LLMSummarizingCondenserConfig(BaseModel):
|
||||
"""Configuration for LLMCondenser."""
|
||||
|
||||
type: Literal['llm'] = Field('llm')
|
||||
type: Literal['llm'] = Field(default='llm')
|
||||
llm_config: LLMConfig = Field(
|
||||
..., description='Configuration for the LLM to use for condensing.'
|
||||
)
|
||||
@@ -82,13 +82,13 @@ class LLMSummarizingCondenserConfig(BaseModel):
|
||||
description='Maximum length of the event representations to be passed to the LLM.',
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
class AmortizedForgettingCondenserConfig(BaseModel):
|
||||
"""Configuration for AmortizedForgettingCondenser."""
|
||||
|
||||
type: Literal['amortized'] = Field('amortized')
|
||||
type: Literal['amortized'] = Field(default='amortized')
|
||||
max_size: int = Field(
|
||||
default=100,
|
||||
description='Maximum size of the condensed history before triggering forgetting.',
|
||||
@@ -102,13 +102,13 @@ class AmortizedForgettingCondenserConfig(BaseModel):
|
||||
ge=0,
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
class LLMAttentionCondenserConfig(BaseModel):
|
||||
"""Configuration for LLMAttentionCondenser."""
|
||||
|
||||
type: Literal['llm_attention'] = Field('llm_attention')
|
||||
type: Literal['llm_attention'] = Field(default='llm_attention')
|
||||
llm_config: LLMConfig = Field(
|
||||
..., description='Configuration for the LLM to use for attention.'
|
||||
)
|
||||
@@ -125,13 +125,13 @@ class LLMAttentionCondenserConfig(BaseModel):
|
||||
ge=0,
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
class StructuredSummaryCondenserConfig(BaseModel):
|
||||
"""Configuration for StructuredSummaryCondenser instances."""
|
||||
|
||||
type: Literal['structured'] = Field('structured')
|
||||
type: Literal['structured'] = Field(default='structured')
|
||||
llm_config: LLMConfig = Field(
|
||||
..., description='Configuration for the LLM to use for condensing.'
|
||||
)
|
||||
@@ -152,22 +152,30 @@ class StructuredSummaryCondenserConfig(BaseModel):
|
||||
description='Maximum length of the event representations to be passed to the LLM.',
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
class CondenserPipelineConfig(BaseModel):
|
||||
"""Configuration for the CondenserPipeline.
|
||||
"""Configuration for the CondenserPipeline."""
|
||||
|
||||
Not currently supported by the TOML or ENV_VAR configuration strategies.
|
||||
"""
|
||||
|
||||
type: Literal['pipeline'] = Field('pipeline')
|
||||
type: Literal['pipeline'] = Field(default='pipeline')
|
||||
condensers: list[CondenserConfig] = Field(
|
||||
default_factory=list,
|
||||
description='List of condenser configurations to be used in the pipeline.',
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
class ConversationWindowCondenserConfig(BaseModel):
|
||||
"""Configuration for ConversationWindowCondenser.
|
||||
|
||||
Not currently supported by the TOML or ENV_VAR configuration strategies.
|
||||
"""
|
||||
|
||||
type: Literal['conversation_window'] = Field(default='conversation_window')
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
|
||||
# Type alias for convenience
|
||||
@@ -181,14 +189,14 @@ CondenserConfig = (
|
||||
| LLMAttentionCondenserConfig
|
||||
| StructuredSummaryCondenserConfig
|
||||
| CondenserPipelineConfig
|
||||
| ConversationWindowCondenserConfig
|
||||
)
|
||||
|
||||
|
||||
def condenser_config_from_toml_section(
|
||||
data: dict, llm_configs: dict | None = None
|
||||
) -> dict[str, CondenserConfig]:
|
||||
"""
|
||||
Create a CondenserConfig instance from a toml dictionary representing the [condenser] section.
|
||||
"""Create a CondenserConfig instance from a toml dictionary representing the [condenser] section.
|
||||
|
||||
For CondenserConfig, the handling is different since it's a union type. The type of condenser
|
||||
is determined by the 'type' field in the section.
|
||||
@@ -210,7 +218,6 @@ def condenser_config_from_toml_section(
|
||||
Returns:
|
||||
dict[str, CondenserConfig]: A mapping where the key "condenser" corresponds to the configuration.
|
||||
"""
|
||||
|
||||
# Initialize the result mapping
|
||||
condenser_mapping: dict[str, CondenserConfig] = {}
|
||||
|
||||
@@ -261,8 +268,7 @@ from_toml_section = condenser_config_from_toml_section
|
||||
|
||||
|
||||
def create_condenser_config(condenser_type: str, data: dict) -> CondenserConfig:
|
||||
"""
|
||||
Create a CondenserConfig instance based on the specified type.
|
||||
"""Create a CondenserConfig instance based on the specified type.
|
||||
|
||||
Args:
|
||||
condenser_type: The type of condenser to create.
|
||||
@@ -284,6 +290,9 @@ def create_condenser_config(condenser_type: str, data: dict) -> CondenserConfig:
|
||||
'amortized': AmortizedForgettingCondenserConfig,
|
||||
'llm_attention': LLMAttentionCondenserConfig,
|
||||
'structured': StructuredSummaryCondenserConfig,
|
||||
'pipeline': CondenserPipelineConfig,
|
||||
'conversation_window': ConversationWindowCondenserConfig,
|
||||
'browser_output_masking': BrowserOutputCondenserConfig,
|
||||
}
|
||||
|
||||
if condenser_type not in condenser_classes:
|
||||
|
||||
@@ -51,7 +51,7 @@ def get_field_info(field: FieldInfo) -> dict[str, Any]:
|
||||
def model_defaults_to_dict(model: BaseModel) -> dict[str, Any]:
|
||||
"""Serialize field information in a dict for the frontend, including type hints, defaults, and whether it's optional."""
|
||||
result = {}
|
||||
for name, field in model.model_fields.items():
|
||||
for name, field in model.__class__.model_fields.items():
|
||||
field_value = getattr(model, name)
|
||||
|
||||
if isinstance(field_value, BaseModel):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
|
||||
class KubernetesConfig(BaseModel):
|
||||
@@ -62,7 +62,7 @@ class KubernetesConfig(BaseModel):
|
||||
description='Run the runtime sandbox container in privileged mode for use with docker-in-docker',
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
@classmethod
|
||||
def from_toml_section(cls, data: dict) -> dict[str, 'KubernetesConfig']:
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, SecretStr, ValidationError
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, ValidationError
|
||||
|
||||
from openhands.core.logger import LOG_DIR
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
@@ -45,6 +45,7 @@ class LLMConfig(BaseModel):
|
||||
native_tool_calling: Whether to use native tool calling if supported by the model. Can be True, False, or not set.
|
||||
reasoning_effort: The effort to put into reasoning. This is a string that can be one of 'low', 'medium', 'high', or 'none'. Exclusive for o1 models.
|
||||
seed: The seed to use for the LLM.
|
||||
safety_settings: Safety settings for models that support them (like Mistral AI and Gemini).
|
||||
"""
|
||||
|
||||
model: str = Field(default='claude-sonnet-4-20250514')
|
||||
@@ -86,8 +87,12 @@ class LLMConfig(BaseModel):
|
||||
native_tool_calling: bool | None = Field(default=None)
|
||||
reasoning_effort: str | None = Field(default='high')
|
||||
seed: int | None = Field(default=None)
|
||||
safety_settings: list[dict[str, str]] | None = Field(
|
||||
default=None,
|
||||
description='Safety settings for models that support them (like Mistral AI and Gemini)',
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
@classmethod
|
||||
def from_toml_section(cls, data: dict) -> dict[str, LLMConfig]:
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openhands.core.config.openhands_config import OpenHandsConfig
|
||||
@@ -72,7 +72,7 @@ class MCPConfig(BaseModel):
|
||||
stdio_servers: list[MCPStdioServerConfig] = Field(default_factory=list)
|
||||
shttp_servers: list[MCPSHTTPServerConfig] = Field(default_factory=list)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
@staticmethod
|
||||
def _normalize_servers(servers_data: list[dict | str]) -> list[dict]:
|
||||
@@ -131,7 +131,9 @@ class MCPConfig(BaseModel):
|
||||
# Convert all entries in sse_servers to MCPSSEServerConfig objects
|
||||
if 'sse_servers' in data:
|
||||
data['sse_servers'] = cls._normalize_servers(data['sse_servers'])
|
||||
servers = []
|
||||
servers: list[
|
||||
MCPSSEServerConfig | MCPStdioServerConfig | MCPSHTTPServerConfig
|
||||
] = []
|
||||
for server in data['sse_servers']:
|
||||
servers.append(MCPSSEServerConfig(**server))
|
||||
data['sse_servers'] = servers
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from pydantic import BaseModel, Field, SecretStr
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr
|
||||
|
||||
from openhands.core import logger
|
||||
from openhands.core.config.agent_config import AgentConfig
|
||||
@@ -46,7 +46,6 @@ class OpenHandsConfig(BaseModel):
|
||||
run_as_openhands: Whether to run as openhands.
|
||||
max_iterations: Maximum number of iterations allowed.
|
||||
max_budget_per_task: Maximum budget per task, agent stops if exceeded.
|
||||
e2b_api_key: E2B API key.
|
||||
disable_color: Whether to disable terminal colors. For terminals that don't support color.
|
||||
debug: Whether to enable debugging mode.
|
||||
file_uploads_max_file_size_mb: Maximum file upload size in MB. `0` means unlimited.
|
||||
@@ -88,19 +87,14 @@ class OpenHandsConfig(BaseModel):
|
||||
run_as_openhands: bool = Field(default=True)
|
||||
max_iterations: int = Field(default=OH_MAX_ITERATIONS)
|
||||
max_budget_per_task: float | None = Field(default=None)
|
||||
e2b_api_key: SecretStr | None = Field(default=None)
|
||||
modal_api_token_id: SecretStr | None = Field(default=None)
|
||||
modal_api_token_secret: SecretStr | None = Field(default=None)
|
||||
|
||||
disable_color: bool = Field(default=False)
|
||||
jwt_secret: SecretStr | None = Field(default=None)
|
||||
debug: bool = Field(default=False)
|
||||
file_uploads_max_file_size_mb: int = Field(default=0)
|
||||
file_uploads_restrict_file_types: bool = Field(default=False)
|
||||
file_uploads_allowed_extensions: list[str] = Field(default_factory=lambda: ['.*'])
|
||||
runloop_api_key: SecretStr | None = Field(default=None)
|
||||
daytona_api_key: SecretStr | None = Field(default=None)
|
||||
daytona_api_url: str = Field(default='https://app.daytona.io/api')
|
||||
daytona_target: str = Field(default='eu')
|
||||
|
||||
cli_multiline_input: bool = Field(default=False)
|
||||
conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds
|
||||
enable_default_condenser: bool = Field(default=True)
|
||||
@@ -114,7 +108,7 @@ class OpenHandsConfig(BaseModel):
|
||||
|
||||
defaults_dict: ClassVar[dict] = {}
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
def get_llm_config(self, name: str = 'llm') -> LLMConfig:
|
||||
"""'llm' is the name for default config (for backward compatibility prior to 0.8)."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
|
||||
|
||||
|
||||
class SandboxConfig(BaseModel):
|
||||
@@ -88,7 +88,8 @@ class SandboxConfig(BaseModel):
|
||||
description="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'",
|
||||
)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
cuda_visible_devices: str | None = Field(default=None)
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
@classmethod
|
||||
def from_toml_section(cls, data: dict) -> dict[str, 'SandboxConfig']:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
|
||||
class SecurityConfig(BaseModel):
|
||||
@@ -12,7 +12,7 @@ class SecurityConfig(BaseModel):
|
||||
confirmation_mode: bool = Field(default=False)
|
||||
security_analyzer: str | None = Field(default=None)
|
||||
|
||||
model_config = {'extra': 'forbid'}
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
@classmethod
|
||||
def from_toml_section(cls, data: dict) -> dict[str, 'SecurityConfig']:
|
||||
|
||||
@@ -67,7 +67,7 @@ def load_from_env(
|
||||
# helper function to set attributes based on env vars
|
||||
def set_attr_from_env(sub_config: BaseModel, prefix: str = '') -> None:
|
||||
"""Set attributes of a config model based on environment variables."""
|
||||
for field_name, field_info in sub_config.model_fields.items():
|
||||
for field_name, field_info in sub_config.__class__.model_fields.items():
|
||||
field_value = getattr(sub_config, field_name)
|
||||
field_type = field_info.annotation
|
||||
|
||||
|
||||
@@ -261,6 +261,7 @@ class SensitiveDataFilter(logging.Filter):
|
||||
'modal_api_token_secret',
|
||||
'llm_api_key',
|
||||
'sandbox_env_github_token',
|
||||
'runloop_api_key',
|
||||
'daytona_api_key',
|
||||
]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user