camel-ai 0.2.59__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 (506) 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 +5012 -902
  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 +39 -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 +94 -0
  28. camel/benchmarks/mock_website/mock_web.py +299 -0
  29. camel/benchmarks/mock_website/requirements.txt +3 -0
  30. camel/benchmarks/mock_website/shopping_mall/app.py +465 -0
  31. camel/benchmarks/mock_website/task.json +104 -0
  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 +26 -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 +8 -7
  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 +3 -3
  52. camel/configs/cometapi_config.py +106 -0
  53. camel/configs/crynux_config.py +94 -0
  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 +3 -3
  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 +8 -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 +3 -3
  75. camel/configs/samba_config.py +8 -6
  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_collector → data_collectors}/__init__.py +2 -2
  84. camel/{data_collector → data_collectors}/alpaca_collector.py +19 -10
  85. camel/{data_collector → data_collectors}/base.py +2 -2
  86. camel/{data_collector → data_collectors}/sharegpt_collector.py +3 -3
  87. camel/datagen/__init__.py +2 -2
  88. camel/datagen/cot_datagen.py +32 -37
  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 +24 -25
  92. camel/datagen/evol_instruct/templates.py +48 -48
  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 +3 -3
  113. camel/datasets/self_instruct_generator.py +2 -2
  114. camel/datasets/static_dataset.py +152 -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 +10 -3
  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 +4 -4
  124. camel/embeddings/together_embedding.py +2 -2
  125. camel/embeddings/vlm_embedding.py +11 -4
  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 +16 -3
  139. camel/interpreters/docker/Dockerfile +53 -7
  140. camel/interpreters/docker_interpreter.py +70 -11
  141. camel/interpreters/e2b_interpreter.py +59 -11
  142. camel/interpreters/internal_python_interpreter.py +81 -4
  143. camel/interpreters/interpreter_error.py +2 -2
  144. camel/interpreters/ipython_interpreter.py +23 -5
  145. camel/interpreters/microsandbox_interpreter.py +395 -0
  146. camel/interpreters/subprocess_interpreter.py +36 -4
  147. camel/loaders/__init__.py +17 -5
  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 +128 -93
  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 +148 -0
  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 +126 -9
  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 +98 -13
  170. camel/messages/__init__.py +2 -2
  171. camel/messages/base.py +193 -46
  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 +18 -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 +263 -63
  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 +81 -21
  192. camel/models/cometapi_model.py +83 -0
  193. camel/models/crynux_model.py +87 -0
  194. camel/models/deepseek_model.py +61 -59
  195. camel/models/fish_audio_model.py +8 -2
  196. camel/models/gemini_model.py +439 -30
  197. camel/models/groq_model.py +11 -19
  198. camel/models/internlm_model.py +11 -18
  199. camel/models/litellm_model.py +94 -34
  200. camel/models/lmstudio_model.py +17 -20
  201. camel/models/minimax_model.py +83 -0
  202. camel/models/mistral_model.py +84 -19
  203. camel/models/model_factory.py +49 -6
  204. camel/models/model_manager.py +33 -11
  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 +234 -27
  215. camel/models/openai_model.py +255 -39
  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 +90 -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 +117 -49
  227. camel/models/sglang_model.py +162 -42
  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 +69 -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 +23 -3
  262. camel/retrievers/base.py +2 -2
  263. camel/retrievers/bm25_retriever.py +3 -4
  264. camel/retrievers/cohere_rerank_retriever.py +2 -2
  265. camel/retrievers/hybrid_retrival.py +4 -4
  266. camel/retrievers/vector_retriever.py +2 -2
  267. camel/runtimes/Dockerfile.multi-toolkit +90 -0
  268. camel/{runtime → runtimes}/__init__.py +2 -2
  269. camel/runtimes/api.py +153 -0
  270. camel/{runtime → runtimes}/base.py +2 -2
  271. camel/{runtime → runtimes}/configs.py +13 -13
  272. camel/{runtime → runtimes}/daytona_runtime.py +18 -19
  273. camel/{runtime → runtimes}/docker_runtime.py +13 -13
  274. camel/{runtime → runtimes}/llm_guard_runtime.py +28 -28
  275. camel/{runtime → runtimes}/remote_http_runtime.py +12 -12
  276. camel/{runtime → runtimes}/ubuntu_docker_runtime.py +3 -3
  277. camel/{runtime → runtimes}/utils/__init__.py +2 -2
  278. camel/{runtime → runtimes}/utils/function_risk_toolkit.py +2 -2
  279. camel/{runtime → 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 +9 -5
  290. camel/societies/workforce/events.py +143 -0
  291. camel/societies/workforce/prompts.py +258 -33
  292. camel/societies/workforce/role_playing_worker.py +95 -30
  293. camel/societies/workforce/single_agent_worker.py +659 -30
  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 +784 -18
  297. camel/societies/workforce/worker.py +96 -28
  298. camel/societies/workforce/workflow_memory_manager.py +1746 -0
  299. camel/societies/workforce/workforce.py +5730 -366
  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 +10 -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 +12 -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 +712 -0
  324. camel/storages/vectordb_storages/milvus.py +2 -2
  325. camel/storages/vectordb_storages/oceanbase.py +16 -17
  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 +714 -0
  331. camel/tasks/__init__.py +2 -2
  332. camel/tasks/task.py +366 -27
  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 +58 -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 +174 -575
  343. camel/toolkits/audio_analysis_toolkit.py +3 -3
  344. camel/toolkits/base.py +65 -7
  345. camel/toolkits/bohrium_toolkit.py +318 -0
  346. camel/toolkits/browser_toolkit.py +306 -566
  347. camel/toolkits/browser_toolkit_commons.py +568 -0
  348. camel/toolkits/code_execution.py +67 -11
  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 +910 -70
  357. camel/toolkits/file_toolkit.py +1402 -0
  358. camel/toolkits/function_tool.py +128 -20
  359. camel/toolkits/github_toolkit.py +148 -43
  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 -3
  395. camel/toolkits/image_generation_toolkit.py +390 -0
  396. camel/toolkits/jina_reranker_toolkit.py +195 -79
  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 +841 -600
  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 +86 -74
  434. camel/toolkits/playwright_mcp_toolkit.py +27 -32
  435. camel/toolkits/pptx_toolkit.py +790 -0
  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 +134 -0
  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 +8 -3
  456. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  457. camel/toolkits/video_analysis_toolkit.py +112 -29
  458. camel/toolkits/video_download_toolkit.py +22 -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 +53 -25
  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 +454 -35
  469. camel/types/mcp_registries.py +2 -2
  470. camel/types/openai_types.py +4 -4
  471. camel/types/unified_model_type.py +43 -6
  472. camel/utils/__init__.py +20 -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 +65 -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 +258 -0
  484. camel/utils/mcp.py +140 -6
  485. camel/utils/mcp_client.py +1056 -0
  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.59.dist-info → camel_ai-0.2.82.dist-info}/METADATA +349 -108
  497. camel_ai-0.2.82.dist-info/RECORD +507 -0
  498. {camel_ai-0.2.59.dist-info → camel_ai-0.2.82.dist-info}/WHEEL +1 -1
  499. {camel_ai-0.2.59.dist-info → camel_ai-0.2.82.dist-info}/licenses/LICENSE +1 -1
  500. camel/loaders/pandas_reader.py +0 -368
  501. camel/runtime/api.py +0 -97
  502. camel/toolkits/dalle_toolkit.py +0 -171
  503. camel/toolkits/file_write_toolkit.py +0 -395
  504. camel/toolkits/openai_agent_toolkit.py +0 -135
  505. camel/toolkits/terminal_toolkit.py +0 -1037
  506. camel_ai-0.2.59.dist-info/RECORD +0 -410
@@ -0,0 +1,1037 @@
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
+
15
+ import asyncio
16
+ import contextlib
17
+ import datetime
18
+ import json
19
+ import os
20
+ import subprocess
21
+ import time
22
+ import uuid
23
+ from contextvars import ContextVar
24
+ from functools import wraps
25
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
26
+
27
+ if TYPE_CHECKING:
28
+ import websockets
29
+ else:
30
+ try:
31
+ import websockets
32
+ except ImportError:
33
+ websockets = None
34
+
35
+ from camel.logger import get_logger
36
+ from camel.utils.tool_result import ToolResult
37
+
38
+ from .installer import check_and_install_dependencies
39
+
40
+ logger = get_logger(__name__)
41
+
42
+ # Context variable to track if we're inside a high-level action
43
+ _in_high_level_action: ContextVar[bool] = ContextVar(
44
+ '_in_high_level_action', default=False
45
+ )
46
+
47
+
48
+ def _create_memory_aware_error(base_msg: str) -> str:
49
+ import psutil
50
+
51
+ mem = psutil.virtual_memory()
52
+ if mem.available < 1024**3:
53
+ return (
54
+ f"{base_msg} "
55
+ f"(likely due to insufficient memory). "
56
+ f"Available memory: {mem.available / 1024**3:.2f}GB "
57
+ f"({mem.percent}% used)"
58
+ )
59
+ return base_msg
60
+
61
+
62
+ async def _cleanup_process_and_tasks(process, log_reader_task, ts_log_file):
63
+ if process:
64
+ with contextlib.suppress(ProcessLookupError, Exception):
65
+ process.kill()
66
+ with contextlib.suppress(Exception):
67
+ process.wait(timeout=2)
68
+
69
+ if log_reader_task and not log_reader_task.done():
70
+ log_reader_task.cancel()
71
+ with contextlib.suppress(asyncio.CancelledError):
72
+ await log_reader_task
73
+
74
+ if ts_log_file:
75
+ with contextlib.suppress(Exception):
76
+ ts_log_file.close()
77
+
78
+
79
+ def action_logger(func):
80
+ """Decorator to add logging to action methods.
81
+
82
+ Skips logging if already inside a high-level action to avoid
83
+ logging internal calls.
84
+ """
85
+
86
+ @wraps(func)
87
+ async def wrapper(self, *args, **kwargs):
88
+ # Skip logging if we're already inside a high-level action
89
+ if _in_high_level_action.get():
90
+ return await func(self, *args, **kwargs)
91
+
92
+ action_name = func.__name__
93
+ start_time = time.time()
94
+
95
+ inputs = {
96
+ "args": args,
97
+ "kwargs": kwargs,
98
+ }
99
+
100
+ try:
101
+ result = await func(self, *args, **kwargs)
102
+ execution_time = time.time() - start_time
103
+
104
+ page_load_time = None
105
+ if isinstance(result, dict) and 'page_load_time_ms' in result:
106
+ page_load_time = result['page_load_time_ms'] / 1000.0
107
+
108
+ await self._log_action(
109
+ action_name=action_name,
110
+ inputs=inputs,
111
+ outputs=result,
112
+ execution_time=execution_time,
113
+ page_load_time=page_load_time,
114
+ )
115
+
116
+ return result
117
+
118
+ except Exception as e:
119
+ execution_time = time.time() - start_time
120
+ error_msg = f"{type(e).__name__}: {e!s}"
121
+
122
+ await self._log_action(
123
+ action_name=action_name,
124
+ inputs=inputs,
125
+ outputs=None,
126
+ execution_time=execution_time,
127
+ error=error_msg,
128
+ )
129
+
130
+ raise
131
+
132
+ return wrapper
133
+
134
+
135
+ def high_level_action(func):
136
+ """Decorator for high-level actions that should suppress low-level logging.
137
+
138
+ When a function is decorated with this, all low-level action_logger
139
+ decorated functions called within it will skip logging. This decorator
140
+ itself will log the high-level action.
141
+ """
142
+
143
+ @wraps(func)
144
+ async def wrapper(self, *args, **kwargs):
145
+ action_name = func.__name__
146
+ start_time = time.time()
147
+
148
+ inputs = {
149
+ "args": args,
150
+ "kwargs": kwargs,
151
+ }
152
+
153
+ # Set the context variable to indicate we're in a high-level action
154
+ token = _in_high_level_action.set(True)
155
+ try:
156
+ result = await func(self, *args, **kwargs)
157
+ execution_time = time.time() - start_time
158
+
159
+ # Log the high-level action
160
+ if hasattr(self, '_get_ws_wrapper'):
161
+ # This is a HybridBrowserToolkit instance
162
+ ws_wrapper = await self._get_ws_wrapper()
163
+ await ws_wrapper._log_action(
164
+ action_name=action_name,
165
+ inputs=inputs,
166
+ outputs=result,
167
+ execution_time=execution_time,
168
+ page_load_time=None,
169
+ )
170
+
171
+ return result
172
+
173
+ except Exception as e:
174
+ execution_time = time.time() - start_time
175
+ error_msg = f"{type(e).__name__}: {e!s}"
176
+
177
+ # Log the error
178
+ if hasattr(self, '_get_ws_wrapper'):
179
+ ws_wrapper = await self._get_ws_wrapper()
180
+ await ws_wrapper._log_action(
181
+ action_name=action_name,
182
+ inputs=inputs,
183
+ outputs=None,
184
+ execution_time=execution_time,
185
+ error=error_msg,
186
+ )
187
+
188
+ raise
189
+ finally:
190
+ # Reset the context variable
191
+ _in_high_level_action.reset(token)
192
+
193
+ return wrapper
194
+
195
+
196
+ class WebSocketBrowserWrapper:
197
+ """Python wrapper for the TypeScript hybrid browser
198
+ toolkit implementation using WebSocket."""
199
+
200
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
201
+ """Initialize the wrapper.
202
+
203
+ Args:
204
+ config: Configuration dictionary for the browser toolkit
205
+ """
206
+ if websockets is None:
207
+ raise ImportError(
208
+ "websockets package is required for WebSocket communication. "
209
+ "Install with: pip install websockets"
210
+ )
211
+
212
+ self.config = config or {}
213
+ self.ts_dir = os.path.join(os.path.dirname(__file__), 'ts')
214
+ self.process: Optional[subprocess.Popen] = None
215
+ self.websocket = None
216
+ self.server_port = None
217
+ self._send_lock = asyncio.Lock()
218
+ self._request_timeout = self.config.get(
219
+ 'requestTimeout', 60
220
+ ) # seconds
221
+ self._receive_task = None
222
+ self._pending_responses: Dict[str, asyncio.Future[Dict[str, Any]]] = {}
223
+ self._browser_opened = False
224
+ self._server_ready_future = None
225
+
226
+ self.browser_log_to_file = (config or {}).get(
227
+ 'browser_log_to_file', False
228
+ )
229
+ self.log_dir = (config or {}).get('log_dir', 'browser_log')
230
+ self.session_id = (config or {}).get('session_id', 'default')
231
+ self.log_file_path: Optional[str] = None
232
+ self.log_buffer: List[Dict[str, Any]] = []
233
+ self.ts_log_file_path: Optional[str] = None
234
+ self.ts_log_file = None
235
+ self._log_reader_task = None
236
+
237
+ if self.browser_log_to_file:
238
+ log_dir = self.log_dir if self.log_dir else "browser_log"
239
+ os.makedirs(log_dir, exist_ok=True)
240
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
241
+ self.log_file_path = os.path.join(
242
+ log_dir,
243
+ f"hybrid_browser_toolkit_ws_{timestamp}_{self.session_id}.log",
244
+ )
245
+ self.ts_log_file_path = os.path.join(
246
+ log_dir,
247
+ f"typescript_console_{timestamp}_{self.session_id}.log",
248
+ )
249
+
250
+ async def __aenter__(self):
251
+ """Async context manager entry."""
252
+ await self.start()
253
+ return self
254
+
255
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
256
+ """Async context manager exit."""
257
+ await self.stop()
258
+
259
+ async def _cleanup_existing_processes(self):
260
+ """Clean up any existing Node.js WebSocket server processes."""
261
+ import psutil
262
+
263
+ cleaned_count = 0
264
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
265
+ try:
266
+ if (
267
+ proc.info['name']
268
+ and 'node' in proc.info['name'].lower()
269
+ and proc.info['cmdline']
270
+ and any(
271
+ 'websocket-server.js' in arg
272
+ for arg in proc.info['cmdline']
273
+ )
274
+ ):
275
+ if any(self.ts_dir in arg for arg in proc.info['cmdline']):
276
+ logger.warning(
277
+ f"Found existing WebSocket server process "
278
+ f"(PID: {proc.info['pid']}). "
279
+ f"Terminating it to prevent conflicts."
280
+ )
281
+ proc.terminate()
282
+ try:
283
+ proc.wait(timeout=3)
284
+ except psutil.TimeoutExpired:
285
+ proc.kill()
286
+ cleaned_count += 1
287
+ except (
288
+ psutil.NoSuchProcess,
289
+ psutil.AccessDenied,
290
+ psutil.ZombieProcess,
291
+ ):
292
+ pass
293
+
294
+ if cleaned_count > 0:
295
+ logger.warning(
296
+ f"Cleaned up {cleaned_count} existing WebSocket server "
297
+ f"process(es). This may have been caused by improper "
298
+ f"shutdown in previous sessions."
299
+ )
300
+ await asyncio.sleep(0.5)
301
+
302
+ async def start(self):
303
+ """Start the WebSocket server and connect to it."""
304
+ await self._cleanup_existing_processes()
305
+
306
+ npm_cmd, node_cmd = await check_and_install_dependencies(self.ts_dir)
307
+
308
+ import platform
309
+
310
+ use_shell = platform.system() == 'Windows'
311
+
312
+ self.process = subprocess.Popen(
313
+ [node_cmd, 'websocket-server.js'],
314
+ cwd=self.ts_dir,
315
+ stdout=subprocess.PIPE,
316
+ stderr=subprocess.STDOUT,
317
+ text=True,
318
+ encoding='utf-8',
319
+ bufsize=1,
320
+ shell=use_shell,
321
+ )
322
+
323
+ self._server_ready_future = asyncio.get_running_loop().create_future()
324
+
325
+ self._log_reader_task = asyncio.create_task(
326
+ self._read_and_log_output()
327
+ )
328
+
329
+ if self.browser_log_to_file and self.ts_log_file_path:
330
+ logger.info(
331
+ f"TypeScript console logs will be written to: "
332
+ f"{self.ts_log_file_path}"
333
+ )
334
+
335
+ server_ready = False
336
+ timeout = 10
337
+
338
+ try:
339
+ await asyncio.wait_for(self._server_ready_future, timeout=timeout)
340
+ server_ready = True
341
+ except asyncio.TimeoutError:
342
+ server_ready = False
343
+
344
+ if not server_ready:
345
+ await _cleanup_process_and_tasks(
346
+ self.process,
347
+ self._log_reader_task,
348
+ getattr(self, 'ts_log_file', None),
349
+ )
350
+ self.ts_log_file = None
351
+ self.process = None
352
+
353
+ error_msg = _create_memory_aware_error(
354
+ "WebSocket server failed to start within timeout"
355
+ )
356
+ raise RuntimeError(error_msg)
357
+
358
+ max_retries = 3
359
+ retry_delays = [1, 2, 4]
360
+
361
+ for attempt in range(max_retries):
362
+ try:
363
+ connect_timeout = 10.0 + (attempt * 5.0)
364
+
365
+ logger.info(
366
+ f"Attempting to connect to WebSocket server "
367
+ f"(attempt {attempt + 1}/{max_retries}, "
368
+ f"timeout: {connect_timeout}s)"
369
+ )
370
+
371
+ self.websocket = await asyncio.wait_for(
372
+ websockets.connect(
373
+ f"ws://localhost:{self.server_port}",
374
+ ping_interval=30,
375
+ ping_timeout=10,
376
+ max_size=50 * 1024 * 1024,
377
+ ),
378
+ timeout=connect_timeout,
379
+ )
380
+ logger.info("Connected to WebSocket server")
381
+ break
382
+
383
+ except asyncio.TimeoutError:
384
+ if attempt < max_retries - 1:
385
+ delay = retry_delays[attempt]
386
+ logger.warning(
387
+ f"WebSocket handshake timeout "
388
+ f"(attempt {attempt + 1}/{max_retries}). "
389
+ f"Retrying in {delay} seconds..."
390
+ )
391
+ await asyncio.sleep(delay)
392
+ else:
393
+ raise RuntimeError(
394
+ f"Failed to connect to WebSocket server after "
395
+ f"{max_retries} attempts: Handshake timeout"
396
+ )
397
+
398
+ except Exception as e:
399
+ if attempt < max_retries - 1 and "timed out" in str(e).lower():
400
+ delay = retry_delays[attempt]
401
+ logger.warning(
402
+ f"WebSocket connection failed "
403
+ f"(attempt {attempt + 1}/{max_retries}): {e}. "
404
+ f"Retrying in {delay} seconds..."
405
+ )
406
+ await asyncio.sleep(delay)
407
+ else:
408
+ break
409
+
410
+ if not self.websocket:
411
+ await _cleanup_process_and_tasks(
412
+ self.process,
413
+ self._log_reader_task,
414
+ getattr(self, 'ts_log_file', None),
415
+ )
416
+ self.ts_log_file = None
417
+ self.process = None
418
+
419
+ error_msg = _create_memory_aware_error(
420
+ "Failed to connect to WebSocket server after multiple attempts"
421
+ )
422
+ raise RuntimeError(error_msg)
423
+
424
+ self._receive_task = asyncio.create_task(self._receive_loop())
425
+
426
+ await self._send_command('init', self.config)
427
+
428
+ if self.config.get('cdpUrl'):
429
+ self._browser_opened = True
430
+
431
+ async def stop(self):
432
+ """Stop the WebSocket connection and server."""
433
+ if self.websocket:
434
+ with contextlib.suppress(asyncio.TimeoutError, Exception):
435
+ await asyncio.wait_for(
436
+ self._send_command('shutdown', {}),
437
+ timeout=2.0,
438
+ )
439
+
440
+ with contextlib.suppress(Exception):
441
+ await self.websocket.close()
442
+ self.websocket = None
443
+
444
+ self._browser_opened = False
445
+
446
+ # Gracefully stop the Node process before cancelling the log reader
447
+ if self.process:
448
+ try:
449
+ # give the process a short grace period to exit after shutdown
450
+ self.process.wait(timeout=2)
451
+ except subprocess.TimeoutExpired:
452
+ try:
453
+ self.process.terminate()
454
+ self.process.wait(timeout=3)
455
+ except subprocess.TimeoutExpired:
456
+ with contextlib.suppress(ProcessLookupError, Exception):
457
+ self.process.kill()
458
+ self.process.wait()
459
+ except Exception as e:
460
+ logger.warning(f"Error terminating process: {e}")
461
+ except Exception as e:
462
+ logger.warning(f"Error waiting for process: {e}")
463
+
464
+ # Now cancel background tasks (reader won't block on readline)
465
+ tasks_to_cancel = [
466
+ ('_receive_task', self._receive_task),
467
+ ('_log_reader_task', self._log_reader_task),
468
+ ]
469
+ for _, task in tasks_to_cancel:
470
+ if task and not task.done():
471
+ task.cancel()
472
+ with contextlib.suppress(asyncio.CancelledError):
473
+ await task
474
+
475
+ # Close TS log file if open
476
+ if getattr(self, 'ts_log_file', None):
477
+ with contextlib.suppress(Exception):
478
+ self.ts_log_file.close()
479
+ self.ts_log_file = None
480
+
481
+ # Ensure process handle cleared
482
+ self.process = None
483
+
484
+ async def disconnect_only(self):
485
+ """Disconnect WebSocket and stop server without closing the browser.
486
+
487
+ This is useful for CDP mode where the browser should remain open.
488
+ """
489
+ if self.websocket:
490
+ with contextlib.suppress(Exception):
491
+ await self.websocket.close()
492
+ self.websocket = None
493
+
494
+ self._browser_opened = False
495
+
496
+ # Stop the Node process
497
+ if self.process:
498
+ try:
499
+ # Send SIGTERM to gracefully shutdown
500
+ self.process.terminate()
501
+ self.process.wait(timeout=3)
502
+ except subprocess.TimeoutExpired:
503
+ # Force kill if needed
504
+ with contextlib.suppress(ProcessLookupError, Exception):
505
+ self.process.kill()
506
+ self.process.wait()
507
+ except Exception as e:
508
+ logger.warning(f"Error terminating process: {e}")
509
+
510
+ # Cancel background tasks
511
+ tasks_to_cancel = [
512
+ ('_receive_task', self._receive_task),
513
+ ('_log_reader_task', self._log_reader_task),
514
+ ]
515
+ for _, task in tasks_to_cancel:
516
+ if task and not task.done():
517
+ task.cancel()
518
+ with contextlib.suppress(asyncio.CancelledError):
519
+ await task
520
+
521
+ # Close TS log file if open
522
+ if getattr(self, 'ts_log_file', None):
523
+ with contextlib.suppress(Exception):
524
+ self.ts_log_file.close()
525
+ self.ts_log_file = None
526
+
527
+ # Ensure process handle cleared
528
+ self.process = None
529
+
530
+ logger.info("WebSocket disconnected without closing browser")
531
+
532
+ async def _log_action(
533
+ self,
534
+ action_name: str,
535
+ inputs: Dict[str, Any],
536
+ outputs: Any,
537
+ execution_time: float,
538
+ page_load_time: Optional[float] = None,
539
+ error: Optional[str] = None,
540
+ ) -> None:
541
+ """Log action details with comprehensive
542
+ information including detailed timing breakdown."""
543
+ if not self.browser_log_to_file or not self.log_file_path:
544
+ return
545
+
546
+ # Create log entry
547
+ log_entry = {
548
+ "timestamp": datetime.datetime.now().isoformat(),
549
+ "session_id": self.session_id,
550
+ "action": action_name,
551
+ "execution_time_ms": round(execution_time * 1000, 2),
552
+ "inputs": inputs,
553
+ }
554
+
555
+ if error:
556
+ log_entry["error"] = error
557
+ else:
558
+ # Handle ToolResult objects for JSON serialization
559
+ if hasattr(outputs, 'text') and hasattr(outputs, 'images'):
560
+ # This is a ToolResult object
561
+ log_entry["outputs"] = {
562
+ "text": outputs.text,
563
+ "images_count": len(outputs.images)
564
+ if outputs.images
565
+ else 0,
566
+ }
567
+ else:
568
+ log_entry["outputs"] = outputs
569
+
570
+ if page_load_time is not None:
571
+ log_entry["page_load_time_ms"] = round(page_load_time * 1000, 2)
572
+
573
+ # Write to log file
574
+ try:
575
+ with open(self.log_file_path, 'a', encoding='utf-8') as f:
576
+ f.write(
577
+ json.dumps(log_entry, ensure_ascii=False, indent=2) + '\n'
578
+ )
579
+ except Exception as e:
580
+ logger.error(f"Failed to write to log file: {e}")
581
+
582
+ async def _receive_loop(self):
583
+ r"""Background task to receive messages from WebSocket."""
584
+ try:
585
+ while self.websocket:
586
+ try:
587
+ response_data = await self.websocket.recv()
588
+ response = json.loads(response_data)
589
+
590
+ message_id = response.get('id')
591
+ if message_id and message_id in self._pending_responses:
592
+ # Set the result for the waiting coroutine
593
+ future = self._pending_responses.pop(message_id)
594
+ if not future.done():
595
+ future.set_result(response)
596
+ else:
597
+ # Log unexpected messages
598
+ logger.warning(
599
+ f"Received unexpected message: {response}"
600
+ )
601
+
602
+ except asyncio.CancelledError:
603
+ break
604
+ except Exception as e:
605
+ # Check if it's a normal WebSocket close
606
+ if isinstance(e, websockets.exceptions.ConnectionClosed):
607
+ if e.code == 1000: # Normal closure
608
+ logger.debug(f"WebSocket closed normally: {e}")
609
+ else:
610
+ logger.warning(
611
+ f"WebSocket closed with code {e.code}: {e}"
612
+ )
613
+ else:
614
+ logger.error(f"Error in receive loop: {e}")
615
+ # Notify all pending futures of the error
616
+ for future in self._pending_responses.values():
617
+ if not future.done():
618
+ future.set_exception(e)
619
+ self._pending_responses.clear()
620
+ break
621
+ finally:
622
+ logger.debug("Receive loop terminated")
623
+
624
+ async def _ensure_connection(self) -> None:
625
+ """Ensure WebSocket connection is alive."""
626
+ if not self.websocket:
627
+ error_msg = _create_memory_aware_error("WebSocket not connected")
628
+ raise RuntimeError(error_msg)
629
+
630
+ # Check if connection is still alive
631
+ try:
632
+ # Send a ping and wait for the corresponding pong (bounded wait)
633
+ pong_waiter = await self.websocket.ping()
634
+ await asyncio.wait_for(pong_waiter, timeout=5.0)
635
+ except Exception as e:
636
+ logger.warning(f"WebSocket ping failed: {e}")
637
+ self.websocket = None
638
+
639
+ error_msg = _create_memory_aware_error("WebSocket connection lost")
640
+ raise RuntimeError(error_msg)
641
+
642
+ async def _send_command(
643
+ self, command: str, params: Dict[str, Any]
644
+ ) -> Dict[str, Any]:
645
+ """Send a command to the WebSocket server and get response."""
646
+ await self._ensure_connection()
647
+
648
+ # Process params to ensure refs have 'e' prefix
649
+ params = self._process_refs_in_params(params)
650
+
651
+ message_id = str(uuid.uuid4())
652
+ message = {'id': message_id, 'command': command, 'params': params}
653
+
654
+ # Create a future for this message
655
+ loop = asyncio.get_running_loop()
656
+ future: asyncio.Future[Dict[str, Any]] = loop.create_future()
657
+ self._pending_responses[message_id] = future
658
+
659
+ try:
660
+ # Use lock only for sending to prevent interleaved messages
661
+ async with self._send_lock:
662
+ if self.websocket is None:
663
+ raise RuntimeError("WebSocket connection not established")
664
+ await self.websocket.send(json.dumps(message))
665
+
666
+ # Wait for response (no lock needed, handled by background
667
+ # receiver)
668
+ try:
669
+ response = await asyncio.wait_for(
670
+ future, timeout=self._request_timeout
671
+ )
672
+
673
+ if not response.get('success'):
674
+ raise RuntimeError(
675
+ f"Command failed: {response.get('error')}"
676
+ )
677
+ return response['result']
678
+
679
+ except asyncio.TimeoutError:
680
+ # Remove from pending if timeout
681
+ self._pending_responses.pop(message_id, None)
682
+ # Special handling for shutdown command
683
+ if command == 'shutdown':
684
+ logger.debug(
685
+ "Shutdown command timeout is expected - "
686
+ "server may have closed before responding"
687
+ )
688
+ # Return a success response for shutdown
689
+ return {
690
+ 'message': 'Browser shutdown (no response received)'
691
+ }
692
+ raise RuntimeError(
693
+ f"Timeout waiting for response to command: {command}"
694
+ )
695
+
696
+ except Exception as e:
697
+ # Clean up the pending response
698
+ self._pending_responses.pop(message_id, None)
699
+
700
+ # Check if it's a connection closed error
701
+ if (
702
+ "close frame" in str(e)
703
+ or "connection closed" in str(e).lower()
704
+ ):
705
+ # Special handling for shutdown command
706
+ if command == 'shutdown':
707
+ logger.debug(
708
+ f"Connection closed during shutdown (expected): {e}"
709
+ )
710
+ return {'message': 'Browser shutdown (connection closed)'}
711
+ logger.error(f"WebSocket connection closed unexpectedly: {e}")
712
+ # Mark connection as closed
713
+ self.websocket = None
714
+ raise RuntimeError(
715
+ f"WebSocket connection lost "
716
+ f"during {command} operation: {e}"
717
+ )
718
+ else:
719
+ logger.error(f"WebSocket communication error: {e}")
720
+ raise
721
+
722
+ # Browser action methods
723
+ @action_logger
724
+ async def open_browser(
725
+ self, start_url: Optional[str] = None
726
+ ) -> Dict[str, Any]:
727
+ """Open browser."""
728
+ response = await self._send_command(
729
+ 'open_browser', {'startUrl': start_url}
730
+ )
731
+ self._browser_opened = True
732
+ return response
733
+
734
+ @action_logger
735
+ async def close_browser(self) -> str:
736
+ """Close browser."""
737
+ response = await self._send_command('close_browser', {})
738
+ self._browser_opened = False
739
+ return response['message']
740
+
741
+ @action_logger
742
+ async def visit_page(self, url: str) -> Dict[str, Any]:
743
+ """Visit a page.
744
+
745
+ In non-CDP mode, automatically opens browser if not already open.
746
+ """
747
+ if not self._browser_opened:
748
+ is_cdp_mode = bool(self.config.get('cdpUrl'))
749
+
750
+ if not is_cdp_mode:
751
+ logger.info(
752
+ "Browser not open, automatically opening browser..."
753
+ )
754
+ await self.open_browser()
755
+
756
+ response = await self._send_command('visit_page', {'url': url})
757
+ return response
758
+
759
+ @action_logger
760
+ async def get_page_snapshot(self, viewport_limit: bool = False) -> str:
761
+ """Get page snapshot."""
762
+ response = await self._send_command(
763
+ 'get_page_snapshot', {'viewport_limit': viewport_limit}
764
+ )
765
+ # The backend returns the snapshot string directly,
766
+ # not wrapped in an object
767
+ if isinstance(response, str):
768
+ return response
769
+ # Fallback if wrapped in an object
770
+ return response.get('snapshot', '')
771
+
772
+ @action_logger
773
+ async def get_snapshot_for_ai(self) -> Dict[str, Any]:
774
+ """Get snapshot for AI with element details."""
775
+ response = await self._send_command('get_snapshot_for_ai', {})
776
+ return response
777
+
778
+ @action_logger
779
+ async def get_som_screenshot(self) -> ToolResult:
780
+ """Get screenshot."""
781
+ logger.info("Requesting screenshot via WebSocket...")
782
+ start_time = time.time()
783
+
784
+ response = await self._send_command('get_som_screenshot', {})
785
+
786
+ end_time = time.time()
787
+ logger.info(f"Screenshot completed in {end_time - start_time:.2f}s")
788
+
789
+ return ToolResult(text=response['text'], images=response['images'])
790
+
791
+ def _ensure_ref_prefix(self, ref: str) -> str:
792
+ """Ensure ref has proper prefix"""
793
+ if not ref:
794
+ return ref
795
+
796
+ # If ref is purely numeric, add 'e' prefix for main frame
797
+ if ref.isdigit():
798
+ return f'e{ref}'
799
+
800
+ return ref
801
+
802
+ def _process_refs_in_params(
803
+ self, params: Dict[str, Any]
804
+ ) -> Dict[str, Any]:
805
+ """Process parameters to ensure all refs have 'e' prefix."""
806
+ if not params:
807
+ return params
808
+
809
+ # Create a copy to avoid modifying the original
810
+ processed = params.copy()
811
+
812
+ # Handle direct ref parameters
813
+ if 'ref' in processed:
814
+ processed['ref'] = self._ensure_ref_prefix(processed['ref'])
815
+
816
+ # Handle from_ref and to_ref for drag operations
817
+ if 'from_ref' in processed:
818
+ processed['from_ref'] = self._ensure_ref_prefix(
819
+ processed['from_ref']
820
+ )
821
+ if 'to_ref' in processed:
822
+ processed['to_ref'] = self._ensure_ref_prefix(processed['to_ref'])
823
+
824
+ # Handle inputs array for type_multiple
825
+ if 'inputs' in processed and isinstance(processed['inputs'], list):
826
+ processed_inputs = []
827
+ for input_item in processed['inputs']:
828
+ if isinstance(input_item, dict) and 'ref' in input_item:
829
+ processed_input = input_item.copy()
830
+ processed_input['ref'] = self._ensure_ref_prefix(
831
+ input_item['ref']
832
+ )
833
+ processed_inputs.append(processed_input)
834
+ else:
835
+ processed_inputs.append(input_item)
836
+ processed['inputs'] = processed_inputs
837
+
838
+ return processed
839
+
840
+ @action_logger
841
+ async def click(self, ref: str) -> Dict[str, Any]:
842
+ """Click an element."""
843
+ response = await self._send_command('click', {'ref': ref})
844
+ return response
845
+
846
+ @action_logger
847
+ async def type(self, ref: str, text: str) -> Dict[str, Any]:
848
+ """Type text into an element."""
849
+ response = await self._send_command('type', {'ref': ref, 'text': text})
850
+ # Log the response for debugging
851
+ logger.debug(f"Type response for ref {ref}: {response}")
852
+ return response
853
+
854
+ @action_logger
855
+ async def type_multiple(
856
+ self, inputs: List[Dict[str, str]]
857
+ ) -> Dict[str, Any]:
858
+ """Type text into multiple elements."""
859
+ response = await self._send_command('type', {'inputs': inputs})
860
+ return response
861
+
862
+ @action_logger
863
+ async def select(self, ref: str, value: str) -> Dict[str, Any]:
864
+ """Select an option."""
865
+ response = await self._send_command(
866
+ 'select', {'ref': ref, 'value': value}
867
+ )
868
+ return response
869
+
870
+ @action_logger
871
+ async def scroll(self, direction: str, amount: int) -> Dict[str, Any]:
872
+ """Scroll the page."""
873
+ response = await self._send_command(
874
+ 'scroll', {'direction': direction, 'amount': amount}
875
+ )
876
+ return response
877
+
878
+ @action_logger
879
+ async def enter(self) -> Dict[str, Any]:
880
+ """Press enter."""
881
+ response = await self._send_command('enter', {})
882
+ return response
883
+
884
+ @action_logger
885
+ async def mouse_control(
886
+ self, control: str, x: float, y: float
887
+ ) -> Dict[str, Any]:
888
+ """Control the mouse to interact with browser with x, y coordinates."""
889
+ response = await self._send_command(
890
+ 'mouse_control', {'control': control, 'x': x, 'y': y}
891
+ )
892
+ return response
893
+
894
+ @action_logger
895
+ async def mouse_drag(self, from_ref: str, to_ref: str) -> Dict[str, Any]:
896
+ """Control the mouse to drag and drop in the browser using ref IDs."""
897
+ response = await self._send_command(
898
+ 'mouse_drag',
899
+ {'from_ref': from_ref, 'to_ref': to_ref},
900
+ )
901
+ return response
902
+
903
+ @action_logger
904
+ async def press_key(self, keys: List[str]) -> Dict[str, Any]:
905
+ """Press key and key combinations."""
906
+ response = await self._send_command('press_key', {'keys': keys})
907
+ return response
908
+
909
+ @action_logger
910
+ async def back(self) -> Dict[str, Any]:
911
+ """Navigate back."""
912
+ response = await self._send_command('back', {})
913
+ return response
914
+
915
+ @action_logger
916
+ async def forward(self) -> Dict[str, Any]:
917
+ """Navigate forward."""
918
+ response = await self._send_command('forward', {})
919
+ return response
920
+
921
+ @action_logger
922
+ async def switch_tab(self, tab_id: str) -> Dict[str, Any]:
923
+ """Switch to a tab."""
924
+ response = await self._send_command('switch_tab', {'tabId': tab_id})
925
+ return response
926
+
927
+ @action_logger
928
+ async def close_tab(self, tab_id: str) -> Dict[str, Any]:
929
+ """Close a tab."""
930
+ response = await self._send_command('close_tab', {'tabId': tab_id})
931
+ return response
932
+
933
+ @action_logger
934
+ async def get_tab_info(self) -> List[Dict[str, Any]]:
935
+ """Get tab information."""
936
+ response = await self._send_command('get_tab_info', {})
937
+ # The backend returns the tab list directly, not wrapped in an object
938
+ if isinstance(response, list):
939
+ return response
940
+ # Fallback if wrapped in an object
941
+ return response.get('tabs', [])
942
+
943
+ @action_logger
944
+ async def console_view(self) -> List[Dict[str, Any]]:
945
+ """Get current page console view"""
946
+ response = await self._send_command('console_view', {})
947
+
948
+ if isinstance(response, list):
949
+ return response
950
+
951
+ return response.get('logs', [])
952
+
953
+ @action_logger
954
+ async def console_exec(self, code: str) -> Dict[str, Any]:
955
+ """Execute javascript code and get result."""
956
+ response = await self._send_command('console_exec', {'code': code})
957
+ return response
958
+
959
+ @action_logger
960
+ async def wait_user(
961
+ self, timeout_sec: Optional[float] = None
962
+ ) -> Dict[str, Any]:
963
+ """Wait for user input."""
964
+ response = await self._send_command(
965
+ 'wait_user', {'timeout': timeout_sec}
966
+ )
967
+ return response
968
+
969
+ async def _read_and_log_output(self):
970
+ """Read stdout from Node.js process & handle SERVER_READY + logging."""
971
+ if not self.process:
972
+ return
973
+
974
+ try:
975
+ with contextlib.ExitStack() as stack:
976
+ if self.ts_log_file_path:
977
+ self.ts_log_file = stack.enter_context(
978
+ open(self.ts_log_file_path, 'w', encoding='utf-8')
979
+ )
980
+ self.ts_log_file.write(
981
+ f"TypeScript Console Log - Started at "
982
+ f"{time.strftime('%Y-%m-%d %H:%M:%S')}\n"
983
+ )
984
+ self.ts_log_file.write("=" * 80 + "\n")
985
+ self.ts_log_file.flush()
986
+
987
+ while self.process and self.process.poll() is None:
988
+ try:
989
+ line = (
990
+ await asyncio.get_running_loop().run_in_executor(
991
+ None, self.process.stdout.readline
992
+ )
993
+ )
994
+ if not line: # EOF
995
+ break
996
+
997
+ # Check for SERVER_READY message
998
+ if line.startswith('SERVER_READY:'):
999
+ try:
1000
+ self.server_port = int(
1001
+ line.split(':', 1)[1].strip()
1002
+ )
1003
+ logger.info(
1004
+ f"WebSocket server ready on port "
1005
+ f"{self.server_port}"
1006
+ )
1007
+ if (
1008
+ self._server_ready_future
1009
+ and not self._server_ready_future.done()
1010
+ ):
1011
+ self._server_ready_future.set_result(True)
1012
+ except (ValueError, IndexError) as e:
1013
+ logger.error(
1014
+ f"Failed to parse SERVER_READY: {e}"
1015
+ )
1016
+
1017
+ # Write all output to log file
1018
+ if self.ts_log_file:
1019
+ timestamp = time.strftime('%H:%M:%S')
1020
+ self.ts_log_file.write(f"[{timestamp}] {line}")
1021
+ self.ts_log_file.flush()
1022
+
1023
+ except Exception as e:
1024
+ logger.warning(f"Error reading stdout: {e}")
1025
+ break
1026
+
1027
+ # Footer if we had a file
1028
+ if self.ts_log_file:
1029
+ self.ts_log_file.write("\n" + "=" * 80 + "\n")
1030
+ self.ts_log_file.write(
1031
+ f"TypeScript Console Log - Ended at "
1032
+ f"{time.strftime('%Y-%m-%d %H:%M:%S')}\n"
1033
+ )
1034
+ # ExitStack closes file; clear handle
1035
+ self.ts_log_file = None
1036
+ except Exception as e:
1037
+ logger.warning(f"Error in _read_and_log_output: {e}")