Compare commits

..

56 Commits

Author SHA1 Message Date
Graham Neubig
ddd6bb3830 Update run_infer to add github prior knowledge 2025-01-21 21:25:08 -05:00
Engel Nyst
f0dbb02ee1 Adjust prompt to use view command (#5506)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-21 23:50:39 +01:00
tofarr
318c811817 Added check to shutdown hook (#6402) 2025-01-21 22:32:46 +00:00
Xingyao Wang
b468150f2a fix(codeact): make sure agent sees the prefix/suffix as part of observation (#6400) 2025-01-21 21:54:57 +00:00
Engel Nyst
b9a3f1c753 Fix eval on remote runtime (#6398) 2025-01-21 20:49:30 +00:00
tofarr
09e8a1eeba Fix: Keeping runtimes alive again (For now) (#6395) 2025-01-21 19:20:35 +00:00
Xingyao Wang
ff3880c76d fix(remote_runtime): define runtime_id first to fix attrbute error (#6393) 2025-01-21 18:13:43 +00:00
Calvin Smith
8bd7613724 fix: Settings modal properly tracks if an API key is set (#6394)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-01-21 11:04:30 -07:00
Engel Nyst
5b7fcfbe1a Disable prompt extensions in SWE-bench (#6391) 2025-01-21 17:18:30 +00:00
Robert Brennan
8ae36481df Fix API key again (#6390) 2025-01-21 17:00:59 +00:00
Robert Brennan
25fdb0c3bf fix api key value (#6388) 2025-01-21 16:15:28 +00:00
louria
7f57dbebda Update MiniWoB README (#6385) 2025-01-21 16:26:47 +01:00
dependabot[bot]
54589d7e83 chore(deps-dev): bump pre-commit from 4.0.1 to 4.1.0 in the pre-commit group (#6384)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 15:10:20 +00:00
Boxuan Li
b7f34c3f8d (feat) Add button to export trajectory on chat panel (#6378) 2025-01-21 22:10:00 +08:00
dependabot[bot]
210eeee94a chore(deps-dev): bump the eslint group in /frontend with 2 updates (#6358)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 13:46:56 +04:00
Robert Brennan
509892cf0e Revert changes to config defaults (#6370) 2025-01-21 04:23:21 +01:00
Engel Nyst
89963e93d8 Re-add reasoning effort (#6371) 2025-01-21 04:22:48 +01:00
tofarr
b6804f9e1e Fix: Static assets should not have the same rate limit (#6360)
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-20 21:55:49 +00:00
mamoodi
d30211da18 Update running OpenHands guide with detailed prerequisites (#6366) 2025-01-20 13:53:14 -05:00
Boxuan Li
06121bf20f chore(deps): Revert vite upgrade (#6349) 2025-01-20 19:11:32 +01:00
tofarr
541a445dfc Fix: API meta for OpenHands (#6295) 2025-01-20 09:47:57 -07:00
dependabot[bot]
03e496fb60 chore(deps): bump the version-all group with 7 updates (#6359)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 17:04:22 +01:00
Xingyao Wang
1b6e444ecb feat(remote runtime): do not resume runtime if not keep_runtime_alive (#6355)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-19 21:42:00 +00:00
Xingyao Wang
2b04ee2e62 feat(eval): reliability improvement for SWE-Bench eval_infer (#6347) 2025-01-18 14:02:59 -05:00
Boxuan Li
4383be1ab4 (feat) Add trajectory replay for headless mode (#6215) 2025-01-18 05:48:22 +00:00
tofarr
b4d20e3e18 Feat: settings default (#6328)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-17 20:17:18 -07:00
mamoodi
532c7cdf02 Attempt to fix doc deploy (#6337) 2025-01-18 00:16:47 +00:00
mamoodi
987861b5e7 Remove broken browser counter logic (#6334)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-17 22:41:31 +00:00
Calvin Smith
f07ec7a09c fix: Conversation creation accessing secret without unwrapping (#6335)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-01-17 22:16:57 +00:00
Xingyao Wang
b1fa6301f0 feat: add prompt for generating repo.md for an arbiratry repo (#6034)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-01-17 21:47:27 +00:00
Graham Neubig
62fbe4c622 docs: improve custom sandbox guide with more configuration options (#5589)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-18 05:05:41 +08:00
dependabot[bot]
a1a87af69d chore(deps): bump the version-all group across 1 directory with 18 updates (#6332)
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-17 23:46:57 +04:00
Calvin Smith
a12087243a Pydantic-based configuration and setting objects (#6321)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-17 12:33:22 -07:00
Xingyao Wang
899c1f8360 fix(bash): also show timeout reminder when no_change_timeout is triggered (#6318)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-18 03:31:23 +08:00
manna_and_poem
c5d7caf01f remove useless axtree str (#6315) 2025-01-17 17:50:40 +00:00
Engel Nyst
85a760e561 Simplify draft llm (#6281)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-17 17:38:02 +00:00
dependabot[bot]
8e9c315729 chore(deps): bump the version-all group with 8 updates (#6331)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-17 15:19:45 +00:00
Rohit Malhotra
000055ba73 Add initial user msg to /new_conversation route (#6314) 2025-01-17 14:43:03 +00:00
Aleksandr Kadykov
2edb2337c2 Fix typo in Development.md (#6330) 2025-01-17 13:51:53 +00:00
Amaechi-Okorie Onyedikachi Hope
7c8a0162ae feat: add slack etiquettes (#6178) 2025-01-17 01:28:23 +00:00
Robert Brennan
c10f18b3bd Better message when trying to reconnect (#6323) 2025-01-16 22:10:48 +00:00
tofarr
313c8eca20 Fix closing sessions (again) (#6322)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-16 15:03:38 -07:00
tofarr
eff9e07272 Fix for issue with user id (#6320) 2025-01-16 20:33:36 +00:00
Xingyao Wang
f8a3aeccd6 fix: Restore missing translation keys (#6317)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-16 19:21:46 +00:00
Robert Brennan
9375e0d756 fix browser async lock (#6316) 2025-01-16 19:17:17 +00:00
Xingyao Wang
72af7bbba2 feat(eval): misc SWE-Bench improvement - use different resources for different instances (#6313)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-17 02:48:41 +08:00
OpenHands
0661c69bd3 Fix issue #6273: [Feature]: Disable LitLLM Print Message (#6274)
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-01-17 02:43:55 +08:00
Xingyao Wang
0c961bfd8b refactor(prompt): move runtime/repo info to user message and disable them in eval (#6291) 2025-01-16 17:53:10 +00:00
Xingyao Wang
0bed17758f fix: incorrect soft-timeout implementation & fix hard-timeout follow-up command (#6280) 2025-01-17 01:27:00 +08:00
Ryan H. Tran
da1a6035ac Enable runtime build in openhands-resolver.yml (#6312) 2025-01-16 11:42:22 -05:00
Rohit Malhotra
df050e4786 Separate data extraction and convo creation logic (#6298) 2025-01-16 11:40:03 -05:00
Xingyao Wang
e211647eba fix: llm-proxy response_cost being 0 (#6293) 2025-01-16 15:33:22 +00:00
Alejandro Cuadron Lafuente
8579710c82 [Fix] Restored FC default for GPT-4o (#6311) 2025-01-16 15:27:57 +00:00
dependabot[bot]
8c35150c3e chore(deps-dev): bump llama-index from 0.12.10 to 0.12.11 in the llama group (#6308)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-16 16:18:37 +01:00
Alejandro Cuadron Lafuente
578291e961 Enabled native function calling for O1 + added support for reasoning_effort config in the config. (#6256)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-16 14:53:11 +00:00
Xingyao Wang
efe04baf34 Revert "Fix closing sessions" (#6300) 2025-01-16 09:14:56 -05:00
145 changed files with 3147 additions and 2024 deletions

View File

@@ -184,6 +184,7 @@ jobs:
});
- name: Install OpenHands
id: install_openhands
uses: actions/github-script@v7
env:
COMMENT_BODY: ${{ github.event.comment.body || '' }}
@@ -196,7 +197,6 @@ jobs:
const reviewBody = process.env.REVIEW_BODY.trim();
const labelName = process.env.LABEL_NAME.trim();
const eventName = process.env.EVENT_NAME.trim();
// Check conditions
const isExperimentalLabel = labelName === "fix-me-experimental";
const isIssueCommentExperimental =
@@ -205,6 +205,9 @@ jobs:
const isReviewCommentExperimental =
eventName === "pull_request_review" && reviewBody.includes("@openhands-agent-exp");
// Set output variable
core.setOutput('isExperimental', isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental);
// Perform package installation
if (isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental) {
console.log("Installing experimental OpenHands...");
@@ -230,7 +233,8 @@ jobs:
--issue-number ${{ env.ISSUE_NUMBER }} \
--issue-type ${{ env.ISSUE_TYPE }} \
--max-iterations ${{ env.MAX_ITERATIONS }} \
--comment-id ${{ env.COMMENT_ID }}
--comment-id ${{ env.COMMENT_ID }} \
--is-experimental ${{ steps.install_openhands.outputs.isExperimental }}
- name: Check resolution result
id: check_result

View File

@@ -1,167 +0,0 @@
---
name: wandb
type: AIOpsAgent
version: 0.0.1
agent: CodeActAgent
triggers:
- wandb
- weights and biases
- weights & biases
- weave
---
## wandagent
You are wandbagent, a specialized assistant created by Weights & Biases to help users with machine learning and AI developer workflows. You maintain friendly, helpful communication while keeping responses concise and to the point.
## Primary Products and Audiences:
1. W&B Models
- Uses the `wandb` Python library for MLOps lifecycle management
- Instll using `pip install wandb`, always ensure you're on the latest version
- Primary audience: Machine Learning engineers working in Python
- Features: Training, fine-tuning, reporting, hyperparameter sweep automation, registry for versioning model weights and datasets
2. W&B Weave
- Uses the `weave` library (available in Python and TypeScript)
- Install using `pip install weave` or `pnpm install weave` depending on the appropirate programming language to use. Always ensure you're on the latest version
- Primary audience: Software developers working in Python or TypeScript
- Features: Tracing, code logging, evaluation creation and visualization, dataset versioning, cost estimates, LLM playground, guardrails
- Note: Do not assume users have experience with data science or machine learning libraries
Authentication Protocol:
Always check for WANDB_API_KEY environment variable first. If not present, instruct users to:
1. Visit https://wandb.ai/authorize
2. Retrieve their API key
3. Provide the key to the agent
## Documentation Resources:
W&B Weave Documentation:
- User documentation: https://weave-docs.wandb.ai/
- Python reference: https://weave-docs.wandb.ai/reference/python-sdk/weave/
- TypeScript reference: https://weave-docs.wandb.ai/reference/typescript-sdk/weave/
- Service API reference: https://weave-docs.wandb.ai/reference/service-api/call-start-call-start-post
W&B Models Documentation:
- User guides: https://docs.wandb.ai/guides/
- Reference documentation: https://docs.wandb.ai/ref/
## Querying Weave Data
Weave data is stored in traces, which often have relevant information stored in child calls. You might be able to query for a trace/op name directly but other times you might need to traverse the tree of child calls to find the right op with the right data. Use the W&B documentation links above as well as the example code below to guide you on the correct api to use.
### How to Access Specific Calls in a Weave Trace
0. Filtering and Querying
Filtering Weave calls can be quite powerful, for example the following filter can be used to filter by op name. For example:
Filtering by Op Name "QueryEnhancer-call":
```python
"filter": {"op_names": ["weave:///wandbot/wandbot-dev/op/QueryEnhancer-call:*"]},
```
Querying by logged value "logging":
```python
"query": {"$expr":{"$contains":{"input":{"$getField":"inputs.inputs.query"},"substr":{"$literal":"logging"}}}},
```
Querying based on timestamp, after "12:00am January 14th, 2024" and before "12:00am January 16th 2024":
```python
"query": {"$expr":{"$and":[{"$gt":[{"$getField":"started_at"},{"$literal":1736809200}]},{"$not":[{"$gt":[{"$getField":"started_at"},{"$literal":1736982000}]}]}]}},
```
2. Get a Parent Trace
- Use calls_query or calls_query_stream to get a root trace:
```python
client = weave.init("project/name")
parent_calls = client.server.calls_query({
"project_id": "project/name",
"filter": {"trace_roots_only": True}, # Important: gets root traces only
"query": {"$expr": {"$eq": [{"$getField": "your.filter.path"}, {"$literal": "your_value"}]}},
"sort_by": [{"field": "started_at", "direction": "desc"}],
"limit": 1 # Get just one trace to start
})
parent = parent_calls.calls[0] # Get the first parent trace
```
3. Navigate the Trace Tree
- Get a call object using the client.get_call() method
- Use the children() method to traverse down the tree:
```python
call_obj = client.get_call(call_id=parent.id)
children = call_obj.children()
```
4. Search Through Children
- Iterate through children and look for your target operation:
```
for child in children:
print(f"Operation: {child.op_name}") # See what operations are available
if "YourTargetOperation" in child.op_name:
# Found it!
target_call = child
```
5. Access Nested Children
- Remember calls can have multiple levels - you may need to go deeper:
```
for child in call_obj.children():
child_obj = client.get_call(call_id=child.id)
for grandchild in child_obj.children():
if "YourTargetOperation" in grandchild.op_name:
# Found it at level 2!
target_call = grandchild
```
6. Access Call Data
- Once you find your target call, access its data:
- Inputs: target_call.inputs
- Outputs: target_call.output
- Metadata: target_call.started_at, target_call.id, etc.
Key Points:
- Don't filter too early - get the full trace tree first, then search within it
- Use `client.get_call()` and `.children()` to navigate the tree
- Check op_name to identify specific operations
- Be prepared to go multiple levels deep in the tree
- Remember calls are hierarchical: parent -> children -> grandchildren etc.
### Using Weave links
If the users provides you with a URL to a Weave project or trace, navigate to that link to help understand the request and the trace name or id being referred to as well as any of the relevant inputs, metadata or outputs to the conversation. If you are stuck and unable to find the correct data via the api you can ask the user to pass you a URL link to an example trace, from which you can extract useful information from the image.
## Results Management
Always offer to save analysis results or visualizations to W&B Reports.
Use the `wandb-workspaces` Python library for W&B Reports management.
Follow the W&B Reports documentation at https://docs.wandb.ai/guides/reports/ for:
- Creating new reports
- Editing existing reports
- Cloning reports
- Other report management tasks
Use the code examples from the W&B SDK tab in the documentation
## Error Handling
If repeatedly encountering errors using the `wandb` or `weave` api, make sure to search the documentation links provided above as well as doing a general internet search to help resolve the issue.
## Core Guidelines:
- Always verify that weave is installed before beginning to use the library
- Keep responses focused and concise while maintaining completeness
- Ensure proper Weights & Biases authentication before accessing any data
- Consider both technical audiences (ML engineers and software developers)
- Always offer options to save and share results to Weigths & Biases Reports
- Regularly reference the Weights & Biase documentation

View File

@@ -113,6 +113,20 @@ individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
### Slack and Discord Etiquettes
These Slack and Discord etiquette guidelines are designed to foster an inclusive, respectful, and productive environment for all community members. By following these best practices, we ensure effective communication and collaboration while minimizing disruptions. Lets work together to build a supportive and welcoming community!
- Communicate respectfully and professionally, avoiding sarcasm or harsh language, and remember that tone can be difficult to interpret in text.
- Use threads for specific discussions to keep channels organized and easier to follow.
- Tag others only when their input is critical or urgent, and use @here, @channel or @everyone sparingly to minimize disruptions.
- Be patient, as open-source contributors and maintainers often have other commitments and may need time to respond.
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://app.slack.com/client/T06P212QSEA/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions, [discord - #general](https://discord.com/channels/1222935860639563850/1222935861386018885)).
- When asking for help or raising issues, include necessary details like links, screenshots, or clear explanations to provide context.
- Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the matter is sensitive or private.
- Always adhere to [our standards](https://github.com/All-Hands-AI/OpenHands/blob/main/CODE_OF_CONDUCT.md#our-standards) to ensure a welcoming and collaborative environment.
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For example, if you're here for discussions about LLMs, mute the channel if its too busy, but set notifications to alert you only when “LLMs” appears in messages. Also for Discord, go to the channel notifications and choose the option that best describes your need.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],

View File

@@ -5,7 +5,7 @@ Otherwise, you can clone the OpenHands project directly.
## Start the Server for Development
### 1. Requirements
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install) [Ubuntu <= 22.04]
* Linux, Mac OS, or [WSL on Windows](https://learn.microsoft.com/en-us/windows/wsl/install) [Ubuntu >= 22.04]
* [Docker](https://docs.docker.com/engine/install/) (For those on MacOS, make sure to allow the default Docker socket to be used from advanced settings!)
* [Python](https://www.python.org/downloads/) = 3.12
* [NodeJS](https://nodejs.org/en/download/package-manager) >= 20.x

View File

@@ -39,7 +39,7 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [
## ⚡ Quick Start
The easiest way to run OpenHands is in Docker.
See the [Installation](https://docs.all-hands.dev/modules/usage/installation) guide for
See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) guide for
system requirements and more information.
```bash
@@ -69,7 +69,7 @@ run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
or run it on tagged issues with [a github action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
Visit [Installation](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
Visit [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
> [!CAUTION]
> OpenHands is meant to be run by a single user on their local workstation.

View File

@@ -23,6 +23,9 @@ workspace_base = "./workspace"
# Cache directory path
#cache_dir = "/tmp/cache"
# Reasoning effort for o1 models (low, medium, high, or not set)
#reasoning_effort = "medium"
# Debugging enabled
#debug = false
@@ -36,6 +39,11 @@ workspace_base = "./workspace"
# If it's a folder, the session id will be used as the file name
#save_trajectory_path="./trajectories"
# Path to replay a trajectory, must be a file path
# If provided, trajectory will be loaded and replayed before the
# agent responds to any user instruction
#replay_trajectory_path = ""
# File store path
#file_store_path = "/tmp/file_store"
@@ -220,8 +228,8 @@ codeact_enable_jupyter = true
# LLM config group to use
#llm_config = 'your-llm-config-group'
# Whether to use microagents at all
#use_microagents = true
# Whether to use prompt extension (e.g., microagent, repo/runtime info) at all
#enable_prompt_extensions = true
# List of microagents to disable
#disabled_microagents = []

View File

@@ -373,7 +373,7 @@ Les options de configuration de l'agent sont définies dans les sections `[agent
- Description : Si l'éditeur LLM est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)
**Utilisation du micro-agent**
- `use_microagents`
- `enable_prompt_extensions`
- Type : `bool`
- Valeur par défaut : `true`
- Description : Indique si l'utilisation des micro-agents est activée ou non

View File

@@ -373,7 +373,7 @@ Agent 配置选项在 `config.toml` 文件的 `[agent]` 和 `[agent.<agent_name>
- 描述: 是否在 action space 中启用 Jupyter
**Microagent 使用**
- `use_microagents`
- `enable_prompt_extensions`
- 类型: `bool`
- 默认值: `true`
- 描述: 是否使用 microagents

View File

@@ -55,6 +55,11 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `"./trajectories"`
- Description: Path to store trajectories (can be a folder or a file). If it's a folder, the trajectories will be saved in a file named with the session id name and .json extension, in that folder.
- `replay_trajectory_path`
- Type: `str`
- Default: `""`
- Description: Path to load a trajectory and replay. If given, must be a path to the trajectory file in JSON format. The actions in the trajectory file would be replayed first before any user instruction is executed.
### File Store
- `file_store_path`
- Type: `str`
@@ -336,7 +341,7 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Description: Whether Jupyter is enabled in the action space
### Microagent Usage
- `use_microagents`
- `enable_prompt_extensions`
- Type: `bool`
- Default: `true`
- Description: Whether to use microagents at all

View File

@@ -1,6 +1,6 @@
# Getting Started with OpenHands
So you've [installed OpenHands](./installation) and have
So you've [run OpenHands](./installation) and have
[set up your LLM](./installation#setup). Now what?
OpenHands can help you tackle a wide variety of engineering tasks. But the technology

View File

@@ -18,15 +18,21 @@ If you choose the first option, you can skip the `Create Your Docker Image` sect
To create a custom Docker image, it must be Debian based.
For example, if you want OpenHands to have `ruby` installed, create a `Dockerfile` with the following content:
For example, if you want OpenHands to have `ruby` installed, you could create a `Dockerfile` with the following content:
```dockerfile
FROM debian:latest
FROM nikolaik/python-nodejs:python3.12-nodejs22
# Install required packages
RUN apt-get update && apt-get install -y ruby
```
Or you could use a Ruby-specific base image:
```dockerfile
FROM ruby:latest
```
Save this file in a folder. Then, build your Docker image (e.g., named custom-image) by navigating to the folder in
the terminal and running::
```bash
@@ -55,6 +61,28 @@ This can be an image youve already pulled or one youve built:
sandbox_base_container_image="custom-image"
```
### Additional Configuration Options
The `config.toml` file supports several other options for customizing your sandbox:
```toml
[core]
# Install additional dependencies when the runtime is built
# Can contain any valid shell commands
# If you need the path to the Python interpreter in any of these commands, you can use the $OH_INTERPRETER_PATH variable
runtime_extra_deps = """
pip install numpy pandas
apt-get update && apt-get install -y ffmpeg
"""
# Set environment variables for the runtime
# Useful for configuration that needs to be available at runtime
runtime_startup_env_vars = { DATABASE_URL = "postgresql://user:pass@localhost/db" }
# Specify platform for multi-architecture builds (e.g., "linux/amd64" or "linux/arm64")
platform = "linux/amd64"
```
### Run
Run OpenHands by running ```make run``` in the top level directory.

View File

@@ -1,12 +1,51 @@
# Installation
# Running OpenHands
## 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).
- MacOS with [Docker Desktop support](https://docs.docker.com/desktop/setup/install/mac-install/#system-requirements)
- Linux
- Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements)
## Start the app
## Prerequisites
<details>
<summary>MacOS</summary>
### Docker Desktop
1. [Install Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install).
2. Open Docker Desktop, go to `Settings > Advanced` and ensure `Allow the default Docker socket to be used` is enabled.
</details>
<details>
<summary>Linux</summary>
:::note
Tested with Ubuntu 22.04.
:::
### Docker Desktop
1. [Install Docker Desktop on Linux](https://docs.docker.com/desktop/setup/install/linux/).
</details>
<details>
<summary>Windows</summary>
### WSL
1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
2. Run `wsl --version` in powershell and confirm `Default Version: 2`.
### Docker Desktop
1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install).
2. Open Docker Desktop, go to `Settings` and confirm the following:
- General: `Use the WSL 2 based engine` is enabled.
- Resources > WSL Integration: `Enable integration with my default WSL distro` is enabled.
</details>
## Start the App
The easiest way to run OpenHands is in Docker.

View File

@@ -101,6 +101,36 @@ In this example:
- Code generation uses GPT-4 with a higher token limit for generating larger code blocks
- The default configuration remains available for other tasks
# Custom Configurations with Reserved Names
OpenHands can use custom LLM configurations named with reserved names, for specific use cases. If you specify the model and other settings under the reserved names, then OpenHands will load and them for a specific purpose. As of now, one such configuration is implemented: draft editor.
## Draft Editor Configuration
The `draft_editor` configuration is a group of settings you can provide, to specify the model to use for preliminary drafting of code edits, for any tasks that involve editing and refining code. You need to provide it under the section `[llm.draft_editor]`.
For example, you can define in `config.toml` a draft editor like this:
```toml
[llm.draft_editor]
model = "gpt-4"
temperature = 0.2
top_p = 0.95
presence_penalty = 0.0
frequency_penalty = 0.0
```
This configuration:
- Uses GPT-4 for high-quality edits and suggestions
- Sets a low temperature (0.2) to maintain consistency while allowing some flexibility
- Uses a high top_p value (0.95) to consider a wide range of token options
- Disables presence and frequency penalties to maintain focus on the specific edits needed
Use this configuration when you want to let an LLM draft edits before making them. In general, it may be useful to:
- Review and suggest code improvements
- Refine existing content while maintaining its core meaning
- Make precise, focused changes to code or text
:::note
Custom LLM configurations are only available when using OpenHands in development mode, via `main.py` or `cli.py`. When running via `docker run`, please use the standard configuration options.
:::

View File

@@ -5,7 +5,7 @@ const sidebars: SidebarsConfig = {
docsSidebar: [
{
type: 'doc',
label: 'Installation',
label: 'Running OpenHands',
id: 'usage/installation',
},
{

View File

@@ -76,7 +76,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -60,7 +60,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -68,7 +68,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
# copy 'draft_editor' config if exists
config_copy = copy.deepcopy(config)

View File

@@ -74,7 +74,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -87,7 +87,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -51,7 +51,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -171,7 +171,7 @@ def initialize_runtime(
action = CmdRunAction(
command=f'git clone -b commit0_combined https://github.com/{instance["repo"]}.git'
)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -181,7 +181,7 @@ def initialize_runtime(
)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -191,7 +191,7 @@ def initialize_runtime(
)
action = CmdRunAction(command='git checkout -b openhands')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -201,7 +201,7 @@ def initialize_runtime(
# Install commit0
action = CmdRunAction(command='/root/.cargo/bin/uv pip install commit0')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -231,7 +231,7 @@ def complete_runtime(
workspace_dir_name = _get_commit0_workspace_dir_name(instance)
action = CmdRunAction(command='git add .')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -241,7 +241,7 @@ def complete_runtime(
)
action = CmdRunAction(command='git commit -m "openhands edits"')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -258,7 +258,7 @@ def complete_runtime(
action = CmdRunAction(
command=f"git diff {instance['base_commit']} HEAD -- . ':(exclude)spec.pdf.bz2'"
)
action.timeout = 600 + 100 * n_retries
action.set_hard_timeout(600 + 100 * n_retries)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -282,7 +282,7 @@ def complete_runtime(
action = CmdRunAction(
command=f"{instance['test']['test_cmd']} --json-report --json-report-file=report.json --continue-on-collection-errors {test_dir} > test_output.txt 2>&1"
)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -292,7 +292,7 @@ def complete_runtime(
)
# Read test output
action = CmdRunAction(command='cat test_output.txt')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -305,7 +305,7 @@ def complete_runtime(
# Save pytest exit code
action = CmdRunAction(command='echo $?')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -318,7 +318,7 @@ def complete_runtime(
# Read the test report
action = CmdRunAction(command='cat report.json')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -330,7 +330,7 @@ def complete_runtime(
repo_name = instance['repo'].split('/')[1]
repo_name = repo_name.replace('.', '-')
action = CmdRunAction(command=f'commit0 get-tests {repo_name}')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})

View File

@@ -78,7 +78,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
agent_config = AgentConfig(
function_calling=False,
codeact_enable_jupyter=True,

View File

@@ -63,7 +63,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -56,7 +56,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -77,7 +77,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -98,7 +98,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -62,7 +62,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -8,6 +8,9 @@ Please follow instruction [here](../../README.md#setup) to setup your local deve
## Test if your environment works
Follow the instructions here https://miniwob.farama.org/content/getting_started/ & https://miniwob.farama.org/content/viewing/
to set up MiniWoB server in your local environment at http://localhost:8080/miniwob/
Access with browser the above MiniWoB URLs and see if they load correctly.
## Run Evaluation

View File

@@ -120,7 +120,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -93,7 +93,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -1,3 +1,4 @@
import json
import os
import tempfile
import time
@@ -11,7 +12,11 @@ from swebench.harness.run_evaluation import (
)
from swebench.harness.test_spec import SWEbenchInstance, TestSpec, make_test_spec
from swebench.harness.utils import load_swebench_dataset
from tqdm import tqdm
from evaluation.benchmarks.swe_bench.resource.mapping import (
get_instance_resource_factor,
)
from evaluation.benchmarks.swe_bench.run_infer import get_instance_docker_image
from evaluation.utils.shared import (
EvalMetadata,
@@ -66,7 +71,7 @@ def process_git_patch(patch):
return patch
def get_config(instance: pd.Series) -> AppConfig:
def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
# We use a different instance image for the each instance of swe-bench eval
base_container_image = get_instance_docker_image(instance['instance_id'])
logger.info(
@@ -81,10 +86,14 @@ def get_config(instance: pd.Series) -> AppConfig:
base_container_image=base_container_image,
use_host_network=False,
# large enough timeout, since some testcases take very long to run
timeout=1800,
timeout=600,
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
remote_runtime_init_timeout=3600,
remote_runtime_resource_factor=get_instance_resource_factor(
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
),
),
# do not mount workspace
workspace_base=None,
@@ -123,7 +132,7 @@ def process_instance(
else:
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
config = get_config(instance)
config = get_config(metadata, instance)
instance_id = instance.instance_id
model_patch = instance['model_patch']
test_spec: TestSpec = instance['test_spec']
@@ -151,52 +160,52 @@ def process_instance(
if runtime_failure_count > 0:
config.sandbox.remote_runtime_resource_factor = min(
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
4, # hardcode maximum resource factor to 4
8,
)
logger.warning(
f'This is the second attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
# Get patch and save it to /tmp/patch.diff
with tempfile.TemporaryDirectory() as temp_dir:
# Patch file
patch_file_path = os.path.join(temp_dir, 'patch.diff')
with open(patch_file_path, 'w') as f:
f.write(model_patch)
runtime.copy_to(patch_file_path, '/tmp')
# Eval script
eval_script_path = os.path.join(temp_dir, 'eval.sh')
with open(eval_script_path, 'w') as f:
f.write(test_spec.eval_script)
runtime.copy_to(eval_script_path, '/tmp')
# Set +x
action = CmdRunAction(command='chmod +x /tmp/eval.sh')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
# Apply patch
exec_command = (
'cd /testbed && '
"(git apply -v /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
"(echo 'Failed to apply patch with git apply, trying with patch command...' && "
"(patch --batch --fuzz=5 -p1 -i /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
"echo 'APPLY_PATCH_FAIL')))"
)
action = CmdRunAction(command=exec_command)
action.timeout = 600
obs = runtime.run_action(action)
assert isinstance(obs, CmdOutputObservation)
apply_patch_output = obs.content
assert isinstance(apply_patch_output, str)
instance['test_result']['apply_patch_output'] = apply_patch_output
try:
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
# Get patch and save it to /tmp/patch.diff
with tempfile.TemporaryDirectory() as temp_dir:
# Patch file
patch_file_path = os.path.join(temp_dir, 'patch.diff')
with open(patch_file_path, 'w') as f:
f.write(model_patch)
runtime.copy_to(patch_file_path, '/tmp')
# Eval script
eval_script_path = os.path.join(temp_dir, 'eval.sh')
with open(eval_script_path, 'w') as f:
f.write(test_spec.eval_script)
runtime.copy_to(eval_script_path, '/tmp')
# Set +x
action = CmdRunAction(command='chmod +x /tmp/eval.sh')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
# Apply patch
exec_command = (
'cd /testbed && '
"(git apply -v /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
"(echo 'Failed to apply patch with git apply, trying with patch command...' && "
"(patch --batch --fuzz=5 -p1 -i /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
"echo 'APPLY_PATCH_FAIL')))"
)
action = CmdRunAction(command=exec_command)
action.set_hard_timeout(600)
obs = runtime.run_action(action)
assert isinstance(obs, CmdOutputObservation)
apply_patch_output = obs.content
assert isinstance(apply_patch_output, str)
instance['test_result']['apply_patch_output'] = apply_patch_output
if 'APPLY_PATCH_FAIL' in apply_patch_output:
logger.info(f'[{instance_id}] {APPLY_PATCH_FAIL}:\n{apply_patch_output}')
instance['test_result']['report']['failed_apply_patch'] = True
@@ -212,7 +221,7 @@ def process_instance(
# Run eval script in background and save output to log file
log_file = '/tmp/eval_output.log'
action = CmdRunAction(command=f'/tmp/eval.sh > {log_file} 2>&1 & echo $!')
action.timeout = 60 # Short timeout just to get the process ID
action.set_hard_timeout(300) # Short timeout just to get the process ID
obs = runtime.run_action(action)
if isinstance(obs, CmdOutputObservation) and obs.exit_code == 0:
@@ -235,7 +244,7 @@ def process_instance(
check_action = CmdRunAction(
command=f'ps -p {pid} > /dev/null; echo $?'
)
check_action.timeout = 60
check_action.set_hard_timeout(300)
check_obs = runtime.run_action(check_action)
if (
isinstance(check_obs, CmdOutputObservation)
@@ -252,7 +261,7 @@ def process_instance(
# Read the log file
cat_action = CmdRunAction(command=f'cat {log_file}')
cat_action.timeout = 300
cat_action.set_hard_timeout(300)
cat_obs = runtime.run_action(cat_action)
# Grade answer
@@ -352,7 +361,14 @@ if __name__ == '__main__':
# Load predictions
assert args.input_file.endswith('.jsonl'), 'Input file must be a jsonl file.'
predictions = pd.read_json(args.input_file, lines=True)
required_fields = ['instance_id', 'model_patch', 'test_result']
with open(args.input_file) as f:
predictions = pd.DataFrame.from_records(
[
{k: v for k, v in json.loads(line).items() if k in required_fields}
for line in tqdm(f, desc='Loading predictions')
]
)
assert (
'instance_id' in predictions.columns
), 'Input file must contain instance_id column.'

View File

@@ -0,0 +1,38 @@
"""Mapping instance_id to resource_factor.
Different instances may have different resource requirements.
e.g., some instances may require more memory/CPU to run inference.
This file tracks the resource requirements of different instances.
"""
import json
import os
from openhands.core.logger import openhands_logger as logger
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_RUNTIME_RESOURCE_FACTOR = int(
os.environ.get('DEFAULT_RUNTIME_RESOURCE_FACTOR', 1)
)
# dataset to resource mapping
_global_resource_mapping: dict[str, dict[str, float]] = {}
def get_resource_mapping(dataset_name: str) -> dict[str, float]:
if dataset_name not in _global_resource_mapping:
file_path = os.path.join(CUR_DIR, f'{dataset_name}.json')
if not os.path.exists(file_path):
logger.warning(f'Resource mapping for {dataset_name} not found.')
return None
with open(file_path, 'r') as f:
_global_resource_mapping[dataset_name] = json.load(f)
logger.info(f'Loaded resource mapping for {dataset_name}')
return _global_resource_mapping[dataset_name]
def get_instance_resource_factor(dataset_name: str, instance_id: str) -> int:
resource_mapping = get_resource_mapping(dataset_name)
if resource_mapping is None:
return DEFAULT_RUNTIME_RESOURCE_FACTOR
return int(resource_mapping.get(instance_id, DEFAULT_RUNTIME_RESOURCE_FACTOR))

View File

@@ -0,0 +1 @@
{"pydata__xarray-6721": 8, "pytest-dev__pytest-7236": 8, "matplotlib__matplotlib-24627": 4, "django__django-15561": 4, "django__django-15098": 4, "django__django-14771": 4, "sympy__sympy-21612": 4, "sympy__sympy-15345": 4, "psf__requests-5414": 4, "astropy__astropy-14508": 2, "django__django-11451": 2, "django__django-11477": 2, "django__django-10880": 2, "django__django-11163": 2, "django__django-11815": 2, "astropy__astropy-14369": 2, "django__django-10097": 2, "django__django-10554": 2, "django__django-12304": 2, "django__django-12325": 2, "django__django-11551": 2, "django__django-11734": 2, "django__django-13109": 2, "django__django-13089": 2, "django__django-13343": 2, "django__django-13363": 2, "django__django-13809": 2, "django__django-13810": 2, "django__django-13786": 2, "django__django-13807": 2, "django__django-14493": 2, "django__django-11820": 2, "django__django-11951": 2, "django__django-11964": 2, "astropy__astropy-14309": 2, "astropy__astropy-14365": 2, "astropy__astropy-12907": 2, "astropy__astropy-14182": 2, "django__django-15161": 2, "django__django-15128": 2, "django__django-14999": 2, "django__django-14915": 2, "django__django-14752": 2, "django__django-14765": 2, "django__django-14089": 2, "django__django-15252": 2, "django__django-15380": 2, "django__django-15382": 2, "django__django-15499": 2, "django__django-15467": 2, "django__django-15280": 2, "django__django-15315": 2, "django__django-15277": 2, "django__django-15268": 2, "django__django-15629": 2, "django__django-15695": 2, "django__django-15732": 2, "django__django-15863": 2, "django__django-16082": 2, "django__django-16145": 2, "django__django-16256": 2, "django__django-16429": 2, "django__django-16454": 2, "django__django-16493": 2, "matplotlib__matplotlib-13989": 2, "matplotlib__matplotlib-20488": 2, "django__django-15503": 2, "django__django-15525": 2, "django__django-15375": 2, "django__django-15278": 2, "matplotlib__matplotlib-21568": 2, "matplotlib__matplotlib-20859": 2, "matplotlib__matplotlib-20826": 2, "matplotlib__matplotlib-20676": 2, "matplotlib__matplotlib-23412": 2, "matplotlib__matplotlib-22719": 2, "matplotlib__matplotlib-23299": 2, "matplotlib__matplotlib-22865": 2, "matplotlib__matplotlib-24149": 2, "matplotlib__matplotlib-24177": 2, "matplotlib__matplotlib-24570": 2, "matplotlib__matplotlib-24637": 2, "matplotlib__matplotlib-24970": 2, "matplotlib__matplotlib-23476": 2, "matplotlib__matplotlib-24026": 2, "matplotlib__matplotlib-23314": 2, "matplotlib__matplotlib-25332": 2, "matplotlib__matplotlib-25311": 2, "matplotlib__matplotlib-25122": 2, "matplotlib__matplotlib-25479": 2, "matplotlib__matplotlib-26342": 2, "psf__requests-2317": 2, "matplotlib__matplotlib-25960": 2, "matplotlib__matplotlib-25775": 2, "pydata__xarray-4356": 2, "pydata__xarray-4075": 2, "pydata__xarray-6461": 2, "pydata__xarray-4687": 2, "pydata__xarray-6599": 2, "pylint-dev__pylint-4661": 2, "django__django-15554": 2, "django__django-15563": 2, "pytest-dev__pytest-5262": 2, "pytest-dev__pytest-10081": 2, "scikit-learn__scikit-learn-12973": 2, "scikit-learn__scikit-learn-13124": 2, "scikit-learn__scikit-learn-13779": 2, "scikit-learn__scikit-learn-14141": 2, "scikit-learn__scikit-learn-13439": 2, "scikit-learn__scikit-learn-13496": 2, "scikit-learn__scikit-learn-15100": 2, "scikit-learn__scikit-learn-25102": 2, "scikit-learn__scikit-learn-25232": 2, "scikit-learn__scikit-learn-25747": 2, "scikit-learn__scikit-learn-26323": 2, "scikit-learn__scikit-learn-9288": 2, "scikit-learn__scikit-learn-14496": 2, "scikit-learn__scikit-learn-14629": 2, "sphinx-doc__sphinx-8265": 2, "sphinx-doc__sphinx-8548": 2, "sphinx-doc__sphinx-8593": 2, "sphinx-doc__sphinx-8595": 2, "sphinx-doc__sphinx-8621": 2, "sphinx-doc__sphinx-8638": 2, "sphinx-doc__sphinx-9229": 2, "sphinx-doc__sphinx-9281": 2, "sphinx-doc__sphinx-9461": 2, "sphinx-doc__sphinx-9591": 2, "sphinx-doc__sphinx-9658": 2, "sphinx-doc__sphinx-9673": 2, "sympy__sympy-12096": 2, "sympy__sympy-12481": 2, "sphinx-doc__sphinx-10323": 2, "sphinx-doc__sphinx-7590": 2, "sympy__sympy-13877": 2, "sympy__sympy-12489": 2, "sympy__sympy-15809": 2, "sympy__sympy-14711": 2, "sympy__sympy-16597": 2, "sympy__sympy-16766": 2, "sympy__sympy-16792": 2, "sympy__sympy-15875": 2, "sympy__sympy-17655": 2, "sympy__sympy-18189": 2, "sympy__sympy-18763": 2, "sympy__sympy-19040": 2, "sympy__sympy-19495": 2, "sympy__sympy-19637": 2, "sympy__sympy-19783": 2, "sympy__sympy-17630": 2, "sympy__sympy-20428": 2, "sympy__sympy-20590": 2, "sympy__sympy-20801": 2, "sympy__sympy-21379": 2, "sympy__sympy-21847": 2, "sympy__sympy-22456": 2, "sympy__sympy-22714": 2, "sympy__sympy-22914": 2, "sympy__sympy-23262": 2, "sympy__sympy-23413": 2, "sympy__sympy-23534": 2, "sympy__sympy-24066": 2, "sympy__sympy-24213": 2, "sympy__sympy-24443": 2, "sympy__sympy-24562": 2, "sympy__sympy-24661": 2}

View File

@@ -9,6 +9,9 @@ import toml
from datasets import load_dataset
import openhands.agenthub
from evaluation.benchmarks.swe_bench.resource.mapping import (
get_instance_resource_factor,
)
from evaluation.utils.shared import (
EvalException,
EvalMetadata,
@@ -41,9 +44,10 @@ from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false').lower() == 'true'
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'true').lower() == 'true'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
}
@@ -67,15 +71,16 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
f'<pr_description>\n'
f'{instance.problem_statement}\n'
'</pr_description>\n\n'
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <pr_description> are met?\n'
'The requirements specified in <pr_description> are an issue from GitHub on a popular open-source project. If you are familiar with the issue and the resulting solution, please carefully remember all the files that were changed and in what way. Come up with a detailed plan to reproduce the patch.\n'
"I've already taken care of all changes to any of the test files described in the <pr_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!\n"
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied.\n'
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied, ideally with something similar to the existing patch from GitHub, but if you are not familiar with it just code it out.\n'
'Follow these steps to resolve the issue:\n'
'1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.\n'
'2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
'3. Edit the sourcecode of the repo to resolve the issue\n'
'4. Rerun your reproduce script and confirm that the error is fixed!\n'
'5. Think about edgecases and make sure your fix handles them as well\n'
'1. Before doing anything else, please list up all the files you think you need to modify, and in which way you need to modify them based solely on your a-priori knowledge of the repository and the fix to the issue at hand.'
'2. Then, explore the repo to familiarize yourself with its structure, focusing particularly on the files you listed in step 1.\n'
'3. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
'4. Edit the sourcecode of the repo to resolve the issue\n'
'5. Rerun your reproduce script and confirm that the error is fixed!\n'
'6. Think about edgecases and make sure your fix handles them as well\n'
"Your thinking should be thorough and so it's fine if it's very long.\n"
)
@@ -135,6 +140,10 @@ def get_config(
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
remote_runtime_resource_factor=get_instance_resource_factor(
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
),
),
# do not mount workspace
workspace_base=None,
@@ -150,6 +159,7 @@ def get_config(
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
config.set_agent_config(agent_config)
return config
@@ -173,7 +183,7 @@ def initialize_runtime(
action = CmdRunAction(
command=f"""echo 'export SWE_INSTANCE_ID={instance['instance_id']}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo "alias git='git --no-pager'" >> ~/.bashrc"""
)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -182,7 +192,7 @@ def initialize_runtime(
)
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -194,7 +204,7 @@ def initialize_runtime(
# inject the instance info
action = CmdRunAction(command='mkdir -p /swe_util/eval_data/instances')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -223,14 +233,14 @@ def initialize_runtime(
'/swe_util/',
)
action = CmdRunAction(command='cat ~/.bashrc')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to cat ~/.bashrc: {str(obs)}')
action = CmdRunAction(command='source ~/.bashrc')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -239,7 +249,7 @@ def initialize_runtime(
assert_and_raise(obs.exit_code == 0, f'Failed to source ~/.bashrc: {str(obs)}')
action = CmdRunAction(command='source /swe_util/instance_swe_entry.sh')
action.timeout = 3600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -249,7 +259,7 @@ def initialize_runtime(
)
else:
action = CmdRunAction(command='source /swe_util/swe_entry.sh')
action.timeout = 1800
action.set_hard_timeout(1800)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -259,7 +269,7 @@ def initialize_runtime(
)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -269,7 +279,7 @@ def initialize_runtime(
)
action = CmdRunAction(command='git reset --hard')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -278,14 +288,14 @@ def initialize_runtime(
action = CmdRunAction(
command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
action = CmdRunAction(command='which python')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -316,7 +326,7 @@ def complete_runtime(
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -326,7 +336,7 @@ def complete_runtime(
)
action = CmdRunAction(command='git config --global core.pager ""')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -336,7 +346,7 @@ def complete_runtime(
)
action = CmdRunAction(command='git add -A')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -351,7 +361,7 @@ def complete_runtime(
action = CmdRunAction(
command=f'git diff --no-color --cached {instance["base_commit"]}'
)
action.timeout = 600 + 100 * n_retries
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -399,7 +409,7 @@ def process_instance(
8,
)
logger.warning(
f'This is the second attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
@@ -479,6 +489,10 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
subset = dataset[dataset[filter_column].isin(selected_ids)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
if len(skip_ids) > 0:
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')
return dataset[~dataset[filter_column].isin(skip_ids)]
return dataset
@@ -501,8 +515,10 @@ if __name__ == '__main__':
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
# so we don't need to manage file uploading to OpenHands's repo
dataset = load_dataset(args.dataset, split=args.split)
logger.info(f'Loaded dataset {args.dataset} with split {args.split}')
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
logger.info(
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_bench_tests)} tasks'
)
llm_config = None
if args.llm_config:
@@ -531,6 +547,7 @@ if __name__ == '__main__':
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
print(f'### OUTPUT FILE: {output_file} ###')
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
if len(instances) > 0 and not isinstance(

View File

@@ -0,0 +1,69 @@
import argparse
import gzip
import json
import os
from glob import glob
from tqdm import tqdm
tqdm.pandas()
# Load trajectories for resolved instances
def load_completions(output_dir: str, instance_id: str):
glob_path = os.path.join(output_dir, 'llm_completions', instance_id, '*.json')
files = sorted(glob(glob_path)) # this is ascending order
# pick the last file (last turn)
try:
file_path = files[-1]
except IndexError:
# print(f'No files found for instance {instance_id}: files={files}')
return None
with open(file_path, 'r') as f:
result = json.load(f)
# create messages
messages = result['messages']
messages.append(result['response']['choices'][0]['message'])
tools = result['kwargs']['tools']
return {
'messages': messages,
'tools': tools,
}
parser = argparse.ArgumentParser()
parser.add_argument('jsonl_path', type=str)
args = parser.parse_args()
output_dir = os.path.dirname(args.jsonl_path)
output_path = os.path.join(output_dir, 'output.with_completions.jsonl.gz')
# Check if output would be different from input
needs_update = False
with open(args.jsonl_path, 'r') as f_in:
for line in tqdm(f_in, desc='Checking for changes'):
data = json.loads(line)
new_completions = load_completions(output_dir, data['instance_id'])
current_completions = data.get('raw_completions')
if current_completions != new_completions:
needs_update = True
break
if not needs_update:
print('No updates required. Skipping file update.')
exit(0)
if os.path.exists(output_path):
print(f'Output file already exists at {output_path}, overwriting? (y/n)')
if input() != 'y':
print('Exiting...')
exit(0)
# Process line by line
with open(args.jsonl_path, 'r') as f_in, gzip.open(output_path, 'wt') as f_out:
for line in tqdm(f_in):
data = json.loads(line)
data['raw_completions'] = load_completions(output_dir, data['instance_id'])
f_out.write(json.dumps(data) + '\n')
print(f'Saved compressed output to {output_path}')

View File

@@ -22,7 +22,8 @@ def convert_row_to_swebench_format(row):
elif 'test_result' in row and 'git_patch' in row['test_result']:
model_patch = row['test_result']['git_patch']
else:
raise ValueError(f'Row {row} does not have a git_patch')
print(f'WARNING: Row {row} does not have a git_patch')
model_patch = ''
return {
'instance_id': row['instance_id'],

View File

@@ -3,7 +3,7 @@ import json
import os
from collections import defaultdict
import pandas as pd
from tqdm import tqdm
parser = argparse.ArgumentParser()
parser.add_argument('input_file', type=str)
@@ -11,8 +11,7 @@ args = parser.parse_args()
dirname = os.path.dirname(args.input_file)
df = pd.read_json(args.input_file, lines=True)
# Initialize counters and data structures
instance_id_to_status = defaultdict(
lambda: {
'empty_generation': False,
@@ -23,15 +22,7 @@ instance_id_to_status = defaultdict(
}
)
# Apply the status to the dataframe
def apply_report(row):
instance_id = row['instance_id']
if instance_id in instance_id_to_status:
return dict(instance_id_to_status[instance_id])
return row.get('report', {})
# Process official report if it exists
swebench_official_report_json = os.path.join(dirname, 'report.json')
openhands_remote_report_jsonl = args.input_file.replace(
'.jsonl', '.swebench_eval.jsonl'
@@ -90,113 +81,159 @@ if os.path.exists(swebench_official_report_json):
f'- [{instance_id}](./eval_outputs/{instance_id}/run_instance.log)\n'
)
df['report'] = df.apply(apply_report, axis=1)
with open(output_md_filepath, 'w') as f:
f.write(output_md)
elif os.path.exists(openhands_remote_report_jsonl):
output_md_filepath = args.input_file.replace('.jsonl', '.swebench_eval.md')
df_eval = pd.read_json(openhands_remote_report_jsonl, lines=True, orient='records')
# First pass: Read eval report and count instances
instance_ids = set()
eval_instance_ids = set()
assert len(df['instance_id'].unique()) == len(
df
), 'There are duplicate instance ids in the original output which is not allowed'
assert len(df_eval['instance_id'].unique()) == len(
df_eval
), 'There are duplicate instance ids in the eval report which is not allowed'
# Count instances in original file
n_instances = 0
with open(args.input_file, 'r') as f:
for line in tqdm(f, desc='Counting instances in original file'):
data = json.loads(line)
instance_ids.add(data['instance_id'])
n_instances += 1
print(f'Total instances in original file: {n_instances}')
for _, row in df_eval.iterrows():
instance_id_to_status[row['instance_id']] = row['test_result']['report']
df['report'] = df.apply(apply_report, axis=1)
# Process eval report
n_eval_instances = 0
with open(openhands_remote_report_jsonl, 'r') as f:
for line in tqdm(f, desc='Processing eval report'):
data = json.loads(line)
instance_id = data['instance_id']
eval_instance_ids.add(instance_id)
n_eval_instances += 1
instance_id_to_status[instance_id] = data['test_result']['report']
print(f'Total instances in eval report: {n_eval_instances}')
report_is_dict = df['report'].apply(lambda x: isinstance(x, dict))
if not report_is_dict.all():
print(df[~report_is_dict])
raise ValueError(f'Report is not a dict, but a {type(row["report"])}')
# Verify no duplicates
assert (
len(instance_ids) == n_instances
), 'Duplicate instance ids found in original output'
assert (
len(eval_instance_ids) == n_eval_instances
), 'Duplicate instance ids found in eval report'
_n_instances = len(df)
_n_resolved = len(df[df['report'].apply(lambda x: x.get('resolved', False))])
_n_unresolved = _n_instances - _n_resolved
_n_empty_patch = len(
df[df['report'].apply(lambda x: x.get('empty_generation', False))]
)
_n_error = len(df[df['report'].apply(lambda x: x.get('error_eval', False))])
# Initialize counters
stats = {'total': len(instance_ids), 'resolved': 0, 'empty_patch': 0, 'error': 0}
# Collect instance IDs by category
resolved_ids = []
unresolved_ids = []
error_ids = []
empty_patch_ids = []
timeout_ids = []
# Process original file and categorize instances
with open(args.input_file, 'r') as f:
for line in f:
data = json.loads(line)
instance_id = data['instance_id']
report = instance_id_to_status[instance_id]
if report.get('resolved', False):
stats['resolved'] += 1
resolved_ids.append(instance_id)
else:
unresolved_ids.append(instance_id)
if report.get('empty_generation', False):
stats['empty_patch'] += 1
empty_patch_ids.append(instance_id)
if report.get('error_eval', False):
stats['error'] += 1
error_ids.append(instance_id)
if report.get('test_timeout', False):
timeout_ids.append(instance_id)
# Generate markdown report
def _instance_id_to_log_path(instance_id):
path = f"{args.input_file.replace('.jsonl', '.swebench_eval.logs')}/instance_{instance_id}.log"
return os.path.relpath(path, start=dirname)
# ... rest of markdown generation code remains the same ...
output_md = (
'# SWE-bench Report\n'
'This folder contains the evaluation results of the SWE-bench using the [official evaluation docker containerization](https://github.com/princeton-nlp/SWE-bench/blob/main/docs/20240627_docker/README.md#choosing-the-right-cache_level).\n\n'
'## Summary\n'
f'- submitted instances: {_n_instances}\n'
f'- empty patch instances: {_n_empty_patch}\n'
f'- resolved instances: {_n_resolved}\n'
f'- unresolved instances: {_n_unresolved}\n'
f'- error instances: {_n_error}\n'
f'- submitted instances: {stats["total"]}\n'
f'- empty patch instances: {stats["empty_patch"]}\n'
f'- resolved instances: {stats["resolved"]}\n'
f'- unresolved instances: {len(unresolved_ids)}\n'
f'- error instances: {stats["error"]}\n'
)
def _instance_id_to_log_path(instance_id):
path = f"{args.input_file.replace('.jsonl', '.swebench_eval.logs')}/instance_{instance_id}.log"
# make it relative path
path = os.path.relpath(path, start=dirname)
return path
output_md += '\n## Resolved Instances\n'
# instance_id to status
for instance_id in sorted(
df[df['report'].apply(lambda x: x.get('resolved', False))][
'instance_id'
].unique()
):
for instance_id in resolved_ids:
instance_id_to_status[instance_id]['resolved'] = True
output_md += f'- [{instance_id}]({_instance_id_to_log_path(instance_id)})\n'
output_md += '\n## Unresolved Instances\n'
for instance_id in sorted(
df[~df['report'].apply(lambda x: x.get('resolved', False))][
'instance_id'
].unique()
):
for instance_id in unresolved_ids:
output_md += f'- [{instance_id}]({_instance_id_to_log_path(instance_id)})\n'
output_md += '\n## Error Instances\n'
for instance_id in sorted(
df[df['report'].apply(lambda x: x.get('error_eval', False))][
'instance_id'
].unique()
):
for instance_id in error_ids:
instance_id_to_status[instance_id]['error_eval'] = True
output_md += f'- [{instance_id}]({_instance_id_to_log_path(instance_id)})\n'
output_md += '\n## Empty Patch Instances\n'
for instance_id in sorted(
df[df['report'].apply(lambda x: x.get('empty_generation', False))][
'instance_id'
].unique()
):
for instance_id in empty_patch_ids:
instance_id_to_status[instance_id]['empty_generation'] = True
output_md += f'- [{instance_id}]({_instance_id_to_log_path(instance_id)})\n'
output_md += '\n## Incomplete Instances\n'
for instance_id in sorted(
df[df['report'].apply(lambda x: x.get('test_timeout', False))][
'instance_id'
].unique()
):
for instance_id in timeout_ids:
output_md += f'- [{instance_id}]({_instance_id_to_log_path(instance_id)})\n'
with open(output_md_filepath, 'w') as f:
f.write(output_md)
else:
print(
f'No report file found: Both {swebench_official_report_json} and {openhands_remote_report_jsonl} do not exist.'
)
exit()
# Before backup and update, check if any changes would be made
needs_update = False
with open(args.input_file, 'r') as infile:
for line in tqdm(infile, desc='Checking for changes'):
data = json.loads(line)
instance_id = data['instance_id']
if instance_id in instance_id_to_status:
current_report = data.get('report', {})
new_report = instance_id_to_status[instance_id]
if current_report != new_report:
needs_update = True
break
if not needs_update:
print('No updates detected. Skipping file update.')
exit()
# Backup and update the original file row by row
if os.path.exists(args.input_file + '.bak'):
conf = input('Existing backup file found. Do you want to overwrite it? (y/n)')
if conf != 'y':
exit()
os.remove(args.input_file + '.bak')
# backup the original file
os.rename(args.input_file, args.input_file + '.bak')
df.to_json(args.input_file, orient='records', lines=True)
# Process and write file row by row
with open(args.input_file + '.bak', 'r') as infile, open(
args.input_file, 'w'
) as outfile:
for line in tqdm(infile, desc='Updating output file'):
data = json.loads(line)
instance_id = data['instance_id']
if instance_id in instance_id_to_status:
data['report'] = instance_id_to_status[instance_id]
outfile.write(json.dumps(data) + '\n')

View File

@@ -108,7 +108,14 @@ if [ -z "$N_RUNS" ]; then
echo "N_RUNS not specified, use default $N_RUNS"
fi
# Skip runs if the run number is in the SKIP_RUNS list
# read from env variable SKIP_RUNS as a comma separated list of run numbers
SKIP_RUNS=(${SKIP_RUNS//,/ })
for i in $(seq 1 $N_RUNS); do
if [[ " ${SKIP_RUNS[@]} " =~ " $i " ]]; then
echo "Skipping run $i"
continue
fi
current_eval_note="$EVAL_NOTE-run_$i"
echo "EVAL_NOTE: $current_eval_note"
run_eval $current_eval_note

View File

@@ -262,7 +262,7 @@ def pre_login(
instruction = action.to_instruction()
browser_action = BrowseInteractiveAction(browser_actions=instruction)
browser_action.timeout = 10000
browser_action.set_hard_timeout(10000)
logger.info(browser_action, extra={'msg_type': 'ACTION'})
obs: BrowserOutputObservation = runtime.run_action(browser_action)
logger.debug(obs, extra={'msg_type': 'OBSERVATION'})

View File

@@ -80,13 +80,13 @@ def load_dependencies(runtime: Runtime) -> List[str]:
def init_task_env(runtime: Runtime, hostname: str, env_llm_config: LLMConfig):
command = (
f'SERVER_HOSTNAME={hostname} '
f'LITELLM_API_KEY={env_llm_config.api_key} '
f'LITELLM_API_KEY={env_llm_config.api_key.get_secret_value() if env_llm_config.api_key else None} '
f'LITELLM_BASE_URL={env_llm_config.base_url} '
f'LITELLM_MODEL={env_llm_config.model} '
'bash /utils/init.sh'
)
action = CmdRunAction(command=command)
action.timeout = 900
action.set_hard_timeout(900)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -165,14 +165,14 @@ def run_evaluator(
runtime: Runtime, env_llm_config: LLMConfig, trajectory_path: str, result_path: str
):
command = (
f'LITELLM_API_KEY={env_llm_config.api_key} '
f'LITELLM_API_KEY={env_llm_config.api_key.get_secret_value() if env_llm_config.api_key else None} '
f'LITELLM_BASE_URL={env_llm_config.base_url} '
f'LITELLM_MODEL={env_llm_config.model} '
f"DECRYPTION_KEY='theagentcompany is all you need' " # Hardcoded Key
f'python_default /utils/eval.py --trajectory_path {trajectory_path} --result_path {result_path}'
)
action = CmdRunAction(command=command)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})

View File

@@ -57,7 +57,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -78,7 +78,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -0,0 +1,73 @@
import hashlib
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from openhands.events.action import (
AgentFinishAction,
FileWriteAction,
MessageAction,
)
from openhands.events.event import Event
from openhands.events.observation import AgentDelegateObservation
from openhands.runtime.base import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = 'Execute the python script /workspace/python_script.py with input "John" and "25" and tell me the secret number.'
SECRET_NUMBER = int(hashlib.sha256(str(25).encode()).hexdigest()[:8], 16) % 1000
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
from openhands.core.logger import openhands_logger as logger
action = FileWriteAction(
path='/workspace/python_script.py',
content=(
'name = input("Enter your name: "); age = input("Enter your age: "); '
'import hashlib; secret = int(hashlib.sha256(str(age).encode()).hexdigest()[:8], 16) % 1000; '
'print(f"Hello {name}, you are {age} years old. Tell you a secret number: {secret}")'
),
)
logger.info(action, extra={'msg_type': 'ACTION'})
observation = runtime.run_action(action)
logger.info(observation, extra={'msg_type': 'OBSERVATION'})
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
from openhands.core.logger import openhands_logger as logger
# check if the license information is in any message
message_actions = [
event
for event in histories
if isinstance(
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
)
]
logger.info(f'Total message-like events: {len(message_actions)}')
for event in message_actions:
try:
if isinstance(event, AgentDelegateObservation):
content = event.content
elif isinstance(event, AgentFinishAction):
content = event.outputs.get('content', '')
if event.thought:
content += f'\n\n{event.thought}'
elif isinstance(event, MessageAction):
content = event.content
else:
logger.warning(f'Unexpected event type: {type(event)}')
continue
if str(cls.SECRET_NUMBER) in content:
return TestResult(success=True)
except Exception as e:
logger.error(f'Error processing event: {e}')
logger.debug(
f'Total messages: {len(message_actions)}. Messages: {message_actions}'
)
return TestResult(
success=False,
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}.',
)

View File

@@ -52,30 +52,6 @@ class EvalMetadata(BaseModel):
details: dict[str, Any] | None = None
condenser_config: CondenserConfig | None = None
def model_dump(self, *args, **kwargs):
dumped_dict = super().model_dump(*args, **kwargs)
# avoid leaking sensitive information
dumped_dict['llm_config'] = self.llm_config.to_safe_dict()
if hasattr(self.condenser_config, 'llm_config'):
dumped_dict['condenser_config']['llm_config'] = (
self.condenser_config.llm_config.to_safe_dict()
)
return dumped_dict
def model_dump_json(self, *args, **kwargs):
dumped = super().model_dump_json(*args, **kwargs)
dumped_dict = json.loads(dumped)
# avoid leaking sensitive information
dumped_dict['llm_config'] = self.llm_config.to_safe_dict()
if hasattr(self.condenser_config, 'llm_config'):
dumped_dict['condenser_config']['llm_config'] = (
self.condenser_config.llm_config.to_safe_dict()
)
logger.debug(f'Dumped metadata: {dumped_dict}')
return json.dumps(dumped_dict)
class EvalOutput(BaseModel):
# NOTE: User-specified
@@ -98,23 +74,6 @@ class EvalOutput(BaseModel):
# Optionally save the input test instance
instance: dict[str, Any] | None = None
def model_dump(self, *args, **kwargs):
dumped_dict = super().model_dump(*args, **kwargs)
# Remove None values
dumped_dict = {k: v for k, v in dumped_dict.items() if v is not None}
# Apply custom serialization for metadata (to avoid leaking sensitive information)
if self.metadata is not None:
dumped_dict['metadata'] = self.metadata.model_dump()
return dumped_dict
def model_dump_json(self, *args, **kwargs):
dumped = super().model_dump_json(*args, **kwargs)
dumped_dict = json.loads(dumped)
# Apply custom serialization for metadata (to avoid leaking sensitive information)
if 'metadata' in dumped_dict:
dumped_dict['metadata'] = json.loads(self.metadata.model_dump_json())
return json.dumps(dumped_dict)
class EvalException(Exception):
pass
@@ -314,7 +273,7 @@ def update_progress(
logger.info(
f'Finished evaluation for instance {result.instance_id}: {str(result.test_result)[:300]}...\n'
)
output_fp.write(json.dumps(result.model_dump()) + '\n')
output_fp.write(result.model_dump_json() + '\n')
output_fp.flush()
@@ -371,7 +330,6 @@ def _process_instance_wrapper(
error = str(e)
stacktrace = traceback.format_exc()
if attempt == max_retries:
logger.exception(e)
msg = (
'-' * 10
+ '\n'
@@ -395,19 +353,15 @@ def _process_instance_wrapper(
+ '-' * 10
+ '\n'
)
if isinstance(
e,
(
AgentRuntimeDisconnectedError,
AgentRuntimeUnavailableError,
AgentRuntimeNotFoundError,
),
):
# e is likely an EvalException, so we can't directly infer it from type
# but rather check if it's a fatal error
# But it can also be AgentRuntime**Error (e.g., swe_bench/eval_infer.py)
_error_str = type(e).__name__ + ': ' + str(e)
if is_fatal_runtime_error(_error_str):
runtime_failure_count += 1
msg += f'Runtime disconnected error detected for instance {instance.instance_id}, runtime failure count: {runtime_failure_count}'
msg += '\n' + '-' * 10 + '\n'
logger.error(msg)
if use_mp:
print(msg) # use print to directly print to console
time.sleep(5)
@@ -564,6 +518,7 @@ def is_fatal_evaluation_error(error: str | None) -> bool:
AgentRuntimeNotReadyError,
AgentRuntimeDisconnectedError,
AgentRuntimeNotFoundError,
ConnectionError,
]
if any(exception.__name__ in error for exception in FATAL_EXCEPTIONS):
@@ -573,6 +528,24 @@ def is_fatal_evaluation_error(error: str | None) -> bool:
return False
def is_fatal_runtime_error(error: str | None) -> bool:
if not error:
return False
FATAL_RUNTIME_ERRORS = [
AgentRuntimeTimeoutError,
AgentRuntimeUnavailableError,
AgentRuntimeDisconnectedError,
AgentRuntimeNotFoundError,
]
if any(exception.__name__ in error for exception in FATAL_RUNTIME_ERRORS):
logger.error(f'Fatal runtime error detected: {error}')
return True
return False
def get_metrics(state: State) -> dict[str, Any]:
"""Extract metrics from the state."""
metrics = state.metrics.get() if state.metrics else {}

View File

@@ -37,7 +37,6 @@ describe("Browser", () => {
browser: {
url: "https://example.com",
screenshotSrc: "",
updateCount: 0,
},
},
});
@@ -53,7 +52,6 @@ describe("Browser", () => {
url: "https://example.com",
screenshotSrc:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
updateCount: 0,
},
},
});

View File

@@ -5,7 +5,10 @@ import { ContextMenuListItem } from "#/components/features/context-menu/context-
describe("ContextMenuListItem", () => {
it("should render the component with the children", () => {
render(<ContextMenuListItem onClick={vi.fn}>Test</ContextMenuListItem>);
const onClickMock = vi.fn();
render(
<ContextMenuListItem onClick={onClickMock}>Test</ContextMenuListItem>,
);
expect(screen.getByTestId("context-menu-list-item")).toBeInTheDocument();
expect(screen.getByText("Test")).toBeInTheDocument();

View File

@@ -156,7 +156,7 @@ describe("Sidebar", () => {
await user.click(advancedOptionsSwitch);
const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i);
await user.type(apiKeyInput, "SET");
await user.type(apiKeyInput, "**********");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",

View File

@@ -4,13 +4,21 @@ import { describe, it, vi, expect } from "vitest";
import { BaseModal } from "#/components/shared/modals/base-modal/base-modal";
describe("BaseModal", () => {
const onOpenChangeMock = vi.fn();
it("should render if the modal is open", () => {
const { rerender } = render(
<BaseModal isOpen={false} onOpenChange={vi.fn} title="Settings" />,
<BaseModal
isOpen={false}
onOpenChange={onOpenChangeMock}
title="Settings"
/>,
);
expect(screen.queryByText("Settings")).not.toBeInTheDocument();
rerender(<BaseModal title="Settings" onOpenChange={vi.fn} isOpen />);
rerender(
<BaseModal title="Settings" onOpenChange={onOpenChangeMock} isOpen />,
);
expect(screen.getByText("Settings")).toBeInTheDocument();
});
@@ -18,7 +26,7 @@ describe("BaseModal", () => {
render(
<BaseModal
isOpen
onOpenChange={vi.fn}
onOpenChange={onOpenChangeMock}
title="Settings"
subtitle="Subtitle"
/>,
@@ -43,7 +51,7 @@ describe("BaseModal", () => {
render(
<BaseModal
isOpen
onOpenChange={vi.fn}
onOpenChange={onOpenChangeMock}
title="Settings"
actions={[primaryAction, secondaryAction]}
/>,
@@ -60,7 +68,6 @@ describe("BaseModal", () => {
});
it("should close the modal after an action is performed", async () => {
const onOpenChangeMock = vi.fn();
render(
<BaseModal
isOpen
@@ -82,7 +89,7 @@ describe("BaseModal", () => {
it("should render children", () => {
render(
<BaseModal isOpen onOpenChange={vi.fn} title="Settings">
<BaseModal isOpen onOpenChange={onOpenChangeMock} title="Settings">
<div>Children</div>
</BaseModal>,
);
@@ -93,7 +100,7 @@ describe("BaseModal", () => {
const { rerender } = render(
<BaseModal
isOpen
onOpenChange={vi.fn}
onOpenChange={onOpenChangeMock}
title="Settings"
actions={[
{
@@ -110,7 +117,7 @@ describe("BaseModal", () => {
rerender(
<BaseModal
isOpen
onOpenChange={vi.fn}
onOpenChange={onOpenChangeMock}
title="Settings"
actions={[
{
@@ -126,7 +133,6 @@ describe("BaseModal", () => {
});
it.skip("should not close if the backdrop or escape key is pressed", () => {
const onOpenChangeMock = vi.fn();
render(
<BaseModal
isOpen

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,11 @@
"dependencies": {
"@monaco-editor/react": "^4.7.0-rc.0",
"@nextui-org/react": "^2.6.11",
"@react-router/node": "^7.1.1",
"@react-router/serve": "^7.1.1",
"@react-types/shared": "^3.25.0",
"@react-router/node": "^7.1.2",
"@react-router/serve": "^7.1.2",
"@react-types/shared": "^3.27.0",
"@reduxjs/toolkit": "^2.5.0",
"@tanstack/react-query": "^5.63.0",
"@tanstack/react-query": "^5.64.1",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
@@ -26,7 +26,7 @@
"isbot": "^5.1.21",
"jose": "^5.9.4",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.205.0",
"posthog-js": "^1.207.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-highlight": "^0.15.0",
@@ -35,7 +35,7 @@
"react-icons": "^5.4.0",
"react-markdown": "^9.0.3",
"react-redux": "^9.2.0",
"react-router": "^7.1.1",
"react-router": "^7.1.2",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.7",
"remark-gfm": "^4.0.0",
@@ -77,21 +77,21 @@
"devDependencies": {
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.49.1",
"@react-router/dev": "^7.1.1",
"@react-router/dev": "^7.1.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.62.16",
"@tanstack/eslint-plugin-query": "^5.64.2",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.10.5",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.0",
"@types/node": "^22.10.7",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^1.6.0",
"@vitest/coverage-v8": "^3.0.2",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
@@ -100,20 +100,20 @@
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^26.0.0",
"lint-staged": "^15.3.0",
"lint-staged": "^15.4.1",
"msw": "^2.6.6",
"postcss": "^8.4.47",
"postcss": "^8.5.1",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^1.6.0"
"vitest": "^3.0.2"
},
"packageManager": "npm@10.5.0",
"volta": {

View File

@@ -10,6 +10,7 @@ import {
AuthenticateResponse,
Conversation,
ResultSet,
GetTrajectoryResponse,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { ApiSettings } from "#/services/settings";
@@ -354,6 +355,15 @@ class OpenHands {
return response.data.items;
}
static async getTrajectory(
conversationId: string,
): Promise<GetTrajectoryResponse> {
const { data } = await openHands.get<GetTrajectoryResponse>(
`/api/conversations/${conversationId}/trajectory`,
);
return data;
}
}
export default OpenHands;

View File

@@ -55,6 +55,11 @@ export interface GetVSCodeUrlResponse {
error?: string;
}
export interface GetTrajectoryResponse {
trajectory: unknown[] | null;
error?: string;
}
export interface AuthenticateResponse {
message?: string;
error?: string;

View File

@@ -1,8 +1,11 @@
import { useDispatch, useSelector } from "react-redux";
import toast from "react-hot-toast";
import React from "react";
import posthog from "posthog-js";
import { useParams } from "react-router";
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
import { FeedbackActions } from "../feedback/feedback-actions";
import { ExportActions } from "../export/export-actions";
import { createChatMessage } from "#/services/chat-service";
import { InteractiveChatBox } from "./interactive-chat-box";
import { addUserMessage } from "#/state/chat-slice";
@@ -19,6 +22,8 @@ import { ActionSuggestions } from "./action-suggestions";
import { ContinueButton } from "#/components/shared/buttons/continue-button";
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
import { downloadTrajectory } from "#/utils/download-files";
function getEntryPoint(
hasRepository: boolean | null,
@@ -47,6 +52,8 @@ export function ChatInterface() {
const { selectedRepository, importedProjectZip } = useSelector(
(state: RootState) => state.initialQuery,
);
const params = useParams();
const { mutate: getTrajectory } = useGetTrajectory();
const handleSendMessage = async (content: string, files: File[]) => {
if (messages.length === 0) {
@@ -90,6 +97,25 @@ export function ChatInterface() {
setFeedbackPolarity(polarity);
};
const onClickExportTrajectoryButton = () => {
if (!params.conversationId) {
toast.error("ConversationId unknown, cannot download trajectory");
return;
}
getTrajectory(params.conversationId, {
onSuccess: async (data) => {
await downloadTrajectory(
params.conversationId ?? "unknown",
data.trajectory,
);
},
onError: (error) => {
toast.error(error.message);
},
});
};
const isWaitingForUserInput =
curAgentState === AgentState.AWAITING_USER_INPUT ||
curAgentState === AgentState.FINISHED;
@@ -137,6 +163,9 @@ export function ChatInterface() {
onClickShareFeedbackActionButton("negative")
}
/>
<ExportActions
onExportTrajectory={() => onClickExportTrajectoryButton()}
/>
<div className="absolute left-1/2 transform -translate-x-1/2 bottom-0">
{messages.length > 2 &&

View File

@@ -5,11 +5,16 @@ import toast from "react-hot-toast";
import { RootState } from "#/store";
import { AgentState } from "#/types/agent-state";
import { AGENT_STATUS_MAP } from "../../agent-status-map.constant";
import {
useWsClient,
WsClientProviderStatus,
} from "#/context/ws-client-provider";
export function AgentStatusBar() {
const { t, i18n } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { curStatusMessage } = useSelector((state: RootState) => state.status);
const { status } = useWsClient();
const [statusMessage, setStatusMessage] = React.useState<string>("");
@@ -37,7 +42,11 @@ export function AgentStatusBar() {
}, [curStatusMessage.id]);
React.useEffect(() => {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
if (status === WsClientProviderStatus.DISCONNECTED) {
setStatusMessage("Trying to reconnect...");
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);
}
}, [curAgentState]);
return (

View File

@@ -0,0 +1,17 @@
import ExportIcon from "#/icons/export.svg?react";
import { ExportActionButton } from "#/components/shared/buttons/export-action-button";
interface ExportActionsProps {
onExportTrajectory: () => void;
}
export function ExportActions({ onExportTrajectory }: ExportActionsProps) {
return (
<div data-testid="export-actions" className="flex gap-1">
<ExportActionButton
onClick={onExportTrajectory}
icon={<ExportIcon width={15} height={15} />}
/>
</div>
);
}

View File

@@ -0,0 +1,17 @@
interface ExportActionButtonProps {
onClick: () => void;
icon: React.ReactNode;
}
export function ExportActionButton({ onClick, icon }: ExportActionButtonProps) {
return (
<button
type="button"
onClick={onClick}
className="button-base p-1 hover:bg-neutral-500"
title="Export trajectory"
>
{icon}
</button>
);
}

View File

@@ -171,7 +171,7 @@ export function SettingsForm({
<APIKeyInput
isDisabled={!!disabled}
isSet={settings.LLM_API_KEY === "SET"}
isSet={settings.LLM_API_KEY === "**********"}
/>
{showAdvancedOptions && (

View File

@@ -34,7 +34,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
...newSettings,
};
if (updatedSettings.LLM_API_KEY === "SET") {
if (updatedSettings.LLM_API_KEY === "**********") {
delete updatedSettings.LLM_API_KEY;
}

View File

@@ -81,6 +81,9 @@ export function updateStatusWhenErrorMessagePresent(data: ErrorArg | unknown) {
!!val && typeof val === "object";
const isString = (val: unknown): val is string => typeof val === "string";
if (isObject(data) && "message" in data && isString(data.message)) {
if (data.message === "websocket error") {
return;
}
let msgId: string | undefined;
if (
"data" in data &&

View File

@@ -0,0 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
export const useGetTrajectory = () =>
useMutation({
mutationFn: (cid: string) => OpenHands.getTrajectory(cid),
});

View File

@@ -1259,6 +1259,28 @@
"no": "Kunne ikke hente modeller og agenter",
"ja": "モデルとエージェントの取得に失敗しました"
},
"CONFIGURATION$SETTINGS_NOT_FOUND": {
"en": "Settings not found. Please check your API key",
"es": "Configuraciones no encontradas. Por favor revisa tu API key"
},
"CONNECT_TO_GITHUB_BY_TOKEN_MODAL$TERMS_OF_SERVICE": {
"en": "terms of service",
"es": "términos de servicio"
},
"SESSION$SERVER_CONNECTED_MESSAGE": {
"en": "Connected to server",
"zh-CN": "已连接到服务器",
"de": "Verbindung zum Server hergestellt",
"zh-TW": "已連接到伺服器",
"es": "Conectado al servidor",
"fr": "Connecté au serveur",
"it": "Connesso al server",
"pt": "Conectado ao servidor",
"ko-KR": "서버에 연결됨",
"ar": "تم الاتصال بالخادم",
"tr": "Sunucuya bağlandı",
"no": "Koblet til server"
},
"SESSION$SESSION_HANDLING_ERROR_MESSAGE": {
"en": "Error handling message",
"zh-CN": "处理会话时出错",

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 16" fill="none">
<path
d="M11.875 9.5h-2.5V3.25c0-.16576-.0658-.32473-.1831-.44194-.1172-.11721-.276-.18306-.4419-.18306h-2.5c-.16576 0-.32473.06585-.44194.18306C5.68585 2.92527 5.62 3.08424 5.62 3.25V9.5h-2.5c-.13855 0-.27293.0483-.38002.1367-.10708.0883-.18294.2124-.21493.3508-.03199.1385-.01839.2839.03873.4142.05712.1304.15543.2397.27872.3108l4.375 2.5c.09664.0552.20607.0842.3175.0842.11144 0 .22087-.029.3175-.0842l4.375-2.5c.1233-.0711.2216-.1804.2787-.3108.0571-.1303.0707-.2757.0387-.4142-.032-.1384-.1078-.2625-.2149-.3508-.1071-.0884-.2415-.1367-.38-.1367zM3.75 13.375v1.25c0 .1658.06585.3247.18306.4419.11721.1172.27618.1831.44194.1831h6.25c.1657 0 .3247-.0659.4419-.1831.1172-.1172.1831-.2761.1831-.4419v-1.25c0-.1657-.0659-.3247-.1831-.4419-.1172-.1172-.2762-.1831-.4419-.1831h-6.25c-.16576 0-.32473.0659-.44194.1831C3.81585 13.0503 3.75 13.2093 3.75 13.375z"
fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@@ -1,7 +1,7 @@
import { useDisclosure } from "@nextui-org/react";
import React from "react";
import { Outlet } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { FaServer } from "react-icons/fa";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
@@ -11,7 +11,6 @@ import {
useConversation,
} from "#/context/conversation-context";
import { Controls } from "#/components/features/controls/controls";
import { RootState } from "#/store";
import { clearMessages } from "#/state/chat-slice";
import { clearTerminal } from "#/state/command-slice";
import { useEffectOnce } from "#/hooks/use-effect-once";
@@ -33,7 +32,6 @@ import {
import Security from "#/components/shared/modals/security/security";
import { useEndSession } from "#/hooks/use-end-session";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { CountBadge } from "#/components/layout/count-badge";
import { ServedAppLabel } from "#/components/layout/served-app-label";
import { TerminalStatusLabel } from "#/components/features/terminal/terminal-status-label";
import { useSettings } from "#/hooks/query/use-settings";
@@ -52,7 +50,6 @@ function AppContent() {
const endSession = useEndSession();
const [width, setWidth] = React.useState(window.innerWidth);
const { updateCount } = useSelector((state: RootState) => state.browser);
const secrets = React.useMemo(
() => [gitHubToken].filter((secret) => secret !== null),
@@ -144,7 +141,6 @@ function AppContent() {
label: (
<div className="flex items-center gap-1">
{t(I18nKey.BROWSER$TITLE)}
{updateCount > 0 && <CountBadge count={updateCount} />}
</div>
),
to: "browser",

View File

@@ -5,8 +5,6 @@ export const initialState = {
url: "https://github.com/All-Hands-AI/OpenHands",
// Base64-encoded screenshot of browser window (placeholder for now, will be replaced with the actual screenshot later)
screenshotSrc: "",
// Counter for browser updates
updateCount: 0,
};
export const browserSlice = createSlice({
@@ -18,7 +16,6 @@ export const browserSlice = createSlice({
},
setScreenshotSrc: (state, action) => {
state.screenshotSrc = action.payload;
state.updateCount += 1;
},
},
});

View File

@@ -26,6 +26,18 @@ interface FileSystemDirectoryHandle {
): Promise<FileSystemFileHandle>;
}
interface SaveFilePickerOptions {
suggestedName?: string;
types?: Array<{
description?: string;
accept: Record<string, string[]>;
}>;
excludeAcceptAllOption?: boolean;
}
interface Window {
showDirectoryPicker(): Promise<FileSystemDirectoryHandle>;
showSaveFilePicker(
options?: SaveFilePickerOptions,
): Promise<FileSystemFileHandle>;
}

View File

@@ -22,6 +22,13 @@ function isFileSystemAccessSupported(): boolean {
return "showDirectoryPicker" in window;
}
/**
* Checks if the Save File Picker API is supported
*/
function isSaveFilePickerSupported(): boolean {
return "showSaveFilePicker" in window;
}
/**
* Creates subdirectories and returns the final directory handle
*/
@@ -162,6 +169,39 @@ async function processBatch(
};
}
export async function downloadTrajectory(
conversationId: string,
data: unknown[] | null,
): Promise<void> {
try {
if (!isSaveFilePickerSupported()) {
throw new Error(
"Your browser doesn't support downloading folders. Please use Chrome, Edge, or another browser that supports the File System Access API.",
);
}
const options = {
suggestedName: `trajectory-${conversationId}.json`,
types: [
{
description: "JSON File",
accept: {
"application/json": [".json"],
},
},
],
};
const handle = await window.showSaveFilePicker(options);
const writable = await handle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
} catch (error) {
throw new Error(
`Failed to download file: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Downloads files from the workspace one by one
* @param initialPath Initial path to start downloading from. If not provided, downloads from root

View File

@@ -3,10 +3,7 @@ import { cleanup } from "@testing-library/react";
import { server } from "#/mocks/node";
import "@testing-library/jest-dom/vitest";
// @ts-expect-error - Mock for Terminal tests
HTMLCanvasElement.prototype.getContext = vi.fn();
// @ts-expect-error - handle TypeError: dom.scrollTo is not a function
HTMLElement.prototype.scrollTo = vi.fn();
// Mock the i18n provider

View File

@@ -0,0 +1,65 @@
---
name: add_openhands_repo_instruction
type: task
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- name: REPO_FOLDER_NAME
description: "Branch for the agent to work on"
required: false
---
Please browse the current repository under /workspace/{{ REPO_FOLDER_NAME }}, look at the documentation and relevant code, and understand the purpose of this repository.
Specifically, I want you to create a `.openhands/microagents/repo.md` file. This file should contain succinct information that summarizes (1) the purpose of this repository, (2) the general setup of this repo, and (3) a brief description of the structure of this repo.
Here's an example:
```markdown
---
name: repo
type: repo
agent: CodeActAgent
---
This repository contains the code for runtime-API, an automated AI software engineer. It has a Python backend
(in the `openhands` directory) and React frontend (in the `frontend` directory).
## General Setup:
To set up the entire repo, including frontend and backend, run `make build`.
You don't need to do this unless the user asks you to, or if you're trying to run the entire application.
Before pushing any changes, you should ensure that any lint errors or simple test errors have been fixed.
* If you've made changes to the backend, you should run `pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml`
* If you've made changes to the frontend, you should run `cd frontend && npm run lint:fix && npm run build ; cd ..`
If either command fails, it may have automatically fixed some issues. You should fix any issues that weren't automatically fixed,
then re-run the command to ensure it passes.
## Repository Structure
Backend:
- Located in the `openhands` directory
- Testing:
- All tests are in `tests/unit/test_*.py`
- To test new code, run `poetry run pytest tests/unit/test_xxx.py` where `xxx` is the appropriate file for the current functionality
- Write all tests with pytest
Frontend:
- Located in the `frontend` directory
- Prerequisites: A recent version of NodeJS / NPM
- Setup: Run `npm install` in the frontend directory
- Testing:
- Run tests: `npm run test`
- To run specific tests: `npm run test -- -t "TestName"`
- Building:
- Build for production: `npm run build`
- Environment Variables:
- Set in `frontend/.env` or as environment variables
- Available variables: VITE_BACKEND_HOST, VITE_USE_TLS, VITE_INSECURE_SKIP_VERIFY, VITE_FRONTEND_PORT
- Internationalization:
- Generate i18n declaration file: `npm run make-i18n`
```
Now, please write a similar markdown for the current repository.
Read all the GitHub workflows under .github/ of the repository (if this folder exists) to understand the CI checks (e.g., linter, pre-commit), and include those in the repo.md file.

View File

@@ -111,7 +111,7 @@ class CodeActAgent(Agent):
os.path.dirname(os.path.dirname(openhands.__file__)),
'microagents',
)
if self.config.use_microagents
if self.config.enable_prompt_extensions
else None,
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
disabled_microagents=self.config.disabled_microagents,
@@ -277,16 +277,11 @@ class CodeActAgent(Agent):
# if it doesn't have tool call metadata, it was triggered by a user action
if obs.tool_call_metadata is None:
text = truncate_content(
f'\nObserved result of command executed by user:\n{obs.content}',
f'\nObserved result of command executed by user:\n{obs.to_agent_observation()}',
max_message_chars,
)
else:
text = truncate_content(
obs.content
+ f'\n[Python Interpreter: {obs.metadata.py_interpreter_path}]',
max_message_chars,
)
text += f'\n[Command finished with exit code {obs.exit_code}]'
text = truncate_content(obs.to_agent_observation(), max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, IPythonRunCellObservation):
text = obs.content
@@ -448,6 +443,17 @@ class CodeActAgent(Agent):
)
)
# Repository and runtime info
additional_info = self.prompt_manager.get_additional_info()
if self.config.enable_prompt_extensions and additional_info:
# only add these if prompt extension is enabled
messages.append(
Message(
role='user',
content=[TextContent(text=additional_info)],
)
)
pending_tool_call_action_messages: dict[str, Message] = {}
tool_call_id_to_message: dict[str, Message] = {}

View File

@@ -31,7 +31,7 @@ from openhands.events.tool import ToolCallMetadata
_BASH_DESCRIPTION = """Execute a bash command in the terminal.
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.
* Interactive: If a bash command returns exit code `-1`, this means the process is not yet finished. The assistant must then send a second call to terminal with an empty `command` (which will retrieve any additional logs), or it can send additional text (set `command` to the text) to STDIN of the running process, or it can send command like `C-c` (Ctrl+C) to interrupt the process.
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process.
"""
CmdRunTool = ChatCompletionToolParam(
@@ -46,6 +46,11 @@ CmdRunTool = ChatCompletionToolParam(
'type': 'string',
'description': 'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process.',
},
'is_input': {
'type': 'string',
'description': 'If True, the command is an input to the running process. If False, the command is a bash command to be executed in the terminal. Default is False.',
'enum': ['true', 'false'],
},
},
'required': ['command'],
},
@@ -488,6 +493,12 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
f'Failed to parse tool call arguments: {tool_call.function.arguments}'
) from e
if tool_call.function.name == 'execute_bash':
# this is an LLM error: add empty command to avoid breaking the tool call
if 'command' not in arguments:
arguments['command'] = ''
# convert is_input to boolean
if 'is_input' in arguments:
arguments['is_input'] = arguments['is_input'] == 'true'
action = CmdRunAction(**arguments)
elif tool_call.function.name == 'execute_ipython_cell':
action = IPythonRunCellAction(**arguments)

View File

@@ -1,28 +1,7 @@
You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
<IMPORTANT>
* If user provides a path, you should NOT assume it's relative to the current working directory. Instead, you should explore the file system to find the file before working on it.
* You should start exploring the file system with your view command, unless you need to explore more deeply.
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
* The assistant MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior.
{{ runtime_info }}
* You MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior.
</IMPORTANT>
{% if repository_info %}
<REPOSITORY_INFO>
At the user's request, repository {{ repository_info.repo_name }} has been cloned to directory {{ repository_info.repo_directory }}.
</REPOSITORY_INFO>
{% endif %}
{% if repository_instructions -%}
<REPOSITORY_INSTRUCTIONS>
{{ repository_instructions }}
</REPOSITORY_INSTRUCTIONS>
{% endif %}
{% if runtime_info and runtime_info.available_hosts -%}
<RUNTIME_INFORMATION>
The user has access to the following hosts for accessing a web application,
each of which has a corresponding port:
{% for host, port in runtime_info.available_hosts.items() -%}
* {{ host }} (port {{ port }})
{% endfor %}
When starting a web server, use the corresponding ports. You should also
set any options to allow iframes and CORS requests.
</RUNTIME_INFORMATION>
{% endif %}

View File

@@ -12,6 +12,7 @@ from litellm.exceptions import (
)
from openhands.controller.agent import Agent
from openhands.controller.replay import ReplayManager
from openhands.controller.state.state import State, TrafficControlState
from openhands.controller.stuck import StuckDetector
from openhands.core.config import AgentConfig, LLMConfig
@@ -90,6 +91,7 @@ class AgentController:
is_delegate: bool = False,
headless_mode: bool = True,
status_callback: Callable | None = None,
replay_events: list[Event] | None = None,
):
"""Initializes a new instance of the AgentController class.
@@ -108,6 +110,7 @@ class AgentController:
is_delegate: Whether this controller is a delegate.
headless_mode: Whether the agent is run in headless mode.
status_callback: Optional callback function to handle status updates.
replay_events: A list of logs to replay.
"""
self.id = sid
self.agent = agent
@@ -139,6 +142,9 @@ class AgentController:
self._stuck_detector = StuckDetector(self.state)
self.status_callback = status_callback
# replay-related
self._replay_manager = ReplayManager(replay_events)
async def close(self) -> None:
"""Closes the agent controller, canceling any ongoing tasks and unsubscribing from the event stream.
@@ -234,6 +240,11 @@ class AgentController:
await self._react_to_exception(reported)
def should_step(self, event: Event) -> bool:
"""
Whether the agent should take a step based on an event. In general,
the agent should take a step if it receives a message from the user,
or observes something in the environment (after acting).
"""
# it might be the delegate's day in the sun
if self.delegate is not None:
return False
@@ -641,42 +652,50 @@ class AgentController:
self.update_state_before_step()
action: Action = NullAction()
try:
action = self.agent.step(self.state)
if action is None:
raise LLMNoActionError('No action was returned')
except (
LLMMalformedActionError,
LLMNoActionError,
LLMResponseError,
FunctionCallValidationError,
FunctionCallNotExistsError,
) as e:
self.event_stream.add_event(
ErrorObservation(
content=str(e),
),
EventSource.AGENT,
)
return
except (ContextWindowExceededError, BadRequestError) as e:
# FIXME: this is a hack until a litellm fix is confirmed
# Check if this is a nested context window error
error_str = str(e).lower()
if (
'contextwindowexceedederror' in error_str
or 'prompt is too long' in error_str
or isinstance(e, ContextWindowExceededError)
):
# When context window is exceeded, keep roughly half of agent interactions
self.state.history = self._apply_conversation_window(self.state.history)
# Save the ID of the first event in our truncated history for future reloading
if self.state.history:
self.state.start_id = self.state.history[0].id
# Don't add error event - let the agent retry with reduced context
if self._replay_manager.should_replay():
# in replay mode, we don't let the agent to proceed
# instead, we replay the action from the replay trajectory
action = self._replay_manager.step()
else:
try:
action = self.agent.step(self.state)
if action is None:
raise LLMNoActionError('No action was returned')
except (
LLMMalformedActionError,
LLMNoActionError,
LLMResponseError,
FunctionCallValidationError,
FunctionCallNotExistsError,
) as e:
self.event_stream.add_event(
ErrorObservation(
content=str(e),
),
EventSource.AGENT,
)
return
raise
except (ContextWindowExceededError, BadRequestError) as e:
# FIXME: this is a hack until a litellm fix is confirmed
# Check if this is a nested context window error
error_str = str(e).lower()
if (
'contextwindowexceedederror' in error_str
or 'prompt is too long' in error_str
or isinstance(e, ContextWindowExceededError)
):
# When context window is exceeded, keep roughly half of agent interactions
self.state.history = self._apply_conversation_window(
self.state.history
)
# Save the ID of the first event in our truncated history for future reloading
if self.state.history:
self.state.start_id = self.state.history[0].id
# Don't add error event - let the agent retry with reduced context
return
raise
if action.runnable:
if self.state.confirmation_mode and (

View File

@@ -0,0 +1,52 @@
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.action import Action
from openhands.events.event import Event, EventSource
class ReplayManager:
"""ReplayManager manages the lifecycle of a replay session of a given trajectory.
Replay manager keeps track of a list of events, replays actions, and ignore
messages and observations. It could lead to unexpected or even errorneous
results if any action is non-deterministic, or if the initial state before
the replay session is different from the initial state of the trajectory.
"""
def __init__(self, replay_events: list[Event] | None):
if replay_events:
logger.info(f'Replay logs loaded, events length = {len(replay_events)}')
self.replay_events = replay_events
self.replay_mode = bool(replay_events)
self.replay_index = 0
def _replayable(self) -> bool:
return (
self.replay_events is not None
and self.replay_index < len(self.replay_events)
and isinstance(self.replay_events[self.replay_index], Action)
and self.replay_events[self.replay_index].source != EventSource.USER
)
def should_replay(self) -> bool:
"""
Whether the controller is in trajectory replay mode, and the replay
hasn't finished. Note: after the replay is finished, the user and
the agent could continue to message/act.
This method also moves "replay_index" to the next action, if applicable.
"""
if not self.replay_mode:
return False
assert self.replay_events is not None
while self.replay_index < len(self.replay_events) and not self._replayable():
self.replay_index += 1
return self._replayable()
def step(self) -> Action:
assert self.replay_events is not None
event = self.replay_events[self.replay_index]
assert isinstance(event, Action)
self.replay_index += 1
return event

View File

@@ -37,21 +37,17 @@ export SANDBOX_TIMEOUT='300'
## Type Handling
The `load_from_env` function attempts to cast environment variable values to the types specified in the dataclasses. It handles:
The `load_from_env` function attempts to cast environment variable values to the types specified in the models. It handles:
- Basic types (str, int, bool)
- Optional types (e.g., `str | None`)
- Nested dataclasses
- Nested models
If type casting fails, an error is logged, and the default value is retained.
## Default Values
If an environment variable is not set, the default value specified in the dataclass is used.
## Nested Configurations
The `AppConfig` class contains nested configurations like `LLMConfig` and `AgentConfig`. The `load_from_env` function handles these by recursively processing nested dataclasses with updated prefixes.
If an environment variable is not set, the default value specified in the model is used.
## Security Considerations

View File

@@ -1,11 +1,9 @@
from dataclasses import dataclass, field, fields
from pydantic import BaseModel, Field
from openhands.core.config.condenser_config import CondenserConfig, NoOpCondenserConfig
from openhands.core.config.config_utils import get_field_info
@dataclass
class AgentConfig:
class AgentConfig(BaseModel):
"""Configuration for the agent.
Attributes:
@@ -17,25 +15,18 @@ class AgentConfig:
memory_enabled: Whether long-term memory (embeddings) is enabled.
memory_max_threads: The maximum number of threads indexing at the same time for embeddings.
llm_config: The name of the llm config to use. If specified, this will override global llm config.
use_microagents: Whether to use microagents at all. Default is True.
enable_prompt_extensions: Whether to use prompt extensions (e.g., microagents, inject runtime info). Default is True.
disabled_microagents: A list of microagents to disable. Default is None.
condenser: Configuration for the memory condenser. Default is NoOpCondenserConfig.
"""
codeact_enable_browsing: bool = True
codeact_enable_llm_editor: bool = False
codeact_enable_jupyter: bool = True
micro_agent_name: str | None = None
memory_enabled: bool = False
memory_max_threads: int = 3
llm_config: str | None = None
use_microagents: bool = True
disabled_microagents: list[str] | None = None
condenser: CondenserConfig = field(default_factory=NoOpCondenserConfig) # type: ignore
def defaults_to_dict(self) -> dict:
"""Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional."""
result = {}
for f in fields(self):
result[f.name] = get_field_info(f)
return result
codeact_enable_browsing: bool = Field(default=True)
codeact_enable_llm_editor: bool = Field(default=False)
codeact_enable_jupyter: bool = Field(default=True)
micro_agent_name: str | None = Field(default=None)
memory_enabled: bool = Field(default=False)
memory_max_threads: int = Field(default=3)
llm_config: str | None = Field(default=None)
enable_prompt_extensions: bool = Field(default=True)
disabled_microagents: list[str] | None = Field(default=None)
condenser: CondenserConfig = Field(default_factory=NoOpCondenserConfig)

View File

@@ -1,20 +1,20 @@
from dataclasses import dataclass, field, fields, is_dataclass
from typing import ClassVar
from pydantic import BaseModel, Field, SecretStr
from openhands.core import logger
from openhands.core.config.agent_config import AgentConfig
from openhands.core.config.config_utils import (
OH_DEFAULT_AGENT,
OH_MAX_ITERATIONS,
get_field_info,
model_defaults_to_dict,
)
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
@dataclass
class AppConfig:
class AppConfig(BaseModel):
"""Configuration for the app.
Attributes:
@@ -28,6 +28,7 @@ class AppConfig:
file_store: Type of file store to use.
file_store_path: Path to the file store.
save_trajectory_path: Either a folder path to store trajectories with auto-generated filenames, or a designated trajectory file path.
replay_trajectory_path: Path to load trajectory and replay. If provided, trajectory would be replayed first before user's instruction.
workspace_base: Base path for the workspace. Defaults to `./workspace` as absolute path.
workspace_mount_path: Path to mount the workspace. Defaults to `workspace_base`.
workspace_mount_path_in_sandbox: Path to mount the workspace in sandbox. Defaults to `/workspace`.
@@ -46,37 +47,40 @@ class AppConfig:
input is read line by line. When enabled, input continues until /exit command.
"""
llms: dict[str, LLMConfig] = field(default_factory=dict)
agents: dict = field(default_factory=dict)
default_agent: str = OH_DEFAULT_AGENT
sandbox: SandboxConfig = field(default_factory=SandboxConfig)
security: SecurityConfig = field(default_factory=SecurityConfig)
runtime: str = 'docker'
file_store: str = 'local'
file_store_path: str = '/tmp/openhands_file_store'
save_trajectory_path: str | None = None
workspace_base: str | None = None
workspace_mount_path: str | None = None
workspace_mount_path_in_sandbox: str = '/workspace'
workspace_mount_rewrite: str | None = None
cache_dir: str = '/tmp/cache'
run_as_openhands: bool = True
max_iterations: int = OH_MAX_ITERATIONS
max_budget_per_task: float | None = None
e2b_api_key: str = ''
modal_api_token_id: str = ''
modal_api_token_secret: str = ''
disable_color: bool = False
jwt_secret: str = ''
debug: bool = False
file_uploads_max_file_size_mb: int = 0
file_uploads_restrict_file_types: bool = False
file_uploads_allowed_extensions: list[str] = field(default_factory=lambda: ['.*'])
runloop_api_key: str | None = None
cli_multiline_input: bool = False
llms: dict[str, LLMConfig] = Field(default_factory=dict)
agents: dict = Field(default_factory=dict)
default_agent: str = Field(default=OH_DEFAULT_AGENT)
sandbox: SandboxConfig = Field(default_factory=SandboxConfig)
security: SecurityConfig = Field(default_factory=SecurityConfig)
runtime: str = Field(default='docker')
file_store: str = Field(default='local')
file_store_path: str = Field(default='/tmp/openhands_file_store')
save_trajectory_path: str | None = Field(default=None)
replay_trajectory_path: str | None = Field(default=None)
workspace_base: str | None = Field(default=None)
workspace_mount_path: str | None = Field(default=None)
workspace_mount_path_in_sandbox: str = Field(default='/workspace')
workspace_mount_rewrite: str | None = Field(default=None)
cache_dir: str = Field(default='/tmp/cache')
run_as_openhands: bool = Field(default=True)
max_iterations: int = Field(default=OH_MAX_ITERATIONS)
max_budget_per_task: float | None = Field(default=None)
e2b_api_key: SecretStr | None = Field(default=None)
modal_api_token_id: SecretStr | None = Field(default=None)
modal_api_token_secret: SecretStr | None = Field(default=None)
disable_color: bool = Field(default=False)
jwt_secret: SecretStr | None = Field(default=None)
debug: bool = Field(default=False)
file_uploads_max_file_size_mb: int = Field(default=0)
file_uploads_restrict_file_types: bool = Field(default=False)
file_uploads_allowed_extensions: list[str] = Field(default_factory=lambda: ['.*'])
runloop_api_key: SecretStr | None = Field(default=None)
cli_multiline_input: bool = Field(default=False)
defaults_dict: ClassVar[dict] = {}
model_config = {'extra': 'forbid'}
def get_llm_config(self, name='llm') -> LLMConfig:
"""'llm' is the name for default config (for backward compatibility prior to 0.8)."""
if name in self.llms:
@@ -115,42 +119,7 @@ class AppConfig:
def get_agent_configs(self) -> dict[str, AgentConfig]:
return self.agents
def __post_init__(self):
def model_post_init(self, __context):
"""Post-initialization hook, called when the instance is created with only default values."""
AppConfig.defaults_dict = self.defaults_to_dict()
def defaults_to_dict(self) -> dict:
"""Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional."""
result = {}
for f in fields(self):
field_value = getattr(self, f.name)
# dataclasses compute their defaults themselves
if is_dataclass(type(field_value)):
result[f.name] = field_value.defaults_to_dict()
else:
result[f.name] = get_field_info(f)
return result
def __str__(self):
attr_str = []
for f in fields(self):
attr_name = f.name
attr_value = getattr(self, f.name)
if attr_name in [
'e2b_api_key',
'github_token',
'jwt_secret',
'modal_api_token_id',
'modal_api_token_secret',
'runloop_api_key',
]:
attr_value = '******' if attr_value else None
attr_str.append(f'{attr_name}={repr(attr_value)}')
return f"AppConfig({', '.join(attr_str)}"
def __repr__(self):
return self.__str__()
super().model_post_init(__context)
AppConfig.defaults_dict = model_defaults_to_dict(self)

View File

@@ -1,19 +1,22 @@
from types import UnionType
from typing import get_args, get_origin
from typing import Any, get_args, get_origin
from pydantic import BaseModel
from pydantic.fields import FieldInfo
OH_DEFAULT_AGENT = 'CodeActAgent'
OH_MAX_ITERATIONS = 500
def get_field_info(f):
def get_field_info(field: FieldInfo) -> dict[str, Any]:
"""Extract information about a dataclass field: type, optional, and default.
Args:
f: The field to extract information from.
field: The field to extract information from.
Returns: A dict with the field's type, whether it's optional, and its default value.
"""
field_type = f.type
field_type = field.annotation
optional = False
# for types like str | None, find the non-None type and set optional to True
@@ -33,7 +36,21 @@ def get_field_info(f):
)
# default is always present
default = f.default
default = field.default
# return a schema with the useful info for frontend
return {'type': type_name.lower(), 'optional': optional, 'default': default}
def model_defaults_to_dict(model: BaseModel) -> dict[str, Any]:
"""Serialize field information in a dict for the frontend, including type hints, defaults, and whether it's optional."""
result = {}
for name, field in model.model_fields.items():
field_value = getattr(model, name)
if isinstance(field_value, BaseModel):
result[name] = model_defaults_to_dict(field_value)
else:
result[name] = get_field_info(field)
return result

View File

@@ -1,15 +1,14 @@
import os
from dataclasses import dataclass, fields
from typing import Optional
from __future__ import annotations
import os
from typing import Any
from pydantic import BaseModel, Field, SecretStr
from openhands.core.config.config_utils import get_field_info
from openhands.core.logger import LOG_DIR
LLM_SENSITIVE_FIELDS = ['api_key', 'aws_access_key_id', 'aws_secret_access_key']
@dataclass
class LLMConfig:
class LLMConfig(BaseModel):
"""Configuration for the LLM model.
Attributes:
@@ -43,114 +42,62 @@ class LLMConfig:
caching_prompt: Use the prompt caching feature if provided by the LLM and supported by the provider.
log_completions: Whether to log LLM completions to the state.
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.
reasoning_effort: The effort to put into reasoning. This is a string that can be one of 'low', 'medium', 'high', or 'none'. Exclusive for o1 models.
"""
model: str = 'claude-3-5-sonnet-20241022'
api_key: str | None = None
base_url: str | None = None
api_version: str | None = None
embedding_model: str = 'local'
embedding_base_url: str | None = None
embedding_deployment_name: str | None = None
aws_access_key_id: str | None = None
aws_secret_access_key: str | None = None
aws_region_name: str | None = None
openrouter_site_url: str = 'https://docs.all-hands.dev/'
openrouter_app_name: str = 'OpenHands'
num_retries: int = 8
retry_multiplier: float = 2
retry_min_wait: int = 15
retry_max_wait: int = 120
timeout: int | None = None
max_message_chars: int = 30_000 # maximum number of characters in an observation's content when sent to the llm
temperature: float = 0.0
top_p: float = 1.0
custom_llm_provider: str | None = None
max_input_tokens: int | None = None
max_output_tokens: int | None = None
input_cost_per_token: float | None = None
output_cost_per_token: float | None = None
ollama_base_url: str | None = None
model: str = Field(default='claude-3-5-sonnet-20241022')
api_key: SecretStr | None = Field(default=None)
base_url: str | None = Field(default=None)
api_version: str | None = Field(default=None)
embedding_model: str = Field(default='local')
embedding_base_url: str | None = Field(default=None)
embedding_deployment_name: str | None = Field(default=None)
aws_access_key_id: SecretStr | None = Field(default=None)
aws_secret_access_key: SecretStr | None = Field(default=None)
aws_region_name: str | None = Field(default=None)
openrouter_site_url: str = Field(default='https://docs.all-hands.dev/')
openrouter_app_name: str = Field(default='OpenHands')
num_retries: int = Field(default=8)
retry_multiplier: float = Field(default=2)
retry_min_wait: int = Field(default=15)
retry_max_wait: int = Field(default=120)
timeout: int | None = Field(default=None)
max_message_chars: int = Field(
default=30_000
) # maximum number of characters in an observation's content when sent to the llm
temperature: float = Field(default=0.0)
top_p: float = Field(default=1.0)
custom_llm_provider: str | None = Field(default=None)
max_input_tokens: int | None = Field(default=None)
max_output_tokens: int | None = Field(default=None)
input_cost_per_token: float | None = Field(default=None)
output_cost_per_token: float | None = Field(default=None)
ollama_base_url: str | None = Field(default=None)
# This setting can be sent in each call to litellm
drop_params: bool = True
drop_params: bool = Field(default=True)
# Note: this setting is actually global, unlike drop_params
modify_params: bool = True
disable_vision: bool | None = None
caching_prompt: bool = True
log_completions: bool = False
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
modify_params: bool = Field(default=True)
disable_vision: bool | None = Field(default=None)
caching_prompt: bool = Field(default=True)
log_completions: bool = Field(default=False)
log_completions_folder: str = Field(default=os.path.join(LOG_DIR, 'completions'))
custom_tokenizer: str | None = Field(default=None)
native_tool_calling: bool | None = Field(default=None)
reasoning_effort: str | None = Field(default=None)
def defaults_to_dict(self) -> dict:
"""Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional."""
result = {}
for f in fields(self):
result[f.name] = get_field_info(f)
return result
model_config = {'extra': 'forbid'}
def model_post_init(self, __context: Any):
"""Post-initialization hook to assign OpenRouter-related variables to environment variables.
def __post_init__(self):
"""
Post-initialization hook to assign OpenRouter-related variables to environment variables.
This ensures that these values are accessible to litellm at runtime.
"""
super().model_post_init(__context)
# Assign OpenRouter-specific variables to environment variables
if self.openrouter_site_url:
os.environ['OR_SITE_URL'] = self.openrouter_site_url
if self.openrouter_app_name:
os.environ['OR_APP_NAME'] = self.openrouter_app_name
def __str__(self):
attr_str = []
for f in fields(self):
attr_name = f.name
attr_value = getattr(self, f.name)
if attr_name in LLM_SENSITIVE_FIELDS:
attr_value = '******' if attr_value else None
attr_str.append(f'{attr_name}={repr(attr_value)}')
return f"LLMConfig({', '.join(attr_str)})"
def __repr__(self):
return self.__str__()
def to_safe_dict(self):
"""Return a dict with the sensitive fields replaced with ******."""
ret = self.__dict__.copy()
for k, v in ret.items():
if k in LLM_SENSITIVE_FIELDS:
ret[k] = '******' if v else None
elif isinstance(v, LLMConfig):
ret[k] = v.to_safe_dict()
return ret
@classmethod
def from_dict(cls, llm_config_dict: dict) -> 'LLMConfig':
"""Create an LLMConfig object from a dictionary.
This function is used to create an LLMConfig object from a dictionary,
with the exception of the 'draft_editor' key, which is a nested LLMConfig object.
"""
# Keep None values to preserve defaults, filter out other dicts
args = {
k: v
for k, v in llm_config_dict.items()
if not isinstance(v, dict) or v is None
}
if (
'draft_editor' in llm_config_dict
and llm_config_dict['draft_editor'] is not None
):
if isinstance(llm_config_dict['draft_editor'], LLMConfig):
args['draft_editor'] = llm_config_dict['draft_editor']
else:
draft_editor_config = LLMConfig(**llm_config_dict['draft_editor'])
args['draft_editor'] = draft_editor_config
return cls(**args)

View File

@@ -1,11 +1,9 @@
import os
from dataclasses import dataclass, field, fields
from openhands.core.config.config_utils import get_field_info
from pydantic import BaseModel, Field
@dataclass
class SandboxConfig:
class SandboxConfig(BaseModel):
"""Configuration for the sandbox.
Attributes:
@@ -39,48 +37,32 @@ class SandboxConfig:
This should be a JSON string that will be parsed into a dictionary.
"""
remote_runtime_api_url: str = 'http://localhost:8000'
local_runtime_url: str = 'http://localhost'
keep_runtime_alive: bool = False
rm_all_containers: bool = False
api_key: str | None = None
base_container_image: str = 'nikolaik/python-nodejs:python3.12-nodejs22' # default to nikolaik/python-nodejs:python3.12-nodejs22 for eventstream runtime
runtime_container_image: str | None = None
user_id: int = os.getuid() if hasattr(os, 'getuid') else 1000
timeout: int = 120
remote_runtime_init_timeout: int = 180
enable_auto_lint: bool = (
False # once enabled, OpenHands would lint files after editing
remote_runtime_api_url: str = Field(default='http://localhost:8000')
local_runtime_url: str = Field(default='http://localhost')
keep_runtime_alive: bool = Field(default=True)
rm_all_containers: bool = Field(default=False)
api_key: str | None = Field(default=None)
base_container_image: str = Field(
default='nikolaik/python-nodejs:python3.12-nodejs22'
)
use_host_network: bool = False
runtime_extra_build_args: list[str] | None = None
initialize_plugins: bool = True
force_rebuild_runtime: bool = False
runtime_extra_deps: str | None = None
runtime_startup_env_vars: dict[str, str] = field(default_factory=dict)
browsergym_eval_env: str | None = None
platform: str | None = None
close_delay: int = 15
remote_runtime_resource_factor: int = 1
enable_gpu: bool = False
docker_runtime_kwargs: str | None = None
runtime_container_image: str | None = Field(default=None)
user_id: int = Field(default=os.getuid() if hasattr(os, 'getuid') else 1000)
timeout: int = Field(default=120)
remote_runtime_init_timeout: int = Field(default=180)
enable_auto_lint: bool = Field(
default=False # once enabled, OpenHands would lint files after editing
)
use_host_network: bool = Field(default=False)
runtime_extra_build_args: list[str] | None = Field(default=None)
initialize_plugins: bool = Field(default=True)
force_rebuild_runtime: bool = Field(default=False)
runtime_extra_deps: str | None = Field(default=None)
runtime_startup_env_vars: dict[str, str] = Field(default_factory=dict)
browsergym_eval_env: str | None = Field(default=None)
platform: str | None = Field(default=None)
close_delay: int = Field(default=15)
remote_runtime_resource_factor: int = Field(default=1)
enable_gpu: bool = Field(default=False)
docker_runtime_kwargs: str | None = Field(default=None)
def defaults_to_dict(self) -> dict:
"""Serialize fields to a dict for the frontend, including type hints, defaults, and whether it's optional."""
dict = {}
for f in fields(self):
dict[f.name] = get_field_info(f)
return dict
def __str__(self):
attr_str = []
for f in fields(self):
attr_name = f.name
attr_value = getattr(self, f.name)
attr_str.append(f'{attr_name}={repr(attr_value)}')
return f"SandboxConfig({', '.join(attr_str)})"
def __repr__(self):
return self.__str__()
model_config = {'extra': 'forbid'}

View File

@@ -1,10 +1,7 @@
from dataclasses import dataclass, fields
from openhands.core.config.config_utils import get_field_info
from pydantic import BaseModel, Field
@dataclass
class SecurityConfig:
class SecurityConfig(BaseModel):
"""Configuration for security related functionalities.
Attributes:
@@ -12,29 +9,5 @@ class SecurityConfig:
security_analyzer: The security analyzer to use.
"""
confirmation_mode: bool = False
security_analyzer: str | 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."""
dict = {}
for f in fields(self):
dict[f.name] = get_field_info(f)
return dict
def __str__(self):
attr_str = []
for f in fields(self):
attr_name = f.name
attr_value = getattr(self, f.name)
attr_str.append(f'{attr_name}={repr(attr_value)}')
return f"SecurityConfig({', '.join(attr_str)})"
@classmethod
def from_dict(cls, security_config_dict: dict) -> 'SecurityConfig':
return cls(**security_config_dict)
def __repr__(self):
return self.__str__()
confirmation_mode: bool = Field(default=False)
security_analyzer: str | None = Field(default=None)

View File

@@ -3,13 +3,13 @@ import os
import pathlib
import platform
import sys
from dataclasses import is_dataclass
from types import UnionType
from typing import Any, MutableMapping, get_args, get_origin
from uuid import uuid4
import toml
from dotenv import load_dotenv
from pydantic import BaseModel, ValidationError
from openhands.core import logger
from openhands.core.config.agent_config import AgentConfig
@@ -43,17 +43,19 @@ def load_from_env(cfg: AppConfig, env_or_toml_dict: dict | MutableMapping[str, s
return next((t for t in types if t is not type(None)), None)
# helper function to set attributes based on env vars
def set_attr_from_env(sub_config: Any, prefix=''):
"""Set attributes of a config dataclass based on environment variables."""
for field_name, field_type in sub_config.__annotations__.items():
def set_attr_from_env(sub_config: BaseModel, prefix=''):
"""Set attributes of a config model based on environment variables."""
for field_name, field_info in sub_config.model_fields.items():
field_value = getattr(sub_config, field_name)
field_type = field_info.annotation
# compute the expected env var name from the prefix and field name
# e.g. LLM_BASE_URL
env_var_name = (prefix + field_name).upper()
if is_dataclass(field_type):
# nested dataclass
nested_sub_config = getattr(sub_config, field_name)
set_attr_from_env(nested_sub_config, prefix=field_name + '_')
if isinstance(field_value, BaseModel):
set_attr_from_env(field_value, prefix=field_name + '_')
elif env_var_name in env_or_toml_dict:
# convert the env var to the correct type and set it
value = env_or_toml_dict[env_var_name]
@@ -125,32 +127,50 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
if isinstance(value, dict):
try:
if key is not None and key.lower() == 'agent':
# Every entry here is either a field for the default `agent` config group, or itself a group
# The best way to tell the difference is to try to parse it as an AgentConfig object
agent_group_ids: set[str] = set()
for nested_key, nested_value in value.items():
if isinstance(nested_value, dict):
try:
agent_config = AgentConfig(**nested_value)
except ValidationError:
continue
agent_group_ids.add(nested_key)
cfg.set_agent_config(agent_config, nested_key)
logger.openhands_logger.debug(
'Attempt to load default agent config from config toml'
)
non_dict_fields = {
k: v for k, v in value.items() if not isinstance(v, dict)
value_without_groups = {
k: v for k, v in value.items() if k not in agent_group_ids
}
agent_config = AgentConfig(**non_dict_fields)
agent_config = AgentConfig(**value_without_groups)
cfg.set_agent_config(agent_config, 'agent')
elif key is not None and key.lower() == 'llm':
# Every entry here is either a field for the default `llm` config group, or itself a group
# The best way to tell the difference is to try to parse it as an LLMConfig object
llm_group_ids: set[str] = set()
for nested_key, nested_value in value.items():
if isinstance(nested_value, dict):
logger.openhands_logger.debug(
f'Attempt to load group {nested_key} from config toml as agent config'
)
agent_config = AgentConfig(**nested_value)
cfg.set_agent_config(agent_config, nested_key)
elif key is not None and key.lower() == 'llm':
try:
llm_config = LLMConfig(**nested_value)
except ValidationError:
continue
llm_group_ids.add(nested_key)
cfg.set_llm_config(llm_config, nested_key)
logger.openhands_logger.debug(
'Attempt to load default LLM config from config toml'
)
# TODO clean up draft_editor
# Extract generic LLM fields, keeping draft_editor
# Extract generic LLM fields, which are not nested LLM configs
generic_llm_fields = {}
for k, v in value.items():
if not isinstance(v, dict) or k == 'draft_editor':
if not isinstance(v, dict):
generic_llm_fields[k] = v
generic_llm_config = LLMConfig.from_dict(generic_llm_fields)
generic_llm_config = LLMConfig(**generic_llm_fields)
cfg.set_llm_config(generic_llm_config, 'llm')
# Process custom named LLM configs
@@ -168,35 +188,25 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
# results in num_retries APPLIED to claude-3-5-sonnet
custom_fields = {}
for k, v in nested_value.items():
if not isinstance(v, dict) or k == 'draft_editor':
if not isinstance(v, dict):
custom_fields[k] = v
merged_llm_dict = generic_llm_config.__dict__.copy()
merged_llm_dict = generic_llm_fields.copy()
merged_llm_dict.update(custom_fields)
# TODO clean up draft_editor
# Handle draft_editor with fallback values:
# - If draft_editor is "null", use None
# - If draft_editor is in custom fields, use that value
# - If draft_editor is not specified, fall back to generic config value
if 'draft_editor' in custom_fields:
if custom_fields['draft_editor'] == 'null':
merged_llm_dict['draft_editor'] = None
else:
merged_llm_dict['draft_editor'] = (
generic_llm_config.draft_editor
)
custom_llm_config = LLMConfig.from_dict(merged_llm_dict)
custom_llm_config = LLMConfig(**merged_llm_dict)
cfg.set_llm_config(custom_llm_config, nested_key)
elif key is not None and key.lower() == 'security':
logger.openhands_logger.debug(
'Attempt to load security config from config toml'
)
security_config = SecurityConfig.from_dict(value)
security_config = SecurityConfig(**value)
cfg.security = security_config
elif not key.startswith('sandbox') and key.lower() != 'core':
logger.openhands_logger.warning(
f'Unknown key in {toml_file}: "{key}"'
)
except (TypeError, KeyError) as e:
except (TypeError, KeyError, ValidationError) as e:
logger.openhands_logger.warning(
f'Cannot parse [{key}] config from toml, values have not been applied.\nError: {e}',
)
@@ -232,7 +242,7 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml'):
logger.openhands_logger.warning(
f'Unknown config key "{key}" in [core] section'
)
except (TypeError, KeyError) as e:
except (TypeError, KeyError, ValidationError) as e:
logger.openhands_logger.warning(
f'Cannot parse [sandbox] config from toml, values have not been applied.\nError: {e}',
)
@@ -335,7 +345,7 @@ def get_llm_config_arg(
# update the llm config with the specified section
if 'llm' in toml_config and llm_config_arg in toml_config['llm']:
return LLMConfig.from_dict(toml_config['llm'][llm_config_arg])
return LLMConfig(**toml_config['llm'][llm_config_arg])
logger.openhands_logger.debug(f'Loading from toml failed for {llm_config_arg}')
return None

View File

@@ -8,10 +8,31 @@ from datetime import datetime
from types import TracebackType
from typing import Any, Literal, Mapping
import litellm
from termcolor import colored
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
DEBUG = os.getenv('DEBUG', 'False').lower() in ['true', '1', 'yes']
DEBUG_LLM = os.getenv('DEBUG_LLM', 'False').lower() in ['true', '1', 'yes']
# Configure litellm logging based on DEBUG_LLM
if DEBUG_LLM:
confirmation = input(
'\n⚠️ WARNING: You are enabling DEBUG_LLM which may expose sensitive information like API keys.\n'
'This should NEVER be enabled in production.\n'
"Type 'y' to confirm you understand the risks: "
)
if confirmation.lower() == 'y':
litellm.suppress_debug_info = False
litellm.set_verbose = True
else:
print('DEBUG_LLM disabled due to lack of confirmation')
litellm.suppress_debug_info = True
litellm.set_verbose = False
else:
litellm.suppress_debug_info = True
litellm.set_verbose = False
if DEBUG:
LOG_LEVEL = 'DEBUG'

View File

@@ -2,6 +2,7 @@ import asyncio
import json
import os
import sys
from pathlib import Path
from typing import Callable, Protocol
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
@@ -22,10 +23,11 @@ from openhands.core.setup import (
generate_sid,
)
from openhands.events import EventSource, EventStreamSubscriber
from openhands.events.action import MessageAction
from openhands.events.action import MessageAction, NullAction
from openhands.events.action.action import Action
from openhands.events.event import Event
from openhands.events.observation import AgentStateChangedObservation
from openhands.events.serialization import event_from_dict
from openhands.events.serialization.event import event_to_trajectory
from openhands.runtime.base import Runtime
@@ -101,7 +103,17 @@ async def run_controller(
if agent is None:
agent = create_agent(runtime, config)
controller, initial_state = create_controller(agent, runtime, config)
replay_events: list[Event] | None = None
if config.replay_trajectory_path:
logger.info('Trajectory replay is enabled')
assert isinstance(initial_user_action, NullAction)
replay_events, initial_user_action = load_replay_log(
config.replay_trajectory_path
)
controller, initial_state = create_controller(
agent, runtime, config, replay_events=replay_events
)
assert isinstance(
initial_user_action, Action
@@ -194,21 +206,64 @@ def auto_continue_response(
return message
def load_replay_log(trajectory_path: str) -> tuple[list[Event] | None, Action]:
"""
Load trajectory from given path, serialize it to a list of events, and return
two things:
1) A list of events except the first action
2) First action (user message, a.k.a. initial task)
"""
try:
path = Path(trajectory_path).resolve()
if not path.exists():
raise ValueError(f'Trajectory file not found: {path}')
if not path.is_file():
raise ValueError(f'Trajectory path is a directory, not a file: {path}')
with open(path, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, list):
raise ValueError(
f'Expected a list in {path}, got {type(data).__name__}'
)
events = []
for item in data:
event = event_from_dict(item)
# cannot add an event with _id to event stream
event._id = None # type: ignore[attr-defined]
events.append(event)
assert isinstance(events[0], MessageAction)
return events[1:], events[0]
except json.JSONDecodeError as e:
raise ValueError(f'Invalid JSON format in {trajectory_path}: {e}')
if __name__ == '__main__':
args = parse_arguments()
config = setup_config_from_args(args)
# Determine the task
task_str = ''
if args.file:
task_str = read_task_from_file(args.file)
elif args.task:
task_str = args.task
elif not sys.stdin.isatty():
task_str = read_task_from_stdin()
initial_user_action: Action = NullAction()
if config.replay_trajectory_path:
if task_str:
raise ValueError(
'User-specified task is not supported under trajectory replay mode'
)
elif task_str:
initial_user_action = MessageAction(content=task_str)
else:
raise ValueError('No task provided. Please specify a task through -t, -f.')
initial_user_action: MessageAction = MessageAction(content=task_str)
config = setup_config_from_args(args)
# Set session name
session_name = args.name

View File

@@ -11,6 +11,7 @@ from openhands.core.config import (
)
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
from openhands.events.event import Event
from openhands.llm.llm import LLM
from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
@@ -78,7 +79,11 @@ def create_agent(runtime: Runtime, config: AppConfig) -> Agent:
def create_controller(
agent: Agent, runtime: Runtime, config: AppConfig, headless_mode: bool = True
agent: Agent,
runtime: Runtime,
config: AppConfig,
headless_mode: bool = True,
replay_events: list[Event] | None = None,
) -> Tuple[AgentController, State | None]:
event_stream = runtime.event_stream
initial_state = None
@@ -101,6 +106,7 @@ def create_controller(
initial_state=initial_state,
headless_mode=headless_mode,
confirmation_mode=config.security.confirmation_mode,
replay_events=replay_events,
)
return (controller, initial_state)

View File

@@ -11,8 +11,10 @@ from openhands.events.action.action import (
@dataclass
class CmdRunAction(Action):
command: str
# When `command` is empty, it will be used to print the current tmux window
command: (
str # When `command` is empty, it will be used to print the current tmux window
)
is_input: bool = False # if True, the command is an input to the running process
thought: str = ''
blocking: bool = False
# If blocking is True, the command will be run in a blocking manner.
@@ -28,7 +30,7 @@ class CmdRunAction(Action):
return f'Running command: {self.command}'
def __str__(self) -> str:
ret = f'**CmdRunAction (source={self.source})**\n'
ret = f'**CmdRunAction (source={self.source}, is_input={self.is_input})**\n'
if self.thought:
ret += f'THOUGHT: {self.thought}\n'
ret += f'COMMAND:\n{self.command}'

View File

@@ -24,6 +24,8 @@ class FileReadSource(str, Enum):
@dataclass
class Event:
INVALID_ID = -1
@property
def message(self) -> str | None:
if hasattr(self, '_message'):
@@ -34,7 +36,7 @@ class Event:
def id(self) -> int:
if hasattr(self, '_id'):
return self._id # type: ignore[attr-defined]
return -1
return Event.INVALID_ID
@property
def timestamp(self):
@@ -64,8 +66,12 @@ class Event:
return self._timeout # type: ignore[attr-defined]
return None
@timeout.setter
def timeout(self, value: int | None) -> None:
def set_hard_timeout(self, value: int | None, blocking: bool = True) -> None:
"""Set the timeout for the event.
NOTE, this is a hard timeout, meaning that the event will be blocked
until the timeout is reached.
"""
self._timeout = value
if value is not None and value > 600:
from openhands.core.logger import openhands_logger as logger
@@ -78,7 +84,7 @@ class Event:
# Check if .blocking is an attribute of the event
if hasattr(self, 'blocking'):
# .blocking needs to be set to True if .timeout is set
self.blocking = True
self.blocking = blocking
# optional metadata, LLM call cost of the edit
@property

View File

@@ -12,7 +12,7 @@ class BrowserOutputObservation(Observation):
url: str
trigger_by_action: str
screenshot: str = field(repr=False) # don't show in repr
screenshot: str = field(repr=False, default='') # don't show in repr
error: bool = False
observation: str = ObservationType.BROWSE
# do not include in the memory
@@ -100,5 +100,4 @@ class BrowserOutputObservation(Observation):
skip_generic=False,
filter_visible_only=filter_visible_only,
)
self._axtree_str = cur_axtree_txt
return cur_axtree_txt

View File

@@ -149,16 +149,18 @@ class CmdOutputObservation(Observation):
f'**CmdOutputObservation (source={self.source}, exit code={self.exit_code}, '
f'metadata={json.dumps(self.metadata.model_dump(), indent=2)})**\n'
'--BEGIN AGENT OBSERVATION--\n'
f'{self._to_agent_observation()}\n'
f'{self.to_agent_observation()}\n'
'--END AGENT OBSERVATION--'
)
def _to_agent_observation(self) -> str:
def to_agent_observation(self) -> str:
ret = f'{self.metadata.prefix}{self.content}{self.metadata.suffix}'
if self.metadata.working_dir:
ret += f'\n[Current working directory: {self.metadata.working_dir}]'
if self.metadata.py_interpreter_path:
ret += f'\n[Python interpreter: {self.metadata.py_interpreter_path}]'
if self.metadata.exit_code != -1:
ret += f'\n[Command finished with exit code {self.metadata.exit_code}]'
return ret

View File

@@ -74,7 +74,8 @@ def action_from_dict(action: dict) -> Action:
try:
decoded_action = action_class(**args)
if 'timeout' in action:
decoded_action.timeout = action['timeout']
blocking = args.get('blocking', False)
decoded_action.set_hard_timeout(action['timeout'], blocking=blocking)
# Set timestamp if it was provided
if timestamp:

View File

@@ -6,7 +6,11 @@ from litellm import acompletion as litellm_acompletion
from openhands.core.exceptions import UserCancelledError
from openhands.core.logger import openhands_logger as logger
from openhands.llm.llm import LLM, LLM_RETRY_EXCEPTIONS
from openhands.llm.llm import (
LLM,
LLM_RETRY_EXCEPTIONS,
REASONING_EFFORT_SUPPORTED_MODELS,
)
from openhands.utils.shutdown_listener import should_continue
@@ -19,7 +23,9 @@ class AsyncLLM(LLM):
self._async_completion = partial(
self._call_acompletion,
model=self.config.model,
api_key=self.config.api_key,
api_key=self.config.api_key.get_secret_value()
if self.config.api_key
else None,
base_url=self.config.base_url,
api_version=self.config.api_version,
custom_llm_provider=self.config.custom_llm_provider,
@@ -55,6 +61,10 @@ class AsyncLLM(LLM):
elif 'messages' in kwargs:
messages = kwargs['messages']
# Set reasoning effort for models that support it
if self.config.model.lower() in REASONING_EFFORT_SUPPORTED_MODELS:
kwargs['reasoning_effort'] = self.config.reasoning_effort
# ensure we work with a list of messages
messages = messages if isinstance(messages, list) else [messages]

View File

@@ -71,6 +71,15 @@ FUNCTION_CALLING_SUPPORTED_MODELS = [
'claude-3-5-haiku-20241022',
'gpt-4o-mini',
'gpt-4o',
'o1-2024-12-17',
]
REASONING_EFFORT_SUPPORTED_MODELS = [
'o1-2024-12-17',
]
MODELS_WITHOUT_STOP_WORDS = [
'o1-mini',
]
@@ -132,7 +141,9 @@ class LLM(RetryMixin, DebugMixin):
self._completion = partial(
litellm_completion,
model=self.config.model,
api_key=self.config.api_key,
api_key=self.config.api_key.get_secret_value()
if self.config.api_key
else None,
base_url=self.config.base_url,
api_version=self.config.api_version,
custom_llm_provider=self.config.custom_llm_provider,
@@ -141,6 +152,12 @@ class LLM(RetryMixin, DebugMixin):
temperature=self.config.temperature,
top_p=self.config.top_p,
drop_params=self.config.drop_params,
# add reasoning_effort, only if the model is supported
**(
{'reasoning_effort': self.config.reasoning_effort}
if self.config.model.lower() in REASONING_EFFORT_SUPPORTED_MODELS
else {}
),
)
self._completion_unwrapped = self._completion
@@ -186,7 +203,8 @@ class LLM(RetryMixin, DebugMixin):
messages, kwargs['tools']
)
kwargs['messages'] = messages
kwargs['stop'] = STOP_WORDS
if self.config.model not in MODELS_WITHOUT_STOP_WORDS:
kwargs['stop'] = STOP_WORDS
mock_fncall_tools = kwargs.pop('tools')
# if we have no messages, something went very wrong
@@ -213,7 +231,6 @@ class LLM(RetryMixin, DebugMixin):
try:
# Record start time for latency measurement
start_time = time.time()
# we don't support streaming here, thus we get a ModelResponse
resp: ModelResponse = self._completion_unwrapped(*args, **kwargs)
@@ -318,7 +335,9 @@ class LLM(RetryMixin, DebugMixin):
# GET {base_url}/v1/model/info with litellm_model_id as path param
response = requests.get(
f'{self.config.base_url}/v1/model/info',
headers={'Authorization': f'Bearer {self.config.api_key}'},
headers={
'Authorization': f'Bearer {self.config.api_key.get_secret_value() if self.config.api_key else None}'
},
)
resp_json = response.json()
if 'data' not in resp_json:
@@ -597,17 +616,16 @@ class LLM(RetryMixin, DebugMixin):
logger.debug(f'Using custom cost per token: {cost_per_token}')
extra_kwargs['custom_cost_per_token'] = cost_per_token
try:
# try directly get response_cost from response
_hidden_params = getattr(response, '_hidden_params', {})
cost = _hidden_params.get('response_cost', None)
if cost is None:
cost = float(
_hidden_params.get('additional_headers', {}).get(
'llm_provider-x-litellm-response-cost', 0.0
)
)
# try directly get response_cost from response
_hidden_params = getattr(response, '_hidden_params', {})
cost = _hidden_params.get('additional_headers', {}).get(
'llm_provider-x-litellm-response-cost', None
)
if cost is not None:
cost = float(cost)
logger.debug(f'Got response_cost from response: {cost}')
try:
if cost is None:
try:
cost = litellm_completion_cost(

View File

@@ -5,6 +5,7 @@ from typing import Any
from openhands.core.exceptions import UserCancelledError
from openhands.core.logger import openhands_logger as logger
from openhands.llm.async_llm import LLM_RETRY_EXCEPTIONS, AsyncLLM
from openhands.llm.llm import REASONING_EFFORT_SUPPORTED_MODELS
class StreamingLLM(AsyncLLM):
@@ -16,7 +17,9 @@ class StreamingLLM(AsyncLLM):
self._async_streaming_completion = partial(
self._call_acompletion,
model=self.config.model,
api_key=self.config.api_key,
api_key=self.config.api_key.get_secret_value()
if self.config.api_key
else None,
base_url=self.config.base_url,
api_version=self.config.api_version,
custom_llm_provider=self.config.custom_llm_provider,
@@ -61,6 +64,10 @@ class StreamingLLM(AsyncLLM):
'The messages list is empty. At least one message is required.'
)
# Set reasoning effort for models that support it
if self.config.model.lower() in REASONING_EFFORT_SUPPORTED_MODELS:
kwargs['reasoning_effort'] = self.config.reasoning_effort
self.log_prompt(messages)
try:

View File

@@ -118,7 +118,7 @@ async def complete_runtime(
git_patch = None
while n_retries < 5:
action = CmdRunAction(command=f'git diff --no-color --cached {base_commit}')
action.timeout = 600 + 100 * n_retries
action.set_hard_timeout(600 + 100 * n_retries)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})

View File

@@ -120,6 +120,9 @@ class ActionExecutor:
self.bash_session = BashSession(
work_dir=self._initial_cwd,
username=self.username,
no_change_timeout_seconds=int(
os.environ.get('NO_CHANGE_TIMEOUT_SECONDS', 30)
),
)
self.bash_session.initialize()
await wait_all(
@@ -163,7 +166,7 @@ class ActionExecutor:
logger.debug(f'Initializing by running {len(INIT_COMMANDS)} bash commands...')
for command in INIT_COMMANDS:
action = CmdRunAction(command=command)
action.timeout = 300
action.set_hard_timeout(300)
logger.debug(f'Executing init command: {command}')
obs = await self.run(action)
assert isinstance(obs, CmdOutputObservation)

View File

@@ -121,7 +121,9 @@ class Runtime(FileEditRuntimeMixin):
)
# Load mixins
FileEditRuntimeMixin.__init__(self)
FileEditRuntimeMixin.__init__(
self, enable_llm_editor=config.get_agent_config().codeact_enable_llm_editor
)
def setup_initial_env(self) -> None:
if self.attach_to_existing:
@@ -182,7 +184,8 @@ class Runtime(FileEditRuntimeMixin):
async def _handle_action(self, event: Action) -> None:
if event.timeout is None:
event.timeout = self.config.sandbox.timeout
# We don't block the command if this is a default timeout action
event.set_hard_timeout(self.config.sandbox.timeout, blocking=False)
assert event.timeout is not None
try:
observation: Observation = await call_sync_from_async(
@@ -194,9 +197,10 @@ class Runtime(FileEditRuntimeMixin):
e, AgentRuntimeDisconnectedError
):
err_id = 'STATUS$ERROR_RUNTIME_DISCONNECTED'
self.log('error', f'Unexpected error while running action: {str(e)}')
error_message = f'{type(e).__name__}: {str(e)}'
self.log('error', f'Unexpected error while running action: {error_message}')
self.log('error', f'Problematic action: {str(event)}')
self.send_error_message(err_id, str(e))
self.send_error_message(err_id, error_message)
self.close()
return

View File

@@ -5,6 +5,7 @@ from openhands.core.schema import ActionType
from openhands.events.action import BrowseInteractiveAction, BrowseURLAction
from openhands.events.observation import BrowserOutputObservation
from openhands.runtime.browser.browser_env import BrowserEnv
from openhands.utils.async_utils import call_sync_from_async
async def browse(
@@ -29,7 +30,7 @@ async def browse(
try:
# obs provided by BrowserGym: see https://github.com/ServiceNow/BrowserGym/blob/main/core/src/browsergym/core/env.py#L396
obs = browser.step(action_str)
obs = await call_sync_from_async(browser.step, action_str)
return BrowserOutputObservation(
content=obs['text_content'], # text content of the page
url=obs.get('url', ''), # URL of the page

View File

@@ -59,6 +59,7 @@ class ActionExecutionClient(Runtime):
self.session = HttpSession()
self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time
self._runtime_initialized: bool = False
self._runtime_closed: bool = False
self._vscode_token: str | None = None # initial dummy value
super().__init__(
config,
@@ -217,7 +218,8 @@ class ActionExecutionClient(Runtime):
# set timeout to default if not set
if action.timeout is None:
action.timeout = self.config.sandbox.timeout
# We don't block the command if this is a default timeout action
action.set_hard_timeout(self.config.sandbox.timeout, blocking=False)
with self.action_semaphore:
if not action.runnable:
@@ -282,4 +284,9 @@ class ActionExecutionClient(Runtime):
return self.send_action_for_execution(action)
def close(self) -> None:
# Make sure we don't close the session multiple times
# Can happen in evaluation
if self._runtime_closed:
return
self._runtime_closed = True
self.session.close()

View File

@@ -66,7 +66,7 @@ class DockerRuntime(ActionExecutionClient):
headless_mode: bool = True,
):
global _atexit_registered
if not _atexit_registered:
if not _atexit_registered and not config.sandbox.keep_runtime_alive:
_atexit_registered = True
atexit.register(remove_all_runtime_containers)
@@ -228,6 +228,8 @@ class DockerRuntime(ActionExecutionClient):
}
if self.config.debug or DEBUG:
environment['DEBUG'] = 'true'
# also update with runtime_startup_env_vars
environment.update(self.config.sandbox.runtime_startup_env_vars)
self.log('debug', f'Workspace Base: {self.config.workspace_base}')
if (

View File

@@ -59,7 +59,8 @@ class ModalRuntime(ActionExecutionClient):
self.sandbox = None
self.modal_client = modal.Client.from_credentials(
config.modal_api_token_id, config.modal_api_token_secret
config.modal_api_token_id.get_secret_value(),
config.modal_api_token_secret.get_secret_value(),
)
self.app = modal.App.lookup(
'openhands', create_if_missing=True, client=self.modal_client

View File

@@ -13,6 +13,7 @@ from openhands.core.exceptions import (
AgentRuntimeNotReadyError,
AgentRuntimeUnavailableError,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events import EventStream
from openhands.runtime.builder.remote import RemoteRuntimeBuilder
from openhands.runtime.impl.action_execution.action_execution_client import (
@@ -30,6 +31,9 @@ class RemoteRuntime(ActionExecutionClient):
"""This runtime will connect to a remote oh-runtime-client."""
port: int = 60000 # default port for the remote runtime client
runtime_id: str | None = None
runtime_url: str | None = None
_runtime_initialized: bool = False
def __init__(
self,
@@ -70,10 +74,11 @@ class RemoteRuntime(ActionExecutionClient):
self.config.sandbox.api_key,
self.session,
)
self.runtime_id: str | None = None
self.runtime_url: str | None = None
self.available_hosts: dict[str, int] = {}
self._runtime_initialized: bool = False
def log(self, level: str, message: str) -> None:
message = f'[runtime session_id={self.sid} runtime_id={self.runtime_id or "unknown"}] {message}'
getattr(logger, level)(message, stacklevel=2)
def _get_action_execution_server_host(self):
return self.runtime_url
@@ -225,7 +230,7 @@ class RemoteRuntime(ActionExecutionClient):
f'Runtime started. URL: {self.runtime_url}',
)
except requests.HTTPError as e:
self.log('error', f'Unable to start runtime: {e}')
self.log('error', f'Unable to start runtime: {str(e)}')
raise AgentRuntimeUnavailableError() from e
def _resume_runtime(self):
@@ -310,10 +315,11 @@ class RemoteRuntime(ActionExecutionClient):
self.check_if_alive()
except requests.HTTPError as e:
self.log(
'warning', f"Runtime /alive failed, but pod says it's ready: {e}"
'warning',
f"Runtime /alive failed, but pod says it's ready: {str(e)}",
)
raise AgentRuntimeNotReadyError(
f'Runtime /alive failed to respond with 200: {e}'
f'Runtime /alive failed to respond with 200: {str(e)}'
)
return
elif (
@@ -350,20 +356,34 @@ class RemoteRuntime(ActionExecutionClient):
super().close()
return
try:
with self._send_runtime_api_request(
'POST',
f'{self.config.sandbox.remote_runtime_api_url}/stop',
json={'runtime_id': self.runtime_id},
):
self.log('debug', 'Runtime stopped.')
if not self._runtime_closed:
with self._send_runtime_api_request(
'POST',
f'{self.config.sandbox.remote_runtime_api_url}/stop',
json={'runtime_id': self.runtime_id},
):
self.log('debug', 'Runtime stopped.')
except Exception as e:
self.log('error', f'Unable to stop runtime: {str(e)}')
raise e
finally:
super().close()
def _send_runtime_api_request(self, method, url, **kwargs):
return send_request(self.session, method, url, **kwargs)
try:
return send_request(self.session, method, url, **kwargs)
except requests.Timeout:
self.log(
'error',
f'No response received within the timeout period for url: {url}',
)
raise
@tenacity.retry(
retry=tenacity.retry_if_exception_type(ConnectionError),
stop=tenacity.stop_after_attempt(3) | stop_if_should_exit(),
wait=tenacity.wait_exponential(multiplier=1, min=4, max=60),
)
def _send_action_server_request(self, method, url, **kwargs):
try:
return super()._send_action_server_request(method, url, **kwargs)
@@ -375,18 +395,23 @@ class RemoteRuntime(ActionExecutionClient):
raise
except requests.HTTPError as e:
if e.response.status_code in (404, 502):
if e.response.status_code in (404, 502, 504):
if e.response.status_code == 404:
raise AgentRuntimeDisconnectedError(
'Runtime is not responding. This may be temporary, please try again.'
f'Runtime is not responding. This may be temporary, please try again. Original error: {e}'
) from e
else: # 502
else: # 502, 504
raise AgentRuntimeDisconnectedError(
'Runtime is temporarily unavailable. This may be due to a restart or network issue, please try again.'
f'Runtime is temporarily unavailable. This may be due to a restart or network issue, please try again. Original error: {e}'
) from e
elif e.response.status_code == 503:
self.log('warning', 'Runtime appears to be paused. Resuming...')
self._resume_runtime()
return super()._send_action_server_request(method, url, **kwargs)
if self.config.sandbox.keep_runtime_alive:
self.log('warning', 'Runtime appears to be paused. Resuming...')
self._resume_runtime()
return super()._send_action_server_request(method, url, **kwargs)
else:
raise AgentRuntimeDisconnectedError(
f'Runtime is temporarily unavailable. This may be due to a restart or network issue, please try again. Original error: {e}'
) from e
else:
raise e

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