Compare commits

..

86 Commits

Author SHA1 Message Date
enyst 6c8c93add4 SDK Minimal Python: MCP-only tools (camelCase), remove fallbacks/handlers; Conversation delegates to runtime.execute_tool; Runtime.get_tools MCP-shape + execute_tool dispatcher; fix imports 2025-08-24 23:14:06 +00:00
enyst 0a3f389bc4 PRD: add Runtime and SDK sections for MCP-first minimal SDK (get_tools, execute_tool, sdk.Tool, Conversation flow, Anthropic sequencing) 2025-08-24 22:57:31 +00:00
enyst 37f4784e05 Runtime: add get_tools() in MCP format (name/description/inputSchema); SDK: Conversation uses runtime.get_tools() with fallback, binds handlers for execute_bash/file_read/file_write; keep SDK Tool param conversion\n\nCo-authored-by: openhands <openhands@all-hands.dev> 2025-08-24 22:23:06 +00:00
enyst 475947ebcd Apply pre-commit autofixes (ruff/format)\n\nCo-authored-by: openhands <openhands@all-hands.dev> 2025-08-24 22:08:41 +00:00
enyst 5a6b741612 I am OpenHands-GPT-5, an AI agent — Option B: vendor CodeAct system_prompt and include; render with cli_mode=True for SDK system_message.\n\n- Copy system_prompt.j2 and security_risk_assessment.j2 under openhands/sdk/prompts\n- Render via Jinja2 and refine_prompt; persist as system_message\n\nCo-authored-by: openhands <openhands@all-hands.dev> 2025-08-24 19:23:11 +00:00
enyst 3d8f2dcd67 I am OpenHands-GPT-5, an AI agent — remove local task docs from branch before pushing 2025-08-24 18:58:47 +00:00
enyst 8db1a6034c I am OpenHands-GPT-5, an AI agent — Embed fully-rendered CodeAct system prompt in SDK system_message; PRD updated to specify exact source.\n\n- Load system prompt from CodeActAgent system_prompt.j2 and persist as system_message at loop start\n- Allow appending simple system_prompt_extensions\n\nCo-authored-by: openhands <openhands@all-hands.dev> 2025-08-24 18:55:15 +00:00
enyst 0a9db47533 I am OpenHands-GPT-5, an AI agent — Minimal Python SDK groundwork: autoresume synthesis of assistant tool_calls; TUI snippet formatting and headless exit semantics; expand system_message in PRD.\n\n- Implement synthesized assistant tool_calls in _reconstruct_messages_from_events (canonical OpenAI format)\n- Include system_message reconstruction\n- TUI: concise per-step logs with truncation policy; headless returns non-zero on fatal\n- Expand PRD: system prompt source, behaviors\n\nCo-authored-by: openhands <openhands@all-hands.dev> 2025-08-24 18:32:42 +00:00
Engel Nyst d9bc5824a0 docs: add shell guidance to avoid set -e variants in this environment (#10579)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-24 13:39:37 +08:00
Xingyao Wang fd5b5075d6 Simplify CLI markdown rendering; remove python-markdown deps; update tests (#10538)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-24 01:23:06 +08:00
Hiep Le f5cd7b256d feat(frontend): Implement LLM risk analyzer UI (#10569)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: llamantino <213239228+llamantino@users.noreply.github.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ryan H. Tran <descience.thh10@gmail.com>
Co-authored-by: Neeraj Panwar <49247372+npneeraj@users.noreply.github.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Insop <1240382+insop@users.noreply.github.com>
Co-authored-by: test <test@test.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Zhonghao Jiang <zhonghao.J@outlook.com>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-08-23 02:08:45 +07:00
Neeraj Panwar df86fd275d Fixes bug 9682 (#9692)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-08-22 16:51:53 +00:00
Xingyao Wang d22a2e39e7 feat(agent): add security-related items in system prompt to defense against data exfiltration (#10477)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-22 15:53:13 +00:00
Xingyao Wang ca424ec15d [agent] Add LLM risk analyzer (#9349)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: llamantino <213239228+llamantino@users.noreply.github.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: Hiep Le <69354317+hieptl@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ryan H. Tran <descience.thh10@gmail.com>
Co-authored-by: Neeraj Panwar <49247372+npneeraj@users.noreply.github.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Insop <1240382+insop@users.noreply.github.com>
Co-authored-by: test <test@test.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Zhonghao Jiang <zhonghao.J@outlook.com>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-08-22 14:02:36 +00:00
Xingyao Wang 4507a25b85 Evaluation: redirect sessions to repo-local .eval_sessions via helper; apply across entrypoints; add tests (#10540)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-22 13:34:02 +00:00
llamantino d9cf5b7302 ci: add GitHub Action to post welcome message on good first issues (#9707)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-22 09:09:45 -04:00
Xingyao Wang 2a86e32263 fix(CI): Pin @modelcontextprotocol/server-filesystem to version 2025.8.18 (#10561)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-22 05:00:11 +08:00
Engel Nyst b311ae6e15 fix: normalize malformed <parameter> tags (Qwen3) (#10539) 2025-08-21 19:03:20 +02:00
Ryan H. Tran adb773789a Upgrade aci to 0.3.2: clamp view_range end to file length and emit warning instead of error (#10502) 2025-08-21 23:01:54 +07:00
Engel Nyst 91d3d1d20a Fix: expose aggregated LLM metrics in State for evaluation scripts (#10537)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-21 17:43:09 +02:00
llamantino e9e2c98946 fix(tests): increase hard timeout in test_bash_server to avoid timeout on Windows (#9930) 2025-08-21 17:12:42 +02:00
Engel Nyst 7861c1ddf7 fix(anthropic): disable extended thinking for Opus 4.1 (#10532)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-21 00:13:15 +02:00
Engel Nyst 5ce5469bfa docs: update OpenAPI specification to include all current endpoints (#10412)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-20 21:58:35 +02:00
Xingyao Wang 4a3f5dd9b4 fix(runtime): correctly set session_api_key for local runtime (#10506) 2025-08-21 03:51:19 +08:00
Joe O'Connor bc8b995dd3 Add additional networks (#9566)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-20 18:52:31 +00:00
chuckbutkus 07c4742496 Add useful tools jq and gettext to image (#10531) 2025-08-20 18:27:09 +00:00
mamoodi b5887f8a9d Fix CLI docs command (#10520) 2025-08-20 14:53:15 +00:00
mamoodi 0166df6575 Release 0.54.0 (#10465) 2025-08-20 10:29:15 -04:00
Ryan H. Tran e03a1f4e37 Move TASKS.md to session-specific directory in ~/.openhands (#10493)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-20 22:26:55 +08:00
sp.wack c763f0e368 chroe(vscode): Refresh vscode integration lockfile (#9965)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-20 15:33:11 +02:00
Engel Nyst bb0e24d23b Centralize model feature checks (#10414)
Co-authored-by: OpenHands-GPT-5 <openhands@all-hands.dev>
2025-08-19 20:30:07 +00:00
sp.wack aa6b454772 fix: Enhance GitHub repository search to include user organizations (#10324)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-19 15:56:15 +00:00
sp.wack 0297b3da18 Fix conversation ID validation to return 400 instead of 500 for long IDs (#10496) 2025-08-19 18:03:05 +04:00
Hiep Le 476954f3a4 refactor(frontend): update the styling for the microagent management page. (#10494) 2025-08-19 19:50:42 +07:00
dependabot[bot] f296d7bde5 chore(deps): bump abatilo/actions-poetry from 3 to 4 (#10487)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-19 13:58:39 +02:00
Zacharias Fisches f866b3f8ea Update modal runtime for modal>=1.0 (#10479)
Co-authored-by: Ryan H. Tran <descience.thh10@gmail.com>
2025-08-19 10:33:03 +00:00
Zacharias Fisches 36d31b74f7 fix jinja / dockerfile syntax by removing newlines (#10476)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-08-19 02:50:41 +00:00
Engel Nyst 634a7691a2 tests: reorganize unit tests into subdirectories mirroring source modules (#10484)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-19 01:11:07 +02:00
Xingyao Wang 81ba4399fa fix(frontend): fix MCP tab in frontend unit tests (#10481)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-18 21:25:09 +00:00
Rohit Malhotra 875036d920 (Hotfix): Fix logs and filestore init for llm registry (#10470) 2025-08-18 20:57:08 +00:00
Xingyao Wang 39333dd5de feat: enable MCP in SaaS (#10480) 2025-08-18 20:40:42 +00:00
Rohit Malhotra 3660933d59 refactor: replace 'convo' naming with 'conversation' (#10473)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-18 15:10:32 -04:00
Xingyao Wang baf2cc5c7e Pin OpenAI Python SDK to 1.99.9 to avoid LiteLLM import breakage (BerriAI/litellm#13711) (#10471)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-08-18 18:45:34 +00:00
Rohit Malhotra 7b31d57a2f Update conversation stats filename (#10472) 2025-08-18 18:09:13 +00:00
Rohit Malhotra 61d90c31eb (Hotfix): Fix eval pipeline (#10466) 2025-08-18 12:51:51 -04:00
Xingyao Wang 3fea7fd2fc feat: improve MCP config UI with comprehensive add/edit/delete functionality (#10145)
Co-authored-by: OpenHands <openhands@all-hands.dev>
2025-08-18 16:33:27 +00:00
suixinio c64b1ae111 fix(openrouter): Force string serialization for openrouter/anthropic/claude-sonnet-4 model (#10454) 2025-08-18 17:50:01 +02:00
Kevin Musgrave 74ba21bad0 feat(evaluation): Added INSTRUCTION_TEMPLATE_NAME to run_infer.py in swe_bench (#10270)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-08-18 14:18:08 +00:00
Engel Nyst bef6b1afee cli: fix Ubuntu white-on-white model autocomplete by merging default prompt_toolkit UI style (#10347) 2025-08-18 20:32:09 +08:00
Graham Neubig ad85e3249a test(e2e): Add web browsing catchphrase E2E for #10378 and wire into CI (#10401)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-18 08:28:42 -04:00
Engel Nyst 822ce86150 Ensure .bashrc exists (#10461) 2025-08-18 20:18:11 +08:00
Graham Neubig 305caf1257 Implement configurable base URL for E2E tests (#10394)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-18 07:44:07 -04:00
Rohit Malhotra 25d9cf2890 [Refactor]: Add LLMRegistry for llm services (#9589)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-08-18 02:11:20 -04:00
Engel Nyst 17b1a21296 chore(ci): enhance lint-fix workflow for FE (#10448)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-18 05:21:13 +02:00
Engel Nyst 97bcb2162d Add instruction to use existing repository labels in PR/MR microagents (#10446)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-18 04:35:20 +02:00
Engel Nyst 8401641f7e Docs + Code: rename ‘convo’ to ‘conversation’ across codebase and docs (#10447) 2025-08-18 04:35:02 +02:00
Engel Nyst e2343c0927 Runtime-backend docs update (arch) - cron agent run (#10423)
Co-authored-by: OpenHands-GPT-5 <openhands@all-hands.dev>
2025-08-18 02:04:31 +02:00
Xingyao Wang 277064720c chore: remove timeout >600s warning log in Event.set_hard_timeout (#10444)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-17 23:25:13 +02:00
Xingyao Wang ef3e0c8dfe Fix think observation redundant rendering in frontend (#10409)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-17 10:55:03 +08:00
Engel Nyst 315d391414 Revert "tests: reorganize unit tests into subdirectories mirroring source modules" (#10437) 2025-08-17 00:33:17 +00:00
olyashok 95ef8965b7 Allow user actions over websockets (#10420)
Co-authored-by: Xingyao Wang <xingyaoww@gmail.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-08-16 21:29:28 +00:00
Ray Myers ab9fb50c4f fix - Thread-safety in BatchedWebHookFileStore (#10339) 2025-08-16 18:06:40 +00:00
Engel Nyst f866da6bf2 tests: reorganize unit tests into subdirectories mirroring source modules (#10427)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-16 19:13:50 +02:00
Zhonghao Jiang 7229a16b45 feat(evaluation): Add NoCode-bench evaluation script (#10229) 2025-08-16 16:41:22 +00:00
llamantino 19105a2a13 fix(cli): send authentication error resume message to user, not llm (#10421) 2025-08-16 18:01:42 +02:00
Ryan H. Tran fe486ad1f1 Add task tracking tool for long-horizon tasks (#10166)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-08-16 20:05:59 +07:00
Engel Nyst 0ec6ed20cb fix(frontend): browser tab notification respects user-renamed titles; add unit test (#10406) 2025-08-16 07:00:45 +00:00
Xingyao Wang 794381c22b Add "The agent didn't finish the job" feedback reason to Likert scale (#10417) 2025-08-16 00:25:19 -04:00
Tim O'Farrell 0c581ea946 fix(nested_event_store): correct reverse pagination in search_events and add unit test (#10418)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 19:29:35 -06:00
Engel Nyst f7f4fcf98f chore(eval): remove old, unused regression test framework under evaluation/regression (#10419) 2025-08-16 01:08:23 +02:00
Xingyao Wang ab004478f6 feat(agent): include a new roleplay-based prompt (#10306)
Co-authored-by: test <test@test.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-16 06:04:28 +08:00
Xingyao Wang 340606e68a microagent: Add /codereview-roasted microagent with Linus Torvalds engineering mindset (#10405)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 21:49:57 +00:00
Tim O'Farrell daec23b5d7 Add get_issue_comments method to GitLabService (#10361)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 14:55:39 -06:00
Xingyao Wang 587b4c311a runtime(bash): clarify guidance when previous command still running; recommend execute_bash timeout (issue #10350) (#10389)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-16 02:15:05 +08:00
Xingyao Wang 7a86402c9c Add process management guidance to system prompt (#10083)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-16 02:09:20 +08:00
Insop 06d283dfa0 Temp fix for docker file and debug log (#10345) 2025-08-15 15:34:46 +00:00
sp.wack a6a4246e30 fix(frontend): Failing tests (#10369) 2025-08-15 15:18:35 +00:00
Xingyao Wang 4830b9a67d fix(llm): include gpt-5 to fn call model; set top p default value to None (#10363) 2025-08-15 15:08:01 +00:00
Neeraj Panwar d4489d62d7 fix(llm): set AWS credentials in config.toml (#10351) 2025-08-15 22:16:50 +08:00
Ryan H. Tran e41c020073 [CLI] Fix MCP toml formatting issue (#10312)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 13:56:46 +00:00
Ryan H. Tran 3f44c8436f Fix swebench modal patch eval intermittent crash (#10353) 2025-08-15 21:51:03 +08:00
Graham Neubig b740944075 Split E2E settings and conversation tests; run settings first in workflow (#10359)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 09:19:27 -04:00
dependabot[bot] 5618a3eebb chore(deps): bump the version-all group in /frontend with 9 updates (#10318)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-15 16:51:41 +04:00
Hiep Le a1ffe5c936 fix(frontend): frontend UI keep flashing (#10352) 2025-08-15 12:19:30 +04:00
Hiep Le f8376a9702 fix(frontend): status message missing (#10349) 2025-08-15 12:11:42 +07:00
Tim O'Farrell 985a634d60 Fix for issue where static system commands are truncated (#10292)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-08-15 04:16:50 +00:00
489 changed files with 31065 additions and 24512 deletions
+6 -2
View File
@@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4
- name: Install poetry via pipx
uses: abatilo/actions-poetry@v3
uses: abatilo/actions-poetry@v4
with:
poetry-version: 2.1.3
@@ -183,7 +183,11 @@ jobs:
# Run the tests with detailed output
cd tests/e2e
poetry run python -m pytest test_e2e_workflow.py::test_github_token_configuration test_e2e_workflow.py::test_conversation_start -v --no-header --capture=no --timeout=600
poetry run python -m pytest \
test_settings.py::test_github_token_configuration \
test_conversation.py::test_conversation_start \
test_browsing_catchphrase.py::test_browsing_catchphrase \
-v --no-header --capture=no --timeout=900
- name: Upload test results
if: always()
+8 -2
View File
@@ -29,6 +29,12 @@ jobs:
run: |
cd frontend
npm install --frozen-lockfile
- name: Generate i18n and route types
run: |
cd frontend
npm run make-i18n
npx react-router typegen || true
- name: Fix frontend lint issues
run: |
cd frontend
@@ -45,7 +51,7 @@ jobs:
git config --local user.email "openhands@all-hands.dev"
git config --local user.name "OpenHands Bot"
git add -A
git commit -m "🤖 Auto-fix frontend linting issues"
git commit -m "🤖 Auto-fix frontend linting issues" --no-verify
git push
# Python lint fixes
@@ -87,5 +93,5 @@ jobs:
git config --local user.email "openhands@all-hands.dev"
git config --local user.name "OpenHands Bot"
git add -A
git commit -m "🤖 Auto-fix Python linting issues"
git commit -m "🤖 Auto-fix Python linting issues" --no-verify
git push
+1 -1
View File
@@ -73,7 +73,7 @@ jobs:
- name: Install Python dependencies using Poetry
run: poetry install --with dev,test,runtime
- name: Run Windows unit tests
run: poetry run pytest -svv tests/unit/test_windows_bash.py
run: poetry run pytest -svv tests/unit/runtime/utils/test_windows_bash.py
env:
PYTHONPATH: ".;$env:PYTHONPATH"
DEBUG: "1"
@@ -0,0 +1,50 @@
name: Welcome Good First Issue
on:
issues:
types: [labeled]
permissions:
issues: write
jobs:
comment-on-good-first-issue:
if: github.event.label.name == 'good first issue'
runs-on: ubuntu-latest
steps:
- name: Check if welcome comment already exists
id: check_comment
uses: actions/github-script@v7
with:
result-encoding: string
script: |
const issueNumber = context.issue.number;
const comments = await github.rest.issues.listComments({
...context.repo,
issue_number: issueNumber
});
const alreadyCommented = comments.data.some(
(comment) =>
comment.body.includes('<!-- auto-comment:good-first-issue -->')
);
return alreadyCommented ? 'true' : 'false';
- name: Leave welcome comment
if: steps.check_comment.outputs.result == 'false'
uses: actions/github-script@v7
with:
script: |
const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`;
await github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: "🙌 **Hey there, future contributor!** 🙌\n\n" +
"This issue has been labeled as **good first issue**, which means it's a great place to get started with the OpenHands project.\n\n" +
"If you're interested in working on it, feel free to! No need to ask for permission.\n\n" +
"Be sure to check out our [development setup guide](" + repoUrl + "/blob/main/Development.md) to get your environment set up, and follow our [contribution guidelines](" + repoUrl + "/blob/main/CONTRIBUTING.md) when you're ready to submit a fix.\n\n" +
"🙌 Happy hacking! 🙌\n\n" +
"<!-- auto-comment:good-first-issue -->"
});
+2
View File
@@ -257,3 +257,5 @@ containers/runtime/code
# test results
test-results
.sessions
.eval_sessions
+31
View File
@@ -87,6 +87,8 @@ VSCode Extension:
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
If you need to add labels when opening a PR, check the existing labels defined on that repository and select from existing ones. Do not invent your own labels.
## Implementation Details
These details may or may not be useful for your current task.
@@ -142,6 +144,35 @@ Your specialized knowledge and instructions here...
- Add the setting to the `Settings` model in `openhands/storage/data_models/settings.py`
- Update any relevant backend code to apply the setting (e.g., in session creation)
#### Settings UI Patterns:
There are two main patterns for saving settings in the OpenHands frontend:
**Pattern 1: Entity-based Resources (Immediate Save)**
- Used for: API Keys, Secrets, MCP Servers
- Behavior: Changes are saved immediately when user performs actions (add/edit/delete)
- Implementation:
- No "Save Changes" button
- No local state management or `isDirty` tracking
- Uses dedicated mutation hooks for each operation (e.g., `use-add-mcp-server.ts`, `use-delete-mcp-server.ts`)
- Each mutation triggers immediate API call with query invalidation for UI updates
- Example: MCP settings, API Keys & Secrets tabs
- Benefits: Simpler UX, no risk of losing changes, consistent with modern web app patterns
**Pattern 2: Form-based Settings (Manual Save)**
- Used for: Application settings, LLM configuration
- Behavior: Changes are accumulated locally and saved when user clicks "Save Changes"
- Implementation:
- Has "Save Changes" button that becomes enabled when changes are detected
- Uses local state management with `isDirty` tracking
- Uses `useSaveSettings` hook to save all changes at once
- Example: LLM tab, Application tab
- Benefits: Allows bulk changes, explicit save action, can validate all fields before saving
**When to use each pattern:**
- Use Pattern 1 (Immediate Save) for entity management where each item is independent
- Use Pattern 2 (Manual Save) for configuration forms where settings are interdependent or need validation
### Adding New LLM Models
To add a new LLM model to OpenHands, you need to update multiple files across both frontend and backend:
+1 -1
View File
@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.53-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.54-nikolaik`
## Develop inside Docker container
+3 -3
View File
@@ -79,17 +79,17 @@ You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)
You can also run OpenHands directly with Docker:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
</details>
+3 -3
View File
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
+3 -3
View File
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
-180
View File
@@ -1,180 +0,0 @@
# Tool Decoupling Refactoring Plan
## Current State Analysis
**Where we are:**
- New `openhands/tools/` module with unified Tool architecture (✅ committed)
- Existing tools scattered in `openhands/agenthub/codeact_agent/tools/` (old approach)
- Function calling logic hardcoded in `function_calling.py` with manual validation
- Multiple agents (codeact, loc, readonly) each have their own function_calling.py
- Tool schemas defined as dictionaries in individual tool files
**Key Integration Points:**
1. `openhands/agenthub/codeact_agent/function_calling.py` - main function call processor
2. `openhands/agenthub/codeact_agent/codeact_agent.py` - imports tools for schema generation
3. `openhands/agenthub/loc_agent/function_calling.py` - similar pattern
4. `openhands/agenthub/readonly_agent/function_calling.py` - similar pattern
## Target State
**Where we need to get to:**
- All agents use the new Tool classes for consistent behavior
- Function calling delegates to `Tool.validate_function_call()` for parameter validation
- Tool schemas come from `Tool.get_schema()`
- Action creation remains in function_calling.py (simple, no over-abstraction)
- Remove duplicated tool logic across agents
- **No registry needed** - agents directly import and use the tools they need
## Minimal Refactoring Strategy
### Phase 1: Create Bridge Layer (Non-breaking)
**Goal:** Make new tools work alongside existing system without breaking anything
1. **Create tool adapter in function_calling.py**
- Add import for new `openhands.tools` (BashTool, FileEditorTool, etc.)
- Create helper function `validate_with_new_tools()` that attempts new tool validation
- Fall back to existing hardcoded logic if tool not found
- This allows gradual migration without breaking existing functionality
2. **Update tool imports in codeact_agent.py**
- Import new Tool classes alongside existing tool imports
- Modify `get_tools()` method to include schemas from both old and new tools
- Ensure no duplicate tool names
### Phase 2: Migrate Core Tools (One by one)
**Goal:** Replace existing tools with new implementations
1. **Start with bash tool (lowest risk)**
- Update function_calling.py to use BashTool for execute_bash calls
- Remove old bash tool logic once confirmed working
- Keep old bash.py file temporarily for reference
2. **Migrate str_replace_editor tool**
- Update function_calling.py to use FileEditorTool
- Remove complex str_replace_editor logic from function_calling.py
- Keep old str_replace_editor.py temporarily
3. **Migrate remaining tools one by one**
- finish, browser, think, ipython, condensation_request
- Each migration should be a separate commit for easy rollback
### Phase 3: Clean Up (Remove old code)
**Goal:** Remove duplicate/obsolete code
1. **Remove old tool files**
- Delete `openhands/agenthub/codeact_agent/tools/` directory
- Update imports in codeact_agent.py
2. **Simplify function_calling.py**
- Remove all hardcoded tool logic
- Replace with simple registry lookup and delegation
- Should be ~50 lines instead of ~250 lines
### Phase 4: Extend to Other Agents (Optional)
**Goal:** Apply same pattern to loc_agent and readonly_agent
1. **Update loc_agent and readonly_agent**
- Replace their function_calling.py with registry-based approach
- Reuse same tool implementations
## Implementation Details
### Bridge Function (Phase 1)
```python
def validate_with_new_tools(tool_call):
"""Try new tool classes for validation, fall back to old logic"""
from openhands.tools import BashTool, FileEditorTool
# Map tool names to tool instances
tools = {
'execute_bash': BashTool(),
'str_replace_editor': FileEditorTool(),
}
tool = tools.get(tool_call.function.name)
if tool:
try:
return tool.validate_function_call(tool_call.function)
except ToolValidationError as e:
raise FunctionCallValidationError(str(e))
# Fall back to existing hardcoded validation
return None # Signal to use old logic
```
### Simplified function_calling.py (Phase 3)
```python
def response_to_actions(response: ModelResponse, mcp_tool_names: list[str] | None = None) -> list[Action]:
"""Convert LLM response to OpenHands actions using new tool classes"""
from openhands.tools import BashTool, FileEditorTool
# Create tool instances (could be module-level for efficiency)
tools = {
'execute_bash': BashTool(),
'str_replace_editor': FileEditorTool(),
}
actions = []
# ... existing response parsing logic ...
for tool_call in assistant_msg.tool_calls:
tool = tools.get(tool_call.function.name)
if tool:
# Validate parameters using tool
try:
validated_params = tool.validate_function_call(tool_call.function)
except ToolValidationError as e:
raise FunctionCallValidationError(str(e))
# Create action based on tool type (simple logic remains here)
if tool_call.function.name == 'execute_bash':
action = CmdRunAction(command=validated_params['command'], ...)
elif tool_call.function.name == 'str_replace_editor':
action = FileEditAction(path=validated_params['path'], ...)
# ... etc for other tools
actions.append(action)
elif mcp_tool_names and tool_call.function.name in mcp_tool_names:
# Handle MCP tools
actions.append(MCPAction(...))
else:
raise FunctionCallNotExistsError(f'Tool {tool_call.function.name} not found')
return actions
```
## Risk Mitigation
1. **Incremental approach** - Each phase can be tested independently
2. **Backward compatibility** - Bridge layer ensures nothing breaks during transition
3. **Easy rollback** - Each tool migration is a separate commit
4. **Minimal changes** - Don't touch agent logic, only function calling layer
5. **Keep it simple** - Don't over-engineer, just replace existing functionality
## Success Criteria
- [ ] All existing tests pass
- [ ] Function calling behavior unchanged from user perspective
- [ ] Tool logic consolidated in single location
- [ ] Easy to add new tools by extending Tool base class
- [ ] Reduced code duplication across agents
- [ ] Cleaner, more maintainable codebase
## Files to Modify
**Phase 1:**
- `openhands/agenthub/codeact_agent/function_calling.py` (add bridge)
- `openhands/agenthub/codeact_agent/codeact_agent.py` (import registry)
**Phase 2:**
- `openhands/agenthub/codeact_agent/function_calling.py` (migrate tools one by one)
**Phase 3:**
- `openhands/agenthub/codeact_agent/function_calling.py` (simplify)
- Remove `openhands/agenthub/codeact_agent/tools/` directory
**Phase 4 (Optional):**
- `openhands/agenthub/loc_agent/function_calling.py`
- `openhands/agenthub/readonly_agent/function_calling.py`
This plan prioritizes **working incrementally** while **maintaining stability** throughout the refactoring process.
-219
View File
@@ -1,219 +0,0 @@
# OpenHands Tool Decoupling - Complete Implementation Plan
## 🎯 Goal
Decouple AI agent tools into their own classes to encapsulate tool definitions, error validation, and response interpretation separate from regular agent LLM response processing.
## 📊 Current Status: CRITICAL MILESTONE ACHIEVED ✅
**function_calling.py Migration Complete**: Successfully migrated CodeActAgent to use unified tool validation for all 4 core tools!
### 🏗️ Architecture Summary
- **CodeActAgent**: 4 base tools (BashTool, FileEditorTool, BrowserTool, FinishTool)
- **ReadOnlyAgent**: Inherits FinishTool + adds 3 safe tools (ViewTool, GrepTool, GlobTool)
- **LocAgent**: Inherits all CodeAct tools + adds 3 search tools (SearchEntityTool, SearchRepoTool, ExploreStructureTool)
### 🚀 Migration Achievement: function_calling.py Complete
-**Fixed legacy tool import conflicts** with proper aliasing (LegacyBrowserTool, LegacyFinishTool)
-**Updated BrowserTool interface** to match legacy (code parameter instead of action)
-**All 4 core tools using unified validation**:
- BashTool: `validate_parameters()` with proper error handling
- FinishTool: `validate_parameters()` with parameter mapping (summary/outputs)
- FileEditorTool: `validate_parameters()` with command handling (view/edit)
- BrowserTool: `validate_parameters()` with code parameter validation
-**Fixed tool name constant references** throughout function_calling.py
-**Created comprehensive integration tests** verifying tool validation works
-**Maintained backward compatibility** with legacy fallback paths
### 🧪 Testing Status
- **192 total tests** (all passing)
- **Integration tests passing** for all 4 core tools
- **163 original tests**: Base Tool class, validation, error handling, inheritance patterns
- **29 new LocAgent tests**: Complete coverage of search tools and inheritance
### 🔧 Implementation Status
-**Tool base class** with abstract methods and validation framework
-**CodeAct tools** with full parameter validation and schema generation
-**ReadOnly tools** with inheritance pattern and safety validation
-**LocAgent tools** with complex parameter validation and search capabilities
-**Comprehensive test suite** covering all tools and edge cases
-**CodeActAgent function_calling.py migration** with unified tool validation
## Architecture Decision: Agent-Specific Tool Organization
After exploring the codebase, we discovered that **agent-specific tool organization** is the correct approach because:
1. **CodeActAgent** is the base agent with comprehensive tools (bash, file editing, browsing, etc.)
2. **ReadOnlyAgent** and **LocAgent** inherit from CodeActAgent but completely override `_get_tools()`
3. Each agent has its own `tools/` directory and `function_calling.py` module
4. Child agents can selectively inherit parent tools and add their own
## Current Architecture
```
openhands/agenthub/codeact_agent/tools/unified/
├── __init__.py # Exports all CodeAct tools
├── base.py # Tool base class with validation
├── bash_tool.py # Full bash access
├── file_editor_tool.py # File editing capabilities
├── browser_tool.py # Web browsing
└── finish_tool.py # Task completion
openhands/agenthub/readonly_agent/tools/unified/
├── __init__.py # Imports FinishTool from CodeAct + own tools
├── view_tool.py # Safe file/directory viewing
├── grep_tool.py # Safe text search
└── glob_tool.py # Safe file pattern matching
openhands/agenthub/loc_agent/tools/unified/
└── [TODO] Inherit from CodeAct + add search tools
```
## Implementation Status
### ✅ COMPLETED (Phase 1: Tool Architecture)
- [x] Base Tool class with schema definition and parameter validation
- [x] CodeAct unified tools (BashTool, FileEditorTool, BrowserTool, FinishTool)
- [x] ReadOnly unified tools (ViewTool, GrepTool, GlobTool)
- [x] Inheritance pattern: ReadOnly imports FinishTool from CodeAct parent
- [x] Parameter validation with comprehensive error handling
- [x] Schema generation compatible with LiteLLM function calling
### ✅ COMPLETED (Phase 2: Tool Architecture & Testing)
- [x] **Comprehensive unit tests** (192 tests, all passing)
- [x] **LocAgent tool organization** (inherit from CodeAct + add search tools)
- [x] All agent-specific tool architectures complete
### 🔄 IN PROGRESS (Phase 3: Integration & Migration)
-**CodeActAgent function_calling.py migration** (COMPLETED!)
- [ ] ReadOnlyAgent function_calling.py migration (NEXT)
- [ ] LocAgent function_calling.py migration (NEXT)
### 📋 TODO (Phase 3: Full Migration)
- [ ] Remove old tool definitions after migration complete
- [ ] Documentation and cleanup
- [ ] Performance testing and optimization
## Detailed Implementation Plan
### Phase 2: Testing & Integration (CURRENT)
#### 2.1 Comprehensive Unit Tests (IMMEDIATE)
Create `tests/unit/tools/` with complete test coverage:
**Base Infrastructure Tests:**
- `test_base_tool.py` - Tool base class, validation, error handling
- `test_tool_inheritance.py` - Agent inheritance patterns
**CodeAct Tool Tests:**
- `test_bash_tool.py` - BashTool schema and validation
- `test_file_editor_tool.py` - FileEditorTool schema and validation
- `test_browser_tool.py` - BrowserTool schema and validation
- `test_finish_tool.py` - FinishTool schema and validation
**ReadOnly Tool Tests:**
- `test_view_tool.py` - ViewTool schema and validation
- `test_grep_tool.py` - GrepTool schema and validation
- `test_glob_tool.py` - GlobTool schema and validation
**Integration Tests:**
- `test_agent_tool_integration.py` - Agent-specific tool loading
- `test_function_call_validation.py` - End-to-end function call processing
#### 2.2 Bridge Layer Implementation
- Create adapter functions in each agent's function_calling.py
- Gradual migration: new tools alongside existing ones
- Validation layer that uses new Tool classes
#### 2.3 Integration Points
- Update `openhands/agenthub/codeact_agent/function_calling.py`
- Update `openhands/agenthub/readonly_agent/function_calling.py`
- Ensure backward compatibility during transition
### Phase 3: Full Migration
#### 3.1 LocAgent Tool Organization ✅
```
openhands/agenthub/loc_agent/tools/unified/
├── __init__.py # Inherit from CodeAct + add search tools
├── search_entity_tool.py # SearchEntityTool for entity retrieval
├── search_repo_tool.py # SearchRepoTool for code snippet search
└── explore_structure_tool.py # ExploreStructureTool for dependency analysis
```
#### 3.2 Complete Migration
- Replace all old tool definitions with new unified classes
- Update all function_calling.py modules
- Remove legacy tool code
- Update agent `_get_tools()` methods to use new architecture
#### 3.3 Cleanup & Documentation
- Remove unused tool files
- Update documentation
- Add migration guide for future tool additions
## Key Benefits of This Architecture
1. **Encapsulation**: Tool logic separated from agent processing
2. **Inheritance**: Child agents can reuse parent tools selectively
3. **Validation**: Centralized parameter validation with clear error messages
4. **Extensibility**: Easy to add new tools or modify existing ones
5. **Type Safety**: Proper typing and schema validation
6. **Testing**: Each tool can be unit tested independently
## Testing Strategy
### Unit Test Coverage Requirements
- **Schema Generation**: Verify correct LiteLLM-compatible schemas
- **Parameter Validation**: Test all validation rules and edge cases
- **Error Handling**: Test all error conditions and messages
- **Inheritance**: Verify child agents can inherit and extend parent tools
- **Integration**: Test function call processing end-to-end
### Test Categories
1. **Positive Tests**: Valid inputs produce expected outputs
2. **Negative Tests**: Invalid inputs produce appropriate errors
3. **Edge Cases**: Boundary conditions, empty values, type mismatches
4. **Integration Tests**: Agent-tool interaction, function calling flow
## Migration Strategy
1. **Parallel Implementation**: New tools alongside existing ones
2. **Gradual Adoption**: Migrate one agent at a time
3. **Backward Compatibility**: Maintain existing functionality during transition
4. **Validation**: Comprehensive testing at each step
5. **Cleanup**: Remove old code only after full migration
## Success Criteria
- [ ] All agents use unified tool architecture
- [ ] 100% test coverage for tool functionality
- [ ] No regression in existing functionality
- [ ] Clear separation of concerns between tools and agents
- [ ] Easy to add new tools or modify existing ones
- [ ] Comprehensive error handling and validation
## Current State Summary
**MAJOR MILESTONE ACHIEVED**: ReadOnlyAgent function_calling.py migration complete!
### Phase 2 Complete: Agent-Specific Tool Implementation ✅
- **CodeActAgent tools**: 4 unified tools (BashTool, FileEditorTool, BrowserTool, FinishTool)
- **ReadOnlyAgent tools**: 4 unified tools (ViewTool, GrepTool, GlobTool, FinishTool inherited)
- **LocAgent tools**: 3 specialized tools + all CodeAct tools inherited
- **All 192 tests passing** (163 original + 29 LocAgent tests)
### Phase 3 In Progress: function_calling.py Migration 🔄
- **CodeActAgent function_calling.py**: ✅ COMPLETE (unified validation for all 4 tools)
- **ReadOnlyAgent function_calling.py**: ✅ COMPLETE (unified validation for all 4 tools)
- **LocAgent function_calling.py**: ⏳ PENDING (next step)
### Architecture Summary
- **Tool Classes**: Encapsulate schema definition and parameter validation
- **Inheritance Pattern**: Child agents import parent tools + add their own
- **Validation Strategy**: Unified validation with legacy fallbacks
- **Error Handling**: Comprehensive ToolValidationError system
- **Testing**: 192 comprehensive unit tests covering all scenarios
**CURRENT**: LocAgent function_calling.py migration
**NEXT**: Final integration testing and cleanup
**GOAL**: Complete tool decoupling with zero regression
+3 -2
View File
@@ -363,10 +363,11 @@ classpath = "my_package.my_module.MyCustomAgent"
#confirmation_mode = false
# The security analyzer to use (For Headless / CLI only - In Web this is overridden by Session Init)
#security_analyzer = ""
# Available options: 'llm' (default), 'invariant'
#security_analyzer = "llm"
# Whether to enable security analyzer
#enable_security_analyzer = false
#enable_security_analyzer = true
#################################### Condenser #################################
# Condensers control how conversation history is managed and compressed when
+1 -1
View File
@@ -21,7 +21,7 @@ ENV POETRY_NO_INTERACTION=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
RUN apt-get update -y \
&& apt-get install -y curl make git build-essential \
&& apt-get install -y curl make git build-essential jq gettext \
&& python3 -m pip install poetry --break-system-packages
COPY pyproject.toml poetry.lock ./
+1 -1
View File
@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.53-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.54-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+3295 -1449
View File
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

+81 -34
View File
@@ -2,55 +2,102 @@
title: Backend Architecture
---
<div style={{ textAlign: 'center' }}>
<img src="https://github.com/All-Hands-AI/OpenHands/assets/16201837/97d747e3-29d8-4ccb-8d34-6ad1adb17f38" alt="OpenHands System Architecture Diagram Jul 4 2024" />
<p><em>OpenHands System Architecture Diagram (July 4, 2024)</em></p>
</div>
This is a high-level overview of the system architecture. The system is divided into two main components: the frontend and the backend. The frontend is responsible for handling user interactions and displaying the results. The backend is responsible for handling the business logic and executing the agents.
# Frontend architecture
# System overview
![system_architecture.svg](/static/img/system_architecture.svg)
```mermaid
flowchart LR
U["User"] --> FE["Frontend (SPA)"]
FE -- "HTTP/WS" --> BE["OpenHands Backend"]
BE --> ES["EventStream"]
BE --> ST["Storage"]
BE --> RT["Runtime Interface"]
BE --> LLM["LLM Providers"]
subgraph Runtime
direction TB
RT --> DRT["Docker Runtime"]
RT --> LRT["Local Runtime"]
RT --> RRT["Remote Runtime"]
DRT --> AES["Action Execution Server"]
LRT --> AES
RRT --> AES
AES --> Bash["Bash Session"]
AES --> Jupyter["Jupyter Plugin"]
AES --> Browser["BrowserEnv"]
end
```
This Overview is simplified to show the main components and their interactions. For a more detailed view of the backend architecture, see the Backend Architecture section below.
# Backend Architecture
_**Disclaimer**: The backend architecture is a work in progress and is subject to change. The following diagram shows the current architecture of the backend based on the commit that is shown in the footer of the diagram._
![backend_architecture.svg](/static/img/backend_architecture.svg)
```mermaid
classDiagram
class Agent {
<<abstract>>
+sandbox_plugins: list[PluginRequirement]
}
class CodeActAgent {
+tools
}
Agent <|-- CodeActAgent
class EventStream
class Observation
class Action
Action --> Observation
Agent --> EventStream
class Runtime {
+connect()
+send_action_for_execution()
}
class ActionExecutionClient {
+_send_action_server_request()
}
class DockerRuntime
class LocalRuntime
class RemoteRuntime
Runtime <|-- ActionExecutionClient
ActionExecutionClient <|-- DockerRuntime
ActionExecutionClient <|-- LocalRuntime
ActionExecutionClient <|-- RemoteRuntime
class ActionExecutionServer {
+/execute_action
+/alive
}
class BashSession
class JupyterPlugin
class BrowserEnv
ActionExecutionServer --> BashSession
ActionExecutionServer --> JupyterPlugin
ActionExecutionServer --> BrowserEnv
Agent --> Runtime
Runtime ..> ActionExecutionServer : REST
```
<details>
<summary>Updating this Diagram</summary>
<div>
The generation of the backend architecture diagram is partially automated.
The diagram is generated from the type hints in the code using the py2puml
tool. The diagram is then manually reviewed, adjusted and exported to PNG
and SVG.
We maintain architecture diagrams inline with Mermaid in this MDX.
## Prerequisites
- Running python environment in which openhands is executable
(according to the instructions in the README.md file in the root of the repository)
- [py2puml](https://github.com/lucsorel/py2puml) installed
## Steps
1. Autogenerate the diagram by running the following command from the root of the repository:
`py2puml openhands openhands > docs/architecture/backend_architecture.puml`
2. Open the generated file in a PlantUML editor, e.g. Visual Studio Code with the PlantUML extension or [PlantText](https://www.planttext.com/)
3. Review the generated PUML and make all necessary adjustments to the diagram (add missing parts, fix mistakes, improve positioning).
_py2puml creates the diagram based on the type hints in the code, so missing or incorrect type hints may result in an incomplete or incorrect diagram._
4. Review the diff between the new and the previous diagram and manually check if the changes are correct.
_Make sure not to remove parts that were manually added to the diagram in the past and are still relevant._
5. Add the commit hash of the commit that was used to generate the diagram to the diagram footer.
6. Export the diagram as PNG and SVG files and replace the existing diagrams in the `docs/architecture` directory. This can be done with (e.g. [PlantText](https://www.planttext.com/))
Guidance:
- Edit the Mermaid blocks directly (flowchart/classDiagram).
- Quote labels and edge text for GitHub preview compatibility.
- Keep relationships concise and reflect stable abstractions (agents, runtime client/server, plugins).
- Verify accuracy against code:
- openhands/runtime/impl/action_execution/action_execution_client.py
- openhands/runtime/impl/docker/docker_runtime.py
- openhands/runtime/impl/local/local_runtime.py
- openhands/runtime/action_execution_server.py
- openhands/runtime/plugins/*
- Build docs locally or view on GitHub to confirm diagrams render.
</div>
</details>
+42 -8
View File
@@ -52,7 +52,7 @@ graph TD
2. Image Building: OpenHands builds a new Docker image (the "OH runtime image") based on the user-provided image. This new image includes OpenHands-specific code, primarily the "runtime client"
3. Container Launch: When OpenHands starts, it launches a Docker container using the OH runtime image
4. Action Execution Server Initialization: The action execution server initializes an `ActionExecutor` inside the container, setting up necessary components like a bash shell and loading any specified plugins
5. Communication: The OpenHands backend (`openhands/runtime/impl/eventstream/eventstream_runtime.py`) communicates with the action execution server over RESTful API, sending actions and receiving observations
5. Communication: The OpenHands backend (client: `openhands/runtime/impl/action_execution/action_execution_client.py`; runtimes: `openhands/runtime/impl/docker/docker_runtime.py`, `openhands/runtime/impl/local/local_runtime.py`) communicates with the action execution server over RESTful API, sending actions and receiving observations
6. Action Execution: The runtime client receives actions from the backend, executes them in the sandboxed environment, and sends back observations
7. Observation Return: The action execution server sends execution results back to the OpenHands backend as observations
@@ -72,7 +72,7 @@ Check out the [relevant code](https://github.com/All-Hands-AI/OpenHands/blob/mai
### Image Tagging System
OpenHands uses a three-tag system for its runtime images to balance reproducibility with flexibility.
Tags may be in one of 2 formats:
The tags are:
- **Versioned Tag**: `oh_v{openhands_version}_{base_image}` (e.g.: `oh_v0.9.9_nikolaik_s_python-nodejs_t_python3.12-nodejs22`)
- **Lock Tag**: `oh_v{openhands_version}_{16_digit_lock_hash}` (e.g.: `oh_v0.9.9_1234567890abcdef`)
@@ -119,18 +119,52 @@ This tagging approach allows OpenHands to efficiently manage both development an
2. The system can quickly rebuild images when minor changes occur (by leveraging recent compatible images)
3. The **lock** tag (e.g., `runtime:oh_v0.9.3_1234567890abcdef`) always points to the latest build for a particular base image, dependency, and OpenHands version combination
## Volume mounts: named volumes and overlay
OpenHands supports both bind mounts and Docker named volumes in SandboxConfig.volumes:
- Bind mount: "/abs/host/path:/container/path[:mode]"
- Named volume: "volume:<name>:/container/path[:mode]" or any non-absolute host spec treated as a named volume
Overlay mode (copy-on-write layer) is supported for bind mounts by appending ":overlay" to the mode (e.g., ":ro,overlay").
To enable overlay COW, set SANDBOX_VOLUME_OVERLAYS to a writable host directory; per-container upper/work dirs are created under it. If SANDBOX_VOLUME_OVERLAYS is unset, overlay mounts are skipped.
Implementation references:
- openhands/runtime/impl/docker/docker_runtime.py (named volumes in _build_docker_run_args; overlay mounts in _process_overlay_mounts)
- openhands/core/config/sandbox_config.py (volumes field)
## Runtime Plugin System
The OpenHands Runtime supports a plugin system that allows for extending functionality and customizing the runtime environment. Plugins are initialized when the runtime client starts up.
The OpenHands Runtime supports a plugin system that allows for extending functionality and customizing the runtime environment. Plugins are initialized when the action execution server starts up inside the runtime.
Check [an example of Jupyter plugin here](https://github.com/All-Hands-AI/OpenHands/blob/ecf4aed28b0cf7c18d4d8ff554883ba182fc6bdd/openhands/runtime/plugins/jupyter/__init__.py#L21-L55) if you want to implement your own plugin.
## Ports and URLs
*More details about the Plugin system are still under construction - contributions are welcomed!*
- Host port allocation uses file-locked ranges for stability and concurrency:
- Main runtime port: find_available_port_with_lock on configured range
- VSCode port: SandboxConfig.sandbox.vscode_port if provided, else find_available_port_with_lock in VSCODE_PORT_RANGE
- App ports: two additional ranges for plugin/web apps
- DOCKER_HOST_ADDR (if set) adjusts how URLs are formed for LocalRuntime/Docker environments.
- VSCode URL is exposed with a connection token from the action execution server endpoint /vscode/connection_token and rendered as:
- Docker/Local: http://localhost:{port}/?tkn={token}&folder={workspace_mount_path_in_sandbox}
- RemoteRuntime: scheme://vscode-{host}/?tkn={token}&folder={workspace_mount_path_in_sandbox}
References:
- openhands/runtime/impl/docker/docker_runtime.py (port ranges, locking, DOCKER_HOST_ADDR, vscode_url)
- openhands/runtime/impl/local/local_runtime.py (vscode_url factory)
- openhands/runtime/impl/remote/remote_runtime.py (vscode_url mapping)
- openhands/runtime/action_execution_server.py (/vscode/connection_token)
Examples:
- Jupyter: openhands/runtime/plugins/jupyter/__init__.py (JupyterPlugin, Kernel Gateway)
- VS Code: openhands/runtime/plugins/vscode/* (VSCodePlugin, exposes tokenized URL)
- Agent Skills: openhands/runtime/plugins/agent_skills/*
Key aspects of the plugin system:
1. Plugin Definition: Plugins are defined as Python classes that inherit from a base `Plugin` class
2. Plugin Registration: Available plugins are registered in an `ALL_PLUGINS` dictionary
2. Plugin Registration: Available plugins are registered in `openhands/runtime/plugins/__init__.py` via `ALL_PLUGINS`
3. Plugin Specification: Plugins are associated with `Agent.sandbox_plugins: list[PluginRequirement]`. Users can specify which plugins to load when initializing the runtime
4. Initialization: Plugins are initialized asynchronously when the runtime client starts
5. Usage: The runtime client can use initialized plugins to extend its capabilities (e.g., the JupyterPlugin for running IPython cells)
4. Initialization: Plugins are initialized asynchronously when the runtime starts and are accessible to actions
5. Usage: Plugins extend capabilities (e.g., Jupyter for IPython cells); the server exposes any web endpoints (ports) via host port mapping
+1 -1
View File
@@ -65,7 +65,7 @@ To send follow-up messages for the same conversation, mention `@openhands` in a
Conversation is started by mentioning `@openhands`.
![slack-create-convo.png](/static/img/slack-create-convo.png)
![slack-create-conversation.png](/static/img/slack-create-conversation.png)
### See agent response and send follow up messages
+52
View File
@@ -0,0 +1,52 @@
# Confirmation Mode and Security Analyzers
OpenHands provides a security framework to help protect users from potentially risky actions through **Confirmation Mode** and **Security Analyzers**. This system analyzes agent actions and prompts users for confirmation when high-risk operations are detected.
## Overview
The security system consists of two main components:
1. **Confirmation Mode**: When enabled, the agent will pause and ask for user confirmation before executing actions that are flagged as high-risk by the security analyzer.
2. **Security Analyzers**: These are modules that evaluate the risk level of agent actions and determine whether user confirmation is required.
## Configuration
### CLI
In CLI mode, confirmation is enabled by default. You will have an option to uses the LLM Analyzer and will automatically confirm LOW and MEDIUM risk actions, only prompting for HIGH risk actions.
## Security Analyzers
OpenHands includes multiple analyzers:
- **No Analyzer**: Do not use any security analyzer. The agent will prompt you to confirm *EVERY* action.
- **LLM Risk Analyzer** (default): Uses the same LLM as the agent to assess action risk levels
- **Invariant Analyzer**: Uses Invariant Labs' policy engine to evaluate action traces against security policies
### LLM Risk Analyzer
The default analyzer that leverages the agent's LLM to evaluate the security risk of each action. It considers the action type, parameters, and context to assign risk levels.
### Invariant Analyzer
An advanced analyzer that:
- Collects conversation events and parses them into a trace
- Checks the trace against an Invariant policy to classify risk (low, medium, high)
- Manages an Invariant server container automatically if needed
- Supports optional browsing-alignment and harmful-content checks
## How It Works
1. **Action Analysis**: When the agent wants to perform an action, the selected security analyzer evaluates its risk level.
2. **Risk Assessment**: The analyzer returns one of three risk levels:
- **LOW**: Action proceeds without confirmation
- **MEDIUM**: Action proceeds without confirmation (may be configurable in future)
- **HIGH**: Action is paused, and user confirmation is requested
3. **User Confirmation**: For high-risk actions, a confirmation dialog appears with:
- Description of the action
- Risk assessment explanation
- Options to approve or deny action
4. **Action Execution**: Based on user response:
- **Approve**: Action proceeds as planned
- **Deny**: Action is cancelled
+3 -3
View File
@@ -119,7 +119,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -128,8 +128,8 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.53 \
python -m openhands.cli.main --override-cli-mode true
docker.all-hands.dev/all-hands-ai/openhands:0.54 \
python -m openhands.cli.entry --override-cli-mode true
```
<Note>
+2 -2
View File
@@ -61,7 +61,7 @@ export GITHUB_TOKEN="your-token" # Required for repository operations
# Run OpenHands
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -73,7 +73,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.53 \
docker.all-hands.dev/all-hands-ai/openhands:0.54 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+4 -4
View File
@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
2. Wait until the server is running (see log below):
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.53
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.54
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
+3 -3
View File
@@ -109,17 +109,17 @@ Note that you'll still need `uv` installed for the default MCP servers to work p
<Accordion title="Docker Command (Click to expand)">
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.53-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.54-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.53
docker.all-hands.dev/all-hands-ai/openhands:0.54
```
</Accordion>
+25
View File
@@ -130,3 +130,28 @@ docker run # ... \
<Note>
**Docker Desktop Required**: Network isolation features, including custom networks and `host.docker.internal` routing, require Docker Desktop. Docker Engine alone does not support these features on localhost across custom networks. If you're using Docker Engine without Docker Desktop, network isolation may not work as expected.
</Note>
### Sidecar Containers
If you want to run sidecar containers to the sandbox 'runner' containers without exposing the sandbox containers to the host network, you can use the `SANDBOX_ADDITIONAL_NETWORKS` environment variable to specify additional Docker network names that should be added to the sandbox containers.
```bash
docker network create openhands-sccache
docker run -d \
--hostname openhandsredis \
--network openhands-sccache \
redis
docker run # ...
-e SANDBOX_ADDITIONAL_NETWORKS='["openhands-sccache"]' \
# ...
```
Then all sandbox instances will have to access a shared redis instance at `openhandsredis:6379`.
#### Docker Compose gotcha
Note that Docker Compose adds a prefix (a scope) by default to created networks, which is not taken into account by the additional networks config. Therefore when using docker compose you have to either:
- specify a network name via the `name` field to remove the scoping (https://docs.docker.com/reference/compose-file/networks/#name)
- or provide the scope within the given config (e.g. `SANDBOX_ADDITIONAL_NETWORKS: '["myscope_openhands-sccache"]'` where `myscope` is the docker-compose assigned prefix).
+10 -12
View File
@@ -9,7 +9,8 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -60,18 +61,15 @@ AGENT_CLS_TO_INST_SUFFIX = {
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
# Create config with EDA-specific container image
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
# Override the container image for EDA
config.sandbox.base_container_image = 'python:3.12-bookworm'
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.enable_prompt_extensions = False
@@ -146,7 +144,7 @@ def process_instance(
logger.info(f'Final message: {final_message} | Ground truth: {instance["text"]}')
test_result = game.reward()
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
+8 -14
View File
@@ -17,7 +17,8 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -40,19 +41,12 @@ from openhands.utils.async_utils import call_async_from_sync
def get_config(
metadata: EvalMetadata,
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-slim'
# Create config with agent_bench-specific container image
config = get_openhands_config_for_eval(metadata=metadata)
# Override the container image for agent_bench
config.sandbox.base_container_image = 'python:3.12-slim'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.enable_prompt_extensions = False
@@ -273,7 +267,7 @@ def process_instance(
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
@@ -17,6 +17,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -49,15 +51,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.11-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
sandbox_config=sandbox_config,
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -246,7 +243,7 @@ def process_instance(
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
+6 -9
View File
@@ -15,6 +15,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -60,15 +62,10 @@ def get_config(
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = BIOCODER_BENCH_CONTAINER_IMAGE
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -294,7 +291,7 @@ def process_instance(
raise ValueError('State should not be None.')
test_result = complete_runtime(runtime, instance)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
+6 -9
View File
@@ -18,6 +18,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -74,15 +76,10 @@ def get_config(
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -422,7 +419,7 @@ def process_instance(
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
@@ -11,6 +11,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -39,14 +41,8 @@ def get_config(
)
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
workspace_base=None,
workspace_mount_path=None,
config = get_openhands_config_for_eval(
metadata=metadata, runtime='docker', sandbox_config=sandbox_config
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -88,7 +84,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
+7 -10
View File
@@ -16,6 +16,8 @@ from evaluation.utils.shared import (
assert_and_raise,
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -113,16 +115,11 @@ def get_config(
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
enable_browser=RUN_WITH_BROWSING,
config = get_openhands_config_for_eval(
metadata=metadata,
sandbox_config=sandbox_config,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
enable_browser=RUN_WITH_BROWSING,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -480,7 +477,7 @@ def process_instance(
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
histories = [event_to_dict(event) for event in state.history]
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
@@ -17,6 +17,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -64,15 +66,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -294,7 +291,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
test_result = complete_runtime(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
+6 -9
View File
@@ -22,6 +22,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -59,15 +61,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'nikolaik/python-nodejs:python3.12-nodejs22'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
sandbox_config=sandbox_config,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
if metadata.agent_config:
@@ -269,7 +266,7 @@ Here is the task:
'model_answer': model_answer,
'ground_truth': instance['Final answer'],
}
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
+6 -9
View File
@@ -12,6 +12,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -42,15 +44,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -108,7 +105,7 @@ def process_instance(
# attempt to parse model_answer
ast_eval_fn = instance['ast_eval']
correct, hallucination = ast_eval_fn(instance_id, model_answer_raw)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
logger.info(
f'Final message: {model_answer_raw} | Correctness: {correct} | Hallucination: {hallucination}'
)
+6 -9
View File
@@ -30,6 +30,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -63,15 +65,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -292,7 +289,7 @@ Ok now its time to start solving the question. Good luck!
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
output = EvalOutput(
@@ -23,6 +23,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -84,15 +86,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -248,7 +245,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
test_result = complete_runtime(runtime, instance)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
@@ -16,6 +16,7 @@ import ruamel.yaml
from evaluation.utils.shared import (
EvalMetadata,
get_default_sandbox_config_for_eval,
get_openhands_config_for_eval,
make_metadata,
)
from openhands.core.config import (
@@ -37,15 +38,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -22,6 +22,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -47,15 +49,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -335,7 +332,7 @@ Be thorough in your exploration, testing, and reasoning. It's fine if your think
)
)
assert state is not None
metrics = state.metrics.get() if state.metrics else {}
metrics = get_metrics(state)
test_result = complete_runtime(runtime, instance)
@@ -10,6 +10,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -51,15 +53,10 @@ def get_config(
'$OH_INTERPRETER_PATH -m pip install scitools-pyke'
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -247,7 +244,7 @@ def process_instance(
)
test_result['final_message'] = final_message
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
+6 -9
View File
@@ -13,6 +13,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -57,15 +59,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'xingyaoww/od-eval-miniwob:v1.0'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -174,7 +171,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Instruction is the first message from the USER
instruction = ''
+6 -9
View File
@@ -15,6 +15,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -109,15 +111,10 @@ def get_config(
f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}'
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -205,7 +202,7 @@ def process_instance(
task_state = state.extra_data['task_state']
logger.info('Task state: ' + str(task_state.to_dict()))
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
+6 -9
View File
@@ -26,6 +26,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -79,15 +81,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'public.ecr.aws/i5g0m1f6/ml-bench'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -250,7 +247,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
)
)
assert state is not None
metrics = state.metrics.get() if state.metrics else {}
metrics = get_metrics(state)
test_result = complete_runtime(runtime)
@@ -23,6 +23,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
get_default_sandbox_config_for_eval,
get_openhands_config_for_eval,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
@@ -87,13 +88,9 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
run_as_openhands=False,
config = get_openhands_config_for_eval(
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
return config
@@ -21,6 +21,7 @@ from evaluation.utils.shared import (
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -341,16 +342,11 @@ def get_config(
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
config = get_openhands_config_for_eval(
metadata=metadata,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -0,0 +1,45 @@
# Evaluate OpenHands on NoCode-bench
## LLM Setup
Please follow [here](../../README.md#setup).
## Docker image download
Evaluating OpenHands on NoCode-bench need instance-level docker image.
Please follow the instructions of NoCode-bench image setup to build or download all instance-level dokcer [here](https://github.com/NoCode-bench/NoCode-bench).
## Generate patch
Please follow the instructions [here](../swe_bench/README.md#running-locally-with-docker)
For example,
```bash
bash ./evaluation/benchmarks/nocode_bench/scripts/run_infer_nc.sh llm.claude HEAD CodeActAgent 114 100 10 NoCode-bench/NoCode-bench_Verified test
```
The results will be generated in evaluation/evaluation_outputs/outputs/XXX/CodeActAgent/YYY/output.jsonl.
## Runing evaluation
First, install [NoCode-bench](https://github.com/NoCode-bench/NoCode-bench).
Second, convert the output.jsonl to patch.jsonl with [script](scripts/eval/convert.py).
```bash
python evaluation/benchmarks/multi_swe_bench/scripts/eval/convert.py
```
Finally, evaluate with NoCode-bench.
```bash
export PYTHONPATH=$PYTHONPATH:$(pwd)
python ./evaluation/eval.py \
--predictions_path ./all_preds.jsonl \ # <path_to_your_predictions>
--log_dir ./evaluation/logs \ # <path_to_your_log_dir>
--bench_tasks NoCode-bench/NoCode-bench_Verified \ # <dataset_name>
--max_workers 110 \ # <number_of_workers>
--output_file eval_result.txt \ # <path_to_your_output_file>
--image_level repo \ # <cache_image_level>
--timeout 600 \ # <timeout_in_seconds>
--proxy None # <proxy_if_needed>
```
@@ -0,0 +1,52 @@
"""
Utilities for handling binary files and patch generation in SWE-bench evaluation.
"""
def remove_binary_diffs(patch_text):
"""
Remove binary file diffs from a git patch.
Args:
patch_text (str): The git patch text
Returns:
str: The cleaned patch text with binary diffs removed
"""
lines = patch_text.splitlines()
cleaned_lines = []
block = []
is_binary_block = False
for line in lines:
if line.startswith('diff --git '):
if block and not is_binary_block:
cleaned_lines.extend(block)
block = [line]
is_binary_block = False
elif 'Binary files' in line:
is_binary_block = True
block.append(line)
else:
block.append(line)
if block and not is_binary_block:
cleaned_lines.extend(block)
return '\n'.join(cleaned_lines)
def remove_binary_files_from_git():
"""
Generate a bash command to remove binary files from git staging.
Returns:
str: A bash command that removes binary files from git staging
"""
return """
for file in $(git status --porcelain | grep -E "^(M| M|\\?\\?|A| A)" | cut -c4-); do
if [ -f "$file" ] && (file "$file" | grep -q "executable" || git check-attr binary "$file" | grep -q "binary: set"); then
git rm -f "$file" 2>/dev/null || rm -f "$file"
echo "Removed: $file"
fi
done
""".strip()
@@ -0,0 +1,545 @@
DOCPATH_PATTERNS = [
r'docs/',
r'^CHANGES\.rst$',
r'doc/',
r'ChangeLog',
r'^changelog/',
r'^CHANGES$',
]
MATPLOTLIB_CONFIG = {
k: {
'python': '3.11',
'conda_env': 'matplotlib_35',
'install': 'python -m pip install -e .',
'test_cmd': 'pytest -rA --color=no',
}
for k in ['3.5', '3.6', '3.7', '3.8', '3.9']
}
MATPLOTLIB_CONFIG.update(
{
k: {
'python': '3.8',
'conda_env': 'matplotlib_31',
'install': 'python -m pip install -e .',
'test_cmd': 'pytest -rA --color=no',
}
for k in ['3.1', '3.2', '3.3', '3.4']
}
)
MATPLOTLIB_CONFIG.update(
{
k: {
'python': '3.5',
'install': 'python setup.py build; python setup.py install',
'conda_env': 'matplotlib_11',
'nonroot': True,
'test_cmd': 'pytest -rA --color=no',
}
for k in ['2.0', '2.1', '2.2', '1.0', '1.1', '1.2', '1.3', '1.4', '1.5']
}
)
for k in ['3.8', '3.9']:
MATPLOTLIB_CONFIG[k]['install'] = (
'python -m pip install --no-build-isolation -e ".[dev]"'
)
SYMPY_CONFIG = {}
SYMPY_CONFIG.update(
{
'1.0': {
'conda_env': 'sympy_10',
'install': 'pip install -e .',
'test_cmd': 'bin/test -C -v',
# testfile -k testname
}
}
)
REQUESTS_CONFIG = {}
REQUESTS_CONFIG.update(
{
k: {
'conda_env': 'requests_227',
'install': 'pip install -r requirements-dev.txt',
'test_cmd': 'pytest -rA',
}
for k in ['2.27']
}
)
REQUESTS_CONFIG.update(
{
k: {
'conda_env': 'requests_226',
'install': 'pip install -e .',
'test_cmd': 'pytest -rA',
}
for k in ['2.26']
}
)
PYTEST_CONFIG = {}
PYTEST_CONFIG.update(
{
k: {
'conda_env': 'pytest_33',
'install': 'pip install -e .',
'test_cmd': 'pytest -v --color=no',
}
for k in ['4.4', '4.1', '3.7', '3.4', '3.3']
}
)
PYLINT_CONFIG = {}
PYLINT_CONFIG.update(
{
k: {
'conda_env': 'pylint_210',
'install': 'pip install -r requirements_test.txt',
'test_cmd': 'pytest -rA --color=no',
}
for k in [
'2.10',
'2.11',
'2.13',
'2.14',
'2.15',
'2.16',
'2.17',
'3.0',
'3.1',
'3.2',
'3.3',
]
}
)
PYLINT_CONFIG.update(
{
k: {
'conda_env': 'pylint_210',
'pre_install': [
r"sed -i 's/setuptools==[0-9.]\+/setuptools==58.0.0/' requirements_test_min.txt"
],
'install': 'pip install -r requirements_test.txt',
'test_cmd': 'pytest -rA --color=no',
}
for k in ['3.0', '3.1', '3.2', '3.3']
}
)
ASTROPY_CONFIG = {}
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_11',
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['1.1', '1.2', '1.3', '2.0']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_30',
'pre_install': """echo '[pytest]
filterwarnings =
ignore::DeprecationWarning' > pytest.ini""",
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['3.0', '3.1', '3.2']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_40',
'pre_install': [
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml"""
],
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['4.0']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_41',
'pre_install': [
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml""",
"""sed -i 's/^qt_no_exception_capture = 1$/; qt_no_exception_capture = 1/' setup.cfg""",
r"""sed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.tomlsed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.toml""",
],
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['4.1']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_42',
'pre_install': [
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml""",
r"""sed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.tomlsed -i '/setuptools==68.0.0",/a \ "markupsafe==2.0.1",' pyproject.toml""",
],
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['4.2', '4.3', '5.0', '5.1']
}
)
ASTROPY_CONFIG.update(
{
k: {
'conda_env': 'astropy_52',
'pre_install': [
r"""sed -i 's/requires = \["setuptools",/requires = \["setuptools==68.0.0",/' pyproject.toml"""
],
'install': 'python -m pip install -e .[test] --verbose',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['5.2', '5.3', '6.0', '6.1', '7.0']
}
)
DJANGO_CONFIG = {}
DJANGO_CONFIG.update(
{
k: {
'install': 'pip install -e .',
'conda_env': 'django_22',
'test_cmd': 'python tests/runtests.py --verbosity 2',
}
for k in ['1.9', '2.2']
}
)
DJANGO_CONFIG.update(
{
'3.2': {
'install': 'pip install -e .',
'conda_env': 'django_32',
'test_cmd': 'python tests/runtests.py --verbosity 2',
},
'4.2': {
'install': 'pip install -e .',
'conda_env': 'django_42',
'test_cmd': 'python tests/runtests.py --verbosity 2',
},
'5.1': {
'install': 'pip install -e .',
'conda_env': 'django_51',
'test_cmd': 'python tests/runtests.py --verbosity 2',
},
}
)
SPHINX_CONFIG = {}
SPHINX_CONFIG.update(
{ # 1.x 版本问题,实际无用
k: {
'conda_env': 'sphinx_20',
'install': 'python -m pip install -e .[test]',
'pre_install': ["sed -i 's/pytest/pytest -rA/' tox.ini"],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['1.3', '1.4', '1.5', '1.6', '1.7', '1.8']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_20',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['2.0', '2.1', '2.2', '2.3', '2.4']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_30',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
"sed -i 's/sphinxcontrib-htmlhelp/sphinxcontrib-htmlhelp<=2.0.4/' setup.py",
"sed -i 's/sphinxcontrib-serializinghtml/sphinxcontrib-serializinghtml<=1.1.9/' setup.py",
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['3.0', '3.1', '3.2', '3.3', '3.4', '3.5', '4.0']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_30',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
(
"grep -q 'sphinxcontrib-htmlhelp>=2.0.0' setup.py && "
"sed -i 's/sphinxcontrib-htmlhelp>=2.0.0/sphinxcontrib-htmlhelp>=2.0.0,<=2.0.4/' setup.py || "
"sed -i 's/sphinxcontrib-htmlhelp/sphinxcontrib-htmlhelp<=2.0.4/' setup.py"
),
(
"grep -q 'sphinxcontrib-serializinghtml>=1.1.5' setup.py && "
"sed -i 's/sphinxcontrib-serializinghtml>=1.1.5/sphinxcontrib-serializinghtml>=1.1.5,<=1.1.9/' setup.py || "
"sed -i 's/sphinxcontrib-serializinghtml/sphinxcontrib-serializinghtml<=1.1.9/' setup.py"
),
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['4.1']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_30',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
"sed -i 's/Jinja2>=2.3/Jinja2<3.0/' setup.py",
"sed -i 's/sphinxcontrib-applehelp/sphinxcontrib-applehelp<=1.0.7/' setup.py",
"sed -i 's/sphinxcontrib-devhelp/sphinxcontrib-devhelp<=1.0.5/' setup.py",
"sed -i 's/sphinxcontrib-qthelp/sphinxcontrib-qthelp<=1.0.6/' setup.py",
"sed -i 's/alabaster>=0.7,<0.8/alabaster>=0.7,<0.7.12/' setup.py",
"sed -i \"s/'packaging',/'packaging', 'markupsafe<=2.0.1',/\" setup.py",
"sed -i 's/sphinxcontrib-htmlhelp>=2.0.0/sphinxcontrib-htmlhelp>=2.0.0,<=2.0.4/' setup.py",
"sed -i 's/sphinxcontrib-serializinghtml>=1.1.5/sphinxcontrib-serializinghtml>=1.1.5,<=1.1.9/' setup.py",
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['4.2', '4.3', '4.4']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_30',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
],
'test_cmd': 'tox --current-env -epy37 -v --',
}
for k in ['4.5', '5.0', '5.1', '5.2']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_60',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
],
'test_cmd': 'tox --current-env -epy39 -v --',
}
for k in ['6.0', '6.2', '7.0', '7.1']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_72',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
'apt-get update && apt-get install -y graphviz',
],
'test_cmd': 'tox --current-env -epy39 -v --',
}
for k in ['7.2', '7.3', '7.4']
}
)
SPHINX_CONFIG.update(
{
k: {
'conda_env': 'sphinx_80',
'install': 'python -m pip install -e .[test]',
'pre_install': [
"sed -i 's/pytest/pytest -rA/' tox.ini",
],
'test_cmd': 'tox --current-env -epy310 -v --',
}
for k in ['8.0', '8.1']
}
)
SKLEARN_CONFIG = {}
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_020',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.20', '0.21', '0.22']
}
)
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_100',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.23', '0.24', '1.00', '1.01', '1.02']
}
)
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_104',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['1.03', '1.04', '1.05']
}
)
SEABORN_CONFIG = {}
SEABORN_CONFIG.update(
{
k: {
'conda_env': 'seaborn_010',
'install': 'pip install -e .[dev]',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.3', '0.4', '0.5', '0.6', '0.11', '0.12', '0.13', '0.14']
}
)
XARRAY_CONFIG = {}
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_0014',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0014', '0015', '0016']
}
)
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_0017',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0017', '0018', '0019', '0020', '0021']
}
)
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_2203',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['2203', '2206', '2209', '2210', '2211', '2212']
}
)
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_2303',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in [
'2303',
'2304',
'2305',
'2306',
'2308',
'2309',
'2310',
'2311',
'2312',
]
}
)
XARRAY_CONFIG.update(
{
k: {
'conda_env': 'xarray_2401',
'install': 'pip install -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['2401', '2402', '2403', '2405', '2407', '2409', '2410', '2411']
}
)
SKLEARN_CONFIG = {}
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_020',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.20', '0.21', '0.22']
}
)
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_100',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['0.23', '0.24', '1.00', '1.01', '1.02']
}
)
SKLEARN_CONFIG.update(
{
k: {
'conda_env': 'skl_104',
'install': 'pip install -v --no-use-pep517 --no-build-isolation -e .',
'test_cmd': 'pytest --color=no -rA',
}
for k in ['1.03', '1.04', '1.05', '1.06', '1.07']
}
)
MAP_REPO_TO_CONFIG = {
'pydata/xarray': XARRAY_CONFIG,
'mwaskom/seaborn': SEABORN_CONFIG,
'scikit-learn/scikit-learn': SKLEARN_CONFIG,
'sphinx-doc/sphinx': SPHINX_CONFIG,
'django/django': DJANGO_CONFIG,
'astropy/astropy': ASTROPY_CONFIG,
'pylint-dev/pylint': PYLINT_CONFIG,
'pytest-dev/pytest': PYTEST_CONFIG,
'psf/requests': REQUESTS_CONFIG,
'sympy/sympy': SYMPY_CONFIG,
'matplotlib/matplotlib': MATPLOTLIB_CONFIG,
}
@@ -0,0 +1,65 @@
<uploaded_files>
/workspace/{{ workspace_dir_name }}
</uploaded_files>
I've uploaded a python code repository in the directory {{ workspace_dir_name }}. Consider the following issue description:
<doc_change>
{{ instance.problem_statement }}
</doc_change>
Can you help me add the new features to the repository based on the changes in the <doc_change>?
I've already taken care of all changes to any of the test files described in the <doc_change>. This means you DON'T have to modify the testing logic or any of the tests in any way!
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
Your task is to make the minimal changes to non-test files in the /workspace/{{ workspace_dir_name }} directory to implement the new features required by the documentation updates.
Follow these phases to resolve the issue:
Phase 1. READING: read the requirements and reword it in clearer terms
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
1.2 Hightlight method names, variables, file names, stack traces, and technical details, particularly those related to new features.
1.3 Explain the new feature requirements in clear terms.
1.4 Specify functional scope and expected behavior of new features.
1.5 Hightlight any best practices to take into account when developing and testing the new feature.
Phase 2. RUNNING: install and run the functionality in the repository to validate the new features
2.1 Follow the readme.
2.2 Install the environment and anything needed.
2.2 Iterate and figure out how to validate the newly added features.
Phase 3. EXPLORATION: find the files related to the new features and possible implementation solutions
3.1 Use `grep` to search for relevant methods, classes, keywords and feature requirements.
3.2 Identify all files related to the new features.
3.3 Propose the methods and files to implement the new features and explain why.
3.4 From the possible file locations, select the most likely location to implement the new features.
Phase 4. TEST CREATION: before implementing any new features, create a script to validate the feature's correctness.
4.1 Look at existing test files in the repository to understand the test format/structure.
4.2 Create a minimal validation script to verify the newly added features.
4.3 Run the validation script to confirm the new features are successfully added and working as expected.
4.4 Adjust the validation script as necessary to ensure the new features fully meet the requirements.
Phase 5. FEATURE ANALYSIS: state clearly the new feature and how to implement it
5.1 State clearly what the new feature is.
5.2 State clearly where the feature should be implemented.
5.3 State clearly how the test validates the new feature.
5.4 State clearly the best practices to take into account when implementing the new feature.
5.5 State clearly how to implement the new feature.
Phase 6. FEATURE IMPLEMENTATION: edit the source code to implement your chosen solution for the new feature
6.1 Make minimal, focused changes to implement the new feature.
Phase 7. VERIFICATION: Test your new feature thoroughly.
7.1 Run your validation script to verify the new feature works as expected.
7.2 Add edge cases to your test script to ensure comprehensive coverage of the new feature.
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
Phase 8. FINAL REVIEW: Carefully re-read the feature requirements and compare your changes with the base commit {{ instance.base_commit }}
8.1 Ensure you've fully implemented all required features.
8.2 Run any tests in the repository related to:
8.2.1 The new features you are adding
8.2.2 The files you modified
8.2.3 The functions you changed
8.3 If any tests fail, revise your implementation until all tests pass and the new feature works as expected.
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
@@ -0,0 +1,39 @@
"""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.info(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.debug(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))
@@ -0,0 +1,905 @@
import asyncio
import copy
import json
import os
import tempfile
from typing import Any, Literal
import numpy as np
import pandas as pd
import toml
from datasets import load_dataset
from jinja2 import Environment, FileSystemLoader
import openhands.agenthub
from evaluation.benchmarks.nocode_bench.binary_patch_utils import (
remove_binary_diffs,
remove_binary_files_from_git,
)
from evaluation.benchmarks.nocode_bench.consistants import MAP_REPO_TO_CONFIG
from evaluation.benchmarks.nocode_bench.resource.mapping import (
get_instance_resource_factor,
)
from evaluation.benchmarks.nocode_bench.scripts.utils.evaluation_utils import (
run_evaluation_nocode_bench,
)
from evaluation.utils.shared import (
EvalException,
EvalMetadata,
EvalOutput,
assert_and_raise,
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
)
from openhands.core.config.condenser_config import NoOpCondenserConfig
from openhands.core.config.utils import get_condenser_config_arg
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.critic import AgentFinishedCritic
from openhands.events.action import CmdRunAction, FileReadAction, MessageAction
from openhands.events.observation import (
CmdOutputObservation,
ErrorObservation,
FileReadObservation,
)
from openhands.events.serialization.event import event_from_dict, event_to_dict
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
ENABLE_LLM_EDITOR = os.environ.get('ENABLE_LLM_EDITOR', 'false').lower() == 'true'
BenchMode = Literal['swe', 'swt', 'swt-ci']
# Global variable to track dataset type
DATASET_TYPE = 'nc_bench'
def set_dataset_type(dataset_name: str) -> str:
"""Set dataset type based on dataset name."""
global DATASET_TYPE
DATASET_TYPE = 'nc_bench'
logger.info(f'Dataset type set to: {DATASET_TYPE}')
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
}
def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
return f'{instance.repo.split("/")[-1]}'
def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageAction:
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
metadata.details['mode']
# Determine the template file based on mode and LLM
template_name = 'nc.j2'
# Set up Jinja2 environment
# Assuming templates are in 'evaluation/benchmarks/swe_bench/prompts' relative to this script
prompts_dir = os.path.join(os.path.dirname(__file__), 'prompts')
env = Environment(loader=FileSystemLoader(prompts_dir))
template = env.get_template(template_name)
# Prepare context for rendering
context = {
'instance': instance,
'workspace_dir_name': workspace_dir_name,
'metadata': metadata, # Pass metadata if needed in templates
}
context['test_instructions'] = '' # Ensure it's defined for other modes
# Render the instruction
instruction = template.render(context)
if RUN_WITH_BROWSING:
instruction += (
'<IMPORTANT!>\nYou SHOULD NEVER attempt to browse the web. </IMPORTANT!>\n'
)
if 'image_assets' in instance:
assets = json.loads(instance['image_assets'])
assert 'problem_statement' in assets, (
'problem_statement is required in image_assets'
)
image_urls = assets['problem_statement']
return MessageAction(content=instruction, image_urls=image_urls)
return MessageAction(content=instruction)
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get(
'EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/'
)
logger.info(f'Default docker image prefix: {DEFAULT_DOCKER_IMAGE_PREFIX}')
def get_instance_docker_image(
instance_id: str,
swebench_official_image: bool = False,
) -> str:
if swebench_official_image:
# Official NoCode-Bench image
image_name = f'ncbench_{instance_id}:latest'.lower()
logger.debug(f'Using official NoCode-Bench image: {image_name}')
return image_name
else:
raise
def get_config(
instance: pd.Series,
metadata: EvalMetadata,
) -> OpenHandsConfig:
# We use a different instance image for the each instance of NoCode-bench eval
use_swebench_official_image = True
base_container_image = get_instance_docker_image(
instance['instance_id'],
swebench_official_image=use_swebench_official_image,
)
logger.info(
f'Using instance container image: {base_container_image}. '
f'Please make sure this image exists. '
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
sandbox_config.enable_auto_lint = True
sandbox_config.use_host_network = False
# Add platform to the sandbox config to solve issue 4401
sandbox_config.platform = 'linux/amd64'
sandbox_config.remote_runtime_resource_factor = get_instance_resource_factor(
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = get_openhands_config_for_eval(
metadata=metadata,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
)
)
# get 'draft_editor' config if exists
config.set_llm_config(get_llm_config_arg('draft_editor'), 'draft_editor')
agent_config = AgentConfig(
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=ENABLE_LLM_EDITOR,
enable_mcp=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
config.set_agent_config(agent_config)
return config
def make_serializable(obj):
if isinstance(obj, pd.Series):
obj = obj.to_dict()
if isinstance(obj, dict):
return {k: make_serializable(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [make_serializable(v) for v in obj]
elif isinstance(obj, tuple):
return tuple(make_serializable(v) for v in obj)
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, pd.Timestamp):
return str(obj)
else:
return obj
def initialize_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required
metadata: EvalMetadata,
):
"""Initialize the runtime for the agent.
This function is called before the runtime is used to run the agent.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Initialization Fn')
logger.info('-' * 30)
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
obs: CmdOutputObservation
# Set instance id and git configuration
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 && git config --global core.pager "" && git config --global diff.binary false"""
)
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 export SWE_INSTANCE_ID and configure git: {str(obs)}',
)
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
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 export USER: {str(obs)}')
# inject the init script
script_dir = os.path.dirname(__file__)
# inject the instance info
action = CmdRunAction(command='mkdir -p /swe_util/eval_data/instances')
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 create /swe_util/eval_data/instances: {str(obs)}',
)
swe_instance_json_name = 'swe-bench-instance.json'
with tempfile.TemporaryDirectory() as temp_dir:
# Construct the full path for the desired file name within the temporary directory
temp_file_path = os.path.join(temp_dir, swe_instance_json_name)
# Write to the file with the desired name within the temporary directory
with open(temp_file_path, 'w') as f:
if not isinstance(instance, dict):
instance_dict = make_serializable(instance)
else:
instance_dict = dict(instance)
if DATASET_TYPE == 'nc_bench':
config = MAP_REPO_TO_CONFIG.get(instance['repo'], {}).get(
instance['version'], []
)
docker_conda_env_name = config['conda_env']
instance_dict['conda_env'] = docker_conda_env_name
json.dump([instance_dict], f)
# Copy the file to the desired location
runtime.copy_to(temp_file_path, '/swe_util/eval_data/instances/')
# inject the instance swe entry
entry_script_path = 'instance_nc_entry.sh'
runtime.copy_to(
str(os.path.join(script_dir, f'scripts/setup/{entry_script_path}')),
'/swe_util/',
)
action = CmdRunAction(command='cat ~/.bashrc')
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.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if isinstance(obs, ErrorObservation):
logger.error(f'Failed to source ~/.bashrc: {str(obs)}')
assert_and_raise(obs.exit_code == 0, f'Failed to source ~/.bashrc: {str(obs)}')
action = CmdRunAction(command=f'source /swe_util/{entry_script_path}')
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 source /swe_util/{entry_script_path}: {str(obs)}',
)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
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 cd to /workspace/{workspace_dir_name}: {str(obs)}',
)
action = CmdRunAction(command='git reset --hard')
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 git reset --hard: {str(obs)}')
action = CmdRunAction(
command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
)
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)}')
if DATASET_TYPE != 'Multimodal' and DATASET_TYPE != 'SWE-bench-Live':
# Only for non-multimodal datasets, we need to activate the testbed environment for Python
# SWE-Bench multimodal datasets and SWE-bench-Live are not using the testbed environment
action = CmdRunAction(command='which python')
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'Expected to find python interpreter, but got: {str(obs)}',
)
logger.info('-' * 30)
logger.info('END Runtime Initialization Fn')
logger.info('-' * 30)
def complete_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required, but it is used to get the workspace_dir_name
) -> dict[str, Any]:
"""Complete the runtime for the agent.
This function is called before the runtime is used to run the agent.
If you need to do something in the sandbox to get the correctness metric after
the agent has run, modify this function.
"""
logger.info('-' * 30)
logger.info('BEGIN Runtime Completion Fn')
logger.info('-' * 30)
obs: CmdOutputObservation
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if obs.exit_code == -1:
# The previous command is still running
# We need to kill previous command
logger.info('The previous command is still running, trying to kill it...')
action = CmdRunAction(command='C-c')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
# Then run the command again
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
if obs.exit_code == -1:
# The previous command is still running
# We need to kill previous command
logger.info('The previous command is still running, trying to ctrl+z it...')
action = CmdRunAction(command='C-z')
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
# Then run the command again
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
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(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to cd to /workspace/{workspace_dir_name}: {str(obs)}',
)
action = CmdRunAction(command='git config --global core.pager ""')
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(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to git config --global core.pager "": {str(obs)}',
)
# First check for any git repositories in subdirectories
action = CmdRunAction(command='find . -type d -name .git -not -path "./.git"')
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(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to find git repositories: {str(obs)}',
)
git_dirs = [p for p in obs.content.strip().split('\n') if p]
if git_dirs:
# Remove all .git directories in subdirectories
for git_dir in git_dirs:
action = CmdRunAction(command=f'rm -rf "{git_dir}"')
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(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to remove git directory {git_dir}: {str(obs)}',
)
# add all files
action = CmdRunAction(command='git add -A')
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(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to git add -A: {str(obs)}',
)
# Remove binary files from git staging
action = CmdRunAction(command=remove_binary_files_from_git())
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(
isinstance(obs, CmdOutputObservation) and obs.exit_code == 0,
f'Failed to remove binary files: {str(obs)}',
)
n_retries = 0
git_patch = None
while n_retries < 5:
action = CmdRunAction(
command=f'git diff --no-color --cached {instance["base_commit"]} > patch.diff'
)
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'})
n_retries += 1
if isinstance(obs, CmdOutputObservation):
if obs.exit_code == 0:
# Read the patch file
action = FileReadAction(path='patch.diff')
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'})
if isinstance(obs, FileReadObservation):
git_patch = obs.content
break
elif isinstance(obs, ErrorObservation):
# Fall back to cat "patch.diff" to get the patch
assert 'File could not be decoded as utf-8' in obs.content
action = CmdRunAction(command='cat patch.diff')
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert isinstance(obs, CmdOutputObservation) and obs.exit_code == 0
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
git_patch = obs.content
break
else:
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
else:
logger.info('Failed to get git diff, retrying...')
sleep_if_should_continue(10)
elif isinstance(obs, ErrorObservation):
logger.error(f'Error occurred: {obs.content}. Retrying...')
sleep_if_should_continue(10)
else:
assert_and_raise(False, f'Unexpected observation type: {str(obs)}')
assert_and_raise(git_patch is not None, 'Failed to get git diff (None)')
# Remove binary diffs from the patch
git_patch = remove_binary_diffs(git_patch)
logger.info('-' * 30)
logger.info('END Runtime Completion Fn')
logger.info('-' * 30)
return {'git_patch': git_patch}
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
runtime_failure_count: int = 0,
) -> EvalOutput:
config = get_config(instance, metadata)
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
# Increase resource_factor with increasing attempt_id
if runtime_failure_count > 0:
config.sandbox.remote_runtime_resource_factor = min(
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
8,
)
logger.warning(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
metadata = copy.deepcopy(metadata)
metadata.details['runtime_failure_count'] = runtime_failure_count
metadata.details['remote_runtime_resource_factor'] = (
config.sandbox.remote_runtime_resource_factor
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance, metadata)
message_action = get_instruction(instance, metadata)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=message_action,
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
metadata.agent_class
],
)
)
# if fatal error, throw EvalError to trigger re-run
if is_fatal_evaluation_error(state.last_error):
raise EvalException('Fatal error detected: ' + state.last_error)
# ======= THIS IS SWE-Bench specific =======
# Get git patch
if DATASET_TYPE == 'SWE-bench-Live':
from evaluation.benchmarks.swe_bench.live_utils import (
complete_runtime as complete_runtime_fn,
)
else:
complete_runtime_fn = complete_runtime
return_val = complete_runtime_fn(runtime, instance)
git_patch = return_val['git_patch']
logger.info(
f'Got git diff for instance {instance.instance_id}:\n--------\n{git_patch}\n--------'
)
finally:
runtime.close()
# ==========================================
# ======= Attempt to evaluate the agent's edits =======
# we use eval_infer.sh to evaluate the agent's edits, not here
# because the agent may alter the environment / testcases
test_result = {
'git_patch': git_patch,
}
# If you are working on some simpler benchmark that only evaluates the final model output (e.g., in a MessageAction)
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
if state is None:
raise ValueError('State should not be None.')
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
histories = [event_to_dict(event) for event in state.history]
metrics = get_metrics(state)
# Save the output
instruction = message_action.content
if message_action.image_urls:
instruction += (
'\n\n<image_urls>' + '\n'.join(message_action.image_urls) + '</image_urls>'
)
output = EvalOutput(
instance_id=instance.instance_id,
instruction=instruction,
instance=instance.to_dict(), # SWE Bench specific
test_result=test_result,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
)
return output
def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.toml')
if os.path.exists(file_path):
with open(file_path, 'r') as file:
data = toml.load(file)
if 'selected_ids' in data:
selected_ids = data['selected_ids']
logger.info(
f'Filtering {len(selected_ids)} tasks from "selected_ids"...'
)
subset = dataset[dataset[filter_column].isin(selected_ids)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
if 'selected_repos' in data:
# repos for the swe-bench instances:
# ['astropy/astropy', 'django/django', 'matplotlib/matplotlib', 'mwaskom/seaborn', 'pallets/flask', 'psf/requests', 'pydata/xarray', 'pylint-dev/pylint', 'pytest-dev/pytest', 'scikit-learn/scikit-learn', 'sphinx-doc/sphinx', 'sympy/sympy']
selected_repos = data['selected_repos']
if isinstance(selected_repos, str):
selected_repos = [selected_repos]
assert isinstance(selected_repos, list)
logger.info(
f'Filtering {selected_repos} tasks from "selected_repos"...'
)
subset = dataset[dataset['repo'].isin(selected_repos)]
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
if __name__ == '__main__':
parser = get_evaluation_parser()
parser.add_argument(
'--dataset',
type=str,
default='NoCode-bench/NoCode-bench_Verified',
help='data set to evaluate on, either full-test or lite-test',
)
parser.add_argument(
'--split',
type=str,
default='test',
help='split to evaluate on',
)
parser.add_argument(
'--mode',
type=str,
default='swe',
choices=['swe', 'swt', 'swt-ci'],
help="mode to run the evaluation, either 'swe', 'swt', or 'swt-ci'",
)
args, _ = parser.parse_known_args()
# 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, args.split)
# Set the global dataset type based on dataset name
set_dataset_type(args.dataset)
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:
llm_config = get_llm_config_arg(args.llm_config)
llm_config.log_completions = True
# modify_params must be False for evaluation purpose, for reproducibility and accurancy of results
llm_config.modify_params = False
if llm_config is None:
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
# Get condenser config from environment variable
condenser_name = os.environ.get('EVAL_CONDENSER')
if condenser_name:
condenser_config = get_condenser_config_arg(condenser_name)
if condenser_config is None:
raise ValueError(
f'Could not find Condenser config: EVAL_CONDENSER={condenser_name}'
)
else:
# If no specific condenser config is provided via env var, default to NoOpCondenser
condenser_config = NoOpCondenserConfig()
logger.debug(
'No Condenser config provided via EVAL_CONDENSER, using NoOpCondenser.'
)
details = {'mode': args.mode}
_agent_cls = openhands.agenthub.Agent.get_cls(args.agent_cls)
dataset_descrption = (
args.dataset.replace('/', '__') + '-' + args.split.replace('/', '__')
)
metadata = make_metadata(
llm_config,
dataset_descrption,
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
details=details,
condenser_config=condenser_config,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
print(f'### OUTPUT FILE: {output_file} ###')
# Run evaluation in iterative mode:
# If a rollout fails to output AgentFinishAction, we will try again until it succeeds OR total 3 attempts have been made.
ITERATIVE_EVAL_MODE = (
os.environ.get('ITERATIVE_EVAL_MODE', 'false').lower() == 'true'
)
ITERATIVE_EVAL_MODE_MAX_ATTEMPTS = int(
os.environ.get('ITERATIVE_EVAL_MODE_MAX_ATTEMPTS', '3')
)
if not ITERATIVE_EVAL_MODE:
# load the dataset
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
if len(instances) > 0 and not isinstance(
instances['PASS2PASS'][instances['PASS2PASS'].index[0]], str
):
for col in ['PASS2PASS', 'FAIL2PASS']:
instances[col] = instances[col].apply(lambda x: str(x))
run_evaluation_nocode_bench(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
else:
critic = AgentFinishedCritic()
def get_cur_output_file_path(attempt: int) -> str:
return (
f'{output_file.removesuffix(".jsonl")}.critic_attempt_{attempt}.jsonl'
)
eval_ids = None
for attempt in range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1):
cur_output_file = get_cur_output_file_path(attempt)
logger.info(
f'Running evaluation with critic {critic.__class__.__name__} for attempt {attempt} of {ITERATIVE_EVAL_MODE_MAX_ATTEMPTS}.'
)
# For deterministic eval, we set temperature to 0.1 for (>1) attempt
# so hopefully we get slightly different results
if attempt > 1 and metadata.llm_config.temperature == 0:
logger.info(
f'Detected temperature is 0 for (>1) attempt {attempt}. Setting temperature to 0.1...'
)
metadata.llm_config.temperature = 0.1
# Load instances - at first attempt, we evaluate all instances
# On subsequent attempts, we only evaluate the instances that failed the previous attempt determined by critic
instances = prepare_dataset(
swe_bench_tests, cur_output_file, args.eval_n_limit, eval_ids=eval_ids
)
if len(instances) > 0 and not isinstance(
instances['PASS2PASS'][instances['PASS2PASS'].index[0]], str
):
for col in ['PASS2PASS', 'FAIL2PASS']:
instances[col] = instances[col].apply(lambda x: str(x))
# Run evaluation - but save them to cur_output_file
logger.info(
f'Evaluating {len(instances)} instances for attempt {attempt}...'
)
run_evaluation_nocode_bench(
instances,
metadata,
cur_output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=8
* 60
* 60, # 8 hour PER instance should be more than enough
max_retries=5,
)
# When eval is done, we update eval_ids to the instances that failed the current attempt
instances_failed = []
logger.info(
f'Use critic {critic.__class__.__name__} to check {len(instances)} instances for attempt {attempt}...'
)
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
try:
history = [
event_from_dict(event) for event in instance['history']
]
critic_result = critic.evaluate(
history, instance['test_result'].get('git_patch', '')
)
if not critic_result.success:
instances_failed.append(instance['instance_id'])
except Exception as e:
logger.error(
f'Error loading history for instance {instance["instance_id"]}: {e}'
)
instances_failed.append(instance['instance_id'])
logger.info(
f'{len(instances_failed)} instances failed the current attempt {attempt}: {instances_failed}'
)
eval_ids = instances_failed
# If no instances failed, we break
if len(instances_failed) == 0:
break
# Then we should aggregate the results from all attempts into the original output file
# and remove the intermediate files
logger.info(
'Aggregating results from all attempts into the original output file...'
)
fout = open(output_file, 'w')
added_instance_ids = set()
for attempt in reversed(range(1, ITERATIVE_EVAL_MODE_MAX_ATTEMPTS + 1)):
cur_output_file = get_cur_output_file_path(attempt)
if not os.path.exists(cur_output_file):
logger.warning(
f'Intermediate output file {cur_output_file} does not exist. Skipping...'
)
continue
with open(cur_output_file, 'r') as f:
for line in f:
instance = json.loads(line)
# Also make sure git_patch is not empty - otherwise we fall back to previous attempt (empty patch is worse than anything else)
if (
instance['instance_id'] not in added_instance_ids
and instance['test_result'].get('git_patch', '').strip()
):
fout.write(line)
added_instance_ids.add(instance['instance_id'])
logger.info(
f'Aggregated instances from {cur_output_file}. Total instances added so far: {len(added_instance_ids)}'
)
fout.close()
logger.info(
f'Done! Total {len(added_instance_ids)} instances added to {output_file}'
)
@@ -0,0 +1,33 @@
import argparse
import json
def main(output_jsonl: str):
with open(output_jsonl, 'r') as f:
for line in f:
try:
output = json.loads(line)
pred = {
'instance_id': output['instance_id'],
'model_name_or_path': output['metadata']['llm_config']['model'],
'model_patch': output['test_result']['git_patch'],
}
except Exception as e:
print(
f'Error while reading output of instance {output["instance_id"]}: {e}'
)
print(json.dumps(pred))
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument(
'--output_jsonl',
type=str,
required=True,
help='Path to the prediction file (.../outputs.jsonl)',
)
args = parser.parse_args()
main(args.output_jsonl)
@@ -0,0 +1,104 @@
import argparse
import pandas as pd
from openhands.core.logger import openhands_logger as logger
def verify_instance_costs(row: pd.Series) -> float:
"""
Verifies that the accumulated_cost matches the sum of individual costs in metrics.
Also checks for duplicate consecutive costs which might indicate buggy counting.
If the consecutive costs are identical, the file is affected by this bug:
https://github.com/All-Hands-AI/OpenHands/issues/5383
Args:
row: DataFrame row containing instance data with metrics
Returns:
float: The verified total cost for this instance (corrected if needed)
"""
try:
metrics = row.get('metrics')
if not metrics:
logger.warning(f'Instance {row["instance_id"]}: No metrics found')
return 0.0
accumulated = metrics.get('accumulated_cost')
costs = metrics.get('costs', [])
if accumulated is None:
logger.warning(
f'Instance {row["instance_id"]}: No accumulated_cost in metrics'
)
return 0.0
# Check for duplicate consecutive costs and systematic even-odd pairs
has_duplicate = False
all_pairs_match = True
# Check each even-odd pair (0-1, 2-3, etc.)
for i in range(0, len(costs) - 1, 2):
if abs(costs[i]['cost'] - costs[i + 1]['cost']) < 1e-6:
has_duplicate = True
logger.debug(
f'Instance {row["instance_id"]}: Possible buggy double-counting detected! '
f'Steps {i} and {i + 1} have identical costs: {costs[i]["cost"]:.2f}'
)
else:
all_pairs_match = False
break
# Calculate total cost, accounting for buggy double counting if detected
if len(costs) >= 2 and has_duplicate and all_pairs_match:
paired_steps_cost = sum(
cost_entry['cost']
for cost_entry in costs[: -1 if len(costs) % 2 else None]
)
real_paired_cost = paired_steps_cost / 2
unpaired_cost = costs[-1]['cost'] if len(costs) % 2 else 0
total_cost = real_paired_cost + unpaired_cost
else:
total_cost = sum(cost_entry['cost'] for cost_entry in costs)
if not abs(total_cost - accumulated) < 1e-6:
logger.warning(
f'Instance {row["instance_id"]}: Cost mismatch: '
f'accumulated: {accumulated:.2f}, sum of costs: {total_cost:.2f}, '
)
return total_cost
except Exception as e:
logger.error(
f'Error verifying costs for instance {row.get("instance_id", "UNKNOWN")}: {e}'
)
return 0.0
def main():
parser = argparse.ArgumentParser(
description='Verify costs in SWE-bench output file'
)
parser.add_argument(
'input_filepath', type=str, help='Path to the output.jsonl file'
)
args = parser.parse_args()
try:
# Load and verify the JSONL file
df = pd.read_json(args.input_filepath, lines=True)
logger.info(f'Loaded {len(df)} instances from {args.input_filepath}')
# Verify costs for each instance and sum up total
total_cost = df.apply(verify_instance_costs, axis=1).sum()
logger.info(f'Total verified cost across all instances: ${total_cost:.2f}')
except Exception as e:
logger.error(f'Failed to process file: {e}')
raise
if __name__ == '__main__':
main()
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
MODEL_CONFIG=$1
COMMIT_HASH=$2
AGENT=$3
EVAL_LIMIT=$4
MAX_ITER=$5
NUM_WORKERS=$6
DATASET=$7
SPLIT=$8
N_RUNS=$9
MODE=${10}
if [ -z "$NUM_WORKERS" ]; then
NUM_WORKERS=1
echo "Number of workers not specified, use default $NUM_WORKERS"
fi
checkout_eval_branch
if [ -z "$AGENT" ]; then
echo "Agent not specified, use default CodeActAgent"
AGENT="CodeActAgent"
fi
if [ -z "$MAX_ITER" ]; then
echo "MAX_ITER not specified, use default 100"
MAX_ITER=100
fi
if [ -z "$RUN_WITH_BROWSING" ]; then
echo "RUN_WITH_BROWSING not specified, use default false"
RUN_WITH_BROWSING=false
fi
if [ -z "$DATASET" ]; then
echo "DATASET not specified, use default princeton-nlp/SWE-bench_Lite"
DATASET="princeton-nlp/SWE-bench_Lite"
fi
if [ -z "$SPLIT" ]; then
echo "SPLIT not specified, use default test"
SPLIT="test"
fi
if [ -z "$MODE" ]; then
MODE="swe"
echo "MODE not specified, use default $MODE"
fi
if [ -n "$EVAL_CONDENSER" ]; then
echo "Using Condenser Config: $EVAL_CONDENSER"
else
echo "No Condenser Config provided via EVAL_CONDENSER, use default (NoOpCondenser)."
fi
export RUN_WITH_BROWSING=$RUN_WITH_BROWSING
echo "RUN_WITH_BROWSING: $RUN_WITH_BROWSING"
get_openhands_version
echo "AGENT: $AGENT"
echo "OPENHANDS_VERSION: $OPENHANDS_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
echo "DATASET: $DATASET"
echo "SPLIT: $SPLIT"
echo "MAX_ITER: $MAX_ITER"
echo "NUM_WORKERS: $NUM_WORKERS"
echo "COMMIT_HASH: $COMMIT_HASH"
echo "MODE: $MODE"
echo "EVAL_CONDENSER: $EVAL_CONDENSER"
# Default to NOT use Hint
if [ -z "$USE_HINT_TEXT" ]; then
export USE_HINT_TEXT=false
fi
echo "USE_HINT_TEXT: $USE_HINT_TEXT"
EVAL_NOTE="$OPENHANDS_VERSION"
# if not using Hint, add -no-hint to the eval note
if [ "$USE_HINT_TEXT" = false ]; then
EVAL_NOTE="$EVAL_NOTE-no-hint"
fi
if [ "$RUN_WITH_BROWSING" = true ]; then
EVAL_NOTE="$EVAL_NOTE-with-browsing"
fi
if [ -n "$EXP_NAME" ]; then
EVAL_NOTE="$EVAL_NOTE-$EXP_NAME"
fi
# if mode != swe, add mode to the eval note
if [ "$MODE" != "swe" ]; then
EVAL_NOTE="${EVAL_NOTE}-${MODE}"
fi
# Add condenser config to eval note if provided
if [ -n "$EVAL_CONDENSER" ]; then
EVAL_NOTE="${EVAL_NOTE}-${EVAL_CONDENSER}"
fi
function run_eval() {
local eval_note="${1}"
COMMAND="poetry run python evaluation/benchmarks/nocode_bench/run_infer_nc.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations $MAX_ITER \
--eval-num-workers $NUM_WORKERS \
--eval-note $eval_note \
--dataset $DATASET \
--split $SPLIT \
--mode $MODE"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
# Run the command
eval $COMMAND
}
unset SANDBOX_ENV_GITHUB_TOKEN # prevent the agent from using the github token to push
if [ -z "$N_RUNS" ]; then
N_RUNS=1
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
done
checkout_original_branch
@@ -0,0 +1,54 @@
"""This script compares gold patches with OpenHands-generated patches and check whether
OpenHands found the right (set of) files to modify.
"""
import argparse
import json
import re
def extract_modified_files(patch):
modified_files = set()
file_pattern = re.compile(r'^diff --git a/(.*?) b/')
for line in patch.split('\n'):
match = file_pattern.match(line)
if match:
modified_files.add(match.group(1))
return modified_files
def process_report(oh_output_file):
succ = 0
fail = 0
for line in open(oh_output_file):
line = json.loads(line)
instance_id = line['instance_id']
gold_patch = line['swe_instance']['patch']
generated_patch = line['git_patch']
gold_modified_files = extract_modified_files(gold_patch)
# swe-bench lite only: a gold patch always contains exactly one file
assert len(gold_modified_files) == 1
generated_modified_files = extract_modified_files(generated_patch)
# Check if all files in gold_patch are also in generated_patch
all_files_in_generated = gold_modified_files.issubset(generated_modified_files)
if all_files_in_generated:
succ += 1
else:
fail += 1
print(
f'{instance_id}: file mismatch, gold = {gold_modified_files}, generated = {generated_modified_files}'
)
print(
f'\nSUMMARY: {succ} out of {succ + fail} instances found correct files to edit, success rate = {succ / float(succ + fail)}'
)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--oh_output_file', help='Path to the OH output file')
args = parser.parse_args()
process_report(args.oh_output_file)
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
source ~/.bashrc
SWEUTIL_DIR=/swe_util
if [ -z "$SWE_INSTANCE_ID" ]; then
echo "Error: SWE_INSTANCE_ID is not set." >&2
exit 1
fi
item=$(jq --arg INSTANCE_ID "$SWE_INSTANCE_ID" '.[] | select(.instance_id == $INSTANCE_ID)' $SWEUTIL_DIR/eval_data/instances/swe-bench-instance.json)
if [[ -z "$item" ]]; then
echo "No item found for the provided instance ID."
exit 1
fi
REPO_NAME=$(echo "$item" | jq -r '.repo | split("/")[-1]')
WORKSPACE_NAME="$REPO_NAME"
echo "WORKSPACE_NAME: $WORKSPACE_NAME"
# Clear the workspace
if [ -d /workspace ]; then
rm -rf /workspace/*
else
mkdir /workspace
fi
# Copy repo to workspace
if [ -d /workspace/$WORKSPACE_NAME ]; then
rm -rf /workspace/$WORKSPACE_NAME
fi
mkdir -p /workspace
SRC_DIR="/root/$REPO_NAME"
DEST_DIR="/workspace/$WORKSPACE_NAME"
cp -r "$SRC_DIR" "$DEST_DIR"
echo ">> Extracting conda environment name..."
CONDA_ENV_NAME=$(echo "$item" | jq -r '.conda_env // empty')
# Activate instance-specific environment
if [ -d /opt/miniconda3 ]; then
. /opt/miniconda3/etc/profile.d/conda.sh
conda activate $CONDA_ENV_NAME
fi
@@ -0,0 +1,154 @@
import json
import multiprocessing as mp
from typing import Awaitable, Callable, TextIO
import numpy as np
import pandas as pd
from pydantic import SecretStr
from tqdm import tqdm
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
_process_instance_wrapper,
_process_instance_wrapper_mp,
)
from openhands.core.logger import openhands_logger as logger
def update_progress_nc(
result: EvalOutput,
pbar: tqdm,
output_fp: TextIO,
):
"""Update the progress bar and write the result to the output file."""
pbar.update(1)
pbar.set_description(f'Instance {result.instance_id}')
pbar.set_postfix_str(f'Test Result: {str(result.test_result)[:300]}...')
logger.info(
f'Finished evaluation for instance {result.instance_id}: '
f'{str(result.test_result)[:300]}...\n'
)
def make_serializable(obj):
if isinstance(obj, pd.Series):
return make_serializable(obj.to_dict())
if isinstance(obj, dict):
return {k: make_serializable(v) for k, v in obj.items()}
elif isinstance(obj, (list, tuple, set)):
converted = [make_serializable(v) for v in obj]
if isinstance(obj, list):
return converted
elif isinstance(obj, tuple):
return tuple(converted)
else: # set
return converted
elif isinstance(obj, np.ndarray):
return obj.tolist()
elif isinstance(obj, np.generic):
return obj.item()
elif isinstance(obj, pd.Timestamp):
return obj.isoformat()
elif SecretStr is not None and isinstance(obj, SecretStr):
return str(obj)
else:
return obj
try:
raw_data = result.model_dump(mode='python', round_trip=False)
safe_data = make_serializable(raw_data)
output_fp.write(json.dumps(safe_data, ensure_ascii=False) + '\n')
output_fp.flush()
except Exception as e:
logger.error(f'Failed to write full result: {e}')
fallback = {
'instance_id': result.instance_id,
'model_patch': result.test_result.get('git_patch', ''),
}
try:
output_fp.write(json.dumps(fallback, ensure_ascii=False) + '\n')
output_fp.flush()
logger.info(
f'Wrote fallback result for instance {result.instance_id}: only instance_id and model_patch.'
)
except Exception as e2:
logger.error(f'Failed to write fallback result: {e2}')
def cleanup():
print('Cleaning up child processes...')
for process in mp.active_children():
print(f'Terminating child process: {process.name}')
process.terminate()
process.join()
def run_evaluation_nocode_bench(
dataset: pd.DataFrame,
metadata: EvalMetadata | None,
output_file: str,
num_workers: int,
process_instance_func: Callable[
[pd.Series, EvalMetadata, bool], Awaitable[EvalOutput]
],
max_retries: int = 5, # number of retries for each instance
timeout_seconds: int | None = None,
):
use_multiprocessing = num_workers > 1
if metadata is not None:
logger.info(
f'Evaluation started with Agent {metadata.agent_class}:\n'
f'model {metadata.llm_config.model}, max iterations {metadata.max_iterations}.\n'
)
else:
logger.warning('Running evaluation without metadata.')
logger.info(f'Evaluation started with {num_workers} workers.')
total_instances = len(dataset)
pbar = tqdm(total=total_instances, desc='Instances processed')
output_fp = open(output_file, 'a')
try:
if use_multiprocessing:
with mp.Pool(num_workers) as pool:
args_iter = (
(
process_instance_func,
instance,
metadata,
True,
max_retries,
timeout_seconds,
)
for _, instance in dataset.iterrows()
)
results = pool.imap_unordered(_process_instance_wrapper_mp, args_iter)
for result in results:
update_progress_nc(result, pbar, output_fp)
else:
for _, instance in dataset.iterrows():
result = _process_instance_wrapper(
process_instance_func=process_instance_func,
instance=instance,
metadata=metadata,
use_mp=False,
max_retries=max_retries,
)
update_progress_nc(result, pbar, output_fp)
except KeyboardInterrupt:
print('\nKeyboardInterrupt received. Cleaning up...\n')
cleanup()
output_fp.close()
logger.info('\nEvaluation finished.\n')
@@ -12,6 +12,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -63,16 +65,10 @@ def get_config(
sandbox_config.base_container_image = (
'docker.io/xingyaoww/openhands-eval-scienceagentbench'
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime=os.environ.get('RUNTIME', 'docker'),
max_budget_per_task=4,
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -218,7 +214,7 @@ If the program uses some packages that are incompatible, please figure out alter
# You can simply get the LAST `MessageAction` from the returned `state.history` and parse it for evaluation.
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
@@ -93,6 +93,9 @@ export USE_HINT_TEXT=true # Ignore this if you are not sure.
# Specify a condenser configuration for memory management (default: NoOpCondenser)
export EVAL_CONDENSER=summarizer_for_eval # Name of the condenser config group in config.toml
# Specify the instruction prompt template file name
export INSTRUCTION_TEMPLATE_NAME=swe_custom.j2 # Name of the file in the swe_bench/prompts folder.
```
Let's say you'd like to run 10 instances using `llm.eval_gpt4_1106_preview` and CodeActAgent,
@@ -19,6 +19,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
get_default_sandbox_config_for_eval,
get_openhands_config_for_eval,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
@@ -83,13 +84,9 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> OpenHandsConfig:
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
run_as_openhands=False,
config = get_openhands_config_for_eval(
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
return config
+8 -9
View File
@@ -32,6 +32,7 @@ from evaluation.utils.shared import (
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -108,7 +109,9 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageActio
llm_model = metadata.llm_config.model
# Determine the template file based on mode and LLM
if mode.startswith('swt'):
if metadata.instruction_template_name:
template_name = metadata.instruction_template_name
elif mode.startswith('swt'):
template_name = 'swt.j2'
elif mode == 'swe':
if 'gpt-4.1' in llm_model:
@@ -122,6 +125,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata) -> MessageActio
logger.error(f'Unexpected evaluation mode: {mode}. Falling back to default.')
template_name = 'swe_default.j2'
logger.debug(f'Using instruction template file: {template_name}')
# Set up Jinja2 environment
# Assuming templates are in 'evaluation/benchmarks/swe_bench/prompts' relative to this script
prompts_dir = os.path.join(os.path.dirname(__file__), 'prompts')
@@ -224,16 +228,11 @@ def get_config(
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
config = get_openhands_config_for_eval(
metadata=metadata,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
@@ -21,6 +21,7 @@ from evaluation.utils.shared import (
EvalException,
EvalMetadata,
EvalOutput,
get_metrics,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -179,7 +180,7 @@ def process_instance(
raise ValueError('State should not be None.')
histories = [event_to_dict(event) for event in state.history]
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Save the output
instruction = message_action.content
@@ -20,6 +20,7 @@ from evaluation.utils.shared import (
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -199,16 +200,11 @@ def get_config(
'REPO_PATH': f'/workspace/{workspace_dir_name}/',
}
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
config = get_openhands_config_for_eval(
metadata=metadata,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
+15 -13
View File
@@ -37,6 +37,7 @@ from evaluation.benchmarks.testgeneval.utils import load_testgeneval_dataset
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
get_openhands_config_for_eval,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
@@ -58,20 +59,21 @@ def get_config(instance: pd.Series) -> OpenHandsConfig:
f'Invalid container image for instance {instance["instance_id_swebench"]}.'
)
logger.info(f'Using instance container image: {base_container_image}.')
return OpenHandsConfig(
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'eventstream'),
sandbox=SandboxConfig(
base_container_image=base_container_image,
use_host_network=False,
timeout=1800,
api_key=os.environ.get('ALLHANDS_API_KEY'),
remote_runtime_api_url=os.environ.get(
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
),
# Create custom sandbox config for testgeneval with specific requirements
sandbox_config = SandboxConfig(
base_container_image=base_container_image,
use_host_network=False,
timeout=1800, # Longer timeout than default (300)
api_key=os.environ.get('ALLHANDS_API_KEY'),
remote_runtime_api_url=os.environ.get(
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
),
workspace_base=None,
workspace_mount_path=None,
)
return get_openhands_config_for_eval(
sandbox_config=sandbox_config,
runtime=os.environ.get('RUNTIME', 'docker'), # Different default runtime
)
+20 -22
View File
@@ -25,6 +25,7 @@ from evaluation.utils.shared import (
assert_and_raise,
codeact_user_response,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -126,29 +127,26 @@ def get_config(
f'Submit an issue on https://github.com/All-Hands-AI/OpenHands if you run into any issues.'
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
runtime=os.environ.get('RUNTIME', 'eventstream'),
sandbox=SandboxConfig(
base_container_image=base_container_image,
enable_auto_lint=True,
use_host_network=False,
# large enough timeout, since some testcases take very long to run
timeout=300,
# Add platform to the sandbox config to solve issue 4401
platform='linux/amd64',
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get(
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
sandbox_config = SandboxConfig(
base_container_image=base_container_image,
enable_auto_lint=True,
use_host_network=False,
# large enough timeout, since some testcases take very long to run
timeout=300,
# Add platform to the sandbox config to solve issue 4401
platform='linux/amd64',
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get(
'SANDBOX_REMOTE_RUNTIME_API_URL', 'http://localhost:8000'
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
)
config = get_openhands_config_for_eval(
metadata=metadata,
sandbox_config=sandbox_config,
runtime=os.environ.get('RUNTIME', 'docker'),
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -12,7 +12,10 @@ import tempfile
import yaml
from browsing import pre_login
from evaluation.utils.shared import get_default_sandbox_config_for_eval
from evaluation.utils.shared import (
get_default_sandbox_config_for_eval,
get_openhands_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
LLMConfig,
@@ -42,19 +45,17 @@ def get_config(
sandbox_config.enable_auto_lint = True
# If the web services are running on the host machine, this must be set to True
sandbox_config.use_host_network = True
config = OpenHandsConfig(
run_as_openhands=False,
max_budget_per_task=4,
config = get_openhands_config_for_eval(
max_iterations=100,
save_trajectory_path=os.path.join(
mount_path_on_host, f'traj_{task_short_name}.json'
),
sandbox=sandbox_config,
# we mount trajectories path so that trajectories, generated by OpenHands
# controller, can be accessible to the evaluator file in the runtime container
sandbox_config=sandbox_config,
workspace_mount_path=mount_path_on_host,
workspace_mount_path_in_sandbox='/outputs',
)
config.save_trajectory_path = os.path.join(
mount_path_on_host, f'traj_{task_short_name}.json'
)
config.max_budget_per_task = 4
config.set_llm_config(llm_config)
if agent_config:
config.set_agent_config(agent_config)
+6 -9
View File
@@ -11,6 +11,8 @@ from evaluation.utils.shared import (
codeact_user_response,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -43,15 +45,10 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = 'python:3.12-bookworm'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -134,7 +131,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
correct = eval_answer(str(model_answer_raw), str(answer))
logger.info(f'Final message: {model_answer_raw} | Correctness: {correct}')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
@@ -20,6 +20,7 @@ from evaluation.utils.shared import (
codeact_user_response,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
is_fatal_evaluation_error,
make_metadata,
prepare_dataset,
@@ -160,16 +161,11 @@ def get_config(
instance_id=instance['instance_id'],
)
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
max_iterations=metadata.max_iterations,
config = get_openhands_config_for_eval(
metadata=metadata,
enable_browser=RUN_WITH_BROWSING,
runtime=os.environ.get('RUNTIME', 'docker'),
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -12,6 +12,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -72,16 +74,10 @@ def get_config(
'VWA_WIKIPEDIA': f'{base_url}:8888',
'VWA_HOMEPAGE': f'{base_url}:4399',
}
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
attach_to_existing=True,
sandbox_config=sandbox_config,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
@@ -179,7 +175,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Instruction obtained from the first message from the USER
instruction = ''
+6 -9
View File
@@ -12,6 +12,8 @@ from evaluation.utils.shared import (
EvalOutput,
compatibility_for_eval_history_pairs,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -64,15 +66,10 @@ def get_config(
'MAP': f'{base_url}:3000',
'HOMEPAGE': f'{base_url}:4399',
}
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
sandbox_config=sandbox_config,
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
@@ -163,7 +160,7 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
# Instruction is the first message from the USER
instruction = ''
+7 -11
View File
@@ -9,6 +9,8 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
get_default_sandbox_config_for_eval,
get_metrics,
get_openhands_config_for_eval,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -44,18 +46,12 @@ def get_config(
) -> OpenHandsConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.platform = 'linux/amd64'
config = OpenHandsConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
config = get_openhands_config_for_eval(
metadata=metadata,
runtime=os.environ.get('RUNTIME', 'docker'),
max_iterations=metadata.max_iterations,
sandbox=sandbox_config,
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
# debug
debug=True,
sandbox_config=sandbox_config,
)
config.debug = True
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance_id
@@ -135,7 +131,7 @@ def process_instance(
assert len(histories) > 0, 'History should not be empty'
test_result: TestResult = test_class.verify_result(runtime, histories)
metrics = state.metrics.get() if state.metrics else None
metrics = get_metrics(state)
finally:
runtime.close()
-2
View File
@@ -1,2 +0,0 @@
node_modules
outputs
-70
View File
@@ -1,70 +0,0 @@
# OpenHands - Regression Test Framework
OpenHands project is an open-source software engineering AI that can solve various software engineering tasks. This repository contains the regression test framework for OpenHands project.
## Running the Tests
To run the tests for OpenHands project, you can use the provided test runner script. Follow these steps:
1. Ensure you have Python 3.6 or higher installed on your system.
2. Install the required dependencies by running the following command in your terminal:
```
pip install -r requirements.txt
```
3. Navigate to the root directory of the project.
4. Run the test suite using the test runner script with the required arguments:
```
python evaluation/regression/run_tests.py --OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxx --model=gpt-4o
```
Replace `sk-xxxxxxxxxxxxxxxxxxxxxx` with your actual OpenAI API key. The default model is `gpt-4o`, but you can specify a different model if needed.
The test runner will discover and execute all the test cases in the `cases/` directory, and display the results of the test suite, including the status of each individual test case and the overall summary.
## Test Case Structure
The test cases for OpenHands project are organized in the `cases/` directory. Each test case has the following structure:
```
cases/
├── hello-world/
│ ├── task.txt
│ ├── outputs/
│ │ └── codeact_agent/
│ │ └── workspace/
│ │ ├── hello_world.sh
│ └── test_hello_world.py
├── create_web_app/
│ ├── task.txt
│ ├── outputs/
│ │ └── codeact_agent/
│ │ └── workspace/
│ │ ├── app.py
│ │ ├── requirements.txt
│ │ ├── static/
│ │ └── templates/
│ └── test_create_web_app.py
└── ...
```
- `task.txt`: This file contains the task description provided by the user.
- `outputs/`: This directory contains the output generated by OpenHands for each agent.
- `outputs/*/workspace/`: This directory contains the actual output files generated by OpenHands.
- `test_*.py`: These are the test scripts that validate the output of OpenHands.
## Adding New Test Cases
To add a new test case to the regression test framework, follow the same steps as described in the previous sections.
## Customizing the Test Cases
The test cases can be customized by modifying the fixtures defined in the `conftest.py` file. The available fixtures are:
- `test_cases_dir`: The directory containing the test cases.
- `task_file`: The path to the `task.txt` file for the current test case.
- `workspace_dir`: The path to the `workspace/` directory for the current test case.
- `model`: The model selected start the generation.
- `run_test_case`: A fixture that runs OpenHands and generates the workspace for the current test case.
You can modify these fixtures to change the behavior of the test cases or add new ones as needed.
If you have any questions or need further assistance, feel free to reach out to the project maintainers.
@@ -1 +0,0 @@
Write an API server in node express which responds with a random number, and a frontend in React that displays the next number from the API
@@ -1 +0,0 @@
Write a simple hello world server in node Express
@@ -1,2 +0,0 @@
#!/usr/bin/env bash
echo "hello world"
@@ -1 +0,0 @@
Rewrite the script so that it prints the user's name, using the first argument. If there's no name, default to "world"
@@ -1 +0,0 @@
Write a bash script named "hello_world.sh" that prints "Hello, World!"
@@ -1,20 +0,0 @@
import os
import pytest
from conftest import agents
@pytest.mark.parametrize('agent', agents())
def test_hello_world(task_file, run_test_case, agent):
"""Test case for the "Hello, World!" Bash script using different agents."""
# Run the test case for the specified agent
workspace_dir = run_test_case(agent, 'hello-world')
# Validate the generated workspace
assert os.path.exists(workspace_dir)
assert os.path.isfile(os.path.join(workspace_dir, 'hello_world.sh'))
# Execute the hello_world.sh script
os.chdir(workspace_dir)
output = os.popen('bash hello_world.sh').read()
assert output == 'Hello, World!\n'
@@ -1,2 +0,0 @@
def string_length(s):
return len(s)
@@ -1,2 +0,0 @@
def to_lowercase(s):
return s.lower()
@@ -1,2 +0,0 @@
def reverse_string(s):
return s[::-1]
@@ -1,7 +0,0 @@
import random
def scramble_string(s):
s_list = list(s)
random.shuffle(s_list)
return ''.join(s_list)
@@ -1,8 +0,0 @@
def spongebob_case(s):
result = ''
for i, char in enumerate(s):
if i % 2 == 0:
result += char.lower()
else:
result += char.upper()
return result
@@ -1,2 +0,0 @@
def to_uppercase(s):
return s.upper()
@@ -1,55 +0,0 @@
import sys
def print_help():
help_text = """
Usage: python string_cli.py <command> <string>
Commands:
reverse - Reverses the input string.
uppercase - Converts the input string to uppercase.
lowercase - Converts the input string to lowercase.
spongebob - Converts the input string to spongebob case.
length - Returns the length of the input string.
scramble - Randomly scrambles the characters in the input string.
"""
print(help_text)
if __name__ == '__main__':
if len(sys.argv) == 2 and sys.argv[1] == '--help':
print_help()
sys.exit(0)
elif len(sys.argv) < 3:
print('Usage: python string_cli.py <command> <string>')
sys.exit(1)
command = sys.argv[1]
input_string = sys.argv[2]
if command == 'reverse':
from commands.reverse import reverse_string
print(reverse_string(input_string))
elif command == 'uppercase':
from commands.uppercase import to_uppercase
print(to_uppercase(input_string))
elif command == 'lowercase':
from commands.lowercase import to_lowercase
print(to_lowercase(input_string))
elif command == 'spongebob':
from commands.spongebob import spongebob_case
print(spongebob_case(input_string))
elif command == 'length':
from commands.length import string_length
print(string_length(input_string))
elif command == 'scramble':
from commands.scramble import scramble_string
print(scramble_string(input_string))
else:
print('Invalid command!')
@@ -1 +0,0 @@
Please rewrite the entire CLI in node.js
@@ -1,2 +0,0 @@
def string_length(s):
return len(s)
@@ -1,2 +0,0 @@
def to_lowercase(s):
return s.lower()
@@ -1,2 +0,0 @@
def reverse_string(s):
return s[::-1]
@@ -1,7 +0,0 @@
import random
def scramble_string(s):
s_list = list(s)
random.shuffle(s_list)
return ''.join(s_list)
@@ -1,8 +0,0 @@
def spongebob_case(s):
result = ''
for i, char in enumerate(s):
if i % 2 == 0:
result += char.lower()
else:
result += char.upper()
return result
@@ -1,2 +0,0 @@
def to_uppercase(s):
return s.upper()
@@ -1,36 +0,0 @@
import sys
if __name__ == '__main__':
if len(sys.argv) < 3:
print('Usage: python string_cli.py <command> <string>')
sys.exit(1)
command = sys.argv[1]
input_string = sys.argv[2]
if command == 'reverse':
from commands.reverse import reverse_string
print(reverse_string(input_string))
elif command == 'uppercase':
from commands.uppercase import to_uppercase
print(to_uppercase(input_string))
elif command == 'lowercase':
from commands.lowercase import to_lowercase
print(to_lowercase(input_string))
elif command == 'spongebob':
from commands.spongebob import spongebob_case
print(spongebob_case(input_string))
elif command == 'length':
from commands.length import string_length
print(string_length(input_string))
elif command == 'scramble':
from commands.scramble import scramble_string
print(scramble_string(input_string))
else:
print('Invalid command!')
@@ -1 +0,0 @@
Please add a --help option to the CLI, with a detailed description of each command

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