mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
63 Commits
0.46.0
...
update-mic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93287ef9ac | ||
|
|
e70595f46f | ||
|
|
1d3ff66987 | ||
|
|
1a95f86802 | ||
|
|
eee12bfd94 | ||
|
|
8c2d4dbe8b | ||
|
|
0ca3188afa | ||
|
|
283f503870 | ||
|
|
0691e5c0d0 | ||
|
|
fc16da8fd2 | ||
|
|
bd3ff43c67 | ||
|
|
0fe5b808af | ||
|
|
6c49686ff0 | ||
|
|
17212bb2f2 | ||
|
|
9d9f931e95 | ||
|
|
6fe9680474 | ||
|
|
53c80d1c92 | ||
|
|
401262f353 | ||
|
|
58845b01a3 | ||
|
|
469d184157 | ||
|
|
4837c4dc74 | ||
|
|
6763f21cc3 | ||
|
|
32e610ac1d | ||
|
|
85c65391ca | ||
|
|
c444dbfbbf | ||
|
|
dd988d0f14 | ||
|
|
6f1a74e286 | ||
|
|
7b956b6103 | ||
|
|
34b097115d | ||
|
|
3e4ab4f379 | ||
|
|
54cd9f7e44 | ||
|
|
802b765f98 | ||
|
|
18c88f99ff | ||
|
|
f3934be07b | ||
|
|
6ce9f49d1e | ||
|
|
fc07622b20 | ||
|
|
da935f9d8f | ||
|
|
642cc52a1a | ||
|
|
4c361ab9e5 | ||
|
|
5dfa1bb6eb | ||
|
|
a07cf972a5 | ||
|
|
f2e3bc3254 | ||
|
|
3790ec7d60 | ||
|
|
3c0719309e | ||
|
|
0236e0943e | ||
|
|
cd464c0022 | ||
|
|
4519a7f4f3 | ||
|
|
fdc591330b | ||
|
|
98e454e82c | ||
|
|
e088d2d24a | ||
|
|
58c574af1e | ||
|
|
405f0069f8 | ||
|
|
f26d770d03 | ||
|
|
bf2c3de219 | ||
|
|
7c35ce16e5 | ||
|
|
f4024ccd94 | ||
|
|
b55bfed831 | ||
|
|
cb0994027f | ||
|
|
bcc9bd0b9a | ||
|
|
6c144e6b5a | ||
|
|
e90b841b0d | ||
|
|
a1e6ed4dff | ||
|
|
ad6311d3cd |
@@ -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. Located in `microagents/`.
|
||||
A general-purpose microagent available to all OpenHands users, triggered by specific keywords.
|
||||
|
||||
#### Repository Microagent
|
||||
A type of microagent that provides repository-specific context and guidelines, stored in the `.openhands/microagents/` directory.
|
||||
|
||||
@@ -68,29 +68,6 @@ 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.46-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.45-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
15
README.md
15
README.md
@@ -62,17 +62,17 @@ system requirements and more information.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
```
|
||||
|
||||
> **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,14 +85,15 @@ works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
|
||||
|
||||
## 💡 Other ways to run OpenHands
|
||||
|
||||
> [!WARNING]
|
||||
> [!CAUTION]
|
||||
> 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, check out the source-available, commercially-licensed
|
||||
> [OpenHands Cloud Helm Chart](https://github.com/all-Hands-AI/OpenHands-cloud)
|
||||
> 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.
|
||||
|
||||
You can [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
|
||||
You can also [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).
|
||||
|
||||
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
```
|
||||
|
||||
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
|
||||
|
||||
@@ -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.46-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
```
|
||||
|
||||
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
|
||||
|
||||
@@ -201,27 +201,6 @@ 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 = ""
|
||||
@@ -339,9 +318,6 @@ 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.46-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.45-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/|openhands-ui/)
|
||||
exclude: docs/modules/python
|
||||
- id: end-of-file-fixer
|
||||
exclude: ^(docs/|modules/|python/|openhands-ui/)
|
||||
exclude: docs/modules/python
|
||||
- id: check-yaml
|
||||
args: ["--allow-multiple-documents"]
|
||||
- id: debug-statements
|
||||
|
||||
@@ -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.46-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.45-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:
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
---
|
||||
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.
|
||||
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.
|
||||
---
|
||||
|
||||
<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.
|
||||
|
||||
@@ -14,34 +14,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.
|
||||
|
||||
The conversation history will be saved in `~/.openhands/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
|
||||
```
|
||||
|
||||
### Running with Docker
|
||||
|
||||
@@ -55,7 +55,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -64,21 +64,16 @@ 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.46 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45 \
|
||||
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>
|
||||
> **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.
|
||||
|
||||
This launches the CLI in Docker, allowing you to interact with OpenHands.
|
||||
This launches the CLI in Docker, allowing you to interact with OpenHands as described above.
|
||||
|
||||
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,7 +1,6 @@
|
||||
---
|
||||
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.46-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45 \
|
||||
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,15 +73,6 @@ 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.46-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
```
|
||||
|
||||
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.46
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
Starting OpenHands...
|
||||
Running OpenHands as root
|
||||
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
|
||||
|
||||
@@ -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.46-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.45
|
||||
```
|
||||
|
||||
> **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,7 +1,6 @@
|
||||
---
|
||||
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
|
||||
|
||||
@@ -5,26 +5,111 @@ description: Keyword-triggered microagents provide OpenHands with specific instr
|
||||
|
||||
## Usage
|
||||
|
||||
These microagents are only loaded when a prompt includes one of the trigger words.
|
||||
Keyword-triggered microagents are only loaded when a prompt includes one of the trigger words. There are two types of keyword-triggered microagents:
|
||||
|
||||
1. **Standard Keyword Microagents**: Triggered by keywords embedded in text
|
||||
2. **Command-Style Microagents**: Triggered by command-style inputs (e.g., `/fix_test`) that can prompt for user input
|
||||
|
||||
Additionally, there's a special type of microagent that's always active:
|
||||
|
||||
3. **Repository Microagents**: Always active for a specific repository, providing repository-specific context and tools
|
||||
|
||||
## Frontmatter Syntax
|
||||
|
||||
Frontmatter is required for keyword-triggered microagents. It must be placed at the top of the file,
|
||||
above the guidelines.
|
||||
above the guidelines. Enclose the frontmatter in triple dashes (---).
|
||||
|
||||
Enclose the frontmatter in triple dashes (---) and include the following fields:
|
||||
### Standard Keyword Microagents
|
||||
|
||||
For standard keyword microagents, include the following fields:
|
||||
|
||||
| Field | Description | Required | Default |
|
||||
|------------|--------------------------------------------------|----------|------------------|
|
||||
| `name` | The name of the microagent | No | Filename |
|
||||
| `type` | The type of microagent (`knowledge`) | No | Inferred |
|
||||
| `triggers` | A list of keywords that activate the microagent. | Yes | None |
|
||||
| `agent` | The agent this microagent applies to. | No | 'CodeActAgent' |
|
||||
|
||||
### Command-Style Microagents
|
||||
|
||||
## Example
|
||||
For command-style microagents that require user input, include the following fields:
|
||||
|
||||
Keyword-triggered microagent file example located at `.openhands/microagents/yummy.md`:
|
||||
```
|
||||
| Field | Description | Required | Default |
|
||||
|------------|------------------------------------------------------------|----------|------------------|
|
||||
| `name` | The name of the microagent | No | Filename |
|
||||
| `type` | The type of microagent (`task`) | No | Inferred |
|
||||
| `triggers` | A list of command triggers (e.g., `/fix_test`) | No | `/[name]` |
|
||||
| `inputs` | A list of input variables the microagent requires | Yes | None |
|
||||
|
||||
### Repository Microagents
|
||||
|
||||
Repository microagents are always active for a specific repository. They provide repository-specific context and tools.
|
||||
|
||||
| Field | Description | Required | Default |
|
||||
|------------|------------------------------------------------------------|----------|------------------|
|
||||
| `name` | The name of the microagent | No | Filename |
|
||||
| `type` | The type of microagent (`repo`) | No | Inferred |
|
||||
|
||||
#### Repository Microagent Example
|
||||
|
||||
Here's an example of a repository microagent:
|
||||
|
||||
```yaml
|
||||
---
|
||||
# The type field is optional and will be inferred as 'repo' when no triggers are present
|
||||
---
|
||||
|
||||
# Repository Guidelines
|
||||
|
||||
This repository follows these coding standards:
|
||||
1. Use PEP 8 for Python code
|
||||
2. Use ESLint for JavaScript code
|
||||
3. Write unit tests for all new features
|
||||
```
|
||||
|
||||
This microagent is always active when working with the repository and provides repository-specific guidelines.
|
||||
|
||||
### MCP Tools Support
|
||||
|
||||
Microagents can also provide additional MCP (Model-Code-Prompt) tools to the agent. This is useful for extending the agent's capabilities with custom tools.
|
||||
|
||||
| Field | Description | Required | Default |
|
||||
|--------------|-----------------------------------------------------------|----------|------------------|
|
||||
| `mcp_tools` | Configuration for additional MCP tools | No | None |
|
||||
|
||||
#### MCP Tools Example
|
||||
|
||||
Here's an example of a microagent that provides an additional MCP tool (the `fetch` tool for accessing web content):
|
||||
|
||||
```yaml
|
||||
---
|
||||
# The type field is optional and will be inferred as 'repo' when no triggers are present
|
||||
mcp_tools:
|
||||
stdio_servers:
|
||||
- name: "fetch"
|
||||
command: uvx
|
||||
args:
|
||||
- mcp-server-fetch
|
||||
---
|
||||
```
|
||||
|
||||
This microagent is a repository microagent (always active) that adds the `fetch` tool to the agent's capabilities.
|
||||
|
||||
Each input in the `inputs` list requires:
|
||||
|
||||
| Field | Description | Required |
|
||||
|---------------|--------------------------------------------------|----------|
|
||||
| `name` | The name of the input variable | Yes |
|
||||
| `description` | A description of what the input should contain | Yes |
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
### Standard Keyword Microagent Example
|
||||
|
||||
Standard keyword microagent file example located at `.openhands/microagents/yummy.md`:
|
||||
```yaml
|
||||
---
|
||||
# The type field is optional and will be inferred as 'knowledge' when triggers are present
|
||||
triggers:
|
||||
- yummyhappy
|
||||
- happyyummy
|
||||
@@ -33,4 +118,58 @@ triggers:
|
||||
The user has said the magic word. Respond with "That was delicious!"
|
||||
```
|
||||
|
||||
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
|
||||
### Command-Style Microagent Example
|
||||
|
||||
Command-style microagent file example located at `.openhands/microagents/fix_test.md`:
|
||||
```yaml
|
||||
---
|
||||
# The type field is optional and will be inferred as 'task' when inputs are present
|
||||
triggers:
|
||||
- /fix_test
|
||||
inputs:
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch for the agent to work on"
|
||||
- name: TEST_COMMAND_TO_RUN
|
||||
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
|
||||
- name: FUNCTION_TO_FIX
|
||||
description: "The name of function to fix"
|
||||
- name: FILE_FOR_FUNCTION
|
||||
description: "The path of the file that contains the function"
|
||||
---
|
||||
|
||||
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
|
||||
|
||||
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
|
||||
|
||||
PLEASE DO NOT modify the tests by yourself -- Let me know if you think some of the tests are incorrect.
|
||||
```
|
||||
|
||||
## Using Command-Style Microagents
|
||||
|
||||
Command-style microagents are designed to streamline common development tasks by providing structured templates for specific operations. They are triggered using a command-style format and will prompt the user for any required inputs.
|
||||
|
||||
### How to Use
|
||||
|
||||
1. Type `/` in the chat input to see available command-style microagents
|
||||
2. Select a microagent from the dropdown or type its name (e.g., `/fix_test`)
|
||||
3. The agent will prompt you for any required inputs
|
||||
4. Provide the requested information
|
||||
5. The agent will execute the task with your inputs
|
||||
|
||||
### Template Variables
|
||||
|
||||
In the body of a command-style microagent, you can reference input variables using the `{{ VARIABLE_NAME }}` syntax. These will be replaced with the user-provided values when the microagent is triggered.
|
||||
|
||||
### Available Command-Style Microagents
|
||||
|
||||
OpenHands includes several built-in command-style microagents:
|
||||
|
||||
| Command | Description |
|
||||
|----------------------|-------------------------------------------------------|
|
||||
| `/fix_test` | Fix failing tests by modifying a specific function |
|
||||
| `/update_test` | Update tests for a new implementation |
|
||||
| `/update_pr` | Update a pull request description |
|
||||
| `/address_pr_comments` | Address comments on a pull request |
|
||||
| `/add_repo_instruction` | Add instructions to the repository microagent |
|
||||
|
||||
[See examples of microagents in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
|
||||
|
||||
@@ -8,7 +8,7 @@ description: Microagents are specialized prompts that enhance OpenHands with dom
|
||||
Currently OpenHands supports the following types of microagents:
|
||||
|
||||
- [General Microagents](./microagents-repo): General guidelines for OpenHands about the repository.
|
||||
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts.
|
||||
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts, including command-style microagents that prompt for user inputs.
|
||||
|
||||
To customize OpenHands' behavior, create a .openhands/microagents/ directory in the root of your repository and
|
||||
add `<microagent_name>.md` files inside. For repository-specific guidelines, you can ask OpenHands to analyze your repository and create a comprehensive `repo.md` file (see [General Microagents](./microagents-repo) for details).
|
||||
@@ -34,7 +34,7 @@ some-repository/
|
||||
Each microagent file may include frontmatter that provides additional information. In some cases, this frontmatter
|
||||
is required:
|
||||
|
||||
| Microagent Type | Required |
|
||||
|---------------------------------|----------|
|
||||
| `General Microagents` | No |
|
||||
| `Keyword-Triggered Microagents` | Yes |
|
||||
| Microagent Type | Required |
|
||||
|------------------------------------------------|----------|
|
||||
| `General Microagents` | No |
|
||||
| `Keyword-Triggered Microagents (all types)` | Yes |
|
||||
|
||||
@@ -3,6 +3,7 @@ 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,6 +3,8 @@ 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,8 +3,7 @@ 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
|
||||
|
||||
@@ -19,13 +18,9 @@ SDK to spawn and control these sandboxes.
|
||||
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)
|
||||
@@ -39,6 +34,5 @@ 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,8 +1,6 @@
|
||||
---
|
||||
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>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
---
|
||||
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,7 +1,6 @@
|
||||
---
|
||||
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,12 +22,13 @@ 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.
|
||||
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.
|
||||
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
|
||||
|
||||
<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
|
||||
@@ -44,23 +45,22 @@ search_api_key = "tvly-your-api-key-here"
|
||||
|
||||
When the search engine is configured:
|
||||
|
||||
- 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.
|
||||
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
|
||||
|
||||
### 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
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.46.0",
|
||||
"version": "0.45.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.46.0",
|
||||
"version": "0.45.0",
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.0-beta.9",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.46.0",
|
||||
"version": "0.45.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
||||
@@ -35,7 +35,6 @@ interface EventMessageProps {
|
||||
hasObservationPair: boolean;
|
||||
isAwaitingUserConfirmation: boolean;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
}
|
||||
|
||||
export function EventMessage({
|
||||
@@ -43,52 +42,24 @@ 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(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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
} = useFeedbackExists(isFinishAction(event) ? event.id : undefined);
|
||||
|
||||
if (isErrorObservation(event)) {
|
||||
return (
|
||||
<>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
{renderLikertScale()}
|
||||
</>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,11 +70,24 @@ 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} />
|
||||
{renderLikertScale()}
|
||||
{showLikertScale && (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -112,20 +96,15 @@ 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>
|
||||
{isAssistantMessage(event) &&
|
||||
event.action === "message" &&
|
||||
renderLikertScale()}
|
||||
</>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
hasObservationPair={actionHasObservationPair(message)}
|
||||
isAwaitingUserConfirmation={isAwaitingUserConfirmation}
|
||||
isLastMessage={messages.length - 1 === index}
|
||||
isInLast10Actions={messages.length - 1 - index < 10}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -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 기반 개발을 사용하여 소프트웨어를 쉽게 구축하고 유지할 수 있게 합니다.",
|
||||
|
||||
@@ -111,7 +111,6 @@ const openHandsHandlers = [
|
||||
"gpt-4o-mini",
|
||||
"anthropic/claude-3.5",
|
||||
"anthropic/claude-sonnet-4-20250514",
|
||||
"sambanova/Meta-Llama-3.1-8B-Instruct",
|
||||
]),
|
||||
),
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ 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();
|
||||
@@ -94,15 +93,13 @@ function LlmSettingsScreen() {
|
||||
};
|
||||
|
||||
const basicFormAction = (formData: FormData) => {
|
||||
const providerDisplay = formData.get("llm-provider-input")?.toString();
|
||||
const provider = providerDisplay
|
||||
? getProviderId(providerDisplay)
|
||||
: undefined;
|
||||
const provider = formData.get("llm-provider-input")?.toString();
|
||||
const model = formData.get("llm-model-input")?.toString();
|
||||
const apiKey = formData.get("llm-api-key-input")?.toString();
|
||||
const searchApiKey = formData.get("search-api-key-input")?.toString();
|
||||
|
||||
const fullLlmModel = provider && model && `${provider}/${model}`;
|
||||
const fullLlmModel =
|
||||
provider && model && `${provider}/${model}`.toLowerCase();
|
||||
|
||||
saveSettings(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseMaxBudgetPerTask, extractSettings } from "../settings-utils";
|
||||
import { parseMaxBudgetPerTask } from "../settings-utils";
|
||||
|
||||
describe("parseMaxBudgetPerTask", () => {
|
||||
it("should return null for empty string", () => {
|
||||
@@ -47,45 +47,3 @@ 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,10 +29,3 @@ 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,12 +1,10 @@
|
||||
import { Settings } from "#/types/settings";
|
||||
import { getProviderId } from "#/utils/map-provider";
|
||||
|
||||
const extractBasicFormData = (formData: FormData) => {
|
||||
const providerDisplay = formData.get("llm-provider-input")?.toString();
|
||||
const provider = providerDisplay ? getProviderId(providerDisplay) : undefined;
|
||||
const provider = formData.get("llm-provider-input")?.toString();
|
||||
const model = formData.get("llm-model-input")?.toString();
|
||||
|
||||
const LLM_MODEL = `${provider}/${model}`;
|
||||
const LLM_MODEL = `${provider}/${model}`.toLowerCase();
|
||||
const LLM_API_KEY = formData.get("llm-api-key-input")?.toString();
|
||||
const AGENT = formData.get("agent")?.toString();
|
||||
const LANGUAGE = formData.get("language")?.toString();
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
---
|
||||
name: add_agent
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- new agent
|
||||
- new microagent
|
||||
- create agent
|
||||
- create an agent
|
||||
- create microagent
|
||||
- create a microagent
|
||||
- add agent
|
||||
- add an agent
|
||||
- add microagent
|
||||
- add a microagent
|
||||
- microagent template
|
||||
- new agent
|
||||
- new microagent
|
||||
- create agent
|
||||
- create an agent
|
||||
- create microagent
|
||||
- create a microagent
|
||||
- add agent
|
||||
- add an agent
|
||||
- add microagent
|
||||
- add a microagent
|
||||
- microagent template
|
||||
---
|
||||
|
||||
This agent helps create new microagents in the `.openhands/microagents` directory by providing guidance and templates.
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
---
|
||||
name: add_repo_inst
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- description: Branch for the agent to work on
|
||||
name: REPO_FOLDER_NAME
|
||||
triggers:
|
||||
- /add_repo_inst
|
||||
inputs:
|
||||
- name: REPO_FOLDER_NAME
|
||||
description: "Branch for the agent to work on"
|
||||
---
|
||||
|
||||
Please browse the current repository under /workspace/{{ REPO_FOLDER_NAME }}, look at the documentation and relevant code, and understand the purpose of this repository.
|
||||
@@ -18,7 +14,6 @@ Here's an example:
|
||||
```markdown
|
||||
---
|
||||
name: repo
|
||||
type: repo
|
||||
agent: CodeActAgent
|
||||
---
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
---
|
||||
name: address_pr_comments
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- description: URL of the pull request
|
||||
name: PR_URL
|
||||
- description: Branch name corresponds to the pull request
|
||||
name: BRANCH_NAME
|
||||
triggers:
|
||||
- /address_pr_comments
|
||||
inputs:
|
||||
- name: PR_URL
|
||||
description: "URL of the pull request"
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch name corresponds to the pull request"
|
||||
---
|
||||
|
||||
First, check the branch {{ BRANCH_NAME }} and read the diff against the main branch to understand the purpose.
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
name: agent_memory
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- /remember
|
||||
---
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
---
|
||||
# This is a repo microagent that is always activated
|
||||
# to include necessary default tools implemented with MCP
|
||||
name: default-tools
|
||||
type: repo
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
mcp_tools:
|
||||
stdio_servers:
|
||||
- name: "fetch"
|
||||
command: "uvx"
|
||||
args: ["mcp-server-fetch"]
|
||||
# We leave the body empty because MCP tools will automatically add the
|
||||
# tool description for LLMs in tool calls, so there's no need to add extra descriptions.
|
||||
- args:
|
||||
- mcp-server-fetch
|
||||
command: uvx
|
||||
name: fetch
|
||||
---
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
name: docker
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- docker
|
||||
- container
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
---
|
||||
name: fix_test
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- description: Branch for the agent to work on
|
||||
name: BRANCH_NAME
|
||||
- description: The test command you want the agent to work on. For example, `pytest
|
||||
tests/unit/test_bash_parsing.py`
|
||||
name: TEST_COMMAND_TO_RUN
|
||||
- description: The name of function to fix
|
||||
name: FUNCTION_TO_FIX
|
||||
- description: The path of the file that contains the function
|
||||
name: FILE_FOR_FUNCTION
|
||||
triggers:
|
||||
- /fix_test
|
||||
inputs:
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch for the agent to work on"
|
||||
- name: TEST_COMMAND_TO_RUN
|
||||
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
|
||||
- name: FUNCTION_TO_FIX
|
||||
description: "The name of function to fix"
|
||||
- name: FILE_FOR_FUNCTION
|
||||
description: "The path of the file that contains the function"
|
||||
---
|
||||
|
||||
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
name: flarglebargle
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- flarglebargle
|
||||
---
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
name: github
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- github
|
||||
- git
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
name: gitlab
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- gitlab
|
||||
- git
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
name: kubernetes
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- kubernetes
|
||||
- k8s
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
name: npm
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- npm
|
||||
---
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
---
|
||||
name: pdflatex
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- pdflatex
|
||||
---
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
---
|
||||
name: security
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- security
|
||||
- vulnerability
|
||||
- authentication
|
||||
- authorization
|
||||
- permissions
|
||||
- security
|
||||
- vulnerability
|
||||
- authentication
|
||||
- authorization
|
||||
- permissions
|
||||
---
|
||||
|
||||
This document provides guidance on security best practices
|
||||
|
||||
You should always be considering security implications when developing.
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
---
|
||||
name: SSH Microagent
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- ssh
|
||||
- remote server
|
||||
- remote machine
|
||||
- remote host
|
||||
- remote connection
|
||||
- secure shell
|
||||
- ssh keys
|
||||
- ssh
|
||||
- remote server
|
||||
- remote machine
|
||||
- remote host
|
||||
- remote connection
|
||||
- secure shell
|
||||
- ssh keys
|
||||
---
|
||||
|
||||
# SSH Microagent
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
---
|
||||
name: swift-linux
|
||||
type: knowledge
|
||||
agent: CodeActAgent
|
||||
version: 1.0.0
|
||||
triggers:
|
||||
- swift-linux
|
||||
- swift-debian
|
||||
- swift-installation
|
||||
triggers:
|
||||
- swift-linux
|
||||
- swift-debian
|
||||
- swift-installation
|
||||
---
|
||||
|
||||
# Swift Installation Guide for Debian Linux
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
---
|
||||
name: update_pr_description
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- description: URL of the pull request
|
||||
name: PR_URL
|
||||
type: string
|
||||
validation:
|
||||
pattern: ^https://github.com/.+/.+/pull/[0-9]+$
|
||||
- description: Branch name corresponds to the pull request
|
||||
name: BRANCH_NAME
|
||||
type: string
|
||||
triggers:
|
||||
- /update_pr_description
|
||||
inputs:
|
||||
- name: PR_URL
|
||||
description: "URL of the pull request"
|
||||
type: string
|
||||
validation:
|
||||
pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch name corresponds to the pull request"
|
||||
type: string
|
||||
---
|
||||
|
||||
Please check the branch "{{ BRANCH_NAME }}" and look at the diff against the main branch. This branch belongs to this PR "{{ PR_URL }}".
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
---
|
||||
name: update_test
|
||||
version: 1.0.0
|
||||
author: openhands
|
||||
agent: CodeActAgent
|
||||
inputs:
|
||||
- description: Branch for the agent to work on
|
||||
name: BRANCH_NAME
|
||||
- description: The test command you want the agent to work on. For example, `pytest
|
||||
tests/unit/test_bash_parsing.py`
|
||||
name: TEST_COMMAND_TO_RUN
|
||||
triggers:
|
||||
- /update_test
|
||||
inputs:
|
||||
- name: BRANCH_NAME
|
||||
description: "Branch for the agent to work on"
|
||||
- name: TEST_COMMAND_TO_RUN
|
||||
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
|
||||
---
|
||||
|
||||
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
|
||||
|
||||
34
openhands-ui/.gitignore
vendored
34
openhands-ui/.gitignore
vendored
@@ -1,34 +0,0 @@
|
||||
# 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
|
||||
@@ -1,15 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"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 +0,0 @@
|
||||
console.log("Hello via Bun!");
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from openhands.core.config.condenser_config import CondenserConfig, NoOpCondenserConfig
|
||||
from openhands.core.config.extended_config import ExtendedConfig
|
||||
@@ -47,7 +47,7 @@ class AgentConfig(BaseModel):
|
||||
extended: ExtendedConfig = Field(default_factory=lambda: ExtendedConfig({}))
|
||||
"""Extended configuration for the agent."""
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
@classmethod
|
||||
def from_toml_section(cls, data: dict) -> dict[str, AgentConfig]:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Literal, cast
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
from pydantic import BaseModel, Field, ValidationError
|
||||
|
||||
from openhands.core import logger
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
@@ -13,7 +13,7 @@ class NoOpCondenserConfig(BaseModel):
|
||||
|
||||
type: Literal['noop'] = Field('noop')
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
class ObservationMaskingCondenserConfig(BaseModel):
|
||||
@@ -26,7 +26,7 @@ class ObservationMaskingCondenserConfig(BaseModel):
|
||||
ge=1,
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
class BrowserOutputCondenserConfig(BaseModel):
|
||||
@@ -55,7 +55,7 @@ class RecentEventsCondenserConfig(BaseModel):
|
||||
default=100, description='Maximum number of events to keep.', ge=1
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
class LLMSummarizingCondenserConfig(BaseModel):
|
||||
@@ -82,7 +82,7 @@ class LLMSummarizingCondenserConfig(BaseModel):
|
||||
description='Maximum length of the event representations to be passed to the LLM.',
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
class AmortizedForgettingCondenserConfig(BaseModel):
|
||||
@@ -102,7 +102,7 @@ class AmortizedForgettingCondenserConfig(BaseModel):
|
||||
ge=0,
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
class LLMAttentionCondenserConfig(BaseModel):
|
||||
@@ -125,7 +125,7 @@ class LLMAttentionCondenserConfig(BaseModel):
|
||||
ge=0,
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
class StructuredSummaryCondenserConfig(BaseModel):
|
||||
@@ -152,7 +152,7 @@ class StructuredSummaryCondenserConfig(BaseModel):
|
||||
description='Maximum length of the event representations to be passed to the LLM.',
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
class CondenserPipelineConfig(BaseModel):
|
||||
@@ -167,7 +167,7 @@ class CondenserPipelineConfig(BaseModel):
|
||||
description='List of condenser configurations to be used in the pipeline.',
|
||||
)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
# Type alias for convenience
|
||||
|
||||
@@ -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.__class__.model_fields.items():
|
||||
for name, field in model.model_fields.items():
|
||||
field_value = getattr(model, name)
|
||||
|
||||
if isinstance(field_value, BaseModel):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
from pydantic import BaseModel, 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 = ConfigDict(extra='forbid')
|
||||
model_config = {'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, ConfigDict, Field, SecretStr, ValidationError
|
||||
from pydantic import BaseModel, Field, SecretStr, ValidationError
|
||||
|
||||
from openhands.core.logger import LOG_DIR
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
@@ -45,7 +45,6 @@ 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')
|
||||
@@ -87,12 +86,8 @@ 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 = ConfigDict(extra='forbid')
|
||||
model_config = {'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, ConfigDict, Field, ValidationError, model_validator
|
||||
from pydantic import BaseModel, 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 = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_servers(servers_data: list[dict | str]) -> list[dict]:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr
|
||||
from pydantic import BaseModel, Field, SecretStr
|
||||
|
||||
from openhands.core import logger
|
||||
from openhands.core.config.agent_config import AgentConfig
|
||||
@@ -114,7 +114,7 @@ class OpenHandsConfig(BaseModel):
|
||||
|
||||
defaults_dict: ClassVar[dict] = {}
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'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, ConfigDict, Field, ValidationError, model_validator
|
||||
from pydantic import BaseModel, Field, ValidationError, model_validator
|
||||
|
||||
|
||||
class SandboxConfig(BaseModel):
|
||||
@@ -88,8 +88,7 @@ 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'",
|
||||
)
|
||||
|
||||
cuda_visible_devices: str | None = Field(default=None)
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
@classmethod
|
||||
def from_toml_section(cls, data: dict) -> dict[str, 'SandboxConfig']:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
from pydantic import BaseModel, 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 = ConfigDict(extra='forbid')
|
||||
model_config = {'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.__class__.model_fields.items():
|
||||
for field_name, field_info in sub_config.model_fields.items():
|
||||
field_value = getattr(sub_config, field_name)
|
||||
field_type = field_info.annotation
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Annotated, Any, Coroutine, Literal, overload
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
SecretStr,
|
||||
WithJsonSchema,
|
||||
@@ -35,10 +34,10 @@ class ProviderToken(BaseModel):
|
||||
user_id: str | None = Field(default=None)
|
||||
host: str | None = Field(default=None)
|
||||
|
||||
model_config = ConfigDict(
|
||||
frozen=True, # Makes the entire model immutable
|
||||
validate_assignment=True,
|
||||
)
|
||||
model_config = {
|
||||
'frozen': True, # Makes the entire model immutable
|
||||
'validate_assignment': True,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_value(cls, token_value: ProviderToken | dict[str, str]) -> ProviderToken:
|
||||
@@ -63,10 +62,10 @@ class CustomSecret(BaseModel):
|
||||
secret: SecretStr = Field(default_factory=lambda: SecretStr(''))
|
||||
description: str = Field(default='')
|
||||
|
||||
model_config = ConfigDict(
|
||||
frozen=True, # Makes the entire model immutable
|
||||
validate_assignment=True,
|
||||
)
|
||||
model_config = {
|
||||
'frozen': True, # Makes the entire model immutable
|
||||
'validate_assignment': True,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_value(cls, secret_value: CustomSecret | dict[str, str]) -> CustomSecret:
|
||||
|
||||
@@ -182,12 +182,6 @@ class LLM(RetryMixin, DebugMixin):
|
||||
kwargs['max_tokens'] = self.config.max_output_tokens
|
||||
kwargs.pop('max_completion_tokens')
|
||||
|
||||
# Add safety settings for models that support them
|
||||
if 'mistral' in self.config.model.lower() and self.config.safety_settings:
|
||||
kwargs['safety_settings'] = self.config.safety_settings
|
||||
elif 'gemini' in self.config.model.lower() and self.config.safety_settings:
|
||||
kwargs['safety_settings'] = self.config.safety_settings
|
||||
|
||||
self._completion = partial(
|
||||
litellm_completion,
|
||||
model=self.config.model,
|
||||
|
||||
@@ -4,7 +4,7 @@ from fastmcp import Client
|
||||
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
|
||||
from mcp import McpError
|
||||
from mcp.types import CallToolResult
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from openhands.core.config.mcp_config import MCPSHTTPServerConfig, MCPSSEServerConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
@@ -16,13 +16,14 @@ class MCPClient(BaseModel):
|
||||
A collection of tools that connects to an MCP server and manages available tools through the Model Context Protocol.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
client: Optional[Client] = None
|
||||
description: str = 'MCP client tools for server interaction'
|
||||
tools: list[MCPClientTool] = Field(default_factory=list)
|
||||
tool_map: dict[str, MCPClientTool] = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
async def _initialize_and_list_tools(self) -> None:
|
||||
"""Initialize session and populate tool map."""
|
||||
if not self.client:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from mcp.types import Tool
|
||||
from pydantic import ConfigDict
|
||||
|
||||
|
||||
class MCPClientTool(Tool):
|
||||
@@ -10,7 +9,8 @@ class MCPClientTool(Tool):
|
||||
by the MCPClient for each operation.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def to_param(self) -> dict:
|
||||
"""Convert tool to function call format."""
|
||||
|
||||
@@ -40,6 +40,11 @@ class BaseMicroagent(BaseModel):
|
||||
derived_name = None
|
||||
if microagent_dir is not None:
|
||||
derived_name = str(path.relative_to(microagent_dir).with_suffix(''))
|
||||
else:
|
||||
derived_name = path.with_suffix('').name
|
||||
logger.warning(
|
||||
f'No microagent_dir provided. Microagent name will be the file name: {derived_name}'
|
||||
)
|
||||
|
||||
# Only load directly from path if file_content is not provided
|
||||
if file_content is None:
|
||||
@@ -95,6 +100,16 @@ class BaseMicroagent(BaseModel):
|
||||
MicroagentType.TASK: TaskMicroagent,
|
||||
}
|
||||
|
||||
# We will always use derived_name if available
|
||||
assert derived_name is not None
|
||||
agent_name = derived_name
|
||||
if metadata.name is not None:
|
||||
logger.warning(
|
||||
f'Detected `name:` field in frontmatter for microagent {metadata.name}. '
|
||||
"This is deprecated. Microagent's name will use the file name "
|
||||
f'({derived_name}) instead.'
|
||||
)
|
||||
|
||||
# Infer the agent type:
|
||||
# 1. If inputs exist -> TASK
|
||||
# 2. If triggers exist -> KNOWLEDGE
|
||||
@@ -102,8 +117,7 @@ class BaseMicroagent(BaseModel):
|
||||
inferred_type: MicroagentType
|
||||
if metadata.inputs:
|
||||
inferred_type = MicroagentType.TASK
|
||||
# Add a trigger for the agent name if not already present
|
||||
trigger = f'/{metadata.name}'
|
||||
trigger = f'/{agent_name}'
|
||||
if not metadata.triggers or trigger not in metadata.triggers:
|
||||
if not metadata.triggers:
|
||||
metadata.triggers = [trigger]
|
||||
@@ -120,9 +134,6 @@ class BaseMicroagent(BaseModel):
|
||||
# This should theoretically not happen with the logic above
|
||||
raise ValueError(f'Could not determine microagent type for: {path}')
|
||||
|
||||
# Use derived_name if available (from relative path), otherwise fallback to metadata.name
|
||||
agent_name = derived_name if derived_name is not None else metadata.name
|
||||
|
||||
agent_class = subclass_map[inferred_type]
|
||||
return agent_class(
|
||||
name=agent_name,
|
||||
|
||||
@@ -25,10 +25,12 @@ class InputMetadata(BaseModel):
|
||||
class MicroagentMetadata(BaseModel):
|
||||
"""Metadata for all microagents."""
|
||||
|
||||
name: str = 'default'
|
||||
name: str = Field(default='default', exclude=True)
|
||||
type: MicroagentType = Field(default=MicroagentType.REPO_KNOWLEDGE)
|
||||
version: str = Field(default='1.0.0')
|
||||
agent: str = Field(default='CodeActAgent')
|
||||
# Keep these fields for backward compatibility but they're not used
|
||||
version: str = Field(default='1.0.0', exclude=True)
|
||||
agent: str = Field(default='CodeActAgent', exclude=True)
|
||||
author: str = Field(default='', exclude=True)
|
||||
triggers: list[str] = [] # optional, only exists for knowledge microagents
|
||||
inputs: list[InputMetadata] = [] # optional, only exists for task microagents
|
||||
mcp_tools: MCPConfig | None = (
|
||||
|
||||
@@ -720,8 +720,7 @@ fi
|
||||
)
|
||||
|
||||
# Clean up the org repo directory
|
||||
action = CmdRunAction(f'rm -rf {org_repo_dir}')
|
||||
self.run_action(action)
|
||||
shutil.rmtree(org_repo_dir)
|
||||
else:
|
||||
self.log(
|
||||
'info',
|
||||
|
||||
@@ -360,21 +360,7 @@ class DockerRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
command = self.get_action_execution_server_startup_command()
|
||||
if self.config.sandbox.enable_gpu:
|
||||
gpu_ids = self.config.sandbox.cuda_visible_devices
|
||||
if gpu_ids is None:
|
||||
device_requests = [
|
||||
docker.types.DeviceRequest(capabilities=[['gpu']], count=-1)
|
||||
]
|
||||
else:
|
||||
device_requests = [
|
||||
docker.types.DeviceRequest(
|
||||
capabilities=[['gpu']],
|
||||
device_ids=[str(i) for i in gpu_ids.split(',')],
|
||||
)
|
||||
]
|
||||
else:
|
||||
device_requests = None
|
||||
|
||||
try:
|
||||
if self.runtime_container_image is None:
|
||||
raise ValueError('Runtime container image is not set')
|
||||
@@ -390,7 +376,11 @@ class DockerRuntime(ActionExecutionClient):
|
||||
detach=True,
|
||||
environment=environment,
|
||||
volumes=volumes, # type: ignore
|
||||
device_requests=device_requests,
|
||||
device_requests=(
|
||||
[docker.types.DeviceRequest(capabilities=[['gpu']], count=-1)]
|
||||
if self.config.sandbox.enable_gpu
|
||||
else None
|
||||
),
|
||||
**(self.config.sandbox.docker_runtime_kwargs or {}),
|
||||
)
|
||||
self.log('debug', f'Container started. Server url: {self.api_url}')
|
||||
|
||||
@@ -40,7 +40,7 @@ Two configuration options are required to use the Kubernetes runtime:
|
||||
2. **Runtime Container Image**: Specify the container image to use for the runtime environment
|
||||
```toml
|
||||
[sandbox]
|
||||
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik"
|
||||
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik"
|
||||
```
|
||||
|
||||
#### Additional Kubernetes Options
|
||||
|
||||
@@ -39,40 +39,6 @@ Within the `<updated_code>` tag, include only the final code after updation. Do
|
||||
<update_snippet>{draft_changes}</update_snippet>
|
||||
"""
|
||||
|
||||
CORRECT_SYS_MSG = """You are a code repair assistant. Now you have an original file content and error information from a static code checking tool (lint tool). Your task is to automatically modify and return the repaired complete code based on these error messages and refer to the current file content.
|
||||
|
||||
The following are the specific task steps you need to complete:
|
||||
|
||||
Carefully read the current file content to ensure that you fully understand its code structure.
|
||||
|
||||
According to the lint error prompt, accurately locate and analyze the cause of the problem.
|
||||
|
||||
Modify the original file content and fix all errors prompted by the lint tool.
|
||||
|
||||
Return complete, runnable, and error-fixed code, paying attention to maintaining the overall style and specifications of the original code.
|
||||
|
||||
Please note:
|
||||
|
||||
Please strictly follow the lint error prompts to make modifications and do not miss any problems.
|
||||
|
||||
The modified code must be complete and cannot introduce new errors or bugs.
|
||||
|
||||
The modified code must maintain the original code function and logic, and no changes unrelated to error repair should be made."""
|
||||
|
||||
CORRECT_USER_MSG = """
|
||||
THE FOLLOWING ARE THE ORIGINAL FILE CONTENTS AND THE ERROR INFORMATION REPORTED BY THE LINT TOOL
|
||||
|
||||
# CURRENT FILE CONTENT:
|
||||
```
|
||||
{file_content}
|
||||
```
|
||||
|
||||
# ERROR MESSAGE FROM STATIC CODE CHECKING TOOL:
|
||||
```
|
||||
{lint_error}
|
||||
```
|
||||
""".strip()
|
||||
|
||||
|
||||
def _extract_code(string: str) -> str | None:
|
||||
pattern = r'<updated_code>(.*?)</updated_code>'
|
||||
@@ -230,7 +196,7 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
|
||||
return ErrorObservation(error_message)
|
||||
return None
|
||||
|
||||
def llm_based_edit(self, action: FileEditAction, retry_num: int = 0) -> Observation:
|
||||
def llm_based_edit(self, action: FileEditAction) -> Observation:
|
||||
obs = self.read(FileReadAction(path=action.path))
|
||||
if (
|
||||
isinstance(obs, ErrorObservation)
|
||||
@@ -287,14 +253,7 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
|
||||
diff,
|
||||
)
|
||||
if error_obs is not None:
|
||||
self.write(
|
||||
FileWriteAction(path=action.path, content=updated_content)
|
||||
)
|
||||
return self.correct_edit(
|
||||
file_content=updated_content,
|
||||
error_obs=error_obs,
|
||||
retry_num=retry_num,
|
||||
)
|
||||
return error_obs
|
||||
|
||||
obs = self.write(FileWriteAction(path=action.path, content=updated_content))
|
||||
return FileEditObservation(
|
||||
@@ -321,8 +280,7 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
|
||||
error_msg = (
|
||||
f'[Edit error: The range of lines to edit is too long.]\n'
|
||||
f'[The maximum number of lines allowed to edit at once is {self.MAX_LINES_TO_EDIT}. '
|
||||
f'Got (L{start_idx + 1}-L{end_idx}) {length_of_range} lines.]\n'
|
||||
# [start_idx, end_idx), so no need to + 1
|
||||
f'Got (L{start_idx + 1}-L{end_idx}) {length_of_range} lines.]\n' # [start_idx, end_idx), so no need to + 1
|
||||
)
|
||||
# search for relevant ranges to hint the agent
|
||||
topk_chunks: list[Chunk] = get_top_k_chunk_matches(
|
||||
@@ -375,12 +333,7 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
|
||||
)
|
||||
if error_obs is not None:
|
||||
error_obs.llm_metrics = self.draft_editor_llm.metrics
|
||||
self.write(FileWriteAction(path=action.path, content=updated_content))
|
||||
return self.correct_edit(
|
||||
file_content=updated_content,
|
||||
error_obs=error_obs,
|
||||
retry_num=retry_num,
|
||||
)
|
||||
return error_obs
|
||||
|
||||
obs = self.write(FileWriteAction(path=action.path, content=updated_content))
|
||||
ret_obs = FileEditObservation(
|
||||
@@ -392,40 +345,3 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
|
||||
)
|
||||
ret_obs.llm_metrics = self.draft_editor_llm.metrics
|
||||
return ret_obs
|
||||
|
||||
def check_retry_num(self, retry_num):
|
||||
correct_num = self.draft_editor_llm.config.correct_num
|
||||
return correct_num < retry_num
|
||||
|
||||
def correct_edit(
|
||||
self, file_content: str, error_obs: ErrorObservation, retry_num: int = 0
|
||||
) -> Observation:
|
||||
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
|
||||
from openhands.agenthub.codeact_agent.tools import LLMBasedFileEditTool
|
||||
from openhands.llm.llm_utils import check_tools
|
||||
|
||||
_retry_num = retry_num + 1
|
||||
if self.check_retry_num(_retry_num):
|
||||
return error_obs
|
||||
tools = check_tools([LLMBasedFileEditTool], self.draft_editor_llm.config)
|
||||
messages = [
|
||||
{'role': 'system', 'content': CORRECT_SYS_MSG},
|
||||
{
|
||||
'role': 'user',
|
||||
'content': CORRECT_USER_MSG.format(
|
||||
file_content=file_content, lint_error=error_obs.content
|
||||
),
|
||||
},
|
||||
]
|
||||
params: dict = {'messages': messages, 'tools': tools}
|
||||
try:
|
||||
response = self.draft_editor_llm.completion(**params)
|
||||
actions = codeact_function_calling.response_to_actions(response)
|
||||
if len(actions) != 1:
|
||||
return error_obs
|
||||
for action in actions:
|
||||
if isinstance(action, FileEditAction):
|
||||
return self.llm_based_edit(action, _retry_num)
|
||||
except Exception as e:
|
||||
logger.error(f'correct lint error is failed: {e}')
|
||||
return error_obs
|
||||
|
||||
@@ -7,7 +7,7 @@ from datetime import datetime, timezone
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
@@ -88,7 +88,7 @@ class InitSessionRequest(BaseModel):
|
||||
if os.getenv('ALLOW_SET_CONVERSATION_ID', '0') == '1':
|
||||
conversation_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
model_config = {'extra': 'forbid'}
|
||||
|
||||
|
||||
class ConversationResponse(BaseModel):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pydantic import ConfigDict, Field
|
||||
from pydantic import Field
|
||||
|
||||
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE, PROVIDER_TOKEN_TYPE
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
@@ -18,6 +18,6 @@ class ConversationInitData(Settings):
|
||||
conversation_instructions: str | None = Field(default=None)
|
||||
git_provider: ProviderType | None = Field(default=None)
|
||||
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
model_config = {
|
||||
'arbitrary_types_allowed': True,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
SecretStr,
|
||||
)
|
||||
|
||||
@@ -40,7 +39,7 @@ class GETSettingsModel(Settings):
|
||||
llm_api_key_set: bool
|
||||
search_api_key_set: bool = False
|
||||
|
||||
model_config = ConfigDict(use_enum_values=True)
|
||||
model_config = {'use_enum_values': True}
|
||||
|
||||
|
||||
class CustomSecretWithoutValueModel(BaseModel):
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from openhands.core.config.utils import load_openhands_config
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.server.config.server_config import ServerConfig
|
||||
from openhands.storage.conversation.conversation_store import ConversationStore
|
||||
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
|
||||
from openhands.utils.conversation_summary import get_default_conversation_title
|
||||
from openhands.utils.import_utils import get_impl
|
||||
|
||||
|
||||
@@ -30,44 +23,7 @@ class ConversationValidator:
|
||||
cookies_str: str,
|
||||
authorization_header: str | None = None,
|
||||
) -> str | None:
|
||||
user_id = None
|
||||
metadata = await self._ensure_metadata_exists(conversation_id, user_id)
|
||||
return metadata.user_id
|
||||
|
||||
async def _ensure_metadata_exists(
|
||||
self,
|
||||
conversation_id: str,
|
||||
user_id: str | None,
|
||||
) -> ConversationMetadata:
|
||||
config = load_openhands_config()
|
||||
server_config = ServerConfig()
|
||||
|
||||
conversation_store_class: type[ConversationStore] = get_impl(
|
||||
ConversationStore,
|
||||
server_config.conversation_store_class,
|
||||
)
|
||||
conversation_store = await conversation_store_class.get_instance(
|
||||
config, user_id
|
||||
)
|
||||
|
||||
try:
|
||||
metadata = await conversation_store.get_metadata(conversation_id)
|
||||
except FileNotFoundError:
|
||||
logger.info(
|
||||
f'Creating new conversation metadata for {conversation_id}',
|
||||
extra={'session_id': conversation_id},
|
||||
)
|
||||
await conversation_store.save_metadata(
|
||||
ConversationMetadata(
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
title=get_default_conversation_title(conversation_id),
|
||||
last_updated_at=datetime.now(timezone.utc),
|
||||
selected_repository=None,
|
||||
)
|
||||
)
|
||||
metadata = await conversation_store.get_metadata(conversation_id)
|
||||
return metadata
|
||||
return None
|
||||
|
||||
|
||||
def create_conversation_validator() -> ConversationValidator:
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
SecretStr,
|
||||
SerializationInfo,
|
||||
@@ -46,9 +45,9 @@ class Settings(BaseModel):
|
||||
email: str | None = None
|
||||
email_verified: bool | None = None
|
||||
|
||||
model_config = ConfigDict(
|
||||
validate_assignment=True,
|
||||
)
|
||||
model_config = {
|
||||
'validate_assignment': True,
|
||||
}
|
||||
|
||||
@field_serializer('llm_api_key', 'search_api_key')
|
||||
def api_key_serializer(self, api_key: SecretStr | None, info: SerializationInfo):
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Any
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
SerializationInfo,
|
||||
field_serializer,
|
||||
@@ -32,11 +31,11 @@ class UserSecrets(BaseModel):
|
||||
default_factory=lambda: MappingProxyType({})
|
||||
)
|
||||
|
||||
model_config = ConfigDict(
|
||||
frozen=True,
|
||||
validate_assignment=True,
|
||||
arbitrary_types_allowed=True,
|
||||
)
|
||||
model_config = {
|
||||
'frozen': True,
|
||||
'validate_assignment': True,
|
||||
'arbitrary_types_allowed': True,
|
||||
}
|
||||
|
||||
@field_serializer('provider_tokens')
|
||||
def provider_tokens_serializer(
|
||||
|
||||
19
poetry.lock
generated
19
poetry.lock
generated
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aioboto3"
|
||||
@@ -462,7 +462,7 @@ description = "LTS Port of Python audioop"
|
||||
optional = false
|
||||
python-versions = ">=3.13"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.13\""
|
||||
markers = "python_version == \"3.13\""
|
||||
files = [
|
||||
{file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a"},
|
||||
{file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e"},
|
||||
@@ -1644,7 +1644,7 @@ files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", dev = "os_name == \"nt\" or sys_platform == \"win32\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
|
||||
markers = {main = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", dev = "os_name == \"nt\" or sys_platform == \"win32\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
|
||||
|
||||
[[package]]
|
||||
name = "comm"
|
||||
@@ -3053,8 +3053,8 @@ files = [
|
||||
google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]}
|
||||
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev"
|
||||
proto-plus = [
|
||||
{version = ">=1.22.3,<2.0.0dev"},
|
||||
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.22.3,<2.0.0dev"},
|
||||
]
|
||||
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
|
||||
|
||||
@@ -3076,8 +3076,8 @@ googleapis-common-protos = ">=1.56.2,<2.0.0"
|
||||
grpcio = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
|
||||
grpcio-status = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
|
||||
proto-plus = [
|
||||
{version = ">=1.22.3,<2.0.0"},
|
||||
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.22.3,<2.0.0"},
|
||||
]
|
||||
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
|
||||
requests = ">=2.18.0,<3.0.0"
|
||||
@@ -3295,8 +3295,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras
|
||||
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
|
||||
grpc-google-iam-v1 = ">=0.14.0,<1.0.0"
|
||||
proto-plus = [
|
||||
{version = ">=1.22.3,<2.0.0"},
|
||||
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.22.3,<2.0.0"},
|
||||
]
|
||||
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
|
||||
|
||||
@@ -6586,8 +6586,8 @@ files = [
|
||||
[package.dependencies]
|
||||
googleapis-common-protos = ">=1.52,<2.0"
|
||||
grpcio = [
|
||||
{version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""},
|
||||
{version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""},
|
||||
{version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""},
|
||||
]
|
||||
opentelemetry-api = ">=1.15,<2.0"
|
||||
opentelemetry-exporter-otlp-proto-common = "1.34.1"
|
||||
@@ -9350,7 +9350,6 @@ files = [
|
||||
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
|
||||
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
|
||||
]
|
||||
markers = {evaluation = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
|
||||
@@ -9593,7 +9592,7 @@ description = "Standard library aifc redistribution. \"dead battery\"."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.13\""
|
||||
markers = "python_version == \"3.13\""
|
||||
files = [
|
||||
{file = "standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66"},
|
||||
{file = "standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43"},
|
||||
@@ -9610,7 +9609,7 @@ description = "Standard library chunk redistribution. \"dead battery\"."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "python_version >= \"3.13\""
|
||||
markers = "python_version == \"3.13\""
|
||||
files = [
|
||||
{file = "standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c"},
|
||||
{file = "standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654"},
|
||||
|
||||
@@ -6,7 +6,7 @@ requires = [
|
||||
|
||||
[tool.poetry]
|
||||
name = "openhands-ai"
|
||||
version = "0.46.0"
|
||||
version = "0.45.0"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
authors = [ "OpenHands" ]
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Tests for microagent loading in runtime."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -20,7 +19,7 @@ from openhands.microagent.microagent import (
|
||||
RepoMicroagent,
|
||||
TaskMicroagent,
|
||||
)
|
||||
from openhands.microagent.types import MicroagentType
|
||||
from openhands.microagent.types import InputMetadata, MicroagentType
|
||||
|
||||
|
||||
def _create_test_microagents(test_dir: str):
|
||||
@@ -32,10 +31,6 @@ def _create_test_microagents(test_dir: str):
|
||||
knowledge_dir = microagents_dir / 'knowledge'
|
||||
knowledge_dir.mkdir(exist_ok=True)
|
||||
knowledge_agent = """---
|
||||
name: test_knowledge_agent
|
||||
type: knowledge
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
triggers:
|
||||
- test
|
||||
- pytest
|
||||
@@ -45,17 +40,10 @@ triggers:
|
||||
|
||||
Testing best practices and guidelines.
|
||||
"""
|
||||
(knowledge_dir / 'knowledge.md').write_text(knowledge_agent)
|
||||
(knowledge_dir / 'test_knowledge_agent.md').write_text(knowledge_agent)
|
||||
|
||||
# Create test repo agent
|
||||
repo_agent = """---
|
||||
name: test_repo_agent
|
||||
type: repo
|
||||
version: 1.0.0
|
||||
agent: CodeActAgent
|
||||
---
|
||||
|
||||
# Test Repository Agent
|
||||
repo_agent = """# Test Repository Agent
|
||||
|
||||
Repository-specific test instructions.
|
||||
"""
|
||||
@@ -89,7 +77,7 @@ def test_load_microagents_with_trailing_slashes(
|
||||
# Check knowledge agents
|
||||
assert len(knowledge_agents) == 1
|
||||
agent = knowledge_agents[0]
|
||||
assert agent.name == 'knowledge/knowledge'
|
||||
assert agent.name == 'knowledge/test_knowledge_agent'
|
||||
assert 'test' in agent.triggers
|
||||
assert 'pytest' in agent.triggers
|
||||
|
||||
@@ -126,7 +114,7 @@ def test_load_microagents_with_selected_repo(temp_dir, runtime_cls, run_as_openh
|
||||
# Check knowledge agents
|
||||
assert len(knowledge_agents) == 1
|
||||
agent = knowledge_agents[0]
|
||||
assert agent.name == 'knowledge/knowledge'
|
||||
assert agent.name == 'knowledge/test_knowledge_agent'
|
||||
assert 'test' in agent.triggers
|
||||
assert 'pytest' in agent.triggers
|
||||
|
||||
@@ -180,7 +168,7 @@ Repository-specific test instructions.
|
||||
_close_test_runtime(runtime)
|
||||
|
||||
|
||||
def test_task_microagent_creation():
|
||||
def test_task_microagent_creation(temp_dir):
|
||||
"""Test that a TaskMicroagent is created correctly."""
|
||||
content = """---
|
||||
name: test_task
|
||||
@@ -196,21 +184,43 @@ inputs:
|
||||
|
||||
This is a test task microagent with a variable: ${test_var}.
|
||||
"""
|
||||
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert agent.type == MicroagentType.TASK
|
||||
assert agent.name == 'test_task'
|
||||
assert '/test_task' in agent.triggers
|
||||
assert "If the user didn't provide any of these variables" in agent.content
|
||||
assert agent.inputs == [InputMetadata(name='TEST_VAR', description='Test variable')]
|
||||
simplified_content = """---
|
||||
triggers:
|
||||
- /test_task
|
||||
inputs:
|
||||
- name: TEST_VAR
|
||||
description: "Test variable"
|
||||
---
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert agent.type == MicroagentType.TASK
|
||||
assert agent.name == 'test_task'
|
||||
assert '/test_task' in agent.triggers
|
||||
assert "If the user didn't provide any of these variables" in agent.content
|
||||
This is a test task microagent with a variable: ${test_var}.
|
||||
"""
|
||||
|
||||
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
|
||||
f.write(simplified_content)
|
||||
|
||||
simplified_agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
|
||||
|
||||
assert isinstance(simplified_agent, TaskMicroagent)
|
||||
assert simplified_agent.type == MicroagentType.TASK
|
||||
assert simplified_agent.name == 'test_task'
|
||||
assert '/test_task' in simplified_agent.triggers
|
||||
assert (
|
||||
"If the user didn't provide any of these variables" in simplified_agent.content
|
||||
)
|
||||
|
||||
|
||||
def test_task_microagent_variable_extraction():
|
||||
def test_task_microagent_variable_extraction(temp_dir):
|
||||
"""Test that variables are correctly extracted from the content."""
|
||||
content = """---
|
||||
name: test_task
|
||||
@@ -227,19 +237,18 @@ inputs:
|
||||
This is a test with variables: ${var1}, ${var2}, and ${var3}.
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
variables = agent.extract_variables(agent.content)
|
||||
assert set(variables) == {'var1', 'var2', 'var3'}
|
||||
assert agent.requires_user_input()
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
variables = agent.extract_variables(agent.content)
|
||||
assert set(variables) == {'var1', 'var2', 'var3'}
|
||||
assert agent.requires_user_input()
|
||||
|
||||
|
||||
def test_knowledge_microagent_no_prompt():
|
||||
def test_knowledge_microagent_no_prompt(temp_dir):
|
||||
"""Test that a regular KnowledgeMicroagent doesn't get the prompt."""
|
||||
content = """---
|
||||
name: test_knowledge
|
||||
@@ -252,19 +261,17 @@ triggers:
|
||||
|
||||
This is a test knowledge microagent.
|
||||
"""
|
||||
with open(os.path.join(temp_dir, 'test_knowledge.md'), 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_knowledge.md'))
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
|
||||
assert isinstance(agent, KnowledgeMicroagent)
|
||||
assert agent.type == MicroagentType.KNOWLEDGE
|
||||
assert "If the user didn't provide any of these variables" not in agent.content
|
||||
assert isinstance(agent, KnowledgeMicroagent)
|
||||
assert agent.type == MicroagentType.KNOWLEDGE
|
||||
assert "If the user didn't provide any of these variables" not in agent.content
|
||||
|
||||
|
||||
def test_task_microagent_trigger_addition():
|
||||
def test_task_microagent_trigger_addition(temp_dir):
|
||||
"""Test that a trigger is added if not present."""
|
||||
content = """---
|
||||
name: test_task
|
||||
@@ -278,18 +285,16 @@ inputs:
|
||||
|
||||
This is a test task microagent.
|
||||
"""
|
||||
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert '/test_task' in agent.triggers
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert '/test_task' in agent.triggers
|
||||
|
||||
|
||||
def test_task_microagent_no_duplicate_trigger():
|
||||
def test_task_microagent_no_duplicate_trigger(temp_dir):
|
||||
"""Test that a trigger is not duplicated if already present."""
|
||||
content = """---
|
||||
name: test_task
|
||||
@@ -306,21 +311,19 @@ inputs:
|
||||
|
||||
This is a test task microagent.
|
||||
"""
|
||||
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert agent.triggers.count('/test_task') == 1 # No duplicates
|
||||
assert len(agent.triggers) == 2
|
||||
assert 'another_trigger' in agent.triggers
|
||||
assert '/test_task' in agent.triggers
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert agent.triggers.count('/test_task') == 1 # No duplicates
|
||||
assert len(agent.triggers) == 2
|
||||
assert 'another_trigger' in agent.triggers
|
||||
assert '/test_task' in agent.triggers
|
||||
|
||||
|
||||
def test_task_microagent_match_trigger():
|
||||
def test_task_microagent_match_trigger(temp_dir):
|
||||
"""Test that a task microagent matches its trigger correctly."""
|
||||
content = """---
|
||||
name: test_task
|
||||
@@ -337,17 +340,16 @@ inputs:
|
||||
This is a test task microagent.
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.md') as f:
|
||||
f.write(content.encode())
|
||||
f.flush()
|
||||
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
agent = BaseMicroagent.load(f.name)
|
||||
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
|
||||
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert agent.match_trigger('/test_task') == '/test_task'
|
||||
assert agent.match_trigger(' /test_task ') == '/test_task'
|
||||
assert agent.match_trigger('This contains /test_task') == '/test_task'
|
||||
assert agent.match_trigger('/other_task') is None
|
||||
assert isinstance(agent, TaskMicroagent)
|
||||
assert agent.match_trigger('/test_task') == '/test_task'
|
||||
assert agent.match_trigger(' /test_task ') == '/test_task'
|
||||
assert agent.match_trigger('This contains /test_task') == '/test_task'
|
||||
assert agent.match_trigger('/other_task') is None
|
||||
|
||||
|
||||
def test_default_tools_microagent_exists():
|
||||
@@ -369,15 +371,12 @@ def test_default_tools_microagent_exists():
|
||||
with open(default_tools_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Verify it's a repo microagent (always activated)
|
||||
assert 'type: repo' in content, 'default-tools.md should be a repo microagent'
|
||||
assert 'command: uvx' in content, 'default-tools.md should use uvx command'
|
||||
assert 'mcp-server-fetch' in content, 'default-tools.md should use mcp-server-fetch'
|
||||
|
||||
# Verify it has the fetch tool configured
|
||||
assert 'name: "fetch"' in content, 'default-tools.md should have a fetch tool'
|
||||
assert 'command: "uvx"' in content, 'default-tools.md should use uvx command'
|
||||
assert 'args: ["mcp-server-fetch"]' in content, (
|
||||
'default-tools.md should use mcp-server-fetch'
|
||||
)
|
||||
agent = BaseMicroagent.load(default_tools_path)
|
||||
|
||||
assert isinstance(agent, RepoMicroagent)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -7,7 +7,7 @@ from openhands.microagent.types import MicroagentType
|
||||
def test_load_markdown_without_frontmatter():
|
||||
"""Test loading a markdown file without frontmatter."""
|
||||
content = '# Test Content\nThis is a test markdown file without frontmatter.'
|
||||
path = Path('test.md')
|
||||
path = Path('default.md')
|
||||
|
||||
# Load the agent from content using keyword argument
|
||||
agent = BaseMicroagent.load(path=path, file_content=content)
|
||||
@@ -26,7 +26,7 @@ def test_load_markdown_with_empty_frontmatter():
|
||||
content = (
|
||||
'---\n---\n# Test Content\nThis is a test markdown file with empty frontmatter.'
|
||||
)
|
||||
path = Path('test.md')
|
||||
path = Path('default.md')
|
||||
|
||||
# Load the agent from content using keyword argument
|
||||
agent = BaseMicroagent.load(path=path, file_content=content)
|
||||
@@ -50,12 +50,12 @@ name: custom_name
|
||||
---
|
||||
# Test Content
|
||||
This is a test markdown file with partial frontmatter."""
|
||||
path = Path('test.md')
|
||||
path = Path('custom_name.md')
|
||||
|
||||
# Load the agent from content using keyword argument
|
||||
agent = BaseMicroagent.load(path=path, file_content=content)
|
||||
|
||||
# Verify it uses provided name but default values for other fields
|
||||
# Verify it uses filename instead of provided name (filename takes precedence)
|
||||
assert isinstance(agent, RepoMicroagent)
|
||||
assert agent.name == 'custom_name'
|
||||
assert (
|
||||
@@ -77,12 +77,12 @@ version: 2.0.0
|
||||
---
|
||||
# Test Content
|
||||
This is a test markdown file with full frontmatter."""
|
||||
path = Path('test.md')
|
||||
path = Path('test_agent.md')
|
||||
|
||||
# Load the agent from content using keyword argument
|
||||
agent = BaseMicroagent.load(path=path, file_content=content)
|
||||
|
||||
# Verify all provided values are used
|
||||
# Verify filename is used for name but other metadata values are preserved
|
||||
assert isinstance(agent, RepoMicroagent)
|
||||
assert agent.name == 'test_agent'
|
||||
assert (
|
||||
|
||||
Reference in New Issue
Block a user