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