camel-ai 0.2.65__py3-none-any.whl → 0.2.82__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of camel-ai might be problematic. Click here for more details.

Files changed (505) hide show
  1. camel/__init__.py +3 -3
  2. camel/agents/__init__.py +2 -2
  3. camel/agents/_types.py +9 -4
  4. camel/agents/_utils.py +40 -2
  5. camel/agents/base.py +2 -2
  6. camel/agents/chat_agent.py +4835 -947
  7. camel/agents/critic_agent.py +2 -2
  8. camel/agents/deductive_reasoner_agent.py +56 -56
  9. camel/agents/embodied_agent.py +2 -2
  10. camel/agents/knowledge_graph_agent.py +20 -20
  11. camel/agents/mcp_agent.py +35 -36
  12. camel/agents/multi_hop_generator_agent.py +3 -3
  13. camel/agents/programmed_agent_instruction.py +2 -2
  14. camel/agents/repo_agent.py +4 -3
  15. camel/agents/role_assignment_agent.py +2 -2
  16. camel/agents/search_agent.py +2 -2
  17. camel/agents/task_agent.py +2 -2
  18. camel/agents/tool_agents/__init__.py +2 -2
  19. camel/agents/tool_agents/base.py +2 -2
  20. camel/agents/tool_agents/hugging_face_tool_agent.py +3 -3
  21. camel/benchmarks/__init__.py +2 -2
  22. camel/benchmarks/apibank.py +5 -5
  23. camel/benchmarks/apibench.py +2 -2
  24. camel/benchmarks/base.py +2 -2
  25. camel/benchmarks/browsecomp.py +44 -33
  26. camel/benchmarks/gaia.py +17 -13
  27. camel/benchmarks/mock_website/README.md +1 -3
  28. camel/benchmarks/mock_website/mock_web.py +2 -2
  29. camel/benchmarks/mock_website/requirements.txt +1 -1
  30. camel/benchmarks/mock_website/shopping_mall/app.py +2 -2
  31. camel/benchmarks/mock_website/task.json +1 -1
  32. camel/benchmarks/nexus.py +3 -3
  33. camel/benchmarks/ragbench.py +2 -2
  34. camel/bots/__init__.py +2 -2
  35. camel/bots/discord/__init__.py +2 -2
  36. camel/bots/discord/discord_app.py +2 -2
  37. camel/bots/discord/discord_installation.py +2 -2
  38. camel/bots/discord/discord_store.py +3 -3
  39. camel/bots/slack/__init__.py +2 -2
  40. camel/bots/slack/models.py +4 -4
  41. camel/bots/slack/slack_app.py +2 -2
  42. camel/bots/telegram_bot.py +2 -2
  43. camel/configs/__init__.py +23 -2
  44. camel/configs/aihubmix_config.py +90 -0
  45. camel/configs/aiml_config.py +2 -2
  46. camel/configs/amd_config.py +70 -0
  47. camel/configs/anthropic_config.py +2 -2
  48. camel/configs/base_config.py +2 -2
  49. camel/configs/bedrock_config.py +5 -3
  50. camel/configs/cerebras_config.py +98 -0
  51. camel/configs/cohere_config.py +2 -2
  52. camel/configs/cometapi_config.py +106 -0
  53. camel/configs/crynux_config.py +2 -2
  54. camel/configs/deepseek_config.py +9 -8
  55. camel/configs/gemini_config.py +6 -4
  56. camel/configs/groq_config.py +6 -4
  57. camel/configs/internlm_config.py +6 -4
  58. camel/configs/litellm_config.py +2 -2
  59. camel/configs/lmstudio_config.py +6 -4
  60. camel/configs/minimax_config.py +95 -0
  61. camel/configs/mistral_config.py +2 -2
  62. camel/configs/modelscope_config.py +5 -3
  63. camel/configs/moonshot_config.py +2 -2
  64. camel/configs/nebius_config.py +105 -0
  65. camel/configs/netmind_config.py +2 -2
  66. camel/configs/novita_config.py +2 -2
  67. camel/configs/nvidia_config.py +2 -2
  68. camel/configs/ollama_config.py +2 -2
  69. camel/configs/openai_config.py +5 -3
  70. camel/configs/openrouter_config.py +6 -4
  71. camel/configs/ppio_config.py +2 -2
  72. camel/configs/qianfan_config.py +85 -0
  73. camel/configs/qwen_config.py +2 -2
  74. camel/configs/reka_config.py +2 -2
  75. camel/configs/samba_config.py +6 -4
  76. camel/configs/sglang_config.py +2 -2
  77. camel/configs/siliconflow_config.py +2 -2
  78. camel/configs/togetherai_config.py +2 -2
  79. camel/configs/vllm_config.py +4 -2
  80. camel/configs/watsonx_config.py +2 -2
  81. camel/configs/yi_config.py +6 -4
  82. camel/configs/zhipuai_config.py +6 -4
  83. camel/data_collectors/__init__.py +2 -2
  84. camel/data_collectors/alpaca_collector.py +18 -9
  85. camel/data_collectors/base.py +2 -2
  86. camel/data_collectors/sharegpt_collector.py +2 -2
  87. camel/datagen/__init__.py +2 -2
  88. camel/datagen/cot_datagen.py +3 -3
  89. camel/datagen/evol_instruct/__init__.py +2 -2
  90. camel/datagen/evol_instruct/evol_instruct.py +2 -2
  91. camel/datagen/evol_instruct/scorer.py +12 -12
  92. camel/datagen/evol_instruct/templates.py +16 -16
  93. camel/datagen/self_improving_cot.py +5 -5
  94. camel/datagen/self_instruct/__init__.py +2 -2
  95. camel/datagen/self_instruct/filter/__init__.py +2 -2
  96. camel/datagen/self_instruct/filter/filter_function.py +2 -2
  97. camel/datagen/self_instruct/filter/filter_registry.py +2 -2
  98. camel/datagen/self_instruct/filter/instruction_filter.py +2 -2
  99. camel/datagen/self_instruct/self_instruct.py +2 -2
  100. camel/datagen/self_instruct/templates.py +47 -47
  101. camel/datagen/source2synth/__init__.py +2 -2
  102. camel/datagen/source2synth/data_processor.py +2 -2
  103. camel/datagen/source2synth/models.py +2 -2
  104. camel/datagen/source2synth/user_data_processor_config.py +2 -2
  105. camel/datahubs/__init__.py +2 -2
  106. camel/datahubs/base.py +2 -2
  107. camel/datahubs/huggingface.py +2 -2
  108. camel/datahubs/models.py +2 -2
  109. camel/datasets/__init__.py +2 -2
  110. camel/datasets/base_generator.py +41 -12
  111. camel/datasets/few_shot_generator.py +18 -18
  112. camel/datasets/models.py +2 -2
  113. camel/datasets/self_instruct_generator.py +2 -2
  114. camel/datasets/static_dataset.py +2 -2
  115. camel/embeddings/__init__.py +2 -2
  116. camel/embeddings/azure_embedding.py +2 -2
  117. camel/embeddings/base.py +2 -2
  118. camel/embeddings/gemini_embedding.py +2 -2
  119. camel/embeddings/jina_embedding.py +2 -2
  120. camel/embeddings/mistral_embedding.py +2 -2
  121. camel/embeddings/openai_compatible_embedding.py +2 -2
  122. camel/embeddings/openai_embedding.py +2 -2
  123. camel/embeddings/sentence_transformers_embeddings.py +2 -2
  124. camel/embeddings/together_embedding.py +2 -2
  125. camel/embeddings/vlm_embedding.py +2 -2
  126. camel/environments/__init__.py +14 -2
  127. camel/environments/models.py +2 -2
  128. camel/environments/multi_step.py +2 -2
  129. camel/environments/rlcards_env.py +860 -0
  130. camel/environments/single_step.py +30 -5
  131. camel/environments/tic_tac_toe.py +3 -3
  132. camel/extractors/__init__.py +2 -2
  133. camel/extractors/base.py +2 -2
  134. camel/extractors/python_strategies.py +2 -2
  135. camel/generators.py +2 -2
  136. camel/human.py +2 -2
  137. camel/interpreters/__init__.py +4 -2
  138. camel/interpreters/base.py +2 -2
  139. camel/interpreters/docker/Dockerfile +14 -24
  140. camel/interpreters/docker_interpreter.py +5 -4
  141. camel/interpreters/e2b_interpreter.py +36 -3
  142. camel/interpreters/internal_python_interpreter.py +53 -4
  143. camel/interpreters/interpreter_error.py +2 -2
  144. camel/interpreters/ipython_interpreter.py +2 -2
  145. camel/interpreters/microsandbox_interpreter.py +395 -0
  146. camel/interpreters/subprocess_interpreter.py +2 -2
  147. camel/loaders/__init__.py +13 -4
  148. camel/loaders/apify_reader.py +2 -2
  149. camel/loaders/base_io.py +2 -2
  150. camel/loaders/base_loader.py +85 -0
  151. camel/loaders/chunkr_reader.py +11 -2
  152. camel/loaders/crawl4ai_reader.py +2 -2
  153. camel/loaders/firecrawl_reader.py +6 -6
  154. camel/loaders/jina_url_reader.py +2 -2
  155. camel/loaders/markitdown.py +2 -2
  156. camel/loaders/mineru_extractor.py +2 -2
  157. camel/loaders/mistral_reader.py +2 -2
  158. camel/loaders/scrapegraph_reader.py +2 -2
  159. camel/loaders/unstructured_io.py +2 -2
  160. camel/logger.py +5 -5
  161. camel/memories/__init__.py +2 -2
  162. camel/memories/agent_memories.py +86 -3
  163. camel/memories/base.py +36 -2
  164. camel/memories/blocks/__init__.py +2 -2
  165. camel/memories/blocks/chat_history_block.py +125 -7
  166. camel/memories/blocks/vectordb_block.py +10 -3
  167. camel/memories/context_creators/__init__.py +2 -2
  168. camel/memories/context_creators/score_based.py +31 -239
  169. camel/memories/records.py +90 -10
  170. camel/messages/__init__.py +2 -2
  171. camel/messages/base.py +178 -43
  172. camel/messages/conversion/__init__.py +2 -2
  173. camel/messages/conversion/alpaca.py +2 -2
  174. camel/messages/conversion/conversation_models.py +2 -2
  175. camel/messages/conversion/sharegpt/__init__.py +2 -2
  176. camel/messages/conversion/sharegpt/function_call_formatter.py +2 -2
  177. camel/messages/conversion/sharegpt/hermes/__init__.py +2 -2
  178. camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py +2 -2
  179. camel/messages/func_message.py +54 -17
  180. camel/models/__init__.py +16 -2
  181. camel/models/_utils.py +3 -3
  182. camel/models/aihubmix_model.py +83 -0
  183. camel/models/aiml_model.py +11 -18
  184. camel/models/amd_model.py +101 -0
  185. camel/models/anthropic_model.py +127 -20
  186. camel/models/aws_bedrock_model.py +12 -35
  187. camel/models/azure_openai_model.py +212 -89
  188. camel/models/base_audio_model.py +5 -3
  189. camel/models/base_model.py +195 -26
  190. camel/models/cerebras_model.py +83 -0
  191. camel/models/cohere_model.py +16 -21
  192. camel/models/cometapi_model.py +83 -0
  193. camel/models/crynux_model.py +11 -18
  194. camel/models/deepseek_model.py +18 -58
  195. camel/models/fish_audio_model.py +8 -2
  196. camel/models/gemini_model.py +389 -26
  197. camel/models/groq_model.py +11 -19
  198. camel/models/internlm_model.py +11 -18
  199. camel/models/litellm_model.py +56 -34
  200. camel/models/lmstudio_model.py +17 -20
  201. camel/models/minimax_model.py +83 -0
  202. camel/models/mistral_model.py +18 -19
  203. camel/models/model_factory.py +37 -3
  204. camel/models/model_manager.py +26 -8
  205. camel/models/modelscope_model.py +13 -193
  206. camel/models/moonshot_model.py +195 -21
  207. camel/models/nebius_model.py +83 -0
  208. camel/models/nemotron_model.py +19 -9
  209. camel/models/netmind_model.py +11 -18
  210. camel/models/novita_model.py +11 -18
  211. camel/models/nvidia_model.py +11 -18
  212. camel/models/ollama_model.py +14 -21
  213. camel/models/openai_audio_models.py +2 -2
  214. camel/models/openai_compatible_model.py +188 -45
  215. camel/models/openai_model.py +216 -71
  216. camel/models/openrouter_model.py +11 -19
  217. camel/models/ppio_model.py +11 -18
  218. camel/models/qianfan_model.py +89 -0
  219. camel/models/qwen_model.py +13 -193
  220. camel/models/reka_model.py +21 -21
  221. camel/models/reward/__init__.py +2 -2
  222. camel/models/reward/base_reward_model.py +2 -2
  223. camel/models/reward/evaluator.py +2 -2
  224. camel/models/reward/nemotron_model.py +2 -2
  225. camel/models/reward/skywork_model.py +2 -2
  226. camel/models/samba_model.py +48 -47
  227. camel/models/sglang_model.py +88 -40
  228. camel/models/siliconflow_model.py +12 -35
  229. camel/models/stub_model.py +10 -7
  230. camel/models/togetherai_model.py +11 -18
  231. camel/models/vllm_model.py +10 -18
  232. camel/models/volcano_model.py +16 -20
  233. camel/models/watsonx_model.py +7 -19
  234. camel/models/yi_model.py +11 -18
  235. camel/models/zhipuai_model.py +70 -18
  236. camel/parsers/__init__.py +18 -0
  237. camel/parsers/mcp_tool_call_parser.py +176 -0
  238. camel/personas/__init__.py +2 -2
  239. camel/personas/persona.py +2 -2
  240. camel/personas/persona_hub.py +2 -2
  241. camel/prompts/__init__.py +2 -2
  242. camel/prompts/ai_society.py +2 -2
  243. camel/prompts/base.py +2 -2
  244. camel/prompts/code.py +2 -2
  245. camel/prompts/evaluation.py +2 -2
  246. camel/prompts/generate_text_embedding_data.py +2 -2
  247. camel/prompts/image_craft.py +2 -2
  248. camel/prompts/misalignment.py +2 -2
  249. camel/prompts/multi_condition_image_craft.py +2 -2
  250. camel/prompts/object_recognition.py +2 -2
  251. camel/prompts/persona_hub.py +3 -3
  252. camel/prompts/prompt_templates.py +2 -2
  253. camel/prompts/role_description_prompt_template.py +2 -2
  254. camel/prompts/solution_extraction.py +8 -8
  255. camel/prompts/task_prompt_template.py +2 -2
  256. camel/prompts/translation.py +2 -2
  257. camel/prompts/video_description_prompt.py +3 -3
  258. camel/responses/__init__.py +2 -2
  259. camel/responses/agent_responses.py +2 -2
  260. camel/retrievers/__init__.py +2 -2
  261. camel/retrievers/auto_retriever.py +3 -2
  262. camel/retrievers/base.py +2 -2
  263. camel/retrievers/bm25_retriever.py +2 -2
  264. camel/retrievers/cohere_rerank_retriever.py +2 -2
  265. camel/retrievers/hybrid_retrival.py +2 -2
  266. camel/retrievers/vector_retriever.py +2 -2
  267. camel/runtimes/Dockerfile.multi-toolkit +90 -0
  268. camel/runtimes/__init__.py +2 -2
  269. camel/runtimes/api.py +79 -23
  270. camel/runtimes/base.py +2 -2
  271. camel/runtimes/configs.py +13 -13
  272. camel/runtimes/daytona_runtime.py +17 -18
  273. camel/runtimes/docker_runtime.py +12 -12
  274. camel/runtimes/llm_guard_runtime.py +26 -26
  275. camel/runtimes/remote_http_runtime.py +11 -11
  276. camel/runtimes/ubuntu_docker_runtime.py +2 -2
  277. camel/runtimes/utils/__init__.py +2 -2
  278. camel/runtimes/utils/function_risk_toolkit.py +2 -2
  279. camel/runtimes/utils/ignore_risk_toolkit.py +2 -2
  280. camel/schemas/__init__.py +2 -2
  281. camel/schemas/base.py +2 -2
  282. camel/schemas/openai_converter.py +3 -3
  283. camel/schemas/outlines_converter.py +2 -2
  284. camel/services/agent_openapi_server.py +380 -0
  285. camel/societies/__init__.py +4 -2
  286. camel/societies/babyagi_playing.py +2 -2
  287. camel/societies/role_playing.py +201 -80
  288. camel/societies/workforce/__init__.py +10 -3
  289. camel/societies/workforce/base.py +2 -2
  290. camel/societies/workforce/events.py +143 -0
  291. camel/societies/workforce/prompts.py +258 -33
  292. camel/societies/workforce/role_playing_worker.py +88 -31
  293. camel/societies/workforce/single_agent_worker.py +638 -40
  294. camel/societies/workforce/structured_output_handler.py +512 -0
  295. camel/societies/workforce/task_channel.py +182 -38
  296. camel/societies/workforce/utils.py +780 -65
  297. camel/societies/workforce/worker.py +92 -26
  298. camel/societies/workforce/workflow_memory_manager.py +1746 -0
  299. camel/societies/workforce/workforce.py +5276 -355
  300. camel/societies/workforce/workforce_callback.py +103 -0
  301. camel/societies/workforce/workforce_logger.py +647 -0
  302. camel/societies/workforce/workforce_metrics.py +33 -0
  303. camel/storages/__init__.py +6 -2
  304. camel/storages/graph_storages/__init__.py +2 -2
  305. camel/storages/graph_storages/base.py +2 -2
  306. camel/storages/graph_storages/graph_element.py +2 -2
  307. camel/storages/graph_storages/nebula_graph.py +4 -4
  308. camel/storages/graph_storages/neo4j_graph.py +7 -7
  309. camel/storages/key_value_storages/__init__.py +2 -2
  310. camel/storages/key_value_storages/base.py +2 -2
  311. camel/storages/key_value_storages/in_memory.py +2 -2
  312. camel/storages/key_value_storages/json.py +17 -4
  313. camel/storages/key_value_storages/mem0_cloud.py +50 -49
  314. camel/storages/key_value_storages/redis.py +2 -2
  315. camel/storages/object_storages/__init__.py +2 -2
  316. camel/storages/object_storages/amazon_s3.py +2 -2
  317. camel/storages/object_storages/azure_blob.py +2 -2
  318. camel/storages/object_storages/base.py +2 -2
  319. camel/storages/object_storages/google_cloud.py +3 -3
  320. camel/storages/vectordb_storages/__init__.py +8 -2
  321. camel/storages/vectordb_storages/base.py +2 -2
  322. camel/storages/vectordb_storages/chroma.py +731 -0
  323. camel/storages/vectordb_storages/faiss.py +2 -2
  324. camel/storages/vectordb_storages/milvus.py +2 -2
  325. camel/storages/vectordb_storages/oceanbase.py +15 -15
  326. camel/storages/vectordb_storages/pgvector.py +349 -0
  327. camel/storages/vectordb_storages/qdrant.py +6 -6
  328. camel/storages/vectordb_storages/surreal.py +372 -0
  329. camel/storages/vectordb_storages/tidb.py +11 -8
  330. camel/storages/vectordb_storages/weaviate.py +2 -2
  331. camel/tasks/__init__.py +2 -2
  332. camel/tasks/task.py +348 -26
  333. camel/tasks/task_prompt.py +3 -3
  334. camel/terminators/__init__.py +2 -2
  335. camel/terminators/base.py +2 -2
  336. camel/terminators/response_terminator.py +2 -2
  337. camel/terminators/token_limit_terminator.py +2 -2
  338. camel/toolkits/__init__.py +54 -10
  339. camel/toolkits/aci_toolkit.py +66 -21
  340. camel/toolkits/arxiv_toolkit.py +8 -8
  341. camel/toolkits/ask_news_toolkit.py +2 -2
  342. camel/toolkits/async_browser_toolkit.py +4 -4
  343. camel/toolkits/audio_analysis_toolkit.py +3 -3
  344. camel/toolkits/base.py +65 -7
  345. camel/toolkits/bohrium_toolkit.py +2 -2
  346. camel/toolkits/browser_toolkit.py +34 -21
  347. camel/toolkits/browser_toolkit_commons.py +4 -4
  348. camel/toolkits/code_execution.py +31 -4
  349. camel/toolkits/context_summarizer_toolkit.py +684 -0
  350. camel/toolkits/craw4ai_toolkit.py +93 -0
  351. camel/toolkits/dappier_toolkit.py +12 -8
  352. camel/toolkits/data_commons_toolkit.py +2 -2
  353. camel/toolkits/dingtalk.py +1135 -0
  354. camel/toolkits/earth_science_toolkit.py +5367 -0
  355. camel/toolkits/edgeone_pages_mcp_toolkit.py +49 -0
  356. camel/toolkits/excel_toolkit.py +905 -71
  357. camel/toolkits/file_toolkit.py +1402 -0
  358. camel/toolkits/function_tool.py +126 -18
  359. camel/toolkits/github_toolkit.py +109 -22
  360. camel/toolkits/gmail_toolkit.py +1839 -0
  361. camel/toolkits/google_calendar_toolkit.py +40 -6
  362. camel/toolkits/google_drive_mcp_toolkit.py +54 -0
  363. camel/toolkits/google_maps_toolkit.py +2 -2
  364. camel/toolkits/google_scholar_toolkit.py +2 -2
  365. camel/toolkits/human_toolkit.py +36 -12
  366. camel/toolkits/hybrid_browser_toolkit/__init__.py +18 -0
  367. camel/toolkits/hybrid_browser_toolkit/config_loader.py +185 -0
  368. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +246 -0
  369. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1973 -0
  370. camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
  371. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4589 -0
  372. camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
  373. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
  374. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +1929 -0
  375. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +233 -0
  376. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +589 -0
  377. camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
  378. camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
  379. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
  380. camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
  381. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +129 -0
  382. camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +27 -0
  383. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +319 -0
  384. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +1037 -0
  385. camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
  386. camel/toolkits/hybrid_browser_toolkit_py/actions.py +575 -0
  387. camel/toolkits/hybrid_browser_toolkit_py/agent.py +311 -0
  388. camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +787 -0
  389. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +490 -0
  390. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2390 -0
  391. camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +233 -0
  392. camel/toolkits/hybrid_browser_toolkit_py/stealth_script.js +0 -0
  393. camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +1043 -0
  394. camel/toolkits/image_analysis_toolkit.py +3 -6
  395. camel/toolkits/image_generation_toolkit.py +390 -0
  396. camel/toolkits/jina_reranker_toolkit.py +5 -6
  397. camel/toolkits/klavis_toolkit.py +7 -3
  398. camel/toolkits/linkedin_toolkit.py +2 -2
  399. camel/toolkits/markitdown_toolkit.py +104 -0
  400. camel/toolkits/math_toolkit.py +66 -12
  401. camel/toolkits/mcp_toolkit.py +412 -36
  402. camel/toolkits/memory_toolkit.py +7 -3
  403. camel/toolkits/meshy_toolkit.py +2 -2
  404. camel/toolkits/message_agent_toolkit.py +608 -0
  405. camel/toolkits/message_integration.py +724 -0
  406. camel/toolkits/mineru_toolkit.py +2 -2
  407. camel/toolkits/minimax_mcp_toolkit.py +195 -0
  408. camel/toolkits/networkx_toolkit.py +2 -2
  409. camel/toolkits/note_taking_toolkit.py +277 -0
  410. camel/toolkits/notion_mcp_toolkit.py +224 -0
  411. camel/toolkits/notion_toolkit.py +2 -2
  412. camel/toolkits/open_api_specs/biztoc/__init__.py +2 -2
  413. camel/toolkits/open_api_specs/biztoc/ai-plugin.json +1 -1
  414. camel/toolkits/open_api_specs/coursera/__init__.py +2 -2
  415. camel/toolkits/open_api_specs/create_qr_code/__init__.py +2 -2
  416. camel/toolkits/open_api_specs/klarna/__init__.py +2 -2
  417. camel/toolkits/open_api_specs/nasa_apod/__init__.py +2 -2
  418. camel/toolkits/open_api_specs/outschool/__init__.py +2 -2
  419. camel/toolkits/open_api_specs/outschool/ai-plugin.json +1 -1
  420. camel/toolkits/open_api_specs/outschool/openapi.yaml +1 -1
  421. camel/toolkits/open_api_specs/outschool/paths/__init__.py +2 -2
  422. camel/toolkits/open_api_specs/outschool/paths/get_classes.py +2 -2
  423. camel/toolkits/open_api_specs/outschool/paths/search_teachers.py +2 -2
  424. camel/toolkits/open_api_specs/security_config.py +2 -2
  425. camel/toolkits/open_api_specs/speak/__init__.py +2 -2
  426. camel/toolkits/open_api_specs/web_scraper/__init__.py +2 -2
  427. camel/toolkits/open_api_specs/web_scraper/ai-plugin.json +1 -1
  428. camel/toolkits/open_api_specs/web_scraper/paths/__init__.py +2 -2
  429. camel/toolkits/open_api_specs/web_scraper/paths/scraper.py +2 -2
  430. camel/toolkits/open_api_toolkit.py +2 -2
  431. camel/toolkits/openbb_toolkit.py +7 -3
  432. camel/toolkits/origene_mcp_toolkit.py +56 -0
  433. camel/toolkits/page_script.js +53 -53
  434. camel/toolkits/playwright_mcp_toolkit.py +13 -31
  435. camel/toolkits/pptx_toolkit.py +36 -23
  436. camel/toolkits/pubmed_toolkit.py +2 -2
  437. camel/toolkits/pulse_mcp_search_toolkit.py +2 -2
  438. camel/toolkits/pyautogui_toolkit.py +2 -2
  439. camel/toolkits/reddit_toolkit.py +2 -2
  440. camel/toolkits/resend_toolkit.py +168 -0
  441. camel/toolkits/retrieval_toolkit.py +2 -2
  442. camel/toolkits/screenshot_toolkit.py +213 -0
  443. camel/toolkits/search_toolkit.py +539 -146
  444. camel/toolkits/searxng_toolkit.py +2 -2
  445. camel/toolkits/semantic_scholar_toolkit.py +2 -2
  446. camel/toolkits/slack_toolkit.py +108 -58
  447. camel/toolkits/sql_toolkit.py +712 -0
  448. camel/toolkits/stripe_toolkit.py +2 -2
  449. camel/toolkits/sympy_toolkit.py +3 -3
  450. camel/toolkits/task_planning_toolkit.py +5 -5
  451. camel/toolkits/terminal_toolkit/__init__.py +18 -0
  452. camel/toolkits/terminal_toolkit/terminal_toolkit.py +1070 -0
  453. camel/toolkits/terminal_toolkit/utils.py +532 -0
  454. camel/toolkits/thinking_toolkit.py +3 -3
  455. camel/toolkits/twitter_toolkit.py +2 -2
  456. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  457. camel/toolkits/video_analysis_toolkit.py +109 -29
  458. camel/toolkits/video_download_toolkit.py +19 -16
  459. camel/toolkits/weather_toolkit.py +2 -2
  460. camel/toolkits/web_deploy_toolkit.py +1219 -0
  461. camel/toolkits/wechat_official_toolkit.py +483 -0
  462. camel/toolkits/whatsapp_toolkit.py +2 -2
  463. camel/toolkits/wolfram_alpha_toolkit.py +2 -2
  464. camel/toolkits/zapier_toolkit.py +7 -3
  465. camel/types/__init__.py +4 -4
  466. camel/types/agents/__init__.py +2 -2
  467. camel/types/agents/tool_calling_record.py +6 -3
  468. camel/types/enums.py +378 -39
  469. camel/types/mcp_registries.py +2 -2
  470. camel/types/openai_types.py +4 -4
  471. camel/types/unified_model_type.py +38 -6
  472. camel/utils/__init__.py +2 -2
  473. camel/utils/async_func.py +2 -2
  474. camel/utils/chunker/__init__.py +2 -2
  475. camel/utils/chunker/base.py +2 -2
  476. camel/utils/chunker/code_chunker.py +2 -2
  477. camel/utils/chunker/uio_chunker.py +2 -2
  478. camel/utils/commons.py +38 -7
  479. camel/utils/constants.py +5 -2
  480. camel/utils/context_utils.py +1134 -0
  481. camel/utils/deduplication.py +2 -2
  482. camel/utils/filename.py +2 -2
  483. camel/utils/langfuse.py +2 -2
  484. camel/utils/mcp.py +140 -6
  485. camel/utils/mcp_client.py +48 -38
  486. camel/utils/message_summarizer.py +148 -0
  487. camel/utils/response_format.py +2 -2
  488. camel/utils/token_counting.py +45 -22
  489. camel/utils/tool_result.py +44 -0
  490. camel/verifiers/__init__.py +2 -2
  491. camel/verifiers/base.py +2 -2
  492. camel/verifiers/math_verifier.py +2 -2
  493. camel/verifiers/models.py +2 -2
  494. camel/verifiers/physics_verifier.py +2 -2
  495. camel/verifiers/python_verifier.py +2 -2
  496. {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/METADATA +327 -94
  497. camel_ai-0.2.82.dist-info/RECORD +507 -0
  498. {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/WHEEL +1 -1
  499. {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/licenses/LICENSE +1 -1
  500. camel/loaders/pandas_reader.py +0 -368
  501. camel/toolkits/dalle_toolkit.py +0 -175
  502. camel/toolkits/file_write_toolkit.py +0 -444
  503. camel/toolkits/openai_agent_toolkit.py +0 -135
  504. camel/toolkits/terminal_toolkit.py +0 -1037
  505. camel_ai-0.2.65.dist-info/RECORD +0 -426
@@ -0,0 +1,1070 @@
1
+ # ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
14
+ import os
15
+ import platform
16
+ import select
17
+ import shlex
18
+ import subprocess
19
+ import sys
20
+ import threading
21
+ import time
22
+ from queue import Empty, Queue
23
+ from typing import Any, Dict, List, Optional
24
+
25
+ from camel.logger import get_logger
26
+ from camel.toolkits.base import BaseToolkit
27
+ from camel.toolkits.function_tool import FunctionTool
28
+ from camel.toolkits.terminal_toolkit.utils import (
29
+ check_nodejs_availability,
30
+ clone_current_environment,
31
+ ensure_uv_available,
32
+ sanitize_command,
33
+ setup_initial_env_with_uv,
34
+ setup_initial_env_with_venv,
35
+ )
36
+ from camel.utils import MCPServer
37
+
38
+ logger = get_logger(__name__)
39
+
40
+ # Try to import docker, but don't make it a hard requirement
41
+ try:
42
+ import docker
43
+ from docker.errors import APIError, NotFound
44
+ except ImportError:
45
+ docker = None
46
+ NotFound = None
47
+ APIError = None
48
+
49
+
50
+ def _to_plain(text: str) -> str:
51
+ r"""Convert ANSI text to plain text using rich if available."""
52
+ try:
53
+ from rich.text import Text as _RichText
54
+
55
+ return _RichText.from_ansi(text).plain
56
+ except Exception:
57
+ return text
58
+
59
+
60
+ @MCPServer()
61
+ class TerminalToolkit(BaseToolkit):
62
+ r"""A toolkit for LLM agents to execute and interact with terminal commands
63
+ in either a local or a sandboxed Docker environment.
64
+
65
+ Args:
66
+ timeout (Optional[float]): The default timeout in seconds for blocking
67
+ commands. Defaults to 20.0.
68
+ working_directory (Optional[str]): The base directory for operations.
69
+ For the local backend, this acts as a security sandbox.
70
+ For the Docker backend, this sets the working directory inside
71
+ the container.
72
+ If not specified, defaults to "./workspace" for local and
73
+ "/workspace" for Docker.
74
+ use_docker_backend (bool): If True, all commands are executed in a
75
+ Docker container. Defaults to False.
76
+ docker_container_name (Optional[str]): The name of the Docker
77
+ container to use. Required if use_docker_backend is True.
78
+ session_logs_dir (Optional[str]): The directory to store session
79
+ logs. Defaults to a 'terminal_logs' subfolder in the
80
+ working directory.
81
+ safe_mode (bool): Whether to apply security checks to commands.
82
+ Defaults to True.
83
+ allowed_commands (Optional[List[str]]): List of allowed commands
84
+ when safe_mode is True. If None, uses default safety rules.
85
+ clone_current_env (bool): Whether to clone the current Python
86
+ environment for local execution. Defaults to False.
87
+ install_dependencies (List): A list of user specified libraries
88
+ to install.
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ timeout: Optional[float] = 20.0,
94
+ working_directory: Optional[str] = None,
95
+ use_docker_backend: bool = False,
96
+ docker_container_name: Optional[str] = None,
97
+ session_logs_dir: Optional[str] = None,
98
+ safe_mode: bool = True,
99
+ allowed_commands: Optional[List[str]] = None,
100
+ clone_current_env: bool = False,
101
+ install_dependencies: Optional[List[str]] = None,
102
+ ):
103
+ # auto-detect if running inside a CAMEL runtime container
104
+ # when inside a runtime, use local execution (already sandboxed)
105
+ runtime_env = os.environ.get("CAMEL_RUNTIME", "").lower()
106
+ self._in_runtime = runtime_env == "true"
107
+ if self._in_runtime and use_docker_backend:
108
+ logger.info(
109
+ "Detected CAMEL_RUNTIME environment - disabling Docker "
110
+ "backend since we're already inside a sandboxed container"
111
+ )
112
+ use_docker_backend = False
113
+ docker_container_name = None
114
+
115
+ self.use_docker_backend = use_docker_backend
116
+ self.timeout = timeout
117
+ self.shell_sessions: Dict[str, Dict[str, Any]] = {}
118
+ # Thread-safe guard for concurrent access to
119
+ # shell_sessions and session state
120
+ self._session_lock = threading.RLock()
121
+
122
+ # Initialize docker_workdir with proper type
123
+ self.docker_workdir: Optional[str] = None
124
+
125
+ if self.use_docker_backend:
126
+ # For Docker backend, working_directory is path inside container
127
+ if working_directory:
128
+ self.docker_workdir = working_directory
129
+ else:
130
+ self.docker_workdir = "/workspace"
131
+ # For logs and local file operations, use a local workspace
132
+ camel_workdir = os.environ.get("CAMEL_WORKDIR")
133
+ if camel_workdir:
134
+ self.working_dir = os.path.abspath(camel_workdir)
135
+ else:
136
+ self.working_dir = os.path.abspath("./workspace")
137
+ else:
138
+ # For local backend, working_directory is the local path
139
+ if working_directory:
140
+ self.working_dir = os.path.abspath(working_directory)
141
+ else:
142
+ camel_workdir = os.environ.get("CAMEL_WORKDIR")
143
+ if camel_workdir:
144
+ self.working_dir = os.path.abspath(camel_workdir)
145
+ else:
146
+ self.working_dir = os.path.abspath("./workspace")
147
+
148
+ # Only create local directory for logs and local backend operations
149
+ if not os.path.exists(self.working_dir):
150
+ os.makedirs(self.working_dir, exist_ok=True)
151
+ self.safe_mode = safe_mode
152
+
153
+ # Initialize whitelist of allowed commands if provided
154
+ self.allowed_commands = (
155
+ set(allowed_commands) if allowed_commands else None
156
+ )
157
+
158
+ # Environment management attributes
159
+ self.clone_current_env = clone_current_env
160
+ self.cloned_env_path: Optional[str] = None
161
+ self.initial_env_path: Optional[str] = None
162
+ self.python_executable = sys.executable
163
+ self.install_dependencies = install_dependencies or []
164
+
165
+ self.log_dir = os.path.abspath(
166
+ session_logs_dir or os.path.join(self.working_dir, "terminal_logs")
167
+ )
168
+ self.blocking_log_file = os.path.join(
169
+ self.log_dir, "blocking_commands.log"
170
+ )
171
+ self.os_type = platform.system()
172
+
173
+ os.makedirs(self.log_dir, exist_ok=True)
174
+
175
+ # Clean the file in terminal_logs folder
176
+ for file in os.listdir(self.log_dir):
177
+ if file.endswith(".log"):
178
+ os.remove(os.path.join(self.log_dir, file))
179
+
180
+ if self.use_docker_backend:
181
+ if docker is None:
182
+ raise ImportError(
183
+ "The 'docker' library is required to use the "
184
+ "Docker backend. Please install it with "
185
+ "'pip install docker'."
186
+ )
187
+ if not docker_container_name:
188
+ raise ValueError(
189
+ "docker_container_name must be "
190
+ "provided when using Docker backend."
191
+ )
192
+ try:
193
+ # APIClient is used for operations that need a timeout,
194
+ # like exec_start
195
+ self.docker_api_client = docker.APIClient(
196
+ base_url='unix://var/run/docker.sock', timeout=self.timeout
197
+ )
198
+ self.docker_client = docker.from_env()
199
+ try:
200
+ # Try to get existing container
201
+ self.container = self.docker_client.containers.get(
202
+ docker_container_name
203
+ )
204
+ logger.info(
205
+ f"Successfully attached to existing Docker container "
206
+ f"'{docker_container_name}'."
207
+ )
208
+ except NotFound:
209
+ raise RuntimeError(
210
+ f"Container '{docker_container_name}' not found. "
211
+ )
212
+
213
+ # Ensure the working directory exists inside the container
214
+ if self.docker_workdir:
215
+ try:
216
+ quoted_dir = shlex.quote(self.docker_workdir)
217
+ mkdir_cmd = f'sh -lc "mkdir -p -- {quoted_dir}"'
218
+ _init = self.docker_api_client.exec_create(
219
+ self.container.id, mkdir_cmd
220
+ )
221
+ self.docker_api_client.exec_start(_init['Id'])
222
+ except Exception as e:
223
+ logger.warning(
224
+ f"[Docker] Failed to ensure workdir "
225
+ f"'{self.docker_workdir}': {e}"
226
+ )
227
+ except NotFound:
228
+ raise RuntimeError(
229
+ f"Docker container '{docker_container_name}' not found."
230
+ )
231
+ except APIError as e:
232
+ raise RuntimeError(f"Failed to connect to Docker daemon: {e}")
233
+
234
+ # Set up environments (only for local backend, skip in runtime mode)
235
+ if self._in_runtime:
236
+ logger.info(
237
+ "[ENV] Skipping environment setup - running inside "
238
+ "CAMEL runtime container"
239
+ )
240
+ elif not self.use_docker_backend:
241
+ if self.clone_current_env:
242
+ self._setup_cloned_environment()
243
+ else:
244
+ # Default: set up initial environment with Python 3.10
245
+ self._setup_initial_environment()
246
+ elif self.clone_current_env:
247
+ logger.info(
248
+ "[ENV CLONE] Skipping environment setup for Docker backend "
249
+ "- container is already isolated"
250
+ )
251
+
252
+ # Install dependencies
253
+ if self.install_dependencies:
254
+ self._install_dependencies()
255
+
256
+ def _setup_cloned_environment(self):
257
+ r"""Set up a cloned Python environment."""
258
+ self.cloned_env_path = os.path.join(self.working_dir, ".venv")
259
+
260
+ def update_callback(msg: str):
261
+ logger.info(f"[ENV CLONE] {msg.strip()}")
262
+
263
+ success = clone_current_environment(
264
+ self.cloned_env_path, self.working_dir, update_callback
265
+ )
266
+
267
+ if success:
268
+ # Update python executable to use the cloned environment
269
+ if self.os_type == 'Windows':
270
+ self.python_executable = os.path.join(
271
+ self.cloned_env_path, "Scripts", "python.exe"
272
+ )
273
+ else:
274
+ self.python_executable = os.path.join(
275
+ self.cloned_env_path, "bin", "python"
276
+ )
277
+ else:
278
+ logger.info(
279
+ "[ENV CLONE] Failed to create cloned environment, "
280
+ "using system Python"
281
+ )
282
+
283
+ def _install_dependencies(self):
284
+ r"""Install user specified dependencies in the current environment."""
285
+ if not self.install_dependencies:
286
+ return
287
+
288
+ logger.info("Installing dependencies...")
289
+
290
+ if self.use_docker_backend:
291
+ pkg_str = " ".join(
292
+ shlex.quote(p) for p in self.install_dependencies
293
+ )
294
+ install_cmd = f'sh -lc "pip install {pkg_str}"'
295
+
296
+ try:
297
+ exec_id = self.docker_api_client.exec_create(
298
+ self.container.id, install_cmd
299
+ )["Id"]
300
+ log = self.docker_api_client.exec_start(exec_id)
301
+ logger.info(f"Package installation output:\n{log}")
302
+
303
+ # Check exit code to ensure installation succeeded
304
+ exec_info = self.docker_api_client.exec_inspect(exec_id)
305
+ if exec_info['ExitCode'] != 0:
306
+ error_msg = (
307
+ f"Failed to install dependencies in Docker: "
308
+ f"{log.decode('utf-8', errors='ignore')}"
309
+ )
310
+ logger.error(error_msg)
311
+ raise RuntimeError(error_msg)
312
+
313
+ logger.info(
314
+ "Successfully installed all dependencies in Docker."
315
+ )
316
+ except Exception as e:
317
+ if not isinstance(e, RuntimeError):
318
+ logger.error(f"Docker dependency installation error: {e}")
319
+ raise RuntimeError(
320
+ f"Docker dependency installation error: {e}"
321
+ ) from e
322
+ raise
323
+
324
+ else:
325
+ pip_cmd = [
326
+ self.python_executable,
327
+ "-m",
328
+ "pip",
329
+ "install",
330
+ "--upgrade",
331
+ *self.install_dependencies,
332
+ ]
333
+
334
+ try:
335
+ subprocess.run(
336
+ pip_cmd,
337
+ check=True,
338
+ cwd=self.working_dir,
339
+ capture_output=True,
340
+ text=True,
341
+ timeout=300, # 5 minutes timeout for installation
342
+ )
343
+ logger.info("Successfully installed all dependencies.")
344
+ except subprocess.CalledProcessError as e:
345
+ logger.error(f"Failed to install dependencies: {e.stderr}")
346
+ raise RuntimeError(
347
+ f"Failed to install dependencies: {e.stderr}"
348
+ ) from e
349
+ except subprocess.TimeoutExpired:
350
+ logger.error(
351
+ "Dependency installation timed out after 5 minutes"
352
+ )
353
+ raise RuntimeError(
354
+ "Dependency installation timed out after 5 minutes"
355
+ )
356
+
357
+ def _setup_initial_environment(self):
358
+ r"""Set up an initial environment with Python 3.10."""
359
+ self.initial_env_path = os.path.join(self.working_dir, ".initial_env")
360
+
361
+ def update_callback(msg: str):
362
+ logger.info(f"[ENV INIT] {msg.strip()}")
363
+
364
+ # Try to ensure uv is available first
365
+ success, uv_path = ensure_uv_available(update_callback)
366
+
367
+ if success and uv_path:
368
+ success = setup_initial_env_with_uv(
369
+ self.initial_env_path,
370
+ uv_path,
371
+ self.working_dir,
372
+ update_callback,
373
+ )
374
+ else:
375
+ update_callback(
376
+ "Falling back to standard venv for environment setup\n"
377
+ )
378
+ success = setup_initial_env_with_venv(
379
+ self.initial_env_path, self.working_dir, update_callback
380
+ )
381
+
382
+ if success:
383
+ # Update python executable to use the initial environment
384
+ if self.os_type == 'Windows':
385
+ self.python_executable = os.path.join(
386
+ self.initial_env_path, "Scripts", "python.exe"
387
+ )
388
+ else:
389
+ self.python_executable = os.path.join(
390
+ self.initial_env_path, "bin", "python"
391
+ )
392
+
393
+ # Check Node.js availability
394
+ check_nodejs_availability(update_callback)
395
+ else:
396
+ logger.info(
397
+ "[ENV INIT] Failed to create initial environment, "
398
+ "using system Python"
399
+ )
400
+
401
+ def _adapt_command_for_environment(self, command: str) -> str:
402
+ r"""Adapt command to use virtual environment if available."""
403
+ # Only adapt for local backend
404
+ if self.use_docker_backend:
405
+ return command
406
+
407
+ # Check if we have any virtual environment (cloned or initial)
408
+ env_path = None
409
+ if self.cloned_env_path and os.path.exists(self.cloned_env_path):
410
+ env_path = self.cloned_env_path
411
+ elif self.initial_env_path and os.path.exists(self.initial_env_path):
412
+ env_path = self.initial_env_path
413
+
414
+ if not env_path:
415
+ return command
416
+
417
+ # Check if command starts with python or pip
418
+ command_lower = command.strip().lower()
419
+ if command_lower.startswith('python'):
420
+ # Replace 'python' with the virtual environment python
421
+ return command.replace('python', f'"{self.python_executable}"', 1)
422
+ elif command_lower.startswith('pip'):
423
+ # Replace 'pip' with python -m pip from virtual environment
424
+ return command.replace(
425
+ 'pip', f'"{self.python_executable}" -m pip', 1
426
+ )
427
+
428
+ return command
429
+
430
+ def _write_to_log(self, log_file: str, content: str) -> None:
431
+ r"""Write content to log file with optional ANSI stripping.
432
+
433
+ Args:
434
+ log_file (str): Path to the log file
435
+ content (str): Content to write
436
+ """
437
+ # Convert ANSI escape sequences to plain text
438
+ with open(log_file, "a", encoding="utf-8") as f:
439
+ f.write(_to_plain(content) + "\n")
440
+
441
+ def _sanitize_command(self, command: str) -> tuple[bool, str]:
442
+ r"""A comprehensive command sanitizer for both local and
443
+ Docker backends."""
444
+ return sanitize_command(
445
+ command=command,
446
+ use_docker_backend=self.use_docker_backend,
447
+ safe_mode=self.safe_mode,
448
+ working_dir=self.working_dir,
449
+ allowed_commands=self.allowed_commands,
450
+ )
451
+
452
+ def _start_output_reader_thread(self, session_id: str):
453
+ r"""Starts a thread to read stdout from a non-blocking process."""
454
+ with self._session_lock:
455
+ session = self.shell_sessions[session_id]
456
+
457
+ def reader():
458
+ try:
459
+ if session["backend"] == "local":
460
+ # For local processes, read line by line from stdout
461
+ try:
462
+ for line in iter(
463
+ session["process"].stdout.readline, ''
464
+ ):
465
+ session["output_stream"].put(line)
466
+ self._write_to_log(session["log_file"], line)
467
+ finally:
468
+ session["process"].stdout.close()
469
+ elif session["backend"] == "docker":
470
+ # For Docker, read from the raw socket
471
+ socket = session["process"]._sock
472
+ while True:
473
+ # Check if the socket is still open before reading
474
+ if socket.fileno() == -1:
475
+ break
476
+ try:
477
+ ready, _, _ = select.select([socket], [], [], 0.1)
478
+ except (ValueError, OSError):
479
+ # Socket may have been closed by another thread
480
+ break
481
+ if ready:
482
+ data = socket.recv(4096)
483
+ if not data:
484
+ break
485
+ decoded_data = data.decode(
486
+ 'utf-8', errors='ignore'
487
+ )
488
+ session["output_stream"].put(decoded_data)
489
+ self._write_to_log(
490
+ session["log_file"], decoded_data
491
+ )
492
+ # Check if the process is still running
493
+ if not self.docker_api_client.exec_inspect(
494
+ session["exec_id"]
495
+ )['Running']:
496
+ break
497
+ except Exception as e:
498
+ # Log the exception for diagnosis and store it on the session
499
+ logger.exception(f"[SESSION {session_id}] Reader thread error")
500
+ try:
501
+ with self._session_lock:
502
+ if session_id in self.shell_sessions:
503
+ self.shell_sessions[session_id]["error"] = str(e)
504
+ except Exception as cleanup_error:
505
+ logger.warning(
506
+ f"[SESSION {session_id}] Failed to store error state: "
507
+ f"{cleanup_error}"
508
+ )
509
+ finally:
510
+ try:
511
+ with self._session_lock:
512
+ if session_id in self.shell_sessions:
513
+ self.shell_sessions[session_id]["running"] = False
514
+ except Exception:
515
+ pass
516
+
517
+ thread = threading.Thread(target=reader, daemon=True)
518
+ thread.start()
519
+
520
+ def _collect_output_until_idle(
521
+ self,
522
+ id: str,
523
+ idle_duration: float = 0.5,
524
+ check_interval: float = 0.1,
525
+ max_wait: float = 5.0,
526
+ ) -> str:
527
+ r"""Collects output from a session until it's idle or a max wait time
528
+ is reached.
529
+
530
+ Args:
531
+ id (str): The session ID.
532
+ idle_duration (float): How long the stream must be empty to be
533
+ considered idle.(default: 0.5)
534
+ check_interval (float): The time to sleep between checks.
535
+ (default: 0.1)
536
+ max_wait (float): The maximum total time to wait for the process
537
+ to go idle. (default: 5.0)
538
+
539
+ Returns:
540
+ str: The collected output. If max_wait is reached while
541
+ the process is still outputting, a warning is appended.
542
+ """
543
+ with self._session_lock:
544
+ if id not in self.shell_sessions:
545
+ return f"Error: No session found with ID '{id}'."
546
+
547
+ output_parts = []
548
+ idle_time = 0.0
549
+ start_time = time.time()
550
+
551
+ while time.time() - start_time < max_wait:
552
+ new_output = self.shell_view(id)
553
+
554
+ # Check for terminal state messages from shell_view
555
+ if "--- SESSION TERMINATED ---" in new_output:
556
+ # Append the final output before the termination message
557
+ final_part = new_output.replace(
558
+ "--- SESSION TERMINATED ---", ""
559
+ ).strip()
560
+ if final_part:
561
+ output_parts.append(final_part)
562
+ # Session is dead, return what we have plus the message
563
+ return "".join(output_parts) + "\n--- SESSION TERMINATED ---"
564
+
565
+ if new_output.startswith("Error: No session found"):
566
+ return new_output
567
+
568
+ if new_output:
569
+ output_parts.append(new_output)
570
+ idle_time = 0.0 # Reset idle timer
571
+ else:
572
+ idle_time += check_interval
573
+ if idle_time >= idle_duration:
574
+ # Process is idle, success
575
+ return "".join(output_parts)
576
+ time.sleep(check_interval)
577
+
578
+ # If we exit the loop, it means max_wait was reached.
579
+ # Check one last time for any final output.
580
+ final_output = self.shell_view(id)
581
+ if final_output:
582
+ output_parts.append(final_output)
583
+
584
+ warning_message = (
585
+ "\n--- WARNING: Process is still actively outputting "
586
+ "after max wait time. Consider waiting before "
587
+ "sending the next command. ---"
588
+ )
589
+ return "".join(output_parts) + warning_message
590
+
591
+ def shell_exec(self, id: str, command: str, block: bool = True) -> str:
592
+ r"""Executes a shell command in blocking or non-blocking mode.
593
+
594
+ Args:
595
+ id (str): A unique identifier for the command's session. This ID is
596
+ used to interact with non-blocking processes.
597
+ command (str): The shell command to execute.
598
+ block (bool, optional): Determines the execution mode. Defaults to
599
+ True. If `True` (blocking mode), the function waits for the
600
+ command to complete and returns the full output. Use this for
601
+ most commands . If `False` (non-blocking mode), the function
602
+ starts the command in the background. Use this only for
603
+ interactive sessions or long-running tasks, or servers.
604
+
605
+ Returns:
606
+ str: The output of the command execution, which varies by mode.
607
+ In blocking mode, returns the complete standard output and
608
+ standard error from the command.
609
+ In non-blocking mode, returns a confirmation message with the
610
+ session `id`. To interact with the background process, use
611
+ other functions: `shell_view(id)` to see output,
612
+ `shell_write_to_process(id, "input")` to send input, and
613
+ `shell_kill_process(id)` to terminate.
614
+ """
615
+ if self.safe_mode:
616
+ is_safe, message = self._sanitize_command(command)
617
+ if not is_safe:
618
+ return f"Error: {message}"
619
+ command = message
620
+
621
+ if self.use_docker_backend:
622
+ # For Docker, we always run commands in a shell
623
+ # to support complex commands
624
+ command = f'bash -c "{command}"'
625
+ else:
626
+ # For local execution, check if we need to use cloned environment
627
+ command = self._adapt_command_for_environment(command)
628
+
629
+ session_id = id
630
+
631
+ if block:
632
+ # --- BLOCKING EXECUTION ---
633
+ log_entry = (
634
+ f"--- Executing blocking command at "
635
+ f"{time.ctime()} ---\n> {command}\n"
636
+ )
637
+ output = ""
638
+ try:
639
+ if not self.use_docker_backend:
640
+ # LOCAL BLOCKING
641
+ result = subprocess.run(
642
+ command,
643
+ capture_output=True,
644
+ text=True,
645
+ shell=True,
646
+ timeout=self.timeout,
647
+ cwd=self.working_dir,
648
+ encoding="utf-8",
649
+ )
650
+ stdout = result.stdout or ""
651
+ stderr = result.stderr or ""
652
+ output = stdout + (
653
+ f"\nSTDERR:\n{stderr}" if stderr else ""
654
+ )
655
+ else:
656
+ # DOCKER BLOCKING
657
+ assert (
658
+ self.docker_workdir is not None
659
+ ) # Docker backend always has workdir
660
+ exec_instance = self.docker_api_client.exec_create(
661
+ self.container.id, command, workdir=self.docker_workdir
662
+ )
663
+ exec_output = self.docker_api_client.exec_start(
664
+ exec_instance['Id']
665
+ )
666
+ output = exec_output.decode('utf-8', errors='ignore')
667
+
668
+ log_entry += f"--- Output ---\n{output}\n"
669
+ if output.strip():
670
+ return _to_plain(output)
671
+ else:
672
+ return "Command executed successfully (no output)."
673
+ except subprocess.TimeoutExpired:
674
+ error_msg = (
675
+ f"Error: Command timed out after {self.timeout} seconds."
676
+ )
677
+ log_entry += f"--- Error ---\n{error_msg}\n"
678
+ return error_msg
679
+ except Exception as e:
680
+ if (
681
+ isinstance(e, (subprocess.TimeoutExpired, TimeoutError))
682
+ or "timed out" in str(e).lower()
683
+ ):
684
+ error_msg = (
685
+ f"Error: Command timed out after "
686
+ f"{self.timeout} seconds."
687
+ )
688
+ else:
689
+ error_msg = f"Error executing command: {e}"
690
+ log_entry += f"--- Error ---\n{error_msg}\n"
691
+ return error_msg
692
+ finally:
693
+ self._write_to_log(self.blocking_log_file, log_entry + "\n")
694
+ else:
695
+ # --- NON-BLOCKING EXECUTION ---
696
+ session_log_file = os.path.join(
697
+ self.log_dir, f"session_{session_id}.log"
698
+ )
699
+
700
+ self._write_to_log(
701
+ session_log_file,
702
+ f"--- Starting non-blocking session at {time.ctime()} ---\n"
703
+ f"> {command}\n",
704
+ )
705
+
706
+ # PYTHONUNBUFFERED=1 for real-time output
707
+ # Without this, Python subprocesses buffer output (4KB buffer)
708
+ # and shell_view() won't see output until buffer fills or process
709
+ # exits
710
+ env_vars = os.environ.copy()
711
+ env_vars["PYTHONUNBUFFERED"] = "1"
712
+ docker_env = {"PYTHONUNBUFFERED": "1"}
713
+
714
+ with self._session_lock:
715
+ self.shell_sessions[session_id] = {
716
+ "id": session_id,
717
+ "process": None,
718
+ "output_stream": Queue(),
719
+ "command_history": [command],
720
+ "running": True,
721
+ "log_file": session_log_file,
722
+ "backend": "docker"
723
+ if self.use_docker_backend
724
+ else "local",
725
+ }
726
+
727
+ process = None
728
+ exec_socket = None
729
+ try:
730
+ if not self.use_docker_backend:
731
+ process = subprocess.Popen(
732
+ command,
733
+ stdin=subprocess.PIPE,
734
+ stdout=subprocess.PIPE,
735
+ stderr=subprocess.STDOUT,
736
+ shell=True,
737
+ text=True,
738
+ cwd=self.working_dir,
739
+ encoding="utf-8",
740
+ env=env_vars,
741
+ )
742
+ with self._session_lock:
743
+ self.shell_sessions[session_id]["process"] = process
744
+ else:
745
+ assert (
746
+ self.docker_workdir is not None
747
+ ) # Docker backend always has workdir
748
+ exec_instance = self.docker_api_client.exec_create(
749
+ self.container.id,
750
+ command,
751
+ stdin=True,
752
+ tty=True,
753
+ workdir=self.docker_workdir,
754
+ environment=docker_env,
755
+ )
756
+ exec_id = exec_instance['Id']
757
+ exec_socket = self.docker_api_client.exec_start(
758
+ exec_id, tty=True, stream=True, socket=True
759
+ )
760
+ with self._session_lock:
761
+ self.shell_sessions[session_id]["process"] = (
762
+ exec_socket
763
+ )
764
+ self.shell_sessions[session_id]["exec_id"] = exec_id
765
+
766
+ self._start_output_reader_thread(session_id)
767
+
768
+ # Return immediately with session ID and instructions
769
+ return (
770
+ f"Session '{session_id}' started.\n\n"
771
+ f"You could use:\n"
772
+ f" - shell_view('{session_id}') - get output\n"
773
+ f" - shell_write_to_process('{session_id}', '<input>')"
774
+ f" - send input\n"
775
+ f" - shell_kill_process('{session_id}') - terminate"
776
+ )
777
+
778
+ except Exception as e:
779
+ # Clean up resources on failure
780
+ if process is not None:
781
+ try:
782
+ process.terminate()
783
+ except Exception:
784
+ pass
785
+ if exec_socket is not None:
786
+ try:
787
+ exec_socket.close()
788
+ except Exception:
789
+ pass
790
+
791
+ with self._session_lock:
792
+ if session_id in self.shell_sessions:
793
+ self.shell_sessions[session_id]["running"] = False
794
+ error_msg = f"Error starting non-blocking command: {e}"
795
+ self._write_to_log(
796
+ session_log_file, f"--- Error ---\n{error_msg}\n"
797
+ )
798
+ return error_msg
799
+
800
+ def shell_write_to_process(self, id: str, command: str) -> str:
801
+ r"""This function sends command to a running non-blocking
802
+ process and returns the resulting output after the process
803
+ becomes idle again. A newline \n is automatically appended
804
+ to the input command.
805
+
806
+ Args:
807
+ id (str): The unique session ID of the non-blocking process.
808
+ command (str): The text to write to the process's standard input.
809
+
810
+ Returns:
811
+ str: The output from the process after the command is sent.
812
+ """
813
+ with self._session_lock:
814
+ if (
815
+ id not in self.shell_sessions
816
+ or not self.shell_sessions[id]["running"]
817
+ ):
818
+ return (
819
+ f"Error: No active non-blocking "
820
+ f"session found with ID '{id}'."
821
+ )
822
+ session = self.shell_sessions[id]
823
+
824
+ # Flush any lingering output from previous commands.
825
+ self._collect_output_until_idle(id, idle_duration=0.3, max_wait=2.0)
826
+
827
+ with self._session_lock:
828
+ session["command_history"].append(command)
829
+ log_file = session["log_file"]
830
+ backend = session["backend"]
831
+ process = session["process"]
832
+
833
+ # Log command to the raw log file
834
+ self._write_to_log(log_file, f"> {command}\n")
835
+
836
+ try:
837
+ if backend == "local":
838
+ process.stdin.write(command + '\n')
839
+ process.stdin.flush()
840
+ else: # docker
841
+ socket = process._sock
842
+ socket.sendall((command + '\n').encode('utf-8'))
843
+
844
+ # Wait for and collect the new output
845
+ output = self._collect_output_until_idle(id)
846
+
847
+ if output.strip():
848
+ return output
849
+ else:
850
+ return (
851
+ f"Input sent to session '{id}' successfully (no output)."
852
+ )
853
+
854
+ except Exception as e:
855
+ return f"Error writing to session '{id}': {e}"
856
+
857
+ def shell_view(self, id: str) -> str:
858
+ r"""Retrieves new output from a non-blocking session.
859
+
860
+ This function returns only NEW output since the last call. It does NOT
861
+ wait or block - it returns immediately with whatever is available.
862
+
863
+ Args:
864
+ id (str): The unique session ID of the non-blocking process.
865
+
866
+ Returns:
867
+ str: New output if available, or a status message.
868
+ """
869
+ with self._session_lock:
870
+ if id not in self.shell_sessions:
871
+ return f"Error: No session found with ID '{id}'."
872
+ session = self.shell_sessions[id]
873
+ is_running = session["running"]
874
+
875
+ # If session is terminated, drain the queue and return
876
+ if not is_running:
877
+ final_output = []
878
+ try:
879
+ while True:
880
+ final_output.append(session["output_stream"].get_nowait())
881
+ except Empty:
882
+ pass
883
+
884
+ if final_output:
885
+ return "".join(final_output) + "\n\n--- SESSION TERMINATED ---"
886
+ else:
887
+ return "--- SESSION TERMINATED (no new output) ---"
888
+
889
+ # For running session, check for new output
890
+ output = []
891
+ try:
892
+ while True:
893
+ output.append(session["output_stream"].get_nowait())
894
+ except Empty:
895
+ pass
896
+
897
+ if output:
898
+ return "".join(output)
899
+ else:
900
+ # No new output - guide the agent
901
+ return (
902
+ "[No new output]\n"
903
+ "Session is running but idle. Actions could take:\n"
904
+ " - For interactive sessions: Send input "
905
+ "with shell_write_to_process()\n"
906
+ " - For long tasks: Check again later (don't poll "
907
+ "too frequently)"
908
+ )
909
+
910
+ def shell_kill_process(self, id: str) -> str:
911
+ r"""This function forcibly terminates a running non-blocking process.
912
+
913
+ Args:
914
+ id (str): The unique session ID of the process to kill.
915
+
916
+ Returns:
917
+ str: A confirmation message indicating the process was terminated.
918
+ """
919
+ with self._session_lock:
920
+ if (
921
+ id not in self.shell_sessions
922
+ or not self.shell_sessions[id]["running"]
923
+ ):
924
+ return f"Error: No active session found with ID '{id}'."
925
+ session = self.shell_sessions[id]
926
+ try:
927
+ if session["backend"] == "local":
928
+ session["process"].terminate()
929
+ time.sleep(0.5)
930
+ if session["process"].poll() is None:
931
+ session["process"].kill()
932
+ # Ensure stdio streams are closed to unblock reader thread
933
+ try:
934
+ if getattr(session["process"], "stdin", None):
935
+ session["process"].stdin.close()
936
+ except Exception:
937
+ pass
938
+ try:
939
+ if getattr(session["process"], "stdout", None):
940
+ session["process"].stdout.close()
941
+ except Exception:
942
+ pass
943
+ else: # docker
944
+ # Docker exec processes stop when the socket is closed.
945
+ session["process"].close()
946
+ with self._session_lock:
947
+ if id in self.shell_sessions:
948
+ self.shell_sessions[id]["running"] = False
949
+ return f"Process in session '{id}' has been terminated."
950
+ except Exception as e:
951
+ return f"Error killing process in session '{id}': {e}"
952
+
953
+ def shell_ask_user_for_help(self, id: str, prompt: str) -> str:
954
+ r"""This function pauses execution and asks a human for help
955
+ with an interactive session.
956
+
957
+ This method can handle different scenarios:
958
+ 1. If session exists: Shows session output and allows interaction
959
+ 2. If session doesn't exist: Creates a temporary session for help
960
+
961
+ Args:
962
+ id (str): The session ID of the interactive process needing help.
963
+ Can be empty string for general help without session context.
964
+ prompt (str): The question or instruction from the LLM to show the
965
+ human user (e.g., "The program is asking for a filename. Please
966
+ enter 'config.json'.").
967
+
968
+ Returns:
969
+ str: The output from the shell session after the user's command has
970
+ been executed, or help information for general queries.
971
+ """
972
+ logger.info("\n" + "=" * 60)
973
+ logger.info("LLM Agent needs your help!")
974
+ logger.info(f"PROMPT: {prompt}")
975
+
976
+ # Case 1: Session doesn't exist - offer to create one
977
+ if id not in self.shell_sessions:
978
+ try:
979
+ user_input = input("Your response: ").strip()
980
+ if not user_input:
981
+ return "No user response."
982
+ else:
983
+ logger.info(
984
+ f"Creating session '{id}' and executing command..."
985
+ )
986
+ result = self.shell_exec(id, user_input, block=True)
987
+ return (
988
+ f"Session '{id}' created and "
989
+ f"executed command:\n{result}"
990
+ )
991
+ except EOFError:
992
+ return f"User input interrupted for session '{id}' creation."
993
+
994
+ # Case 2: Session exists - show context and interact
995
+ else:
996
+ # Get the latest output to show the user the current state
997
+ last_output = self._collect_output_until_idle(id)
998
+ last_output_display = (
999
+ last_output.strip() if last_output.strip() else "(no output)"
1000
+ )
1001
+
1002
+ logger.info(f"SESSION: '{id}' (active)")
1003
+ logger.info("=" * 60)
1004
+ logger.info("--- LAST OUTPUT ---")
1005
+ logger.info(last_output_display)
1006
+ logger.info("-------------------")
1007
+
1008
+ try:
1009
+ user_input = input("Your input: ").strip()
1010
+ if not user_input:
1011
+ return f"User provided no input for session '{id}'."
1012
+ else:
1013
+ # Send input to the existing session
1014
+ return self.shell_write_to_process(id, user_input)
1015
+ except EOFError:
1016
+ return f"User input interrupted for session '{id}'."
1017
+
1018
+ def __enter__(self):
1019
+ r"""Context manager entry."""
1020
+ return self
1021
+
1022
+ def __exit__(self, exc_type, exc_val, exc_tb):
1023
+ r"""Context manager exit - clean up all sessions."""
1024
+ self.cleanup()
1025
+ return False
1026
+
1027
+ def cleanup(self):
1028
+ r"""Clean up all active sessions."""
1029
+ with self._session_lock:
1030
+ session_ids = list(self.shell_sessions.keys())
1031
+ for session_id in session_ids:
1032
+ with self._session_lock:
1033
+ is_running = self.shell_sessions.get(session_id, {}).get(
1034
+ "running", False
1035
+ )
1036
+ if is_running:
1037
+ try:
1038
+ self.shell_kill_process(session_id)
1039
+ except Exception as e:
1040
+ logger.warning(
1041
+ f"Failed to kill session '{session_id}' "
1042
+ f"during cleanup: {e}"
1043
+ )
1044
+
1045
+ cleanup._manual_timeout = True # type: ignore[attr-defined]
1046
+
1047
+ def __del__(self):
1048
+ r"""Fallback cleanup in destructor."""
1049
+ try:
1050
+ self.cleanup()
1051
+ except Exception:
1052
+ pass
1053
+
1054
+ __del__._manual_timeout = True # type: ignore[attr-defined]
1055
+
1056
+ def get_tools(self) -> List[FunctionTool]:
1057
+ r"""Returns a list of FunctionTool objects representing the functions
1058
+ in the toolkit.
1059
+
1060
+ Returns:
1061
+ List[FunctionTool]: A list of FunctionTool objects representing the
1062
+ functions in the toolkit.
1063
+ """
1064
+ return [
1065
+ FunctionTool(self.shell_exec),
1066
+ FunctionTool(self.shell_view),
1067
+ FunctionTool(self.shell_write_to_process),
1068
+ FunctionTool(self.shell_kill_process),
1069
+ FunctionTool(self.shell_ask_user_for_help),
1070
+ ]