Compare commits

...

241 Commits

Author SHA1 Message Date
Jiayi Zhang ee3ac165ce Merge pull request #1262 from zjyhhhher/ospp/chart-visualization-joy
Pre-commit checks / pre-commit-check (push) Failing after 0s
OSPP/feat: Integrate VMind to enable automated insight annotation and enhance report generation pipeline
2025-10-29 11:40:08 +08:00
zjyhhhher 224a5748d7 Revert "Revert "ospp/openmanus + vmind, report generation optimization""
This reverts commit 359785b162.
2025-10-28 23:04:33 +08:00
zjyhhhher d5f4a5f06f Revert "Revert "ospp/openmanus + vmind, Report Generation Function Optimization""
This reverts commit 63fbd7ebbb.
2025-10-28 23:03:38 +08:00
Jiayi Zhang 1380c20a53 Merge pull request #1260 from zjyhhhher/ospp/chart-visualization
Ospp/chart visualization  revert
2025-10-28 21:53:03 +08:00
zjyhhhher 63fbd7ebbb Revert "ospp/openmanus + vmind, Report Generation Function Optimization"
This reverts commit 68e5e6d10e.
2025-10-28 20:21:14 +08:00
zjyhhhher 359785b162 Revert "ospp/openmanus + vmind, report generation optimization"
This reverts commit 284d654293.
2025-10-28 20:20:41 +08:00
zjyhhhher 60a58a3ac8 Merge branch 'ospp/chart-visualization' of github.com:zjyhhhher/OpenManus into ospp/chart-visualization 2025-10-28 19:59:09 +08:00
zjyhhhher 284d654293 ospp/openmanus + vmind, report generation optimization 2025-10-28 19:48:24 +08:00
zjyhhhher 68e5e6d10e ospp/openmanus + vmind, Report Generation Function Optimization 2025-10-28 19:48:11 +08:00
Jiayi Zhang a058b97400 Merge pull request #1258 from FoundationAgents/revert-1257-revert-1238-ospp/chart-visualization
Revert "Revert "OSPP/feat: Integrate VMind to enable automated insight annotation and enhance report generation pipeline""
2025-10-28 18:57:43 +08:00
Jiayi Zhang 9a8c9dedd7 Revert "Revert "OSPP/feat: Integrate VMind to enable automated insight annotation and enhance report generation pipeline""
Pre-commit checks / pre-commit-check (push) Failing after 0s
2025-10-28 18:52:08 +08:00
Jiayi Zhang c3f1af3ef8 Merge pull request #1257 from FoundationAgents/revert-1238-ospp/chart-visualization
Revert "OSPP/feat: Integrate VMind to enable automated insight annotation and enhance report generation pipeline"
2025-10-28 18:32:06 +08:00
Jiayi Zhang d06fbfb5b3 Revert "OSPP/feat: Integrate VMind to enable automated insight annotation and enhance report generation pipeline"
Pre-commit checks / pre-commit-check (push) Failing after 1s
2025-10-28 18:31:05 +08:00
Jiayi Zhang b2f5e2a29d Merge pull request #1238 from zjyhhhher/ospp/chart-visualization
OSPP/feat: Integrate VMind to enable automated insight annotation and enhance report generation pipeline
2025-10-28 18:26:30 +08:00
Jiayi Zhang 37e7f9c8f9 Merge pull request #1256 from FoundationAgents/chart-visualization
Merge pull request #861 from 666haiwen/feat/chart-visualization
2025-10-28 17:52:27 +08:00
zjyhhhher b56a8cbac1 ospp/openmanus + vmind, report generation optimization 2025-09-30 11:41:52 +08:00
lhahah e2a41f4062 ospp/openmanus + vmind, Report Generation Function Optimization 2025-09-30 11:28:43 +08:00
Jiayi Zhang 67d6c1c00d Merge pull request #1206 from XYDT-AI/sandbox
Feat: support 4 sandbox tools by using daytona
2025-09-19 15:29:19 +08:00
GhostC efccc9fcc9 Merge branch 'sandbox' of https://github.com/XYDT-AI/OpenManus-improve into sandbox 2025-09-14 18:19:22 +08:00
GhostC c26bc9262e put base64 in the currect position for sb_vision 2025-09-14 18:15:01 +08:00
GhostC 52cf406467 Merge branch 'FoundationAgents:main' into sandbox 2025-09-08 15:53:40 +08:00
Jiayi Zhang 20c7207bda Update README.md 2025-09-04 18:39:07 +08:00
GhostC decd25ab34 Merge branch 'FoundationAgents:main' into sandbox 2025-08-16 17:47:07 +08:00
GhostC 5e7aba3e4b modify sb_browser_use tool's result 2025-08-16 17:46:02 +08:00
Jiayi Zhang 3e604acb93 Update Group Link. 2025-08-13 10:38:39 +08:00
GhostC 23f2e85ae9 modify requirements.txt 2025-07-30 23:36:44 +08:00
GhostC 9cb3bd16c4 remove sb_*.py to tool/sandbox&modify requirements.txt 2025-07-30 18:59:33 +08:00
GhostC 802e212ef2 Apply pre-commit formatting fixes 2025-07-27 22:31:39 +08:00
GhostC f08ed23914 Merge branch 'sandbox' of https://github.com/XYDT-AI/OpenManus-sandbox into sandbox 2025-07-27 22:27:35 +08:00
GhostC a010fe7048 Apply pre-commit formatting fixes 2025-07-27 22:26:50 +08:00
GhostC 386aeb46b6 Merge branch 'FoundationAgents:main' into sandbox 2025-07-27 22:00:14 +08:00
GhostC 66f4ae046e feat:support 4 sandbox tools by using daytona 2025-07-27 21:59:18 +08:00
GhostC f99dfac5bb add README and delete sandbox_id 2025-07-27 18:38:53 +08:00
Jiayi Zhang ac66e8fd44 Merge pull request #1197 from SNHuan/tool/Crawl4aiTool
add Crawl4aiTool
2025-07-23 17:47:52 +08:00
SNHuan 08cd0443b6 Format crawl4ai.py with pre-commit hooks 2025-07-23 17:36:53 +08:00
white-rm 50f56f5708 eliminate sandox redundant information 2025-07-22 23:50:13 +08:00
SNHuan 4a28a7912c add Crawl4aiTool 2025-07-22 14:29:16 +08:00
cyb1278588254 0246513495 debug sb_files_tool & sb_vision_tool 2025-07-20 17:26:15 +08:00
GhostC c94422748a Merge branch 'sandbox' of https://github.com/XYDT-AI/OpenManus-sandbox into sandbox 2025-07-20 16:02:58 +08:00
GhostC 3301d54625 support use existing sandbox by id 2025-07-20 16:02:18 +08:00
GhostC 0a6207d26f Merge branch 'FoundationAgents:main' into sandbox 2025-07-20 14:39:36 +08:00
GhostC 398ae434ed add main for sandbox_agent 2025-07-20 02:17:08 +08:00
cyb1278588254 5731b63984 init sb_files_tool & sb_vision_tool 2025-07-19 23:48:23 +08:00
white-rm 6fb48f4dbe update browser sandbox tool 2 2025-07-19 23:37:01 +08:00
white-rm c016d5627c update browser sandbox tool message 2025-07-16 00:24:56 +08:00
Jiayi Zhang be1e22c79c Merge pull request #1101 from CZH-THU/main
Pre-commit checks / pre-commit-check (push) Failing after 0s
feat:Support A2A protocol
2025-07-14 14:30:37 +08:00
white-rm e4c4d4a00f update browser sandbox tool 2025-07-13 21:09:58 +08:00
GhostC c37a145edc specify a2a-sdk version 2025-07-11 23:14:27 +08:00
GhostC cc283546c0 Merge branch 'sandbox' of https://github.com/XYDT-AI/OpenManus-sandbox into sandbox 2025-07-10 23:03:36 +08:00
GhostC 7ce31d1c34 init sandbox agent 2025-07-10 23:02:50 +08:00
white-rm 904dec240b update shell sandox 2025-07-08 22:40:59 +08:00
GhostC 17e1becb9f test sb_browser_use_tool 2025-07-08 22:24:02 +08:00
GhostC 41e12eb1de Merge branch 'sandbox' of https://github.com/XYDT-AI/OpenManus-sandbox into sandbox 2025-07-07 00:57:04 +08:00
GhostC fb30c432f3 add daytona config 2025-07-06 23:57:00 +08:00
white-rm 686ace2b09 add shell sandox 2025-07-06 22:42:09 +08:00
GhostC d7e3c14b60 debug computer use tool 2025-07-06 15:13:41 +08:00
GhostC 2a6287422d add sandbox tool 2025-07-01 21:49:22 +08:00
GhostC 9513686155 modify BaseTool 2025-06-30 23:12:44 +08:00
white-rm a6a45d7e02 add sandox utils 2025-06-30 00:49:18 +08:00
GhostC 527fd6483b add utils 2025-06-29 10:12:23 +08:00
GhostC fd5b070cb4 sandbox init 2025-06-27 19:38:41 +08:00
GhostC c37100846b parse host&port with argparse and reformat files 2025-06-25 19:03:02 +08:00
GhostC 8faab601e7 feat:support A2A protocol 2025-06-24 22:19:38 +08:00
GhostC e499149ac4 Merge branch 'FoundationAgents:main' into main 2025-06-11 09:04:28 +08:00
Jiayi Zhang 36713cda62 Merge pull request #1145 from ryantzr1/main
fix: sanitize MCP tool names for OpenAI API compliance
2025-06-09 16:22:09 +08:00
Jiayi Zhang 151082099b Merge pull request #1153 from Kunlun-Zhu/main
Add prompt input parser into main.py
2025-06-05 23:49:57 +08:00
Kunlun Zhu e1499ee1f6 fixing isort errors 2025-06-05 09:30:27 -05:00
Kunlun Zhu 7b59b64f77 add prompt input parser into main.py 2025-06-05 09:23:21 -05:00
ryantzr1 8597ef4925 fix: sanitize MCP tool names for OpenAI API compliance
Tool names with invalid characters (slashes, dots) from server paths
were causing OpenAI API 400 errors. Added sanitization to ensure
names match required pattern ^[a-zA-Z0-9_-]{1,64}$.

Fixes #1143
2025-05-29 22:54:36 +08:00
GhostC b386fec6b9 [feat]:Support A2A protocol with a2a-sdk 2025-05-28 20:15:03 +08:00
GhostC fe690ec0c3 Merge branch 'main' of https://github.com/CZH-THU/OpenManus 2025-05-28 19:30:51 +08:00
GhostC 1886c72cf2 Merge branch 'FoundationAgents:main' into main 2025-05-28 19:29:28 +08:00
GhostC 1952af4b2d Merge branch 'main' of https://github.com/FoundationAgents/OpenManus 2025-05-28 19:04:46 +08:00
Jiayi Zhang 7cd3057dda Merge pull request #1139 from 666haiwen/feat/add_agent_in_runflow
Feat/add data analysis agent in runflow
2025-05-27 15:41:33 +08:00
ZJU_czx 6c89485694 fix: refactor code by pre-commit 2025-05-22 15:17:46 +08:00
ZJU_czx cb33ca3fea feat: add data_analysis_agent into run_flow && using puppeteer instead of canvas in package 2025-05-22 12:04:09 +08:00
GhostC 415a45f466 Merge branch 'FoundationAgents:main' into main 2025-05-21 15:09:45 +08:00
Jiayi Zhang d901a98064 Update Project URL. 2025-05-20 17:53:27 +08:00
GhostC 303f736dd5 Merge branch 'FoundationAgents:main' into main 2025-05-10 10:50:26 +08:00
liangxinbing 36627a12e2 format code 2025-05-07 11:55:10 +08:00
JK_Lu 0810b28d9d BUGFIX: FIX MCPAgent Bug 2025-05-02 21:25:20 +08:00
GhostC 390ac132b8 add requirements.txt 2025-05-01 19:18:56 +08:00
GhostC 8d23b06e46 use factory to create Manus Agent instance dynamically 2025-04-28 23:01:58 +08:00
GhostC f457ec8462 use Manus.create() to create instance 2025-04-27 22:46:05 +08:00
GhostC 03a1bd8586 Merge branch 'mannaandpoem:main' into main 2025-04-27 21:56:49 +08:00
ZJU_czx 0158c7233e fix: fix wrong resolved url in pacakges-lock.json 2025-04-27 16:29:58 +07:00
Xinbing Liang b9f6534315 Merge pull request #1104 from zoe-yyx/simplify-code
Simplify return statement in count_image by removing redundant comments
2025-04-27 01:44:49 +08:00
yxyang a77823636f remove comments 2025-04-26 15:14:57 +08:00
GhostC 3dfb21d1e3 Change README 2025-04-24 22:53:35 +08:00
GhostC 638f155a10 Support A2A protocol 2025-04-24 22:43:40 +08:00
Xinbing Liang 8593116982 Merge pull request #1085 from zhenjunMa/delete_duplicate_code
delete duplicate code
2025-04-22 23:27:10 +08:00
liangxinbing 0c3f39836f delete useless tools and prompt 2025-04-22 23:12:55 +08:00
Xinbing Liang 18844dd108 Merge pull request #1097 from 666haiwen/feat/chart-visualization
feat: add data analysis agent to do data process / data report / chart generation / ... tasks
2025-04-22 23:04:41 +08:00
ZJU_czx f162d629a6 refactor: refactor code by pre-commit 2025-04-22 18:54:22 +08:00
ZJU_czx 3928a5dc8b refactor: remove useless code 2025-04-22 16:37:33 +08:00
ZJU_czx 42ba8ac51f feat: update demo and fix spec string bug in chart visualization 2025-04-22 16:29:21 +08:00
ZJU_czx 8dc281f637 feat: refactor code to prepare merge into main 2025-04-22 10:43:56 +08:00
ZJU_czx f917597d0d Merge remote-tracking branch 'upstream/main' into feat/data_visualization_hackathon 2025-04-22 09:54:22 +08:00
ZJU_czx 90d60d673a feat: resize steps limits 2025-04-22 09:53:56 +08:00
古今 fe56b80c8e delete duplicate code 2025-04-20 00:04:51 +08:00
Sheng Fan f760ebfb15 chore(github): correct config.yml 2025-04-17 14:47:55 +08:00
Xinbing Liang 7abe2949a7 Merge pull request #116 from Randool/dev/add_human_interact
feat: Adding human interaction functionality to Manus
2025-04-16 21:32:20 +08:00
Xinbing Liang 7326cca450 Merge pull request #1061 from cyzus/feat-include-mcp-in-manus-agent
Manus Agent to include MCP tools
2025-04-16 21:00:29 +08:00
Xinbing Liang 8592c631a2 Merge pull request #1067 from cnJasonZ/feat/ppio-config-example
feat: add config example for PPIO
2025-04-15 18:29:38 +08:00
cnJasonZ 82838bd3b4 feat: add config example for PPIO 2025-04-15 18:24:49 +08:00
Yizhou df9312dfca remove session close 2025-04-15 18:12:54 +08:00
Yizhou ead168d56c fix cancel scope issue 2025-04-15 18:05:23 +08:00
Yizhou 597691befa avoid repetitive mcp tool adding 2025-04-15 17:18:46 +08:00
Yizhou db3c293683 catch mcp cancel scope issue 2025-04-15 16:57:15 +08:00
Yizhou baee2ea1e2 make sure tool is not repetitively added because mcp servers may provide the same tool 2025-04-15 16:56:19 +08:00
Yizhou 9b0a11a1ea if mcp.json is not provided, do not report anything 2025-04-14 13:56:09 +08:00
Yizhou f65bd9bbf5 1. support multiple mcp servers
2. allow manus agent to connect to mcp servers whilst keeping the local tool set
3. include automatic mcp config loading
2025-04-14 12:18:04 +08:00
Sheng Fan 1a12b67e4b Merge pull request #1050 from Avalephh/addPrompt
add terminate for NEXT_STEP_PROMPT in browser & manus
2025-04-10 14:22:04 +08:00
avaleph 0e7f5ee0f9 add terminate for NEXT_STEP_PROMPT in browser & manus 2025-04-10 14:09:23 +08:00
liangxinbing a541822350 Add DOI badge 2025-04-10 10:26:17 +08:00
ZJU_czx b09f51ec64 feat: update readme and demo data 2025-04-03 22:59:17 +08:00
ZJU_czx a66cd94532 feat: cdn update to default 2025-04-03 21:22:21 +08:00
ZJU_czx 391ca78207 feat: add default params + transfer vchart.min.js to cdn 2025-04-03 21:20:28 +08:00
ZJU_czx 0971678eef Revert "feat: optimize chart generation file name"
This reverts commit f1cad2f69e.
2025-04-03 21:09:13 +08:00
ZJU_czx 3f544a49c5 Revert "feat: update prompt"
This reverts commit 235b7af89d.
2025-04-03 21:09:06 +08:00
ZJU_czx 951e2856b6 Revert "feat: revert insights hint in next steps"
This reverts commit 9dbc2306c8.
2025-04-03 21:09:03 +08:00
ZJU_czx 9dbc2306c8 feat: revert insights hint in next steps 2025-04-03 19:30:31 +08:00
ZJU_czx 235b7af89d feat: update prompt 2025-04-03 19:03:09 +08:00
ZJU_czx f1cad2f69e feat: optimize chart generation file name 2025-04-03 18:48:41 +08:00
ZJU_czx ca63cb1b32 feat: update prompt and test of hack demo 2025-04-03 17:58:11 +08:00
ZJU_czx 8c101e1738 fix: fix bse64 png show error 2025-04-03 14:00:50 +08:00
ZJU_czx 1c8bd4e1f7 fix: fix package.lock bnpm issus 2025-04-02 18:07:09 +08:00
ZJU_czx 19204ca8a7 feat: update hack_demo to data analysis agent 2025-04-02 17:35:42 +08:00
ZJU_czx cf1500aa7d feat: update hack_demo 2025-04-02 17:31:22 +08:00
ZJU_czx acc6ac69e1 Merge remote-tracking branch 'upstream/main' into feat/data_visualization_hackathon 2025-04-02 17:27:42 +08:00
ZJU_czx 1f3b3d2a03 Merge pull request #5 from 666haiwen/feat/data_visualization_hack_czx
Feat/data visualization hack czx
2025-04-02 17:26:31 +08:00
ZJU_czx 5fd3baaf77 feat: update vmind version 2025-04-02 17:25:32 +08:00
ZJU_czx 9f0a647357 feat: update prompt of data analysis agent 2025-04-02 17:10:07 +08:00
ZJU_czx 3359113b03 Merge pull request #4 from 666haiwen/feat/data_visualization_hack_czx
Feat/data visualization hack czx
2025-04-02 14:31:17 +08:00
ZJU_czx 7db2887373 Merge remote-tracking branch 'origin/feat/data_visualization_hackathon' into feat/data_visualization_hack_czx 2025-04-02 14:22:39 +08:00
ZJU_czx 0d2fe2b560 Merge pull request #2 from 666haiwen/feat/data_visualization_hackathon_zzx
Feat/data visualization hackathon zzx
2025-04-02 14:20:53 +08:00
ZhangZixunCodeSpace 40d5c83edc Feat: delete unrequired part in data analysis tool 2025-04-02 13:02:28 +08:00
Jiayi Zhang 896a5d8d8d Add Sponsors to ReadMe. 2025-04-01 20:42:31 +08:00
ZJU_czx 84aec2c5ac feat: update prompt and hint 2025-04-01 20:13:51 +08:00
ZJU_czx eb844fdf2e feat: add specInsight func / language params in data visualization tool 2025-04-01 19:41:48 +08:00
ZhangZixunCodeSpace 5ecb2405d3 Feat: Update data preprocessing and exploration function in data analysis tool 2025-04-01 16:46:29 +08:00
liangxinbing f616c5d43d update _BROWSER_DESCRIPTION 2025-03-31 21:30:01 +08:00
ZJU_czx b8bd822b39 Merge branch 'feat/data_visualization_hackathon' into feat/data_visualization_hackathon_zzx 2025-03-31 20:49:39 +08:00
ZhangZixunCodeSpace 49d1e99728 feat(zzx): add data preprocessing module 2025-03-31 20:31:37 +08:00
ZJU_czx cecedb62bd Merge pull request #1 from 666haiwen/feat/data_visualization_hack_czx
Feat: Update chart generation tools in data analysis
2025-03-31 20:30:02 +08:00
ZJU_czx 0a4dce5522 fix: remove wrong default prompt in function tool 2025-03-31 17:45:23 +08:00
ZJU_czx 76d62a6ab3 Merge branch 'feat/data_visualization_hackathon' into feat/data_visualization_hack_czx 2025-03-31 17:37:58 +08:00
ZJU_czx d3313a6b39 feat: update data analysis agent prompt and output content 2025-03-31 17:34:33 +08:00
liangxinbing 964b2afc0f update Manus, BrowserAgent and ToolCallAgent 2025-03-31 00:30:01 +08:00
liangxinbing ec18292c8e update web_search action 2025-03-31 00:30:01 +08:00
liangxinbing f4ad612a47 update WebSearch and DeepResearch 2025-03-31 00:16:31 +08:00
liangxinbing 2d4116d98d update config.example-model-*.toml 2025-03-30 23:57:03 +08:00
liangxinbing 94426f1a0d update DeepResearch, ResearchSummary and WebContentFetcher 2025-03-30 23:54:36 +08:00
liangxinbing 8629c0a4e9 update extract_content action for BrowserUseTool 2025-03-30 23:22:49 +08:00
liangxinbing ae83bc7370 fix bug of stream for ask_tool 2025-03-30 23:05:17 +08:00
liangxinbing 5e479d7777 update deep_research.py 2025-03-30 23:04:32 +08:00
liangxinbing 9f603c6c3d fix bug of type hint 2025-03-30 18:40:23 +08:00
mannaandpoem 8b940a22f9 Merge pull request #951 from a-holm/fix-error-with-cleaning-up
Fix asyncio resource leaks by implementing agent cleanup hierarchy
2025-03-30 18:31:06 +08:00
mannaandpoem 3bf06dfd7c Merge pull request #954 from zyren123/fixbug
Fix manus agent's next step prompt accidentally overwritten by browser agent
2025-03-30 17:26:17 +08:00
liangxinbing 2435469f29 remove unused tools 2025-03-30 17:05:34 +08:00
mannaandpoem 6dc43e404d Merge pull request #984 from mannaandpoem/update-search-setting
update WebSearch and SearchSettings
2025-03-30 14:08:13 +08:00
liangxinbing 55015797c6 Add type hint for WebSearch and update SearchSettings 2025-03-30 14:04:34 +08:00
Johan A. Holm af46bfada2 Merge branch 'mannaandpoem:main' into fix-error-with-cleaning-up 2025-03-29 21:35:48 +01:00
liangxinbing 782687f3b7 update WebSearch Tool 2025-03-30 00:30:01 +08:00
liangxinbing ebe9602fbe update the structure of the examples 2025-03-30 00:19:52 +08:00
mannaandpoem b17e451d86 Merge pull request #810 from a-holm/separate-config-examples
Separate config file examples for each LLM API provider
2025-03-30 00:08:51 +08:00
mannaandpoem 49e106fd2c Merge pull request #794 from zyren123/memorylimit
Added message limit to the function "add_messages" to ensure that the…
2025-03-29 20:33:52 +08:00
ZJU_czx f9ad4362f6 Revert "feat: generate structured analysis reports"
This reverts commit 96c23f1f56.
2025-03-29 19:45:15 +08:00
ZJU_czx 6a2ff78ffd Revert "Merge branch 'feat/data_visualization_hackathon' of https://github.com/666haiwen/OpenManus into feat/data_visualization_hackathon"
This reverts commit b0e9384502, reversing
changes made to 96c23f1f56.
2025-03-29 19:45:00 +08:00
mannaandpoem f821ee729c Merge pull request #980 from mannaandpoem/update_readme_for_hf_space
update readme
2025-03-29 18:46:49 +08:00
liangxinbing 346666d946 update readme for adding hf space 2025-03-29 18:43:41 +08:00
ZhangZixunCodeSpace b0e9384502 Merge branch 'feat/data_visualization_hackathon' of https://github.com/666haiwen/OpenManus into feat/data_visualization_hackathon
Merge remote-tracking branch 'feat/data_visualization_hackathon'

Sync local work with upstream changes from hackathon branch
2025-03-29 17:50:43 +08:00
ZhangZixunCodeSpace 96c23f1f56 feat: generate structured analysis reports
- Add data_exploration.md with dataset metadata and statistics
- Create preprocessing_result.md for cleaning logs and metrics
- Ensure markdown-only outputs without visual elements
2025-03-29 17:47:00 +08:00
mannaandpoem c28d036ee3 Merge pull request #864 from yihong0618/hy/fix_issue_787
fix: simple browser use tool desc for gpt4o max len close #787
2025-03-29 16:50:41 +08:00
mannaandpoem da554cd335 Merge pull request #923 from mannaandpoem/dependabot/pip/browsergym-related-2da9c1d327
chore(deps): update playwright requirement from ~=1.50.0 to ~=1.51.0 in the browsergym-related group
2025-03-29 16:26:58 +08:00
mannaandpoem 91e131a915 Merge pull request #924 from mannaandpoem/dependabot/pip/version-all-52fec19601
chore(deps): bump the version-all group across 1 directory with 6 updates
2025-03-29 16:26:39 +08:00
mannaandpoem d674249804 Merge pull request #963 from Jeffrey95/fix/manus-inf-loop
fix: infinite loop in simple question until max_step is reached
2025-03-29 16:03:33 +08:00
mannaandpoem 34c610857f Update toolcall.py
remove `self.state = AgentState.FINISHED`
2025-03-29 16:02:28 +08:00
liangxinbing 128ba7fbc8 temporarily remove CoTAgent and PlanningAgent 2025-03-29 15:52:10 +08:00
mannaandpoem d2a67d876d Merge pull request #964 from zyren123/feature_mcpconfig
Update the MCP configuration, replace the hardcoding value with serve…
2025-03-29 15:09:52 +08:00
Randool 628aaf00d1 feat: Adding human interaction functionality to Manus 2025-03-29 13:23:29 +08:00
ZJU_czx 722d5c787d feat: add chart prepare tools and update chart geneartion tool to generate chart parallel 2025-03-28 16:22:30 +08:00
zhiyuanren e10aa08013 Update the MCP configuration, replace the hardcoding value with server_reference in config, and add the MCPSettings class in config.py to support MCP configuration. 2025-03-28 10:19:23 +08:00
Jeffrey95 a8625ee5d9 fix: infinite loop in simple question until max_step is reached 2025-03-28 00:03:15 +08:00
zhiyuanren 7d3b10ef83 Fix bugs next_step_prompt accidentally overwritten 2025-03-27 10:35:00 +08:00
zhiyuanren 9275c1f7e8 Remove redundant code 2025-03-27 10:21:19 +08:00
liangxinbing a10b973494 Fix bug of SWEAgent 2025-03-27 08:55:01 +08:00
Johan Holm 6057b9b376 precommit isort change 2025-03-26 11:17:26 +01:00
Johan Holm e86ce6d79f I have implemented a fix to asyncio resource leak errors when the script finishes. A cleanup method has been added to the agent hierarchy (ToolCallAgent, BrowserAgent, Manus), and main.py now ensures this cleanup is called using a try...finally block before the program exits. This should prevent the ValueError: I/O operation on closed pipe exceptions by properly closing browser resources. 2025-03-26 11:08:51 +01:00
ZJU_czx 8f89685f02 feat: add test demo 2025-03-26 17:33:16 +08:00
liangxinbing 3f3d262ebe Fixed the wrong import 2025-03-25 13:10:00 +08:00
ZJU_czx 449ab5e25a Merge branch 'feat/chart-visualization' into feat/data_visualization_hackathon 2025-03-25 10:06:00 +08:00
dependabot[bot] ac3bc1e061 chore(deps): bump the version-all group across 1 directory with 6 updates
Updates the requirements on [datasets](https://github.com/huggingface/datasets), [gymnasium](https://github.com/Farama-Foundation/Gymnasium), [pillow](https://github.com/python-pillow/Pillow), [duckduckgo-search](https://github.com/deedy5/duckduckgo_search), [mcp](https://github.com/modelcontextprotocol/python-sdk) and [boto3](https://github.com/boto/boto3) to permit the latest version.

Updates `datasets` to 3.4.1
- [Release notes](https://github.com/huggingface/datasets/releases)
- [Commits](https://github.com/huggingface/datasets/compare/3.2.0...3.4.1)

Updates `gymnasium` to 1.1.1
- [Release notes](https://github.com/Farama-Foundation/Gymnasium/releases)
- [Commits](https://github.com/Farama-Foundation/Gymnasium/compare/v1.0.0...v1.1.1)

Updates `pillow` to 11.1.0
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/10.4.0...11.1.0)

Updates `duckduckgo-search` to 7.5.3
- [Release notes](https://github.com/deedy5/duckduckgo_search/releases)
- [Commits](https://github.com/deedy5/duckduckgo_search/compare/v7.5.1...v7.5.3)

Updates `mcp` to 1.5.0
- [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases)
- [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md)
- [Commits](https://github.com/modelcontextprotocol/python-sdk/compare/v1.4.1...v1.5.0)

Updates `boto3` to 1.37.18
- [Release notes](https://github.com/boto/boto3/releases)
- [Commits](https://github.com/boto/boto3/compare/1.37.16...1.37.18)

---
updated-dependencies:
- dependency-name: datasets
  dependency-type: direct:production
  dependency-group: version-all
- dependency-name: gymnasium
  dependency-type: direct:production
  dependency-group: version-all
- dependency-name: pillow
  dependency-type: direct:production
  dependency-group: version-all
- dependency-name: duckduckgo-search
  dependency-type: direct:production
  dependency-group: version-all
- dependency-name: mcp
  dependency-type: direct:production
  dependency-group: version-all
- dependency-name: boto3
  dependency-type: direct:production
  dependency-group: version-all
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 07:57:56 +00:00
dependabot[bot] ccfde08fcb chore(deps): update playwright requirement
Updates the requirements on [playwright](https://github.com/microsoft/playwright-python) to permit the latest version.

Updates `playwright` to 1.51.0
- [Release notes](https://github.com/microsoft/playwright-python/releases)
- [Commits](https://github.com/microsoft/playwright-python/compare/v1.50.0...v1.51.0)

---
updated-dependencies:
- dependency-name: playwright
  dependency-type: direct:production
  dependency-group: browsergym-related
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 07:56:22 +00:00
mannaandpoem c7a3f465da Merge pull request #859 from minbang930/fix/extracted-content-properties
fix/extracted content properties
2025-03-22 14:20:32 +08:00
mannaandpoem 8127bf26ad Merge pull request #893 from zxmfke/feature-cot
feat: feature cot agent
2025-03-22 13:57:38 +08:00
mannaandpoem f21c7a2e55 Merge pull request #861 from 666haiwen/feat/chart-visualization
feat: add chart visualization tools which support png/html output
2025-03-22 13:54:27 +08:00
zxmfke cdbefb5fc7 fix: pre-commit local 2025-03-22 13:38:27 +08:00
zxmfke f37c3c29a1 Merge branch 'main' of github.com:mannaandpoem/OpenManus into feature-cot 2025-03-22 09:01:39 +08:00
Sheng Fan c432ec9286 chore(boto3): restore boto3 dependency 2025-03-21 18:46:48 +08:00
Sheng Fan 65470c2ae9 style(pre-commit): fix 2025-03-21 18:25:58 +08:00
Sheng Fan 30342247c0 feat: Add shortcut script for launching OpenManus MCP server 2025-03-21 18:24:23 +08:00
liangxinbing a61ef9b737 Comment boto3 to resolve dependency conflicts for requirements.txt 2025-03-21 17:54:22 +08:00
ZJU_czx 14bf016134 refactor: update code formatter 2025-03-21 17:39:06 +08:00
liangxinbing 82e3140357 update structure of flow 2025-03-21 17:37:16 +08:00
liangxinbing d0492a500e update run_default for run_mcp.py 2025-03-21 17:36:29 +08:00
ZJU_czx 0d6e3e5457 feat: add data analysis agent with chart generation tools 2025-03-21 17:29:32 +08:00
zxmfke 12bf9a107f fix: Delete non-core code in test and README_cot documentation, leaving only the most basic code 2025-03-21 14:15:49 +08:00
zxmfke 8b031958b1 Merge branch 'main' of github.com:mannaandpoem/OpenManus into feature-cot 2025-03-21 14:12:02 +08:00
mannaandpoem 35209978e1 Merge pull request #883 from leeseett/main
Remove merge conflict hint code
2025-03-21 13:09:42 +08:00
leeseett 3dd990e554 删除合并冲突提示代码 2025-03-21 11:49:26 +08:00
Sheng Fan e218c0655f Merge pull request #882 from fred913/patch/mcp-server
Patch/mcp server
2025-03-21 11:28:03 +08:00
Sheng Fan 5d18b5dc69 chore: remove useless file 2025-03-21 11:25:16 +08:00
Sheng Fan 567bffb441 style: pre-commit 2025-03-21 11:23:55 +08:00
Sheng Fan acb435f9f5 Merge branch 'mannaandpoem:main' into patch/mcp-server 2025-03-21 11:22:07 +08:00
mannaandpoem d63e88f089 Merge pull request #772 from a-holm/fix-for-search-rate-limits
feat(search): Add configurable fallback engines and retry logic for robust web search
2025-03-21 01:56:21 +08:00
zxmfke 2425ed71fb fix: modify test 2025-03-20 23:13:02 +08:00
zxmfke 2bb72029be Merge branch 'main' of github.com:mannaandpoem/OpenManus into feature-cot 2025-03-20 23:10:31 +08:00
mannaandpoem c3de3ad6f7 Merge pull request #862 from Shellmode/main
Add Amazon Bedrock support modular non-intrusive
2025-03-20 16:19:28 +08:00
zxmfke c768e6a668 fix: merge mcp agent 2025-03-20 16:09:23 +08:00
zxmfke 2f59b3e798 feat: cot example 2025-03-20 16:05:40 +08:00
yihong0618 8567344eb3 fix: simple browser use tool desc for gpt4o max len close #787
Signed-off-by: yihong0618 <zouzou0208@gmail.com>
2025-03-20 15:56:57 +08:00
ZJU_czx 802274bd3a feat: extract utils function in chart generation tools and optimize tool description 2025-03-20 15:20:27 +08:00
Sheng Fan 08a20f6880 chore(mcp.server): remove irregular environment patch
refactor(mcp.server): prevent browser-use from affecting mcp stdio communication
2025-03-20 13:25:56 +08:00
Array 2a13cb49f3 feat: modular non-intrusive Amazon Bedrock support, add config example 2025-03-20 12:25:53 +08:00
Array 4e10b42b30 feat: modular non-intrusive Amazon Bedrock support 2025-03-20 12:15:34 +08:00
ZJU_czx 56e4c8a0d7 feat: remove useless test code 2025-03-20 10:56:08 +08:00
ZJU_czx a0a535e2f9 feat: add chart visualization tools which support png/html output 2025-03-20 10:51:37 +08:00
minbang930 eef10607e1 Fix using isort 2025-03-20 08:08:51 +09:00
minbang930 8535f41215 fix: Add properties to extracted_content object in browser tool schema
The OpenAI API was rejecting function declarations due to empty properties in
the extracted_content object type. This fix adds required properties to comply
with OpenAI's schema requirements:

- Add 'text' property for storing extracted page content
- Add 'metadata' object with 'source' property for content attribution
- Maintain existing functionality while satisfying API schema validation

Error resolved:
"GenerateContentRequest.tools[0].function_declarations[0].parameters.properties[extracted_content].properties: should be non-empty for OBJECT type"
2025-03-20 07:49:46 +09:00
Johan Holm baf439f6c3 Merge branch 'main' of https://github.com/a-holm/OpenManus into separate-config-examples 2025-03-19 11:02:42 +01:00
Johan Holm 59a92257be Merge branch 'main' of https://github.com/a-holm/OpenManus into fix-for-search-rate-limits 2025-03-19 10:50:25 +01:00
a-holm 855caad4d9 Merge branch 'fix-for-search-rate-limits' of https://github.com/a-holm/OpenManus into fix-for-search-rate-limits 2025-03-18 21:53:34 +01:00
a-holm 95e3487402 Merge branch 'main' of https://github.com/a-holm/OpenManus into fix-for-search-rate-limits 2025-03-18 21:47:16 +01:00
Johan A. Holm fe44fe726d Merge branch 'main' into fix-for-search-rate-limits 2025-03-18 20:51:45 +01:00
Johan Holm ba05d625fd fix spelling error 2025-03-18 11:32:50 +01:00
Johan Holm c7858c2eb4 Make sure to only include fallback search engines 2025-03-18 09:53:30 +01:00
Johan Holm 19b24cbdf7 Separate files for each type provider 2025-03-18 09:24:51 +01:00
zhiyuanren e39046d175 Added message limit to the function "add_messages" to ensure that the number of messages does not exceed the maximum limit. 2025-03-18 11:12:59 +08:00
Johan Holm 9fa12e594c update from pre-commit 2025-03-17 11:05:03 +01:00
Johan Holm 711c2805e4 feat(search): Add robust fallback system with configurable retries and enhanced error handling
- Implement multi-engine failover system with configurable fallback order
    - Add retry logic with exponential backoff and rate limit detection
    - Introduce search configuration options:
      * fallback_engines: Ordered list of backup search providers
      * retry_delay: Seconds between retry batches (default: 60)
      * max_retries: Maximum system-wide retry attempts (default: 3)
    - Improve error resilience with:
      - Automatic engine switching on 429/Too Many Requests
      - Full system retries after configurable cooldown periods
      - Detailed logging for diagnostics and monitoring
    - Enhance engine prioritization logic:
      1. Primary configured engine
      2. Configured fallback engines
      3. Remaining available engines

    Example configuration:
    [search]
    engine = "Google"
    fallback_engines = ["DuckDuckGo", "Baidu"]  # Cascading fallback order
    retry_delay = 60                            # 1 minute between retry batches
    max_retries = 3                             # Attempt 3 full system retries

    This addresses critical reliability issues by:
    - Preventing search failures due to single-engine rate limits
    - Enabling recovery from transient network errors
    - Providing operational flexibility through configurable parameters
    - Improving visibility through granular logging (INFO/WARN/ERROR)
2025-03-17 10:43:42 +01:00
116 changed files with 17480 additions and 1272 deletions
-4
View File
@@ -1,4 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: "📑 Read online docs"
about: Find tutorials, use cases, and guides in the OpenManus documentation.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: "Join the Community Group"
about: Join the OpenManus community to discuss and get help from others
url: https://github.com/FoundationAgents/OpenManus?tab=readme-ov-file#community-group
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
contents: read
name: Display and label top issues
runs-on: ubuntu-latest
if: github.repository == 'mannaandpoem/OpenManus'
if: github.repository == 'FoundationAgents/OpenManus'
steps:
- name: Run top issues action
uses: rickstaa/top-issues-action@7e8dda5d5ae3087670f9094b9724a9a091fc3ba1 # v1.3.101
+5
View File
@@ -8,6 +8,8 @@ data/
# Workspace
workspace/
config/
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -197,3 +199,6 @@ cython_debug/
# OSX
.DS_Store
# node
node_modules
+13 -15
View File
@@ -16,24 +16,22 @@ repos:
rev: v2.0.1
hooks:
- id: autoflake
args: [
--remove-all-unused-imports,
--ignore-init-module-imports,
--expand-star-imports,
--remove-duplicate-keys,
--remove-unused-variables,
--recursive,
--in-place,
--exclude=__init__.py,
]
args:
[
--remove-all-unused-imports,
--ignore-init-module-imports,
--expand-star-imports,
--remove-duplicate-keys,
--remove-unused-variables,
--recursive,
--in-place,
--exclude=__init__.py,
]
files: \.py$
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
args: [
"--profile", "black",
"--filter-files",
"--lines-after-imports=2",
]
args:
["--profile", "black", "--filter-files", "--lines-after-imports=2"]
+2 -1
View File
@@ -16,5 +16,6 @@
},
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,
"editor.formatOnSave": true
"editor.formatOnSave": true,
"liveServer.settings.port": 5501
}
+1 -1
View File
@@ -130,7 +130,7 @@ minimizing disruptions. Lets work together to build a supportive and welcomin
provide context.
- Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the
matter is sensitive or private.
- Always adhere to [our standards](https://github.com/mannaandpoem/OpenManus/blob/main/CODE_OF_CONDUCT.md#our-standards)
- Always adhere to [our standards](https://github.com/FoundationAgents/OpenManus/blob/main/CODE_OF_CONDUCT.md#our-standards)
to ensure a welcoming and collaborative environment.
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For
Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For
+195 -176
View File
@@ -1,176 +1,195 @@
<p align="center">
<img src="assets/logo.jpg" width="200"/>
</p>
English | [中文](README_zh.md) | [한국어](README_ko.md) | [日本語](README_ja.md)
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
&ensp;
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
# 👋 OpenManus
Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
Enjoy your own agent with OpenManus!
We're also excited to introduce [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL), an open-source project dedicated to reinforcement learning (RL)- based (such as GRPO) tuning methods for LLM agents, developed collaboratively by researchers from UIUC and OpenManus.
## Project Demo
<video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
## Installation
We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management.
### Method 1: Using conda
1. Create a new conda environment:
```bash
conda create -n open_manus python=3.12
conda activate open_manus
```
2. Clone the repository:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
### Method 2: Using uv (Recommended)
1. Install uv (A fast Python package installer and resolver):
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
2. Clone the repository:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
cd OpenManus
```
3. Create a new virtual environment and activate it:
```bash
uv venv --python 3.12
source .venv/bin/activate # On Unix/macOS
# Or on Windows:
# .venv\Scripts\activate
```
4. Install dependencies:
```bash
uv pip install -r requirements.txt
```
### Browser Automation Tool (Optional)
```bash
playwright install
```
## Configuration
OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration:
1. Create a `config.toml` file in the `config` directory (you can copy from the example):
```bash
cp config/config.example.toml config/config.toml
```
2. Edit `config/config.toml` to add your API keys and customize settings:
```toml
# Global LLM configuration
[llm]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # Replace with your actual API key
max_tokens = 4096
temperature = 0.0
# Optional configuration for specific LLM models
[llm.vision]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # Replace with your actual API key
```
## Quick Start
One line for run OpenManus:
```bash
python main.py
```
Then input your idea via terminal!
For MCP tool version, you can run:
```bash
python run_mcp.py
```
For unstable multi-agent version, you also can run:
```bash
python run_flow.py
```
## How to contribute
We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests.
Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com
**Note**: Before submitting a pull request, please use the pre-commit tool to check your changes. Run `pre-commit run --all-files` to execute the checks.
## Community Group
Join our networking group on Feishu and share your experience with other developers!
<div align="center" style="display: flex; gap: 20px;">
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
</div>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date)
## Acknowledgement
Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo)
and [browser-use](https://github.com/browser-use/browser-use) for providing basic support for this project!
Additionally, we are grateful to [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands) and [SWE-agent](https://github.com/SWE-agent/SWE-agent).
OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community!
## Cite
```bibtex
@misc{openmanus2025,
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong},
title = {OpenManus: An open-source framework for building general AI agents},
year = {2025},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/mannaandpoem/OpenManus}},
}
```
<p align="center">
<img src="assets/logo.jpg" width="200"/>
</p>
English | [中文](README_zh.md) | [한국어](README_ko.md) | [日本語](README_ja.md)
[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers)
&ensp;
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407)
# 👋 OpenManus
Manus is incredible, but OpenManus can achieve any idea without an *Invite Code* 🛫!
Our team members [@Xinbin Liang](https://github.com/mannaandpoem) and [@Jinyu Xiang](https://github.com/XiangJinyu) (core authors), along with [@Zhaoyang Yu](https://github.com/MoshiQAQ), [@Jiayi Zhang](https://github.com/didiforgithub), and [@Sirui Hong](https://github.com/stellaHSR), we are from [@MetaGPT](https://github.com/geekan/MetaGPT). The prototype is launched within 3 hours and we are keeping building!
It's a simple implementation, so we welcome any suggestions, contributions, and feedback!
Enjoy your own agent with OpenManus!
We're also excited to introduce [OpenManus-RL](https://github.com/OpenManus/OpenManus-RL), an open-source project dedicated to reinforcement learning (RL)- based (such as GRPO) tuning methods for LLM agents, developed collaboratively by researchers from UIUC and OpenManus.
## Project Demo
<video src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" data-canonical-src="https://private-user-images.githubusercontent.com/61239030/420168772-6dcfd0d2-9142-45d9-b74e-d10aa75073c6.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDEzMTgwNTksIm5iZiI6MTc0MTMxNzc1OSwicGF0aCI6Ii82MTIzOTAzMC80MjAxNjg3NzItNmRjZmQwZDItOTE0Mi00NWQ5LWI3NGUtZDEwYWE3NTA3M2M2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAzMDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMzA3VDAzMjIzOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdiZjFkNjlmYWNjMmEzOTliM2Y3M2VlYjgyNDRlZDJmOWE3NWZhZjE1MzhiZWY4YmQ3NjdkNTYwYTU5ZDA2MzYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.UuHQCgWYkh0OQq9qsUWqGsUbhG3i9jcZDAMeHjLt5T4" controls="controls" muted="muted" class="d-block rounded-bottom-2 border-top width-fit" style="max-height:640px; min-height: 200px"></video>
## Installation
We provide two installation methods. Method 2 (using uv) is recommended for faster installation and better dependency management.
### Method 1: Using conda
1. Create a new conda environment:
```bash
conda create -n open_manus python=3.12
conda activate open_manus
```
2. Clone the repository:
```bash
git clone https://github.com/FoundationAgents/OpenManus.git
cd OpenManus
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
### Method 2: Using uv (Recommended)
1. Install uv (A fast Python package installer and resolver):
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
2. Clone the repository:
```bash
git clone https://github.com/FoundationAgents/OpenManus.git
cd OpenManus
```
3. Create a new virtual environment and activate it:
```bash
uv venv --python 3.12
source .venv/bin/activate # On Unix/macOS
# Or on Windows:
# .venv\Scripts\activate
```
4. Install dependencies:
```bash
uv pip install -r requirements.txt
```
### Browser Automation Tool (Optional)
```bash
playwright install
```
## Configuration
OpenManus requires configuration for the LLM APIs it uses. Follow these steps to set up your configuration:
1. Create a `config.toml` file in the `config` directory (you can copy from the example):
```bash
cp config/config.example.toml config/config.toml
```
2. Edit `config/config.toml` to add your API keys and customize settings:
```toml
# Global LLM configuration
[llm]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # Replace with your actual API key
max_tokens = 4096
temperature = 0.0
# Optional configuration for specific LLM models
[llm.vision]
model = "gpt-4o"
base_url = "https://api.openai.com/v1"
api_key = "sk-..." # Replace with your actual API key
```
## Quick Start
One line for run OpenManus:
```bash
python main.py
```
Then input your idea via terminal!
For MCP tool version, you can run:
```bash
python run_mcp.py
```
For unstable multi-agent version, you also can run:
```bash
python run_flow.py
```
### Custom Adding Multiple Agents
Currently, besides the general OpenManus Agent, we have also integrated the DataAnalysis Agent, which is suitable for data analysis and data visualization tasks. You can add this agent to `run_flow` in `config.toml`.
```toml
# Optional configuration for run-flow
[runflow]
use_data_analysis_agent = true # Disabled by default, change to true to activate
```
In addition, you need to install the relevant dependencies to ensure the agent runs properly: [Detailed Installation Guide](app/tool/chart_visualization/README.md##Installation)
## How to contribute
We welcome any friendly suggestions and helpful contributions! Just create issues or submit pull requests.
Or contact @mannaandpoem via 📧email: mannaandpoem@gmail.com
**Note**: Before submitting a pull request, please use the pre-commit tool to check your changes. Run `pre-commit run --all-files` to execute the checks.
## Community Group
Join our networking group on Feishu and share your experience with other developers!
<div align="center" style="display: flex; gap: 20px;">
<img src="assets/community_group.jpg" alt="OpenManus 交流群" width="300" />
</div>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date)
## Sponsors
Thanks to [PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) for computing source support.
> PPIO: The most affordable and easily-integrated MaaS and GPU cloud solution.
## Acknowledgement
Thanks to [anthropic-computer-use](https://github.com/anthropics/anthropic-quickstarts/tree/main/computer-use-demo), [browser-use](https://github.com/browser-use/browser-use) and [crawl4ai](https://github.com/unclecode/crawl4ai) for providing basic support for this project!
Additionally, we are grateful to [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands) and [SWE-agent](https://github.com/SWE-agent/SWE-agent).
We also thank stepfun(阶跃星辰) for supporting our Hugging Face demo space.
OpenManus is built by contributors from MetaGPT. Huge thanks to this agent community!
## Cite
```bibtex
@misc{openmanus2025,
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang},
title = {OpenManus: An open-source framework for building general AI agents},
year = {2025},
publisher = {Zenodo},
doi = {10.5281/zenodo.15186407},
url = {https://doi.org/10.5281/zenodo.15186407},
}
```
+26 -8
View File
@@ -4,10 +4,12 @@
[English](README.md) | [中文](README_zh.md) | [한국어](README_ko.md) | 日本語
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers)
&ensp;
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407)
# 👋 OpenManus
@@ -41,7 +43,7 @@ conda activate open_manus
2. リポジトリをクローンします:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
git clone https://github.com/FoundationAgents/OpenManus.git
cd OpenManus
```
@@ -62,7 +64,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
2. リポジトリをクローンします:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
git clone https://github.com/FoundationAgents/OpenManus.git
cd OpenManus
```
@@ -135,6 +137,19 @@ python run_mcp.py
python run_flow.py
```
## カスタムマルチエージェントの追加
現在、一般的なOpenManusエージェントに加えて、データ分析とデータ可視化タスクに適したDataAnalysisエージェントが組み込まれています。このエージェントを`config.toml``run_flow`に追加することができます。
```toml
# run-flowのオプション設定
[runflow]
use_data_analysis_agent = true # デフォルトでは無効、trueに変更すると有効化されます
```
これに加えて、エージェントが正常に動作するために必要な依存関係をインストールする必要があります:[具体的なインストールガイド](app/tool/chart_visualization/README_ja.md##インストール)
## 貢献方法
我々は建設的な意見や有益な貢献を歓迎します!issueを作成するか、プルリクエストを提出してください。
@@ -152,7 +167,7 @@ Feishuのネットワーキンググループに参加して、他の開発者
## スター履歴
[![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date)
## 謝辞
@@ -161,15 +176,18 @@ Feishuのネットワーキンググループに参加して、他の開発者
さらに、[AAAJ](https://github.com/metauto-ai/agent-as-a-judge)、[MetaGPT](https://github.com/geekan/MetaGPT)、[OpenHands](https://github.com/All-Hands-AI/OpenHands)、[SWE-agent](https://github.com/SWE-agent/SWE-agent)にも感謝します。
また、Hugging Face デモスペースをサポートしてくださった阶跃星辰 (stepfun)にも感謝いたします。
OpenManusはMetaGPTのコントリビューターによって構築されました。このエージェントコミュニティに大きな感謝を!
## 引用
```bibtex
@misc{openmanus2025,
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong},
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang},
title = {OpenManus: An open-source framework for building general AI agents},
year = {2025},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/mannaandpoem/OpenManus}},
publisher = {Zenodo},
doi = {10.5281/zenodo.15186407},
url = {https://doi.org/10.5281/zenodo.15186407},
}
```
+24 -8
View File
@@ -4,10 +4,12 @@
[English](README.md) | [中文](README_zh.md) | 한국어 | [日本語](README_ja.md)
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers)
&ensp;
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407)
# 👋 OpenManus
@@ -41,7 +43,7 @@ conda activate open_manus
2. 저장소를 클론합니다:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
git clone https://github.com/FoundationAgents/OpenManus.git
cd OpenManus
```
@@ -62,7 +64,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
2. 저장소를 클론합니다:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
git clone https://github.com/FoundationAgents/OpenManus.git
cd OpenManus
```
@@ -135,6 +137,18 @@ python run_mcp.py
python run_flow.py
```
### 사용자 정의 다중 에이전트 추가
현재 일반 OpenManus 에이전트 외에도 데이터 분석 및 데이터 시각화 작업에 적합한 DataAnalysis 에이전트를 통합했습니다. 이 에이전트를 `config.toml``run_flow`에 추가할 수 있습니다.
```toml
# run-flow에 대한 선택적 구성
[runflow]
use_data_analysis_agent = true # 기본적으로 비활성화되어 있으며, 활성화하려면 true로 변경
```
또한, 에이전트가 제대로 작동하도록 관련 종속성을 설치해야 합니다: [상세 설치 가이드](app/tool/chart_visualization/README.md##Installation)
## 기여 방법
모든 친절한 제안과 유용한 기여를 환영합니다! 이슈를 생성하거나 풀 리퀘스트를 제출해 주세요.
@@ -152,7 +166,7 @@ Feishu 네트워킹 그룹에 참여하여 다른 개발자들과 경험을 공
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date)
## 감사의 글
@@ -161,16 +175,18 @@ Feishu 네트워킹 그룹에 참여하여 다른 개발자들과 경험을 공
또한, [AAAJ](https://github.com/metauto-ai/agent-as-a-judge), [MetaGPT](https://github.com/geekan/MetaGPT), [OpenHands](https://github.com/All-Hands-AI/OpenHands), [SWE-agent](https://github.com/SWE-agent/SWE-agent)에 깊은 감사를 드립니다.
또한 Hugging Face 데모 공간을 지원해 주신 阶跃星辰 (stepfun)에게 감사드립니다.
OpenManus는 MetaGPT 기여자들에 의해 개발되었습니다. 이 에이전트 커뮤니티에 깊은 감사를 전합니다!
## 인용
```bibtex
@misc{openmanus2025,
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong},
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang},
title = {OpenManus: An open-source framework for building general AI agents},
year = {2025},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/mannaandpoem/OpenManus}},
publisher = {Zenodo},
doi = {10.5281/zenodo.15186407},
url = {https://doi.org/10.5281/zenodo.15186407},
}
```
+29 -10
View File
@@ -4,10 +4,12 @@
[English](README.md) | 中文 | [한국어](README_ko.md) | [日本語](README_ja.md)
[![GitHub stars](https://img.shields.io/github/stars/mannaandpoem/OpenManus?style=social)](https://github.com/mannaandpoem/OpenManus/stargazers)
[![GitHub stars](https://img.shields.io/github/stars/FoundationAgents/OpenManus?style=social)](https://github.com/FoundationAgents/OpenManus/stargazers)
&ensp;
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) &ensp;
[![Discord Follow](https://dcbadge.vercel.app/api/server/DYn29wFk9z?style=flat)](https://discord.gg/DYn29wFk9z)
[![Demo](https://img.shields.io/badge/Demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/lyh-917/OpenManusDemo)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.15186407.svg)](https://doi.org/10.5281/zenodo.15186407)
# 👋 OpenManus
@@ -42,7 +44,7 @@ conda activate open_manus
2. 克隆仓库:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
git clone https://github.com/FoundationAgents/OpenManus.git
cd OpenManus
```
@@ -63,7 +65,7 @@ curl -LsSf https://astral.sh/uv/install.sh | sh
2. 克隆仓库:
```bash
git clone https://github.com/mannaandpoem/OpenManus.git
git clone https://github.com/FoundationAgents/OpenManus.git
cd OpenManus
```
@@ -136,6 +138,17 @@ python run_mcp.py
python run_flow.py
```
## 添加自定义多智能体
目前除了通用的 OpenManus Agent, 我们还内置了DataAnalysis Agent,适用于数据分析和数据可视化任务,你可以在`config.toml`中将这个智能体加入到`run_flow`
```toml
# run-flow可选配置
[runflow]
use_data_analysis_agent = true # 默认关闭,将其改为true则为激活
```
除此之外,你还需要安装相关的依赖来确保智能体正常运行:[具体安装指南](app/tool/chart_visualization/README_zh.md##安装)
## 贡献指南
我们欢迎任何友好的建议和有价值的贡献!可以直接创建 issue 或提交 pull request。
@@ -154,7 +167,12 @@ python run_flow.py
## Star 数量
[![Star History Chart](https://api.star-history.com/svg?repos=mannaandpoem/OpenManus&type=Date)](https://star-history.com/#mannaandpoem/OpenManus&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=FoundationAgents/OpenManus&type=Date)](https://star-history.com/#FoundationAgents/OpenManus&Date)
## 赞助商
感谢[PPIO](https://ppinfra.com/user/register?invited_by=OCPKCN&utm_source=github_openmanus&utm_medium=github_readme&utm_campaign=link) 提供的算力支持。
> PPIO派欧云:一键调用高性价比的开源模型API和GPU容器
## 致谢
@@ -163,17 +181,18 @@ python run_flow.py
此外,我们感谢 [AAAJ](https://github.com/metauto-ai/agent-as-a-judge)[MetaGPT](https://github.com/geekan/MetaGPT)[OpenHands](https://github.com/All-Hands-AI/OpenHands) 和 [SWE-agent](https://github.com/SWE-agent/SWE-agent).
我们也感谢阶跃星辰 (stepfun) 提供的 Hugging Face 演示空间支持。
OpenManus 由 MetaGPT 社区的贡献者共同构建,感谢这个充满活力的智能体开发者社区!
## 引用我们
## 引用
```bibtex
@misc{openmanus2025,
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong},
author = {Xinbin Liang and Jinyu Xiang and Zhaoyang Yu and Jiayi Zhang and Sirui Hong and Sheng Fan and Xiao Tang},
title = {OpenManus: An open-source framework for building general AI agents},
year = {2025},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/mannaandpoem/OpenManus}},
publisher = {Zenodo},
doi = {10.5281/zenodo.15186407},
url = {https://doi.org/10.5281/zenodo.15186407},
}
```
-2
View File
@@ -1,7 +1,6 @@
from app.agent.base import BaseAgent
from app.agent.browser import BrowserAgent
from app.agent.mcp import MCPAgent
from app.agent.planning import PlanningAgent
from app.agent.react import ReActAgent
from app.agent.swe import SWEAgent
from app.agent.toolcall import ToolCallAgent
@@ -10,7 +9,6 @@ from app.agent.toolcall import ToolCallAgent
__all__ = [
"BaseAgent",
"BrowserAgent",
"PlanningAgent",
"ReActAgent",
"SWEAgent",
"ToolCallAgent",
+87 -87
View File
@@ -1,13 +1,87 @@
import json
from typing import Any, Optional
from typing import TYPE_CHECKING, Optional
from pydantic import Field
from pydantic import Field, model_validator
from app.agent.toolcall import ToolCallAgent
from app.logger import logger
from app.prompt.browser import NEXT_STEP_PROMPT, SYSTEM_PROMPT
from app.schema import Message, ToolChoice
from app.tool import BrowserUseTool, Terminate, ToolCollection
from app.tool.sandbox.sb_browser_tool import SandboxBrowserTool
# Avoid circular import if BrowserAgent needs BrowserContextHelper
if TYPE_CHECKING:
from app.agent.base import BaseAgent # Or wherever memory is defined
class BrowserContextHelper:
def __init__(self, agent: "BaseAgent"):
self.agent = agent
self._current_base64_image: Optional[str] = None
async def get_browser_state(self) -> Optional[dict]:
browser_tool = self.agent.available_tools.get_tool(BrowserUseTool().name)
if not browser_tool:
browser_tool = self.agent.available_tools.get_tool(
SandboxBrowserTool().name
)
if not browser_tool or not hasattr(browser_tool, "get_current_state"):
logger.warning("BrowserUseTool not found or doesn't have get_current_state")
return None
try:
result = await browser_tool.get_current_state()
if result.error:
logger.debug(f"Browser state error: {result.error}")
return None
if hasattr(result, "base64_image") and result.base64_image:
self._current_base64_image = result.base64_image
else:
self._current_base64_image = None
return json.loads(result.output)
except Exception as e:
logger.debug(f"Failed to get browser state: {str(e)}")
return None
async def format_next_step_prompt(self) -> str:
"""Gets browser state and formats the browser prompt."""
browser_state = await self.get_browser_state()
url_info, tabs_info, content_above_info, content_below_info = "", "", "", ""
results_info = "" # Or get from agent if needed elsewhere
if browser_state and not browser_state.get("error"):
url_info = f"\n URL: {browser_state.get('url', 'N/A')}\n Title: {browser_state.get('title', 'N/A')}"
tabs = browser_state.get("tabs", [])
if tabs:
tabs_info = f"\n {len(tabs)} tab(s) available"
pixels_above = browser_state.get("pixels_above", 0)
pixels_below = browser_state.get("pixels_below", 0)
if pixels_above > 0:
content_above_info = f" ({pixels_above} pixels)"
if pixels_below > 0:
content_below_info = f" ({pixels_below} pixels)"
if self._current_base64_image:
image_message = Message.user_message(
content="Current browser screenshot:",
base64_image=self._current_base64_image,
)
self.agent.memory.add_message(image_message)
self._current_base64_image = None # Consume the image after adding
return NEXT_STEP_PROMPT.format(
url_placeholder=url_info,
tabs_placeholder=tabs_info,
content_above_placeholder=content_above_info,
content_below_placeholder=content_below_info,
results_placeholder=results_info,
)
async def cleanup_browser(self):
browser_tool = self.agent.available_tools.get_tool(BrowserUseTool().name)
if browser_tool and hasattr(browser_tool, "cleanup"):
await browser_tool.cleanup()
class BrowserAgent(ToolCallAgent):
@@ -36,94 +110,20 @@ class BrowserAgent(ToolCallAgent):
tool_choices: ToolChoice = ToolChoice.AUTO
special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
_current_base64_image: Optional[str] = None
browser_context_helper: Optional[BrowserContextHelper] = None
async def _handle_special_tool(self, name: str, result: Any, **kwargs):
if not self._is_special_tool(name):
return
else:
await self.available_tools.get_tool(BrowserUseTool().name).cleanup()
await super()._handle_special_tool(name, result, **kwargs)
async def get_browser_state(self) -> Optional[dict]:
"""Get the current browser state for context in next steps."""
browser_tool = self.available_tools.get_tool(BrowserUseTool().name)
if not browser_tool:
return None
try:
# Get browser state directly from the tool
result = await browser_tool.get_current_state()
if result.error:
logger.debug(f"Browser state error: {result.error}")
return None
# Store screenshot if available
if hasattr(result, "base64_image") and result.base64_image:
self._current_base64_image = result.base64_image
# Parse the state info
return json.loads(result.output)
except Exception as e:
logger.debug(f"Failed to get browser state: {str(e)}")
return None
@model_validator(mode="after")
def initialize_helper(self) -> "BrowserAgent":
self.browser_context_helper = BrowserContextHelper(self)
return self
async def think(self) -> bool:
"""Process current state and decide next actions using tools, with browser state info added"""
# Add browser state to the context
browser_state = await self.get_browser_state()
# Initialize placeholder values
url_info = ""
tabs_info = ""
content_above_info = ""
content_below_info = ""
results_info = ""
if browser_state and not browser_state.get("error"):
# URL and title info
url_info = f"\n URL: {browser_state.get('url', 'N/A')}\n Title: {browser_state.get('title', 'N/A')}"
# Tab information
if "tabs" in browser_state:
tabs = browser_state.get("tabs", [])
if tabs:
tabs_info = f"\n {len(tabs)} tab(s) available"
# Content above/below viewport
pixels_above = browser_state.get("pixels_above", 0)
pixels_below = browser_state.get("pixels_below", 0)
if pixels_above > 0:
content_above_info = f" ({pixels_above} pixels)"
if pixels_below > 0:
content_below_info = f" ({pixels_below} pixels)"
# Add screenshot as base64 if available
if self._current_base64_image:
# Create a message with image attachment
image_message = Message.user_message(
content="Current browser screenshot:",
base64_image=self._current_base64_image,
)
self.memory.add_message(image_message)
# Replace placeholders with actual browser state info
self.next_step_prompt = NEXT_STEP_PROMPT.format(
url_placeholder=url_info,
tabs_placeholder=tabs_info,
content_above_placeholder=content_above_info,
content_below_placeholder=content_below_info,
results_placeholder=results_info,
self.next_step_prompt = (
await self.browser_context_helper.format_next_step_prompt()
)
return await super().think()
# Call parent implementation
result = await super().think()
# Reset the next_step_prompt to its original state
self.next_step_prompt = NEXT_STEP_PROMPT
return result
async def cleanup(self):
"""Clean up browser agent resources by calling parent cleanup."""
await self.browser_context_helper.cleanup_browser()
+85
View File
@@ -0,0 +1,85 @@
from pydantic import Field
from app.agent.toolcall import ToolCallAgent
from app.config import config
from app.prompt.visualization import NEXT_STEP_PROMPT, SYSTEM_PROMPT
from app.tool import Terminate, ToolCollection
from app.tool.chart_visualization.python_execute import NormalPythonExecute
# from app.tool.chart_visualization.chart_prepare import VisualizationPrepare
# from app.tool.chart_visualization.data_visualization import DataVisualization
# from app.tool.chart_visualization.initial_report_generation import GenerateInitialReport
# from app.tool.chart_visualization.final_report_generation import GenerateFinalReport
# from app.tool.chart_visualization.search_report_template import SearchReportTemplate
# from app.tool.chart_visualization.report_template_generation import ReportTemplateGeneration
# from app.tool.chart_visualization.initial_information_collection import InitialInformationCollection
from app.tool.chart_visualization.chart_prepare import VisualizationPrepare
from app.tool.chart_visualization.select_insights import SelectInsights
from app.tool.chart_visualization.add_insights import AddInsights
from app.tool.chart_visualization.data_visualization import DataVisualization
from app.tool.chart_visualization.v2.search_html_library import SearchHtmlLibrary
from app.tool.chart_visualization.v2.initial_report_generation import GenerateInitialReport
from app.tool.chart_visualization.v2.report_template_generation import ReportTemplateGeneration
from app.tool.chart_visualization.v2.final_report_generation import GenerateFinalReport
from app.tool.chart_visualization.v2.report_beautify import ReportBeautify
class DataAnalysis(ToolCallAgent):
"""
A data analysis agent that uses planning to solve various data analysis tasks.
This agent extends ToolCallAgent with a comprehensive set of tools and capabilities,
including Data Analysis, Chart Visualization, Data Report.
"""
name: str = "Data_Analysis"
description: str = """
A data science agent specializing in Python-based analytics and advanced visualization techniques
for solving complex data analysis challenges.
Standard Report Generation Workflow:
1. Template Preparation:
- SearchHtmlLibrary: Identify suitable visualization templates
- ReportTemplateGeneration & GenerateInitialReport: Create initial report structure
2. Visualization Pipeline:
- VisualizationPrepare: Configure data for visualization
- DataVisualization: Generate interactive charts and graphs
3. Insight Enhancement:
- SelectInsights: Extract key findings from visualizations
- AddInsights: Annotate charts with analytical insights
4. Report Finalization:
- GenerateFinalReport: Replace the placeholders with charts
- ReportBeautify: Apply professional styling and formatting
Operational Protocol:
- First determine optimal visualization types based on dataset characteristics
- Utilize HTML template library to establish report framework
- Execute visualization pipeline to create data representations
- Enhance each chart with key insights you selected
- Assemble final report by embedding enriched visualizations
"""
system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root)
next_step_prompt: str = NEXT_STEP_PROMPT
max_observe: int = 15000
max_steps: int = 20
# Add general-purpose tools to the tool collection
available_tools: ToolCollection = Field(
default_factory=lambda: ToolCollection(
NormalPythonExecute(),
SearchHtmlLibrary(),
ReportTemplateGeneration(),
GenerateInitialReport(),
GenerateFinalReport(),
ReportBeautify(),
AddInsights(),
VisualizationPrepare(),
DataVisualization(),
SelectInsights(),
Terminate(),
)
)
+125 -23
View File
@@ -1,28 +1,25 @@
from pydantic import Field
from typing import Dict, List, Optional
from app.agent.browser import BrowserAgent
from pydantic import Field, model_validator
from app.agent.browser import BrowserContextHelper
from app.agent.toolcall import ToolCallAgent
from app.config import config
from app.prompt.browser import NEXT_STEP_PROMPT as BROWSER_NEXT_STEP_PROMPT
from app.logger import logger
from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT
from app.tool import Terminate, ToolCollection
from app.tool.ask_human import AskHuman
from app.tool.browser_use_tool import BrowserUseTool
from app.tool.mcp import MCPClients, MCPClientTool
from app.tool.python_execute import PythonExecute
from app.tool.str_replace_editor import StrReplaceEditor
class Manus(BrowserAgent):
"""
A versatile general-purpose agent that uses planning to solve various tasks.
This agent extends BrowserAgent with a comprehensive set of tools and capabilities,
including Python execution, web browsing, file operations, and information retrieval
to handle a wide range of user requests.
"""
class Manus(ToolCallAgent):
"""A versatile general-purpose agent with support for both local and MCP tools."""
name: str = "Manus"
description: str = (
"A versatile agent that can solve various tasks using multiple tools"
)
description: str = "A versatile agent that can solve various tasks using multiple tools including MCP-based tools"
system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root)
next_step_prompt: str = NEXT_STEP_PROMPT
@@ -30,31 +27,136 @@ class Manus(BrowserAgent):
max_observe: int = 10000
max_steps: int = 20
# MCP clients for remote tool access
mcp_clients: MCPClients = Field(default_factory=MCPClients)
# Add general-purpose tools to the tool collection
available_tools: ToolCollection = Field(
default_factory=lambda: ToolCollection(
PythonExecute(), BrowserUseTool(), StrReplaceEditor(), Terminate()
PythonExecute(),
BrowserUseTool(),
StrReplaceEditor(),
AskHuman(),
Terminate(),
)
)
special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
browser_context_helper: Optional[BrowserContextHelper] = None
# Track connected MCP servers
connected_servers: Dict[str, str] = Field(
default_factory=dict
) # server_id -> url/command
_initialized: bool = False
@model_validator(mode="after")
def initialize_helper(self) -> "Manus":
"""Initialize basic components synchronously."""
self.browser_context_helper = BrowserContextHelper(self)
return self
@classmethod
async def create(cls, **kwargs) -> "Manus":
"""Factory method to create and properly initialize a Manus instance."""
instance = cls(**kwargs)
await instance.initialize_mcp_servers()
instance._initialized = True
return instance
async def initialize_mcp_servers(self) -> None:
"""Initialize connections to configured MCP servers."""
for server_id, server_config in config.mcp_config.servers.items():
try:
if server_config.type == "sse":
if server_config.url:
await self.connect_mcp_server(server_config.url, server_id)
logger.info(
f"Connected to MCP server {server_id} at {server_config.url}"
)
elif server_config.type == "stdio":
if server_config.command:
await self.connect_mcp_server(
server_config.command,
server_id,
use_stdio=True,
stdio_args=server_config.args,
)
logger.info(
f"Connected to MCP server {server_id} using command {server_config.command}"
)
except Exception as e:
logger.error(f"Failed to connect to MCP server {server_id}: {e}")
async def connect_mcp_server(
self,
server_url: str,
server_id: str = "",
use_stdio: bool = False,
stdio_args: List[str] = None,
) -> None:
"""Connect to an MCP server and add its tools."""
if use_stdio:
await self.mcp_clients.connect_stdio(
server_url, stdio_args or [], server_id
)
self.connected_servers[server_id or server_url] = server_url
else:
await self.mcp_clients.connect_sse(server_url, server_id)
self.connected_servers[server_id or server_url] = server_url
# Update available tools with only the new tools from this server
new_tools = [
tool for tool in self.mcp_clients.tools if tool.server_id == server_id
]
self.available_tools.add_tools(*new_tools)
async def disconnect_mcp_server(self, server_id: str = "") -> None:
"""Disconnect from an MCP server and remove its tools."""
await self.mcp_clients.disconnect(server_id)
if server_id:
self.connected_servers.pop(server_id, None)
else:
self.connected_servers.clear()
# Rebuild available tools without the disconnected server's tools
base_tools = [
tool
for tool in self.available_tools.tools
if not isinstance(tool, MCPClientTool)
]
self.available_tools = ToolCollection(*base_tools)
self.available_tools.add_tools(*self.mcp_clients.tools)
async def cleanup(self):
"""Clean up Manus agent resources."""
if self.browser_context_helper:
await self.browser_context_helper.cleanup_browser()
# Disconnect from all MCP servers only if we were initialized
if self._initialized:
await self.disconnect_mcp_server()
self._initialized = False
async def think(self) -> bool:
"""Process current state and decide next actions with appropriate context."""
# Store original prompt
original_prompt = self.next_step_prompt
if not self._initialized:
await self.initialize_mcp_servers()
self._initialized = True
# Only check recent messages (last 3) for browser activity
original_prompt = self.next_step_prompt
recent_messages = self.memory.messages[-3:] if self.memory.messages else []
browser_in_use = any(
"browser_use" in msg.content.lower()
tc.function.name == BrowserUseTool().name
for msg in recent_messages
if hasattr(msg, "content") and isinstance(msg.content, str)
if msg.tool_calls
for tc in msg.tool_calls
)
if browser_in_use:
# Override with browser-specific prompt temporarily to get browser context
self.next_step_prompt = BROWSER_NEXT_STEP_PROMPT
self.next_step_prompt = (
await self.browser_context_helper.format_next_step_prompt()
)
# Call parent's think method
result = await super().think()
# Restore original prompt
+4 -4
View File
@@ -90,11 +90,11 @@ class MCPAgent(ToolCallAgent):
Returns:
A tuple of (added_tools, removed_tools)
"""
if not self.mcp_clients.session:
if not self.mcp_clients.sessions:
return [], []
# Get current tool schemas directly from the server
response = await self.mcp_clients.session.list_tools()
response = await self.mcp_clients.list_tools()
current_tools = {tool.name: tool.inputSchema for tool in response.tools}
# Determine added, removed, and changed tools
@@ -134,7 +134,7 @@ class MCPAgent(ToolCallAgent):
async def think(self) -> bool:
"""Process current state and decide next action."""
# Check MCP session and tools availability
if not self.mcp_clients.session or not self.mcp_clients.tool_map:
if not self.mcp_clients.sessions or not self.mcp_clients.tool_map:
logger.info("MCP service is no longer available, ending interaction")
self.state = AgentState.FINISHED
return False
@@ -171,7 +171,7 @@ class MCPAgent(ToolCallAgent):
async def cleanup(self) -> None:
"""Clean up MCP connection when done."""
if self.mcp_clients.session:
if self.mcp_clients.sessions:
await self.mcp_clients.disconnect()
logger.info("MCP connection closed")
-259
View File
@@ -1,259 +0,0 @@
import time
from typing import Dict, List, Optional
from pydantic import Field, model_validator
from app.agent.toolcall import ToolCallAgent
from app.logger import logger
from app.prompt.planning import NEXT_STEP_PROMPT, PLANNING_SYSTEM_PROMPT
from app.schema import TOOL_CHOICE_TYPE, Message, ToolCall, ToolChoice
from app.tool import PlanningTool, Terminate, ToolCollection
class PlanningAgent(ToolCallAgent):
"""
An agent that creates and manages plans to solve tasks.
This agent uses a planning tool to create and manage structured plans,
and tracks progress through individual steps until task completion.
"""
name: str = "planning"
description: str = "An agent that creates and manages plans to solve tasks"
system_prompt: str = PLANNING_SYSTEM_PROMPT
next_step_prompt: str = NEXT_STEP_PROMPT
available_tools: ToolCollection = Field(
default_factory=lambda: ToolCollection(PlanningTool(), Terminate())
)
tool_choices: TOOL_CHOICE_TYPE = ToolChoice.AUTO # type: ignore
special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name])
tool_calls: List[ToolCall] = Field(default_factory=list)
active_plan_id: Optional[str] = Field(default=None)
# Add a dictionary to track the step status for each tool call
step_execution_tracker: Dict[str, Dict] = Field(default_factory=dict)
current_step_index: Optional[int] = None
max_steps: int = 20
@model_validator(mode="after")
def initialize_plan_and_verify_tools(self) -> "PlanningAgent":
"""Initialize the agent with a default plan ID and validate required tools."""
self.active_plan_id = f"plan_{int(time.time())}"
if "planning" not in self.available_tools.tool_map:
self.available_tools.add_tool(PlanningTool())
return self
async def think(self) -> bool:
"""Decide the next action based on plan status."""
prompt = (
f"CURRENT PLAN STATUS:\n{await self.get_plan()}\n\n{self.next_step_prompt}"
if self.active_plan_id
else self.next_step_prompt
)
self.messages.append(Message.user_message(prompt))
# Get the current step index before thinking
self.current_step_index = await self._get_current_step_index()
result = await super().think()
# After thinking, if we decided to execute a tool and it's not a planning tool or special tool,
# associate it with the current step for tracking
if result and self.tool_calls:
latest_tool_call = self.tool_calls[0] # Get the most recent tool call
if (
latest_tool_call.function.name != "planning"
and latest_tool_call.function.name not in self.special_tool_names
and self.current_step_index is not None
):
self.step_execution_tracker[latest_tool_call.id] = {
"step_index": self.current_step_index,
"tool_name": latest_tool_call.function.name,
"status": "pending", # Will be updated after execution
}
return result
async def act(self) -> str:
"""Execute a step and track its completion status."""
result = await super().act()
# After executing the tool, update the plan status
if self.tool_calls:
latest_tool_call = self.tool_calls[0]
# Update the execution status to completed
if latest_tool_call.id in self.step_execution_tracker:
self.step_execution_tracker[latest_tool_call.id]["status"] = "completed"
self.step_execution_tracker[latest_tool_call.id]["result"] = result
# Update the plan status if this was a non-planning, non-special tool
if (
latest_tool_call.function.name != "planning"
and latest_tool_call.function.name not in self.special_tool_names
):
await self.update_plan_status(latest_tool_call.id)
return result
async def get_plan(self) -> str:
"""Retrieve the current plan status."""
if not self.active_plan_id:
return "No active plan. Please create a plan first."
result = await self.available_tools.execute(
name="planning",
tool_input={"command": "get", "plan_id": self.active_plan_id},
)
return result.output if hasattr(result, "output") else str(result)
async def run(self, request: Optional[str] = None) -> str:
"""Run the agent with an optional initial request."""
if request:
await self.create_initial_plan(request)
return await super().run()
async def update_plan_status(self, tool_call_id: str) -> None:
"""
Update the current plan progress based on completed tool execution.
Only marks a step as completed if the associated tool has been successfully executed.
"""
if not self.active_plan_id:
return
if tool_call_id not in self.step_execution_tracker:
logger.warning(f"No step tracking found for tool call {tool_call_id}")
return
tracker = self.step_execution_tracker[tool_call_id]
if tracker["status"] != "completed":
logger.warning(f"Tool call {tool_call_id} has not completed successfully")
return
step_index = tracker["step_index"]
try:
# Mark the step as completed
await self.available_tools.execute(
name="planning",
tool_input={
"command": "mark_step",
"plan_id": self.active_plan_id,
"step_index": step_index,
"step_status": "completed",
},
)
logger.info(
f"Marked step {step_index} as completed in plan {self.active_plan_id}"
)
except Exception as e:
logger.warning(f"Failed to update plan status: {e}")
async def _get_current_step_index(self) -> Optional[int]:
"""
Parse the current plan to identify the first non-completed step's index.
Returns None if no active step is found.
"""
if not self.active_plan_id:
return None
plan = await self.get_plan()
try:
plan_lines = plan.splitlines()
steps_index = -1
# Find the index of the "Steps:" line
for i, line in enumerate(plan_lines):
if line.strip() == "Steps:":
steps_index = i
break
if steps_index == -1:
return None
# Find the first non-completed step
for i, line in enumerate(plan_lines[steps_index + 1 :], start=0):
if "[ ]" in line or "[→]" in line: # not_started or in_progress
# Mark current step as in_progress
await self.available_tools.execute(
name="planning",
tool_input={
"command": "mark_step",
"plan_id": self.active_plan_id,
"step_index": i,
"step_status": "in_progress",
},
)
return i
return None # No active step found
except Exception as e:
logger.warning(f"Error finding current step index: {e}")
return None
async def create_initial_plan(self, request: str) -> None:
"""Create an initial plan based on the request."""
logger.info(f"Creating initial plan with ID: {self.active_plan_id}")
messages = [
Message.user_message(
f"Analyze the request and create a plan with ID {self.active_plan_id}: {request}"
)
]
self.memory.add_messages(messages)
response = await self.llm.ask_tool(
messages=messages,
system_msgs=[Message.system_message(self.system_prompt)],
tools=self.available_tools.to_params(),
tool_choice=ToolChoice.AUTO,
)
assistant_msg = Message.from_tool_calls(
content=response.content, tool_calls=response.tool_calls
)
self.memory.add_message(assistant_msg)
plan_created = False
for tool_call in response.tool_calls:
if tool_call.function.name == "planning":
result = await self.execute_tool(tool_call)
logger.info(
f"Executed tool {tool_call.function.name} with result: {result}"
)
# Add tool response to memory
tool_msg = Message.tool_message(
content=result,
tool_call_id=tool_call.id,
name=tool_call.function.name,
)
self.memory.add_message(tool_msg)
plan_created = True
break
if not plan_created:
logger.warning("No plan created from initial request")
tool_msg = Message.assistant_message(
"Error: Parameter `plan_id` is required for command: create"
)
self.memory.add_message(tool_msg)
async def main():
# Configure and run the agent
agent = PlanningAgent(available_tools=ToolCollection(PlanningTool(), Terminate()))
result = await agent.run("Help me plan a trip to the moon")
print(result)
if __name__ == "__main__":
import asyncio
asyncio.run(main())
+223
View File
@@ -0,0 +1,223 @@
from typing import Dict, List, Optional
from pydantic import Field, model_validator
from app.agent.browser import BrowserContextHelper
from app.agent.toolcall import ToolCallAgent
from app.config import config
from app.daytona.sandbox import create_sandbox, delete_sandbox
from app.daytona.tool_base import SandboxToolsBase
from app.logger import logger
from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT
from app.tool import Terminate, ToolCollection
from app.tool.ask_human import AskHuman
from app.tool.mcp import MCPClients, MCPClientTool
from app.tool.sandbox.sb_browser_tool import SandboxBrowserTool
from app.tool.sandbox.sb_files_tool import SandboxFilesTool
from app.tool.sandbox.sb_shell_tool import SandboxShellTool
from app.tool.sandbox.sb_vision_tool import SandboxVisionTool
class SandboxManus(ToolCallAgent):
"""A versatile general-purpose agent with support for both local and MCP tools."""
name: str = "SandboxManus"
description: str = "A versatile agent that can solve various tasks using multiple sandbox-tools including MCP-based tools"
system_prompt: str = SYSTEM_PROMPT.format(directory=config.workspace_root)
next_step_prompt: str = NEXT_STEP_PROMPT
max_observe: int = 10000
max_steps: int = 20
# MCP clients for remote tool access
mcp_clients: MCPClients = Field(default_factory=MCPClients)
# Add general-purpose tools to the tool collection
available_tools: ToolCollection = Field(
default_factory=lambda: ToolCollection(
# PythonExecute(),
# BrowserUseTool(),
# StrReplaceEditor(),
AskHuman(),
Terminate(),
)
)
special_tool_names: list[str] = Field(default_factory=lambda: [Terminate().name])
browser_context_helper: Optional[BrowserContextHelper] = None
# Track connected MCP servers
connected_servers: Dict[str, str] = Field(
default_factory=dict
) # server_id -> url/command
_initialized: bool = False
sandbox_link: Optional[dict[str, dict[str, str]]] = Field(default_factory=dict)
@model_validator(mode="after")
def initialize_helper(self) -> "SandboxManus":
"""Initialize basic components synchronously."""
self.browser_context_helper = BrowserContextHelper(self)
return self
@classmethod
async def create(cls, **kwargs) -> "SandboxManus":
"""Factory method to create and properly initialize a Manus instance."""
instance = cls(**kwargs)
await instance.initialize_mcp_servers()
await instance.initialize_sandbox_tools()
instance._initialized = True
return instance
async def initialize_sandbox_tools(
self,
password: str = config.daytona.VNC_password,
) -> None:
try:
# 创建新沙箱
if password:
sandbox = create_sandbox(password=password)
self.sandbox = sandbox
else:
raise ValueError("password must be provided")
vnc_link = sandbox.get_preview_link(6080)
website_link = sandbox.get_preview_link(8080)
vnc_url = vnc_link.url if hasattr(vnc_link, "url") else str(vnc_link)
website_url = (
website_link.url if hasattr(website_link, "url") else str(website_link)
)
# Get the actual sandbox_id from the created sandbox
actual_sandbox_id = sandbox.id if hasattr(sandbox, "id") else "new_sandbox"
if not self.sandbox_link:
self.sandbox_link = {}
self.sandbox_link[actual_sandbox_id] = {
"vnc": vnc_url,
"website": website_url,
}
logger.info(f"VNC URL: {vnc_url}")
logger.info(f"Website URL: {website_url}")
SandboxToolsBase._urls_printed = True
sb_tools = [
SandboxBrowserTool(sandbox),
SandboxFilesTool(sandbox),
SandboxShellTool(sandbox),
SandboxVisionTool(sandbox),
]
self.available_tools.add_tools(*sb_tools)
except Exception as e:
logger.error(f"Error initializing sandbox tools: {e}")
raise
async def initialize_mcp_servers(self) -> None:
"""Initialize connections to configured MCP servers."""
for server_id, server_config in config.mcp_config.servers.items():
try:
if server_config.type == "sse":
if server_config.url:
await self.connect_mcp_server(server_config.url, server_id)
logger.info(
f"Connected to MCP server {server_id} at {server_config.url}"
)
elif server_config.type == "stdio":
if server_config.command:
await self.connect_mcp_server(
server_config.command,
server_id,
use_stdio=True,
stdio_args=server_config.args,
)
logger.info(
f"Connected to MCP server {server_id} using command {server_config.command}"
)
except Exception as e:
logger.error(f"Failed to connect to MCP server {server_id}: {e}")
async def connect_mcp_server(
self,
server_url: str,
server_id: str = "",
use_stdio: bool = False,
stdio_args: List[str] = None,
) -> None:
"""Connect to an MCP server and add its tools."""
if use_stdio:
await self.mcp_clients.connect_stdio(
server_url, stdio_args or [], server_id
)
self.connected_servers[server_id or server_url] = server_url
else:
await self.mcp_clients.connect_sse(server_url, server_id)
self.connected_servers[server_id or server_url] = server_url
# Update available tools with only the new tools from this server
new_tools = [
tool for tool in self.mcp_clients.tools if tool.server_id == server_id
]
self.available_tools.add_tools(*new_tools)
async def disconnect_mcp_server(self, server_id: str = "") -> None:
"""Disconnect from an MCP server and remove its tools."""
await self.mcp_clients.disconnect(server_id)
if server_id:
self.connected_servers.pop(server_id, None)
else:
self.connected_servers.clear()
# Rebuild available tools without the disconnected server's tools
base_tools = [
tool
for tool in self.available_tools.tools
if not isinstance(tool, MCPClientTool)
]
self.available_tools = ToolCollection(*base_tools)
self.available_tools.add_tools(*self.mcp_clients.tools)
async def delete_sandbox(self, sandbox_id: str) -> None:
"""Delete a sandbox by ID."""
try:
await delete_sandbox(sandbox_id)
logger.info(f"Sandbox {sandbox_id} deleted successfully")
if sandbox_id in self.sandbox_link:
del self.sandbox_link[sandbox_id]
except Exception as e:
logger.error(f"Error deleting sandbox {sandbox_id}: {e}")
raise e
async def cleanup(self):
"""Clean up Manus agent resources."""
if self.browser_context_helper:
await self.browser_context_helper.cleanup_browser()
# Disconnect from all MCP servers only if we were initialized
if self._initialized:
await self.disconnect_mcp_server()
await self.delete_sandbox(self.sandbox.id if self.sandbox else "unknown")
self._initialized = False
async def think(self) -> bool:
"""Process current state and decide next actions with appropriate context."""
if not self._initialized:
await self.initialize_mcp_servers()
self._initialized = True
original_prompt = self.next_step_prompt
recent_messages = self.memory.messages[-3:] if self.memory.messages else []
browser_in_use = any(
tc.function.name == SandboxBrowserTool().name
for msg in recent_messages
if msg.tool_calls
for tc in msg.tool_calls
)
if browser_in_use:
self.next_step_prompt = (
await self.browser_context_helper.format_next_step_prompt()
)
result = await super().think()
# Restore original prompt
self.next_step_prompt = original_prompt
return result
+3 -17
View File
@@ -3,7 +3,7 @@ from typing import List
from pydantic import Field
from app.agent.toolcall import ToolCallAgent
from app.prompt.swe import NEXT_STEP_TEMPLATE, SYSTEM_PROMPT
from app.prompt.swe import SYSTEM_PROMPT
from app.tool import Bash, StrReplaceEditor, Terminate, ToolCollection
@@ -14,25 +14,11 @@ class SWEAgent(ToolCallAgent):
description: str = "an autonomous AI programmer that interacts directly with the computer to solve tasks."
system_prompt: str = SYSTEM_PROMPT
next_step_prompt: str = NEXT_STEP_TEMPLATE
next_step_prompt: str = ""
available_tools: ToolCollection = ToolCollection(
Bash(), StrReplaceEditor(), Terminate()
)
special_tool_names: List[str] = Field(default_factory=lambda: [Terminate().name])
max_steps: int = 30
bash: Bash = Field(default_factory=Bash)
working_dir: str = "."
async def think(self) -> bool:
"""Process current state and decide next action"""
# Update working directory
result = await self.bash.execute("pwd")
self.working_dir = result.output
self.next_step_prompt = self.next_step_prompt.format(
current_dir=self.working_dir
)
return await super().think()
max_steps: int = 20
+24 -8
View File
@@ -1,3 +1,4 @@
import asyncio
import json
from typing import Any, List, Optional, Union
@@ -187,14 +188,6 @@ class ToolCallAgent(ReActAgent):
# Store the base64_image for later use in tool_message
self._current_base64_image = result.base64_image
# Format result for display
observation = (
f"Observed output of cmd `{name}` executed:\n{str(result)}"
if result
else f"Cmd `{name}` completed with no output"
)
return observation
# Format result for display (standard case)
observation = (
f"Observed output of cmd `{name}` executed:\n{str(result)}"
@@ -232,3 +225,26 @@ class ToolCallAgent(ReActAgent):
def _is_special_tool(self, name: str) -> bool:
"""Check if tool name is in special tools list"""
return name.lower() in [n.lower() for n in self.special_tool_names]
async def cleanup(self):
"""Clean up resources used by the agent's tools."""
logger.info(f"🧹 Cleaning up resources for agent '{self.name}'...")
for tool_name, tool_instance in self.available_tools.tool_map.items():
if hasattr(tool_instance, "cleanup") and asyncio.iscoroutinefunction(
tool_instance.cleanup
):
try:
logger.debug(f"🧼 Cleaning up tool: {tool_name}")
await tool_instance.cleanup()
except Exception as e:
logger.error(
f"🚨 Error cleaning up tool '{tool_name}': {e}", exc_info=True
)
logger.info(f"✨ Cleanup complete for agent '{self.name}'.")
async def run(self, request: Optional[str] = None) -> str:
"""Run the agent with cleanup when done."""
try:
return await super().run(request)
finally:
await self.cleanup()
+334
View File
@@ -0,0 +1,334 @@
import json
import sys
import time
import uuid
from datetime import datetime
from typing import Dict, List, Literal, Optional
import boto3
# Global variables to track the current tool use ID across function calls
# Tmp solution
CURRENT_TOOLUSE_ID = None
# Class to handle OpenAI-style response formatting
class OpenAIResponse:
def __init__(self, data):
# Recursively convert nested dicts and lists to OpenAIResponse objects
for key, value in data.items():
if isinstance(value, dict):
value = OpenAIResponse(value)
elif isinstance(value, list):
value = [
OpenAIResponse(item) if isinstance(item, dict) else item
for item in value
]
setattr(self, key, value)
def model_dump(self, *args, **kwargs):
# Convert object to dict and add timestamp
data = self.__dict__
data["created_at"] = datetime.now().isoformat()
return data
# Main client class for interacting with Amazon Bedrock
class BedrockClient:
def __init__(self):
# Initialize Bedrock client, you need to configure AWS env first
try:
self.client = boto3.client("bedrock-runtime")
self.chat = Chat(self.client)
except Exception as e:
print(f"Error initializing Bedrock client: {e}")
sys.exit(1)
# Chat interface class
class Chat:
def __init__(self, client):
self.completions = ChatCompletions(client)
# Core class handling chat completions functionality
class ChatCompletions:
def __init__(self, client):
self.client = client
def _convert_openai_tools_to_bedrock_format(self, tools):
# Convert OpenAI function calling format to Bedrock tool format
bedrock_tools = []
for tool in tools:
if tool.get("type") == "function":
function = tool.get("function", {})
bedrock_tool = {
"toolSpec": {
"name": function.get("name", ""),
"description": function.get("description", ""),
"inputSchema": {
"json": {
"type": "object",
"properties": function.get("parameters", {}).get(
"properties", {}
),
"required": function.get("parameters", {}).get(
"required", []
),
}
},
}
}
bedrock_tools.append(bedrock_tool)
return bedrock_tools
def _convert_openai_messages_to_bedrock_format(self, messages):
# Convert OpenAI message format to Bedrock message format
bedrock_messages = []
system_prompt = []
for message in messages:
if message.get("role") == "system":
system_prompt = [{"text": message.get("content")}]
elif message.get("role") == "user":
bedrock_message = {
"role": message.get("role", "user"),
"content": [{"text": message.get("content")}],
}
bedrock_messages.append(bedrock_message)
elif message.get("role") == "assistant":
bedrock_message = {
"role": "assistant",
"content": [{"text": message.get("content")}],
}
openai_tool_calls = message.get("tool_calls", [])
if openai_tool_calls:
bedrock_tool_use = {
"toolUseId": openai_tool_calls[0]["id"],
"name": openai_tool_calls[0]["function"]["name"],
"input": json.loads(
openai_tool_calls[0]["function"]["arguments"]
),
}
bedrock_message["content"].append({"toolUse": bedrock_tool_use})
global CURRENT_TOOLUSE_ID
CURRENT_TOOLUSE_ID = openai_tool_calls[0]["id"]
bedrock_messages.append(bedrock_message)
elif message.get("role") == "tool":
bedrock_message = {
"role": "user",
"content": [
{
"toolResult": {
"toolUseId": CURRENT_TOOLUSE_ID,
"content": [{"text": message.get("content")}],
}
}
],
}
bedrock_messages.append(bedrock_message)
else:
raise ValueError(f"Invalid role: {message.get('role')}")
return system_prompt, bedrock_messages
def _convert_bedrock_response_to_openai_format(self, bedrock_response):
# Convert Bedrock response format to OpenAI format
content = ""
if bedrock_response.get("output", {}).get("message", {}).get("content"):
content_array = bedrock_response["output"]["message"]["content"]
content = "".join(item.get("text", "") for item in content_array)
if content == "":
content = "."
# Handle tool calls in response
openai_tool_calls = []
if bedrock_response.get("output", {}).get("message", {}).get("content"):
for content_item in bedrock_response["output"]["message"]["content"]:
if content_item.get("toolUse"):
bedrock_tool_use = content_item["toolUse"]
global CURRENT_TOOLUSE_ID
CURRENT_TOOLUSE_ID = bedrock_tool_use["toolUseId"]
openai_tool_call = {
"id": CURRENT_TOOLUSE_ID,
"type": "function",
"function": {
"name": bedrock_tool_use["name"],
"arguments": json.dumps(bedrock_tool_use["input"]),
},
}
openai_tool_calls.append(openai_tool_call)
# Construct final OpenAI format response
openai_format = {
"id": f"chatcmpl-{uuid.uuid4()}",
"created": int(time.time()),
"object": "chat.completion",
"system_fingerprint": None,
"choices": [
{
"finish_reason": bedrock_response.get("stopReason", "end_turn"),
"index": 0,
"message": {
"content": content,
"role": bedrock_response.get("output", {})
.get("message", {})
.get("role", "assistant"),
"tool_calls": openai_tool_calls
if openai_tool_calls != []
else None,
"function_call": None,
},
}
],
"usage": {
"completion_tokens": bedrock_response.get("usage", {}).get(
"outputTokens", 0
),
"prompt_tokens": bedrock_response.get("usage", {}).get(
"inputTokens", 0
),
"total_tokens": bedrock_response.get("usage", {}).get("totalTokens", 0),
},
}
return OpenAIResponse(openai_format)
async def _invoke_bedrock(
self,
model: str,
messages: List[Dict[str, str]],
max_tokens: int,
temperature: float,
tools: Optional[List[dict]] = None,
tool_choice: Literal["none", "auto", "required"] = "auto",
**kwargs,
) -> OpenAIResponse:
# Non-streaming invocation of Bedrock model
(
system_prompt,
bedrock_messages,
) = self._convert_openai_messages_to_bedrock_format(messages)
response = self.client.converse(
modelId=model,
system=system_prompt,
messages=bedrock_messages,
inferenceConfig={"temperature": temperature, "maxTokens": max_tokens},
toolConfig={"tools": tools} if tools else None,
)
openai_response = self._convert_bedrock_response_to_openai_format(response)
return openai_response
async def _invoke_bedrock_stream(
self,
model: str,
messages: List[Dict[str, str]],
max_tokens: int,
temperature: float,
tools: Optional[List[dict]] = None,
tool_choice: Literal["none", "auto", "required"] = "auto",
**kwargs,
) -> OpenAIResponse:
# Streaming invocation of Bedrock model
(
system_prompt,
bedrock_messages,
) = self._convert_openai_messages_to_bedrock_format(messages)
response = self.client.converse_stream(
modelId=model,
system=system_prompt,
messages=bedrock_messages,
inferenceConfig={"temperature": temperature, "maxTokens": max_tokens},
toolConfig={"tools": tools} if tools else None,
)
# Initialize response structure
bedrock_response = {
"output": {"message": {"role": "", "content": []}},
"stopReason": "",
"usage": {},
"metrics": {},
}
bedrock_response_text = ""
bedrock_response_tool_input = ""
# Process streaming response
stream = response.get("stream")
if stream:
for event in stream:
if event.get("messageStart", {}).get("role"):
bedrock_response["output"]["message"]["role"] = event[
"messageStart"
]["role"]
if event.get("contentBlockDelta", {}).get("delta", {}).get("text"):
bedrock_response_text += event["contentBlockDelta"]["delta"]["text"]
print(
event["contentBlockDelta"]["delta"]["text"], end="", flush=True
)
if event.get("contentBlockStop", {}).get("contentBlockIndex") == 0:
bedrock_response["output"]["message"]["content"].append(
{"text": bedrock_response_text}
)
if event.get("contentBlockStart", {}).get("start", {}).get("toolUse"):
bedrock_tool_use = event["contentBlockStart"]["start"]["toolUse"]
tool_use = {
"toolUseId": bedrock_tool_use["toolUseId"],
"name": bedrock_tool_use["name"],
}
bedrock_response["output"]["message"]["content"].append(
{"toolUse": tool_use}
)
global CURRENT_TOOLUSE_ID
CURRENT_TOOLUSE_ID = bedrock_tool_use["toolUseId"]
if event.get("contentBlockDelta", {}).get("delta", {}).get("toolUse"):
bedrock_response_tool_input += event["contentBlockDelta"]["delta"][
"toolUse"
]["input"]
print(
event["contentBlockDelta"]["delta"]["toolUse"]["input"],
end="",
flush=True,
)
if event.get("contentBlockStop", {}).get("contentBlockIndex") == 1:
bedrock_response["output"]["message"]["content"][1]["toolUse"][
"input"
] = json.loads(bedrock_response_tool_input)
print()
openai_response = self._convert_bedrock_response_to_openai_format(
bedrock_response
)
return openai_response
def create(
self,
model: str,
messages: List[Dict[str, str]],
max_tokens: int,
temperature: float,
stream: Optional[bool] = True,
tools: Optional[List[dict]] = None,
tool_choice: Literal["none", "auto", "required"] = "auto",
**kwargs,
) -> OpenAIResponse:
# Main entry point for chat completion
bedrock_tools = []
if tools is not None:
bedrock_tools = self._convert_openai_tools_to_bedrock_format(tools)
if stream:
return self._invoke_bedrock_stream(
model,
messages,
max_tokens,
temperature,
bedrock_tools,
tool_choice,
**kwargs,
)
else:
return self._invoke_bedrock(
model,
messages,
max_tokens,
temperature,
bedrock_tools,
tool_choice,
**kwargs,
)
+136
View File
@@ -1,3 +1,4 @@
import json
import threading
import tomllib
from pathlib import Path
@@ -37,6 +38,32 @@ class ProxySettings(BaseModel):
class SearchSettings(BaseModel):
engine: str = Field(default="Google", description="Search engine the llm to use")
fallback_engines: List[str] = Field(
default_factory=lambda: ["DuckDuckGo", "Baidu", "Bing"],
description="Fallback search engines to try if the primary engine fails",
)
retry_delay: int = Field(
default=60,
description="Seconds to wait before retrying all engines again after they all fail",
)
max_retries: int = Field(
default=3,
description="Maximum number of times to retry all engines when all fail",
)
lang: str = Field(
default="en",
description="Language code for search results (e.g., en, zh, fr)",
)
country: str = Field(
default="us",
description="Country code for search results (e.g., us, cn, uk)",
)
class RunflowSettings(BaseModel):
use_data_analysis_agent: bool = Field(
default=False, description="Enable data analysis agent in run flow"
)
class BrowserSettings(BaseModel):
@@ -78,6 +105,72 @@ class SandboxSettings(BaseModel):
)
class DaytonaSettings(BaseModel):
daytona_api_key: str
daytona_server_url: Optional[str] = Field(
"https://app.daytona.io/api", description=""
)
daytona_target: Optional[str] = Field("us", description="enum ['eu', 'us']")
sandbox_image_name: Optional[str] = Field("whitezxj/sandbox:0.1.0", description="")
sandbox_entrypoint: Optional[str] = Field(
"/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf",
description="",
)
# sandbox_id: Optional[str] = Field(
# None, description="ID of the daytona sandbox to use, if any"
# )
VNC_password: Optional[str] = Field(
"123456", description="VNC password for the vnc service in sandbox"
)
class MCPServerConfig(BaseModel):
"""Configuration for a single MCP server"""
type: str = Field(..., description="Server connection type (sse or stdio)")
url: Optional[str] = Field(None, description="Server URL for SSE connections")
command: Optional[str] = Field(None, description="Command for stdio connections")
args: List[str] = Field(
default_factory=list, description="Arguments for stdio command"
)
class MCPSettings(BaseModel):
"""Configuration for MCP (Model Context Protocol)"""
server_reference: str = Field(
"app.mcp.server", description="Module reference for the MCP server"
)
servers: Dict[str, MCPServerConfig] = Field(
default_factory=dict, description="MCP server configurations"
)
@classmethod
def load_server_config(cls) -> Dict[str, MCPServerConfig]:
"""Load MCP server configuration from JSON file"""
config_path = PROJECT_ROOT / "config" / "mcp.json"
try:
config_file = config_path if config_path.exists() else None
if not config_file:
return {}
with config_file.open() as f:
data = json.load(f)
servers = {}
for server_id, server_config in data.get("mcpServers", {}).items():
servers[server_id] = MCPServerConfig(
type=server_config["type"],
url=server_config.get("url"),
command=server_config.get("command"),
args=server_config.get("args", []),
)
return servers
except Exception as e:
raise ValueError(f"Failed to load MCP server config: {e}")
class AppConfig(BaseModel):
llm: Dict[str, LLMSettings]
sandbox: Optional[SandboxSettings] = Field(
@@ -89,6 +182,13 @@ class AppConfig(BaseModel):
search_config: Optional[SearchSettings] = Field(
None, description="Search configuration"
)
mcp_config: Optional[MCPSettings] = Field(None, description="MCP configuration")
run_flow_config: Optional[RunflowSettings] = Field(
None, description="Run flow configuration"
)
daytona_config: Optional[DaytonaSettings] = Field(
None, description="Daytona configuration"
)
class Config:
arbitrary_types_allowed = True
@@ -190,7 +290,26 @@ class Config:
sandbox_settings = SandboxSettings(**sandbox_config)
else:
sandbox_settings = SandboxSettings()
daytona_config = raw_config.get("daytona", {})
if daytona_config:
daytona_settings = DaytonaSettings(**daytona_config)
else:
daytona_settings = DaytonaSettings()
mcp_config = raw_config.get("mcp", {})
mcp_settings = None
if mcp_config:
# Load server configurations from JSON
mcp_config["servers"] = MCPSettings.load_server_config()
mcp_settings = MCPSettings(**mcp_config)
else:
mcp_settings = MCPSettings(servers=MCPSettings.load_server_config())
run_flow_config = raw_config.get("runflow")
if run_flow_config:
run_flow_settings = RunflowSettings(**run_flow_config)
else:
run_flow_settings = RunflowSettings()
config_dict = {
"llm": {
"default": default_settings,
@@ -202,6 +321,9 @@ class Config:
"sandbox": sandbox_settings,
"browser_config": browser_settings,
"search_config": search_settings,
"mcp_config": mcp_settings,
"run_flow_config": run_flow_settings,
"daytona_config": daytona_settings,
}
self._config = AppConfig(**config_dict)
@@ -214,6 +336,10 @@ class Config:
def sandbox(self) -> SandboxSettings:
return self._config.sandbox
@property
def daytona(self) -> DaytonaSettings:
return self._config.daytona_config
@property
def browser_config(self) -> Optional[BrowserSettings]:
return self._config.browser_config
@@ -222,6 +348,16 @@ class Config:
def search_config(self) -> Optional[SearchSettings]:
return self._config.search_config
@property
def mcp_config(self) -> MCPSettings:
"""Get the MCP configuration"""
return self._config.mcp_config
@property
def run_flow_config(self) -> RunflowSettings:
"""Get the Run Flow configuration"""
return self._config.run_flow_config
@property
def workspace_root(self) -> Path:
"""Get the workspace root directory"""
+57
View File
@@ -0,0 +1,57 @@
# Agent with Daytona sandbox
## Prerequisites
- conda activate 'Your OpenManus python env'
- pip install daytona==0.21.8 structlog==25.4.0
## Setup & Running
1. daytona config :
```bash
cd OpenManus
cp config/config.example-daytona.toml config/config.toml
```
2. get daytona apikey :
goto https://app.daytona.io/dashboard/keys and create your apikey
3. set your apikey in config.toml
```toml
# daytona config
[daytona]
daytona_api_key = ""
#daytona_server_url = "https://app.daytona.io/api"
#daytona_target = "us" #Daytona is currently available in the following regions:United States (us)、Europe (eu)
#sandbox_image_name = "whitezxj/sandbox:0.1.0" #If you don't use this default image,sandbox tools may be useless
#sandbox_entrypoint = "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf" #If you change this entrypoint,server in sandbox may be useless
#VNC_password = #The password you set to log in sandbox by VNC,it will be 123456 if you don't set
```
2. Run :
```bash
cd OpenManus
python sandbox_main.py
```
3. Send tasks to Agent
You can sent tasks to Agent by terminate,agent will use sandbox tools to handle your tasks.
4. See results
If agent use sb_browser_use tool, you can see the operations by VNC link, The VNC link will print in the termination,e.g.:https://6080-sandbox-123456.h7890.daytona.work.
If agent use sb_shell tool, you can see the results by terminate of sandbox in https://app.daytona.io/dashboard/sandboxes.
Agent can use sb_files tool to operate files to sandbox.
## Example
You can send task e.g.:"帮我在https://hk.trip.com/travel-guide/guidebook/nanjing-9696/?ishideheader=true&isHideNavBar=YES&disableFontScaling=1&catalogId=514634&locale=zh-HK查询相关信息上制定一份南京旅游攻略,并在工作区保存为index.html"
Then you can see the agent's browser action in VNC link(https://6080-sandbox-123456.h7890.proxy.daytona.work) and you can see the html made by agent in Website URL(https://8080-sandbox-123456.h7890.proxy.daytona.work).
## Learn More
- [Daytona Documentation](https://www.daytona.io/docs/)
+165
View File
@@ -0,0 +1,165 @@
import time
from daytona import (
CreateSandboxFromImageParams,
Daytona,
DaytonaConfig,
Resources,
Sandbox,
SandboxState,
SessionExecuteRequest,
)
from app.config import config
from app.utils.logger import logger
# load_dotenv()
daytona_settings = config.daytona
logger.info("Initializing Daytona sandbox configuration")
daytona_config = DaytonaConfig(
api_key=daytona_settings.daytona_api_key,
server_url=daytona_settings.daytona_server_url,
target=daytona_settings.daytona_target,
)
if daytona_config.api_key:
logger.info("Daytona API key configured successfully")
else:
logger.warning("No Daytona API key found in environment variables")
if daytona_config.server_url:
logger.info(f"Daytona server URL set to: {daytona_config.server_url}")
else:
logger.warning("No Daytona server URL found in environment variables")
if daytona_config.target:
logger.info(f"Daytona target set to: {daytona_config.target}")
else:
logger.warning("No Daytona target found in environment variables")
daytona = Daytona(daytona_config)
logger.info("Daytona client initialized")
async def get_or_start_sandbox(sandbox_id: str):
"""Retrieve a sandbox by ID, check its state, and start it if needed."""
logger.info(f"Getting or starting sandbox with ID: {sandbox_id}")
try:
sandbox = daytona.get(sandbox_id)
# Check if sandbox needs to be started
if (
sandbox.state == SandboxState.ARCHIVED
or sandbox.state == SandboxState.STOPPED
):
logger.info(f"Sandbox is in {sandbox.state} state. Starting...")
try:
daytona.start(sandbox)
# Wait a moment for the sandbox to initialize
# sleep(5)
# Refresh sandbox state after starting
sandbox = daytona.get(sandbox_id)
# Start supervisord in a session when restarting
start_supervisord_session(sandbox)
except Exception as e:
logger.error(f"Error starting sandbox: {e}")
raise e
logger.info(f"Sandbox {sandbox_id} is ready")
return sandbox
except Exception as e:
logger.error(f"Error retrieving or starting sandbox: {str(e)}")
raise e
def start_supervisord_session(sandbox: Sandbox):
"""Start supervisord in a session."""
session_id = "supervisord-session"
try:
logger.info(f"Creating session {session_id} for supervisord")
sandbox.process.create_session(session_id)
# Execute supervisord command
sandbox.process.execute_session_command(
session_id,
SessionExecuteRequest(
command="exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf",
var_async=True,
),
)
time.sleep(25) # Wait a bit to ensure supervisord starts properly
logger.info(f"Supervisord started in session {session_id}")
except Exception as e:
logger.error(f"Error starting supervisord session: {str(e)}")
raise e
def create_sandbox(password: str, project_id: str = None):
"""Create a new sandbox with all required services configured and running."""
logger.info("Creating new Daytona sandbox environment")
logger.info("Configuring sandbox with browser-use image and environment variables")
labels = None
if project_id:
logger.info(f"Using sandbox_id as label: {project_id}")
labels = {"id": project_id}
params = CreateSandboxFromImageParams(
image=daytona_settings.sandbox_image_name,
public=True,
labels=labels,
env_vars={
"CHROME_PERSISTENT_SESSION": "true",
"RESOLUTION": "1024x768x24",
"RESOLUTION_WIDTH": "1024",
"RESOLUTION_HEIGHT": "768",
"VNC_PASSWORD": password,
"ANONYMIZED_TELEMETRY": "false",
"CHROME_PATH": "",
"CHROME_USER_DATA": "",
"CHROME_DEBUGGING_PORT": "9222",
"CHROME_DEBUGGING_HOST": "localhost",
"CHROME_CDP": "",
},
resources=Resources(
cpu=2,
memory=4,
disk=5,
),
auto_stop_interval=15,
auto_archive_interval=24 * 60,
)
# Create the sandbox
sandbox = daytona.create(params)
logger.info(f"Sandbox created with ID: {sandbox.id}")
# Start supervisord in a session for new sandbox
start_supervisord_session(sandbox)
logger.info(f"Sandbox environment successfully initialized")
return sandbox
async def delete_sandbox(sandbox_id: str):
"""Delete a sandbox by its ID."""
logger.info(f"Deleting sandbox with ID: {sandbox_id}")
try:
# Get the sandbox
sandbox = daytona.get(sandbox_id)
# Delete the sandbox
daytona.delete(sandbox)
logger.info(f"Successfully deleted sandbox {sandbox_id}")
return True
except Exception as e:
logger.error(f"Error deleting sandbox {sandbox_id}: {str(e)}")
raise e
+138
View File
@@ -0,0 +1,138 @@
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, ClassVar, Dict, Optional
from daytona import Daytona, DaytonaConfig, Sandbox, SandboxState
from pydantic import Field
from app.config import config
from app.daytona.sandbox import create_sandbox, start_supervisord_session
from app.tool.base import BaseTool
from app.utils.files_utils import clean_path
from app.utils.logger import logger
# load_dotenv()
daytona_settings = config.daytona
daytona_config = DaytonaConfig(
api_key=daytona_settings.daytona_api_key,
server_url=daytona_settings.daytona_server_url,
target=daytona_settings.daytona_target,
)
daytona = Daytona(daytona_config)
@dataclass
class ThreadMessage:
"""
Represents a message to be added to a thread.
"""
type: str
content: Dict[str, Any]
is_llm_message: bool = False
metadata: Optional[Dict[str, Any]] = None
timestamp: Optional[float] = field(
default_factory=lambda: datetime.now().timestamp()
)
def to_dict(self) -> Dict[str, Any]:
"""Convert the message to a dictionary for API calls"""
return {
"type": self.type,
"content": self.content,
"is_llm_message": self.is_llm_message,
"metadata": self.metadata or {},
"timestamp": self.timestamp,
}
class SandboxToolsBase(BaseTool):
"""Base class for all sandbox tools that provides project-based sandbox access."""
# Class variable to track if sandbox URLs have been printed
_urls_printed: ClassVar[bool] = False
# Required fields
project_id: Optional[str] = None
# thread_manager: Optional[ThreadManager] = None
# Private fields (not part of the model schema)
_sandbox: Optional[Sandbox] = None
_sandbox_id: Optional[str] = None
_sandbox_pass: Optional[str] = None
workspace_path: str = Field(default="/workspace", exclude=True)
_sessions: dict[str, str] = {}
class Config:
arbitrary_types_allowed = True # Allow non-pydantic types like ThreadManager
underscore_attrs_are_private = True
async def _ensure_sandbox(self) -> Sandbox:
"""Ensure we have a valid sandbox instance, retrieving it from the project if needed."""
if self._sandbox is None:
# Get or start the sandbox
try:
self._sandbox = create_sandbox(password=config.daytona.VNC_password)
# Log URLs if not already printed
if not SandboxToolsBase._urls_printed:
vnc_link = self._sandbox.get_preview_link(6080)
website_link = self._sandbox.get_preview_link(8080)
vnc_url = (
vnc_link.url if hasattr(vnc_link, "url") else str(vnc_link)
)
website_url = (
website_link.url
if hasattr(website_link, "url")
else str(website_link)
)
print("\033[95m***")
print(f"VNC URL: {vnc_url}")
print(f"Website URL: {website_url}")
print("***\033[0m")
SandboxToolsBase._urls_printed = True
except Exception as e:
logger.error(f"Error retrieving or starting sandbox: {str(e)}")
raise e
else:
if (
self._sandbox.state == SandboxState.ARCHIVED
or self._sandbox.state == SandboxState.STOPPED
):
logger.info(f"Sandbox is in {self._sandbox.state} state. Starting...")
try:
daytona.start(self._sandbox)
# Wait a moment for the sandbox to initialize
# sleep(5)
# Refresh sandbox state after starting
# Start supervisord in a session when restarting
start_supervisord_session(self._sandbox)
except Exception as e:
logger.error(f"Error starting sandbox: {e}")
raise e
return self._sandbox
@property
def sandbox(self) -> Sandbox:
"""Get the sandbox instance, ensuring it exists."""
if self._sandbox is None:
raise RuntimeError("Sandbox not initialized. Call _ensure_sandbox() first.")
return self._sandbox
@property
def sandbox_id(self) -> str:
"""Get the sandbox ID, ensuring it exists."""
if self._sandbox_id is None:
raise RuntimeError(
"Sandbox ID not initialized. Call _ensure_sandbox() first."
)
return self._sandbox_id
def clean_path(self, path: str) -> str:
"""Clean and normalize a path to be relative to /workspace."""
cleaned_path = clean_path(path, self.workspace_path)
logger.debug(f"Cleaned path: {path} -> {cleaned_path}")
return cleaned_path
-34
View File
@@ -1,5 +1,4 @@
from abc import ABC, abstractmethod
from enum import Enum
from typing import Dict, List, Optional, Union
from pydantic import BaseModel
@@ -7,10 +6,6 @@ from pydantic import BaseModel
from app.agent.base import BaseAgent
class FlowType(str, Enum):
PLANNING = "planning"
class BaseFlow(BaseModel, ABC):
"""Base class for execution flows supporting multiple agents"""
@@ -60,32 +55,3 @@ class BaseFlow(BaseModel, ABC):
@abstractmethod
async def execute(self, input_text: str) -> str:
"""Execute the flow with given input"""
class PlanStepStatus(str, Enum):
"""Enum class defining possible statuses of a plan step"""
NOT_STARTED = "not_started"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
BLOCKED = "blocked"
@classmethod
def get_all_statuses(cls) -> list[str]:
"""Return a list of all possible step status values"""
return [status.value for status in cls]
@classmethod
def get_active_statuses(cls) -> list[str]:
"""Return a list of values representing active statuses (not started or in progress)"""
return [cls.NOT_STARTED.value, cls.IN_PROGRESS.value]
@classmethod
def get_status_marks(cls) -> Dict[str, str]:
"""Return a mapping of statuses to their marker symbols"""
return {
cls.COMPLETED.value: "[✓]",
cls.IN_PROGRESS.value: "[→]",
cls.BLOCKED.value: "[!]",
cls.NOT_STARTED.value: "[ ]",
}
+6 -1
View File
@@ -1,10 +1,15 @@
from enum import Enum
from typing import Dict, List, Union
from app.agent.base import BaseAgent
from app.flow.base import BaseFlow, FlowType
from app.flow.base import BaseFlow
from app.flow.planning import PlanningFlow
class FlowType(str, Enum):
PLANNING = "planning"
class FlowFactory:
"""Factory for creating different types of flows with support for multiple agents"""
+52 -4
View File
@@ -1,17 +1,47 @@
import json
import time
from enum import Enum
from typing import Dict, List, Optional, Union
from pydantic import Field
from app.agent.base import BaseAgent
from app.flow.base import BaseFlow, PlanStepStatus
from app.flow.base import BaseFlow
from app.llm import LLM
from app.logger import logger
from app.schema import AgentState, Message, ToolChoice
from app.tool import PlanningTool
class PlanStepStatus(str, Enum):
"""Enum class defining possible statuses of a plan step"""
NOT_STARTED = "not_started"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
BLOCKED = "blocked"
@classmethod
def get_all_statuses(cls) -> list[str]:
"""Return a list of all possible step status values"""
return [status.value for status in cls]
@classmethod
def get_active_statuses(cls) -> list[str]:
"""Return a list of values representing active statuses (not started or in progress)"""
return [cls.NOT_STARTED.value, cls.IN_PROGRESS.value]
@classmethod
def get_status_marks(cls) -> Dict[str, str]:
"""Return a mapping of statuses to their marker symbols"""
return {
cls.COMPLETED.value: "[✓]",
cls.IN_PROGRESS.value: "[→]",
cls.BLOCKED.value: "[!]",
cls.NOT_STARTED.value: "[ ]",
}
class PlanningFlow(BaseFlow):
"""A flow that manages planning and execution of tasks using agents."""
@@ -107,12 +137,30 @@ class PlanningFlow(BaseFlow):
"""Create an initial plan based on the request using the flow's LLM and PlanningTool."""
logger.info(f"Creating initial plan with ID: {self.active_plan_id}")
# Create a system message for plan creation
system_message = Message.system_message(
system_message_content = (
"You are a planning assistant. Create a concise, actionable plan with clear steps. "
"Focus on key milestones rather than detailed sub-steps. "
"Optimize for clarity and efficiency."
)
agents_description = []
for key in self.executor_keys:
if key in self.agents:
agents_description.append(
{
"name": key.upper(),
"description": self.agents[key].description,
}
)
if len(agents_description) > 1:
# Add description of agents to select
system_message_content += (
f"\nNow we have {agents_description} agents. "
f"The infomation of them are below: {json.dumps(agents_description)}\n"
"When creating steps in the planning tool, please specify the agent names using the format '[agent_name]'."
)
# Create a system message for plan creation
system_message = Message.system_message(system_message_content)
# Create a user message with the request
user_message = Message.user_message(
@@ -240,7 +288,7 @@ class PlanningFlow(BaseFlow):
YOUR CURRENT TASK:
You are now working on step {self.current_step_index}: "{step_text}"
Please execute this step using the appropriate tools. When you're done, provide a summary of what you accomplished.
Please only execute this current step using the appropriate tools. When you're done, provide a summary of what you accomplished.
"""
# Use agent.run() to execute the step
+9 -12
View File
@@ -10,7 +10,7 @@ from openai import (
OpenAIError,
RateLimitError,
)
from openai.types.chat.chat_completion_message import ChatCompletionMessage
from openai.types.chat import ChatCompletion, ChatCompletionMessage
from tenacity import (
retry,
retry_if_exception_type,
@@ -18,6 +18,7 @@ from tenacity import (
wait_random_exponential,
)
from app.bedrock import BedrockClient
from app.config import LLMSettings, config
from app.exceptions import TokenLimitExceeded
from app.logger import logger # Assuming a logger is set up in your app
@@ -87,16 +88,9 @@ class TokenCounter:
width, height = image_item["dimensions"]
return self._calculate_high_detail_tokens(width, height)
# Default values when dimensions aren't available or detail level is unknown
if detail == "high":
# Default to a 1024x1024 image calculation for high detail
return self._calculate_high_detail_tokens(1024, 1024) # 765 tokens
elif detail == "medium":
# Default to a medium-sized image for medium detail
return 1024 # This matches the original default
else:
# For unknown detail levels, use medium as default
return 1024
return (
self._calculate_high_detail_tokens(1024, 1024) if detail == "high" else 1024
)
def _calculate_high_detail_tokens(self, width: int, height: int) -> int:
"""Calculate tokens for high detail images based on dimensions"""
@@ -225,6 +219,8 @@ class LLM:
api_key=self.api_key,
api_version=self.api_version,
)
elif self.api_type == "aws":
self.client = BedrockClient()
else:
self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
@@ -732,8 +728,9 @@ class LLM:
temperature if temperature is not None else self.temperature
)
params["stream"] = False # Always use non-streaming for tool requests
response: ChatCompletion = await self.client.chat.completions.create(
**params, stream=False
**params
)
# Check if response is valid
+7 -23
View File
@@ -1,30 +1,19 @@
import logging
import sys
logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler(sys.stderr)])
import argparse
import asyncio
import atexit
import json
import logging
import os
import sys
from inspect import Parameter, Signature
from typing import Any, Dict, Optional
from mcp.server.fastmcp import FastMCP
# Add directories to Python path (needed for proper importing)
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
root_dir = os.path.dirname(parent_dir)
sys.path.insert(0, parent_dir)
sys.path.insert(0, current_dir)
sys.path.insert(0, root_dir)
# Configure logging (using the same format as original)
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("mcp-server")
from app.logger import logger
from app.tool.base import BaseTool
from app.tool.bash import Bash
from app.tool.browser_use_tool import BrowserUseTool
@@ -45,11 +34,6 @@ class MCPServer:
self.tools["editor"] = StrReplaceEditor()
self.tools["terminate"] = Terminate()
from app.logger import logger as app_logger
global logger
logger = app_logger
def register_tool(self, tool: BaseTool, method_name: Optional[str] = None) -> None:
"""Register a tool with parameter validation and documentation."""
tool_name = method_name or tool.name
+2
View File
@@ -89,4 +89,6 @@ For browser interactions:
Consider both what's visible and what might be beyond the current viewport.
Be methodical - remember your progress and what you've learned so far.
If you want to stop the interaction at any point, use the `terminate` tool/function call.
"""
+3 -1
View File
@@ -1,8 +1,10 @@
SYSTEM_PROMPT = (
"You are OpenManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. Whether it's programming, information retrieval, file processing, or web browsing, you can handle it all."
"You are OpenManus, an all-capable AI assistant, aimed at solving any task presented by the user. You have various tools at your disposal that you can call upon to efficiently complete complex requests. Whether it's programming, information retrieval, file processing, web browsing, or human interaction (only for extreme cases), you can handle it all."
"The initial directory is: {directory}"
)
NEXT_STEP_PROMPT = """
Based on user needs, proactively select the most appropriate tool or combination of tools. For complex tasks, you can break down the problem and use different tools step by step to solve it. After using each tool, clearly explain the execution results and suggest the next steps.
If you want to stop the interaction at any point, use the `terminate` tool/function call.
"""
-6
View File
@@ -20,9 +20,3 @@ Remember, you should always include a _SINGLE_ tool call/function call and then
If you'd like to issue two commands at once, PLEASE DO NOT DO THAT! Please instead first submit just the first tool call, and then after receiving a response you'll be able to issue the second tool call.
Note that the environment does NOT support interactive session commands (e.g. python, vim), so please do not invoke them.
"""
NEXT_STEP_TEMPLATE = """{{observation}}
(Open file: {{open_file}})
(Current directory: {{working_dir}})
bash-$
"""
+36
View File
@@ -0,0 +1,36 @@
SYSTEM_PROMPT = """You are an AI agent designed to data analysis / visualization task. You have various tools at your disposal that you can call upon to efficiently complete complex requests.
# Note:
1. The workspace directory is: {directory}; Read / write file in workspace
2. Generate analysis conclusion report in the end
Standard Report Generation Workflow:
1. Template Preparation:
- SearchHtmlLibrary: Identify suitable visualization templates
- ReportTemplateGeneration & GenerateInitialReport: Create initial report structure
2. Visualization Pipeline:
- VisualizationPrepare: Configure data for visualization
- DataVisualization: Generate interactive charts and graphs
3. Insight Enhancement:
- SelectInsights: Extract key findings from visualizations
- AddInsights: Annotate charts with analytical insights
4. Report Finalization:
- GenerateFinalReport: Replace the placeholders with charts
- ReportBeautify: Apply professional styling and formatting
Operational Protocol:
- First determine optimal visualization types based on dataset characteristics
- Utilize HTML template library to establish report framework
- Execute visualization pipeline to create data representations
- Enhance each chart with key insights you selected
- Assemble final report by embedding enriched visualizations
"""
NEXT_STEP_PROMPT = """Based on user needs, break down the problem and use different tools step by step to solve it.
# Note
1. Each step select the most appropriate tool proactively (ONLY ONE).
2. After using each tool, clearly explain the execution results and suggest the next steps.
3. When observation with Error, review and fix it."""
+3
View File
@@ -170,6 +170,9 @@ class Memory(BaseModel):
def add_messages(self, messages: List[Message]) -> None:
"""Add multiple messages to memory"""
self.messages.extend(messages)
# Optional: Implement message limit
if len(self.messages) > self.max_messages:
self.messages = self.messages[-self.max_messages :]
def clear(self) -> None:
"""Clear all messages"""
+4
View File
@@ -1,11 +1,13 @@
from app.tool.base import BaseTool
from app.tool.bash import Bash
from app.tool.browser_use_tool import BrowserUseTool
from app.tool.crawl4ai import Crawl4aiTool
from app.tool.create_chat_completion import CreateChatCompletion
from app.tool.planning import PlanningTool
from app.tool.str_replace_editor import StrReplaceEditor
from app.tool.terminate import Terminate
from app.tool.tool_collection import ToolCollection
from app.tool.web_search import WebSearch
__all__ = [
@@ -14,7 +16,9 @@ __all__ = [
"BrowserUseTool",
"Terminate",
"StrReplaceEditor",
"WebSearch",
"ToolCollection",
"CreateChatCompletion",
"PlanningTool",
"Crawl4aiTool",
]
+21
View File
@@ -0,0 +1,21 @@
from app.tool import BaseTool
class AskHuman(BaseTool):
"""Add a tool to ask human for help."""
name: str = "ask_human"
description: str = "Use this tool to ask human for help."
parameters: str = {
"type": "object",
"properties": {
"inquire": {
"type": "string",
"description": "The question you want to ask human.",
}
},
"required": ["inquire"],
}
async def execute(self, inquire: str) -> str:
return input(f"""Bot: {inquire}\n\nYou: """).strip()
+124 -23
View File
@@ -1,35 +1,38 @@
import json
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional
from typing import Any, Dict, Optional, Union
from pydantic import BaseModel, Field
from app.utils.logger import logger
class BaseTool(ABC, BaseModel):
name: str
description: str
parameters: Optional[dict] = None
class Config:
arbitrary_types_allowed = True
# class BaseTool(ABC, BaseModel):
# name: str
# description: str
# parameters: Optional[dict] = None
async def __call__(self, **kwargs) -> Any:
"""Execute the tool with given parameters."""
return await self.execute(**kwargs)
# class Config:
# arbitrary_types_allowed = True
@abstractmethod
async def execute(self, **kwargs) -> Any:
"""Execute the tool with given parameters."""
# async def __call__(self, **kwargs) -> Any:
# """Execute the tool with given parameters."""
# return await self.execute(**kwargs)
def to_param(self) -> Dict:
"""Convert tool to function call format."""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters,
},
}
# @abstractmethod
# async def execute(self, **kwargs) -> Any:
# """Execute the tool with given parameters."""
# def to_param(self) -> Dict:
# """Convert tool to function call format."""
# return {
# "type": "function",
# "function": {
# "name": self.name,
# "description": self.description,
# "parameters": self.parameters,
# },
# }
class ToolResult(BaseModel):
@@ -72,6 +75,104 @@ class ToolResult(BaseModel):
return type(self)(**{**self.dict(), **kwargs})
class BaseTool(ABC, BaseModel):
"""Consolidated base class for all tools combining BaseModel and Tool functionality.
Provides:
- Pydantic model validation
- Schema registration
- Standardized result handling
- Abstract execution interface
Attributes:
name (str): Tool name
description (str): Tool description
parameters (dict): Tool parameters schema
_schemas (Dict[str, List[ToolSchema]]): Registered method schemas
"""
name: str
description: str
parameters: Optional[dict] = None
# _schemas: Dict[str, List[ToolSchema]] = {}
class Config:
arbitrary_types_allowed = True
underscore_attrs_are_private = False
# def __init__(self, **data):
# """Initialize tool with model validation and schema registration."""
# super().__init__(**data)
# logger.debug(f"Initializing tool class: {self.__class__.__name__}")
# self._register_schemas()
# def _register_schemas(self):
# """Register schemas from all decorated methods."""
# for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
# if hasattr(method, 'tool_schemas'):
# self._schemas[name] = method.tool_schemas
# logger.debug(f"Registered schemas for method '{name}' in {self.__class__.__name__}")
async def __call__(self, **kwargs) -> Any:
"""Execute the tool with given parameters."""
return await self.execute(**kwargs)
@abstractmethod
async def execute(self, **kwargs) -> Any:
"""Execute the tool with given parameters."""
def to_param(self) -> Dict:
"""Convert tool to function call format.
Returns:
Dictionary with tool metadata in OpenAI function calling format
"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters,
},
}
# def get_schemas(self) -> Dict[str, List[ToolSchema]]:
# """Get all registered tool schemas.
# Returns:
# Dict mapping method names to their schema definitions
# """
# return self._schemas
def success_response(self, data: Union[Dict[str, Any], str]) -> ToolResult:
"""Create a successful tool result.
Args:
data: Result data (dictionary or string)
Returns:
ToolResult with success=True and formatted output
"""
if isinstance(data, str):
text = data
else:
text = json.dumps(data, indent=2)
logger.debug(f"Created success response for {self.__class__.__name__}")
return ToolResult(output=text)
def fail_response(self, msg: str) -> ToolResult:
"""Create a failed tool result.
Args:
msg: Error message describing the failure
Returns:
ToolResult with success=False and error message
"""
logger.debug(f"Tool {self.__class__.__name__} returned failed result: {msg}")
return ToolResult(error=msg)
class CLIResult(ToolResult):
"""A ToolResult that can be rendered as a CLI output."""
+75 -129
View File
@@ -16,34 +16,21 @@ from app.tool.base import BaseTool, ToolResult
from app.tool.web_search import WebSearch
_BROWSER_DESCRIPTION = """
Interact with a web browser to perform various actions such as navigation, element interaction, content extraction, and tab management. This tool provides a comprehensive set of browser automation capabilities:
_BROWSER_DESCRIPTION = """\
A powerful browser automation tool that allows interaction with web pages through various actions.
* This tool provides commands for controlling a browser session, navigating web pages, and extracting information
* It maintains state across calls, keeping the browser session alive until explicitly closed
* Use this when you need to browse websites, fill forms, click buttons, extract content, or perform web searches
* Each action requires specific parameters as defined in the tool's dependencies
Navigation:
- 'go_to_url': Go to a specific URL in the current tab
- 'go_back': Go back
- 'refresh': Refresh the current page
- 'web_search': Search the query in the current tab, the query should be a search query like humans search in web, concrete and not vague or super long. More the single most important items.
Key capabilities include:
* Navigation: Go to specific URLs, go back, search the web, or refresh pages
* Interaction: Click elements, input text, select from dropdowns, send keyboard commands
* Scrolling: Scroll up/down by pixel amount or scroll to specific text
* Content extraction: Extract and analyze content from web pages based on specific goals
* Tab management: Switch between tabs, open new tabs, or close tabs
Element Interaction:
- 'click_element': Click an element by index
- 'input_text': Input text into a form element
- 'scroll_down'/'scroll_up': Scroll the page (with optional pixel amount)
- 'scroll_to_text': If you dont find something which you want to interact with, scroll to it
- 'send_keys': Send strings of special keys like Escape,Backspace, Insert, PageDown, Delete, Enter, Shortcuts such as `Control+o`, `Control+Shift+T` are supported as well. This gets used in keyboard.press.
- 'get_dropdown_options': Get all options from a dropdown
- 'select_dropdown_option': Select dropdown option for interactive element index by the text of the option you want to select
Content Extraction:
- 'extract_content': Extract page content to retrieve specific information from the page, e.g. all company names, a specifc description, all information about, links with companies in structured format or simply links
Tab Management:
- 'switch_tab': Switch to a specific tab
- 'open_tab': Open a new tab with a URL
- 'close_tab': Close the current tab
Utility:
- 'wait': Wait for a specified number of seconds
Note: When using element indices, refer to the numbered elements shown in the current browser state.
"""
Context = TypeVar("Context")
@@ -266,32 +253,19 @@ class BrowserUseTool(BaseTool, Generic[Context]):
return ToolResult(
error="Query is required for 'web_search' action"
)
search_results = await self.web_search_tool.execute(query)
# Execute the web search and return results directly without browser navigation
search_response = await self.web_search_tool.execute(
query=query, fetch_content=True, num_results=1
)
# Navigate to the first search result
first_search_result = search_response.results[0]
url_to_navigate = first_search_result.url
if search_results:
# Navigate to the first search result
first_result = search_results[0]
if isinstance(first_result, dict) and "url" in first_result:
url_to_navigate = first_result["url"]
elif isinstance(first_result, str):
url_to_navigate = first_result
else:
return ToolResult(
error=f"Invalid search result format: {first_result}"
)
page = await context.get_current_page()
await page.goto(url_to_navigate)
await page.wait_for_load_state()
page = await context.get_current_page()
await page.goto(url_to_navigate)
await page.wait_for_load_state()
return ToolResult(
output=f"Searched for '{query}' and navigated to first result: {url_to_navigate}\nAll results:"
+ "\n".join([str(r) for r in search_results])
)
else:
return ToolResult(
error=f"No search results found for '{query}'"
)
return search_response
# Element interaction actions
elif action == "click_element":
@@ -403,99 +377,71 @@ class BrowserUseTool(BaseTool, Generic[Context]):
return ToolResult(
error="Goal is required for 'extract_content' action"
)
page = await context.get_current_page()
try:
# Get page content and convert to markdown for better processing
html_content = await page.content()
import markdownify
# Import markdownify here to avoid global import
try:
import markdownify
content = markdownify.markdownify(await page.content())
content = markdownify.markdownify(html_content)
except ImportError:
# Fallback if markdownify is not available
content = html_content
# Create prompt for LLM
prompt_text = """
prompt = f"""\
Your task is to extract the content of the page. You will be given a page and a goal, and you should extract all relevant information around this goal from the page. If the goal is vague, summarize the page. Respond in json format.
Extraction goal: {goal}
Page content:
{page}
{content[:max_content_length]}
"""
# Format the prompt with the goal and content
max_content_length = min(50000, len(content))
formatted_prompt = prompt_text.format(
goal=goal, page=content[:max_content_length]
)
messages = [{"role": "system", "content": prompt}]
# Create a proper message list for the LLM
from app.schema import Message
messages = [Message.user_message(formatted_prompt)]
# Define extraction function for the tool
extraction_function = {
"type": "function",
"function": {
"name": "extract_content",
"description": "Extract specific information from a webpage based on a goal",
"parameters": {
"type": "object",
"properties": {
"extracted_content": {
"type": "object",
"description": "The content extracted from the page according to the goal",
}
},
"required": ["extracted_content"],
# Define extraction function schema
extraction_function = {
"type": "function",
"function": {
"name": "extract_content",
"description": "Extract specific information from a webpage based on a goal",
"parameters": {
"type": "object",
"properties": {
"extracted_content": {
"type": "object",
"description": "The content extracted from the page according to the goal",
"properties": {
"text": {
"type": "string",
"description": "Text content extracted from the page",
},
"metadata": {
"type": "object",
"description": "Additional metadata about the extracted content",
"properties": {
"source": {
"type": "string",
"description": "Source of the extracted content",
}
},
},
},
}
},
"required": ["extracted_content"],
},
}
},
}
# Use LLM to extract content with required function calling
response = await self.llm.ask_tool(
messages,
tools=[extraction_function],
tool_choice="required",
# Use LLM to extract content with required function calling
response = await self.llm.ask_tool(
messages,
tools=[extraction_function],
tool_choice="required",
)
if response and response.tool_calls:
args = json.loads(response.tool_calls[0].function.arguments)
extracted_content = args.get("extracted_content", {})
return ToolResult(
output=f"Extracted from page:\n{extracted_content}\n"
)
# Extract content from function call response
if (
response
and response.tool_calls
and len(response.tool_calls) > 0
):
# Get the first tool call arguments
tool_call = response.tool_calls[0]
# Parse the JSON arguments
try:
args = json.loads(tool_call.function.arguments)
extracted_content = args.get("extracted_content", {})
# Format extracted content as JSON string
content_json = json.dumps(
extracted_content, indent=2, ensure_ascii=False
)
msg = f"Extracted from page:\n{content_json}\n"
except Exception as e:
msg = f"Error parsing extraction result: {str(e)}\nRaw response: {tool_call.function.arguments}"
else:
msg = "No content was extracted from the page."
return ToolResult(output=msg)
except Exception as e:
# Provide a more helpful error message
error_msg = f"Failed to extract content: {str(e)}"
try:
# Try to return a portion of the page content as fallback
return ToolResult(
output=f"{error_msg}\nHere's a portion of the page content:\n{content[:2000]}..."
)
except:
# If all else fails, just return the error
return ToolResult(error=error_msg)
return ToolResult(output="No content was extracted from the page.")
# Tab management actions
elif action == "switch_tab":
+146
View File
@@ -0,0 +1,146 @@
# Chart Visualization Tool
The chart visualization tool generates data processing code through Python and ultimately invokes [@visactor/vmind](https://github.com/VisActor/VMind) to obtain chart specifications. Chart rendering is implemented using [@visactor/vchart](https://github.com/VisActor/VChart).
## Installation (Mac / Linux)
1. Install node >= 18
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# Activate nvm, for example in Bash
source ~/.bashrc
# Then install the latest stable release of Node
nvm install node
# Activate usage, for example if the latest stable release is 22, then use 22
nvm use 22
```
2. Install dependencies
```bash
# Navigate to the appropriate location in the current repository
cd app/tool/chart_visualization
npm install
```
## Installation (Windows)
1. Install nvm-windows
Download the latest version `nvm-setup.exe` from the [official GitHub page](https://github.com/coreybutler/nvm-windows?tab=readme-ov-file#readme) and install it.
2. Use nvm to install node
```powershell
# Then install the latest stable release of Node
nvm install node
# Activate usage, for example if the latest stable release is 22, then use 22
nvm use 22
```
3. Install dependencies
```bash
# Navigate to the appropriate location in the current repository
cd app/tool/chart_visualization
npm install
```
## Tool
### python_execute
Execute the necessary parts of data analysis (excluding data visualization) using Python code, including data processing, data summary, report generation, and some general Python script code.
#### Input
```typescript
{
// Code type: data processing/data report/other general tasks
code_type: "process" | "report" | "others"
// Final execution code
code: string;
}
```
#### Output
Python execution results, including the saving of intermediate files and print output results.
### visualization_preparation
A pre-tool for data visualization with two purposes,
#### Data -> Chart
Used to extract the data needed for analysis (.csv) and the corresponding visualization description from the data, ultimately outputting a JSON configuration file.
#### Chart + Insight -> Chart
Select existing charts and corresponding data insights, choose data insights to add to the chart in the form of data annotations, and finally generate a JSON configuration file.
#### Input
```typescript
{
// Code type: data visualization or data insight addition
code_type: "visualization" | "insight"
// Python code used to produce the final JSON file
code: string;
}
```
#### Output
A configuration file for data visualization, used for the `data_visualization tool`.
## data_visualization
Generate specific data visualizations based on the content of `visualization_preparation`.
### Input
```typescript
{
// Configuration file path
json_path: string;
// Current purpose, data visualization or insight annotation addition
tool_type: "visualization" | "insight";
// Final product png or html; html supports vchart rendering and interaction
output_type: 'png' | 'html'
// Language, currently supports Chinese and English
language: "zh" | "en"
}
```
## VMind Configuration
### LLM
VMind requires LLM invocation for intelligent chart generation. By default, it uses the `config.llm["default"]` configuration.
### Generation Settings
Main configurations include chart dimensions, theme, and generation method:
### Generation Method
Default: png. Currently supports automatic selection of `output_type` by LLM based on context.
### Dimensions
Default dimensions are unspecified. For HTML output, charts fill the entire page by default. For PNG output, defaults to `1000*1000`.
### Theme
Default theme: `'light'`. VChart supports multiple themes. See [Themes](https://www.visactor.io/vchart/guide/tutorial_docs/Theme/Theme_Extension).
## Test
Currently, three tasks of different difficulty levels are set for testing.
### Simple Chart Generation Task
Provide data and specific chart generation requirements, test results, execute the command:
```bash
python -m app.tool.chart_visualization.test.chart_demo
```
The results should be located under `workspace\visualization`, involving 9 different chart results.
### Simple Data Report Task
Provide simple raw data analysis requirements, requiring simple processing of the data, execute the command:
```bash
python -m app.tool.chart_visualization.test.report_demo
```
The results are also located under `workspace\visualization`.
+114
View File
@@ -0,0 +1,114 @@
# グラフ可視化ツール
グラフ可視化ツールは、Pythonを使用してデータ処理コードを生成し、最終的に[@visactor/vmind](https://github.com/VisActor/VMind)を呼び出してグラフのspec結果を得ます。グラフのレンダリングには[@visactor/vchart](https://github.com/VisActor/VChart)を使用します。
## インストール (Mac / Linux)
1. Node >= 18をインストール
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# nvmを有効化、例としてBashを使用
source ~/.bashrc
# その後、最新の安定版Nodeをインストール
nvm install node
# 使用を有効化、例えば最新の安定版が22の場合、use 22
nvm use 22
```
2. 依存関係をインストール
```bash
cd app/tool/chart_visualization
npm install
```
## インストール (Windows)
1. nvm-windowsをインストール
[GitHub公式サイト](https://github.com/coreybutler/nvm-windows?tab=readme-ov-file#readme)から最新バージョンの`nvm-setup.exe`をダウンロードしてインストール
2. nvmを使用してNodeをインストール
```powershell
# その後、最新の安定版Nodeをインストール
nvm install node
# 使用を有効化、例えば最新の安定版が22の場合、use 22
nvm use 22
```
3. 依存関係をインストール
```bash
# 現在のリポジトリで適切な位置に移動
cd app/tool/chart_visualization
npm install
```
## ツール
### python_execute
Pythonコードを使用してデータ分析(データ可視化を除く)に必要な部分を実行します。これにはデータ処理、データ要約、レポート生成、および一般的なPythonスクリプトコードが含まれます。
#### 入力
```typescript
{
// コードタイプ:データ処理/データレポート/その他の一般的なタスク
code_type: "process" | "report" | "others"
// 最終実行コード
code: string;
}
```
#### 出力
Python実行結果、中間ファイルの保存とprint出力結果を含む
### visualization_preparation
データ可視化の準備ツールで、2つの用途があります。
#### Data -> Chart
データから分析に必要なデータ(.csv)と対応する可視化の説明を抽出し、最終的にJSON設定ファイルを出力します。
#### Chart + Insight -> Chart
既存のグラフと対応するデータインサイトを選択し、データインサイトをデータ注釈の形式でグラフに追加し、最終的にJSON設定ファイルを生成します。
#### 入力
```typescript
{
// コードタイプ:データ可視化またはデータインサイト追加
code_type: "visualization" | "insight"
// 最終的なJSONファイルを生成するためのPythonコード
code: string;
}
```
#### 出力
データ可視化の設定ファイル、`data_visualization tool`で使用
## data_visualization
`visualization_preparation`の内容に基づいて具体的なデータ可視化を生成
### 入力
```typescript
{
// 設定ファイルのパス
json_path: string;
// 現在の用途、データ可視化またはインサイト注釈追加
tool_type: "visualization" | "insight";
// 最終成果物pngまたはhtml;htmlではvchartのレンダリングとインタラクションをサポート
output_type: 'png' | 'html'
// 言語、現在は中国語と英語をサポート
language: "zh" | "en"
}
```
## 出力
最終的に'png'または'html'の形式でローカルに保存され、保存されたグラフのパスとグラフ内で発見されたデータインサイトを出力
## VMind設定
### LLM
VMind自体
+128
View File
@@ -0,0 +1,128 @@
# 차트 시각화 도구
차트 시각화 도구는 Python을 통해 데이터 처리 코드를 생성하고, 최종적으로 [@visactor/vmind](https://github.com/VisActor/VMind)를 호출하여 차트 사양을 얻습니다. 차트 렌더링은 [@visactor/vchart](https://github.com/VisActor/VChart)를 사용하여 구현됩니다.
## 설치 (Mac / Linux)
1. Node.js 18 이상 설치
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# nvm 활성화, 예를 들어 Bash
source ~/.bashrc
# 그런 다음 최신 안정 버전의 Node 설치
nvm install node
# 사용 활성화, 예를 들어 최신 안정 버전이 22인 경우 use 22
nvm use 22
```
2. 의존성 설치
```bash
# 현재 저장소에서 해당 위치로 이동
cd app/tool/chart_visualization
npm install
```
## 설치 (Windows)
1. nvm-windows 설치
[공식 GitHub 페이지](https://github.com/coreybutler/nvm-windows?tab=readme-ov-file#readme)에서 최신 버전의 `nvm-setup.exe`를 다운로드하고 설치합니다.
2. nvm을 사용하여 Node.js 설치
```powershell
# 그런 다음 최신 안정 버전의 Node 설치
nvm install node
# 사용 활성화, 예를 들어 최신 안정 버전이 22인 경우 use 22
nvm use 22
```
3. 의존성 설치
```bash
# 현재 저장소에서 해당 위치로 이동
cd app/tool/chart_visualization
npm install
```
## 도구
### python_execute
Python 코드를 사용하여 데이터 분석의 필요한 부분(데이터 시각화 제외)을 실행합니다. 여기에는 데이터 처리, 데이터 요약, 보고서 생성 및 일부 일반적인 Python 스크립트 코드가 포함됩니다.
#### 입력
```typescript
{
// 코드 유형: 데이터 처리/데이터 보고서/기타 일반 작업
code_type: "process" | "report" | "others"
// 최종 실행 코드
code: string;
}
```
#### 출력
Python 실행 결과, 중간 파일 저장 및 출력 결과 포함.
### visualization_preparation
데이터 시각화를 위한 사전 도구로 두 가지 목적이 있습니다.
#### 데이터 -> 차트
분석에 필요한 데이터(.csv)와 해당 시각화 설명을 데이터에서 추출하여 최종적으로 JSON 구성 파일을 출력합니다.
#### 차트 + 인사이트 -> 차트
기존 차트와 해당 데이터 인사이트를 선택하고, 데이터 주석 형태로 차트에 추가할 데이터 인사이트를 선택하여 최종적으로 JSON 구성 파일을 생성합니다.
#### 입력
```typescript
{
// 코드 유형: 데이터 시각화 또는 데이터 인사이트 추가
code_type: "visualization" | "insight"
// 최종 JSON 파일을 생성하는 데 사용되는 Python 코드
code: string;
}
```
#### 출력
`data_visualization tool`에 사용되는 데이터 시각화를 위한 구성 파일.
## data_visualization
`visualization_preparation`의 내용을 기반으로 특정 데이터 시각화를 생성합니다.
### 입력
```typescript
{
// 구성 파일 경로
json_path: string;
// 현재 목적, 데이터 시각화 또는 인사이트 주석 추가
tool_type: "visualization" | "insight";
// 최종 제품 png 또는 html; html은 vchart 렌더링 및 상호작용 지원
output_type: 'png' | 'html'
// 언어, 현재 중국어 및 영어 지원
language: "zh" | "en"
}
```
## VMind 구성
### LLM
VMind는 지능형 차트 생성을 위해 LLM 호출이 필요합니다. 기본적으로 `config.llm["default"]` 구성을 사용합니다.
### 생성 설정
주요 구성에는 차트 크기, 테마 및 생성 방법이 포함됩니다.
### 생성 방법
기본값: png. 현재 LLM이 컨텍스트에 따라 `output_type`을 자동으로 선택하는 것을 지원합니다.
### 크기
기본 크기는 지정되지 않았습니다. HTML 출력의 경우 차트는 기본적으로 전체 페이지를 채웁니다. PNG 출력의 경우 기본값은 `1000*1000`입니다.
### 테마
기본 테마: `'light'`. VChart는 여러 테마를 지원합니다. [테마](https://www.visactor.io/vchart/guide/tutorial_docs/Theme/Theme_Extension)를 참조하세요.
## 테스트
현재, 서로 다른 난이도의
+147
View File
@@ -0,0 +1,147 @@
# 图表可视化工具
图表可视化工具,通过python生成数据处理代码,最终调用[@visactor/vmind](https://github.com/VisActor/VMind)得到图表的spec结果,图表渲染使用[@visactor/vchart](https://github.com/VisActor/VChart)
## 安装(Mac / Linux)
1. 安装node >= 18
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# 激活nvm,以Bash为例
source ~/.bashrc
# 然后安装 Node 最近一个稳定颁布
nvm install node
# 激活使用,例如最新一个稳定颁布为22,则use 22
nvm use 22
```
2. 安装依赖
```bash
cd app/tool/chart_visualization
npm install
```
## 安装(Windows)
1. 安装nvm-windows
从[github官网](https://github.com/coreybutler/nvm-windows?tab=readme-ov-file#readme)上下载最新版本`nvm-setup.exe`并且安装
2. 使用nvm安装node
```powershell
# 然后安装 Node 最近一个稳定颁布
nvm install node
# 激活使用,例如最新一个稳定颁布为22,则use 22
nvm use 22
```
3. 安装依赖
```bash
# 在当前仓库下定位到相应位置
cd app/tool/chart_visualization
npm install
```
## Tool
### python_execute
用python代码执行数据分析(除数据可视化以外)中需要的部分,包括数据处理,数据总结摘要,报告生成以及一些通用python脚本代码
#### 输入
```typescript
{
// 代码类型:数据处理/数据报告/其他通用任务
code_type: "process" | "report" | "others"
// 最终执行代码
code: string;
}
```
#### 输出
python执行结果,带有中间文件的保存和print输出结果
### visualization_preparation
数据可视化前置工具,有两种用途,
#### Data -〉 Chart
用于从数据中提取需要分析的数据(.csv)和对应可视化的描述,最终输出一份json配置文件。
#### Chart + Insight -> Chart
选取已有的图表和对应的数据洞察,挑选数据洞察以数据标注的形式增加到图表中,最终生成一份json配置文件。
#### 输入
```typescript
{
// 代码类型:数据可视化 或者 数据洞察添加
code_type: "visualization" | "insight"
// 用于生产最终json文件的python代码
code: string;
}
```
#### 输出
数据可视化的配置文件,用于`data_visualization tool`
## data_visualization
根据`visualization_preparation`的内容,生成具体的数据可视化
### 输入
```typescript
{
// 配置文件路径
json_path: string;
// 当前用途,数据可视化或者洞察标注添加
tool_type: "visualization" | "insight";
// 最终产物png或者html;html下支持vchart渲染和交互
output_type: 'png' | 'html'
// 语言,目前支持中文和英文
language: "zh" | "en"
}
```
## 输出
最终以'png'或者'html'的形式保存在本地,输出保存的图表路径以及图表中发现的数据洞察
## VMind配置
### LLM
VMind本身也需要通过调用大模型得到智能图表生成结果,目前默认会使用`config.llm["default"]`配置
### 生成配置
主要生成配置包括图表的宽高、主题以及生成方式;
### 生成方式
默认为png,目前支持大模型根据上下文自己选择`output_type`
### 宽高
目前默认不指定宽高,`html`下默认占满整个页面,'png'下默认为`1000 * 1000`
### 主题
目前默认主题为`'light'`VChart图表支持多种主题,详见[主题](https://www.visactor.io/vchart/guide/tutorial_docs/Theme/Theme_Extension)
## 测试
当前设置了三种不同难度的任务用于测试
### 简单图表生成任务
给予数据和具体的图表生成需求,测试结果,执行命令:
```bash
python -m app.tool.chart_visualization.test.chart_demo
```
结果应位于`worksapce\visualization`下,涉及到9种不同的图表结果
### 简单数据报表任务
给予简单原始数据可分析需求,需要对数据进行简单加工处理,执行命令:
```bash
python -m app.tool.chart_visualization.test.report_demo
```
结果同样位于`worksapce\visualization`
+3
View File
@@ -0,0 +1,3 @@
from app.tool.chart_visualization.chart_prepare import VisualizationPrepare
from app.tool.chart_visualization.data_visualization import DataVisualization
from app.tool.chart_visualization.python_execute import NormalPythonExecute
@@ -0,0 +1,228 @@
import sys
import asyncio
import json
import os
print(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
from typing import Any, Hashable
import pandas as pd
from pydantic import Field, model_validator
from app.config import config
from app.llm import LLM
from app.logger import logger
from app.tool.base import BaseTool
class AddInsights(BaseTool):
name: str = "add_insights"
description: str = (
"Enhances charts by adding insights markers and annotations "
"using JSON data generated by the insights_selection tool. "
"This creates the final annotated visualization output."
)
parameters: dict = {
"type": "object",
"properties": {
"json_path": {
"type": "string",
"description": """Path to the JSON file generated by insights_selection tool.
Contains chart insights data in format:
{
"chartPath": string,
"insights_id": number[]
}""",
},
"output_type": {
"type": "string",
"description": "Visualization output format selection",
"default": "html",
"enum": [
"png", # Static image format
"html" # Interactive web format (recommended)
],
},
},
"required": ["json_path"],
}
llm: LLM = Field(default_factory=LLM, description="Language model instance")
@model_validator(mode="after")
def initialize_llm(self):
"""Initialize llm with default settings if not provided."""
if self.llm is None or not isinstance(self.llm, LLM):
self.llm = LLM(config_name=self.name.lower())
return self
def load_chart_with_css(self, chart_path):
# 读取 HTML 文件
with open(chart_path, 'r', encoding='utf-8') as f:
html_content = f.read()
html_content = html_content.replace('`', "'")
# 在 <head> 里插入 CSS
css = """
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
#chart-container {
width: 100%;
height: 100%;
}
</style>
"""
# 如果原文件没有 <head>,直接插入到最前面
if "<head>" in html_content:
html_content = html_content.replace("<head>", "<head>" + css)
else:
html_content = css + html_content
with open(chart_path, 'w', encoding='utf-8') as f:
f.write(html_content)
def get_file_path(
self,
json_info: list[dict[str, str]],
path_str: str,
directory: str = None,
) -> list[str]:
res = []
for item in json_info:
if os.path.exists(item[path_str]):
res.append(item[path_str])
elif os.path.exists(
os.path.join(f"{directory or config.workspace_root}", item[path_str])
):
res.append(
os.path.join(
f"{directory or config.workspace_root}", item[path_str]
)
)
else:
raise Exception(f"No such file or directory: {item[path_str]}")
return res
async def add_insights(
self, json_info: list[dict[str, str]], output_type: str
) -> str:
data_list = []
chart_file_path = self.get_file_path(
json_info, "chartPath", os.path.join(config.workspace_root, "visualization")
)
for index, item in enumerate(json_info):
if "insights_id" in item:
data_list.append(
{
"file_name": os.path.basename(chart_file_path[index]).replace(
f".{output_type}", ""
),
"insights_id": item["insights_id"],
}
)
tasks = [
self.invoke_vmind(
insights_id=item["insights_id"],
file_name=item["file_name"],
output_type=output_type,
task_type="insight",
)
for item in data_list
]
results = await asyncio.gather(*tasks)
error_list = []
success_list = []
for index, result in enumerate(results):
chart_path = chart_file_path[index]
if "error" in result and "chart_path" not in result:
error_list.append(f"Error in {chart_path}: {result['error']}")
else:
success_list.append(chart_path)
self.load_chart_with_css(chart_path)
success_template = (
f"# Charts Update with Insights\n{','.join(success_list)}"
if len(success_list) > 0
else ""
)
if len(error_list) > 0:
return {
"observation": f"# Error in chart insights:{'\n'.join(error_list)}\n{success_template}",
"success": False,
}
else:
return {"observation": f"{success_template}"}
async def execute(
self,
json_path: str,
output_type: str | None = "html",
tool_type: str | None = "visualization",
language: str | None = "en",
) -> str:
try:
logger.info(f"📈 data_visualization with {json_path} in: {tool_type} ")
with open(json_path, "r", encoding="utf-8") as file:
json_info = json.load(file)
return await self.add_insights(json_info, output_type)
except Exception as e:
return {
"observation": f"Error: {e}",
"success": False,
}
async def invoke_vmind(
self,
file_name: str,
output_type: str,
task_type: str,
insights_id: list[str] = None,
dict_data: list[dict[Hashable, Any]] = None,
chart_description: str = None,
language: str = "en",
):
llm_config = {
"base_url": self.llm.base_url,
"model": self.llm.model,
"api_key": self.llm.api_key,
}
vmind_params = {
"llm_config": llm_config,
"user_prompt": chart_description,
"dataset": dict_data,
"file_name": file_name,
"output_type": output_type,
"insights_id": insights_id,
"task_type": task_type,
"directory": str(config.workspace_root),
"language": language,
}
process = await asyncio.create_subprocess_exec(
"npx",
"ts-node",
"src/chartVisualize.ts",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=os.path.dirname(__file__),
)
input_json = json.dumps(vmind_params, ensure_ascii=False).encode("utf-8")
try:
stdout, stderr = await process.communicate(input_json)
stdout_str = stdout.decode("utf-8")
stderr_str = stderr.decode("utf-8")
if process.returncode == 0:
return json.loads(stdout_str)
else:
return {"error": f"Node.js Error: {stderr_str}"}
except Exception as e:
return {"error": f"Subprocess Error: {str(e)}"}
@@ -0,0 +1,38 @@
from app.tool.chart_visualization.python_execute import NormalPythonExecute
class VisualizationPrepare(NormalPythonExecute):
"""A tool for Chart Generation Preparation"""
name: str = "visualization_preparation"
description: str = """
You need some charts to replace initial report's placeholders. So you need to use this tool first to prepare metadata for data_visualization tool.
Using Python code to generates metadata of data_visualization tool. Outputs: 1) JSON Information. 2) Cleaned CSV data files (Optional).
"""
parameters: dict = {
"type": "object",
"properties": {
"code_type": {
"description": "code type, visualization: csv -> chart",
"type": "string",
"default": "visualization"
},
"code": {
"type": "string",
"description": """Python code for data_visualization prepare.
## Visualization Type (Initial Step)
1. Data loading logic
2. Csv Data and chart description generate
2.1 Csv data (The data you want to visulazation, cleaning / transform from origin data, saved in .csv)
2.2 Chart description of csv data (The chart title or description should be concise and clear. Examples: 'Product sales distribution', 'Monthly revenue trend'.)
3. Save information in json file.( format: {"csvFilePath": string, "chartTitle": string}[])
# Best Practices
1. Generate one or multiple csv data with different visualization needs based on the initial report
2. Make each chart data simple, clean and distinct
4. Json file saving in utf-8 with path print: print(json_path)
""",
},
},
"required": ["code", "code_type"],
}
@@ -0,0 +1,290 @@
import asyncio
import json
import os
from typing import Any, Hashable
import pandas as pd
from pydantic import Field, model_validator
from app.config import config
from app.llm import LLM
from app.logger import logger
from app.tool.base import BaseTool
class DataVisualization(BaseTool):
name: str = "data_visualization"
description: str = """Visualize statistical chart with JSON info from visualization_preparation tool.
Outputs: Charts (png/html)"""
parameters: dict = {
"type": "object",
"properties": {
"json_path": {
"type": "string",
"description": """file path of json info with ".json" in the end""",
},
"output_type": {
"description": "Rendering format (html=interactive)",
"type": "string",
"default": "html",
"enum": ["png", "html"],
},
"tool_type": {
"description": "visualize",
"type": "string",
"default": "visualization",
},
"language": {
"description": "english(en) / chinese(zh)",
"type": "string",
"default": "en",
"enum": ["zh", "en"],
},
},
"required": ["code"],
}
llm: LLM = Field(default_factory=LLM, description="Language model instance")
@model_validator(mode="after")
def initialize_llm(self):
"""Initialize llm with default settings if not provided."""
if self.llm is None or not isinstance(self.llm, LLM):
self.llm = LLM(config_name=self.name.lower())
return self
def get_file_path(
self,
json_info: list[dict[str, str]],
path_str: str,
directory: str = None,
) -> list[str]:
res = []
for item in json_info:
if os.path.exists(item[path_str]):
res.append(item[path_str])
elif os.path.exists(
os.path.join(f"{directory or config.workspace_root}", item[path_str])
):
res.append(
os.path.join(
f"{directory or config.workspace_root}", item[path_str]
)
)
else:
raise Exception(f"No such file or directory: {item[path_str]}")
return res
def load_chart_with_css(self, chart_path):
# 读取 HTML 文件
with open(chart_path, 'r', encoding='utf-8') as f:
html_content = f.read()
# 在 <head> 里插入 CSS
css = """
<style>
body, html {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
#chart-container {
width: 100%;
height: 100%;
}
</style>
"""
# 如果原文件没有 <head>,直接插入到最前面
if "<head>" in html_content:
html_content = html_content.replace("<head>", "<head>" + css)
else:
html_content = css + html_content
with open(chart_path, 'w', encoding='utf-8') as f:
f.write(html_content)
def success_output_template(self, result: list[dict[str, str]]) -> str:
content = ""
if len(result) == 0:
return "Is EMPTY!"
for item in result:
chart_path=item['chart_path']
self.load_chart_with_css(chart_path)
content += f"""## {item['title']}\nChart saved in: {item['chart_path']}"""
if "insight_path" in item and item["insight_path"] and "insight_md" in item:
content += "\n" + item["insight_md"]
else:
content += "\n"
return f"Chart Generated Successful!\n{content}"
async def data_visualization(
self, json_info: list[dict[str, str]], output_type: str, language: str
) -> str:
data_list = []
csv_file_path = self.get_file_path(json_info, "csvFilePath")
for index, item in enumerate(json_info):
df = pd.read_csv(csv_file_path[index], encoding="utf-8")
df = df.astype(object)
df = df.where(pd.notnull(df), None)
data_dict_list = df.to_json(orient="records", force_ascii=False)
data_list.append(
{
"file_name": os.path.basename(csv_file_path[index]).replace(
".csv", ""
),
"dict_data": data_dict_list,
"chartTitle": item["chartTitle"],
}
)
tasks = [
self.invoke_vmind(
dict_data=item["dict_data"],
chart_description=item["chartTitle"],
file_name=item["file_name"],
output_type=output_type,
task_type="visualization",
language=language,
)
for item in data_list
]
results = await asyncio.gather(*tasks)
error_list = []
success_list = []
for index, result in enumerate(results):
csv_path = csv_file_path[index]
if "error" in result and "chart_path" not in result:
error_list.append(f"Error in {csv_path}: {result['error']}")
else:
success_list.append(
{
**result,
"title": json_info[index]["chartTitle"],
}
)
if len(error_list) > 0:
return {
"observation": f"# Error chart generated{'\n'.join(error_list)}\n{self.success_output_template(success_list)}",
"success": False,
}
else:
return {"observation": f"{self.success_output_template(success_list)}"}
async def add_insights(
self, json_info: list[dict[str, str]], output_type: str
) -> str:
data_list = []
chart_file_path = self.get_file_path(
json_info, "chartPath", os.path.join(config.workspace_root, "visualization")
)
for index, item in enumerate(json_info):
if "insights_id" in item:
data_list.append(
{
"file_name": os.path.basename(chart_file_path[index]).replace(
f".{output_type}", ""
),
"insights_id": item["insights_id"],
}
)
tasks = [
self.invoke_vmind(
insights_id=item["insights_id"],
file_name=item["file_name"],
output_type=output_type,
task_type="insight",
)
for item in data_list
]
results = await asyncio.gather(*tasks)
error_list = []
success_list = []
for index, result in enumerate(results):
chart_path = chart_file_path[index]
if "error" in result and "chart_path" not in result:
error_list.append(f"Error in {chart_path}: {result['error']}")
else:
success_list.append(chart_path)
success_template = (
f"# Charts Update with Insights\n{','.join(success_list)}"
if len(success_list) > 0
else ""
)
if len(error_list) > 0:
return {
"observation": f"# Error in chart insights:{'\n'.join(error_list)}\n{success_template}",
"success": False,
}
else:
return {"observation": f"{success_template}"}
async def execute(
self,
json_path: str,
output_type: str | None = "html",
tool_type: str | None = "visualization",
language: str | None = "en",
) -> str:
try:
logger.info(f"📈 data_visualization with {json_path} in: {tool_type} ")
with open(json_path, "r", encoding="utf-8") as file:
json_info = json.load(file)
if tool_type == "visualization":
return await self.data_visualization(json_info, output_type, language)
else:
return await self.add_insights(json_info, output_type)
except Exception as e:
return {
"observation": f"Error: {e}",
"success": False,
}
async def invoke_vmind(
self,
file_name: str,
output_type: str,
task_type: str,
insights_id: list[str] = None,
dict_data: list[dict[Hashable, Any]] = None,
chart_description: str = None,
language: str = "en",
):
llm_config = {
"base_url": self.llm.base_url,
"model": self.llm.model,
"api_key": self.llm.api_key,
}
vmind_params = {
"llm_config": llm_config,
"user_prompt": chart_description,
"dataset": dict_data,
"file_name": file_name,
"output_type": output_type,
"insights_id": insights_id,
"task_type": task_type,
"directory": str(config.workspace_root),
"language": language,
}
# build async sub process
process = await asyncio.create_subprocess_exec(
"npx",
"ts-node",
"src/chartVisualize.ts",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=os.path.dirname(__file__),
)
input_json = json.dumps(vmind_params, ensure_ascii=False).encode("utf-8")
try:
stdout, stderr = await process.communicate(input_json)
stdout_str = stdout.decode("utf-8")
stderr_str = stderr.decode("utf-8")
if process.returncode == 0:
return json.loads(stdout_str)
else:
return {"error": f"Node.js Error: {stderr_str}"}
except Exception as e:
return {"error": f"Subprocess Error: {str(e)}"}
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "chart_visualization",
"version": "1.0.0",
"main": "src/index.ts",
"devDependencies": {
"@types/node": "^22.10.1",
"ts-node": "^10.9.2",
"typescript": "^5.7.2"
},
"dependencies": {
"@visactor/vchart": "^1.13.7",
"@visactor/vmind": "2.0.6-alpha.2",
"get-stdin": "^9.0.0",
"puppeteer": "^24.9.0"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": ""
}
@@ -0,0 +1,36 @@
from app.config import config
from app.tool.python_execute import PythonExecute
class NormalPythonExecute(PythonExecute):
"""A tool for executing Python code with timeout and safety restrictions."""
name: str = "python_execute"
description: str = """Execute Python code for in-depth data analysis / data report(task conclusion) / other normal task without direct visualization."""
parameters: dict = {
"type": "object",
"properties": {
"code_type": {
"description": "code type, data process / data report / others",
"type": "string",
"default": "process",
"enum": ["process", "report", "others"],
},
"code": {
"type": "string",
"description": """Python code to execute.
# Note
1. The code should generate a comprehensive text-based report containing dataset overview, column details, basic statistics, derived metrics, timeseries comparisons, outliers, and key insights.
2. Use print() for all outputs so the analysis (including sections like 'Dataset Overview' or 'Preprocessing Results') is clearly visible and save it also
3. Save any report / processed files / each analysis result in worksapce directory: {directory}
4. Data reports need to be content-rich, including your overall analysis process and corresponding data visualization.
5. You can invode this tool step-by-step to do data analysis from summary to in-depth with data report saved also""".format(
directory=config.workspace_root
),
},
},
"required": ["code"],
}
async def execute(self, code: str, code_type: str | None = None, timeout=5):
return await super().execute(code, timeout)
@@ -0,0 +1,54 @@
from app.tool.chart_visualization.python_execute import NormalPythonExecute
class SelectInsights(NormalPythonExecute):
name: str = "insights_selection"
description: str = (
"This tool analyzes data_visualization tool's outputs and identifies key data insights for each chart."
"based on their importance ranking. Insights are prioritized in three tiers:\n"
"1 **Critical Insights**: 'abnormal_trend', 'abnormal_band', 'turning_point', 'overall_trend'\n"
"2 **Important Insights**: 'outlier', 'extreme_value', 'majority_value', 'avg'\n"
"3 **Basic Insights**: 'min', 'max'\n\n"
"**!Must be called immediately after data_visualization completes!**"
"**!All insights_id must come from the data_visualization analysis results!**"
)
parameters: dict = {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": """Python code to analyze visualized charts and extract insights.
# PRIORITY REQUIREMENTS
Insights must be selected and ranked according to these importance tiers:
1. **First Priority**: Always include 'abnormal_trend', 'abnormal_band', 'turning_point', 'overall_trend' when present
2. **Second Priority**: Include 'outlier', 'extreme_value', 'majority_value', 'avg' if no first-tier insights exist
3. **Third Priority**: Fall back to 'min', 'max' only when no higher-priority insights are available
# EXECUTION REQUIREMENTS
1. **Timing**: MUST be called immediately after data_visualization completes
2. **Dependency**: MUST use insights from data_visualization output as the only source for insights_id
# CODE REQUIREMENTS
Your Python code must:
1. Analyze the data_visualization results to identify significant insights for each chart.
2. Save the findings in JSON format:
```json
[
{
"chartPath": "string", // Path to the generated chart
"insights_id": number[] // Array of key insight IDs FROM DATA_VISUALIZATION RESULTS
},
{
"chartPath": "string", // Path to the generated chart
"insights_id": number[] // Array of key insight IDs FROM DATA_VISUALIZATION RESULTS
},
...
]
```
Json file saving in utf-8 with path print: print(json_path)
""",
},
},
"required": ["code"],
}
@@ -0,0 +1,375 @@
import path from "path";
import fs from "fs";
import puppeteer from "puppeteer";
import VMind, { ChartType } from "@visactor/vmind";
import type { DataTable } from '@visactor/generate-vchart';
import { isString } from "@visactor/vutils";
enum AlgorithmType {
OverallTrending = "overallTrend",
AbnormalTrend = "abnormalTrend",
PearsonCorrelation = "pearsonCorrelation",
SpearmanCorrelation = "spearmanCorrelation",
ExtremeValue = "extremeValue",
MajorityValue = "majorityValue",
StatisticsAbnormal = "statisticsAbnormal",
StatisticsBase = "statisticsBase",
DbscanOutlier = "dbscanOutlier",
LOFOutlier = "lofOutlier",
TurningPoint = "turningPoint",
PageHinkley = "pageHinkley",
DifferenceOutlier = "differenceOutlier",
Volatility = "volatility",
}
const getBase64 = async (spec: any, width?: number, height?: number) => {
spec.animation = false;
width && (spec.width = width);
height && (spec.height = height);
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(getHtmlVChart(spec, width, height));
const dataUrl = await page.evaluate(() => {
const canvas: any = document
.getElementById("chart-container")
?.querySelector("canvas");
return canvas?.toDataURL("image/png");
});
const base64Data = dataUrl.replace(/^data:image\/png;base64,/, "");
await browser.close();
return Buffer.from(base64Data, "base64");
};
const serializeSpec = (spec: any) => {
return JSON.stringify(spec, (key, value) => {
if (typeof value === "function") {
const funcStr = value
.toString()
.replace(/(\r\n|\n|\r)/gm, "")
.replace(/\s+/g, " ");
return `__FUNCTION__${funcStr}`;
}
return value;
});
};
function getHtmlVChart(spec: any, width?: number, height?: number) {
return `<!DOCTYPE html>
<html>
<head>
<title>VChart Demo</title>
<script src="https://unpkg.com/@visactor/vchart/build/index.min.js"></script>
</head>
<body>
<div id="chart-container" style="width: ${width ? `${width}px` : "100%"
}; height: ${height ? `${height}px` : "100%"};"></div>
<script>
// parse spec with function
function parseSpec(stringSpec) {
return JSON.parse(stringSpec, (k, v) => {
if (typeof v === 'string' && v.startsWith('__FUNCTION__')) {
const funcBody = v.slice(12); // 移除标记
try {
return new Function('return (' + funcBody + ')')();
} catch(e) {
console.error('函数解析失败:', e);
return () => {};
}
}
return v;
});
}
const spec = parseSpec(\'${serializeSpec(spec)}\');
const chart = new VChart.VChart(spec, {
dom: 'chart-container'
});
chart.renderSync();
</script>
</body>
</html>
`;
}
/**
* get file path saved string
* @param isUpdate {boolean} default: false, update existed file when is true
*/
function getSavedPathName(
directory: string,
fileName: string,
outputType: "html" | "png" | "json" | "md",
isUpdate: boolean = false
) {
let newFileName = fileName;
while (
!isUpdate &&
fs.existsSync(
path.join(directory, "visualization", `${newFileName}.${outputType}`)
)
) {
newFileName += "_new";
}
return path.join(directory, "visualization", `${newFileName}.${outputType}`);
}
const readStdin = (): Promise<string> => {
return new Promise((resolve) => {
let input = "";
process.stdin.setEncoding("utf-8"); // 确保编码与 Python 端一致
process.stdin.on("data", (chunk) => (input += chunk));
process.stdin.on("end", () => resolve(input));
});
};
/** Save insights markdown in local, and return content && path */
const setInsightTemplate = (
path: string,
title: string,
insights: string[]
) => {
let res = "";
if (insights.length) {
res += `## ${title} Insights`;
insights.forEach((insight, index) => {
res += `\n${index + 1}. ${insight}`;
});
}
if (res) {
fs.writeFileSync(path, res, "utf-8");
return { insight_path: path, insight_md: res };
}
return {};
};
/** Save vmind result into local file, Return chart file path */
async function saveChartRes(options: {
spec: any;
directory: string;
outputType: "png" | "html";
fileName: string;
width?: number;
height?: number;
isUpdate?: boolean;
}) {
const { directory, fileName, spec, outputType, width, height, isUpdate } =
options;
const specPath = getSavedPathName(directory, fileName, "json", isUpdate);
fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
const savedPath = getSavedPathName(directory, fileName, outputType, isUpdate);
if (outputType === "png") {
const base64 = await getBase64(spec, width, height);
fs.writeFileSync(savedPath, base64);
} else {
const html = getHtmlVChart(spec, width, height);
fs.writeFileSync(savedPath, html, "utf-8");
}
return savedPath;
}
async function generateChart(
vmind: VMind,
options: {
dataset: string | DataTable;
userPrompt: string;
directory: string;
outputType: "png" | "html";
fileName: string;
width?: number;
height?: number;
language?: "en" | "zh";
}
) {
let res: {
chart_path?: string;
error?: string;
insight_path?: string;
insight_md?: string;
} = {};
const {
dataset,
userPrompt,
directory,
width,
height,
outputType,
fileName,
language,
} = options;
try {
// Get chart spec and save in local file
const jsonDataset = isString(dataset) ? JSON.parse(dataset) : dataset;
const { spec, error, chartType } = await vmind.generateChart(
userPrompt,
undefined,
jsonDataset,
{
enableDataQuery: false,
theme: "light",
}
);
if (error || !spec) {
return {
error: error || "Spec of Chart was Empty!",
};
}
spec.title = {
text: userPrompt,
};
if (!fs.existsSync(path.join(directory, "visualization"))) {
fs.mkdirSync(path.join(directory, "visualization"));
}
const specPath = getSavedPathName(directory, fileName, "json");
res.chart_path = await saveChartRes({
directory,
spec,
width,
height,
fileName,
outputType,
});
// get chart insights and save in local
const insights = [];
if (
chartType &&
[
ChartType.BarChart,
ChartType.LineChart,
ChartType.AreaChart,
ChartType.ScatterPlot,
ChartType.DualAxisChart,
].includes(chartType as ChartType)
) {
const { insights: vmindInsights } = await vmind.getInsights(spec, {
maxNum: 6,
algorithms: [
AlgorithmType.OverallTrending,
AlgorithmType.AbnormalTrend,
AlgorithmType.PearsonCorrelation,
AlgorithmType.SpearmanCorrelation,
AlgorithmType.StatisticsAbnormal,
AlgorithmType.LOFOutlier,
AlgorithmType.DbscanOutlier,
AlgorithmType.MajorityValue,
AlgorithmType.PageHinkley,
AlgorithmType.TurningPoint,
AlgorithmType.StatisticsBase,
AlgorithmType.Volatility,
],
usePolish: false,
language: language === "en" ? "english" : "chinese",
});
insights.push(...vmindInsights);
}
const insightsText = insights
.map((insight) => insight.textContent?.plainText)
.filter((insight) => !!insight) as string[];
spec.insights = insights;
fs.writeFileSync(specPath, JSON.stringify(spec, null, 2));
res = {
...res,
...setInsightTemplate(
getSavedPathName(directory, fileName, "md"),
userPrompt,
insightsText
),
};
} catch (error: any) {
res.error = error.toString();
} finally {
return res;
}
}
async function updateChartWithInsight(
vmind: VMind,
options: {
directory: string;
outputType: "png" | "html";
fileName: string;
insightsId: number[];
}
) {
const { directory, outputType, fileName, insightsId } = options;
//???????为什么打印不出来???????
//console.log(options)
let res: { error?: string; chart_path?: string } = {};
try {
const specPath = getSavedPathName(directory, fileName, "json", true);
const spec = JSON.parse(fs.readFileSync(specPath, "utf8"));
// llm select index from 1
//console.log(spec)
const insights = (spec.insights || []).filter(
(_insight: any, index: number) => insightsId.includes(index + 1)
);
const { newSpec, error } = await vmind.updateSpecByInsights(spec, insights);
if (error) {
throw error;
}
res.chart_path = await saveChartRes({
spec: newSpec,
directory,
outputType,
fileName,
isUpdate: true,
});
} catch (error: any) {
res.error = error.toString();
} finally {
return res;
}
}
async function executeVMind() {
const input = await readStdin();
const inputData = JSON.parse(input);
let res;
const {
llm_config,
width,
dataset = [],
height,
directory,
user_prompt: userPrompt,
output_type: outputType = "png",
file_name: fileName,
task_type: taskType = "visualization",
insights_id: insightsId = [],
language = "en",
} = inputData;
const { base_url: baseUrl, model, api_key: apiKey } = llm_config;
const vmind = new VMind({
url: `${baseUrl}/chat/completions`,
model,
headers: {
"api-key": apiKey,
Authorization: `Bearer ${apiKey}`,
},
});
if (taskType === "visualization") {
res = await generateChart(vmind, {
dataset,
userPrompt,
directory,
outputType,
fileName,
width,
height,
language,
});
} else if (taskType === "insight" && insightsId.length) {
res = await updateChartWithInsight(vmind, {
directory,
fileName,
outputType,
insightsId,
});
}
console.log(JSON.stringify(res));
}
executeVMind();
@@ -0,0 +1,108 @@
import asyncio
import os
import sys
print(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))))
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))))
from app.agent.data_analysis import DataAnalysis
from app.logger import logger
prefix = "Help me generate charts and save them locally, specifically:"
tasks = [
{
"prompt": "Help me show the daily sales performance metrics over time. ",
"data":
"""Table: 每日销售数据明细​
OrderDate,RegionCode,SalesAmount,DataQuality
2023-04-01,SW,25860.24,正常
2023-04-01,NE,5877.65,正常
2023-04-01,C,34271.58,正常
2023-04-01,N,6251.42,正常
2023-04-01,E,3113.46,正常
2023-04-02,NW,87717.84,正常
2023-04-02,E,53058.96,正常
2023-04-02,N,14409.92,正常
2023-04-02,NE,5540.92,正常
2023-04-03,C,41802.49,正常
2023-04-03,NE,9202.20,正常
2023-04-03,SW,583.30,正常
2023-04-03,N,560.56,正常
2023-04-03,E,96269.32,正常
2023-04-04,NE,106208.48,正常
2023-04-04,C,6231.62,正常
2023-04-04,E,84454.83,正常
2023-04-05,NE,312.82,正常
2023-04-05,SW,5718.89,正常
2023-04-05,C,39811.91,正常
2023-04-05,E,244163.47,正常
2023-04-05,N,79487.41,正常
2023-04-06,C,28648.34,正常
2023-04-06,N,18288.76,正常
2023-04-08,SW,67434.58,正常
2023-04-08,C,1176.00,正常
2023-04-08,NW,81264.54,正常
2023-04-08,E,87750.96,正常
2023-04-09,SW,67434.58,正常
2023-04-09,C,5902.34,正常
2023-04-09,E,22252.27,正常
2023-04-09,NE,98844.76,正常
2023-04-10,E,2677.36,正常
2023-04-10,SW,1444.52,正常
2023-04-10,NE,62082.61,正常
2023-04-10,C,677.38,正常
2023-04-11,SW,776.16,正常
2023-04-11,C,7487.00,正常
2023-04-11,E,57016.40,正常
2023-04-12,SW,7131.85,正常
2023-04-12,E,11837.81,正常
2023-04-12,C,207763.14,正常
2023-04-13,C,24299.30,正常
2023-04-13,E,9847.04,正常
2023-04-13,NE,15919.12,正常
2023-04-15,SW,33544.42,正常
2023-04-15,C,39935.98,正常
2023-04-15,N,60416.80,正常
2023-04-15,NE,1234.80,正常
2023-04-15,E,36007.16,正常
2023-04-16,E,108484.04,正常
2023-04-16,SW,2450.00,正常
2023-04-17,E,78099.14,正常
2023-04-17,C,49350.84,正常
2023-04-17,N,18480.84,正常
2023-04-18,NE,48419.84,正常
2023-04-18,C,118951.67,正常
2023-04-18,N,10853.89,正常
2023-04-18,SW,1627.58,正常
2023-04-18,E,32777.28,正常
2023-04-19,N,41905.78,正常
2023-04-19,E,47942.58,正常
2023-04-19,NE,48259.51,正常
2023-04-19,C,18021.22,正常
2023-04-22,NE,58530.50,正常
2023-04-22,E,22004.72,正常
2023-04-22,C,7729.26,正常
2023-04-23,E,49218.54,正常
2023-04-23,NE,13944.42,正常
2023-04-23,SW,3843.56,正常
2023-04-24,SW,16754.08,正常
2023-04-24,NE,2343.18,正常
2023-04-24,E,41413.82,正常
2023-04-24,C,723.24,正常
2023-04-25,C,10678.08,正常
2023-04-25,E,44791.10,正常"""
}
]
async def main():
for index, item in enumerate(tasks):
logger.info(f"Begin task {index} / {len(tasks)}!")
agent = DataAnalysis()
await agent.run(
f"{prefix},chart_description:{item['prompt']},Data:{item['data']}"
)
logger.info(f"Finish with {item['prompt']}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,84 @@
import asyncio
import os
import sys
print(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))))
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))))
from app.agent.data_analysis import DataAnalysis
from app.agent.manus import Manus
# from app.agent.manus import Manus
async def main():
# agent = DataAnalysis()
agent = Manus()
await agent.run(
"""Requirement: Analyze the following data and generate a simple data report with some charts in HTML format.
Table1: 学生基本信息
Name,Age,Gender,Grade,Class,StudentID,EnrollmentDate,GuardianName,GuardianPhone,Address,PreviousSchool
王悦,16,女,高一,3班,S20230147,2023-09-01,王建国,138-1234-5678,北京市海淀区学院路101号,海淀实验中学
Table2: 学科成绩详细数据
Subject,Teacher,TestType,TestDate,Score,ClassRank,GradeRank,ScoreChange,DifficultyLevel
数学,张老师,单元测验,2023-09-05,82,12,45,-,中等
数学,张老师,周测,2023-09-12,85,10,38,+3,中等
数学,张老师,月考,2023-09-28,87,8,35,+2,中等偏难
数学,张老师,期中考试,2023-10-15,92,5,22,+5,难
语文,李老师,单元测验,2023-09-06,80,15,50,-,中等
语文,李老师,周测,2023-09-13,83,12,42,+3,中等
语文,李老师,月考,2023-09-29,85,10,40,+2,中等偏难
语文,李老师,期中考试,2023-10-16,88,8,35,+3,难
英语,王老师,单元测验,2023-09-07,87,8,30,-,中等
英语,王老师,周测,2023-09-14,89,6,25,+2,中等
英语,王老师,月考,2023-09-30,90,5,20,+1,中等偏难
英语,王老师,期中考试,2023-10-17,93,4,18,+3,难
物理,刘老师,单元测验,2023-09-08,75,18,65,-,难
物理,刘老师,周测,2023-09-15,77,16,60,+2,难
物理,刘老师,月考,2023-10-01,78,15,58,+1,难
物理,刘老师,期中考试,2023-10-18,85,12,40,+7,中等偏难
化学,陈老师,单元测验,2023-09-09,79,14,55,-,中等偏难
化学,陈老师,周测,2023-09-16,81,12,50,+2,中等偏难
化学,陈老师,月考,2023-10-02,82,11,48,+1,中等偏难
化学,陈老师,期中考试,2023-10-19,84,10,45,+2,中等
Table3: 学习行为日数据(2023年9月)
Date,Weekday,StudyHours,HomeworkHours,ReadingMinutes,ScreenTime,PhysicalActivity,ClassAttendance,ParticipationScore,SleepHours,MoodScore
2023-09-01,周五,3.5,2.0,45,1.5,1.0,出勤,85,8.2,4
2023-09-02,周六,4.0,1.5,30,2.0,0.8,出勤,90,7.8,5
2023-09-03,周日,3.0,2.5,60,1.0,1.2,出勤,88,8.5,4
2023-09-04,周一,5.0,2.0,50,1.2,0.7,出勤,92,7.9,5
2023-09-05,周二,4.5,1.8,40,1.8,1.0,出勤,87,8.0,4
2023-09-06,周三,3.8,2.2,55,1.3,0.9,出勤,89,8.3,5
2023-09-07,周四,4.2,1.7,35,1.6,1.1,出勤,91,7.7,4
2023-09-08,周五,3.5,2.1,48,1.4,0.8,出勤,86,8.1,3
2023-09-09,周六,4.8,1.9,42,1.7,1.3,出勤,93,7.6,5
2023-09-10,周日,3.2,2.3,52,1.1,0.7,出勤,88,8.4,4
2023-09-11,周一,4.5,1.6,38,1.9,1.0,出勤,90,7.9,5
2023-09-12,周二,3.9,2.4,57,1.2,0.9,出勤,87,8.2,4
2023-09-13,周三,4.1,1.8,44,1.5,1.2,出勤,91,7.8,5
2023-09-14,周四,3.7,2.6,49,1.4,0.8,出勤,89,8.3,4
2023-09-15,周五,4.3,1.9,36,1.8,1.1,出勤,92,7.7,5
2023-09-16,周六,3.4,2.2,53,1.3,0.9,出勤,86,8.1,4
2023-09-17,周日,4.6,1.7,41,1.6,1.3,出勤,90,7.6,5
2023-09-18,周一,3.8,2.5,47,1.2,0.7,出勤,88,8.4,4
2023-09-19,周二,4.2,1.8,39,1.7,1.0,出勤,91,7.9,5
2023-09-20,周三,3.9,2.3,51,1.4,0.8,出勤,87,8.2,4
2023-09-21,周四,4.4,1.6,43,1.5,1.2,出勤,93,7.8,5
2023-09-22,周五,3.6,2.4,46,1.3,0.9,出勤,89,8.3,4
2023-09-23,周六,4.7,1.9,37,1.8,1.1,出勤,86,7.7,5
2023-09-24,周日,3.5,2.1,50,1.4,0.8,出勤,90,8.1,4
2023-09-25,周一,4.3,1.7,42,1.6,1.3,出勤,88,7.6,5
2023-09-26,周二,3.8,2.6,48,1.2,0.7,出勤,91,8.4,4
2023-09-27,周三,4.1,1.8,40,1.7,1.0,出勤,87,7.9,5
2023-09-28,周四,3.9,2.3,52,1.5,0.9,出勤,92,8.2,4
2023-09-29,周五,4.5,1.6,45,1.4,1.2,出勤,89,7.8,5
2023-09-30,周六,3.7,2.4,44,1.3,0.8,出勤,86,8.3,4
"""
)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,75 @@
import asyncio
import os
import sys
print(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))))
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))))
from app.agent.data_analysis import DataAnalysis
from app.agent.manus import Manus
# from app.agent.manus import Manus
async def main():
# agent = DataAnalysis()
agent = Manus()
await agent.run(
"""Requirement: Analyze the following data and generate a graphical data report in HTML format. The final product should be a data report.
Table1: 用户基本信息
Name,Age,Gender,Height(cm),Weight(kg),MeasureDate
张三,45,男,170,98.6,2023-10-01
Table2: 身体成分数据
Date,BodyFat(%),MuscleMass(kg),WaterContent(%),BoneMass(kg),VisceralFat,Protein(%)
2023-10-01,35.2,42.3,43.1,2.8,18,14.2
Table3: 每日营养活动数据
Date,CaloriesIntake(kcal),CaloriesBurned(kcal),ProteinIntake(g),CarbIntake(g),FatIntake(g),Steps,ActiveMinutes,SleepHours,MorningWeight(kg)
2023-09-01,3850,1250,85,480,120,3200,12,5.2,99.2
2023-09-02,4200,1100,90,520,135,2800,8,4.8,99.5
2023-09-03,3600,950,78,450,110,2500,5,5.5,99.8
2023-09-04,3950,1050,82,490,125,2900,10,4.5,100.1
2023-09-05,4100,1150,88,510,130,3100,15,5.0,100.3
2023-09-06,3750,980,80,460,115,2600,6,4.7,100.6
2023-09-07,4300,1200,92,540,140,3300,18,4.3,100.9
2023-09-08,3450,900,75,430,105,2400,4,5.2,101.2
2023-09-09,4000,1000,84,500,122,2700,9,4.9,101.5
2023-09-10,3800,975,79,470,118,2550,7,5.1,101.8
2023-09-11,4150,1100,86,515,128,3000,14,4.6,102.0
2023-09-12,3550,925,77,440,108,2300,3,5.4,102.3
2023-09-13,4250,1180,94,530,138,3400,20,4.2,102.6
2023-09-14,3700,990,81,465,113,2650,8,5.3,102.9
2023-09-15,4050,1070,87,505,127,2950,13,4.8,103.1
2023-09-16,3500,910,76,435,104,2250,2,5.6,103.4
2023-09-17,3900,1020,83,485,120,2750,11,4.7,103.7
2023-09-18,4350,1250,96,550,145,3500,22,4.0,104.0
2023-09-19,3650,960,79,455,112,2450,5,5.2,104.3
2023-09-20,4000,1030,85,495,124,2850,12,4.9,104.5
2023-09-21,3450,890,74,425,103,2200,1,5.5,104.8
2023-09-22,3850,995,82,475,119,2700,10,4.6,105.1
2023-09-23,4100,1120,89,515,131,3050,16,4.3,105.4
2023-09-24,3550,935,77,440,107,2350,4,5.4,105.7
2023-09-25,3950,1010,84,490,123,2800,13,4.8,106.0
2023-09-26,4200,1150,91,525,136,3200,19,4.1,106.3
2023-09-27,3600,945,78,445,109,2400,6,5.3,106.6
2023-09-28,4050,1080,87,505,128,2900,15,4.7,106.9
2023-09-29,3750,970,80,460,116,2600,9,5.0,107.2
2023-09-30,4300,1220,95,540,142,3350,23,3.9,107.5
Table4: 健康指标参考范围
Category,SubCategory,Range,Unit
BMI,Underweight,<18.5,kg/m²
BMI,Normal,18.5-24.9,kg/m²
BMI,Overweight,25-29.9,kg/m²
BMI,Obese,≥30,kg/m²
BodyFat,Male(40-59),11-22,%
BodyFat,Female(40-59),23-34,%
VisceralFat,Normal,1-9,Level
VisceralFat,High,10-14,Level
VisceralFat,Very High,≥15,Level
Sleep,Recommended,7-9,hours
Steps,Active,≥10000,steps/day
"""
)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,123 @@
import asyncio
import os
import sys
print(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))))
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))))
from app.agent.data_analysis import DataAnalysis
from app.agent.manus import Manus
# from app.agent.manus import Manus
async def main():
# agent = DataAnalysis()
agent = Manus()
await agent.run(
"""Requirement: Analyze the following data and generate a graphical data report in HTML format. The final product should be a data report.
Table1: 销售区域基本信息​
RegionCode,RegionName,Description
SW,西南,西南地区销售数据
NE,东北,东北地区销售数据
C,中南,中南地区销售数据
N,华北,华北地区销售数据
E,华东,华东地区销售数据
NW,西北,西北地区销售数据
Table2: 每日销售数据明细​
OrderDate,RegionCode,SalesAmount,DataQuality
2023-04-01,SW,25860.24,正常
2023-04-01,NE,5877.65,正常
2023-04-01,C,34271.58,正常
2023-04-01,N,6251.42,正常
2023-04-01,E,3113.46,正常
2023-04-02,NW,87717.84,正常
2023-04-02,E,53058.96,正常
2023-04-02,N,14409.92,正常
2023-04-02,NE,5540.92,正常
2023-04-03,C,41802.49,正常
2023-04-03,NE,9202.20,正常
2023-04-03,SW,583.30,正常
2023-04-03,N,560.56,正常
2023-04-03,E,96269.32,正常
2023-04-04,NE,106208.48,正常
2023-04-04,C,6231.62,正常
2023-04-04,E,84454.83,正常
2023-04-05,NE,312.82,正常
2023-04-05,SW,5718.89,正常
2023-04-05,C,39811.91,正常
2023-04-05,E,244163.47,正常
2023-04-05,N,79487.41,正常
2023-04-06,C,28648.34,正常
2023-04-06,N,18288.76,正常
2023-04-08,SW,67434.58,正常
2023-04-08,C,1176.00,正常
2023-04-08,NW,81264.54,正常
2023-04-08,E,87750.96,正常
2023-04-09,SW,67434.58,正常
2023-04-09,C,5902.34,正常
2023-04-09,E,22252.27,正常
2023-04-09,NE,98844.76,正常
2023-04-10,E,2677.36,正常
2023-04-10,SW,1444.52,正常
2023-04-10,NE,62082.61,正常
2023-04-10,C,677.38,正常
2023-04-11,SW,776.16,正常
2023-04-11,C,7487.00,正常
2023-04-11,E,57016.40,正常
2023-04-12,SW,7131.85,正常
2023-04-12,E,11837.81,正常
2023-04-12,C,207763.14,正常
2023-04-13,C,24299.30,正常
2023-04-13,E,9847.04,正常
2023-04-13,NE,15919.12,正常
2023-04-15,SW,33544.42,正常
2023-04-15,C,39935.98,正常
2023-04-15,N,60416.80,正常
2023-04-15,NE,1234.80,正常
2023-04-15,E,36007.16,正常
2023-04-16,E,108484.04,正常
2023-04-16,SW,2450.00,正常
2023-04-17,E,78099.14,正常
2023-04-17,C,49350.84,正常
2023-04-17,N,18480.84,正常
2023-04-18,NE,48419.84,正常
2023-04-18,C,118951.67,正常
2023-04-18,N,10853.89,正常
2023-04-18,SW,1627.58,正常
2023-04-18,E,32777.28,正常
2023-04-19,N,41905.78,正常
2023-04-19,E,47942.58,正常
2023-04-19,NE,48259.51,正常
2023-04-19,C,18021.22,正常
2023-04-22,NE,58530.50,正常
2023-04-22,E,22004.72,正常
2023-04-22,C,7729.26,正常
2023-04-23,E,49218.54,正常
2023-04-23,NE,13944.42,正常
2023-04-23,SW,3843.56,正常
2023-04-24,SW,16754.08,正常
2023-04-24,NE,2343.18,正常
2023-04-24,E,41413.82,正常
2023-04-24,C,723.24,正常
2023-04-25,C,10678.08,正常
2023-04-25,E,44791.10,正常
Table3: 销售指标分析参考​
Category,SubCategory,Range,Level,Description
销售金额,日销售额,>50000,优秀,单日销售额超过5万
销售金额,日销售额,20000-50000,良好,单日销售额2-5万
销售金额,日销售额,5000-20000,一般,单日销售额0.5-2万
销售金额,日销售额,<5000,待提升,单日销售额低于5千
区域表现,华东地区,持续领先,优秀,销售额稳定高位
区域表现,中南地区,波动较大,关注,销售额波动明显
区域表现,西南地区,整体偏低,待提升,需要业务拓展
数据完整性,日期覆盖,4月1-25日,完整,覆盖主要业务日期
数据质量,金额精度,两位小数,标准,财务数据标准格式
业务连续性,工作日,正常营业,良好,除周末外正常营业
"""
)
if __name__ == "__main__":
asyncio.run(main())
File diff suppressed because one or more lines are too long
+109
View File
@@ -0,0 +1,109 @@
{
"include": [
"src/**/*.ts",
],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
"typeRoots": [
"./node_modules/@types",
"src/types"
], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
"checkJs": false, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}
@@ -0,0 +1,47 @@
from app.tool.chart_visualization.python_execute import NormalPythonExecute
class GenerateFinalReport(NormalPythonExecute):
"""A tool for generating final data analysis report"""
name: str = "generate_final_report"
description: str = """Replace the all placeholders in initial report and refine to generate final report.
Outputs: 1) HTML report file path"""
parameters: dict = {
"type": "object",
"properties": {
"code_type": {
"description": "code type, replacing: html with placeholders -> html with specific chart; check_refine: Make final adjustments and checks on the report",
"type": "string",
"default": "replacing",
"enum": ["replacing", "check_refine"],
},
"code": {
"type": "string",
"description": """Python code for replacing chart placeholders with specific chart file path, or for report checking and refining.
# Replacing Type
1. Find all placeholders
2. When replacing the chart path placeholder, use a relative path. ** src="/workspace/visualization/chart_name.html" **
3. When replacing the text placeholders, generate a detailed text description.
**When filling in the key insights below the charts, the insights must correspond with those previously added by the add insight tool.**
## Notice:
Use the absolute path starting with /workspace, for example, **/workspace/visualization/chart_name.html**
# Check_refine Type:
1. Check the entire html file
- Have all placeholders been filled?
- Whether the path of the chart is: /workspace/visualization/***.html
- If text-related placeholders still exist, please fill them in as much as possible. If chart path placeholders still exist, please delete that chart part of the report.
**When filling in the key insights below the charts, the insights must correspond with those previously added by the add insight tool.**
## Output Requirements
1. Generate **report.html** file
2. Print the file path: print(report_path)
3. Make sure the HTML includes Bootstrap for responsive design
""",
},
},
"required": ["code", "code_type"],
}
@@ -0,0 +1,35 @@
from app.tool.chart_visualization.python_execute import NormalPythonExecute
from typing import ClassVar
from pathlib import Path
class GenerateInitialReport(NormalPythonExecute):
"""A tool for generating initial data analysis reports based on the report template and user's input data"""
name: str = "generate_initial_report"
description: str = """Generates an initial HTML data analysis report based on the report template and user's input data.
After searhing and reading the report template, you should dynamically adapt the template content according to user input data,
intelligently determine which charts should be included in the report,
and automatically populate the fillable placeholders with corresponding data.
Outputs: 1) HTML report file path"""
parameters: dict = {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": f"""Python code for generating initial HTML data report based on the report template and user's input data.
## Output Requirements
1. Generate initial_report.html file
2. Print the file path: print(report_path)
# Notes:
1. Refer to the searched report templates
2. Complete the applicable placeholders, and leave any unfilled ones as '[placeholder: ...]'.
""",
},
},
"required": ["code", "code_type"],
}
@@ -0,0 +1,48 @@
from app.tool.chart_visualization.python_execute import NormalPythonExecute
class ReportBeautify(NormalPythonExecute):
"""A tool for transforming a basic health report into a professional, visually appealing final version"""
name: str = "report_beautify"
description: str = """
This tool should be called **LAST** in the workflow to perform final beautification of the data report.
It will:
1. Apply advanced styling and layout enhancements
2. Add interactive elements and visual polish
3. Ensure mobile responsiveness
Key beautification features to implement:
- colorful and fancy background and color scheme
- Modern CSS styling with gradients and shadows
- Font Awesome icons for visual cues
- Animated progress bars for metrics
- Card-based layout with hover effects
- Responsive design for all devices
- Scroll-triggered animations
- Professional typography hierarchy
## Output Requirements
1. Generate **beautify_report.html** file
2. Print the file path: print(report_path)
"""
parameters: dict = {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": """
Python code that beautify the report:
1. CSS/JS enhancements for modern styling
2. Structure optimization for better readability
3. Mobile responsiveness adjustments
Example tasks:
- Add Bootstrap 5 + Font Awesome
- Add metric cards with progress bars
- Create responsive tables
""",
},
},
"required": ["code"],
}
@@ -0,0 +1,82 @@
from app.tool.base import BaseTool, ToolResult
from typing import ClassVar
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pathlib import Path
from app.config import config
from app.tool.chart_visualization.python_execute import NormalPythonExecute
class ReportTemplateGeneration(NormalPythonExecute):
"""A tool for generating the report template in users' local file system"""
name: str = "report_template_generation"
description: str = """Generate a customized HTML report template based on user input data and the natural language description of the report from the previous step.
When user requires a report, you need to use the search_html_library tool first, and then use this tool based on your search result.
In this tool, you will need to process two input parameters to generate a customized HTML report.
(1) First Input Parameter: Analyze the user-provided dataset for the report and generate: A natural language suggestion for the HTML report customization (e.g., layout, structure, recommended charts, and visualizations).
(2) Second Input Parameter: Based on the natural language suggestion, produce: Python code that dynamically generates the corresponding HTML report. The code should also save the HTML file to the local filesystem.
Outputs: HTML report template file path"""
parameters: dict = {
"type": "object",
"properties": {
"report_template_description": {
"description": "Natural language suggestion for HTML report structure (layout, sections, chart recommendations). Focus on framework only - no actual content needed.",
"type": "string",
},
"code": {
"type": "string",
"description": """Python code to generate an HTML template with standardized placeholders. Requirements:
1. **Use placeholder format: [placeholder: description]**
2. Generate template structure only - no real content or data
3. Use the components and theme you have searched before.
4. For text sections: Use placeholders for paragraphs/lists
5. For charts: Use iframe elements with placeholder paths (/workspace/visualization/[filename].html)
6. Maintain semantic HTML structure with appropriate classes
## Output Requirements
1. Generate **report_template.html** file
2. Print the file path: print(report_path)
Examples:
<div class="section">
<h2>Executive Summary</h2>
<div class="card">
<p>[placeholder: 1-3 paragraph summary]</p>
<div class="highlight">
<strong>Key Findings:</strong>
<ul>
<li>[placeholder: Key insight 1]</li>
<li>[placeholder: Key insight 2]</li>
</ul>
</div>
</div>
</div>
<div class="section">
<h2>[placeholder: Section name]</h2>
<div class="card">
<div class="card-header">[placeholder: Chart title]</div>
<div class="card-body">
<div class="chart-container">
<iframe src="[placeholder: /workspace/visualization/chart_name.html]"
width="100%" height="100%" frameborder="0"></iframe>
</div>
<div class="mt-3">
<h4>Key Insights:</h4>
<ul>
<li>[placeholder: Chart insight 1]</li>
<li>[placeholder: Chart insight 2]</li>
</ul>
</div>
</div>
</div>
</div>"""
},
},
"required": ["report_template_description", "code"],
}
async def execute(self, code: str, report_template_description: str | None = None, timeout=5):
return await super().execute(code, timeout)
@@ -0,0 +1,142 @@
from app.tool.base import BaseTool, ToolResult
from typing import ClassVar, Dict
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pathlib import Path
from app.config import config
from app.tool.file_operators import (
FileOperator,
LocalFileOperator,
PathLike,
SandboxFileOperator,
)
from typing import List
class SearchHtmlLibraryResponse(ToolResult):
"""Structured response from the SearchHtmlLibrary tool, inheriting ToolResult."""
report_bootstrap_theme: str = Field(description="The theme of the bootstrap report")
components_content: Dict[str, str] = Field(description="The UI components content as a dictionary (component_name: html_content)")
def __str__(self) -> str:
"""Formatted string with indented HTML content"""
components_info = []
for name, content in self.components_content.items():
components_info.append(
f"{name}\n"
f"{content.strip()}\n"
f"{'-'*40}"
)
return (
f"📊 Report Theme: {self.report_bootstrap_theme}\n\n"
f"🛠️ Components Content:\n\n"
f"{'\n'.join(components_info)}"
)
class SearchHtmlLibrary(BaseTool):
"""A tool for searching the html library in users' local file system"""
name: str = "search_html_library"
description: str = """Check the type of user's input data, and select proper bootstrap theme and component from user's local file system.
When user requires a report, you need to use this tool first, to search the corresponding ui components and serve as a reference for subsequent report generation.
Then, use other tools to generate fancy report template, specific chart...
Outputs: 1) HTML components, 2) Bootstrap theme """
parameters: dict = {
"type": "object",
"properties": {
"report_bootstrap_theme":{
"description": "The theme of bootstrap template to use.",
"enum": [
# Light themes
"Brite"
"Cerulean",
"Materia",
"Cosmo",
"Flatly",
"Journal",
"Litera",
"Lumen",
"Minty",
"Pulse",
"Sandstone",
"Simplex",
"Sketchy",
"Spacelab",
"United",
"Zephyr",
# Dark themes
"Cyborg",
"Darkly",
"Slate",
"Solar",
"Superhero",
"Vapor",
"Lux",
# Special styles
"Quartz",
"Morph",
"Yeti"
],
"default": "Materia",
"type": "string",
},
"components": {
"description": "List of components to you will use in the report, you need to decide based on user's input data.",
"type": "array",
"items": {
"type": "string",
"enum": ["blockquote", "card", "chart", "indicator", "list", "nav", "nvabar", "progress", "table", "typography"]
},
"default": ["card", "chart", "table"],
"minItems": 2,
"uniqueItems": True
}
},
"required": ["report_bootstrap_theme", "components"],
}
_local_operator: LocalFileOperator = LocalFileOperator()
_sandbox_operator: SandboxFileOperator = SandboxFileOperator()
# def _get_operator(self, use_sandbox: bool) -> FileOperator:
def _get_operator(self) -> FileOperator:
"""Get the appropriate file operator based on execution mode."""
return (
self._sandbox_operator
if config.sandbox.use_sandbox
else self._local_operator
)
async def execute(
self,
report_bootstrap_theme: str,
components: List[str]
) -> SearchHtmlLibraryResponse:
"""
Execute the tool with the given parameters.
Reads HTML component files and returns their content in a dictionary.
"""
operator = self._get_operator()
components_content = {} # Initialize an empty dictionary to store component contents
for component in components:
path = f"/home/vm3/JoyZhao/OSPP/OpenManus/workspace/html_library/{component}.html"
print(f"Reading component: {path}")
try:
# Read the HTML file content
component_content = await operator.read_file(path)
# Store in dictionary with component name as key
components_content[component] = component_content
except Exception as e:
print(f"Failed to read component {component}: {str(e)}")
components_content[component] = f"Error loading {component} component"
theme=f"https://cdn.jsdelivr.net/npm/bootswatch@5/dist/{report_bootstrap_theme.lower()}/bootstrap.min.css"
return SearchHtmlLibraryResponse(
report_bootstrap_theme=theme,
components_content=components_content
)
+487
View File
@@ -0,0 +1,487 @@
import asyncio
import base64
import logging
import os
import time
from typing import Dict, Literal, Optional
import aiohttp
from pydantic import Field
from app.daytona.tool_base import Sandbox, SandboxToolsBase
from app.tool.base import ToolResult
KEYBOARD_KEYS = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"enter",
"esc",
"backspace",
"tab",
"space",
"delete",
"ctrl",
"alt",
"shift",
"win",
"up",
"down",
"left",
"right",
"f1",
"f2",
"f3",
"f4",
"f5",
"f6",
"f7",
"f8",
"f9",
"f10",
"f11",
"f12",
"ctrl+c",
"ctrl+v",
"ctrl+x",
"ctrl+z",
"ctrl+a",
"ctrl+s",
"alt+tab",
"alt+f4",
"ctrl+alt+delete",
]
MOUSE_BUTTONS = ["left", "right", "middle"]
_COMPUTER_USE_DESCRIPTION = """\
A comprehensive computer automation tool that allows interaction with the desktop environment.
* This tool provides commands for controlling mouse, keyboard, and taking screenshots
* It maintains state including current mouse position
* Use this when you need to automate desktop applications, fill forms, or perform GUI interactions
Key capabilities include:
* Mouse Control: Move, click, drag, scroll
* Keyboard Input: Type text, press keys or key combinations
* Screenshots: Capture and save screen images
* Waiting: Pause execution for specified duration
"""
class ComputerUseTool(SandboxToolsBase):
"""Computer automation tool for controlling the desktop environment."""
name: str = "computer_use"
description: str = _COMPUTER_USE_DESCRIPTION
parameters: dict = {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"move_to",
"click",
"scroll",
"typing",
"press",
"wait",
"mouse_down",
"mouse_up",
"drag_to",
"hotkey",
"screenshot",
],
"description": "The computer action to perform",
},
"x": {"type": "number", "description": "X coordinate for mouse actions"},
"y": {"type": "number", "description": "Y coordinate for mouse actions"},
"button": {
"type": "string",
"enum": MOUSE_BUTTONS,
"description": "Mouse button for click/drag actions",
"default": "left",
},
"num_clicks": {
"type": "integer",
"description": "Number of clicks",
"enum": [1, 2, 3],
"default": 1,
},
"amount": {
"type": "integer",
"description": "Scroll amount (positive for up, negative for down)",
"minimum": -10,
"maximum": 10,
},
"text": {"type": "string", "description": "Text to type"},
"key": {
"type": "string",
"enum": KEYBOARD_KEYS,
"description": "Key to press",
},
"keys": {
"type": "string",
"enum": KEYBOARD_KEYS,
"description": "Key combination to press",
},
"duration": {
"type": "number",
"description": "Duration in seconds to wait",
"default": 0.5,
},
},
"required": ["action"],
"dependencies": {
"move_to": ["x", "y"],
"click": [],
"scroll": ["amount"],
"typing": ["text"],
"press": ["key"],
"wait": [],
"mouse_down": [],
"mouse_up": [],
"drag_to": ["x", "y"],
"hotkey": ["keys"],
"screenshot": [],
},
}
session: Optional[aiohttp.ClientSession] = Field(default=None, exclude=True)
mouse_x: int = Field(default=0, exclude=True)
mouse_y: int = Field(default=0, exclude=True)
api_base_url: Optional[str] = Field(default=None, exclude=True)
def __init__(self, sandbox: Optional[Sandbox] = None, **data):
"""Initialize with optional sandbox."""
super().__init__(**data)
if sandbox is not None:
self._sandbox = sandbox # 直接操作基类的私有属性
self.api_base_url = sandbox.get_preview_link(8000).url
logging.info(
f"Initialized ComputerUseTool with API URL: {self.api_base_url}"
)
@classmethod
def create_with_sandbox(cls, sandbox: Sandbox) -> "ComputerUseTool":
"""Factory method to create a tool with sandbox."""
return cls(sandbox=sandbox) # 通过构造函数初始化
async def _get_session(self) -> aiohttp.ClientSession:
"""Get or create aiohttp session for API requests."""
if self.session is None or self.session.closed:
self.session = aiohttp.ClientSession()
return self.session
async def _api_request(
self, method: str, endpoint: str, data: Optional[Dict] = None
) -> Dict:
"""Send request to automation service API."""
try:
session = await self._get_session()
url = f"{self.api_base_url}/api{endpoint}"
logging.debug(f"API request: {method} {url} {data}")
if method.upper() == "GET":
async with session.get(url) as response:
result = await response.json()
else: # POST
async with session.post(url, json=data) as response:
result = await response.json()
logging.debug(f"API response: {result}")
return result
except Exception as e:
logging.error(f"API request failed: {str(e)}")
return {"success": False, "error": str(e)}
async def execute(
self,
action: Literal[
"move_to",
"click",
"scroll",
"typing",
"press",
"wait",
"mouse_down",
"mouse_up",
"drag_to",
"hotkey",
"screenshot",
],
x: Optional[float] = None,
y: Optional[float] = None,
button: str = "left",
num_clicks: int = 1,
amount: Optional[int] = None,
text: Optional[str] = None,
key: Optional[str] = None,
keys: Optional[str] = None,
duration: float = 0.5,
**kwargs,
) -> ToolResult:
"""
Execute a specified computer automation action.
Args:
action: The action to perform
x: X coordinate for mouse actions
y: Y coordinate for mouse actions
button: Mouse button for click/drag actions
num_clicks: Number of clicks to perform
amount: Scroll amount (positive for up, negative for down)
text: Text to type
key: Key to press
keys: Key combination to press
duration: Duration in seconds to wait
**kwargs: Additional arguments
Returns:
ToolResult with the action's output or error
"""
try:
if action == "move_to":
if x is None or y is None:
return ToolResult(error="x and y coordinates are required")
x_int = int(round(float(x)))
y_int = int(round(float(y)))
result = await self._api_request(
"POST", "/automation/mouse/move", {"x": x_int, "y": y_int}
)
if result.get("success", False):
self.mouse_x = x_int
self.mouse_y = y_int
return ToolResult(output=f"Moved to ({x_int}, {y_int})")
else:
return ToolResult(
error=f"Failed to move: {result.get('error', 'Unknown error')}"
)
elif action == "click":
x_val = x if x is not None else self.mouse_x
y_val = y if y is not None else self.mouse_y
x_int = int(round(float(x_val)))
y_int = int(round(float(y_val)))
num_clicks = int(num_clicks)
result = await self._api_request(
"POST",
"/automation/mouse/click",
{
"x": x_int,
"y": y_int,
"clicks": num_clicks,
"button": button.lower(),
},
)
if result.get("success", False):
self.mouse_x = x_int
self.mouse_y = y_int
return ToolResult(
output=f"{num_clicks} {button} click(s) performed at ({x_int}, {y_int})"
)
else:
return ToolResult(
error=f"Failed to click: {result.get('error', 'Unknown error')}"
)
elif action == "scroll":
if amount is None:
return ToolResult(error="Scroll amount is required")
amount = int(float(amount))
amount = max(-10, min(10, amount))
result = await self._api_request(
"POST",
"/automation/mouse/scroll",
{"clicks": amount, "x": self.mouse_x, "y": self.mouse_y},
)
if result.get("success", False):
direction = "up" if amount > 0 else "down"
steps = abs(amount)
return ToolResult(
output=f"Scrolled {direction} {steps} step(s) at position ({self.mouse_x}, {self.mouse_y})"
)
else:
return ToolResult(
error=f"Failed to scroll: {result.get('error', 'Unknown error')}"
)
elif action == "typing":
if text is None:
return ToolResult(error="Text is required for typing")
text = str(text)
result = await self._api_request(
"POST",
"/automation/keyboard/write",
{"message": text, "interval": 0.01},
)
if result.get("success", False):
return ToolResult(output=f"Typed: {text}")
else:
return ToolResult(
error=f"Failed to type: {result.get('error', 'Unknown error')}"
)
elif action == "press":
if key is None:
return ToolResult(error="Key is required for press action")
key = str(key).lower()
result = await self._api_request(
"POST", "/automation/keyboard/press", {"keys": key, "presses": 1}
)
if result.get("success", False):
return ToolResult(output=f"Pressed key: {key}")
else:
return ToolResult(
error=f"Failed to press key: {result.get('error', 'Unknown error')}"
)
elif action == "wait":
duration = float(duration)
duration = max(0, min(10, duration))
await asyncio.sleep(duration)
return ToolResult(output=f"Waited {duration} seconds")
elif action == "mouse_down":
x_val = x if x is not None else self.mouse_x
y_val = y if y is not None else self.mouse_y
x_int = int(round(float(x_val)))
y_int = int(round(float(y_val)))
result = await self._api_request(
"POST",
"/automation/mouse/down",
{"x": x_int, "y": y_int, "button": button.lower()},
)
if result.get("success", False):
self.mouse_x = x_int
self.mouse_y = y_int
return ToolResult(
output=f"{button} button pressed at ({x_int}, {y_int})"
)
else:
return ToolResult(
error=f"Failed to press button: {result.get('error', 'Unknown error')}"
)
elif action == "mouse_up":
x_val = x if x is not None else self.mouse_x
y_val = y if y is not None else self.mouse_y
x_int = int(round(float(x_val)))
y_int = int(round(float(y_val)))
result = await self._api_request(
"POST",
"/automation/mouse/up",
{"x": x_int, "y": y_int, "button": button.lower()},
)
if result.get("success", False):
self.mouse_x = x_int
self.mouse_y = y_int
return ToolResult(
output=f"{button} button released at ({x_int}, {y_int})"
)
else:
return ToolResult(
error=f"Failed to release button: {result.get('error', 'Unknown error')}"
)
elif action == "drag_to":
if x is None or y is None:
return ToolResult(error="x and y coordinates are required")
target_x = int(round(float(x)))
target_y = int(round(float(y)))
start_x = self.mouse_x
start_y = self.mouse_y
result = await self._api_request(
"POST",
"/automation/mouse/drag",
{"x": target_x, "y": target_y, "duration": 0.3, "button": "left"},
)
if result.get("success", False):
self.mouse_x = target_x
self.mouse_y = target_y
return ToolResult(
output=f"Dragged from ({start_x}, {start_y}) to ({target_x}, {target_y})"
)
else:
return ToolResult(
error=f"Failed to drag: {result.get('error', 'Unknown error')}"
)
elif action == "hotkey":
if keys is None:
return ToolResult(error="Keys are required for hotkey action")
keys = str(keys).lower().strip()
key_sequence = keys.split("+")
result = await self._api_request(
"POST",
"/automation/keyboard/hotkey",
{"keys": key_sequence, "interval": 0.01},
)
if result.get("success", False):
return ToolResult(output=f"Pressed key combination: {keys}")
else:
return ToolResult(
error=f"Failed to press keys: {result.get('error', 'Unknown error')}"
)
elif action == "screenshot":
result = await self._api_request("POST", "/automation/screenshot")
if "image" in result:
base64_str = result["image"]
timestamp = time.strftime("%Y%m%d_%H%M%S")
# Save screenshot to file
screenshots_dir = "screenshots"
if not os.path.exists(screenshots_dir):
os.makedirs(screenshots_dir)
timestamped_filename = os.path.join(
screenshots_dir, f"screenshot_{timestamp}.png"
)
latest_filename = "latest_screenshot.png"
# Decode base64 string and save to file
img_data = base64.b64decode(base64_str)
with open(timestamped_filename, "wb") as f:
f.write(img_data)
# Save a copy as the latest screenshot
with open(latest_filename, "wb") as f:
f.write(img_data)
return ToolResult(
output=f"Screenshot saved as {timestamped_filename}",
base64_image=base64_str,
)
else:
return ToolResult(error="Failed to capture screenshot")
else:
return ToolResult(error=f"Unknown action: {action}")
except Exception as e:
return ToolResult(error=f"Computer action failed: {str(e)}")
async def cleanup(self):
"""Clean up resources."""
if self.session and not self.session.closed:
await self.session.close()
self.session = None
def __del__(self):
"""Ensure cleanup on destruction."""
if hasattr(self, "session") and self.session is not None:
try:
asyncio.run(self.cleanup())
except RuntimeError:
loop = asyncio.new_event_loop()
loop.run_until_complete(self.cleanup())
loop.close()
+269
View File
@@ -0,0 +1,269 @@
"""
Crawl4AI Web Crawler Tool for OpenManus
This tool integrates Crawl4AI, a high-performance web crawler designed for LLMs and AI agents,
providing fast, precise, and AI-ready data extraction with clean Markdown generation.
"""
import asyncio
from typing import List, Union
from urllib.parse import urlparse
from app.logger import logger
from app.tool.base import BaseTool, ToolResult
class Crawl4aiTool(BaseTool):
"""
Web crawler tool powered by Crawl4AI.
Provides clean markdown extraction optimized for AI processing.
"""
name: str = "crawl4ai"
description: str = """Web crawler that extracts clean, AI-ready content from web pages.
Features:
- Extracts clean markdown content optimized for LLMs
- Handles JavaScript-heavy sites and dynamic content
- Supports multiple URLs in a single request
- Fast and reliable with built-in error handling
Perfect for content analysis, research, and feeding web content to AI models."""
parameters: dict = {
"type": "object",
"properties": {
"urls": {
"type": "array",
"items": {"type": "string"},
"description": "(required) List of URLs to crawl. Can be a single URL or multiple URLs.",
"minItems": 1,
},
"timeout": {
"type": "integer",
"description": "(optional) Timeout in seconds for each URL. Default is 30.",
"default": 30,
"minimum": 5,
"maximum": 120,
},
"bypass_cache": {
"type": "boolean",
"description": "(optional) Whether to bypass cache and fetch fresh content. Default is false.",
"default": False,
},
"word_count_threshold": {
"type": "integer",
"description": "(optional) Minimum word count for content blocks. Default is 10.",
"default": 10,
"minimum": 1,
},
},
"required": ["urls"],
}
async def execute(
self,
urls: Union[str, List[str]],
timeout: int = 30,
bypass_cache: bool = False,
word_count_threshold: int = 10,
) -> ToolResult:
"""
Execute web crawling for the specified URLs.
Args:
urls: Single URL string or list of URLs to crawl
timeout: Timeout in seconds for each URL
bypass_cache: Whether to bypass cache
word_count_threshold: Minimum word count for content blocks
Returns:
ToolResult with crawl results
"""
# Normalize URLs to list
if isinstance(urls, str):
url_list = [urls]
else:
url_list = urls
# Validate URLs
valid_urls = []
for url in url_list:
if self._is_valid_url(url):
valid_urls.append(url)
else:
logger.warning(f"Invalid URL skipped: {url}")
if not valid_urls:
return ToolResult(error="No valid URLs provided")
try:
# Import crawl4ai components
from crawl4ai import (
AsyncWebCrawler,
BrowserConfig,
CacheMode,
CrawlerRunConfig,
)
# Configure browser settings
browser_config = BrowserConfig(
headless=True,
verbose=False,
browser_type="chromium",
ignore_https_errors=True,
java_script_enabled=True,
)
# Configure crawler settings
run_config = CrawlerRunConfig(
cache_mode=CacheMode.BYPASS if bypass_cache else CacheMode.ENABLED,
word_count_threshold=word_count_threshold,
process_iframes=True,
remove_overlay_elements=True,
excluded_tags=["script", "style"],
page_timeout=timeout * 1000, # Convert to milliseconds
verbose=False,
wait_until="domcontentloaded",
)
results = []
successful_count = 0
failed_count = 0
# Process each URL
async with AsyncWebCrawler(config=browser_config) as crawler:
for url in valid_urls:
try:
logger.info(f"🕷️ Crawling URL: {url}")
start_time = asyncio.get_event_loop().time()
result = await crawler.arun(url=url, config=run_config)
end_time = asyncio.get_event_loop().time()
execution_time = end_time - start_time
if result.success:
# Count words in markdown
word_count = 0
if hasattr(result, "markdown") and result.markdown:
word_count = len(result.markdown.split())
# Count links
links_count = 0
if hasattr(result, "links") and result.links:
internal_links = result.links.get("internal", [])
external_links = result.links.get("external", [])
links_count = len(internal_links) + len(external_links)
# Count images
images_count = 0
if hasattr(result, "media") and result.media:
images = result.media.get("images", [])
images_count = len(images)
results.append(
{
"url": url,
"success": True,
"status_code": getattr(result, "status_code", 200),
"title": result.metadata.get("title")
if result.metadata
else None,
"markdown": result.markdown
if hasattr(result, "markdown")
else None,
"word_count": word_count,
"links_count": links_count,
"images_count": images_count,
"execution_time": execution_time,
}
)
successful_count += 1
logger.info(
f"✅ Successfully crawled {url} in {execution_time:.2f}s"
)
else:
results.append(
{
"url": url,
"success": False,
"error_message": getattr(
result, "error_message", "Unknown error"
),
"execution_time": execution_time,
}
)
failed_count += 1
logger.warning(f"❌ Failed to crawl {url}")
except Exception as e:
error_msg = f"Error crawling {url}: {str(e)}"
logger.error(error_msg)
results.append(
{"url": url, "success": False, "error_message": error_msg}
)
failed_count += 1
# Format output
output_lines = [f"🕷️ Crawl4AI Results Summary:"]
output_lines.append(f"📊 Total URLs: {len(valid_urls)}")
output_lines.append(f"✅ Successful: {successful_count}")
output_lines.append(f"❌ Failed: {failed_count}")
output_lines.append("")
for i, result in enumerate(results, 1):
output_lines.append(f"{i}. {result['url']}")
if result["success"]:
output_lines.append(
f" ✅ Status: Success (HTTP {result.get('status_code', 'N/A')})"
)
if result.get("title"):
output_lines.append(f" 📄 Title: {result['title']}")
if result.get("markdown"):
# Show first 300 characters of markdown content
content_preview = result["markdown"]
if len(result["markdown"]) > 300:
content_preview += "..."
output_lines.append(f" 📝 Content: {content_preview}")
output_lines.append(
f" 📊 Stats: {result.get('word_count', 0)} words, {result.get('links_count', 0)} links, {result.get('images_count', 0)} images"
)
if result.get("execution_time"):
output_lines.append(
f" ⏱️ Time: {result['execution_time']:.2f}s"
)
else:
output_lines.append(f" ❌ Status: Failed")
if result.get("error_message"):
output_lines.append(f" 🚫 Error: {result['error_message']}")
output_lines.append("")
return ToolResult(output="\n".join(output_lines))
except ImportError:
error_msg = "Crawl4AI is not installed. Please install it with: pip install crawl4ai"
logger.error(error_msg)
return ToolResult(error=error_msg)
except Exception as e:
error_msg = f"Crawl4AI execution failed: {str(e)}"
logger.error(error_msg)
return ToolResult(error=error_msg)
def _is_valid_url(self, url: str) -> bool:
"""Validate if a URL is properly formatted."""
try:
result = urlparse(url)
return all([result.scheme, result.netloc]) and result.scheme in [
"http",
"https",
]
except Exception:
return False
-67
View File
@@ -1,67 +0,0 @@
import os
import aiofiles
from app.config import WORKSPACE_ROOT
from app.tool.base import BaseTool
class FileSaver(BaseTool):
name: str = "file_saver"
description: str = """Save content to a local file at a specified path.
Use this tool when you need to save text, code, or generated content to a file on the local filesystem.
The tool accepts content and a file path, and saves the content to that location.
"""
parameters: dict = {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "(required) The content to save to the file.",
},
"file_path": {
"type": "string",
"description": "(required) The path where the file should be saved, including filename and extension.",
},
"mode": {
"type": "string",
"description": "(optional) The file opening mode. Default is 'w' for write. Use 'a' for append.",
"enum": ["w", "a"],
"default": "w",
},
},
"required": ["content", "file_path"],
}
async def execute(self, content: str, file_path: str, mode: str = "w") -> str:
"""
Save content to a file at the specified path.
Args:
content (str): The content to save to the file.
file_path (str): The path where the file should be saved.
mode (str, optional): The file opening mode. Default is 'w' for write. Use 'a' for append.
Returns:
str: A message indicating the result of the operation.
"""
try:
# Place the generated file in the workspace directory
if os.path.isabs(file_path):
file_name = os.path.basename(file_path)
full_path = os.path.join(WORKSPACE_ROOT, file_name)
else:
full_path = os.path.join(WORKSPACE_ROOT, file_path)
# Ensure the directory exists
directory = os.path.dirname(full_path)
if directory and not os.path.exists(directory):
os.makedirs(directory)
# Write directly to the file
async with aiofiles.open(full_path, mode, encoding="utf-8") as file:
await file.write(content)
return f"Content successfully saved to {full_path}"
except Exception as e:
return f"Error saving file: {str(e)}"
+122 -43
View File
@@ -1,10 +1,10 @@
from contextlib import AsyncExitStack
from typing import List, Optional
from typing import Dict, List, Optional
from mcp import ClientSession, StdioServerParameters
from mcp.client.sse import sse_client
from mcp.client.stdio import stdio_client
from mcp.types import TextContent
from mcp.types import ListToolsResult, TextContent
from app.logger import logger
from app.tool.base import BaseTool, ToolResult
@@ -15,6 +15,8 @@ class MCPClientTool(BaseTool):
"""Represents a tool proxy that can be called on the MCP server from the client side."""
session: Optional[ClientSession] = None
server_id: str = "" # Add server identifier
original_name: str = ""
async def execute(self, **kwargs) -> ToolResult:
"""Execute the tool by making a remote call to the MCP server."""
@@ -22,7 +24,8 @@ class MCPClientTool(BaseTool):
return ToolResult(error="Not connected to MCP server")
try:
result = await self.session.call_tool(self.name, kwargs)
logger.info(f"Executing tool: {self.original_name}")
result = await self.session.call_tool(self.original_name, kwargs)
content_str = ", ".join(
item.text for item in result.content if isinstance(item, TextContent)
)
@@ -33,83 +36,159 @@ class MCPClientTool(BaseTool):
class MCPClients(ToolCollection):
"""
A collection of tools that connects to an MCP server and manages available tools through the Model Context Protocol.
A collection of tools that connects to multiple MCP servers and manages available tools through the Model Context Protocol.
"""
session: Optional[ClientSession] = None
exit_stack: AsyncExitStack = None
sessions: Dict[str, ClientSession] = {}
exit_stacks: Dict[str, AsyncExitStack] = {}
description: str = "MCP client tools for server interaction"
def __init__(self):
super().__init__() # Initialize with empty tools list
self.name = "mcp" # Keep name for backward compatibility
self.exit_stack = AsyncExitStack()
async def connect_sse(self, server_url: str) -> None:
async def connect_sse(self, server_url: str, server_id: str = "") -> None:
"""Connect to an MCP server using SSE transport."""
if not server_url:
raise ValueError("Server URL is required.")
if self.session:
await self.disconnect()
server_id = server_id or server_url
# Always ensure clean disconnection before new connection
if server_id in self.sessions:
await self.disconnect(server_id)
exit_stack = AsyncExitStack()
self.exit_stacks[server_id] = exit_stack
streams_context = sse_client(url=server_url)
streams = await self.exit_stack.enter_async_context(streams_context)
self.session = await self.exit_stack.enter_async_context(
ClientSession(*streams)
)
streams = await exit_stack.enter_async_context(streams_context)
session = await exit_stack.enter_async_context(ClientSession(*streams))
self.sessions[server_id] = session
await self._initialize_and_list_tools()
await self._initialize_and_list_tools(server_id)
async def connect_stdio(self, command: str, args: List[str]) -> None:
async def connect_stdio(
self, command: str, args: List[str], server_id: str = ""
) -> None:
"""Connect to an MCP server using stdio transport."""
if not command:
raise ValueError("Server command is required.")
if self.session:
await self.disconnect()
server_id = server_id or command
# Always ensure clean disconnection before new connection
if server_id in self.sessions:
await self.disconnect(server_id)
exit_stack = AsyncExitStack()
self.exit_stacks[server_id] = exit_stack
server_params = StdioServerParameters(command=command, args=args)
stdio_transport = await self.exit_stack.enter_async_context(
stdio_transport = await exit_stack.enter_async_context(
stdio_client(server_params)
)
read, write = stdio_transport
self.session = await self.exit_stack.enter_async_context(
ClientSession(read, write)
)
session = await exit_stack.enter_async_context(ClientSession(read, write))
self.sessions[server_id] = session
await self._initialize_and_list_tools()
await self._initialize_and_list_tools(server_id)
async def _initialize_and_list_tools(self) -> None:
async def _initialize_and_list_tools(self, server_id: str) -> None:
"""Initialize session and populate tool map."""
if not self.session:
raise RuntimeError("Session not initialized.")
session = self.sessions.get(server_id)
if not session:
raise RuntimeError(f"Session not initialized for server {server_id}")
await self.session.initialize()
response = await self.session.list_tools()
# Clear existing tools
self.tools = tuple()
self.tool_map = {}
await session.initialize()
response = await session.list_tools()
# Create proper tool objects for each server tool
for tool in response.tools:
original_name = tool.name
tool_name = f"mcp_{server_id}_{original_name}"
tool_name = self._sanitize_tool_name(tool_name)
server_tool = MCPClientTool(
name=tool.name,
name=tool_name,
description=tool.description,
parameters=tool.inputSchema,
session=self.session,
session=session,
server_id=server_id,
original_name=original_name,
)
self.tool_map[tool.name] = server_tool
self.tool_map[tool_name] = server_tool
# Update tools tuple
self.tools = tuple(self.tool_map.values())
logger.info(
f"Connected to server with tools: {[tool.name for tool in response.tools]}"
f"Connected to server {server_id} with tools: {[tool.name for tool in response.tools]}"
)
async def disconnect(self) -> None:
"""Disconnect from the MCP server and clean up resources."""
if self.session and self.exit_stack:
await self.exit_stack.aclose()
self.session = None
self.tools = tuple()
def _sanitize_tool_name(self, name: str) -> str:
"""Sanitize tool name to match MCPClientTool requirements."""
import re
# Replace invalid characters with underscores
sanitized = re.sub(r"[^a-zA-Z0-9_-]", "_", name)
# Remove consecutive underscores
sanitized = re.sub(r"_+", "_", sanitized)
# Remove leading/trailing underscores
sanitized = sanitized.strip("_")
# Truncate to 64 characters if needed
if len(sanitized) > 64:
sanitized = sanitized[:64]
return sanitized
async def list_tools(self) -> ListToolsResult:
"""List all available tools."""
tools_result = ListToolsResult(tools=[])
for session in self.sessions.values():
response = await session.list_tools()
tools_result.tools += response.tools
return tools_result
async def disconnect(self, server_id: str = "") -> None:
"""Disconnect from a specific MCP server or all servers if no server_id provided."""
if server_id:
if server_id in self.sessions:
try:
exit_stack = self.exit_stacks.get(server_id)
# Close the exit stack which will handle session cleanup
if exit_stack:
try:
await exit_stack.aclose()
except RuntimeError as e:
if "cancel scope" in str(e).lower():
logger.warning(
f"Cancel scope error during disconnect from {server_id}, continuing with cleanup: {e}"
)
else:
raise
# Clean up references
self.sessions.pop(server_id, None)
self.exit_stacks.pop(server_id, None)
# Remove tools associated with this server
self.tool_map = {
k: v
for k, v in self.tool_map.items()
if v.server_id != server_id
}
self.tools = tuple(self.tool_map.values())
logger.info(f"Disconnected from MCP server {server_id}")
except Exception as e:
logger.error(f"Error disconnecting from server {server_id}: {e}")
else:
# Disconnect from all servers in a deterministic order
for sid in sorted(list(self.sessions.keys())):
await self.disconnect(sid)
self.tool_map = {}
logger.info("Disconnected from MCP server")
self.tools = tuple()
logger.info("Disconnected from all MCP servers")
+450
View File
@@ -0,0 +1,450 @@
import base64
import io
import json
import traceback
from typing import Optional # Add this import for Optional
from PIL import Image
from pydantic import Field
from app.daytona.tool_base import ( # Ensure Sandbox is imported correctly
Sandbox,
SandboxToolsBase,
ThreadMessage,
)
from app.tool.base import ToolResult
from app.utils.logger import logger
# Context = TypeVar("Context")
_BROWSER_DESCRIPTION = """\
A sandbox-based browser automation tool that allows interaction with web pages through various actions.
* This tool provides commands for controlling a browser session in a sandboxed environment
* It maintains state across calls, keeping the browser session alive until explicitly closed
* Use this when you need to browse websites, fill forms, click buttons, or extract content in a secure sandbox
* Each action requires specific parameters as defined in the tool's dependencies
Key capabilities include:
* Navigation: Go to specific URLs, go back in history
* Interaction: Click elements by index, input text, send keyboard commands
* Scrolling: Scroll up/down by pixel amount or scroll to specific text
* Tab management: Switch between tabs or close tabs
* Content extraction: Get dropdown options or select dropdown options
"""
# noinspection PyArgumentList
class SandboxBrowserTool(SandboxToolsBase):
"""Tool for executing tasks in a Daytona sandbox with browser-use capabilities."""
name: str = "sandbox_browser"
description: str = _BROWSER_DESCRIPTION
parameters: dict = {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"navigate_to",
"go_back",
"wait",
"click_element",
"input_text",
"send_keys",
"switch_tab",
"close_tab",
"scroll_down",
"scroll_up",
"scroll_to_text",
"get_dropdown_options",
"select_dropdown_option",
"click_coordinates",
"drag_drop",
],
"description": "The browser action to perform",
},
"url": {
"type": "string",
"description": "URL for 'navigate_to' action",
},
"index": {
"type": "integer",
"description": "Element index for interaction actions",
},
"text": {
"type": "string",
"description": "Text for input or scroll actions",
},
"amount": {
"type": "integer",
"description": "Pixel amount to scroll",
},
"page_id": {
"type": "integer",
"description": "Tab ID for tab management actions",
},
"keys": {
"type": "string",
"description": "Keys to send for keyboard actions",
},
"seconds": {
"type": "integer",
"description": "Seconds to wait",
},
"x": {
"type": "integer",
"description": "X coordinate for click or drag actions",
},
"y": {
"type": "integer",
"description": "Y coordinate for click or drag actions",
},
"element_source": {
"type": "string",
"description": "Source element for drag and drop",
},
"element_target": {
"type": "string",
"description": "Target element for drag and drop",
},
},
"required": ["action"],
"dependencies": {
"navigate_to": ["url"],
"click_element": ["index"],
"input_text": ["index", "text"],
"send_keys": ["keys"],
"switch_tab": ["page_id"],
"close_tab": ["page_id"],
"scroll_down": ["amount"],
"scroll_up": ["amount"],
"scroll_to_text": ["text"],
"get_dropdown_options": ["index"],
"select_dropdown_option": ["index", "text"],
"click_coordinates": ["x", "y"],
"drag_drop": ["element_source", "element_target"],
"wait": ["seconds"],
},
}
browser_message: Optional[ThreadMessage] = Field(default=None, exclude=True)
def __init__(
self, sandbox: Optional[Sandbox] = None, thread_id: Optional[str] = None, **data
):
"""Initialize with optional sandbox and thread_id."""
super().__init__(**data)
if sandbox is not None:
self._sandbox = sandbox # Directly set the base class private attribute
def _validate_base64_image(
self, base64_string: str, max_size_mb: int = 10
) -> tuple[bool, str]:
"""
Validate base64 image data.
Args:
base64_string: The base64 encoded image data
max_size_mb: Maximum allowed image size in megabytes
Returns:
Tuple of (is_valid, error_message)
"""
try:
if not base64_string or len(base64_string) < 10:
return False, "Base64 string is empty or too short"
if base64_string.startswith("data:"):
try:
base64_string = base64_string.split(",", 1)[1]
except (IndexError, ValueError):
return False, "Invalid data URL format"
import re
if not re.match(r"^[A-Za-z0-9+/]*={0,2}$", base64_string):
return False, "Invalid base64 characters detected"
if len(base64_string) % 4 != 0:
return False, "Invalid base64 string length"
try:
image_data = base64.b64decode(base64_string, validate=True)
except Exception as e:
return False, f"Base64 decoding failed: {str(e)}"
max_size_bytes = max_size_mb * 1024 * 1024
if len(image_data) > max_size_bytes:
return False, f"Image size exceeds limit ({max_size_bytes} bytes)"
try:
image_stream = io.BytesIO(image_data)
with Image.open(image_stream) as img:
img.verify()
supported_formats = {"JPEG", "PNG", "GIF", "BMP", "WEBP", "TIFF"}
if img.format not in supported_formats:
return False, f"Unsupported image format: {img.format}"
image_stream.seek(0)
with Image.open(image_stream) as img_check:
width, height = img_check.size
max_dimension = 8192
if width > max_dimension or height > max_dimension:
return (
False,
f"Image dimensions exceed limit ({max_dimension}x{max_dimension})",
)
if width < 1 or height < 1:
return False, f"Invalid image dimensions: {width}x{height}"
except Exception as e:
return False, f"Invalid image data: {str(e)}"
return True, "Valid image"
except Exception as e:
logger.error(f"Unexpected error during base64 image validation: {e}")
return False, f"Validation error: {str(e)}"
async def _execute_browser_action(
self, endpoint: str, params: dict = None, method: str = "POST"
) -> ToolResult:
"""Execute a browser automation action through the sandbox API."""
try:
await self._ensure_sandbox()
url = f"http://localhost:8003/api/automation/{endpoint}"
if method == "GET" and params:
query_params = "&".join([f"{k}={v}" for k, v in params.items()])
url = f"{url}?{query_params}"
curl_cmd = (
f"curl -s -X {method} '{url}' -H 'Content-Type: application/json'"
)
else:
curl_cmd = (
f"curl -s -X {method} '{url}' -H 'Content-Type: application/json'"
)
if params:
json_data = json.dumps(params)
curl_cmd += f" -d '{json_data}'"
logger.debug(f"Executing curl command: {curl_cmd}")
response = self.sandbox.process.exec(curl_cmd, timeout=30)
if response.exit_code == 0:
try:
result = json.loads(response.result)
result.setdefault("content", "")
result.setdefault("role", "assistant")
if "screenshot_base64" in result:
screenshot_data = result["screenshot_base64"]
is_valid, validation_message = self._validate_base64_image(
screenshot_data
)
if not is_valid:
logger.warning(
f"Screenshot validation failed: {validation_message}"
)
result["image_validation_error"] = validation_message
del result["screenshot_base64"]
# added_message = await self.thread_manager.add_message(
# thread_id=self.thread_id,
# type="browser_state",
# content=result,
# is_llm_message=False
# )
message = ThreadMessage(
type="browser_state", content=result, is_llm_message=False
)
self.browser_message = message
success_response = {
"success": result.get("success", False),
"message": result.get("message", "Browser action completed"),
}
# if added_message and 'message_id' in added_message:
# success_response['message_id'] = added_message['message_id']
for field in [
"url",
"title",
"element_count",
"pixels_below",
"ocr_text",
"image_url",
]:
if field in result:
success_response[field] = result[field]
return (
self.success_response(success_response)
if success_response["success"]
else self.fail_response(success_response)
)
except json.JSONDecodeError as e:
logger.error(f"Failed to parse response JSON: {e}")
return self.fail_response(f"Failed to parse response JSON: {e}")
else:
logger.error(f"Browser automation request failed: {response}")
return self.fail_response(
f"Browser automation request failed: {response}"
)
except Exception as e:
logger.error(f"Error executing browser action: {e}")
logger.debug(traceback.format_exc())
return self.fail_response(f"Error executing browser action: {e}")
async def execute(
self,
action: str,
url: Optional[str] = None,
index: Optional[int] = None,
text: Optional[str] = None,
amount: Optional[int] = None,
page_id: Optional[int] = None,
keys: Optional[str] = None,
seconds: Optional[int] = None,
x: Optional[int] = None,
y: Optional[int] = None,
element_source: Optional[str] = None,
element_target: Optional[str] = None,
**kwargs,
) -> ToolResult:
"""
Execute a browser action in the sandbox environment.
Args:
action: The browser action to perform
url: URL for navigation
index: Element index for interaction
text: Text for input or scroll actions
amount: Pixel amount to scroll
page_id: Tab ID for tab management
keys: Keys to send for keyboard actions
seconds: Seconds to wait
x: X coordinate for click/drag
y: Y coordinate for click/drag
element_source: Source element for drag and drop
element_target: Target element for drag and drop
Returns:
ToolResult with the action's output or error
"""
# async with self.lock:
try:
# Navigation actions
if action == "navigate_to":
if not url:
return self.fail_response("URL is required for navigation")
return await self._execute_browser_action("navigate_to", {"url": url})
elif action == "go_back":
return await self._execute_browser_action("go_back", {})
# Interaction actions
elif action == "click_element":
if index is None:
return self.fail_response("Index is required for click_element")
return await self._execute_browser_action(
"click_element", {"index": index}
)
elif action == "input_text":
if index is None or not text:
return self.fail_response(
"Index and text are required for input_text"
)
return await self._execute_browser_action(
"input_text", {"index": index, "text": text}
)
elif action == "send_keys":
if not keys:
return self.fail_response("Keys are required for send_keys")
return await self._execute_browser_action("send_keys", {"keys": keys})
# Tab management
elif action == "switch_tab":
if page_id is None:
return self.fail_response("Page ID is required for switch_tab")
return await self._execute_browser_action(
"switch_tab", {"page_id": page_id}
)
elif action == "close_tab":
if page_id is None:
return self.fail_response("Page ID is required for close_tab")
return await self._execute_browser_action(
"close_tab", {"page_id": page_id}
)
# Scrolling actions
elif action == "scroll_down":
params = {"amount": amount} if amount is not None else {}
return await self._execute_browser_action("scroll_down", params)
elif action == "scroll_up":
params = {"amount": amount} if amount is not None else {}
return await self._execute_browser_action("scroll_up", params)
elif action == "scroll_to_text":
if not text:
return self.fail_response("Text is required for scroll_to_text")
return await self._execute_browser_action(
"scroll_to_text", {"text": text}
)
# Dropdown actions
elif action == "get_dropdown_options":
if index is None:
return self.fail_response(
"Index is required for get_dropdown_options"
)
return await self._execute_browser_action(
"get_dropdown_options", {"index": index}
)
elif action == "select_dropdown_option":
if index is None or not text:
return self.fail_response(
"Index and text are required for select_dropdown_option"
)
return await self._execute_browser_action(
"select_dropdown_option", {"index": index, "text": text}
)
# Coordinate-based actions
elif action == "click_coordinates":
if x is None or y is None:
return self.fail_response(
"X and Y coordinates are required for click_coordinates"
)
return await self._execute_browser_action(
"click_coordinates", {"x": x, "y": y}
)
elif action == "drag_drop":
if not element_source or not element_target:
return self.fail_response(
"Source and target elements are required for drag_drop"
)
return await self._execute_browser_action(
"drag_drop",
{
"element_source": element_source,
"element_target": element_target,
},
)
# Utility actions
elif action == "wait":
seconds_to_wait = seconds if seconds is not None else 3
return await self._execute_browser_action(
"wait", {"seconds": seconds_to_wait}
)
else:
return self.fail_response(f"Unknown action: {action}")
except Exception as e:
logger.error(f"Error executing browser action: {e}")
return self.fail_response(f"Error executing browser action: {e}")
async def get_current_state(
self, message: Optional[ThreadMessage] = None
) -> ToolResult:
"""
Get the current browser state as a ToolResult.
If context is not provided, uses self.context.
"""
try:
# Use provided context or fall back to self.context
message = message or self.browser_message
if not message:
return ToolResult(error="Browser context not initialized")
state = message.content
screenshot = state.get("screenshot_base64")
# Build the state info with all required fields
state_info = {
"url": state.get("url", ""),
"title": state.get("title", ""),
"tabs": [tab.model_dump() for tab in state.get("tabs", [])],
"pixels_above": getattr(state, "pixels_above", 0),
"pixels_below": getattr(state, "pixels_below", 0),
"help": "[0], [1], [2], etc., represent clickable indices corresponding to the elements listed. Clicking on these indices will navigate to or interact with the respective content behind them.",
}
return ToolResult(
output=json.dumps(state_info, indent=4, ensure_ascii=False),
base64_image=screenshot,
)
except Exception as e:
return ToolResult(error=f"Failed to get browser state: {str(e)}")
@classmethod
def create_with_sandbox(cls, sandbox: Sandbox) -> "SandboxBrowserTool":
"""Factory method to create a tool with sandbox."""
return cls(sandbox=sandbox)
+361
View File
@@ -0,0 +1,361 @@
import asyncio
from typing import Optional, TypeVar
from pydantic import Field
from app.daytona.tool_base import Sandbox, SandboxToolsBase
from app.tool.base import ToolResult
from app.utils.files_utils import clean_path, should_exclude_file
from app.utils.logger import logger
Context = TypeVar("Context")
_FILES_DESCRIPTION = """\
A sandbox-based file system tool that allows file operations in a secure sandboxed environment.
* This tool provides commands for creating, reading, updating, and deleting files in the workspace
* All operations are performed relative to the /workspace directory for security
* Use this when you need to manage files, edit code, or manipulate file contents in a sandbox
* Each action requires specific parameters as defined in the tool's dependencies
Key capabilities include:
* File creation: Create new files with specified content and permissions
* File modification: Replace specific strings or completely rewrite files
* File deletion: Remove files from the workspace
* File reading: Read file contents with optional line range specification
"""
class SandboxFilesTool(SandboxToolsBase):
name: str = "sandbox_files"
description: str = _FILES_DESCRIPTION
parameters: dict = {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"create_file",
"str_replace",
"full_file_rewrite",
"delete_file",
],
"description": "The file operation to perform",
},
"file_path": {
"type": "string",
"description": "Path to the file, relative to /workspace (e.g., 'src/main.py')",
},
"file_contents": {
"type": "string",
"description": "Content to write to the file",
},
"old_str": {
"type": "string",
"description": "Text to be replaced (must appear exactly once)",
},
"new_str": {
"type": "string",
"description": "Replacement text",
},
"permissions": {
"type": "string",
"description": "File permissions in octal format (e.g., '644')",
"default": "644",
},
},
"required": ["action"],
"dependencies": {
"create_file": ["file_path", "file_contents"],
"str_replace": ["file_path", "old_str", "new_str"],
"full_file_rewrite": ["file_path", "file_contents"],
"delete_file": ["file_path"],
},
}
SNIPPET_LINES: int = Field(default=4, exclude=True)
# workspace_path: str = Field(default="/workspace", exclude=True)
# sandbox: Optional[Sandbox] = Field(default=None, exclude=True)
def __init__(
self, sandbox: Optional[Sandbox] = None, thread_id: Optional[str] = None, **data
):
"""Initialize with optional sandbox and thread_id."""
super().__init__(**data)
if sandbox is not None:
self._sandbox = sandbox
def clean_path(self, path: str) -> str:
"""Clean and normalize a path to be relative to /workspace"""
return clean_path(path, self.workspace_path)
def _should_exclude_file(self, rel_path: str) -> bool:
"""Check if a file should be excluded based on path, name, or extension"""
return should_exclude_file(rel_path)
def _file_exists(self, path: str) -> bool:
"""Check if a file exists in the sandbox"""
try:
self.sandbox.fs.get_file_info(path)
return True
except Exception:
return False
async def get_workspace_state(self) -> dict:
"""Get the current workspace state by reading all files"""
files_state = {}
try:
# Ensure sandbox is initialized
await self._ensure_sandbox()
files = self.sandbox.fs.list_files(self.workspace_path)
for file_info in files:
rel_path = file_info.name
# Skip excluded files and directories
if self._should_exclude_file(rel_path) or file_info.is_dir:
continue
try:
full_path = f"{self.workspace_path}/{rel_path}"
content = self.sandbox.fs.download_file(full_path).decode()
files_state[rel_path] = {
"content": content,
"is_dir": file_info.is_dir,
"size": file_info.size,
"modified": file_info.mod_time,
}
except Exception as e:
print(f"Error reading file {rel_path}: {e}")
except UnicodeDecodeError:
print(f"Skipping binary file: {rel_path}")
return files_state
except Exception as e:
print(f"Error getting workspace state: {str(e)}")
return {}
async def execute(
self,
action: str,
file_path: Optional[str] = None,
file_contents: Optional[str] = None,
old_str: Optional[str] = None,
new_str: Optional[str] = None,
permissions: Optional[str] = "644",
**kwargs,
) -> ToolResult:
"""
Execute a file operation in the sandbox environment.
Args:
action: The file operation to perform
file_path: Path to the file relative to /workspace
file_contents: Content to write to the file
old_str: Text to be replaced (for str_replace)
new_str: Replacement text (for str_replace)
permissions: File permissions in octal format
Returns:
ToolResult with the operation's output or error
"""
async with asyncio.Lock():
try:
# File creation
if action == "create_file":
if not file_path or not file_contents:
return self.fail_response(
"file_path and file_contents are required for create_file"
)
return await self._create_file(
file_path, file_contents, permissions
)
# String replacement
elif action == "str_replace":
if not file_path or not old_str or not new_str:
return self.fail_response(
"file_path, old_str, and new_str are required for str_replace"
)
return await self._str_replace(file_path, old_str, new_str)
# Full file rewrite
elif action == "full_file_rewrite":
if not file_path or not file_contents:
return self.fail_response(
"file_path and file_contents are required for full_file_rewrite"
)
return await self._full_file_rewrite(
file_path, file_contents, permissions
)
# File deletion
elif action == "delete_file":
if not file_path:
return self.fail_response(
"file_path is required for delete_file"
)
return await self._delete_file(file_path)
else:
return self.fail_response(f"Unknown action: {action}")
except Exception as e:
logger.error(f"Error executing file action: {e}")
return self.fail_response(f"Error executing file action: {e}")
async def _create_file(
self, file_path: str, file_contents: str, permissions: str = "644"
) -> ToolResult:
"""Create a new file with the provided contents"""
try:
# Ensure sandbox is initialized
await self._ensure_sandbox()
file_path = self.clean_path(file_path)
full_path = f"{self.workspace_path}/{file_path}"
if self._file_exists(full_path):
return self.fail_response(
f"File '{file_path}' already exists. Use full_file_rewrite to modify existing files."
)
# Create parent directories if needed
parent_dir = "/".join(full_path.split("/")[:-1])
if parent_dir:
self.sandbox.fs.create_folder(parent_dir, "755")
# Write the file content
self.sandbox.fs.upload_file(file_contents.encode(), full_path)
self.sandbox.fs.set_file_permissions(full_path, permissions)
message = f"File '{file_path}' created successfully."
# Check if index.html was created and add 8080 server info (only in root workspace)
if file_path.lower() == "index.html":
try:
website_link = self.sandbox.get_preview_link(8080)
website_url = (
website_link.url
if hasattr(website_link, "url")
else str(website_link).split("url='")[1].split("'")[0]
)
message += f"\n\n[Auto-detected index.html - HTTP server available at: {website_url}]"
message += "\n[Note: Use the provided HTTP server URL above instead of starting a new server]"
except Exception as e:
logger.warning(
f"Failed to get website URL for index.html: {str(e)}"
)
return self.success_response(message)
except Exception as e:
return self.fail_response(f"Error creating file: {str(e)}")
async def _str_replace(
self, file_path: str, old_str: str, new_str: str
) -> ToolResult:
"""Replace specific text in a file"""
try:
# Ensure sandbox is initialized
await self._ensure_sandbox()
file_path = self.clean_path(file_path)
full_path = f"{self.workspace_path}/{file_path}"
if not self._file_exists(full_path):
return self.fail_response(f"File '{file_path}' does not exist")
content = self.sandbox.fs.download_file(full_path).decode()
old_str = old_str.expandtabs()
new_str = new_str.expandtabs()
occurrences = content.count(old_str)
if occurrences == 0:
return self.fail_response(f"String '{old_str}' not found in file")
if occurrences > 1:
lines = [
i + 1
for i, line in enumerate(content.split("\n"))
if old_str in line
]
return self.fail_response(
f"Multiple occurrences found in lines {lines}. Please ensure string is unique"
)
# Perform replacement
new_content = content.replace(old_str, new_str)
self.sandbox.fs.upload_file(new_content.encode(), full_path)
# Show snippet around the edit
replacement_line = content.split(old_str)[0].count("\n")
start_line = max(0, replacement_line - self.SNIPPET_LINES)
end_line = replacement_line + self.SNIPPET_LINES + new_str.count("\n")
snippet = "\n".join(new_content.split("\n")[start_line : end_line + 1])
message = f"Replacement successful."
return self.success_response(message)
except Exception as e:
return self.fail_response(f"Error replacing string: {str(e)}")
async def _full_file_rewrite(
self, file_path: str, file_contents: str, permissions: str = "644"
) -> ToolResult:
"""Completely rewrite an existing file with new content"""
try:
# Ensure sandbox is initialized
await self._ensure_sandbox()
file_path = self.clean_path(file_path)
full_path = f"{self.workspace_path}/{file_path}"
if not self._file_exists(full_path):
return self.fail_response(
f"File '{file_path}' does not exist. Use create_file to create a new file."
)
self.sandbox.fs.upload_file(file_contents.encode(), full_path)
self.sandbox.fs.set_file_permissions(full_path, permissions)
message = f"File '{file_path}' completely rewritten successfully."
# Check if index.html was rewritten and add 8080 server info (only in root workspace)
if file_path.lower() == "index.html":
try:
website_link = self.sandbox.get_preview_link(8080)
website_url = (
website_link.url
if hasattr(website_link, "url")
else str(website_link).split("url='")[1].split("'")[0]
)
message += f"\n\n[Auto-detected index.html - HTTP server available at: {website_url}]"
message += "\n[Note: Use the provided HTTP server URL above instead of starting a new server]"
except Exception as e:
logger.warning(
f"Failed to get website URL for index.html: {str(e)}"
)
return self.success_response(message)
except Exception as e:
return self.fail_response(f"Error rewriting file: {str(e)}")
async def _delete_file(self, file_path: str) -> ToolResult:
"""Delete a file at the given path"""
try:
# Ensure sandbox is initialized
await self._ensure_sandbox()
file_path = self.clean_path(file_path)
full_path = f"{self.workspace_path}/{file_path}"
if not self._file_exists(full_path):
return self.fail_response(f"File '{file_path}' does not exist")
self.sandbox.fs.delete_file(full_path)
return self.success_response(f"File '{file_path}' deleted successfully.")
except Exception as e:
return self.fail_response(f"Error deleting file: {str(e)}")
async def cleanup(self):
"""Clean up sandbox resources."""
@classmethod
def create_with_context(cls, context: Context) -> "SandboxFilesTool[Context]":
"""Factory method to create a SandboxFilesTool with a specific context."""
raise NotImplementedError(
"create_with_context not implemented for SandboxFilesTool"
)
+419
View File
@@ -0,0 +1,419 @@
import asyncio
import time
from typing import Any, Dict, Optional, TypeVar
from uuid import uuid4
from app.daytona.tool_base import Sandbox, SandboxToolsBase
from app.tool.base import ToolResult
from app.utils.logger import logger
Context = TypeVar("Context")
_SHELL_DESCRIPTION = """\
Execute a shell command in the workspace directory.
IMPORTANT: Commands are non-blocking by default and run in a tmux session.
This is ideal for long-running operations like starting servers or build processes.
Uses sessions to maintain state between commands.
This tool is essential for running CLI tools, installing packages, and managing system operations.
"""
class SandboxShellTool(SandboxToolsBase):
"""Tool for executing tasks in a Daytona sandbox with browser-use capabilities.
Uses sessions for maintaining state between commands and provides comprehensive process management.
"""
name: str = "sandbox_shell"
description: str = _SHELL_DESCRIPTION
parameters: dict = {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"execute_command",
"check_command_output",
"terminate_command",
"list_commands",
],
"description": "The shell action to perform",
},
"command": {
"type": "string",
"description": "The shell command to execute. Use this for running CLI tools, installing packages, "
"or system operations. Commands can be chained using &&, ||, and | operators.",
},
"folder": {
"type": "string",
"description": "Optional relative path to a subdirectory of /workspace where the command should be "
"executed. Example: 'data/pdfs'",
},
"session_name": {
"type": "string",
"description": "Optional name of the tmux session to use. Use named sessions for related commands "
"that need to maintain state. Defaults to a random session name.",
},
"blocking": {
"type": "boolean",
"description": "Whether to wait for the command to complete. Defaults to false for non-blocking "
"execution.",
"default": False,
},
"timeout": {
"type": "integer",
"description": "Optional timeout in seconds for blocking commands. Defaults to 60. Ignored for "
"non-blocking commands.",
"default": 60,
},
"kill_session": {
"type": "boolean",
"description": "Whether to terminate the tmux session after checking. Set to true when you're done "
"with the command.",
"default": False,
},
},
"required": ["action"],
"dependencies": {
"execute_command": ["command"],
"check_command_output": ["session_name"],
"terminate_command": ["session_name"],
"list_commands": [],
},
}
def __init__(
self, sandbox: Optional[Sandbox] = None, thread_id: Optional[str] = None, **data
):
"""Initialize with optional sandbox and thread_id."""
super().__init__(**data)
if sandbox is not None:
self._sandbox = sandbox
async def _ensure_session(self, session_name: str = "default") -> str:
"""Ensure a session exists and return its ID."""
if session_name not in self._sessions:
session_id = str(uuid4())
try:
await self._ensure_sandbox() # Ensure sandbox is initialized
self.sandbox.process.create_session(session_id)
self._sessions[session_name] = session_id
except Exception as e:
raise RuntimeError(f"Failed to create session: {str(e)}")
return self._sessions[session_name]
async def _cleanup_session(self, session_name: str):
"""Clean up a session if it exists."""
if session_name in self._sessions:
try:
await self._ensure_sandbox() # Ensure sandbox is initialized
self.sandbox.process.delete_session(self._sessions[session_name])
del self._sessions[session_name]
except Exception as e:
print(f"Warning: Failed to cleanup session {session_name}: {str(e)}")
async def _execute_raw_command(self, command: str) -> Dict[str, Any]:
"""Execute a raw command directly in the sandbox."""
# Ensure session exists for raw commands
session_id = await self._ensure_session("raw_commands")
# Execute command in session
from app.daytona.sandbox import SessionExecuteRequest
req = SessionExecuteRequest(
command=command, run_async=False, cwd=self.workspace_path
)
response = self.sandbox.process.execute_session_command(
session_id=session_id,
req=req,
timeout=30, # Short timeout for utility commands
)
logs = self.sandbox.process.get_session_command_logs(
session_id=session_id, command_id=response.cmd_id
)
return {"output": logs, "exit_code": response.exit_code}
async def _execute_command(
self,
command: str,
folder: Optional[str] = None,
session_name: Optional[str] = None,
blocking: bool = False,
timeout: int = 60,
) -> ToolResult:
try:
# Ensure sandbox is initialized
await self._ensure_sandbox()
# Set up working directory
cwd = self.workspace_path
if folder:
folder = folder.strip("/")
cwd = f"{self.workspace_path}/{folder}"
# Generate a session name if not provided
if not session_name:
session_name = f"session_{str(uuid4())[:8]}"
# Check if tmux session already exists
check_session = await self._execute_raw_command(
f"tmux has-session -t {session_name} 2>/dev/null || echo 'not_exists'"
)
session_exists = "not_exists" not in check_session.get("output", "")
if not session_exists:
# Create a new tmux session
await self._execute_raw_command(
f"tmux new-session -d -s {session_name}"
)
# Ensure we're in the correct directory and send command to tmux
full_command = f"cd {cwd} && {command}"
wrapped_command = full_command.replace('"', '\\"') # Escape double quotes
# Send command to tmux session
await self._execute_raw_command(
f'tmux send-keys -t {session_name} "{wrapped_command}" Enter'
)
if blocking:
# For blocking execution, wait and capture output
start_time = time.time()
while (time.time() - start_time) < timeout:
# Wait a bit before checking
time.sleep(2)
# Check if session still exists (command might have exited)
check_result = await self._execute_raw_command(
f"tmux has-session -t {session_name} 2>/dev/null || echo 'ended'"
)
if "ended" in check_result.get("output", ""):
break
# Get current output and check for common completion indicators
output_result = await self._execute_raw_command(
f"tmux capture-pane -t {session_name} -p -S - -E -"
)
current_output = output_result.get("output", "")
# Check for prompt indicators that suggest command completion
last_lines = current_output.split("\n")[-3:]
completion_indicators = [
"$",
"#",
">",
"Done",
"Completed",
"Finished",
"",
]
if any(
indicator in line
for indicator in completion_indicators
for line in last_lines
):
break
# Capture final output
output_result = await self._execute_raw_command(
f"tmux capture-pane -t {session_name} -p -S - -E -"
)
final_output = output_result.get("output", "")
# Kill the session after capture
await self._execute_raw_command(f"tmux kill-session -t {session_name}")
return self.success_response(
{
"output": final_output,
"session_name": session_name,
"cwd": cwd,
"completed": True,
}
)
else:
# For non-blocking, just return immediately
return self.success_response(
{
"session_name": session_name,
"cwd": cwd,
"message": f"Command sent to tmux session '{session_name}'. Use check_command_output to view results.",
"completed": False,
}
)
except Exception as e:
# Attempt to clean up session in case of error
if session_name:
try:
await self._execute_raw_command(
f"tmux kill-session -t {session_name}"
)
except:
pass
return self.fail_response(f"Error executing command: {str(e)}")
async def _check_command_output(
self, session_name: str, kill_session: bool = False
) -> ToolResult:
try:
# Ensure sandbox is initialized
await self._ensure_sandbox()
# Check if session exists
check_result = await self._execute_raw_command(
f"tmux has-session -t {session_name} 2>/dev/null || echo 'not_exists'"
)
if "not_exists" in check_result.get("output", ""):
return self.fail_response(
f"Tmux session '{session_name}' does not exist."
)
# Get output from tmux pane
output_result = await self._execute_raw_command(
f"tmux capture-pane -t {session_name} -p -S - -E -"
)
output = output_result.get("output", "")
# Kill session if requested
if kill_session:
await self._execute_raw_command(f"tmux kill-session -t {session_name}")
termination_status = "Session terminated."
else:
termination_status = "Session still running."
return self.success_response(
{
"output": output,
"session_name": session_name,
"status": termination_status,
}
)
except Exception as e:
return self.fail_response(f"Error checking command output: {str(e)}")
async def _terminate_command(self, session_name: str) -> ToolResult:
try:
# Ensure sandbox is initialized
await self._ensure_sandbox()
# Check if session exists
check_result = await self._execute_raw_command(
f"tmux has-session -t {session_name} 2>/dev/null || echo 'not_exists'"
)
if "not_exists" in check_result.get("output", ""):
return self.fail_response(
f"Tmux session '{session_name}' does not exist."
)
# Kill the session
await self._execute_raw_command(f"tmux kill-session -t {session_name}")
return self.success_response(
{"message": f"Tmux session '{session_name}' terminated successfully."}
)
except Exception as e:
return self.fail_response(f"Error terminating command: {str(e)}")
async def _list_commands(self) -> ToolResult:
try:
# Ensure sandbox is initialized
await self._ensure_sandbox()
# List all tmux sessions
result = await self._execute_raw_command(
"tmux list-sessions 2>/dev/null || echo 'No sessions'"
)
output = result.get("output", "")
if "No sessions" in output or not output.strip():
return self.success_response(
{"message": "No active tmux sessions found.", "sessions": []}
)
# Parse session list
sessions = []
for line in output.split("\n"):
if line.strip():
parts = line.split(":")
if parts:
session_name = parts[0].strip()
sessions.append(session_name)
return self.success_response(
{
"message": f"Found {len(sessions)} active sessions.",
"sessions": sessions,
}
)
except Exception as e:
return self.fail_response(f"Error listing commands: {str(e)}")
async def execute(
self,
action: str,
command: str,
folder: Optional[str] = None,
session_name: Optional[str] = None,
blocking: bool = False,
timeout: int = 60,
kill_session: bool = False,
) -> ToolResult:
"""
Execute a browser action in the sandbox environment.
Args:
timeout:
blocking:
session_name:
folder:
command:
kill_session:
action: The browser action to perform
Returns:
ToolResult with the action's output or error
"""
async with asyncio.Lock():
try:
# Navigation actions
if action == "execute_command":
if not command:
return self.fail_response("command is required for navigation")
return await self._execute_command(
command, folder, session_name, blocking, timeout
)
elif action == "check_command_output":
if session_name is None:
return self.fail_response(
"session_name is required for navigation"
)
return await self._check_command_output(session_name, kill_session)
elif action == "terminate_command":
if session_name is None:
return self.fail_response(
"session_name is required for click_element"
)
return await self._terminate_command(session_name)
elif action == "list_commands":
return await self._list_commands()
else:
return self.fail_response(f"Unknown action: {action}")
except Exception as e:
logger.error(f"Error executing shell action: {e}")
return self.fail_response(f"Error executing shell action: {e}")
async def cleanup(self):
"""Clean up all sessions."""
for session_name in list(self._sessions.keys()):
await self._cleanup_session(session_name)
# Also clean up any tmux sessions
try:
await self._ensure_sandbox()
await self._execute_raw_command("tmux kill-server 2>/dev/null || true")
except Exception as e:
logger.error(f"Error shell box cleanup action: {e}")
+178
View File
@@ -0,0 +1,178 @@
import base64
import mimetypes
import os
from io import BytesIO
from typing import Optional
from PIL import Image
from pydantic import Field
from app.daytona.tool_base import Sandbox, SandboxToolsBase, ThreadMessage
from app.tool.base import ToolResult
# 最大文件大小(原图10MB,压缩后5MB)
MAX_IMAGE_SIZE = 10 * 1024 * 1024
MAX_COMPRESSED_SIZE = 5 * 1024 * 1024
# 压缩设置
DEFAULT_MAX_WIDTH = 1920
DEFAULT_MAX_HEIGHT = 1080
DEFAULT_JPEG_QUALITY = 85
DEFAULT_PNG_COMPRESS_LEVEL = 6
_VISION_DESCRIPTION = """
A sandbox-based vision tool that allows the agent to read image files inside the sandbox using the see_image action.
* Only the see_image action is supported, with the parameter being the relative path of the image under /workspace.
* The image will be compressed and converted to base64 for use in subsequent context.
* Supported formats: JPG, PNG, GIF, WEBP. Maximum size: 10MB.
"""
class SandboxVisionTool(SandboxToolsBase):
name: str = "sandbox_vision"
description: str = _VISION_DESCRIPTION
parameters: dict = {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["see_image"],
"description": "要执行的视觉动作,目前仅支持 see_image",
},
"file_path": {
"type": "string",
"description": "图片在 /workspace 下的相对路径,如 'screenshots/image.png'",
},
},
"required": ["action", "file_path"],
"dependencies": {"see_image": ["file_path"]},
}
# def __init__(self, project_id: str, thread_id: str, thread_manager: ThreadManager):
# super().__init__(project_id=project_id, thread_manager=thread_manager)
# self.thread_id = thread_id
# self.thread_manager = thread_manager
vision_message: Optional[ThreadMessage] = Field(default=None, exclude=True)
def __init__(
self, sandbox: Optional[Sandbox] = None, thread_id: Optional[str] = None, **data
):
"""Initialize with optional sandbox and thread_id."""
super().__init__(**data)
if sandbox is not None:
self._sandbox = sandbox
def compress_image(self, image_bytes: bytes, mime_type: str, file_path: str):
"""压缩图片,保持合理质量。"""
try:
img = Image.open(BytesIO(image_bytes))
if img.mode in ("RGBA", "LA", "P"):
background = Image.new("RGB", img.size, (255, 255, 255))
if img.mode == "P":
img = img.convert("RGBA")
background.paste(
img, mask=img.split()[-1] if img.mode == "RGBA" else None
)
img = background
width, height = img.size
if width > DEFAULT_MAX_WIDTH or height > DEFAULT_MAX_HEIGHT:
ratio = min(DEFAULT_MAX_WIDTH / width, DEFAULT_MAX_HEIGHT / height)
new_width = int(width * ratio)
new_height = int(height * ratio)
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
output = BytesIO()
if mime_type == "image/gif":
img.save(output, format="GIF", optimize=True)
output_mime = "image/gif"
elif mime_type == "image/png":
img.save(
output,
format="PNG",
optimize=True,
compress_level=DEFAULT_PNG_COMPRESS_LEVEL,
)
output_mime = "image/png"
else:
img.save(
output, format="JPEG", quality=DEFAULT_JPEG_QUALITY, optimize=True
)
output_mime = "image/jpeg"
compressed_bytes = output.getvalue()
return compressed_bytes, output_mime
except Exception:
return image_bytes, mime_type
async def execute(
self, action: str, file_path: Optional[str] = None, **kwargs
) -> ToolResult:
"""
执行视觉动作,目前仅支持 see_image。
参数:
action: 必须为 'see_image'
file_path: 图片相对路径
"""
if action != "see_image":
return self.fail_response(f"未知的视觉动作: {action}")
if not file_path:
return self.fail_response("file_path 参数不能为空")
try:
await self._ensure_sandbox()
cleaned_path = self.clean_path(file_path)
full_path = f"{self.workspace_path}/{cleaned_path}"
try:
file_info = self.sandbox.fs.get_file_info(full_path)
if file_info.is_dir:
return self.fail_response(f"路径 '{cleaned_path}' 是目录,不是图片文件。")
except Exception:
return self.fail_response(f"图片文件未找到: '{cleaned_path}'")
if file_info.size > MAX_IMAGE_SIZE:
return self.fail_response(
f"图片文件 '{cleaned_path}' 过大 ({file_info.size / (1024*1024):.2f}MB),最大允许 {MAX_IMAGE_SIZE / (1024*1024)}MB。"
)
try:
image_bytes = self.sandbox.fs.download_file(full_path)
except Exception:
return self.fail_response(f"无法读取图片文件: {cleaned_path}")
mime_type, _ = mimetypes.guess_type(full_path)
if not mime_type or not mime_type.startswith("image/"):
ext = os.path.splitext(cleaned_path)[1].lower()
if ext == ".jpg" or ext == ".jpeg":
mime_type = "image/jpeg"
elif ext == ".png":
mime_type = "image/png"
elif ext == ".gif":
mime_type = "image/gif"
elif ext == ".webp":
mime_type = "image/webp"
else:
return self.fail_response(
f"不支持或未知的图片格式: '{cleaned_path}'。支持: JPG, PNG, GIF, WEBP。"
)
compressed_bytes, compressed_mime_type = self.compress_image(
image_bytes, mime_type, cleaned_path
)
if len(compressed_bytes) > MAX_COMPRESSED_SIZE:
return self.fail_response(
f"图片文件 '{cleaned_path}' 压缩后仍过大 ({len(compressed_bytes) / (1024*1024):.2f}MB),最大允许 {MAX_COMPRESSED_SIZE / (1024*1024)}MB。"
)
base64_image = base64.b64encode(compressed_bytes).decode("utf-8")
image_context_data = {
"mime_type": compressed_mime_type,
"base64": base64_image,
"file_path": cleaned_path,
"original_size": file_info.size,
"compressed_size": len(compressed_bytes),
}
message = ThreadMessage(
type="image_context", content=image_context_data, is_llm_message=False
)
self.vision_message = message
# return self.success_response(f"成功加载并压缩图片 '{cleaned_path}' (由 {file_info.size / 1024:.1f}KB 压缩到 {len(compressed_bytes) / 1024:.1f}KB)。")
return ToolResult(
output=f"成功加载并压缩图片 '{cleaned_path}'",
base64_image=base64_image,
)
except Exception as e:
return self.fail_response(f"see_image 执行异常: {str(e)}")
+49 -4
View File
@@ -1,9 +1,54 @@
from typing import List
from baidusearch.baidusearch import search
from app.tool.search.base import WebSearchEngine
from app.tool.search.base import SearchItem, WebSearchEngine
class BaiduSearchEngine(WebSearchEngine):
def perform_search(self, query, num_results=10, *args, **kwargs):
"""Baidu search engine."""
return search(query, num_results=num_results)
def perform_search(
self, query: str, num_results: int = 10, *args, **kwargs
) -> List[SearchItem]:
"""
Baidu search engine.
Returns results formatted according to SearchItem model.
"""
raw_results = search(query, num_results=num_results)
# Convert raw results to SearchItem format
results = []
for i, item in enumerate(raw_results):
if isinstance(item, str):
# If it's just a URL
results.append(
SearchItem(title=f"Baidu Result {i+1}", url=item, description=None)
)
elif isinstance(item, dict):
# If it's a dictionary with details
results.append(
SearchItem(
title=item.get("title", f"Baidu Result {i+1}"),
url=item.get("url", ""),
description=item.get("abstract", None),
)
)
else:
# Try to get attributes directly
try:
results.append(
SearchItem(
title=getattr(item, "title", f"Baidu Result {i+1}"),
url=getattr(item, "url", ""),
description=getattr(item, "abstract", None),
)
)
except Exception:
# Fallback to a basic result
results.append(
SearchItem(
title=f"Baidu Result {i+1}", url=str(item), description=None
)
)
return results
+27 -4
View File
@@ -1,9 +1,32 @@
class WebSearchEngine(object):
from typing import List, Optional
from pydantic import BaseModel, Field
class SearchItem(BaseModel):
"""Represents a single search result item"""
title: str = Field(description="The title of the search result")
url: str = Field(description="The URL of the search result")
description: Optional[str] = Field(
default=None, description="A description or snippet of the search result"
)
def __str__(self) -> str:
"""String representation of a search result item."""
return f"{self.title} - {self.url}"
class WebSearchEngine(BaseModel):
"""Base class for web search engines."""
model_config = {"arbitrary_types_allowed": True}
def perform_search(
self, query: str, num_results: int = 10, *args, **kwargs
) -> list[dict]:
) -> List[SearchItem]:
"""
Perform a web search and return a list of URLs.
Perform a web search and return a list of search items.
Args:
query (str): The search query to submit to the search engine.
@@ -12,6 +35,6 @@ class WebSearchEngine(object):
kwargs: Additional keyword arguments.
Returns:
List: A list of dict matching the search query.
List[SearchItem]: A list of SearchItem objects matching the search query.
"""
raise NotImplementedError
+29 -31
View File
@@ -1,10 +1,10 @@
from typing import List
from typing import List, Optional, Tuple
import requests
from bs4 import BeautifulSoup
from app.logger import logger
from app.tool.search.base import WebSearchEngine
from app.tool.search.base import SearchItem, WebSearchEngine
ABSTRACT_MAX_LENGTH = 300
@@ -36,7 +36,7 @@ BING_SEARCH_URL = "https://www.bing.com/search?q="
class BingSearchEngine(WebSearchEngine):
session: requests.Session = None
session: Optional[requests.Session] = None
def __init__(self, **data):
"""Initialize the BingSearch tool with a requests session."""
@@ -44,21 +44,16 @@ class BingSearchEngine(WebSearchEngine):
self.session = requests.Session()
self.session.headers.update(HEADERS)
def _search_sync(self, query: str, num_results: int = 10) -> List[str]:
def _search_sync(self, query: str, num_results: int = 10) -> List[SearchItem]:
"""
Synchronous Bing search implementation to retrieve a list of URLs matching a query.
Synchronous Bing search implementation to retrieve search results.
Args:
query (str): The search query to submit to Bing. Must not be empty.
num_results (int, optional): The maximum number of URLs to return. Defaults to 10.
query (str): The search query to submit to Bing.
num_results (int, optional): Maximum number of results to return. Defaults to 10.
Returns:
List[str]: A list of URLs from the search results, capped at `num_results`.
Returns an empty list if the query is empty or no results are found.
Notes:
- Pagination is handled by incrementing the `first` parameter and following `next_url` links.
- If fewer results than `num_results` are available, all found URLs are returned.
List[SearchItem]: A list of search items with title, URL, and description.
"""
if not query:
return []
@@ -72,25 +67,21 @@ class BingSearchEngine(WebSearchEngine):
next_url, rank_start=len(list_result), first=first
)
if data:
list_result.extend([item["url"] for item in data])
list_result.extend(data)
if not next_url:
break
first += 10
return list_result[:num_results]
def _parse_html(self, url: str, rank_start: int = 0, first: int = 1) -> tuple:
def _parse_html(
self, url: str, rank_start: int = 0, first: int = 1
) -> Tuple[List[SearchItem], str]:
"""
Parse Bing search result HTML synchronously to extract search results and the next page URL.
Parse Bing search result HTML to extract search results and the next page URL.
Args:
url (str): The URL of the Bing search results page to parse.
rank_start (int, optional): The starting rank for numbering the search results. Defaults to 0.
first (int, optional): Unused parameter (possibly legacy). Defaults to 1.
Returns:
tuple: A tuple containing:
- list: A list of dictionaries with keys 'title', 'abstract', 'url', and 'rank' for each result.
- str or None: The URL of the next results page, or None if there is no next page.
tuple: (List of SearchItem objects, next page URL or None)
"""
try:
res = self.session.get(url=url)
@@ -120,13 +111,14 @@ class BingSearchEngine(WebSearchEngine):
abstract = abstract[:ABSTRACT_MAX_LENGTH]
rank_start += 1
# Create a SearchItem object
list_data.append(
{
"title": title,
"abstract": abstract,
"url": url,
"rank": rank_start,
}
SearchItem(
title=title or f"Bing Result {rank_start}",
url=url,
description=abstract,
)
)
except Exception:
continue
@@ -141,6 +133,12 @@ class BingSearchEngine(WebSearchEngine):
logger.warning(f"Error parsing HTML: {e}")
return [], None
def perform_search(self, query, num_results=10, *args, **kwargs):
"""Bing search engine."""
def perform_search(
self, query: str, num_results: int = 10, *args, **kwargs
) -> List[SearchItem]:
"""
Bing search engine.
Returns results formatted according to SearchItem model.
"""
return self._search_sync(query, num_results=num_results)
+52 -4
View File
@@ -1,9 +1,57 @@
from typing import List
from duckduckgo_search import DDGS
from app.tool.search.base import WebSearchEngine
from app.tool.search.base import SearchItem, WebSearchEngine
class DuckDuckGoSearchEngine(WebSearchEngine):
async def perform_search(self, query, num_results=10, *args, **kwargs):
"""DuckDuckGo search engine."""
return DDGS.text(query, num_results=num_results)
def perform_search(
self, query: str, num_results: int = 10, *args, **kwargs
) -> List[SearchItem]:
"""
DuckDuckGo search engine.
Returns results formatted according to SearchItem model.
"""
raw_results = DDGS().text(query, max_results=num_results)
results = []
for i, item in enumerate(raw_results):
if isinstance(item, str):
# If it's just a URL
results.append(
SearchItem(
title=f"DuckDuckGo Result {i + 1}", url=item, description=None
)
)
elif isinstance(item, dict):
# Extract data from the dictionary
results.append(
SearchItem(
title=item.get("title", f"DuckDuckGo Result {i + 1}"),
url=item.get("href", ""),
description=item.get("body", None),
)
)
else:
# Try to extract attributes directly
try:
results.append(
SearchItem(
title=getattr(item, "title", f"DuckDuckGo Result {i + 1}"),
url=getattr(item, "href", ""),
description=getattr(item, "body", None),
)
)
except Exception:
# Fallback
results.append(
SearchItem(
title=f"DuckDuckGo Result {i + 1}",
url=str(item),
description=None,
)
)
return results
+28 -4
View File
@@ -1,9 +1,33 @@
from typing import List
from googlesearch import search
from app.tool.search.base import WebSearchEngine
from app.tool.search.base import SearchItem, WebSearchEngine
class GoogleSearchEngine(WebSearchEngine):
def perform_search(self, query, num_results=10, *args, **kwargs):
"""Google search engine."""
return search(query, num_results=num_results)
def perform_search(
self, query: str, num_results: int = 10, *args, **kwargs
) -> List[SearchItem]:
"""
Google search engine.
Returns results formatted according to SearchItem model.
"""
raw_results = search(query, num_results=num_results, advanced=True)
results = []
for i, item in enumerate(raw_results):
if isinstance(item, str):
# If it's just a URL
results.append(
{"title": f"Google Result {i+1}", "url": item, "description": ""}
)
else:
results.append(
SearchItem(
title=item.title, url=item.url, description=item.description
)
)
return results
-182
View File
@@ -1,182 +0,0 @@
import asyncio
import os
import shlex
from typing import Optional
from app.tool.base import BaseTool, CLIResult
class Terminal(BaseTool):
name: str = "execute_command"
description: str = """Request to execute a CLI command on the system.
Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task.
You must tailor your command to the user's system and provide a clear explanation of what the command does.
Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run.
Commands will be executed in the current working directory.
Note: You MUST append a `sleep 0.05` to the end of the command for commands that will complete in under 50ms, as this will circumvent a known issue with the terminal tool where it will sometimes not return the output when the command completes too quickly.
"""
parameters: dict = {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "(required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.",
}
},
"required": ["command"],
}
process: Optional[asyncio.subprocess.Process] = None
current_path: str = os.getcwd()
lock: asyncio.Lock = asyncio.Lock()
async def execute(self, command: str) -> CLIResult:
"""
Execute a terminal command asynchronously with persistent context.
Args:
command (str): The terminal command to execute.
Returns:
str: The output, and error of the command execution.
"""
# Split the command by & to handle multiple commands
commands = [cmd.strip() for cmd in command.split("&") if cmd.strip()]
final_output = CLIResult(output="", error="")
for cmd in commands:
sanitized_command = self._sanitize_command(cmd)
# Handle 'cd' command internally
if sanitized_command.lstrip().startswith("cd "):
result = await self._handle_cd_command(sanitized_command)
else:
async with self.lock:
try:
self.process = await asyncio.create_subprocess_shell(
sanitized_command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=self.current_path,
)
stdout, stderr = await self.process.communicate()
result = CLIResult(
output=stdout.decode().strip(),
error=stderr.decode().strip(),
)
except Exception as e:
result = CLIResult(output="", error=str(e))
finally:
self.process = None
# Combine outputs
if result.output:
final_output.output += (
(result.output + "\n") if final_output.output else result.output
)
if result.error:
final_output.error += (
(result.error + "\n") if final_output.error else result.error
)
# Remove trailing newlines
final_output.output = final_output.output.rstrip()
final_output.error = final_output.error.rstrip()
return final_output
async def execute_in_env(self, env_name: str, command: str) -> CLIResult:
"""
Execute a terminal command asynchronously within a specified Conda environment.
Args:
env_name (str): The name of the Conda environment.
command (str): The terminal command to execute within the environment.
Returns:
str: The output, and error of the command execution.
"""
sanitized_command = self._sanitize_command(command)
# Construct the command to run within the Conda environment
# Using 'conda run -n env_name command' to execute without activating
conda_command = f"conda run -n {shlex.quote(env_name)} {sanitized_command}"
return await self.execute(conda_command)
async def _handle_cd_command(self, command: str) -> CLIResult:
"""
Handle 'cd' commands to change the current path.
Args:
command (str): The 'cd' command to process.
Returns:
TerminalOutput: The result of the 'cd' command.
"""
try:
parts = shlex.split(command)
if len(parts) < 2:
new_path = os.path.expanduser("~")
else:
new_path = os.path.expanduser(parts[1])
# Handle relative paths
if not os.path.isabs(new_path):
new_path = os.path.join(self.current_path, new_path)
new_path = os.path.abspath(new_path)
if os.path.isdir(new_path):
self.current_path = new_path
return CLIResult(
output=f"Changed directory to {self.current_path}", error=""
)
else:
return CLIResult(output="", error=f"No such directory: {new_path}")
except Exception as e:
return CLIResult(output="", error=str(e))
@staticmethod
def _sanitize_command(command: str) -> str:
"""
Sanitize the command for safe execution.
Args:
command (str): The command to sanitize.
Returns:
str: The sanitized command.
"""
# Example sanitization: restrict certain dangerous commands
dangerous_commands = ["rm", "sudo", "shutdown", "reboot"]
try:
parts = shlex.split(command)
if any(cmd in dangerous_commands for cmd in parts):
raise ValueError("Use of dangerous commands is restricted.")
except Exception:
# If shlex.split fails, try basic string comparison
if any(cmd in command for cmd in dangerous_commands):
raise ValueError("Use of dangerous commands is restricted.")
# Additional sanitization logic can be added here
return command
async def close(self):
"""Close the persistent shell process if it exists."""
async with self.lock:
if self.process:
self.process.terminate()
try:
await asyncio.wait_for(self.process.wait(), timeout=5)
except asyncio.TimeoutError:
self.process.kill()
await self.process.wait()
finally:
self.process = None
async def __aenter__(self):
"""Enter the asynchronous context manager."""
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Exit the asynchronous context manager and close the process."""
await self.close()
+13
View File
@@ -2,6 +2,7 @@
from typing import Any, Dict, List
from app.exceptions import ToolError
from app.logger import logger
from app.tool.base import BaseTool, ToolFailure, ToolResult
@@ -48,11 +49,23 @@ class ToolCollection:
return self.tool_map.get(name)
def add_tool(self, tool: BaseTool):
"""Add a single tool to the collection.
If a tool with the same name already exists, it will be skipped and a warning will be logged.
"""
if tool.name in self.tool_map:
logger.warning(f"Tool {tool.name} already exists in collection, skipping")
return self
self.tools += (tool,)
self.tool_map[tool.name] = tool
return self
def add_tools(self, *tools: BaseTool):
"""Add multiple tools to the collection.
If any tool has a name conflict with an existing tool, it will be skipped and a warning will be logged.
"""
for tool in tools:
self.add_tool(tool)
return self
+357 -40
View File
@@ -1,10 +1,14 @@
import asyncio
from typing import List
from typing import Any, Dict, List, Optional
import requests
from bs4 import BeautifulSoup
from pydantic import BaseModel, ConfigDict, Field, model_validator
from tenacity import retry, stop_after_attempt, wait_exponential
from app.config import config
from app.tool.base import BaseTool
from app.logger import logger
from app.tool.base import BaseTool, ToolResult
from app.tool.search import (
BaiduSearchEngine,
BingSearchEngine,
@@ -12,13 +16,150 @@ from app.tool.search import (
GoogleSearchEngine,
WebSearchEngine,
)
from app.tool.search.base import SearchItem
class SearchResult(BaseModel):
"""Represents a single search result returned by a search engine."""
model_config = ConfigDict(arbitrary_types_allowed=True)
position: int = Field(description="Position in search results")
url: str = Field(description="URL of the search result")
title: str = Field(default="", description="Title of the search result")
description: str = Field(
default="", description="Description or snippet of the search result"
)
source: str = Field(description="The search engine that provided this result")
raw_content: Optional[str] = Field(
default=None, description="Raw content from the search result page if available"
)
def __str__(self) -> str:
"""String representation of a search result."""
return f"{self.title} ({self.url})"
class SearchMetadata(BaseModel):
"""Metadata about the search operation."""
model_config = ConfigDict(arbitrary_types_allowed=True)
total_results: int = Field(description="Total number of results found")
language: str = Field(description="Language code used for the search")
country: str = Field(description="Country code used for the search")
class SearchResponse(ToolResult):
"""Structured response from the web search tool, inheriting ToolResult."""
query: str = Field(description="The search query that was executed")
results: List[SearchResult] = Field(
default_factory=list, description="List of search results"
)
metadata: Optional[SearchMetadata] = Field(
default=None, description="Metadata about the search"
)
@model_validator(mode="after")
def populate_output(self) -> "SearchResponse":
"""Populate output or error fields based on search results."""
if self.error:
return self
result_text = [f"Search results for '{self.query}':"]
for i, result in enumerate(self.results, 1):
# Add title with position number
title = result.title.strip() or "No title"
result_text.append(f"\n{i}. {title}")
# Add URL with proper indentation
result_text.append(f" URL: {result.url}")
# Add description if available
if result.description.strip():
result_text.append(f" Description: {result.description}")
# Add content preview if available
if result.raw_content:
content_preview = result.raw_content[:1000].replace("\n", " ").strip()
if len(result.raw_content) > 1000:
content_preview += "..."
result_text.append(f" Content: {content_preview}")
# Add metadata at the bottom if available
if self.metadata:
result_text.extend(
[
f"\nMetadata:",
f"- Total results: {self.metadata.total_results}",
f"- Language: {self.metadata.language}",
f"- Country: {self.metadata.country}",
]
)
self.output = "\n".join(result_text)
return self
class WebContentFetcher:
"""Utility class for fetching web content."""
@staticmethod
async def fetch_content(url: str, timeout: int = 10) -> Optional[str]:
"""
Fetch and extract the main content from a webpage.
Args:
url: The URL to fetch content from
timeout: Request timeout in seconds
Returns:
Extracted text content or None if fetching fails
"""
headers = {
"WebSearch": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
try:
# Use asyncio to run requests in a thread pool
response = await asyncio.get_event_loop().run_in_executor(
None, lambda: requests.get(url, headers=headers, timeout=timeout)
)
if response.status_code != 200:
logger.warning(
f"Failed to fetch content from {url}: HTTP {response.status_code}"
)
return None
# Parse HTML with BeautifulSoup
soup = BeautifulSoup(response.text, "html.parser")
# Remove script and style elements
for script in soup(["script", "style", "header", "footer", "nav"]):
script.extract()
# Get text content
text = soup.get_text(separator="\n", strip=True)
# Clean up whitespace and limit size (100KB max)
text = " ".join(text.split())
return text[:10000] if text else None
except Exception as e:
logger.warning(f"Error fetching content from {url}: {e}")
return None
class WebSearch(BaseTool):
"""Search the web for information using various search engines."""
name: str = "web_search"
description: str = """Perform a web search and return a list of relevant links.
This function attempts to use the primary search engine API to get up-to-date results.
If an error occurs, it falls back to an alternative search engine."""
description: str = """Search the web for real-time information about any topic.
This tool returns comprehensive search results with relevant information, URLs, titles, and descriptions.
If the primary search engine fails, it automatically falls back to alternative engines."""
parameters: dict = {
"type": "object",
"properties": {
@@ -28,8 +169,23 @@ class WebSearch(BaseTool):
},
"num_results": {
"type": "integer",
"description": "(optional) The number of search results to return. Default is 10.",
"default": 10,
"description": "(optional) The number of search results to return. Default is 5.",
"default": 5,
},
"lang": {
"type": "string",
"description": "(optional) Language code for search results (default: en).",
"default": "en",
},
"country": {
"type": "string",
"description": "(optional) Country code for search results (default: us).",
"default": "us",
},
"fetch_content": {
"type": "boolean",
"description": "(optional) Whether to fetch full content from result pages. Default is false.",
"default": False,
},
},
"required": ["query"],
@@ -40,62 +196,223 @@ class WebSearch(BaseTool):
"duckduckgo": DuckDuckGoSearchEngine(),
"bing": BingSearchEngine(),
}
content_fetcher: WebContentFetcher = WebContentFetcher()
async def execute(self, query: str, num_results: int = 10) -> List[str]:
async def execute(
self,
query: str,
num_results: int = 5,
lang: Optional[str] = None,
country: Optional[str] = None,
fetch_content: bool = False,
) -> SearchResponse:
"""
Execute a Web search and return a list of URLs.
Execute a Web search and return detailed search results.
Args:
query (str): The search query to submit to the search engine.
num_results (int, optional): The number of search results to return. Default is 10.
query: The search query to submit to the search engine
num_results: The number of search results to return (default: 5)
lang: Language code for search results (default from config)
country: Country code for search results (default from config)
fetch_content: Whether to fetch content from result pages (default: False)
Returns:
List[str]: A list of URLs matching the search query.
A structured response containing search results and metadata
"""
# Get settings from config
retry_delay = (
getattr(config.search_config, "retry_delay", 60)
if config.search_config
else 60
)
max_retries = (
getattr(config.search_config, "max_retries", 3)
if config.search_config
else 3
)
# Use config values for lang and country if not specified
if lang is None:
lang = (
getattr(config.search_config, "lang", "en")
if config.search_config
else "en"
)
if country is None:
country = (
getattr(config.search_config, "country", "us")
if config.search_config
else "us"
)
search_params = {"lang": lang, "country": country}
# Try searching with retries when all engines fail
for retry_count in range(max_retries + 1):
results = await self._try_all_engines(query, num_results, search_params)
if results:
# Fetch content if requested
if fetch_content:
results = await self._fetch_content_for_results(results)
# Return a successful structured response
return SearchResponse(
status="success",
query=query,
results=results,
metadata=SearchMetadata(
total_results=len(results),
language=lang,
country=country,
),
)
if retry_count < max_retries:
# All engines failed, wait and retry
logger.warning(
f"All search engines failed. Waiting {retry_delay} seconds before retry {retry_count + 1}/{max_retries}..."
)
await asyncio.sleep(retry_delay)
else:
logger.error(
f"All search engines failed after {max_retries} retries. Giving up."
)
# Return an error response
return SearchResponse(
query=query,
error="All search engines failed to return results after multiple retries.",
results=[],
)
async def _try_all_engines(
self, query: str, num_results: int, search_params: Dict[str, Any]
) -> List[SearchResult]:
"""Try all search engines in the configured order."""
engine_order = self._get_engine_order()
failed_engines = []
for engine_name in engine_order:
engine = self._search_engine[engine_name]
try:
links = await self._perform_search_with_engine(
engine, query, num_results
logger.info(f"🔎 Attempting search with {engine_name.capitalize()}...")
search_items = await self._perform_search_with_engine(
engine, query, num_results, search_params
)
if not search_items:
continue
if failed_engines:
logger.info(
f"Search successful with {engine_name.capitalize()} after trying: {', '.join(failed_engines)}"
)
if links:
return links
except Exception as e:
print(f"Search engine '{engine_name}' failed with error: {e}")
# Transform search items into structured results
return [
SearchResult(
position=i + 1,
url=item.url,
title=item.title
or f"Result {i+1}", # Ensure we always have a title
description=item.description or "",
source=engine_name,
)
for i, item in enumerate(search_items)
]
if failed_engines:
logger.error(f"All search engines failed: {', '.join(failed_engines)}")
return []
async def _fetch_content_for_results(
self, results: List[SearchResult]
) -> List[SearchResult]:
"""Fetch and add web content to search results."""
if not results:
return []
# Create tasks for each result
tasks = [self._fetch_single_result_content(result) for result in results]
# Type annotation to help type checker
fetched_results = await asyncio.gather(*tasks)
# Explicit validation of return type
return [
(
result
if isinstance(result, SearchResult)
else SearchResult(**result.dict())
)
for result in fetched_results
]
async def _fetch_single_result_content(self, result: SearchResult) -> SearchResult:
"""Fetch content for a single search result."""
if result.url:
content = await self.content_fetcher.fetch_content(result.url)
if content:
result.raw_content = content
return result
def _get_engine_order(self) -> List[str]:
"""
Determines the order in which to try search engines.
Preferred engine is first (based on configuration), followed by the remaining engines.
"""Determines the order in which to try search engines."""
preferred = (
getattr(config.search_config, "engine", "google").lower()
if config.search_config
else "google"
)
fallbacks = (
[engine.lower() for engine in config.search_config.fallback_engines]
if config.search_config
and hasattr(config.search_config, "fallback_engines")
else []
)
Returns:
List[str]: Ordered list of search engine names.
"""
preferred = "google"
if config.search_config and config.search_config.engine:
preferred = config.search_config.engine.lower()
# Start with preferred engine, then fallbacks, then remaining engines
engine_order = [preferred] if preferred in self._search_engine else []
engine_order.extend(
[
fb
for fb in fallbacks
if fb in self._search_engine and fb not in engine_order
]
)
engine_order.extend([e for e in self._search_engine if e not in engine_order])
engine_order = []
if preferred in self._search_engine:
engine_order.append(preferred)
for key in self._search_engine:
if key not in engine_order:
engine_order.append(key)
return engine_order
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10)
)
async def _perform_search_with_engine(
self,
engine: WebSearchEngine,
query: str,
num_results: int,
) -> List[str]:
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
None, lambda: list(engine.perform_search(query, num_results=num_results))
search_params: Dict[str, Any],
) -> List[SearchItem]:
"""Execute search with the given engine and parameters."""
return await asyncio.get_event_loop().run_in_executor(
None,
lambda: list(
engine.perform_search(
query,
num_results=num_results,
lang=search_params.get("lang"),
country=search_params.get("country"),
)
),
)
if __name__ == "__main__":
web_search = WebSearch()
search_response = asyncio.run(
web_search.execute(
query="Python programming", fetch_content=True, num_results=1
)
)
print(search_response.to_tool_result())
+1
View File
@@ -0,0 +1 @@
# Utility functions and constants for agent tools
+87
View File
@@ -0,0 +1,87 @@
import os
# Files to exclude from operations
EXCLUDED_FILES = {
".DS_Store",
".gitignore",
"package-lock.json",
"postcss.config.js",
"postcss.config.mjs",
"jsconfig.json",
"components.json",
"tsconfig.tsbuildinfo",
"tsconfig.json",
}
# Directories to exclude from operations
EXCLUDED_DIRS = {"node_modules", ".next", "dist", "build", ".git"}
# File extensions to exclude from operations
EXCLUDED_EXT = {
".ico",
".svg",
".png",
".jpg",
".jpeg",
".gif",
".bmp",
".tiff",
".webp",
".db",
".sql",
}
def should_exclude_file(rel_path: str) -> bool:
"""Check if a file should be excluded based on path, name, or extension
Args:
rel_path: Relative path of the file to check
Returns:
True if the file should be excluded, False otherwise
"""
# Check filename
filename = os.path.basename(rel_path)
if filename in EXCLUDED_FILES:
return True
# Check directory
dir_path = os.path.dirname(rel_path)
if any(excluded in dir_path for excluded in EXCLUDED_DIRS):
return True
# Check extension
_, ext = os.path.splitext(filename)
if ext.lower() in EXCLUDED_EXT:
return True
return False
def clean_path(path: str, workspace_path: str = "/workspace") -> str:
"""Clean and normalize a path to be relative to the workspace
Args:
path: The path to clean
workspace_path: The base workspace path to remove (default: "/workspace")
Returns:
The cleaned path, relative to the workspace
"""
# Remove any leading slash
path = path.lstrip("/")
# Remove workspace prefix if present
if path.startswith(workspace_path.lstrip("/")):
path = path[len(workspace_path.lstrip("/")) :]
# Remove workspace/ prefix if present
if path.startswith("workspace/"):
path = path[9:]
# Remove any remaining leading slash
path = path.lstrip("/")
return path
+32
View File
@@ -0,0 +1,32 @@
import logging
import os
import structlog
ENV_MODE = os.getenv("ENV_MODE", "LOCAL")
renderer = [structlog.processors.JSONRenderer()]
if ENV_MODE.lower() == "local".lower():
renderer = [structlog.dev.ConsoleRenderer()]
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.dict_tracebacks,
structlog.processors.CallsiteParameterAdder(
{
structlog.processors.CallsiteParameter.FILENAME,
structlog.processors.CallsiteParameter.FUNC_NAME,
structlog.processors.CallsiteParameter.LINENO,
}
),
structlog.processors.TimeStamper(fmt="iso"),
structlog.contextvars.merge_contextvars,
*renderer,
],
cache_logger_on_first_use=True,
)
logger: structlog.stdlib.BoundLogger = structlog.get_logger(level=logging.DEBUG)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

+114
View File
@@ -0,0 +1,114 @@
# Global LLM configuration
[llm]
model = "claude-3-7-sonnet-20250219" # The LLM model to use
base_url = "https://api.anthropic.com/v1/" # API endpoint URL
api_key = "YOUR_API_KEY" # Your API key
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness
# [llm] # Amazon Bedrock
# api_type = "aws" # Required
# model = "us.anthropic.claude-3-7-sonnet-20250219-v1:0" # Bedrock supported modelID
# base_url = "bedrock-runtime.us-west-2.amazonaws.com" # Not used now
# max_tokens = 8192
# temperature = 1.0
# api_key = "bear" # Required but not used for Bedrock
# [llm] #AZURE OPENAI:
# api_type= 'azure'
# model = "YOUR_MODEL_NAME" #"gpt-4o-mini"
# base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}"
# api_key = "AZURE API KEY"
# max_tokens = 8096
# temperature = 0.0
# api_version="AZURE API VERSION" #"2024-08-01-preview"
# [llm] #OLLAMA:
# api_type = 'ollama'
# model = "llama3.2"
# base_url = "http://localhost:11434/v1"
# api_key = "ollama"
# max_tokens = 4096
# temperature = 0.0
# Optional configuration for specific LLM models
[llm.vision]
model = "claude-3-7-sonnet-20250219" # The vision model to use
base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model
api_key = "YOUR_API_KEY" # Your API key for vision model
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness for vision model
# [llm.vision] #OLLAMA VISION:
# api_type = 'ollama'
# model = "llama3.2-vision"
# base_url = "http://localhost:11434/v1"
# api_key = "ollama"
# max_tokens = 4096
# temperature = 0.0
# Optional configuration for specific browser configuration
# [browser]
# Whether to run browser in headless mode (default: false)
#headless = false
# Disable browser security features (default: true)
#disable_security = true
# Extra arguments to pass to the browser
#extra_chromium_args = []
# Path to a Chrome instance to use to connect to your normal browser
# e.g. '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
#chrome_instance_path = ""
# Connect to a browser instance via WebSocket
#wss_url = ""
# Connect to a browser instance via CDP
#cdp_url = ""
# Optional configuration, Proxy settings for the browser
# [browser.proxy]
# server = "http://proxy-server:port"
# username = "proxy-username"
# password = "proxy-password"
# Optional configuration, Search settings.
# [search]
# Search engine for agent to use. Default is "Google", can be set to "Baidu" or "DuckDuckGo" or "Bing".
#engine = "Google"
# Fallback engine order. Default is ["DuckDuckGo", "Baidu", "Bing"] - will try in this order after primary engine fails.
#fallback_engines = ["DuckDuckGo", "Baidu", "Bing"]
# Seconds to wait before retrying all engines again when they all fail due to rate limits. Default is 60.
#retry_delay = 60
# Maximum number of times to retry all engines when all fail. Default is 3.
#max_retries = 3
# Language code for search results. Options: "en" (English), "zh" (Chinese), etc.
#lang = "en"
# Country code for search results. Options: "us" (United States), "cn" (China), etc.
#country = "us"
## Sandbox configuration
#[sandbox]
#use_sandbox = false
#image = "python:3.12-slim"
#work_dir = "/workspace"
#memory_limit = "1g" # 512m
#cpu_limit = 2.0
#timeout = 300
#network_enabled = true
# Daytona configuration
[daytona]
daytona_api_key = ""
#daytona_server_url = "https://app.daytona.io/api"
#daytona_target = "us" #Daytona is currently available in the following regions:United States (us)、Europe (eu)
#sandbox_image_name = "whitezxj/sandbox:0.1.0" #If you don't use this default image,sandbox tools may be useless
#sandbox_entrypoint = "/usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf" #If you change this entrypoint,server in sandbox may be useless
#VNC_password = #The password you set to log in sandbox by VNC,it will be 123456 if you don't set
# MCP (Model Context Protocol) configuration
[mcp]
server_reference = "app.mcp.server" # default server module reference
# Optional Runflow configuration
# Your can add additional agents into run-flow workflow to solve different-type tasks.
[runflow]
use_data_analysis_agent = false # The Data Analysi Agent to solve various data analysis tasks
@@ -0,0 +1,16 @@
# Global LLM configuration
[llm]
model = "claude-3-7-sonnet-latest" # The LLM model to use
base_url = "https://api.anthropic.com/v1/" # API endpoint URL
api_key = "YOUR_API_KEY" # Your API key
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness
# Optional configuration for specific LLM models
[llm.vision]
model = "claude-3-7-sonnet-20250219" # The vision model to use
base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model
api_key = "YOUR_API_KEY" # Your API key for vision model
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness for vision model
+18
View File
@@ -0,0 +1,18 @@
# Global LLM configuration
[llm] #AZURE OPENAI:
api_type= 'azure'
model = "gpt-4o-mini" # The LLM model to use
base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}" # API endpoint URL
api_key = "YOUR_API_KEY" # Your API key
max_tokens = 8096 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness
api_version="AZURE API VERSION" #"2024-08-01-preview" # Azure Openai version if AzureOpenai
# Optional configuration for specific LLM models
[llm.vision]
model = "gpt-4o" # The vision model to use
base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}"
api_key = "YOUR_API_KEY" # Your API key for vision model
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness for vision model
+16
View File
@@ -0,0 +1,16 @@
# Global LLM configuration
[llm]
model = "gemini-2.0-flash" # The LLM model to use
base_url = "https://generativelanguage.googleapis.com/v1beta/openai/" # API endpoint URL
api_key = "YOUR_API_KEY" # Your API key
temperature = 0.0 # Controls randomness
max_tokens = 8096 # Maximum number of tokens in the response
# Optional configuration for specific LLM models for Google
[llm.vision]
model = "gemini-2.0-flash-exp" # The vision model to use
base_url = "https://generativelanguage.googleapis.com/v1beta/openai/" # API endpoint URL for vision model
api_key = "YOUR_API_KEY" # Your API key for vision model
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness for vision model
+17
View File
@@ -0,0 +1,17 @@
# Global LLM configuration
[llm] #OLLAMA:
api_type = 'ollama'
model = "llama3.2" # The LLM model to use
base_url = "http://localhost:11434/v1" # API endpoint URL
api_key = "ollama" # Your API key
max_tokens = 4096 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness
[llm.vision] #OLLAMA VISION:
api_type = 'ollama'
model = "llama3.2-vision" # The vision model to use
base_url = "http://localhost:11434/v1" # API endpoint URL for vision model
api_key = "ollama" # Your API key for vision model
max_tokens = 4096 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness for vision model
+17
View File
@@ -0,0 +1,17 @@
# Global LLM configuration
[llm] #PPIO:
api_type = 'ppio'
model = "deepseek/deepseek-v3-0324" # The LLM model to use
base_url = "https://api.ppinfra.com/v3/openai" # API endpoint URL
api_key = "your ppio api key" # Your API key
max_tokens = 16000 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness
[llm.vision] #PPIO VISION:
api_type = 'ppio'
model = "qwen/qwen2.5-vl-72b-instruct" # The vision model to use
base_url = "https://api.ppinfra.com/v3/openai" # API endpoint URL for vision model
api_key = "your ppio api key" # Your API key for vision model
max_tokens = 96000 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness for vision model
+40 -12
View File
@@ -1,15 +1,23 @@
# Global LLM configuration
[llm]
model = "claude-3-7-sonnet-20250219" # The LLM model to use
base_url = "https://api.anthropic.com/v1/" # API endpoint URL
api_key = "YOUR_API_KEY" # Your API key
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness
model = "claude-3-7-sonnet-20250219" # The LLM model to use
base_url = "https://api.anthropic.com/v1/" # API endpoint URL
api_key = "YOUR_API_KEY" # Your API key
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness
# [llm] # Amazon Bedrock
# api_type = "aws" # Required
# model = "us.anthropic.claude-3-7-sonnet-20250219-v1:0" # Bedrock supported modelID
# base_url = "bedrock-runtime.us-west-2.amazonaws.com" # Not used now
# max_tokens = 8192
# temperature = 1.0
# api_key = "bear" # Required but not used for Bedrock
# [llm] #AZURE OPENAI:
# api_type= 'azure'
# model = "YOUR_MODEL_NAME" #"gpt-4o-mini"
# base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPOLYMENT_ID}"
# base_url = "{YOUR_AZURE_ENDPOINT.rstrip('/')}/openai/deployments/{AZURE_DEPLOYMENT_ID}"
# api_key = "AZURE API KEY"
# max_tokens = 8096
# temperature = 0.0
@@ -25,11 +33,11 @@ temperature = 0.0 # Controls randomness
# Optional configuration for specific LLM models
[llm.vision]
model = "claude-3-7-sonnet-20250219" # The vision model to use
base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model
api_key = "YOUR_API_KEY" # Your API key for vision model
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness for vision model
model = "claude-3-7-sonnet-20250219" # The vision model to use
base_url = "https://api.anthropic.com/v1/" # API endpoint URL for vision model
api_key = "YOUR_API_KEY" # Your API key for vision model
max_tokens = 8192 # Maximum number of tokens in the response
temperature = 0.0 # Controls randomness for vision model
# [llm.vision] #OLLAMA VISION:
# api_type = 'ollama'
@@ -63,8 +71,19 @@ temperature = 0.0 # Controls randomness for vision mod
# Optional configuration, Search settings.
# [search]
# Search engine for agent to use. Default is "Google", can be set to "Baidu" or "DuckDuckGo".
# Search engine for agent to use. Default is "Google", can be set to "Baidu" or "DuckDuckGo" or "Bing".
#engine = "Google"
# Fallback engine order. Default is ["DuckDuckGo", "Baidu", "Bing"] - will try in this order after primary engine fails.
#fallback_engines = ["DuckDuckGo", "Baidu", "Bing"]
# Seconds to wait before retrying all engines again when they all fail due to rate limits. Default is 60.
#retry_delay = 60
# Maximum number of times to retry all engines when all fail. Default is 3.
#max_retries = 3
# Language code for search results. Options: "en" (English), "zh" (Chinese), etc.
#lang = "en"
# Country code for search results. Options: "us" (United States), "cn" (China), etc.
#country = "us"
## Sandbox configuration
#[sandbox]
@@ -75,3 +94,12 @@ temperature = 0.0 # Controls randomness for vision mod
#cpu_limit = 2.0
#timeout = 300
#network_enabled = true
# MCP (Model Context Protocol) configuration
[mcp]
server_reference = "app.mcp.server" # default server module reference
# Optional Runflow configuration
# Your can add additional agents into run-flow workflow to solve different-type tasks.
[runflow]
use_data_analysis_agent = false # The Data Analysi Agent to solve various data analysis tasks
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"server1": {
"type": "sse",
"url": "http://localhost:8000/sse"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
"""
OpenManus benchmark system for standardized agent evaluation.
"""

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

@@ -11,6 +11,6 @@ The Model we use is `claude3.5`.
I need a 7-day Japan itinerary for April 15-23 from Seattle, with a $2500-5000 budget for my fiancée and me. We love historical sites, hidden gems, and Japanese culture (kendo, tea ceremonies, Zen meditation). We want to see Nara's deer and explore cities on foot. I plan to propose during this trip and need a special location recommendation. Please provide a detailed itinerary and a simple HTML travel handbook with maps, attraction descriptions, essential Japanese phrases, and travel tips we can reference throughout our journey.
```
**preview**
![alt text](./pictures/japan-travel-plan-1.png)
![alt text](picturesapan-travel-plan-1.png)
![alt text](./pictures/japan-travel-plan-2.png)
![alt text](picturesapan-travel-plan-2.png)

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