Compare commits

...

273 Commits

Author SHA1 Message Date
openhands
0c09a8062f Use SearchConfig for configuration instead of environment variables 2025-03-18 12:43:16 +00:00
openhands
7051669835 Revert unrelated changes to workflow and config files 2025-03-17 21:53:49 +00:00
openhands
c1c7e8dc72 Change onBrokenLinks to warn to allow build to complete 2025-03-17 21:51:22 +00:00
openhands
ecff077b5e Fix pydoc-markdown config to only document openhands module 2025-03-17 21:41:40 +00:00
openhands
4972b20986 Allow docs workflow to run on PRs targeting any branch and on forks 2025-03-17 21:34:27 +00:00
openhands
48db2a1c09 Fix docs workflow to run pydoc-markdown from root directory 2025-03-17 21:07:58 +00:00
openhands
e6ea2c8a9b Merge remote-tracking branch 'aditya/search_engine' 2025-03-17 20:41:53 +00:00
openhands
b8f04eea42 Add dedicated search configuration documentation 2025-03-17 20:33:36 +00:00
openhands
2f3b9cfe4a Add documentation for search engine feature 2025-03-17 20:16:12 +00:00
openhands
bc4e8632e1 Sort imports alphabetically 2025-03-17 17:53:02 +00:00
openhands
2015c13826 Address review comments 2025-03-17 17:47:02 +00:00
openhands
1c72e2bb31 Add tests for search engine feature 2025-03-17 17:37:19 +00:00
openhands
22e13a0a4d Fix merge conflicts with main 2025-03-17 17:30:57 +00:00
openhands
05dabbbb13 Add SEARCH action type and SearchAction to __all__ 2025-03-17 17:09:08 +00:00
openhands
de312941ee Merge main into search_engine 2025-03-17 17:06:02 +00:00
Engel Nyst
a4b836b5f9 Don't try to send the new events in the UI (#7277) 2025-03-17 14:50:22 +01:00
Xingyao Wang
a4d632498c SWE-Gym rollout stability fix & using a validated SWE-Gym set (#7182)
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-03-17 21:15:01 +08:00
Engel Nyst
4f017081fc Quick fix docs (#7299)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-17 05:50:05 +00:00
Engel Nyst
51fb1fae88 RecallObservations (#7292) 2025-03-17 03:18:22 +01:00
Graham Neubig
106b230fea Update Slack invitation links (#7296)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-17 02:06:48 +00:00
Xingyao Wang
9b262dd057 fix retry on ConnectionError & retry on remote runtime by default (#7294) 2025-03-17 01:18:54 +00:00
chuckbutkus
8074b261d3 Move current user_id to github_user_id and create a new user_id field (#7231)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-03-16 16:32:27 -04:00
dependabot[bot]
999a59f938 chore(deps): bump the version-all group with 5 updates (#7253)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-16 19:51:08 +00:00
chuckbutkus
fbba57d3b5 Fix saving of settings (#7282) 2025-03-16 19:06:46 +00:00
Engel Nyst
3f6c8a2338 Fix visual browsing (#7278)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-16 16:50:25 +01:00
Engel Nyst
dd09d46ccb Remove DelegatorAgent (fix #7280)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-16 16:49:28 +01:00
tofarr
8897b45eeb Fix for too much reaction in logs (#7276) 2025-03-16 08:21:30 -06:00
Ryan H. Tran
30109e8f20 Separate tool descriptions to support models with limited description length (#7258) 2025-03-16 09:48:13 +01:00
adityasoni9998
635dc5098a Rename env var to BRAVE_API_KEY and clean up dead code 2025-03-14 15:36:45 -04:00
adityasoni9998
3a765db531 Restore run_infer.sh. 2025-03-14 15:11:50 -04:00
adityasoni9998
929b971ef5 Minor fixes 2025-03-14 15:08:05 -04:00
adityasoni9998
d518fc8f8c Merge branch 'main' into search_engine 2025-03-14 14:51:00 -04:00
adityasoni9998
00d7425f4c Merge remote-tracking branch 'upstream/main' 2025-03-14 14:50:51 -04:00
adityasoni9998
b24e34c3eb Changes for search engine tool and minor error fix in browser process 2025-03-14 14:49:29 -04:00
adityasoni9998
a05b39d5c5 Added fixes for output formatting, scrolling, timeouts for actions other than goto, and file downloads. 2025-03-13 22:47:20 -04:00
adityasoni9998
a880f55a63 Added fixes for file downloads, goto action timeouts, and some other browser fixes. 2025-03-13 16:21:16 -04:00
adityasoni9998
35ab168b88 Merge branch 'main' into eval_fixes 2025-03-13 10:35:29 -04:00
adityasoni9998
a578cf39a9 Merge remote-tracking branch 'upstream/main' 2025-03-13 10:24:49 -04:00
adityasoni9998
d071acf501 Update browser tool description 2025-03-02 23:13:22 -05:00
adityasoni9998
f73edd7220 Merge remote-tracking branch 'upstream/main' 2025-03-02 22:10:00 -05:00
Engel Nyst
869e2911c6 [Refactor] split runtime initialization (create, connect, init) in cli scripts (#7036) 2025-03-02 22:09:50 -05:00
Graham Neubig
3004073d39 More explicit feedback message about how to report errors to developers (#7063) 2025-03-02 22:09:50 -05:00
Engel Nyst
d050f481e8 Fix GitLab CI environment variable check (issue #7050) (#7052)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:50 -05:00
Ivan Dagelic
96654069f7 chore: daytona readme quick start verbosity (#7056)
Signed-off-by: Ivan Dagelic <dagelic.ivan@gmail.com>
2025-03-02 22:09:50 -05:00
Ivan Dagelic
78db84fe38 chore: update daytona readme (#7053)
Signed-off-by: Ivan Dagelic <dagelic.ivan@gmail.com>
2025-03-02 22:09:50 -05:00
mamoodi
d6605dee56 Updates to the ISSUE TRIAGE (#7043) 2025-03-02 22:09:50 -05:00
Engel Nyst
92cc51951f Fix argument in swe-bench grading scripts (#7046) 2025-03-02 22:09:50 -05:00
David Thompson
72953443bc Update docker.py to support podman (#6778) 2025-03-02 22:09:50 -05:00
Engel Nyst
96e4831379 Separate microagent template (#7041) 2025-03-02 22:09:50 -05:00
きわみざむらい
06e698818f Create CITATION.cff (#7037) 2025-03-02 22:09:50 -05:00
Robert Brennan
ea95639d2b Remove hard error on session reuse (#7026)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-03-02 22:09:50 -05:00
Ray Myers
3e4dd38212 Structured logging mode (#7034) 2025-03-02 22:09:49 -05:00
chuckbutkus
de09b368ba Fix URL for staging stack (#7030) 2025-03-02 22:09:49 -05:00
Xingyao Wang
b69ecc5cc2 Add Kubernetes microagent (#7028)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-03-02 22:09:49 -05:00
Xingyao Wang
979135b5e1 Add Docker microagent for installation and usage (#7027)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:49 -05:00
Christopher Pereira
0dec0fbab0 Bug fixes (#6460)
Co-authored-by: OH <openhands@example.com>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-03-02 22:09:49 -05:00
chuckbutkus
69a1c9abc7 Keycloak changes (#6986) 2025-03-02 22:09:49 -05:00
Christopher Pereira
8a9fdbe7c5 Support docker_runtime_kwargs dict (#7025) 2025-03-02 22:09:49 -05:00
Calvin Smith
fae10c8856 Fix: Update context window exceeded detection (#7024)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:49 -05:00
Ryan H. Tran
21ef0890bc Add diff for edit observation and display in UI (#7014) 2025-03-02 22:09:49 -05:00
Fredy Sierra
80b955279e feat: Adding sandbox property runtime_binding_address to specify whic… (#6992) 2025-03-02 22:09:49 -05:00
Xingyao Wang
8d0e4235b4 [agent] improve finish tool for sonnet 3.7 (#7002) 2025-03-02 22:09:49 -05:00
dependabot[bot]
cdd9f5860d chore(deps-dev): bump llama-index from 0.12.20 to 0.12.21 in the llama group (#7015)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:49 -05:00
Engel Nyst
f5c794212c Refactor to a helper class for the agent's history (ConversationMemory) (#7008)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <email@cjsmith.io>
2025-03-02 22:09:49 -05:00
Magic Mai
7612a56a76 fix: Remove nested git repositories before adding files in SWE-bench (#6536)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-03-02 22:09:49 -05:00
Engel Nyst
9db439f2c0 Separate additional_info template (#6996) 2025-03-02 22:09:49 -05:00
Xingyao Wang
a74d972d99 Add Memory Monitor VSCode Extension (#6951)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:49 -05:00
Xingyao Wang
f2503fe392 [agent] Add "thinking" tool only (#6977) 2025-03-02 22:09:49 -05:00
Engel Nyst
fd27d4ffa3 Re-add separators between user messages (#7004) 2025-03-02 22:09:49 -05:00
dependabot[bot]
033f004952 chore(deps): bump the version-all group across 1 directory with 7 updates (#7005)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:49 -05:00
jaybutera
242e5b1ec6 add add_agent.md (#6891)
Co-authored-by: smd <>
2025-03-02 22:09:49 -05:00
tofarr
9c48604410 Page Refresh now restarts agent loop if status is STOPPED or ERROR (#6829) 2025-03-02 22:09:49 -05:00
Xingyao Wang
065ab7bdf5 feat: add sound and browser notifications for agent state changes (#6530)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-03-02 22:09:48 -05:00
mamoodi
57a1768a3e Release 0.27.0 (#6993) 2025-03-02 22:09:48 -05:00
tofarr
82abb2326d Refactor: Moving ConversationInfo to server module (#6981) 2025-03-02 22:09:48 -05:00
zchn
17dda08bb1 Fix image tag inconsistency in forked-PR workflows (#6998) 2025-03-02 22:09:48 -05:00
sp.wack
c839a5f0cd hotfix(frontend): Truncate long conversation card titles (#7001) 2025-03-02 22:09:48 -05:00
tofarr
d1546f4cbe Feat out of credits msg (#6969)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:48 -05:00
tofarr
cb8cfe06a6 Fix for error cleaning stale (#6971) 2025-03-02 22:09:48 -05:00
Engel Nyst
7d0befc429 Refactor sandbox and security configurations (#6973) 2025-03-02 22:09:48 -05:00
Xingyao Wang
2ebc8169c0 [eval] Upgrade SWE-Bench to use official image and latest harness (#6838)
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-03-02 22:09:48 -05:00
dependabot[bot]
39ae2bd48c chore(deps): bump react-icons from 5.4.0 to 5.5.0 in /docs in the version-all group (#6962)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-02 22:09:48 -05:00
Xingyao Wang
32fda5fce4 [agent] System message update (#6787)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:48 -05:00
Ray Myers
855ccb8c63 Add system event listeners for monitoring (#6929) 2025-03-02 22:09:48 -05:00
Engel Nyst
994f4f8d23 Azure completion_tokens fix (take two) (#6975) 2025-03-02 22:09:48 -05:00
dependabot[bot]
288f46ab4d chore(deps): bump the version-all group across 1 directory with 11 updates (#6966)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:48 -05:00
Xingyao Wang
16c54e02c9 refactor: codeact tools into separate files (#6978) 2025-03-02 22:09:48 -05:00
Engel Nyst
4f5c7d26c7 Refactor agent_config loading from toml (#6967) 2025-03-02 22:09:48 -05:00
Engel Nyst
3815cfc318 Fix microagent matching to the user message, not previous enhancements (#6963) 2025-03-02 22:09:48 -05:00
sp.wack
a44fd2c7ca hotfix(frontend): Consistent buttons and their styles throughout the app (#6835)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-03-02 22:09:48 -05:00
Engel Nyst
1a9b284b9b Add selected_repo to command line (#6949) 2025-03-02 22:09:48 -05:00
tofarr
04175c747e Fix fd leak (#6950) 2025-03-02 22:09:48 -05:00
Ray Myers
7fa0959681 Add ability to define custom runtime classes (#6955)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:48 -05:00
Engel Nyst
9858c89155 Refactor llm config from toml and clean up (#6923) 2025-03-02 22:09:47 -05:00
Ryan H. Tran
e2fb79620a fix: task_str validation not required for trajectory replay (#6957) 2025-03-02 22:09:47 -05:00
Rohit Malhotra
218dc545bf [Feat]: Adding endpoint for suggested tasks Openhands could tackle (#6844)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:47 -05:00
Robert Brennan
693da0a052 Add pause_closed_runtimes config to pause instead of stop runtimes (#6885)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:47 -05:00
Graham Neubig
facd03aa29 feat(llm): Add Claude 3.7 backend configurations (#6937)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:47 -05:00
sp.wack
aebfa7666f chore(frontend): Claude 3.7 is visible in dropdown for selection (#6931) 2025-03-02 22:09:47 -05:00
mamoodi
6d248eee06 Add documentation checkbox to PR template (#6924) 2025-03-02 22:09:47 -05:00
mamoodi
53f8dc1061 Release 0.26.0 (#6915) 2025-03-02 22:09:47 -05:00
celek
584aa7c8b0 add extended generic section (#5932)
Co-authored-by: Christophe Elek <christophe.elek@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:47 -05:00
tofarr
2422d703e3 Revert "Fix file descriptor leak (#6897)" (#6921) 2025-03-02 22:09:47 -05:00
Mateusz Kwiatkowski
03cdded50b Replace shebang with /usr/bin/env bash for improved portability (#6876)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-03-02 22:09:47 -05:00
Xingyao Wang
3e455ad323 chore: Make remote runtime class default to None (#6919) 2025-03-02 22:09:47 -05:00
Christopher Pereira
591e0d91a3 Handle Docker version string with +dfsg1 (#6732)
Co-authored-by: Christoper Pereira <kripper@gmail.com>
2025-03-02 22:09:47 -05:00
Engel Nyst
bae3887040 Small rename to long term memory (#6914) 2025-03-02 22:09:47 -05:00
tofarr
5a1697940b Fix file descriptor leak (#6897)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:47 -05:00
Graham Neubig
8ba8990a52 Fix mypy errors in core directory (#6901)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:47 -05:00
Graham Neubig
974a46c582 Fix mypy errors in security/invariant directory (#6908)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:47 -05:00
Ivan Dagelic
30d76c3733 Daytona Runtime (#6863)
Signed-off-by: jsrzic <josip.srzic@gmail.com>
Signed-off-by: Ivan Dagelic <dagelic.ivan@gmail.com>
Co-authored-by: jsrzic <65179822+jsrzic@users.noreply.github.com>
2025-03-02 22:09:47 -05:00
sp.wack
8cd6e60415 hotfix: Fix switch color regression (#6881) 2025-03-02 22:09:47 -05:00
Engel Nyst
cb3edf2885 Display session ID in CLI mode
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:47 -05:00
Engel Nyst
65524f171d Use LLM APIs responses in token counting (#5604)
Co-authored-by: Calvin Smith <email@cjsmith.io>
2025-03-02 22:09:47 -05:00
Engel Nyst
c26c7478c2 Keep the first user message by default in condensers (#6888) 2025-03-02 22:09:47 -05:00
tofarr
cf1265dec2 Fix for regression where conversations are not clickable (#6886) 2025-03-02 22:09:46 -05:00
Engel Nyst
aa640afbe0 Revert "Fix: File Descriptor leak" (#6887) 2025-03-02 22:09:46 -05:00
tofarr
afc76055d8 Fix: Increase Entropy Requirement for Secret Redaction to Reduce False Positives (#6875) 2025-03-02 22:09:46 -05:00
Dai Dao
11c37e04c1 refactor : Improve frontend setup doc and locale error (#6850)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:46 -05:00
tofarr
b32b4beb73 Fix: File Descriptor leak (#6883) 2025-03-02 22:09:46 -05:00
Robert Brennan
d36a477d3c Add info logs for microagent loading and triggering (#6882)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:46 -05:00
Calvin Smith
25b01925fe (feat): Enable memory condensation from settings page (#6868)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-03-02 22:09:46 -05:00
Graham Neubig
4915d20cdd Fix mypy errors in agenthub directory (#6811)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:46 -05:00
Graham Neubig
fe42b7e88f fix: Add missing type annotations in utils/ directory (#6687)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:46 -05:00
sp.wack
d26d925e04 chore(frontend): Remove latest conversation text in home screen (#6851) 2025-03-02 22:09:46 -05:00
tofarr
f8d468cfff Fix jumpy conversation panel (#6874) 2025-03-02 22:09:46 -05:00
Boxuan Li
58cfd16c53 Save complete trajectory in presence of history truncation (#6751)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:46 -05:00
Boxuan Li
316322a942 Add enable_history_truncation option to disable history truncation (#6820)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:46 -05:00
Xingyao Wang
aaf3e0819b Docs: Clarify config.toml usage in evaluation harness (#6828)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:46 -05:00
dependabot[bot]
18d6deeb6c chore(deps): bump the version-all group across 1 directory with 10 updates (#6870)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:46 -05:00
Engel Nyst
cf727c9ac6 Fix: Simplify prompt caching for new Anthropic API (#6860)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:46 -05:00
Rohit Malhotra
85c51410d7 [Bug]: Fix workflow definition for installation phase of resolver (#6861) 2025-03-02 22:09:46 -05:00
sp.wack
5a8056bf27 chore(frontend): Standardize custom colors used throughout the app (#6833) 2025-03-02 22:09:46 -05:00
Robert Brennan
2984ed740b Add conversation age limit configuration (#6763)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:46 -05:00
tofarr
dc045d2cde Fix: Less squashed logo (#6853) 2025-03-02 22:09:46 -05:00
sp.wack
507aa39c80 hotfix: Set proper minimum and maximum defaults that can be entered in billing input (#6842) 2025-03-02 22:09:46 -05:00
sp.wack
58ff252f50 hotfix: Remove external link in billing settings UI (#6841) 2025-03-02 22:09:46 -05:00
Calvin Smith
1ddb68e0fa fix: LLM summarization prompt handles user messages (#6837)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-03-02 22:09:46 -05:00
Engel Nyst
e5fc1afb4e Refactor I/O utils; allow 'task' command line parameter in cli.py (#6187)
Co-authored-by: OpenHands Bot <openhands@all-hands.dev>
2025-03-02 22:09:46 -05:00
Engel Nyst
336eb98ea3 Clean up NullObservations from the stream (#6260) 2025-03-02 22:09:45 -05:00
mamoodi
a25324304a Update documentation with new settings page (#6716)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:45 -05:00
mamoodi
659ca022f2 Release 0.25.0 (#6782) 2025-03-02 22:09:45 -05:00
Engel Nyst
578bf144d5 hotfix azure (#6806) 2025-03-02 22:09:45 -05:00
Xingyao Wang
ec1ce2f9fa feat: better error logging for remote runtime (#6805) 2025-03-02 22:09:45 -05:00
Ryan H. Tran
83ecc2049e Update openhands-aci to 0.2.5 (#6834)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-03-02 22:09:45 -05:00
diwu-sf
bb559a2dca Fix download workspace zip file event loop hanging (#6722) 2025-03-02 22:09:45 -05:00
Calvin Smith
d0ca8db912 fix: Avoid infinite loop with rolling condensers and history truncation (#6795)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-03-02 22:09:45 -05:00
Graham Neubig
6108434f06 Fix mypy errors in storage directory (#6809)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:45 -05:00
Ryan H. Tran
ce218a167b Fix diskcache breaking CI & eval intermittently (#6817) 2025-03-02 22:09:45 -05:00
Graham Neubig
843f092b76 Fix type checking errors in resolver directory (#6738)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:45 -05:00
Xingyao Wang
bf70965565 Add sysbox support to remote runtime for eval; Add memory monitor, stress tests to help debug memory issue (#6684)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-03-02 22:09:45 -05:00
Calvin Smith
d5ee33811a enh: Refactor Event -> Message pipeline outside of CodeActAgent (#6715)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:45 -05:00
sp.wack
a4c60d9986 feat(SaaS): Billing settings screen (#6495)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-03-02 22:09:45 -05:00
sp.wack
4f00bf4846 fix(frontend): Hide modal when in settings page if first time (#6792) 2025-03-02 22:09:45 -05:00
sp.wack
5ac5e229d6 hotfix: Conversation panel toggle should change color given state (#6791) 2025-03-02 22:09:45 -05:00
dependabot[bot]
ffa5fbe9c2 chore(deps): bump the version-all group across 1 directory with 9 updates (#6783)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-03-02 22:09:45 -05:00
sp.wack
d2398fef4b hotfix(frontend): Input set/unset state and disable runtime input (#6788) 2025-03-02 22:09:45 -05:00
sp.wack
a5ea1db1af hotfix: Consistent background color (#6786) 2025-03-02 22:09:45 -05:00
nottherealironman
a195ada8bf docs: add guide for minimum computing and storage requirements (#6575) 2025-03-02 22:09:45 -05:00
tofarr
1ef0d8fa21 CSS Fixes (#6770) 2025-03-02 22:09:44 -05:00
Rohit Malhotra
dbe2b333a0 Add selected branch to convo metadata (#6773) 2025-03-02 22:09:44 -05:00
mamoodi
e5679f6450 Update OpenHands Cloud docs with correct permissions and instructions (#6774) 2025-03-02 22:09:44 -05:00
Graham Neubig
7aeaac5261 Upgrade tree sitter (#6740)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:44 -05:00
Rohit Malhotra
2f3bd3d808 [Docs]: Cloud Openhands (#6747)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-03-02 22:09:44 -05:00
Rohit Malhotra
c42c9cbaf8 hotfix(Secrets): Add event stream filter for refreshed secret (#6764) 2025-03-02 22:09:44 -05:00
tofarr
a1ffdcea34 Enable the multi conversation UI for all users (#6374) 2025-03-02 22:09:44 -05:00
Xingyao Wang
2988b10d00 fix: disable prlimit since limiting --vm breaks nodejs (#6765) 2025-03-02 22:09:44 -05:00
tofarr
849d2719bf Improve SensitiveDataFilter and add comprehensive tests (#6755)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:44 -05:00
Robert Brennan
cb9eeae313 Fix caps in status message (#6761) 2025-03-02 22:09:44 -05:00
Robert Brennan
e5599ef094 Better LLM retry behavior (#6557)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:44 -05:00
tofarr
a3e134bdf4 Added iterate method and additional tests for search functions (#6756) 2025-03-02 22:09:44 -05:00
tofarr
ee0225c062 feat: implement optimistic updates for conversation deletion (#6745)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-03-02 22:09:44 -05:00
李师胡
debd00075d docs(runtime): fix broken links of benchmarks (#6744)
Co-authored-by: jianhao1 <jianhao1@taobao.com>
2025-03-02 22:09:44 -05:00
dependabot[bot]
c6f4dddf70 chore(deps): bump the version-all group in /frontend with 4 updates (#6725)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-03-02 22:09:44 -05:00
Arpan Koirala
af2e91d6da fix: no interaction when clearing poetry cache (#6752) 2025-03-02 22:09:44 -05:00
Christopher Pereira
643664a0a9 Show docker build errors (#6695) 2025-03-02 22:09:44 -05:00
Boxuan Li
67fbf5906f A few fixes for TAC evaluation harness (#6586) 2025-03-02 22:09:44 -05:00
Boxuan Li
42ff2b70ed Add a sanity test for load_app_config and get_agent_config_arg (#6723) 2025-03-02 22:09:44 -05:00
Cheng Yang
5e469c588b docs: improve docstrings for CLI and config utils (#5398)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:44 -05:00
dependabot[bot]
93e54418be chore(deps): bump the version-all group across 1 directory with 12 updates (#6736)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:44 -05:00
Ryan H. Tran
3ea122fa30 chore: upgrade openhands-aci to 0.2.2 (#6731) 2025-03-02 22:09:44 -05:00
Rohit Malhotra
1227e503b2 [Resolver]: Prep env in expectation of release (#6735) 2025-03-02 22:09:43 -05:00
sp.wack
f16bab079c feat(frontend): Settings screen (#6550) 2025-03-02 22:09:43 -05:00
Rohit Malhotra
c50bc33cb5 hotfix(Resolver): Workflow definition is out of sync with released package (#6719) 2025-03-02 22:09:43 -05:00
Rohit Malhotra
227f2f7dec fix: Simplify nested f-string to fix pydoc-markdown parsing (#6717) 2025-03-02 22:09:43 -05:00
wtiger9218
72237e4557 feat(resolver): implement gitlab resolver (#6458)
Signed-off-by: José Luis Di Biase <josx@interorganic.com.ar>
Co-authored-by: José Luis Di Biase <josx@interorganic.com.ar>
Co-authored-by: Oriana <oriana@camba.coop>
Co-authored-by: Charlie <charlie@camba.coop>
Co-authored-by: Juan Manuel Daza <61162223+juanmanueldaza@users.noreply.github.com>
Co-authored-by: Juan Manuel Daza <juandaza@camba.coop>
Co-authored-by: Cody Kociemba <cody@symbaventures.com>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:43 -05:00
Boxuan Li
a418d0dacf Evaluation harness: Add agent config option (#6662) 2025-03-02 22:09:43 -05:00
dependabot[bot]
871095184d chore(deps): bump the version-all group across 1 directory with 5 updates (#6712)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-03-02 22:09:43 -05:00
Calvin Smith
dc4ca3ad91 fix: Filter AgentCondensationObservation events from agent state (#6705)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-03-02 22:09:43 -05:00
tofarr
3626e29150 More effective remote runtime identification (#6714) 2025-03-02 22:09:43 -05:00
mamoodi
6c49c5e8e3 Release 0.24.0 (#6689) 2025-03-02 22:09:43 -05:00
tofarr
759a03f406 Agent session no longer stuck in starting on raised exception (#6703) 2025-03-02 22:09:43 -05:00
Rohit Malhotra
bc188694af Feat: Add selected branch param to backend (#6508) 2025-03-02 22:09:43 -05:00
sp.wack
7f5d7675ee chore: Throw a 404 instead of returning defaults if settings does not exist (#6704) 2025-03-02 22:09:43 -05:00
tofarr
1f01edb0da Fix log formatting error (#6699) 2025-03-02 22:09:43 -05:00
Xingyao Wang
70d7982548 using all available system memory when RUNTIME_MAX_MEMORY_GB is not set (#6691) 2025-03-02 22:09:43 -05:00
sp.wack
dfbd928b20 Revert "Only show start project button in conversations" (#6698) 2025-03-02 22:09:43 -05:00
Xingyao Wang
e59878af5f refactor: do not add DEBUG env var when it is not set (#6690) 2025-03-02 22:09:43 -05:00
dependabot[bot]
e2add5c57e chore(deps-dev): bump @tanstack/eslint-plugin-query from 5.66.0 to 5.66.1 in /frontend in the eslint group (#6682)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-02 22:09:43 -05:00
Robert Brennan
26f235ff00 Fix debug in remote runtime (#6688) 2025-03-02 22:09:43 -05:00
Xingyao Wang
b71b723e91 refactor(runtime): Use openhands-aci file editor directly in runtime instead of execute it through ipython (#6671)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:43 -05:00
Eric Zhang
25b73f9606 fix(frontend): fix public github repo cannot be selected (#6680) 2025-03-02 22:09:43 -05:00
Xingyao Wang
807b0bfbfe feat(runtime): use prlimit to limit resource usage of command to avoid OOM Runtime Kill (#6338)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-03-02 22:09:43 -05:00
Engel Nyst
a98f31fbdb Clean up global in llm.py (we figured it's not needed) (#6675) 2025-03-02 22:09:43 -05:00
Xingyao Wang
508dea41a0 Bump OpenHands ACI to 0.2.1 (#6678) 2025-03-02 22:09:42 -05:00
sp.wack
fba1f1b7e7 hotfix: Typecheck routes during frontend build (#6676) 2025-03-02 22:09:42 -05:00
dependabot[bot]
b60acd5073 chore(deps): bump the version-all group across 1 directory with 9 updates (#6667)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:42 -05:00
dependabot[bot]
27e39fb377 chore(deps): bump docker/setup-qemu-action from 3.3.0 to 3.4.0 (#6666)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:42 -05:00
Fredy Sierra
a0c9451a32 fix: adding support for environment variables type dict (#6672)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:42 -05:00
Graham Neubig
fcfc807bf1 fix: Normalize whitespace when comparing patch context lines (#6541)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:42 -05:00
tofarr
1a5c252043 Fix for issue where temp file is empty (#6669) 2025-03-02 22:09:42 -05:00
Graham Neubig
8e55ced1a9 Fix issue #6262: Add success/failure indicators for file read/edit operations (#6653)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:42 -05:00
Rohit Malhotra
7113d387cb [Resolver]: Add target branch param (#6668) 2025-03-02 22:09:42 -05:00
dependabot[bot]
01d23ae05e chore(deps): bump the version-all group in /frontend with 4 updates (#6665)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-03-02 22:09:42 -05:00
Rohit Malhotra
477b369f65 [Enhancement]: Handle GH token refresh inside runtime (#6632) 2025-03-02 22:09:42 -05:00
Robert Brennan
0588311628 Add comprehensive OpenHands glossary (#6310)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-03-02 22:09:42 -05:00
Rohit Malhotra
6dce886b63 [Bug fix]: Standardize SecretStr use (#6660)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:42 -05:00
tofarr
3d2268ef35 Removed in page callback (#6657) 2025-03-02 22:09:42 -05:00
dependabot[bot]
4b23a77464 chore(deps): bump the version-all group across 1 directory with 3 updates (#6648)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-03-02 22:09:42 -05:00
zchn
04d5691cf7 fix(6223): More properly add 'pyproject.toml' and 'poetry.lock' to the pip package (#6658) 2025-03-02 22:09:42 -05:00
Xingyao Wang
102f2e4d60 fix: set tool_choice to none for non-fncall models (#6652) 2025-03-02 22:09:42 -05:00
sp.wack
f2781a52c1 chore(frontend): Take into account other error message types (#6647) 2025-03-02 22:09:42 -05:00
Xingyao Wang
8cf58c62ba feat: Add LocalRuntime (#5284)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-02 22:09:42 -05:00
tofarr
72a4ece435 fix: handle SAAS mode properly in useSettings hook (#6646)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-02 22:09:42 -05:00
Graham Neubig
edea1e967e Optimize memory usage in FileEditObservation (#6622)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2025-03-02 22:09:42 -05:00
adityasoni9998
840b35097b Added search engine tool in openhands. Refine prompt for GAIA. Made browsing tool single-action 2025-03-02 22:04:50 -05:00
adityasoni9998
e9c32f708f Merge branch 'main' into eval_fixes 2025-03-01 18:41:08 -05:00
adityasoni9998
19e5e5bbc1 Changes to GAIA prompt and browser tool. 2025-03-01 18:39:33 -05:00
adityasoni9998
e561aa974b Merge remote-tracking branch 'upstream/main' 2025-03-01 16:49:23 -05:00
adityasoni9998
3b7d86ec59 Code for evaluation run 2 on GAIA. 2025-03-01 15:26:04 -05:00
adityasoni9998
677488d450 Merge branch 'main' into browser_condenser 2025-02-27 12:20:52 -05:00
adityasoni9998
3b504e10fd Merge remote-tracking branch 'upstream/main' 2025-02-27 12:10:38 -05:00
adityasoni9998
b5391f5b77 Merge remote-tracking branch 'upstream/main' 2025-02-20 18:57:57 -05:00
adityasoni9998
3fa1fb72ac Merge remote-tracking branch 'upstream/main' 2025-02-06 20:52:45 -05:00
mamoodi
03f4745f7e Add o1 to verfied models (#6642) 2025-02-06 20:52:18 -05:00
Graham Neubig
945bdd7a6a Better error logging in posthog (#6346)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-02-06 20:52:18 -05:00
sp.wack
7c1c19c095 chore(frontend): Migrate from NextUI to HeroUI via codemod (#6635) 2025-02-06 20:52:18 -05:00
mamoodi
f1911bec24 Only show start project button in conversations (#6626)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-02-06 20:52:18 -05:00
dependabot[bot]
7e08383168 chore(deps): bump the version-all group across 1 directory with 15 updates (#6617)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-02-06 20:52:18 -05:00
Peter Dave Hello
0b361f318e Update and Improve zh-TW Traditional Chinese locale (#6621) 2025-02-06 20:52:18 -05:00
Graham Neubig
6e7aa1531e Fix memory leak in JSON encoder (#6620)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2025-02-06 20:52:18 -05:00
Graham Neubig
904b2b3f9f Remove free disk space steps from workflows to test if they are necessary (#6618)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-02-06 20:52:18 -05:00
adityasoni9998
0f24032f05 Merge remote-tracking branch 'upstream/main' 2025-02-06 09:47:04 -05:00
adityasoni9998
f49b7a98d1 Merge remote-tracking branch 'upstream/main' 2025-02-05 19:13:27 -05:00
adityasoni9998
0b1ec8a621 Merge remote-tracking branch 'upstream/main' into main 2025-02-05 11:16:20 -05:00
Aditya Bharat Soni
d35a225729 Merge branch 'main' into browser_condenser 2025-02-01 21:51:27 -05:00
Aditya Bharat Soni
3b05b68d3b Merge branch 'main' into browser_condenser 2025-02-01 15:35:17 -05:00
adityasoni9998
65bf992263 Browser output condenser to condense observation outputs from browser use when providing context to the LLM. 2025-02-01 14:15:10 -05:00
adityasoni9998
83fa5b08c0 Rename visual browsing flag in agent config. 2025-02-01 11:39:44 -05:00
adityasoni9998
ee1173e841 Merge branch 'main' into codeact_browsing 2025-02-01 11:37:32 -05:00
adityasoni9998
e66a1132c5 Merge remote-tracking branch 'upstream/main' 2025-02-01 11:37:15 -05:00
adityasoni9998
d34c41261b Merge branch 'main' into codeact_browsing 2025-01-30 13:48:32 -05:00
adityasoni9998
a699a0d306 Merge remote-tracking branch 'upstream/main' 2025-01-30 13:48:13 -05:00
adityasoni9998
818533f402 Merge branch 'main' into codeact_browsing 2025-01-30 13:47:28 -05:00
adityasoni9998
26c4f72e21 Merge remote-tracking branch 'upstream/main' 2025-01-30 13:46:05 -05:00
adityasoni9998
a7d38cd421 Undo changes in package-lock.json. 2025-01-27 18:42:45 -05:00
adityasoni9998
70772ccf31 Merge remote-tracking branch 'upstream/main' into codeact_browsing 2025-01-27 18:41:19 -05:00
adityasoni9998
6b94c97cc2 Merge remote-tracking branch 'upstream/main' 2025-01-27 18:38:45 -05:00
adityasoni9998
9b742c5042 Added LLM-check for visual browsing tool usage. (not support for GPT-4o models) 2025-01-26 13:09:14 -05:00
adityasoni9998
f45e7ec29f Merge branch 'main' into codeact_browsing 2025-01-25 20:33:09 -05:00
adityasoni9998
dfee306e06 Merge remote-tracking branch 'upstream/main' 2025-01-25 20:32:23 -05:00
adityasoni9998
4315c228fb Allow screenshot-based browsing in openhands with set-of-marks annotated screenshot. 2025-01-25 20:31:17 -05:00
adityasoni9998
f5eed29995 Visual browsing using Set-of-marks annotated screenshot in CodeActAgent 2025-01-24 19:21:49 -05:00
adityasoni9998
ce0979f715 Merge remote-tracking branch 'upstream/main' 2025-01-24 13:30:05 -05:00
adityasoni9998
072d956f3b Merge remote-tracking branch 'upstream/main' 2025-01-19 19:23:27 -05:00
adityasoni9998
dcdb44863e Merge remote-tracking branch 'upstream/main' 2024-12-06 10:09:39 -05:00
adityasoni9998
5258fe8d29 Merge remote-tracking branch 'upstream/main' 2024-12-05 22:57:48 -05:00
adityasoni9998
bad7ccfee4 Merge remote-tracking branch 'upstream/main' 2024-12-05 21:38:01 -05:00
adityasoni9998
1fef52a152 Merge remote-tracking branch 'upstream/main' 2024-11-05 19:07:55 -05:00
adityasoni9998
6aba4621da Merge remote-tracking branch 'upstream/main' 2024-10-12 14:32:09 -04:00
adityasoni9998
c2505c0e34 Merge remote-tracking branch 'upstream/main' 2024-10-07 00:27:34 -04:00
adityasoni9998
a3c8bcc2b9 Merge remote-tracking branch 'upstream/main' 2024-10-05 19:24:43 -04:00
adityasoni9998
965cee7d48 added gitignore 2024-09-28 19:37:43 -04:00
79 changed files with 5614 additions and 534 deletions

View File

@@ -11,6 +11,7 @@ on:
paths:
- 'docs/**'
- '.github/workflows/deploy-docs.yml'
- 'pydoc-markdown.yml'
branches:
- main
@@ -39,7 +40,10 @@ jobs:
with:
python-version: '3.12'
- name: Generate Python Docs
run: rm -rf docs/modules/python && pip install pydoc-markdown && pydoc-markdown
run: |
rm -rf docs/modules/python
pip install pydoc-markdown
pydoc-markdown
- name: Install dependencies
run: cd docs && npm ci
- name: Build website

View File

@@ -12,7 +12,7 @@
<a href="https://codecov.io/github/All-Hands-AI/OpenHands?branch=main"><img alt="CodeCov" src="https://img.shields.io/codecov/c/github/All-Hands-AI/OpenHands?style=for-the-badge&color=blue"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw"><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/>
@@ -96,7 +96,7 @@ troubleshooting resources, and advanced configuration options.
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2ypg5jweb-d~6hObZDbXi_HEL8PDrbHg) - Here we talk about research, architecture, and future development.
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2ngejmfw6-9gW4APWOC9XUp1n~SiQ6iw) - Here we talk about research, architecture, and future development.
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.

View File

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

View File

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

View File

@@ -308,6 +308,11 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Default: `false`
- Description: Whether Jupyter is enabled in the action space
- `enable_search_engine`
- Type: `bool`
- Default: `false`
- Description: Whether the search engine tool is enabled in the action space. See [Search Configuration](./search/search-configuration.md) for details.
- `enable_history_truncation`
- Type: `bool`
- Default: `true`

View File

@@ -0,0 +1,113 @@
# Search Configuration
OpenHands provides a search engine capability that allows agents to perform web searches using the Brave Search API. This guide explains how to configure and use the search feature.
## Overview
The search engine feature enables agents to:
- Execute web search queries programmatically
- Get structured results including web pages, news, videos, and FAQs
- Avoid CAPTCHA challenges that often occur when using browser-based search
## Configuration
### Enabling Search
To enable the search engine feature, set the following in your `config.toml`:
```toml
[agent]
enable_search_engine = true
```
Or when using Docker, set the environment variable:
```bash
-e AGENT_ENABLE_SEARCH_ENGINE=true
```
### API Key Setup
The search feature requires a Brave Search API key. You can obtain one from the [Brave Search API Dashboard](https://api.search.brave.com/app/keys).
Set the API key in your `config.toml`:
```toml
[search]
enabled = true
api_key = "your-api-key-here"
```
Or when using Docker:
```bash
-e SEARCH_ENABLED=true
-e SEARCH_API_KEY="your-api-key-here"
```
## Search Results
When a search is performed, the results are returned in a structured format that includes:
- Web search results
- News articles
- Video content
- FAQ entries
- Discussion threads
- Infoboxes (when available)
- Location information (when relevant)
Each result type includes:
- Title
- URL (when applicable)
- Description or snippet
- Additional metadata specific to the result type
## Usage Example
When the search feature is enabled, agents can use the `search_engine` tool to perform searches. For example:
```python
# The agent can make a tool call like this:
{
"name": "search_engine",
"arguments": {
"query": "latest developments in AI"
}
}
```
The search results will be returned in a markdown-formatted structure that's easy for the agent to parse and understand.
## Best Practices
1. **Query Formulation**
- Keep queries focused and specific
- Include relevant keywords
- Avoid overly complex or compound queries
2. **Rate Limiting**
- Be mindful of API rate limits
- Cache results when appropriate
- Implement retries with exponential backoff for failed requests
3. **Error Handling**
- Handle API errors gracefully
- Provide meaningful feedback when searches fail
- Have fallback strategies when search is unavailable
## Troubleshooting
Common issues and solutions:
1. **Search Not Working**
- Verify `enable_search_engine` is set to `true`
- Confirm the Brave API key is correctly set
- Check API key permissions and quotas
2. **No Results**
- Verify the query is not empty
- Try reformulating the search query
- Check for any API response errors
3. **Rate Limiting**
- Monitor API usage
- Implement caching if needed
- Consider upgrading API tier if limits are consistently hit

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import copy
import json
import os
import subprocess
@@ -175,6 +176,11 @@ def process_instance(
logger.warning(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
metadata = copy.deepcopy(metadata)
metadata.details['runtime_failure_count'] = runtime_failure_count
metadata.details['remote_runtime_resource_factor'] = (
config.sandbox.remote_runtime_resource_factor
)
try:
runtime = create_runtime(config)
@@ -296,14 +302,20 @@ def process_instance(
with open(test_output_path, 'w') as f:
f.write(test_output)
try:
extra_kwargs = {}
if 'SWE-Gym' in metadata.dataset:
# SWE-Gym uses a different version of the package, hence a different eval report argument
extra_kwargs['log_path'] = test_output_path
else:
extra_kwargs['test_log_path'] = test_output_path
_report = conditional_imports.get_eval_report(
test_spec=test_spec,
prediction={
'model_patch': model_patch,
'instance_id': instance_id,
},
test_log_path=test_output_path,
include_tests_status=True,
**extra_kwargs,
)
report = _report[instance_id]
logger.info(
@@ -463,6 +475,7 @@ if __name__ == '__main__':
.decode('utf-8')
.strip(), # Current commit
dataset=args.dataset, # Dataset name from args
details={},
)
# The evaluation harness constrains the signature of `process_instance_func` but we need to

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@ def get_resource_mapping(dataset_name: str) -> dict[str, float]:
if dataset_name not in _global_resource_mapping:
file_path = os.path.join(CUR_DIR, f'{dataset_name}.json')
if not os.path.exists(file_path):
logger.warning(f'Resource mapping for {dataset_name} not found.')
logger.info(f'Resource mapping for {dataset_name} not found.')
return None
with open(file_path, 'r') as f:

View File

@@ -1,4 +1,5 @@
import asyncio
import copy
import json
import os
import tempfile
@@ -149,7 +150,8 @@ def get_config(
) -> AppConfig:
# We use a different instance image for the each instance of swe-bench eval
use_official_image = bool(
'verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower()
('verified' in metadata.dataset.lower() or 'lite' in metadata.dataset.lower())
and 'swe-gym' not in metadata.dataset.lower()
)
base_container_image = get_instance_docker_image(
instance['instance_id'], use_official_image
@@ -475,6 +477,13 @@ def process_instance(
logger.warning(
f'This is the {runtime_failure_count + 1}th attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
)
metadata = copy.deepcopy(metadata)
metadata.details['runtime_failure_count'] = runtime_failure_count
metadata.details['remote_runtime_resource_factor'] = (
config.sandbox.remote_runtime_resource_factor
)
runtime = create_runtime(config)
call_async_from_sync(runtime.connect)
@@ -560,20 +569,6 @@ def filter_dataset(dataset: pd.DataFrame, filter_column: str) -> pd.DataFrame:
return dataset
# A list of instances that are known to be tricky to infer
# (will cause runtime failure even with resource factor = 8)
SWEGYM_EXCLUDE_IDS = [
'dask__dask-10422',
'pandas-dev__pandas-50548',
'pandas-dev__pandas-53672',
'pandas-dev__pandas-54174',
'pandas-dev__pandas-55518',
'pandas-dev__pandas-58383',
'pydata__xarray-6721',
'pytest-dev__pytest-10081',
'pytest-dev__pytest-7236',
]
if __name__ == '__main__':
parser = get_parser()
parser.add_argument(
@@ -598,11 +593,20 @@ if __name__ == '__main__':
f'Loaded dataset {args.dataset} with split {args.split}: {len(swe_bench_tests)} tasks'
)
if 'SWE-Gym' in args.dataset:
swe_bench_tests = swe_bench_tests[
~swe_bench_tests['instance_id'].isin(SWEGYM_EXCLUDE_IDS)
]
with open(
os.path.join(
os.path.dirname(os.path.abspath(__file__)),
'split',
'swegym_verified_instances.json',
),
'r',
) as f:
swegym_verified_instances = json.load(f)
swe_bench_tests = swe_bench_tests[
swe_bench_tests['instance_id'].isin(swegym_verified_instances)
]
logger.info(
f'{len(swe_bench_tests)} tasks left after excluding SWE-Gym excluded tasks'
f'{len(swe_bench_tests)} tasks left after filtering for SWE-Gym verified instances'
)
llm_config = None

View File

@@ -9,7 +9,7 @@ parser.add_argument(
'--dataset_name',
type=str,
help='Name of the dataset to download',
default='princeton-nlp/SWE-bench_Lite',
default='princeton-nlp/SWE-bench_Verified',
)
parser.add_argument('--split', type=str, help='Split to download', default='test')
args = parser.parse_args()
@@ -20,7 +20,12 @@ print(
f'Downloading gold patches from {args.dataset_name} (split: {args.split}) to {output_filepath}'
)
patches = [
{'instance_id': row['instance_id'], 'model_patch': row['patch']} for row in dataset
{
'instance_id': row['instance_id'],
'model_patch': row['patch'],
'model_name_or_path': 'gold',
}
for row in dataset
]
print(f'{len(patches)} gold patches loaded')
pd.DataFrame(patches).to_json(output_filepath, lines=True, orient='records')

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -6,7 +6,6 @@ load_dotenv()
from openhands.agenthub import ( # noqa: E402
browsing_agent,
codeact_agent,
delegator_agent,
dummy_agent,
visualbrowsing_agent,
)
@@ -15,7 +14,6 @@ from openhands.controller.agent import Agent # noqa: E402
__all__ = [
'Agent',
'codeact_agent',
'delegator_agent',
'dummy_agent',
'browsing_agent',
'visualbrowsing_agent',

View File

@@ -70,9 +70,11 @@ class CodeActAgent(Agent):
codeact_enable_browsing=self.config.codeact_enable_browsing,
codeact_enable_jupyter=self.config.codeact_enable_jupyter,
codeact_enable_llm_editor=self.config.codeact_enable_llm_editor,
codeact_enable_search_engine=self.config.enable_search_engine,
llm=self.llm,
)
logger.debug(
f'TOOLS loaded for CodeActAgent: {', '.join([tool.get('function').get('name') for tool in self.tools])}'
f"TOOLS loaded for CodeActAgent: {', '.join([tool.get('function').get('name') for tool in self.tools])}"
)
self.prompt_manager = PromptManager(
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),

View File

@@ -12,13 +12,14 @@ from litellm import (
from openhands.agenthub.codeact_agent.tools import (
BrowserTool,
CmdRunTool,
FinishTool,
IPythonTool,
LLMBasedFileEditTool,
StrReplaceEditorTool,
SearchEngineTool,
ThinkTool,
WebReadTool,
create_cmd_run_tool,
create_str_replace_editor_tool,
)
from openhands.core.exceptions import (
FunctionCallNotExistsError,
@@ -36,9 +37,11 @@ from openhands.events.action import (
FileReadAction,
IPythonRunCellAction,
MessageAction,
SearchAction,
)
from openhands.events.event import FileEditSource, FileReadSource
from openhands.events.tool import ToolCallMetadata
from openhands.llm import LLM
def combine_thought(action: Action, thought: str) -> Action:
@@ -80,7 +83,7 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
# CmdRunTool (Bash)
# ================================================
if tool_call.function.name == CmdRunTool['function']['name']:
if tool_call.function.name == create_cmd_run_tool()['function']['name']:
if 'command' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "command" in tool call {tool_call.function.name}'
@@ -131,7 +134,10 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
start=arguments.get('start', 1),
end=arguments.get('end', -1),
)
elif tool_call.function.name == StrReplaceEditorTool['function']['name']:
elif (
tool_call.function.name
== create_str_replace_editor_tool()['function']['name']
):
if 'command' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "command" in tool call {tool_call.function.name}'
@@ -187,6 +193,15 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
f'Missing required argument "url" in tool call {tool_call.function.name}'
)
action = BrowseURLAction(url=arguments['url'])
# ================================================
# SearchEngineTool (search the web using text queries)
# ================================================
elif tool_call.function.name == SearchEngineTool['function']['name']:
if 'query' not in arguments:
raise FunctionCallNotExistsError(
f'Missing required argument "query" in tool call {tool_call.function.name}'
)
action = SearchAction(query=arguments['query'])
else:
raise FunctionCallNotExistsError(
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
@@ -219,8 +234,25 @@ def get_tools(
codeact_enable_browsing: bool = False,
codeact_enable_llm_editor: bool = False,
codeact_enable_jupyter: bool = False,
codeact_enable_search_engine: bool = False,
llm: LLM | None = None,
) -> list[ChatCompletionToolParam]:
tools = [CmdRunTool, ThinkTool, FinishTool]
SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-', 'o3', 'o1']
use_simplified_tool_desc = False
if llm is not None:
use_simplified_tool_desc = any(
model_substr in llm.config.model
for model_substr in SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS
)
tools = [
create_cmd_run_tool(use_simplified_description=use_simplified_tool_desc),
ThinkTool,
FinishTool,
]
if codeact_enable_search_engine:
tools.append(SearchEngineTool)
if codeact_enable_browsing:
tools.append(WebReadTool)
tools.append(BrowserTool)
@@ -229,5 +261,9 @@ def get_tools(
if codeact_enable_llm_editor:
tools.append(LLMBasedFileEditTool)
else:
tools.append(StrReplaceEditorTool)
tools.append(
create_str_replace_editor_tool(
use_simplified_description=use_simplified_tool_desc
)
)
return tools

View File

@@ -1,19 +1,21 @@
from .bash import CmdRunTool
from .bash import create_cmd_run_tool
from .browser import BrowserTool
from .finish import FinishTool
from .ipython import IPythonTool
from .llm_based_edit import LLMBasedFileEditTool
from .str_replace_editor import StrReplaceEditorTool
from .search_engine import SearchEngineTool
from .str_replace_editor import create_str_replace_editor_tool
from .think import ThinkTool
from .web_read import WebReadTool
__all__ = [
'BrowserTool',
'CmdRunTool',
'create_cmd_run_tool',
'FinishTool',
'IPythonTool',
'LLMBasedFileEditTool',
'StrReplaceEditorTool',
'SearchEngineTool',
'create_str_replace_editor_tool',
'WebReadTool',
'ThinkTool',
]

View File

@@ -1,6 +1,6 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
_DETAILED_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
### Command Execution
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, use `&&` or `;` to chain them together.
@@ -22,25 +22,39 @@ _BASH_DESCRIPTION = """Execute a bash command in the terminal within a persisten
* Output truncation: If the output exceeds a maximum length, it will be truncated before being returned.
"""
CmdRunTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='execute_bash',
description=_BASH_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'command': {
'type': 'string',
'description': 'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process. Note: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.',
},
'is_input': {
'type': 'string',
'description': 'If True, the command is an input to the running process. If False, the command is a bash command to be executed in the terminal. Default is False.',
'enum': ['true', 'false'],
_SIMPLIFIED_BASH_DESCRIPTION = """Execute a bash command in the terminal.
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process.
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together."""
def create_cmd_run_tool(
use_simplified_description: bool = False,
) -> ChatCompletionToolParam:
description = (
_SIMPLIFIED_BASH_DESCRIPTION
if use_simplified_description
else _DETAILED_BASH_DESCRIPTION
)
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='execute_bash',
description=description,
parameters={
'type': 'object',
'properties': {
'command': {
'type': 'string',
'description': 'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process. Note: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.',
},
'is_input': {
'type': 'string',
'description': 'If True, the command is an input to the running process. If False, the command is a bash command to be executed in the terminal. Default is False.',
'enum': ['true', 'false'],
},
},
'required': ['command'],
},
'required': ['command'],
},
),
)
),
)

View File

@@ -0,0 +1,24 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_SEARCH_ENGINE_DESCRIPTION = """Execute a web search query (similar to Google search).
NOTE: When you need to search for information online, please use the `search_engine` tool rather than the `browser` or `web_read` tools. The `search_engine` tool connects directly to a search engine, which will help avoid CAPTCHA challenges that would otherwise block your access.
"""
SearchEngineTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='search_engine',
description=_SEARCH_ENGINE_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'query': {
'type': 'string',
'description': 'The web search query (must be a non-empty string).',
},
},
'required': ['query'],
},
),
)

View File

@@ -1,6 +1,6 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
_DETAILED_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
* State is persistent across command calls and discussions with the user
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
* The `create` command cannot be used if the specified `path` already exists as a file
@@ -31,46 +31,73 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL:
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.
"""
StrReplaceEditorTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='str_replace_editor',
description=_STR_REPLACE_EDITOR_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'command': {
'description': 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.',
'enum': ['view', 'create', 'str_replace', 'insert', 'undo_edit'],
'type': 'string',
},
'path': {
'description': 'Absolute path to file or directory, e.g. `/workspace/file.py` or `/workspace`.',
'type': 'string',
},
'file_text': {
'description': 'Required parameter of `create` command, with the content of the file to be created.',
'type': 'string',
},
'old_str': {
'description': 'Required parameter of `str_replace` command containing the string in `path` to replace.',
'type': 'string',
},
'new_str': {
'description': 'Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.',
'type': 'string',
},
'insert_line': {
'description': 'Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.',
'type': 'integer',
},
'view_range': {
'description': 'Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.',
'items': {'type': 'integer'},
'type': 'array',
_SIMPLIFIED_STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating and editing files in plain-text format
* State is persistent across command calls and discussions with the user
* If `path` is a file, `view` displays the result of applying `cat -n`. If `path` is a directory, `view` lists non-hidden files and directories up to 2 levels deep
* The `create` command cannot be used if the specified `path` already exists as a file
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
* The `undo_edit` command will revert the last edit made to the file at `path`
Notes for using the `str_replace` command:
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
* The `new_str` parameter should contain the edited lines that should replace the `old_str`
"""
def create_str_replace_editor_tool(
use_simplified_description: bool = False,
) -> ChatCompletionToolParam:
description = (
_SIMPLIFIED_STR_REPLACE_EDITOR_DESCRIPTION
if use_simplified_description
else _DETAILED_STR_REPLACE_EDITOR_DESCRIPTION
)
return ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='str_replace_editor',
description=description,
parameters={
'type': 'object',
'properties': {
'command': {
'description': 'The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`, `undo_edit`.',
'enum': [
'view',
'create',
'str_replace',
'insert',
'undo_edit',
],
'type': 'string',
},
'path': {
'description': 'Absolute path to file or directory, e.g. `/workspace/file.py` or `/workspace`.',
'type': 'string',
},
'file_text': {
'description': 'Required parameter of `create` command, with the content of the file to be created.',
'type': 'string',
},
'old_str': {
'description': 'Required parameter of `str_replace` command containing the string in `path` to replace.',
'type': 'string',
},
'new_str': {
'description': 'Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert.',
'type': 'string',
},
'insert_line': {
'description': 'Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`.',
'type': 'integer',
},
'view_range': {
'description': 'Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file.',
'items': {'type': 'integer'},
'type': 'array',
},
},
'required': ['command', 'path'],
},
'required': ['command', 'path'],
},
),
)
),
)

View File

@@ -1,4 +0,0 @@
from openhands.agenthub.delegator_agent.agent import DelegatorAgent
from openhands.controller.agent import Agent
Agent.register('DelegatorAgent', DelegatorAgent)

View File

@@ -1,87 +0,0 @@
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.events.action import Action, AgentDelegateAction, AgentFinishAction
from openhands.events.observation import AgentDelegateObservation, Observation
from openhands.llm.llm import LLM
class DelegatorAgent(Agent):
VERSION = '1.0'
"""
The Delegator Agent is responsible for delegating tasks to other agents based on the current task.
"""
current_delegate: str = ''
def __init__(self, llm: LLM, config: AgentConfig):
"""Initialize the Delegator Agent with an LLM
Parameters:
- llm (LLM): The llm to be used by this agent
"""
super().__init__(llm, config)
def step(self, state: State) -> Action:
"""Checks to see if current step is completed, returns AgentFinishAction if True.
Otherwise, delegates the task to the next agent in the pipeline.
Parameters:
- state (State): The current state given the previous actions and observations
Returns:
- AgentFinishAction: If the last state was 'completed', 'verified', or 'abandoned'
- AgentDelegateAction: The next agent to delegate the task to
"""
if self.current_delegate == '':
self.current_delegate = 'study'
task, _ = state.get_current_user_intent()
return AgentDelegateAction(
agent='StudyRepoForTaskAgent', inputs={'task': task}
)
# last observation in history should be from the delegate
last_observation = None
for event in reversed(state.history):
if isinstance(event, Observation):
last_observation = event
break
if not isinstance(last_observation, AgentDelegateObservation):
raise Exception('Last observation is not an AgentDelegateObservation')
goal, _ = state.get_current_user_intent()
if self.current_delegate == 'study':
self.current_delegate = 'coder'
return AgentDelegateAction(
agent='CoderAgent',
inputs={
'task': goal,
'summary': last_observation.outputs['summary'],
},
)
elif self.current_delegate == 'coder':
self.current_delegate = 'verifier'
return AgentDelegateAction(
agent='VerifierAgent',
inputs={
'task': goal,
},
)
elif self.current_delegate == 'verifier':
if (
'completed' in last_observation.outputs
and last_observation.outputs['completed']
):
return AgentFinishAction()
else:
self.current_delegate = 'coder'
return AgentDelegateAction(
agent='CoderAgent',
inputs={
'task': goal,
'summary': last_observation.outputs['summary'],
},
)
else:
raise Exception('Invalid delegate state')

View File

@@ -202,6 +202,7 @@ Note:
tabs = ''
last_obs = None
last_action = None
set_of_marks = None # Initialize set_of_marks to None
if len(state.history) == 1:
# for visualwebarena, webarena and miniwob++ eval, we need to retrieve the initial observation already in browser env
@@ -217,6 +218,9 @@ Note:
# agent has responded, task finished.
return AgentFinishAction(outputs={'content': event.content})
elif isinstance(event, Observation):
# Only process BrowserOutputObservation and skip other observation types
if not isinstance(event, BrowserOutputObservation):
continue
last_obs = event
if len(prev_actions) >= 1: # ignore noop()

View File

@@ -8,6 +8,7 @@ from openhands.core.config.config_utils import (
from openhands.core.config.extended_config import ExtendedConfig
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.search_config import SearchConfig
from openhands.core.config.security_config import SecurityConfig
from openhands.core.config.utils import (
finalize_config,
@@ -28,6 +29,7 @@ __all__ = [
'AppConfig',
'LLMConfig',
'SandboxConfig',
'SearchConfig',
'SecurityConfig',
'ExtendedConfig',
'load_app_config',

View File

@@ -2,7 +2,10 @@ from __future__ import annotations
from pydantic import BaseModel, Field, ValidationError
from openhands.core.config.condenser_config import CondenserConfig, NoOpCondenserConfig
from openhands.core.config.condenser_config import (
CondenserConfig,
NoOpCondenserConfig,
)
from openhands.core.logger import openhands_logger as logger
@@ -30,6 +33,7 @@ class AgentConfig(BaseModel):
disabled_microagents: list[str] = Field(default_factory=list)
enable_history_truncation: bool = Field(default=True)
enable_som_visual_browsing: bool = Field(default=False)
enable_search_engine: bool = Field(default=False)
condenser: CondenserConfig = Field(default_factory=NoOpCondenserConfig)
model_config = {'extra': 'forbid'}

View File

@@ -12,6 +12,7 @@ from openhands.core.config.config_utils import (
from openhands.core.config.extended_config import ExtendedConfig
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.search_config import SearchConfig
from openhands.core.config.security_config import SecurityConfig
@@ -53,6 +54,7 @@ class AppConfig(BaseModel):
default_agent: str = Field(default=OH_DEFAULT_AGENT)
sandbox: SandboxConfig = Field(default_factory=SandboxConfig)
security: SecurityConfig = Field(default_factory=SecurityConfig)
search: SearchConfig = Field(default_factory=SearchConfig)
extended: ExtendedConfig = Field(default_factory=lambda: ExtendedConfig({}))
runtime: str = Field(default='docker')
file_store: str = Field(default='local')

View File

@@ -15,6 +15,7 @@ class SandboxConfig(BaseModel):
timeout: The timeout for the default sandbox action execution.
remote_runtime_init_timeout: The timeout for the remote runtime to start.
remote_runtime_api_timeout: The timeout for the remote runtime API requests.
remote_runtime_enable_retries: Whether to enable retries (on recoverable errors like requests.ConnectionError) for the remote runtime API requests.
enable_auto_lint: Whether to enable auto-lint.
use_host_network: Whether to use the host network.
runtime_binding_address: The binding address for the runtime ports. It specifies which network interface on the host machine Docker should bind the runtime ports to.
@@ -53,7 +54,7 @@ class SandboxConfig(BaseModel):
timeout: int = Field(default=120)
remote_runtime_init_timeout: int = Field(default=180)
remote_runtime_api_timeout: int = Field(default=10)
remote_runtime_enable_retries: bool = Field(default=False)
remote_runtime_enable_retries: bool = Field(default=True)
remote_runtime_class: str | None = Field(
default=None
) # can be "None" (default to gvisor) or "sysbox" (support docker inside runtime + more stable)

View File

@@ -0,0 +1,35 @@
"""Configuration for search engine functionality."""
import os
from typing import Any
from pydantic import BaseModel, Field, SecretStr
class SearchConfig(BaseModel):
"""Configuration for search engine functionality.
Attributes:
enabled: Whether search engine functionality is enabled.
api_key: The API key for the search engine.
api_url: The base URL for the search API.
"""
enabled: bool = Field(default=False)
api_key: SecretStr | None = Field(default=None)
api_url: str = Field(default="https://api.search.brave.com/res/v1/web/search")
model_config = {"extra": "forbid"}
def model_post_init(self, __context: Any) -> None:
"""Post-initialization hook to assign search-related variables to environment variables.
This ensures that these values are accessible to the search engine at runtime.
"""
super().model_post_init(__context)
# Set environment variables for search engine
if self.api_key:
os.environ["BRAVE_API_KEY"] = self.api_key.get_secret_value()
if self.api_url:
os.environ["BRAVE_API_URL"] = self.api_url

View File

@@ -240,7 +240,7 @@ class SensitiveDataFilter(logging.Filter):
if (
len(value) > 2
and value != 'default'
and any(s in key_upper for s in ('SECRET', 'KEY', 'CODE', 'TOKEN'))
and any(s in key_upper for s in ('SECRET', '_KEY', '_CODE', '_TOKEN'))
):
sensitive_values.append(value)

View File

@@ -82,6 +82,9 @@ class ActionTypeSchema(BaseModel):
SEND_PR: str = Field(default='send_pr')
"""Send a PR to github."""
SEARCH: str = Field(default='search')
"""Queries a search engine."""
RECALL: str = Field(default='recall')
"""Retrieves content from a user workspace, microagent, or other source."""

View File

@@ -49,8 +49,11 @@ class ObservationTypeSchema(BaseModel):
CONDENSE: str = Field(default='condense')
"""Result of a condensation operation."""
MICROAGENT: str = Field(default='microagent')
"""Result of a microagent retrieval operation."""
SEARCH: str = Field(default='search')
"""Result of querying a search engine."""
RECALL: str = Field(default='recall')
"""Result of a recall operation. This can be the workspace context, a microagent, or other types of information."""
ObservationType = ObservationTypeSchema()

View File

@@ -17,6 +17,7 @@ from openhands.events.action.files import (
FileWriteAction,
)
from openhands.events.action.message import MessageAction
from openhands.events.action.search_engine import SearchAction
__all__ = [
'Action',
@@ -36,5 +37,6 @@ __all__ = [
'MessageAction',
'ActionConfirmationStatus',
'AgentThinkAction',
'SearchAction',
'RecallAction',
]

View File

@@ -0,0 +1,24 @@
from dataclasses import dataclass
from typing import ClassVar
from openhands.core.schema import ActionType
from openhands.events.action.action import Action
@dataclass
class SearchAction(Action):
query: str
thought: str = ''
action: str = ActionType.SEARCH
runnable: ClassVar[bool] = True
@property
def message(self) -> str:
return f'I am querying the search engine to search for {self.query}'
def __str__(self) -> str:
ret = '**SearchAction**\n'
if self.thought:
ret += f'THOUGHT: {self.thought}\n'
ret += f'QUERY: {self.query}'
return ret

View File

@@ -3,8 +3,9 @@ from openhands.events.observation.agent import (
AgentCondensationObservation,
AgentStateChangedObservation,
AgentThinkObservation,
MicroagentObservation,
RecallObservation,
)
from openhands.events.observation.search_engine import SearchEngineObservation
from openhands.events.observation.browse import BrowserOutputObservation
from openhands.events.observation.commands import (
CmdOutputMetadata,
@@ -42,6 +43,7 @@ __all__ = [
'SuccessObservation',
'UserRejectObservation',
'AgentCondensationObservation',
'MicroagentObservation',
'SearchEngineObservation',
'RecallObservation',
'RecallType',
]

View File

@@ -60,13 +60,13 @@ class MicroagentKnowledge:
@dataclass
class MicroagentObservation(Observation):
class RecallObservation(Observation):
"""The retrieval of content from a microagent or more microagents."""
recall_type: RecallType
observation: str = ObservationType.MICROAGENT
observation: str = ObservationType.RECALL
# environment
# workspace context
repo_name: str = ''
repo_directory: str = ''
repo_instructions: str = ''
@@ -95,22 +95,36 @@ class MicroagentObservation(Observation):
@property
def message(self) -> str:
return self.__str__()
return (
'Added workspace context'
if self.recall_type == RecallType.WORKSPACE_CONTEXT
else 'Added microagent knowledge'
)
def __str__(self) -> str:
# Build a string representation of all fields
fields = [
f'recall_type={self.recall_type}',
f'repo_name={self.repo_name}',
f'repo_instructions={self.repo_instructions[:20]}...',
f'runtime_hosts={self.runtime_hosts}',
f'additional_agent_instructions={self.additional_agent_instructions[:20]}...',
]
# Only include microagent_knowledge if it's not empty
# Build a string representation
fields = []
if self.recall_type == RecallType.WORKSPACE_CONTEXT:
fields.extend(
[
f'recall_type={self.recall_type}',
f'repo_name={self.repo_name}',
f'repo_instructions={self.repo_instructions[:20]}...',
f'runtime_hosts={self.runtime_hosts}',
f'additional_agent_instructions={self.additional_agent_instructions[:20]}...',
]
)
else:
fields.extend(
[
f'recall_type={self.recall_type}',
]
)
if self.microagent_knowledge:
fields.append(
f'microagent_knowledge={", ".join([m.name for m in self.microagent_knowledge])}'
fields.extend(
[
f'microagent_knowledge={", ".join([m.name for m in self.microagent_knowledge])}',
]
)
return f'**MicroagentObservation**\n{", ".join(fields)}'
return f'**RecallObservation**\n{", ".join(fields)}'

View File

@@ -0,0 +1,22 @@
from dataclasses import dataclass
from openhands.core.schema import ObservationType
from openhands.events.observation.observation import Observation
@dataclass
class SearchEngineObservation(Observation):
query: str
observation: str = ObservationType.SEARCH
@property
def message(self) -> str:
return f'Searched for: {self.query}'
def __str__(self) -> str:
ret = (
'**SearchEngineObservation**\n'
f'Query: {self.query}\n'
f'Search Results: {self.content}\n'
)
return ret

View File

@@ -22,6 +22,7 @@ from openhands.events.action.files import (
FileWriteAction,
)
from openhands.events.action.message import MessageAction
from openhands.events.action.search_engine import SearchAction
actions = (
NullAction,
@@ -39,6 +40,7 @@ actions = (
RecallAction,
ChangeAgentStateAction,
MessageAction,
SearchAction,
)
ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]

View File

@@ -122,7 +122,7 @@ def event_to_dict(event: 'Event') -> dict:
# props is a dict whose values can include a complex object like an instance of a BaseModel subclass
# such as CmdOutputMetadata
# we serialize it along with the rest
# we also handle the Enum conversion for MicroagentObservation
# we also handle the Enum conversion for RecallObservation
d['extras'] = {
k: (v.value if isinstance(v, Enum) else _convert_pydantic_to_dict(v))
for k, v in props.items()

View File

@@ -6,7 +6,7 @@ from openhands.events.observation.agent import (
AgentStateChangedObservation,
AgentThinkObservation,
MicroagentKnowledge,
MicroagentObservation,
RecallObservation,
)
from openhands.events.observation.browse import BrowserOutputObservation
from openhands.events.observation.commands import (
@@ -43,7 +43,7 @@ observations = (
UserRejectObservation,
AgentCondensationObservation,
AgentThinkObservation,
MicroagentObservation,
RecallObservation,
)
OBSERVATION_TYPE_TO_CLASS = {
@@ -114,7 +114,7 @@ def observation_from_dict(observation: dict) -> Observation:
else:
extras['metadata'] = CmdOutputMetadata()
if observation_class is MicroagentObservation:
if observation_class is RecallObservation:
# handle the Enum conversion
if 'recall_type' in extras:
extras['recall_type'] = RecallType(extras['recall_type'])

View File

@@ -5,6 +5,7 @@ from typing import Any
import httpx
from pydantic import SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.service_types import (
AuthenticationError,
GitService,
@@ -15,7 +16,7 @@ from openhands.integrations.service_types import (
User,
)
from openhands.utils.import_utils import get_impl
from openhands.core.logger import openhands_logger as logger
class GitHubService(GitService):
BASE_URL = 'https://api.github.com'
@@ -25,6 +26,7 @@ class GitHubService(GitService):
def __init__(
self,
user_id: str | None = None,
external_auth_id: str | None = None,
external_auth_token: SecretStr | None = None,
token: SecretStr | None = None,
external_token_manager: bool = False,

View File

@@ -27,11 +27,12 @@ from openhands.events.observation import (
FileEditObservation,
FileReadObservation,
IPythonRunCellObservation,
SearchEngineObservation,
UserRejectObservation,
)
from openhands.events.observation.agent import (
MicroagentKnowledge,
MicroagentObservation,
RecallObservation,
)
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.observation import Observation
@@ -52,7 +53,6 @@ class ConversationMemory:
initial_messages: list[Message],
max_message_chars: int | None = None,
vision_is_active: bool = False,
enable_som_visual_browsing: bool = False,
) -> list[Message]:
"""Process state history into a list of messages for the LLM.
@@ -64,11 +64,13 @@ class ConversationMemory:
max_message_chars: The maximum number of characters in the content of an event included
in the prompt to the LLM. Larger observations are truncated.
vision_is_active: Whether vision is active in the LLM. If True, image URLs will be included.
enable_som_visual_browsing: Whether to enable visual browsing for the SOM model.
"""
events = condensed_history
# log visual browsing status
logger.debug(f'Visual browsing: {self.agent_config.enable_som_visual_browsing}')
# Process special events first (system prompts, etc.)
messages = initial_messages
@@ -384,20 +386,23 @@ class ConversationMemory:
elif isinstance(obs, AgentCondensationObservation):
text = truncate_content(obs.content, max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, SearchEngineObservation):
text = truncate_content(obs.content, max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif (
isinstance(obs, MicroagentObservation)
isinstance(obs, RecallObservation)
and self.agent_config.enable_prompt_extensions
):
if obs.recall_type == RecallType.WORKSPACE_CONTEXT:
# everything is optional, check if they are present
repo_info = (
RepositoryInfo(
if obs.repo_name or obs.repo_directory:
repo_info = RepositoryInfo(
repo_name=obs.repo_name or '',
repo_directory=obs.repo_directory or '',
)
if obs.repo_name or obs.repo_directory
else None
)
else:
repo_info = None
if obs.runtime_hosts or obs.additional_agent_instructions:
runtime_info = RuntimeInfo(
available_hosts=obs.runtime_hosts,
@@ -420,22 +425,49 @@ class ConversationMemory:
)
has_repo_instructions = bool(repo_instructions.strip())
# Build additional info if we have something to render
# Filter and process microagent knowledge
filtered_agents = []
if obs.microagent_knowledge:
# Exclude disabled microagents
filtered_agents = [
agent
for agent in obs.microagent_knowledge
if agent.name not in self.agent_config.disabled_microagents
]
has_microagent_knowledge = bool(filtered_agents)
# Generate appropriate content based on what is present
message_content = []
# Build the workspace context information
if has_repo_info or has_runtime_info or has_repo_instructions:
# ok, now we can build the additional info
formatted_text = self.prompt_manager.build_additional_info(
repository_info=repo_info,
runtime_info=runtime_info,
repo_instructions=repo_instructions,
formatted_workspace_text = (
self.prompt_manager.build_workspace_context(
repository_info=repo_info,
runtime_info=runtime_info,
repo_instructions=repo_instructions,
)
)
message = Message(
role='user', content=[TextContent(text=formatted_text)]
message_content.append(TextContent(text=formatted_workspace_text))
# Add microagent knowledge if present
if has_microagent_knowledge:
formatted_microagent_text = (
self.prompt_manager.build_microagent_info(
triggered_agents=filtered_agents,
)
)
message_content.append(TextContent(text=formatted_microagent_text))
# Return the combined message if we have any content
if message_content:
message = Message(role='user', content=message_content)
else:
return []
elif obs.recall_type == RecallType.KNOWLEDGE:
# Use prompt manager to build the microagent info
# First, filter out agents that appear in earlier MicroagentObservations
# First, filter out agents that appear in earlier RecallObservations
filtered_agents = self._filter_agents_in_microagent_obs(
obs, current_index, events or []
)
@@ -464,7 +496,7 @@ class ConversationMemory:
# Return empty list if no microagents to include or all were disabled
return []
elif (
isinstance(obs, MicroagentObservation)
isinstance(obs, RecallObservation)
and not self.agent_config.enable_prompt_extensions
):
# If prompt extensions are disabled, we don't add any additional info
@@ -504,12 +536,12 @@ class ConversationMemory:
break
def _filter_agents_in_microagent_obs(
self, obs: MicroagentObservation, current_index: int, events: list[Event]
self, obs: RecallObservation, current_index: int, events: list[Event]
) -> list[MicroagentKnowledge]:
"""Filter out agents that appear in earlier MicroagentObservations.
"""Filter out agents that appear in earlier RecallObservations.
Args:
obs: The current MicroagentObservation to filter
obs: The current RecallObservation to filter
current_index: The index of the current event in the events list
events: The list of all events
@@ -532,7 +564,7 @@ class ConversationMemory:
def _has_agent_in_earlier_events(
self, agent_name: str, current_index: int, events: list[Event]
) -> bool:
"""Check if an agent appears in any earlier MicroagentObservation in the event list.
"""Check if an agent appears in any earlier RecallObservation in the event list.
Args:
agent_name: The name of the agent to look for
@@ -540,13 +572,11 @@ class ConversationMemory:
events: The list of all events
Returns:
bool: True if the agent appears in an earlier MicroagentObservation, False otherwise
bool: True if the agent appears in an earlier RecallObservation, False otherwise
"""
for event in events[:current_index]:
if (
isinstance(event, MicroagentObservation)
and event.recall_type == RecallType.KNOWLEDGE
):
# Note that this check includes the WORKSPACE_CONTEXT
if isinstance(event, RecallObservation):
if any(
agent.name == agent_name for agent in event.microagent_knowledge
):

View File

@@ -9,7 +9,7 @@ from openhands.events.action.agent import RecallAction
from openhands.events.event import Event, EventSource, RecallType
from openhands.events.observation.agent import (
MicroagentKnowledge,
MicroagentObservation,
RecallObservation,
)
from openhands.events.observation.empty import NullObservation
from openhands.events.stream import EventStream, EventStreamSubscriber
@@ -31,7 +31,7 @@ GLOBAL_MICROAGENTS_DIR = os.path.join(
class Memory:
"""
Memory is a component that listens to the EventStream for information retrieval actions
(a RecallAction) and publishes observations with the content (such as MicroagentObservation).
(a RecallAction) and publishes observations with the content (such as RecallObservation).
"""
sid: str
@@ -75,48 +75,59 @@ class Memory:
async def _on_event(self, event: Event):
"""Handle an event from the event stream asynchronously."""
try:
observation: MicroagentObservation | NullObservation | None = None
if isinstance(event, RecallAction):
# if this is a workspace context recall (on first user message)
# create and add a MicroagentObservation
# with info about repo and runtime.
# create and add a RecallObservation
# with info about repo, runtime, instructions, etc. including microagent knowledge if any
if (
event.source == EventSource.USER
and event.recall_type == RecallType.WORKSPACE_CONTEXT
):
observation = self._on_first_microagent_action(event)
logger.debug('Workspace context recall')
workspace_obs: RecallObservation | NullObservation | None = None
# continue with the next handler, to include knowledge microagents if suitable for this query
assert observation is None or isinstance(
observation, MicroagentObservation
), f'Expected a MicroagentObservation, but got {type(observation)}'
observation = self._on_microagent_action(
event, prev_observation=observation
)
workspace_obs = self._on_workspace_context_recall(event)
if workspace_obs is None:
workspace_obs = NullObservation(content='')
if observation is None:
observation = NullObservation(content='')
# important: this will release the execution flow from waiting for the retrieval to complete
workspace_obs._cause = event.id # type: ignore[union-attr]
# important: this will release the execution flow from waiting for the retrieval to complete
observation._cause = event.id # type: ignore[union-attr]
self.event_stream.add_event(workspace_obs, EventSource.ENVIRONMENT)
return
self.event_stream.add_event(observation, EventSource.ENVIRONMENT)
# Handle knowledge recall (triggered microagents)
elif (
event.source == EventSource.USER
and event.recall_type == RecallType.KNOWLEDGE
):
logger.debug('Microagent knowledge recall')
microagent_obs: RecallObservation | NullObservation | None = None
microagent_obs = self._on_microagent_recall(event)
if microagent_obs is None:
microagent_obs = NullObservation(content='')
# important: this will release the execution flow from waiting for the retrieval to complete
microagent_obs._cause = event.id # type: ignore[union-attr]
self.event_stream.add_event(microagent_obs, EventSource.ENVIRONMENT)
return
except Exception as e:
error_str = f'Error: {str(e.__class__.__name__)}'
logger.error(error_str)
self.send_error_message('STATUS$ERROR_MEMORY', error_str)
return
def _on_first_microagent_action(
def _on_workspace_context_recall(
self, event: RecallAction
) -> MicroagentObservation | None:
"""Add repository and runtime information to the stream as a MicroagentObservation."""
) -> RecallObservation | None:
"""Add repository and runtime information to the stream as a RecallObservation."""
# Create ENVIRONMENT info:
# Create WORKSPACE_CONTEXT info:
# - repository_info
# - runtime_info
# - repository_instructions
# - microagent_knowledge
# Collect raw repository instructions
repo_instructions = ''
@@ -130,9 +141,17 @@ class Memory:
repo_instructions += '\n\n'
repo_instructions += microagent.content
# Find any matched microagents based on the query
microagent_knowledge = self._find_microagent_knowledge(event.query)
# Create observation if we have anything
if self.repository_info or self.runtime_info or repo_instructions:
obs = MicroagentObservation(
if (
self.repository_info
or self.runtime_info
or repo_instructions
or microagent_knowledge
):
obs = RecallObservation(
recall_type=RecallType.WORKSPACE_CONTEXT,
repo_name=self.repository_info.repo_name
if self.repository_info and self.repository_info.repo_name is not None
@@ -149,29 +168,47 @@ class Memory:
if self.runtime_info
and self.runtime_info.additional_agent_instructions is not None
else '',
microagent_knowledge=[],
content='Retrieved environment info',
microagent_knowledge=microagent_knowledge,
content='Added workspace context',
)
return obs
return None
def _on_microagent_action(
def _on_microagent_recall(
self,
event: RecallAction,
prev_observation: MicroagentObservation | None = None,
) -> MicroagentObservation | None:
"""When a microagent action triggers microagents, create a MicroagentObservation with structured data."""
# If there's no query, do nothing
query = event.query.strip()
if not query:
return prev_observation
) -> RecallObservation | None:
"""When a microagent action triggers microagents, create a RecallObservation with structured data."""
assert prev_observation is None or isinstance(
prev_observation, MicroagentObservation
), f'Expected a MicroagentObservation, but got {type(prev_observation)}'
# Find any matched microagents based on the query
microagent_knowledge = self._find_microagent_knowledge(event.query)
# Process text to find suitable microagents and create a MicroagentObservation.
# Create observation if we have anything
if microagent_knowledge:
obs = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=microagent_knowledge,
content='Retrieved knowledge from microagents',
)
return obs
return None
def _find_microagent_knowledge(self, query: str) -> list[MicroagentKnowledge]:
"""Find microagent knowledge based on a query.
Args:
query: The query to search for microagent triggers
Returns:
A list of MicroagentKnowledge objects for matched triggers
"""
recalled_content: list[MicroagentKnowledge] = []
# skip empty queries
if not query:
return recalled_content
# Search for microagent triggers in the query
for name, microagent in self.knowledge_microagents.items():
trigger = microagent.match_trigger(query)
if trigger:
@@ -183,22 +220,7 @@ class Memory:
content=microagent.content,
)
)
if recalled_content:
if prev_observation is not None:
# it may be on the first user message that already found some repo info etc
prev_observation.microagent_knowledge.extend(recalled_content)
else:
# if it's not the first user message, we may not have found any information this step
obs = MicroagentObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=recalled_content,
content='Retrieved knowledge from microagents',
)
return obs
return prev_observation
return recalled_content
def load_user_workspace_microagents(
self, user_microagents: list[BaseMicroAgent]

View File

@@ -41,6 +41,7 @@ from openhands.events.action import (
FileReadAction,
FileWriteAction,
IPythonRunCellAction,
SearchAction,
)
from openhands.events.event import FileEditSource, FileReadSource
from openhands.events.observation import (
@@ -56,6 +57,7 @@ from openhands.events.serialization import event_from_dict, event_to_dict
from openhands.runtime.browser import browse
from openhands.runtime.browser.browser_env import BrowserEnv
from openhands.runtime.plugins import ALL_PLUGINS, JupyterPlugin, Plugin, VSCodePlugin
from openhands.runtime.search_engine.brave_search import search
from openhands.runtime.utils.bash import BashSession
from openhands.runtime.utils.files import insert_lines, read_lines
from openhands.runtime.utils.memory_monitor import MemoryMonitor
@@ -163,7 +165,6 @@ class ActionExecutor:
self.start_time = time.time()
self.last_execution_time = self.start_time
self._initialized = False
self.max_memory_gb: int | None = None
if _override_max_memory_gb := os.environ.get('RUNTIME_MAX_MEMORY_GB', None):
self.max_memory_gb = int(_override_max_memory_gb)
@@ -464,6 +465,10 @@ class ActionExecutor:
async def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
return await browse(action, self.browser)
async def search(self, action: SearchAction) -> Observation:
obs = await call_sync_from_async(search, action)
return obs
def close(self):
self.memory_monitor.stop_monitoring()
if self.bash_session is not None:

View File

@@ -97,7 +97,7 @@ class Runtime(FileEditRuntimeMixin):
status_callback: Callable | None = None,
attach_to_existing: bool = False,
headless_mode: bool = False,
github_user_id: str | None = None,
user_id: str | None = None,
):
self.sid = sid
self.event_stream = event_stream
@@ -130,7 +130,7 @@ class Runtime(FileEditRuntimeMixin):
self, enable_llm_editor=config.get_agent_config().codeact_enable_llm_editor
)
self.github_user_id = github_user_id
self.user_id = user_id
def setup_initial_env(self) -> None:
if self.attach_to_existing:
@@ -220,9 +220,9 @@ class Runtime(FileEditRuntimeMixin):
assert event.timeout is not None
try:
if isinstance(event, CmdRunAction):
if self.github_user_id and '$GITHUB_TOKEN' in event.command:
if self.user_id and '$GITHUB_TOKEN' in event.command:
gh_client = GithubServiceImpl(
user_id=self.github_user_id, external_token_manager=True
external_auth_id=self.user_id, external_token_manager=True
)
token = await gh_client.get_latest_token()
if token:

View File

@@ -24,6 +24,7 @@ from openhands.events.action import (
FileReadAction,
FileWriteAction,
IPythonRunCellAction,
SearchAction,
)
from openhands.events.action.action import Action
from openhands.events.action.files import FileEditSource
@@ -59,7 +60,7 @@ class ActionExecutionClient(Runtime):
status_callback: Any | None = None,
attach_to_existing: bool = False,
headless_mode: bool = True,
github_user_id: str | None = None,
user_id: str | None = None,
):
self.session = HttpSession()
self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time
@@ -75,7 +76,7 @@ class ActionExecutionClient(Runtime):
status_callback,
attach_to_existing,
headless_mode,
github_user_id,
user_id,
)
@abstractmethod
@@ -297,6 +298,9 @@ class ActionExecutionClient(Runtime):
def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
return self.send_action_for_execution(action)
def search(self, action: SearchAction) -> Observation:
return self.send_action_for_execution(action)
def close(self) -> None:
# Make sure we don't close the session multiple times
# Can happen in evaluation

View File

@@ -1,3 +1,4 @@
import logging
import os
from typing import Callable
from urllib.parse import urlparse
@@ -45,7 +46,7 @@ class RemoteRuntime(ActionExecutionClient):
status_callback: Callable | None = None,
attach_to_existing: bool = False,
headless_mode: bool = True,
github_user_id: str | None = None,
user_id: str | None = None,
):
super().__init__(
config,
@@ -56,7 +57,7 @@ class RemoteRuntime(ActionExecutionClient):
status_callback,
attach_to_existing,
headless_mode,
github_user_id,
user_id,
)
if self.config.sandbox.api_key is None:
raise ValueError(
@@ -425,10 +426,11 @@ class RemoteRuntime(ActionExecutionClient):
return self._send_action_server_request_impl(method, url, **kwargs)
retry_decorator = tenacity.retry(
retry=tenacity.retry_if_exception_type(ConnectionError),
retry=tenacity.retry_if_exception_type(requests.ConnectionError),
stop=tenacity.stop_after_attempt(3)
| stop_if_should_exit()
| self._stop_if_closed,
before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
wait=tenacity.wait_exponential(multiplier=1, min=4, max=60),
)
return retry_decorator(self._send_action_server_request_impl)(

View File

@@ -0,0 +1,3 @@
from openhands.runtime.search_engine.brave_search import search
__all__ = ['search']

View File

@@ -0,0 +1,239 @@
import os
import re
import requests
import tenacity
from openhands.core.config import AppConfig
from openhands.events.action import SearchAction
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.search_engine import SearchEngineObservation
from openhands.utils.tenacity_stop import stop_if_should_exit
def get_title(result):
return f"### Title: {result['title']}\n" if 'title' in result else ''
def get_url(result):
return f"### URL: {result['url']}\n" if 'url' in result else ''
def get_description(result):
return (
f"### Description: {result['description']}\n" if 'description' in result else ''
)
def get_question(result):
return f"### Question: {result['question']}\n" if 'question' in result else ''
def get_answer(result):
return f"### Answer: {result['answer']}\n" if 'answer' in result else ''
def get_cluster(result):
if 'cluster' in result:
output = ''
for i, result_obj in enumerate(result['cluster']):
title = get_title(result_obj)
url = get_url(result_obj)
description = get_description(result_obj)
discussion_output = (
f'### Related webpage\n#{title}#{url}#{description}\n'
if url != ''
else ''
)
output += discussion_output
return output
else:
return ''
def response_to_markdown(results, query):
all_results = {}
# discussions
discussion_results = []
if 'discussions' in results and 'results' in results['discussions']['results']:
for result in results['discussions']['results']:
title = get_title(result)
url = get_url(result)
description = get_description(result)
cluster = get_cluster(result)
discussion_output = f'## Discussion\n{title}{url}{description}{cluster}\n'
discussion_results.append(discussion_output)
all_results['discussions'] = discussion_results
# FAQs
faq_results = []
if 'faq' in results and 'results' in results['faq']:
for result in results['faq']['results']:
title = get_title(result)
url = get_url(result)
question = get_question(result)
answer = get_answer(result)
faq_output = f'## FAQ\n{title}{url}{question}{answer}\n'
faq_results.append(faq_output)
all_results['faq'] = faq_results
# News
news_results = []
if 'news' in results and 'results' in results['news']:
for result in results['news']['results']:
title = get_title(result)
url = get_url(result)
description = get_description(result)
news_output = f'## News\n{title}{url}{description}\n'
news_results.append(news_output)
all_results['news'] = news_results
# Videos
video_results = []
if 'videos' in results and 'results' in results['videos']:
for result in results['videos']['results']:
title = get_title(result)
url = get_url(result)
description = get_description(result)
video_output = f'## Video\n{title}{url}{description}\n'
video_results.append(video_output)
all_results['videos'] = video_results
# Web Search Results
websearch_results = []
if 'web' in results and 'results' in results['web']:
for result in results['web']['results']:
title = get_title(result)
url = get_url(result)
description = get_description(result)
cluster = get_cluster(result)
if cluster:
websearch_output = f'## Webpage\n{title}{url}{description}\n{cluster}\n'
else:
websearch_output = f'## Webpage\n{title}{url}{description}\n'
websearch_results.append(websearch_output)
all_results['web'] = websearch_results
# infobox
infobox_results = []
if 'infobox' in results and 'results' in results['infobox']:
for result in results['infobox']['results']:
title = get_title(result)
url = get_url(result)
description = get_description(result)
infobox_output = f'## Infobox\n{title}{url}{description}\n'
infobox_results.append(infobox_output)
all_results['infobox'] = infobox_results
# locations
location_results = []
if 'locations' in results and 'results' in results['location']:
for result in results['locations']['results']:
title = get_title(result)
url = get_url(result)
description = get_description(result)
location_output = f'## Location\n{title}{url}{description}\n'
location_results.append(location_output)
all_results['locations'] = location_results
markdown = '# Search Results\n\n'
markdown += f'**Searched query**: {query}\n\n'
# ranked results if available
if 'mixed' in results:
for rank_type in ['main', 'top', 'side']:
if rank_type not in results['mixed']:
continue
for ranked_result in results['mixed'][rank_type]:
result_type = ranked_result['type']
if result_type in all_results:
include_all = ranked_result['all']
idx = ranked_result.get('index', None)
if include_all:
markdown += ''.join(all_results[result_type])
elif idx is not None and idx < len(all_results[result_type]):
markdown += all_results[result_type][idx]
for result_list in all_results.values():
for result in result_list:
if result in markdown:
continue
else:
markdown += result
else:
markdown += ''.join(
websearch_results
+ video_results
+ news_results
+ infobox_results
+ faq_results
+ discussion_results
+ location_results
)
return markdown
def return_error(retry_state: tenacity.RetryCallState):
return ErrorObservation('Failed to query Brave Search API.')
@tenacity.retry(
wait=tenacity.wait_exponential(min=2, max=10),
stop=tenacity.stop_after_attempt(5) | stop_if_should_exit(),
retry_error_callback=return_error,
)
def query_api(query: str, API_KEY, BRAVE_SEARCH_URL):
headers = {'Accept': 'application/json', 'X-Subscription-Token': API_KEY}
params: list[tuple[str, str | int | bool]] = [
('q', query),
('count', 20), # Number of results to return, max allowed = 20
('extra_snippets', False), # TODO: Should we keep it as true?
]
response = requests.get(
BRAVE_SEARCH_URL,
headers=headers,
params=params, # type: ignore
timeout=10,
)
response.raise_for_status() # Raise exception for 4XX/5XX responses
results = response.json()
markdown_content = response_to_markdown(results, query)
# TODO: Handle other types of HTML tags? I couldn't find any other tags in brave search responses for the queries I tried.
markdown_content = re.sub(r'</?strong>', '', markdown_content)
return SearchEngineObservation(query=query, content=markdown_content)
def search(action: SearchAction, config: AppConfig):
"""Execute a search query using the Brave Search API.
Args:
action: The search action containing the query.
config: The application configuration.
Returns:
SearchEngineObservation: The search results in markdown format.
ErrorObservation: If the query is empty or search is not enabled.
"""
if not config.search.enabled:
return ErrorObservation(
content='Search engine functionality is not enabled. Enable it by setting search.enabled=true in config.'
)
query = action.query
if query is None or len(query.strip()) == 0:
return ErrorObservation(
content='The query string for search_engine tool must be a non-empty string.'
)
if config.search.api_key is None:
return ErrorObservation(
content='Search API key not configured. Set search.api_key in config.'
)
return query_api(
query=query,
API_KEY=config.search.api_key.get_secret_value(),
BRAVE_SEARCH_URL=config.search.api_url
)

View File

@@ -46,7 +46,12 @@ class ConversationManager(ABC):
@abstractmethod
async def join_conversation(
self, sid: str, connection_id: str, settings: Settings, user_id: str | None
self,
sid: str,
connection_id: str,
settings: Settings,
user_id: str | None,
github_user_id: str | None,
) -> EventStream | None:
"""Join a conversation and return its event stream."""
@@ -74,6 +79,7 @@ class ConversationManager(ABC):
settings: Settings,
user_id: str | None,
initial_user_msg: MessageAction | None = None,
github_user_id: str | None = None,
) -> EventStream:
"""Start an event loop if one is not already running"""

View File

@@ -106,7 +106,12 @@ class StandaloneConversationManager(ConversationManager):
return c
async def join_conversation(
self, sid: str, connection_id: str, settings: Settings, user_id: str | None
self,
sid: str,
connection_id: str,
settings: Settings,
user_id: str | None,
github_user_id: str | None,
):
logger.info(
f'join_conversation:{sid}:{connection_id}',
@@ -116,7 +121,9 @@ class StandaloneConversationManager(ConversationManager):
self._local_connection_id_to_session_id[connection_id] = sid
event_stream = await self._get_event_stream(sid)
if not event_stream:
return await self.maybe_start_agent_loop(sid, settings, user_id)
return await self.maybe_start_agent_loop(
sid, settings, user_id, github_user_id=github_user_id
)
for event in event_stream.get_events(reverse=True):
if isinstance(event, AgentStateChangedObservation):
if event.agent_state in (
@@ -187,14 +194,18 @@ class StandaloneConversationManager(ConversationManager):
logger.error('error_cleaning_stale')
await asyncio.sleep(_CLEANUP_INTERVAL)
async def _get_conversation_store(self, user_id: str | None) -> ConversationStore:
async def _get_conversation_store(
self, user_id: str | None, github_user_id: str | None
) -> ConversationStore:
conversation_store_class = self._conversation_store_class
if not conversation_store_class:
self._conversation_store_class = conversation_store_class = get_impl(
ConversationStore, # type: ignore
self.server_config.conversation_store_class,
)
store = await conversation_store_class.get_instance(self.config, user_id)
store = await conversation_store_class.get_instance(
self.config, user_id, github_user_id
)
return store
async def get_running_agent_loops(
@@ -243,6 +254,7 @@ class StandaloneConversationManager(ConversationManager):
settings: Settings,
user_id: str | None,
initial_user_msg: MessageAction | None = None,
github_user_id: str | None = None,
) -> EventStream:
logger.info(f'maybe_start_agent_loop:{sid}', extra={'session_id': sid})
session: Session | None = None
@@ -256,7 +268,9 @@ class StandaloneConversationManager(ConversationManager):
extra={'session_id': sid, 'user_id': user_id},
)
# Get the conversations sorted (oldest first)
conversation_store = await self._get_conversation_store(user_id)
conversation_store = await self._get_conversation_store(
user_id, github_user_id
)
conversations = await conversation_store.get_all_metadata(response_ids)
conversations.sort(key=_last_updated_at_key, reverse=True)
@@ -277,7 +291,9 @@ class StandaloneConversationManager(ConversationManager):
try:
session.agent_session.event_stream.subscribe(
EventStreamSubscriber.SERVER,
self._create_conversation_update_callback(user_id, sid),
self._create_conversation_update_callback(
user_id, github_user_id, sid
),
UPDATED_AT_CALLBACK_ID,
)
except ValueError:
@@ -374,22 +390,23 @@ class StandaloneConversationManager(ConversationManager):
)
def _create_conversation_update_callback(
self, user_id: str | None, conversation_id: str
self, user_id: str | None, github_user_id: str | None, conversation_id: str
) -> Callable:
def callback(*args, **kwargs):
call_async_from_sync(
self._update_timestamp_for_conversation,
GENERAL_TIMEOUT,
user_id,
github_user_id,
conversation_id,
)
return callback
async def _update_timestamp_for_conversation(
self, user_id: str, conversation_id: str
self, user_id: str, github_user_id: str, conversation_id: str
):
conversation_store = await self._get_conversation_store(user_id)
conversation_store = await self._get_conversation_store(user_id, github_user_id)
conversation = await conversation_store.get_metadata(conversation_id)
conversation.last_updated_at = datetime.now(timezone.utc)
await conversation_store.save_metadata(conversation)

View File

@@ -6,10 +6,14 @@ from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
NullAction,
)
from openhands.events.action.agent import RecallAction
from openhands.events.observation import (
NullObservation,
)
from openhands.events.observation.agent import AgentStateChangedObservation
from openhands.events.observation.agent import (
AgentStateChangedObservation,
RecallObservation,
)
from openhands.events.serialization import event_to_dict
from openhands.events.stream import AsyncEventStreamWrapper
from openhands.server.shared import (
@@ -35,7 +39,9 @@ async def connect(connection_id: str, environ):
cookies_str = environ.get('HTTP_COOKIE', '')
conversation_validator = ConversationValidatorImpl()
user_id = await conversation_validator.validate(conversation_id, cookies_str)
user_id, github_user_id = await conversation_validator.validate(
conversation_id, cookies_str
)
settings_store = await SettingsStoreImpl.get_instance(config, user_id)
settings = await settings_store.load()
@@ -46,7 +52,7 @@ async def connect(connection_id: str, environ):
)
event_stream = await conversation_manager.join_conversation(
conversation_id, connection_id, settings, user_id
conversation_id, connection_id, settings, user_id, github_user_id
)
agent_state_changed = None
@@ -54,10 +60,7 @@ async def connect(connection_id: str, environ):
async for event in async_stream:
if isinstance(
event,
(
NullAction,
NullObservation,
),
(NullAction, NullObservation, RecallAction, RecallObservation),
):
continue
elif isinstance(event, AgentStateChangedObservation):

View File

@@ -10,7 +10,12 @@ from openhands.events.action.message import MessageAction
from openhands.integrations.github.github_service import GithubServiceImpl
from openhands.integrations.provider import ProviderType
from openhands.runtime import get_runtime_cls
from openhands.server.auth import get_provider_tokens, get_access_token, get_github_user_id
from openhands.server.auth import (
get_access_token,
get_github_user_id,
get_provider_tokens,
get_user_id,
)
from openhands.server.data_models.conversation_info import ConversationInfo
from openhands.server.data_models.conversation_info_result_set import (
ConversationInfoResultSet,
@@ -73,12 +78,12 @@ async def _create_new_conversation(
logger.warn('Settings not present, not starting conversation')
raise MissingSettingsError('Settings not found')
session_init_args['github_token'] = token or SecretStr('')
session_init_args['provider_token'] = token
session_init_args['selected_repository'] = selected_repository
session_init_args['selected_branch'] = selected_branch
conversation_init_data = ConversationInitData(**session_init_args)
logger.info('Loading conversation store')
conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
conversation_store = await ConversationStoreImpl.get_instance(config, user_id, None)
logger.info('Conversation store loaded')
conversation_id = uuid.uuid4().hex
@@ -100,7 +105,8 @@ async def _create_new_conversation(
ConversationMetadata(
conversation_id=conversation_id,
title=conversation_title,
github_user_id=user_id,
user_id=user_id,
github_user_id=None,
selected_repository=selected_repository,
selected_branch=selected_branch,
)
@@ -122,7 +128,10 @@ async def _create_new_conversation(
image_urls=image_urls or [],
)
await conversation_manager.maybe_start_agent_loop(
conversation_id, conversation_init_data, user_id, initial_message_action
conversation_id,
conversation_init_data,
user_id,
initial_user_msg=initial_message_action,
)
logger.info(f'Finished initializing conversation {conversation_id}')
@@ -158,7 +167,7 @@ async def new_conversation(request: Request, data: InitSessionRequest):
try:
# Create conversation with initial message
conversation_id = await _create_new_conversation(
user_id,
get_user_id(request),
github_token,
selected_repository,
selected_branch,
@@ -197,7 +206,7 @@ async def search_conversations(
limit: int = 20,
) -> ConversationInfoResultSet:
conversation_store = await ConversationStoreImpl.get_instance(
config, get_github_user_id(request)
config, get_user_id(request), get_github_user_id(request)
)
conversation_metadata_result_set = await conversation_store.search(page_id, limit)
@@ -216,7 +225,7 @@ async def search_conversations(
conversation.conversation_id for conversation in filtered_results
)
running_conversations = await conversation_manager.get_running_agent_loops(
get_github_user_id(request), set(conversation_ids)
get_user_id(request), set(conversation_ids)
)
result = ConversationInfoResultSet(
results=await wait_all(
@@ -236,7 +245,7 @@ async def get_conversation(
conversation_id: str, request: Request
) -> ConversationInfo | None:
conversation_store = await ConversationStoreImpl.get_instance(
config, get_github_user_id(request)
config, get_user_id(request), get_github_user_id(request)
)
try:
metadata = await conversation_store.get_metadata(conversation_id)
@@ -252,7 +261,7 @@ async def update_conversation(
request: Request, conversation_id: str, title: str = Body(embed=True)
) -> bool:
conversation_store = await ConversationStoreImpl.get_instance(
config, get_github_user_id(request)
config, get_user_id(request), get_github_user_id(request)
)
metadata = await conversation_store.get_metadata(conversation_id)
if not metadata:
@@ -268,7 +277,7 @@ async def delete_conversation(
request: Request,
) -> bool:
conversation_store = await ConversationStoreImpl.get_instance(
config, get_github_user_id(request)
config, get_user_id(request), get_github_user_id(request)
)
try:
await conversation_store.get_metadata(conversation_id)

View File

@@ -90,30 +90,38 @@ async def store_settings(
existing_settings.user_consents_to_analytics
)
if existing_settings.secrets_store:
existing_providers = [
provider.value
for provider in existing_settings.secrets_store.provider_tokens
]
# Merge incoming settings store with the existing one
for provider, token_value in settings.provider_tokens.items():
if provider in existing_providers and not token_value:
provider_type = ProviderType(provider)
existing_token = (
existing_settings.secrets_store.provider_tokens.get(
provider_type
)
)
if existing_token and existing_token.token:
settings.provider_tokens[provider] = (
existing_token.token.get_secret_value()
)
# Merge provider tokens with existing ones
if settings.unset_github_token: # Only merge if not unsetting tokens
if settings.unset_github_token:
settings.secrets_store.provider_tokens = {}
settings.provider_tokens = {}
else: # Only merge if not unsetting tokens
if settings.provider_tokens:
if existing_settings.secrets_store:
existing_providers = [
provider.value
for provider in existing_settings.secrets_store.provider_tokens
]
# Merge incoming settings store with the existing one
for provider, token_value in settings.provider_tokens.items():
if provider in existing_providers and not token_value:
provider_type = ProviderType(provider)
existing_token = (
existing_settings.secrets_store.provider_tokens.get(
provider_type
)
)
if existing_token and existing_token.token:
settings.provider_tokens[provider] = (
existing_token.token.get_secret_value()
)
else: # nothing passed in means keep current settings
provider_tokens = existing_settings.secrets_store.provider_tokens
settings.provider_tokens = {
provider.value: data.token.get_secret_value()
if data.token
else None
for provider, data in provider_tokens.items()
}
# Update sandbox config with new settings
if settings.remote_runtime_resource_factor is not None:

View File

@@ -53,7 +53,7 @@ class AgentSession:
sid: str,
file_store: FileStore,
status_callback: Callable | None = None,
github_user_id: str | None = None,
user_id: str | None = None,
):
"""Initializes a new instance of the Session class
@@ -66,9 +66,9 @@ class AgentSession:
self.event_stream = EventStream(sid, file_store)
self.file_store = file_store
self._status_callback = status_callback
self.github_user_id = github_user_id
self.user_id = user_id
self.logger = OpenHandsLoggerAdapter(
extra={'session_id': sid, 'user_id': github_user_id}
extra={'session_id': sid, 'user_id': user_id}
)
async def start(
@@ -241,7 +241,7 @@ class AgentSession:
kwargs = {}
if runtime_cls == RemoteRuntime:
kwargs['github_user_id'] = self.github_user_id
kwargs['user_id'] = self.user_id
self.runtime = runtime_cls(
config=config,

View File

@@ -8,6 +8,6 @@ class ConversationInitData(Settings):
Session initialization data for the web environment - a deep copy of the global config is made and then overridden with this data.
"""
github_token: SecretStr | None = Field(default=None)
provider_token: SecretStr | None = Field(default=None)
selected_repository: str | None = Field(default=None)
selected_branch: str | None = Field(default=None)

View File

@@ -61,7 +61,7 @@ class Session:
sid,
file_store,
status_callback=self.queue_status_message,
github_user_id=user_id,
user_id=user_id,
)
self.agent_session.event_stream.subscribe(
EventStreamSubscriber.SERVER, self.on_event, self.sid
@@ -123,11 +123,11 @@ class Session:
agent = Agent.get_cls(agent_cls)(llm, agent_config)
github_token = None
provider_token = None
selected_repository = None
selected_branch = None
if isinstance(settings, ConversationInitData):
github_token = settings.github_token
provider_token = settings.provider_token
selected_repository = settings.selected_repository
selected_branch = settings.selected_branch
@@ -140,7 +140,7 @@ class Session:
max_budget_per_task=self.config.max_budget_per_task,
agent_to_llm_config=self.config.get_agent_to_llm_config_map(),
agent_configs=self.config.get_agent_configs(),
github_token=github_token,
github_token=provider_token,
selected_repository=selected_repository,
selected_branch=selected_branch,
initial_message=initial_message,

View File

@@ -43,7 +43,7 @@ class Settings(BaseModel):
if context and context.get('expose_secrets', False):
return llm_api_key.get_secret_value()
return pydantic_encoder(llm_api_key)
return pydantic_encoder(llm_api_key) if llm_api_key else None
@staticmethod
def _convert_token_value(

View File

@@ -12,25 +12,36 @@ from openhands.utils.async_utils import wait_all
class ConversationStore(ABC):
"""
Storage for conversation metadata. May or may not support multiple users depending on the environment
"""
"""Storage for conversation metadata. May or may not support multiple users depending on the environment."""
@abstractmethod
async def save_metadata(self, metadata: ConversationMetadata) -> None:
"""Store conversation metadata"""
"""Store conversation metadata."""
@abstractmethod
async def get_metadata(self, conversation_id: str) -> ConversationMetadata:
"""Load conversation metadata"""
"""Load conversation metadata."""
async def validate_metadata(
self, conversation_id: str, user_id: str, github_user_id: str
) -> bool:
"""Validate that conversation belongs to the current user."""
# TODO: remove github_user_id after transition to Keycloak is complete.
metadata = await self.get_metadata(conversation_id)
if (not metadata.user_id and not metadata.github_user_id) or (
metadata.user_id != user_id and metadata.github_user_id != github_user_id
):
return False
else:
return True
@abstractmethod
async def delete_metadata(self, conversation_id: str) -> None:
"""delete conversation metadata"""
"""Delete conversation metadata."""
@abstractmethod
async def exists(self, conversation_id: str) -> bool:
"""Check if conversation exists"""
"""Check if conversation exists."""
@abstractmethod
async def search(
@@ -49,6 +60,6 @@ class ConversationStore(ABC):
@classmethod
@abstractmethod
async def get_instance(
cls, config: AppConfig, user_id: str | None
cls, config: AppConfig, user_id: str | None, github_user_id: str | None
) -> ConversationStore:
"""Get a store for the user represented by the token given"""

View File

@@ -7,7 +7,7 @@ class ConversationValidator:
"""Storage for conversation metadata. May or may not support multiple users depending on the environment."""
async def validate(self, conversation_id: str, cookies_str: str):
return None
return None, None
conversation_validator_cls = os.environ.get(

View File

@@ -101,7 +101,7 @@ class FileConversationStore(ConversationStore):
@classmethod
async def get_instance(
cls, config: AppConfig, user_id: str | None
cls, config: AppConfig, user_id: str | None, github_user_id: str | None
) -> FileConversationStore:
file_store = get_file_store(config.file_store, config.file_store_path)
return FileConversationStore(file_store)

View File

@@ -5,6 +5,7 @@ from datetime import datetime, timezone
@dataclass
class ConversationMetadata:
conversation_id: str
user_id: str | None
github_user_id: str | None
selected_repository: str | None
selected_branch: str | None = None

View File

@@ -76,7 +76,7 @@ class PromptManager:
if example_message:
message.content.insert(0, TextContent(text=example_message))
def build_additional_info(
def build_workspace_context(
self,
repository_info: RepositoryInfo | None,
runtime_info: RuntimeInfo | None,

74
poetry.lock generated
View File

@@ -496,18 +496,18 @@ files = [
[[package]]
name = "boto3"
version = "1.37.11"
version = "1.37.12"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "boto3-1.37.11-py3-none-any.whl", hash = "sha256:da6c22fc8a7e9bca5d7fc465a877ac3d45b6b086d776bd1a6c55bdde60523741"},
{file = "boto3-1.37.11.tar.gz", hash = "sha256:8eec08363ef5db05c2fbf58e89f0c0de6276cda2fdce01e76b3b5f423cd5c0f4"},
{file = "boto3-1.37.12-py3-none-any.whl", hash = "sha256:516feaa0d2afaeda1515216fd09291368a1215754bbccb0f28414c0a91a830a2"},
{file = "boto3-1.37.12.tar.gz", hash = "sha256:9412d404f103ad6d14f033eb29cd5e0cdca2b9b08cbfa9d4dabd1d7be2de2625"},
]
[package.dependencies]
botocore = ">=1.37.11,<1.38.0"
botocore = ">=1.37.12,<1.38.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.11.0,<0.12.0"
@@ -516,14 +516,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
version = "1.37.11"
version = "1.37.12"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "botocore-1.37.11-py3-none-any.whl", hash = "sha256:02505309b1235f9f15a6da79103ca224b3f3dc5f6a62f8630fbb2c6ed05e2da8"},
{file = "botocore-1.37.11.tar.gz", hash = "sha256:72eb3a9a58b064be26ba154e5e56373633b58f951941c340ace0d379590d98b5"},
{file = "botocore-1.37.12-py3-none-any.whl", hash = "sha256:ba1948c883bbabe20d95ff62c3e36954c9269686f7db9361857835677ca3e676"},
{file = "botocore-1.37.12.tar.gz", hash = "sha256:ae2d5328ce6ad02eb615270507235a6e90fd3eeed615a6c0732b5a68b12f2017"},
]
[package.dependencies]
@@ -3547,14 +3547,14 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (>
[[package]]
name = "jupyterlab"
version = "4.3.5"
version = "4.3.6"
description = "JupyterLab computational environment"
optional = false
python-versions = ">=3.8"
groups = ["runtime"]
files = [
{file = "jupyterlab-4.3.5-py3-none-any.whl", hash = "sha256:571bbdee20e4c5321ab5195bc41cf92a75a5cff886be5e57ce78dfa37a5e9fdb"},
{file = "jupyterlab-4.3.5.tar.gz", hash = "sha256:c779bf72ced007d7d29d5bcef128e7fdda96ea69299e19b04a43635a7d641f9d"},
{file = "jupyterlab-4.3.6-py3-none-any.whl", hash = "sha256:fc9eb0455562a56a9bd6d2977cf090842f321fa1a298fcee9bf8c19de353d5fd"},
{file = "jupyterlab-4.3.6.tar.gz", hash = "sha256:2900ffdbfca9ed37c4ad7fdda3eb76582fd945d46962af3ac64741ae2d6b2ff4"},
]
[package.dependencies]
@@ -4251,14 +4251,14 @@ files = [
[[package]]
name = "modal"
version = "0.73.98"
version = "0.73.102"
description = "Python client library for Modal"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
files = [
{file = "modal-0.73.98-py3-none-any.whl", hash = "sha256:a49cd5f5b46d1a6c6a0d528618d3cbb73ac2908e199716590ec3a5275d79ed98"},
{file = "modal-0.73.98.tar.gz", hash = "sha256:817f73c222fa39a16d6888a92eb7a6847ecae574e44ef04e2dce5e534bdd2df9"},
{file = "modal-0.73.102-py3-none-any.whl", hash = "sha256:26151ef6164e0b93b0d1961f73d5a715deb72f23e2641215f5410cf58bf403d3"},
{file = "modal-0.73.102.tar.gz", hash = "sha256:198876cf94ff13633283e251d8b37cc1f1bb5e27a7aa547e02072def1f29b66e"},
]
[package.dependencies]
@@ -4670,19 +4670,19 @@ files = [
[[package]]
name = "notebook"
version = "7.3.2"
version = "7.3.3"
description = "Jupyter Notebook - A web-based notebook environment for interactive computing"
optional = false
python-versions = ">=3.8"
groups = ["runtime"]
files = [
{file = "notebook-7.3.2-py3-none-any.whl", hash = "sha256:e5f85fc59b69d3618d73cf27544418193ff8e8058d5bf61d315ce4f473556288"},
{file = "notebook-7.3.2.tar.gz", hash = "sha256:705e83a1785f45b383bf3ee13cb76680b92d24f56fb0c7d2136fe1d850cd3ca8"},
{file = "notebook-7.3.3-py3-none-any.whl", hash = "sha256:b193df0878956562d5171c8e25c9252b8e86c9fcc16163b8ee3fe6c5e3f422f7"},
{file = "notebook-7.3.3.tar.gz", hash = "sha256:707a313fb882d35f921989eb3d204de942ed5132a44e4aa1fe0e8f24bb9dc25d"},
]
[package.dependencies]
jupyter-server = ">=2.4.0,<3"
jupyterlab = ">=4.3.4,<4.4"
jupyterlab = ">=4.3.6,<4.4"
jupyterlab-server = ">=2.27.1,<3"
notebook-shim = ">=0.2,<0.3"
tornado = ">=6.2.0"
@@ -6947,30 +6947,30 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.9.10"
version = "0.11.0"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev", "evaluation"]
files = [
{file = "ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d"},
{file = "ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d"},
{file = "ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d"},
{file = "ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c"},
{file = "ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e"},
{file = "ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12"},
{file = "ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16"},
{file = "ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52"},
{file = "ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1"},
{file = "ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c"},
{file = "ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43"},
{file = "ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c"},
{file = "ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5"},
{file = "ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8"},
{file = "ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029"},
{file = "ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1"},
{file = "ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69"},
{file = "ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7"},
{file = "ruff-0.11.0-py3-none-linux_armv6l.whl", hash = "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb"},
{file = "ruff-0.11.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639"},
{file = "ruff-0.11.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88"},
{file = "ruff-0.11.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2"},
{file = "ruff-0.11.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8"},
{file = "ruff-0.11.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905"},
{file = "ruff-0.11.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329"},
{file = "ruff-0.11.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844"},
{file = "ruff-0.11.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e"},
{file = "ruff-0.11.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db"},
{file = "ruff-0.11.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445"},
{file = "ruff-0.11.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7"},
{file = "ruff-0.11.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7"},
{file = "ruff-0.11.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6"},
{file = "ruff-0.11.0-py3-none-win32.whl", hash = "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2"},
{file = "ruff-0.11.0-py3-none-win_amd64.whl", hash = "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21"},
{file = "ruff-0.11.0-py3-none-win_arm64.whl", hash = "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657"},
{file = "ruff-0.11.0.tar.gz", hash = "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2"},
]
[[package]]
@@ -9056,4 +9056,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "6a644bc65782a717a49718496bd279ecb888807ec625d992af4448cc5d9271c1"
content-hash = "9b74f62a4afa719a1f7167e0b3b45cdaf282c2e18fd2931da91c0f1b22776178"

View File

@@ -80,7 +80,7 @@ daytona-sdk = "0.10.2"
python-json-logger = "^3.2.1"
[tool.poetry.group.dev.dependencies]
ruff = "0.9.10"
ruff = "0.11.0"
mypy = "1.15.0"
pre-commit = "4.1.0"
build = "*"

View File

@@ -19,7 +19,7 @@ from openhands.events.event import RecallType
from openhands.events.observation import (
ErrorObservation,
)
from openhands.events.observation.agent import MicroagentObservation
from openhands.events.observation.agent import RecallObservation
from openhands.events.serialization import event_to_dict
from openhands.llm import LLM
from openhands.llm.metrics import Metrics, TokenUsage
@@ -192,7 +192,7 @@ async def test_run_controller_with_fatal_error(test_event_stream, mock_memory):
def on_event_memory(event: Event):
if isinstance(event, RecallAction):
microagent_obs = MicroagentObservation(
microagent_obs = RecallObservation(
content='Test microagent content',
recall_type=RecallType.KNOWLEDGE,
)
@@ -249,7 +249,7 @@ async def test_run_controller_stop_with_stuck(test_event_stream, mock_memory):
def on_event_memory(event: Event):
if isinstance(event, RecallAction):
microagent_obs = MicroagentObservation(
microagent_obs = RecallObservation(
content='Test microagent content',
recall_type=RecallType.KNOWLEDGE,
)
@@ -596,7 +596,7 @@ async def test_run_controller_max_iterations_has_metrics(
def on_event_memory(event: Event):
if isinstance(event, RecallAction):
microagent_obs = MicroagentObservation(
microagent_obs = RecallObservation(
content='Test microagent content',
recall_type=RecallType.KNOWLEDGE,
)
@@ -718,7 +718,7 @@ async def test_run_controller_with_context_window_exceeded_with_truncation(
def on_event_memory(event: Event):
if isinstance(event, RecallAction):
microagent_obs = MicroagentObservation(
microagent_obs = RecallObservation(
content='Test microagent content',
recall_type=RecallType.KNOWLEDGE,
)
@@ -795,7 +795,7 @@ async def test_run_controller_with_context_window_exceeded_without_truncation(
def on_event_memory(event: Event):
if isinstance(event, RecallAction):
microagent_obs = MicroagentObservation(
microagent_obs = RecallObservation(
content='Test microagent content',
recall_type=RecallType.KNOWLEDGE,
)
@@ -845,23 +845,30 @@ async def test_run_controller_with_memory_error(test_event_stream):
config = AppConfig()
event_stream = test_event_stream
# Create a propert agent that returns an action without an ID
agent = MagicMock(spec=Agent)
agent.llm = MagicMock(spec=LLM)
agent.llm.metrics = Metrics()
agent.llm.config = config.get_llm_config()
# Create a real action to return from the mocked step function
def agent_step_fn(state):
return MessageAction(content='Agent returned a message')
agent.step = agent_step_fn
runtime = MagicMock(spec=Runtime)
runtime.event_stream = event_stream
# Create a real Memory instance
memory = Memory(event_stream=event_stream, sid='test-memory')
# Patch the _on_microagent_action method to raise our test exception
def mock_on_microagent_action(*args, **kwargs):
# Patch the _find_microagent_knowledge method to raise our test exception
def mock_find_microagent_knowledge(*args, **kwargs):
raise RuntimeError('Test memory error')
with patch.object(
memory, '_on_microagent_action', side_effect=mock_on_microagent_action
memory, '_find_microagent_knowledge', side_effect=mock_find_microagent_knowledge
):
state = await run_controller(
config=config,

View File

@@ -19,7 +19,7 @@ from openhands.events.action import (
)
from openhands.events.action.agent import RecallAction
from openhands.events.event import Event, RecallType
from openhands.events.observation.agent import MicroagentObservation
from openhands.events.observation.agent import RecallObservation
from openhands.events.stream import EventStreamSubscriber
from openhands.llm.llm import LLM
from openhands.llm.metrics import Metrics
@@ -86,10 +86,10 @@ async def test_delegation_flow(mock_parent_agent, mock_child_agent, mock_event_s
def on_event(event: Event):
if isinstance(event, RecallAction):
# create a MicroagentObservation
microagent_observation = MicroagentObservation(
# create a RecallObservation
microagent_observation = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
content='microagent',
content='Found info',
)
microagent_observation._cause = event.id # ignore attr-defined warning
mock_event_stream.add_event(microagent_observation, EventSource.ENVIRONMENT)
@@ -111,14 +111,14 @@ async def test_delegation_flow(mock_parent_agent, mock_child_agent, mock_event_s
# Give time for the async step() to execute
await asyncio.sleep(1)
# Verify that a MicroagentObservation was added to the event stream
# Verify that a RecallObservation was added to the event stream
events = list(mock_event_stream.get_events())
assert (
mock_event_stream.get_latest_event_id() == 3
) # Microagents and AgentChangeState
# a MicroagentObservation and an AgentDelegateAction should be in the list
assert any(isinstance(event, MicroagentObservation) for event in events)
# a RecallObservation and an AgentDelegateAction should be in the list
assert any(isinstance(event, RecallObservation) for event in events)
assert any(isinstance(event, AgentDelegateAction) for event in events)
# Verify that a delegate agent controller is created

View File

@@ -0,0 +1,83 @@
"""Tests for the Brave Search functionality."""
from unittest.mock import Mock, patch
import pytest
from openhands.core.config import AppConfig, SearchConfig
from openhands.events.action import SearchAction
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.search_engine import SearchEngineObservation
from openhands.runtime.search_engine.brave_search import search
@pytest.fixture
def mock_config():
"""Create a mock config with search enabled."""
config = AppConfig()
config.search = SearchConfig(
enabled=True,
api_key="test_key",
api_url="https://test.url"
)
return config
@pytest.fixture
def mock_query_api():
"""Create a mock query_api function."""
with patch("openhands.runtime.search_engine.brave_search.query_api") as mock:
mock.return_value = SearchEngineObservation(
query="test query",
content="test content"
)
yield mock
def test_search_disabled(mock_query_api):
"""Test that search returns error when disabled."""
config = AppConfig()
config.search = SearchConfig(enabled=False)
action = SearchAction(query="test query")
result = search(action, config)
assert isinstance(result, ErrorObservation)
assert "not enabled" in result.content
mock_query_api.assert_not_called()
def test_search_no_api_key(mock_query_api):
"""Test that search returns error when API key is not set."""
config = AppConfig()
config.search = SearchConfig(enabled=True)
action = SearchAction(query="test query")
result = search(action, config)
assert isinstance(result, ErrorObservation)
assert "API key not configured" in result.content
mock_query_api.assert_not_called()
def test_search_empty_query(mock_query_api, mock_config):
"""Test that search returns error when query is empty."""
action = SearchAction(query="")
result = search(action, mock_config)
assert isinstance(result, ErrorObservation)
assert "must be a non-empty string" in result.content
mock_query_api.assert_not_called()
def test_search_success(mock_query_api, mock_config):
"""Test that search returns results when everything is configured correctly."""
action = SearchAction(query="test query")
result = search(action, mock_config)
assert isinstance(result, SearchEngineObservation)
assert result.query == "test query"
assert result.content == "test content"
mock_query_api.assert_called_once_with(
query="test query",
API_KEY="test_key",
BRAVE_SEARCH_URL="https://test.url"
)

View File

@@ -6,11 +6,11 @@ from litellm import ChatCompletionMessageToolCall
from openhands.agenthub.codeact_agent.codeact_agent import CodeActAgent
from openhands.agenthub.codeact_agent.function_calling import (
BrowserTool,
CmdRunTool,
IPythonTool,
LLMBasedFileEditTool,
StrReplaceEditorTool,
WebReadTool,
create_cmd_run_tool,
create_str_replace_editor_tool,
get_tools,
response_to_actions,
)
@@ -25,6 +25,7 @@ from openhands.core.message import ImageContent, Message, TextContent
from openhands.events.action import (
CmdRunAction,
MessageAction,
SearchAction,
)
from openhands.events.event import EventSource
from openhands.events.observation.commands import (
@@ -100,25 +101,30 @@ def test_get_tools_with_options():
codeact_enable_browsing=True,
codeact_enable_jupyter=True,
codeact_enable_llm_editor=True,
codeact_enable_search_engine=True,
)
tool_names = [tool['function']['name'] for tool in tools]
assert 'browser' in tool_names
assert 'execute_ipython_cell' in tool_names
assert 'edit_file' in tool_names
assert 'search_engine' in tool_names
# Test with all options disabled
tools = get_tools(
codeact_enable_browsing=False,
codeact_enable_jupyter=False,
codeact_enable_llm_editor=False,
codeact_enable_search_engine=False,
)
tool_names = [tool['function']['name'] for tool in tools]
assert 'browser' not in tool_names
assert 'execute_ipython_cell' not in tool_names
assert 'edit_file' not in tool_names
assert 'search_engine' not in tool_names
def test_cmd_run_tool():
CmdRunTool = create_cmd_run_tool()
assert CmdRunTool['type'] == 'function'
assert CmdRunTool['function']['name'] == 'execute_bash'
assert 'command' in CmdRunTool['function']['parameters']['properties']
@@ -149,6 +155,7 @@ def test_llm_based_file_edit_tool():
def test_str_replace_editor_tool():
StrReplaceEditorTool = create_str_replace_editor_tool()
assert StrReplaceEditorTool['type'] == 'function'
assert StrReplaceEditorTool['function']['name'] == 'str_replace_editor'
@@ -174,6 +181,15 @@ def test_web_read_tool():
assert WebReadTool['function']['parameters']['required'] == ['url']
def test_search_engine_tool():
from openhands.agenthub.codeact_agent.tools import SearchEngineTool
assert SearchEngineTool['type'] == 'function'
assert SearchEngineTool['function']['name'] == 'search_engine'
assert 'query' in SearchEngineTool['function']['parameters']['properties']
assert SearchEngineTool['function']['parameters']['required'] == ['query']
def test_browser_tool():
assert BrowserTool['type'] == 'function'
assert BrowserTool['function']['name'] == 'browser'
@@ -210,6 +226,42 @@ def test_browser_tool():
assert 'description' in BrowserTool['function']['parameters']['properties']['code']
def test_response_to_actions_search_engine():
# Test response with search engine tool call
from litellm import ChatCompletionMessageToolCall, Choices, Message, ModelResponse
mock_response = ModelResponse(
id='mock_id',
choices=[
Choices(
message=Message(
content='Let me search for that',
tool_calls=[
ChatCompletionMessageToolCall(
id='tool_call_10',
function={
'name': 'search_engine',
'arguments': '{"query": "test query"}',
},
type='function',
)
],
role='assistant',
),
index=0,
finish_reason='tool_calls',
)
],
model='mock_model',
usage={'total_tokens': 100},
)
actions = response_to_actions(mock_response)
assert len(actions) == 1
assert isinstance(actions[0], SearchAction)
assert actions[0].query == 'test query'
def test_response_to_actions_invalid_tool():
# Test response with invalid tool call
mock_response = Mock()
@@ -236,7 +288,11 @@ def test_step_with_no_pending_actions(mock_state: State):
mock_response.choices[0].message.content = 'Task completed'
mock_response.choices[0].message.tool_calls = []
mock_config = Mock()
mock_config.model = 'mock_model'
llm = Mock()
llm.config = mock_config
llm.completion = Mock(return_value=mock_response)
llm.is_function_calling_active = Mock(return_value=True) # Enable function calling
llm.is_caching_prompt_active = Mock(return_value=False)
@@ -260,6 +316,28 @@ def test_step_with_no_pending_actions(mock_state: State):
assert action.content == 'Task completed'
def test_correct_tool_description_loaded_based_on_model_name(mock_state: State):
"""Tests that the simplified tool descriptions are loaded for specific models."""
o3_mock_config = Mock()
o3_mock_config.model = 'mock_o3_model'
llm = Mock()
llm.config = o3_mock_config
agent = CodeActAgent(llm=llm, config=AgentConfig())
for tool in agent.tools:
# Assert all descriptions have less than 1024 characters
assert len(tool['function']['description']) < 1024
sonnet_mock_config = Mock()
sonnet_mock_config.model = 'mock_sonnet_model'
llm.config = sonnet_mock_config
agent = CodeActAgent(llm=llm, config=AgentConfig())
# Assert existence of the detailed tool descriptions that are longer than 1024 characters
assert any(len(tool['function']['description']) > 1024 for tool in agent.tools)
def test_mismatched_tool_call_events(mock_state: State):
"""Tests that the agent can convert mismatched tool call events (i.e., an observation with no corresponding action) into messages."""
agent = CodeActAgent(llm=LLM(LLMConfig()), config=AgentConfig())

View File

@@ -32,6 +32,7 @@ def _patch_store():
'selected_repository': 'foobar',
'conversation_id': 'some_conversation_id',
'github_user_id': '12345',
'user_id': '12345',
'created_at': '2025-01-01T00:00:00+00:00',
'last_updated_at': '2025-01-01T00:01:00+00:00',
}

View File

@@ -22,7 +22,7 @@ from openhands.events.event import (
from openhands.events.observation import CmdOutputObservation
from openhands.events.observation.agent import (
MicroagentKnowledge,
MicroagentObservation,
RecallObservation,
)
from openhands.events.observation.browse import BrowserOutputObservation
from openhands.events.observation.commands import (
@@ -51,7 +51,7 @@ def agent_config():
def conversation_memory(agent_config):
prompt_manager = MagicMock(spec=PromptManager)
prompt_manager.get_system_message.return_value = 'System message'
prompt_manager.build_additional_info.return_value = (
prompt_manager.build_workspace_context.return_value = (
'Formatted repository and runtime info'
)
@@ -353,10 +353,10 @@ def test_process_events_with_user_reject_observation(conversation_memory):
def test_process_events_with_empty_environment_info(conversation_memory):
"""Test that empty environment info observations return an empty list of messages without calling build_additional_info."""
# Create a MicroagentObservation with empty info
"""Test that empty environment info observations return an empty list of messages without calling build_workspace_context."""
# Create a RecallObservation with empty info
empty_obs = MicroagentObservation(
empty_obs = RecallObservation(
recall_type=RecallType.WORKSPACE_CONTEXT,
repo_name='',
repo_directory='',
@@ -382,8 +382,8 @@ def test_process_events_with_empty_environment_info(conversation_memory):
assert len(messages) == 1
assert messages[0].role == 'system'
# Verify that build_additional_info was NOT called since all input values were empty
conversation_memory.prompt_manager.build_additional_info.assert_not_called()
# Verify that build_workspace_context was NOT called since all input values were empty
conversation_memory.prompt_manager.build_workspace_context.assert_not_called()
def test_process_events_with_function_calling_observation(conversation_memory):
@@ -527,8 +527,8 @@ def test_apply_prompt_caching(conversation_memory):
def test_process_events_with_environment_microagent_observation(conversation_memory):
"""Test processing a MicroagentObservation with ENVIRONMENT info type."""
obs = MicroagentObservation(
"""Test processing a RecallObservation with ENVIRONMENT info type."""
obs = RecallObservation(
recall_type=RecallType.WORKSPACE_CONTEXT,
repo_name='test-repo',
repo_directory='/path/to/repo',
@@ -556,8 +556,8 @@ def test_process_events_with_environment_microagent_observation(conversation_mem
assert result.content[0].text == 'Formatted repository and runtime info'
# Verify the prompt_manager was called with the correct parameters
conversation_memory.prompt_manager.build_additional_info.assert_called_once()
call_args = conversation_memory.prompt_manager.build_additional_info.call_args[1]
conversation_memory.prompt_manager.build_workspace_context.assert_called_once()
call_args = conversation_memory.prompt_manager.build_workspace_context.call_args[1]
assert isinstance(call_args['repository_info'], RepositoryInfo)
assert call_args['repository_info'].repo_name == 'test-repo'
assert call_args['repository_info'].repo_directory == '/path/to/repo'
@@ -572,7 +572,7 @@ def test_process_events_with_environment_microagent_observation(conversation_mem
def test_process_events_with_knowledge_microagent_microagent_observation(
conversation_memory,
):
"""Test processing a MicroagentObservation with KNOWLEDGE type."""
"""Test processing a RecallObservation with KNOWLEDGE type."""
microagent_knowledge = [
MicroagentKnowledge(
name='test_agent',
@@ -591,7 +591,7 @@ def test_process_events_with_knowledge_microagent_microagent_observation(
),
]
obs = MicroagentObservation(
obs = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=microagent_knowledge,
content='Retrieved knowledge from microagents',
@@ -634,11 +634,11 @@ def test_process_events_with_knowledge_microagent_microagent_observation(
def test_process_events_with_microagent_observation_extensions_disabled(
agent_config, conversation_memory
):
"""Test processing a MicroagentObservation when prompt extensions are disabled."""
"""Test processing a RecallObservation when prompt extensions are disabled."""
# Modify the agent config to disable prompt extensions
agent_config.enable_prompt_extensions = False
obs = MicroagentObservation(
obs = RecallObservation(
recall_type=RecallType.WORKSPACE_CONTEXT,
repo_name='test-repo',
repo_directory='/path/to/repo',
@@ -656,18 +656,18 @@ def test_process_events_with_microagent_observation_extensions_disabled(
vision_is_active=False,
)
# When prompt extensions are disabled, the MicroagentObservation should be ignored
# When prompt extensions are disabled, the RecallObservation should be ignored
assert len(messages) == 1 # Only the initial system message
assert messages[0].role == 'system'
# Verify the prompt_manager was not called
conversation_memory.prompt_manager.build_additional_info.assert_not_called()
conversation_memory.prompt_manager.build_workspace_context.assert_not_called()
conversation_memory.prompt_manager.build_microagent_info.assert_not_called()
def test_process_events_with_empty_microagent_knowledge(conversation_memory):
"""Test processing a MicroagentObservation with empty microagent knowledge."""
obs = MicroagentObservation(
"""Test processing a RecallObservation with empty microagent knowledge."""
obs = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[],
content='Retrieved knowledge from microagents',
@@ -693,7 +693,7 @@ def test_process_events_with_empty_microagent_knowledge(conversation_memory):
def test_conversation_memory_processes_microagent_observation(prompt_dir):
"""Test that ConversationMemory processes MicroagentObservations correctly."""
"""Test that ConversationMemory processes RecallObservations correctly."""
# Create a microagent_info.j2 template file
template_path = os.path.join(prompt_dir, 'microagent_info.j2')
if not os.path.exists(template_path):
@@ -722,8 +722,8 @@ It may or may not be relevant to the user's request.
config=agent_config, prompt_manager=prompt_manager
)
# Create a MicroagentObservation with microagent knowledge
microagent_observation = MicroagentObservation(
# Create a RecallObservation with microagent knowledge
microagent_observation = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
MicroagentKnowledge(
@@ -761,7 +761,7 @@ This is triggered content for testing.
def test_conversation_memory_processes_environment_microagent_observation(prompt_dir):
"""Test that ConversationMemory processes environment info MicroagentObservations correctly."""
"""Test that ConversationMemory processes environment info RecallObservations correctly."""
# Create an additional_info.j2 template file
template_path = os.path.join(prompt_dir, 'additional_info.j2')
if not os.path.exists(template_path):
@@ -802,8 +802,8 @@ each of which has a corresponding port:
config=agent_config, prompt_manager=prompt_manager
)
# Create a MicroagentObservation with environment info
microagent_observation = MicroagentObservation(
# Create a RecallObservation with environment info
microagent_observation = RecallObservation(
recall_type=RecallType.WORKSPACE_CONTEXT,
repo_name='owner/repo',
repo_directory='/workspace/repo',
@@ -839,13 +839,13 @@ each of which has a corresponding port:
def test_process_events_with_microagent_observation_deduplication(conversation_memory):
"""Test that MicroagentObservations are properly deduplicated based on agent name.
"""Test that RecallObservations are properly deduplicated based on agent name.
The deduplication logic should keep the FIRST occurrence of each microagent
and filter out later occurrences to avoid redundant information.
"""
# Create a sequence of MicroagentObservations with overlapping agents
obs1 = MicroagentObservation(
# Create a sequence of RecallObservations with overlapping agents
obs1 = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
MicroagentKnowledge(
@@ -867,7 +867,7 @@ def test_process_events_with_microagent_observation_deduplication(conversation_m
content='First retrieval',
)
obs2 = MicroagentObservation(
obs2 = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
MicroagentKnowledge(
@@ -879,7 +879,7 @@ def test_process_events_with_microagent_observation_deduplication(conversation_m
content='Second retrieval',
)
obs3 = MicroagentObservation(
obs3 = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
MicroagentKnowledge(
@@ -918,8 +918,8 @@ def test_process_events_with_microagent_observation_deduplication_disabled_agent
conversation_memory,
):
"""Test that disabled agents are filtered out and deduplication keeps the first occurrence."""
# Create a sequence of MicroagentObservations with disabled agents
obs1 = MicroagentObservation(
# Create a sequence of RecallObservations with disabled agents
obs1 = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
MicroagentKnowledge(
@@ -936,7 +936,7 @@ def test_process_events_with_microagent_observation_deduplication_disabled_agent
content='First retrieval',
)
obs2 = MicroagentObservation(
obs2 = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
MicroagentKnowledge(
@@ -973,8 +973,8 @@ def test_process_events_with_microagent_observation_deduplication_disabled_agent
def test_process_events_with_microagent_observation_deduplication_empty(
conversation_memory,
):
"""Test that empty MicroagentObservations are handled correctly."""
obs = MicroagentObservation(
"""Test that empty RecallObservations are handled correctly."""
obs = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[],
content='Empty retrieval',
@@ -991,7 +991,7 @@ def test_process_events_with_microagent_observation_deduplication_empty(
vision_is_active=False,
)
# Verify that empty MicroagentObservations are handled gracefully
# Verify that empty RecallObservations are handled gracefully
assert (
len(messages) == 1
) # system message, because an empty microagent is not added to Messages
@@ -999,8 +999,8 @@ def test_process_events_with_microagent_observation_deduplication_empty(
def test_has_agent_in_earlier_events(conversation_memory):
"""Test the _has_agent_in_earlier_events helper method."""
# Create test MicroagentObservations
obs1 = MicroagentObservation(
# Create test RecallObservations
obs1 = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
MicroagentKnowledge(
@@ -1012,7 +1012,7 @@ def test_has_agent_in_earlier_events(conversation_memory):
content='First retrieval',
)
obs2 = MicroagentObservation(
obs2 = RecallObservation(
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
MicroagentKnowledge(
@@ -1024,7 +1024,7 @@ def test_has_agent_in_earlier_events(conversation_memory):
content='Second retrieval',
)
obs3 = MicroagentObservation(
obs3 = RecallObservation(
recall_type=RecallType.WORKSPACE_CONTEXT,
content='Environment info',
)

View File

@@ -13,7 +13,8 @@ async def test_load_store():
store = FileConversationStore(InMemoryFileStore({}))
expected = ConversationMetadata(
conversation_id='some-conversation-id',
github_user_id='some-user-id',
user_id='some-user-id',
github_user_id='12345',
selected_repository='some-repo',
title="Let's talk about trains",
)
@@ -31,6 +32,7 @@ async def test_load_int_user_id():
{
'conversation_id': 'some-conversation-id',
'github_user_id': 12345,
'user_id': '67890',
'selected_repository': 'some-repo',
'title': "Let's talk about trains",
'created_at': '2025-01-16T19:51:04.886331Z',
@@ -41,6 +43,7 @@ async def test_load_int_user_id():
)
found = await store.get_metadata('some-conversation-id')
assert found.github_user_id == '12345'
assert found.user_id == '67890'
@pytest.mark.asyncio
@@ -61,6 +64,7 @@ async def test_search_basic():
{
'conversation_id': 'conv1',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': 'First conversation',
'created_at': '2025-01-16T19:51:04Z',
@@ -70,6 +74,7 @@ async def test_search_basic():
{
'conversation_id': 'conv2',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': 'Second conversation',
'created_at': '2025-01-17T19:51:04Z',
@@ -79,6 +84,7 @@ async def test_search_basic():
{
'conversation_id': 'conv3',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': 'Third conversation',
'created_at': '2025-01-15T19:51:04Z',
@@ -107,6 +113,7 @@ async def test_search_pagination():
{
'conversation_id': f'conv{i}',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': f'Conversation {i}',
'created_at': f'2025-01-{15+i}T19:51:04Z',
@@ -148,6 +155,7 @@ async def test_search_with_invalid_conversation():
{
'conversation_id': 'conv1',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': 'Valid conversation',
'created_at': '2025-01-16T19:51:04Z',
@@ -176,6 +184,7 @@ async def test_get_all_metadata():
{
'conversation_id': 'conv1',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': 'First conversation',
'created_at': '2025-01-16T19:51:04Z',
@@ -185,6 +194,7 @@ async def test_get_all_metadata():
{
'conversation_id': 'conv2',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': 'Second conversation',
'created_at': '2025-01-17T19:51:04Z',

View File

@@ -14,7 +14,7 @@ from openhands.events.action.agent import RecallAction
from openhands.events.action.message import MessageAction
from openhands.events.event import EventSource
from openhands.events.observation.agent import (
MicroagentObservation,
RecallObservation,
RecallType,
)
from openhands.events.stream import EventStream
@@ -74,7 +74,7 @@ async def test_memory_on_event_exception_handling(memory, event_stream):
# Mock Memory method to raise an exception
with patch.object(
memory, '_on_first_microagent_action', side_effect=Exception('Test error')
memory, '_on_workspace_context_recall', side_effect=Exception('Test error')
):
state = await run_controller(
config=AppConfig(),
@@ -93,10 +93,10 @@ async def test_memory_on_event_exception_handling(memory, event_stream):
@pytest.mark.asyncio
async def test_memory_on_first_microagent_action_exception_handling(
async def test_memory_on_workspace_context_recall_exception_handling(
memory, event_stream
):
"""Test that exceptions in Memory._on_first_microagent_action are properly handled via status callback."""
"""Test that exceptions in Memory._on_workspace_context_recall are properly handled via status callback."""
# Create a dummy agent for the controller
agent = MagicMock(spec=Agent)
@@ -108,11 +108,11 @@ async def test_memory_on_first_microagent_action_exception_handling(
runtime = MagicMock(spec=Runtime)
runtime.event_stream = event_stream
# Mock Memory._on_first_microagent_action to raise an exception
# Mock Memory._on_workspace_context_recall to raise an exception
with patch.object(
memory,
'_on_first_microagent_action',
side_effect=Exception('Test error from _on_first_microagent_action'),
'_find_microagent_knowledge',
side_effect=Exception('Test error from _find_microagent_knowledge'),
):
state = await run_controller(
config=AppConfig(),
@@ -130,12 +130,13 @@ async def test_memory_on_first_microagent_action_exception_handling(
assert state.last_error == 'Error: Exception'
def test_memory_with_microagents():
@pytest.mark.asyncio
async def test_memory_with_microagents():
"""Test that Memory loads microagents from the global directory and processes microagent actions.
This test verifies that:
1. Memory loads microagents from the global GLOBAL_MICROAGENTS_DIR
2. When a microagent action with a trigger word is processed, a MicroagentObservation is created
2. When a microagent action with a trigger word is processed, a RecallObservation is created
"""
# Create a mock event stream
event_stream = MagicMock(spec=EventStream)
@@ -158,6 +159,9 @@ def test_memory_with_microagents():
query='Hello, flarglebargle!', recall_type=RecallType.KNOWLEDGE
)
# Set the source to USER
microagent_action._source = EventSource.USER # type: ignore[attr-defined]
# Mock the event_stream.add_event method
added_events = []
@@ -173,12 +177,12 @@ def test_memory_with_microagents():
added_events.clear()
# Process the microagent action
memory.on_event(microagent_action)
await memory._on_event(microagent_action)
# Verify a MicroagentObservation was added to the event stream
# Verify a RecallObservation was added to the event stream
assert len(added_events) == 1
observation, source = added_events[0]
assert isinstance(observation, MicroagentObservation)
assert isinstance(observation, RecallObservation)
assert source == EventSource.ENVIRONMENT
assert observation.recall_type == RecallType.KNOWLEDGE
assert len(observation.microagent_knowledge) == 1
@@ -188,7 +192,7 @@ def test_memory_with_microagents():
def test_memory_repository_info(prompt_dir):
"""Test that Memory adds repository info to MicroagentObservations."""
"""Test that Memory adds repository info to RecallObservations."""
# Create an in-memory file store and real event stream
file_store = InMemoryFileStore()
event_stream = EventStream(sid='test-session', file_store=file_store)
@@ -241,15 +245,15 @@ REPOSITORY INSTRUCTIONS: This is a test repository.
# Get all events from the stream
events = list(event_stream.get_events())
# Find the MicroagentObservation event
# Find the RecallObservation event
microagent_obs_events = [
event for event in events if isinstance(event, MicroagentObservation)
event for event in events if isinstance(event, RecallObservation)
]
# We should have at least one MicroagentObservation
# We should have at least one RecallObservation
assert len(microagent_obs_events) > 0
# Get the first MicroagentObservation
# Get the first RecallObservation
observation = microagent_obs_events[0]
assert observation.recall_type == RecallType.WORKSPACE_CONTEXT
assert observation.repo_name == 'owner/repo'

View File

@@ -5,8 +5,8 @@ from openhands.events.observation import (
CmdOutputMetadata,
CmdOutputObservation,
FileEditObservation,
MicroagentObservation,
Observation,
RecallObservation,
)
from openhands.events.observation.agent import MicroagentKnowledge
from openhands.events.serialization import (
@@ -245,9 +245,9 @@ def test_file_edit_observation_legacy_serialization():
def test_microagent_observation_serialization():
original_observation_dict = {
'observation': 'microagent',
'observation': 'recall',
'content': '',
'message': "**MicroagentObservation**\nrecall_type=RecallType.WORKSPACE_CONTEXT, repo_name=some_repo_name, repo_instructions=complex_repo_instruc..., runtime_hosts={'host1': 8080, 'host2': 8081}, additional_agent_instructions=You know it all abou...",
'message': 'Added workspace context',
'extras': {
'recall_type': 'workspace_context',
'repo_name': 'some_repo_name',
@@ -258,14 +258,14 @@ def test_microagent_observation_serialization():
'microagent_knowledge': [],
},
}
serialization_deserialization(original_observation_dict, MicroagentObservation)
serialization_deserialization(original_observation_dict, RecallObservation)
def test_microagent_observation_microagent_knowledge_serialization():
original_observation_dict = {
'observation': 'microagent',
'observation': 'recall',
'content': '',
'message': '**MicroagentObservation**\nrecall_type=RecallType.KNOWLEDGE, repo_name=, repo_instructions=..., runtime_hosts={}, additional_agent_instructions=..., microagent_knowledge=microagent1, microagent2',
'message': 'Added microagent knowledge',
'extras': {
'recall_type': 'knowledge',
'repo_name': '',
@@ -287,13 +287,13 @@ def test_microagent_observation_microagent_knowledge_serialization():
],
},
}
serialization_deserialization(original_observation_dict, MicroagentObservation)
serialization_deserialization(original_observation_dict, RecallObservation)
def test_microagent_observation_knowledge_microagent_serialization():
"""Test serialization of a MicroagentObservation with KNOWLEDGE_MICROAGENT type."""
# Create a MicroagentObservation with microagent knowledge content
original = MicroagentObservation(
"""Test serialization of a RecallObservation with KNOWLEDGE_MICROAGENT type."""
# Create a RecallObservation with microagent knowledge content
original = RecallObservation(
content='Knowledge microagent information',
recall_type=RecallType.KNOWLEDGE,
microagent_knowledge=[
@@ -314,13 +314,13 @@ def test_microagent_observation_knowledge_microagent_serialization():
serialized = event_to_dict(original)
# Verify serialized data structure
assert serialized['observation'] == ObservationType.MICROAGENT
assert serialized['observation'] == ObservationType.RECALL
assert serialized['content'] == 'Knowledge microagent information'
assert serialized['extras']['recall_type'] == RecallType.KNOWLEDGE.value
assert len(serialized['extras']['microagent_knowledge']) == 2
assert serialized['extras']['microagent_knowledge'][0]['trigger'] == 'python'
# Deserialize back to MicroagentObservation
# Deserialize back to RecallObservation
deserialized = observation_from_dict(serialized)
# Verify properties are preserved
@@ -336,9 +336,9 @@ def test_microagent_observation_knowledge_microagent_serialization():
def test_microagent_observation_environment_serialization():
"""Test serialization of a MicroagentObservation with ENVIRONMENT type."""
# Create a MicroagentObservation with environment info
original = MicroagentObservation(
"""Test serialization of a RecallObservation with ENVIRONMENT type."""
# Create a RecallObservation with environment info
original = RecallObservation(
content='Environment information',
recall_type=RecallType.WORKSPACE_CONTEXT,
repo_name='OpenHands',
@@ -352,7 +352,7 @@ def test_microagent_observation_environment_serialization():
serialized = event_to_dict(original)
# Verify serialized data structure
assert serialized['observation'] == ObservationType.MICROAGENT
assert serialized['observation'] == ObservationType.RECALL
assert serialized['content'] == 'Environment information'
assert serialized['extras']['recall_type'] == RecallType.WORKSPACE_CONTEXT.value
assert serialized['extras']['repo_name'] == 'OpenHands'
@@ -364,7 +364,7 @@ def test_microagent_observation_environment_serialization():
serialized['extras']['additional_agent_instructions']
== 'You know it all about this runtime'
)
# Deserialize back to MicroagentObservation
# Deserialize back to RecallObservation
deserialized = observation_from_dict(serialized)
# Verify properties are preserved
@@ -382,11 +382,11 @@ def test_microagent_observation_environment_serialization():
def test_microagent_observation_combined_serialization():
"""Test serialization of a MicroagentObservation with both types of information."""
# Create a MicroagentObservation with both environment and microagent info
"""Test serialization of a RecallObservation with both types of information."""
# Create a RecallObservation with both environment and microagent info
# Note: In practice, recall_type would still be one specific type,
# but the object could contain both types of fields
original = MicroagentObservation(
original = RecallObservation(
content='Combined information',
recall_type=RecallType.WORKSPACE_CONTEXT,
# Environment info
@@ -419,7 +419,7 @@ def test_microagent_observation_combined_serialization():
serialized['extras']['additional_agent_instructions']
== 'You know it all about this runtime'
)
# Deserialize back to MicroagentObservation
# Deserialize back to RecallObservation
deserialized = observation_from_dict(serialized)
# Verify all properties are preserved

View File

@@ -51,7 +51,7 @@ At the user's request, repository {{ repository_info.repo_name }} has been clone
assert 'System prompt: bar' in system_msg
# Test building additional info
additional_info = manager.build_additional_info(
additional_info = manager.build_workspace_context(
repository_info=repo_info, runtime_info=None, repo_instructions=''
)
assert '<REPOSITORY_INFO>' in additional_info
@@ -199,7 +199,7 @@ def test_add_turns_left_reminder(prompt_dir):
)
def test_build_additional_info_with_repo_and_runtime(prompt_dir):
def test_build_workspace_context_with_repo_and_runtime(prompt_dir):
"""Test building additional info with repository and runtime information."""
# Create an additional_info.j2 template file
with open(os.path.join(prompt_dir, 'additional_info.j2'), 'w') as f:
@@ -245,7 +245,7 @@ each of which has a corresponding port:
repo_instructions = 'This repository contains important code.'
# Build additional info
result = manager.build_additional_info(
result = manager.build_workspace_context(
repository_info=repo_info,
runtime_info=runtime_info,
repo_instructions=repo_instructions,

View File

@@ -49,6 +49,7 @@ async def test_iterate_single_page():
{
'conversation_id': 'conv1',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': 'First conversation',
'created_at': '2025-01-16T19:51:04Z',
@@ -58,6 +59,7 @@ async def test_iterate_single_page():
{
'conversation_id': 'conv2',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': 'Second conversation',
'created_at': '2025-01-17T19:51:04Z',
@@ -86,6 +88,7 @@ async def test_iterate_multiple_pages():
{
'conversation_id': f'conv{i}',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': f'Conversation {i}',
'created_at': f'2025-01-{15+i}T19:51:04Z',
@@ -120,6 +123,7 @@ async def test_iterate_with_invalid_conversation():
{
'conversation_id': 'conv1',
'github_user_id': '123',
'user_id': '123',
'selected_repository': 'repo1',
'title': 'Valid conversation',
'created_at': '2025-01-16T19:51:04Z',

View File

@@ -61,7 +61,7 @@ async def test_init_new_local_session():
'new-session-id', ConversationInitData(), 1
)
await conversation_manager.join_conversation(
'new-session-id', 'new-session-id', ConversationInitData(), 1
'new-session-id', 'new-session-id', ConversationInitData(), 1, '12345'
)
assert session_instance.initialize_agent.call_count == 1
assert sio.enter_room.await_count == 1
@@ -93,10 +93,18 @@ async def test_join_local_session():
'new-session-id', ConversationInitData(), None
)
await conversation_manager.join_conversation(
'new-session-id', 'new-session-id', ConversationInitData(), None
'new-session-id',
'new-session-id',
ConversationInitData(),
None,
'12345',
)
await conversation_manager.join_conversation(
'new-session-id', 'new-session-id', ConversationInitData(), None
'new-session-id',
'new-session-id',
ConversationInitData(),
None,
'12345',
)
assert session_instance.initialize_agent.call_count == 1
assert sio.enter_room.await_count == 2
@@ -128,7 +136,7 @@ async def test_add_to_local_event_stream():
'new-session-id', ConversationInitData(), 1
)
await conversation_manager.join_conversation(
'new-session-id', 'connection-id', ConversationInitData(), 1
'new-session-id', 'connection-id', ConversationInitData(), 1, '12345'
)
await conversation_manager.send_to_event_stream(
'connection-id', {'event_type': 'some_event'}

View File

@@ -23,7 +23,13 @@ def mock_event_stream():
def mock_agent():
agent = MagicMock()
agent.llm = MagicMock()
agent.llm.config = MagicMock()
# Create a step function that returns an action without an ID
def agent_step_fn(state):
return MessageAction(content='Agent returned a message')
agent.step = agent_step_fn
return agent