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