Compare commits

..

59 Commits

Author SHA1 Message Date
openhands
c377f9e9ae Merge main into feature/runtime-manager 2025-01-03 20:37:08 +00:00
Robert Brennan
825a9ba893 default to local fs (#6016) 2025-01-03 15:18:52 -05:00
tofarr
a6d392322a Fix conversation sorting and pagination (#6014)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-03 19:35:20 +00:00
Xingyao Wang
1ddf398a81 fix(microagent): remove extra unnecessary check (#6012) 2025-01-04 02:58:17 +08:00
mamoodi
4de6c782cc Add doc style guide and make docs adhere to it (#5983)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-03 12:14:14 -05:00
dependabot[bot]
9fef6f909a chore(deps): bump the version-all group across 1 directory with 5 updates (#6008)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-01-03 16:54:48 +00:00
sp.wack
ff466d0f17 fix(frontend): Prevent rendering loading spinner in chat interface too frequently (#6009) 2025-01-03 16:34:06 +00:00
sp.wack
4c59cff2a3 fix(frontend): Memoize messages (#6006) 2025-01-03 16:12:28 +00:00
dependabot[bot]
fa44bdb390 chore(deps-dev): bump chromadb from 0.6.0 to 0.6.1 in the chromadb group (#6004)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-03 17:12:08 +01:00
Xingyao Wang
dd10f37f66 chore: remove extra debugging print (#6005) 2025-01-03 16:02:48 +00:00
Robert Brennan
3b26678a77 feat(frontend): enhance GitHub repo picker with search and sorting (#5783)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-01-03 19:44:32 +04:00
Xingyao Wang
f14f75b064 feat: runtime improvements for rate-limit and 502/503/404 error (#5975) 2025-01-03 08:36:19 -07:00
Robert Brennan
ef8e04aee3 Update github microagent for draft PRs and not creating new branches/PRs (#5986)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-01-03 09:29:17 -05:00
Robert Brennan
23df4a09d2 Handle BadRequests in agent controller (#5991)
Co-authored-by: OpenHands Bot <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-03 09:29:01 -05:00
sp.wack
eb93113b7a feat(frontend): Add active status for ws (#5944) 2025-01-03 16:38:03 +04:00
Xingyao Wang
c40b0b9ae1 chore: remove extra debug print (#5994) 2025-01-03 02:57:24 +00:00
Xingyao Wang
61ebec9ff7 feat(eval): better visualization for comparing two swe-bench runs (#5993) 2025-01-03 02:36:51 +00:00
Engel Nyst
c567c11267 Enable/disable function calling by user configuration (#5992)
Co-authored-by: co <yc5@tju.edu.cn>
Co-authored-by: Cheng Yang <93481273+young010101@users.noreply.github.com>
2025-01-03 01:40:49 +01:00
Robert Brennan
e628615094 Revert "feat(config): enable/disable LLM model tools/funcs usage by config" (#5989)
Co-authored-by: tofarr <tofarr@gmail.com>
2025-01-03 00:28:07 +01:00
tofarr
50f821f9b9 Feat conversations CRUDS API (#5775)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-02 16:09:08 -07:00
Xingyao Wang
15e0a50ff4 chore: fix linter error for microagent re-structure (#5987) 2025-01-02 23:02:32 +00:00
dependabot[bot]
e52cdfd70a chore(deps): bump the version-all group with 6 updates (#5973)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-02 23:38:34 +01:00
Xingyao Wang
c1b514e9d3 refactor: restructure microagents system (#5886)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-01-03 07:13:18 +09:00
Robert Brennan
8983d719bd Support microagents in CLI and Headless (#5971)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-02 16:52:45 -05:00
Xingyao Wang
9dd5463e06 Set default value of use_microagents to False to prevent breaking eval (#5976)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-03 05:39:17 +08:00
Cheng Yang
d5b2ce18cb Test/improve config loading tests (#5399)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-02 21:32:23 +00:00
Cheng Yang
8d627e52cb feat(config): enable/disable LLM model tools/funcs usage by config (#5576)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-02 21:20:37 +00:00
Robert Brennan
9a6084c6d5 only destroy runtime if no one is active 2024-12-24 16:13:45 -05:00
Robert Brennan
30c1d032e3 move close 2024-12-24 16:12:24 -05:00
Robert Brennan
615eabe5ed add log statement 2024-12-24 16:08:16 -05:00
Robert Brennan
3ecd214d69 Merge branch 'main' into feature/runtime-manager 2024-12-24 15:54:40 -05:00
Robert Brennan
c9a6402103 add cleanup logic 2024-12-24 15:52:58 -05:00
Robert Brennan
33a1dd89e7 remove conversation logic 2024-12-24 15:48:11 -05:00
Robert Brennan
d3f726df51 change connect logic 2024-12-24 15:33:04 -05:00
Robert Brennan
333f9a5bdf fix tests 2024-12-24 15:27:57 -05:00
Robert Brennan
0d454d46f2 Revert "refactor: move runtime creation logic to RuntimeManager.get_runtime()"
This reverts commit 42730014d5.
2024-12-24 15:16:32 -05:00
Robert Brennan
e7685f185c fix cli 2024-12-24 15:15:08 -05:00
Robert Brennan
749da6367e update cli 2024-12-24 15:14:18 -05:00
Robert Brennan
4b497c8e64 fix runtime_manager plumbing 2024-12-24 15:12:15 -05:00
openhands
42730014d5 refactor: move runtime creation logic to RuntimeManager.get_runtime() 2024-12-24 19:58:14 +00:00
openhands
81110671b2 refactor: use singleton RuntimeManager from shared.py 2024-12-24 19:57:06 +00:00
openhands
25f3349e1a refactor: use RuntimeManager to get existing runtime in Conversation 2024-12-24 19:52:35 +00:00
openhands
30f6166bf6 fix: Fix async mock in test_process_issue 2024-12-24 17:55:26 +00:00
Robert Brennan
1f706fe2f2 fix test 2024-12-24 12:32:02 -05:00
Robert Brennan
4123c65317 Merge branch 'main' into feature/runtime-manager 2024-12-24 12:04:33 -05:00
Robert Brennan
6dfd54be9f fix plumbing 2024-12-24 11:53:17 -05:00
openhands
8eef9b2563 Use server's shared config for RuntimeManager 2024-12-24 16:39:27 +00:00
openhands
5d5978c6cb Move runtime class resolution to RuntimeManager and remove redundant error callback 2024-12-24 16:35:14 +00:00
openhands
1a17972b4e Move RuntimeManager to module level and simplify config handling 2024-12-24 16:30:14 +00:00
openhands
4de7a4f85d Simplify RuntimeManager config handling and fix type issues 2024-12-24 16:25:24 +00:00
openhands
8befeca41d Fix linting issues and add missing await for create_runtime 2024-12-24 16:16:16 +00:00
openhands
918139e886 Move AppConfig to RuntimeManager class level and update initialization flow 2024-12-24 16:13:09 +00:00
openhands
6374174095 Update main.py to use RuntimeManager for runtime creation 2024-12-24 16:02:47 +00:00
Robert Brennan
138f6932eb move import 2024-12-24 10:08:13 -05:00
Robert Brennan
7181efd26d move import 2024-12-24 10:07:24 -05:00
Robert Brennan
3a52360ab0 remove exit 2024-12-24 10:05:07 -05:00
openhands
cd9eb1d85c Fix singleton import and add tests for RuntimeManager 2024-12-24 15:03:10 +00:00
openhands
ada657b476 Fix linting issues in runtime_manager.py 2024-12-24 14:54:11 +00:00
openhands
b630d65626 Add RuntimeManager for centralized runtime management 2024-12-24 14:52:19 +00:00
130 changed files with 2632 additions and 922 deletions

View File

@@ -1,6 +1,7 @@
---
name: repo
agent: CodeAct
type: repo
agent: CodeActAgent
---
This repository contains the code for OpenHands, an automated AI software engineer. It has a Python backend
(in the `openhands` directory) and React frontend (in the `frontend` directory).

View File

@@ -180,6 +180,12 @@ model = "gpt-4o"
# https://docs.litellm.ai/docs/completion/token_usage
#custom_tokenizer = ""
# Whether to use native tool calling if supported by the model. Can be true, false, or None by default, which chooses the model's default behavior based on the evaluation.
# ATTENTION: Based on evaluation, enabling native function calling may lead to worse results
# in some scenarios. Use with caution and consider testing with your specific use case.
# https://github.com/All-Hands-AI/OpenHands/pull/4711
#native_tool_calling = None
[llm.gpt4o-mini]
api_key = "your-api-key"
model = "gpt-4o"

View File

@@ -71,6 +71,7 @@ ENV VIRTUAL_ENV=/app/.venv \
COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
RUN playwright install --with-deps chromium
COPY --chown=openhands:app --chmod=770 ./microagents ./microagents
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
COPY --chown=openhands:app --chmod=770 ./openhands/agenthub ./openhands/agenthub

48
docs/DOC_STYLE_GUIDE.md Normal file
View File

@@ -0,0 +1,48 @@
# Documentation Style Guide
## General Writing Principles
- **Clarity & Conciseness**: Always prioritize clarity and brevity. Avoid unnecessary jargon or overly complex explanations.
Keep sentences short and to the point.
- **Gradual Complexity**: Start with the simplest, most basic setup, and then gradually introduce more advanced
concepts and configurations.
## Formatting Guidelines
### Headers
Use **Title Case** for the first and second level headers.
Example:
- **Basic Usage**
- **Advanced Configuration Options**
### Lists
When listing items or options, use bullet points to enhance readability.
Example:
- Option A
- Option B
- Option C
### Procedures
For instructions or processes that need to be followed in a specific order, use numbered steps.
Example:
1. Step one: Do this.
2. Step two: Complete this action.
3. Step three: Verify the result.
### Code Blocks
* Use code blocks for multi-line inputs, outputs, commands and code samples.
Example:
```bash
docker run -it \
-e THIS=this \
-e THAT=that
...
```

View File

@@ -4,10 +4,9 @@
Achieving full replication of production-grade applications with LLMs is a complex endeavor. Our strategy involves:
1. **Core Technical Research:** Focusing on foundational research to understand and improve the technical aspects of code generation and handling
2. **Specialist Abilities:** Enhancing the effectiveness of core components through data curation, training methods, and more
3. **Task Planning:** Developing capabilities for bug detection, codebase management, and optimization
4. **Evaluation:** Establishing comprehensive evaluation metrics to better understand and improve our models
- **Core Technical Research:** Focusing on foundational research to understand and improve the technical aspects of code generation and handling.
- **Task Planning:** Developing capabilities for bug detection, codebase management, and optimization.
- **Evaluation:** Establishing comprehensive evaluation metrics to better understand and improve our agents.
## Default Agent
@@ -15,11 +14,14 @@ Our default Agent is currently the [CodeActAgent](agents), which is capable of g
## Built With
OpenHands is built using a combination of powerful frameworks and libraries, providing a robust foundation for its development. Here are the key technologies used in the project:
OpenHands is built using a combination of powerful frameworks and libraries, providing a robust foundation for its
development. Here are the key technologies used in the project:
![FastAPI](https://img.shields.io/badge/FastAPI-black?style=for-the-badge) ![uvicorn](https://img.shields.io/badge/uvicorn-black?style=for-the-badge) ![LiteLLM](https://img.shields.io/badge/LiteLLM-black?style=for-the-badge) ![Docker](https://img.shields.io/badge/Docker-black?style=for-the-badge) ![Ruff](https://img.shields.io/badge/Ruff-black?style=for-the-badge) ![MyPy](https://img.shields.io/badge/MyPy-black?style=for-the-badge) ![LlamaIndex](https://img.shields.io/badge/LlamaIndex-black?style=for-the-badge) ![React](https://img.shields.io/badge/React-black?style=for-the-badge)
Please note that the selection of these technologies is in progress, and additional technologies may be added or existing ones may be removed as the project evolves. We strive to adopt the most suitable and efficient tools to enhance the capabilities of OpenHands.
Please note that the selection of these technologies is in progress, and additional technologies may be added or
existing ones may be removed as the project evolves. We strive to adopt the most suitable and efficient tools to
enhance the capabilities of OpenHands.
## License

View File

@@ -11,7 +11,7 @@ take precedence.
# Table of Contents
1. [Core Configuration](#core-configuration)
- [Core Configuration](#core-configuration)
- [API Keys](#api-keys)
- [Workspace](#workspace)
- [Debugging and Logging](#debugging-and-logging)
@@ -21,7 +21,7 @@ take precedence.
- [Task Management](#task-management)
- [Sandbox Configuration](#sandbox-configuration)
- [Miscellaneous](#miscellaneous)
2. [LLM Configuration](#llm-configuration)
- [LLM Configuration](#llm-configuration)
- [AWS Credentials](#aws-credentials)
- [API Configuration](#api-configuration)
- [Custom LLM Provider](#custom-llm-provider)
@@ -30,20 +30,20 @@ take precedence.
- [Model Selection](#model-selection)
- [Retrying](#retrying)
- [Advanced Options](#advanced-options)
3. [Agent Configuration](#agent-configuration)
- [Agent Configuration](#agent-configuration)
- [Microagent Configuration](#microagent-configuration)
- [Memory Configuration](#memory-configuration)
- [LLM Configuration](#llm-configuration-2)
- [ActionSpace Configuration](#actionspace-configuration)
- [Microagent Usage](#microagent-usage)
4. [Sandbox Configuration](#sandbox-configuration-2)
- [Sandbox Configuration](#sandbox-configuration)
- [Execution](#execution)
- [Container Image](#container-image)
- [Networking](#networking)
- [Linting and Plugins](#linting-and-plugins)
- [Dependencies and Environment](#dependencies-and-environment)
- [Evaluation](#evaluation)
5. [Security Configuration](#security-configuration)
- [Security Configuration](#security-configuration)
- [Confirmation Mode](#confirmation-mode)
- [Security Analyzer](#security-analyzer)

View File

@@ -1,10 +1,14 @@
# ✅ Providing Feedback
When using OpenHands, you will encounter cases where things work well, and others where they don't. We encourage you to provide feedback when you use OpenHands to help give feedback to the development team, and perhaps more importantly, create an open corpus of coding agent training examples -- Share-OpenHands!
When using OpenHands, you will encounter cases where things work well, and others where they don't. We encourage you to
provide feedback when you use OpenHands to help give feedback to the development team, and perhaps more importantly,
create an open corpus of coding agent training examples -- Share-OpenHands!
## 📝 How to Provide Feedback
Providing feedback is easy! When you are using OpenHands, you can press the thumbs-up or thumbs-down button at any point during your interaction. You will be prompted to provide your email address (e.g. so we can contact you if we want to ask any follow-up questions), and you can choose whether you want to provide feedback publicly or privately.
Providing feedback is easy! When you are using OpenHands, you can press the thumbs-up or thumbs-down button at any point
during your interaction. You will be prompted to provide your email address
(e.g. so we can contact you if we want to ask any follow-up questions), and you can choose whether you want to provide feedback publicly or privately.
<iframe width="560" height="315" src="https://www.youtube.com/embed/5rFx-StMVV0?si=svo7xzp6LhGK_GXr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
@@ -14,8 +18,11 @@ Providing feedback is easy! When you are using OpenHands, you can press the thum
When you submit data, you can submit it either publicly or privately.
* **Public** data will be distributed under the MIT License, like OpenHands itself, and can be used by the community to train and test models. Obviously, feedback that you can make public will be more valuable for the community as a whole, so when you are not dealing with sensitive information, we would encourage you to choose this option!
* **Private** data will only be shared with the OpenHands team for the purpose of improving OpenHands.
- **Public** data will be distributed under the MIT License, like OpenHands itself, and can be used by the community to
train and test models. Obviously, feedback that you can make public will be more valuable for the community as a whole,
so when you are not dealing with sensitive information, we would encourage you to choose this option!
- **Private** data will be made available to the OpenHands team for the purpose of improving OpenHands.
However, a link with a unique ID will still be created that you can share publicly with others.
### Who collects and stores the data?
@@ -27,13 +34,17 @@ The public data will be released when we hit fixed milestones, such as 1,000 pub
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.
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.
### What if I want my data deleted?
For data on the All Hands AI servers, we are happy to delete it at request:
**One Piece of Data:** If you want one piece of data deleted, we will shortly be adding a mechanism to delete pieces of data using the link and password that is displayed on the interface when you submit data.
**One Piece of Data:** If you want one piece of data deleted, we will shortly be adding a mechanism to delete pieces of
data using the link and password that is displayed on the interface when you submit data.
**All Data:** If you would like all pieces of your data deleted, or you do not have the ID and password that you received when submitting the data, please contact `contact@all-hands.dev` from the email address that you registered when you originally submitted the data.
**All Data:** If you would like all pieces of your data deleted, or you do not have the ID and password that you
received when submitting the data, please contact `contact@all-hands.dev` from the email address that you registered
when you originally submitted the data.

View File

@@ -44,7 +44,7 @@ For example, we might build a TODO app:
We can keep iterating on the app once the skeleton is there:
> Please allow adding an optional due date to every task
> Please allow adding an optional due date to every task.
Just like with normal development, it's good to commit and push your code frequently.
This way you can always revert back to an old state if the agent goes off track.
@@ -59,15 +59,15 @@ OpenHands can also do a great job adding new code to an existing code base.
For example, you can ask OpenHands to add a new GitHub action to your project
which lints your code. OpenHands may take a peek at your codebase to see what language
it should use, but then it can just drop a new file into `./github/workflows/lint.yml`
it should use and then drop a new file into `./github/workflows/lint.yml`.
> Please add a GitHub action that lints the code in this repository
> Please add a GitHub action that lints the code in this repository.
Some tasks might require a bit more context. While OpenHands can use `ls` and `grep`
to search through your codebase, providing context up front allows it to move faster,
and more accurately. And it'll cost you fewer tokens!
> Please modify ./backend/api/routes.js to add a new route that returns a list of all tasks
> Please modify ./backend/api/routes.js to add a new route that returns a list of all tasks.
> Please add a new React component that displays a list of Widgets to the ./frontend/components
> directory. It should use the existing Widget component.
@@ -78,15 +78,15 @@ OpenHands does great at refactoring existing code, especially in small chunks.
You probably don't want to try rearchitecting your whole codebase, but breaking up
long files and functions, renaming variables, etc. tend to work very well.
> Please rename all the single-letter variables in ./app.go
> Please rename all the single-letter variables in ./app.go.
> Please break the function `build_and_deploy_widgets` into two functions, `build_widgets` and `deploy_widgets` in widget.php
> Please break the function `build_and_deploy_widgets` into two functions, `build_widgets` and `deploy_widgets` in widget.php.
> Please break ./api/routes.js into separate files for each route
> Please break ./api/routes.js into separate files for each route.
## Bug Fixes
OpenHands can also help you track down and fix bugs in your code. But, as any
OpenHands can also help you track down and fix bugs in your code. But as any
developer knows, bug fixing can be extremely tricky, and often OpenHands will need more context.
It helps if you've diagnosed the bug, but want OpenHands to figure out the logic.
@@ -94,18 +94,18 @@ It helps if you've diagnosed the bug, but want OpenHands to figure out the logic
> The `search_widgets` function in ./app.py is doing a case-sensitive search. Please make it case-insensitive.
It often helps to do test-driven development when bugfixing with an agent.
It often helps to do test-driven development when bug fixing with an agent.
You can ask the agent to write a new test, and then iterate until it fixes the bug:
> The `hello` function crashes on the empty string. Please write a test that reproduces this bug, then fix the code so it passes.
## More
OpenHands is capable of helping out on just about any coding task. But it takes some practice
OpenHands is capable of helping out on just about any coding task but it takes some practice
to get the most out of it. Remember to:
* Keep your tasks small
* Be as specific as possible
* Provide as much context as possible
* Commit and push frequently
* Keep your tasks small.
* Be as specific as possible.
* Provide as much context as possible.
* Commit and push frequently.
See [Prompting Best Practices](./prompting/prompting-best-practices) for more tips on how to get the most out of OpenHands.

View File

@@ -26,9 +26,9 @@ To run OpenHands in CLI mode with Docker:
1. Set the following environmental variables in your terminal:
* `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
* `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
* `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
- `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
- `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
- `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
2. Run the following Docker command:

View File

@@ -9,8 +9,8 @@ as python and Node.js but may need other software installed by default.
You have two options for customization:
1. Use an existing image with the required software.
2. Create your own custom Docker image.
- Use an existing image with the required software.
- Create your own custom Docker image.
If you choose the first option, you can skip the `Create Your Docker Image` section.
@@ -58,7 +58,3 @@ sandbox_base_container_image="custom-image"
### Run
Run OpenHands by running ```make run``` in the top level directory.
## Technical Explanation
Please refer to [custom docker image section of the runtime documentation](https://docs.all-hands.dev/modules/usage/architecture/runtime#advanced-how-openhands-builds-and-maintains-od-runtime-images) for more details.

View File

@@ -21,10 +21,10 @@ the [README for the OpenHands Resolver](https://github.com/All-Hands-AI/OpenHand
### Iterative resolution
1. Create an issue in the repository.
2. Add the `fix-me` label to the issue, or leave a comment starting with `@openhands-agent`
3. Review the attempt to resolve the issue by checking the pull request
4. Follow up with feedback through general comments, review comments, or inline thread comments
5. Add the `fix-me` label to the pull request, or address a specific comment by starting with `@openhands-agent`
2. Add the `fix-me` label to the issue, or leave a comment starting with `@openhands-agent`.
3. Review the attempt to resolve the issue by checking the pull request.
4. Follow up with feedback through general comments, review comments, or inline thread comments.
5. Add the `fix-me` label to the pull request, or address a specific comment by starting with `@openhands-agent`.
### Label versus Macro

View File

@@ -2,12 +2,12 @@
## Introduction
OpenHands provides a user-friendly Graphical User Interface (GUI) mode for interacting with the AI assistant. This mode offers an intuitive way to set up the environment, manage settings, and communicate with the AI.
OpenHands provides a user-friendly Graphical User Interface (GUI) mode for interacting with the AI assistant.
This mode offers an intuitive way to set up the environment, manage settings, and communicate with the AI.
## Installation and Setup
1. Follow the instructions in the [Installation](../installation) guide to install OpenHands.
2. After running the command, access OpenHands at [http://localhost:3000](http://localhost:3000).
## Interacting with the GUI
@@ -23,39 +23,39 @@ OpenHands provides a user-friendly Graphical User Interface (GUI) mode for inter
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
1. **Locally (OSS)**: The user directly inputs their GitHub token
2. **Online (SaaS)**: The token is obtained through GitHub OAuth authentication
- **Locally (OSS)**: The user directly inputs their GitHub token.
- **Online (SaaS)**: The token is obtained through GitHub OAuth authentication.
#### Setting Up a Local GitHub Token
1. **Generate a Personal Access Token (PAT)**:
- Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic)
- Click "Generate new token (classic)"
- Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
- Click "Generate new token (classic)".
- Required scopes:
- `repo` (Full control of private repositories)
- `workflow` (Update GitHub Action workflows)
- `read:org` (Read organization data)
2. **Enter Token in OpenHands**:
- Click the Settings button (gear icon) in the top right
- Navigate to the "GitHub" section
- Paste your token in the "GitHub Token" field
- Click "Save" to apply the changes
- Click the Settings button (gear icon) in the top right.
- Navigate to the "GitHub" section.
- Paste your token in the "GitHub Token" field.
- Click "Save" to apply the changes.
#### Organizational Token Policies
If you're working with organizational repositories, additional setup may be required:
1. **Check Organization Requirements**:
- Organization admins may enforce specific token policies
- Some organizations require tokens to be created with SSO enabled
- Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization)
- Organization admins may enforce specific token policies.
- Some organizations require tokens to be created with SSO enabled.
- Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization).
2. **Verify Organization Access**:
- Go to your token settings on GitHub
- Look for the organization under "Organization access"
- If required, click "Enable SSO" next to your organization
- Complete the SSO authorization process
- Go to your token settings on GitHub.
- Look for the organization under "Organization access".
- If required, click "Enable SSO" next to your organization.
- Complete the SSO authorization process.
#### OAuth Authentication (Online Mode)
@@ -67,31 +67,31 @@ When using OpenHands in online mode, the GitHub OAuth flow:
- Organization read access
2. Authentication steps:
- Click "Sign in with GitHub" when prompted
- Review the requested permissions
- Authorize OpenHands to access your GitHub account
- If using an organization, authorize organization access if prompted
- Click "Sign in with GitHub" when prompted.
- Review the requested permissions.
- Authorize OpenHands to access your GitHub account.
- If using an organization, authorize organization access if prompted.
#### Troubleshooting
Common issues and solutions:
1. **Token Not Recognized**:
- Ensure the token is properly saved in settings
- Check that the token hasn't expired
- Verify the token has the required scopes
- Try regenerating the token
- Ensure the token is properly saved in settings.
- Check that the token hasn't expired.
- Verify the token has the required scopes.
- Try regenerating the token.
2. **Organization Access Denied**:
- Check if SSO is required but not enabled
- Verify organization membership
- Contact organization admin if token policies are blocking access
- Check if SSO is required but not enabled.
- Verify organization membership.
- Contact organization admin if token policies are blocking access.
3. **Verifying Token Works**:
- The app will show a green checkmark if the token is valid
- Try accessing a repository to confirm permissions
- Check the browser console for any error messages
- Use the "Test Connection" button in settings if available
- The app will show a green checkmark if the token is valid.
- Try accessing a repository to confirm permissions.
- Check the browser console for any error messages.
- Use the "Test Connection" button in settings if available.
### Advanced Settings
@@ -103,11 +103,11 @@ Common issues and solutions:
The main interface consists of several key components:
1. **Chat Window**: The central area where you can view the conversation history with the AI assistant.
2. **Input Box**: Located at the bottom of the screen, use this to type your messages or commands to the AI.
3. **Send Button**: Click this to send your message to the AI.
4. **Settings Button**: A gear icon that opens the settings modal, allowing you to adjust your configuration at any time.
5. **Workspace Panel**: Displays the files and folders in your workspace, allowing you to navigate and view files, or the agent's past commands or web browsing history.
- **Chat Window**: The central area where you can view the conversation history with the AI assistant.
- **Input Box**: Located at the bottom of the screen, use this to type your messages or commands to the AI.
- **Send Button**: Click this to send your message to the AI.
- **Settings Button**: A gear icon that opens the settings modal, allowing you to adjust your configuration at any time.
- **Workspace Panel**: Displays the files and folders in your workspace, allowing you to navigate and view files, or the agent's past commands or web browsing history.
### Interacting with the AI
@@ -118,8 +118,9 @@ The main interface consists of several key components:
## Tips for Effective Use
1. Be specific in your requests to get the most accurate and helpful responses, as described in the [prompting best practices](../prompting/prompting-best-practices).
2. Use the workspace panel to explore your project structure.
3. Use one of the recommended models, as described in the [LLMs section](usage/llms/llms.md).
- Be specific in your requests to get the most accurate and helpful responses, as described in the [prompting best practices](../prompting/prompting-best-practices).
- Use the workspace panel to explore your project structure.
- Use one of the recommended models, as described in the [LLMs section](usage/llms/llms.md).
Remember, the GUI mode of OpenHands is designed to make your interaction with the AI assistant as smooth and intuitive as possible. Don't hesitate to explore its features to maximize your productivity.
Remember, the GUI mode of OpenHands is designed to make your interaction with the AI assistant as smooth and intuitive
as possible. Don't hesitate to explore its features to maximize your productivity.

View File

@@ -23,9 +23,9 @@ To run OpenHands in Headless mode with Docker:
1. Set the following environmental variables in your terminal:
* `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
* `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
* `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
- `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
- `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
- `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
2. Run the following Docker command:
@@ -53,4 +53,4 @@ To view all available configuration options for headless mode, run the Python co
### Additional Logs
For the headless mode to log all the agent actions, in your terminal run: `export LOG_ALL_EVENTS=true`
For the headless mode to log all the agent actions, in the terminal run: `export LOG_ALL_EVENTS=true`

View File

@@ -1,6 +1,6 @@
# Persisting Session Data
Using the standard installation, the session data is stored in memory. Currently, if OpenHands' service is restarted,
Using the standard Development Workflow, the session data is stored in memory. Currently, if OpenHands' service is restarted,
previous sessions become invalid (a new secret is generated) and thus not recoverable.
## How to Persist Session Data

View File

@@ -2,9 +2,9 @@
## System Requirements
* Docker version 26.0.0+ or Docker Desktop 4.31.0+.
* You must be using Linux or Mac OS.
* If you are on Windows, you must use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
- Docker version 26.0.0+ or Docker Desktop 4.31.0+.
- You must be using Linux or Mac OS.
- If you are on Windows, you must use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
## Start the app
@@ -33,8 +33,6 @@ or run it on tagged issues with [a github action](https://docs.all-hands.dev/mod
## Setup
After running the command above, you'll find OpenHands running at [http://localhost:3000](http://localhost:3000).
Upon launching OpenHands, you'll see a settings modal. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
These can be changed at any time by selecting the `Settings` button (gear icon) in the UI.

View File

@@ -18,17 +18,18 @@ docker run -it --pull=always \
...
```
Then set the following in the OpenHands UI through the Settings:
Then in the OpenHands UI Settings:
:::note
You will need your ChatGPT deployment name which can be found on the deployments page in Azure. This is referenced as
&lt;deployment-name&gt; below.
:::
* Enable `Advanced Options`
* `Custom Model` to azure/&lt;deployment-name&gt;
* `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)
* `API Key` to your Azure API key
1. Enable `Advanced Options`
2. Set the following:
- `Custom Model` to azure/&lt;deployment-name&gt;
- `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)
- `API Key` to your Azure API key
## Embeddings

View File

@@ -8,10 +8,10 @@ OpenHands uses LiteLLM to make calls to Google's chat models. You can find their
## Gemini - Google AI Studio Configs
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
* `LLM Provider` to `Gemini`
* `LLM Model` to the model you will be using.
- `LLM Provider` to `Gemini`
- `LLM Model` to the model you will be using.
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. gemini/&lt;model-name&gt; like `gemini/gemini-1.5-pro`).
* `API Key` to your Gemini API key
- `API Key` to your Gemini API key
## VertexAI - Google Cloud Platform Configs
@@ -25,6 +25,6 @@ VERTEXAI_LOCATION="<your-gcp-location>"
```
Then set the following in the OpenHands UI through the Settings:
* `LLM Provider` to `VertexAI`
* `LLM Model` to the model you will be using.
- `LLM Provider` to `VertexAI`
- `LLM Model` to the model you will be using.
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. vertex_ai/&lt;model-name&gt;).

View File

@@ -5,19 +5,20 @@ OpenHands uses LiteLLM to make calls to chat models on Groq. You can find their
## Configuration
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
* `LLM Provider` to `Groq`
* `LLM Model` to the model you will be using. [Visit here to see the list of
- `LLM Provider` to `Groq`
- `LLM Model` to the model you will be using. [Visit here to see the list of
models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list, toggle
`Advanced Options`, and enter it in `Custom Model` (e.g. groq/&lt;model-name&gt; like `groq/llama3-70b-8192`).
* `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys).
- `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys).
## Using Groq as an OpenAI-Compatible Endpoint
The Groq endpoint for chat completion is [mostly OpenAI-compatible](https://console.groq.com/docs/openai). Therefore, you can access Groq models as you
would access any OpenAI-compatible endpoint. You can set the following in the OpenHands UI through the Settings:
* Enable `Advanced Options`
* `Custom Model` to the prefix `openai/` + the model you will be using (e.g. `openai/llama3-70b-8192`)
* `Base URL` to `https://api.groq.com/openai/v1`
* `API Key` to your Groq API key
would access any OpenAI-compatible endpoint. In the OpenHands UI through the Settings:
1. Enable `Advanced Options`
2. Set the following:
- `Custom Model` to the prefix `openai/` + the model you will be using (e.g. `openai/llama3-70b-8192`)
- `Base URL` to `https://api.groq.com/openai/v1`
- `API Key` to your Groq API key

View File

@@ -4,7 +4,9 @@ OpenHands can connect to any LLM supported by LiteLLM. However, it requires a po
## Model Recommendations
Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some recommendations for model selection. Some analyses can be found in [this blog article comparing LLMs](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) and [this blog article with some more recent results](https://www.all-hands.dev/blog/openhands-codeact-21-an-open-state-of-the-art-software-development-agent).
Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some
recommendations for model selection. Some analyses can be found in [this blog article comparing LLMs](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) and
[this blog article with some more recent results](https://www.all-hands.dev/blog/openhands-codeact-21-an-open-state-of-the-art-software-development-agent).
When choosing a model, consider both the quality of outputs and the associated costs. Here's a summary of the findings:
@@ -69,9 +71,11 @@ We have a few guides for running OpenHands with specific model providers:
### API retries and rate limits
LLM providers typically have rate limits, sometimes very low, and may require retries. OpenHands will automatically retry requests if it receives a Rate Limit Error (429 error code), API connection error, or other transient errors.
LLM providers typically have rate limits, sometimes very low, and may require retries. OpenHands will automatically
retry requests if it receives a Rate Limit Error (429 error code), API connection error, or other transient errors.
You can customize these options as you need for the provider you're using. Check their documentation, and set the following environment variables to control the number of retries and the time between retries:
You can customize these options as you need for the provider you're using. Check their documentation, and set the
following environment variables to control the number of retries and the time between retries:
- `LLM_NUM_RETRIES` (Default of 8)
- `LLM_RETRY_MIN_WAIT` (Default of 15 seconds)

View File

@@ -17,8 +17,9 @@ Just as for OpenAI Chat completions, we use LiteLLM for OpenAI-compatible endpoi
## Using an OpenAI Proxy
If you're using an OpenAI proxy, you'll need to set the following in the OpenHands UI through the Settings:
* Enable `Advanced Options`
* `Custom Model` to openai/&lt;model-name&gt; (e.g. `openai/gpt-4o` or openai/&lt;proxy-prefix&gt;/&lt;model-name&gt;)
* `Base URL` to the URL of your OpenAI proxy
* `API Key` to your OpenAI API key
If you're using an OpenAI proxy, in the OpenHands UI through the Settings:
1. Enable `Advanced Options`
2. Set the following:
- `Custom Model` to openai/&lt;model-name&gt; (e.g. `openai/gpt-4o` or openai/&lt;proxy-prefix&gt;/&lt;model-name&gt;)
- `Base URL` to the URL of your OpenAI proxy
- `API Key` to your OpenAI API key

View File

@@ -9,11 +9,11 @@ You can customize OpenHands' behavior for your repository by creating a `.openha
be given to the agent every time it works with this repository.
We suggest including the following information:
1. **Repository Overview**: A brief description of your project's purpose and architecture
2. **Directory Structure**: Key directories and their purposes
3. **Development Guidelines**: Project-specific coding standards and practices
4. **Testing Requirements**: How to run tests and what types of tests are required
5. **Setup Instructions**: Steps needed to build and run the project
- **Repository Overview**: A brief description of your project's purpose and architecture.
- **Directory Structure**: Key directories and their purposes.
- **Development Guidelines**: Project-specific coding standards and practices.
- **Testing Requirements**: How to run tests and what types of tests are required.
- **Setup Instructions**: Steps needed to build and run the project.
### Example Repository Configuration
Example `.openhands/microagents/repo.md` file:
@@ -39,11 +39,11 @@ Guidelines:
### Customizing Prompts
When working with a customized repository:
When working with a repository:
1. **Reference Project Standards**: Mention specific coding standards or patterns used in your project
2. **Include Context**: Reference relevant documentation or existing implementations
3. **Specify Testing Requirements**: Include project-specific testing requirements in your prompts
- **Reference Project Standards**: Mention specific coding standards or patterns used in your project.
- **Include Context**: Reference relevant documentation or existing implementations.
- **Specify Testing Requirements**: Include project-specific testing requirements in your prompts.
Example customized prompt:
```
@@ -54,14 +54,14 @@ The component should use our shared styling from src/styles/components.
### Best Practices for Repository Customization
1. **Keep Instructions Updated**: Regularly update your `.openhands` directory as your project evolves
2. **Be Specific**: Include specific paths, patterns, and requirements unique to your project
3. **Document Dependencies**: List all tools and dependencies required for development
4. **Include Examples**: Provide examples of good code patterns from your project
5. **Specify Conventions**: Document naming conventions, file organization, and code style preferences
- **Keep Instructions Updated**: Regularly update your `.openhands` directory as your project evolves.
- **Be Specific**: Include specific paths, patterns, and requirements unique to your project.
- **Document Dependencies**: List all tools and dependencies required for development.
- **Include Examples**: Provide examples of good code patterns from your project.
- **Specify Conventions**: Document naming conventions, file organization, and code style preferences.
By customizing OpenHands for your repository, you'll get more accurate and consistent results that align with your project's standards and requirements.
## Other Microagents
You can create other instructions in the `.openhands/microagents/` directory
that will be sent to the agent if a particular keyword is found, like `test`, `frontend`, or `migration`. See [Microagents](microagents.md) for more information.
that will be sent to the agent if a particular keyword is found, like `test`, `frontend`, or `migration`. See [Micro-Agents](microagents.md) for more information.

View File

@@ -6,10 +6,10 @@ OpenHands uses specialized micro-agents to handle specific tasks and contexts ef
Micro-agents are defined in markdown files under the `openhands/agenthub/codeact_agent/micro/` directory. Each micro-agent is configured with:
- A unique name
- The agent type (typically CodeActAgent)
- Trigger keywords that activate the agent
- Specific instructions and capabilities
- A unique name.
- The agent type (typically CodeActAgent).
- Trigger keywords that activate the agent.
- Specific instructions and capabilities.
## Available Micro-Agents
@@ -18,10 +18,10 @@ Micro-agents are defined in markdown files under the `openhands/agenthub/codeact
**Triggers**: `github`, `git`
The GitHub agent specializes in GitHub API interactions and repository management. It:
- Has access to a `GITHUB_TOKEN` for API authentication
- Follows strict guidelines for repository interactions
- Handles branch management and pull requests
- Uses the GitHub API instead of web browser interactions
- Has access to a `GITHUB_TOKEN` for API authentication.
- Follows strict guidelines for repository interactions.
- Handles branch management and pull requests.
- Uses the GitHub API instead of web browser interactions.
Key features:
- Branch protection (prevents direct pushes to main/master)
@@ -34,13 +34,14 @@ Key features:
**Triggers**: `npm`
Specializes in handling npm package management with specific focus on:
- Non-interactive shell operations
- Automated confirmation handling using Unix 'yes' command
- Package installation automation
- Non-interactive shell operations.
- Automated confirmation handling using Unix 'yes' command.
- Package installation automation.
### Custom Micro-Agents
You can create your own micro-agents by adding new markdown files to the micro-agents directory. Each file should follow this structure:
You can create your own micro-agents by adding new markdown files to the micro-agents directory.
Each file should follow this structure:
```markdown
---
@@ -57,19 +58,18 @@ Instructions and capabilities for the micro-agent...
## Best Practices
When working with micro-agents:
1. **Use Appropriate Triggers**: Ensure your commands include the relevant trigger words to activate the correct micro-agent
2. **Follow Agent Guidelines**: Each agent has specific instructions and limitations - respect these for optimal results
3. **API-First Approach**: When available, use API endpoints rather than web interfaces
4. **Automation Friendly**: Design commands that work well in non-interactive environments
- **Use Appropriate Triggers**: Ensure your commands include the relevant trigger words to activate the correct micro-agent.
- **Follow Agent Guidelines**: Each agent has specific instructions and limitations. Respect these for optimal results.
- **API-First Approach**: When available, use API endpoints rather than web interfaces.
- **Automation Friendly**: Design commands that work well in non-interactive environments.
## Integration
Micro-agents are automatically integrated into OpenHands' workflow. They:
- Monitor incoming commands for their trigger words
- Activate when relevant triggers are detected
- Apply their specialized knowledge and capabilities
- Follow their specific guidelines and restrictions
- Monitor incoming commands for their trigger words.
- Activate when relevant triggers are detected.
- Apply their specialized knowledge and capabilities.
- Follow their specific guidelines and restrictions.
## Example Usage
@@ -105,7 +105,7 @@ Create a new markdown file in `openhands/agenthub/codeact_agent/micro/` with a d
Your micro-agent file must include:
1. **Front Matter**: YAML metadata at the start of the file:
- **Front Matter**: YAML metadata at the start of the file:
```markdown
---
name: your_agent_name
@@ -116,7 +116,7 @@ triggers:
---
```
2. **Instructions**: Clear, specific guidelines for the agent's behavior:
- **Instructions**: Clear, specific guidelines for the agent's behavior:
```markdown
You are responsible for [specific task/domain].
@@ -135,19 +135,19 @@ Examples of usage:
### 4. Best Practices for Micro-Agent Development
1. **Clear Scope**: Keep the agent focused on a specific domain or task
2. **Explicit Instructions**: Provide clear, unambiguous guidelines
3. **Useful Examples**: Include practical examples of common use cases
4. **Safety First**: Include necessary warnings and constraints
5. **Integration Awareness**: Consider how the agent interacts with other components
- **Clear Scope**: Keep the agent focused on a specific domain or task.
- **Explicit Instructions**: Provide clear, unambiguous guidelines.
- **Useful Examples**: Include practical examples of common use cases.
- **Safety First**: Include necessary warnings and constraints.
- **Integration Awareness**: Consider how the agent interacts with other components.
### 5. Testing Your Micro-Agent
Before submitting:
1. Test the agent with various prompts
2. Verify trigger words activate the agent correctly
3. Ensure instructions are clear and comprehensive
4. Check for potential conflicts with existing agents
- Test the agent with various prompts.
- Verify trigger words activate the agent correctly.
- Ensure instructions are clear and comprehensive.
- Check for potential conflicts with existing agents.
### 6. Example Implementation
@@ -199,11 +199,12 @@ Remember to:
### 7. Submission Process
1. Create your micro-agent file in the correct directory
2. Test thoroughly
1. Create your micro-agent file in the correct directory.
2. Test thoroughly.
3. Submit a pull request with:
- The new micro-agent file
- Updated documentation if needed
- Description of the agent's purpose and capabilities
- The new micro-agent file.
- Updated documentation if needed.
- Description of the agent's purpose and capabilities.
Remember that micro-agents are a powerful way to extend OpenHands' capabilities in specific domains. Well-designed agents can significantly improve the system's ability to handle specialized tasks.
Remember that micro-agents are a powerful way to extend OpenHands' capabilities in specific domains. Well-designed
agents can significantly improve the system's ability to handle specialized tasks.

View File

@@ -6,35 +6,31 @@ When working with OpenHands AI software developer, it's crucial to provide clear
Good prompts are:
1. **Concrete**: They explain exactly what functionality should be added or what error needs to be fixed.
2. **Location-specific**: If known, they explain the locations in the code base that should be modified.
3. **Appropriately scoped**: They should be the size of a single feature, typically not exceeding 100 lines of code.
- **Concrete**: They explain exactly what functionality should be added or what error needs to be fixed.
- **Location-specific**: If known, they explain the locations in the code base that should be modified.
- **Appropriately scoped**: They should be the size of a single feature, typically not exceeding 100 lines of code.
## Examples
### Good Prompt Examples
1. "Add a function `calculate_average` in `utils/math_operations.py` that takes a list of numbers as input and returns their average."
2. "Fix the TypeError in `frontend/src/components/UserProfile.tsx` occurring on line 42. The error suggests we're trying to access a property of undefined."
3. "Implement input validation for the email field in the registration form. Update `frontend/src/components/RegistrationForm.tsx` to check if the email is in a valid format before submission."
- "Add a function `calculate_average` in `utils/math_operations.py` that takes a list of numbers as input and returns their average."
- "Fix the TypeError in `frontend/src/components/UserProfile.tsx` occurring on line 42. The error suggests we're trying to access a property of undefined."
- "Implement input validation for the email field in the registration form. Update `frontend/src/components/RegistrationForm.tsx` to check if the email is in a valid format before submission."
### Bad Prompt Examples
1. "Make the code better." (Too vague, not concrete)
2. "Rewrite the entire backend to use a different framework." (Not appropriately scoped)
3. "There's a bug somewhere in the user authentication. Can you find and fix it?" (Lacks specificity and location information)
- "Make the code better." (Too vague, not concrete)
- "Rewrite the entire backend to use a different framework." (Not appropriately scoped)
- "There's a bug somewhere in the user authentication. Can you find and fix it?" (Lacks specificity and location information)
## Tips for Effective Prompting
1. Be as specific as possible about the desired outcome or the problem to be solved.
2. Provide context, including relevant file paths and line numbers if available.
3. Break down large tasks into smaller, manageable prompts.
4. Include any relevant error messages or logs.
5. Specify the programming language or framework if it's not obvious from the context.
- Be as specific as possible about the desired outcome or the problem to be solved.
- Provide context, including relevant file paths and line numbers if available.
- Break down large tasks into smaller, manageable prompts.
- Include any relevant error messages or logs.
- Specify the programming language or framework if it's not obvious from the context.
Remember, the more precise and informative your prompt is, the better the AI can assist you in developing or modifying the OpenHands software.

View File

@@ -26,30 +26,29 @@ that contains our Runtime server, as well as some basic utilities for Python and
You can also [build your own runtime image](how-to/custom-sandbox-guide).
### Connecting to Your filesystem
One useful feature here is the ability to connect to your local filesystem.
One useful feature here is the ability to connect to your local filesystem. To mount your filesystem into the runtime:
1. Set `WORKSPACE_BASE`:
To mount your filesystem into the runtime, first set WORKSPACE_BASE:
```bash
export WORKSPACE_BASE=/path/to/your/code
```bash
export WORKSPACE_BASE=/path/to/your/code
# Linux and Mac Example
# export WORKSPACE_BASE=$HOME/OpenHands
# Will set $WORKSPACE_BASE to /home/<username>/OpenHands
#
# WSL on Windows Example
# export WORKSPACE_BASE=/mnt/c/dev/OpenHands
# Will set $WORKSPACE_BASE to C:\dev\OpenHands
```
# Linux and Mac Example
# export WORKSPACE_BASE=$HOME/OpenHands
# Will set $WORKSPACE_BASE to /home/<username>/OpenHands
#
# WSL on Windows Example
# export WORKSPACE_BASE=/mnt/c/dev/OpenHands
# Will set $WORKSPACE_BASE to C:\dev\OpenHands
```
2. Add the following options to the `docker run` command:
then add the following options to the `docker run` command:
```bash
docker run # ...
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-v $WORKSPACE_BASE:/opt/workspace_base \
# ...
```
```bash
docker run # ...
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-v $WORKSPACE_BASE:/opt/workspace_base \
# ...
```
Be careful! There's nothing stopping the OpenHands agent from deleting or modifying
any files that are mounted into its workspace.
@@ -59,7 +58,7 @@ but seems to work well on most systems.
## 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#-join-our-community) for an invite).
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).
To use the All Hands Runtime, set the following environment variables when
starting OpenHands:

View File

@@ -75,6 +75,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -59,6 +59,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -67,6 +67,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
# copy 'draft_editor' config if exists
config_copy = copy.deepcopy(config)

View File

@@ -73,6 +73,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -86,6 +86,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -50,6 +50,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -77,6 +77,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config = AgentConfig(
function_calling=False,
codeact_enable_jupyter=True,

View File

@@ -62,6 +62,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -55,6 +55,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -76,6 +76,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -97,6 +97,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -61,6 +61,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -119,6 +119,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -92,6 +92,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -1,13 +1,20 @@
#!/usr/bin/env python3
import argparse
import os
import pandas as pd
from termcolor import colored
parser = argparse.ArgumentParser(
description='Compare two swe_bench output JSONL files and print the resolved diff'
)
parser.add_argument('input_file_1', type=str)
parser.add_argument('input_file_2', type=str)
parser.add_argument(
'--show-paths',
action='store_true',
help='Show visualization paths for failed instances',
)
args = parser.parse_args()
df1 = pd.read_json(args.input_file_1, orient='records', lines=True)
@@ -58,10 +65,60 @@ df_diff_y_only = df_diff[~df_diff['resolved_x'] & df_diff['resolved_y']].sort_va
print(f'# y resolved but x not={df_diff_y_only.shape[0]}')
print(df_diff_y_only[['instance_id', 'report_x', 'report_y']])
# get instance_id from df_diff_y_only
print('-' * 100)
print('Instances that x resolved but y not:')
print(df_diff_x_only['instance_id'].tolist())
x_only_by_repo = {}
for instance_id in df_diff_x_only['instance_id'].tolist():
repo = instance_id.split('__')[0]
x_only_by_repo.setdefault(repo, []).append(instance_id)
y_only_by_repo = {}
for instance_id in df_diff_y_only['instance_id'].tolist():
repo = instance_id.split('__')[0]
y_only_by_repo.setdefault(repo, []).append(instance_id)
print('-' * 100)
print('Instances that y resolved but x not:')
print(df_diff_y_only['instance_id'].tolist())
print(
colored('Repository comparison (x resolved vs y resolved):', 'cyan', attrs=['bold'])
)
all_repos = sorted(set(list(x_only_by_repo.keys()) + list(y_only_by_repo.keys())))
# Calculate diffs and sort repos by diff magnitude
repo_diffs = []
for repo in all_repos:
x_count = len(x_only_by_repo.get(repo, []))
y_count = len(y_only_by_repo.get(repo, []))
diff = abs(x_count - y_count)
repo_diffs.append((repo, diff))
# Sort by diff (descending) and then by repo name
repo_diffs.sort(key=lambda x: (-x[1], x[0]))
threshold = max(
3, sum(d[1] for d in repo_diffs) / len(repo_diffs) * 1.5 if repo_diffs else 0
)
x_input_file_folder = os.path.join(os.path.dirname(args.input_file_1), 'output.viz')
for repo, diff in repo_diffs:
x_instances = x_only_by_repo.get(repo, [])
y_instances = y_only_by_repo.get(repo, [])
# Determine if this repo has a significant diff
is_significant = diff >= threshold
repo_color = 'red' if is_significant else 'yellow'
print(colored(f'Difference: {diff} instances!', repo_color, attrs=['bold']))
print(f"\n{colored(repo, repo_color, attrs=['bold'])}:")
print(colored(f'X resolved but Y failed: ({len(x_instances)} instances)', 'green'))
if x_instances:
print(' ' + str(x_instances))
print(colored(f'Y resolved but X failed: ({len(y_instances)} instances)', 'red'))
if y_instances:
print(' ' + str(y_instances))
if args.show_paths:
print(
colored(' Visualization path for X failed:', 'cyan', attrs=['bold'])
)
for instance_id in y_instances:
instance_file = os.path.join(
x_input_file_folder, f'false.{instance_id}.md'
)
print(f' {instance_file}')

View File

@@ -56,6 +56,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -77,6 +77,8 @@ def get_config(
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
return config

View File

@@ -376,7 +376,12 @@ def _process_instance_wrapper(
+ '\n'
)
if isinstance(
e, (AgentRuntimeDisconnectedError, AgentRuntimeUnavailableError)
e,
(
AgentRuntimeDisconnectedError,
AgentRuntimeUnavailableError,
AgentRuntimeNotFoundError,
),
):
runtime_failure_count += 1
msg += f'Runtime disconnected error detected for instance {instance.instance_id}, runtime failure count: {runtime_failure_count}'

View File

@@ -73,6 +73,7 @@
}
}
],
"react/prop-types": "off",
"react/no-array-index-key": "off",
"react-hooks/exhaustive-deps": "off",
"import/no-extraneous-dependencies": "off",

View File

@@ -0,0 +1,76 @@
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { GitHubRepositorySelector } from "#/components/features/github/github-repo-selector";
import OpenHands from "#/api/open-hands";
import * as GitHubAPI from "#/api/github";
describe("GitHubRepositorySelector", () => {
const onInputChangeMock = vi.fn();
const onSelectMock = vi.fn();
it("should render the search input", () => {
renderWithProviders(
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
repositories={[]}
/>,
);
expect(
screen.getByPlaceholderText("Select a GitHub project"),
).toBeInTheDocument();
});
it("should show the GitHub login button in OSS mode", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
APP_SLUG: "openhands",
GITHUB_CLIENT_ID: "test-client-id",
POSTHOG_CLIENT_KEY: "test-posthog-key",
});
renderWithProviders(
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
repositories={[]}
/>,
);
expect(screen.getByTestId("github-repo-selector")).toBeInTheDocument();
});
it("should show the search results", () => {
const mockSearchedRepos = [
{
id: 1,
full_name: "test/repo1",
stargazers_count: 100,
},
{
id: 2,
full_name: "test/repo2",
stargazers_count: 200,
},
];
const searchPublicRepositoriesSpy = vi.spyOn(
GitHubAPI,
"searchPublicRepositories",
);
searchPublicRepositoriesSpy.mockResolvedValue(mockSearchedRepos);
renderWithProviders(
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
repositories={[]}
/>,
);
expect(screen.getByTestId("github-repo-selector")).toBeInTheDocument();
});
});

View File

@@ -14,7 +14,7 @@
"@react-router/serve": "^7.1.1",
"@react-types/shared": "^3.25.0",
"@reduxjs/toolkit": "^2.5.0",
"@tanstack/react-query": "^5.62.11",
"@tanstack/react-query": "^5.62.12",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
@@ -27,11 +27,11 @@
"isbot": "^5.1.19",
"jose": "^5.9.4",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.203.2",
"posthog-js": "^1.203.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
"react-hot-toast": "^2.5.1",
"react-i18next": "^15.4.0",
"react-icons": "^5.4.0",
"react-markdown": "^9.0.1",
@@ -56,7 +56,7 @@
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.10.2",
"@types/node": "^22.10.5",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@types/react-highlight": "^0.12.8",
@@ -83,7 +83,7 @@
"postcss": "^8.4.47",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.6.3",
"typescript": "^5.7.2",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^1.6.0"
@@ -5361,22 +5361,20 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.62.9",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.9.tgz",
"integrity": "sha512-lwePd8hNYhyQ4nM/iRQ+Wz2cDtspGeZZHFZmCzHJ7mfKXt+9S301fULiY2IR2byJYY6Z03T427E5PoVfMexHjw==",
"license": "MIT",
"version": "5.62.12",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.12.tgz",
"integrity": "sha512-6igFeBgymHkCxVgaEk+yiLwkMf9haui/EQLmI3o9CatOyDThEoFKe8toLWvWliZC/Jf+h7NwHi/zjfyLArr1ow==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.62.11",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.11.tgz",
"integrity": "sha512-Xb1nw0cYMdtFmwkvH9+y5yYFhXvLRCnXoqlzSw7UkqtCVFq3cG8q+rHZ2Yz1XrC+/ysUaTqbLKJqk95mCgC1oQ==",
"license": "MIT",
"version": "5.62.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.12.tgz",
"integrity": "sha512-yt8p7l5MlHA3QCt6xF1Cu9dw1Anf93yTK+DMDJQ64h/mshAymVAtcwj8TpsyyBrZNWAAZvza/m76bnTSR79ZtQ==",
"dependencies": {
"@tanstack/query-core": "5.62.9"
"@tanstack/query-core": "5.62.12"
},
"funding": {
"type": "github",
@@ -5627,11 +5625,10 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"version": "22.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
@@ -13810,10 +13807,9 @@
"license": "MIT"
},
"node_modules/posthog-js": {
"version": "1.203.2",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.203.2.tgz",
"integrity": "sha512-3aLpEhM4i9sQQtobRmDttJ3rTW1+gwQ9HL7QiOeDueE2T7CguYibYS7weY1UhXMerx5lh1A7+szlOJTTibifLQ==",
"license": "MIT",
"version": "1.203.3",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.203.3.tgz",
"integrity": "sha512-DTK6LfL87xC7PPleKDParEIfkXl7hXtuDeSOPfhcyCXLuVspq0z7YyRB5dQE9Pbalf3yoGqUKvomYFp/BGVfQg==",
"dependencies": {
"core-js": "^3.38.1",
"fflate": "^0.4.8",
@@ -14144,12 +14140,12 @@
}
},
"node_modules/react-hot-toast": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
"integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
"license": "MIT",
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.1.tgz",
"integrity": "sha512-54Gq1ZD1JbmAb4psp9bvFHjS7lje+8ubboUmvKZkCsQBLH6AOpZ9JemfRvIdHcfb9AZXRaFLrb3qUobGYDJhFQ==",
"dependencies": {
"goober": "^2.1.10"
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"

View File

@@ -13,7 +13,7 @@
"@react-router/serve": "^7.1.1",
"@react-types/shared": "^3.25.0",
"@reduxjs/toolkit": "^2.5.0",
"@tanstack/react-query": "^5.62.11",
"@tanstack/react-query": "^5.62.12",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
@@ -26,11 +26,11 @@
"isbot": "^5.1.19",
"jose": "^5.9.4",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.203.2",
"posthog-js": "^1.203.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.4.1",
"react-hot-toast": "^2.5.1",
"react-i18next": "^15.4.0",
"react-icons": "^5.4.0",
"react-markdown": "^9.0.1",
@@ -83,7 +83,7 @@
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.10.2",
"@types/node": "^22.10.5",
"@types/react": "^19.0.2",
"@types/react-dom": "^19.0.2",
"@types/react-highlight": "^0.12.8",
@@ -110,7 +110,7 @@
"postcss": "^8.4.47",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.6.3",
"typescript": "^5.7.2",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^1.6.0"

View File

@@ -104,6 +104,31 @@ export const retrieveGitHubUser = async () => {
return user;
};
export const searchPublicRepositories = async (
query: string,
per_page = 5,
sort: "" | "updated" | "stars" | "forks" = "stars",
order: "desc" | "asc" = "desc",
): Promise<GitHubRepository[]> => {
const sanitizedQuery = query.trim();
if (!sanitizedQuery) {
return [];
}
const response = await github.get<{ items: GitHubRepository[] }>(
"/search/repositories",
{
params: {
q: sanitizedQuery,
per_page,
sort,
order,
},
},
);
return response.data.items;
};
export const retrieveLatestGitHubCommit = async (
repository: string,
): Promise<GitHubCommit | null> => {

View File

@@ -1,3 +1,4 @@
import React from "react";
import { ChatMessage } from "#/components/features/chat/chat-message";
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
import { ImageCarousel } from "../images/image-carousel";
@@ -8,32 +9,36 @@ interface MessagesProps {
isAwaitingUserConfirmation: boolean;
}
export function Messages({
messages,
isAwaitingUserConfirmation,
}: MessagesProps) {
return messages.map((message, index) => {
if (message.type === "error" || message.type === "action") {
return (
<ExpandableMessage
key={index}
type={message.type}
id={message.translationID}
message={message.content}
success={message.success}
/>
);
}
export const Messages: React.FC<MessagesProps> = React.memo(
({ messages, isAwaitingUserConfirmation }) =>
messages.map((message, index) => {
if (message.type === "error" || message.type === "action") {
return (
<ExpandableMessage
key={index}
type={message.type}
id={message.translationID}
message={message.content}
success={message.success}
/>
);
}
return (
<ChatMessage key={index} type={message.sender} message={message.content}>
{message.imageUrls && message.imageUrls.length > 0 && (
<ImageCarousel size="small" images={message.imageUrls} />
)}
{messages.length - 1 === index &&
message.sender === "assistant" &&
isAwaitingUserConfirmation && <ConfirmationButtons />}
</ChatMessage>
);
});
}
return (
<ChatMessage
key={index}
type={message.sender}
message={message.content}
>
{message.imageUrls && message.imageUrls.length > 0 && (
<ImageCarousel size="small" images={message.imageUrls} />
)}
{messages.length - 1 === index &&
message.sender === "assistant" &&
isAwaitingUserConfirmation && <ConfirmationButtons />}
</ChatMessage>
);
}),
);
Messages.displayName = "Messages";

View File

@@ -5,60 +5,49 @@ import posthog from "posthog-js";
import { setSelectedRepository } from "#/state/initial-query-slice";
import { useConfig } from "#/hooks/query/use-config";
interface GitHubRepositoryWithPublic extends GitHubRepository {
is_public?: boolean;
}
interface GitHubRepositorySelectorProps {
onInputChange: (value: string) => void;
onSelect: () => void;
repositories: GitHubRepository[];
repositories: GitHubRepositoryWithPublic[];
}
export function GitHubRepositorySelector({
onInputChange,
onSelect,
repositories,
}: GitHubRepositorySelectorProps) {
const { data: config } = useConfig();
const [selectedKey, setSelectedKey] = React.useState<string | null>(null);
// Add option to install app onto more repos
const finalRepositories =
config?.APP_MODE === "saas"
? [{ id: -1000, full_name: "Add more repositories..." }, ...repositories]
: repositories;
const dispatch = useDispatch();
const handleRepoSelection = (id: string | null) => {
const repo = finalRepositories.find((r) => r.id.toString() === id);
if (id === "-1000") {
if (config?.APP_SLUG)
window.open(
`https://github.com/apps/${config.APP_SLUG}/installations/new`,
"_blank",
);
} else if (repo) {
// set query param
dispatch(setSelectedRepository(repo.full_name));
posthog.capture("repository_selected");
onSelect();
setSelectedKey(id);
const repo = repositories.find((r) => r.id.toString() === id);
if (!repo) return;
if (repo.id === -1000) {
window.open(
`https://github.com/apps/${config?.APP_SLUG}/installations/new`,
"_blank",
);
return;
}
dispatch(setSelectedRepository(repo.full_name));
posthog.capture("repository_selected");
onSelect();
setSelectedKey(id);
};
const handleClearSelection = () => {
// clear query param
dispatch(setSelectedRepository(null));
};
const emptyContent = config?.APP_SLUG ? (
<a
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
target="_blank"
rel="noreferrer noopener"
className="underline"
>
Add more repositories...
</a>
) : (
"No results found."
);
const emptyContent = "No results found.";
return (
<Autocomplete
@@ -67,6 +56,7 @@ export function GitHubRepositorySelector({
aria-label="GitHub Repository"
placeholder="Select a GitHub project"
selectedKey={selectedKey}
items={repositories}
inputProps={{
classNames: {
inputWrapper:
@@ -74,20 +64,29 @@ export function GitHubRepositorySelector({
},
}}
onSelectionChange={(id) => handleRepoSelection(id?.toString() ?? null)}
clearButtonProps={{ onClick: handleClearSelection }}
onInputChange={onInputChange}
clearButtonProps={{ onPress: handleClearSelection }}
listboxProps={{
emptyContent,
}}
>
{finalRepositories.map((repo) => (
{(item) => (
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
key={item.id}
value={item.id}
textValue={item.full_name}
>
{repo.full_name}
<div className="flex items-center justify-between">
{item.full_name}
{item.is_public && !!item.stargazers_count && (
<span className="text-xs text-gray-400">
({item.stargazers_count})
</span>
)}
</div>
</AutocompleteItem>
))}
)}
</Autocomplete>
);
}

View File

@@ -6,22 +6,54 @@ import { ModalButton } from "#/components/shared/buttons/modal-button";
import { ConnectToGitHubModal } from "#/components/shared/modals/connect-to-github-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { isGitHubErrorReponse } from "#/api/github-axios-instance";
import { useAppRepositories } from "#/hooks/query/use-app-repositories";
import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
import { sanitizeQuery } from "#/utils/sanitize-query";
import { useDebounce } from "#/hooks/use-debounce";
import { useConfig } from "#/hooks/query/use-config";
interface GitHubRepositoriesSuggestionBoxProps {
handleSubmit: () => void;
repositories: GitHubRepository[];
gitHubAuthUrl: string | null;
user: GitHubErrorReponse | GitHubUser | null;
}
export function GitHubRepositoriesSuggestionBox({
handleSubmit,
repositories,
gitHubAuthUrl,
user,
}: GitHubRepositoriesSuggestionBoxProps) {
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
React.useState(false);
const [searchQuery, setSearchQuery] = React.useState<string>("");
const debouncedSearchQuery = useDebounce(searchQuery, 300);
const { data: config } = useConfig();
// TODO: Use `useQueries` to fetch all repositories in parallel
const { data: appRepositories } = useAppRepositories();
const { data: userRepositories } = useUserRepositories();
const { data: searchedRepos } = useSearchRepositories(
sanitizeQuery(debouncedSearchQuery),
);
const saasPlaceholderRepository = React.useMemo(() => {
if (config?.APP_MODE === "saas" && config?.APP_SLUG) {
return [
{
id: -1000,
full_name: "Add more repositories...",
},
];
}
return [];
}, [config]);
const repositories =
userRepositories?.pages.flatMap((page) => page.data) ||
appRepositories?.pages.flatMap((page) => page.data) ||
[];
const handleConnectToGitHub = () => {
if (gitHubAuthUrl) {
@@ -40,8 +72,13 @@ export function GitHubRepositoriesSuggestionBox({
content={
isLoggedIn ? (
<GitHubRepositorySelector
onInputChange={setSearchQuery}
onSelect={handleSubmit}
repositories={repositories}
repositories={[
...saasPlaceholderRepository,
...searchedRepos,
...repositories,
]}
/>
) : (
<ModalButton

View File

@@ -25,9 +25,7 @@ export function SettingsUpToDateProvider({
);
return (
<SettingsUpToDateContext.Provider value={value}>
{children}
</SettingsUpToDateContext.Provider>
<SettingsUpToDateContext value={value}>{children}</SettingsUpToDateContext>
);
}

View File

@@ -1,13 +1,24 @@
import posthog from "posthog-js";
import React from "react";
import { io, Socket } from "socket.io-client";
import EventLogger from "#/utils/event-logger";
import { handleAssistantMessage } from "#/services/actions";
import { useRate } from "#/hooks/use-rate";
import { OpenHandsParsedEvent } from "#/types/core";
import { AgentStateChangeObservation } from "#/types/core/observations";
const isOpenHandsMessage = (event: Record<string, unknown>) =>
event.action === "message";
const isOpenHandsMessage = (event: unknown): event is OpenHandsParsedEvent =>
typeof event === "object" &&
event !== null &&
"id" in event &&
"source" in event &&
"message" in event &&
"timestamp" in event;
const isAgentStateChangeObservation = (
event: OpenHandsParsedEvent,
): event is AgentStateChangeObservation =>
"observation" in event && event.observation === "agent_state_changed";
export enum WsClientProviderStatus {
CONNECTED,
@@ -63,7 +74,7 @@ export function WsClientProvider({
}
function handleMessage(event: Record<string, unknown>) {
if (isOpenHandsMessage(event)) {
if (isOpenHandsMessage(event) && !isAgentStateChangeObservation(event)) {
messageRateHandler.record(new Date().getTime());
}
setEvents((prevEvents) => [...prevEvents, event]);

View File

@@ -17,7 +17,7 @@ export const useConversationConfig = () => {
if (!conversationId) throw new Error("No conversation ID");
return OpenHands.getRuntimeId(conversationId);
},
enabled: status === WsClientProviderStatus.CONNECTED && !!conversationId,
enabled: status !== WsClientProviderStatus.DISCONNECTED && !!conversationId,
});
React.useEffect(() => {

View File

@@ -1,10 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
import { useSelector } from "react-redux";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
interface UseListFilesConfig {
path?: string;
@@ -17,8 +16,8 @@ const DEFAULT_CONFIG: UseListFilesConfig = {
export const useListFiles = (config: UseListFilesConfig = DEFAULT_CONFIG) => {
const { conversationId } = useConversation();
const { status } = useWsClient();
const isActive = status === WsClientProviderStatus.CONNECTED;
const { curAgentState } = useSelector((state: RootState) => state.agent);
const isActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
return useQuery({
queryKey: ["files", conversationId, config?.path],

View File

@@ -0,0 +1,12 @@
import { useQuery } from "@tanstack/react-query";
import { searchPublicRepositories } from "#/api/github";
export function useSearchRepositories(query: string) {
return useQuery({
queryKey: ["repositories", query],
queryFn: () => searchPublicRepositories(query, 3),
enabled: !!query,
select: (data) => data.map((repo) => ({ ...repo, is_public: true })),
initialData: [],
});
}

View File

@@ -17,6 +17,7 @@ export const useClickOutsideElement = <T extends HTMLElement>(
};
document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside);
}, []);

View File

@@ -0,0 +1,12 @@
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}

View File

@@ -3,9 +3,6 @@ import { useDispatch } from "react-redux";
import posthog from "posthog-js";
import { setImportedProjectZip } from "#/state/initial-query-slice";
import { convertZipToBase64 } from "#/utils/convert-zip-to-base64";
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
import { useAppRepositories } from "#/hooks/query/use-app-repositories";
import { useGitHubUser } from "#/hooks/query/use-github-user";
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
import { useConfig } from "#/hooks/query/use-config";
@@ -22,8 +19,6 @@ function Home() {
const { data: config } = useConfig();
const { data: user } = useGitHubUser();
const { data: appRepositories } = useAppRepositories();
const { data: userRepositories } = useUserRepositories();
const gitHubAuthUrl = useGitHubAuthUrl({
gitHubToken,
@@ -47,11 +42,6 @@ function Home() {
<div className="flex gap-4 w-full flex-col md:flex-row">
<GitHubRepositoriesSuggestionBox
handleSubmit={() => formRef.current?.requestSubmit()}
repositories={
userRepositories?.pages.flatMap((page) => page.data) ||
appRepositories?.pages.flatMap((page) => page.data) ||
[]
}
gitHubAuthUrl={gitHubAuthUrl}
user={user || null}
/>

View File

@@ -71,7 +71,7 @@ export const useWSStatusChange = () => {
}
statusRef.current = status;
if (status === WsClientProviderStatus.CONNECTED && initialQuery) {
if (status !== WsClientProviderStatus.DISCONNECTED && initialQuery) {
dispatch(
addUserMessage({
content: initialQuery,

View File

@@ -16,6 +16,7 @@ interface GitHubUser {
interface GitHubRepository {
id: number;
full_name: string;
stargazers_count?: number;
}
interface GitHubAppRepository {

View File

@@ -0,0 +1,6 @@
export const sanitizeQuery = (query: string) =>
query
.replace(/https?:\/\//, "")
.replace(/github.com\//, "")
.replace(/\.git$/, "")
.toLowerCase();

View File

@@ -68,7 +68,13 @@ export function renderWithProviders(
<Provider store={store}>
<AuthProvider>
<SettingsUpToDateProvider>
<QueryClientProvider client={new QueryClient()}>
<QueryClientProvider
client={
new QueryClient({
defaultOptions: { queries: { retry: false } },
})
}
>
<ConversationProvider>
<I18nextProvider i18n={i18n}>{children}</I18nextProvider>
</ConversationProvider>

163
microagents/README.md Normal file
View File

@@ -0,0 +1,163 @@
# OpenHands MicroAgents
MicroAgents are specialized prompts that enhance OpenHands with domain-specific knowledge and task-specific workflows. They help developers by providing expert guidance, automating common tasks, and ensuring consistent practices across projects. Each microagent is designed to excel in a specific area, from Git operations to code review processes.
## Sources of Microagents
OpenHands loads microagents from two sources:
### 1. Shareable Microagents (Public)
This directory (`OpenHands/microagents/`) contains shareable microagents that are:
- Available to all OpenHands users
- Maintained in the OpenHands repository
- Perfect for reusable knowledge and common workflows
Directory structure:
```
OpenHands/microagents/
├── knowledge/ # Keyword-triggered expertise
│ ├── git.md # Git operations
│ ├── testing.md # Testing practices
│ └── docker.md # Docker guidelines
└── tasks/ # Interactive workflows
├── pr_review.md # PR review process
├── bug_fix.md # Bug fixing workflow
└── feature.md # Feature implementation
```
### 2. Repository Instructions (Private)
Each repository can have its own instructions in `.openhands/microagents/repo.md`. These instructions are:
- Private to that repository
- Automatically loaded when working with that repository
- Perfect for repository-specific guidelines and team practices
Example repository structure:
```
your-repository/
└── .openhands/
└── microagents/
└── repo.md # Repository-specific instructions
└── knowledges/ # Private micro-agents that are only available inside this repo
└── tasks/ # Private micro-agents that are only available inside this repo
```
## Loading Order
When OpenHands works with a repository, it:
1. Loads repository-specific instructions from `.openhands/microagents/repo.md` if present
2. Loads relevant knowledge agents based on keywords in conversations
3. Enable task agent if user select one of them
## Types of MicroAgents
All microagents use markdown files with YAML frontmatter.
### 1. Knowledge Agents
Knowledge agents provide specialized expertise that's triggered by keywords in conversations. They help with:
- Language best practices
- Framework guidelines
- Common patterns
- Tool usage
Key characteristics:
- **Trigger-based**: Activated by specific keywords in conversations
- **Context-aware**: Provide relevant advice based on file types and content
- **Reusable**: Knowledge can be applied across multiple projects
- **Versioned**: Support multiple versions of tools/frameworks
You can see an example of a knowledge-based agent in [OpenHands's github microagent](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/github.md).
### 2. Repository Agents
Repository agents provide repository-specific knowledge and guidelines. They are:
- Loaded from `.openhands/microagents/repo.md`
- Specific to individual repositories
- Automatically activated for their repository
- Perfect for team practices and project conventions
Key features:
- **Project-specific**: Contains guidelines unique to the repository
- **Team-focused**: Enforces team conventions and practices
- **Always active**: Automatically loaded for the repository
- **Locally maintained**: Updated with the project
You can see an example of a repo agent in [the agent for the OpenHands repo itself](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands/microagents/repo.md).
### 3. Task Agents
Task agents provide interactive workflows that guide users through common development tasks. They:
- Accept user inputs
- Follow predefined steps
- Adapt to context
- Provide consistent results
Key capabilities:
- **Interactive**: Guide users through complex processes
- **Validating**: Check inputs and conditions
- **Flexible**: Adapt to different scenarios
- **Reproducible**: Ensure consistent outcomes
Example workflow:
You can see an example of a task-based agent in [OpenHands's pull request updating microagent](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks/update_pr_description.md).
## Contributing
### When to Contribute
1. **Knowledge Agents** - When you have:
- Language/framework best practices
- Tool usage patterns
- Common problem solutions
- General development guidelines
2. **Task Agents** - When you have:
- Repeatable workflows
- Multi-step processes
- Common development tasks
- Standard procedures
3. **Repository Agents** - When you need:
- Project-specific guidelines
- Team conventions and practices
- Custom workflow documentation
- Repository-specific setup instructions
### Best Practices
1. **For Knowledge Agents**:
- Choose distinctive triggers
- Focus on one area of expertise
- Include practical examples
- Use file patterns when relevant
- Keep knowledge general and reusable
2. **For Task Agents**:
- Break workflows into clear steps
- Validate user inputs
- Provide helpful defaults
- Include usage examples
- Make steps adaptable
3. **For Repository Agents**:
- Document clear setup instructions
- Include repository structure details
- Specify testing and build procedures
- List environment requirements
- Maintain up-to-date team practices
### Submission Process
1. Create your agent file in the appropriate directory:
- `knowledge/` for expertise (public, shareable)
- `tasks/` for workflows (public, shareable)
- Note: Repository agents should remain in their respective repositories' `.openhands/microagents/` directory
2. Test thoroughly
3. Submit a pull request to OpenHands
## License
All microagents are subject to the same license as OpenHands. See the root LICENSE file for details.

View File

@@ -1,5 +1,7 @@
---
name: flarglebargle
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- flarglebargle

View File

@@ -1,5 +1,7 @@
---
name: github
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- github
@@ -17,8 +19,10 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
* Git config (username and email) is pre-set. Do not modify.
* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.
* Use the GitHub API to create a pull request, if you haven't already
* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
* Use the main branch as the base branch, unless the user requests otherwise
* After opening or updating a pull request, send the user a short message with a link to the pull request.
* Prefer "Draft" pull requests when possible
* Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands:
```bash
git remote -v && git branch # to find the current org, repo and branch

View File

@@ -1,5 +1,7 @@
---
name: npm
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- npm

View File

@@ -0,0 +1,20 @@
---
name: address_pr_comments
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- name: PR_URL
description: "URL of the pull request"
required: true
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
required: true
---
First, check the branch {{ BRANCH_NAME }} and read the diff against the main branch to understand the purpose.
This branch corresponds to this PR {{ PR_URL }}
Next, you should use the GitHub API to read the reviews and comments on this PR and address them.

View File

@@ -0,0 +1,28 @@
---
name: get_test_to_pass
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
required: true
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
required: true
- name: FUNCTION_TO_FIX
description: "The name of function to fix"
required: false
- name: FILE_FOR_FUNCTION
description: "The path of the file that contains the function"
required: false
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
{%- if FUNCTION_TO_FIX and FILE_FOR_FUNCTION %}
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
{%- endif %}
PLEASE DO NOT modify the tests by yourselves -- Let me know if you think some of the tests are incorrect.

View File

@@ -0,0 +1,22 @@
---
name: update_pr_description
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- name: PR_URL
description: "URL of the pull request"
type: string
required: true
validation:
pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
type: string
required: true
---
Please check the branch "{{ BRANCH_NAME }}" and look at the diff against the main branch. This branch belongs to this PR "{{ PR_URL }}".
Once you understand the purpose of the diff, please use Github API to read the existing PR description, and update it to be more reflective of the changes we've made when necessary.

View File

@@ -0,0 +1,22 @@
---
name: update_test_for_new_implementation
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
required: true
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
required: true
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
{%- if FUNCTION_TO_FIX and FILE_FOR_FUNCTION %}
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
{%- endif %}
PLEASE DO NOT modify the tests by yourselves -- Let me know if you think some of the tests are incorrect.

View File

@@ -4,6 +4,7 @@ from collections import deque
from litellm import ModelResponse
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
@@ -104,7 +105,10 @@ class CodeActAgent(Agent):
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(__file__), 'micro')
microagent_dir=os.path.join(
os.path.dirname(os.path.dirname(openhands.__file__)),
'microagents',
)
if self.config.use_microagents
else None,
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),

View File

@@ -11,7 +11,9 @@ from openhands.core.exceptions import (
)
from openhands.llm.llm import LLM
from openhands.runtime.plugins import PluginRequirement
from openhands.utils.prompt import PromptManager
if TYPE_CHECKING:
from openhands.utils.prompt import PromptManager
class Agent(ABC):
@@ -34,7 +36,7 @@ class Agent(ABC):
self.llm = llm
self.config = config
self._complete = False
self.prompt_manager: PromptManager | None = None
self.prompt_manager: 'PromptManager' | None = None
@property
def complete(self) -> bool:

View File

@@ -206,12 +206,13 @@ class AgentController:
reported = RuntimeError(
'There was an unexpected error while running the agent.'
)
if isinstance(e, litellm.AuthenticationError):
if isinstance(e, litellm.AuthenticationError) or isinstance(
e, litellm.BadRequestError
):
reported = e
await self._react_to_exception(reported)
def should_step(self, event: Event) -> bool:
print('should step?', event)
if isinstance(event, Action):
if isinstance(event, MessageAction) and event.source == EventSource.USER:
return True
@@ -536,9 +537,7 @@ class AgentController:
self.update_state_before_step()
action: Action = NullAction()
try:
print('STEP AGENT')
action = self.agent.step(self.state)
print('GOT ACTION', action)
if action is None:
raise LLMNoActionError('No action was returned')
except (

View File

@@ -1,15 +1,12 @@
import asyncio
import logging
import sys
from typing import Type
from uuid import uuid4
from termcolor import colored
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
from openhands import __version__
from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.core.config import (
AppConfig,
get_parser,
@@ -18,7 +15,8 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.loop import run_agent_until_done
from openhands.core.schema import AgentState
from openhands.events import EventSource, EventStream, EventStreamSubscriber
from openhands.core.setup import create_agent, create_controller, create_runtime
from openhands.events import EventSource, EventStreamSubscriber
from openhands.events.action import (
Action,
ActionConfirmationStatus,
@@ -34,11 +32,8 @@ from openhands.events.observation import (
FileEditObservation,
NullObservation,
)
from openhands.llm.llm import LLM
from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
from openhands.security import SecurityAnalyzer, options
from openhands.storage import get_file_store
from openhands.runtime.runtime_manager import RuntimeManager
def display_message(message: str):
@@ -114,39 +109,12 @@ async def main(loop):
config = load_app_config(config_file=args.config_file)
sid = str(uuid4())
agent_cls: Type[Agent] = Agent.get_cls(config.default_agent)
agent_config = config.get_agent_config(config.default_agent)
llm_config = config.get_llm_config_from_agent(config.default_agent)
agent = agent_cls(
llm=LLM(config=llm_config),
config=agent_config,
)
runtime = create_runtime(config, sid=sid, headless_mode=True)
await runtime.connect()
agent = create_agent(runtime, config)
controller, _ = create_controller(agent, runtime, config)
file_store = get_file_store(config.file_store, config.file_store_path)
event_stream = EventStream(sid, file_store)
runtime_cls = get_runtime_cls(config.runtime)
runtime: Runtime = runtime_cls( # noqa: F841
config=config,
event_stream=event_stream,
sid=sid,
plugins=agent_cls.sandbox_plugins,
headless_mode=True,
)
if config.security.security_analyzer:
options.SecurityAnalyzers.get(
config.security.security_analyzer, SecurityAnalyzer
)(event_stream)
controller = AgentController(
agent=agent,
max_iterations=config.max_iterations,
max_budget_per_task=config.max_budget_per_task,
agent_to_llm_config=config.get_agent_to_llm_config_map(),
event_stream=event_stream,
confirmation_mode=config.security.confirmation_mode,
)
event_stream = runtime.event_stream
async def prompt_for_next_task():
# Run input() in a thread pool to avoid blocking the event loop
@@ -196,8 +164,6 @@ async def main(loop):
event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, str(uuid4()))
await runtime.connect()
asyncio.create_task(prompt_for_next_task())
await run_agent_until_done(

View File

@@ -50,8 +50,8 @@ class AppConfig:
sandbox: SandboxConfig = field(default_factory=SandboxConfig)
security: SecurityConfig = field(default_factory=SecurityConfig)
runtime: str = 'docker'
file_store: str = 'memory'
file_store_path: str = '/tmp/file_store'
file_store: str = 'local'
file_store_path: str = '/tmp/openhands_file_store'
trajectories_path: str | None = None
workspace_base: str | None = None
workspace_mount_path: str | None = None

View File

@@ -45,6 +45,7 @@ class LLMConfig:
log_completions_folder: The folder to log LLM completions to. Required if log_completions is True.
draft_editor: A more efficient LLM to use for file editing. Introduced in [PR 3985](https://github.com/All-Hands-AI/OpenHands/pull/3985).
custom_tokenizer: A custom tokenizer to use for token counting.
native_tool_calling: Whether to use native tool calling if supported by the model. Can be True, False, or not set.
"""
model: str = 'claude-3-5-sonnet-20241022'
@@ -83,6 +84,7 @@ class LLMConfig:
log_completions_folder: str = os.path.join(LOG_DIR, 'completions')
draft_editor: Optional['LLMConfig'] = None
custom_tokenizer: str | None = None
native_tool_calling: bool | None = None
def defaults_to_dict(self) -> dict:
"""Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional."""

View File

@@ -94,6 +94,10 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
Args:
cfg: The AppConfig object to update attributes of.
toml_file: The path to the toml file. Defaults to 'config.toml'.
See Also:
- `config.template.toml` for the full list of config options.
- `SandboxConfig` for the sandbox-specific config options.
"""
# try to read the config.toml file into the config object
try:
@@ -161,11 +165,11 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
)
except (TypeError, KeyError) as e:
logger.openhands_logger.warning(
f'Cannot parse config from toml, toml values have not been applied.\n Error: {e}',
f'Cannot parse [{key}] config from toml, values have not been applied.\nError: {e}',
exc_info=False,
)
else:
logger.openhands_logger.warning(f'Unknown key in {toml_file}: "{key}')
logger.openhands_logger.warning(f'Unknown section [{key}] in {toml_file}')
try:
# set sandbox config from the toml file
@@ -179,7 +183,9 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
# read the key in sandbox and remove it from core
setattr(sandbox_config, new_key, core_config.pop(key))
else:
logger.openhands_logger.warning(f'Unknown sandbox config: {key}')
logger.openhands_logger.warning(
f'Unknown config key "{key}" in [sandbox] section'
)
# the new style values override the old style values
if 'sandbox' in toml_config:
@@ -191,10 +197,12 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
if hasattr(cfg, key):
setattr(cfg, key, value)
else:
logger.openhands_logger.warning(f'Unknown core config key: {key}')
logger.openhands_logger.warning(
f'Unknown config key "{key}" in [core] section'
)
except (TypeError, KeyError) as e:
logger.openhands_logger.warning(
f'Cannot parse config from toml, toml values have not been applied.\nError: {e}',
f'Cannot parse [sandbox] config from toml, values have not been applied.\nError: {e}',
exc_info=False,
)

View File

@@ -91,11 +91,6 @@ class UserCancelledError(Exception):
super().__init__(message)
class MicroAgentValidationError(Exception):
def __init__(self, message='Micro agent validation failed'):
super().__init__(message)
class OperationCancelled(Exception):
"""Exception raised when an operation is cancelled (e.g. by a keyboard interrupt)."""
@@ -204,3 +199,21 @@ class BrowserUnavailableException(Exception):
message='Browser environment is not available, please check if has been initialized',
):
super().__init__(message)
# ============================================
# Microagent Exceptions
# ============================================
class MicroAgentError(Exception):
"""Base exception for all microagent errors."""
pass
class MicroAgentValidationError(MicroAgentError):
"""Raised when there's a validation error in microagent metadata."""
def __init__(self, message='Micro agent validation failed'):
super().__init__(message)

View File

@@ -1,13 +1,10 @@
import asyncio
import hashlib
import json
import os
import sys
import uuid
from typing import Callable, Protocol, Type
from typing import Callable, Protocol
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -19,16 +16,20 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.loop import run_agent_until_done
from openhands.core.schema import AgentState
from openhands.events import EventSource, EventStream, EventStreamSubscriber
from openhands.core.setup import (
create_agent,
create_controller,
create_runtime,
generate_sid,
)
from openhands.events import EventSource, EventStreamSubscriber
from openhands.events.action import MessageAction
from openhands.events.action.action import Action
from openhands.events.event import Event
from openhands.events.observation import AgentStateChangedObservation
from openhands.events.serialization.event import event_to_trajectory
from openhands.llm.llm import LLM
from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
from openhands.storage import get_file_store
from openhands.runtime.runtime_manager import RuntimeManager
class FakeUserResponseFunc(Protocol):
@@ -51,44 +52,6 @@ def read_task_from_stdin() -> str:
return sys.stdin.read()
def create_runtime(
config: AppConfig,
sid: str | None = None,
headless_mode: bool = True,
) -> Runtime:
"""Create a runtime for the agent to run on.
config: The app config.
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
where we don't want to have the VSCode UI open, so it defaults to True.
"""
# if sid is provided on the command line, use it as the name of the event stream
# otherwise generate it on the basis of the configured jwt_secret
# we can do this better, this is just so that the sid is retrieved when we want to restore the session
session_id = sid or generate_sid(config)
# set up the event stream
file_store = get_file_store(config.file_store, config.file_store_path)
event_stream = EventStream(session_id, file_store)
# agent class
agent_cls = openhands.agenthub.Agent.get_cls(config.default_agent)
# runtime and tools
runtime_cls = get_runtime_cls(config.runtime)
logger.debug(f'Initializing runtime: {runtime_cls.__name__}')
runtime: Runtime = runtime_cls(
config=config,
event_stream=event_stream,
sid=session_id,
plugins=agent_cls.sandbox_plugins,
headless_mode=headless_mode,
)
return runtime
async def run_controller(
config: AppConfig,
@@ -115,47 +78,17 @@ async def run_controller(
(could be None) and returns a fake user response.
headless_mode: Whether the agent is run in headless mode.
"""
# Create the agent
if agent is None:
agent_cls: Type[Agent] = Agent.get_cls(config.default_agent)
agent_config = config.get_agent_config(config.default_agent)
llm_config = config.get_llm_config_from_agent(config.default_agent)
agent = agent_cls(
llm=LLM(config=llm_config),
config=agent_config,
)
# make sure the session id is set
sid = sid or generate_sid(config)
if runtime is None:
runtime = create_runtime(config, sid=sid, headless_mode=headless_mode)
await runtime.connect()
runtime = await create_runtime(config, sid=sid, headless_mode=headless_mode)
event_stream = runtime.event_stream
# restore cli session if available
initial_state = None
try:
logger.debug(
f'Trying to restore agent state from cli session {event_stream.sid} if available'
)
initial_state = State.restore_from_session(
event_stream.sid, event_stream.file_store
)
except Exception as e:
logger.debug(f'Cannot restore agent state: {e}')
if agent is None:
agent = create_agent(runtime, config)
# init controller with this initial state
controller = AgentController(
agent=agent,
max_iterations=config.max_iterations,
max_budget_per_task=config.max_budget_per_task,
agent_to_llm_config=config.get_agent_to_llm_config_map(),
event_stream=event_stream,
initial_state=initial_state,
headless_mode=headless_mode,
)
controller, initial_state = create_controller(agent, runtime, config)
assert isinstance(
initial_user_action, Action
@@ -234,15 +167,6 @@ async def run_controller(
return state
def generate_sid(config: AppConfig, session_name: str | None = None) -> str:
"""Generate a session id based on the session name and the jwt secret."""
session_name = session_name or str(uuid.uuid4())
jwt_secret = config.jwt_secret
hash_str = hashlib.sha256(f'{session_name}{jwt_secret}'.encode('utf-8')).hexdigest()
return f'{session_name}-{hash_str[:16]}'
def auto_continue_response(
state: State,
encapsulate_solution: bool = False,

114
openhands/core/setup.py Normal file
View File

@@ -0,0 +1,114 @@
import hashlib
import uuid
from typing import Tuple, Type
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
from openhands.controller import AgentController
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
from openhands.llm.llm import LLM
from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
from openhands.security import SecurityAnalyzer, options
from openhands.storage import get_file_store
def create_runtime(
config: AppConfig,
sid: str | None = None,
headless_mode: bool = True,
) -> Runtime:
"""Create a runtime for the agent to run on.
config: The app config.
sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing.
Set it to incompatible value will cause unexpected behavior on RemoteRuntime.
headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts,
where we don't want to have the VSCode UI open, so it defaults to True.
"""
# if sid is provided on the command line, use it as the name of the event stream
# otherwise generate it on the basis of the configured jwt_secret
# we can do this better, this is just so that the sid is retrieved when we want to restore the session
session_id = sid or generate_sid(config)
# set up the event stream
file_store = get_file_store(config.file_store, config.file_store_path)
event_stream = EventStream(session_id, file_store)
# agent class
agent_cls = openhands.agenthub.Agent.get_cls(config.default_agent)
# runtime and tools
runtime_cls = get_runtime_cls(config.runtime)
logger.debug(f'Initializing runtime: {runtime_cls.__name__}')
runtime: Runtime = runtime_cls(
config=config,
event_stream=event_stream,
sid=session_id,
plugins=agent_cls.sandbox_plugins,
headless_mode=headless_mode,
)
return runtime
def create_agent(runtime: Runtime, config: AppConfig) -> Agent:
agent_cls: Type[Agent] = Agent.get_cls(config.default_agent)
agent_config = config.get_agent_config(config.default_agent)
llm_config = config.get_llm_config_from_agent(config.default_agent)
agent = agent_cls(
llm=LLM(config=llm_config),
config=agent_config,
)
if agent.prompt_manager:
microagents = runtime.get_microagents_from_selected_repo(None)
agent.prompt_manager.load_microagents(microagents)
if config.security.security_analyzer:
options.SecurityAnalyzers.get(
config.security.security_analyzer, SecurityAnalyzer
)(runtime.event_stream)
return agent
def create_controller(
agent: Agent, runtime: Runtime, config: AppConfig, headless_mode: bool = True
) -> Tuple[AgentController, State | None]:
event_stream = runtime.event_stream
initial_state = None
try:
logger.debug(
f'Trying to restore agent state from session {event_stream.sid} if available'
)
initial_state = State.restore_from_session(
event_stream.sid, event_stream.file_store
)
except Exception as e:
logger.debug(f'Cannot restore agent state: {e}')
controller = AgentController(
agent=agent,
max_iterations=config.max_iterations,
max_budget_per_task=config.max_budget_per_task,
agent_to_llm_config=config.get_agent_to_llm_config_map(),
event_stream=event_stream,
initial_state=initial_state,
headless_mode=headless_mode,
confirmation_mode=config.security.confirmation_mode,
)
return (controller, initial_state)
def generate_sid(config: AppConfig, session_name: str | None = None) -> str:
"""Generate a session id based on the session name and the jwt secret."""
session_name = session_name or str(uuid.uuid4())
jwt_secret = config.jwt_secret
hash_str = hashlib.sha256(f'{session_name}{jwt_secret}'.encode('utf-8')).hexdigest()
return f'{session_name}-{hash_str[:16]}'

View File

@@ -440,13 +440,31 @@ class LLM(RetryMixin, DebugMixin):
)
def is_function_calling_active(self) -> bool:
# Check if model name is in supported list before checking model_info
# Check if model name is in our supported list
model_name_supported = (
self.config.model in FUNCTION_CALLING_SUPPORTED_MODELS
or self.config.model.split('/')[-1] in FUNCTION_CALLING_SUPPORTED_MODELS
or any(m in self.config.model for m in FUNCTION_CALLING_SUPPORTED_MODELS)
)
return model_name_supported
# Handle native_tool_calling user-defined configuration
if self.config.native_tool_calling is None:
logger.debug(
f'Using default tool calling behavior based on model evaluation: {model_name_supported}'
)
return model_name_supported
elif self.config.native_tool_calling is False:
logger.debug('Function calling explicitly disabled via configuration')
return False
else:
# try to enable native tool calling if supported by the model
supports_fn_call = litellm.supports_function_calling(
model=self.config.model
)
logger.debug(
f'Function calling explicitly enabled, litellm support: {supports_fn_call}'
)
return supports_fn_call
def _post_completion(self, response: ModelResponse) -> float:
"""Post-process the completion response.

View File

@@ -0,0 +1,19 @@
from .microagent import (
BaseMicroAgent,
KnowledgeMicroAgent,
RepoMicroAgent,
TaskMicroAgent,
load_microagents_from_dir,
)
from .types import MicroAgentMetadata, MicroAgentType, TaskInput
__all__ = [
'BaseMicroAgent',
'KnowledgeMicroAgent',
'RepoMicroAgent',
'TaskMicroAgent',
'MicroAgentMetadata',
'MicroAgentType',
'TaskInput',
'load_microagents_from_dir',
]

View File

@@ -0,0 +1,164 @@
import io
from pathlib import Path
from typing import Union
import frontmatter
from pydantic import BaseModel
from openhands.core.exceptions import (
MicroAgentValidationError,
)
from openhands.microagent.types import MicroAgentMetadata, MicroAgentType
class BaseMicroAgent(BaseModel):
"""Base class for all microagents."""
name: str
content: str
metadata: MicroAgentMetadata
source: str # path to the file
type: MicroAgentType
@classmethod
def load(
cls, path: Union[str, Path], file_content: str | None = None
) -> 'BaseMicroAgent':
"""Load a microagent from a markdown file with frontmatter."""
path = Path(path) if isinstance(path, str) else path
# Only load directly from path if file_content is not provided
if file_content is None:
with open(path) as f:
file_content = f.read()
# Legacy repo instructions are stored in .openhands_instructions
if path.name == '.openhands_instructions':
return RepoMicroAgent(
name='repo_legacy',
content=file_content,
metadata=MicroAgentMetadata(name='repo_legacy'),
source=str(path),
type=MicroAgentType.REPO_KNOWLEDGE,
)
file_io = io.StringIO(file_content)
loaded = frontmatter.load(file_io)
content = loaded.content
try:
metadata = MicroAgentMetadata(**loaded.metadata)
except Exception as e:
raise MicroAgentValidationError(f'Error loading metadata: {e}') from e
# Create appropriate subclass based on type
subclass_map = {
MicroAgentType.KNOWLEDGE: KnowledgeMicroAgent,
MicroAgentType.REPO_KNOWLEDGE: RepoMicroAgent,
MicroAgentType.TASK: TaskMicroAgent,
}
if metadata.type not in subclass_map:
raise ValueError(f'Unknown microagent type: {metadata.type}')
agent_class = subclass_map[metadata.type]
return agent_class(
name=metadata.name,
content=content,
metadata=metadata,
source=str(path),
type=metadata.type,
)
class KnowledgeMicroAgent(BaseMicroAgent):
"""Knowledge micro-agents provide specialized expertise that's triggered by keywords in conversations. They help with:
- Language best practices
- Framework guidelines
- Common patterns
- Tool usage
"""
def __init__(self, **data):
super().__init__(**data)
if self.type != MicroAgentType.KNOWLEDGE:
raise ValueError('KnowledgeMicroAgent must have type KNOWLEDGE')
def match_trigger(self, message: str) -> str | None:
"""Match a trigger in the message.
It returns the first trigger that matches the message.
"""
message = message.lower()
for trigger in self.triggers:
if trigger.lower() in message:
return trigger
return None
@property
def triggers(self) -> list[str]:
return self.metadata.triggers
class RepoMicroAgent(BaseMicroAgent):
"""MicroAgent specialized for repository-specific knowledge and guidelines.
RepoMicroAgents are loaded from `.openhands/microagents/repo.md` files within repositories
and contain private, repository-specific instructions that are automatically loaded when
working with that repository. They are ideal for:
- Repository-specific guidelines
- Team practices and conventions
- Project-specific workflows
- Custom documentation references
"""
def __init__(self, **data):
super().__init__(**data)
if self.type != MicroAgentType.REPO_KNOWLEDGE:
raise ValueError('RepoMicroAgent must have type REPO_KNOWLEDGE')
class TaskMicroAgent(BaseMicroAgent):
"""MicroAgent specialized for task-based operations."""
def __init__(self, **data):
super().__init__(**data)
if self.type != MicroAgentType.TASK:
raise ValueError('TaskMicroAgent must have type TASK')
def load_microagents_from_dir(
microagent_dir: Union[str, Path],
) -> tuple[
dict[str, RepoMicroAgent], dict[str, KnowledgeMicroAgent], dict[str, TaskMicroAgent]
]:
"""Load all microagents from the given directory.
Args:
microagent_dir: Path to the microagents directory.
Returns:
Tuple of (repo_agents, knowledge_agents, task_agents) dictionaries
"""
if isinstance(microagent_dir, str):
microagent_dir = Path(microagent_dir)
repo_agents = {}
knowledge_agents = {}
task_agents = {}
# Load all agents
for file in microagent_dir.rglob('*.md'):
# skip README.md
if file.name == 'README.md':
continue
try:
agent = BaseMicroAgent.load(file)
if isinstance(agent, RepoMicroAgent):
repo_agents[agent.name] = agent
elif isinstance(agent, KnowledgeMicroAgent):
knowledge_agents[agent.name] = agent
elif isinstance(agent, TaskMicroAgent):
task_agents[agent.name] = agent
except Exception as e:
raise ValueError(f'Error loading agent from {file}: {e}')
return repo_agents, knowledge_agents, task_agents

View File

@@ -0,0 +1,29 @@
from enum import Enum
from pydantic import BaseModel, Field
class MicroAgentType(str, Enum):
"""Type of microagent."""
KNOWLEDGE = 'knowledge'
REPO_KNOWLEDGE = 'repo'
TASK = 'task'
class MicroAgentMetadata(BaseModel):
"""Metadata for all microagents."""
name: str = 'default'
type: MicroAgentType = Field(default=MicroAgentType.KNOWLEDGE)
version: str = Field(default='1.0.0')
agent: str = Field(default='CodeActAgent')
triggers: list[str] = [] # optional, only exists for knowledge microagents
class TaskInput(BaseModel):
"""Input parameter for task-based agents."""
name: str
description: str
required: bool = True

View File

@@ -199,8 +199,7 @@ async def process_issue(
)
config.set_llm_config(llm_config)
runtime = create_runtime(config)
await runtime.connect()
runtime = await create_runtime(config)
def on_event(evt):
logger.info(evt)

View File

@@ -29,11 +29,18 @@ from openhands.events.event import Event
from openhands.events.observation import (
CmdOutputObservation,
ErrorObservation,
FileReadObservation,
NullObservation,
Observation,
UserRejectObservation,
)
from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS
from openhands.microagent import (
BaseMicroAgent,
KnowledgeMicroAgent,
RepoMicroAgent,
TaskMicroAgent,
)
from openhands.runtime.plugins import (
JupyterRequirement,
PluginRequirement,
@@ -219,52 +226,73 @@ class Runtime(FileEditRuntimeMixin):
self.log('info', f'Cloning repo: {selected_repository}')
self.run_action(action)
def maybe_run_setup_script(self, selected_repository: str | None):
"""Run .openhands/setup.sh if it exists in the workspace or repository."""
setup_script = '.openhands/setup.sh'
def get_microagents_from_selected_repo(
self, selected_repository: str | None
) -> list[BaseMicroAgent]:
loaded_microagents: list[BaseMicroAgent] = []
dir_name = Path('.openhands') / 'microagents'
if selected_repository:
repo_name = selected_repository.split('/')[1]
setup_script = f'{repo_name}/.openhands/setup.sh'
dir_name = Path('/workspace') / selected_repository.split('/')[1] / dir_name
# Try to read the setup script
read_obs = self.read(FileReadAction(path=setup_script))
if isinstance(read_obs, ErrorObservation):
return
# Execute the script
action = CmdRunAction(f'chmod +x {setup_script} && {setup_script}')
obs = self.run_action(action)
if isinstance(obs, CmdOutputObservation) and obs.exit_code != 0:
self.log('error', f'Setup script failed: {obs.content}')
def get_custom_microagents(self, selected_repository: str | None) -> list[str]:
custom_microagents_content = []
custom_microagents_dir = Path('.openhands') / 'microagents'
dir_name = str(custom_microagents_dir)
if selected_repository:
dir_name = str(
Path(selected_repository.split('/')[1]) / custom_microagents_dir
)
# Legacy Repo Instructions
# Check for legacy .openhands_instructions file
obs = self.read(FileReadAction(path='.openhands_instructions'))
if isinstance(obs, ErrorObservation):
self.log('debug', 'openhands_instructions not present')
else:
openhands_instructions = obs.content
self.log('info', f'openhands_instructions: {openhands_instructions}')
custom_microagents_content.append(openhands_instructions)
self.log(
'debug',
f'openhands_instructions not present, trying to load from {dir_name}',
)
obs = self.read(
FileReadAction(path=str(dir_name / '.openhands_instructions'))
)
files = self.list_files(dir_name)
if isinstance(obs, FileReadObservation):
self.log('info', 'openhands_instructions microagent loaded.')
loaded_microagents.append(
BaseMicroAgent.load(
path='.openhands_instructions', file_content=obs.content
)
)
self.log('info', f'Found {len(files)} custom microagents.')
# Check for local repository microagents
files = self.list_files(str(dir_name))
self.log('info', f'Found {len(files)} local microagents.')
if 'repo.md' in files:
obs = self.read(FileReadAction(path=str(dir_name / 'repo.md')))
if isinstance(obs, FileReadObservation):
self.log('info', 'repo.md microagent loaded.')
loaded_microagents.append(
RepoMicroAgent.load(
path=str(dir_name / 'repo.md'), file_content=obs.content
)
)
for fname in files:
content = self.read(
FileReadAction(path=str(custom_microagents_dir / fname))
).content
custom_microagents_content.append(content)
if 'knowledge' in files:
knowledge_dir = dir_name / 'knowledge'
_knowledge_microagents_files = self.list_files(str(knowledge_dir))
for fname in _knowledge_microagents_files:
obs = self.read(FileReadAction(path=str(knowledge_dir / fname)))
if isinstance(obs, FileReadObservation):
self.log('info', f'knowledge/{fname} microagent loaded.')
loaded_microagents.append(
KnowledgeMicroAgent.load(
path=str(knowledge_dir / fname), file_content=obs.content
)
)
return custom_microagents_content
if 'tasks' in files:
tasks_dir = dir_name / 'tasks'
_tasks_microagents_files = self.list_files(str(tasks_dir))
for fname in _tasks_microagents_files:
obs = self.read(FileReadAction(path=str(tasks_dir / fname)))
if isinstance(obs, FileReadObservation):
self.log('info', f'tasks/{fname} microagent loaded.')
loaded_microagents.append(
TaskMicroAgent.load(
path=str(tasks_dir / fname), file_content=obs.content
)
)
return loaded_microagents
def run_action(self, action: Action) -> Observation:
"""Run an action and return the resulting observation.

View File

@@ -5,11 +5,6 @@ class E2BFileStore(FileStore):
def __init__(self, filesystem):
self.filesystem = filesystem
def get_full_path(self, path: str) -> str:
if path.startswith('/'):
path = path[1:]
return path
def write(self, path: str, contents: str) -> None:
self.filesystem.write(path, contents)

View File

@@ -21,7 +21,6 @@ from openhands.runtime.impl.action_execution.action_execution_client import (
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.utils.command import get_remote_startup_command
from openhands.runtime.utils.request import (
RequestHTTPError,
send_request,
)
from openhands.runtime.utils.runtime_build import build_runtime_image
@@ -367,10 +366,14 @@ class RemoteRuntime(ActionExecutionClient):
except requests.Timeout:
self.log('error', 'No response received within the timeout period.')
raise
except RequestHTTPError as e:
if e.response.status_code in (404, 502):
except requests.HTTPError as e:
if e.response.status_code == 404:
raise AgentRuntimeNotFoundError(
'Runtime unavailable: System resources may be exhausted due to running commands. This may be fixed by retrying.'
) from e
elif e.response.status_code == 502:
raise AgentRuntimeDisconnectedError(
f'{e.response.status_code} error while connecting to {self.runtime_url}'
'Runtime disconnected: System resources may be exhausted due to running commands. This may be fixed by retrying.'
) from e
elif e.response.status_code == 503:
self.log('warning', 'Runtime appears to be paused. Resuming...')

View File

@@ -0,0 +1,79 @@
from typing import Dict, List, Optional
from openhands.core.config import AppConfig
from openhands.core.exceptions import AgentRuntimeUnavailableError
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.utils.singleton import Singleton
class RuntimeManager(metaclass=Singleton):
def __init__(self, config: AppConfig):
self._runtimes: Dict[str, Runtime] = {}
self._config = config
@property
def config(self) -> AppConfig:
return self._config
async def create_runtime(
self,
event_stream: EventStream,
sid: str,
plugins: Optional[List[PluginRequirement]] = None,
env_vars: Optional[Dict[str, str]] = None,
status_callback=None,
attach_to_existing: bool = False,
headless_mode: bool = False,
) -> Runtime:
if sid in self._runtimes:
raise RuntimeError(f'Runtime with ID {sid} already exists')
runtime_class = get_runtime_cls(self.config.runtime)
logger.debug(f'Initializing runtime: {runtime_class.__name__}')
runtime = runtime_class(
config=self.config,
event_stream=event_stream,
sid=sid,
plugins=plugins,
env_vars=env_vars,
status_callback=status_callback,
attach_to_existing=attach_to_existing,
headless_mode=headless_mode,
)
try:
await runtime.connect()
except AgentRuntimeUnavailableError as e:
logger.error(f'Runtime initialization failed: {e}', exc_info=True)
if status_callback:
status_callback('error', 'STATUS$ERROR_RUNTIME_DISCONNECTED', str(e))
raise
self._runtimes[sid] = runtime
logger.info(
f'Created runtime with ID: {sid}. There are now {len(self._runtimes)} runtimes active.'
)
return runtime
def get_runtime(self, sid: str) -> Optional[Runtime]:
return self._runtimes.get(sid)
def list_runtimes(self) -> List[str]:
return list(self._runtimes.keys())
def destroy_runtime(self, sid: str) -> bool:
runtime = self._runtimes.get(sid)
if runtime:
del self._runtimes[sid]
runtime.close()
logger.info(f'Destroyed runtime with ID: {sid}')
return True
return False
async def destroy_all_runtimes(self):
for runtime_id in list(self._runtimes.keys()):
self.destroy_runtime(runtime_id)

View File

@@ -2,6 +2,9 @@ import json
from typing import Any
import requests
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential
from openhands.utils.tenacity_stop import stop_if_should_exit
class RequestHTTPError(requests.HTTPError):
@@ -18,6 +21,18 @@ class RequestHTTPError(requests.HTTPError):
return s
def is_rate_limit_error(exception):
return (
isinstance(exception, requests.HTTPError)
and exception.response.status_code == 429
)
@retry(
retry=retry_if_exception(is_rate_limit_error),
stop=stop_after_attempt(3) | stop_if_should_exit(),
wait=wait_exponential(multiplier=1, min=4, max=60),
)
def send_request(
session: requests.Session,
method: str,

View File

@@ -0,0 +1,14 @@
class Singleton(type):
"""Metaclass for creating singleton classes.
Usage:
class MyClass(metaclass=Singleton):
pass
"""
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]

View File

@@ -15,16 +15,19 @@ from openhands.server.middleware import (
LocalhostCORSMiddleware,
NoCacheMiddleware,
RateLimitMiddleware,
session_manager,
)
from openhands.server.routes.conversation import app as conversation_api_router
from openhands.server.routes.feedback import app as feedback_api_router
from openhands.server.routes.files import app as files_api_router
from openhands.server.routes.github import app as github_api_router
from openhands.server.routes.new_conversation import app as new_conversation_api_router
from openhands.server.routes.manage_conversations import (
app as manage_conversation_api_router,
)
from openhands.server.routes.public import app as public_api_router
from openhands.server.routes.security import app as security_api_router
from openhands.server.routes.settings import app as settings_router
from openhands.server.shared import openhands_config, session_manager
from openhands.server.shared import openhands_config
from openhands.utils.import_utils import get_impl
@@ -58,7 +61,7 @@ app.include_router(files_api_router)
app.include_router(security_api_router)
app.include_router(feedback_api_router)
app.include_router(conversation_api_router)
app.include_router(new_conversation_api_router)
app.include_router(manage_conversation_api_router)
app.include_router(settings_router)
app.include_router(github_api_router)

View File

@@ -14,9 +14,10 @@ from openhands.events.observation import (
from openhands.events.observation.agent import AgentStateChangedObservation
from openhands.events.serialization import event_to_dict
from openhands.events.stream import AsyncEventStreamWrapper
from openhands.server.middleware import session_manager
from openhands.server.routes.settings import ConversationStoreImpl, SettingsStoreImpl
from openhands.server.session.manager import ConversationDoesNotExistError
from openhands.server.shared import config, openhands_config, session_manager, sio
from openhands.server.shared import config, openhands_config, sio
from openhands.server.types import AppMode
from openhands.utils.async_utils import call_sync_from_async

View File

@@ -10,9 +10,12 @@ from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp
from openhands.server.shared import session_manager
from openhands.server.session import SessionManager
from openhands.server.shared import config, file_store, runtime_manager, sio
from openhands.server.types import SessionMiddlewareInterface
session_manager = SessionManager(sio, config, file_store)
class LocalhostCORSMiddleware(CORSMiddleware):
"""
@@ -121,7 +124,7 @@ class AttachConversationMiddleware(SessionMiddlewareInterface):
if request.url.path.startswith('/api/conversation'):
# FIXME: we should be able to use path_params
path_parts = request.url.path.split('/')
if len(path_parts) > 3:
if len(path_parts) > 4:
conversation_id = request.url.path.split('/')[3]
if not conversation_id:
return False
@@ -134,10 +137,17 @@ class AttachConversationMiddleware(SessionMiddlewareInterface):
"""
Attach the user's session based on the provided authentication token.
"""
request.state.conversation = await session_manager.attach_to_conversation(
request.state.sid
)
if not request.state.conversation:
request.state.runtime = runtime_manager.get_runtime(request.state.sid)
if request.state.runtime is None:
event_stream = await session_manager.get_event_stream(request.state.sid)
if event_stream:
request.state.runtime = await runtime_manager.create_runtime(
event_stream=event_stream,
sid=request.state.sid,
attach_to_existing=True,
headless_mode=False,
)
if not request.state.runtime:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={'error': 'Session not found'},
@@ -148,7 +158,7 @@ class AttachConversationMiddleware(SessionMiddlewareInterface):
"""
Detach the user's session.
"""
await session_manager.detach_from_conversation(request.state.conversation)
pass
async def __call__(self, request: Request, call_next: Callable):
if not self._should_attach(request):

View File

@@ -13,7 +13,7 @@ async def get_remote_runtime_config(request: Request):
Currently, this is the session ID and runtime ID (if available).
"""
runtime = request.state.conversation.runtime
runtime = request.state.runtime
runtime_id = runtime.runtime_id if hasattr(runtime, 'runtime_id') else None
session_id = runtime.sid if hasattr(runtime, 'sid') else None
return JSONResponse(
@@ -37,7 +37,7 @@ async def get_vscode_url(request: Request):
JSONResponse: A JSON response indicating the success of the operation.
"""
try:
runtime: Runtime = request.state.conversation.runtime
runtime: Runtime = request.state.runtime
logger.debug(f'Runtime type: {type(runtime)}')
logger.debug(f'Runtime VSCode URL: {runtime.vscode_url}')
return JSONResponse(status_code=200, content={'vscode_url': runtime.vscode_url})
@@ -81,12 +81,12 @@ async def search_events(
HTTPException: If conversation is not found
ValueError: If limit is less than 1 or greater than 100
"""
if not request.state.conversation:
if not request.state.runtime:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail='Conversation not found'
)
# Get matching events from the stream
event_stream = request.state.conversation.event_stream
event_stream = request.state.runtime.event_stream
matching_events = event_stream.get_matching_events(
query=query,
event_type=event_type,

View File

@@ -35,7 +35,7 @@ async def submit_feedback(request: Request, conversation_id: str):
# and there is a function to handle the storage.
body = await request.json()
async_stream = AsyncEventStreamWrapper(
request.state.conversation.event_stream, filter_hidden=True
request.state.runtime.event_stream, filter_hidden=True
)
trajectory = []
async for event in async_stream:

View File

@@ -58,13 +58,13 @@ async def list_files(request: Request, conversation_id: str, path: str | None =
Raises:
HTTPException: If there's an error listing the files.
"""
if not request.state.conversation.runtime:
if not request.state.runtime:
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={'error': 'Runtime not yet initialized'},
)
runtime: Runtime = request.state.conversation.runtime
runtime: Runtime = request.state.runtime
try:
file_list = await call_sync_from_async(runtime.list_files, path)
except AgentRuntimeUnavailableError as e:
@@ -124,7 +124,7 @@ async def select_file(file: str, request: Request):
Raises:
HTTPException: If there's an error opening the file.
"""
runtime: Runtime = request.state.conversation.runtime
runtime: Runtime = request.state.runtime
file = os.path.join(runtime.config.workspace_mount_path_in_sandbox, file)
read_action = FileReadAction(file)
@@ -199,7 +199,7 @@ async def upload_file(request: Request, conversation_id: str, files: list[Upload
tmp_file.write(file_contents)
tmp_file.flush()
runtime: Runtime = request.state.conversation.runtime
runtime: Runtime = request.state.runtime
try:
await call_sync_from_async(
runtime.copy_to,
@@ -276,7 +276,7 @@ async def save_file(request: Request):
raise HTTPException(status_code=400, detail='Missing filePath or content')
# Save the file to the agent's runtime file store
runtime: Runtime = request.state.conversation.runtime
runtime: Runtime = request.state.runtime
file_path = os.path.join(
runtime.config.workspace_mount_path_in_sandbox, file_path
)
@@ -316,7 +316,7 @@ async def zip_current_workspace(
):
try:
logger.debug('Zipping workspace')
runtime: Runtime = request.state.conversation.runtime
runtime: Runtime = request.state.runtime
path = runtime.config.workspace_mount_path_in_sandbox
try:
zip_file = await call_sync_from_async(runtime.copy_from, path)

View File

@@ -0,0 +1,224 @@
import uuid
from datetime import datetime
from typing import Callable
from fastapi import APIRouter, Body, Request
from fastapi.responses import JSONResponse
from github import Github
from pydantic import BaseModel
from openhands.core.logger import openhands_logger as logger
from openhands.events.stream import EventStreamSubscriber
from openhands.server.routes.settings import ConversationStoreImpl, SettingsStoreImpl
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.shared import config, session_manager
from openhands.storage.data_models.conversation_info import ConversationInfo
from openhands.storage.data_models.conversation_info_result_set import (
ConversationInfoResultSet,
)
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
from openhands.storage.data_models.conversation_status import ConversationStatus
from openhands.utils.async_utils import (
GENERAL_TIMEOUT,
call_async_from_sync,
call_sync_from_async,
wait_all,
)
app = APIRouter(prefix='/api')
UPDATED_AT_CALLBACK_ID = 'updated_at_callback_id'
class InitSessionRequest(BaseModel):
github_token: str | None = None
latest_event_id: int = -1
selected_repository: str | None = None
args: dict | None = None
@app.post('/conversations')
async def new_conversation(request: Request, data: InitSessionRequest):
"""Initialize a new session or join an existing one.
After successful initialization, the client should connect to the WebSocket
using the returned conversation ID
"""
logger.info('Initializing new conversation')
github_token = data.github_token or ''
logger.info('Loading settings')
settings_store = await SettingsStoreImpl.get_instance(config, github_token)
settings = await settings_store.load()
logger.info('Settings loaded')
session_init_args: dict = {}
if settings:
session_init_args = {**settings.__dict__, **session_init_args}
session_init_args['github_token'] = github_token
session_init_args['selected_repository'] = data.selected_repository
conversation_init_data = ConversationInitData(**session_init_args)
logger.info('Loading conversation store')
conversation_store = await ConversationStoreImpl.get_instance(config, github_token)
logger.info('Conversation store loaded')
conversation_id = uuid.uuid4().hex
while await conversation_store.exists(conversation_id):
logger.warning(f'Collision on conversation ID: {conversation_id}. Retrying...')
conversation_id = uuid.uuid4().hex
logger.info(f'New conversation ID: {conversation_id}')
user_id = ''
if data.github_token:
logger.info('Fetching Github user ID')
with Github(data.github_token) as g:
gh_user = await call_sync_from_async(g.get_user)
user_id = gh_user.id
logger.info(f'Saving metadata for conversation {conversation_id}')
await conversation_store.save_metadata(
ConversationMetadata(
conversation_id=conversation_id,
github_user_id=user_id,
selected_repository=data.selected_repository,
)
)
logger.info(f'Starting agent loop for conversation {conversation_id}')
event_stream = await session_manager.maybe_start_agent_loop(
conversation_id, conversation_init_data
)
try:
event_stream.subscribe(
EventStreamSubscriber.SERVER,
_create_conversation_update_callback(
data.github_token or '', conversation_id
),
UPDATED_AT_CALLBACK_ID,
)
except ValueError:
pass # Already subscribed - take no action
logger.info(f'Finished initializing conversation {conversation_id}')
return JSONResponse(content={'status': 'ok', 'conversation_id': conversation_id})
@app.get('/conversations')
async def search_conversations(
request: Request,
page_id: str | None = None,
limit: int = 20,
) -> ConversationInfoResultSet:
github_token = getattr(request.state, 'github_token', '') or ''
conversation_store = await ConversationStoreImpl.get_instance(config, github_token)
conversation_metadata_result_set = await conversation_store.search(page_id, limit)
conversation_ids = set(
conversation.conversation_id
for conversation in conversation_metadata_result_set.results
)
running_conversations = await session_manager.get_agent_loop_running(
set(conversation_ids)
)
result = ConversationInfoResultSet(
results=await wait_all(
_get_conversation_info(
conversation=conversation,
is_running=conversation.conversation_id in running_conversations,
)
for conversation in conversation_metadata_result_set.results
),
next_page_id=conversation_metadata_result_set.next_page_id,
)
return result
@app.get('/conversations/{conversation_id}')
async def get_conversation(
conversation_id: str, request: Request
) -> ConversationInfo | None:
github_token = getattr(request.state, 'github_token', '') or ''
conversation_store = await ConversationStoreImpl.get_instance(config, github_token)
try:
metadata = await conversation_store.get_metadata(conversation_id)
is_running = await session_manager.is_agent_loop_running(conversation_id)
conversation_info = await _get_conversation_info(metadata, is_running)
return conversation_info
except FileNotFoundError:
return None
@app.patch('/conversations/{conversation_id}')
async def update_conversation(
request: Request, conversation_id: str, title: str = Body(embed=True)
) -> bool:
github_token = getattr(request.state, 'github_token', '') or ''
conversation_store = await ConversationStoreImpl.get_instance(config, github_token)
metadata = await conversation_store.get_metadata(conversation_id)
if not metadata:
return False
metadata.title = title
await conversation_store.save_metadata(metadata)
return True
@app.delete('/conversations/{conversation_id}')
async def delete_conversation(
conversation_id: str,
request: Request,
) -> bool:
github_token = getattr(request.state, 'github_token', '') or ''
conversation_store = await ConversationStoreImpl.get_instance(config, github_token)
try:
await conversation_store.get_metadata(conversation_id)
except FileNotFoundError:
return False
is_running = await session_manager.is_agent_loop_running(conversation_id)
if is_running:
return False
await conversation_store.delete_metadata(conversation_id)
return True
async def _get_conversation_info(
conversation: ConversationMetadata,
is_running: bool,
) -> ConversationInfo | None:
try:
title = conversation.title
if not title:
title = f'Conversation {conversation.conversation_id[:5]}'
return ConversationInfo(
conversation_id=conversation.conversation_id,
title=title,
last_updated_at=conversation.last_updated_at,
selected_repository=conversation.selected_repository,
status=ConversationStatus.RUNNING
if is_running
else ConversationStatus.STOPPED,
)
except Exception: # type: ignore
logger.warning(
f'Error loading conversation: {conversation.conversation_id[:5]}',
exc_info=True,
stack_info=True,
)
return None
def _create_conversation_update_callback(
github_token: str, conversation_id: str
) -> Callable:
def callback(*args, **kwargs):
call_async_from_sync(
_update_timestamp_for_conversation,
GENERAL_TIMEOUT,
github_token,
conversation_id,
)
return callback
async def _update_timestamp_for_conversation(github_token: str, conversation_id: str):
conversation_store = await ConversationStoreImpl.get_instance(config, github_token)
conversation = await conversation_store.get_metadata(conversation_id)
conversation.last_updated_at = datetime.now()
await conversation_store.save_metadata(conversation)

View File

@@ -6,10 +6,14 @@ from github import Github
from pydantic import BaseModel
from openhands.core.logger import openhands_logger as logger
from openhands.server.data_models.conversation_metadata import ConversationMetadata
from openhands.server.routes.settings import ConversationStoreImpl, SettingsStoreImpl
from openhands.server.middleware import session_manager
from openhands.server.routes.settings import SettingsStoreImpl
from openhands.server.session.conversation_init_data import ConversationInitData
from openhands.server.shared import config, session_manager
from openhands.server.shared import config
from openhands.storage.conversation.conversation_store import (
ConversationMetadata,
ConversationStore,
)
from openhands.utils.async_utils import call_sync_from_async
app = APIRouter(prefix='/api')
@@ -28,42 +32,37 @@ async def new_conversation(request: Request, data: InitSessionRequest):
After successful initialization, the client should connect to the WebSocket
using the returned conversation ID
"""
logger.info('Initializing new conversation')
github_token = ''
if data.github_token:
github_token = data.github_token
logger.info('Loading settings')
settings_store = await SettingsStoreImpl.get_instance(config, github_token)
settings = await settings_store.load()
logger.info('Settings loaded')
session_init_args: dict = {}
if settings:
session_init_args = {**settings.__dict__, **session_init_args}
if data.args:
for key, value in data.args.items():
session_init_args[key.lower()] = value
session_init_args['github_token'] = github_token
session_init_args['selected_repository'] = data.selected_repository
conversation_init_data = ConversationInitData(**session_init_args)
logger.info('Loading conversation store')
conversation_store = await ConversationStoreImpl.get_instance(config, github_token)
logger.info('Conversation store loaded')
conversation_store = await ConversationStore.get_instance(config)
conversation_id = uuid.uuid4().hex
while await conversation_store.exists(conversation_id):
logger.warning(f'Collision on conversation ID: {conversation_id}. Retrying...')
conversation_id = uuid.uuid4().hex
logger.info(f'New conversation ID: {conversation_id}')
user_id = ''
if data.github_token:
logger.info('Fetching Github user ID')
with Github(data.github_token) as g:
gh_user = await call_sync_from_async(g.get_user)
user_id = gh_user.id
g = Github(data.github_token)
gh_user = await call_sync_from_async(g.get_user)
user_id = gh_user.id
logger.info(f'Saving metadata for conversation {conversation_id}')
await conversation_store.save_metadata(
ConversationMetadata(
conversation_id=conversation_id,
@@ -72,9 +71,7 @@ async def new_conversation(request: Request, data: InitSessionRequest):
)
)
logger.info(f'Starting agent loop for conversation {conversation_id}')
await session_manager.maybe_start_agent_loop(
conversation_id, conversation_init_data
)
logger.info(f'Finished initializing conversation {conversation_id}')
return JSONResponse(content={'status': 'ok', 'conversation_id': conversation_id})

View File

@@ -4,6 +4,9 @@ from fastapi import (
Request,
)
from openhands.security import SecurityAnalyzer, options
from openhands.server.shared import config
app = APIRouter(prefix='/api/conversations/{conversation_id}')
@@ -22,9 +25,10 @@ async def security_api(request: Request):
Raises:
HTTPException: If the security analyzer is not initialized.
"""
if not request.state.conversation.security_analyzer:
if not request.state.runtime:
raise HTTPException(status_code=404, detail='Security analyzer not initialized')
security_analyzer = options.SecurityAnalyzers.get(
config.security.security_analyzer or '', SecurityAnalyzer
)(request.state.runtime.event_stream)
return await request.state.conversation.security_analyzer.handle_api_request(
request
)
return await security_analyzer.handle_api_request(request)

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