Compare commits

..

2 Commits

Author SHA1 Message Date
mamoodi
acc283b36d Update runtime bind address in doc 2025-03-10 10:30:23 -04:00
mamoodi
15e4daae31 Release 0.28.0 2025-03-10 10:26:52 -04:00
215 changed files with 5584 additions and 9643 deletions

View File

@@ -10,6 +10,12 @@ updates:
pre-commit:
patterns:
- "pre-commit"
llama:
patterns:
- "llama*"
chromadb:
patterns:
- "chromadb"
browsergym:
patterns:
- "browsergym*"

View File

@@ -36,7 +36,7 @@ jobs:
python-version: '3.12'
cache: 'poetry'
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation
run: poetry install --without evaluation,llama-index
- name: Build Environment
run: make build
- name: Run tests

View File

@@ -54,7 +54,7 @@ jobs:
Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation
run: poetry install --without evaluation,llama-index
- name: Configure config.toml for testing with Haiku
env:
@@ -117,6 +117,68 @@ jobs:
echo "EOF" >> $GITHUB_ENV
# -------------------------------------------------------------
# Run DelegatorAgent tests for Haiku, limited to t01 and t02
- name: Wait a little bit (again)
run: sleep 5
- name: Configure config.toml for testing DelegatorAgent (Haiku)
env:
LLM_MODEL: "litellm_proxy/claude-3-5-haiku-20241022"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 30
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for DelegatorAgent (Haiku)
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD DelegatorAgent '' 30 $N_PROCESSES "t01_fix_simple_typo,t02_add_bash_hello" 'delegator_haiku_run'
# Find and export the delegator test results
REPORT_FILE_DELEGATOR_HAIKU=$(find evaluation/evaluation_outputs/outputs/integration_tests/DelegatorAgent/*haiku*_maxiter_30_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE_DELEGATOR_HAIKU: $REPORT_FILE_DELEGATOR_HAIKU"
echo "INTEGRATION_TEST_REPORT_DELEGATOR_HAIKU<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_DELEGATOR_HAIKU >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# -------------------------------------------------------------
# Run DelegatorAgent tests for DeepSeek, limited to t01 and t02
- name: Wait a little bit (again)
run: sleep 5
- name: Configure config.toml for testing DelegatorAgent (DeepSeek)
env:
LLM_MODEL: "litellm_proxy/deepseek-chat"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 30
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for DelegatorAgent (DeepSeek)
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD DelegatorAgent '' 30 $N_PROCESSES "t01_fix_simple_typo,t02_add_bash_hello" 'delegator_deepseek_run'
# Find and export the delegator test results
REPORT_FILE_DELEGATOR_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/DelegatorAgent/deepseek*_maxiter_30_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE_DELEGATOR_DEEPSEEK: $REPORT_FILE_DELEGATOR_DEEPSEEK"
echo "INTEGRATION_TEST_REPORT_DELEGATOR_DEEPSEEK<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_DELEGATOR_DEEPSEEK >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# -------------------------------------------------------------
# Run VisualBrowsingAgent tests for DeepSeek, limited to t05 and t06
- name: Wait a little bit (again)
run: sleep 5
@@ -151,7 +213,7 @@ jobs:
run: |
TIMESTAMP=$(date +'%y-%m-%d-%H-%M')
cd evaluation/evaluation_outputs/outputs # Change to the outputs directory
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/DelegatorAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
- name: Upload evaluation results as artifact
uses: actions/upload-artifact@v4
@@ -192,6 +254,12 @@ jobs:
**Integration Tests Report (DeepSeek)**
DeepSeek LLM Test Results:
${{ env.INTEGRATION_TEST_REPORT_DEEPSEEK }}
---
**Integration Tests Report Delegator (Haiku)**
${{ env.INTEGRATION_TEST_REPORT_DELEGATOR_HAIKU }}
---
**Integration Tests Report Delegator (DeepSeek)**
${{ env.INTEGRATION_TEST_REPORT_DELEGATOR_DEEPSEEK }}
---
**Integration Tests Report VisualBrowsing (DeepSeek)**
${{ env.INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK }}

View File

@@ -44,11 +44,11 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation
run: poetry install --without evaluation,llama-index
- name: Build Environment
run: make build
- name: Run Tests
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_long_term_memory.py
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:

View File

@@ -133,7 +133,7 @@ install-python-dependencies:
export HNSWLIB_NO_NATIVE=1; \
poetry run pip install chroma-hnswlib; \
fi
@poetry install
@poetry install --without llama-index
@if [ -f "/etc/manjaro-release" ]; then \
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
poetry run pip install playwright; \
@@ -265,6 +265,35 @@ setup-config-prompts:
@read -p "Enter your LLM base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
if [[ ! -z "$$llm_base_url" ]]; then echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
@echo "Enter your LLM Embedding Model"; \
echo "Choices are:"; \
echo " - openai"; \
echo " - azureopenai"; \
echo " - Embeddings available only with OllamaEmbedding:"; \
echo " - llama2"; \
echo " - mxbai-embed-large"; \
echo " - nomic-embed-text"; \
echo " - all-minilm"; \
echo " - stable-code"; \
echo " - bge-m3"; \
echo " - bge-large"; \
echo " - paraphrase-multilingual"; \
echo " - snowflake-arctic-embed"; \
echo " - Leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
read -p "> " llm_embedding_model; \
echo "embedding_model=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
if [ "$$llm_embedding_model" = "llama2" ] || [ "$$llm_embedding_model" = "mxbai-embed-large" ] || [ "$$llm_embedding_model" = "nomic-embed-text" ] || [ "$$llm_embedding_model" = "all-minilm" ] || [ "$$llm_embedding_model" = "stable-code" ]; then \
read -p "Enter the local model URL for the embedding model (will set llm.embedding_base_url): " llm_embedding_base_url; \
echo "embedding_base_url=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
elif [ "$$llm_embedding_model" = "azureopenai" ]; then \
read -p "Enter the Azure endpoint URL (will overwrite llm.base_url): " llm_base_url; \
echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the Azure LLM Embedding Deployment Name: " llm_embedding_deployment_name; \
echo "embedding_deployment_name=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the Azure API Version: " llm_api_version; \
echo "api_version=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
fi
# Develop in container
docker-dev:

View File

@@ -12,7 +12,7 @@
<a href="https://codecov.io/github/All-Hands-AI/OpenHands?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/All-Hands-AI/OpenHands?style=for-the-badge&color=blue"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
<br/>
@@ -96,7 +96,7 @@ troubleshooting resources, and advanced configuration options.
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw) - Here we talk about research, architecture, and future development.
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg) - Here we talk about research, architecture, and future development.
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.

View File

@@ -132,6 +132,15 @@ api_key = ""
# Custom LLM provider
#custom_llm_provider = ""
# Embedding API base URL
#embedding_base_url = ""
# Embedding deployment name
#embedding_deployment_name = ""
# Embedding model to use
embedding_model = "local"
# Maximum number of characters in an observation's content
#max_message_chars = 10000
@@ -218,6 +227,9 @@ codeact_enable_llm_editor = false
# whether the IPython tool is enabled
codeact_enable_jupyter = true
# Name of the micro agent to use for this agent
#micro_agent_name = ""
# Memory enabled
#memory_enabled = false

View File

@@ -26,7 +26,7 @@ RUN apt-get update -y \
COPY ./pyproject.toml ./poetry.lock ./
RUN touch README.md
RUN export POETRY_CACHE_DIR && poetry install --without evaluation --no-root && rm -rf $POETRY_CACHE_DIR
RUN export POETRY_CACHE_DIR && poetry install --without evaluation,llama-index --no-root && rm -rf $POETRY_CACHE_DIR
FROM python:3.12.3-slim AS openhands-app

View File

@@ -333,6 +333,12 @@ Pour les configurations de développement, vous pouvez également définir des c
Les options de configuration de l'agent sont définies dans les sections `[agent]` et `[agent.<agent_name>]` du fichier `config.toml`.
**Configuration du micro-agent**
- `micro_agent_name`
- Type : `str`
- Valeur par défaut : `""`
- Description : Nom du micro-agent à utiliser pour cet agent
**Configuration de la mémoire**
- `memory_enabled`
- Type : `bool`

View File

@@ -42,7 +42,7 @@ Explorez le code source d'OpenHands sur [GitHub](https://github.com/All-Hands-AI
/>
</a>
<br></br>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw">
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg">
<img
src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge"
alt="Join our Slack community"

View File

@@ -328,6 +328,12 @@ LLM(大语言模型)配置选项在 `config.toml` 文件的 `[llm]` 部分中定
Agent 配置选项在 `config.toml` 文件的 `[agent]``[agent.<agent_name>]` 部分中定义。
**Microagent 配置**
- `micro_agent_name`
- 类型: `str`
- 默认值: `""`
- 描述: 用于此 agent 的 micro agent 名称
**内存配置**
- `memory_enabled`
- 类型: `bool`

View File

@@ -42,7 +42,7 @@ OpenHands 是一个**自主 AI 软件工程师**,能够执行复杂的工程
/>
</a>
<br></br>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw">
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg">
<img
src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge"
alt="Join our Slack community"

View File

@@ -4,30 +4,18 @@ The GitHub Resolver automates code fixes and provides intelligent assistance for
## Setup
The Cloud GitHub Resolver is available automatically when you
The Cloud Github Resolver is available automatically when you
[grant OpenHands Cloud repository access](./openhands-cloud.md#adding-repositories).
## Usage
After granting OpenHands Cloud repository access, you can use the Cloud GitHub Resolver on the issues and pull requests
on the repository.
### Issues
On your repository, label an issue with `openhands`. OpenHands will:
1. Comment on the issue to let you know it is working on it.
- You can click on the link to track the progress on OpenHands Cloud.
2. Open a pull request if it determines that the issue has been successfully resolved.
3. Comment on the issue with a summary of the performed tasks and a link to the pull request.
On your repository, label an issue with `openhands`. OpenHands will attempt to fix the issue.
### Pull Requests
To get OpenHands to work on pull requests, use `@openhands` in top level or inline comments to:
In order to get OpenHands to work on pull requests, use `@openhands` in top level or single inline comments to:
- Ask questions
- Request updates
- Get code explanations
OpenHands will:
1. Comment on the PR to let you know it is working on it.
2. Perform the task.

View File

@@ -12,16 +12,14 @@ instructions on how to access it.
After visiting OpenHands Cloud, you will be asked to connect with your GitHub account:
1. After reading and accepting the terms of service, click `Connect to GitHub`.
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands AI`.
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands by All Hands AI`.
- OpenHands will require some permissions from your GitHub account. To read more about these permissions,
you can click the `Learn more` link on the GitHub authorize page.
## Repository Access
### Adding Repository Access
## Adding Repositories
You can grant OpenHands specific repository access:
1. Click the `Select a GitHub project` dropdown, select `Add more repositories...`.
1. Under the `Select a GitHub project` dropdown, select `Add more repositories...`.
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
- Openhands requests short-lived tokens (8-hour expiry) with these permissions:
- Actions: Read and write
@@ -36,10 +34,6 @@ You can grant OpenHands specific repository access:
- Repository access for a user is granted based on:
- Granted permission for the repository.
- User's GitHub permissions (owner/collaborator).
3. Click on `Install & Authorize`.
### Modifying Repository Access
You can modify repository access at any time by:
* Using the same `Select a GitHub project > Add more repositories` workflow, or
* Visiting the Settings page and selecting `Configure GitHub Repositories` under the `GitHub Settings` section.
You can manage repository access any time by following the above workflow or visiting the Settings page and selecting
`Configure GitHub Repositories` under the `GitHub Settings` section.

View File

@@ -197,6 +197,21 @@ For development setups, you can also define custom named LLM configurations. See
- Default: `""`
- Description: Custom LLM provider
### Embeddings
- `embedding_base_url`
- Type: `str`
- Default: `""`
- Description: Embedding API base URL
- `embedding_deployment_name`
- Type: `str`
- Default: `""`
- Description: Embedding deployment name
- `embedding_model`
- Type: `str`
- Default: `"local"`
- Description: Embedding model to use
### Message Handling
- `max_message_chars`
@@ -281,6 +296,23 @@ For development setups, you can also define custom named LLM configurations. See
The agent configuration options are defined in the `[agent]` and `[agent.<agent_name>]` sections of the `config.toml` file.
### Microagent Configuration
- `micro_agent_name`
- Type: `str`
- Default: `""`
- Description: Name of the micro agent to use for this agent
### Memory Configuration
- `memory_enabled`
- Type: `bool`
- Default: `false`
- Description: Whether long-term memory (embeddings) is enabled
- `memory_max_threads`
- Type: `int`
- Default: `3`
- Description: The maximum number of threads indexing at the same time for embeddings
### LLM Configuration
- `llm_config`
- Type: `str`

View File

@@ -36,7 +36,7 @@ At this time, we will follow the following release process:
1. All people who contributed public feedback will receive an email describing the data release and being given an opportunity to opt out.
2. The person or people in charge of the data release will perform quality control of the data, removing low-quality feedback,
removing email submitter email addresses, and attempting to remove any sensitive information.
3. The data will be released publicly under the MIT license through commonly used sites such as GitHub or Hugging Face.
3. The data will be released publicly under the MIT license through commonly used sites such as github or Hugging Face.
### What if I want my data deleted?

View File

@@ -39,7 +39,7 @@ You can provide custom directions for OpenHands by following the [README for the
### Custom configurations
GitHub resolver will automatically check for valid [repository secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions?tool=webui#creating-secrets-for-a-repository) or [repository variables](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) to customize its behavior.
Github resolver will automatically check for valid [repository secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions?tool=webui#creating-secrets-for-a-repository) or [repository variables](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) to customize its behavior.
The customization options you can set are:
| **Attribute name** | **Type** | **Purpose** | **Example** |

View File

@@ -76,7 +76,7 @@ You'll find OpenHands running at http://localhost:3000!
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes#connecting-to-your-filesystem),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
or run it on tagged issues with [a GitHub action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
or run it on tagged issues with [a github action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
## Setup

View File

@@ -31,11 +31,17 @@ You will need your ChatGPT deployment name which can be found on the deployments
- `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)
- `API Key` to your Azure API key
## Embeddings
OpenHands uses llama-index for embeddings. You can find their documentation on Azure [here](https://docs.llamaindex.ai/en/stable/api_reference/embeddings/azure_openai/).
### Azure OpenAI Configuration
When running OpenHands, set the following environment variable using `-e` in the
When running OpenHands, set the following environment variables using `-e` in the
[docker run command](/modules/usage/installation#start-the-app):
```
LLM_EMBEDDING_MODEL="azureopenai"
LLM_EMBEDDING_DEPLOYMENT_NAME="<your-embedding-deployment-name>" # e.g. "TextEmbedding...<etc>"
LLM_API_VERSION="<api-version>" # e.g. "2024-02-15-preview"
```

View File

@@ -10,9 +10,6 @@ We also support "remote" runtimes, which are typically managed by third-parties.
They can make setup a bit simpler and more scalable, especially
if you're running many OpenHands conversations in parallel (e.g. to do evaluation).
Additionally, we provide a "local" runtime that runs directly on your machine without Docker,
which can be useful in controlled environments like CI pipelines.
## Docker Runtime
This is the default Runtime that's used when you start OpenHands. You might notice
some flags being passed to `docker run` that make this possible:
@@ -59,12 +56,11 @@ any files that are mounted into its workspace.
This setup can cause some issues with file permissions (hence the `SANDBOX_USER_ID` variable)
but seems to work well on most systems.
## OpenHands Remote Runtime
## All Hands Runtime
The All Hands Runtime is currently in beta. You can request access by joining
the #remote-runtime-limited-beta channel on Slack ([see the README](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-how-to-join-the-community) for an invite).
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!
To use the OpenHands Remote Runtime, set the following environment variables when
To use the All Hands Runtime, set the following environment variables when
starting OpenHands:
```bash
@@ -121,66 +117,3 @@ bash -i <(curl -sL https://get.daytona.io/openhands)
Once executed, OpenHands should be running locally and ready for use.
For more details and manual initialization, view the entire [README.md](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/impl/daytona/README.md)
## Local Runtime
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.
:::caution
**Security Warning**: The Local Runtime runs without any sandbox isolation. The agent can directly access and modify files on your machine. Only use this runtime in controlled environments or when you fully understand the security implications.
:::
### Prerequisites
Before using the Local Runtime, ensure you have the following dependencies installed:
1. You have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
2. tmux is available on your system.
### Configuration
To use the Local Runtime, besides required configurations like the model, API key, you'll need to set the following options via environment variables or the [config.toml file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) when starting OpenHands:
- Via environment variables:
```bash
# Required
export RUNTIME=local
# Optional but recommended
export WORKSPACE_BASE=/path/to/your/workspace
```
- Via `config.toml`:
```toml
[core]
runtime = "local"
workspace_base = "/path/to/your/workspace"
```
If `WORKSPACE_BASE` is not set, the runtime will create a temporary directory for the agent to work in.
### Example Usage
Here's an example of how to start OpenHands with the Local Runtime in Headless Mode:
```bash
# Set the runtime type to local
export RUNTIME=local
# Optionally set a workspace directory
export WORKSPACE_BASE=/path/to/your/project
# Start OpenHands
poetry run python -m openhands.core.main -t "write a bash script that prints hi"
```
### Use Cases
The Local Runtime is particularly useful for:
- CI/CD pipelines where Docker is not available.
- Testing and development of OpenHands itself.
- Environments where container usage is restricted.
- Scenarios where direct file system access is required.

View File

@@ -8,7 +8,7 @@ function CustomFooter() {
<footer className="custom-footer">
<div className="footer-content">
<div className="footer-icons">
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw" target="_blank" rel="noopener noreferrer">
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg" target="_blank" rel="noopener noreferrer">
<FaSlack />
</a>
<a href="https://discord.gg/ESHStjSjD4" target="_blank" rel="noopener noreferrer">

View File

@@ -46,7 +46,7 @@ export function HomepageHeader() {
<a href="https://codecov.io/github/All-Hands-AI/OpenHands?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" /></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License" /></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community" /></a>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community" /></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community" /></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits" /></a>
<br/>

View File

@@ -1,6 +1,6 @@
# Commit0 Evaluation with OpenHands
This folder contains the evaluation harness that we built on top of the original [Commit0](https://commit-0.github.io/) ([paper](https://arxiv.org/abs/2412.01769v1)).
This folder contains the evaluation harness that we built on top of the original [Commit0](https://commit-0.github.io/) ([paper](TBD)).
The evaluation consists of three steps:

View File

@@ -18,20 +18,6 @@ Please follow instruction [here](../../README.md#setup) to setup your local deve
## Run Inference (Rollout) on SWE-Bench Instances: Generate Patch from Problem Statement
> [!NOTE]
> **Iterative Evaluation Protocol**
>
> We have an iterative approach for more stable and reproducible results:
> - For each instance, we attempt to generate a solution up to 3 times
> - Each attempt continues until either:
> 1. The agent successfully produces a patch with `AgentFinishAction`, or
> 2. The attempt reaches the maximum iteration limit
> - If an attempt fails, we retry with a fresh attempt (up to the 3-attempt maximum)
> - If your LLM config has temperature=0, we will automatically use temperature=0.1 for the 2nd and 3rd attempts
>
> To enable this iterative protocol, set `export ITERATIVE_EVAL_MODE=true`
### Running Locally with Docker
Make sure your Docker daemon is running, and you have ample disk space (at least 200-500GB, depends on the SWE-Bench set you are running on) for the instance-level docker image.
@@ -59,7 +45,7 @@ to `CodeActAgent`.
default, the script evaluates the entire SWE-bench_Lite test set (300 issues). Note:
in order to use `eval_limit`, you must also set `agent`.
- `max_iter`, e.g. `20`, is the maximum number of iterations for the agent to run. By
default, it is set to 60.
default, it is set to 30.
- `num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
default, it is set to 1.
- `dataset`, a huggingface dataset name. e.g. `princeton-nlp/SWE-bench`, `princeton-nlp/SWE-bench_Lite`, or `princeton-nlp/SWE-bench_Verified`, specifies which dataset to evaluate on.

View File

@@ -1,12 +1,9 @@
import copy
import json
import os
import subprocess
import tempfile
import time
from dataclasses import dataclass
from functools import partial
from typing import Callable
import pandas as pd
from tqdm import tqdm
@@ -94,22 +91,12 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
return config
@dataclass
class ConditionalImports:
"""We instantiate the values in this dataclass differently if we're evaluating SWE-bench or SWE-Gym."""
get_eval_report: Callable
APPLY_PATCH_FAIL: str
APPLY_PATCH_PASS: str
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
log_dir: str | None = None,
runtime_failure_count: int = 0,
conditional_imports: ConditionalImports | None = None,
) -> EvalOutput:
"""
Evaluate agent performance on a SWE-bench problem instance.
@@ -121,18 +108,9 @@ def process_instance(
log_dir (str | None, default=None): Path to directory where log files will be written. Must
be provided if `reset_logger` is set.
conditional_imports: A dataclass containing values that are imported differently based on
whether we're evaluating SWE-bench or SWE-Gym.
Raises:
AssertionError: if the `reset_logger` flag is set without a provided log directory.
AssertionError: if `conditional_imports` is not provided.
"""
assert (
conditional_imports is not None
), 'conditional_imports must be provided to run process_instance using multiprocessing'
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
assert (
@@ -146,7 +124,7 @@ def process_instance(
config = get_config(metadata, instance)
instance_id = instance.instance_id
model_patch = instance['model_patch']
test_spec = instance['test_spec']
test_spec: TestSpec = instance['test_spec']
logger.info(f'Starting evaluation for instance {instance_id}.')
if 'test_result' not in instance.keys():
@@ -176,11 +154,6 @@ def process_instance(
logger.warning(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
metadata = copy.deepcopy(metadata)
metadata.details['runtime_failure_count'] = runtime_failure_count
metadata.details['remote_runtime_resource_factor'] = (
config.sandbox.remote_runtime_resource_factor
)
try:
runtime = create_runtime(config)
@@ -223,9 +196,7 @@ def process_instance(
instance['test_result']['apply_patch_output'] = apply_patch_output
if 'APPLY_PATCH_FAIL' in apply_patch_output:
logger.info(
f'[{instance_id}] {conditional_imports.APPLY_PATCH_FAIL}:\n{apply_patch_output}'
)
logger.info(f'[{instance_id}] {APPLY_PATCH_FAIL}:\n{apply_patch_output}')
instance['test_result']['report']['failed_apply_patch'] = True
return EvalOutput(
@@ -234,9 +205,7 @@ def process_instance(
metadata=metadata,
)
elif 'APPLY_PATCH_PASS' in apply_patch_output:
logger.info(
f'[{instance_id}] {conditional_imports.APPLY_PATCH_PASS}:\n{apply_patch_output}'
)
logger.info(f'[{instance_id}] {APPLY_PATCH_PASS}:\n{apply_patch_output}')
# Run eval script in background and save output to log file
log_file = '/tmp/eval_output.log'
@@ -302,20 +271,14 @@ def process_instance(
with open(test_output_path, 'w') as f:
f.write(test_output)
try:
extra_kwargs = {}
if 'SWE-Gym' in metadata.dataset:
# SWE-Gym uses a different version of the package, hence a different eval report argument
extra_kwargs['log_path'] = test_output_path
else:
extra_kwargs['test_log_path'] = test_output_path
_report = conditional_imports.get_eval_report(
_report = get_eval_report(
test_spec=test_spec,
prediction={
'model_patch': model_patch,
'instance_id': instance_id,
},
test_log_path=test_output_path,
include_tests_status=True,
**extra_kwargs,
)
report = _report[instance_id]
logger.info(
@@ -382,6 +345,7 @@ if __name__ == '__main__':
)
from swegym.harness.test_spec import (
SWEbenchInstance,
TestSpec,
make_test_spec,
)
from swegym.harness.utils import load_swebench_dataset
@@ -393,6 +357,7 @@ if __name__ == '__main__':
)
from swebench.harness.test_spec.test_spec import (
SWEbenchInstance,
TestSpec,
make_test_spec,
)
from swebench.harness.utils import load_swebench_dataset
@@ -475,21 +440,12 @@ if __name__ == '__main__':
.decode('utf-8')
.strip(), # Current commit
dataset=args.dataset, # Dataset name from args
details={},
)
# The evaluation harness constrains the signature of `process_instance_func` but we need to
# pass extra information. Build a new function object to avoid issues with multiprocessing.
process_instance_func = partial(
process_instance,
log_dir=output_file.replace('.jsonl', '.logs'),
# We have to explicitly pass these imports to the process_instance function, otherwise
# they won't be available in the multiprocessing context.
conditional_imports=ConditionalImports(
get_eval_report=get_eval_report,
APPLY_PATCH_FAIL=APPLY_PATCH_FAIL,
APPLY_PATCH_PASS=APPLY_PATCH_PASS,
),
process_instance, log_dir=output_file.replace('.jsonl', '.logs')
)
run_evaluation(

View File

@@ -23,7 +23,7 @@ def get_resource_mapping(dataset_name: str) -> dict[str, float]:
if dataset_name not in _global_resource_mapping:
file_path = os.path.join(CUR_DIR, f'{dataset_name}.json')
if not os.path.exists(file_path):
logger.info(f'Resource mapping for {dataset_name} not found.')
logger.warning(f'Resource mapping for {dataset_name} not found.')
return None
with open(file_path, 'r') as f:

View File

@@ -1,5 +1,4 @@
import asyncio
import copy
import json
import os
import tempfile
@@ -37,10 +36,9 @@ from openhands.core.config import (
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.critic import AgentFinishedCritic
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation, ErrorObservation
from openhands.events.serialization.event import event_from_dict, event_to_dict
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
@@ -123,28 +121,24 @@ You SHOULD NEVER attempt to browse the web.
# TODO: migrate all swe-bench docker to ghcr.io/openhands
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get(
'EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/'
)
logger.info(f'Default docker image prefix: {DEFAULT_DOCKER_IMAGE_PREFIX}')
DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/')
logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}')
def get_instance_docker_image(instance_id: str, official_image: bool = False) -> str:
if official_image:
# Official SWE-Bench image
# swebench/sweb.eval.x86_64.django_1776_django-11333:v1
docker_image_prefix = 'docker.io/swebench/'
repo, name = instance_id.split('__')
image_name = f'sweb.eval.x86_64.{repo}_1776_{name}:latest'
logger.warning(f'Using official SWE-Bench image: {image_name}')
else:
# OpenHands version of the image
docker_image_prefix = DEFAULT_DOCKER_IMAGE_PREFIX
image_name = 'sweb.eval.x86_64.' + instance_id
image_name = image_name.replace(
'__', '_s_'
) # to comply with docker image naming convention
return (docker_image_prefix.rstrip('/') + '/' + image_name).lower()
return (DOCKER_IMAGE_PREFIX.rstrip('/') + '/' + image_name).lower()
def get_config(
@@ -153,8 +147,7 @@ def get_config(
) -> AppConfig:
# We use a different instance image for the each instance of swe-bench eval
use_official_image = bool(
('verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower())
and 'swe-gym' not in metadata.dataset.lower()
'verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower()
)
base_container_image = get_instance_docker_image(
instance['instance_id'], use_official_image
@@ -480,13 +473,6 @@ def process_instance(
logger.warning(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
metadata = copy.deepcopy(metadata)
metadata.details['runtime_failure_count'] = runtime_failure_count
metadata.details['remote_runtime_resource_factor'] = (
config.sandbox.remote_runtime_resource_factor
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
@@ -572,6 +558,20 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
return dataset
# A list of instances that are known to be tricky to infer
# (will cause runtime failure even with resource factor = 8)
SWEGYM_EXCLUDE_IDS = [
'dask__dask-10422',
'pandas-dev__pandas-50548',
'pandas-dev__pandas-53672',
'pandas-dev__pandas-54174',
'pandas-dev__pandas-55518',
'pandas-dev__pandas-58383',
'pydata__xarray-6721',
'pytest-dev__pytest-10081',
'pytest-dev__pytest-7236',
]
if __name__ == '__main__':
parser = get_parser()
parser.add_argument(
@@ -596,20 +596,11 @@ if __name__ == '__main__':
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_bench_tests)} tasks'
)
if 'SWE-Gym' in args.dataset:
with open(
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'split',
'swegym_verified_instances.json',
),
'r',
) as f:
swegym_verified_instances = json.load(f)
swe_bench_tests = swe_bench_tests[
swe_bench_tests['instance_id'].isin(swegym_verified_instances)
]
swe_bench_tests = swe_bench_tests[
~swe_bench_tests['instance_id'].isin(SWEGYM_EXCLUDE_IDS)
]
logger.info(
f'{len(swe_bench_tests)} tasks left after filtering for SWE-Gym verified instances'
f'{len(swe_bench_tests)} tasks left after excluding SWE-Gym excluded tasks'
)
llm_config = None
@@ -640,132 +631,20 @@ if __name__ == '__main__':
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
print(f'### OUTPUT FILE: {output_file} ###')
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
# Run evaluation in iterative mode:
# If a rollout fails to output AgentFinishAction, we will try again until it succeeds OR total 3 attempts have been made.
ITERATIVE_EVAL_MODE = (
os.environ.get('ITERATIVE_EVAL_MODE', 'false').lower() == 'true'
if len(instances) > 0 and not isinstance(
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
):
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
instances[col] = instances[col].apply(lambda x: str(x))
run_evaluation(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8 * 60 * 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
ITERATIVE_EVAL_MODE_MAX_ATTEMPTS = int(
os.environ.get('ITERATIVE_EVAL_MODE_MAX_ATTEMPTS', '3')
)
if not ITERATIVE_EVAL_MODE:
# load the dataset
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
if len(instances) > 0 and not isinstance(
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
):
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
instances[col] = instances[col].apply(lambda x: str(x))
run_evaluation(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
else:
critic = AgentFinishedCritic()
def get_cur_output_file_path(attempt: int) -> str:
return (
f'{output_file.removesuffix(".jsonl")}.critic_attempt_{attempt}.jsonl'
)
eval_ids = None
for attempt in range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1):
cur_output_file = get_cur_output_file_path(attempt)
logger.info(
f'Running evaluation with critic {critic.__class__.__name__} for attempt {attempt} of {ITERATIVE_EVAL_MODE_MAX_ATTEMPTS}.'
)
# For deterministic eval, we set temperature to 0.1 for (>1) attempt
# so hopefully we get slightly different results
if attempt > 1 and metadata.llm_config.temperature == 0:
logger.info(
f'Detected temperature is 0 for (>1) attempt {attempt}. Setting temperature to 0.1...'
)
metadata.llm_config.temperature = 0.1
# Load instances - at first attempt, we evaluate all instances
# On subsequent attempts, we only evaluate the instances that failed the previous attempt determined by critic
instances = prepare_dataset(
swe_bench_tests, cur_output_file, args.eval_n_limit, eval_ids=eval_ids
)
if len(instances) > 0 and not isinstance(
instances['PASS_TO_PASS'][instances['PASS_TO_PASS'].index[0]], str
):
for col in ['PASS_TO_PASS', 'FAIL_TO_PASS']:
instances[col] = instances[col].apply(lambda x: str(x))
# Run evaluation - but save them to cur_output_file
logger.info(
f'Evaluating {len(instances)} instances for attempt {attempt}...'
)
run_evaluation(
instances,
metadata,
cur_output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
# When eval is done, we update eval_ids to the instances that failed the current attempt
instances_failed = []
logger.info(
f'Use critic {critic.__class__.__name__} to check {len(instances)} instances for attempt {attempt}...'
)
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
history = [event_from_dict(event) for event in instance['history']]
critic_result = critic.evaluate(history)
if not critic_result.success:
instances_failed.append(instance['instance_id'])
logger.info(
f'{len(instances_failed)} instances failed the current attempt {attempt}: {instances_failed}'
)
eval_ids = instances_failed
# If no instances failed, we break
if len(instances_failed) == 0:
break
# Then we should aggregate the results from all attempts into the original output file
# and remove the intermediate files
logger.info(
'Aggregating results from all attempts into the original output file...'
)
fout = open(output_file, 'w')
added_instance_ids = set()
for attempt in reversed(range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1)):
cur_output_file = get_cur_output_file_path(attempt)
if not os.path.exists(cur_output_file):
logger.warning(
f'Intermediate output file {cur_output_file} does not exist. Skipping...'
)
continue
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
if instance['instance_id'] not in added_instance_ids:
fout.write(line)
added_instance_ids.add(instance['instance_id'])
logger.info(
f'Aggregated instances from {cur_output_file}. Total instances added so far: {len(added_instance_ids)}'
)
fout.close()
logger.info(
f'Done! Total {len(added_instance_ids)} instances added to {output_file}'
)

View File

@@ -9,7 +9,7 @@ parser.add_argument(
'--dataset_name',
type=str,
help='Name of the dataset to download',
default='princeton-nlp/SWE-bench_Verified',
default='princeton-nlp/SWE-bench_Lite',
)
parser.add_argument('--split', type=str, help='Split to download', default='test')
args = parser.parse_args()
@@ -20,12 +20,7 @@ print(
f'Downloading gold patches from {args.dataset_name} (split: {args.split}) to {output_filepath}'
)
patches = [
{
'instance_id': row['instance_id'],
'model_patch': row['patch'],
'model_name_or_path': 'gold',
}
for row in dataset
{'instance_id': row['instance_id'], 'model_patch': row['patch']} for row in dataset
]
print(f'{len(patches)} gold patches loaded')
pd.DataFrame(patches).to_json(output_filepath, lines=True, orient='records')

View File

@@ -25,8 +25,8 @@ if [ -z "$AGENT" ]; then
fi
if [ -z "$MAX_ITER" ]; then
echo "MAX_ITER not specified, use default 60"
MAX_ITER=60
echo "MAX_ITER not specified, use default 100"
MAX_ITER=100
fi
if [ -z "$RUN_WITH_BROWSING" ]; then

View File

@@ -34,6 +34,7 @@ from openhands.utils.async_utils import call_async_from_sync
FAKE_RESPONSES = {
'CodeActAgent': fake_user_response,
'DelegatorAgent': fake_user_response,
'VisualBrowsingAgent': fake_user_response,
}

View File

@@ -4,10 +4,8 @@ import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
import OpenHands from "#/api/open-hands";
import { PaymentForm } from "#/components/features/payment/payment-form";
import * as featureFlags from "#/utils/feature-flags";
describe("PaymentForm", () => {
const billingSettingsSpy = vi.spyOn(featureFlags, "BILLING_SETTINGS");
const getBalanceSpy = vi.spyOn(OpenHands, "getBalance");
const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession");
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
@@ -28,7 +26,6 @@ describe("PaymentForm", () => {
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});
billingSettingsSpy.mockReturnValue(true);
});
afterEach(() => {

View File

@@ -26,32 +26,33 @@ const createAxiosNotFoundErrorObject = () =>
},
);
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const RouterStub = createRoutesStub([
{
// layout route
Component: MainApp,
path: "/",
children: [
{
// home route
Component: Home,
path: "/",
},
{
Component: SettingsScreen,
path: "/settings",
},
],
},
]);
afterEach(() => {
vi.clearAllMocks();
});
describe("Home Screen", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const RouterStub = createRoutesStub([
{
// layout route
Component: MainApp,
path: "/",
children: [
{
// home route
Component: Home,
path: "/",
},
{
Component: SettingsScreen,
path: "/settings",
},
],
},
]);
afterEach(() => {
vi.clearAllMocks();
});
it("should render the home screen", () => {
renderWithProviders(<RouterStub initialEntries={["/"]} />);
});
@@ -78,82 +79,57 @@ describe("Home Screen", () => {
const settingsScreen = await screen.findByTestId("settings-screen");
expect(settingsScreen).toBeInTheDocument();
});
});
describe("Settings 404", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
describe("Settings 404", () => {
it("should open the settings modal if GET /settings fails with a 404", async () => {
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
it("should open the settings modal if GET /settings fails with a 404", async () => {
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
renderWithProviders(<RouterStub initialEntries={["/"]} />);
renderWithProviders(<RouterStub initialEntries={["/"]} />);
const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
});
it("should navigate to the settings screen when clicking the advanced settings button", async () => {
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
const user = userEvent.setup();
renderWithProviders(<RouterStub initialEntries={["/"]} />);
const settingsScreen = screen.queryByTestId("settings-screen");
expect(settingsScreen).not.toBeInTheDocument();
const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
const advancedSettingsButton = await screen.findByTestId(
"advanced-settings-link",
);
await user.click(advancedSettingsButton);
const settingsScreenAfter = await screen.findByTestId("settings-screen");
expect(settingsScreenAfter).toBeInTheDocument();
const settingsModalAfter = screen.queryByTestId("ai-config-modal");
expect(settingsModalAfter).not.toBeInTheDocument();
});
it("should not open the settings modal if GET /settings fails but is SaaS mode", async () => {
// TODO: Remove HIDE_LLM_SETTINGS check once released
vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true);
// @ts-expect-error - we only need APP_MODE for this test
getConfigSpy.mockResolvedValue({ APP_MODE: "saas" });
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
renderWithProviders(<RouterStub initialEntries={["/"]} />);
// small hack to wait for the modal to not appear
await expect(
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
).rejects.toThrow();
});
});
describe("Setup Payment modal", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
afterEach(() => {
vi.resetAllMocks();
});
it("should only render if SaaS mode and is new user", async () => {
// @ts-expect-error - we only need the APP_MODE for this test
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
});
vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true);
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
renderWithProviders(<RouterStub initialEntries={["/"]} />);
it("should navigate to the settings screen when clicking the advanced settings button", async () => {
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
const setupPaymentModal = await screen.findByTestId("proceed-to-stripe-button");
expect(setupPaymentModal).toBeInTheDocument();
const user = userEvent.setup();
renderWithProviders(<RouterStub initialEntries={["/"]} />);
const settingsScreen = screen.queryByTestId("settings-screen");
expect(settingsScreen).not.toBeInTheDocument();
const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
const advancedSettingsButton = await screen.findByTestId(
"advanced-settings-link",
);
await user.click(advancedSettingsButton);
const settingsScreenAfter = await screen.findByTestId("settings-screen");
expect(settingsScreenAfter).toBeInTheDocument();
const settingsModalAfter = screen.queryByTestId("ai-config-modal");
expect(settingsModalAfter).not.toBeInTheDocument();
});
it("should not open the settings modal if GET /settings fails but is SaaS mode", async () => {
// TODO: Remove HIDE_LLM_SETTINGS check once released
vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true);
// @ts-expect-error - we only need APP_MODE for this test
getConfigSpy.mockResolvedValue({ APP_MODE: "saas" });
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
renderWithProviders(<RouterStub initialEntries={["/"]} />);
// small hack to wait for the modal to not appear
await expect(
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
).rejects.toThrow();
});
});
});

View File

@@ -95,10 +95,7 @@ describe("Settings Screen", () => {
await waitFor(() => {
screen.getByTestId("github-token-input");
// Check for GitHub link instead of the help anchor
screen.getByRole("link", { name: "GitHub" });
// Check for documentation link
screen.getByRole("link", { name: "documentation" });
screen.getByTestId("github-token-help-anchor");
screen.getByTestId("language-input");
screen.getByTestId("enable-analytics-switch");
});
@@ -240,12 +237,10 @@ describe("Settings Screen", () => {
await waitFor(() => {
const input = screen.queryByTestId("github-token-input");
const githubLink = screen.queryByText("GitHub");
const documentationLink = screen.queryByText("documentation");
const helpAnchor = screen.queryByTestId("github-token-help-anchor");
expect(input).not.toBeInTheDocument();
expect(githubLink).not.toBeInTheDocument();
expect(documentationLink).not.toBeInTheDocument();
expect(helpAnchor).not.toBeInTheDocument();
});
});
@@ -726,7 +721,7 @@ describe("Settings Screen", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_api_key: "", // empty because it's not set previously
provider_tokens: undefined,
github_token: undefined,
language: "no",
}),
);
@@ -763,7 +758,7 @@ describe("Settings Screen", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
provider_tokens: undefined,
github_token: undefined,
llm_api_key: "", // empty because it's not set previously
llm_model: "openai/gpt-4o",
}),
@@ -806,7 +801,7 @@ describe("Settings Screen", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith({
...mockCopy,
provider_tokens: undefined, // not set
github_token: undefined, // not set
llm_api_key: "", // reset as well
});
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();

View File

@@ -1,9 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { handleStatusMessage, handleActionMessage } from "#/services/actions";
import { handleStatusMessage } from "#/services/actions";
import store from "#/store";
import { trackError } from "#/utils/error-handler";
import ActionType from "#/types/action-type";
import { ActionMessage } from "#/types/message";
// Mock dependencies
vi.mock("#/utils/error-handler", () => ({
@@ -58,89 +56,4 @@ describe("Actions Service", () => {
}));
});
});
describe("handleActionMessage", () => {
it("should use first-person perspective for task completion messages", () => {
// Test partial completion
const messagePartial: ActionMessage = {
id: 1,
action: ActionType.FINISH,
source: "agent",
message: "",
timestamp: new Date().toISOString(),
args: {
final_thought: "",
task_completed: "partial",
outputs: "",
thought: ""
}
};
// Mock implementation to capture the message
let capturedPartialMessage = "";
(store.dispatch as any).mockImplementation((action: any) => {
if (action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **completed partially**")) {
capturedPartialMessage = action.payload;
}
});
handleActionMessage(messagePartial);
expect(capturedPartialMessage).toContain("I believe that the task was **completed partially**");
// Test not completed
const messageNotCompleted: ActionMessage = {
id: 2,
action: ActionType.FINISH,
source: "agent",
message: "",
timestamp: new Date().toISOString(),
args: {
final_thought: "",
task_completed: "false",
outputs: "",
thought: ""
}
};
// Mock implementation to capture the message
let capturedNotCompletedMessage = "";
(store.dispatch as any).mockImplementation((action: any) => {
if (action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **not completed**")) {
capturedNotCompletedMessage = action.payload;
}
});
handleActionMessage(messageNotCompleted);
expect(capturedNotCompletedMessage).toContain("I believe that the task was **not completed**");
// Test completed successfully
const messageCompleted: ActionMessage = {
id: 3,
action: ActionType.FINISH,
source: "agent",
message: "",
timestamp: new Date().toISOString(),
args: {
final_thought: "",
task_completed: "true",
outputs: "",
thought: ""
}
};
// Mock implementation to capture the message
let capturedCompletedMessage = "";
(store.dispatch as any).mockImplementation((action: any) => {
if (action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **completed successfully**")) {
capturedCompletedMessage = action.payload;
}
});
handleActionMessage(messageCompleted);
expect(capturedCompletedMessage).toContain("I believe that the task was **completed successfully**");
});
});
});

View File

@@ -8,7 +8,7 @@
* - Please do NOT serve this file on production.
*/
const PACKAGE_VERSION = '2.7.3'
const PACKAGE_VERSION = '2.7.0'
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View File

@@ -281,13 +281,6 @@ class OpenHands {
return data.redirect_url;
}
static async createBillingSessionResponse(): Promise<string> {
const { data } = await openHands.post(
"/api/billing/create-customer-setup-session",
);
return data.redirect_url;
}
static async getBalance(): Promise<string> {
const { data } = await openHands.get<{ credits: string }>(
"/api/billing/credits",

View File

@@ -48,7 +48,6 @@ export interface GetConfigResponse {
APP_SLUG?: string;
GITHUB_CLIENT_ID: string;
POSTHOG_CLIENT_KEY: string;
STRIPE_PUBLISHABLE_KEY?: string;
}
export interface GetVSCodeUrlResponse {

View File

@@ -1,48 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import { Trans, useTranslation } from "react-i18next";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import OpenHands from "#/api/open-hands";
import { BrandButton } from "../settings/brand-button";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
export function SetupPaymentModal() {
const { t } = useTranslation();
const { mutate, isPending } = useMutation({
mutationFn: OpenHands.createBillingSessionResponse,
onSuccess: (data) => {
window.location.href = data;
},
onError: () => {
displayErrorToast(t("BILLING$ERROR_WHILE_CREATING_SESSION"));
},
});
return (
<ModalBackdrop>
<ModalBody className="border border-tertiary">
<AllHandsLogo width={68} height={46} />
<div className="flex flex-col gap-2 w-full items-center text-center">
<h1 className="text-2xl font-bold">{t("BILLING$YOUVE_GOT_50")}</h1>
<p>
<Trans
i18nKey="BILLING$CLAIM_YOUR_50"
components={{ b: <strong /> }}
/>
</p>
</div>
<BrandButton
testId="proceed-to-stripe-button"
type="submit"
variant="primary"
className="w-full"
isDisabled={isPending}
onClick={mutate}
>
{t("BILLING$PROCEED_TO_STRIPE")}
</BrandButton>
</ModalBody>
</ModalBackdrop>
);
}

View File

@@ -61,7 +61,7 @@ export function Sidebar() {
displayErrorToast(
"Something went wrong while fetching settings. Please reload the page.",
);
} else if (config?.APP_MODE === "oss" && settingsError?.status === 404) {
} else if (settingsError?.status === 404) {
setSettingsModalIsOpen(true);
}
}, [

View File

@@ -17,7 +17,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
? ""
: settings.LLM_API_KEY?.trim() || undefined,
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
provider_tokens: settings.provider_tokens,
github_token: settings.github_token,
unset_github_token: settings.unset_github_token,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,

View File

@@ -11,6 +11,4 @@ export const useAIConfigOptions = () =>
useQuery({
queryKey: ["ai-config-options"],
queryFn: fetchAiConfigOptions,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});

View File

@@ -14,7 +14,5 @@ export const useAppInstallations = () => {
githubTokenIsSet &&
!!config?.GITHUB_CLIENT_ID &&
config?.APP_MODE === "saas",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
};

View File

@@ -50,8 +50,6 @@ export const useAppRepositories = () => {
Array.isArray(installations) &&
installations.length > 0 &&
config?.APP_MODE === "saas",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached

View File

@@ -1,7 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands";
import { BILLING_SETTINGS } from "#/utils/feature-flags";
export const useBalance = () => {
const { data: config } = useConfig();
@@ -9,6 +8,6 @@ export const useBalance = () => {
return useQuery({
queryKey: ["user", "balance"],
queryFn: OpenHands.getBalance,
enabled: config?.APP_MODE === "saas" && BILLING_SETTINGS(),
enabled: config?.APP_MODE === "saas",
});
};

View File

@@ -5,6 +5,4 @@ export const useConfig = () =>
useQuery({
queryKey: ["config"],
queryFn: OpenHands.getConfig,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});

View File

@@ -18,8 +18,6 @@ export const useConversationConfig = () => {
return OpenHands.getRuntimeId(conversationId);
},
enabled: status !== WsClientProviderStatus.DISCONNECTED && !!conversationId,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
React.useEffect(() => {

View File

@@ -19,8 +19,6 @@ export const useGitHubUser = () => {
queryFn: OpenHands.getGitHubUser,
enabled: githubTokenIsSet && !!config?.APP_MODE,
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
React.useEffect(() => {

View File

@@ -15,7 +15,6 @@ export const useIsAuthed = () => {
queryFn: () => OpenHands.authenticate(appMode!),
enabled: !!appMode,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
retry: false,
meta: {
disableToast: true,

View File

@@ -23,7 +23,5 @@ export const useListFiles = (config: UseListFilesConfig = DEFAULT_CONFIG) => {
queryKey: ["files", conversationId, config?.path],
queryFn: () => OpenHands.getFiles(conversationId, config?.path),
enabled: !!(isActive && config?.enabled),
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
};

View File

@@ -7,7 +7,5 @@ export function useSearchRepositories(query: string) {
queryFn: () => OpenHands.searchGitHubRepositories(query, 3),
enabled: !!query,
select: (data) => data.map((repo) => ({ ...repo, is_public: true })),
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
}

View File

@@ -21,8 +21,6 @@ const getSettingsQueryFn = async () => {
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
PROVIDER_TOKENS: apiSettings.provider_tokens,
IS_NEW_USER: false,
};
};
@@ -36,8 +34,6 @@ export const useSettings = () => {
// would want to show the modal immediately if the
// settings are not found
retry: (_, error) => error.status !== 404,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
meta: {
disableToast: true,
},

View File

@@ -7,6 +7,4 @@ export const useUserConversation = (cid: string | null) =>
queryFn: () => OpenHands.getConversation(cid!),
enabled: !!cid,
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});

View File

@@ -9,5 +9,6 @@ export const useUserConversations = () => {
queryKey: ["user", "conversations"],
queryFn: OpenHands.getUserConversations,
enabled: !!userIsAuthenticated,
staleTime: 0,
});
};

View File

@@ -15,8 +15,6 @@ export const useUserRepositories = () => {
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
enabled: githubTokenIsSet && config?.APP_MODE === "oss",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached

View File

@@ -13,8 +13,6 @@ export const useVSCodeUrl = (config: { enabled: boolean }) => {
},
enabled: !!conversationId && config.enabled,
refetchOnMount: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
return data;

View File

@@ -312,9 +312,4 @@ export enum I18nKey {
BUTTON$MARK_NOT_HELPFUL = "BUTTON$MARK_NOT_HELPFUL",
BUTTON$EXPORT_CONVERSATION = "BUTTON$EXPORT_CONVERSATION",
BILLING$CLICK_TO_TOP_UP = "BILLING$CLICK_TO_TOP_UP",
BILLING$YOUVE_GOT_50 = "BILLING$YOUVE_GOT_50",
BILLING$ERROR_WHILE_CREATING_SESSION = "BILLING$ERROR_WHILE_CREATING_SESSION",
BILLING$CLAIM_YOUR_50 = "BILLING$CLAIM_YOUR_50",
BILLING$PROCEED_TO_STRIPE = "BILLING$PROCEED_TO_STRIPE",
BILLING$YOURE_IN = "BILLING$YOURE_IN",
}

View File

@@ -4647,80 +4647,5 @@
"fr": "Ajouter des fonds à votre compte",
"tr": "Hesabınıza bakiye ekleyin",
"de": "Guthaben zu Ihrem Konto hinzufügen"
},
"BILLING$YOUVE_GOT_50": {
"en": "You've got $50 in free OpenHands credits",
"ja": "OpenHandsの無料クレジット$50を獲得しました",
"zh-CN": "您获得了 $50 的 OpenHands 免费额度",
"zh-TW": "您獲得了 $50 的 OpenHands 免費額度",
"ko-KR": "OpenHands 무료 크레딧 $50를 받았습니다",
"no": "Du har fått $50 i gratis OpenHands-kreditter",
"it": "Hai ottenuto $50 in crediti gratuiti OpenHands",
"pt": "Você ganhou $50 em créditos gratuitos OpenHands",
"es": "Has recibido $50 en créditos gratuitos de OpenHands",
"ar": "لديك 50$ من رصيد OpenHands المجاني",
"fr": "Vous avez reçu $50 de crédits OpenHands gratuits",
"tr": "OpenHands'de $50 ücretsiz kredi kazandınız",
"de": "Sie haben $50 in kostenlosen OpenHands-Guthaben erhalten"
},
"BILLING$ERROR_WHILE_CREATING_SESSION": {
"en": "Error occurred while setting up your payment session. Please try again later.",
"ja": "お支払いセッションの設定中にエラーが発生しました。後ほど再度お試しください。",
"zh-CN": "设置支付会话时发生错误。请稍后再试。",
"zh-TW": "設置支付會話時發生錯誤。請稍後再試。",
"ko-KR": "결제 세션 설정 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
"no": "Det oppstod en feil under oppsett av betalingsøkten. Vennligst prøv igjen senere.",
"it": "Si è verificato un errore durante la configurazione della sessione di pagamento. Si prega di riprovare più tardi.",
"pt": "Ocorreu um erro ao configurar sua sessão de pagamento. Por favor, tente novamente mais tarde.",
"es": "Se produjo un error al configurar tu sesión de pago. Por favor, inténtalo de nuevo más tarde.",
"ar": "حدث خطأ أثناء إعداد جلسة الدفع الخاصة بك. يرجى المحاولة مرة أخرى لاحقًا.",
"fr": "Une erreur s'est produite lors de la configuration de votre session de paiement. Veuillez réessayer plus tard.",
"tr": "Ödeme oturumunuz kurulurken bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
"de": "Beim Einrichten Ihrer Zahlungssitzung ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut."
},
"BILLING$CLAIM_YOUR_50": {
"en": "Add a credit card with Stripe to claim your $50. <b>We won't charge you without asking first!</b>",
"ja": "Stripeでクレジットカードを追加して$50を獲得。<b>事前の確認なしで請求することはありません!</b>",
"zh-CN": "添加 Stripe 信用卡以领取 $50。<b>我们不会在未经您同意的情况下收费!</b>",
"zh-TW": "添加 Stripe 信用卡以領取 $50。<b>我們不會在未經您同意的情況下收費!</b>",
"ko-KR": "Stripe에 신용카드를 추가하여 $50를 받으세요. <b>사전 동의 없이 요금이 청구되지 않습니다!</b>",
"no": "Legg til et kredittkort med Stripe for å få $50. <b>Vi belaster deg ikke uten å spørre først!</b>",
"it": "Aggiungi una carta di credito con Stripe per ottenere $50. <b>Non ti addebiteremo nulla senza chiedere prima!</b>",
"pt": "Adicione um cartão de crédito com Stripe para receber $50. <b>Não cobraremos sem perguntar primeiro!</b>",
"es": "Añade una tarjeta de crédito con Stripe para reclamar tus $50. <b>¡No te cobraremos sin preguntarte primero!</b>",
"ar": "أضف بطاقة ائتمان مع Stripe للحصول على 50$. <b>لن نقوم بالخصم دون إذن مسبق!</b>",
"fr": "Ajoutez une carte de crédit avec Stripe pour obtenir 50$. <b>Nous ne vous facturerons pas sans vous demander d'abord !</b>",
"tr": "50$ almak için Stripe ile kredi kartı ekleyin. <b>Önce sormadan ücret almayacağız!</b>",
"de": "Fügen Sie eine Kreditkarte mit Stripe hinzu, um $50 zu erhalten. <b>Wir belasten Sie nicht ohne vorherige Zustimmung!</b>"
},
"BILLING$PROCEED_TO_STRIPE": {
"en": "Add Billing Info",
"ja": "請求情報を追加",
"zh-CN": "添加账单信息",
"zh-TW": "添加帳單資訊",
"ko-KR": "결제 정보 추가",
"no": "Legg til betalingsinformasjon",
"it": "Aggiungi informazioni di fatturazione",
"pt": "Adicionar informações de pagamento",
"es": "Añadir información de facturación",
"ar": "إضافة معلومات الفواتير",
"fr": "Ajouter les informations de facturation",
"tr": "Fatura Bilgisi Ekle",
"de": "Zahlungsinformationen hinzufügen"
},
"BILLING$YOURE_IN": {
"en": "You're in! You can start using your $50 in free credits now.",
"ja": "登録完了!$50分の無料クレジットを今すぐご利用いただけます。",
"zh-CN": "您已加入!现在可以开始使用$50的免费额度了。",
"zh-TW": "您已加入!現在可以開始使用$50的免費額度了。",
"ko-KR": "가입 완료! 지금 바로 $50 상당의 무료 크레딧을 사용하실 수 있습니다.",
"no": "Du er med! Du kan begynne å bruke dine $50 i gratis kreditter nå.",
"it": "Ci sei! Puoi iniziare a utilizzare i tuoi $50 in crediti gratuiti ora.",
"pt": "Você está dentro! Você pode começar a usar seus $50 em créditos gratuitos agora.",
"es": "¡Ya estás dentro! Puedes empezar a usar tus $50 en créditos gratuitos ahora.",
"ar": "أنت معنا! يمكنك البدء في استخدام رصيدك المجاني البالغ 50 دولارًا الآن.",
"fr": "C'est fait ! Vous pouvez commencer à utiliser vos 50 $ de crédits gratuits maintenant.",
"tr": "Başardın! Şimdi $50 değerindeki ücretsiz kredilerini kullanmaya başlayabilirsin.",
"de": "Du bist dabei! Du kannst jetzt deine $50 an kostenlosen Guthaben nutzen."
}
}

View File

@@ -1,4 +1,8 @@
import { delay, http, HttpResponse } from "msw";
import Stripe from "stripe";
const TEST_STRIPE_SECRET_KEY = "";
const PRICE_ID = "";
export const STRIPE_BILLING_HANDLERS = [
http.get("/api/billing/credits", async () => {
@@ -6,17 +10,27 @@ export const STRIPE_BILLING_HANDLERS = [
return HttpResponse.json({ credits: "100" });
}),
http.post("/api/billing/create-checkout-session", async () => {
http.post("/api/billing/create-checkout-session", async ({ request }) => {
await delay();
return HttpResponse.json({
redirect_url: "https://stripe.com/some-checkout",
});
}),
const body = await request.json();
http.post("/api/billing/create-customer-setup-session", async () => {
await delay();
return HttpResponse.json({
redirect_url: "https://stripe.com/some-customer-setup",
});
if (body && typeof body === "object" && body.amount) {
const stripe = new Stripe(TEST_STRIPE_SECRET_KEY);
const session = await stripe.checkout.sessions.create({
line_items: [
{
price: PRICE_ID,
quantity: body.amount,
},
],
mode: "payment",
success_url: "http://localhost:3001/settings/billing/?checkout=success",
cancel_url: "http://localhost:3001/settings/billing/?checkout=cancel",
});
if (session.url) return HttpResponse.json({ redirect_url: session.url });
}
return HttpResponse.json({ message: "Invalid request" }, { status: 400 });
}),
];

View File

@@ -22,13 +22,12 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
provider_tokens: DEFAULT_SETTINGS.PROVIDER_TOKENS,
};
const MOCK_USER_PREFERENCES: {
settings: ApiSettings | PostApiSettings | null;
settings: ApiSettings | PostApiSettings;
} = {
settings: null,
settings: MOCK_DEFAULT_USER_SETTINGS,
};
const conversations: Conversation[] = [
@@ -175,24 +174,22 @@ export const handlers = [
),
http.get("/api/options/config", () => {
const mockSaas = import.meta.env.VITE_MOCK_SAAS === "true";
const config: GetConfigResponse = {
APP_MODE: mockSaas ? "saas" : "oss",
GITHUB_CLIENT_ID: "fake-github-client-id",
POSTHOG_CLIENT_KEY: "fake-posthog-client-key",
STRIPE_PUBLISHABLE_KEY: "",
};
return HttpResponse.json(config);
}),
http.get("/api/settings", async () => {
await delay();
const { settings } = MOCK_USER_PREFERENCES;
if (!settings) return HttpResponse.json(null, { status: 404 });
if (Object.keys(settings.provider_tokens).length > 0)
settings.github_token_is_set = true;
const settings: ApiSettings = {
...MOCK_USER_PREFERENCES.settings,
language: "no",
};
// @ts-expect-error - mock types
if (settings.github_token) settings.github_token_is_set = true;
return HttpResponse.json(settings);
}),
@@ -204,19 +201,17 @@ export const handlers = [
if (typeof body === "object") {
newSettings = { ...body };
if (newSettings.unset_github_token) {
newSettings.provider_tokens = { github: "", gitlab: "" };
newSettings.github_token = undefined;
newSettings.github_token_is_set = false;
delete newSettings.unset_github_token;
}
}
const fullSettings = {
...MOCK_DEFAULT_USER_SETTINGS,
MOCK_USER_PREFERENCES.settings = {
...MOCK_USER_PREFERENCES.settings,
...newSettings,
};
MOCK_USER_PREFERENCES.settings = fullSettings;
return HttpResponse.json(null, { status: 200 });
}

View File

@@ -32,4 +32,10 @@ export const queryClientConfig: QueryClientConfig = {
}
},
}),
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
},
},
};

View File

@@ -24,10 +24,7 @@ function Home() {
});
return (
<div
data-testid="home-screen"
className="bg-base-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto px-2"
>
<div className="bg-base-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto px-2">
<HeroHeading />
<div className="flex flex-col gap-8 w-full md:w-[600px] items-center">
<div className="flex flex-col gap-2 w-full">

View File

@@ -1,13 +1,5 @@
import React from "react";
import {
useRouteError,
isRouteErrorResponse,
Outlet,
useNavigate,
useLocation,
useSearchParams,
} from "react-router";
import { useTranslation } from "react-i18next";
import { useRouteError, isRouteErrorResponse, Outlet } from "react-router";
import i18n from "#/i18n";
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
@@ -18,10 +10,6 @@ import { AnalyticsConsentFormModal } from "#/components/features/analytics/analy
import { useSettings } from "#/hooks/query/use-settings";
import { useAuth } from "#/context/auth-context";
import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
import { useBalance } from "#/hooks/query/use-balance";
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
import { BILLING_SETTINGS } from "#/utils/feature-flags";
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
export function ErrorBoundary() {
const error = useRouteError();
@@ -56,14 +44,11 @@ export function ErrorBoundary() {
}
export default function MainApp() {
const navigate = useNavigate();
const { pathname } = useLocation();
const [searchParams] = useSearchParams();
const { githubTokenIsSet } = useAuth();
const { data: settings } = useSettings();
const { error, isFetching } = useBalance();
const { migrateUserConsent } = useMigrateUserConsent();
const { t } = useTranslation();
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
const config = useConfig();
const {
@@ -77,8 +62,6 @@ export default function MainApp() {
gitHubClientId: config.data?.GITHUB_CLIENT_ID || null,
});
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
React.useEffect(() => {
if (settings?.LANGUAGE) {
i18n.changeLanguage(settings.LANGUAGE);
@@ -101,17 +84,6 @@ export default function MainApp() {
});
}, []);
React.useEffect(() => {
// Don't allow users to use the app if it 402s
if (error?.status === 402 && pathname !== "/") {
navigate("/");
} else if (!isFetching && searchParams.get("free_credits") === "success") {
displaySuccessToast(t("BILLING$YOURE_IN"));
searchParams.delete("free_credits");
navigate("/");
}
}, [error?.status, pathname, isFetching]);
const userIsAuthed = !!isAuthed && !authError;
const renderWaitlistModal =
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
@@ -144,10 +116,6 @@ export default function MainApp() {
}}
/>
)}
{BILLING_SETTINGS() &&
config.data?.APP_MODE === "saas" &&
settings?.IS_NEW_USER && <SetupPaymentModal />}
</div>
);
}

View File

@@ -61,10 +61,7 @@ function AccountSettings() {
if (isSuccess) {
return (
isCustomModel(resources.models, settings.LLM_MODEL) ||
hasAdvancedSettingsSet({
...settings,
PROVIDER_TOKENS: settings.PROVIDER_TOKENS || {},
})
hasAdvancedSettingsSet(settings)
);
}
@@ -131,42 +128,37 @@ function AccountSettings() {
: llmBaseUrl;
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
const githubToken = formData.get("github-token-input")?.toString();
const newSettings = {
github_token: githubToken,
provider_tokens: githubToken
? {
github: githubToken,
gitlab: "",
}
: undefined,
LANGUAGE: languageValue,
user_consents_to_analytics: userConsentsToAnalytics,
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
LLM_MODEL: finalLlmModel,
LLM_BASE_URL: finalLlmBaseUrl,
LLM_API_KEY: finalLlmApiKey,
AGENT: formData.get("agent-input")?.toString(),
SECURITY_ANALYZER:
formData.get("security-analyzer-input")?.toString() || "",
REMOTE_RUNTIME_RESOURCE_FACTOR:
remoteRuntimeResourceFactor ||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
CONFIRMATION_MODE: confirmationModeIsEnabled,
};
saveSettings(newSettings, {
onSuccess: () => {
handleCaptureConsent(userConsentsToAnalytics);
displaySuccessToast("Settings saved");
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
saveSettings(
{
github_token:
formData.get("github-token-input")?.toString() || undefined,
LANGUAGE: languageValue,
user_consents_to_analytics: userConsentsToAnalytics,
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
LLM_MODEL: finalLlmModel,
LLM_BASE_URL: finalLlmBaseUrl,
LLM_API_KEY: finalLlmApiKey,
AGENT: formData.get("agent-input")?.toString(),
SECURITY_ANALYZER:
formData.get("security-analyzer-input")?.toString() || "",
REMOTE_RUNTIME_RESOURCE_FACTOR:
remoteRuntimeResourceFactor ||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
CONFIRMATION_MODE: confirmationModeIsEnabled,
},
onError: (error) => {
const errorMessage = retrieveAxiosErrorMessage(error);
displayErrorToast(errorMessage);
{
onSuccess: () => {
handleCaptureConsent(userConsentsToAnalytics);
displaySuccessToast("Settings saved");
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
},
onError: (error) => {
const errorMessage = retrieveAxiosErrorMessage(error);
displayErrorToast(errorMessage);
},
},
});
);
};
const handleReset = () => {
@@ -410,31 +402,12 @@ function AccountSettings() {
placeholder={isGitHubTokenSet ? "**********" : ""}
/>
<p className="text-xs">
Generate a token on{" "}
<b>
<a
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
target="_blank"
className="underline underline-offset-2"
rel="noopener noreferrer"
>
GitHub
</a>{" "}
</b>
or see the{" "}
<b>
<a
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
target="_blank"
className="underline underline-offset-2"
rel="noopener noreferrer"
>
documentation
</a>
</b>
.
</p>
<HelpLink
testId="github-token-help-anchor"
text="Get your token"
linkText="here"
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
/>
</>
)}

View File

@@ -62,12 +62,13 @@ const messageActions = {
let successPrediction = "";
if (message.args.task_completed === "partial") {
successPrediction =
"I believe that the task was **completed partially**.";
"The agent thinks that the task was **completed partially**.";
} else if (message.args.task_completed === "false") {
successPrediction = "I believe that the task was **not completed**.";
successPrediction =
"The agent thinks that the task was **not completed**.";
} else if (message.args.task_completed === "true") {
successPrediction =
"I believe that the task was **completed successfully**.";
"The agent thinks that the task was **completed successfully**.";
}
if (successPrediction) {
// if final_thought is not empty, add a new line before the success prediction

View File

@@ -15,11 +15,6 @@ export const DEFAULT_SETTINGS: Settings = {
ENABLE_DEFAULT_CONDENSER: true,
ENABLE_SOUND_NOTIFICATIONS: false,
USER_CONSENTS_TO_ANALYTICS: false,
PROVIDER_TOKENS: {
github: "",
gitlab: "",
},
IS_NEW_USER: true,
};
/**

View File

@@ -21,6 +21,7 @@ export interface InitConfig {
LLM_MODEL: string;
};
token?: string;
github_token?: string;
latest_event_id?: unknown; // Not sure what this is
}

View File

@@ -1,5 +1,3 @@
export type Provider = "github" | "gitlab";
export type Settings = {
LLM_MODEL: string;
LLM_BASE_URL: string;
@@ -13,8 +11,6 @@ export type Settings = {
ENABLE_DEFAULT_CONDENSER: boolean;
ENABLE_SOUND_NOTIFICATIONS: boolean;
USER_CONSENTS_TO_ANALYTICS: boolean | null;
PROVIDER_TOKENS: Record<Provider, string>;
IS_NEW_USER?: boolean;
};
export type ApiSettings = {
@@ -30,17 +26,16 @@ export type ApiSettings = {
enable_default_condenser: boolean;
enable_sound_notifications: boolean;
user_consents_to_analytics: boolean | null;
provider_tokens: Record<Provider, string>;
};
export type PostSettings = Settings & {
provider_tokens: Record<Provider, string>;
github_token: string;
unset_github_token: boolean;
user_consents_to_analytics: boolean | null;
};
export type PostApiSettings = ApiSettings & {
provider_tokens: Record<Provider, string>;
github_token: string;
unset_github_token: boolean;
user_consents_to_analytics: boolean | null;
};

View File

@@ -59,18 +59,6 @@ export const extractSettings = (formData: FormData): Partial<Settings> => {
ENABLE_DEFAULT_CONDENSER,
} = extractAdvancedFormData(formData);
// Extract provider tokens
const githubToken = formData.get("github-token")?.toString();
const gitlabToken = formData.get("gitlab-token")?.toString();
const providerTokens: Record<string, string> = {};
if (githubToken) {
providerTokens.github = githubToken;
}
if (gitlabToken) {
providerTokens.gitlab = gitlabToken;
}
return {
LLM_MODEL: CUSTOM_LLM_MODEL || LLM_MODEL,
LLM_API_KEY,
@@ -80,6 +68,5 @@ export const extractSettings = (formData: FormData): Partial<Settings> => {
CONFIRMATION_MODE,
SECURITY_ANALYZER,
ENABLE_DEFAULT_CONDENSER,
PROVIDER_TOKENS: providerTokens,
};
};

View File

@@ -17,7 +17,45 @@ export default {
tertiary: "#454545", // gray, used for inputs
"tertiary-light": "#B7BDC2", // lighter gray, used for borders and placeholder text
content: "#ECEDEE", // light gray, used mostly for text
"content-2": "#F9FBFE",
},
},
animation: {
enter: "toastIn 400ms cubic-bezier(0.21, 1.02, 0.73, 1)",
leave: "toastOut 100ms ease-in forwards",
},
keyframes: {
toastIn: {
"0%": {
opacity: "0",
transform: "translateY(-100%) scale(0.8)",
},
"80%": {
opacity: "1",
transform: "translateY(0) scale(1.02)",
},
"100%": {
opacity: "1",
transform: "translateY(0) scale(1)",
},
},
toastOut: {
"0%": {
opacity: "1",
transform: "translateY(0) scale(1)",
},
"100%": {
opacity: "0",
transform: "translateY(-100%) scale(0.9)",
},
},
colors: {
primary: "#C9B974", // nice yellow
base: "#171717", // dark background (neutral-900)
"base-secondary": "#262626", // lighter background (neutral-800); also used for tooltips
danger: "#E76A5E",
success: "#A5E75E",
tertiary: "#454545", // gray, used for inputs
"tertiary-light": "#B7BDC2", // lighter gray, used for borders and placeholder text
},
},
},

View File

@@ -14,8 +14,6 @@ the GitHub API.
You can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.
ALWAYS use the GitHub API for operations instead of a web browser.
If you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`
Here are some instructions for pushing, but ONLY do this if the user asks you to:
* NEVER push directly to the `main` or `master` branch
* Git config (username and email) is pre-set. Do not modify.

View File

@@ -1,83 +0,0 @@
---
name: swift-linux
type: knowledge
agent: CodeActAgent
version: 1.0.0
triggers:
- swift-linux
- swift-debian
- swift-installation
---
# Swift Installation Guide for Debian Linux
This document provides instructions for installing Swift on Debian 12 (Bookworm).
> This setup is intended for non-UI development tasks on Swift on Linux.
## Prerequisites
Before installing Swift, you need to install the required dependencies for your system. You can find the most up-to-date list of dependencies for your specific Linux distribution and version at the [Swift.org tarball installation guide](https://www.swift.org/install/linux/tarball/).
FOR EXAMPLE, the dependencies you may need to install for Debian 12 could be:
```bash
sudo apt-get update
sudo apt-get install -y \
binutils-gold \
gcc \
git \
libcurl4-openssl-dev \
libedit-dev \
libicu-dev \
libncurses-dev \
libpython3-dev \
libsqlite3-dev \
libxml2-dev \
pkg-config \
tzdata \
uuid-dev
```
## Download and Install Swift
1. Find the latest Swift version for Debian:
Go to the [Swift.org download page](https://www.swift.org/download/) to find the latest Swift version compatible with Debian 12 (Bookworm).
Look for a tarball named something like `swift-<VERSION>-RELEASE-debian12.tar.gz` (e.g., `swift-6.0.3-RELEASE-debian12.tar.gz`).
The URL pattern is typically:
```
https://download.swift.org/swift-<VERSION>-release/debian12/swift-<VERSION>-RELEASE/swift-<VERSION>-RELEASE-debian12.tar.gz
```
Where `<VERSION>` is the Swift version number (e.g., `6.0.3`).
2. Download the Swift binary for Debian 12:
```bash
cd /workspace
wget https://download.swift.org/swift-6.0.3-release/debian12/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-debian12.tar.gz
```
3. Extract the archive:
> **Note**: Make sure to install Swift in the `/workspace` directory, but outside the git repository to avoid committing the Swift binaries.
4. Add Swift to your PATH by adding the following line to your `~/.bashrc` file:
```bash
echo 'export PATH=/workspace/swift-6.0.3-RELEASE-debian12/usr/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
```
> **Note**: Make sure to update the version number in the PATH to match the version you downloaded.
## Verify Installation
Verify that Swift is correctly installed by running:
```bash
swift --version
```

View File

@@ -1,20 +1,39 @@
from dotenv import load_dotenv
from openhands.agenthub.micro.agent import MicroAgent
from openhands.agenthub.micro.registry import all_microagents
from openhands.controller.agent import Agent
load_dotenv()
from openhands.agenthub import ( # noqa: E402
browsing_agent,
codeact_agent,
delegator_agent,
dummy_agent,
visualbrowsing_agent,
)
from openhands.controller.agent import Agent # noqa: E402
__all__ = [
'Agent',
'codeact_agent',
'delegator_agent',
'dummy_agent',
'browsing_agent',
'visualbrowsing_agent',
]
for agent in all_microagents.values():
name = agent['name']
prompt = agent['prompt']
anon_class = type(
name,
(MicroAgent,),
{
'prompt': prompt,
'agent_definition': agent,
},
)
Agent.register(name, anon_class)

View File

@@ -1,6 +1,8 @@
import json
import os
from collections import deque
import openhands
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
@@ -70,17 +72,23 @@ class CodeActAgent(Agent):
codeact_enable_browsing=self.config.codeact_enable_browsing,
codeact_enable_jupyter=self.config.codeact_enable_jupyter,
codeact_enable_llm_editor=self.config.codeact_enable_llm_editor,
llm=self.llm,
)
logger.debug(
f"TOOLS loaded for CodeActAgent: {', '.join([tool.get('function').get('name') for tool in self.tools])}"
f'TOOLS loaded for CodeActAgent: {json.dumps(self.tools, indent=2, ensure_ascii=False).replace("\\n", "\n")}'
)
self.prompt_manager = PromptManager(
microagent_dir=os.path.join(
os.path.dirname(os.path.dirname(openhands.__file__)),
'microagents',
)
if self.config.enable_prompt_extensions
else None,
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
disabled_microagents=self.config.disabled_microagents,
)
# Create a ConversationMemory instance
self.conversation_memory = ConversationMemory(self.config, self.prompt_manager)
self.conversation_memory = ConversationMemory(self.prompt_manager)
self.condenser = Condenser.from_config(self.config.condenser)
logger.debug(f'Using condenser: {type(self.condenser)}')
@@ -160,7 +168,7 @@ class CodeActAgent(Agent):
if not self.prompt_manager:
raise Exception('Prompt Manager not instantiated.')
# Use ConversationMemory to process initial messages
# Use conversation_memory to process events instead of calling events_to_messages directly
messages = self.conversation_memory.process_initial_messages(
with_caching=self.llm.is_caching_prompt_active()
)
@@ -172,12 +180,12 @@ class CodeActAgent(Agent):
f'Processing {len(events)} events from a total of {len(state.history)} events'
)
# Use ConversationMemory to process events
messages = self.conversation_memory.process_events(
condensed_history=events,
initial_messages=messages,
max_message_chars=self.llm.config.max_message_chars,
vision_is_active=self.llm.vision_is_active(),
enable_som_visual_browsing=self.config.enable_som_visual_browsing,
)
messages = self._enhance_messages(messages)
@@ -208,7 +216,14 @@ class CodeActAgent(Agent):
# compose the first user message with examples
self.prompt_manager.add_examples_to_initial_message(msg)
elif msg.role == 'user':
# and/or repo/runtime info
if self.config.enable_prompt_extensions:
self.prompt_manager.add_info_to_initial_message(msg)
# enhance the user message with additional context based on keywords matched
if msg.role == 'user':
self.prompt_manager.enhance_message(msg)
# Add double newline between consecutive user messages
if prev_role == 'user' and len(msg.content) > 0:
# Find the first TextContent in the message to add newlines

View File

@@ -12,13 +12,13 @@ from litellm import (
from openhands.agenthub.codeact_agent.tools import (
BrowserTool,
CmdRunTool,
FinishTool,
IPythonTool,
LLMBasedFileEditTool,
StrReplaceEditorTool,
ThinkTool,
WebReadTool,
create_cmd_run_tool,
create_str_replace_editor_tool,
)
from openhands.core.exceptions import (
FunctionCallNotExistsError,
@@ -39,7 +39,6 @@ from openhands.events.action import (
)
from openhands.events.event import FileEditSource, FileReadSource
from openhands.events.tool import ToolCallMetadata
from openhands.llm import LLM
def combine_thought(action: Action, thought: str) -> Action:
@@ -81,7 +80,7 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
# CmdRunTool (Bash)
# ================================================
if tool_call.function.name == create_cmd_run_tool()['function']['name']:
if tool_call.function.name == CmdRunTool['function']['name']:
if 'command' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "command" in tool call {tool_call.function.name}'
@@ -132,10 +131,7 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
start=arguments.get('start', 1),
end=arguments.get('end', -1),
)
elif (
tool_call.function.name
== create_str_replace_editor_tool()['function']['name']
):
elif tool_call.function.name == StrReplaceEditorTool['function']['name']:
if 'command' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "command" in tool call {tool_call.function.name}'
@@ -223,22 +219,8 @@ def get_tools(
codeact_enable_browsing: bool = False,
codeact_enable_llm_editor: bool = False,
codeact_enable_jupyter: bool = False,
llm: LLM | None = None,
) -> list[ChatCompletionToolParam]:
SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-', 'o3', 'o1']
use_simplified_tool_desc = False
if llm is not None:
use_simplified_tool_desc = any(
model_substr in llm.config.model
for model_substr in SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS
)
tools = [
create_cmd_run_tool(use_simplified_description=use_simplified_tool_desc),
ThinkTool,
FinishTool,
]
tools = [CmdRunTool, ThinkTool, FinishTool]
if codeact_enable_browsing:
tools.append(WebReadTool)
tools.append(BrowserTool)
@@ -247,9 +229,5 @@ def get_tools(
if codeact_enable_llm_editor:
tools.append(LLMBasedFileEditTool)
else:
tools.append(
create_str_replace_editor_tool(
use_simplified_description=use_simplified_tool_desc
)
)
tools.append(StrReplaceEditorTool)
return tools

View File

@@ -1,6 +1,6 @@
{% if repository_info %}
<REPOSITORY_INFO>
At the user's request, repository {{ repository_info.repo_name }} has been cloned to the current working directory {{ repository_info.repo_directory }}.
At the user's request, repository {{ repository_info.repo_name }} has been cloned to directory {{ repository_info.repo_directory }}.
</REPOSITORY_INFO>
{% endif %}
{% if repository_instructions -%}
@@ -20,8 +20,6 @@ When starting a web server, use the corresponding ports. You should also
set any options to allow iframes and CORS requests, and allow the server to
be accessed from any host (e.g. 0.0.0.0).
{% endif %}
{% if runtime_info.additional_agent_instructions %}
{{ runtime_info.additional_agent_instructions }}
{% endif %}
</RUNTIME_INFORMATION>
{% endif %}

View File

@@ -1,8 +1,8 @@
{% for agent_info in triggered_agents %}
<EXTRA_INFO>
The following information has been included based on a keyword match for "{{ agent_info.trigger }}".
The following information has been included based on a keyword match for "{{ agent_info.trigger_word }}".
It may or may not be relevant to the user's request.
{{ agent_info.content }}
{{ agent_info.agent.content }}
</EXTRA_INFO>
{% endfor %}

View File

@@ -1,19 +1,19 @@
from .bash import create_cmd_run_tool
from .bash import CmdRunTool
from .browser import BrowserTool
from .finish import FinishTool
from .ipython import IPythonTool
from .llm_based_edit import LLMBasedFileEditTool
from .str_replace_editor import create_str_replace_editor_tool
from .str_replace_editor import StrReplaceEditorTool
from .think import ThinkTool
from .web_read import WebReadTool
__all__ = [
'BrowserTool',
'create_cmd_run_tool',
'CmdRunTool',
'FinishTool',
'IPythonTool',
'LLMBasedFileEditTool',
'create_str_replace_editor_tool',
'StrReplaceEditorTool',
'WebReadTool',
'ThinkTool',
]

View File

@@ -1,6 +1,6 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
### Command Execution
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, use `&&` or `;` to chain them together.
@@ -22,39 +22,25 @@ _DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a
* Output truncation: If the output exceeds a maximum length, it will be truncated before being returned.
"""
_SIMPLIFIED_BASH_DESCRIPTION = """Execute a bash command in the terminal.
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process.
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together."""
def create_cmd_run_tool(
use_simplified_description: bool = False,
) -> ChatCompletionToolParam:
description = (
_SIMPLIFIED_BASH_DESCRIPTION
if use_simplified_description
else _DETAILED_BASH_DESCRIPTION
)
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='execute_bash',
description=description,
parameters={
'type': 'object',
'properties': {
'command': {
'type': 'string',
'description': 'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process. Note: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.',
},
'is_input': {
'type': 'string',
'description': 'If True, the command is an input to the running process. If False, the command is a bash command to be executed in the terminal. Default is False.',
'enum': ['true', 'false'],
},
CmdRunTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='execute_bash',
description=_BASH_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'command': {
'type': 'string',
'description': 'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process. Note: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.',
},
'is_input': {
'type': 'string',
'description': 'If True, the command is an input to the running process. If False, the command is a bash command to be executed in the terminal. Default is False.',
'enum': ['true', 'false'],
},
'required': ['command'],
},
),
)
'required': ['command'],
},
),
)

View File

@@ -1,6 +1,6 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_DETAILED_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
* State is persistent across command calls and discussions with the user
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
* The `create` command cannot be used if the specified `path` already exists as a file
@@ -31,73 +31,46 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL:
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.
"""
_SIMPLIFIED_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
* State is persistent across command calls and discussions with the user
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
* The `create` command cannot be used if the specified `path` already exists as a file
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
* The `undo_edit` command will revert the last edit made to the file at `path`
Notes for using the `str_replace` command:
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
* The `new_str` parameter should contain the edited lines that should replace the `old_str`
"""
def create_str_replace_editor_tool(
use_simplified_description: bool = False,
) -> ChatCompletionToolParam:
description = (
_SIMPLIFIED_STR_REPLACE_EDITOR_DESCRIPTION
if use_simplified_description
else _DETAILED_STR_REPLACE_EDITOR_DESCRIPTION
)
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='str_replace_editor',
description=description,
parameters={
'type': 'object',
'properties': {
'command': {
'description': 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.',
'enum': [
'view',
'create',
'str_replace',
'insert',
'undo_edit',
],
'type': 'string',
},
'path': {
'description': 'Absolute path to file or directory, e.g. `/workspace/file.py` or `/workspace`.',
'type': 'string',
},
'file_text': {
'description': 'Required parameter of `create` command, with the content of the file to be created.',
'type': 'string',
},
'old_str': {
'description': 'Required parameter of `str_replace` command containing the string in `path` to replace.',
'type': 'string',
},
'new_str': {
'description': 'Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.',
'type': 'string',
},
'insert_line': {
'description': 'Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.',
'type': 'integer',
},
'view_range': {
'description': 'Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.',
'items': {'type': 'integer'},
'type': 'array',
},
StrReplaceEditorTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='str_replace_editor',
description=_STR_REPLACE_EDITOR_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'command': {
'description': 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.',
'enum': ['view', 'create', 'str_replace', 'insert', 'undo_edit'],
'type': 'string',
},
'path': {
'description': 'Absolute path to file or directory, e.g. `/workspace/file.py` or `/workspace`.',
'type': 'string',
},
'file_text': {
'description': 'Required parameter of `create` command, with the content of the file to be created.',
'type': 'string',
},
'old_str': {
'description': 'Required parameter of `str_replace` command containing the string in `path` to replace.',
'type': 'string',
},
'new_str': {
'description': 'Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.',
'type': 'string',
},
'insert_line': {
'description': 'Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.',
'type': 'integer',
},
'view_range': {
'description': 'Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.',
'items': {'type': 'integer'},
'type': 'array',
},
'required': ['command', 'path'],
},
),
)
'required': ['command', 'path'],
},
),
)

View File

@@ -0,0 +1,4 @@
from openhands.agenthub.delegator_agent.agent import DelegatorAgent
from openhands.controller.agent import Agent
Agent.register('DelegatorAgent', DelegatorAgent)

View File

@@ -0,0 +1,87 @@
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.events.action import Action, AgentDelegateAction, AgentFinishAction
from openhands.events.observation import AgentDelegateObservation, Observation
from openhands.llm.llm import LLM
class DelegatorAgent(Agent):
VERSION = '1.0'
"""
The Delegator Agent is responsible for delegating tasks to other agents based on the current task.
"""
current_delegate: str = ''
def __init__(self, llm: LLM, config: AgentConfig):
"""Initialize the Delegator Agent with an LLM
Parameters:
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm, config)
def step(self, state: State) -> Action:
"""Checks to see if current step is completed, returns AgentFinishAction if True.
Otherwise, delegates the task to the next agent in the pipeline.
Parameters:
- state (State): The current state given the previous actions and observations
Returns:
- AgentFinishAction: If the last state was 'completed', 'verified', or 'abandoned'
- AgentDelegateAction: The next agent to delegate the task to
"""
if self.current_delegate == '':
self.current_delegate = 'study'
task, _ = state.get_current_user_intent()
return AgentDelegateAction(
agent='StudyRepoForTaskAgent', inputs={'task': task}
)
# last observation in history should be from the delegate
last_observation = None
for event in reversed(state.history):
if isinstance(event, Observation):
last_observation = event
break
if not isinstance(last_observation, AgentDelegateObservation):
raise Exception('Last observation is not an AgentDelegateObservation')
goal, _ = state.get_current_user_intent()
if self.current_delegate == 'study':
self.current_delegate = 'coder'
return AgentDelegateAction(
agent='CoderAgent',
inputs={
'task': goal,
'summary': last_observation.outputs['summary'],
},
)
elif self.current_delegate == 'coder':
self.current_delegate = 'verifier'
return AgentDelegateAction(
agent='VerifierAgent',
inputs={
'task': goal,
},
)
elif self.current_delegate == 'verifier':
if (
'completed' in last_observation.outputs
and last_observation.outputs['completed']
):
return AgentFinishAction()
else:
self.current_delegate = 'coder'
return AgentDelegateAction(
agent='CoderAgent',
inputs={
'task': goal,
'summary': last_observation.outputs['summary'],
},
)
else:
raise Exception('Invalid delegate state')

View File

@@ -0,0 +1,17 @@
## Introduction
This package contains definitions of micro-agents. A micro-agent is defined
in the following structure:
```
[AgentName]
├── agent.yaml
└── prompt.md
```
Note that `prompt.md` could use jinja2 template syntax. During runtime, `prompt.md`
is loaded and rendered, and used together with `agent.yaml` to initialize a
micro-agent.
Micro-agents can be used independently. You can also use `ManagerAgent` which knows
how to coordinate the agents and collaboratively finish a task.

View File

@@ -0,0 +1,2 @@
* `browse` - opens a web page. Arguments:
* `url` - the URL to open

View File

@@ -0,0 +1,3 @@
* `delegate` - send a task to another agent from the list provided. Arguments:
* `agent` - the agent to which the task is delegated. MUST match a name in the list of agents provided.
* `inputs` - a dictionary of input parameters to the agent, as specified in the list

View File

@@ -0,0 +1,2 @@
* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
* `outputs` - a dictionary representing the outputs of your task, if any

View File

@@ -0,0 +1,3 @@
* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
* `content` - the thought to record
* `wait_for_response` - set to `true` to wait for the user to respond before proceeding

View File

@@ -0,0 +1,2 @@
* `read` - reads the content of a file. Arguments:
* `path` - the path of the file to read

View File

@@ -0,0 +1,2 @@
* `reject` - reject the task. Arguments:
* `outputs` - a dictionary with only a `reason` attribute

View File

@@ -0,0 +1,2 @@
* `run` - runs a command on the command line in a Linux shell. Arguments:
* `command` - the command to run

View File

@@ -0,0 +1,3 @@
* `write` - writes the content to a file. Arguments:
* `path` - the path of the file to write
* `content` - the content to write to the file

View File

@@ -0,0 +1,5 @@
Your response MUST be in JSON format. It must be an object, and it must contain two fields:
* `action`, which is one of the actions specified here
* `args`, which is a map of key-value pairs, specifying the arguments for that action
You MUST NOT include any other text besides the JSON response

View File

@@ -0,0 +1,4 @@
Here is a recent history of actions you've taken in service of this plan,
as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.

View File

@@ -0,0 +1,88 @@
from jinja2 import BaseLoader, Environment
from openhands.agenthub.micro.instructions import instructions
from openhands.agenthub.micro.registry import all_microagents
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.core.message import ImageContent, Message, TextContent
from openhands.events.action import Action
from openhands.events.event import Event
from openhands.events.serialization.action import action_from_dict
from openhands.events.serialization.event import event_to_memory
from openhands.io import json
from openhands.llm.llm import LLM
def parse_response(orig_response: str) -> Action:
# attempt to load the JSON dict from the response
action_dict = json.loads(orig_response)
# load the action from the dict
return action_from_dict(action_dict)
def to_json(obj: object, **kwargs: dict) -> str:
"""Serialize an object to str format"""
return json.dumps(obj, **kwargs)
class MicroAgent(Agent):
VERSION = '1.0'
prompt = ''
agent_definition: dict = {}
def history_to_json(
self, history: list[Event], max_events: int = 20, **kwargs: dict
) -> str:
"""
Serialize and simplify history to str format
"""
processed_history = []
event_count = 0
for event in reversed(history):
if event_count >= max_events:
break
processed_history.append(
event_to_memory(event, self.llm.config.max_message_chars)
)
event_count += 1
# history is in reverse order, let's fix it
processed_history.reverse()
# everything starts with a message
# the first message is already in the prompt as the task
# TODO: so we don't need to include it in the history
return json.dumps(processed_history, **kwargs)
def __init__(self, llm: LLM, config: AgentConfig):
super().__init__(llm, config)
if 'name' not in self.agent_definition:
raise ValueError('Agent definition must contain a name')
self.prompt_template = Environment(loader=BaseLoader()).from_string(self.prompt)
self.delegates = all_microagents.copy()
del self.delegates[self.agent_definition['name']]
def step(self, state: State) -> Action:
last_user_message, last_image_urls = state.get_current_user_intent()
prompt = self.prompt_template.render(
state=state,
instructions=instructions,
to_json=to_json,
history_to_json=self.history_to_json,
delegates=self.delegates,
latest_user_message=last_user_message,
)
content: list[TextContent | ImageContent] = [TextContent(text=prompt)]
if self.llm.vision_is_active() and last_image_urls:
content.append(ImageContent(image_urls=last_image_urls))
message = Message(role='user', content=content)
resp = self.llm.completion(
messages=self.llm.format_messages_for_llm(message),
)
action_resp = resp['choices'][0]['message']['content']
action = parse_response(action_resp)
return action

View File

@@ -0,0 +1,6 @@
name: CoderAgent
description: Given a particular task, and a detailed description of the codebase, accomplishes the task
inputs:
task: string
summary: string
outputs: {}

View File

@@ -0,0 +1,27 @@
# Task
You are a software engineer. You've inherited an existing codebase, which you
need to modify to complete this task:
{{ state.inputs.task }}
{% if state.inputs.summary %}
Here's a summary of the codebase, as it relates to this task:
{{ state.inputs.summary }}
{% endif %}
## Available Actions
{{ instructions.actions.run }}
{{ instructions.actions.write }}
{{ instructions.actions.read }}
{{ instructions.actions.message }}
{{ instructions.actions.finish }}
Do NOT finish until you have completed the tasks.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history, max_events=20) }}
## Format
{{ instructions.format.action }}

View File

@@ -0,0 +1,25 @@
## Introduction
CommitWriterAgent can help write git commit message. Example:
```bash
WORKSPACE_MOUNT_PATH="`PWD`" \
poetry run python openhands/core/main.py -t "dummy task" -c CommitWriterAgent -d ./
```
This agent is special in the sense that it doesn't need a task. Once called,
it attempts to read all diff in the git staging area and write a good commit
message.
## Future work
### Feedback loop
The commit message could be (optionally) shown to the customer or
other agents, so that CommitWriterAgent could gather feedback to further
improve the commit message.
### Task rejection
When the agent cannot compile a commit message (e.g. not git repository), it
should reject the task with an explanation.

View File

@@ -0,0 +1,6 @@
name: CommitWriterAgent
description: "Write a git commit message for files in the git staging area"
inputs: {}
outputs:
answer: string
reason: string

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