Compare commits

...

193 Commits

Author SHA1 Message Date
rohitvinodmalhotra@gmail.com
4e02e3bb42 use correct property 2025-02-05 19:52:54 -05:00
rohitvinodmalhotra@gmail.com
675d0d8997 export token before action calls it 2025-02-05 19:39:22 -05:00
rohitvinodmalhotra@gmail.com
d680a2fe82 hide export data 2025-02-05 19:19:37 -05:00
rohitvinodmalhotra@gmail.com
43dfcf0a34 use event stream instead 2025-02-05 19:03:15 -05:00
rohitvinodmalhotra@gmail.com
842d716e49 only keep bg task 2025-02-05 17:49:41 -05:00
rohitvinodmalhotra@gmail.com
6c2f020dec rm connection requirement, go to session directly 2025-02-05 17:31:20 -05:00
rohitvinodmalhotra@gmail.com
a072aa099e Update standalone_conversation_manager.py 2025-02-05 17:10:44 -05:00
rohitvinodmalhotra@gmail.com
664f7991f4 Update standalone_conversation_manager.py 2025-02-05 17:09:55 -05:00
rohitvinodmalhotra@gmail.com
5dab465dd0 debug logs 2025-02-05 16:48:53 -05:00
rohitvinodmalhotra@gmail.com
dd3c5dc6af move to background task 2025-02-05 16:36:00 -05:00
rohitvinodmalhotra@gmail.com
0e4ae562a4 move to public route 2025-02-05 15:52:13 -05:00
rohitvinodmalhotra@gmail.com
47013f8d58 fix circular import 2025-02-05 15:43:18 -05:00
openhands
0c1dd28775 fix: update imports in listen_socket.py to resolve circular imports 2025-02-05 20:08:40 +00:00
openhands
e9c1312243 fix: update imports in settings.py to resolve circular imports 2025-02-05 20:07:54 +00:00
openhands
84766aaba1 fix: update imports in manage_conversations.py to resolve circular imports 2025-02-05 20:06:00 +00:00
openhands
8ad6e547b8 fix: move GithubServiceImpl to separate module to resolve circular imports 2025-02-05 20:05:06 +00:00
openhands
8a7bd9645f fix: move config initialization to separate module to resolve circular imports 2025-02-05 20:04:10 +00:00
openhands
0378aefab8 fix: resolve circular import by moving SettingsStoreImpl to separate module 2025-02-05 20:03:07 +00:00
rohitvinodmalhotra@gmail.com
ab5d8391e4 fetch user token from gh service 2025-02-05 15:00:44 -05:00
rohitvinodmalhotra@gmail.com
a36360311f plumb update token route 2025-02-05 14:54:59 -05:00
Peter Dave Hello
ed68034427 Update and Improve zh-TW Traditional Chinese locale (#6621) 2025-02-05 13:37:31 -05:00
Graham Neubig
2832dba27a Fix memory leak in JSON encoder (#6620)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2025-02-05 17:39:04 +00:00
Graham Neubig
5491ad3318 Remove free disk space steps from workflows to test if they are necessary (#6618)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-02-05 12:28:57 -05:00
Calvin Smith
e47aaba4ca Improve performance of LLM summarizing condenser (#6597)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-02-05 03:26:15 +00:00
OpenHands
fe8b92743b Fix issue #6531: [Bug]: GITHUB_TOKEN would missing when the runtime resume (#6533) 2025-02-04 17:48:25 -05:00
Engel Nyst
0d312a645a Simplify fn calling usage (#6596) 2025-02-04 22:54:38 +01:00
mamoodi
f564939780 Release 0.23.0 (#6598) 2025-02-04 15:54:09 -05:00
Akim Tsvigun
be7007bcca Fix/llm prompt fn converter (#6610)
Co-authored-by: Akim Tsvigun <aktsvigun@nebius.com>
2025-02-04 20:24:31 +00:00
sp.wack
240d1c972c hotfix(frontend): Make conversation title clickable (#6609) 2025-02-04 19:49:35 +00:00
Rohit Malhotra
a7239ce799 Move GH Token retrieval to GitHubService class (#6605)
Co-authored-by: tofarr <tofarr@gmail.com>
2025-02-04 18:39:42 +00:00
dependabot[bot]
7c16ca8f27 chore(deps): bump the version-all group with 4 updates (#6604)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-02-04 17:39:42 +00:00
Rohit Malhotra
7151f75340 Use user_id as token set indicator for settings (#6595)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-02-03 20:36:47 -05:00
tofarr
f24fbec165 Fix: re-add github token middleware (#6594) 2025-02-03 19:55:09 +00:00
dependabot[bot]
4dbe831d42 chore(deps): bump the version-all group across 1 directory with 7 updates (#6591)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-02-03 19:15:49 +00:00
Xingyao Wang
90bbd4edbe fix: initialize default metadata with all required fields (#6583)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-02-04 02:52:11 +08:00
tofarr
cc104b2e44 Fix for typo (#6592) 2025-02-03 18:37:09 +00:00
Rohit Malhotra
4adef574c0 Refactor: Github Service (#6580)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-02-03 17:21:36 +00:00
Rohit Malhotra
7d09a158c3 Fix Github service bugs (#6571)
Co-authored-by: tofarr <tofarr@gmail.com>
2025-02-03 16:44:32 +00:00
tofarr
bbfdc62139 Fix for issue where retries continue on a closed runtime (#6564)
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2025-02-03 08:44:09 -07:00
Xingyao Wang
622fc5213d [feat] support o3-mini (#6570) 2025-02-03 23:26:35 +08:00
sp.wack
6d62be518b hotfix(frontend): Only show settings error toast when there is an error (#6587) 2025-02-03 18:29:55 +04:00
Boxuan Li
e487008e74 Trajectory replay: Fix a few corner cases (#6380) 2025-02-02 00:27:22 -08:00
Boxuan Li
62402cd617 The-Agent-Company evaluation harness: Support splits (#6577) 2025-02-02 13:12:01 +08:00
Engel Nyst
be522f1fb9 Upgrade litellm (with o3-mini) (#6581) 2025-02-02 13:06:08 +08:00
Rick van Hattem
4ef09ab897 Update llm.py (#6582) 2025-02-02 03:24:46 +00:00
Ryan Peach
32c5fde562 LLM_API_VERSION in openhands resolver (#6507) 2025-02-02 00:01:56 +01:00
Aditya Bharat Soni
a593d9bc6d Visual browsing in CodeAct using set-of-marks annotated webpage screenshots (#6464) 2025-02-02 04:56:11 +08:00
Engel Nyst
eb8d1600c3 Chore: clean up LLM (prompt caching, supports fn calling), leftover renames (#6095) 2025-02-01 18:14:08 +01:00
mamoodi
3b0bbce54a update custom sandbox instructions with docker method (#6566) 2025-02-01 11:06:43 -05:00
Rohit Malhotra
19e0c32eb7 Fix: RM debug print (#6569) 2025-01-31 20:57:43 +00:00
Rohit Malhotra
17a4100feb Refactor: Move Github endpoint logic to GithubService class (#6558)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-31 15:20:28 -05:00
mamoodi
47b84189a3 Update stale job workflow with operations per run (#6568) 2025-01-31 14:24:27 -05:00
Robert Brennan
7f4b5476dc Add VSCode Hello World extension (#6463)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-31 11:48:59 -05:00
Xingyao Wang
0c84fe58dd Update ACI to 0.2.0 to fix Memory Leak (#6559) 2025-01-31 16:09:38 +00:00
sp.wack
575f4fd347 chore(frontend): Better error toast handling mechanism (#6561) 2025-01-31 15:16:46 +00:00
sp.wack
f7934bed80 chore(backend): GitHub token should be a SecretStr (#6494) 2025-01-31 19:15:19 +04:00
sp.wack
e01fdf2a11 hotfix(frontend): Show error toast if settings errors (#6554) 2025-01-31 18:55:21 +04:00
Ray Myers
fd73f4210e Show LLM retries and allow resume from rate-limit state (#6438)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-30 21:51:47 +00:00
Robert Brennan
1bccfb3492 fix gh middleware (#6556) 2025-01-30 16:17:49 -05:00
Robert Brennan
27fdae6ecc Refactor: move middleware definition (#6552) 2025-01-30 15:32:26 -05:00
mamoodi
5dd4810f58 Add note to Windows that docker command must be run in WSL terminal (#6553) 2025-01-30 20:15:32 +00:00
Ray Myers
83724100e5 fix: Don't close runtime on error (#6549) 2025-01-30 19:09:24 +00:00
sp.wack
6b243155f4 hotfix(frontend): Only open consent form if user truly did not make a choice (#6551) 2025-01-30 18:45:55 +00:00
tofarr
173f824704 Filtering lost+found directory from root of workspace (#6487) 2025-01-30 10:24:12 -07:00
dependabot[bot]
8f881c4df1 chore(deps): bump the version-all group across 1 directory with 5 updates (#6547)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-30 17:52:20 +01:00
Engel Nyst
d0276d1925 Quick fix log leak (#6545) 2025-01-30 17:32:35 +01:00
mamoodi
6e90c30be4 Remove python unit tests on Mac (#6546) 2025-01-30 11:16:58 -05:00
Graham Neubig
8ff0e027a6 Fix share label (#6474) 2025-01-30 10:25:54 -05:00
sp.wack
c54911d877 chore: Move user's analytics consent to the backend (#6505) 2025-01-30 18:28:29 +04:00
sp.wack
0afe889ccd chore(frontend): Handle test warnings (#6538) 2025-01-30 18:25:24 +04:00
Boxuan Li
c9f16248d0 Add tests for trajectory replay (#6513) 2025-01-30 13:56:24 +00:00
Boxuan Li
99d2d01e1a Fix condensers registration (#6537) 2025-01-30 14:32:15 +01:00
Calvin Smith
36090ad8ff enh: Organizing condenser implementations (#6529)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-01-29 18:21:04 -07:00
Xingyao Wang
1a9971b1bf misc: make RemoteRuntime API timeout configurable (#6518)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-30 06:30:18 +08:00
Calvin Smith
473fcae57e fix: Recover from ContextWindowExceededError (#6519)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-01-29 15:25:46 -07:00
Rohit Malhotra
a6eed5b7e9 Remove unused event search route (#6510) 2025-01-29 16:45:24 -05:00
Robert Brennan
b64d130a6e remove old manager (#6525) 2025-01-29 16:45:07 -05:00
mamoodi
a253713ce2 Release 0.22.0 (#6522) 2025-01-29 14:50:22 -05:00
sp.wack
94d833cb5f fix: Update config.template.toml to have empty api keys (#6521) 2025-01-29 18:54:28 +00:00
dependabot[bot]
6909075be8 chore(deps): bump the version-all group in /frontend with 3 updates (#6515)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-01-29 17:40:19 +00:00
sp.wack
28d7127257 hotfix(frontend): Return DEFAULT_SETTINGS if GET /settings is 404 (#6517) 2025-01-29 17:24:17 +00:00
dependabot[bot]
1509f4ce56 chore(deps): bump the version-all group with 6 updates (#6516)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-29 16:36:47 +01:00
sp.wack
a7bb6720ba feat: Better error message handling (#6502) 2025-01-29 15:25:31 +00:00
sp.wack
b987f33a67 chore: Remove settings local storage logic (#6504) 2025-01-29 15:42:20 +04:00
Rohit Malhotra
eb760f32c7 Refactor: Don't serialize matching events when searching event stream (#6509) 2025-01-28 18:17:44 -05:00
sp.wack
35346068d1 chore: Remove root level package.json (#6498) 2025-01-29 00:31:48 +04:00
Chriest Yu
8ae5655157 fix(frontend): make chat message content wrappable (#6421) 2025-01-28 19:03:11 +00:00
dependabot[bot]
de786f930d chore(deps): bump the version-all group across 1 directory with 21 updates (#6493)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-01-28 18:10:09 +00:00
Xingyao Wang
7bf354be53 chore: typo fix for for add_openhands_repo_instruction.md (#6501) 2025-01-28 17:50:11 +00:00
Rohit Malhotra
f18729f5f8 Remove unused refresh func (#6499) 2025-01-28 17:09:29 +00:00
Robert Brennan
f3b8bad09f Fix file descriptor leak in S3FileStore (#6486)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-28 11:47:37 -05:00
Robert Brennan
41e5d12f63 update slack link (#6497) 2025-01-28 11:37:33 -05:00
dependabot[bot]
fa009f0a57 chore(deps): bump the version-all group with 10 updates (#6496)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-28 16:11:50 +00:00
Xingyao Wang
391200510c fix: revert #5506 for SWE-Bench performance regression (#6491)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-28 22:52:57 +08:00
sp.wack
36c2abadc2 chore: Move GitHub logic out of the frontend (#6307)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-28 13:14:32 +00:00
dependabot[bot]
d6655f3470 chore(deps): bump the version-all group in /docs with 3 updates (#6288)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-28 16:57:22 +04:00
Engel Nyst
f2427d7ffa Add the resolver to the bug_template (#6490) 2025-01-28 02:45:24 +00:00
Rohit Malhotra
94a64a47f2 Feat: Filter matching events in reverse order (#6485) 2025-01-27 22:53:16 +00:00
Rohit Malhotra
0ba96ce69e Feat: Ability to filter events by multiple types (#6484) 2025-01-27 22:09:16 +00:00
Engel Nyst
89c7bf59a7 Fix first user message (#6471) 2025-01-27 22:09:03 +01:00
Rohit Malhotra
604534905f Refactor: Use type[Event] instead of str to filter events (#6480)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-27 13:58:09 -05:00
Xingyao Wang
4bde644fab Improve function call validation with better error handling (#6453)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-28 02:42:45 +08:00
tofarr
ffdab28abc Fix Docker runtimes not stopping (#6470)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-27 11:09:09 -07:00
Calvin Smith
12dd23ba1c Enable memory condensation from the frontend (#6333)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-01-27 11:02:35 -07:00
Robert Brennan
9611093458 allow http session reuse (#6478) 2025-01-27 12:29:49 -05:00
tofarr
8a65df6bce refactor: Update get_github_installation_ids to use httpx (#6451)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-27 09:59:50 -07:00
tofarr
c997495200 Fix S3FileStore / GoogleCloudFileStore directory list & deletion (#6449)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-27 08:40:08 -07:00
Calvin Smith
23348af431 Add test for context window truncation in agent controller (#6477)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-01-27 08:35:43 -07:00
dependabot[bot]
5b53dbd85c chore(deps-dev): bump llama-index from 0.12.13 to 0.12.14 in the llama group (#6476)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 15:30:53 +00:00
Ray Myers
e619929909 Log restart reason if runtime reports it (#6455) 2025-01-25 07:20:18 +01:00
Ryan H. Tran
93753ac2e0 Upgrade openhands-aci to 0.1.9 (#6450) 2025-01-24 19:03:00 +00:00
Robert Brennan
38e19d214d Fix up conversation initialization (#6430)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-24 18:43:02 +00:00
dependabot[bot]
19a4f1c3ec chore(deps-dev): bump llama-index from 0.12.12 to 0.12.13 in the llama group (#6448)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-24 16:16:53 +00:00
Rohit Malhotra
45a048f9e3 NIT: Remove unused param (#6446) 2025-01-24 14:51:09 +00:00
sp.wack
358d9cb3f4 hotfix(frontend): Logout and clear token if retrieving user fails (#6436) 2025-01-24 09:49:50 -05:00
Xingyao Wang
e6a2fd3fd4 feat: add prompt to prevent agent execute multiple bash command at the same time (#6428) 2025-01-24 22:43:34 +08:00
OpenHands
c2f308f397 Fix issue #5620: [Bug]: Resolver fails when the existing requirements.txt does not end in a newline character (#6327) 2025-01-24 09:36:59 -05:00
Rohit Malhotra
a1f1c802d9 [Fix]: Fix bugs for target_branch param on resolver (#5745)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-23 21:36:20 -05:00
Xiaohua Zhang
ad2237d7dd feat: vscode support for modal runtime (#6442)
Co-authored-by: Xiaohua Zhang <xiaohua.dev@gmail.com>
2025-01-24 01:39:07 +00:00
Xiaohua Zhang
aa0cd51967 fix(frontend): display confirmation buttons for explandable messages (#6426)
Co-authored-by: Xiaohua Zhang <xiaohua.dev@gmail.com>
2025-01-23 20:14:52 -05:00
Graham Neubig
081a1305f0 Fix resolver linting issues (#6401)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-23 18:21:11 -05:00
Xiaohua Zhang
9912e28576 chore: update config template to use docker runtime by default (#6435)
Co-authored-by: Xiaohua Zhang <xiaohua.dev@gmail.com>
2025-01-23 22:24:00 +00:00
tofarr
b19a33ccad Fix: Filtering conversations with no created at (#6414) 2025-01-23 15:09:57 -07:00
tofarr
21e912d6fb Feat remove redis (#6278)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-23 14:33:16 -07:00
Robert Brennan
0dd9b95dbe change message to connecting (#6433) 2025-01-23 20:42:41 +00:00
Aditya Bharat Soni
aebb583779 Support for VisualWebArena evaluation in OpenHands (#4773)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-01-23 20:18:30 +00:00
chuckbutkus
2ff9ba1229 AWS necessary changes only (#6375)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-23 13:10:11 -05:00
Michael Jewell
a7e6068ba8 build: add required dependencies to package.json (#6423) 2025-01-23 10:07:12 -05:00
dependabot[bot]
24adcee9e3 chore(deps-dev): bump the llama group with 2 updates (#6411)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-23 14:54:27 +00:00
tofarr
21d4ba0bbd Feat: Stop runtimes rather than delete them (#6403)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-23 07:43:02 -07:00
tofarr
5ba9a6d321 Feat: Better mechanism for attaching middleware (#6365) 2025-01-23 07:31:43 -07:00
tofarr
aa223734d4 One more SecretStr fix (#6419) 2025-01-22 18:21:14 -07:00
sp.wack
053723a4d4 fix(frontend): Refetch conversations when toggling the conversation panel (#6190) 2025-01-22 18:19:01 +00:00
mamoodi
5a6dbac5a3 Release 0.21.0 (#6392)
Co-authored-by: Calvin Smith <email@cjsmith.io>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-01-22 11:26:12 -05:00
Robert Brennan
93d74e9b41 make export button more stylistically consistent (#6412) 2025-01-22 11:18:43 -05:00
tofarr
1337d03816 Example usage of httpx (#6325) 2025-01-22 16:06:43 +00:00
Robert Brennan
04e36df4d7 remove dead code (#6386) 2025-01-22 10:26:59 -05:00
Boxuan Li
f9ba16b648 Edit tool prompt tweaking: only plain-text format is supported (#6067)
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-01-21 18:22:01 -08:00
Engel Nyst
f0dbb02ee1 Adjust prompt to use view command (#5506)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-21 23:50:39 +01:00
tofarr
318c811817 Added check to shutdown hook (#6402) 2025-01-21 22:32:46 +00:00
Xingyao Wang
b468150f2a fix(codeact): make sure agent sees the prefix/suffix as part of observation (#6400) 2025-01-21 21:54:57 +00:00
Engel Nyst
b9a3f1c753 Fix eval on remote runtime (#6398) 2025-01-21 20:49:30 +00:00
tofarr
09e8a1eeba Fix: Keeping runtimes alive again (For now) (#6395) 2025-01-21 19:20:35 +00:00
Xingyao Wang
ff3880c76d fix(remote_runtime): define runtime_id first to fix attrbute error (#6393) 2025-01-21 18:13:43 +00:00
Calvin Smith
8bd7613724 fix: Settings modal properly tracks if an API key is set (#6394)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-01-21 11:04:30 -07:00
Engel Nyst
5b7fcfbe1a Disable prompt extensions in SWE-bench (#6391) 2025-01-21 17:18:30 +00:00
Robert Brennan
8ae36481df Fix API key again (#6390) 2025-01-21 17:00:59 +00:00
Robert Brennan
25fdb0c3bf fix api key value (#6388) 2025-01-21 16:15:28 +00:00
louria
7f57dbebda Update MiniWoB README (#6385) 2025-01-21 16:26:47 +01:00
dependabot[bot]
54589d7e83 chore(deps-dev): bump pre-commit from 4.0.1 to 4.1.0 in the pre-commit group (#6384)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 15:10:20 +00:00
Boxuan Li
b7f34c3f8d (feat) Add button to export trajectory on chat panel (#6378) 2025-01-21 22:10:00 +08:00
dependabot[bot]
210eeee94a chore(deps-dev): bump the eslint group in /frontend with 2 updates (#6358)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 13:46:56 +04:00
Robert Brennan
509892cf0e Revert changes to config defaults (#6370) 2025-01-21 04:23:21 +01:00
Engel Nyst
89963e93d8 Re-add reasoning effort (#6371) 2025-01-21 04:22:48 +01:00
tofarr
b6804f9e1e Fix: Static assets should not have the same rate limit (#6360)
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-20 21:55:49 +00:00
mamoodi
d30211da18 Update running OpenHands guide with detailed prerequisites (#6366) 2025-01-20 13:53:14 -05:00
Boxuan Li
06121bf20f chore(deps): Revert vite upgrade (#6349) 2025-01-20 19:11:32 +01:00
tofarr
541a445dfc Fix: API meta for OpenHands (#6295) 2025-01-20 09:47:57 -07:00
dependabot[bot]
03e496fb60 chore(deps): bump the version-all group with 7 updates (#6359)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 17:04:22 +01:00
Xingyao Wang
1b6e444ecb feat(remote runtime): do not resume runtime if not keep_runtime_alive (#6355)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-19 21:42:00 +00:00
Xingyao Wang
2b04ee2e62 feat(eval): reliability improvement for SWE-Bench eval_infer (#6347) 2025-01-18 14:02:59 -05:00
Boxuan Li
4383be1ab4 (feat) Add trajectory replay for headless mode (#6215) 2025-01-18 05:48:22 +00:00
tofarr
b4d20e3e18 Feat: settings default (#6328)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-17 20:17:18 -07:00
mamoodi
532c7cdf02 Attempt to fix doc deploy (#6337) 2025-01-18 00:16:47 +00:00
mamoodi
987861b5e7 Remove broken browser counter logic (#6334)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-17 22:41:31 +00:00
Calvin Smith
f07ec7a09c fix: Conversation creation accessing secret without unwrapping (#6335)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-01-17 22:16:57 +00:00
Xingyao Wang
b1fa6301f0 feat: add prompt for generating repo.md for an arbiratry repo (#6034)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-01-17 21:47:27 +00:00
Graham Neubig
62fbe4c622 docs: improve custom sandbox guide with more configuration options (#5589)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-18 05:05:41 +08:00
dependabot[bot]
a1a87af69d chore(deps): bump the version-all group across 1 directory with 18 updates (#6332)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-01-17 23:46:57 +04:00
Calvin Smith
a12087243a Pydantic-based configuration and setting objects (#6321)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-17 12:33:22 -07:00
Xingyao Wang
899c1f8360 fix(bash): also show timeout reminder when no_change_timeout is triggered (#6318)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-18 03:31:23 +08:00
manna_and_poem
c5d7caf01f remove useless axtree str (#6315) 2025-01-17 17:50:40 +00:00
Engel Nyst
85a760e561 Simplify draft llm (#6281)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-17 17:38:02 +00:00
dependabot[bot]
8e9c315729 chore(deps): bump the version-all group with 8 updates (#6331)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-17 15:19:45 +00:00
Rohit Malhotra
000055ba73 Add initial user msg to /new_conversation route (#6314) 2025-01-17 14:43:03 +00:00
Aleksandr Kadykov
2edb2337c2 Fix typo in Development.md (#6330) 2025-01-17 13:51:53 +00:00
Amaechi-Okorie Onyedikachi Hope
7c8a0162ae feat: add slack etiquettes (#6178) 2025-01-17 01:28:23 +00:00
Robert Brennan
c10f18b3bd Better message when trying to reconnect (#6323) 2025-01-16 22:10:48 +00:00
tofarr
313c8eca20 Fix closing sessions (again) (#6322)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-16 15:03:38 -07:00
tofarr
eff9e07272 Fix for issue with user id (#6320) 2025-01-16 20:33:36 +00:00
Xingyao Wang
f8a3aeccd6 fix: Restore missing translation keys (#6317)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-16 19:21:46 +00:00
Robert Brennan
9375e0d756 fix browser async lock (#6316) 2025-01-16 19:17:17 +00:00
Xingyao Wang
72af7bbba2 feat(eval): misc SWE-Bench improvement - use different resources for different instances (#6313)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-17 02:48:41 +08:00
OpenHands
0661c69bd3 Fix issue #6273: [Feature]: Disable LitLLM Print Message (#6274)
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-01-17 02:43:55 +08:00
Xingyao Wang
0c961bfd8b refactor(prompt): move runtime/repo info to user message and disable them in eval (#6291) 2025-01-16 17:53:10 +00:00
Xingyao Wang
0bed17758f fix: incorrect soft-timeout implementation & fix hard-timeout follow-up command (#6280) 2025-01-17 01:27:00 +08:00
Ryan H. Tran
da1a6035ac Enable runtime build in openhands-resolver.yml (#6312) 2025-01-16 11:42:22 -05:00
Rohit Malhotra
df050e4786 Separate data extraction and convo creation logic (#6298) 2025-01-16 11:40:03 -05:00
Xingyao Wang
e211647eba fix: llm-proxy response_cost being 0 (#6293) 2025-01-16 15:33:22 +00:00
Alejandro Cuadron Lafuente
8579710c82 [Fix] Restored FC default for GPT-4o (#6311) 2025-01-16 15:27:57 +00:00
dependabot[bot]
8c35150c3e chore(deps-dev): bump llama-index from 0.12.10 to 0.12.11 in the llama group (#6308)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-16 16:18:37 +01:00
Alejandro Cuadron Lafuente
578291e961 Enabled native function calling for O1 + added support for reasoning_effort config in the config. (#6256)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-16 14:53:11 +00:00
Xingyao Wang
efe04baf34 Revert "Fix closing sessions" (#6300) 2025-01-16 09:14:56 -05:00
342 changed files with 14423 additions and 6609 deletions

View File

@@ -30,6 +30,7 @@ body:
description: How are you running OpenHands?
options:
- Docker command in README
- GitHub resolver
- Development workflow
- app.all-hands.dev
- Other

View File

@@ -19,20 +19,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3

View File

@@ -41,20 +41,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
with:
@@ -104,20 +90,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
with:
@@ -219,7 +191,7 @@ jobs:
exit 1
fi
# Run unit tests with the EventStream runtime Docker images as root
# Run unit tests with the Docker runtime Docker images as root
test_runtime_root:
name: RT Unit Tests (Root)
needs: [ghcr_build_runtime]
@@ -230,20 +202,6 @@ jobs:
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
@@ -286,7 +244,7 @@ jobs:
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
TEST_RUNTIME=eventstream \
TEST_RUNTIME=docker \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
@@ -297,7 +255,7 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# Run unit tests with the EventStream runtime Docker images as openhands user
# Run unit tests with the Docker runtime Docker images as openhands user
test_runtime_oh:
name: RT Unit Tests (openhands)
runs-on: ubuntu-latest
@@ -307,20 +265,6 @@ jobs:
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: true
# all of these default to true, but feel free to set to
# "false" if necessary for your workflow
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: false
swap-storage: true
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
@@ -363,7 +307,7 @@ jobs:
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
TEST_RUNTIME=eventstream \
TEST_RUNTIME=docker \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \

View File

@@ -160,7 +160,6 @@ jobs:
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for DelegatorAgent (DeepSeek)
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
@@ -174,12 +173,42 @@ jobs:
cat $REPORT_FILE_DELEGATOR_DEEPSEEK >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# -------------------------------------------------------------
# Run VisualBrowsingAgent tests for DeepSeek, limited to t05 and t06
- name: Wait a little bit (again)
run: sleep 5
- name: Configure config.toml for testing VisualBrowsingAgent (DeepSeek)
env:
LLM_MODEL: "litellm_proxy/deepseek-chat"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 15
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for VisualBrowsingAgent (DeepSeek)
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD VisualBrowsingAgent '' 15 $N_PROCESSES "t05_simple_browsing,t06_github_pr_browsing.py" 'visualbrowsing_deepseek_run'
# Find and export the visual browsing agent test results
REPORT_FILE_VISUALBROWSING_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/VisualBrowsingAgent/deepseek*_maxiter_15_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE_VISUALBROWSING_DEEPSEEK: $REPORT_FILE_VISUALBROWSING_DEEPSEEK"
echo "INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_VISUALBROWSING_DEEPSEEK >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create archive of evaluation outputs
run: |
TIMESTAMP=$(date +'%y-%m-%d-%H-%M')
cd evaluation/evaluation_outputs/outputs # Change to the outputs directory
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/DelegatorAgent/* # Only include the actual result directories
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/DelegatorAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
- name: Upload evaluation results as artifact
uses: actions/upload-artifact@v4
@@ -227,4 +256,7 @@ jobs:
**Integration Tests Report Delegator (DeepSeek)**
${{ env.INTEGRATION_TEST_REPORT_DELEGATOR_DEEPSEEK }}
---
**Integration Tests Report VisualBrowsing (DeepSeek)**
${{ env.INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK }}
---
Download testing outputs (includes both Haiku and DeepSeek results): [Download](${{ steps.upload_results_artifact.outputs.artifact-url }})

View File

@@ -20,6 +20,10 @@ on:
required: false
type: string
default: "anthropic/claude-3-5-sonnet-20241022"
LLM_API_VERSION:
required: false
type: string
default: ""
base_container_image:
required: false
type: string
@@ -84,6 +88,10 @@ jobs:
run: |
python -m pip index versions openhands-ai > openhands_versions.txt
OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()')
# Ensure requirements.txt ends with newline before appending
if [ -f requirements.txt ] && [ -s requirements.txt ]; then
sed -i -e '$a\' requirements.txt
fi
echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt
cat requirements.txt
@@ -112,6 +120,7 @@ jobs:
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
GITHUB_TOKEN: ${{ github.token }}
@@ -184,6 +193,7 @@ jobs:
});
- name: Install OpenHands
id: install_openhands
uses: actions/github-script@v7
env:
COMMENT_BODY: ${{ github.event.comment.body || '' }}
@@ -196,7 +206,6 @@ jobs:
const reviewBody = process.env.REVIEW_BODY.trim();
const labelName = process.env.LABEL_NAME.trim();
const eventName = process.env.EVENT_NAME.trim();
// Check conditions
const isExperimentalLabel = labelName === "fix-me-experimental";
const isIssueCommentExperimental =
@@ -205,6 +214,9 @@ jobs:
const isReviewCommentExperimental =
eventName === "pull_request_review" && reviewBody.includes("@openhands-agent-exp");
// Set output variable
core.setOutput('isExperimental', isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental);
// Perform package installation
if (isExperimentalLabel || isIssueCommentExperimental || isReviewCommentExperimental) {
console.log("Installing experimental OpenHands...");
@@ -223,6 +235,7 @@ jobs:
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PYTHONPATH: ""
run: |
cd /tmp && python -m openhands.resolver.resolve_issue \
@@ -230,7 +243,8 @@ jobs:
--issue-number ${{ env.ISSUE_NUMBER }} \
--issue-type ${{ env.ISSUE_TYPE }} \
--max-iterations ${{ env.MAX_ITERATIONS }} \
--comment-id ${{ env.COMMENT_ID }}
--comment-id ${{ env.COMMENT_ID }} \
--is-experimental ${{ steps.install_openhands.outputs.isExperimental }}
- name: Check resolution result
id: check_result
@@ -257,6 +271,7 @@ jobs:
LLM_MODEL: ${{ secrets.LLM_MODEL || inputs.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
LLM_API_VERSION: ${{ inputs.LLM_API_VERSION }}
PYTHONPATH: ""
run: |
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then

View File

@@ -1,98 +0,0 @@
# Workflow that runs python unit tests on mac
name: Run Python Unit Tests Mac
# This job is flaky so only run it nightly
on:
schedule:
- cron: '0 0 * * *'
jobs:
# Run python unit tests on macOS
test-on-macos:
name: Python Unit Tests on macOS
runs-on: macos-14
env:
INSTALL_DOCKER: '1' # Set to '0' to skip Docker installation
strategy:
matrix:
python-version: ['3.12']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Cache Poetry dependencies
uses: actions/cache@v4
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
${{ runner.os }}-poetry-
- name: Install tmux
run: brew install tmux
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation,llama-index
- name: Install & Start Docker
if: env.INSTALL_DOCKER == '1'
run: |
INSTANCE_NAME="colima-${GITHUB_RUN_ID}"
# Uninstall colima to upgrade to the latest version
if brew list colima &>/dev/null; then
brew uninstall colima
# unlinking colima dependency: go
brew uninstall go@1.21
fi
rm -rf ~/.colima ~/.lima
brew install --HEAD colima
brew install docker
start_colima() {
# Find a free port in the range 10000-20000
RANDOM_PORT=$((RANDOM % 10001 + 10000))
# Original line:
if ! colima start --network-address --arch x86_64 --cpu=1 --memory=1 --verbose --ssh-port $RANDOM_PORT; then
echo "Failed to start Colima."
return 1
fi
return 0
}
# Attempt to start Colima for 5 total attempts:
ATTEMPT_LIMIT=5
for ((i=1; i<=ATTEMPT_LIMIT; i++)); do
if start_colima; then
echo "Colima started successfully."
break
else
colima stop -f
sleep 10
colima delete -f
if [ $i -eq $ATTEMPT_LIMIT ]; then
exit 1
fi
sleep 10
fi
done
# For testcontainers to find the Colima socket
# https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
- name: Build Environment
run: make build
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Run Tests
run: poetry run pytest --forked --cov=openhands --cov-report=xml ./tests/unit --ignore=tests/unit/test_memory.py
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -19,3 +19,4 @@ jobs:
close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
days-before-close: 7
operations-per-run: 150

View File

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

View File

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

View File

@@ -12,7 +12,7 @@
<a href="https://codecov.io/github/All-Hands-AI/OpenHands?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/All-Hands-AI/OpenHands?style=for-the-badge&color=blue"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2wkh4pklz-w~h_DVDtEe9H5kyQlcNxVw"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
<br/>
@@ -39,21 +39,21 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [
## ⚡ Quick Start
The easiest way to run OpenHands is in Docker.
See the [Installation](https://docs.all-hands.dev/modules/usage/installation) guide for
See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) guide for
system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.20
docker.all-hands.dev/all-hands-ai/openhands:0.23
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
@@ -69,7 +69,7 @@ run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
or run it on tagged issues with [a github action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
Visit [Installation](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
Visit [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
> [!CAUTION]
> OpenHands is meant to be run by a single user on their local workstation.
@@ -96,7 +96,7 @@ troubleshooting resources, and advanced configuration options.
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2wkh4pklz-w~h_DVDtEe9H5kyQlcNxVw) - Here we talk about research, architecture, and future development.
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg) - Here we talk about research, architecture, and future development.
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.

View File

@@ -23,6 +23,9 @@ workspace_base = "./workspace"
# Cache directory path
#cache_dir = "/tmp/cache"
# Reasoning effort for o1 models (low, medium, high, or not set)
#reasoning_effort = "medium"
# Debugging enabled
#debug = false
@@ -36,6 +39,11 @@ workspace_base = "./workspace"
# If it's a folder, the session id will be used as the file name
#save_trajectory_path="./trajectories"
# Path to replay a trajectory, must be a file path
# If provided, trajectory will be loaded and replayed before the
# agent responds to any user instruction
#replay_trajectory_path = ""
# File store path
#file_store_path = "/tmp/file_store"
@@ -67,7 +75,7 @@ workspace_base = "./workspace"
#run_as_openhands = true
# Runtime environment
#runtime = "eventstream"
#runtime = "docker"
# Name of the default agent
#default_agent = "CodeActAgent"
@@ -96,7 +104,7 @@ workspace_base = "./workspace"
#aws_secret_access_key = ""
# API key to use (For Headless / CLI only - In Web this is overridden by Session Init)
api_key = "your-api-key"
api_key = ""
# API base URL (For Headless / CLI only - In Web this is overridden by Session Init)
#base_url = ""
@@ -187,7 +195,7 @@ model = "gpt-4o"
#native_tool_calling = None
[llm.gpt4o-mini]
api_key = "your-api-key"
api_key = ""
model = "gpt-4o"
@@ -220,8 +228,8 @@ codeact_enable_jupyter = true
# LLM config group to use
#llm_config = 'your-llm-config-group'
# Whether to use microagents at all
#use_microagents = true
# Whether to use prompt extension (e.g., microagent, repo/runtime info) at all
#enable_prompt_extensions = true
# List of microagents to disable
#disabled_microagents = []

View File

@@ -11,7 +11,7 @@ services:
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.20-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.23-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

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.20-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.23-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-state for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -1,8 +1,8 @@
# 📦 Runtime EventStream
# 📦 Runtime Docker
Le Runtime EventStream d'OpenHands est le composant principal qui permet l'exécution sécurisée et flexible des actions des agents d'IA.
Le Runtime Docker d'OpenHands est le composant principal qui permet l'exécution sécurisée et flexible des actions des agents d'IA.
Il crée un environnement en bac à sable (sandbox) en utilisant Docker, où du code arbitraire peut être exécuté en toute sécurité sans risquer le système hôte.
## Pourquoi avons-nous besoin d'un runtime en bac à sable ?

View File

@@ -163,7 +163,7 @@ Les options de configuration de base sont définies dans la section `[core]` du
- `runtime`
- Type : `str`
- Valeur par défaut : `"eventstream"`
- Valeur par défaut : `"docker"`
- Description : Environnement d'exécution
- `default_agent`
@@ -373,7 +373,7 @@ Les options de configuration de l'agent sont définies dans les sections `[agent
- Description : Si l'éditeur LLM est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)
**Utilisation du micro-agent**
- `use_microagents`
- `enable_prompt_extensions`
- Type : `bool`
- Valeur par défaut : `true`
- Description : Indique si l'utilisation des micro-agents est activée ou non

View File

@@ -95,7 +95,3 @@ sandbox_user_id="1001"
### Erreurs de port d'utilisation
Si vous voyez un message d'erreur indiquant que le port est utilisé ou indisponible, essayez de supprimer toutes les containers docker en cours d'exécution (exécutez `docker ps` et `docker rm` des containers concernés) puis ré-exécutez ```make run```
## Discuter
Pour d'autres problèmes ou questions rejoignez le [Slack](https://join.slack.com/t/openhands-ai/shared_invite/zt-2wkh4pklz-w~h_DVDtEe9H5kyQlcNxVw) ou le [Discord](https://discord.gg/ESHStjSjD4) et demandez!

View File

@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -61,7 +61,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
python -m openhands.core.cli
```

View File

@@ -114,7 +114,7 @@ Pour créer un workflow d'évaluation pour votre benchmark, suivez ces étapes :
def get_config(instance: pd.Series, metadata: EvalMetadata) -> AppConfig:
config = AppConfig(
default_agent=metadata.agent_class,
runtime='eventstream',
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='your_container_image',

View File

@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -56,6 +56,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```

View File

@@ -13,16 +13,16 @@
La façon la plus simple d'exécuter OpenHands est avec Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.20
docker.all-hands.dev/all-hands-ai/openhands:0.23
```
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).

View File

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

View File

@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -1,8 +1,8 @@
以下是翻译后的内容:
# 📦 EventStream 运行时
# 📦 Docker 运行时
OpenHands EventStream 运行时是实现 AI 代理操作安全灵活执行的核心组件。
OpenHands Docker 运行时是实现 AI 代理操作安全灵活执行的核心组件。
它使用 Docker 创建一个沙盒环境,可以安全地运行任意代码而不会危及主机系统。
## 为什么我们需要沙盒运行时?

View File

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

View File

@@ -96,7 +96,3 @@ sandbox_user_id="1001"
### 端口使用错误
如果您遇到端口被占用或不可用的错误提示,可以尝试先用`docker ps`命令列出所有运行中的 Docker 容器,然后使用`docker rm`命令删除相关容器,最后再重新执行```make run```命令。
## 讨论
对于其他问题或疑问,请加入 [Slack](https://join.slack.com/t/openhands-ai/shared_invite/zt-2wkh4pklz-w~h_DVDtEe9H5kyQlcNxVw) 或 [Discord](https://discord.gg/ESHStjSjD4) 提问!

View File

@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -59,7 +59,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
python -m openhands.core.cli
```

View File

@@ -112,7 +112,7 @@ OpenHands 的主要入口点在 `openhands/core/main.py` 中。以下是它的
def get_config(instance: pd.Series, metadata: EvalMetadata) -> AppConfig:
config = AppConfig(
default_agent=metadata.agent_class,
runtime='eventstream',
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='your_container_image',

View File

@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -57,6 +57,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```

View File

@@ -11,16 +11,16 @@
在 Docker 中运行 OpenHands 是最简单的方式。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.20
docker.all-hands.dev/all-hands-ai/openhands:0.23
```
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。

View File

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

View File

@@ -11,7 +11,7 @@
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -1,6 +1,6 @@
# 📦 EventStream Runtime
# 📦 Docker Runtime
The OpenHands EventStream Runtime is the core component that enables secure and flexible execution of AI agent's action.
The OpenHands Docker Runtime is the core component that enables secure and flexible execution of AI agent's action.
It creates a sandboxed environment using Docker, where arbitrary code can be run safely without risking the host system.
## Why do we need a sandboxed runtime?

View File

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

View File

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

View File

@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
python -m openhands.core.cli
```

View File

@@ -18,15 +18,21 @@ If you choose the first option, you can skip the `Create Your Docker Image` sect
To create a custom Docker image, it must be Debian based.
For example, if you want OpenHands to have `ruby` installed, create a `Dockerfile` with the following content:
For example, if you want OpenHands to have `ruby` installed, you could create a `Dockerfile` with the following content:
```dockerfile
FROM debian:latest
FROM nikolaik/python-nodejs:python3.12-nodejs22
# Install required packages
RUN apt-get update && apt-get install -y ruby
```
Or you could use a Ruby-specific base image:
```dockerfile
FROM ruby:latest
```
Save this file in a folder. Then, build your Docker image (e.g., named custom-image) by navigating to the folder in
the terminal and running::
```bash
@@ -35,8 +41,16 @@ docker build -t custom-image .
This will produce a new image called `custom-image`, which will be available in Docker.
> Note that in the configuration described in this document, OpenHands will run as user "openhands" inside the
> sandbox and thus all packages installed via the docker file should be available to all users on the system, not just root.
## Using the Docker Command
When running OpenHands using [the docker command](/modules/usage/installation#start-the-app), replace
`-e SANDBOX_RUNTIME_CONTAINER_IMAGE=...` with `-e SANDBOX_BASE_CONTAINER_IMAGE=<custom image name>`:
```commandline
docker run -it --rm --pull=always \
-e SANDBOX_BASE_CONTAINER_IMAGE=custom-image \
...
```
## Using the Development Workflow
@@ -55,6 +69,28 @@ This can be an image youve already pulled or one youve built:
sandbox_base_container_image="custom-image"
```
### Additional Configuration Options
The `config.toml` file supports several other options for customizing your sandbox:
```toml
[core]
# Install additional dependencies when the runtime is built
# Can contain any valid shell commands
# If you need the path to the Python interpreter in any of these commands, you can use the $OH_INTERPRETER_PATH variable
runtime_extra_deps = """
pip install numpy pandas
apt-get update && apt-get install -y ffmpeg
"""
# Set environment variables for the runtime
# Useful for configuration that needs to be available at runtime
runtime_startup_env_vars = { DATABASE_URL = "postgresql://user:pass@localhost/db" }
# Specify platform for multi-architecture builds (e.g., "linux/amd64" or "linux/arm64")
platform = "linux/amd64"
```
### Run
Run OpenHands by running ```make run``` in the top level directory.

View File

@@ -112,7 +112,7 @@ To create an evaluation workflow for your benchmark, follow these steps:
def get_config(instance: pd.Series, metadata: EvalMetadata) -> AppConfig:
config = AppConfig(
default_agent=metadata.agent_class,
runtime='eventstream',
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='your_container_image',

View File

@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--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.20 \
docker.all-hands.dev/all-hands-ai/openhands:0.23 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -1,27 +1,70 @@
# Installation
# Running OpenHands
## System Requirements
- Docker version 26.0.0+ or Docker Desktop 4.31.0+.
- You must be using Linux or Mac OS.
- If you are on Windows, you must use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
- MacOS with [Docker Desktop support](https://docs.docker.com/desktop/setup/install/mac-install/#system-requirements)
- Linux
- Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements)
## Start the app
## Prerequisites
<details>
<summary>MacOS</summary>
### Docker Desktop
1. [Install Docker Desktop on Mac](https://docs.docker.com/desktop/setup/install/mac-install).
2. Open Docker Desktop, go to `Settings > Advanced` and ensure `Allow the default Docker socket to be used` is enabled.
</details>
<details>
<summary>Linux</summary>
:::note
Tested with Ubuntu 22.04.
:::
### Docker Desktop
1. [Install Docker Desktop on Linux](https://docs.docker.com/desktop/setup/install/linux/).
</details>
<details>
<summary>Windows</summary>
### WSL
1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
2. Run `wsl --version` in powershell and confirm `Default Version: 2`.
### Docker Desktop
1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install).
2. Open Docker Desktop, go to `Settings` and confirm the following:
- General: `Use the WSL 2 based engine` is enabled.
- Resources > WSL Integration: `Enable integration with my default WSL distro` is enabled.
:::note
The docker command below to start the app must be run inside the WSL terminal.
:::
</details>
## Start the App
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.20
docker.all-hands.dev/all-hands-ai/openhands:0.23
```
You'll find OpenHands running at http://localhost:3000!

View File

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

View File

@@ -16,7 +16,7 @@ some flags being passed to `docker run` that make this possible:
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.23-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

2077
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,8 @@
"@mdx-js/react": "^3.1.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.4.0",
"react-use": "^17.6.0"
},
@@ -31,7 +31,7 @@
"@docusaurus/module-type-aliases": "^3.5.1",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.5.1",
"typescript": "~5.7.2"
"typescript": "~5.7.3"
},
"browserslist": {
"production": [

View File

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

View File

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

View File

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

View File

@@ -69,6 +69,7 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=False,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -76,7 +77,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -53,6 +53,7 @@ def get_config(
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -60,7 +61,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -61,6 +61,7 @@ def get_config(
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=1800,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -68,7 +69,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
# copy 'draft_editor' config if exists
config_copy = copy.deepcopy(config)

View File

@@ -67,6 +67,7 @@ def get_config(
base_container_image=BIOCODER_BENCH_CONTAINER_IMAGE,
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -74,7 +75,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -80,6 +80,7 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -87,7 +88,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -45,13 +45,14 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=False,
use_host_network=False,
remote_runtime_enable_retries=True,
),
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.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -135,6 +135,7 @@ def get_config(
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -171,7 +172,7 @@ def initialize_runtime(
action = CmdRunAction(
command=f'git clone -b commit0_combined https://github.com/{instance["repo"]}.git'
)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -181,7 +182,7 @@ def initialize_runtime(
)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -191,7 +192,7 @@ def initialize_runtime(
)
action = CmdRunAction(command='git checkout -b openhands')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -201,7 +202,7 @@ def initialize_runtime(
# Install commit0
action = CmdRunAction(command='/root/.cargo/bin/uv pip install commit0')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -231,7 +232,7 @@ def complete_runtime(
workspace_dir_name = _get_commit0_workspace_dir_name(instance)
action = CmdRunAction(command='git add .')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -241,7 +242,7 @@ def complete_runtime(
)
action = CmdRunAction(command='git commit -m "openhands edits"')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -258,7 +259,7 @@ def complete_runtime(
action = CmdRunAction(
command=f"git diff {instance['base_commit']} HEAD -- . ':(exclude)spec.pdf.bz2'"
)
action.timeout = 600 + 100 * n_retries
action.set_hard_timeout(600 + 100 * n_retries)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -282,7 +283,7 @@ def complete_runtime(
action = CmdRunAction(
command=f"{instance['test']['test_cmd']} --json-report --json-report-file=report.json --continue-on-collection-errors {test_dir} > test_output.txt 2>&1"
)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -292,7 +293,7 @@ def complete_runtime(
)
# Read test output
action = CmdRunAction(command='cat test_output.txt')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -305,7 +306,7 @@ def complete_runtime(
# Save pytest exit code
action = CmdRunAction(command='echo $?')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -318,7 +319,7 @@ def complete_runtime(
# Read the test report
action = CmdRunAction(command='cat report.json')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -330,7 +331,7 @@ def complete_runtime(
repo_name = instance['repo'].split('/')[1]
repo_name = repo_name.replace('.', '-')
action = CmdRunAction(command=f'commit0 get-tests {repo_name}')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
# logger.info(obs, extra={'msg_type': 'OBSERVATION'})

View File

@@ -71,6 +71,7 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -78,7 +79,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
agent_config = AgentConfig(
function_calling=False,
codeact_enable_jupyter=True,

View File

@@ -56,6 +56,7 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -63,7 +64,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -49,6 +49,7 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -56,7 +57,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -70,6 +70,7 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -77,7 +78,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -91,6 +91,7 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -98,7 +99,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -55,6 +55,7 @@ def get_config(
enable_auto_lint=True,
use_host_network=False,
runtime_extra_deps='$OH_INTERPRETER_PATH -m pip install scitools-pyke',
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -62,7 +63,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

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

View File

@@ -70,6 +70,7 @@ def get_config(
remote_runtime_init_timeout=1800,
keep_runtime_alive=False,
timeout=120,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,

View File

@@ -113,6 +113,7 @@ def get_config(
enable_auto_lint=True,
use_host_network=False,
runtime_extra_deps=f'$OH_INTERPRETER_PATH -m pip install {" ".join(MINT_DEPENDENCIES)}',
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -120,7 +121,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

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

View File

@@ -73,6 +73,7 @@ def get_config(
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,

View File

@@ -1,3 +1,4 @@
import json
import os
import tempfile
import time
@@ -11,7 +12,11 @@ from swebench.harness.run_evaluation import (
)
from swebench.harness.test_spec import SWEbenchInstance, TestSpec, make_test_spec
from swebench.harness.utils import load_swebench_dataset
from tqdm import tqdm
from evaluation.benchmarks.swe_bench.resource.mapping import (
get_instance_resource_factor,
)
from evaluation.benchmarks.swe_bench.run_infer import get_instance_docker_image
from evaluation.utils.shared import (
EvalMetadata,
@@ -66,7 +71,7 @@ def process_git_patch(patch):
return patch
def get_config(instance: pd.Series) -> AppConfig:
def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
# We use a different instance image for the each instance of swe-bench eval
base_container_image = get_instance_docker_image(instance['instance_id'])
logger.info(
@@ -81,10 +86,14 @@ def get_config(instance: pd.Series) -> AppConfig:
base_container_image=base_container_image,
use_host_network=False,
# large enough timeout, since some testcases take very long to run
timeout=1800,
timeout=600,
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
remote_runtime_init_timeout=3600,
remote_runtime_resource_factor=get_instance_resource_factor(
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
),
),
# do not mount workspace
workspace_base=None,
@@ -123,7 +132,7 @@ def process_instance(
else:
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
config = get_config(instance)
config = get_config(metadata, instance)
instance_id = instance.instance_id
model_patch = instance['model_patch']
test_spec: TestSpec = instance['test_spec']
@@ -151,52 +160,52 @@ def process_instance(
if runtime_failure_count > 0:
config.sandbox.remote_runtime_resource_factor = min(
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
4, # hardcode maximum resource factor to 4
8,
)
logger.warning(
f'This is the second attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
# Get patch and save it to /tmp/patch.diff
with tempfile.TemporaryDirectory() as temp_dir:
# Patch file
patch_file_path = os.path.join(temp_dir, 'patch.diff')
with open(patch_file_path, 'w') as f:
f.write(model_patch)
runtime.copy_to(patch_file_path, '/tmp')
# Eval script
eval_script_path = os.path.join(temp_dir, 'eval.sh')
with open(eval_script_path, 'w') as f:
f.write(test_spec.eval_script)
runtime.copy_to(eval_script_path, '/tmp')
# Set +x
action = CmdRunAction(command='chmod +x /tmp/eval.sh')
action.timeout = 600
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
# Apply patch
exec_command = (
'cd /testbed && '
"(git apply -v /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
"(echo 'Failed to apply patch with git apply, trying with patch command...' && "
"(patch --batch --fuzz=5 -p1 -i /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
"echo 'APPLY_PATCH_FAIL')))"
)
action = CmdRunAction(command=exec_command)
action.timeout = 600
obs = runtime.run_action(action)
assert isinstance(obs, CmdOutputObservation)
apply_patch_output = obs.content
assert isinstance(apply_patch_output, str)
instance['test_result']['apply_patch_output'] = apply_patch_output
try:
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
# Get patch and save it to /tmp/patch.diff
with tempfile.TemporaryDirectory() as temp_dir:
# Patch file
patch_file_path = os.path.join(temp_dir, 'patch.diff')
with open(patch_file_path, 'w') as f:
f.write(model_patch)
runtime.copy_to(patch_file_path, '/tmp')
# Eval script
eval_script_path = os.path.join(temp_dir, 'eval.sh')
with open(eval_script_path, 'w') as f:
f.write(test_spec.eval_script)
runtime.copy_to(eval_script_path, '/tmp')
# Set +x
action = CmdRunAction(command='chmod +x /tmp/eval.sh')
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
# Apply patch
exec_command = (
'cd /testbed && '
"(git apply -v /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
"(echo 'Failed to apply patch with git apply, trying with patch command...' && "
"(patch --batch --fuzz=5 -p1 -i /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
"echo 'APPLY_PATCH_FAIL')))"
)
action = CmdRunAction(command=exec_command)
action.set_hard_timeout(600)
obs = runtime.run_action(action)
assert isinstance(obs, CmdOutputObservation)
apply_patch_output = obs.content
assert isinstance(apply_patch_output, str)
instance['test_result']['apply_patch_output'] = apply_patch_output
if 'APPLY_PATCH_FAIL' in apply_patch_output:
logger.info(f'[{instance_id}] {APPLY_PATCH_FAIL}:\n{apply_patch_output}')
instance['test_result']['report']['failed_apply_patch'] = True
@@ -212,7 +221,7 @@ def process_instance(
# Run eval script in background and save output to log file
log_file = '/tmp/eval_output.log'
action = CmdRunAction(command=f'/tmp/eval.sh > {log_file} 2>&1 & echo $!')
action.timeout = 60 # Short timeout just to get the process ID
action.set_hard_timeout(300) # Short timeout just to get the process ID
obs = runtime.run_action(action)
if isinstance(obs, CmdOutputObservation) and obs.exit_code == 0:
@@ -235,7 +244,7 @@ def process_instance(
check_action = CmdRunAction(
command=f'ps -p {pid} > /dev/null; echo $?'
)
check_action.timeout = 60
check_action.set_hard_timeout(300)
check_obs = runtime.run_action(check_action)
if (
isinstance(check_obs, CmdOutputObservation)
@@ -252,7 +261,7 @@ def process_instance(
# Read the log file
cat_action = CmdRunAction(command=f'cat {log_file}')
cat_action.timeout = 300
cat_action.set_hard_timeout(300)
cat_obs = runtime.run_action(cat_action)
# Grade answer
@@ -352,7 +361,14 @@ if __name__ == '__main__':
# Load predictions
assert args.input_file.endswith('.jsonl'), 'Input file must be a jsonl file.'
predictions = pd.read_json(args.input_file, lines=True)
required_fields = ['instance_id', 'model_patch', 'test_result']
with open(args.input_file) as f:
predictions = pd.DataFrame.from_records(
[
{k: v for k, v in json.loads(line).items() if k in required_fields}
for line in tqdm(f, desc='Loading predictions')
]
)
assert (
'instance_id' in predictions.columns
), 'Input file must contain instance_id column.'
@@ -396,6 +412,17 @@ if __name__ == '__main__':
with open(metadata_filepath, 'r') as metadata_file:
data = metadata_file.read()
metadata = EvalMetadata.model_validate_json(data)
else:
# Initialize with a dummy metadata when file doesn't exist
metadata = EvalMetadata(
agent_class="dummy_agent", # Placeholder agent class
llm_config=LLMConfig(model="dummy_model"), # Minimal LLM config
max_iterations=1, # Minimal iterations
eval_output_dir=os.path.dirname(args.input_file), # Use input file dir as output dir
start_time=time.strftime('%Y-%m-%d %H:%M:%S'), # Current time
git_commit=subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf-8').strip(), # Current commit
dataset=args.dataset # Dataset name from args
)
# The evaluation harness constrains the signature of `process_instance_func` but we need to
# pass extra information. Build a new function object to avoid issues with multiprocessing.

View File

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

View File

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

View File

@@ -9,6 +9,9 @@ import toml
from datasets import load_dataset
import openhands.agenthub
from evaluation.benchmarks.swe_bench.resource.mapping import (
get_instance_resource_factor,
)
from evaluation.utils.shared import (
EvalException,
EvalMetadata,
@@ -41,9 +44,10 @@ from openhands.utils.async_utils import call_async_from_sync
from openhands.utils.shutdown_listener import sleep_if_should_continue
USE_HINT_TEXT = os.environ.get('USE_HINT_TEXT', 'false').lower() == 'true'
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'false').lower() == 'true'
USE_INSTANCE_IMAGE = os.environ.get('USE_INSTANCE_IMAGE', 'true').lower() == 'true'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
}
@@ -63,11 +67,11 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
'<uploaded_files>\n'
f'/workspace/{workspace_dir_name}\n'
'</uploaded_files>\n'
f"I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following PR description:\n\n"
f'<pr_description>\n'
f"I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following issue description:\n\n"
f'<issue_description>\n'
f'{instance.problem_statement}\n'
'</pr_description>\n\n'
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <pr_description> are met?\n'
'</issue_description>\n\n'
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?\n'
"I've already taken care of all changes to any of the test files described in the <pr_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!\n"
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied.\n'
'Follow these steps to resolve the issue:\n'
@@ -135,6 +139,12 @@ def get_config(
remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'),
keep_runtime_alive=False,
remote_runtime_init_timeout=3600,
remote_runtime_api_timeout=120,
remote_runtime_resource_factor=get_instance_resource_factor(
dataset_name=metadata.dataset,
instance_id=instance['instance_id'],
),
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -150,6 +160,7 @@ def get_config(
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
config.set_agent_config(agent_config)
return config
@@ -173,7 +184,7 @@ def initialize_runtime(
action = CmdRunAction(
command=f"""echo 'export SWE_INSTANCE_ID={instance['instance_id']}' >> ~/.bashrc && echo 'export PIP_CACHE_DIR=~/.cache/pip' >> ~/.bashrc && echo "alias git='git --no-pager'" >> ~/.bashrc"""
)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -182,7 +193,7 @@ def initialize_runtime(
)
action = CmdRunAction(command="""export USER=$(whoami); echo USER=${USER} """)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -194,7 +205,7 @@ def initialize_runtime(
# inject the instance info
action = CmdRunAction(command='mkdir -p /swe_util/eval_data/instances')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -223,14 +234,14 @@ def initialize_runtime(
'/swe_util/',
)
action = CmdRunAction(command='cat ~/.bashrc')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to cat ~/.bashrc: {str(obs)}')
action = CmdRunAction(command='source ~/.bashrc')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -239,7 +250,7 @@ def initialize_runtime(
assert_and_raise(obs.exit_code == 0, f'Failed to source ~/.bashrc: {str(obs)}')
action = CmdRunAction(command='source /swe_util/instance_swe_entry.sh')
action.timeout = 3600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -249,7 +260,7 @@ def initialize_runtime(
)
else:
action = CmdRunAction(command='source /swe_util/swe_entry.sh')
action.timeout = 1800
action.set_hard_timeout(1800)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -259,7 +270,7 @@ def initialize_runtime(
)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -269,7 +280,7 @@ def initialize_runtime(
)
action = CmdRunAction(command='git reset --hard')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -278,14 +289,14 @@ def initialize_runtime(
action = CmdRunAction(
command='for remote_name in $(git remote); do git remote remove "${remote_name}"; done'
)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
action = CmdRunAction(command='which python')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -316,7 +327,7 @@ def complete_runtime(
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
action = CmdRunAction(command=f'cd /workspace/{workspace_dir_name}')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -326,7 +337,7 @@ def complete_runtime(
)
action = CmdRunAction(command='git config --global core.pager ""')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -336,7 +347,7 @@ def complete_runtime(
)
action = CmdRunAction(command='git add -A')
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -351,7 +362,7 @@ def complete_runtime(
action = CmdRunAction(
command=f'git diff --no-color --cached {instance["base_commit"]}'
)
action.timeout = 600 + 100 * n_retries
action.set_hard_timeout(max(300 + 100 * n_retries, 600))
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -399,7 +410,7 @@ def process_instance(
8,
)
logger.warning(
f'This is the second attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
@@ -479,6 +490,10 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
subset = dataset[dataset[filter_column].isin(selected_ids)]
logger.info(f'Retained {subset.shape[0]} tasks after filtering')
return subset
skip_ids = os.environ.get('SKIP_IDS', '').split(',')
if len(skip_ids) > 0:
logger.info(f'Filtering {len(skip_ids)} tasks from "SKIP_IDS"...')
return dataset[~dataset[filter_column].isin(skip_ids)]
return dataset
@@ -501,8 +516,10 @@ if __name__ == '__main__':
# NOTE: It is preferable to load datasets from huggingface datasets and perform post-processing
# so we don't need to manage file uploading to OpenHands's repo
dataset = load_dataset(args.dataset, split=args.split)
logger.info(f'Loaded dataset {args.dataset} with split {args.split}')
swe_bench_tests = filter_dataset(dataset.to_pandas(), 'instance_id')
logger.info(
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_bench_tests)} tasks'
)
llm_config = None
if args.llm_config:
@@ -531,6 +548,7 @@ if __name__ == '__main__':
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
print(f'### OUTPUT FILE: {output_file} ###')
instances = prepare_dataset(swe_bench_tests, output_file, args.eval_n_limit)
if len(instances) > 0 and not isinstance(

View File

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

View File

@@ -15,11 +15,25 @@ parser.add_argument(
action='store_true',
help='Show visualization paths for failed instances',
)
parser.add_argument(
'--only-x-instances',
action='store_true',
help='Only show instances that are ran by X',
)
args = parser.parse_args()
df1 = pd.read_json(args.input_file_1, orient='records', lines=True)
df2 = pd.read_json(args.input_file_2, orient='records', lines=True)
if args.only_x_instances:
instance_ids_1 = set(df1['instance_id'].tolist())
print(
f'Before removing instances not in X={args.input_file_1}: Y={df2.shape[0]} instances'
)
df2 = df2[df2['instance_id'].isin(instance_ids_1)]
print(
f'After removing instances not in X={args.input_file_1}: Y={df2.shape[0]} instances'
)
# Get the intersection of the instance_ids
df = pd.merge(df1, df2, on='instance_id', how='inner')
@@ -86,7 +100,7 @@ repo_diffs = []
for repo in all_repos:
x_count = len(x_only_by_repo.get(repo, []))
y_count = len(y_only_by_repo.get(repo, []))
diff = abs(x_count - y_count)
diff = y_count - x_count
repo_diffs.append((repo, diff))
# Sort by diff (descending) and then by repo name
@@ -106,7 +120,13 @@ for repo, diff in repo_diffs:
repo_color = 'red' if is_significant else 'yellow'
print(f"\n{colored(repo, repo_color, attrs=['bold'])}:")
print(colored(f'Difference: {diff} instances!', repo_color, attrs=['bold']))
print(
colored(
f'Difference: {diff} instances! (Larger diff = Y better)',
repo_color,
attrs=['bold'],
)
)
print(colored(f'X resolved but Y failed: ({len(x_instances)} instances)', 'green'))
if x_instances:
print(' ' + str(x_instances))

View File

@@ -4,6 +4,7 @@
import argparse
import json
import os
from glob import glob
import pandas as pd
from tqdm import tqdm
@@ -20,6 +21,7 @@ output_md_folder = args.oh_output_file.replace('.jsonl', '.viz')
print(f'Converting {args.oh_output_file} to markdown files in {output_md_folder}')
oh_format = pd.read_json(args.oh_output_file, orient='records', lines=True)
output_dir = os.path.dirname(args.oh_output_file)
swebench_eval_file = args.oh_output_file.replace('.jsonl', '.swebench_eval.jsonl')
if os.path.exists(swebench_eval_file):
@@ -57,22 +59,172 @@ def convert_history_to_str(history):
return ret
# Load trajectories for resolved instances
def load_completions(instance_id: str):
global output_dir
glob_path = os.path.join(output_dir, 'llm_completions', instance_id, '*.json')
files = sorted(glob(glob_path)) # this is ascending order
# pick the last file (last turn)
try:
file_path = files[-1]
except IndexError:
# print(f'No files found for instance {instance_id}: files={files}')
return None
with open(file_path, 'r') as f:
result = json.load(f)
# create messages
messages = result['messages']
messages.append(result['response']['choices'][0]['message'])
tools = result['kwargs']['tools']
return {
'messages': messages,
'tools': tools,
}
def _convert_content(content) -> str:
ret = ''
if isinstance(content, list):
for item in content:
assert item['type'] == 'text', 'Only text is supported for now'
ret += f'{item["text"]}\n'
else:
assert isinstance(content, str), 'Only str is supported for now'
ret = content
return ret
def convert_tool_call_to_string(tool_call: dict) -> str:
"""Convert tool call to content in string format."""
if 'function' not in tool_call:
raise ValueError("Tool call must contain 'function' key.")
if 'id' not in tool_call:
raise ValueError("Tool call must contain 'id' key.")
if 'type' not in tool_call:
raise ValueError("Tool call must contain 'type' key.")
if tool_call['type'] != 'function':
raise ValueError("Tool call type must be 'function'.")
ret = f"<function={tool_call['function']['name']}>\n"
try:
args = json.loads(tool_call['function']['arguments'])
except json.JSONDecodeError as e:
raise ValueError(
f"Failed to parse arguments as JSON. Arguments: {tool_call['function']['arguments']}"
) from e
for param_name, param_value in args.items():
is_multiline = isinstance(param_value, str) and '\n' in param_value
ret += f'<parameter={param_name}>'
if is_multiline:
ret += '\n'
ret += f'{param_value}'
if is_multiline:
ret += '\n'
ret += '</parameter>\n'
ret += '</function>'
return ret
def format_traj(traj, first_n_turns=None, last_n_turns=None) -> str:
output = ''
system_message = None
# Handle system message if present
if traj[0]['role'] == 'system':
system_message = traj[0]
traj = traj[1:]
content = _convert_content(system_message['content'])
output += "*** System Message that describes the assistant's behavior ***\n"
output += f'{content}\n'
# Merge consecutive user messages first
merged_traj = []
current_messages = []
n_turns = len(traj)
for i, message in enumerate(traj):
# Skip this message if...
if (
# Case 1: first_n_turns specified and we're past it
(first_n_turns is not None and i >= first_n_turns and last_n_turns is None)
or
# Case 2: last_n_turns specified and we're before it
(
last_n_turns is not None
and i < n_turns - last_n_turns
and first_n_turns is None
)
or
# Case 3: both specified and we're in the middle section
(
first_n_turns is not None
and last_n_turns is not None
and i >= first_n_turns
and i < n_turns - last_n_turns
)
):
continue
if message['role'] == 'user':
current_messages.append(message)
else:
if current_messages:
# Merge all accumulated user messages into one
merged_content = '\n'.join(
_convert_content(msg['content']) for msg in current_messages
)
merged_traj.append({'role': 'user', 'content': merged_content})
current_messages = []
merged_traj.append(message)
# Don't forget to handle any remaining user messages
if current_messages:
merged_content = '\n'.join(
_convert_content(msg['content']) for msg in current_messages
)
merged_traj.append({'role': 'user', 'content': merged_content})
# Now process the merged trajectory
for i, message in enumerate(merged_traj):
role, content = message['role'], message['content']
content = _convert_content(content) if isinstance(content, list) else content
turn_id = i // 2 + 1
output += '-' * 100 + '\n'
output += f'*** Turn {turn_id} - {role.upper() if role != "tool" else "TOOL EXECUTION RESULT"} ***\n'
if role == 'user':
output += f'{content}\n'
elif role == 'tool':
output += f'{content}\n'
elif role == 'assistant':
output += f'{content}\n'
if (
'tool_calls' in message
and message['tool_calls'] is not None
and len(message['tool_calls']) > 0
):
for toolcall_id, tool_call in enumerate(message['tool_calls']):
output += f'### Tool Call {toolcall_id}\n'
output += f'{convert_tool_call_to_string(tool_call)}\n'
else:
raise ValueError(f'Unexpected role: {role}')
output += '-' * 100 + '\n'
return output
def write_row_to_md_file(row, instance_id_to_test_result):
if 'git_patch' in row:
model_patch = row['git_patch']
elif 'test_result' in row and 'git_patch' in row['test_result']:
model_patch = row['test_result']['git_patch']
else:
raise ValueError(f'Row {row} does not have a git_patch')
print(f'Row {row} does not have a git_patch')
return
test_output = None
if row['instance_id'] in instance_id_to_test_result:
report = instance_id_to_test_result[row['instance_id']].get('report', {})
resolved = report.get('resolved', False)
test_output = instance_id_to_test_result[row['instance_id']].get(
'test_output', None
)
elif 'report' in row and row['report'] is not None:
# Use result from output.jsonl FIRST if available.
if 'report' in row and row['report'] is not None:
if not isinstance(row['report'], dict):
resolved = None
print(
@@ -80,6 +232,12 @@ def write_row_to_md_file(row, instance_id_to_test_result):
)
else:
resolved = row['report'].get('resolved', False)
elif row['instance_id'] in instance_id_to_test_result:
report = instance_id_to_test_result[row['instance_id']].get('report', {})
resolved = report.get('resolved', False)
test_output = instance_id_to_test_result[row['instance_id']].get(
'test_output', None
)
else:
resolved = None
@@ -88,6 +246,8 @@ def write_row_to_md_file(row, instance_id_to_test_result):
os.makedirs(output_md_folder, exist_ok=True)
filepath = os.path.join(output_md_folder, filename)
completions = load_completions(instance_id)
with open(filepath, 'w') as f:
f.write(f'# {instance_id} (resolved: {resolved})\n')
@@ -97,7 +257,12 @@ def write_row_to_md_file(row, instance_id_to_test_result):
f.write(json.dumps(row['metadata'], indent=2))
f.write('\n```\n')
# Trajectory
# Completion
if completions is not None:
f.write('## Completion\n')
traj = completions['messages']
f.write(format_traj(traj))
f.write('## History\n')
f.write(convert_history_to_str(row['history']))

View File

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

View File

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

View File

@@ -76,7 +76,7 @@ echo "Running SWE-bench evaluation"
echo "=============================================================="
RUN_ID=$(date +"%Y%m%d_%H%M%S")
N_PROCESS=16
N_PROCESS=4
if [ -z "$INSTANCE_ID" ]; then
echo "Running SWE-bench evaluation on the whole input file..."
@@ -87,7 +87,7 @@ if [ -z "$INSTANCE_ID" ]; then
--dataset_name "$DATASET_NAME" \
--split "$SPLIT" \
--predictions_path $SWEBENCH_FORMAT_JSONL \
--timeout 1800 \
--timeout 3600 \
--cache_level instance \
--max_workers $N_PROCESS \
--run_id $RUN_ID
@@ -133,7 +133,7 @@ else
--dataset_name "$DATASET_NAME" \
--split "$SPLIT" \
--predictions_path $SWEBENCH_FORMAT_JSONL \
--timeout 1800 \
--timeout 3600 \
--instance_ids $INSTANCE_ID \
--cache_level instance \
--max_workers $N_PROCESS \

View File

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

View File

@@ -17,11 +17,14 @@ When the `run_infer.sh` script is started, it will automatically pull all task i
```bash
./evaluation/benchmarks/the_agent_company/scripts/run_infer.sh \
--agent-llm-config <agent-llm-config> \
--env-llm-config <env-llm-config> \
--outputs-path <outputs-path> \
--server-hostname <server-hostname> \
--version <version>
--agent-llm-config <agent-llm-config, default to 'agent'> \
--env-llm-config <env-llm-config, default to 'env'> \
--outputs-path <outputs-path, default to outputs> \
--server-hostname <server-hostname, default to localhost> \
--version <version, default to 1.0.0> \
--start-percentile <integer from 0 to 99, default to 0> \
--end-percentile <integer from 1 to 100, default to 100>
# Example
./evaluation/benchmarks/the_agent_company/scripts/run_infer.sh \
@@ -29,7 +32,9 @@ When the `run_infer.sh` script is started, it will automatically pull all task i
--env-llm-config claude-3-5-sonnet-20240620 \
--outputs-path outputs \
--server-hostname localhost \
--version 1.0.0
--version 1.0.0 \
--start-percentile 10 \
--end-percentile 20
```
- `agent-llm-config`: the config name for the agent LLM. This should match the config name in config.toml. This is the LLM used by the agent (e.g. CodeActAgent).
@@ -37,7 +42,11 @@ When the `run_infer.sh` script is started, it will automatically pull all task i
- `outputs-path`: the path to save trajectories and evaluation results.
- `server-hostname`: the hostname of the server that hosts all the web services. It could be localhost if you are running the evaluation and services on the same machine. If the services are hosted on a remote machine, you must use the hostname of the remote machine rather than IP address.
- `version`: the version of the task images to use. Currently, the only supported version is 1.0.0.
- `start-percentile`: the start percentile of the task split, must be an integer between 0 to 99.
- `end-percentile`: the end percentile of the task split, must be an integer between 1 to 100 and larger than start-percentile.
The script is idempotent. If you run it again, it will resume from the last checkpoint. It would usually take a few days to finish evaluation.
The script is idempotent. If you run it again, it will resume from the last checkpoint. It would usually take 2 days to finish evaluation if you run the whole task set.
To speed up evaluation, you can use `start-percentile` and `end-percentile` to split the tasks for higher parallelism,
provided concurrent runs are **targeting different servers**.
Note: the script will automatically skip a task if it encounters an error. This usually happens when the OpenHands runtime dies due to some unexpected errors. This means even if the script finishes, it might not have evaluated all tasks. You can manually resume the evaluation by running the script again.

View File

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

View File

@@ -50,6 +50,7 @@ def get_config(
# large enough timeout, since some testcases take very long to run
timeout=300,
api_key=os.environ.get('ALLHANDS_API_KEY', None),
remote_runtime_enable_retries=True,
),
# we mount trajectories path so that trajectories, generated by OpenHands
# controller, can be accessible to the evaluator file in the runtime container
@@ -80,13 +81,13 @@ def load_dependencies(runtime: Runtime) -> List[str]:
def init_task_env(runtime: Runtime, hostname: str, env_llm_config: LLMConfig):
command = (
f'SERVER_HOSTNAME={hostname} '
f'LITELLM_API_KEY={env_llm_config.api_key} '
f'LITELLM_API_KEY={env_llm_config.api_key.get_secret_value() if env_llm_config.api_key else None} '
f'LITELLM_BASE_URL={env_llm_config.base_url} '
f'LITELLM_MODEL={env_llm_config.model} '
'bash /utils/init.sh'
)
action = CmdRunAction(command=command)
action.timeout = 900
action.set_hard_timeout(900)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -165,14 +166,14 @@ def run_evaluator(
runtime: Runtime, env_llm_config: LLMConfig, trajectory_path: str, result_path: str
):
command = (
f'LITELLM_API_KEY={env_llm_config.api_key} '
f'LITELLM_API_KEY={env_llm_config.api_key.get_secret_value() if env_llm_config.api_key else None} '
f'LITELLM_BASE_URL={env_llm_config.base_url} '
f'LITELLM_MODEL={env_llm_config.model} '
f"DECRYPTION_KEY='theagentcompany is all you need' " # Hardcoded Key
f'python_default /utils/eval.py --trajectory_path {trajectory_path} --result_path {result_path}'
)
action = CmdRunAction(command=command)
action.timeout = 600
action.set_hard_timeout(600)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})

View File

@@ -56,6 +56,14 @@ while [[ $# -gt 0 ]]; do
VERSION="$2"
shift 2
;;
--start-percentile)
START_PERCENTILE="$2"
shift 2
;;
--end-percentile)
END_PERCENTILE="$2"
shift 2
;;
*)
echo "Unknown argument: $1"
exit 1
@@ -69,16 +77,53 @@ if [[ ! "$OUTPUTS_PATH" = /* ]]; then
OUTPUTS_PATH="$(cd "$(dirname "$OUTPUTS_PATH")" 2>/dev/null && pwd)/$(basename "$OUTPUTS_PATH")"
fi
: "${START_PERCENTILE:=0}" # Default to 0 percentile (first line)
: "${END_PERCENTILE:=100}" # Default to 100 percentile (last line)
# Validate percentile ranges if provided
if ! [[ "$START_PERCENTILE" =~ ^[0-9]+$ ]] || ! [[ "$END_PERCENTILE" =~ ^[0-9]+$ ]]; then
echo "Error: Percentiles must be integers"
exit 1
fi
if [ "$START_PERCENTILE" -ge "$END_PERCENTILE" ]; then
echo "Error: Start percentile must be less than end percentile"
exit 1
fi
if [ "$START_PERCENTILE" -lt 0 ] || [ "$END_PERCENTILE" -gt 100 ]; then
echo "Error: Percentiles must be between 0 and 100"
exit 1
fi
echo "Using agent LLM config: $AGENT_LLM_CONFIG"
echo "Using environment LLM config: $ENV_LLM_CONFIG"
echo "Outputs path: $OUTPUTS_PATH"
echo "Server hostname: $SERVER_HOSTNAME"
echo "Version: $VERSION"
echo "Start Percentile: $START_PERCENTILE"
echo "End Percentile: $END_PERCENTILE"
echo "Downloading tasks.md..."
rm -f tasks.md
wget https://github.com/TheAgentCompany/TheAgentCompany/releases/download/${VERSION}/tasks.md
total_lines=$(cat tasks.md | grep "ghcr.io/theagentcompany" | wc -l)
if [ "$total_lines" -ne 175 ]; then
echo "Error: Expected 175 tasks in tasks.md but found $total_lines lines"
exit 1
fi
# Calculate line numbers based on percentiles
start_line=$(echo "scale=0; ($total_lines * $START_PERCENTILE / 100) + 1" | bc)
end_line=$(echo "scale=0; $total_lines * $END_PERCENTILE / 100" | bc)
echo "Using tasks No. $start_line to $end_line (inclusive) out of 1-175 tasks"
# Create a temporary file with just the desired range
temp_file="tasks_${START_PERCENTILE}_${END_PERCENTILE}.md"
sed -n "${start_line},${end_line}p" tasks.md > "$temp_file"
while IFS= read -r task_image; do
docker pull $task_image
@@ -108,8 +153,8 @@ while IFS= read -r task_image; do
docker images "ghcr.io/all-hands-ai/runtime" -q | xargs -r docker rmi -f
docker volume prune -f
docker system prune -f
done < tasks.md
done < "$temp_file"
rm tasks.md
rm tasks.md "$temp_file"
echo "All evaluation completed successfully!"

View File

@@ -50,6 +50,7 @@ def get_config(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -57,7 +58,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

@@ -0,0 +1,50 @@
# VisualWebArena Evaluation with OpenHands Browsing Agents
This folder contains evaluation for [VisualWebArena](https://github.com/web-arena-x/visualwebarena) benchmark, powered by [BrowserGym](https://github.com/ServiceNow/BrowserGym) for easy evaluation of how well an agent capable of browsing can perform on realistic web browsing tasks.
## Setup Environment and LLM Configuration
Please follow instruction [here](../../README.md#setup) to setup your local development environment and LLM.
## Setup VisualWebArena Environment
VisualWebArena requires you to set up websites containing pre-populated content that is accessible via URL to the machine running the OpenHands agents.
Follow [this document](https://github.com/web-arena-x/visualwebarena/blob/main/environment_docker/README.md) to set up your own VisualWebArena environment through local servers or AWS EC2 instances.
Take note of the base URL (`$VISUALWEBARENA_BASE_URL`) of the machine where the environment is installed.
## Test if your environment works
Access with browser the above VisualWebArena website URLs and see if they load correctly.
If you cannot access the website, make sure the firewall allows public access of the aforementioned ports on your server
Check the network security policy if you are using an AWS machine.
Follow the VisualWebArena environment setup guide carefully, and make sure the URL fields are populated with the correct base URL of your server.
## Run Evaluation
```bash
export VISUALWEBARENA_BASE_URL=<YOUR_SERVER_URL_HERE>
export OPENAI_API_KEY="yourkey" # this OpenAI API key is required for some visualWebArena validators that utilize LLMs
export OPENAI_BASE_URL="https://api.openai.com/v1/" # base URL for OpenAI model used for VisualWebArena evaluation
bash evaluation/benchmarks/visualwebarena/scripts/run_infer.sh llm.claude HEAD VisualBrowsingAgent
```
Results will be in `evaluation/evaluation_outputs/outputs/visualwebarena/`
To calculate the success rate, run:
```sh
poetry run python evaluation/benchmarks/visualwebarena/get_success_rate.py evaluation/evaluation_outputs/outputs/visualwebarena/SOME_AGENT/EXP_NAME/output.jsonl
```
## Submit your evaluation results
You can start your own fork of [our huggingface evaluation outputs](https://huggingface.co/spaces/OpenHands/evaluation) and submit a PR of your evaluation results following the guide [here](https://huggingface.co/docs/hub/en/repositories-pull-requests-discussions#pull-requests-and-discussions).
## VisualBrowsingAgent V1.0 result
Tested on VisualBrowsingAgent V1.0
VisualWebArena, 910 tasks (high cost, single run due to fixed task), max step 15. Resolve rates are:
- GPT4o: 26.15%
- Claude-3.5 Sonnet: 25.27%

View File

@@ -0,0 +1,40 @@
import argparse
import json
import browsergym.visualwebarena # noqa F401 register visualwebarena tasks as gym environments
import gymnasium as gym
parser = argparse.ArgumentParser(description='Calculate average reward.')
parser.add_argument('output_path', type=str, help='path to output.jsonl')
args = parser.parse_args()
if __name__ == '__main__':
env_ids = [
id
for id in gym.envs.registry.keys()
if id.startswith('browsergym/visualwebarena')
]
total_num = len(env_ids)
print('Total number of tasks: ', total_num)
total_reward = 0
total_cost = 0
actual_num = 0
with open(args.output_path, 'r') as f:
for line in f:
data = json.loads(line)
actual_num += 1
total_cost += data['metrics']['accumulated_cost']
reward = data['test_result']['reward']
if reward >= 0:
total_reward += data['test_result']['reward']
else:
actual_num -= 1
avg_reward = total_reward / total_num
print('Total reward: ', total_reward)
print('Success Rate: ', avg_reward)
avg_cost = total_cost / actual_num
print('Avg Cost: ', avg_cost)
print('Total Cost: ', total_cost)
print('Actual number of tasks finished: ', actual_num)

View File

@@ -0,0 +1,255 @@
import asyncio
import json
import os
from typing import Any
import browsergym.visualwebarena # noqa F401 register visualwebarena tasks as gym environments
import gymnasium as gym
import pandas as pd
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
SandboxConfig,
get_llm_config_arg,
parse_arguments,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import (
BrowseInteractiveAction,
CmdRunAction,
MessageAction,
)
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.runtime.browser.browser_env import (
BROWSER_EVAL_GET_GOAL_ACTION,
BROWSER_EVAL_GET_REWARDS_ACTION,
)
from openhands.utils.async_utils import call_async_from_sync
SUPPORTED_AGENT_CLS = {'VisualBrowsingAgent'}
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'VisualBrowsingAgent': 'Continue the task. IMPORTANT: do not talk to the user until you have finished the task',
}
def get_config(
metadata: EvalMetadata,
env_id: str,
) -> AppConfig:
base_url = os.environ.get('VISUALWEBARENA_BASE_URL', None)
openai_api_key = os.environ.get('OPENAI_API_KEY', None)
openai_base_url = os.environ.get('OPENAI_BASE_URL', None)
assert base_url is not None, 'VISUALWEBARENA_BASE_URL must be set'
assert openai_api_key is not None, 'OPENAI_API_KEY must be set'
assert openai_base_url is not None, 'OPENAI_BASE_URL must be set'
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='docker',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
browsergym_eval_env=env_id,
runtime_startup_env_vars={
'BASE_URL': base_url,
'OPENAI_API_KEY': openai_api_key,
'OPENAI_BASE_URL': openai_base_url,
'VWA_CLASSIFIEDS': f'{base_url}:9980',
'VWA_CLASSIFIEDS_RESET_TOKEN': '4b61655535e7ed388f0d40a93600254c',
'VWA_SHOPPING': f'{base_url}:7770',
'VWA_SHOPPING_ADMIN': f'{base_url}:7780/admin',
'VWA_REDDIT': f'{base_url}:9999',
'VWA_GITLAB': f'{base_url}:8023',
'VWA_WIKIPEDIA': f'{base_url}:8888',
'VWA_HOMEPAGE': f'{base_url}:4399',
},
timeout=300,
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
attach_to_existing=True,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config,
metadata.eval_output_dir,
env_id,
)
)
return config
def initialize_runtime(
runtime: Runtime,
) -> tuple[str, list]:
"""Initialize the runtime for the agent.
This function is called before the runtime is used to run the agent.
"""
logger.info(f"{'-' * 50} BEGIN Runtime Initialization Fn {'-' * 50}")
obs: CmdOutputObservation
# Set instance id
action = CmdRunAction(command='mkdir -p /workspace')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
action = BrowseInteractiveAction(browser_actions=BROWSER_EVAL_GET_GOAL_ACTION)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
goal = obs.content
goal_image_urls = []
if hasattr(obs, 'goal_image_urls'):
goal_image_urls = obs.goal_image_urls
logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
return goal, goal_image_urls
def complete_runtime(
runtime: Runtime,
) -> 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(f"{'-' * 50} BEGIN Runtime Completion Fn {'-' * 50}")
obs: CmdOutputObservation
action = BrowseInteractiveAction(browser_actions=BROWSER_EVAL_GET_REWARDS_ACTION)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
logger.info(f"{'-' * 50} END Runtime Completion Fn {'-' * 50}")
return {
'rewards': json.loads(obs.content),
}
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
):
env_id = instance.instance_id
config = get_config(metadata, env_id)
# 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, env_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {env_id}.')
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
task_str, goal_image_urls = initialize_runtime(runtime)
initial_user_action = MessageAction(content=task_str, image_urls=goal_image_urls)
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=initial_user_action,
runtime=runtime,
)
)
# ======= Attempt to evaluate the agent's environment impact =======
# 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.')
metrics = state.metrics.get() if state.metrics else None
# Instruction obtained from the first message from the USER
instruction = ''
for event in state.history:
if isinstance(event, MessageAction):
instruction = event.content
break
try:
return_val = complete_runtime(runtime)
logger.info(f'Return value from complete_runtime: {return_val}')
reward = max(return_val['rewards'])
except Exception:
reward = -1.0 # kept -1 to identify instances for which evaluation failed.
# 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
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(
instance_id=env_id,
instruction=instruction,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
test_result={
'reward': reward,
},
)
runtime.close()
return output
if __name__ == '__main__':
args = parse_arguments()
dataset = pd.DataFrame(
{
'instance_id': [
id
for id in gym.envs.registry.keys()
if id.startswith('browsergym/visualwebarena')
]
}
)
llm_config = None
if args.llm_config:
llm_config = get_llm_config_arg(args.llm_config)
if llm_config is None:
raise ValueError(f'Could not find LLM config: --llm_config {args.llm_config}')
metadata = make_metadata(
llm_config,
'visualwebarena',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
instances = prepare_dataset(dataset, output_file, args.eval_n_limit)
run_evaluation(
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
)

View File

@@ -0,0 +1,48 @@
#!/bin/bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
# configure browsing agent
export USE_NAV="true"
export USE_CONCISE_ANSWER="true"
MODEL_CONFIG=$1
COMMIT_HASH=$2
AGENT=$3
EVAL_LIMIT=$4
NUM_WORKERS=$5
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 VisualBrowsingAgent"
AGENT="VisualBrowsingAgent"
fi
get_openhands_version
echo "AGENT: $AGENT"
echo "AGENT_VERSION: $OPENHANDS_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
EVAL_NOTE="${OPENHANDS_VERSION}"
COMMAND="poetry run python evaluation/benchmarks/visualwebarena/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations 15 \
--eval-num-workers $NUM_WORKERS \
--eval-note $EVAL_NOTE"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
# Run the command
eval $COMMAND

View File

@@ -71,6 +71,7 @@ def get_config(
'MAP': f'{base_url}:3000',
'HOMEPAGE': f'{base_url}:4399',
},
remote_runtime_enable_retries=True,
),
# do not mount workspace
workspace_base=None,
@@ -78,7 +79,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config.use_microagents = False
agent_config.enable_prompt_extensions = False
return config

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { render, screen } from "@testing-library/react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, test } from "vitest";
import { describe, it, expect } from "vitest";
import { ChatMessage } from "#/components/features/chat/chat-message";
describe("ChatMessage", () => {
@@ -45,7 +45,9 @@ describe("ChatMessage", () => {
await user.click(copyToClipboardButton);
expect(navigator.clipboard.readText()).resolves.toBe("Hello, World!");
await waitFor(() =>
expect(navigator.clipboard.readText()).resolves.toBe("Hello, World!"),
);
});
it("should display an error toast if copying content to clipboard fails", async () => {});

View File

@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import { ExpandableMessage } from "#/components/features/chat/expandable-message";
import { vi } from "vitest";
import { vi } from "vitest"
vi.mock("react-i18next", async () => {
const actual = await vi.importActual("react-i18next");

View File

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

View File

@@ -0,0 +1,45 @@
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
import OpenHands from "#/api/open-hands";
import { SettingsProvider } from "#/context/settings-context";
import { AuthProvider } from "#/context/auth-context";
describe("AnalyticsConsentFormModal", () => {
it("should call saveUserSettings with default settings on confirm reset settings", async () => {
const user = userEvent.setup();
const onCloseMock = vi.fn();
const saveUserSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
render(<AnalyticsConsentFormModal onClose={onCloseMock} />, {
wrapper: ({ children }) => (
<AuthProvider>
<QueryClientProvider client={new QueryClient()}>
<SettingsProvider>{children}</SettingsProvider>
</QueryClientProvider>
</AuthProvider>
),
});
const confirmButton = screen.getByTestId("confirm-preferences");
await user.click(confirmButton);
expect(saveUserSettingsSpy).toHaveBeenCalledWith({
user_consents_to_analytics: true,
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
github_token: undefined,
language: "en",
llm_api_key: undefined,
llm_base_url: "",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
security_analyzer: "",
unset_github_token: undefined,
});
expect(onCloseMock).toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,14 @@
import { render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, test, vi } from "vitest";
import {
afterAll,
afterEach,
beforeAll,
describe,
expect,
it,
test,
vi,
} from "vitest";
import userEvent from "@testing-library/user-event";
import { formatTimeDelta } from "#/utils/format-time-delta";
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
@@ -11,10 +20,18 @@ describe("ConversationCard", () => {
const onChangeTitle = vi.fn();
const onDownloadWorkspace = vi.fn();
beforeAll(() => {
vi.stubGlobal("window", { open: vi.fn() });
});
afterEach(() => {
vi.clearAllMocks();
});
afterAll(() => {
vi.unstubAllGlobals();
});
it("should render the conversation card", () => {
render(
<ConversationCard
@@ -29,9 +46,8 @@ describe("ConversationCard", () => {
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
const card = screen.getByTestId("conversation-card");
const title = within(card).getByTestId("conversation-card-title");
expect(title).toHaveValue("Conversation 1");
within(card).getByText("Conversation 1");
within(card).getByText(expectedDate);
});
@@ -148,10 +164,8 @@ describe("ConversationCard", () => {
/>,
);
const title = screen.getByTestId("conversation-card-title");
expect(title).toBeDisabled();
await clickOnEditButton(user);
const title = screen.getByTestId("conversation-card-title");
expect(title).toBeEnabled();
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
@@ -164,7 +178,6 @@ describe("ConversationCard", () => {
expect(onChangeTitle).toHaveBeenCalledWith("New Conversation Name");
expect(title).toHaveValue("New Conversation Name");
expect(title).toBeDisabled();
});
it("should reset title and not call onChangeTitle when the title is empty", async () => {
@@ -191,7 +204,27 @@ describe("ConversationCard", () => {
expect(title).toHaveValue("Conversation 1");
});
test("clicking the title should not trigger the onClick handler", async () => {
test("clicking the title should trigger the onClick handler", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
isActive
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const title = screen.getByTestId("conversation-card-title");
await user.click(title);
expect(onClick).toHaveBeenCalled();
});
test("clicking the title should not trigger the onClick handler if edit mode", async () => {
const user = userEvent.setup();
render(
<ConversationCard
@@ -204,6 +237,8 @@ describe("ConversationCard", () => {
/>,
);
await clickOnEditButton(user);
const title = screen.getByTestId("conversation-card-title");
await user.click(title);

View File

@@ -1,4 +1,4 @@
import { render, screen, within } from "@testing-library/react";
import { render, screen, waitFor, within } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
QueryClientProvider,
@@ -7,10 +7,12 @@ import {
} from "@tanstack/react-query";
import userEvent from "@testing-library/user-event";
import { createRoutesStub } from "react-router";
import React from "react";
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
import OpenHands from "#/api/open-hands";
import { AuthProvider } from "#/context/auth-context";
import { clickOnEditButton } from "./utils";
import { queryClientConfig } from "#/query-client-config";
describe("ConversationPanel", () => {
const onCloseMock = vi.fn();
@@ -177,9 +179,10 @@ describe("ConversationPanel", () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
const title = within(cards[0]).getByTestId("conversation-card-title");
await clickOnEditButton(user);
const card = cards[0];
await clickOnEditButton(user, card);
const title = within(card).getByTestId("conversation-card-title");
await user.clear(title);
await user.type(title, "Conversation 1 Renamed");
@@ -200,7 +203,10 @@ describe("ConversationPanel", () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
const title = within(cards[0]).getByTestId("conversation-card-title");
const card = cards[0];
await clickOnEditButton(user, card);
const title = within(card).getByTestId("conversation-card-title");
await user.click(title);
await user.tab();
@@ -225,10 +231,53 @@ describe("ConversationPanel", () => {
it("should call onClose after clicking a card", async () => {
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
const firstCard = cards[0];
const firstCard = cards[1];
await userEvent.click(firstCard);
expect(onCloseMock).toHaveBeenCalledOnce();
});
it("should refetch data on rerenders", async () => {
// We need to simulate the toggling of the component to test the refetching
function PanelWithToggle() {
const [isOpen, setIsOpen] = React.useState(true);
return (
<>
<button type="button" onClick={() => setIsOpen((prev) => !prev)}>
Toggle
</button>
{isOpen && <ConversationPanel onClose={onCloseMock} />}
</>
);
}
const MyRouterStub = createRoutesStub([
{
Component: PanelWithToggle,
path: "/",
},
]);
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
render(<MyRouterStub />, {
wrapper: ({ children }) => (
<AuthProvider>
<QueryClientProvider client={new QueryClient(queryClientConfig)}>
{children}
</QueryClientProvider>
</AuthProvider>
),
});
await waitFor(() => expect(getUserConversationsSpy).toHaveBeenCalledOnce());
const button = screen.getByText("Toggle");
await userEvent.click(button);
await userEvent.click(button);
await waitFor(() =>
expect(getUserConversationsSpy).toHaveBeenCalledTimes(2),
);
});
});

View File

@@ -1,11 +1,16 @@
import { screen, within } from "@testing-library/react";
import { UserEvent } from "@testing-library/user-event";
export const clickOnEditButton = async (user: UserEvent) => {
const ellipsisButton = screen.getByTestId("ellipsis-button");
export const clickOnEditButton = async (
user: UserEvent,
container?: HTMLElement,
) => {
const wrapper = container ? within(container) : screen;
const ellipsisButton = wrapper.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = screen.getByTestId("context-menu");
const menu = wrapper.getByTestId("context-menu");
const editButton = within(menu).getByTestId("edit-button");
await user.click(editButton);

View File

@@ -3,49 +3,24 @@ import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import { AxiosError } from "axios";
import { Sidebar } from "#/components/features/sidebar/sidebar";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
import OpenHands from "#/api/open-hands";
import { MOCK_USER_PREFERENCES } from "#/mocks/handlers";
const renderSidebar = () => {
const RouterStub = createRoutesStub([
{
path: "/conversation/:conversationId",
Component: Sidebar,
},
]);
// These tests will now fail because the conversation panel is rendered through a portal
// and technically not a child of the Sidebar component.
const RouterStub = createRoutesStub([
{
path: "/conversation/:conversationId",
Component: () => <Sidebar />,
},
]);
const renderSidebar = () =>
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
};
describe("Sidebar", () => {
it.skipIf(!MULTI_CONVERSATION_UI)(
"should have the conversation panel open by default",
() => {
renderSidebar();
expect(screen.getByTestId("conversation-panel")).toBeInTheDocument();
},
);
it.skipIf(!MULTI_CONVERSATION_UI)(
"should toggle the conversation panel",
async () => {
const user = userEvent.setup();
renderSidebar();
const projectPanelButton = screen.getByTestId(
"toggle-conversation-panel",
);
await user.click(projectPanelButton);
expect(
screen.queryByTestId("conversation-panel"),
).not.toBeInTheDocument();
},
);
describe("Settings", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
@@ -73,35 +48,12 @@ describe("Sidebar", () => {
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
// the actual values are falsey (null or "") but we're checking for undefined
llm_api_key: undefined,
llm_base_url: undefined,
security_analyzer: undefined,
});
});
it("should send all settings data when saving account settings", async () => {
const user = userEvent.setup();
renderSidebar();
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
const menu = screen.getByTestId("account-settings-context-menu");
const accountSettingsButton = within(menu).getByTestId(
"account-settings-button",
);
await user.click(accountSettingsButton);
const accountSettingsModal = screen.getByTestId("account-settings-form");
const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
llm_api_key: undefined, // null or undefined
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
language: "en",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
});
});
@@ -131,14 +83,25 @@ describe("Sidebar", () => {
within(accountSettingsModal).getByLabelText(/GITHUB\$TOKEN_LABEL/i);
await user.type(tokenInput, "new-token");
const analyticsConsentInput =
within(accountSettingsModal).getByTestId("analytics-consent");
await user.click(analyticsConsentInput);
const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
github_token: "new-token",
language: "no",
llm_api_key: undefined, // null or undefined
llm_base_url: "",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
security_analyzer: "",
user_consents_to_analytics: true,
});
});
@@ -152,11 +115,13 @@ describe("Sidebar", () => {
const settingsModal = screen.getByTestId("ai-config-modal");
// Click the advanced options switch to show the API key input
const advancedOptionsSwitch = within(settingsModal).getByTestId("advanced-option-switch");
const advancedOptionsSwitch = within(settingsModal).getByTestId(
"advanced-option-switch",
);
await user.click(advancedOptionsSwitch);
const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i);
await user.type(apiKeyInput, "SET");
await user.type(apiKeyInput, "**********");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",
@@ -164,11 +129,53 @@ describe("Sidebar", () => {
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
llm_api_key: undefined,
agent: "CodeActAgent",
confirmation_mode: false,
enable_default_condenser: false,
language: "en",
llm_base_url: "",
security_analyzer: undefined,
llm_model: "anthropic/claude-3-5-sonnet-20241022",
remote_runtime_resource_factor: 1,
});
});
});
describe("Settings Modal", () => {
it("should open the settings modal if the user clicks the settings button", async () => {
const user = userEvent.setup();
renderSidebar();
expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();
const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);
const settingsModal = screen.getByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
});
it("should open the settings modal if GET /settings fails with a 404", async () => {
const error = new AxiosError(
"Request failed with status code 404",
"ERR_BAD_REQUEST",
undefined,
undefined,
{
status: 404,
statusText: "Not Found",
data: { message: "Settings not found" },
headers: {},
// @ts-expect-error - we only need the response object for this test
config: {},
},
);
vi.spyOn(OpenHands, "getSettings").mockRejectedValue(error);
renderSidebar();
const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
});
});
});

View File

@@ -14,7 +14,7 @@ describe("WaitlistModal", () => {
});
it("should render a tos checkbox that is unchecked by default", () => {
render(<WaitlistModal ghToken={null} githubAuthUrl={null} />);
render(<WaitlistModal ghTokenIsSet={false} githubAuthUrl={null} />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).not.toBeChecked();
@@ -22,7 +22,7 @@ describe("WaitlistModal", () => {
it("should only enable the GitHub button if the tos checkbox is checked", async () => {
const user = userEvent.setup();
render(<WaitlistModal ghToken={null} githubAuthUrl={null} />);
render(<WaitlistModal ghTokenIsSet={false} githubAuthUrl={null} />);
const checkbox = screen.getByRole("checkbox");
const button = screen.getByRole("button", { name: "Connect to GitHub" });
@@ -40,7 +40,7 @@ describe("WaitlistModal", () => {
);
const user = userEvent.setup();
render(<WaitlistModal ghToken={null} githubAuthUrl="mock-url" />);
render(<WaitlistModal ghTokenIsSet={false} githubAuthUrl="mock-url" />);
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);

View File

@@ -1,12 +1,13 @@
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { FeedbackActions } from "#/components/features/feedback/feedback-actions";
import { TrajectoryActions } from "#/components/features/trajectory/trajectory-actions";
describe("FeedbackActions", () => {
describe("TrajectoryActions", () => {
const user = userEvent.setup();
const onPositiveFeedback = vi.fn();
const onNegativeFeedback = vi.fn();
const onExportTrajectory = vi.fn();
afterEach(() => {
vi.clearAllMocks();
@@ -14,9 +15,10 @@ describe("FeedbackActions", () => {
it("should render correctly", () => {
render(
<FeedbackActions
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
/>,
);
@@ -27,9 +29,10 @@ describe("FeedbackActions", () => {
it("should call onPositiveFeedback when positive feedback is clicked", async () => {
render(
<FeedbackActions
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
/>,
);
@@ -41,9 +44,10 @@ describe("FeedbackActions", () => {
it("should call onNegativeFeedback when negative feedback is clicked", async () => {
render(
<FeedbackActions
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
/>,
);
@@ -52,4 +56,19 @@ describe("FeedbackActions", () => {
expect(onNegativeFeedback).toHaveBeenCalled();
});
it("should call onExportTrajectory when negative feedback is clicked", async () => {
render(
<TrajectoryActions
onPositiveFeedback={onPositiveFeedback}
onNegativeFeedback={onNegativeFeedback}
onExportTrajectory={onExportTrajectory}
/>,
);
const exportButton = screen.getByTestId("export-trajectory");
await user.click(exportButton);
expect(onExportTrajectory).toHaveBeenCalled();
});
});

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