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

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

Potentially problematic release.


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

Files changed (505) hide show
  1. camel/__init__.py +3 -3
  2. camel/agents/__init__.py +2 -2
  3. camel/agents/_types.py +9 -4
  4. camel/agents/_utils.py +40 -2
  5. camel/agents/base.py +2 -2
  6. camel/agents/chat_agent.py +4835 -947
  7. camel/agents/critic_agent.py +2 -2
  8. camel/agents/deductive_reasoner_agent.py +56 -56
  9. camel/agents/embodied_agent.py +2 -2
  10. camel/agents/knowledge_graph_agent.py +20 -20
  11. camel/agents/mcp_agent.py +35 -36
  12. camel/agents/multi_hop_generator_agent.py +3 -3
  13. camel/agents/programmed_agent_instruction.py +2 -2
  14. camel/agents/repo_agent.py +4 -3
  15. camel/agents/role_assignment_agent.py +2 -2
  16. camel/agents/search_agent.py +2 -2
  17. camel/agents/task_agent.py +2 -2
  18. camel/agents/tool_agents/__init__.py +2 -2
  19. camel/agents/tool_agents/base.py +2 -2
  20. camel/agents/tool_agents/hugging_face_tool_agent.py +3 -3
  21. camel/benchmarks/__init__.py +2 -2
  22. camel/benchmarks/apibank.py +5 -5
  23. camel/benchmarks/apibench.py +2 -2
  24. camel/benchmarks/base.py +2 -2
  25. camel/benchmarks/browsecomp.py +44 -33
  26. camel/benchmarks/gaia.py +17 -13
  27. camel/benchmarks/mock_website/README.md +1 -3
  28. camel/benchmarks/mock_website/mock_web.py +2 -2
  29. camel/benchmarks/mock_website/requirements.txt +1 -1
  30. camel/benchmarks/mock_website/shopping_mall/app.py +2 -2
  31. camel/benchmarks/mock_website/task.json +1 -1
  32. camel/benchmarks/nexus.py +3 -3
  33. camel/benchmarks/ragbench.py +2 -2
  34. camel/bots/__init__.py +2 -2
  35. camel/bots/discord/__init__.py +2 -2
  36. camel/bots/discord/discord_app.py +2 -2
  37. camel/bots/discord/discord_installation.py +2 -2
  38. camel/bots/discord/discord_store.py +3 -3
  39. camel/bots/slack/__init__.py +2 -2
  40. camel/bots/slack/models.py +4 -4
  41. camel/bots/slack/slack_app.py +2 -2
  42. camel/bots/telegram_bot.py +2 -2
  43. camel/configs/__init__.py +23 -2
  44. camel/configs/aihubmix_config.py +90 -0
  45. camel/configs/aiml_config.py +2 -2
  46. camel/configs/amd_config.py +70 -0
  47. camel/configs/anthropic_config.py +2 -2
  48. camel/configs/base_config.py +2 -2
  49. camel/configs/bedrock_config.py +5 -3
  50. camel/configs/cerebras_config.py +98 -0
  51. camel/configs/cohere_config.py +2 -2
  52. camel/configs/cometapi_config.py +106 -0
  53. camel/configs/crynux_config.py +2 -2
  54. camel/configs/deepseek_config.py +9 -8
  55. camel/configs/gemini_config.py +6 -4
  56. camel/configs/groq_config.py +6 -4
  57. camel/configs/internlm_config.py +6 -4
  58. camel/configs/litellm_config.py +2 -2
  59. camel/configs/lmstudio_config.py +6 -4
  60. camel/configs/minimax_config.py +95 -0
  61. camel/configs/mistral_config.py +2 -2
  62. camel/configs/modelscope_config.py +5 -3
  63. camel/configs/moonshot_config.py +2 -2
  64. camel/configs/nebius_config.py +105 -0
  65. camel/configs/netmind_config.py +2 -2
  66. camel/configs/novita_config.py +2 -2
  67. camel/configs/nvidia_config.py +2 -2
  68. camel/configs/ollama_config.py +2 -2
  69. camel/configs/openai_config.py +5 -3
  70. camel/configs/openrouter_config.py +6 -4
  71. camel/configs/ppio_config.py +2 -2
  72. camel/configs/qianfan_config.py +85 -0
  73. camel/configs/qwen_config.py +2 -2
  74. camel/configs/reka_config.py +2 -2
  75. camel/configs/samba_config.py +6 -4
  76. camel/configs/sglang_config.py +2 -2
  77. camel/configs/siliconflow_config.py +2 -2
  78. camel/configs/togetherai_config.py +2 -2
  79. camel/configs/vllm_config.py +4 -2
  80. camel/configs/watsonx_config.py +2 -2
  81. camel/configs/yi_config.py +6 -4
  82. camel/configs/zhipuai_config.py +6 -4
  83. camel/data_collectors/__init__.py +2 -2
  84. camel/data_collectors/alpaca_collector.py +18 -9
  85. camel/data_collectors/base.py +2 -2
  86. camel/data_collectors/sharegpt_collector.py +2 -2
  87. camel/datagen/__init__.py +2 -2
  88. camel/datagen/cot_datagen.py +3 -3
  89. camel/datagen/evol_instruct/__init__.py +2 -2
  90. camel/datagen/evol_instruct/evol_instruct.py +2 -2
  91. camel/datagen/evol_instruct/scorer.py +12 -12
  92. camel/datagen/evol_instruct/templates.py +16 -16
  93. camel/datagen/self_improving_cot.py +5 -5
  94. camel/datagen/self_instruct/__init__.py +2 -2
  95. camel/datagen/self_instruct/filter/__init__.py +2 -2
  96. camel/datagen/self_instruct/filter/filter_function.py +2 -2
  97. camel/datagen/self_instruct/filter/filter_registry.py +2 -2
  98. camel/datagen/self_instruct/filter/instruction_filter.py +2 -2
  99. camel/datagen/self_instruct/self_instruct.py +2 -2
  100. camel/datagen/self_instruct/templates.py +47 -47
  101. camel/datagen/source2synth/__init__.py +2 -2
  102. camel/datagen/source2synth/data_processor.py +2 -2
  103. camel/datagen/source2synth/models.py +2 -2
  104. camel/datagen/source2synth/user_data_processor_config.py +2 -2
  105. camel/datahubs/__init__.py +2 -2
  106. camel/datahubs/base.py +2 -2
  107. camel/datahubs/huggingface.py +2 -2
  108. camel/datahubs/models.py +2 -2
  109. camel/datasets/__init__.py +2 -2
  110. camel/datasets/base_generator.py +41 -12
  111. camel/datasets/few_shot_generator.py +18 -18
  112. camel/datasets/models.py +2 -2
  113. camel/datasets/self_instruct_generator.py +2 -2
  114. camel/datasets/static_dataset.py +2 -2
  115. camel/embeddings/__init__.py +2 -2
  116. camel/embeddings/azure_embedding.py +2 -2
  117. camel/embeddings/base.py +2 -2
  118. camel/embeddings/gemini_embedding.py +2 -2
  119. camel/embeddings/jina_embedding.py +2 -2
  120. camel/embeddings/mistral_embedding.py +2 -2
  121. camel/embeddings/openai_compatible_embedding.py +2 -2
  122. camel/embeddings/openai_embedding.py +2 -2
  123. camel/embeddings/sentence_transformers_embeddings.py +2 -2
  124. camel/embeddings/together_embedding.py +2 -2
  125. camel/embeddings/vlm_embedding.py +2 -2
  126. camel/environments/__init__.py +14 -2
  127. camel/environments/models.py +2 -2
  128. camel/environments/multi_step.py +2 -2
  129. camel/environments/rlcards_env.py +860 -0
  130. camel/environments/single_step.py +30 -5
  131. camel/environments/tic_tac_toe.py +3 -3
  132. camel/extractors/__init__.py +2 -2
  133. camel/extractors/base.py +2 -2
  134. camel/extractors/python_strategies.py +2 -2
  135. camel/generators.py +2 -2
  136. camel/human.py +2 -2
  137. camel/interpreters/__init__.py +4 -2
  138. camel/interpreters/base.py +2 -2
  139. camel/interpreters/docker/Dockerfile +14 -24
  140. camel/interpreters/docker_interpreter.py +5 -4
  141. camel/interpreters/e2b_interpreter.py +36 -3
  142. camel/interpreters/internal_python_interpreter.py +53 -4
  143. camel/interpreters/interpreter_error.py +2 -2
  144. camel/interpreters/ipython_interpreter.py +2 -2
  145. camel/interpreters/microsandbox_interpreter.py +395 -0
  146. camel/interpreters/subprocess_interpreter.py +2 -2
  147. camel/loaders/__init__.py +13 -4
  148. camel/loaders/apify_reader.py +2 -2
  149. camel/loaders/base_io.py +2 -2
  150. camel/loaders/base_loader.py +85 -0
  151. camel/loaders/chunkr_reader.py +11 -2
  152. camel/loaders/crawl4ai_reader.py +2 -2
  153. camel/loaders/firecrawl_reader.py +6 -6
  154. camel/loaders/jina_url_reader.py +2 -2
  155. camel/loaders/markitdown.py +2 -2
  156. camel/loaders/mineru_extractor.py +2 -2
  157. camel/loaders/mistral_reader.py +2 -2
  158. camel/loaders/scrapegraph_reader.py +2 -2
  159. camel/loaders/unstructured_io.py +2 -2
  160. camel/logger.py +5 -5
  161. camel/memories/__init__.py +2 -2
  162. camel/memories/agent_memories.py +86 -3
  163. camel/memories/base.py +36 -2
  164. camel/memories/blocks/__init__.py +2 -2
  165. camel/memories/blocks/chat_history_block.py +125 -7
  166. camel/memories/blocks/vectordb_block.py +10 -3
  167. camel/memories/context_creators/__init__.py +2 -2
  168. camel/memories/context_creators/score_based.py +31 -239
  169. camel/memories/records.py +90 -10
  170. camel/messages/__init__.py +2 -2
  171. camel/messages/base.py +178 -43
  172. camel/messages/conversion/__init__.py +2 -2
  173. camel/messages/conversion/alpaca.py +2 -2
  174. camel/messages/conversion/conversation_models.py +2 -2
  175. camel/messages/conversion/sharegpt/__init__.py +2 -2
  176. camel/messages/conversion/sharegpt/function_call_formatter.py +2 -2
  177. camel/messages/conversion/sharegpt/hermes/__init__.py +2 -2
  178. camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py +2 -2
  179. camel/messages/func_message.py +54 -17
  180. camel/models/__init__.py +16 -2
  181. camel/models/_utils.py +3 -3
  182. camel/models/aihubmix_model.py +83 -0
  183. camel/models/aiml_model.py +11 -18
  184. camel/models/amd_model.py +101 -0
  185. camel/models/anthropic_model.py +127 -20
  186. camel/models/aws_bedrock_model.py +12 -35
  187. camel/models/azure_openai_model.py +212 -89
  188. camel/models/base_audio_model.py +5 -3
  189. camel/models/base_model.py +195 -26
  190. camel/models/cerebras_model.py +83 -0
  191. camel/models/cohere_model.py +16 -21
  192. camel/models/cometapi_model.py +83 -0
  193. camel/models/crynux_model.py +11 -18
  194. camel/models/deepseek_model.py +18 -58
  195. camel/models/fish_audio_model.py +8 -2
  196. camel/models/gemini_model.py +389 -26
  197. camel/models/groq_model.py +11 -19
  198. camel/models/internlm_model.py +11 -18
  199. camel/models/litellm_model.py +56 -34
  200. camel/models/lmstudio_model.py +17 -20
  201. camel/models/minimax_model.py +83 -0
  202. camel/models/mistral_model.py +18 -19
  203. camel/models/model_factory.py +37 -3
  204. camel/models/model_manager.py +26 -8
  205. camel/models/modelscope_model.py +13 -193
  206. camel/models/moonshot_model.py +195 -21
  207. camel/models/nebius_model.py +83 -0
  208. camel/models/nemotron_model.py +19 -9
  209. camel/models/netmind_model.py +11 -18
  210. camel/models/novita_model.py +11 -18
  211. camel/models/nvidia_model.py +11 -18
  212. camel/models/ollama_model.py +14 -21
  213. camel/models/openai_audio_models.py +2 -2
  214. camel/models/openai_compatible_model.py +188 -45
  215. camel/models/openai_model.py +216 -71
  216. camel/models/openrouter_model.py +11 -19
  217. camel/models/ppio_model.py +11 -18
  218. camel/models/qianfan_model.py +89 -0
  219. camel/models/qwen_model.py +13 -193
  220. camel/models/reka_model.py +21 -21
  221. camel/models/reward/__init__.py +2 -2
  222. camel/models/reward/base_reward_model.py +2 -2
  223. camel/models/reward/evaluator.py +2 -2
  224. camel/models/reward/nemotron_model.py +2 -2
  225. camel/models/reward/skywork_model.py +2 -2
  226. camel/models/samba_model.py +48 -47
  227. camel/models/sglang_model.py +88 -40
  228. camel/models/siliconflow_model.py +12 -35
  229. camel/models/stub_model.py +10 -7
  230. camel/models/togetherai_model.py +11 -18
  231. camel/models/vllm_model.py +10 -18
  232. camel/models/volcano_model.py +16 -20
  233. camel/models/watsonx_model.py +7 -19
  234. camel/models/yi_model.py +11 -18
  235. camel/models/zhipuai_model.py +70 -18
  236. camel/parsers/__init__.py +18 -0
  237. camel/parsers/mcp_tool_call_parser.py +176 -0
  238. camel/personas/__init__.py +2 -2
  239. camel/personas/persona.py +2 -2
  240. camel/personas/persona_hub.py +2 -2
  241. camel/prompts/__init__.py +2 -2
  242. camel/prompts/ai_society.py +2 -2
  243. camel/prompts/base.py +2 -2
  244. camel/prompts/code.py +2 -2
  245. camel/prompts/evaluation.py +2 -2
  246. camel/prompts/generate_text_embedding_data.py +2 -2
  247. camel/prompts/image_craft.py +2 -2
  248. camel/prompts/misalignment.py +2 -2
  249. camel/prompts/multi_condition_image_craft.py +2 -2
  250. camel/prompts/object_recognition.py +2 -2
  251. camel/prompts/persona_hub.py +3 -3
  252. camel/prompts/prompt_templates.py +2 -2
  253. camel/prompts/role_description_prompt_template.py +2 -2
  254. camel/prompts/solution_extraction.py +8 -8
  255. camel/prompts/task_prompt_template.py +2 -2
  256. camel/prompts/translation.py +2 -2
  257. camel/prompts/video_description_prompt.py +3 -3
  258. camel/responses/__init__.py +2 -2
  259. camel/responses/agent_responses.py +2 -2
  260. camel/retrievers/__init__.py +2 -2
  261. camel/retrievers/auto_retriever.py +3 -2
  262. camel/retrievers/base.py +2 -2
  263. camel/retrievers/bm25_retriever.py +2 -2
  264. camel/retrievers/cohere_rerank_retriever.py +2 -2
  265. camel/retrievers/hybrid_retrival.py +2 -2
  266. camel/retrievers/vector_retriever.py +2 -2
  267. camel/runtimes/Dockerfile.multi-toolkit +90 -0
  268. camel/runtimes/__init__.py +2 -2
  269. camel/runtimes/api.py +79 -23
  270. camel/runtimes/base.py +2 -2
  271. camel/runtimes/configs.py +13 -13
  272. camel/runtimes/daytona_runtime.py +17 -18
  273. camel/runtimes/docker_runtime.py +12 -12
  274. camel/runtimes/llm_guard_runtime.py +26 -26
  275. camel/runtimes/remote_http_runtime.py +11 -11
  276. camel/runtimes/ubuntu_docker_runtime.py +2 -2
  277. camel/runtimes/utils/__init__.py +2 -2
  278. camel/runtimes/utils/function_risk_toolkit.py +2 -2
  279. camel/runtimes/utils/ignore_risk_toolkit.py +2 -2
  280. camel/schemas/__init__.py +2 -2
  281. camel/schemas/base.py +2 -2
  282. camel/schemas/openai_converter.py +3 -3
  283. camel/schemas/outlines_converter.py +2 -2
  284. camel/services/agent_openapi_server.py +380 -0
  285. camel/societies/__init__.py +4 -2
  286. camel/societies/babyagi_playing.py +2 -2
  287. camel/societies/role_playing.py +201 -80
  288. camel/societies/workforce/__init__.py +10 -3
  289. camel/societies/workforce/base.py +2 -2
  290. camel/societies/workforce/events.py +143 -0
  291. camel/societies/workforce/prompts.py +258 -33
  292. camel/societies/workforce/role_playing_worker.py +88 -31
  293. camel/societies/workforce/single_agent_worker.py +638 -40
  294. camel/societies/workforce/structured_output_handler.py +512 -0
  295. camel/societies/workforce/task_channel.py +182 -38
  296. camel/societies/workforce/utils.py +780 -65
  297. camel/societies/workforce/worker.py +92 -26
  298. camel/societies/workforce/workflow_memory_manager.py +1746 -0
  299. camel/societies/workforce/workforce.py +5276 -355
  300. camel/societies/workforce/workforce_callback.py +103 -0
  301. camel/societies/workforce/workforce_logger.py +647 -0
  302. camel/societies/workforce/workforce_metrics.py +33 -0
  303. camel/storages/__init__.py +6 -2
  304. camel/storages/graph_storages/__init__.py +2 -2
  305. camel/storages/graph_storages/base.py +2 -2
  306. camel/storages/graph_storages/graph_element.py +2 -2
  307. camel/storages/graph_storages/nebula_graph.py +4 -4
  308. camel/storages/graph_storages/neo4j_graph.py +7 -7
  309. camel/storages/key_value_storages/__init__.py +2 -2
  310. camel/storages/key_value_storages/base.py +2 -2
  311. camel/storages/key_value_storages/in_memory.py +2 -2
  312. camel/storages/key_value_storages/json.py +17 -4
  313. camel/storages/key_value_storages/mem0_cloud.py +50 -49
  314. camel/storages/key_value_storages/redis.py +2 -2
  315. camel/storages/object_storages/__init__.py +2 -2
  316. camel/storages/object_storages/amazon_s3.py +2 -2
  317. camel/storages/object_storages/azure_blob.py +2 -2
  318. camel/storages/object_storages/base.py +2 -2
  319. camel/storages/object_storages/google_cloud.py +3 -3
  320. camel/storages/vectordb_storages/__init__.py +8 -2
  321. camel/storages/vectordb_storages/base.py +2 -2
  322. camel/storages/vectordb_storages/chroma.py +731 -0
  323. camel/storages/vectordb_storages/faiss.py +2 -2
  324. camel/storages/vectordb_storages/milvus.py +2 -2
  325. camel/storages/vectordb_storages/oceanbase.py +15 -15
  326. camel/storages/vectordb_storages/pgvector.py +349 -0
  327. camel/storages/vectordb_storages/qdrant.py +6 -6
  328. camel/storages/vectordb_storages/surreal.py +372 -0
  329. camel/storages/vectordb_storages/tidb.py +11 -8
  330. camel/storages/vectordb_storages/weaviate.py +2 -2
  331. camel/tasks/__init__.py +2 -2
  332. camel/tasks/task.py +348 -26
  333. camel/tasks/task_prompt.py +3 -3
  334. camel/terminators/__init__.py +2 -2
  335. camel/terminators/base.py +2 -2
  336. camel/terminators/response_terminator.py +2 -2
  337. camel/terminators/token_limit_terminator.py +2 -2
  338. camel/toolkits/__init__.py +54 -10
  339. camel/toolkits/aci_toolkit.py +66 -21
  340. camel/toolkits/arxiv_toolkit.py +8 -8
  341. camel/toolkits/ask_news_toolkit.py +2 -2
  342. camel/toolkits/async_browser_toolkit.py +4 -4
  343. camel/toolkits/audio_analysis_toolkit.py +3 -3
  344. camel/toolkits/base.py +65 -7
  345. camel/toolkits/bohrium_toolkit.py +2 -2
  346. camel/toolkits/browser_toolkit.py +34 -21
  347. camel/toolkits/browser_toolkit_commons.py +4 -4
  348. camel/toolkits/code_execution.py +31 -4
  349. camel/toolkits/context_summarizer_toolkit.py +684 -0
  350. camel/toolkits/craw4ai_toolkit.py +93 -0
  351. camel/toolkits/dappier_toolkit.py +12 -8
  352. camel/toolkits/data_commons_toolkit.py +2 -2
  353. camel/toolkits/dingtalk.py +1135 -0
  354. camel/toolkits/earth_science_toolkit.py +5367 -0
  355. camel/toolkits/edgeone_pages_mcp_toolkit.py +49 -0
  356. camel/toolkits/excel_toolkit.py +905 -71
  357. camel/toolkits/file_toolkit.py +1402 -0
  358. camel/toolkits/function_tool.py +126 -18
  359. camel/toolkits/github_toolkit.py +109 -22
  360. camel/toolkits/gmail_toolkit.py +1839 -0
  361. camel/toolkits/google_calendar_toolkit.py +40 -6
  362. camel/toolkits/google_drive_mcp_toolkit.py +54 -0
  363. camel/toolkits/google_maps_toolkit.py +2 -2
  364. camel/toolkits/google_scholar_toolkit.py +2 -2
  365. camel/toolkits/human_toolkit.py +36 -12
  366. camel/toolkits/hybrid_browser_toolkit/__init__.py +18 -0
  367. camel/toolkits/hybrid_browser_toolkit/config_loader.py +185 -0
  368. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +246 -0
  369. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1973 -0
  370. camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
  371. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4589 -0
  372. camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
  373. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
  374. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +1929 -0
  375. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +233 -0
  376. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +589 -0
  377. camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
  378. camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
  379. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
  380. camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
  381. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +129 -0
  382. camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +27 -0
  383. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +319 -0
  384. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +1037 -0
  385. camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
  386. camel/toolkits/hybrid_browser_toolkit_py/actions.py +575 -0
  387. camel/toolkits/hybrid_browser_toolkit_py/agent.py +311 -0
  388. camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +787 -0
  389. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +490 -0
  390. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2390 -0
  391. camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +233 -0
  392. camel/toolkits/hybrid_browser_toolkit_py/stealth_script.js +0 -0
  393. camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +1043 -0
  394. camel/toolkits/image_analysis_toolkit.py +3 -6
  395. camel/toolkits/image_generation_toolkit.py +390 -0
  396. camel/toolkits/jina_reranker_toolkit.py +5 -6
  397. camel/toolkits/klavis_toolkit.py +7 -3
  398. camel/toolkits/linkedin_toolkit.py +2 -2
  399. camel/toolkits/markitdown_toolkit.py +104 -0
  400. camel/toolkits/math_toolkit.py +66 -12
  401. camel/toolkits/mcp_toolkit.py +412 -36
  402. camel/toolkits/memory_toolkit.py +7 -3
  403. camel/toolkits/meshy_toolkit.py +2 -2
  404. camel/toolkits/message_agent_toolkit.py +608 -0
  405. camel/toolkits/message_integration.py +724 -0
  406. camel/toolkits/mineru_toolkit.py +2 -2
  407. camel/toolkits/minimax_mcp_toolkit.py +195 -0
  408. camel/toolkits/networkx_toolkit.py +2 -2
  409. camel/toolkits/note_taking_toolkit.py +277 -0
  410. camel/toolkits/notion_mcp_toolkit.py +224 -0
  411. camel/toolkits/notion_toolkit.py +2 -2
  412. camel/toolkits/open_api_specs/biztoc/__init__.py +2 -2
  413. camel/toolkits/open_api_specs/biztoc/ai-plugin.json +1 -1
  414. camel/toolkits/open_api_specs/coursera/__init__.py +2 -2
  415. camel/toolkits/open_api_specs/create_qr_code/__init__.py +2 -2
  416. camel/toolkits/open_api_specs/klarna/__init__.py +2 -2
  417. camel/toolkits/open_api_specs/nasa_apod/__init__.py +2 -2
  418. camel/toolkits/open_api_specs/outschool/__init__.py +2 -2
  419. camel/toolkits/open_api_specs/outschool/ai-plugin.json +1 -1
  420. camel/toolkits/open_api_specs/outschool/openapi.yaml +1 -1
  421. camel/toolkits/open_api_specs/outschool/paths/__init__.py +2 -2
  422. camel/toolkits/open_api_specs/outschool/paths/get_classes.py +2 -2
  423. camel/toolkits/open_api_specs/outschool/paths/search_teachers.py +2 -2
  424. camel/toolkits/open_api_specs/security_config.py +2 -2
  425. camel/toolkits/open_api_specs/speak/__init__.py +2 -2
  426. camel/toolkits/open_api_specs/web_scraper/__init__.py +2 -2
  427. camel/toolkits/open_api_specs/web_scraper/ai-plugin.json +1 -1
  428. camel/toolkits/open_api_specs/web_scraper/paths/__init__.py +2 -2
  429. camel/toolkits/open_api_specs/web_scraper/paths/scraper.py +2 -2
  430. camel/toolkits/open_api_toolkit.py +2 -2
  431. camel/toolkits/openbb_toolkit.py +7 -3
  432. camel/toolkits/origene_mcp_toolkit.py +56 -0
  433. camel/toolkits/page_script.js +53 -53
  434. camel/toolkits/playwright_mcp_toolkit.py +13 -31
  435. camel/toolkits/pptx_toolkit.py +36 -23
  436. camel/toolkits/pubmed_toolkit.py +2 -2
  437. camel/toolkits/pulse_mcp_search_toolkit.py +2 -2
  438. camel/toolkits/pyautogui_toolkit.py +2 -2
  439. camel/toolkits/reddit_toolkit.py +2 -2
  440. camel/toolkits/resend_toolkit.py +168 -0
  441. camel/toolkits/retrieval_toolkit.py +2 -2
  442. camel/toolkits/screenshot_toolkit.py +213 -0
  443. camel/toolkits/search_toolkit.py +539 -146
  444. camel/toolkits/searxng_toolkit.py +2 -2
  445. camel/toolkits/semantic_scholar_toolkit.py +2 -2
  446. camel/toolkits/slack_toolkit.py +108 -58
  447. camel/toolkits/sql_toolkit.py +712 -0
  448. camel/toolkits/stripe_toolkit.py +2 -2
  449. camel/toolkits/sympy_toolkit.py +3 -3
  450. camel/toolkits/task_planning_toolkit.py +5 -5
  451. camel/toolkits/terminal_toolkit/__init__.py +18 -0
  452. camel/toolkits/terminal_toolkit/terminal_toolkit.py +1070 -0
  453. camel/toolkits/terminal_toolkit/utils.py +532 -0
  454. camel/toolkits/thinking_toolkit.py +3 -3
  455. camel/toolkits/twitter_toolkit.py +2 -2
  456. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  457. camel/toolkits/video_analysis_toolkit.py +109 -29
  458. camel/toolkits/video_download_toolkit.py +19 -16
  459. camel/toolkits/weather_toolkit.py +2 -2
  460. camel/toolkits/web_deploy_toolkit.py +1219 -0
  461. camel/toolkits/wechat_official_toolkit.py +483 -0
  462. camel/toolkits/whatsapp_toolkit.py +2 -2
  463. camel/toolkits/wolfram_alpha_toolkit.py +2 -2
  464. camel/toolkits/zapier_toolkit.py +7 -3
  465. camel/types/__init__.py +4 -4
  466. camel/types/agents/__init__.py +2 -2
  467. camel/types/agents/tool_calling_record.py +6 -3
  468. camel/types/enums.py +378 -39
  469. camel/types/mcp_registries.py +2 -2
  470. camel/types/openai_types.py +4 -4
  471. camel/types/unified_model_type.py +38 -6
  472. camel/utils/__init__.py +2 -2
  473. camel/utils/async_func.py +2 -2
  474. camel/utils/chunker/__init__.py +2 -2
  475. camel/utils/chunker/base.py +2 -2
  476. camel/utils/chunker/code_chunker.py +2 -2
  477. camel/utils/chunker/uio_chunker.py +2 -2
  478. camel/utils/commons.py +38 -7
  479. camel/utils/constants.py +5 -2
  480. camel/utils/context_utils.py +1134 -0
  481. camel/utils/deduplication.py +2 -2
  482. camel/utils/filename.py +2 -2
  483. camel/utils/langfuse.py +2 -2
  484. camel/utils/mcp.py +140 -6
  485. camel/utils/mcp_client.py +48 -38
  486. camel/utils/message_summarizer.py +148 -0
  487. camel/utils/response_format.py +2 -2
  488. camel/utils/token_counting.py +45 -22
  489. camel/utils/tool_result.py +44 -0
  490. camel/verifiers/__init__.py +2 -2
  491. camel/verifiers/base.py +2 -2
  492. camel/verifiers/math_verifier.py +2 -2
  493. camel/verifiers/models.py +2 -2
  494. camel/verifiers/physics_verifier.py +2 -2
  495. camel/verifiers/python_verifier.py +2 -2
  496. {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/METADATA +327 -94
  497. camel_ai-0.2.82.dist-info/RECORD +507 -0
  498. {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/WHEEL +1 -1
  499. {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/licenses/LICENSE +1 -1
  500. camel/loaders/pandas_reader.py +0 -368
  501. camel/toolkits/dalle_toolkit.py +0 -175
  502. camel/toolkits/file_write_toolkit.py +0 -444
  503. camel/toolkits/openai_agent_toolkit.py +0 -135
  504. camel/toolkits/terminal_toolkit.py +0 -1037
  505. camel_ai-0.2.65.dist-info/RECORD +0 -426
@@ -0,0 +1,1219 @@
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
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import json
19
+ import mimetypes
20
+ import os
21
+ import re
22
+ import shutil
23
+ import socket
24
+ import subprocess
25
+ import tempfile
26
+ import time
27
+ from typing import Any, Dict, List, Optional
28
+
29
+ from camel.logger import get_logger
30
+ from camel.toolkits import FunctionTool
31
+ from camel.toolkits.base import BaseToolkit
32
+
33
+ logger = get_logger(__name__)
34
+
35
+
36
+ class WebDeployToolkit(BaseToolkit):
37
+ r"""A simple toolkit for initializing React projects and deploying web.
38
+
39
+ This toolkit provides core functionality to:
40
+ - Initialize new React projects
41
+ - Build React applications
42
+ - Deploy HTML content to local server
43
+ - Serve static websites locally
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ timeout: Optional[float] = None,
49
+ add_branding_tag: bool = True,
50
+ logo_path: str = "../camel/misc/favicon.png",
51
+ tag_text: str = "Created by CAMEL",
52
+ tag_url: str = "https://github.com/camel-ai/camel",
53
+ remote_server_ip: Optional[str] = None,
54
+ remote_server_port: int = 8080,
55
+ ):
56
+ r"""Initialize the WebDeployToolkit.
57
+
58
+ Args:
59
+ timeout (Optional[float]): Command timeout in seconds.
60
+ (default: :obj:`None`)
61
+ add_branding_tag (bool): Whether to add brand tag to deployed
62
+ pages. (default: :obj:`True`)
63
+ logo_path (str): Path to custom logo file (SVG, PNG, JPG, ICO).
64
+ (default: :obj:`../camel/misc/favicon.png`)
65
+ tag_text (str): Text to display in the tag.
66
+ (default: :obj:`Created by CAMEL`)
67
+ tag_url (str): URL to open when tag is clicked.
68
+ (default: :obj:`https://github.com/camel-ai/camel`)
69
+ remote_server_ip (Optional[str]): Remote server IP for deployment.
70
+ (default: :obj:`None` - use local deployment)
71
+ remote_server_port (int): Remote server port.
72
+ (default: :obj:`8080`)
73
+ """
74
+ super().__init__(timeout=timeout)
75
+ self.timeout = timeout
76
+ self.server_instances: Dict[int, Any] = {} # Track running servers
77
+ self.add_branding_tag = add_branding_tag
78
+ self.logo_path = logo_path
79
+ self.tag_text = self._sanitize_text(tag_text)
80
+ self.tag_url = self._validate_url(tag_url)
81
+ self.remote_server_ip = (
82
+ self._validate_ip_or_domain(remote_server_ip)
83
+ if remote_server_ip
84
+ else None
85
+ )
86
+ self.remote_server_port = self._validate_port(remote_server_port)
87
+ self.server_registry_file = os.path.join(
88
+ tempfile.gettempdir(), "web_deploy_servers.json"
89
+ )
90
+ self._load_server_registry()
91
+
92
+ def _validate_ip_or_domain(self, address: str) -> str:
93
+ r"""Validate IP address or domain name format."""
94
+ import ipaddress
95
+ import re
96
+
97
+ try:
98
+ # Try to validate as IP address first
99
+ ipaddress.ip_address(address)
100
+ return address
101
+ except ValueError:
102
+ # If not a valid IP, check if it's a valid domain name
103
+ domain_pattern = re.compile(
104
+ r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?'
105
+ r'(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$'
106
+ )
107
+ if domain_pattern.match(address) and len(address) <= 253:
108
+ return address
109
+ else:
110
+ raise ValueError(
111
+ f"Invalid IP address or domain name: {address}"
112
+ )
113
+
114
+ def _validate_port(self, port: int) -> int:
115
+ r"""Validate port number."""
116
+ if not isinstance(port, int) or port < 1 or port > 65535:
117
+ raise ValueError(f"Invalid port number: {port}")
118
+ return port
119
+
120
+ def _sanitize_text(self, text: str) -> str:
121
+ r"""Sanitize text to prevent XSS."""
122
+ if not isinstance(text, str):
123
+ return ""
124
+ # Remove any HTML/script tags
125
+ text = re.sub(r'<[^>]+>', '', text)
126
+ # Escape special characters
127
+ text = (
128
+ text.replace('&', '&amp;')
129
+ .replace('<', '&lt;')
130
+ .replace('>', '&gt;')
131
+ )
132
+ text = text.replace('"', '&quot;').replace("'", '&#x27;')
133
+ return text[:100] # Limit length
134
+
135
+ def _validate_url(self, url: str) -> str:
136
+ r"""Validate URL format."""
137
+ if not isinstance(url, str):
138
+ raise ValueError("URL must be a string")
139
+ # Basic URL validation
140
+ url_pattern = re.compile(
141
+ r'^https?://'
142
+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|'
143
+ r'localhost|'
144
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
145
+ r'(?::\d+)?'
146
+ r'(?:/?|[/?]\S+)$',
147
+ re.IGNORECASE,
148
+ )
149
+ if not url_pattern.match(url):
150
+ raise ValueError(f"Invalid URL format: {url}")
151
+ return url
152
+
153
+ def _validate_subdirectory(
154
+ self, subdirectory: Optional[str]
155
+ ) -> Optional[str]:
156
+ r"""Validate subdirectory to prevent path traversal."""
157
+ if subdirectory is None:
158
+ return None
159
+
160
+ # Remove any leading/trailing slashes
161
+ subdirectory = subdirectory.strip('/')
162
+
163
+ # Check for path traversal attempts
164
+ if '..' in subdirectory or subdirectory.startswith('/'):
165
+ raise ValueError(f"Invalid subdirectory: {subdirectory}")
166
+
167
+ # Only allow alphanumeric, dash, underscore, and forward slashes
168
+ if not re.match(r'^[a-zA-Z0-9_-]+(?:/[a-zA-Z0-9_-]+)*$', subdirectory):
169
+ raise ValueError(f"Invalid subdirectory format: {subdirectory}")
170
+
171
+ return subdirectory
172
+
173
+ def _is_port_available(self, port: int) -> bool:
174
+ r"""Check if a port is available for binding."""
175
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
176
+ try:
177
+ sock.bind(('127.0.0.1', port))
178
+ return True
179
+ except OSError:
180
+ return False
181
+
182
+ def _load_server_registry(self):
183
+ r"""Load server registry from persistent storage."""
184
+ try:
185
+ if os.path.exists(self.server_registry_file):
186
+ with open(self.server_registry_file, 'r') as f:
187
+ data = json.load(f)
188
+ # Reconstruct server instances from registry
189
+ for port_str, server_info in data.items():
190
+ port = int(port_str)
191
+ pid = server_info.get('pid')
192
+ if pid and self._is_process_running(pid):
193
+ # Create a mock process object for tracking
194
+ self.server_instances[port] = {
195
+ 'pid': pid,
196
+ 'start_time': server_info.get('start_time'),
197
+ 'directory': server_info.get('directory'),
198
+ }
199
+ except Exception as e:
200
+ logger.warning(f"Could not load server registry: {e}")
201
+
202
+ def _save_server_registry(self):
203
+ r"""Save server registry to persistent storage."""
204
+ try:
205
+ registry_data = {}
206
+ for port, server_info in self.server_instances.items():
207
+ if isinstance(server_info, dict):
208
+ registry_data[str(port)] = {
209
+ 'pid': server_info.get('pid'),
210
+ 'start_time': server_info.get('start_time'),
211
+ 'directory': server_info.get('directory'),
212
+ }
213
+ else:
214
+ # Handle subprocess.Popen objects
215
+ registry_data[str(port)] = {
216
+ 'pid': server_info.pid,
217
+ 'start_time': time.time(),
218
+ 'directory': getattr(server_info, 'directory', None),
219
+ }
220
+
221
+ with open(self.server_registry_file, 'w') as f:
222
+ json.dump(registry_data, f, indent=2)
223
+ except Exception as e:
224
+ logger.warning(f"Could not save server registry: {e}")
225
+
226
+ def _is_process_running(self, pid: int) -> bool:
227
+ r"""Check if a process with given PID is still running."""
228
+ try:
229
+ # Send signal 0 to check if process exists
230
+ os.kill(pid, 0)
231
+ return True
232
+ except (OSError, ProcessLookupError):
233
+ return False
234
+
235
+ def _build_custom_url(
236
+ self, domain: str, subdirectory: Optional[str] = None
237
+ ) -> str:
238
+ r"""Build custom URL with optional subdirectory.
239
+
240
+ Args:
241
+ domain (str): Custom domain
242
+ subdirectory (Optional[str]): Subdirectory path
243
+
244
+ Returns:
245
+ str: Complete custom URL
246
+ """
247
+ # Validate domain
248
+ if not re.match(r'^[a-zA-Z0-9.-]+$', domain):
249
+ raise ValueError(f"Invalid domain format: {domain}")
250
+ custom_url = f"http://{domain}:8080"
251
+ if subdirectory:
252
+ subdirectory = self._validate_subdirectory(subdirectory)
253
+ custom_url += f"/{subdirectory}"
254
+ return custom_url
255
+
256
+ def _load_logo_as_data_uri(self, logo_path: str) -> str:
257
+ r"""Load a local logo file and convert it to data URI.
258
+
259
+ Args:
260
+ logo_path (str): Path to the logo file
261
+
262
+ Returns:
263
+ str: Data URI of the logo file
264
+ """
265
+ try:
266
+ if not os.path.exists(logo_path):
267
+ logger.warning(f"Logo file not found: {logo_path}")
268
+ return self._get_default_logo()
269
+
270
+ # Get MIME type
271
+ mime_type, _ = mimetypes.guess_type(logo_path)
272
+ if not mime_type:
273
+ # Default MIME types for common formats
274
+ ext = os.path.splitext(logo_path)[1].lower()
275
+ mime_types_map = {
276
+ '.svg': 'image/svg+xml',
277
+ '.png': 'image/png',
278
+ '.jpg': 'image/jpeg',
279
+ '.jpeg': 'image/jpeg',
280
+ '.ico': 'image/x-icon',
281
+ '.gif': 'image/gif',
282
+ }
283
+ mime_type = mime_types_map.get(ext, 'image/png')
284
+
285
+ # Read file and encode to base64
286
+ with open(logo_path, 'rb') as f:
287
+ file_data = f.read()
288
+
289
+ base64_data = base64.b64encode(file_data).decode('utf-8')
290
+ return f"data:{mime_type};base64,{base64_data}"
291
+
292
+ except Exception as e:
293
+ logger.error(f"Error loading logo file {logo_path}: {e}")
294
+ return self._get_default_logo()
295
+
296
+ def _get_default_logo(self) -> str:
297
+ r"""Get the default logo as data URI.
298
+
299
+ Returns:
300
+ str: Default logo data URI
301
+ """
302
+ default_logo_data_uri = (
303
+ "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' "
304
+ "width='32' height='32' viewBox='0 0 32 32' fill='none'%3E%3Crect "
305
+ "width='32' height='32' rx='8' fill='%23333333'/%3E%3Ctext x='16' "
306
+ "y='22' font-family='system-ui, -apple-system, sans-serif' "
307
+ "font-size='12' font-weight='700' text-anchor='middle' "
308
+ "fill='white'%3EAI%3C/text%3E%3C/svg%3E"
309
+ )
310
+ return default_logo_data_uri
311
+
312
+ def deploy_html_content(
313
+ self,
314
+ html_content: Optional[str] = None,
315
+ html_file_path: Optional[str] = None,
316
+ file_name: str = "index.html",
317
+ port: int = 8000,
318
+ domain: Optional[str] = None,
319
+ subdirectory: Optional[str] = None,
320
+ ) -> Dict[str, Any]:
321
+ r"""Deploy HTML content to a local server or remote server.
322
+
323
+ Args:
324
+ html_content (Optional[str]): HTML content to deploy. Either this
325
+ or html_file_path must be provided.
326
+ html_file_path (Optional[str]): Path to HTML file to deploy. Either
327
+ this or html_content must be provided.
328
+ file_name (str): Name for the HTML file when using html_content.
329
+ (default: :obj:`index.html`)
330
+ port (int): Port to serve on. (default: :obj:`8000`)
331
+ domain (Optional[str]): Custom domain to access the content.
332
+ (e.g., :obj:`example.com`)
333
+ subdirectory (Optional[str]): Subdirectory path for multi-user
334
+ deployment. (e.g., :obj:`user123`)
335
+
336
+ Returns:
337
+ Dict[str, Any]: Deployment result with server URL and custom domain
338
+ info.
339
+ """
340
+ try:
341
+ # Validate inputs
342
+ if html_content is None and html_file_path is None:
343
+ return {
344
+ 'success': False,
345
+ 'error': (
346
+ 'Either html_content or html_file_path must be '
347
+ 'provided'
348
+ ),
349
+ }
350
+
351
+ if html_content is not None and html_file_path is not None:
352
+ return {
353
+ 'success': False,
354
+ 'error': (
355
+ 'Cannot provide both html_content and '
356
+ 'html_file_path'
357
+ ),
358
+ }
359
+
360
+ # Read content from file if file path is provided
361
+ if html_file_path:
362
+ if not os.path.exists(html_file_path):
363
+ return {
364
+ 'success': False,
365
+ 'error': f'HTML file not found: {html_file_path}',
366
+ }
367
+
368
+ try:
369
+ with open(html_file_path, 'r', encoding='utf-8') as f:
370
+ html_content = f.read()
371
+ # Use the original filename if deploying from file
372
+ file_name = os.path.basename(html_file_path)
373
+ except Exception as e:
374
+ return {
375
+ 'success': False,
376
+ 'error': f'Error reading HTML file: {e}',
377
+ }
378
+
379
+ # Check if remote deployment is configured
380
+ if self.remote_server_ip:
381
+ return self._deploy_to_remote_server(
382
+ html_content, # type: ignore[arg-type]
383
+ subdirectory,
384
+ domain,
385
+ )
386
+ else:
387
+ return self._deploy_to_local_server(
388
+ html_content, # type: ignore[arg-type]
389
+ file_name,
390
+ port,
391
+ domain,
392
+ subdirectory,
393
+ )
394
+
395
+ except Exception as e:
396
+ logger.error(f"Error deploying HTML content: {e}")
397
+ return {'success': False, 'error': str(e)}
398
+
399
+ def _deploy_to_remote_server(
400
+ self,
401
+ html_content: str,
402
+ subdirectory: Optional[str] = None,
403
+ domain: Optional[str] = None,
404
+ ) -> Dict[str, Any]:
405
+ r"""Deploy HTML content to remote server via API.
406
+
407
+ Args:
408
+ html_content (str): HTML content to deploy
409
+ subdirectory (Optional[str]): Subdirectory path for deployment
410
+ domain (Optional[str]): Custom domain
411
+
412
+ Returns:
413
+ Dict[str, Any]: Deployment result
414
+ """
415
+ try:
416
+ import requests
417
+
418
+ # Validate subdirectory
419
+ subdirectory = self._validate_subdirectory(subdirectory)
420
+
421
+ # Prepare deployment data
422
+ deploy_data = {
423
+ "html_content": html_content,
424
+ "subdirectory": subdirectory,
425
+ "domain": domain,
426
+ "timestamp": time.time(),
427
+ }
428
+
429
+ # Send to remote server API
430
+ api_url = f"http://{self.remote_server_ip}:{self.remote_server_port}/api/deploy"
431
+
432
+ response = requests.post(
433
+ api_url,
434
+ json=deploy_data,
435
+ timeout=self.timeout,
436
+ # Security: disable redirects to prevent SSRF
437
+ allow_redirects=False,
438
+ # Add headers for security
439
+ headers={'Content-Type': 'application/json'},
440
+ )
441
+
442
+ if response.status_code == 200:
443
+ response.json()
444
+
445
+ # Build URLs
446
+ base_url = (
447
+ f"http://{self.remote_server_ip}:{self.remote_server_port}"
448
+ )
449
+ deployed_url = (
450
+ f"{base_url}/{subdirectory}/" if subdirectory else base_url
451
+ )
452
+
453
+ return {
454
+ 'success': True,
455
+ 'remote_url': deployed_url,
456
+ 'server_ip': self.remote_server_ip,
457
+ 'subdirectory': subdirectory,
458
+ 'domain': domain,
459
+ 'message': f'Successfully deployed to remote server!\n • '
460
+ f'Access URL: {deployed_url}\n • Server: '
461
+ f'{self.remote_server_ip}:{self.remote_server_port}',
462
+ 'branding_tag_added': self.add_branding_tag,
463
+ }
464
+ else:
465
+ return {
466
+ 'success': False,
467
+ 'error': f'Remote deployment failed: HTTP '
468
+ f'{response.status_code}',
469
+ }
470
+
471
+ except ImportError:
472
+ return {
473
+ 'success': False,
474
+ 'error': 'Remote deployment requires requests library. '
475
+ 'Install with: pip install requests',
476
+ }
477
+ except Exception as e:
478
+ return {
479
+ 'success': False,
480
+ 'error': f'Remote deployment error: {e!s}',
481
+ }
482
+
483
+ def _deploy_to_local_server(
484
+ self,
485
+ html_content: str,
486
+ file_name: str,
487
+ port: int,
488
+ domain: Optional[str],
489
+ subdirectory: Optional[str],
490
+ ) -> Dict[str, Any]:
491
+ r"""Deploy HTML content to local server (original functionality).
492
+
493
+ Args:
494
+ html_content (str): HTML content to deploy
495
+ file_name (str): Name for the HTML file
496
+ port (int): Port to serve on (default: 8000)
497
+ domain (Optional[str]): Custom domain
498
+ subdirectory (Optional[str]): Subdirectory path
499
+
500
+ Returns:
501
+ Dict[str, Any]: Deployment result
502
+ """
503
+ temp_dir = None
504
+ try:
505
+ # Validate subdirectory
506
+ subdirectory = self._validate_subdirectory(subdirectory)
507
+
508
+ # Create temporary directory
509
+ temp_dir = tempfile.mkdtemp(prefix="web_deploy_")
510
+
511
+ # Handle subdirectory for multi-user deployment
512
+ if subdirectory:
513
+ deploy_dir = os.path.join(temp_dir, subdirectory)
514
+ os.makedirs(deploy_dir, exist_ok=True)
515
+ html_file_path = os.path.join(deploy_dir, file_name)
516
+ else:
517
+ html_file_path = os.path.join(temp_dir, file_name)
518
+
519
+ # Write enhanced HTML content to file
520
+ with open(html_file_path, 'w', encoding='utf-8') as f:
521
+ f.write(html_content)
522
+
523
+ # Start server
524
+ server_result = self._serve_static_files(temp_dir, port)
525
+
526
+ if server_result['success']:
527
+ # Build URLs with localhost fallback
528
+ local_url = server_result["server_url"]
529
+ if subdirectory:
530
+ local_url += f"/{subdirectory}"
531
+
532
+ # Custom domain URL (if provided)
533
+ custom_url = (
534
+ self._build_custom_url(domain, subdirectory)
535
+ if domain
536
+ else None
537
+ )
538
+
539
+ # Localhost fallback URL
540
+ localhost_url = f"http://localhost:{port}"
541
+ if subdirectory:
542
+ localhost_url += f"/{subdirectory}"
543
+
544
+ # Build message with all access options
545
+ message = 'HTML content deployed successfully!\n'
546
+ message += f' • Local access: {local_url}\n'
547
+ message += f' • Localhost fallback: {localhost_url}'
548
+
549
+ if custom_url:
550
+ message += f'\n • Custom domain: {custom_url}'
551
+
552
+ if self.add_branding_tag:
553
+ message += f'\n • Branding: "{self.tag_text}" tag added'
554
+
555
+ server_result.update(
556
+ {
557
+ 'html_file': html_file_path,
558
+ 'temp_directory': temp_dir,
559
+ 'local_url': local_url,
560
+ 'localhost_url': localhost_url,
561
+ 'custom_url': custom_url,
562
+ 'domain': domain,
563
+ 'subdirectory': subdirectory,
564
+ 'message': message,
565
+ 'branding_tag_added': self.add_branding_tag,
566
+ }
567
+ )
568
+
569
+ return server_result
570
+
571
+ except Exception as e:
572
+ # Clean up temp directory on error
573
+ if temp_dir and os.path.exists(temp_dir):
574
+ try:
575
+ shutil.rmtree(temp_dir)
576
+ except Exception:
577
+ pass
578
+ return {'success': False, 'error': str(e)}
579
+
580
+ def _serve_static_files(self, directory: str, port: int) -> Dict[str, Any]:
581
+ r"""Serve static files from a directory using a local HTTP server
582
+ (as a background process).
583
+
584
+ Args:
585
+ directory (str): Directory to serve files from
586
+ port (int): Port to serve on (default: 8000)
587
+
588
+ Returns:
589
+ Dict[str, Any]: Server information
590
+ """
591
+ import subprocess
592
+
593
+ try:
594
+ if not os.path.exists(directory):
595
+ return {
596
+ 'success': False,
597
+ 'error': f'Directory {directory} does not exist',
598
+ }
599
+
600
+ if not os.path.isdir(directory):
601
+ return {
602
+ 'success': False,
603
+ 'error': f'{directory} is not a directory',
604
+ }
605
+
606
+ # Validate port
607
+ port = self._validate_port(port)
608
+
609
+ # Check if port is already in use
610
+ if port in self.server_instances:
611
+ return {
612
+ 'success': False,
613
+ 'error': f'Port {port} is already in use by this toolkit',
614
+ }
615
+
616
+ # Check if port is available
617
+ if not self._is_port_available(port):
618
+ return {
619
+ 'success': False,
620
+ 'error': (
621
+ f'Port {port} is already in use by another process'
622
+ ),
623
+ }
624
+
625
+ # Start http.server as a background process with security
626
+ # improvements
627
+ process = subprocess.Popen(
628
+ [
629
+ "python3",
630
+ "-m",
631
+ "http.server",
632
+ str(port),
633
+ "--bind",
634
+ "127.0.0.1",
635
+ ],
636
+ cwd=directory,
637
+ stdout=subprocess.DEVNULL,
638
+ stderr=subprocess.DEVNULL,
639
+ shell=False, # Prevent shell injection
640
+ env={**os.environ, 'PYTHONDONTWRITEBYTECODE': '1'},
641
+ )
642
+
643
+ # Store both process and metadata for persistence
644
+ self.server_instances[port] = {
645
+ 'process': process,
646
+ 'pid': process.pid,
647
+ 'start_time': time.time(),
648
+ 'directory': directory,
649
+ }
650
+ self._save_server_registry()
651
+
652
+ # Wait for server to start with timeout
653
+ start_time = time.time()
654
+ while time.time() - start_time < 5:
655
+ if not self._is_port_available(port):
656
+ # Port is now in use, server started
657
+ break
658
+ time.sleep(0.1)
659
+ else:
660
+ # Server didn't start in time
661
+ process.terminate()
662
+ del self.server_instances[port]
663
+ return {
664
+ 'success': False,
665
+ 'error': f'Server failed to start on port {port}',
666
+ }
667
+
668
+ server_url = f"http://localhost:{port}"
669
+
670
+ return {
671
+ 'success': True,
672
+ 'server_url': server_url,
673
+ 'port': port,
674
+ 'directory': directory,
675
+ 'message': f'Static files served from {directory} at '
676
+ f'{server_url} (background process)',
677
+ }
678
+
679
+ except Exception as e:
680
+ logger.error(f"Error serving static files: {e}")
681
+ return {'success': False, 'error': str(e)}
682
+
683
+ def deploy_folder(
684
+ self,
685
+ folder_path: str,
686
+ port: int = 8000,
687
+ domain: Optional[str] = None,
688
+ subdirectory: Optional[str] = None,
689
+ ) -> Dict[str, Any]:
690
+ r"""Deploy a folder containing web files.
691
+
692
+ Args:
693
+ folder_path (str): Path to the folder to deploy.
694
+ port (int): Port to serve on. (default: :obj:`8000`)
695
+ domain (Optional[str]): Custom domain to access the content.
696
+ (e.g., :obj:`example.com`)
697
+ subdirectory (Optional[str]): Subdirectory path for multi-user
698
+ deployment. (e.g., :obj:`user123`)
699
+
700
+ Returns:
701
+ Dict[str, Any]: Deployment result with custom domain info.
702
+ """
703
+ try:
704
+ if not os.path.exists(folder_path):
705
+ return {
706
+ 'success': False,
707
+ 'error': f'Folder {folder_path} does not exist',
708
+ }
709
+
710
+ if not os.path.isdir(folder_path):
711
+ return {
712
+ 'success': False,
713
+ 'error': f'{folder_path} is not a directory',
714
+ }
715
+
716
+ # Validate subdirectory
717
+ subdirectory = self._validate_subdirectory(subdirectory)
718
+
719
+ # Check if remote deployment is configured
720
+ if self.remote_server_ip:
721
+ return self._deploy_folder_to_remote_server(
722
+ folder_path,
723
+ subdirectory,
724
+ domain,
725
+ )
726
+ else:
727
+ return self._deploy_folder_to_local_server(
728
+ folder_path,
729
+ port,
730
+ domain,
731
+ subdirectory,
732
+ )
733
+
734
+ except Exception as e:
735
+ logger.error(f"Error deploying folder: {e}")
736
+ return {'success': False, 'error': str(e)}
737
+
738
+ def _deploy_folder_to_local_server(
739
+ self,
740
+ folder_path: str,
741
+ port: int,
742
+ domain: Optional[str],
743
+ subdirectory: Optional[str],
744
+ ) -> Dict[str, Any]:
745
+ r"""Deploy folder to local server (original functionality).
746
+
747
+ Args:
748
+ folder_path (str): Path to the folder to deploy
749
+ port (int): Port to serve on
750
+ domain (Optional[str]): Custom domain
751
+ subdirectory (Optional[str]): Subdirectory path
752
+
753
+ Returns:
754
+ Dict[str, Any]: Deployment result
755
+ """
756
+ try:
757
+ temp_dir = None
758
+ if self.add_branding_tag:
759
+ # Create temporary directory and copy all files
760
+ temp_dir = tempfile.mkdtemp(prefix="web_deploy_enhanced_")
761
+
762
+ # Handle subdirectory structure
763
+ if subdirectory:
764
+ deploy_base = os.path.join(temp_dir, subdirectory)
765
+ os.makedirs(deploy_base, exist_ok=True)
766
+ shutil.copytree(
767
+ folder_path,
768
+ deploy_base,
769
+ dirs_exist_ok=True,
770
+ )
771
+ deploy_path = deploy_base
772
+ else:
773
+ shutil.copytree(
774
+ folder_path,
775
+ os.path.join(temp_dir, "site"),
776
+ dirs_exist_ok=True,
777
+ )
778
+ deploy_path = os.path.join(temp_dir, "site")
779
+
780
+ # Enhance HTML files with branding tag
781
+ html_files_enhanced = []
782
+ for root, _, files in os.walk(deploy_path):
783
+ for file in files:
784
+ if file.endswith('.html'):
785
+ html_file_path = os.path.join(root, file)
786
+ try:
787
+ with open(
788
+ html_file_path, 'r', encoding='utf-8'
789
+ ) as f:
790
+ original_content = f.read()
791
+
792
+ with open(
793
+ html_file_path, 'w', encoding='utf-8'
794
+ ) as f:
795
+ f.write(original_content)
796
+
797
+ html_files_enhanced.append(
798
+ os.path.relpath(
799
+ html_file_path, deploy_path
800
+ )
801
+ )
802
+ except Exception as e:
803
+ logger.warning(
804
+ f"Failed to enhance {html_file_path}: {e}"
805
+ )
806
+
807
+ # Serve the enhanced folder
808
+ server_result = self._serve_static_files(temp_dir, port)
809
+
810
+ if server_result['success']:
811
+ # Build URLs with localhost fallback
812
+ local_url = server_result["server_url"]
813
+ if subdirectory:
814
+ local_url += f"/{subdirectory}"
815
+
816
+ # Custom domain URL (if provided)
817
+ custom_url = (
818
+ self._build_custom_url(domain, subdirectory)
819
+ if domain
820
+ else None
821
+ )
822
+
823
+ # Localhost fallback URL
824
+ localhost_url = f"http://localhost:{port}"
825
+ if subdirectory:
826
+ localhost_url += f"/{subdirectory}"
827
+
828
+ # Build message with all access options
829
+ message = 'Folder deployed successfully!\n'
830
+ message += f' • Local access: {local_url}\n'
831
+ message += f' • Localhost fallback: {localhost_url}'
832
+
833
+ if custom_url:
834
+ message += f'\n • Custom domain: {custom_url}'
835
+
836
+ if self.add_branding_tag:
837
+ message += f'\n • Branding: "{self.tag_text}" tag '
838
+ message += (
839
+ f'added to {len(html_files_enhanced)} HTML files'
840
+ )
841
+
842
+ server_result.update(
843
+ {
844
+ 'original_folder': folder_path,
845
+ 'enhanced_folder': deploy_path,
846
+ 'html_files_enhanced': html_files_enhanced,
847
+ 'local_url': local_url,
848
+ 'localhost_url': localhost_url,
849
+ 'custom_url': custom_url,
850
+ 'domain': domain,
851
+ 'subdirectory': subdirectory,
852
+ 'branding_tag_added': True,
853
+ 'message': message,
854
+ }
855
+ )
856
+
857
+ return server_result
858
+ else:
859
+ # Check for index.html
860
+ index_html = os.path.join(folder_path, 'index.html')
861
+ if not os.path.exists(index_html):
862
+ logger.warning(f'No index.html found in {folder_path}')
863
+
864
+ # Handle subdirectory for original folder deployment
865
+ if subdirectory:
866
+ temp_dir = tempfile.mkdtemp(prefix="web_deploy_")
867
+ deploy_base = os.path.join(temp_dir, subdirectory)
868
+ shutil.copytree(
869
+ folder_path, deploy_base, dirs_exist_ok=True
870
+ )
871
+ deploy_path = temp_dir
872
+ else:
873
+ deploy_path = folder_path
874
+
875
+ # Serve the folder
876
+ server_result = self._serve_static_files(deploy_path, port)
877
+
878
+ if server_result['success']:
879
+ # Build URLs with localhost fallback
880
+ local_url = server_result["server_url"]
881
+ if subdirectory:
882
+ local_url += f"/{subdirectory}"
883
+
884
+ # Custom domain URL (if provided)
885
+ custom_url = (
886
+ self._build_custom_url(domain, subdirectory)
887
+ if domain
888
+ else None
889
+ )
890
+
891
+ # Localhost fallback URL
892
+ localhost_url = f"http://localhost:{port}"
893
+ if subdirectory:
894
+ localhost_url += f"/{subdirectory}"
895
+
896
+ # Build message with all access options
897
+ message = 'Folder deployed successfully!\n'
898
+ message += f' • Local access: {local_url}\n'
899
+ message += f' • Localhost fallback: {localhost_url}'
900
+
901
+ if custom_url:
902
+ message += f'\n • Custom domain: {custom_url}'
903
+
904
+ server_result.update(
905
+ {
906
+ 'local_url': local_url,
907
+ 'localhost_url': localhost_url,
908
+ 'custom_url': custom_url,
909
+ 'domain': domain,
910
+ 'subdirectory': subdirectory,
911
+ 'message': message,
912
+ 'branding_tag_added': False,
913
+ }
914
+ )
915
+
916
+ return server_result
917
+
918
+ except Exception as e:
919
+ # Clean up temp directory on error
920
+ if (
921
+ 'temp_dir' in locals()
922
+ and temp_dir
923
+ and os.path.exists(temp_dir)
924
+ ):
925
+ try:
926
+ shutil.rmtree(temp_dir)
927
+ except Exception:
928
+ pass
929
+ logger.error(f"Error deploying folder: {e}")
930
+ return {'success': False, 'error': str(e)}
931
+
932
+ def _deploy_folder_to_remote_server(
933
+ self,
934
+ folder_path: str,
935
+ subdirectory: Optional[str] = None,
936
+ domain: Optional[str] = None,
937
+ ) -> Dict[str, Any]:
938
+ r"""Deploy folder to remote server via API.
939
+
940
+ Args:
941
+ folder_path (str): Path to the folder to deploy
942
+ subdirectory (Optional[str]): Subdirectory path for deployment
943
+ domain (Optional[str]): Custom domain
944
+
945
+ Returns:
946
+ Dict[str, Any]: Deployment result
947
+ """
948
+ try:
949
+ import tempfile
950
+ import zipfile
951
+
952
+ import requests
953
+
954
+ # Validate subdirectory
955
+ subdirectory = self._validate_subdirectory(subdirectory)
956
+
957
+ # Create a temporary zip file of the folder
958
+ with tempfile.NamedTemporaryFile(
959
+ suffix='.zip', delete=False
960
+ ) as temp_zip:
961
+ zip_path = temp_zip.name
962
+
963
+ try:
964
+ # Create zip archive
965
+ with zipfile.ZipFile(
966
+ zip_path, 'w', zipfile.ZIP_DEFLATED
967
+ ) as zipf:
968
+ for root, _, files in os.walk(folder_path):
969
+ for file in files:
970
+ file_path = os.path.join(root, file)
971
+ # Calculate relative path within the archive
972
+ arcname = os.path.relpath(file_path, folder_path)
973
+ zipf.write(file_path, arcname)
974
+
975
+ # Read zip file as base64
976
+ with open(zip_path, 'rb') as f:
977
+ zip_data = base64.b64encode(f.read()).decode('utf-8')
978
+
979
+ # Prepare deployment data
980
+ deploy_data = {
981
+ "deployment_type": "folder",
982
+ "folder_data": zip_data,
983
+ "subdirectory": subdirectory,
984
+ "domain": domain,
985
+ "timestamp": time.time(),
986
+ }
987
+
988
+ # Add logo data if custom logo is specified
989
+ if self.logo_path and os.path.exists(self.logo_path):
990
+ try:
991
+ logo_ext = os.path.splitext(self.logo_path)[1]
992
+ logo_filename = f"custom_logo{logo_ext}"
993
+
994
+ with open(self.logo_path, 'rb') as logo_file:
995
+ logo_data = base64.b64encode(
996
+ logo_file.read()
997
+ ).decode('utf-8')
998
+
999
+ deploy_data.update(
1000
+ {
1001
+ "logo_data": logo_data,
1002
+ "logo_ext": logo_ext,
1003
+ "logo_filename": logo_filename,
1004
+ }
1005
+ )
1006
+ except Exception as logo_error:
1007
+ logger.warning(
1008
+ f"Failed to process custom logo: {logo_error}"
1009
+ )
1010
+
1011
+ # Send to remote server API
1012
+ api_url = f"http://{self.remote_server_ip}:{self.remote_server_port}/api/deploy"
1013
+
1014
+ response = requests.post(
1015
+ api_url,
1016
+ json=deploy_data,
1017
+ timeout=self.timeout
1018
+ or 60, # Extended timeout for folder uploads
1019
+ allow_redirects=False,
1020
+ headers={'Content-Type': 'application/json'},
1021
+ )
1022
+
1023
+ if response.status_code == 200:
1024
+ result = response.json()
1025
+
1026
+ # Build URLs
1027
+ base_url = f"http://{self.remote_server_ip}:{self.remote_server_port}"
1028
+ deployed_url = (
1029
+ f"{base_url}/{subdirectory}/"
1030
+ if subdirectory
1031
+ else base_url
1032
+ )
1033
+
1034
+ return {
1035
+ 'success': True,
1036
+ 'remote_url': deployed_url,
1037
+ 'server_ip': self.remote_server_ip,
1038
+ 'subdirectory': subdirectory,
1039
+ 'domain': domain,
1040
+ 'message': (
1041
+ f'Successfully deployed folder to remote server!\n'
1042
+ f' • Access URL: {deployed_url}\n'
1043
+ f' • Server: '
1044
+ f'{self.remote_server_ip}:{self.remote_server_port}'
1045
+ ),
1046
+ 'branding_tag_added': self.add_branding_tag,
1047
+ 'logo_processed': result.get('logo_processed', False),
1048
+ }
1049
+ else:
1050
+ return {
1051
+ 'success': False,
1052
+ 'error': (
1053
+ f'Remote folder deployment failed: '
1054
+ f'HTTP {response.status_code}'
1055
+ ),
1056
+ }
1057
+
1058
+ finally:
1059
+ # Clean up temporary zip file
1060
+ if os.path.exists(zip_path):
1061
+ os.unlink(zip_path)
1062
+
1063
+ except ImportError:
1064
+ return {
1065
+ 'success': False,
1066
+ 'error': 'Remote deployment requires requests library. '
1067
+ 'Install with: pip install requests',
1068
+ }
1069
+ except Exception as e:
1070
+ return {
1071
+ 'success': False,
1072
+ 'error': f'Remote folder deployment error: {e!s}',
1073
+ }
1074
+
1075
+ def stop_server(self, port: int) -> Dict[str, Any]:
1076
+ r"""Stop a running server on the specified port.
1077
+
1078
+ Args:
1079
+ port (int): Port of the server to stop.
1080
+
1081
+ Returns:
1082
+ Dict[str, Any]: Result of stopping the server.
1083
+ """
1084
+ try:
1085
+ # Validate port
1086
+ port = self._validate_port(port)
1087
+ # First check persistent registry for servers
1088
+ self._load_server_registry()
1089
+
1090
+ if port not in self.server_instances:
1091
+ # Check if there's a process running on this port by PID
1092
+ if os.path.exists(self.server_registry_file):
1093
+ with open(self.server_registry_file, 'r') as f:
1094
+ data = json.load(f)
1095
+ port_str = str(port)
1096
+ if port_str in data:
1097
+ pid = data[port_str].get('pid')
1098
+ if pid and self._is_process_running(pid):
1099
+ try:
1100
+ os.kill(pid, 15) # SIGTERM
1101
+ # Remove from registry
1102
+ del data[port_str]
1103
+ with open(
1104
+ self.server_registry_file, 'w'
1105
+ ) as f:
1106
+ json.dump(data, f, indent=2)
1107
+ return {
1108
+ 'success': True,
1109
+ 'port': port,
1110
+ 'message': (
1111
+ f'Server on port {port} stopped '
1112
+ f'successfully (from registry)'
1113
+ ),
1114
+ }
1115
+ except Exception as e:
1116
+ logger.error(
1117
+ f"Error stopping server by PID: {e}"
1118
+ )
1119
+
1120
+ return {
1121
+ 'success': False,
1122
+ 'error': f'No server running on port {port}',
1123
+ }
1124
+
1125
+ server_info = self.server_instances[port]
1126
+ if isinstance(server_info, dict):
1127
+ process = server_info.get('process')
1128
+ pid = server_info.get('pid')
1129
+
1130
+ # Stop the main server process
1131
+ if process:
1132
+ process.terminate()
1133
+ process.wait(
1134
+ timeout=5
1135
+ ) # Wait for process to terminate gracefully
1136
+ elif pid and self._is_process_running(pid):
1137
+ os.kill(pid, 15) # SIGTERM
1138
+
1139
+ else:
1140
+ # Handle old-style direct process objects
1141
+ server_info.terminate()
1142
+ server_info.wait(timeout=5)
1143
+
1144
+ del self.server_instances[port]
1145
+ self._save_server_registry()
1146
+
1147
+ return {
1148
+ 'success': True,
1149
+ 'port': port,
1150
+ 'message': f'Server on port {port} stopped successfully',
1151
+ }
1152
+
1153
+ except subprocess.TimeoutExpired:
1154
+ if isinstance(server_info, dict):
1155
+ process = server_info.get('process')
1156
+
1157
+ if process:
1158
+ process.kill()
1159
+ process.wait(timeout=5)
1160
+ else:
1161
+ server_info.kill()
1162
+ server_info.wait(timeout=5)
1163
+ del self.server_instances[port]
1164
+ self._save_server_registry()
1165
+ return {
1166
+ 'success': True,
1167
+ 'port': port,
1168
+ 'message': f'Server on port {port} stopped after timeout',
1169
+ }
1170
+ except Exception as e:
1171
+ logger.error(f"Error stopping server: {e}")
1172
+ return {'success': False, 'error': str(e)}
1173
+
1174
+ def list_running_servers(self) -> Dict[str, Any]:
1175
+ r"""List all currently running servers.
1176
+
1177
+ Returns:
1178
+ Dict[str, Any]: Information about running servers
1179
+ """
1180
+ try:
1181
+ self._load_server_registry()
1182
+
1183
+ running_servers = []
1184
+ current_time = time.time()
1185
+
1186
+ for port, server_info in self.server_instances.items():
1187
+ if isinstance(server_info, dict):
1188
+ start_time = server_info.get('start_time', 0)
1189
+ running_time = current_time - start_time
1190
+
1191
+ running_servers.append(
1192
+ {
1193
+ 'port': port,
1194
+ 'pid': server_info.get('pid'),
1195
+ 'directory': server_info.get('directory'),
1196
+ 'start_time': start_time,
1197
+ 'running_time': running_time,
1198
+ 'url': f'http://localhost:{port}',
1199
+ }
1200
+ )
1201
+
1202
+ return {
1203
+ 'success': True,
1204
+ 'servers': running_servers,
1205
+ 'total_servers': len(running_servers),
1206
+ }
1207
+
1208
+ except Exception as e:
1209
+ logger.error(f"Error listing servers: {e}")
1210
+ return {'success': False, 'error': str(e)}
1211
+
1212
+ def get_tools(self) -> List[FunctionTool]:
1213
+ r"""Get all available tools from the WebDeployToolkit."""
1214
+ return [
1215
+ FunctionTool(self.deploy_html_content),
1216
+ FunctionTool(self.deploy_folder),
1217
+ FunctionTool(self.stop_server),
1218
+ FunctionTool(self.list_running_servers),
1219
+ ]