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.
- camel/__init__.py +3 -3
- camel/agents/__init__.py +2 -2
- camel/agents/_types.py +9 -4
- camel/agents/_utils.py +40 -2
- camel/agents/base.py +2 -2
- camel/agents/chat_agent.py +4835 -947
- camel/agents/critic_agent.py +2 -2
- camel/agents/deductive_reasoner_agent.py +56 -56
- camel/agents/embodied_agent.py +2 -2
- camel/agents/knowledge_graph_agent.py +20 -20
- camel/agents/mcp_agent.py +35 -36
- camel/agents/multi_hop_generator_agent.py +3 -3
- camel/agents/programmed_agent_instruction.py +2 -2
- camel/agents/repo_agent.py +4 -3
- camel/agents/role_assignment_agent.py +2 -2
- camel/agents/search_agent.py +2 -2
- camel/agents/task_agent.py +2 -2
- camel/agents/tool_agents/__init__.py +2 -2
- camel/agents/tool_agents/base.py +2 -2
- camel/agents/tool_agents/hugging_face_tool_agent.py +3 -3
- camel/benchmarks/__init__.py +2 -2
- camel/benchmarks/apibank.py +5 -5
- camel/benchmarks/apibench.py +2 -2
- camel/benchmarks/base.py +2 -2
- camel/benchmarks/browsecomp.py +44 -33
- camel/benchmarks/gaia.py +17 -13
- camel/benchmarks/mock_website/README.md +1 -3
- camel/benchmarks/mock_website/mock_web.py +2 -2
- camel/benchmarks/mock_website/requirements.txt +1 -1
- camel/benchmarks/mock_website/shopping_mall/app.py +2 -2
- camel/benchmarks/mock_website/task.json +1 -1
- camel/benchmarks/nexus.py +3 -3
- camel/benchmarks/ragbench.py +2 -2
- camel/bots/__init__.py +2 -2
- camel/bots/discord/__init__.py +2 -2
- camel/bots/discord/discord_app.py +2 -2
- camel/bots/discord/discord_installation.py +2 -2
- camel/bots/discord/discord_store.py +3 -3
- camel/bots/slack/__init__.py +2 -2
- camel/bots/slack/models.py +4 -4
- camel/bots/slack/slack_app.py +2 -2
- camel/bots/telegram_bot.py +2 -2
- camel/configs/__init__.py +23 -2
- camel/configs/aihubmix_config.py +90 -0
- camel/configs/aiml_config.py +2 -2
- camel/configs/amd_config.py +70 -0
- camel/configs/anthropic_config.py +2 -2
- camel/configs/base_config.py +2 -2
- camel/configs/bedrock_config.py +5 -3
- camel/configs/cerebras_config.py +98 -0
- camel/configs/cohere_config.py +2 -2
- camel/configs/cometapi_config.py +106 -0
- camel/configs/crynux_config.py +2 -2
- camel/configs/deepseek_config.py +9 -8
- camel/configs/gemini_config.py +6 -4
- camel/configs/groq_config.py +6 -4
- camel/configs/internlm_config.py +6 -4
- camel/configs/litellm_config.py +2 -2
- camel/configs/lmstudio_config.py +6 -4
- camel/configs/minimax_config.py +95 -0
- camel/configs/mistral_config.py +2 -2
- camel/configs/modelscope_config.py +5 -3
- camel/configs/moonshot_config.py +2 -2
- camel/configs/nebius_config.py +105 -0
- camel/configs/netmind_config.py +2 -2
- camel/configs/novita_config.py +2 -2
- camel/configs/nvidia_config.py +2 -2
- camel/configs/ollama_config.py +2 -2
- camel/configs/openai_config.py +5 -3
- camel/configs/openrouter_config.py +6 -4
- camel/configs/ppio_config.py +2 -2
- camel/configs/qianfan_config.py +85 -0
- camel/configs/qwen_config.py +2 -2
- camel/configs/reka_config.py +2 -2
- camel/configs/samba_config.py +6 -4
- camel/configs/sglang_config.py +2 -2
- camel/configs/siliconflow_config.py +2 -2
- camel/configs/togetherai_config.py +2 -2
- camel/configs/vllm_config.py +4 -2
- camel/configs/watsonx_config.py +2 -2
- camel/configs/yi_config.py +6 -4
- camel/configs/zhipuai_config.py +6 -4
- camel/data_collectors/__init__.py +2 -2
- camel/data_collectors/alpaca_collector.py +18 -9
- camel/data_collectors/base.py +2 -2
- camel/data_collectors/sharegpt_collector.py +2 -2
- camel/datagen/__init__.py +2 -2
- camel/datagen/cot_datagen.py +3 -3
- camel/datagen/evol_instruct/__init__.py +2 -2
- camel/datagen/evol_instruct/evol_instruct.py +2 -2
- camel/datagen/evol_instruct/scorer.py +12 -12
- camel/datagen/evol_instruct/templates.py +16 -16
- camel/datagen/self_improving_cot.py +5 -5
- camel/datagen/self_instruct/__init__.py +2 -2
- camel/datagen/self_instruct/filter/__init__.py +2 -2
- camel/datagen/self_instruct/filter/filter_function.py +2 -2
- camel/datagen/self_instruct/filter/filter_registry.py +2 -2
- camel/datagen/self_instruct/filter/instruction_filter.py +2 -2
- camel/datagen/self_instruct/self_instruct.py +2 -2
- camel/datagen/self_instruct/templates.py +47 -47
- camel/datagen/source2synth/__init__.py +2 -2
- camel/datagen/source2synth/data_processor.py +2 -2
- camel/datagen/source2synth/models.py +2 -2
- camel/datagen/source2synth/user_data_processor_config.py +2 -2
- camel/datahubs/__init__.py +2 -2
- camel/datahubs/base.py +2 -2
- camel/datahubs/huggingface.py +2 -2
- camel/datahubs/models.py +2 -2
- camel/datasets/__init__.py +2 -2
- camel/datasets/base_generator.py +41 -12
- camel/datasets/few_shot_generator.py +18 -18
- camel/datasets/models.py +2 -2
- camel/datasets/self_instruct_generator.py +2 -2
- camel/datasets/static_dataset.py +2 -2
- camel/embeddings/__init__.py +2 -2
- camel/embeddings/azure_embedding.py +2 -2
- camel/embeddings/base.py +2 -2
- camel/embeddings/gemini_embedding.py +2 -2
- camel/embeddings/jina_embedding.py +2 -2
- camel/embeddings/mistral_embedding.py +2 -2
- camel/embeddings/openai_compatible_embedding.py +2 -2
- camel/embeddings/openai_embedding.py +2 -2
- camel/embeddings/sentence_transformers_embeddings.py +2 -2
- camel/embeddings/together_embedding.py +2 -2
- camel/embeddings/vlm_embedding.py +2 -2
- camel/environments/__init__.py +14 -2
- camel/environments/models.py +2 -2
- camel/environments/multi_step.py +2 -2
- camel/environments/rlcards_env.py +860 -0
- camel/environments/single_step.py +30 -5
- camel/environments/tic_tac_toe.py +3 -3
- camel/extractors/__init__.py +2 -2
- camel/extractors/base.py +2 -2
- camel/extractors/python_strategies.py +2 -2
- camel/generators.py +2 -2
- camel/human.py +2 -2
- camel/interpreters/__init__.py +4 -2
- camel/interpreters/base.py +2 -2
- camel/interpreters/docker/Dockerfile +14 -24
- camel/interpreters/docker_interpreter.py +5 -4
- camel/interpreters/e2b_interpreter.py +36 -3
- camel/interpreters/internal_python_interpreter.py +53 -4
- camel/interpreters/interpreter_error.py +2 -2
- camel/interpreters/ipython_interpreter.py +2 -2
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/interpreters/subprocess_interpreter.py +2 -2
- camel/loaders/__init__.py +13 -4
- camel/loaders/apify_reader.py +2 -2
- camel/loaders/base_io.py +2 -2
- camel/loaders/base_loader.py +85 -0
- camel/loaders/chunkr_reader.py +11 -2
- camel/loaders/crawl4ai_reader.py +2 -2
- camel/loaders/firecrawl_reader.py +6 -6
- camel/loaders/jina_url_reader.py +2 -2
- camel/loaders/markitdown.py +2 -2
- camel/loaders/mineru_extractor.py +2 -2
- camel/loaders/mistral_reader.py +2 -2
- camel/loaders/scrapegraph_reader.py +2 -2
- camel/loaders/unstructured_io.py +2 -2
- camel/logger.py +5 -5
- camel/memories/__init__.py +2 -2
- camel/memories/agent_memories.py +86 -3
- camel/memories/base.py +36 -2
- camel/memories/blocks/__init__.py +2 -2
- camel/memories/blocks/chat_history_block.py +125 -7
- camel/memories/blocks/vectordb_block.py +10 -3
- camel/memories/context_creators/__init__.py +2 -2
- camel/memories/context_creators/score_based.py +31 -239
- camel/memories/records.py +90 -10
- camel/messages/__init__.py +2 -2
- camel/messages/base.py +178 -43
- camel/messages/conversion/__init__.py +2 -2
- camel/messages/conversion/alpaca.py +2 -2
- camel/messages/conversion/conversation_models.py +2 -2
- camel/messages/conversion/sharegpt/__init__.py +2 -2
- camel/messages/conversion/sharegpt/function_call_formatter.py +2 -2
- camel/messages/conversion/sharegpt/hermes/__init__.py +2 -2
- camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py +2 -2
- camel/messages/func_message.py +54 -17
- camel/models/__init__.py +16 -2
- camel/models/_utils.py +3 -3
- camel/models/aihubmix_model.py +83 -0
- camel/models/aiml_model.py +11 -18
- camel/models/amd_model.py +101 -0
- camel/models/anthropic_model.py +127 -20
- camel/models/aws_bedrock_model.py +12 -35
- camel/models/azure_openai_model.py +212 -89
- camel/models/base_audio_model.py +5 -3
- camel/models/base_model.py +195 -26
- camel/models/cerebras_model.py +83 -0
- camel/models/cohere_model.py +16 -21
- camel/models/cometapi_model.py +83 -0
- camel/models/crynux_model.py +11 -18
- camel/models/deepseek_model.py +18 -58
- camel/models/fish_audio_model.py +8 -2
- camel/models/gemini_model.py +389 -26
- camel/models/groq_model.py +11 -19
- camel/models/internlm_model.py +11 -18
- camel/models/litellm_model.py +56 -34
- camel/models/lmstudio_model.py +17 -20
- camel/models/minimax_model.py +83 -0
- camel/models/mistral_model.py +18 -19
- camel/models/model_factory.py +37 -3
- camel/models/model_manager.py +26 -8
- camel/models/modelscope_model.py +13 -193
- camel/models/moonshot_model.py +195 -21
- camel/models/nebius_model.py +83 -0
- camel/models/nemotron_model.py +19 -9
- camel/models/netmind_model.py +11 -18
- camel/models/novita_model.py +11 -18
- camel/models/nvidia_model.py +11 -18
- camel/models/ollama_model.py +14 -21
- camel/models/openai_audio_models.py +2 -2
- camel/models/openai_compatible_model.py +188 -45
- camel/models/openai_model.py +216 -71
- camel/models/openrouter_model.py +11 -19
- camel/models/ppio_model.py +11 -18
- camel/models/qianfan_model.py +89 -0
- camel/models/qwen_model.py +13 -193
- camel/models/reka_model.py +21 -21
- camel/models/reward/__init__.py +2 -2
- camel/models/reward/base_reward_model.py +2 -2
- camel/models/reward/evaluator.py +2 -2
- camel/models/reward/nemotron_model.py +2 -2
- camel/models/reward/skywork_model.py +2 -2
- camel/models/samba_model.py +48 -47
- camel/models/sglang_model.py +88 -40
- camel/models/siliconflow_model.py +12 -35
- camel/models/stub_model.py +10 -7
- camel/models/togetherai_model.py +11 -18
- camel/models/vllm_model.py +10 -18
- camel/models/volcano_model.py +16 -20
- camel/models/watsonx_model.py +7 -19
- camel/models/yi_model.py +11 -18
- camel/models/zhipuai_model.py +70 -18
- camel/parsers/__init__.py +18 -0
- camel/parsers/mcp_tool_call_parser.py +176 -0
- camel/personas/__init__.py +2 -2
- camel/personas/persona.py +2 -2
- camel/personas/persona_hub.py +2 -2
- camel/prompts/__init__.py +2 -2
- camel/prompts/ai_society.py +2 -2
- camel/prompts/base.py +2 -2
- camel/prompts/code.py +2 -2
- camel/prompts/evaluation.py +2 -2
- camel/prompts/generate_text_embedding_data.py +2 -2
- camel/prompts/image_craft.py +2 -2
- camel/prompts/misalignment.py +2 -2
- camel/prompts/multi_condition_image_craft.py +2 -2
- camel/prompts/object_recognition.py +2 -2
- camel/prompts/persona_hub.py +3 -3
- camel/prompts/prompt_templates.py +2 -2
- camel/prompts/role_description_prompt_template.py +2 -2
- camel/prompts/solution_extraction.py +8 -8
- camel/prompts/task_prompt_template.py +2 -2
- camel/prompts/translation.py +2 -2
- camel/prompts/video_description_prompt.py +3 -3
- camel/responses/__init__.py +2 -2
- camel/responses/agent_responses.py +2 -2
- camel/retrievers/__init__.py +2 -2
- camel/retrievers/auto_retriever.py +3 -2
- camel/retrievers/base.py +2 -2
- camel/retrievers/bm25_retriever.py +2 -2
- camel/retrievers/cohere_rerank_retriever.py +2 -2
- camel/retrievers/hybrid_retrival.py +2 -2
- camel/retrievers/vector_retriever.py +2 -2
- camel/runtimes/Dockerfile.multi-toolkit +90 -0
- camel/runtimes/__init__.py +2 -2
- camel/runtimes/api.py +79 -23
- camel/runtimes/base.py +2 -2
- camel/runtimes/configs.py +13 -13
- camel/runtimes/daytona_runtime.py +17 -18
- camel/runtimes/docker_runtime.py +12 -12
- camel/runtimes/llm_guard_runtime.py +26 -26
- camel/runtimes/remote_http_runtime.py +11 -11
- camel/runtimes/ubuntu_docker_runtime.py +2 -2
- camel/runtimes/utils/__init__.py +2 -2
- camel/runtimes/utils/function_risk_toolkit.py +2 -2
- camel/runtimes/utils/ignore_risk_toolkit.py +2 -2
- camel/schemas/__init__.py +2 -2
- camel/schemas/base.py +2 -2
- camel/schemas/openai_converter.py +3 -3
- camel/schemas/outlines_converter.py +2 -2
- camel/services/agent_openapi_server.py +380 -0
- camel/societies/__init__.py +4 -2
- camel/societies/babyagi_playing.py +2 -2
- camel/societies/role_playing.py +201 -80
- camel/societies/workforce/__init__.py +10 -3
- camel/societies/workforce/base.py +2 -2
- camel/societies/workforce/events.py +143 -0
- camel/societies/workforce/prompts.py +258 -33
- camel/societies/workforce/role_playing_worker.py +88 -31
- camel/societies/workforce/single_agent_worker.py +638 -40
- camel/societies/workforce/structured_output_handler.py +512 -0
- camel/societies/workforce/task_channel.py +182 -38
- camel/societies/workforce/utils.py +780 -65
- camel/societies/workforce/worker.py +92 -26
- camel/societies/workforce/workflow_memory_manager.py +1746 -0
- camel/societies/workforce/workforce.py +5276 -355
- camel/societies/workforce/workforce_callback.py +103 -0
- camel/societies/workforce/workforce_logger.py +647 -0
- camel/societies/workforce/workforce_metrics.py +33 -0
- camel/storages/__init__.py +6 -2
- camel/storages/graph_storages/__init__.py +2 -2
- camel/storages/graph_storages/base.py +2 -2
- camel/storages/graph_storages/graph_element.py +2 -2
- camel/storages/graph_storages/nebula_graph.py +4 -4
- camel/storages/graph_storages/neo4j_graph.py +7 -7
- camel/storages/key_value_storages/__init__.py +2 -2
- camel/storages/key_value_storages/base.py +2 -2
- camel/storages/key_value_storages/in_memory.py +2 -2
- camel/storages/key_value_storages/json.py +17 -4
- camel/storages/key_value_storages/mem0_cloud.py +50 -49
- camel/storages/key_value_storages/redis.py +2 -2
- camel/storages/object_storages/__init__.py +2 -2
- camel/storages/object_storages/amazon_s3.py +2 -2
- camel/storages/object_storages/azure_blob.py +2 -2
- camel/storages/object_storages/base.py +2 -2
- camel/storages/object_storages/google_cloud.py +3 -3
- camel/storages/vectordb_storages/__init__.py +8 -2
- camel/storages/vectordb_storages/base.py +2 -2
- camel/storages/vectordb_storages/chroma.py +731 -0
- camel/storages/vectordb_storages/faiss.py +2 -2
- camel/storages/vectordb_storages/milvus.py +2 -2
- camel/storages/vectordb_storages/oceanbase.py +15 -15
- camel/storages/vectordb_storages/pgvector.py +349 -0
- camel/storages/vectordb_storages/qdrant.py +6 -6
- camel/storages/vectordb_storages/surreal.py +372 -0
- camel/storages/vectordb_storages/tidb.py +11 -8
- camel/storages/vectordb_storages/weaviate.py +2 -2
- camel/tasks/__init__.py +2 -2
- camel/tasks/task.py +348 -26
- camel/tasks/task_prompt.py +3 -3
- camel/terminators/__init__.py +2 -2
- camel/terminators/base.py +2 -2
- camel/terminators/response_terminator.py +2 -2
- camel/terminators/token_limit_terminator.py +2 -2
- camel/toolkits/__init__.py +54 -10
- camel/toolkits/aci_toolkit.py +66 -21
- camel/toolkits/arxiv_toolkit.py +8 -8
- camel/toolkits/ask_news_toolkit.py +2 -2
- camel/toolkits/async_browser_toolkit.py +4 -4
- camel/toolkits/audio_analysis_toolkit.py +3 -3
- camel/toolkits/base.py +65 -7
- camel/toolkits/bohrium_toolkit.py +2 -2
- camel/toolkits/browser_toolkit.py +34 -21
- camel/toolkits/browser_toolkit_commons.py +4 -4
- camel/toolkits/code_execution.py +31 -4
- camel/toolkits/context_summarizer_toolkit.py +684 -0
- camel/toolkits/craw4ai_toolkit.py +93 -0
- camel/toolkits/dappier_toolkit.py +12 -8
- camel/toolkits/data_commons_toolkit.py +2 -2
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/earth_science_toolkit.py +5367 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +49 -0
- camel/toolkits/excel_toolkit.py +905 -71
- camel/toolkits/file_toolkit.py +1402 -0
- camel/toolkits/function_tool.py +126 -18
- camel/toolkits/github_toolkit.py +109 -22
- camel/toolkits/gmail_toolkit.py +1839 -0
- camel/toolkits/google_calendar_toolkit.py +40 -6
- camel/toolkits/google_drive_mcp_toolkit.py +54 -0
- camel/toolkits/google_maps_toolkit.py +2 -2
- camel/toolkits/google_scholar_toolkit.py +2 -2
- camel/toolkits/human_toolkit.py +36 -12
- camel/toolkits/hybrid_browser_toolkit/__init__.py +18 -0
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +185 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +246 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1973 -0
- camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4589 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +1929 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +233 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +589 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +129 -0
- camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +27 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +319 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +1037 -0
- camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
- camel/toolkits/hybrid_browser_toolkit_py/actions.py +575 -0
- camel/toolkits/hybrid_browser_toolkit_py/agent.py +311 -0
- camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +787 -0
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +490 -0
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2390 -0
- camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +233 -0
- camel/toolkits/hybrid_browser_toolkit_py/stealth_script.js +0 -0
- camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +1043 -0
- camel/toolkits/image_analysis_toolkit.py +3 -6
- camel/toolkits/image_generation_toolkit.py +390 -0
- camel/toolkits/jina_reranker_toolkit.py +5 -6
- camel/toolkits/klavis_toolkit.py +7 -3
- camel/toolkits/linkedin_toolkit.py +2 -2
- camel/toolkits/markitdown_toolkit.py +104 -0
- camel/toolkits/math_toolkit.py +66 -12
- camel/toolkits/mcp_toolkit.py +412 -36
- camel/toolkits/memory_toolkit.py +7 -3
- camel/toolkits/meshy_toolkit.py +2 -2
- camel/toolkits/message_agent_toolkit.py +608 -0
- camel/toolkits/message_integration.py +724 -0
- camel/toolkits/mineru_toolkit.py +2 -2
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/networkx_toolkit.py +2 -2
- camel/toolkits/note_taking_toolkit.py +277 -0
- camel/toolkits/notion_mcp_toolkit.py +224 -0
- camel/toolkits/notion_toolkit.py +2 -2
- camel/toolkits/open_api_specs/biztoc/__init__.py +2 -2
- camel/toolkits/open_api_specs/biztoc/ai-plugin.json +1 -1
- camel/toolkits/open_api_specs/coursera/__init__.py +2 -2
- camel/toolkits/open_api_specs/create_qr_code/__init__.py +2 -2
- camel/toolkits/open_api_specs/klarna/__init__.py +2 -2
- camel/toolkits/open_api_specs/nasa_apod/__init__.py +2 -2
- camel/toolkits/open_api_specs/outschool/__init__.py +2 -2
- camel/toolkits/open_api_specs/outschool/ai-plugin.json +1 -1
- camel/toolkits/open_api_specs/outschool/openapi.yaml +1 -1
- camel/toolkits/open_api_specs/outschool/paths/__init__.py +2 -2
- camel/toolkits/open_api_specs/outschool/paths/get_classes.py +2 -2
- camel/toolkits/open_api_specs/outschool/paths/search_teachers.py +2 -2
- camel/toolkits/open_api_specs/security_config.py +2 -2
- camel/toolkits/open_api_specs/speak/__init__.py +2 -2
- camel/toolkits/open_api_specs/web_scraper/__init__.py +2 -2
- camel/toolkits/open_api_specs/web_scraper/ai-plugin.json +1 -1
- camel/toolkits/open_api_specs/web_scraper/paths/__init__.py +2 -2
- camel/toolkits/open_api_specs/web_scraper/paths/scraper.py +2 -2
- camel/toolkits/open_api_toolkit.py +2 -2
- camel/toolkits/openbb_toolkit.py +7 -3
- camel/toolkits/origene_mcp_toolkit.py +56 -0
- camel/toolkits/page_script.js +53 -53
- camel/toolkits/playwright_mcp_toolkit.py +13 -31
- camel/toolkits/pptx_toolkit.py +36 -23
- camel/toolkits/pubmed_toolkit.py +2 -2
- camel/toolkits/pulse_mcp_search_toolkit.py +2 -2
- camel/toolkits/pyautogui_toolkit.py +2 -2
- camel/toolkits/reddit_toolkit.py +2 -2
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/retrieval_toolkit.py +2 -2
- camel/toolkits/screenshot_toolkit.py +213 -0
- camel/toolkits/search_toolkit.py +539 -146
- camel/toolkits/searxng_toolkit.py +2 -2
- camel/toolkits/semantic_scholar_toolkit.py +2 -2
- camel/toolkits/slack_toolkit.py +108 -58
- camel/toolkits/sql_toolkit.py +712 -0
- camel/toolkits/stripe_toolkit.py +2 -2
- camel/toolkits/sympy_toolkit.py +3 -3
- camel/toolkits/task_planning_toolkit.py +5 -5
- camel/toolkits/terminal_toolkit/__init__.py +18 -0
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +1070 -0
- camel/toolkits/terminal_toolkit/utils.py +532 -0
- camel/toolkits/thinking_toolkit.py +3 -3
- camel/toolkits/twitter_toolkit.py +2 -2
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/video_analysis_toolkit.py +109 -29
- camel/toolkits/video_download_toolkit.py +19 -16
- camel/toolkits/weather_toolkit.py +2 -2
- camel/toolkits/web_deploy_toolkit.py +1219 -0
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/toolkits/whatsapp_toolkit.py +2 -2
- camel/toolkits/wolfram_alpha_toolkit.py +2 -2
- camel/toolkits/zapier_toolkit.py +7 -3
- camel/types/__init__.py +4 -4
- camel/types/agents/__init__.py +2 -2
- camel/types/agents/tool_calling_record.py +6 -3
- camel/types/enums.py +378 -39
- camel/types/mcp_registries.py +2 -2
- camel/types/openai_types.py +4 -4
- camel/types/unified_model_type.py +38 -6
- camel/utils/__init__.py +2 -2
- camel/utils/async_func.py +2 -2
- camel/utils/chunker/__init__.py +2 -2
- camel/utils/chunker/base.py +2 -2
- camel/utils/chunker/code_chunker.py +2 -2
- camel/utils/chunker/uio_chunker.py +2 -2
- camel/utils/commons.py +38 -7
- camel/utils/constants.py +5 -2
- camel/utils/context_utils.py +1134 -0
- camel/utils/deduplication.py +2 -2
- camel/utils/filename.py +2 -2
- camel/utils/langfuse.py +2 -2
- camel/utils/mcp.py +140 -6
- camel/utils/mcp_client.py +48 -38
- camel/utils/message_summarizer.py +148 -0
- camel/utils/response_format.py +2 -2
- camel/utils/token_counting.py +45 -22
- camel/utils/tool_result.py +44 -0
- camel/verifiers/__init__.py +2 -2
- camel/verifiers/base.py +2 -2
- camel/verifiers/math_verifier.py +2 -2
- camel/verifiers/models.py +2 -2
- camel/verifiers/physics_verifier.py +2 -2
- camel/verifiers/python_verifier.py +2 -2
- {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/METADATA +327 -94
- camel_ai-0.2.82.dist-info/RECORD +507 -0
- {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/WHEEL +1 -1
- {camel_ai-0.2.65.dist-info → camel_ai-0.2.82.dist-info}/licenses/LICENSE +1 -1
- camel/loaders/pandas_reader.py +0 -368
- camel/toolkits/dalle_toolkit.py +0 -175
- camel/toolkits/file_write_toolkit.py +0 -444
- camel/toolkits/openai_agent_toolkit.py +0 -135
- camel/toolkits/terminal_toolkit.py +0 -1037
- 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('&', '&')
|
|
129
|
+
.replace('<', '<')
|
|
130
|
+
.replace('>', '>')
|
|
131
|
+
)
|
|
132
|
+
text = text.replace('"', '"').replace("'", ''')
|
|
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
|
+
]
|