Compare commits

..

208 Commits

Author SHA1 Message Date
Robert Brennan
2bec240015 update sys prompt 2024-11-15 11:50:09 -05:00
Robert Brennan
a68ac2f5af Merge branch 'main' into rb/dev-intent 2024-11-15 11:49:04 -05:00
mamoodi
00ffc33d1b Release 0.14.0 (#5027) 2024-11-15 16:02:02 +00:00
sp.wack
1acb66c2b3 feat(frontend): Create push to Github action button in chat interface (#4993) 2024-11-15 15:12:13 +00:00
Xingyao Wang
5b3db1bd33 feat: make add_in_context_learning_example configurable in fn call converter (#5018) 2024-11-15 23:05:05 +08:00
Xingyao Wang
bdc4513937 fix(swebench): handle error in eval_infer and run_infer (#5017) 2024-11-15 23:04:56 +08:00
sp.wack
ffc4d32440 feat(frontend): Keep prompt after project upload or repo selection (#4925) 2024-11-15 16:56:47 +02:00
sp.wack
9cd248d475 feat(frontend): Display runtime ID in the browser console if available (#4978) 2024-11-15 16:38:31 +02:00
OpenHands
5f52eebb40 Fix issue #5021: Add links to the resolver messages (#5022) 2024-11-15 13:05:25 +00:00
Graham Neubig
b0c4580999 Update openhands-resolver.yml with correct package name (#5014) 2024-11-15 06:48:18 -05:00
Robert Brennan
f3b35663e9 fix zip downloads (#5009) 2024-11-14 17:17:36 -05:00
OpenHands
be92965209 Fix issue #4944: [Bug]: Missing GitHub token link in account settings (#4946)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2024-11-14 22:21:02 +02:00
sp.wack
89b304ccb7 refactor(frontend): Improve chat input padding (#4928) 2024-11-14 22:19:04 +02:00
sp.wack
01cacf7c33 feat(frontend): Wait for events before rendering messages (#4994)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2024-11-14 22:09:29 +02:00
Engel Nyst
fac5237c69 Fix user commands in terminal with function calling (#4955)
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2024-11-14 19:14:36 +00:00
Robert Brennan
c784151765 fix file descriptor leaks (#4988)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-14 14:06:33 -05:00
Graham Neubig
ce6f99d80e Add GITHUB_USERNAME env var to resolver step (#4999)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-14 18:42:59 +00:00
Ketan Ramaneti
852c90f64a [fix eval] Fix issues with miniwob remote runtime evaluation (#5001) 2024-11-14 18:00:48 +00:00
Ketan Ramaneti
42b49e6c43 [fix eval] Fix issues with aider_bench remote runtime evaluation (#5000) 2024-11-14 17:58:45 +00:00
Xingyao Wang
07f0d1ccb3 feat(llm): convert function call request for non-funcall OSS model (#4711)
Co-authored-by: Calvin Smith <email@cjsmith.io>
2024-11-15 00:40:09 +08:00
Robert Brennan
52a428d74a Fix markdown ordered list numbering (#4989)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-14 10:59:48 -05:00
OpenHands
27cd507cd2 Fix issue #4985: [Bug]: Cannot exit the session when on Jupyter or Browser tab in the UI (#4986) 2024-11-14 10:06:35 -05:00
Graham Neubig
a753babb7a Integrate OpenHands resolver into main repository (#4964)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2024-11-14 09:45:46 -05:00
Rohit Malhotra
38dc41ca42 Fix: [Bug] Do not render editor action buttons (save/discard) when displaying non-code files (#4903) 2024-11-14 09:09:28 +02:00
Engel Nyst
8dee334236 Context Window Exceeded fix (#4977) 2024-11-14 02:42:39 +00:00
Engel Nyst
a93f1402de Clean up file logs (#4979) 2024-11-13 20:17:21 +00:00
Robert Brennan
bc3f0ac24a fix imports (#4974) 2024-11-13 17:04:16 +00:00
Robert Brennan
f55ddbed0e fix docker leak (#4970) 2024-11-14 00:23:07 +08:00
Xingyao Wang
fd81670ba8 feat: add VSCode to OpenHands runtime and UI (#4745)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Robert Brennan <accounts@rbren.io>
2024-11-14 00:20:49 +08:00
sp.wack
79ed4e3567 fix(frontend): Recover full message history if exists (#4961) 2024-11-13 15:38:30 +02:00
sp.wack
b3fbbbaa9d feat(frontend): Move posthog key to config and upgrade posthog-js (#4940) 2024-11-13 07:56:04 +00:00
tofarr
87c02177d7 Reconnecting websockets (#4954) 2024-11-13 09:38:26 +02:00
OpenHands
207df9dd30 Fix issue #4912: [Bug]: BedrockException: "The number of toolResult blocks at messages.2.content exceeds the number of toolUse blocks of previous turn.". (#4937)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2024-11-12 17:23:11 -05:00
tofarr
59f7093428 Fix max iterations (#4949) 2024-11-12 21:09:43 +00:00
sp.wack
123fb4b75d feat(posthog): Add saas login event (#4948) 2024-11-12 20:37:59 +00:00
mamoodi
40e2d28e87 Release 0.13.1 (#4947) 2024-11-12 15:08:10 -05:00
OpenHands
c555611d58 Fix issue #4941: [Bug]: Browser tab does not reset after starting a new session (#4945)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2024-11-12 19:40:12 +00:00
Calvin Smith
50e7da9c3d fix(evaluation): SWE-bench evaluation script supports multiprocessing (#4943) 2024-11-12 12:19:57 -07:00
sp.wack
0cfb132ab7 fix(frontend): Remove dotted outline on focus (#4926) 2024-11-12 18:27:06 +02:00
Robert Brennan
17f4c6e1a9 Refactor sessions a bit, and fix issue where runtimes get killed (#4900) 2024-11-12 16:20:36 +00:00
Xingyao Wang
910b283ac2 fix(llm): bedrock throw errors if content contains empty string (#4935) 2024-11-12 15:53:22 +00:00
OpenHands
b54724ac3f Fix issue #4931: Make use of microagents configurable in codeact_agent (#4932)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-12 15:42:13 +00:00
Robert Brennan
0633a99298 Fix resume runtime after a pause (#4904) 2024-11-12 09:03:02 -05:00
Ryan H. Tran
d9c5f11046 Replace file editor with openhands-aci (#4782) 2024-11-12 21:26:33 +08:00
Engel Nyst
32fdcd58e5 Update litellm (#4927) 2024-11-12 11:24:19 +00:00
sp.wack
de71b7cdb8 test(frontend): Fix failing e2e test due to mock delay (#4923) 2024-11-12 10:50:38 +00:00
sp.wack
04aeccfb69 fix(frontend): Remove quotes from suggestion (#4921) 2024-11-12 12:30:43 +02:00
Faraz Shamim
4eea1286d4 Issue #4399 : Replaced all occurences (#4878)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2024-11-12 10:58:09 +01:00
Robert Brennan
488a320ffd update to use github client lib (#4909) 2024-11-12 00:56:50 +00:00
Robert Brennan
377fadc2eb fix remote runtimes (#4902) 2024-11-12 00:02:34 +00:00
Robert Brennan
7df7f43e3c Revert "Add rate limiting to server endpoints" (#4910) 2024-11-11 23:26:49 +00:00
Engel Nyst
a45aba512a Tweak log levels (#4729) 2024-11-11 22:51:56 +00:00
tofarr
a1a9d2f175 Refactor websocket (#4879)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2024-11-11 22:36:07 +00:00
Robert Brennan
79492b6551 Add rate limiting to server endpoints (#4867)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-11 16:54:22 -05:00
sp.wack
80fdb9a2f4 feat(posthog): Emit user activated event (#4886) 2024-11-11 23:31:41 +02:00
Nafis Reza
975e75531d Move assets/icons to dedicated folder (#4850) 2024-11-11 20:17:04 +00:00
Robert Brennan
1b5f5bcdad fixes for upcoming changes to remote API (#4834) 2024-11-11 14:51:14 -05:00
Rohit Malhotra
8c00d96024 Support displaying images/videos/pdfs in the workspace (#4898) 2024-11-11 20:22:17 +02:00
Robert Brennan
bf8ccc8fc3 fix infinite loop (#4873)
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2024-11-11 10:59:43 +00:00
OpenHands
037d770f66 Fix issue #4884: (chore) add missing FE translations (#4885)
Co-authored-by: tobitege <10787084+tobitege@users.noreply.github.com>
2024-11-11 10:09:46 +00:00
sp.wack
dd50246672 test(frontend): Pass failing tests (#4887) 2024-11-11 09:49:56 +00:00
Graham Neubig
090771674c Update llms.md w/ more recent results (#4874) 2024-11-10 03:12:09 +00:00
Xingyao Wang
d8ab0208ba fix: remove duplicate claude-3-5-sonnet-20241022 model from VERIFIED_MODELS (#4871)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-09 21:41:56 +00:00
Xingyao Wang
a07e8272da fix: improve remote runtime reliability on large-scale evaluation (#4869) 2024-11-09 20:17:10 +00:00
Robert Brennan
be82832eb1 Use keyword matching for CodeAct microagents (#4568)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2024-11-09 11:25:02 -05:00
ross
67c8915d51 feat(runtime): Add prototype Runloop runtime impl (#4598)
Co-authored-by: Robert Brennan <contact@rbren.io>
2024-11-08 23:40:31 -05:00
Daniel Cruz
40b3ccb17c Adds missing spanish translations (#4858) 2024-11-09 05:14:55 +01:00
Robert Brennan
35c68863dc Don't persist cache on reload (#4854) 2024-11-08 22:31:24 +00:00
mamoodi
8bfee87bcf Release 0.13.0 (#4849) 2024-11-08 22:24:56 +00:00
Robert Brennan
e1383afbc3 Add signed cookie-based GitHub authentication caching (#4853)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-08 22:19:34 +00:00
Xingyao Wang
4ce3b9094a Revert "(feat): Prompt engineering to remind o1 to generate a patch" (#4846) 2024-11-08 16:12:57 +00:00
Graham Neubig
0a4e196670 Update openhands-resolver.yml to remove issue number (#4843) 2024-11-08 15:13:56 +00:00
Daniel Cruz
8d32a59f55 Adds missing localization and translation to spanish (#4837)
Co-authored-by: adrianamorenogt <adrianamorenogutierrez@gmail.com>
2024-11-08 09:33:19 +02:00
tofarr
38b92f4251 UX: Show a loading indicator when downloading a zip (#4833) 2024-11-08 09:28:18 +02:00
Boxuan Li
88dbe85594 Make trajectories_path support file path (#4840) 2024-11-08 06:26:12 +00:00
OpenHands
f5003a7449 Fix issue #4830: [Bug]: Copy-paste into the "What do you want to build?" bar doesn't work (#4832)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-07 23:20:43 -06:00
Alejandro Cuadron Lafuente
a6810fa6ad (feat): Prompt engineering to remind o1 to generate a patch (#4807)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: tofarr <tofarr@gmail.com>
Co-authored-by: Robert Brennan <contact@rbren.io>
2024-11-08 03:10:18 +00:00
Robert Brennan
fc05d8d4eb instruct the agent to comment less (#4681) 2024-11-08 05:21:48 +08:00
sp.wack
1d6ef0e18e fix(frontend): Remove runtime indicator (#4829) 2024-11-08 02:37:59 +08:00
Xingyao Wang
dc0e223d1a fix(agent controller): misplaced runtime.connect that cause swebench workspace to fail (#4826) 2024-11-08 01:50:33 +08:00
tofarr
932de79154 Fix: Buffering zip downloads to files rather than holding in memory (#4802) 2024-11-07 10:24:30 -07:00
Robert Brennan
fa625fed70 Retry on github auth failure (#4767) 2024-11-07 16:57:06 +00:00
Xingyao Wang
f9fa1d95cb fix(RemoteRuntime): add retry for pod status after /start (#4825) 2024-11-07 16:22:47 +00:00
sp.wack
5615d54f81 feat(posthog): Emit useful events (#4798)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-07 16:16:33 +00:00
Xingyao Wang
8166bf768a fix(agent, browsing): too long tool description for openai (#4778) 2024-11-08 00:11:08 +08:00
sp.wack
c3991c870d feat(frontend): Cache request data (#4816) 2024-11-07 16:53:34 +02:00
sp.wack
1a27619b39 feat(frontend): Update npm scripts for cross-platform compatibility with PowerShell and Unix shells (#4727) 2024-11-07 16:51:02 +02:00
sp.wack
cc15aee405 fix(frontend): Fix Jupyter tab overflow (#4818) 2024-11-07 22:48:10 +08:00
Xingyao Wang
53390d9885 Fix issue #4583: [Bug]: Unable to pull the full SWE-Bench test set (#4813)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-07 22:35:20 +08:00
sp.wack
0335b1a634 feat(posthog): Identify users logged in with GitHub (#4794) 2024-11-07 08:37:07 +00:00
Daniel Cruz
bb362cd377 Use i18n Keys (2) (#4464)
Co-authored-by: adrianamorenogt <adrianamorenogutierrez@gmail.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2024-11-07 08:34:59 +00:00
Xingyao Wang
4405b109e3 Fix issue #4809: [Bug]: Model does not support image upload when usin… (#4810)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-07 02:28:16 +00:00
Engel Nyst
47464a9cfa Revert "Feature: Add ability to reconnect websockets" (#4801) 2024-11-07 01:56:39 +00:00
Engel Nyst
2b3fd94540 Fix init order in the agent controller (#4796)
Co-authored-by: tofarr <tofarr@gmail.com>
2024-11-06 22:44:12 +00:00
tofarr
1bd46f3832 Fix - terminal not working (#4800) 2024-11-06 20:34:42 +00:00
Xingyao Wang
8a063fdf6a fix(agent): not default to /repo path (#4799) 2024-11-06 20:21:41 +00:00
OpenHands
025dac5d8f Fix issue #4776: [Bug]: Files are not uploaded to the environment (SWE-Bench) (#4795) 2024-11-06 19:05:06 +00:00
tofarr
0e5e754420 Feature: Add ability to reconnect websockets (#4526) 2024-11-06 18:12:31 +00:00
Robert Brennan
7a8e207985 Fix: Implement caching for clientLoader to prevent repeated calls (#4772)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-11-06 12:51:09 -05:00
mamoodi
a4de0f2142 Update leftover versions (#4792) 2024-11-06 17:21:38 +00:00
dependabot[bot]
27716171bf chore(deps): bump the docusaurus group in /docs with 7 updates (#4789)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-06 17:44:32 +02:00
sp.wack
e5d7735d75 ALL-677 fix(frontend) Truncate long CMD outputs to prevent UI freezing (#4785) 2024-11-06 23:43:25 +08:00
OpenHands
83ccb74d36 Fix issue #4780: [Bug]: Initial query is not cleared after submission (#4781) 2024-11-06 09:54:15 +00:00
sp.wack
118957235d feat(frontend): Chat interface empty state (#4737) 2024-11-06 08:55:50 +00:00
Xingyao Wang
4a6406ed71 feat: add drag & paste image support to ChatInput (#4762)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2024-11-06 07:44:16 +00:00
Rohit Malhotra
4bef974a89 Adding PR number variable to openhands-resolver (#4777) 2024-11-06 02:26:04 +00:00
Robert Brennan
e497438085 Remove extra calls to isAuthenticated (#4766) 2024-11-05 22:09:43 +00:00
Robert Brennan
74b3335b7d Bugfix: fix session close (#4765) 2024-11-05 14:11:15 -05:00
Xingyao Wang
55c41212c8 chore: update browser message to be more human-readable in UI (#4761) 2024-11-05 17:05:19 +00:00
mamoodi
4374ea08d3 Patch release 0.12.3 (#4760) 2024-11-05 16:53:08 +00:00
Rohit Malhotra
436ecb80a3 Logger fixes for openhands-resolver (#4710)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-05 16:49:32 +00:00
tofarr
df9e9fca5a Refactor: Shorter syntax. (#4753) 2024-11-05 16:09:14 +00:00
OpenHands
add0e7d05c Fix issue #4756: [Documentation] When GITHUB_TOKEN is provided automatically through the UI (#4757)
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2024-11-05 15:50:39 +00:00
Robert Brennan
145194c87b Fix images in docker run command for PRs (#4674) 2024-11-05 10:50:24 -05:00
sp.wack
6eafe0d2a8 feat(frontend): Redirect user to app after a project upload or repo selection (and add e2e tests) (#4751) 2024-11-05 17:12:58 +02:00
Engel Nyst
eeb2342509 Refactor history/event stream (#3808) 2024-11-05 03:36:14 +01:00
Graham Neubig
edfba4618a Update bug_template.yml to show app.all-hands.dev (#4709) 2024-11-04 20:47:22 -05:00
Robert Brennan
98751a3ee2 Refactor of error handling (#4575)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2024-11-04 23:30:53 +00:00
Xingyao Wang
24117143ae feat(llm): add new haiku into func calling support model (#4738) 2024-11-04 22:38:00 +00:00
mamoodi
78f4712080 Release 0.12.2 (#4741) 2024-11-04 16:33:50 -05:00
Xingyao Wang
1d2a616be7 Fix issue #4739: '[Bug]: The agent doesn'"'"'t know its name' (#4740)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-04 21:24:35 +00:00
OpenHands
ba25b02978 Fix issue #4735: Update msw mocks (#4736) 2024-11-04 16:58:56 +00:00
Xingyao Wang
966da7b7c8 feat(agent, CodeAct 2.2): native CodeAct support for Browsing (#4667)
Co-authored-by: tofarr <tofarr@gmail.com>
2024-11-05 00:27:27 +08:00
sp.wack
f0af90bff3 fix(frontend): Always return user is authed if mode is oss (#4733) 2024-11-04 16:24:23 +00:00
Engel Nyst
1638968509 History microfixes (#4728) 2024-11-04 16:37:22 +01:00
Robert Brennan
250fcbe62c Various async fixes (#4722) 2024-11-04 10:08:09 -05:00
sp.wack
0595d2336a feat: Analytics with PostHog (#4655) 2024-11-04 09:57:56 +00:00
sp.wack
387c8f1df3 feat(frontend): Make loader synchronous (#4689) 2024-11-04 11:26:30 +02:00
Polygons1
f6c2b287bc Fix for #4717 (#4721) 2024-11-04 08:24:00 +08:00
Xingyao Wang
ab188d026d Revert "Fix permissions on __init__.py" (#4718) 2024-11-04 05:10:43 +08:00
Robert Brennan
316fc260f6 Fix list-files async calls (#4720)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2024-11-03 10:52:53 -08:00
Robert Brennan
aab7fa483b Fix permissions on __init__.py (#4713) 2024-11-03 22:14:42 +08:00
Rohit Malhotra
496364ce53 Adding PR label trigger for openhands-resolver (#4712) 2024-11-02 20:19:30 -04:00
Robert Brennan
61036b5bd1 fix empty msg 2024-11-02 20:12:39 -04:00
Robert Brennan
798f280f5f Merge branch 'rb/dockerfile-fix' into rb/dev-intent 2024-11-02 19:27:19 -04:00
Robert Brennan
a847a11e6e chmod 2024-11-02 19:25:23 -04:00
openhands
23cd526f09 fix: handle concurrent delete operations safely
- Only schedule one delete timer per file
- Add test for concurrent delete operations
- Fix KeyError when multiple timers try to handle the same deletion
2024-11-02 22:45:28 +00:00
Robert Brennan
0b3b23df58 better logging 2024-11-02 18:42:16 -04:00
Robert Brennan
c480507332 Merge branch 'rb/dev-intent' of ssh://github.com/all-hands-ai/openhands into rb/dev-intent 2024-11-02 18:33:47 -04:00
Robert Brennan
c422f3670b add agent configs 2024-11-02 18:33:41 -04:00
openhands
c86078654c test: update file watcher tests to expect EventSource.USER 2024-11-02 22:31:42 +00:00
Robert Brennan
f7b2f20e85 change env 2024-11-02 18:27:42 -04:00
Robert Brennan
0481dc0b41 Merge branch 'rb/dev-intent' of ssh://github.com/all-hands-ai/openhands into rb/dev-intent 2024-11-02 18:27:34 -04:00
openhands
c231b9c348 fix: improve handling of atomic renames and neovim operations
- Add detection of atomic renames (delete+create with same content)
- Add delayed deletion handling to avoid spurious events
- Fix handling of file deletions with debouncing disabled
- Add test for atomic rename handling
2024-11-02 22:26:15 +00:00
Robert Brennan
0bb9cdc0a9 set env to user 2024-11-02 18:22:43 -04:00
openhands
0851ad87f6 fix: improve filesystem event handling and add tests
- Add use_debouncing flag to control debouncing behavior
- Fix event source to use EventSource.ENVIRONMENT consistently
- Add proper handling of neovim temporary files
- Add comprehensive tests for file operations and debouncing
2024-11-02 22:15:13 +00:00
openhands
7914d6ae76 fix: debounce filesystem events to handle neovim's file operations 2024-11-02 22:06:50 +00:00
Robert Brennan
40afe4bd9c Revert "fix: handle neovim's delete-create cycle as edit operation"
This reverts commit a44b1a6408.
2024-11-02 18:01:49 -04:00
Robert Brennan
607952f2b4 Merge branch 'rb/dev-intent' of ssh://github.com/all-hands-ai/openhands into rb/dev-intent 2024-11-02 18:01:38 -04:00
Robert Brennan
6867043ff2 add logs 2024-11-02 18:01:32 -04:00
openhands
a44b1a6408 fix: handle neovim's delete-create cycle as edit operation
- Added buffer to track recently deleted files
- Added time window to detect quick delete-create cycles
- Modified file creation handler to detect and convert to edit events
- Added delayed cleanup for unmatched delete events
2024-11-02 21:38:40 +00:00
Robert Brennan
eab6580dc7 fix logs 2024-11-02 17:28:33 -04:00
Robert Brennan
555c8b5135 fix display in cli 2024-11-02 17:28:18 -04:00
Robert Brennan
3ba0d157fa update codeact 2024-11-02 17:07:31 -04:00
Robert Brennan
a96c61ed55 log spam 2024-11-02 16:41:23 -04:00
Robert Brennan
afe8254456 fix waiting user input 2024-11-02 16:38:43 -04:00
openhands
fb330c9b59 Make CLI input non-blocking using asyncio thread executor 2024-11-02 20:25:59 +00:00
Robert Brennan
c001eb70ab fix lint 2024-11-02 16:21:16 -04:00
Robert Brennan
a9d7479d47 fix lint 2024-11-02 16:21:04 -04:00
Robert Brennan
e5eaec9682 add obs checking 2024-11-02 16:20:43 -04:00
Robert Brennan
53061b7d8d update prompt 2024-11-02 16:20:09 -04:00
Robert Brennan
71df9c6f13 fix event source 2024-11-02 16:16:01 -04:00
openhands
8d93bf81f3 Add test for .git directory ignoring 2024-11-02 19:27:59 +00:00
Robert Brennan
b3911fd44f Merge branch 'rb/dev-intent' of ssh://github.com/all-hands-ai/openhands into rb/dev-intent 2024-11-02 15:25:17 -04:00
openhands
4c0e5e7820 Improve .git directory ignoring to handle nested paths 2024-11-02 19:23:40 +00:00
Robert Brennan
e02237716f lock 2024-11-02 15:22:50 -04:00
Robert Brennan
70feb228e8 Merge branch 'rb/dev-intent' of ssh://github.com/all-hands-ai/openhands into rb/dev-intent 2024-11-02 15:19:57 -04:00
openhands
5248c835ab Fix diff generation to remove @@ line number headers 2024-11-02 19:18:43 +00:00
Robert Brennan
27c1c9d310 new event loop 2024-11-02 15:18:24 -04:00
Robert Brennan
d91f915f89 Merge branch 'rb/dev-intent' of ssh://github.com/all-hands-ai/openhands into rb/dev-intent 2024-11-02 15:12:33 -04:00
Robert Brennan
ce5a5fdfc2 revert plugins 2024-11-02 15:10:25 -04:00
openhands
b9df421ce5 Add comprehensive tests for FileWatcher 2024-11-02 19:09:44 +00:00
Robert Brennan
6e7f3b0499 Merge branch 'rb/dev-intent' of ssh://github.com/all-hands-ai/openhands into rb/dev-intent 2024-11-02 15:08:33 -04:00
openhands
527945cb96 Fix gitignore pattern matching for directories like node_modules 2024-11-02 19:07:28 +00:00
Robert Brennan
693ea45092 move watch 2024-11-02 15:00:51 -04:00
openhands
d8bdfa99e2 Add watchdog dependency for file monitoring 2024-11-02 19:00:28 +00:00
openhands
a4342023ba Update FileWatcher to respect .gitignore in watched directory 2024-11-02 18:57:04 +00:00
openhands
c3c59bad9c Add diff generation to FileWatcher's FileEditObservations 2024-11-02 17:54:51 +00:00
openhands
ebfba98f1b Implement --watch functionality in CLI with FileEditObservation logging 2024-11-02 17:42:33 +00:00
openhands
c1e215c343 Update FileWatcher to use FileEditObservation and track file contents 2024-11-02 17:39:57 +00:00
openhands
110c1ad5dc Add FileWatcher class for directory monitoring 2024-11-02 17:35:05 +00:00
openhands
f03fcbfc59 Add --watch option to CLI for directory monitoring 2024-11-02 16:56:09 +00:00
Ryan H. Tran
4446d3180f fix: use None check instead of falsy (#4705) 2024-11-02 12:44:03 -04:00
Robert Brennan
7b8241e424 fix auth when there are no allow lists (#4707) 2024-11-02 16:25:35 +00:00
Abhijeetsingh Meena
8857f02083 [Eval] DiscoveryBench OpenHands Integration (#4627)
Signed-off-by: Abhijeetsingh Meena <abhijeet040403@gmail.com>
Co-authored-by: Harshit Surana <surana.h@gmail.com>
2024-11-02 07:24:34 -04:00
Xingyao Wang
1747b3d6b2 fix: prompt caching (#4704) 2024-11-02 07:21:21 -04:00
Robert Brennan
36623a16da Minor auth fixes (#4699) 2024-11-01 18:33:29 -07:00
OpenHands
9d3b77bffc Fix issue #4695: [Bug]: Dependabot PRs fail on "Update PR Description" github action step (#4697) 2024-11-01 18:32:31 -07:00
OpenHands
2682518d0e Fix issue #4692: [Bug]: Slack link no longer working (#4693) 2024-11-01 18:34:20 -05:00
Robert Brennan
b27fabe504 Add Google Sheets integration for GitHub user verification (#4671)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-11-01 15:17:15 -07:00
Xingyao Wang
adf7ab5849 fix: handle the case where LLM assistant return None instead of empty string (#4690) 2024-11-01 19:13:01 +00:00
Robert Brennan
456998175f Fix authentication (#4686) 2024-11-01 10:54:06 -07:00
Graham Neubig
b4afd9f170 Update README.md w/ github resolver link (#4679) 2024-11-01 13:07:35 +00:00
sp.wack
73c7375b92 fix(frontend): Prevent editor from changing width unpredictably (#4659) 2024-11-01 14:04:39 +02:00
tofarr
6414b1af6e Fix agent session error in logs (#4669) 2024-11-01 10:50:56 +08:00
tofarr
dd55290f4e Fix : app unresponsive on startup (#4668) 2024-10-31 14:30:33 -07:00
tofarr
be77baea31 refactor: remove unused methods and constants from Session class (#4662)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-10-31 14:55:37 -06:00
Robert Brennan
a812e2b5f1 Add cookie-based authentication to all routes (#4642)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2024-10-31 12:18:42 -07:00
tofarr
4ebff5aaf3 Fix unawaited (#4665) 2024-10-31 19:16:37 +00:00
Engel Nyst
0687608feb [Arch proposal] ENVIRONMENT event source (#4584)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2024-11-01 02:33:13 +08:00
Ziru "Ron" Chen
db4e1dbbec [eval] Add ScienceAgentBench. (#4645)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2024-11-01 02:30:55 +08:00
Robert Brennan
9442e4f9e3 dont run pr update on forks (#4663) 2024-11-01 01:55:50 +08:00
Robert Brennan
e17f7b22a6 Remove hidden commands from feedback (#4597)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2024-10-31 08:49:47 -07:00
mamoodi
ce6939fc0d Release 0.12.0 - Pending Release Notes Prep (#4650) 2024-10-31 13:14:01 +00:00
Xingyao Wang
4705ef9ec2 chore: do not include "status" dict in share-openhands (#4620) 2024-10-31 20:35:35 +08:00
Xingyao Wang
9c2b48ff5d fix(eval): SWE-Bench instance with upper-case instance id (#4649) 2024-10-30 21:24:18 +00:00
Robert Brennan
87906b96a7 Add job to update PR description with docker run command (#4550)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-10-30 16:42:03 -04:00
Xingyao Wang
c0a0d46eb2 test(runtime) #4623: file permission when running the file_editor (#4628)
Co-authored-by: openhands <openhands@all-hands.dev>
2024-10-31 04:34:34 +08:00
366 changed files with 27680 additions and 11491 deletions

View File

@@ -31,6 +31,8 @@ body:
options:
- Docker command in README
- Development workflow
- app.all-hands.dev
- Other
default: 0
- type: input

View File

@@ -286,7 +286,6 @@ jobs:
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
SKIP_CONTAINER_LOGS=true \
TEST_RUNTIME=eventstream \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
@@ -364,7 +363,6 @@ jobs:
image_name=ghcr.io/${{ github.repository_owner }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
image_name=$(echo $image_name | tr '[:upper:]' '[:lower:]')
SKIP_CONTAINER_LOGS=true \
TEST_RUNTIME=eventstream \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
@@ -399,3 +397,49 @@ jobs:
run: |
echo "Some runtime tests failed or were cancelled"
exit 1
update_pr_description:
name: Update PR Description
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]'
needs: [ghcr_build_runtime]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get short SHA
id: short_sha
run: echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Update PR Description
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
SHORT_SHA: ${{ steps.short_sha.outputs.SHORT_SHA }}
run: |
echo "updating PR description"
DOCKER_RUN_COMMAND="docker run -it --rm \
-p 3000:3000 \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:$SHORT_SHA-nikolaik \
--name openhands-app-$SHORT_SHA \
docker.all-hands.dev/all-hands-ai/openhands:$SHORT_SHA"
PR_BODY=$(gh pr view $PR_NUMBER --json body --jq .body)
if echo "$PR_BODY" | grep -q "To run this PR locally, use the following command:"; then
UPDATED_PR_BODY=$(echo "${PR_BODY}" | sed -E "s|docker run -it --rm.*|$DOCKER_RUN_COMMAND|")
else
UPDATED_PR_BODY="${PR_BODY}
---
To run this PR locally, use the following command:
\`\`\`
$DOCKER_RUN_COMMAND
\`\`\`"
fi
echo "updated body: $UPDATED_PR_BODY"
gh pr edit $PR_NUMBER --body "$UPDATED_PR_BODY"

View File

@@ -1,13 +1,267 @@
name: Resolve Issues with OpenHands
name: Auto-Fix Tagged Issue with OpenHands
on:
workflow_call:
inputs:
max_iterations:
required: false
type: number
default: 50
macro:
required: false
type: string
default: "@openhands-agent"
secrets:
LLM_MODEL:
required: true
LLM_API_KEY:
required: true
LLM_BASE_URL:
required: false
PAT_TOKEN:
required: true
PAT_USERNAME:
required: true
issues:
types: [labeled]
pull_request:
types: [labeled]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
pull_request_review:
types: [submitted]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
call-openhands-resolver:
uses: All-Hands-AI/openhands-resolver/.github/workflows/openhands-resolver.yml@main
if: github.event.label.name == 'fix-me'
with:
issue_number: ${{ github.event.issue.number }}
secrets: inherit
auto-fix:
if: |
github.event_name == 'workflow_call' ||
github.event.label.name == 'fix-me' ||
github.event.label.name == 'fix-me-experimental' ||
(
((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
startsWith(github.event.comment.body, inputs.macro || '@openhands-agent') &&
(github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER')
) ||
(github.event_name == 'pull_request_review' &&
startsWith(github.event.review.body, inputs.macro || '@openhands-agent') &&
(github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER')
)
)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Get latest versions and create requirements.txt
run: |
python -m pip index versions openhands-ai > openhands_versions.txt
OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()')
echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt
cat requirements.txt
- name: Cache pip dependencies
if: github.event.label.name != 'fix-me-experimental'
uses: actions/cache@v3
with:
path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/*
key: ${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-openhands-resolver-${{ hashFiles('requirements.txt') }}
- name: Check required environment variables
env:
LLM_MODEL: ${{ secrets.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
run: |
required_vars=("LLM_MODEL" "LLM_API_KEY" "PAT_TOKEN" "PAT_USERNAME")
for var in "${required_vars[@]}"; do
if [ -z "${!var}" ]; then
echo "Error: Required environment variable $var is not set."
exit 1
fi
done
- name: Set environment variables
run: |
if [ -n "${{ github.event.review.body }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
elif [ -n "${{ github.event.issue.pull_request }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
elif [ -n "${{ github.event.pull_request.number }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
else
echo "ISSUE_NUMBER=${{ github.event.issue.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=issue" >> $GITHUB_ENV
fi
if [ -n "${{ github.event.review.body }}" ]; then
echo "COMMENT_ID=${{ github.event.review.id || 'None' }}" >> $GITHUB_ENV
else
echo "COMMENT_ID=${{ github.event.comment.id || 'None' }}" >> $GITHUB_ENV
fi
echo "MAX_ITERATIONS=${{ inputs.max_iterations || 50 }}" >> $GITHUB_ENV
echo "SANDBOX_ENV_GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
- name: Comment on issue with start message
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const issueType = process.env.ISSUE_TYPE;
github.rest.issues.createComment({
issue_number: ${{ env.ISSUE_NUMBER }},
owner: context.repo.owner,
repo: context.repo.repo,
body: `[OpenHands](https://github.com/All-Hands-AI/OpenHands) started fixing the ${issueType}! You can monitor the progress [here](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`
});
- name: Install OpenHands
run: |
if [ "${{ github.event.label.name }}" == "fix-me-experimental" ]; then
python -m pip install --upgrade pip
pip install git+https://github.com/all-hands-ai/openhands.git
else
python -m pip install --upgrade -r requirements.txt
fi
- name: Attempt to resolve issue
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME }}
LLM_MODEL: ${{ secrets.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
PYTHONPATH: ""
run: |
cd /tmp && python -m openhands.resolver.resolve_issue \
--repo ${{ github.repository }} \
--issue-number ${{ env.ISSUE_NUMBER }} \
--issue-type ${{ env.ISSUE_TYPE }} \
--max-iterations ${{ env.MAX_ITERATIONS }} \
--comment-id ${{ env.COMMENT_ID }}
- name: Check resolution result
id: check_result
run: |
if cd /tmp && grep -q '"success":true' output/output.jsonl; then
echo "RESOLUTION_SUCCESS=true" >> $GITHUB_OUTPUT
else
echo "RESOLUTION_SUCCESS=false" >> $GITHUB_OUTPUT
fi
- name: Upload output.jsonl as artifact
uses: actions/upload-artifact@v4
if: always() # Upload even if the previous steps fail
with:
name: resolver-output
path: /tmp/output/output.jsonl
retention-days: 30 # Keep the artifact for 30 days
- name: Create draft PR or push branch
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
GITHUB_USERNAME: ${{ secrets.PAT_USERNAME }}
LLM_MODEL: ${{ secrets.LLM_MODEL }}
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
PYTHONPATH: ""
run: |
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type draft | tee pr_result.txt && \
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
else
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type branch \
--send-on-failure | tee branch_result.txt && \
grep "branch created" branch_result.txt | sed 's/.*\///g; s/.expand=1//g' > branch_name.txt
fi
- name: Comment on issue
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const fs = require('fs');
const issueNumber = ${{ env.ISSUE_NUMBER }};
const success = ${{ steps.check_result.outputs.RESOLUTION_SUCCESS }};
let prNumber = '';
let branchName = '';
let logContent = '';
const noChangesMessage = `No changes to commit for issue #${issueNumber}. Skipping commit.`;
try {
if (success){
logContent = fs.readFileSync('/tmp/pr_result.txt', 'utf8').trim();
} else {
logContent = fs.readFileSync('/tmp/branch_result.txt', 'utf8').trim();
}
} catch (error) {
console.error('Error reading results file:', error);
}
try {
if (success) {
prNumber = fs.readFileSync('/tmp/pr_number.txt', 'utf8').trim();
} else {
branchName = fs.readFileSync('/tmp/branch_name.txt', 'utf8').trim();
}
} catch (error) {
console.error('Error reading file:', error);
}
if (logContent.includes(noChangesMessage)) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `The workflow to fix this issue encountered an error. Openhands failed to create any code changes.`
});
} else if (success && prNumber) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `A potential fix has been generated and a draft PR #${prNumber} has been created. Please review the changes.`
});
} else if (!success && branchName) {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `An attempt was made to automatically fix this issue, but it was unsuccessful. A branch named '${branchName}' has been created with the attempted changes. You can view the branch [here](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${branchName}). Manual intervention may be required.`
});
} else {
github.rest.issues.createComment({
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: `The workflow to fix this issue encountered an error. Please check the [workflow logs](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) for more information.`
});
}

4
.gitignore vendored
View File

@@ -174,6 +174,10 @@ evaluation/bird/data
evaluation/gaia/data
evaluation/gorilla/data
evaluation/toolqa/data
evaluation/scienceagentbench/benchmark
# openhands resolver
output/
# frontend

View File

@@ -100,7 +100,7 @@ poetry run pytest ./tests/unit/test_*.py
### 9. Use existing Docker image
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image. Follow these steps:
1. Set the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
2. Example: export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.9-nikolaik
2. Example: export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.14-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/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA"><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-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw"><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/>
@@ -38,15 +38,16 @@ See the [Installation](https://docs.all-hands.dev/modules/usage/installation) gu
system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.14-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
docker run -it --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.14-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
-e LOG_ALL_EVENTS=true \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.11
docker.all-hands.dev/all-hands-ai/openhands:0.14
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
@@ -59,7 +60,8 @@ works best, but you have [many options](https://docs.all-hands.dev/modules/usage
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
or interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode).
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
or run it on tagged issues with [a github action](https://github.com/All-Hands-AI/OpenHands-resolver).
Visit [Installation](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
@@ -92,7 +94,7 @@ For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md).
Whether you're a developer, a researcher, or simply enthusiastic about OpenHands, we'd love to have you in our community.
Let's make software engineering better together!
- [Slack workspace](https://join.slack.com/t/opendevin/shared_invite/zt-2oikve2hu-UDxHeo8nsE69y6T7yFX_BA) - Here we talk about research, architecture, and future development.
- [Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw) - Here we talk about research, architecture, and future development.
- [Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
## 📈 Progress

View File

@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.9-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.14-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -32,7 +32,8 @@ workspace_base = "./workspace"
# Enable saving and restoring the session when run from CLI
#enable_cli_session = false
# Path to store trajectories
# Path to store trajectories, can be a folder or a file
# If it's a folder, the session id will be used as the file name
#trajectories_path="./trajectories"
# File store path

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.9-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.14-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -161,7 +161,7 @@ Pour créer un workflow d'évaluation pour votre benchmark, suivez ces étapes :
instruction=instruction,
test_result=evaluation_result,
metadata=metadata,
history=state.history.compatibility_for_eval_history_pairs(),
history=compatibility_for_eval_history_pairs(state.history),
metrics=state.metrics.get() if state.metrics else None,
error=state.last_error if state and state.last_error else None,
)
@@ -260,7 +260,7 @@ def codeact_user_response(state: State | None) -> str:
# vérifier si l'agent a essayé de parler à l'utilisateur 3 fois, si oui, faire savoir à l'agent qu'il peut abandonner
user_msgs = [
event
for event in state.history.get_events()
for event in state.history
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) >= 2:
@@ -279,4 +279,3 @@ Cette fonction fait ce qui suit :
3. Si l'agent a fait plusieurs tentatives, il lui donne la possibilité d'abandonner
En utilisant cette fonction, vous pouvez garantir un comportement cohérent sur plusieurs exécutions d'évaluation et empêcher l'agent de rester bloqué en attendant une entrée humaine.

View File

@@ -158,7 +158,7 @@ OpenHands 的主要入口点在 `openhands/core/main.py` 中。以下是它工
instruction=instruction,
test_result=evaluation_result,
metadata=metadata,
history=state.history.compatibility_for_eval_history_pairs(),
history=compatibility_for_eval_history_pairs(state.history),
metrics=state.metrics.get() if state.metrics else None,
error=state.last_error if state and state.last_error else None,
)
@@ -257,7 +257,7 @@ def codeact_user_response(state: State | None) -> str:
# 检查代理是否已尝试与用户对话 3 次,如果是,让代理知道它可以放弃
user_msgs = [
event
for event in state.history.get_events()
for event in state.history
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) >= 2:

View File

@@ -58,4 +58,3 @@ docker run -it \
ghcr.io/all-hands-ai/openhands:0.11 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -50,6 +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.14-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -58,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.11 \
docker.all-hands.dev/all-hands-ai/openhands:0.14 \
python -m openhands.core.cli
```
@@ -107,4 +108,3 @@ Expected Output:
```bash
🤖 An error occurred. Please try again.
```

View File

@@ -158,7 +158,7 @@ To create an evaluation workflow for your benchmark, follow these steps:
instruction=instruction,
test_result=evaluation_result,
metadata=metadata,
history=state.history.compatibility_for_eval_history_pairs(),
history=compatibility_for_eval_history_pairs(state.history),
metrics=state.metrics.get() if state.metrics else None,
error=state.last_error if state and state.last_error else None,
)
@@ -257,7 +257,7 @@ def codeact_user_response(state: State | None) -> str:
# check if the agent has tried to talk to the user 3 times, if so, let the agent know it can give up
user_msgs = [
event
for event in state.history.get_events()
for event in state.history
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) >= 2:

View File

@@ -19,6 +19,15 @@ OpenHands provides a user-friendly Graphical User Interface (GUI) mode for inter
3. Enter the corresponding `API Key` for your chosen provider.
4. Click "Save" to apply the settings.
### GitHub Token Setup
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
1. Locally (OSS): The user directly inputs their GitHub token.
2. Online (SaaS): The token is obtained through GitHub OAuth authentication.
When you reach the `/app` route, the app checks if a token is present. If it finds one, it sets it in the environment for the agent to use.
### Advanced Settings
1. Toggle `Advanced Options` to access additional settings.

View File

@@ -44,15 +44,16 @@ 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.14-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
-e LLM_MODEL=$LLM_MODEL \
-e LOG_ALL_EVENTS=true \
-v $WORKSPACE_BASE:/opt/workspace_base \
-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.11 \
docker.all-hands.dev/all-hands-ai/openhands:0.14 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -11,15 +11,16 @@
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.14-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.14-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
-e LOG_ALL_EVENTS=true \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.11
docker.all-hands.dev/all-hands-ai/openhands:0.14
```
You can also run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), as an [interactive CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), or using the [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action).

View File

@@ -0,0 +1,20 @@
# LiteLLM Proxy
OpenHands supports using the [LiteLLM proxy](https://docs.litellm.ai/docs/proxy/quick_start) to access various LLM providers.
## Configuration
To use LiteLLM proxy with OpenHands, you need to:
1. Set up a LiteLLM proxy server (see [LiteLLM documentation](https://docs.litellm.ai/docs/proxy/quick_start))
2. When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
* Enable `Advanced Options`
* `Custom Model` to the prefix `litellm_proxy/` + the model you will be using (e.g. `litellm_proxy/anthropic.claude-3-5-sonnet-20241022-v2:0`)
* `Base URL` to your LiteLLM proxy URL (e.g. `https://your-litellm-proxy.com`)
* `API Key` to your LiteLLM proxy API key
## Supported Models
The supported models depend on your LiteLLM proxy configuration. OpenHands supports any model that your LiteLLM proxy is configured to handle.
Refer to your LiteLLM proxy configuration for the list of available models and their names.

View File

@@ -4,11 +4,11 @@ OpenHands can connect to any LLM supported by LiteLLM. However, it requires a po
## Model Recommendations
Based on a recent evaluation of language models for coding tasks (using the SWE-bench dataset), we can provide some recommendations for model selection. The full analysis can be found in [this blog article](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed).
Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some recommendations for model selection. Some analyses can be found in [this blog article comparing LLMs](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) and [this blog article with some more recent results](https://www.all-hands.dev/blog/openhands-codeact-21-an-open-state-of-the-art-software-development-agent).
When choosing a model, consider both the quality of outputs and the associated costs. Here's a summary of the findings:
- Claude 3.5 Sonnet is the best by a fair amount, achieving a 27% resolve rate with the default agent in OpenHands.
- Claude 3.5 Sonnet is the best by a fair amount, achieving a 53% resolve rate on SWE-Bench Verified with the default agent in OpenHands.
- GPT-4o lags behind, and o1-mini actually performed somewhat worse than GPT-4o. We went in and analyzed the results a little, and briefly it seemed like o1 was sometimes "overthinking" things, performing extra environment configuration tasks when it could just go ahead and finish the task.
- Finally, the strongest open models were Llama 3.1 405 B and deepseek-v2.5, and they performed reasonably, even besting some of the closed models.
@@ -63,6 +63,7 @@ We have a few guides for running OpenHands with specific model providers:
- [Azure](llms/azure-llms)
- [Google](llms/google-llms)
- [Groq](llms/groq)
- [LiteLLM Proxy](llms/litellm-proxy)
- [OpenAI](llms/openai-llms)
- [OpenRouter](llms/openrouter)

View File

@@ -59,8 +59,7 @@ docker run # ...
-e RUNTIME=remote \
-e SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.app.all-hands.dev" \
-e SANDBOX_API_KEY="your-all-hands-api-key" \
-e SANDBOX_KEEP_REMOTE_RUNTIME_ALIVE="true" \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
-e SANDBOX_KEEP_RUNTIME_ALIVE="true" \
# ...
```
@@ -75,5 +74,4 @@ docker run # ...
-e RUNTIME=modal \
-e MODAL_API_TOKEN_ID="your-id" \
-e MODAL_API_TOKEN_SECRET="your-secret" \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.11-nikolaik \
```

2715
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,10 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "^3.5.2",
"@docusaurus/plugin-content-pages": "^3.5.2",
"@docusaurus/preset-classic": "^3.5.2",
"@docusaurus/theme-mermaid": "^3.5.2",
"@docusaurus/core": "^3.6.0",
"@docusaurus/plugin-content-pages": "^3.6.0",
"@docusaurus/preset-classic": "^3.6.0",
"@docusaurus/theme-mermaid": "^3.6.0",
"@mdx-js/react": "^3.1.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.4.0",
@@ -29,7 +29,7 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.5.1",
"@docusaurus/tsconfig": "^3.5.2",
"@docusaurus/tsconfig": "^3.6.0",
"@docusaurus/types": "^3.5.1",
"typescript": "~5.6.3"
},

View File

@@ -76,6 +76,11 @@ const sidebars: SidebarsConfig = {
label: 'Groq',
id: 'usage/llms/groq',
},
{
type: 'doc',
label: 'LiteLLM Proxy',
id: 'usage/llms/litellm-proxy',
},
{
type: 'doc',
label: 'OpenAI',

File diff suppressed because it is too large Load Diff

View File

@@ -87,9 +87,7 @@ class Q20Game:
# others
bingo, anwser_reply = self.judge_winner(response)
if bingo:
return (
'You are bingo! quit now, run: <execute_bash> exit </execute_bash>.\n'
)
return 'You are bingo! Use the "finish" tool to finish the interaction.\n'
if self.curr_turn == self.num_turns - 2:
anwser_reply += " You must guess now, what's it?"
return anwser_reply

View File

@@ -8,6 +8,7 @@ from evaluation.EDA.game import Q20Game, Q20GameCelebrity
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -34,7 +35,8 @@ def codeact_user_response_eda(state: State) -> str:
# retrieve the latest model message from history
if state.history:
model_guess = state.history.get_last_agent_message()
last_agent_message = state.get_last_agent_message()
model_guess = last_agent_message.content if last_agent_message else ''
assert game is not None, 'Game is not initialized.'
msg = game.generate_user_response(model_guess)
@@ -139,7 +141,8 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
final_message = state.history.get_last_agent_message()
last_agent_message = state.get_last_agent_message()
final_message = last_agent_message.content if last_agent_message else ''
logger.info(f'Final message: {final_message} | Ground truth: {instance["text"]}')
test_result = game.reward()
@@ -148,7 +151,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -84,4 +84,3 @@ all the preprocessing/evaluation/analysis scripts.
- Raw data and experimental records should not be stored within this repo.
- For model outputs, they should be stored at [this huggingface space](https://huggingface.co/spaces/OpenHands/evaluation) for visualization.
- Important data files of manageable size and analysis scripts (e.g., jupyter notebooks) can be directly uploaded to this repo.

View File

@@ -16,6 +16,7 @@ from evaluation.agent_bench.helper import (
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -242,7 +243,7 @@ def process_instance(
raw_ans = ''
# retrieve the last agent message or thought
for event in state.history.get_events(reverse=True):
for event in reversed(state.history):
if event.source == 'agent':
if isinstance(event, AgentFinishAction):
raw_ans = event.thought
@@ -271,7 +272,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
metrics = state.metrics.get() if state.metrics else None

View File

@@ -56,6 +56,20 @@ You can update the arguments in the script
./evaluation/aider_bench/scripts/run_infer.sh eval_gpt35_turbo HEAD CodeActAgent 100 1 "1,3,10"
```
### Run Inference on `RemoteRuntime` (experimental)
This is in limited beta. Contact Xingyao over slack if you want to try this out!
```bash
./evaluation/aider_bench/scripts/run_infer.sh [model_config] [git-version] [agent] [eval_limit] [eval-num-workers] [eval_ids]
# Example - This runs evaluation on CodeActAgent for 133 instances on aider_bench test set, with 2 workers running in parallel
export ALLHANDS_API_KEY="YOUR-API-KEY"
export RUNTIME=remote
export SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev"
./evaluation/aider_bench/scripts/run_infer.sh llm.eval HEAD CodeActAgent 133 2
```
## Summarize Results
```bash

View File

@@ -15,6 +15,7 @@ from evaluation.aider_bench.helper import (
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -57,6 +58,9 @@ def get_config(
use_host_network=False,
timeout=100,
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_init_timeout=1800,
),
# do not mount workspace
workspace_base=None,
@@ -250,7 +254,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
metrics = state.metrics.get() if state.metrics else None
# Save the output

View File

@@ -13,6 +13,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -39,7 +40,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n'
}
FILE_EXT_MAP = {
@@ -299,7 +300,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
test_result['generated'] = test_result['metadata']['1_copy_change_code']

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,7 @@ from tqdm import tqdm
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -39,21 +40,21 @@ from openhands.utils.async_utils import call_async_from_sync
def codeact_user_response(state: State) -> str:
msg = (
'Please continue working on the task on whatever approach you think is suitable.\n'
'If you think you have completed the SQL, please run the following command: <execute_bash> exit </execute_bash>.\n'
'If you think you have completed the SQL, please finish the interaction using the "finish" tool.\n'
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP OR USE THE INTERNET TO SOLVE THIS TASK.\n'
)
if state.history:
# check if the agent has tried to talk to the user 3 times, if so, let the agent know it can give up
user_msgs = [
event
for event in state.history.get_events()
for event in state.history
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) > 2:
# let the agent know that it can give up when it has tried 3 times
return (
msg
+ 'If you want to give up, run: <execute_bash> exit </execute_bash>.\n'
+ 'If you want to give up, use the "finish" tool to finish the interaction.\n'
)
return msg
@@ -63,7 +64,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n'
}
@@ -431,7 +432,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -9,6 +9,7 @@ from datasets import load_dataset
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -89,7 +90,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# find the last delegate action
last_delegate_action = None

View File

@@ -0,0 +1,37 @@
# DiscoveryBench with OpenHands
[DiscoveryBench](https://github.com/allenai/discoverybench/) [(Paper)](https://arxiv.org/abs/2407.01725v1) contains 264 tasks collected across 6 diverse domains, such as biology, economics, and sociology. It incorporates discovery workflows from published papers to approximate the real-world challenges faced by researchers.
<p align="center">
<a href="[https://github.com/allenai/discoverybench](https://github.com/allenai/discoverybench)">
<img src="https://raw.githubusercontent.com/allenai/discoverybench/refs/heads/main/assets/discoverybench-openhands-teaser.png" width="100%" alt="DiscoveryBench Background" />
</a>
</p>
## Setup Environment and LLM Configuration
1. Please follow instructions mentioned [here](https://github.com/openlocus/OpenHands/blob/discoverybench-openhands-integration/evaluation/README.md#setup) to setup OpenHands development environment and LLMs locally
2. Execute the bash script to start DiscoveryBench Evaluation
```
./evaluation/discoverybench/scripts/run_infer.sh [YOUR MODEL CONFIG]
```
Replace `[YOUR MODEL CONFIG]` with any model the model that you have set up in `config.toml`
## Run Inference on DiscoveryBench Instances
When the `run_infer.sh` script is started, it will automatically pull the latest DiscoveryBench instances & set up the agent environment. The OpenHands agent is invoked to process the task within this environment, producing a hypothesis. We then evaluate it against the “gold” hypothesis provided by DiscoveryBench. The evaluation result, along with the agent chat history is logged to `output.jsonl` under `evaluation_outputs`.
```
./evaluation/discoverybench/scripts/run_infer.sh [MODEL_CONFIG] [GIT_COMMIT] [AGENT] [EVAL_LIMIT] [NUM_WORKERS]
```
- `MODEL_CONFIG`: Name of the model you want to evaluate with
- `GIT_COMMIT`: This should be the git commit hash or release tag for OpenHands, e.g., HEAD or a specific tag like 0.6.2.
- `AGENT`: Use CoderActAgent, right now it only supports that.
- `EVAL_LIMIT`: Number of samples to evaluate.
- `NUM_WORKERS`: Number of workers to parallelize the evaluation process.

View File

@@ -0,0 +1,7 @@
## DiscoveryBench Evaluation Utils
- **`eval_w_subhypo_gen.py`**: Implements the DiscoveryBench logic for evaluating agent-generated hypotheses.
- **`lm_utils.py`**: Provides utility functions necessary for the evaluation process.
- **`openai_helpers.py`**: Includes helper functions for OpenAI-related tasks.
- **`openai_semantic_gen_prompts.py`**: Contains prompts used for semantic generation.
- **`response_parser.py`**: Handles the parsing of agent-generated hypotheses.

View File

@@ -0,0 +1,538 @@
import json
import logging
from openai import OpenAI
from .lm_utils import run_chatgpt_query_multi_turn
from .openai_helpers import get_response
logging.basicConfig(
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
datefmt='%m/%d/%Y %H:%M:%S',
level=logging.INFO,
)
logger = logging.getLogger(__name__)
def get_score_from_answer(type, answer):
if type == 'context':
answer = answer.replace('Answer:', '').strip()
if answer.startswith('A)'):
return 1.0
elif answer.startswith('B)'):
return 0.0
return -1.0
elif type == 'var':
try:
var_json = json.loads(answer)
# print(f"var_json:{var_json}")
p = 0.0
r = 0.0
f1 = 0.0
if var_json['sizeB']:
p = var_json['intersection'] / var_json['sizeB']
if var_json['sizeA']:
r = var_json['intersection'] / var_json['sizeA']
if p > 0.0 and r > 0.0:
f1 = (2 * p * r) / (p + r)
else:
f1 = 0.0
eval_rec = {
'p': p,
'r': r,
'f1': f1,
'sizeA': var_json['sizeA'],
'sizeB': var_json['sizeB'],
'intersection': var_json['intersection'],
'explanation': var_json['explanation'],
}
print(f'var_eval: {eval_rec}')
return eval_rec
except Exception: # COMMENT: added Exception
return {'p': -1.0, 'r': -1.0, 'f1': -1.0}
elif type == 'rel':
print(answer)
rel_json = json.loads(answer)
answer_str = rel_json['answer'].strip()
if answer_str.startswith('A') or 'very similar' in answer_str:
return 1.0
elif (
answer_str.startswith('B') or 'similar but general than HypoA' in answer_str
):
return 0.5
elif answer_str.startswith('C') or 'different' in answer_str:
return 0.0
return -1.0
return -1.0
def ask_dimension_question(
query,
gold_hypo,
gold_workflow,
gen_hypo,
gen_workflow,
dataset_meta,
llm_used,
dimension,
dataset_type,
use_column_metadata=True,
):
dimension_question = ''
answer = ''
score = 0.0
if dimension == 'var':
score = {'p': -1.0, 'r': -1.0, 'f1': -1.0}
num_tokens = 256
num_retries = 1
json_response = False
messages = [
{
'role': 'system',
'content': 'You are an AI assistant that helps evaluate a data-driven hypothesis. You are a helpful assistant who is not talkative. You only respond with the exact answer to a query without additional conversation.',
},
]
if dimension == 'context':
dimension_question = """\
Question: Is HypoB defined in the same context as HypoA?
(Context refers to assumptions/stratification under which the hypotheses are defined.)
Options: A) same B) different
What is your answer?"""
elif dimension == 'var':
dimension_question = """\
Question: For both HypoA and HypoB, what are the different variables found in the hypotheses? \
Return your answer as a JSON object in the following format:
```json
{{
"sizeA": num of variables used in HypoA
"sizeB": num of variables used in HypoB
"intersection": num of variables common in HypoA and HypoB. Use *fuzzy matching* to determine intersection, accounting for paraphrases or slightly different surface forms
"explanation": a short text explanation about the variables
}}```
Answer:"""
num_tokens = 512
num_retries = 1
json_response = True
elif dimension == 'rel':
dimension_question = """\
Question: Does HypoB exhibit the same relation as HypoA?
Compare using following example hierarchy of relationships (based on specificity): \
"there exists a relationship" > "positive relationship" > "positive AND (linear OR quadratic)" > "positive AND linear".
Options: A) very similar B) similar but general than HypoA C) different
Return your answer as a JSON object in the following format:
```json
{{
"answer": one of the options from A) very similar B) similar but general than HypoA C) different
"explanation": a short text explanation about the relationship comparison
}}```
Answer:"""
num_tokens = 512
num_retries = 1
json_response = True
datasets_json = prepare_dataset_metadata_json(
dataset_meta, dataset_type=dataset_type, use_column_metadata=use_column_metadata
)
dimension_question_str = f"""\
You are going to compare two natural-language hypotheses HypoA and HypoB accompanied with optional workflows: WorkflowA for HypoA and WorkflowB for HypoB. \
Both the hypotheses answer the natural language query "QUERY" over the dataset(s) described by dataset description(s) and column description(s) below. \
Compare HypoA and HypoB in terms of three aspects: Contexts, Variables, and Relations. \
E.g., for the hypothesis "From 1995 to 2009, the number of sandhill cranes around the tundra (Indigilka River) surged by an astounding ~10X":
* Contexts refer to stratification of the data under which the given hypothesis is True. E.g., "For all women", "From 1995 to 2009".
* Variables refer to the set of variables (either dependent or independent) that are mentioned in the hypothesis. E.g., number of sandhill cranes, location.
* Relations refer to the form of relation between the variables. E.g., "surged by ~10x".
Answer following questions for a given pair of hypotheses, HypoA and HypoB, along with an explanation grounded on the QUERY and the DATASET(S).
Here is the metadata for the task:
```json
{{
"datasets": {datasets_json},
"query": {query},
"HypoA": {gold_hypo},
"WorkflowA": {gold_workflow},
"HypoB": {gen_hypo},
"WorkflowB": {gen_workflow}
}}
```
{dimension_question}"""
messages.append({'role': 'user', 'content': dimension_question_str})
for retry in range(num_retries):
response = run_chatgpt_query_multi_turn(
messages=messages,
model_name=llm_used,
max_tokens=num_tokens,
temperature=0, # 0 for greedy best decoding
json_response=json_response,
)
if response is not None: # COMMENT: changed from != to is not
break
if response is not None: # COMMENT: changed from != to is not
answer = response.choices[0].message.content.strip()
score = get_score_from_answer(type=dimension, answer=answer)
return dimension_question, answer, score
def prepare_dataset_metadata_json(dataset_meta, dataset_type, use_column_metadata=True):
if dataset_meta is None: # COMMENT: changed from == to is None
return [
{
'dataset_description': '',
'columns': [],
}
]
datasets_json = []
if dataset_type == 'real':
for d in dataset_meta['datasets']:
datasets_json.append(
{
'dataset_description': d['description'],
'columns': [
{'name': col['name'], 'description': col['description']}
for col in d['columns']['raw']
]
if use_column_metadata
else [],
}
)
else:
for d in dataset_meta['datasets']:
datasets_json.append(
{
'dataset_description': d['description'],
'columns': [
{'name': col['name'], 'description': col['description']}
for col in d['columns']
]
if use_column_metadata
else [],
}
)
return datasets_json
def get_sub_hypotheses(
query,
hypo,
workflow,
dataset_meta,
llm_used,
dataset_type,
use_column_metadata=True,
):
client = OpenAI()
extraction_prompt = """\
Given a set of dataset columns, a ground-truth hypothesis, and the analysis workflow used, your task is to extract three dimensions that define the hypothesis: Context, Variables, and Relations. \
Here are the definitions for these dimensions:
- Contexts: Boundary conditions that limit the scope of a hypothesis. E.g., “for men over \
the age of 30”, “in Asia and Europe”. If the context applies to the full dataset, then extract the context from the dataset_descrption.
- Variables: Known concepts that interact in a meaningful way under a given context to \
produce the hypothesis. E.g., gender, age, income, or "None" if there is no interacting variable.
- Relations: Interactions between a given set of variables under a given context to produce \
the hypothesis. E.g., “quadratic relationship”, “inversely proportional”, piecewise conditionals, \
or "None" if there is no interacting relationship.
Make sure to only use the information present in the hypothesis and the workflow. Do not add any new information. \
For each dimension, be specific, and do not omit any important details.
Here is the metadata for the task:
```json
{
"datasets": %s,
"hypothesis": "%s",
"workflow": "%s"
}
```
Return your answer as a JSON object in the following format:
```json
{
"sub_hypo": [
{
"text": the hypothesis in natural language,
"context": a short text description of the context of the hypothesis,
"variables": a list of columns involved in the hypothesis,
"relations": a short text description of the relationship between the variables of the hypothesis
},
...
]
}```
"""
datasets_json = prepare_dataset_metadata_json(
dataset_meta, dataset_type, use_column_metadata=use_column_metadata
)
_prompt = extraction_prompt % (datasets_json, hypo, workflow)
sub_hypo_json = get_response(client, _prompt, model=llm_used, max_retry=1)
if sub_hypo_json is not None: # COMMENT: changed from != to is not
# print(f"full hypothesis: {hypo}")
print(f'sub_hypo_json: {sub_hypo_json}')
else:
sub_hypo_json = {
'sub_hypo': [],
}
sub_hypo_json['full_hypo'] = hypo
return sub_hypo_json
def match_context_with_gpt(
gold_hyp, gold_context, pred_hyp, pred_context, model='gpt-3.5-turbo'
):
prompt = f"""\
Given a gold hypothesis, a gold context, a predicted hypothesis, and a predicted context, your task is \
to determine if the predicted context semantically matches the ground-truth context. \
Here is the definition for Context: Boundary conditions that limit the scope of a sub-hypothesis. E.g., “for men over the age of 30”, “in Asia and Europe”. If the context applies to the full dataset, then the context is derived from the dataset_descrption. \
Here is the definition for Context: Boundary conditions that limit the scope of a sub-hypothesis. E.g., “for men over the age of 30”, “in Asia and Europe”. If the context applies to the full dataset, then the context is derived from the dataset_descrption. \
If the predicted context matches the gold context, return true, otherwise return false.
If both gold and predicted hypotheses are defined over the context of the full dataset, then also return true.
If both gold and predicted hypotheses are defined over the context of the full dataset, then also return true.
Here is the metadata for the task:
```json
{{
"gold_hypothesis": "{gold_hyp}",
"gold_context": "{gold_context}",
"predicted_hypothesis": "{pred_hyp}",
"predicted_context": "{pred_context}"
}}
```
Return your answer as a JSON object in the following format:
```json
{{
"match": true or false
}}
```"""
client = OpenAI()
output = get_response(client, prompt, model=model)
return output.get('match', False)
def is_matching_context(gold_hyp, gold_context, pred_hyp, pred_context, llm_used):
if gold_context == pred_context:
return True
if 'None' in [gold_context, pred_context]:
return False
return match_context_with_gpt(
gold_hyp, gold_context, pred_hyp, pred_context, model=llm_used
)
def run_eval_gold_vs_gen_NL_subhypo(
query,
gold_hypo,
gold_workflow,
gen_hypo,
gen_workflow,
dataset_meta,
llm_used,
context_score,
dataset_type,
use_column_metadata=True,
):
# GPT-4 based evaluation to evaluate generated hypothesis in terms of context, variables, relation
eval_rec = {
'query': query,
'HypoA': gold_hypo,
'WorkflowA': gold_workflow,
'HypoB': gen_hypo,
'WorkflowB': gen_workflow,
}
for dimension in ['var', 'rel']:
question, answer, score = ask_dimension_question(
query,
gold_hypo,
gold_workflow,
gen_hypo,
gen_workflow,
dataset_meta,
llm_used,
dimension=dimension,
dataset_type=dataset_type,
use_column_metadata=use_column_metadata,
)
eval_rec[dimension] = {'question': question, 'answer': answer, 'score': score}
eval_rec['context'] = context_score
eval_rec['accuracy_score'] = (
1.0
* eval_rec['context']['score']
* eval_rec['var']['score']['f1']
* eval_rec['rel']['score']
)
return eval_rec
def run_eval_gold_vs_gen_NL_hypo_workflow(
query,
gold_hypo,
gold_workflow,
gen_hypo,
gen_workflow,
dataset_meta,
llm_used,
dataset_type,
use_column_metadata=True,
):
# Input: Dataset Metadata, Query, Gold {Hg, Wg}, Predicted {Hp, Wp}
# Output: eval_rec json includes final_score
# Procedure:
# Dataset Metadata, Query, Gold {Hg, Wg}, Pred {Hg, Wg}
# Gold: [Hg1, Hg2] (compute on the fly) Hg1 is a NL form of subhypothesis
# Predicted: [Hp1, Hp2] (compute on the fly)
# Compute Intersection: [(Hg_i, Hp_j), …] # tuples of (gold,pred) that matched with context (do this w/o explicit extraction)
# # filter so that a gold context and a predicted context are only attached to one tuple
# Compute recall_context (programmatically)
# r_v_list = []
# For (Hg_i, Hp_j) in the intersection:
# With Hg_i, Hp_j in NL, ask GPT4 → #variables and #intersection and a paragraph explanation and programmatically calculate f1_v
# Hg_i, Hp_j in NL, ask GPT4 → matching score (0, 0.5 or 1) : A) very similar B) similar but general than HypoA C) different + explanation
# r_v_list ← f1_v * score_r
# accuracy_score = mean(r_v_list)
# score = [ recall_context * mean over predicted context(context_score * var_score *rel_score )]
# recall_context = 1.0 # COMMENT: never used
eval_rec = {
'query': query,
'HypoA': gold_hypo,
'WorkflowA': gold_workflow,
'HypoB': gen_hypo,
'WorkflowB': gen_workflow,
}
gold_sub_hypo_json = get_sub_hypotheses(
query=query,
hypo=gold_hypo,
workflow=gold_workflow,
dataset_meta=dataset_meta,
llm_used=llm_used,
dataset_type=dataset_type,
use_column_metadata=use_column_metadata,
)
if len(gold_sub_hypo_json['sub_hypo']) == 0:
gold_sub_hypo_json['sub_hypo'] = [
{
'text': gold_hypo,
'context': 'None',
'variables': [],
'relations': '',
'explanation': 'unable to segment',
}
]
print(f'gold_sub_hypo_json: {gold_sub_hypo_json}')
gen_sub_hypo_json = get_sub_hypotheses(
query=query,
hypo=gen_hypo,
workflow=gen_workflow,
dataset_meta=dataset_meta,
llm_used=llm_used,
dataset_type=dataset_type,
use_column_metadata=use_column_metadata,
)
if len(gen_sub_hypo_json['sub_hypo']) == 0:
gen_sub_hypo_json['sub_hypo'] = [
{
'text': gen_hypo,
'context': 'None',
'variables': [],
'relations': '',
'explanation': 'unable to segment',
}
]
print(f'gen_sub_hypo_json: {gen_sub_hypo_json}')
eval_rec['gold_sub_hypo'] = gold_sub_hypo_json
eval_rec['gen_sub_hypo'] = gen_sub_hypo_json
gold_subh_covered = []
gen_subh_to_gold_subh = dict()
gen_gold_subh_to_context = dict()
for p_id, gen_subh in enumerate(gen_sub_hypo_json['sub_hypo']):
gen_subh_to_gold_subh[p_id] = -1
for g_id, gold_subh in enumerate(gold_sub_hypo_json['sub_hypo']):
if g_id in gold_subh_covered:
continue
# match context
context_bool = is_matching_context(
gold_subh['text'],
gold_subh.get('context', ''),
gen_subh['text'],
gen_subh.get('context', ''),
llm_used,
)
if context_bool:
context_score = 1.0
else:
context_score = 0.0
if context_score == 1.0: # match only when context_score = 1.0
gen_subh_to_gold_subh[p_id] = g_id
gold_subh_covered.append(g_id)
gen_gold_subh_to_context[f'P{p_id}||G{g_id}'] = {
'question': f"""Comapring: GoldH: {gold_subh["text"]}, GoldC: {gold_subh['context']}\nGenH: {gen_subh['text']}, GenC: {gen_subh['context']}""",
'answer': context_bool,
'score': context_score,
}
break
print(f'gen_subh_to_gold_subh: {gen_subh_to_gold_subh}')
eval_rec['gen_subh_to_gold_subh'] = gen_subh_to_gold_subh
eval_rec['gold_subh_covered'] = gold_subh_covered
matched_gold_gen_subh_evals = dict()
sum_accuracy_score = 0.0
for p_id, g_id in gen_subh_to_gold_subh.items():
if g_id >= 0:
key = f'P{p_id}||G{g_id}'
context_score = gen_gold_subh_to_context[key]
subh_eval_rec = run_eval_gold_vs_gen_NL_subhypo(
query,
gold_hypo,
gold_workflow,
gen_hypo,
gen_workflow,
dataset_meta,
llm_used,
context_score,
dataset_type=dataset_type,
use_column_metadata=use_column_metadata,
)
sum_accuracy_score += subh_eval_rec['accuracy_score']
matched_gold_gen_subh_evals[key] = subh_eval_rec
eval_rec['matched_gold_gen_subh_evals'] = matched_gold_gen_subh_evals
eval_rec['recall_context'] = (
len(gold_subh_covered) / len(gold_sub_hypo_json['sub_hypo'])
if len(gold_sub_hypo_json['sub_hypo'])
else 0.0
)
mean_accuracy_score = (
sum_accuracy_score / len(gen_subh_to_gold_subh)
if len(gen_subh_to_gold_subh)
else 0.0
)
eval_rec['mean_accuracy_score'] = mean_accuracy_score
final_score = eval_rec['recall_context'] * mean_accuracy_score
eval_rec['final_score'] = final_score
print(f'eval_rec: {json.dumps(eval_rec, indent=2)}')
return eval_rec

View File

@@ -0,0 +1,64 @@
import os
import sys
import time
from openai import OpenAI
from tenacity import (
retry,
stop_after_attempt, # type: ignore
wait_random_exponential, # type: ignore
)
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
Model = Literal['gpt-4', 'gpt-3.5-turbo', 'text-davinci-003']
OpenAI.api_key = os.getenv('OPENAI_API_KEY')
OPENAI_GEN_HYP = {
'temperature': 0,
'max_tokens': 250,
'top_p': 1.0,
'frequency_penalty': 0,
'presence_penalty': 0,
}
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def run_chatgpt_query_multi_turn(
messages,
model_name='gpt-4-turbo', # pass "gpt4" for more recent model output
max_tokens=256,
temperature=0.0,
json_response=False,
):
response = None
num_retries = 3
retry = 0
while retry < num_retries:
retry += 1
try:
client = OpenAI()
if json_response:
response = client.chat.completions.create(
model=model_name,
response_format={'type': 'json_object'},
messages=messages,
**OPENAI_GEN_HYP,
)
else:
response = client.chat.completions.create(
model=model_name, messages=messages, **OPENAI_GEN_HYP
)
break
except Exception as e:
print(e)
print('GPT error. Retrying in 2 seconds...')
time.sleep(2)
return response

View File

@@ -0,0 +1,190 @@
import json
def OPENAI_TOPIC_GEN_MESSAGES(n=10):
return [
{
'role': 'system',
'content': 'You are a helpful assistant who is not talkative. You only respond with the exact answer to a query without additional conversation.',
},
{
'role': 'user',
'content': f'Given `n`, come up with a list of `n` distinct topics and their descriptions. The topics can be absolutely anything. Be as creative as possible. Return your answer as a JSON object. \n\nFor example, for `n`=3, a valid answer might be:\n```json\n{{"topics": [\n {{"id": 1, "topic": "cooking", "description": "Related to recipes, ingredients, chefs, etc."}},\n {{"id": 2, "topic": "sports", "description": "Related to players, stadiums, trophies, etc."}},\n {{"id": 3, "topic": "antiquing", "description": "Related to unique items, history, etc."}}\n]}}```\n\nNow, give me a list for `n`={n}. Remember, pick diverse topics from everything possible. No consecutive topics should be broadly similar. Directly respond with the answer JSON object.',
},
]
OPENAI_GEN_HYP = {
'temperature': 1.0,
'max_tokens': 4096,
'top_p': 1.0,
'frequency_penalty': 0,
'presence_penalty': 0,
}
def OPENAI_SEMANTICS_GEN_MESSAGES(dependent, relationship, domain, domain_desc):
return [
{
'role': 'system',
'content': 'You are a helpful assistant who is not talkative. You only respond with the exact answer to a query without additional conversation.',
},
{
'role': 'user',
'content': f'Given the true relationship in a dataset and a given domain, your task is to come up with an interpretation of some real-world concepts that the relationship could be modeling from the provided domain. It\'s okay to be wrong, but suggest something reasonable. Try as much as possible to make sure that the TARGET is actually derivable from the other variables. Give your answer as a JSON object. Here\'s an example:\n\nRelationship for x2 = "(96.4 * x1 ** 3) + (88.72 * x5 ** 2) + (81.96 * x6 ** -2) + (28.13 * x3) + (97.0) + (0 * x4)"\nDomain="Sales"\nDomain description="Related to product distribution, revenues, marketing, etc."\n\nBased on this, the following real-world concepts might be applicable:\n```json\n{{\n "dependent": "x2",\n "relationship": "(96.4 * x1 ** 3) + (88.72 * x5 ** 2) + (81.96 * x6 ** -2) + (28.13 * x3) + (97.0) + (0 * x4)",\n "domain": "Sales",\n "trends": {{\n "x1": "Positive, cubic factor",\n "x2": "TARGET",\n "x3": "Positive, linear factor",\n "x4": "No relation",\n "x5": "Positive quadratic factor",\n "x6": "Positive, inverse quadratic factor"\n }},\n "interpretation": {{\n "x2": {{"description": "Volume of product sales by area", "name": "sales_area", "is_target": true}},\n "x1": {{"description": "Population by area", "name": "pop_area"}},\n "x3": {{"description": "Advertising spending", "name": "ad_spend"}},\n "x4": {{"description": "Gender ratio of marketing team", "name": "gdr_ratio_mkt_team"}},\n "x5": {{"description": "Intensity of marketing campaign", "name": "mkt_intensity"}}\n }},\n "x6": {{"description": "Distance to distribution center", "name": "dist_to_distr_ctr"}}\n}}```\n\nHere\'s a new test question:\nRelationship for {dependent} = "{relationship}"\nDomain = "{domain}"\nDomain description="{domain_desc}"\n\nRespond only with the answer JSON. Make sure that you do not forget to include the TARGET variable in the interpretation object.',
},
]
def OPENAI_SEMANTICS_GEN_W_MAP_MESSAGES(
dependent, relationship, domain, domain_desc, mapping
):
return [
{
'role': 'system',
'content': 'You are a helpful assistant who is not talkative. You only respond with the exact answer to a query without additional conversation.',
},
{
'role': 'user',
'content': f'Given a partial mapping from variables to real-world concepts and a true relationship in a dataset, your task is to come up with an interpretation of real-world concepts for the variables without any assigned mapping (those starting with x). Suggest something reasonable. The dependent variable must be derivable only from the other variables in the dependent relationship. Give your answer as a JSON object. Here\'s an example:\n\nExample partial mapping and relationship:\n```json\n{{\n "domain": "Sales",\n "domain_description": "Related to product distribution, revenues, marketing, etc.",\n "variable_mapping": {{\n "x1": {{"description": "Population by area", "name": "pop_area"}},\n "x2": {{"description": "Volume of product sales by area", "name": "sales_area"}},\n "x4": {{"description": "Gender ratio of marketing team", "name": "gdr_ratio_mkt_team"}},\n "x6": {{"description": "Distance to distribution center", "name": "dist_to_distr_ctr"}}\n }},\n "dependent_variable": "sales_area",\n "dependent_relationship": "(96.4 * pop_area ** 3) + (88.72 * x5 ** 2) + (81.96 * dist_to_distr_ctr ** -2) + (28.13 * x3) + (97.0)"\n}}```\nBased on this, an example answer would be:\n```json\n{{\n "dependent_variable": "sales_area",\n "missing_mapping": ["x3", "x5"],\n "trends": {{\n "x3": "Positive, linear factor",\n "x5": "Positive quadratic factor"\n }},\n "interpretation": {{\n "x3": {{"description": "Advertising spending", "name": "ad_spend"}},\n "x5": {{"description": "Intensity of marketing campaign", "name": "mkt_intensity"}}\n }}\n}}```\n\nHere\'s a new test question:\n```json\n{{\n "domain": "{domain}",\n "domain_description": "{domain_desc}",\n "variable_mapping": {json.dumps(mapping, indent=2)},\n "dependent_variable": "{dependent}",\n "dependent_relationship": "{relationship}"\n}}```\nRespond only with the answer JSON.',
},
]
def OPENAI_SEMANTICS_GEN_SUMMARY_MESSAGES(dataset):
return [
{
'role': 'system',
'content': 'You are a helpful assistant who is not talkative. You only respond with the exact answer to a query without additional conversation.',
},
{
'role': 'user',
'content': f'Given the following descriptions of the columns of a dataset, your task is to come up with a natural language overview of the dataset, which should include (1) what the dataset is about, (2) how the data was collected, (3) when the data was collected, and (3) for what purpose the data was collected. Be specific and creative.\n\nExample dataset:\n```json\n{{ \n "dataset": {{ \n "x6": {{"description": "Ancient artifact significance score", "name": "artifact_significance_score", "is_target": true}},\n "x1": {{"description": "Distance to ancient city center", "name": "dist_to_ancient_city_ctr"}},\n "x2": {{"description": "Quantity of discovered relics", "name": "relic_discovery_qty"}},\n "x3": {{"description": "Years since last archaeological expedition", "name": "years_since_exp"}},\n "x4": {{"description": "Number of artifacts in excavation site", "name": "artifact_qty"}},\n "x5": {{"description": "Soil fertility coefficient", "name": "soil_fertility_coef"}},\n "x7": {{"description": "Distance to ancient burial grounds", "name": "dist_to_burial_grounds"}},\n "x8": {{"description": "Population estimate of ancient civilization", "name": "ancient_civilization_pop_estimate"}},\n "x9": {{"description": "Temperature variation in excavation region", "name": "temp_variation"}}\n }}\n}}```\nExample description:\nThis dataset is about archaeological explorations and findings linked to ancient civilizations. The data was collected in the form of field metrics during various archaeological expeditions during the late mid-20th century. The purpose of the data collection is to evaluate the significance of ancient artifacts discovered during excavations.\n\nHere is a new test dataset.\n{json.dumps(dataset, indent=2)}\nProvide only the description.',
},
]
def OPENAI_GEN_HYPO_MESSAGES(dataset):
return [
{
'role': 'system',
'content': 'You are a helpful assistant who is not talkative. You only respond with the exact answer to a query without additional conversation.',
},
{
'role': 'user',
'content': f'Given a dataset with its descriptions and the true functional relationship between its variables, your task is to generate 3 levels of hypotheses for the stated relationship in plain English. The three levels are "broad", "medium" and "narrow". Make sure that the hypotheses sound natural. *Only include concepts for variables that are present in the provided functional relationship.* Give your answer as a JSON.\n\nFor example, an example dataset might be the following:\n```json\n{{\n "domain": "cybersecurity",\n "summary": "This dataset is about measuring cybersecurity threats in a system. The data was collected by monitoring various cybersecurity metrics in a network environment. The purpose of the data collection is to assess and predict potential cybersecurity risks and vulnerabilities.",\n "variables": [\n {{\n "description": "Level of cybersecurity threat",\n "name": "cybersecurity_threat",\n "is_target": true\n }},\n {{\n "description": "Number of failed login attempts",\n "name": "failed_login_attempts"\n }},\n {{\n "description": "Amount of encrypted data",\n "name": "encrypted_data"\n }},\n {{\n "description": "Frequency of software updates",\n "name": "software_updates"\n }},\n {{\n "description": "Number of antivirus software installed",\n "name": "antivirus_software"\n }},\n {{\n "description": "Quality of firewall protection",\n "name": "firewall_quality"\n }}\n ],\n "relationship": {{\n "dependent": "cybersecurity_threat",\n "relation": "-53.5*encrypted_data**2 - 53.85*failed_login_attempts**2 + 67.75*firewall_quality - 92.16 - 36.68/software_updates**3"\n }}\n}}```\nGiven this dataset, the following is a valid answer:\n```json\n{{\n "broad": {{\n "instruction": "Be vague. Only indicate which concepts might be related but not how they are related",\n "hypothesis": "Threat to cybersecurity is influenced by several factors including the amount of encrypted data, the number of failed login attempts, the quality of the firewall, as well as how often the software is updated."\n }},\n "medium": {{\n "instruction": "Be slightly more specific. For each factor, indicate carefully whether it positively or negatively affects the relationship, but do not indicate what the exponent is.",\n "hypothesis": "Cybersecurity threat tends to decrease with the amount of data encryption, the number of failed login attempts, as well as the frequency of software updates to some extent, while improvement in the firewall quality has a positive effect."\n }},\n "narrow": {{\n "instruction": "Be specific. Communicate the concepts, whether there is a positive or negative effect (be careful), and the meaning of the exponent",\n "hypothesis": "The threat to cybersecurity interacts in a complex manner with various factors. As the amount of encrypted data increases, there is a quadratic decrease in threat. Similarly for the number of failed login attempts, there is a negative quadratic relationship. The quality of the firewall protection on the other hand demonstrates a positive and linear relationship. Finally, the frequency of software updates has an inverse cubic relationship to the threat."\n }},\n}}\n```\n\nBased on this, provide an answer for the following test dataset:\n```json\n{dataset}```\nRespond only with a JSON.',
},
]
def create_prompt(usr_msg):
return [
{
'role': 'system',
'content': 'You are a helpful assistant who is not talkative. You only respond with the exact answer to a query without additional conversation.',
},
{'role': 'user', 'content': usr_msg},
]
def get_response(client, prompt, max_retry=5, model='gpt-3.5-turbo', verbose=False):
n_try = 0
while n_try < max_retry:
response = client.chat.completions.create(
model=model, messages=create_prompt(prompt), **OPENAI_GEN_HYP
)
# COMMENT: changed from
# response.choices[0].message.content.strip().strip('```json').strip('```')
content = response.choices[0].message.content
cleaned_content = content.split('```json')[1].split('```')[0].strip()
output = cleaned_content
try:
response_json = json.loads(output)
return response_json
except ValueError:
if verbose:
print(f'Bad JSON output:\n\n{output}')
n_try += 1
if n_try < max_retry:
if verbose:
print('Retrying...')
else:
if verbose:
print('Retry limit reached')
return None
def get_code_fix(
client, code, error, max_retry=5, model='gpt-3.5-turbo', verbose=False
):
prompt = f"""\
Given the following code snippet and error message, provide a single-line fix for the error. \
Note that the code is going to be executed using python `eval`. \
The code should be executable and should not produce the error message. Be as specific as possible.
Here's the code and the error:
{{
"code": "{code}",
"error": "{error}"
}}
Return only a JSON object with the fixed code in the following format:
```json
{{
"fixed_code": "..."
}}"""
response = get_response(
client, prompt, max_retry=max_retry, model=model, verbose=verbose
)
return response
def get_new_hypothesis(
client, target, old, expr, cols, model='gpt-3.5-turbo', verbose=False
):
prompt = f"""\
Given a target column from a dataset, a pandas expression to derive the column from existing columns, a list of \
existing columns, and a previously written hypothesis text, carefully check if the hypothesis text is consistent with \
the pandas expression or not. If it is consistent, simply return the hypothesis as it is. If it is not consistent, \
provide a new natural language hypothesis that is consistent with the pandas expression using only the provided \
information. Be specific.
Here's the information:
```json
{{
"target_column": "{target}",
"pandas_expression": "{expr}",
"existing_columns": {json.dumps(cols, indent=4)}
"old_hypothesis": "{old}",
}}```
Give your answer as a new JSON with the following format:
```json
{{
"hypothesis": "..."
}}"""
response = get_response(client, prompt, model=model, verbose=verbose)
return response
def replace_variable(client, expr, old, new, model='gpt-3.5-turbo', verbose=False):
prompt = f"""\
Given a pandas "expression", replace mentions of the "old" column with its "new" value such that the resultant \
expression is equivalent to the original expression.
Here's the information:
```json
{{
"expression": "{expr}",
"old": "{old}",
"new": "{new}"
}}```
Give your answer as a new JSON with the following format:
```json
{{
"new_expression": "..."
}}"""
response = get_response(client, prompt, model=model, verbose=verbose)
return response

View File

@@ -0,0 +1,151 @@
common_hypothesis_features = [
'1-2 sentences',
'surprising finding',
'includes numeric concepts',
'includes categorical concepts',
'includes binary concepts',
]
hypothesis_features = [
['requires within-cluster analysis'],
['requires across-cluster analysis'],
['corresponds to a polynomial relationship of some columns'],
['corresponds to a ratio between some columns'],
['requires temporal analysis'],
['relationship is based on descriptive statistics of some columns'],
['requires concepts based on percentage or percentiles'],
['relationship is only applicable to one cluster in the data and not the others'],
]
column_features = [
[
'must have one target column',
'must have quantifiable columns',
'must have a few categorical columns',
'make sure the categorical column values do not contain special characters',
'include a few distractor columns',
]
]
common_pandas_features = [
'must be executable using python `eval` to create the target column in variable `df` (pandas dataframe)',
"for e.g., df['A']**2 + 3*df['B'] + 9, np.where(df['A'] > 3, 'Yes', 'No'), etc.",
'variables in pandas_expression must be from the existing columns listed above',
'variables in pandas_expression must NOT contain the target column itself',
]
pandas_features = [
['expression is a quadratic polynomial'],
['expression is a cubic polynomial'],
['expression is a ratio of existing columns'],
['expression is derived through logical combination of existing columns'],
# workflow
]
pandas_features = [common_pandas_features + p for p in pandas_features]
common_derived_features = [
'1-2 sentences',
'includes numeric concepts',
'includes categorical concepts',
'includes binary concepts',
]
derived_features = [common_derived_features + h for h in hypothesis_features]
hypothesis_features = [common_hypothesis_features + h for h in hypothesis_features]
PROMPT_HYP = """\
Given a dataset topic and description, generate an interesting hypothesis based on \
the provided instructions. Be creative and come up with an unusual finding.
```json
{
"topic": "%s",
"description": "%s",
"hypothesis_features": %s,
"hypothesis": "..."
}```
Give your answer as a new JSON with the following format:
```json
{
"hypothesis": "..."
}
```"""
PROMPT_COL = """\
Given a dataset topic, its description, and a true hypothesis that can be determined from it, \
generate a list of valid columns based on the provided instructions.
```json
{
"topic": "%s",
"description": "%s",
"hypothesis": "%s",
"column_instructions": %s,
"columns": [
{
"col_name": "...", # should be an "_"-separated string
"description": "...",
"data_type": "...", # should be executable using python's `eval` function. E.g., str, float, int, bool
"data_range": {...}, # should be either {"min": ..., "max": ...} or {"values": [...]}
"is_distractor": true/false, # boolean indicating whether this is a distractor that could cause confusion during data analysis
"is_target": true/false # boolean indicating whether this is the target variable for the hypothesis; at least one column should be the target
},
...
],
"pandas_instructions": %s,
"pandas_equation_for_hypothesis": {
"target_col": "...",
"target_col_type": "...",
"target_col_range": {...},
"independent_cols_in_pandas_expression": [], # list of column names that will be used to derive the target column
"pandas_expression": "..." # expression to derive df[target_col] using df[ind_col1], df[ind_col2], etc.
}
}```
Give your answer as a new JSON with the "columns" and "pandas_equation_for_hypothesis" keys filled using the following format:
```json
{
"columns": [...],
"pandas_equation_for_hypothesis": {...}
}
```"""
PROMPT_DER = """\
Given a dataset topic, description, a true hypothesis that can be determined from the data, \
and a target column from the dataset, generate a hypothesis for the target column using new independent columns not present in the existing columns.
```json
{
"topic": "%s",
"description": "%s",
"hypothesis": "%s",
"existing_columns": %s,
"target_column": "%s",
"new_to_target_instructions": %s,
"new_to_target_hypothesis": "...", # describe a relationship between new columns that explains the target column
"new_columns_for_target": [ # do not repeat any of the existing columns in the dataset
{
"col_name": "...", # should be an "_"-separated string
"description": "...",
"data_type": "...", # should be executable using python's `eval` function. E.g., str, float, int, bool
"data_range": {...}, # should be either {"min": ..., "max": ...} or {"values": [...]}
},
...
],
"pandas_instructions": %s,
"pandas_equation_for_new_to_target_hypothesis": {
"target_col": "...",
"target_col_type": "...",
"target_col_range": {...},
"independent_cols_in_pandas_expression": [], # list of column names from new_columns_for_target that will be used to derive target_col
"pandas_expression": "..." # expression to derive df[target_col] using df[ind_col1], df[ind_col2], etc.
}
}```
Give your answer as a new JSON with the "new_to_target_hypothesis", "new_columns_for_target", and \
"pandas_equation_for_new_to_target_hypothesis" keys filled using the following format:
```json
{
"new_to_target_hypothesis": "...",
"new_columns_for_target": [...],
"pandas_equation_for_new_to_target_hypothesis": {...}
}
```"""

View File

@@ -0,0 +1,52 @@
workflow_summary_markers = [
'WORKFLOW SUMMARY',
'WORKFLOW_SUMMARY',
'WORKFLOW-SUMMARY',
'Workflow Summary',
]
final_answer_markers = [
'FINAL ANSWER',
'FINAL_ANSWER',
'FINAL-ANSWER',
'Final Answer',
'Scientific Hypothesis',
'Hypothesis',
]
next_agent_markers = [
'NEXT AGENT',
'NEXT-AGENT',
'NEXT_AGENT',
'FEEDBACK',
]
def extract_between(content, start_markers, end_markers=None):
for marker in start_markers:
if marker in content:
result = content.split(marker, 1)[1]
if end_markers:
for end_marker in end_markers:
if end_marker in result:
result = result.split(end_marker, 1)[0]
return result
return ''
def extract_gen_hypo_from_logs(content: str):
error = ''
gen_workflow = extract_between(
content, workflow_summary_markers, final_answer_markers
)
if not gen_workflow:
error += 'No Workflow Summary found in the line. | '
gen_hypothesis = extract_between(content, final_answer_markers, next_agent_markers)
if not gen_hypothesis:
error += 'No Final Answer in the line.'
return gen_hypothesis, gen_workflow, error

View File

@@ -0,0 +1,492 @@
import asyncio
import json
import os
import git
import pandas as pd
from evaluation.discoverybench.eval_utils.eval_w_subhypo_gen import (
run_eval_gold_vs_gen_NL_hypo_workflow,
)
from evaluation.discoverybench.eval_utils.response_parser import (
extract_gen_hypo_from_logs,
)
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
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 AgentFinishAction, CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
EVALUATION_LLM = 'gpt-4-1106-preview'
DATA_FILES = {}
LIBRARIES = [
'pandas',
'numpy',
'scipy',
'matplotlib',
'seaborn',
'scikit-learn',
'statsmodels',
]
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n'
}
def get_config(
metadata: EvalMetadata,
) -> AppConfig:
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='eventstream',
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='python:3.12-bookworm',
enable_auto_lint=True,
use_host_network=False,
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
agent_config = AgentConfig(
function_calling=False,
codeact_enable_jupyter=True,
codeact_enable_browsing_delegate=True,
)
config.set_agent_config(agent_config)
return config
def get_dv_query_for_real(
datasets, question, domain_knowledge=None, workflow_tags=None
):
"""
Prepare a structured query for the agent to execute on the specified datasets.
This function constructs a query by compiling metadata from the provided datasets, along with any relevant domain knowledge and workflow tags.
Args:
datasets: List of datasets
question: Query to be answered
domain_knowledge: Domain knowledge if any
workflow_tags: Workflow tags if any
Returns:
query_to_dv: Query to be run on the dataset
dataset_meta: Metadata of the dataset
"""
dataset_meta = ''
for dataset_metadata in datasets:
dataset_meta += 'Dataset name: ' + dataset_metadata['name']
dataset_meta += 'Dataset description: ' + dataset_metadata['description']
dataset_meta += '\nBrief description of columns: '
for col in dataset_metadata['columns']['raw']:
dataset_meta += col['name'] + ': ' + col['description'] + ', '
query_to_dv = dataset_meta
query_to_dv += f'\nQuery: {question}'
if domain_knowledge:
query_to_dv += (
'\nAdditionally, we provide some hints that might be useful to solve the task. Domain Knowledge: \n'
+ domain_knowledge
+ '.\n'
)
if workflow_tags:
query_to_dv += 'The meta tags are: ' + workflow_tags + '.\n'
query_to_dv += (
'In the final answer, please write down a scientific hypothesis in '
'natural language, derived from the provided dataset, clearly stating the '
'context of hypothesis (if any), variables chosen (if any) and '
'relationship between those variables (if any) including any statistical significance.'
'Also generate a summary of the full workflow starting from data loading that led to the final answer as WORKFLOW SUMMARY:'
)
# Run the NL query through datavoyager
return query_to_dv, dataset_meta
def initialize_runtime(runtime: Runtime, data_files: list[str]):
"""
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
action = CmdRunAction(command='mkdir -p /workspace')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
action = CmdRunAction(command='cd /workspace')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
for file in data_files:
runtime.copy_to(
file,
'/workspace',
)
for lib in LIBRARIES:
action = CmdRunAction(command=f'pip install {lib}')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
def get_last_agent_finish_action(state: State) -> AgentFinishAction:
for event in reversed(state.history):
if isinstance(event, AgentFinishAction):
return event
return None
def get_last_message_action(state: State) -> MessageAction:
for event in reversed(state.history):
if isinstance(event, MessageAction):
return event
return None
def complete_runtime(state: State):
last_agent_finish_action = get_last_agent_finish_action(state)
last_agent_message_action = get_last_message_action(state)
if last_agent_finish_action is not None:
final_message_1 = last_agent_finish_action.thought
gen_hypo_1, gen_workflow_1, error_1 = extract_gen_hypo_from_logs(
final_message_1
)
else:
gen_hypo_1, gen_workflow_1, error_1 = '', '', ''
if last_agent_message_action is not None:
final_message_2 = last_agent_message_action.content
gen_hypo_2, gen_workflow_2, error_2 = extract_gen_hypo_from_logs(
final_message_2
)
else:
gen_hypo_2, gen_workflow_2, error_2 = '', '', ''
if gen_hypo_1 and gen_hypo_2:
test_result = {
'gen_hypo': last_agent_finish_action.thought
if last_agent_finish_action
else last_agent_message_action.content,
'gen_workflow': '',
'error': '',
}
return test_result
test_result = {
'gen_hypo': gen_hypo_1 if gen_hypo_1 else gen_hypo_2,
'gen_workflow': gen_workflow_1 if gen_workflow_1 else gen_workflow_2,
'error': error_1 if error_1 else error_2,
}
return test_result
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
):
"""
Process and evaluate a single instance of the dataset.
This function executes the OpenHands agent
for a specific instance of the dataset. It retrieves
the agent's results and evaluates them against the gold
hypothesis.
Args:
instance: A single row of the dataset
metadata: Metadata for the evaluation
reset_logger: Whether to reset the logger
Returns:
output: EvalOutput object
"""
config = get_config(metadata)
# use a session id for concurrent evaluation
sid = 'ID_' + str(instance.instance_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, instance.instance_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {instance.instance_id}.')
problem_statement, dataset_metadata = get_dv_query_for_real(
datasets=instance.datasets,
question=instance.query,
domain_knowledge=instance.domain_knowledge,
workflow_tags=instance.workflow_tags,
)
# Prepare instruction
instruction = (
f'You are a discovery agent who can execute a python code only once to answer a query based on one or more datasets. The datasets will be present in the current directory.\n\n'
'Environment has been set up for you to start working. You may assume all necessary tools and datasets are installed.\n\n'
'# Problem Statement\n'
f'{problem_statement}\n\n'
)
instruction += (
'IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\n'
'You should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\n'
'You SHOULD INCLUDE PROPER INDENTATION in your edit commands.\n'
)
# NOTE: You can actually set slightly different instruction for different agents
instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class]
# Here's how you can run the agent (similar to the `main` function) and get the final task state
runtime = create_runtime(config, sid=sid)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance.data_files)
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=MessageAction(content=instruction),
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
metadata.agent_class
),
)
)
if state is None:
raise ValueError('State should not be None.')
metrics = state.metrics.get() if state.metrics else None
test_result = complete_runtime(state)
# history is now available as a stream of events, rather than list of pairs of (Action, Observation)
# for compatibility with the existing output format, we can remake the pairs here
# remove when it becomes unnecessary
histories = compatibility_for_eval_history_pairs(state.history)
# DiscoveryBench Evaluation
eval_rec = run_eval_gold_vs_gen_NL_hypo_workflow(
query=instance.query,
gold_hypo=instance.gold_hypo,
gold_workflow='',
gen_hypo=test_result['gen_hypo'],
gen_workflow='',
dataset_meta=instance.dataset_metadata,
llm_used=EVALUATION_LLM,
dataset_type='real',
)
test_result['eval_rec'] = eval_rec
output = EvalOutput(
instance_id=str(instance.instance_id),
instruction=instruction,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
test_result=test_result,
)
return output
def update_csv_name(name):
name = name.replace('-', '_')
if 'meta_regression' in name:
name = name.replace('meta_regression', 'meta-regression')
if 'ML_enabled' in name:
name = name.replace('ML_enabled', 'ML-enabled')
return name
def list_csv_files(list_of_datasets):
res = []
for ele in list_of_datasets:
for key, value in ele.items():
if key == 'name':
csv_file_name = update_csv_name(value)
res.append(DATA_FILES[csv_file_name])
return res
def create_dataset(repo_location: str, split: str = 'test'):
"""
Create a dataset from the discoverybench repository
by walking through the repository and extracting metadata
from the metadata_{}.json files
Args:
repo_location: Location of the repository
split: Split of the dataset to use
Returns:
df: DataFrame containing the dataset instances
"""
data_dict = {}
data_location = os.path.join(repo_location, 'discoverybench', 'real', split)
answer_key_location = os.path.join(repo_location, 'eval', 'answer_key_real.csv')
idx = 0
for root, dirs, files in os.walk(data_location):
for file in files:
if file.endswith('.json'):
if 'metadata' in file:
metadata = json.load(open(os.path.join(root, file)))
dataset = root.split('/')[-1]
metadata_id = file.split('_')[-1].split('.')[0]
domain = metadata.get('domain', '')
domain_knowledge = metadata.get('domain_knowledge', '')
workflow_tags = metadata.get('workflow_tags', '')
datasets = metadata.get('datasets', [])
queries = metadata.get('queries', [])
gold_workflow = metadata.get('workflow')
# loop through queries list to get queries
# and each query has qid; add that to dictionary
for query in queries[0]:
qid = query.get('qid', '')
data = {
'dataset': dataset,
'metadata_id': metadata_id,
'qid': qid,
'domain': domain,
'domain_knowledge': domain_knowledge,
'workflow_tags': workflow_tags,
'datasets': datasets,
'question_type': query['question_type'],
'query': query['question'],
'gold_workflow': gold_workflow,
'dataset_metadata': metadata,
}
data_dict[idx] = data
idx += 1
if file.endswith('.csv'):
DATA_FILES[file] = os.path.join(root, file)
if file.endswith('.txt'):
DATA_FILES[file] = os.path.join(root, file)
df = pd.DataFrame.from_dict(data_dict, orient='index')
df['instance_id'] = df.index
df['data_files'] = df['datasets'].apply(lambda x: list_csv_files(x))
answer_key = pd.read_csv(answer_key_location)
answer_key = answer_key.rename(
columns={
'metadataid': 'metadata_id',
'query_id': 'qid',
'gold_hypothesis': 'gold_hypothesis',
}
)
df['qid'] = df['qid'].astype(int)
df['metadata_id'] = df['metadata_id'].astype(int)
answer_key['qid'] = answer_key['qid'].astype(int)
answer_key['metadata_id'] = answer_key['metadata_id'].astype(int)
df = pd.merge(df, answer_key, on=['dataset', 'metadata_id', 'qid'], how='left')
return df
if __name__ == '__main__':
args = parse_arguments()
# clone git repositor for csv files
repo_url = 'https://github.com/allenai/discoverybench.git'
repo_location = 'git-discoverybench-allenai'
try:
git.Repo.clone_from(repo_url, repo_location)
except git.exc.GitCommandError:
print('Repository already exists')
dataset = create_dataset(repo_location)
# check if there is any empty csv_file
if dataset['data_files'].isnull().any():
raise ValueError('Some csv files are missing.')
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,
'discoverybench-python',
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,46 @@
#!/bin/bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
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 CodeActAgent"
AGENT="CodeActAgent"
fi
get_agent_version
echo "AGENT: $AGENT"
echo "AGENT_VERSION: $AGENT_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
COMMAND="poetry run python evaluation/discoverybench/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations 10 \
--max-chars 10000000 \
--eval-num-workers $NUM_WORKERS \
--eval-note $AGENT_VERSION"
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

@@ -12,6 +12,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -166,7 +167,7 @@ def process_instance(
model_answer_raw = ''
# get the last message or thought from the agent
for event in state.history.get_events(reverse=True):
for event in reversed(state.history):
if event.source == 'agent':
if isinstance(event, AgentFinishAction):
model_answer_raw = event.thought
@@ -203,7 +204,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -10,6 +10,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -32,7 +33,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have completed the request, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have completed the request, please finish the interaction using the "finish" tool.\n'
}
@@ -101,7 +102,8 @@ def process_instance(
raise ValueError('State should not be None.')
# retrieve the last message from the agent
model_answer_raw = state.history.get_last_agent_message()
last_agent_message = state.get_last_agent_message()
model_answer_raw = last_agent_message.content if last_agent_message else ''
# attempt to parse model_answer
ast_eval_fn = instance['ast_eval']
@@ -114,7 +116,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
output = EvalOutput(
instance_id=instance_id,

View File

@@ -28,6 +28,7 @@ from datasets import load_dataset
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -86,11 +87,10 @@ def gpqa_codeact_user_response(
msg = (
'Please continue working on the task on whatever approach you think is suitable.\n'
'Feel free to use all tools for calculations and solving the problem, and web-search for finding relevant facts during the process if needed\n'
'If you have finished reporting the answer in the expected format, (and only once that is done), please run the following command to submit: <execute_bash> exit </execute_bash>.\n'
'If you have finished reporting the answer in the expected format, (and only once that is done), please use the "finish" tool to finish the interaction.\n'
'Again you are being told a million times to first report the answer in the requested format (see again below for reference) before exiting. DO NOT EXIT WITHOUT REPORTING THE ANSWER FIRST.\n'
'That is, when you have decided on the answer report in the following format:\n'
f'{ACTION_FORMAT}\n'
'<execute_bash> exit </execute_bash>\n'
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP TO SOLVE THIS TASK.\n'
)
return msg
@@ -99,7 +99,7 @@ def gpqa_codeact_user_response(
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {'CodeActAgent': gpqa_codeact_user_response}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': '\n\n SUPER IMPORTANT: When you think you have solved the question, first report it back to the user in the requested format. Only once that is done, in the next turn, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': '\n\n SUPER IMPORTANT: When you think you have solved the question, first report it back to the user in the requested format. Only once that is done, in the next turn, please finish the interaction using the "finish" tool.\n'
}
@@ -204,12 +204,11 @@ Additional Instructions:
- Do not try to solve the question in a single step. Break it down into smaller steps.
- You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.
- SUPER IMPORTANT: When you have reported the answer to the user in the requested format, (and only once that is done) in the next turn, please run the following command: <execute_bash> exit </execute_bash>.
- SUPER IMPORTANT: When you have reported the answer to the user in the requested format, (and only once that is done) in the next turn, please finish the interaction using the "finish" tool.
- Again you are being told a million times to first report the answer in the requested format (see again below for reference) before exiting. DO NOT EXIT WITHOUT REPORTING THE ANSWER FIRST.
That is, when you have decided on the answer report in the following format:
{ACTION_FORMAT}
<execute_bash> exit </execute_bash>
Again do not quit without reporting the answer first.
Ok now its time to start solving the question. Good luck!
@@ -244,7 +243,7 @@ Ok now its time to start solving the question. Good luck!
'C': False,
'D': False,
}
for event in state.history.get_events(reverse=True):
for event in reversed(state.history):
if (
isinstance(event, AgentFinishAction)
and event.source != 'user'
@@ -300,7 +299,7 @@ Ok now its time to start solving the question. Good luck!
instance_id=str(instance.instance_id),
instruction=instruction,
metadata=metadata,
history=state.history.compatibility_for_eval_history_pairs(),
history=compatibility_for_eval_history_pairs(state.history),
metrics=metrics,
error=state.last_error if state and state.last_error else None,
test_result={

View File

@@ -23,7 +23,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
```
{
"task_id": "Python/2",
"instruction": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n",
"instruction": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n",
"metadata": {
"agent_class": "CodeActAgent",
"model_name": "gpt-4",
@@ -38,10 +38,10 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
"id": 27,
"timestamp": "2024-05-22T20:57:24.688651",
"source": "user",
"message": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n",
"message": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n",
"action": "message",
"args": {
"content": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n",
"content": "Please fix the function in Python__2.py such that all test cases pass.\nEnvironment has been set up for you to start working. You may assume all necessary tools are installed.\n\n# Problem Statement\ndef truncate_number(number: float) -> float:\n return number % 1.0 + 1.0\n\n\n\n\n\n\ndef check(truncate_number):\n assert truncate_number(3.5) == 0.5\n assert abs(truncate_number(1.33) - 0.33) < 1e-6\n assert abs(truncate_number(123.456) - 0.456) < 1e-6\n\ncheck(truncate_number)\n\nIMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP.\nYou should NOT modify any existing test case files. If needed, you can add new test cases in a NEW file to reproduce the issue.\nYou SHOULD INCLUDE PROPER INDENTATION in your edit commands.\nWhen you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n",
"wait_for_response": false
}
},

View File

@@ -21,6 +21,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -74,7 +75,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have fixed the issue through code changes, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have fixed the issue through code changes, please finish the interaction using the "finish" tool.\n'
}
@@ -255,7 +256,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -13,6 +13,7 @@ from evaluation.utils.shared import (
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 (
@@ -55,18 +56,14 @@ def get_config(
workspace_base=None,
workspace_mount_path=None,
)
if metadata.llm_config.log_completions:
metadata.llm_config.log_completions_folder = os.path.join(
metadata.eval_output_dir, 'llm_completions', instance_id
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance_id
)
logger.info(
f'Logging LLM completions for instance {instance_id} to '
f'{metadata.llm_config.log_completions_folder}'
)
config.set_llm_config(metadata.llm_config)
)
agent_config = AgentConfig(
codeact_enable_jupyter=True,
codeact_enable_browsing_delegate=True,
codeact_enable_browsing=True,
codeact_enable_llm_editor=False,
)
config.set_agent_config(agent_config)
@@ -132,7 +129,7 @@ def process_instance(
# # result evaluation
# # =============================================
histories = [event_to_dict(event) for event in state.history.get_events()]
histories = [event_to_dict(event) for event in state.history]
test_result: TestResult = test_class.verify_result(runtime, histories)
metrics = state.metrics.get() if state.metrics else None

View File

@@ -0,0 +1,44 @@
from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestResult
from openhands.events.action import AgentFinishAction, MessageAction
from openhands.events.event import Event
from openhands.events.observation import AgentDelegateObservation
from openhands.runtime.base import Runtime
class Test(BaseIntegrationTest):
INSTRUCTION = 'Look at https://github.com/All-Hands-AI/OpenHands/pull/8, and tell me what is happening there and what did @asadm suggest.'
@classmethod
def initialize_runtime(cls, runtime: Runtime) -> None:
pass
@classmethod
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
# check if the "The answer is OpenHands is all you need!" is in any message
message_actions = [
event
for event in histories
if isinstance(
event, (MessageAction, AgentFinishAction, AgentDelegateObservation)
)
]
for event in message_actions:
if isinstance(event, AgentDelegateObservation):
content = event.content
elif isinstance(event, AgentFinishAction):
content = event.outputs.get('content', '')
elif isinstance(event, MessageAction):
content = event.content
else:
raise ValueError(f'Unknown event type: {type(event)}')
if (
'non-commercial' in content
or 'MIT' in content
or 'Apache 2.0' in content
):
return TestResult(success=True)
return TestResult(
success=False,
reason=f'The answer is not found in any message. Total messages: {len(message_actions)}. Messages: {message_actions}',
)

View File

@@ -8,6 +8,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -225,7 +226,7 @@ def process_instance(
raise ValueError('State should not be None.')
final_message = ''
for event in state.history.get_events(reverse=True):
for event in reversed(state.history):
if isinstance(event, AgentFinishAction):
final_message = event.thought
break
@@ -247,7 +248,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -16,6 +16,20 @@ Access with browser the above MiniWoB URLs and see if they load correctly.
./evaluation/miniwob/scripts/run_infer.sh llm.claude-35-sonnet-eval
```
### Run Inference on `RemoteRuntime` (experimental)
This is in limited beta. Contact Xingyao over slack if you want to try this out!
```bash
./evaluation/miniwob/scripts/run_infer.sh [model_config] [git-version] [agent] [note] [eval_limit] [num_workers]
# Example - This runs evaluation on BrowsingAgent for 125 instances on miniwob, with 2 workers running in parallel
export ALLHANDS_API_KEY="YOUR-API-KEY"
export RUNTIME=remote
export SANDBOX_REMOTE_RUNTIME_API_URL="https://runtime.eval.all-hands.dev"
./evaluation/miniwob/scripts/run_infer.sh llm.eval HEAD BrowsingAgent "" 125 2
```
Results will be in `evaluation/evaluation_outputs/outputs/miniwob/`
To calculate the average reward, run:

View File

@@ -23,7 +23,7 @@ if __name__ == '__main__':
data = json.loads(line)
actual_num += 1
total_cost += data['metrics']['accumulated_cost']
total_reward += data['test_result']
total_reward += data['test_result']['reward']
avg_reward = total_reward / total_num
print('Avg Reward: ', avg_reward)

View File

@@ -10,10 +10,13 @@ import pandas as pd
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
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 (
@@ -29,7 +32,10 @@ from openhands.events.action import (
CmdRunAction,
MessageAction,
)
from openhands.events.observation import CmdOutputObservation
from openhands.events.observation import (
BrowserOutputObservation,
CmdOutputObservation,
)
from openhands.runtime.base import Runtime
from openhands.runtime.browser.browser_env import (
BROWSER_EVAL_GET_GOAL_ACTION,
@@ -37,7 +43,12 @@ from openhands.runtime.browser.browser_env import (
)
from openhands.utils.async_utils import call_async_from_sync
SUPPORTED_AGENT_CLS = {'BrowsingAgent'}
SUPPORTED_AGENT_CLS = {'BrowsingAgent', 'CodeActAgent'}
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
'BrowsingAgent': 'Continue the task. IMPORTANT: do not talk to the user until you have finished the task',
}
def get_config(
@@ -47,25 +58,34 @@ def get_config(
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime='eventstream',
runtime=os.environ.get('RUNTIME', 'eventstream'),
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='xingyaoww/od-eval-miniwob:v1.0',
enable_auto_lint=True,
use_host_network=False,
browsergym_eval_env=env_id,
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=1800,
keep_runtime_alive=False,
timeout=120,
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(metadata.llm_config)
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,
) -> str:
) -> tuple[str, BrowserOutputObservation]:
"""Initialize the runtime for the agent.
This function is called before the runtime is used to run the agent.
@@ -85,8 +105,14 @@ def initialize_runtime(
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
goal = obs.content
# Run noop to get the initial browser observation (e.g., the page URL & content)
action = BrowseInteractiveAction(browser_actions='noop(1000)')
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 Initialization Fn {'-' * 50}")
return goal
return goal, obs
def complete_runtime(
@@ -117,7 +143,7 @@ def process_instance(
metadata: EvalMetadata,
reset_logger: bool = True,
) -> EvalOutput:
env_id = instance.id
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
@@ -129,7 +155,12 @@ def process_instance(
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
task_str = initialize_runtime(runtime)
task_str, obs = initialize_runtime(runtime)
task_str += (
f'\nInitial browser state (output of `noop(1000)`):\n{obs.get_agent_obs_text()}'
)
state: State | None = asyncio.run(
run_controller(
config=config,
@@ -137,6 +168,9 @@ def process_instance(
content=task_str
), # take output from initialize_runtime
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
metadata.agent_class
],
)
)
@@ -152,19 +186,19 @@ def process_instance(
# Instruction is the first message from the USER
instruction = ''
for event in state.history.get_events():
for event in state.history:
if isinstance(event, MessageAction):
instruction = event.content
break
return_val = complete_runtime(runtime)
logger.info(f'Return value from complete_runtime: {return_val}')
reward = max(return_val['rewards'])
reward = max(return_val['rewards'], default=0)
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -33,7 +33,7 @@ echo "MODEL_CONFIG: $MODEL_CONFIG"
EVAL_NOTE="${AGENT_VERSION}_${NOTE}"
COMMAND="poetry run python evaluation/miniwob/run_infer.py \
COMMAND="export PYTHONPATH=evaluation/miniwob:\$PYTHONPATH && poetry run python evaluation/miniwob/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations 10 \

View File

@@ -13,6 +13,7 @@ from evaluation.mint.tasks import Task
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -28,6 +29,7 @@ from openhands.core.config import (
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import (
Action,
CmdRunAction,
MessageAction,
)
@@ -45,7 +47,10 @@ def codeact_user_response_mint(state: State, task: Task, task_config: dict[str,
task=task,
task_config=task_config,
)
last_action = state.history.get_last_action()
last_action = next(
(event for event in reversed(state.history) if isinstance(event, Action)),
None,
)
result_state: TaskState = env.step(last_action.message or '')
state.extra_data['task_state'] = result_state
@@ -65,7 +70,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': '\nIMPORTANT: When your answer is confirmed by the user to be correct, you can exit using the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'IMPORTANT: When your answer is confirmed by the user to be correct, you can use the "finish" tool to finish the interaction.\n'
}
with open(os.path.join(os.path.dirname(__file__), 'requirements.txt'), 'r') as f:
@@ -202,7 +207,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -55,7 +55,7 @@ Here's an example of the evaluation output for a single task instance:
{
"instance_id": 3,
"repo": "https://github.com/dmlc/dgl",
"instruction": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please run the following command: <execute_bash> exit </execute_bash>.\n",
"instruction": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please finish the interaction using the "finish" tool.\n",
"metadata": {
"agent_class": "CodeActAgent",
"model_name": "gpt-4-1106-preview",
@@ -70,10 +70,10 @@ Here's an example of the evaluation output for a single task instance:
"id": 0,
"timestamp": "2024-05-26T17:40:41.060009",
"source": "user",
"message": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please run the following command: <execute_bash> exit </execute_bash>.\n",
"message": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please finish the interaction using the "finish" tool.\n",
"action": "message",
"args": {
"content": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please run the following command: <execute_bash> exit </execute_bash>.\n",
"content": "Please complete the Machine Learning task in the following repository: dgl\n\nThe task is: DGL Implementation of NGCF model\n\nI have a deep desire to embark on a journey brimming with knowledge and expertise. My objective is to train a cutting-edge NGCF Model, known for its unparalleled capabilities, on the illustrious dataset known as gowalla. To ensure swift execution, I kindly request your assistance in crafting the code, making use of the powerful GPU #3 and an embedding size of 32. Can you lend a helping hand to transform this dream into a reality?\n\nYou should create a script named `run.sh` under the specified path in the repo to run the task.\n\nYou can find the task repo at: /workspace/dgl/examples/pytorch/NGCF/NGCF\n\nYou should terminate the subprocess after running the task (e.g., call subprocess.Popen(args).wait()).When you think you have completed the task, please finish the interaction using the "finish" tool.\n",
"wait_for_response": false
}
},

View File

@@ -24,6 +24,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -51,7 +52,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have completed the task, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have completed the task, please finish the interaction using the "finish" tool.\n'
}
ID2CONDA = {
@@ -256,7 +257,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -0,0 +1,17 @@
FROM python:3.11-bookworm
# For OpenHands agents to explore the dataset directories, please download the full benchmark [here](https://buckeyemailosu-my.sharepoint.com/:u:/g/personal/chen_8336_buckeyemail_osu_edu/EQuA6uJ3CtRHvRfZ2GiN1tYBRVJE4DSUD10MW61fr7HuSQ?e=sCBegG) and unzip it with password `scienceagentbench`.
# **Please DO NOT redistribute the unzipped data files online.**
# It will download a benchmark.zip file to the current directory.
# unzip it and put the benchmark folder under evaluation/scienceagentbench/
RUN mkdir -p /benchmark
COPY benchmark /benchmark
RUN mkdir -p /workspace
WORKDIR /workspace
# pushd evaluation/scienceagentbench
# docker build -t xingyaoww/openhands-eval-scienceagentbench .
# popd

View File

@@ -0,0 +1,25 @@
FROM mambaorg/micromamba:debian12
USER root
# For https://github.com/OSU-NLP-Group/ScienceAgentBench/tree/main?tab=readme-ov-file#code-generation-with-agents
RUN micromamba create -n sci-agent-eval python=3.10 pip setuptools wheel
RUN micromamba run -n sci-agent-eval pip install pip-tools
RUN mkdir -p /workspace
WORKDIR /workspace
RUN apt-get update && apt-get install -y git
RUN git clone https://github.com/OSU-NLP-Group/ScienceAgentBench.git /workspace/
RUN git checkout 4eddc7db6449a5ade3e37285747c8b208cd54ce7
RUN micromamba create -n sci-agent python=3.10 pip setuptools wheel
RUN micromamba run -n sci-agent pip install -r requirements.txt
# Replace all occurence of conda with micromamba under the /workspace
RUN find ./ -type f -exec sed -i 's/conda/micromamba/g' {} \;
# pushd evaluation/scienceagentbench
# docker build -t xingyaoww/openhands-eval-scienceagentbench-evaluator -f Dockerfile.evaluator .
# popd

View File

@@ -0,0 +1,54 @@
# ScienceAgentBench Evaluation with OpenHands
This folder contains the evaluation harness for [ScienceAgentBench](https://osu-nlp-group.github.io/ScienceAgentBench/) (paper: https://arxiv.org/abs/2410.05080).
## Setup Environment and LLM Configuration
Please follow instruction [here](../README.md#setup) to setup your local development environment and LLM.
## Setup ScienceAgentBench
To prevent benchmark data contamination, we only provide the annotation sheet on [Huggingface](https://huggingface.co/datasets/osunlp/ScienceAgentBench), which includes all necessary *inputs* to run an agent.
## Run Inference on ScienceAgentBench
```bash
./evaluation/scienceagentbench/scripts/run_infer.sh [model_config] [git-version] [use_knowledge] [agent] [eval_limit] [max_iter] [num_workers] [dataset] [dataset_split]
# Example
./evaluation/scienceagentbench/scripts/run_infer.sh llm.eval_gpt4o 0.9.3
```
where `model_config` is mandatory, and the rest are optional.
- `model_config`, e.g. `eval_gpt4_1106_preview`, is the config group name for your
LLM settings, as defined in your `config.toml`.
- `git-version`, e.g. `HEAD`, is the git commit hash of the OpenHands version you would
like to evaluate. It could also be a release tag like `0.6.2`.
- `use_knowledge`, e.g. `true`, specifies whether allowing the agent to use expert-provided knowledge as additional input or not. By default, it is set to `false`.
- `agent`, e.g. `CodeActAgent`, is the name of the agent for benchmarks, defaulting
to `CodeActAgent`.
- `eval_limit`, e.g. `10`, limits the evaluation to the first `eval_limit` instances. By
default, the script evaluates the entire SWE-bench_Lite test set (300 issues). Note:
in order to use `eval_limit`, you must also set `agent`.
- `max_iter`, e.g. `20`, is the maximum number of iterations for the agent to run. By
default, it is set to 30.
- `num_workers`, e.g. `3`, is the number of parallel workers to run the evaluation. By
default, it is set to 1.
## Evaluate Generated Programs
### Extract Necessary Information from OpenHands Log
After the inference is completed, you may use the following command to extract necessary information from the output log for evaluation:
```bash
python post_proc.py [log_fname]
```
- `log_fname`, e.g. `evaluation/.../output.jsonl`, is the automatically saved trajectory log of an OpenHands agent.
Output will be write to e.g. `evaluation/.../output.converted.jsonl`
### Run evaluation
Please follow the steps [here](https://github.com/OSU-NLP-Group/ScienceAgentBench/tree/main?tab=readme-ov-file#evaluation-of-generated-code) to evaluate the generated programs.

View File

@@ -0,0 +1,30 @@
import json
from argparse import ArgumentParser
if __name__ == '__main__':
parser = ArgumentParser()
parser.add_argument(
'log_fname',
type=str,
)
args = parser.parse_args()
fname = args.log_fname
out_fname = args.log_fname.replace('.jsonl', '.converted.jsonl')
log = [json.loads(line) for line in open(fname)]
simple_log = [
json.dumps(
{
'instance_id': ex['instance_id'],
'instruction': ex['instruction'],
'test_result': ex['test_result'],
'cost': ex['metrics']['accumulated_cost'],
}
)
for ex in log
]
with open(out_fname, 'w+', encoding='utf-8') as f:
f.write('\n'.join(simple_log))

View File

@@ -0,0 +1,292 @@
import asyncio
import os
from typing import Any
import pandas as pd
from datasets import load_dataset
from tqdm import tqdm
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
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,
get_parser,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
}
LOCAL_DATASET_PATH = os.path.join(os.path.dirname(__file__), 'benchmark')
def format_task_dict(example, use_knowledge):
task = {
'instance_id': example['instance_id'],
'task_inst': example['task_inst'],
'dataset_path': '/benchmark/datasets/'
+ example['dataset_folder_tree'].split('\n')[0][4:],
'dataset_folder_tree': example['dataset_folder_tree'],
'dataset_preview': example['dataset_preview'],
'pred_program_name': 'pred_' + example['gold_program_name'],
}
if use_knowledge:
task['task_inst'] += '\n' + str(example['domain_knowledge'])
return task
def get_config(
metadata: EvalMetadata,
instance_id: str,
) -> AppConfig:
config = AppConfig(
default_agent=metadata.agent_class,
run_as_openhands=False,
runtime=os.environ.get('RUNTIME', 'eventstream'),
max_budget_per_task=4,
max_iterations=metadata.max_iterations,
sandbox=SandboxConfig(
base_container_image='docker.io/xingyaoww/openhands-eval-scienceagentbench',
enable_auto_lint=True,
use_host_network=False,
timeout=300,
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,
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config,
metadata.eval_output_dir,
instance_id,
)
)
return config
def initialize_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required
):
"""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 up workspace directories
action = CmdRunAction(command='mkdir -p /workspace/pred_programs')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
action = CmdRunAction(command='mkdir -p /workspace/pred_results')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
dataset_name = instance['dataset_folder_tree'].split('\n')[0][4:].rstrip('/')
# Copy the dataset to the workspace
dataset_dir = os.path.join(
LOCAL_DATASET_PATH,
'datasets',
dataset_name,
)
runtime.copy_to(dataset_dir, '/workspace/benchmark/datasets', recursive=True)
# Check the dataset exists
action = CmdRunAction(
command='cd /workspace/benchmark/datasets && ls',
keep_prompt=False,
)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
assert dataset_name in obs.content
logger.info(f"{'-' * 50} END Runtime Initialization Fn {'-' * 50}")
def complete_runtime(
runtime: Runtime,
instance: pd.Series,
) -> 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
test_result = {}
action = CmdRunAction(command='cd /workspace')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
action = CmdRunAction(
command=f'cat pred_programs/{instance.pred_program_name}',
keep_prompt=False,
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
if obs.exit_code == 0:
test_result = {'program': obs.content}
else:
test_result = {'program': 'ERROR'}
logger.info(f"{'-' * 50} END Runtime Completion Fn {'-' * 50}")
return test_result
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
) -> EvalOutput:
instance_id = instance.instance_id.replace('/', '__')
config = get_config(metadata, instance_id)
# Set up the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
log_dir = os.path.join(metadata.eval_output_dir, 'infer_logs')
reset_logger_for_multiprocessing(logger, instance_id, log_dir)
else:
logger.info(f'Starting evaluation for instance {instance_id}.')
instruction = f"""You are an expert Python programming assistant that helps scientist users to write high-quality code to solve their tasks.
Given a user request, you are expected to write a complete program that accomplishes the requested task and save any outputs to `/workspace/pred_results/` in the correct format.
Here's the user request you need to work on:
{instance.task_inst}
You can access the dataset at `{instance.dataset_path}`. Here is the directory structure of the dataset:
```
{instance.dataset_folder_tree}
```
Here are some helpful previews for the dataset file(s):
{instance.dataset_preview}
Please save your program as `/workspace/pred_programs/{instance.pred_program_name}`.
Then, please run the program to check and fix any errors.
Please do NOT run the program in the background.
If the program uses some packages that are incompatible, please figure out alternative implementations and do NOT restart the environment.
"""
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
initialize_runtime(runtime, instance)
# Here's how you can run the agent (similar to the `main` function) and get the final task state
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=MessageAction(content=instruction),
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN.get(
metadata.agent_class
),
)
)
# ======= Attempt to evaluate the agent's edits =======
test_result = complete_runtime(runtime, instance)
# 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
# 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=instance.instance_id,
instruction=instruction,
metadata=metadata,
history=histories,
metrics=metrics,
error=state.last_error if state and state.last_error else None,
test_result=test_result,
)
return output
if __name__ == '__main__':
parser = get_parser()
parser.add_argument(
'--use_knowledge',
type=str,
default='false',
choices=['true', 'false'],
help='use expert-provided knowledge or not',
)
args, _ = parser.parse_known_args()
sab_dataset = load_dataset('osunlp/ScienceAgentBench', split='validation')
dataset_processed = []
for example in tqdm(sab_dataset):
dataset_processed.append(
format_task_dict(example, args.use_knowledge == 'true')
)
dataset = pd.DataFrame(dataset_processed)
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,
'ScienceAgentBench',
args.agent_cls,
args.max_iterations,
args.eval_note,
args.eval_output_dir,
)
output_file = os.path.join(metadata.eval_output_dir, 'output.jsonl')
dataset['instance_id'] = dataset['instance_id'].apply(str)
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,49 @@
#!/bin/bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
MODEL_CONFIG=$1
COMMIT_HASH=$2
USE_KNOWLEDGE=$3
AGENT=$4
EVAL_LIMIT=$5
NUM_WORKERS=$6
if [ -z "$NUM_WORKERS" ]; then
NUM_WORKERS=1
echo "Number of workers not specified, use default $NUM_WORKERS"
fi
checkout_eval_branch
if [ -z "$AGENT" ]; then
echo "Agent not specified, use default CodeActAgent"
AGENT="CodeActAgent"
fi
if [ -z "$USE_KNOWLEDGE" ]; then
echo "Use knowledge not specified, use default False"
USE_KNOWLEDGE=false
fi
get_agent_version
echo "AGENT: $AGENT"
echo "AGENT_VERSION: $AGENT_VERSION"
echo "MODEL_CONFIG: $MODEL_CONFIG"
COMMAND="poetry run python evaluation/scienceagentbench/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--use_knowledge $USE_KNOWLEDGE \
--max-iterations 30 \
--eval-num-workers $NUM_WORKERS \
--eval-note $AGENT_VERSION" \
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

@@ -1,6 +1,7 @@
import os
import tempfile
import time
from functools import partial
import pandas as pd
from swebench.harness.grading import get_eval_report
@@ -83,6 +84,7 @@ def get_config(instance: pd.Series) -> AppConfig:
timeout=1800,
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,
),
# do not mount workspace
workspace_base=None,
@@ -93,13 +95,28 @@ def get_config(instance: pd.Series) -> AppConfig:
def process_instance(
instance: pd.Series,
metadata: EvalMetadata | None = None,
metadata: EvalMetadata,
reset_logger: bool = True,
log_dir: str | None = None,
) -> EvalOutput:
"""
Evaluate agent performance on a SWE-bench problem instance.
Note that this signature differs from the expected input to `run_evaluation`. Use
`functools.partial` to provide optional arguments before passing to the evaluation harness.
Args:
log_dir (str | None, default=None): Path to directory where log files will be written. Must
be provided if `reset_logger` is set.
Raises:
AssertionError: if the `reset_logger` flag is set without a provided log directory.
"""
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
global output_file
log_dir = output_file.replace('.jsonl', '.logs')
assert (
log_dir is not None
), "Can't reset logger without a provided log directory."
os.makedirs(log_dir, exist_ok=True)
reset_logger_for_multiprocessing(logger, instance.instance_id, log_dir)
else:
@@ -126,6 +143,7 @@ def process_instance(
return EvalOutput(
instance_id=instance_id,
test_result=instance['test_result'],
metadata=metadata,
)
runtime = create_runtime(config)
@@ -175,6 +193,7 @@ def process_instance(
return EvalOutput(
instance_id=instance_id,
test_result=instance['test_result'],
metadata=metadata,
)
elif 'APPLY_PATCH_PASS' in apply_patch_output:
logger.info(f'[{instance_id}] {APPLY_PATCH_PASS}:\n{apply_patch_output}')
@@ -239,28 +258,34 @@ def process_instance(
# Create a directory structure that matches the expected format
# NOTE: this is a hack to make the eval report format consistent
# with the original SWE-Bench eval script
log_dir = os.path.join(temp_dir, 'logs', instance_id)
log_dir = os.path.join(temp_dir, 'logs', instance_id.lower())
os.makedirs(log_dir, exist_ok=True)
test_output_path = os.path.join(log_dir, 'test_output.txt')
with open(test_output_path, 'w') as f:
f.write(test_output)
_report = get_eval_report(
test_spec=test_spec,
prediction={
'model_patch': model_patch,
'instance_id': instance_id,
},
log_path=test_output_path,
include_tests_status=True,
)
report = _report[instance_id]
logger.info(
f"[{instance_id}] report: {report}\nResult for {instance_id}: resolved: {report['resolved']}"
)
instance['test_result']['report']['resolved'] = report[
'resolved'
]
try:
_report = get_eval_report(
test_spec=test_spec,
prediction={
'model_patch': model_patch,
'instance_id': instance_id,
},
log_path=test_output_path,
include_tests_status=True,
)
report = _report[instance_id]
logger.info(
f"[{instance_id}] report: {report}\nResult for {instance_id}: resolved: {report['resolved']}"
)
instance['test_result']['report']['resolved'] = report[
'resolved'
]
except Exception as e:
logger.error(
f'[{instance_id}] Error when getting eval report: {e}'
)
instance['test_result']['report']['resolved'] = False
instance['test_result']['report']['error_eval'] = True
else:
logger.info(f'[{instance_id}] Error when starting eval:\n{obs.content}')
instance['test_result']['report']['error_eval'] = True
@@ -268,6 +293,7 @@ def process_instance(
return EvalOutput(
instance_id=instance_id,
test_result=instance['test_result'],
metadata=metadata,
)
else:
logger.info(
@@ -335,7 +361,7 @@ if __name__ == '__main__':
if 'model_patch' not in predictions.columns:
predictions['model_patch'] = predictions['test_result'].apply(
lambda x: x['git_patch']
lambda x: x.get('git_patch', '')
)
assert {'instance_id', 'model_patch'}.issubset(
set(predictions.columns)
@@ -354,12 +380,26 @@ if __name__ == '__main__':
output_file = args.input_file.replace('.jsonl', '.swebench_eval.jsonl')
instances = prepare_dataset(predictions, output_file, args.eval_n_limit)
# If possible, load the relevant metadata to avoid issues with `run_evaluation`.
metadata: EvalMetadata | None = None
metadata_filepath = os.path.join(os.path.dirname(args.input_file), 'metadata.json')
if os.path.exists(metadata_filepath):
with open(metadata_filepath, 'r') as metadata_file:
data = metadata_file.read()
metadata = EvalMetadata.model_validate_json(data)
# The evaluation harness constrains the signature of `process_instance_func` but we need to
# pass extra information. Build a new function object to avoid issues with multiprocessing.
process_instance_func = partial(
process_instance, log_dir=output_file.replace('.jsonl', '.logs')
)
run_evaluation(
instances,
metadata=None,
metadata=metadata,
output_file=output_file,
num_workers=args.eval_num_workers,
process_instance_func=process_instance,
process_instance_func=process_instance_func,
)
# Load evaluated predictions & print number of resolved predictions

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
CODEACT_SWE_PROMPT = """Now, you're going to solve this issue on your own. Your terminal session has started and you're in the repository's root directory. You can use any bash commands or the special interface to help you. Edit all the files you need to and run any checks or tests that you want.
Remember, YOU CAN ONLY ENTER ONE COMMAND AT A TIME. You should always wait for feedback after every command.
When you're satisfied with all of the changes you've made, you can run the following command: <execute_bash> exit </execute_bash>.
When you're satisfied with all of the changes you've made, you can use the "finish" tool to finish the interaction.
Note however that you cannot use any interactive session commands (e.g. vim) in this environment, but you can write scripts and run them. E.g. you can write a python script and then run it with `python <script_name>.py`.
NOTE ABOUT THE EDIT COMMAND: Indentation really matters! When editing a file, make sure to insert appropriate indentation before each line!

View File

@@ -20,6 +20,7 @@ from evaluation.utils.shared import (
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 (
@@ -35,11 +36,12 @@ from openhands.events.action import CmdRunAction, MessageAction
from openhands.events.observation import CmdOutputObservation, ErrorObservation
from openhands.events.serialization.event import event_to_dict
from openhands.runtime.base import Runtime
from openhands.runtime.utils.shutdown_listener import sleep_if_should_continue
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'
RUN_WITH_BROWSING = os.environ.get('RUN_WITH_BROWSING', 'false').lower() == 'true'
AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
'CodeActAgent': codeact_user_response,
@@ -79,7 +81,7 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
'</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'
"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 /repo directory to ensure the <pr_description> is satisfied.\n'
'Your task is to make the minimal changes to non-tests files in the /workspace directory to ensure the <pr_description> is satisfied.\n'
'Follow these steps to resolve the issue:\n'
'1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.\n'
'2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
@@ -88,6 +90,13 @@ def get_instruction(instance: pd.Series, metadata: EvalMetadata):
'5. Think about edgecases and make sure your fix handles them as well\n'
"Your thinking should be thorough and so it's fine if it's very long.\n"
)
if RUN_WITH_BROWSING:
instruction += (
'<IMPORTANT!>\n'
'You SHOULD NEVER attempt to browse the web. '
'</IMPORTANT!>\n'
)
return instruction
@@ -101,7 +110,7 @@ def get_instance_docker_image(instance_id: str) -> str:
image_name = image_name.replace(
'__', '_s_'
) # to comply with docker image naming convention
return DOCKER_IMAGE_PREFIX.rstrip('/') + '/' + image_name
return (DOCKER_IMAGE_PREFIX.rstrip('/') + '/' + image_name).lower()
def get_config(
@@ -137,23 +146,20 @@ 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_remote_runtime_alive=False,
remote_runtime_init_timeout=3600,
),
# do not mount workspace
workspace_base=None,
workspace_mount_path=None,
)
if metadata.llm_config.log_completions:
metadata.llm_config.log_completions_folder = os.path.join(
metadata.eval_output_dir, 'llm_completions', instance['instance_id']
config.set_llm_config(
update_llm_config_for_completions_logging(
metadata.llm_config, metadata.eval_output_dir, instance['instance_id']
)
logger.info(
f'Logging LLM completions for instance {instance["instance_id"]} to '
f'{metadata.llm_config.log_completions_folder}'
)
config.set_llm_config(metadata.llm_config)
)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing_delegate=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
)
config.set_agent_config(agent_config)
@@ -438,7 +444,8 @@ def process_instance(
if state is None:
raise ValueError('State should not be None.')
histories = [event_to_dict(event) for event in state.history.get_events()]
# NOTE: this is NO LONGER the event stream, but an agent history that includes delegate agent's events
histories = [event_to_dict(event) for event in state.history]
metrics = state.metrics.get() if state.metrics else None
# Save the output
@@ -527,5 +534,10 @@ if __name__ == '__main__':
instances[col] = instances[col].apply(lambda x: str(x))
run_evaluation(
instances, metadata, output_file, args.eval_num_workers, process_instance
instances,
metadata,
output_file,
args.eval_num_workers,
process_instance,
timeout_seconds=120 * 60, # 2 hour PER instance should be more than enough
)

View File

@@ -34,6 +34,11 @@ if [ -z "$USE_INSTANCE_IMAGE" ]; then
USE_INSTANCE_IMAGE=true
fi
if [ -z "$RUN_WITH_BROWSING" ]; then
echo "RUN_WITH_BROWSING not specified, use default false"
RUN_WITH_BROWSING=false
fi
if [ -z "$DATASET" ]; then
echo "DATASET not specified, use default princeton-nlp/SWE-bench_Lite"
@@ -47,6 +52,8 @@ fi
export USE_INSTANCE_IMAGE=$USE_INSTANCE_IMAGE
echo "USE_INSTANCE_IMAGE: $USE_INSTANCE_IMAGE"
export RUN_WITH_BROWSING=$RUN_WITH_BROWSING
echo "RUN_WITH_BROWSING: $RUN_WITH_BROWSING"
get_agent_version
@@ -67,6 +74,10 @@ if [ "$USE_HINT_TEXT" = false ]; then
EVAL_NOTE="$EVAL_NOTE-no-hint"
fi
if [ "$RUN_WITH_BROWSING" = true ]; then
EVAL_NOTE="$EVAL_NOTE-with-browsing"
fi
if [ -n "$EXP_NAME" ]; then
EVAL_NOTE="$EVAL_NOTE-$EXP_NAME"
fi

View File

@@ -9,6 +9,7 @@ from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -33,7 +34,7 @@ AGENT_CLS_TO_FAKE_USER_RESPONSE_FN = {
}
AGENT_CLS_TO_INST_SUFFIX = {
'CodeActAgent': 'When you think you have completed the request, please run the following command: <execute_bash> exit </execute_bash>.\n'
'CodeActAgent': 'When you think you have completed the request, please finish the interaction using the "finish" tool.\n'
}
@@ -126,7 +127,8 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
raise ValueError('State should not be None.')
# retrieve the last message from the agent
model_answer_raw = state.history.get_last_agent_message()
last_agent_message = state.get_last_agent_message()
model_answer_raw = last_agent_message.content if last_agent_message else ''
# attempt to parse model_answer
correct = eval_answer(str(model_answer_raw), str(answer))
@@ -137,7 +139,7 @@ def process_instance(instance: Any, metadata: EvalMetadata, reset_logger: bool =
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -18,6 +18,9 @@ from openhands.core.logger import get_console_handler
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import Action
from openhands.events.action.message import MessageAction
from openhands.events.event import Event
from openhands.events.serialization.event import event_to_dict
from openhands.events.utils import get_pairs_from_events
class EvalMetadata(BaseModel):
@@ -112,7 +115,14 @@ def codeact_user_response(
if state.history:
# check if the last action has an answer, if so, early exit
if try_parse is not None:
last_action = state.history.get_last_action()
last_action = next(
(
event
for event in reversed(state.history)
if isinstance(event, Action)
),
None,
)
ans = try_parse(last_action)
if ans is not None:
return '/exit'
@@ -120,14 +130,14 @@ def codeact_user_response(
# check if the agent has tried to talk to the user 3 times, if so, let the agent know it can give up
user_msgs = [
event
for event in state.history.get_events()
for event in state.history
if isinstance(event, MessageAction) and event.source == 'user'
]
if len(user_msgs) >= 2:
# let the agent know that it can give up when it has tried 3 times
return (
msg
+ 'If you want to give up, run: <execute_bash> exit </execute_bash>.\n'
+ 'If you want to give up, use the "finish" tool to finish the interaction.\n'
)
return msg
@@ -336,6 +346,7 @@ def run_evaluation(
f'model {metadata.llm_config.model}, max iterations {metadata.max_iterations}.\n'
)
else:
logger.warning('Running evaluation without metadata.')
logger.info(f'Evaluation started with {num_workers} workers.')
total_instances = len(dataset)
@@ -411,3 +422,35 @@ def reset_logger_for_multiprocessing(
)
file_handler.setLevel(logging.INFO)
logger.addHandler(file_handler)
def update_llm_config_for_completions_logging(
llm_config: LLMConfig,
eval_output_dir: str,
instance_id: str,
) -> LLMConfig:
"""Update the LLM config for logging completions."""
if llm_config.log_completions:
llm_config.log_completions_folder = os.path.join(
eval_output_dir, 'llm_completions', instance_id
)
logger.info(
f'Logging LLM completions for instance {instance_id} to '
f'{llm_config.log_completions_folder}'
)
return llm_config
# history is now available as a filtered stream of events, rather than list of pairs of (Action, Observation)
# we rebuild the pairs here
# for compatibility with the existing output format in evaluations
# remove this when it's no longer necessary
def compatibility_for_eval_history_pairs(
history: list[Event],
) -> list[tuple[dict, dict]]:
history_pairs = []
for action, observation in get_pairs_from_events(history):
history_pairs.append((event_to_dict(action), event_to_dict(observation)))
return history_pairs

View File

@@ -10,6 +10,7 @@ import pandas as pd
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
compatibility_for_eval_history_pairs,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
@@ -166,7 +167,7 @@ def process_instance(
# Instruction is the first message from the USER
instruction = ''
for event in state.history.get_events():
for event in state.history:
if isinstance(event, MessageAction):
instruction = event.content
break
@@ -178,7 +179,7 @@ def process_instance(
# 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 = state.history.compatibility_for_eval_history_pairs()
histories = compatibility_for_eval_history_pairs(state.history)
# Save the output
output = EvalOutput(

View File

@@ -84,4 +84,4 @@
}
}
]
}
}

7
frontend/.gitignore vendored
View File

@@ -1,4 +1,9 @@
# i18n translation files make by script using `make build`
public/locales/**/*
src/i18n/declaration.ts
.env
.env
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -0,0 +1,40 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { clearSession } from "../src/utils/clear-session";
import store from "../src/store";
import { initialState as browserInitialState } from "../src/state/browserSlice";
describe("clearSession", () => {
beforeEach(() => {
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
};
vi.stubGlobal("localStorage", localStorageMock);
// Set initial browser state to non-default values
store.dispatch({
type: "browser/setUrl",
payload: "https://example.com",
});
store.dispatch({
type: "browser/setScreenshotSrc",
payload: "base64screenshot",
});
});
it("should clear localStorage and reset browser state", () => {
clearSession();
// Verify localStorage items were removed
expect(localStorage.removeItem).toHaveBeenCalledWith("token");
expect(localStorage.removeItem).toHaveBeenCalledWith("repo");
// Verify browser state was reset
const state = store.getState();
expect(state.browser.url).toBe(browserInitialState.url);
expect(state.browser.screenshotSrc).toBe(browserInitialState.screenshotSrc);
});
});

View File

@@ -1,5 +1,5 @@
import userEvent from "@testing-library/user-event";
import { render, screen } from "@testing-library/react";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, afterEach, vi, it, expect } from "vitest";
import { ChatInput } from "#/components/chat-input";
@@ -158,4 +158,46 @@ describe("ChatInput", () => {
await user.tab();
expect(onBlurMock).toHaveBeenCalledOnce();
});
it("should handle text paste correctly", () => {
const onSubmit = vi.fn();
const onChange = vi.fn();
render(<ChatInput onSubmit={onSubmit} onChange={onChange} />);
const input = screen.getByTestId("chat-input").querySelector("textarea");
expect(input).toBeTruthy();
// Fire paste event with text data
fireEvent.paste(input!, {
clipboardData: {
getData: (type: string) => type === 'text/plain' ? 'test paste' : '',
files: []
}
});
});
it("should handle image paste correctly", () => {
const onSubmit = vi.fn();
const onImagePaste = vi.fn();
render(<ChatInput onSubmit={onSubmit} onImagePaste={onImagePaste} />);
const input = screen.getByTestId("chat-input").querySelector("textarea");
expect(input).toBeTruthy();
// Create a paste event with an image file
const file = new File(["dummy content"], "image.png", { type: "image/png" });
// Fire paste event with image data
fireEvent.paste(input!, {
clipboardData: {
getData: () => '',
files: [file]
}
});
// Verify image paste was handled
expect(onImagePaste).toHaveBeenCalledWith([file]);
});
});

View File

@@ -1,19 +1,161 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { render, screen, within } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { act, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { ChatInterface } from "#/components/chat-interface";
import { SocketProvider } from "#/context/socket";
import { addUserMessage } from "#/state/chatSlice";
import { SUGGESTIONS } from "#/utils/suggestions";
import * as ChatSlice from "#/state/chatSlice";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const renderChatInterface = (messages: (Message | ErrorMessage)[]) =>
render(<ChatInterface />, { wrapper: SocketProvider });
renderWithProviders(<ChatInterface />);
describe("Empty state", () => {
const { send: sendMock } = vi.hoisted(() => ({
send: vi.fn(),
}));
const { useWsClient: useWsClientMock } = vi.hoisted(() => ({
useWsClient: vi.fn(() => ({ send: sendMock, runtimeActive: true })),
}));
beforeAll(() => {
vi.mock("@remix-run/react", async (importActual) => ({
...(await importActual<typeof import("@remix-run/react")>()),
useRouteLoaderData: vi.fn(() => ({})),
}));
vi.mock("#/context/socket", async (importActual) => ({
...(await importActual<typeof import("#/context/ws-client-provider")>()),
useWsClient: useWsClientMock,
}));
});
describe.skip("ChatInterface", () => {
afterEach(() => {
vi.clearAllMocks();
});
it.todo("should render suggestions if empty");
it("should render suggestions if empty", () => {
const { store } = renderWithProviders(<ChatInterface />, {
preloadedState: {
chat: { messages: [] },
},
});
expect(screen.getByTestId("suggestions")).toBeInTheDocument();
act(() => {
store.dispatch(
addUserMessage({
content: "Hello",
imageUrls: [],
timestamp: new Date().toISOString(),
}),
);
});
expect(screen.queryByTestId("suggestions")).not.toBeInTheDocument();
});
it("should render the default suggestions", () => {
renderWithProviders(<ChatInterface />, {
preloadedState: {
chat: { messages: [] },
},
});
const suggestions = screen.getByTestId("suggestions");
const repoSuggestions = Object.keys(SUGGESTIONS.repo);
// check that there are at most 4 suggestions displayed
const displayedSuggestions = within(suggestions).getAllByRole("button");
expect(displayedSuggestions.length).toBeLessThanOrEqual(4);
// Check that each displayed suggestion is one of the repo suggestions
displayedSuggestions.forEach((suggestion) => {
expect(repoSuggestions).toContain(suggestion.textContent);
});
});
it.fails(
"should load the a user message to the input when selecting",
async () => {
// this is to test that the message is in the UI before the socket is called
useWsClientMock.mockImplementation(() => ({
send: sendMock,
runtimeActive: false, // mock an inactive runtime setup
}));
const addUserMessageSpy = vi.spyOn(ChatSlice, "addUserMessage");
const user = userEvent.setup();
const { store } = renderWithProviders(<ChatInterface />, {
preloadedState: {
chat: { messages: [] },
},
});
const suggestions = screen.getByTestId("suggestions");
const displayedSuggestions = within(suggestions).getAllByRole("button");
const input = screen.getByTestId("chat-input");
await user.click(displayedSuggestions[0]);
// user message loaded to input
expect(addUserMessageSpy).not.toHaveBeenCalled();
expect(screen.queryByTestId("suggestions")).toBeInTheDocument();
expect(store.getState().chat.messages).toHaveLength(0);
expect(input).toHaveValue(displayedSuggestions[0].textContent);
},
);
it.fails(
"should send the message to the socket only if the runtime is active",
async () => {
useWsClientMock.mockImplementation(() => ({
send: sendMock,
runtimeActive: false, // mock an inactive runtime setup
}));
const user = userEvent.setup();
const { rerender } = renderWithProviders(<ChatInterface />, {
preloadedState: {
chat: { messages: [] },
},
});
const suggestions = screen.getByTestId("suggestions");
const displayedSuggestions = within(suggestions).getAllByRole("button");
await user.click(displayedSuggestions[0]);
expect(sendMock).not.toHaveBeenCalled();
useWsClientMock.mockImplementation(() => ({
send: sendMock,
runtimeActive: true, // mock an active runtime setup
}));
rerender(<ChatInterface />);
await waitFor(() =>
expect(sendMock).toHaveBeenCalledWith(expect.any(String)),
);
},
);
});
describe.skip("ChatInterface", () => {
beforeAll(() => {
// mock useScrollToBottom hook
vi.mock("#/hooks/useScrollToBottom", () => ({
useScrollToBottom: vi.fn(() => ({
scrollDomToBottom: vi.fn(),
onChatBodyScroll: vi.fn(),
hitBottom: vi.fn(),
})),
}));
});
afterEach(() => {
vi.clearAllMocks();
});
it("should render messages", () => {
const messages: Message[] = [
@@ -128,14 +270,14 @@ describe.skip("ChatInterface", () => {
timestamp: new Date().toISOString(),
},
{
error: "Woops!",
error: true,
id: "",
message: "Something went wrong",
},
];
renderChatInterface(messages);
const error = screen.getByTestId("error-message");
expect(within(error).getByText("Woops!")).toBeInTheDocument();
expect(within(error).getByText("Something went wrong")).toBeInTheDocument();
});

View File

@@ -5,7 +5,6 @@ import { FeedbackForm } from "#/components/feedback-form";
describe("FeedbackForm", () => {
const user = userEvent.setup();
const onSubmitMock = vi.fn();
const onCloseMock = vi.fn();
afterEach(() => {
@@ -13,7 +12,7 @@ describe("FeedbackForm", () => {
});
it("should render correctly", () => {
render(<FeedbackForm onSubmit={onSubmitMock} onClose={onCloseMock} />);
render(<FeedbackForm polarity="positive" onClose={onCloseMock} />);
screen.getByLabelText("Email");
screen.getByLabelText("Private");
@@ -24,7 +23,7 @@ describe("FeedbackForm", () => {
});
it("should switch between private and public permissions", async () => {
render(<FeedbackForm onSubmit={onSubmitMock} onClose={onCloseMock} />);
render(<FeedbackForm polarity="positive" onClose={onCloseMock} />);
const privateRadio = screen.getByLabelText("Private");
const publicRadio = screen.getByLabelText("Public");
@@ -40,69 +39,11 @@ describe("FeedbackForm", () => {
expect(publicRadio).not.toBeChecked();
});
it("should call onSubmit when the form is submitted", async () => {
render(<FeedbackForm onSubmit={onSubmitMock} onClose={onCloseMock} />);
const email = screen.getByLabelText("Email");
await user.type(email, "test@test.test");
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(onSubmitMock).toHaveBeenCalledWith("private", "test@test.test"); // private is the default value
});
it("should not call onSubmit when the email is invalid", async () => {
render(<FeedbackForm onSubmit={onSubmitMock} onClose={onCloseMock} />);
const email = screen.getByLabelText("Email");
const submitButton = screen.getByRole("button", { name: "Submit" });
await user.click(submitButton);
expect(onSubmitMock).not.toHaveBeenCalled();
await user.type(email, "test");
await user.click(submitButton);
expect(onSubmitMock).not.toHaveBeenCalled();
});
it("should submit public permissions when the public radio is checked", async () => {
render(<FeedbackForm onSubmit={onSubmitMock} onClose={onCloseMock} />);
const email = screen.getByLabelText("Email");
const publicRadio = screen.getByLabelText("Public");
await user.type(email, "test@test.test");
await user.click(publicRadio);
await user.click(screen.getByRole("button", { name: "Submit" }));
expect(onSubmitMock).toHaveBeenCalledWith("public", "test@test.test");
});
it("should call onClose when the close button is clicked", async () => {
render(<FeedbackForm onSubmit={onSubmitMock} onClose={onCloseMock} />);
render(<FeedbackForm polarity="positive" onClose={onCloseMock} />);
await user.click(screen.getByRole("button", { name: "Cancel" }));
expect(onSubmitMock).not.toHaveBeenCalled();
expect(onCloseMock).toHaveBeenCalled();
});
it("should disable the buttons if isSubmitting is true", () => {
const { rerender } = render(
<FeedbackForm onSubmit={onSubmitMock} onClose={onCloseMock} />,
);
const submitButton = screen.getByRole("button", { name: "Submit" });
const cancelButton = screen.getByRole("button", { name: "Cancel" });
expect(submitButton).not.toBeDisabled();
expect(cancelButton).not.toBeDisabled();
rerender(
<FeedbackForm
onSubmit={onSubmitMock}
onClose={onCloseMock}
isSubmitting
/>,
);
expect(submitButton).toBeDisabled();
expect(cancelButton).toBeDisabled();
});
});

View File

@@ -16,13 +16,16 @@ vi.mock("../../services/fileService", async () => ({
}));
const renderFileExplorerWithRunningAgentState = () =>
renderWithProviders(<FileExplorer error={null} />, {
preloadedState: {
agent: {
curAgentState: AgentState.RUNNING,
renderWithProviders(
<FileExplorer error={null} isOpen onToggle={() => {}} />,
{
preloadedState: {
agent: {
curAgentState: AgentState.RUNNING,
},
},
},
});
);
describe.skip("FileExplorer", () => {
afterEach(() => {

View File

@@ -25,6 +25,21 @@ describe("InteractiveChatBox", () => {
within(chatBox).getByTestId("upload-image-input");
});
it.fails("should set custom values", () => {
render(
<InteractiveChatBox
onSubmit={onSubmitMock}
onStop={onStopMock}
value="Hello, world!"
/>,
);
const chatBox = screen.getByTestId("interactive-chat-box");
const chatInput = within(chatBox).getByTestId("chat-input");
expect(chatInput).toHaveValue("Hello, world!");
});
it("should display the image previews when images are uploaded", async () => {
const user = userEvent.setup();
render(<InteractiveChatBox onSubmit={onSubmitMock} onStop={onStopMock} />);

View File

@@ -0,0 +1,30 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SuggestionItem } from "#/components/suggestion-item";
describe("SuggestionItem", () => {
const suggestionItem = { label: "suggestion1", value: "a long text value" };
const onClick = vi.fn();
afterEach(() => {
vi.clearAllMocks();
});
it("should render a suggestion", () => {
render(<SuggestionItem suggestion={suggestionItem} onClick={onClick} />);
expect(screen.getByTestId("suggestion")).toBeInTheDocument();
expect(screen.getByText(/suggestion1/i)).toBeInTheDocument();
});
it("should call onClick when clicking a suggestion", async () => {
const user = userEvent.setup();
render(<SuggestionItem suggestion={suggestionItem} onClick={onClick} />);
const suggestion = screen.getByTestId("suggestion");
await user.click(suggestion);
expect(onClick).toHaveBeenCalledWith("a long text value");
});
});

View File

@@ -0,0 +1,60 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { Suggestions } from "#/components/suggestions";
describe("Suggestions", () => {
const firstSuggestion = {
label: "first-suggestion",
value: "value-of-first-suggestion",
};
const secondSuggestion = {
label: "second-suggestion",
value: "value-of-second-suggestion",
};
const suggestions = [firstSuggestion, secondSuggestion];
const onSuggestionClickMock = vi.fn();
afterEach(() => {
vi.clearAllMocks();
});
it("should render suggestions", () => {
render(
<Suggestions
suggestions={suggestions}
onSuggestionClick={onSuggestionClickMock}
/>,
);
expect(screen.getByTestId("suggestions")).toBeInTheDocument();
const suggestionElements = screen.getAllByTestId("suggestion");
expect(suggestionElements).toHaveLength(2);
expect(suggestionElements[0]).toHaveTextContent("first-suggestion");
expect(suggestionElements[1]).toHaveTextContent("second-suggestion");
});
it("should call onSuggestionClick when clicking a suggestion", async () => {
const user = userEvent.setup();
render(
<Suggestions
suggestions={suggestions}
onSuggestionClick={onSuggestionClickMock}
/>,
);
const suggestionElements = screen.getAllByTestId("suggestion");
await user.click(suggestionElements[0]);
expect(onSuggestionClickMock).toHaveBeenCalledWith(
"value-of-first-suggestion",
);
await user.click(suggestionElements[1]);
expect(onSuggestionClickMock).toHaveBeenCalledWith(
"value-of-second-suggestion",
);
});
});

View File

@@ -0,0 +1,93 @@
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { useRate } from "#/utils/use-rate";
describe("useRate", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("should initialize", () => {
const { result } = renderHook(() => useRate());
expect(result.current.items).toHaveLength(0);
expect(result.current.rate).toBeNull();
expect(result.current.lastUpdated).toBeNull();
expect(result.current.isUnderThreshold).toBe(true);
});
it("should handle the case of a single element", () => {
const { result } = renderHook(() => useRate());
act(() => {
result.current.record(123);
});
expect(result.current.items).toHaveLength(1);
expect(result.current.lastUpdated).not.toBeNull();
});
it("should return the difference between the last two elements", () => {
const { result } = renderHook(() => useRate());
vi.setSystemTime(500);
act(() => {
result.current.record(4);
});
vi.advanceTimersByTime(500);
act(() => {
result.current.record(9);
});
expect(result.current.items).toHaveLength(2);
expect(result.current.rate).toBe(5);
expect(result.current.lastUpdated).toBe(1000);
});
it("should update isUnderThreshold after [threshold]ms of no activity", () => {
const { result } = renderHook(() => useRate({ threshold: 500 }));
expect(result.current.isUnderThreshold).toBe(true);
act(() => {
// not sure if fake timers is buggy with intervals,
// but I need to call it twice to register
vi.advanceTimersToNextTimer();
vi.advanceTimersToNextTimer();
});
expect(result.current.isUnderThreshold).toBe(false);
});
it("should return an isUnderThreshold boolean", () => {
const { result } = renderHook(() => useRate({ threshold: 500 }));
vi.setSystemTime(500);
act(() => {
result.current.record(400);
});
act(() => {
result.current.record(1000);
});
expect(result.current.isUnderThreshold).toBe(false);
act(() => {
result.current.record(1500);
});
expect(result.current.isUnderThreshold).toBe(true);
act(() => {
vi.advanceTimersToNextTimer();
vi.advanceTimersToNextTimer();
});
expect(result.current.isUnderThreshold).toBe(false);
});
});

View File

@@ -2,8 +2,9 @@ import { beforeAll, describe, expect, it, vi } from "vitest";
import { render } from "@testing-library/react";
import { afterEach } from "node:test";
import { useTerminal } from "#/hooks/useTerminal";
import { SocketProvider } from "#/context/socket";
import { Command } from "#/state/commandSlice";
import { WsClientProvider } from "#/context/ws-client-provider";
import { ReactNode } from "react";
interface TestTerminalComponentProps {
commands: Command[];
@@ -18,6 +19,17 @@ function TestTerminalComponent({
return <div ref={ref} />;
}
interface WrapperProps {
children: ReactNode;
}
function Wrapper({children}: WrapperProps) {
return (
<WsClientProvider enabled={true} token="NO_JWT" ghToken="NO_GITHUB" settings={null}>{children}</WsClientProvider>
)
}
describe("useTerminal", () => {
const mockTerminal = vi.hoisted(() => ({
loadAddon: vi.fn(),
@@ -50,7 +62,7 @@ describe("useTerminal", () => {
it("should render", () => {
render(<TestTerminalComponent commands={[]} secrets={[]} />, {
wrapper: SocketProvider,
wrapper: Wrapper,
});
});
@@ -61,7 +73,7 @@ describe("useTerminal", () => {
];
render(<TestTerminalComponent commands={commands} secrets={[]} />, {
wrapper: SocketProvider,
wrapper: Wrapper,
});
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
@@ -85,7 +97,7 @@ describe("useTerminal", () => {
secrets={[secret, anotherSecret]}
/>,
{
wrapper: SocketProvider,
wrapper: Wrapper,
},
);

View File

@@ -0,0 +1,17 @@
import { describe, it, expect } from "vitest";
import store from "../src/store";
import { setInitialQuery, clearInitialQuery } from "../src/state/initial-query-slice";
describe("Initial Query Behavior", () => {
it("should clear initial query when clearInitialQuery is dispatched", () => {
// Set up initial query in the store
store.dispatch(setInitialQuery("test query"));
expect(store.getState().initalQuery.initialQuery).toBe("test query");
// Clear the initial query
store.dispatch(clearInitialQuery());
// Verify initial query is cleared
expect(store.getState().initalQuery.initialQuery).toBeNull();
});
});

View File

@@ -0,0 +1,5 @@
import { describe, it } from "vitest";
describe("App", () => {
it.todo("should render");
});

View File

@@ -0,0 +1,53 @@
import { afterEach } from "node:test";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { cache } from "#/utils/cache";
describe("Cache", () => {
const testKey = "key";
const testData = { message: "Hello, world!" };
const testTTL = 1000; // 1 second
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("gets data from memory if not expired", () => {
cache.set(testKey, testData, testTTL);
expect(cache.get(testKey)).toEqual(testData);
});
it("should expire after 5 minutes by default", () => {
cache.set(testKey, testData);
expect(cache.get(testKey)).not.toBeNull();
vi.advanceTimersByTime(5 * 60 * 1000 + 1);
expect(cache.get(testKey)).toBeNull();
});
it("returns null if cached data is expired", () => {
cache.set(testKey, testData, testTTL);
vi.advanceTimersByTime(testTTL + 1);
expect(cache.get(testKey)).toBeNull();
});
it("deletes data from memory", () => {
cache.set(testKey, testData, testTTL);
cache.delete(testKey);
expect(cache.get(testKey)).toBeNull();
});
it("clears all data with the app prefix from memory", () => {
cache.set(testKey, testData, testTTL);
cache.set("anotherKey", { data: "More data" }, testTTL);
cache.clearAll();
expect(cache.get(testKey)).toBeNull();
expect(cache.get("anotherKey")).toBeNull();
});
});

View File

@@ -59,9 +59,9 @@ describe("extractModelAndProvider", () => {
separator: "/",
});
expect(extractModelAndProvider("claude-3-5-sonnet-20241022")).toEqual({
expect(extractModelAndProvider("claude-3-5-sonnet-20240620")).toEqual({
provider: "anthropic",
model: "claude-3-5-sonnet-20241022",
model: "claude-3-5-sonnet-20240620",
separator: "/",
});
@@ -78,4 +78,3 @@ describe("extractModelAndProvider", () => {
});
});
});

View File

@@ -15,7 +15,7 @@ test("organizeModelsAndProviders", () => {
"gpt-4o",
"together-ai-21.1b-41b",
"gpt-4o-mini",
"claude-3-5-sonnet-20241022",
"anthropic/claude-3-5-sonnet-20241022",
"claude-3-haiku-20240307",
"claude-2",
"claude-2.1",
@@ -63,4 +63,3 @@ test("organizeModelsAndProviders", () => {
},
});
});

View File

@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.11.0",
"version": "0.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.11.0",
"version": "0.14.0",
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"@nextui-org/react": "^2.4.8",
@@ -26,6 +26,7 @@
"isbot": "^5.1.17",
"jose": "^5.9.4",
"monaco-editor": "^0.52.0",
"posthog-js": "^1.184.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-highlight": "^0.15.0",
@@ -45,6 +46,7 @@
"ws": "^8.18.0"
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",
@@ -61,6 +63,7 @@
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^1.6.0",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
@@ -3378,6 +3381,21 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/@playwright/test": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
"integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
"dev": true,
"dependencies": {
"playwright": "1.48.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.28",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
@@ -7864,6 +7882,16 @@
"node": ">=6.6.0"
}
},
"node_modules/core-js": {
"version": "3.38.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz",
"integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
@@ -7896,6 +7924,24 @@
}
}
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
@@ -9666,6 +9712,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -19406,6 +19457,50 @@
"pathe": "^1.1.2"
}
},
"node_modules/playwright": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
"integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
"dev": true,
"dependencies": {
"playwright-core": "1.48.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.48.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
"integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@@ -19653,6 +19748,31 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/posthog-js": {
"version": "1.184.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.184.1.tgz",
"integrity": "sha512-q/1Kdard5SZnL2smrzeKcD+RuUi2PnbidiN4D3ThK20bNrhy5Z2heIy9SnRMvEiARY5lcQ7zxmDCAKPBKGSOtQ==",
"dependencies": {
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
"web-vitals": "^4.2.0"
}
},
"node_modules/posthog-js/node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="
},
"node_modules/preact": {
"version": "10.24.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.11.0",
"version": "0.14.0",
"private": true,
"type": "module",
"engines": {
@@ -25,6 +25,7 @@
"isbot": "^5.1.17",
"jose": "^5.9.4",
"monaco-editor": "^0.52.0",
"posthog-js": "^1.184.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-highlight": "^0.15.0",
@@ -44,11 +45,12 @@
"ws": "^8.18.0"
},
"scripts": {
"dev": "npm run make-i18n && VITE_MOCK_API=false remix vite:dev",
"dev:mock": "npm run make-i18n && VITE_MOCK_API=true remix vite:dev",
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false remix vite:dev",
"dev:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true remix vite:dev",
"build": "npm run make-i18n && tsc && remix vite:build",
"start": "npx sirv-cli build/ --single",
"test": "vitest run",
"test:e2e": "playwright test",
"test:coverage": "npm run make-i18n && vitest run --coverage",
"dev_wsl": "VITE_WATCH_USE_POLLING=true vite",
"preview": "vite preview",
@@ -70,6 +72,7 @@
]
},
"devDependencies": {
"@playwright/test": "^1.48.2",
"@remix-run/dev": "^2.11.2",
"@remix-run/testing": "^2.11.2",
"@tailwindcss/typography": "^0.5.15",
@@ -86,6 +89,7 @@
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^1.6.0",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",

View File

@@ -0,0 +1,79 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev:mock -- --port 3000",
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
},
});

View File

@@ -1,4 +1,5 @@
{
"APP_MODE": "oss",
"GITHUB_CLIENT_ID": ""
}
"GITHUB_CLIENT_ID": "",
"POSTHOG_CLIENT_KEY": "phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA"
}

View File

@@ -122,6 +122,9 @@ export const retrieveGitHubUser = async (
id: data.id,
login: data.login,
avatar_url: data.avatar_url,
company: data.company,
name: data.name,
email: data.email,
};
return user;
@@ -136,33 +139,6 @@ export const retrieveGitHubUser = async (
return error;
};
/**
* Given a GitHub token and a repository name, creates a repository for the authenticated user
* @param token The GitHub token
* @param repositoryName Name of the repository to create
* @param description Description of the repository
* @param isPrivate Boolean indicating if the repository should be private
* @returns The created repository or an error response
*/
export const createGitHubRepository = async (
token: string,
repositoryName: string,
description?: string,
isPrivate = true,
): Promise<GitHubRepository | GitHubErrorReponse> => {
const response = await fetch("https://api.github.com/user/repos", {
method: "POST",
headers: generateGitHubAPIHeaders(token),
body: JSON.stringify({
name: repositoryName,
description,
private: isPrivate,
}),
});
return response.json();
};
export const retrieveLatestGitHubCommit = async (
token: string,
repository: string,

View File

@@ -1,4 +1,5 @@
import { getValidFallbackHost } from "#/utils/get-valid-fallback-host";
import { request } from "#/services/api";
import { cache } from "#/utils/cache";
import {
SaveFileSuccessResponse,
FileUploadSuccessResponse,
@@ -7,38 +8,22 @@ import {
GitHubAccessTokenResponse,
ErrorResponse,
GetConfigResponse,
GetVSCodeUrlResponse,
} from "./open-hands.types";
/**
* Generate the base URL of the OpenHands API
* @returns Base URL of the OpenHands API
*/
const generateBaseURL = () => {
const fallback = getValidFallbackHost();
const baseUrl = import.meta.env.VITE_BACKEND_BASE_URL || fallback;
if (typeof window === "undefined") {
return `http://${baseUrl}`;
}
return `${window.location.protocol}//${baseUrl}`;
};
/**
* Class to interact with the OpenHands API
*/
class OpenHands {
/**
* Base URL of the OpenHands API
*/
static BASE_URL = generateBaseURL();
/**
* Retrieve the list of models available
* @returns List of models available
*/
static async getModels(): Promise<string[]> {
const response = await fetch(`${OpenHands.BASE_URL}/api/options/models`);
return response.json();
const cachedData = cache.get<string[]>("models");
if (cachedData) return cachedData;
const data = await request("/api/options/models");
cache.set("models", data);
return data;
}
/**
@@ -46,8 +31,13 @@ class OpenHands {
* @returns List of agents available
*/
static async getAgents(): Promise<string[]> {
const response = await fetch(`${OpenHands.BASE_URL}/api/options/agents`);
return response.json();
const cachedData = cache.get<string[]>("agents");
if (cachedData) return cachedData;
const data = await request(`/api/options/agents`);
cache.set("agents", data);
return data;
}
/**
@@ -55,178 +45,150 @@ class OpenHands {
* @returns List of security analyzers available
*/
static async getSecurityAnalyzers(): Promise<string[]> {
const response = await fetch(
`${OpenHands.BASE_URL}/api/options/security-analyzers`,
);
return response.json();
const cachedData = cache.get<string[]>("agents");
if (cachedData) return cachedData;
const data = await request(`/api/options/security-analyzers`);
cache.set("security-analyzers", data);
return data;
}
static async getConfig(): Promise<GetConfigResponse> {
const response = await fetch("config.json", {
headers: {
"Cache-Control": "no-cache",
},
});
return response.json();
const cachedData = cache.get<GetConfigResponse>("config");
if (cachedData) return cachedData;
const data = await request("/config.json");
cache.set("config", data);
return data;
}
/**
* Retrieve the list of files available in the workspace
* @param token User token provided by the server
* @param path Path to list files from
* @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace
*/
static async getFiles(token: string, path?: string): Promise<string[]> {
const url = new URL(`${OpenHands.BASE_URL}/api/list-files`);
if (path) url.searchParams.append("path", path);
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.json();
static async getFiles(path?: string): Promise<string[]> {
let url = "/api/list-files";
if (path) url += `?path=${encodeURIComponent(path)}`;
return request(url);
}
/**
* Retrieve the content of a file
* @param token User token provided by the server
* @param path Full path of the file to retrieve
* @returns Content of the file
*/
static async getFile(token: string, path: string): Promise<string> {
const url = new URL(`${OpenHands.BASE_URL}/api/select-file`);
url.searchParams.append("file", path);
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
static async getFile(path: string): Promise<string> {
const url = `/api/select-file?file=${encodeURIComponent(path)}`;
const data = await request(url);
return data.code;
}
/**
* Save the content of a file
* @param token User token provided by the server
* @param path Full path of the file to save
* @param content Content to save in the file
* @returns Success message or error message
*/
static async saveFile(
token: string,
path: string,
content: string,
): Promise<SaveFileSuccessResponse | ErrorResponse> {
const response = await fetch(`${OpenHands.BASE_URL}/api/save-file`, {
return request(`/api/save-file`, {
method: "POST",
body: JSON.stringify({ filePath: path, content }),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
return response.json();
}
/**
* Upload a file to the workspace
* @param token User token provided by the server
* @param file File to upload
* @returns Success message or error message
*/
static async uploadFiles(
token: string,
file: File[],
): Promise<FileUploadSuccessResponse | ErrorResponse> {
const formData = new FormData();
file.forEach((f) => formData.append("files", f));
const response = await fetch(`${OpenHands.BASE_URL}/api/upload-files`, {
return request(`/api/upload-files`, {
method: "POST",
body: formData,
headers: {
Authorization: `Bearer ${token}`,
},
});
return response.json();
}
/**
* Get the blob of the workspace zip
* @param token User token provided by the server
* @returns Blob of the workspace zip
*/
static async getWorkspaceZip(token: string): Promise<Blob> {
const response = await fetch(`${OpenHands.BASE_URL}/api/zip-directory`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
static async getWorkspaceZip(): Promise<Blob> {
const response = await request(`/api/zip-directory`, {}, false, true);
return response.blob();
}
/**
* Send feedback to the server
* @param token User token provided by the server
* @param data Feedback data
* @returns The stored feedback data
*/
static async sendFeedback(
token: string,
data: Feedback,
): Promise<FeedbackResponse> {
const response = await fetch(`${OpenHands.BASE_URL}/api/submit-feedback`, {
static async submitFeedback(data: Feedback): Promise<FeedbackResponse> {
return request(`/api/submit-feedback`, {
method: "POST",
body: JSON.stringify(data),
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});
return response.json();
}
/**
* Get the GitHub access token
* @param code Code provided by GitHub
* @returns GitHub access token
*/
static async getGitHubAccessToken(
code: string,
): Promise<GitHubAccessTokenResponse> {
const response = await fetch(`${OpenHands.BASE_URL}/api/github/callback`, {
return request(`/api/github/callback`, {
method: "POST",
body: JSON.stringify({ code }),
headers: {
"Content-Type": "application/json",
},
});
return response.json();
}
/**
* Check if the user is authenticated
* @param login The user's GitHub login handle
* @returns Whether the user is authenticated
* Authenticate with GitHub token
* @returns Response with authentication status and user info if successful
*/
static async isAuthenticated(login: string): Promise<boolean> {
const response = await fetch(`${OpenHands.BASE_URL}/api/authenticate`, {
method: "POST",
body: JSON.stringify({ login }),
headers: {
"Content-Type": "application/json",
static async authenticate(): Promise<Response> {
return request(
`/api/authenticate`,
{
method: "POST",
},
});
true,
);
}
return response.status === 200;
/**
* Get the VSCode URL
* @returns VSCode URL
*/
static async getVSCodeUrl(): Promise<GetVSCodeUrlResponse> {
return request(`/api/vscode-url`, {}, false, false, 1);
}
static async getRuntimeId(): Promise<{ runtime_id: string }> {
const response = await request("/api/config");
const data = await response.json();
return data;
}
}

View File

@@ -27,16 +27,27 @@ export interface GitHubAccessTokenResponse {
access_token: string;
}
export interface AuthenticationResponse {
message: string;
login?: string; // Only present when allow list is enabled
}
export interface Feedback {
version: string;
email: string;
token: string;
feedback: "positive" | "negative";
polarity: "positive" | "negative";
permissions: "public" | "private";
trajectory: unknown[];
}
export interface GetConfigResponse {
APP_MODE: "saas" | "oss";
GITHUB_CLIENT_ID: string | null;
GITHUB_CLIENT_ID: string;
POSTHOG_CLIENT_KEY: string;
}
export interface GetVSCodeUrlResponse {
vscode_url: string | null;
error?: string;
}

View File

@@ -32,4 +32,4 @@
<rect width="69" height="46" fill="white" transform="translate(0.5)" />
</clipPath>
</defs>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -2,4 +2,4 @@
<path
d="M15.359 21V17.319C15.3974 16.8654 15.3314 16.4095 15.1651 15.9814C14.9989 15.5534 14.7363 15.1631 14.3949 14.8364C17.6154 14.5035 21 13.3716 21 8.17826C20.9997 6.85027 20.4489 5.57321 19.4615 4.61139C19.9291 3.44954 19.896 2.16532 19.3692 1.02548C19.3692 1.02548 18.159 0.692576 15.359 2.43321C13.0082 1.84237 10.5302 1.84237 8.17949 2.43321C5.37949 0.692576 4.16923 1.02548 4.16923 1.02548C3.64244 2.16532 3.60938 3.44954 4.07692 4.61139C3.08218 5.58034 2.53079 6.86895 2.53846 8.2068C2.53846 13.3621 5.92308 14.494 9.14359 14.865C8.80615 15.1883 8.54591 15.574 8.3798 15.9968C8.2137 16.4196 8.14544 16.8701 8.17949 17.319V21M8.17949 18.1465C3.05128 19.5732 3.05128 15.7686 1 15.293L8.17949 18.1465Z"
stroke="white" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 888 B

After

Width:  |  Height:  |  Size: 889 B

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