camel-ai 0.2.65__py3-none-any.whl → 0.2.83a6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of camel-ai might be problematic. Click here for more details.
- 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 +5107 -995
- 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 +29 -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/function_gemma_config.py +59 -0
- 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 +109 -230
- 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 +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 +214 -115
- camel/models/base_audio_model.py +5 -3
- camel/models/base_model.py +378 -31
- camel/models/cerebras_model.py +83 -0
- camel/models/cohere_model.py +18 -49
- camel/models/cometapi_model.py +83 -0
- camel/models/crynux_model.py +11 -18
- camel/models/deepseek_model.py +20 -84
- camel/models/fish_audio_model.py +8 -2
- camel/models/function_gemma_model.py +889 -0
- camel/models/gemini_model.py +391 -52
- camel/models/groq_model.py +11 -19
- camel/models/internlm_model.py +11 -18
- camel/models/litellm_model.py +57 -49
- camel/models/lmstudio_model.py +17 -20
- camel/models/minimax_model.py +83 -0
- camel/models/mistral_model.py +20 -47
- camel/models/model_factory.py +39 -3
- camel/models/model_manager.py +26 -8
- camel/models/modelscope_model.py +13 -193
- camel/models/moonshot_model.py +183 -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 +190 -71
- camel/models/openai_model.py +192 -86
- 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 +23 -49
- 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 +50 -75
- camel/models/sglang_model.py +90 -68
- 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 +158 -19
- camel/models/watsonx_model.py +9 -47
- 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 +145 -0
- camel/societies/workforce/prompts.py +259 -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 +5354 -372
- 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 +57 -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 +106 -6
- 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 +205 -27
- 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 +1958 -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 +1940 -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 +325 -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 +728 -0
- camel/toolkits/microsoft_outlook_mail_toolkit.py +1885 -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 +606 -156
- 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 +1281 -0
- camel/toolkits/terminal_toolkit/utils.py +659 -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 +381 -41
- camel/types/mcp_registries.py +2 -2
- camel/types/openai_types.py +4 -4
- camel/types/unified_model_type.py +46 -10
- camel/utils/__init__.py +5 -2
- camel/utils/agent_context.py +41 -0
- 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 +18 -10
- 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.83a6.dist-info}/METADATA +355 -117
- camel_ai-0.2.83a6.dist-info/RECORD +511 -0
- {camel_ai-0.2.65.dist-info → camel_ai-0.2.83a6.dist-info}/WHEEL +1 -1
- {camel_ai-0.2.65.dist-info → camel_ai-0.2.83a6.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,787 @@
|
|
|
1
|
+
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
#
|
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
#
|
|
8
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
# See the License for the specific language governing permissions and
|
|
12
|
+
# limitations under the License.
|
|
13
|
+
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from collections import deque
|
|
18
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
from camel.logger import get_logger
|
|
21
|
+
|
|
22
|
+
from .actions import ActionExecutor
|
|
23
|
+
from .config_loader import ConfigLoader
|
|
24
|
+
from .snapshot import PageSnapshot
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from playwright.async_api import (
|
|
28
|
+
Browser,
|
|
29
|
+
BrowserContext,
|
|
30
|
+
ConsoleMessage,
|
|
31
|
+
Page,
|
|
32
|
+
Playwright,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TabIdGenerator:
|
|
39
|
+
"""Monotonically increasing tab ID generator."""
|
|
40
|
+
|
|
41
|
+
_counter: int = 0
|
|
42
|
+
_lock: ClassVar[asyncio.Lock] = asyncio.Lock()
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
async def generate_tab_id(cls) -> str:
|
|
46
|
+
"""Generate a monotonically increasing tab ID."""
|
|
47
|
+
async with cls._lock:
|
|
48
|
+
cls._counter += 1
|
|
49
|
+
return f"tab-{cls._counter:03d}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class HybridBrowserSession:
|
|
53
|
+
"""Lightweight wrapper around Playwright for
|
|
54
|
+
browsing with multi-tab support.
|
|
55
|
+
|
|
56
|
+
It provides multiple *Page* instances plus helper utilities (snapshot &
|
|
57
|
+
executor). Multiple toolkits or agents can reuse this class without
|
|
58
|
+
duplicating Playwright setup code.
|
|
59
|
+
|
|
60
|
+
This class is a singleton per event-loop and session-id combination.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Class-level registry for singleton instances
|
|
64
|
+
# Format: {(loop_id, session_id): HybridBrowserSession}
|
|
65
|
+
_instances: ClassVar[Dict[Tuple[Any, str], "HybridBrowserSession"]] = {}
|
|
66
|
+
_instances_lock: ClassVar[asyncio.Lock] = asyncio.Lock()
|
|
67
|
+
|
|
68
|
+
_initialized: bool
|
|
69
|
+
_creation_params: Dict[str, Any]
|
|
70
|
+
|
|
71
|
+
def __new__(
|
|
72
|
+
cls,
|
|
73
|
+
*,
|
|
74
|
+
headless: bool = True,
|
|
75
|
+
user_data_dir: Optional[str] = None,
|
|
76
|
+
stealth: bool = False,
|
|
77
|
+
session_id: Optional[str] = None,
|
|
78
|
+
default_timeout: Optional[int] = None,
|
|
79
|
+
short_timeout: Optional[int] = None,
|
|
80
|
+
navigation_timeout: Optional[int] = None,
|
|
81
|
+
network_idle_timeout: Optional[int] = None,
|
|
82
|
+
) -> "HybridBrowserSession":
|
|
83
|
+
# Create a unique key for this event loop and session combination
|
|
84
|
+
# We defer the event loop lookup to avoid issues with creation
|
|
85
|
+
# outside async context
|
|
86
|
+
instance = super().__new__(cls)
|
|
87
|
+
instance._initialized = False
|
|
88
|
+
instance._session_id = session_id or "default"
|
|
89
|
+
instance._creation_params = {
|
|
90
|
+
"headless": headless,
|
|
91
|
+
"user_data_dir": user_data_dir,
|
|
92
|
+
"stealth": stealth,
|
|
93
|
+
"session_id": session_id,
|
|
94
|
+
"default_timeout": default_timeout,
|
|
95
|
+
"short_timeout": short_timeout,
|
|
96
|
+
"navigation_timeout": navigation_timeout,
|
|
97
|
+
"network_idle_timeout": network_idle_timeout,
|
|
98
|
+
}
|
|
99
|
+
return instance
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
async def _get_or_create_instance(
|
|
103
|
+
cls,
|
|
104
|
+
instance: "HybridBrowserSession",
|
|
105
|
+
) -> "HybridBrowserSession":
|
|
106
|
+
"""Get or create singleton instance for the current event loop and
|
|
107
|
+
session."""
|
|
108
|
+
try:
|
|
109
|
+
loop = asyncio.get_running_loop()
|
|
110
|
+
loop_id = str(id(loop))
|
|
111
|
+
except RuntimeError:
|
|
112
|
+
# No event loop running, use a unique identifier for sync context
|
|
113
|
+
import threading
|
|
114
|
+
|
|
115
|
+
loop_id = f"sync_{threading.current_thread().ident}"
|
|
116
|
+
|
|
117
|
+
# Ensure session_id is never None for the key
|
|
118
|
+
session_id = (
|
|
119
|
+
instance._session_id
|
|
120
|
+
if instance._session_id is not None
|
|
121
|
+
else "default"
|
|
122
|
+
)
|
|
123
|
+
session_key = (loop_id, session_id)
|
|
124
|
+
|
|
125
|
+
# Use class-level lock to protect the instances registry
|
|
126
|
+
async with cls._instances_lock:
|
|
127
|
+
if session_key in cls._instances:
|
|
128
|
+
existing_instance = cls._instances[session_key]
|
|
129
|
+
logger.debug(
|
|
130
|
+
f"Reusing existing browser session for session_id: "
|
|
131
|
+
f"{session_id}"
|
|
132
|
+
)
|
|
133
|
+
return existing_instance
|
|
134
|
+
|
|
135
|
+
# Register this new instance
|
|
136
|
+
cls._instances[session_key] = instance
|
|
137
|
+
logger.debug(
|
|
138
|
+
f"Created new browser session for session_id: {session_id}"
|
|
139
|
+
)
|
|
140
|
+
return instance
|
|
141
|
+
|
|
142
|
+
def __init__(
|
|
143
|
+
self,
|
|
144
|
+
*,
|
|
145
|
+
headless: bool = True,
|
|
146
|
+
user_data_dir: Optional[str] = None,
|
|
147
|
+
stealth: bool = False,
|
|
148
|
+
session_id: Optional[str] = None,
|
|
149
|
+
default_timeout: Optional[int] = None,
|
|
150
|
+
short_timeout: Optional[int] = None,
|
|
151
|
+
navigation_timeout: Optional[int] = None,
|
|
152
|
+
network_idle_timeout: Optional[int] = None,
|
|
153
|
+
):
|
|
154
|
+
if self._initialized:
|
|
155
|
+
return
|
|
156
|
+
self._initialized = True
|
|
157
|
+
|
|
158
|
+
self._headless = headless
|
|
159
|
+
self._user_data_dir = user_data_dir
|
|
160
|
+
self._stealth = stealth
|
|
161
|
+
self._session_id = session_id or "default"
|
|
162
|
+
|
|
163
|
+
# Store timeout configuration for ActionExecutor instances and
|
|
164
|
+
# browser operations
|
|
165
|
+
self._default_timeout = default_timeout
|
|
166
|
+
self._short_timeout = short_timeout
|
|
167
|
+
self._navigation_timeout = ConfigLoader.get_navigation_timeout(
|
|
168
|
+
navigation_timeout
|
|
169
|
+
)
|
|
170
|
+
self._network_idle_timeout = ConfigLoader.get_network_idle_timeout(
|
|
171
|
+
network_idle_timeout
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Initialize _creation_params to fix linter error
|
|
175
|
+
self._creation_params = {
|
|
176
|
+
"headless": headless,
|
|
177
|
+
"user_data_dir": user_data_dir,
|
|
178
|
+
"stealth": stealth,
|
|
179
|
+
"session_id": session_id,
|
|
180
|
+
"default_timeout": default_timeout,
|
|
181
|
+
"short_timeout": short_timeout,
|
|
182
|
+
"navigation_timeout": navigation_timeout,
|
|
183
|
+
"network_idle_timeout": network_idle_timeout,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
self._playwright: Optional[Playwright] = None
|
|
187
|
+
self._browser: Optional[Browser] = None
|
|
188
|
+
self._context: Optional[BrowserContext] = None
|
|
189
|
+
self._page: Optional[Page] = None
|
|
190
|
+
|
|
191
|
+
# Dictionary-based tab management with monotonic IDs
|
|
192
|
+
self._pages: Dict[str, Page] = {} # tab_id -> Page object
|
|
193
|
+
self._console_logs: Dict[str, Any] = {} # tab_id -> page logs
|
|
194
|
+
self._current_tab_id: Optional[str] = None # Current active tab ID
|
|
195
|
+
self.log_limit: int = ConfigLoader.get_max_log_limit() or 1000
|
|
196
|
+
|
|
197
|
+
self.snapshot: Optional[PageSnapshot] = None
|
|
198
|
+
self.executor: Optional[ActionExecutor] = None
|
|
199
|
+
|
|
200
|
+
# Protect browser initialisation against concurrent calls
|
|
201
|
+
self._ensure_lock: "asyncio.Lock" = asyncio.Lock()
|
|
202
|
+
|
|
203
|
+
# Load stealth script and config on initialization
|
|
204
|
+
self._stealth_script: Optional[str] = None
|
|
205
|
+
self._stealth_config: Optional[Dict[str, Any]] = None
|
|
206
|
+
if self._stealth:
|
|
207
|
+
self._stealth_script = self._load_stealth_script()
|
|
208
|
+
stealth_config_class = ConfigLoader.get_stealth_config()
|
|
209
|
+
self._stealth_config = stealth_config_class.get_stealth_config()
|
|
210
|
+
|
|
211
|
+
def _load_stealth_script(self) -> str:
|
|
212
|
+
r"""Load the stealth JavaScript script from file."""
|
|
213
|
+
import os
|
|
214
|
+
|
|
215
|
+
script_path = os.path.join(
|
|
216
|
+
os.path.dirname(os.path.abspath(__file__)), "stealth_script.js"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
with open(
|
|
221
|
+
script_path, "r", encoding='utf-8', errors='replace'
|
|
222
|
+
) as f:
|
|
223
|
+
script_content = f.read()
|
|
224
|
+
|
|
225
|
+
if not script_content.strip():
|
|
226
|
+
raise ValueError(f"Stealth script is empty: {script_path}")
|
|
227
|
+
|
|
228
|
+
logger.debug(
|
|
229
|
+
f"Loaded stealth script ({len(script_content)} chars)"
|
|
230
|
+
)
|
|
231
|
+
return script_content
|
|
232
|
+
except FileNotFoundError:
|
|
233
|
+
logger.error(f"Stealth script not found: {script_path}")
|
|
234
|
+
raise FileNotFoundError(f"Stealth script not found: {script_path}")
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.error(f"Error loading stealth script: {e}")
|
|
237
|
+
raise RuntimeError(f"Failed to load stealth script: {e}") from e
|
|
238
|
+
|
|
239
|
+
# ------------------------------------------------------------------
|
|
240
|
+
# Multi-tab management methods
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
async def create_new_tab(self, url: Optional[str] = None) -> str:
|
|
243
|
+
r"""Create a new tab and optionally navigate to a URL.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
url: Optional URL to navigate to in the new tab
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
str: ID of the newly created tab
|
|
250
|
+
"""
|
|
251
|
+
await self.ensure_browser()
|
|
252
|
+
|
|
253
|
+
if self._context is None:
|
|
254
|
+
raise RuntimeError("Browser context is not available")
|
|
255
|
+
|
|
256
|
+
# Generate unique tab ID
|
|
257
|
+
tab_id = await TabIdGenerator.generate_tab_id()
|
|
258
|
+
|
|
259
|
+
# Create new page
|
|
260
|
+
new_page = await self._context.new_page()
|
|
261
|
+
|
|
262
|
+
# Apply stealth modifications if enabled
|
|
263
|
+
if self._stealth and self._stealth_script:
|
|
264
|
+
try:
|
|
265
|
+
await new_page.add_init_script(self._stealth_script)
|
|
266
|
+
logger.debug("Applied stealth script to new tab")
|
|
267
|
+
except Exception as e:
|
|
268
|
+
logger.warning(
|
|
269
|
+
f"Failed to apply stealth script to new tab: {e}"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Store in pages dictionary
|
|
273
|
+
await self._register_new_page(tab_id, new_page)
|
|
274
|
+
|
|
275
|
+
# Navigate if URL provided
|
|
276
|
+
if url:
|
|
277
|
+
try:
|
|
278
|
+
await new_page.goto(url, timeout=self._navigation_timeout)
|
|
279
|
+
await new_page.wait_for_load_state('domcontentloaded')
|
|
280
|
+
except Exception as e:
|
|
281
|
+
logger.warning(f"Failed to navigate new tab to {url}: {e}")
|
|
282
|
+
|
|
283
|
+
logger.info(
|
|
284
|
+
f"Created new tab {tab_id}, total tabs: {len(self._pages)}"
|
|
285
|
+
)
|
|
286
|
+
return tab_id
|
|
287
|
+
|
|
288
|
+
async def _register_new_page(self, tab_id: str, new_page: "Page") -> None:
|
|
289
|
+
r"""Register a page and add console event listerers.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
new_page (Page): The new page object to register.
|
|
293
|
+
"""
|
|
294
|
+
# Add new page
|
|
295
|
+
self._pages[tab_id] = new_page
|
|
296
|
+
# Create log for the page
|
|
297
|
+
self._console_logs[tab_id] = deque(maxlen=self.log_limit)
|
|
298
|
+
|
|
299
|
+
# Add event function
|
|
300
|
+
def handle_console_log(msg: ConsoleMessage):
|
|
301
|
+
logs = self._console_logs.get(tab_id)
|
|
302
|
+
if logs is not None:
|
|
303
|
+
logs.append({"type": msg.type, "text": msg.text})
|
|
304
|
+
|
|
305
|
+
# Add event listener for console logs
|
|
306
|
+
new_page.on(event="console", f=handle_console_log)
|
|
307
|
+
|
|
308
|
+
def handle_page_close(page: "Page"):
|
|
309
|
+
self._console_logs.pop(tab_id, None)
|
|
310
|
+
|
|
311
|
+
# Add event listener for cleanup
|
|
312
|
+
new_page.on(event="close", f=handle_page_close)
|
|
313
|
+
|
|
314
|
+
async def register_page(self, new_page: "Page") -> str:
|
|
315
|
+
r"""Register a page that was created externally (e.g., by a click).
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
new_page (Page): The new page object to register.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
str: The ID of the (newly) registered tab.
|
|
322
|
+
"""
|
|
323
|
+
# Check if page is already registered
|
|
324
|
+
for tab_id, page in self._pages.items():
|
|
325
|
+
if page is new_page:
|
|
326
|
+
return tab_id
|
|
327
|
+
|
|
328
|
+
# Create new ID for the page
|
|
329
|
+
tab_id = await TabIdGenerator.generate_tab_id()
|
|
330
|
+
await self._register_new_page(tab_id, new_page)
|
|
331
|
+
|
|
332
|
+
logger.info(
|
|
333
|
+
f"Registered new tab {tab_id} (opened by user action). "
|
|
334
|
+
f"Total tabs: {len(self._pages)}"
|
|
335
|
+
)
|
|
336
|
+
return tab_id
|
|
337
|
+
|
|
338
|
+
async def switch_to_tab(self, tab_id: str) -> bool:
|
|
339
|
+
r"""Switch to a specific tab by ID.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
tab_id: ID of the tab to switch to
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
bool: True if successful, False if tab ID is invalid
|
|
346
|
+
"""
|
|
347
|
+
if tab_id not in self._pages:
|
|
348
|
+
logger.warning(f"Invalid tab ID: {tab_id}")
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
page = self._pages[tab_id]
|
|
352
|
+
|
|
353
|
+
# Check if page is still valid
|
|
354
|
+
if page.is_closed():
|
|
355
|
+
logger.warning(f"Tab {tab_id} is closed, removing from registry")
|
|
356
|
+
# Clean up closed tab
|
|
357
|
+
del self._pages[tab_id]
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
# Switch to the tab
|
|
362
|
+
self._current_tab_id = tab_id
|
|
363
|
+
self._page = page
|
|
364
|
+
|
|
365
|
+
# Bring the tab to the front in the browser window
|
|
366
|
+
await page.bring_to_front()
|
|
367
|
+
|
|
368
|
+
# Update utilities for new tab
|
|
369
|
+
self.executor = ActionExecutor(
|
|
370
|
+
page,
|
|
371
|
+
self,
|
|
372
|
+
default_timeout=self._default_timeout,
|
|
373
|
+
short_timeout=self._short_timeout,
|
|
374
|
+
)
|
|
375
|
+
self.snapshot = PageSnapshot(page)
|
|
376
|
+
|
|
377
|
+
logger.info(f"Switched to tab {tab_id}")
|
|
378
|
+
return True
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.warning(f"Error switching to tab {tab_id}: {e}")
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
async def close_tab(self, tab_id: str) -> bool:
|
|
385
|
+
r"""Close a specific tab by ID.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
tab_id: ID of the tab to close
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
bool: True if successful, False if tab ID is invalid
|
|
392
|
+
"""
|
|
393
|
+
if tab_id not in self._pages:
|
|
394
|
+
logger.warning(f"Invalid tab ID: {tab_id}")
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
page = self._pages[tab_id]
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
# Close the page if not already closed
|
|
401
|
+
if not page.is_closed():
|
|
402
|
+
await page.close()
|
|
403
|
+
|
|
404
|
+
# Remove from our dictionary
|
|
405
|
+
del self._pages[tab_id]
|
|
406
|
+
|
|
407
|
+
# If we closed the current tab, switch to another one
|
|
408
|
+
if tab_id == self._current_tab_id:
|
|
409
|
+
if self._pages:
|
|
410
|
+
# Switch to any available tab (first one we find)
|
|
411
|
+
next_tab_id = next(iter(self._pages.keys()))
|
|
412
|
+
await self.switch_to_tab(next_tab_id)
|
|
413
|
+
else:
|
|
414
|
+
# No tabs left
|
|
415
|
+
self._current_tab_id = None
|
|
416
|
+
self._page = None
|
|
417
|
+
self.executor = None
|
|
418
|
+
self.snapshot = None
|
|
419
|
+
|
|
420
|
+
logger.info(
|
|
421
|
+
f"Closed tab {tab_id}, remaining tabs: {len(self._pages)}"
|
|
422
|
+
)
|
|
423
|
+
return True
|
|
424
|
+
|
|
425
|
+
except Exception as e:
|
|
426
|
+
logger.warning(f"Error closing tab {tab_id}: {e}")
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
async def get_tab_info(self) -> List[Dict[str, Any]]:
|
|
430
|
+
r"""Get information about all open tabs including IDs.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
List of dictionaries containing tab information
|
|
434
|
+
"""
|
|
435
|
+
tab_info = []
|
|
436
|
+
tabs_to_cleanup = []
|
|
437
|
+
|
|
438
|
+
# Process all tabs in dictionary
|
|
439
|
+
for tab_id, page in list(self._pages.items()):
|
|
440
|
+
try:
|
|
441
|
+
if not page.is_closed():
|
|
442
|
+
title = await page.title()
|
|
443
|
+
url = page.url
|
|
444
|
+
is_current = tab_id == self._current_tab_id
|
|
445
|
+
tab_info.append(
|
|
446
|
+
{
|
|
447
|
+
"tab_id": tab_id,
|
|
448
|
+
"title": title,
|
|
449
|
+
"url": url,
|
|
450
|
+
"is_current": is_current,
|
|
451
|
+
}
|
|
452
|
+
)
|
|
453
|
+
else:
|
|
454
|
+
# Mark for cleanup
|
|
455
|
+
tabs_to_cleanup.append(tab_id)
|
|
456
|
+
except Exception as e:
|
|
457
|
+
logger.warning(f"Error getting info for tab {tab_id}: {e}")
|
|
458
|
+
tabs_to_cleanup.append(tab_id)
|
|
459
|
+
|
|
460
|
+
# Clean up closed/invalid tabs
|
|
461
|
+
for tab_id in tabs_to_cleanup:
|
|
462
|
+
if tab_id in self._pages:
|
|
463
|
+
del self._pages[tab_id]
|
|
464
|
+
|
|
465
|
+
return tab_info
|
|
466
|
+
|
|
467
|
+
async def get_current_tab_id(self) -> Optional[str]:
|
|
468
|
+
r"""Get the id for the current active tab."""
|
|
469
|
+
if not self._current_tab_id or not self._pages:
|
|
470
|
+
return None
|
|
471
|
+
return self._current_tab_id
|
|
472
|
+
|
|
473
|
+
# ------------------------------------------------------------------
|
|
474
|
+
# Browser lifecycle helpers
|
|
475
|
+
# ------------------------------------------------------------------
|
|
476
|
+
async def ensure_browser(self) -> None:
|
|
477
|
+
r"""Ensure browser is ready. Each session_id gets its own browser
|
|
478
|
+
instance."""
|
|
479
|
+
# First, get the singleton instance for this session
|
|
480
|
+
singleton_instance = await self._get_or_create_instance(self)
|
|
481
|
+
|
|
482
|
+
# If this isn't the singleton instance, delegate to the singleton
|
|
483
|
+
if singleton_instance is not self:
|
|
484
|
+
await singleton_instance.ensure_browser()
|
|
485
|
+
# Copy the singleton's browser state to this instance
|
|
486
|
+
self._playwright = singleton_instance._playwright
|
|
487
|
+
self._browser = singleton_instance._browser
|
|
488
|
+
self._context = singleton_instance._context
|
|
489
|
+
self._page = singleton_instance._page
|
|
490
|
+
self._pages = singleton_instance._pages
|
|
491
|
+
self._console_logs = singleton_instance._console_logs
|
|
492
|
+
self._current_tab_id = singleton_instance._current_tab_id
|
|
493
|
+
self.snapshot = singleton_instance.snapshot
|
|
494
|
+
self.executor = singleton_instance.executor
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
# Serialise initialisation to avoid race conditions where multiple
|
|
498
|
+
# concurrent coroutine calls create multiple browser instances for
|
|
499
|
+
# the same HybridBrowserSession.
|
|
500
|
+
async with self._ensure_lock:
|
|
501
|
+
await self._ensure_browser_inner()
|
|
502
|
+
|
|
503
|
+
# Moved original logic to helper
|
|
504
|
+
async def _ensure_browser_inner(self) -> None:
|
|
505
|
+
r"""Internal browser initialization logic."""
|
|
506
|
+
from playwright.async_api import async_playwright
|
|
507
|
+
|
|
508
|
+
if self._page is not None:
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
self._playwright = await async_playwright().start()
|
|
512
|
+
|
|
513
|
+
# Prepare stealth options
|
|
514
|
+
launch_options: Dict[str, Any] = {"headless": self._headless}
|
|
515
|
+
context_options: Dict[str, Any] = {}
|
|
516
|
+
if self._stealth and self._stealth_config:
|
|
517
|
+
# Use preloaded stealth configuration
|
|
518
|
+
launch_options['args'] = self._stealth_config['launch_args']
|
|
519
|
+
context_options.update(self._stealth_config['context_options'])
|
|
520
|
+
|
|
521
|
+
if self._user_data_dir:
|
|
522
|
+
context = (
|
|
523
|
+
await self._playwright.chromium.launch_persistent_context(
|
|
524
|
+
user_data_dir=self._user_data_dir,
|
|
525
|
+
**launch_options,
|
|
526
|
+
**context_options,
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
self._context = context
|
|
530
|
+
# Get the first (default) page
|
|
531
|
+
pages = context.pages
|
|
532
|
+
if pages:
|
|
533
|
+
self._page = pages[0]
|
|
534
|
+
# Create ID for initial page
|
|
535
|
+
initial_tab_id = await TabIdGenerator.generate_tab_id()
|
|
536
|
+
await self._register_new_page(initial_tab_id, pages[0])
|
|
537
|
+
self._current_tab_id = initial_tab_id
|
|
538
|
+
# Handle additional pages if any
|
|
539
|
+
for page in pages[1:]:
|
|
540
|
+
tab_id = await TabIdGenerator.generate_tab_id()
|
|
541
|
+
await self._register_new_page(tab_id, page)
|
|
542
|
+
else:
|
|
543
|
+
self._page = await context.new_page()
|
|
544
|
+
initial_tab_id = await TabIdGenerator.generate_tab_id()
|
|
545
|
+
await self._register_new_page(initial_tab_id, self._page)
|
|
546
|
+
self._current_tab_id = initial_tab_id
|
|
547
|
+
else:
|
|
548
|
+
self._browser = await self._playwright.chromium.launch(
|
|
549
|
+
**launch_options
|
|
550
|
+
)
|
|
551
|
+
self._context = await self._browser.new_context(**context_options)
|
|
552
|
+
self._page = await self._context.new_page()
|
|
553
|
+
|
|
554
|
+
# Create ID for initial page
|
|
555
|
+
initial_tab_id = await TabIdGenerator.generate_tab_id()
|
|
556
|
+
await self._register_new_page(initial_tab_id, self._page)
|
|
557
|
+
self._current_tab_id = initial_tab_id
|
|
558
|
+
|
|
559
|
+
# Apply stealth modifications if enabled
|
|
560
|
+
if self._stealth and self._stealth_script:
|
|
561
|
+
try:
|
|
562
|
+
await self._page.add_init_script(self._stealth_script)
|
|
563
|
+
logger.debug("Applied stealth script to main page")
|
|
564
|
+
except Exception as e:
|
|
565
|
+
logger.warning(f"Failed to apply stealth script: {e}")
|
|
566
|
+
|
|
567
|
+
# Set up timeout for navigation
|
|
568
|
+
self._page.set_default_navigation_timeout(self._navigation_timeout)
|
|
569
|
+
self._page.set_default_timeout(self._navigation_timeout)
|
|
570
|
+
|
|
571
|
+
# Initialize utilities
|
|
572
|
+
self.snapshot = PageSnapshot(self._page)
|
|
573
|
+
self.executor = ActionExecutor(
|
|
574
|
+
self._page,
|
|
575
|
+
self,
|
|
576
|
+
default_timeout=self._default_timeout,
|
|
577
|
+
short_timeout=self._short_timeout,
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
logger.info("Browser session initialized successfully")
|
|
581
|
+
|
|
582
|
+
async def close(self) -> None:
|
|
583
|
+
r"""Close browser session and clean up resources."""
|
|
584
|
+
if self._page is None:
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
logger.debug("Closing browser session...")
|
|
589
|
+
await self._close_session()
|
|
590
|
+
|
|
591
|
+
# Remove from singleton registry
|
|
592
|
+
try:
|
|
593
|
+
try:
|
|
594
|
+
loop = asyncio.get_running_loop()
|
|
595
|
+
loop_id = str(id(loop))
|
|
596
|
+
except RuntimeError:
|
|
597
|
+
# Use same logic as _get_or_create_instance
|
|
598
|
+
import threading
|
|
599
|
+
|
|
600
|
+
loop_id = f"sync_{threading.current_thread().ident}"
|
|
601
|
+
|
|
602
|
+
session_id = (
|
|
603
|
+
self._session_id
|
|
604
|
+
if self._session_id is not None
|
|
605
|
+
else "default"
|
|
606
|
+
)
|
|
607
|
+
session_key = (loop_id, session_id)
|
|
608
|
+
|
|
609
|
+
async with self._instances_lock:
|
|
610
|
+
if (
|
|
611
|
+
session_key in self._instances
|
|
612
|
+
and self._instances[session_key] is self
|
|
613
|
+
):
|
|
614
|
+
del self._instances[session_key]
|
|
615
|
+
logger.debug(
|
|
616
|
+
f"Removed session {session_id} from registry"
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
except Exception as registry_error:
|
|
620
|
+
logger.warning(f"Error cleaning up registry: {registry_error}")
|
|
621
|
+
|
|
622
|
+
logger.debug("Browser session closed successfully")
|
|
623
|
+
except Exception as e:
|
|
624
|
+
logger.error(f"Error during browser session close: {e}")
|
|
625
|
+
finally:
|
|
626
|
+
self._page = None
|
|
627
|
+
self._pages = {}
|
|
628
|
+
self._current_tab_id = None
|
|
629
|
+
self.snapshot = None
|
|
630
|
+
self.executor = None
|
|
631
|
+
|
|
632
|
+
async def _close_session(self) -> None:
|
|
633
|
+
r"""Internal session close logic with thorough cleanup."""
|
|
634
|
+
try:
|
|
635
|
+
# Close all pages first
|
|
636
|
+
pages_to_close = list(self._pages.values())
|
|
637
|
+
for page in pages_to_close:
|
|
638
|
+
try:
|
|
639
|
+
if not page.is_closed():
|
|
640
|
+
await page.close()
|
|
641
|
+
logger.debug(
|
|
642
|
+
f"Closed page: "
|
|
643
|
+
f"{page.url if hasattr(page, 'url') else 'unknown'}" # noqa:E501
|
|
644
|
+
)
|
|
645
|
+
except Exception as e:
|
|
646
|
+
logger.warning(f"Error closing page: {e}")
|
|
647
|
+
|
|
648
|
+
# Clear the pages dictionary
|
|
649
|
+
self._pages.clear()
|
|
650
|
+
|
|
651
|
+
# Close context with explicit wait
|
|
652
|
+
if self._context:
|
|
653
|
+
try:
|
|
654
|
+
await self._context.close()
|
|
655
|
+
logger.debug("Browser context closed")
|
|
656
|
+
except Exception as e:
|
|
657
|
+
logger.warning(f"Error closing context: {e}")
|
|
658
|
+
finally:
|
|
659
|
+
self._context = None
|
|
660
|
+
|
|
661
|
+
# Close browser with explicit wait
|
|
662
|
+
if self._browser:
|
|
663
|
+
try:
|
|
664
|
+
await self._browser.close()
|
|
665
|
+
logger.debug("Browser instance closed")
|
|
666
|
+
except Exception as e:
|
|
667
|
+
logger.warning(f"Error closing browser: {e}")
|
|
668
|
+
finally:
|
|
669
|
+
self._browser = None
|
|
670
|
+
|
|
671
|
+
# Stop playwright with increased delay for cleanup
|
|
672
|
+
if self._playwright:
|
|
673
|
+
try:
|
|
674
|
+
await self._playwright.stop()
|
|
675
|
+
logger.debug("Playwright stopped")
|
|
676
|
+
|
|
677
|
+
# Give more time for complete subprocess cleanup
|
|
678
|
+
import asyncio
|
|
679
|
+
|
|
680
|
+
await asyncio.sleep(0.5)
|
|
681
|
+
|
|
682
|
+
except Exception as e:
|
|
683
|
+
logger.warning(f"Error stopping playwright: {e}")
|
|
684
|
+
finally:
|
|
685
|
+
self._playwright = None
|
|
686
|
+
|
|
687
|
+
except Exception as e:
|
|
688
|
+
logger.error(f"Error during session cleanup: {e}")
|
|
689
|
+
finally:
|
|
690
|
+
# Ensure all attributes are cleared regardless of errors
|
|
691
|
+
self._page = None
|
|
692
|
+
self._pages = {}
|
|
693
|
+
self._current_tab_id = None
|
|
694
|
+
self._context = None
|
|
695
|
+
self._browser = None
|
|
696
|
+
self._playwright = None
|
|
697
|
+
|
|
698
|
+
@classmethod
|
|
699
|
+
async def close_all_sessions(cls) -> None:
|
|
700
|
+
r"""Close all browser sessions and clean up the singleton registry."""
|
|
701
|
+
logger.debug("Closing all browser sessions...")
|
|
702
|
+
async with cls._instances_lock:
|
|
703
|
+
# Close all active sessions
|
|
704
|
+
instances_to_close = list(cls._instances.values())
|
|
705
|
+
cls._instances.clear()
|
|
706
|
+
logger.debug(f"Closing {len(instances_to_close)} sessions.")
|
|
707
|
+
|
|
708
|
+
# Close sessions outside the lock to avoid deadlock
|
|
709
|
+
for instance in instances_to_close:
|
|
710
|
+
try:
|
|
711
|
+
await instance._close_session()
|
|
712
|
+
logger.debug(f"Closed session: {instance._session_id}")
|
|
713
|
+
except Exception as e:
|
|
714
|
+
logger.error(
|
|
715
|
+
f"Error closing session {instance._session_id}: {e}"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
logger.debug("All browser sessions closed and registry cleared")
|
|
719
|
+
|
|
720
|
+
@classmethod
|
|
721
|
+
async def close_all(cls) -> None:
|
|
722
|
+
"""Alias for close_all_sessions for backward compatibility."""
|
|
723
|
+
await cls.close_all_sessions()
|
|
724
|
+
|
|
725
|
+
# ------------------------------------------------------------------
|
|
726
|
+
# Page interaction
|
|
727
|
+
# ------------------------------------------------------------------
|
|
728
|
+
async def visit(self, url: str) -> str:
|
|
729
|
+
r"""Navigate current tab to URL."""
|
|
730
|
+
await self.ensure_browser()
|
|
731
|
+
page = await self.get_page()
|
|
732
|
+
|
|
733
|
+
await page.goto(url, timeout=self._navigation_timeout)
|
|
734
|
+
await page.wait_for_load_state('domcontentloaded')
|
|
735
|
+
|
|
736
|
+
# Try to wait for network idle
|
|
737
|
+
try:
|
|
738
|
+
await page.wait_for_load_state(
|
|
739
|
+
'networkidle', timeout=self._network_idle_timeout
|
|
740
|
+
)
|
|
741
|
+
except Exception:
|
|
742
|
+
logger.debug("Network idle timeout - continuing anyway")
|
|
743
|
+
|
|
744
|
+
return f"Navigated to {url}"
|
|
745
|
+
|
|
746
|
+
async def get_snapshot(
|
|
747
|
+
self,
|
|
748
|
+
*,
|
|
749
|
+
force_refresh: bool = False,
|
|
750
|
+
diff_only: bool = False,
|
|
751
|
+
viewport_limit: bool = False,
|
|
752
|
+
) -> str:
|
|
753
|
+
r"""Get snapshot for current tab."""
|
|
754
|
+
if not self.snapshot:
|
|
755
|
+
return "<empty>"
|
|
756
|
+
return await self.snapshot.capture(
|
|
757
|
+
force_refresh=force_refresh,
|
|
758
|
+
diff_only=diff_only,
|
|
759
|
+
viewport_limit=viewport_limit,
|
|
760
|
+
)
|
|
761
|
+
|
|
762
|
+
async def exec_action(self, action: Dict[str, Any]) -> Dict[str, Any]:
|
|
763
|
+
r"""Execute action on current tab."""
|
|
764
|
+
if not self.executor:
|
|
765
|
+
return {
|
|
766
|
+
"success": False,
|
|
767
|
+
"message": "No executor available",
|
|
768
|
+
"details": {},
|
|
769
|
+
}
|
|
770
|
+
return await self.executor.execute(action)
|
|
771
|
+
|
|
772
|
+
async def get_page(self) -> "Page":
|
|
773
|
+
r"""Get current active page."""
|
|
774
|
+
await self.ensure_browser()
|
|
775
|
+
if self._page is None:
|
|
776
|
+
raise RuntimeError("No active page available")
|
|
777
|
+
return self._page
|
|
778
|
+
|
|
779
|
+
async def get_console_logs(self) -> Dict[str, Any]:
|
|
780
|
+
r"""Get current active logs."""
|
|
781
|
+
await self.ensure_browser()
|
|
782
|
+
if self._current_tab_id is None:
|
|
783
|
+
raise RuntimeError("No active tab available")
|
|
784
|
+
logs = self._console_logs.get(self._current_tab_id, None)
|
|
785
|
+
if logs is None:
|
|
786
|
+
raise RuntimeError("No active logs available for the page")
|
|
787
|
+
return logs
|