camel-ai 0.2.82__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 +2 -2
- camel/agents/_utils.py +2 -2
- camel/agents/base.py +2 -2
- camel/agents/chat_agent.py +765 -541
- camel/agents/critic_agent.py +2 -2
- camel/agents/deductive_reasoner_agent.py +2 -2
- camel/agents/embodied_agent.py +2 -2
- camel/agents/knowledge_graph_agent.py +2 -2
- camel/agents/mcp_agent.py +2 -2
- camel/agents/multi_hop_generator_agent.py +2 -2
- camel/agents/programmed_agent_instruction.py +2 -2
- camel/agents/repo_agent.py +2 -2
- 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 +2 -2
- camel/benchmarks/__init__.py +2 -2
- camel/benchmarks/apibank.py +2 -2
- camel/benchmarks/apibench.py +2 -2
- camel/benchmarks/base.py +2 -2
- camel/benchmarks/browsecomp.py +2 -2
- camel/benchmarks/gaia.py +2 -2
- camel/benchmarks/mock_website/mock_web.py +2 -2
- camel/benchmarks/mock_website/shopping_mall/app.py +2 -2
- camel/benchmarks/nexus.py +2 -2
- 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 +2 -2
- camel/bots/slack/__init__.py +2 -2
- camel/bots/slack/models.py +2 -2
- camel/bots/slack/slack_app.py +2 -2
- camel/bots/telegram_bot.py +2 -2
- camel/configs/__init__.py +8 -2
- camel/configs/aihubmix_config.py +2 -2
- camel/configs/aiml_config.py +2 -2
- camel/configs/amd_config.py +2 -2
- camel/configs/anthropic_config.py +2 -2
- camel/configs/base_config.py +2 -2
- camel/configs/bedrock_config.py +2 -2
- camel/configs/cerebras_config.py +2 -2
- camel/configs/cohere_config.py +2 -2
- camel/configs/cometapi_config.py +2 -2
- camel/configs/crynux_config.py +2 -2
- camel/configs/deepseek_config.py +2 -2
- camel/configs/function_gemma_config.py +59 -0
- camel/configs/gemini_config.py +2 -2
- camel/configs/groq_config.py +2 -2
- camel/configs/internlm_config.py +2 -2
- camel/configs/litellm_config.py +2 -2
- camel/configs/lmstudio_config.py +2 -2
- camel/configs/minimax_config.py +2 -2
- camel/configs/mistral_config.py +2 -2
- camel/configs/modelscope_config.py +2 -2
- camel/configs/moonshot_config.py +2 -2
- camel/configs/nebius_config.py +2 -2
- 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 +2 -2
- camel/configs/openrouter_config.py +2 -2
- camel/configs/ppio_config.py +2 -2
- camel/configs/qianfan_config.py +2 -2
- camel/configs/qwen_config.py +2 -2
- camel/configs/reka_config.py +2 -2
- camel/configs/samba_config.py +2 -2
- 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 +2 -2
- camel/configs/watsonx_config.py +2 -2
- camel/configs/yi_config.py +2 -2
- camel/configs/zhipuai_config.py +2 -2
- camel/data_collectors/__init__.py +2 -2
- camel/data_collectors/alpaca_collector.py +2 -2
- 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 +2 -2
- camel/datagen/evol_instruct/__init__.py +2 -2
- camel/datagen/evol_instruct/evol_instruct.py +2 -2
- camel/datagen/evol_instruct/scorer.py +2 -2
- camel/datagen/evol_instruct/templates.py +2 -2
- camel/datagen/self_improving_cot.py +2 -2
- 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 +2 -2
- 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 +2 -2
- camel/datasets/few_shot_generator.py +2 -2
- 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 +2 -2
- camel/environments/models.py +2 -2
- camel/environments/multi_step.py +2 -2
- camel/environments/rlcards_env.py +2 -2
- camel/environments/single_step.py +2 -2
- camel/environments/tic_tac_toe.py +2 -2
- 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 +2 -2
- camel/interpreters/base.py +2 -2
- camel/interpreters/docker_interpreter.py +2 -2
- camel/interpreters/e2b_interpreter.py +2 -2
- camel/interpreters/internal_python_interpreter.py +2 -2
- camel/interpreters/interpreter_error.py +2 -2
- camel/interpreters/ipython_interpreter.py +2 -2
- camel/interpreters/microsandbox_interpreter.py +2 -2
- camel/interpreters/subprocess_interpreter.py +2 -2
- camel/loaders/__init__.py +2 -2
- camel/loaders/apify_reader.py +2 -2
- camel/loaders/base_io.py +2 -2
- camel/loaders/base_loader.py +2 -2
- camel/loaders/chunkr_reader.py +2 -2
- camel/loaders/crawl4ai_reader.py +2 -2
- camel/loaders/firecrawl_reader.py +2 -2
- 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 +2 -2
- camel/memories/__init__.py +2 -2
- camel/memories/agent_memories.py +2 -2
- camel/memories/base.py +2 -2
- camel/memories/blocks/__init__.py +2 -2
- camel/memories/blocks/chat_history_block.py +2 -2
- camel/memories/blocks/vectordb_block.py +2 -2
- camel/memories/context_creators/__init__.py +2 -2
- camel/memories/context_creators/score_based.py +89 -2
- camel/memories/records.py +2 -2
- camel/messages/__init__.py +2 -2
- camel/messages/base.py +2 -2
- 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 +2 -2
- camel/models/__init__.py +4 -2
- camel/models/_utils.py +2 -2
- camel/models/aihubmix_model.py +2 -2
- camel/models/aiml_model.py +2 -2
- camel/models/amd_model.py +2 -2
- camel/models/anthropic_model.py +2 -2
- camel/models/aws_bedrock_model.py +2 -2
- camel/models/azure_openai_model.py +4 -28
- camel/models/base_audio_model.py +2 -2
- camel/models/base_model.py +192 -14
- camel/models/cerebras_model.py +2 -2
- camel/models/cohere_model.py +4 -30
- camel/models/cometapi_model.py +2 -2
- camel/models/crynux_model.py +2 -2
- camel/models/deepseek_model.py +4 -28
- camel/models/fish_audio_model.py +2 -2
- camel/models/function_gemma_model.py +889 -0
- camel/models/gemini_model.py +4 -28
- camel/models/groq_model.py +2 -2
- camel/models/internlm_model.py +2 -2
- camel/models/litellm_model.py +3 -17
- camel/models/lmstudio_model.py +2 -2
- camel/models/minimax_model.py +2 -2
- camel/models/mistral_model.py +4 -30
- camel/models/model_factory.py +4 -2
- camel/models/model_manager.py +2 -2
- camel/models/modelscope_model.py +2 -2
- camel/models/moonshot_model.py +3 -15
- camel/models/nebius_model.py +2 -2
- camel/models/nemotron_model.py +2 -2
- camel/models/netmind_model.py +2 -2
- camel/models/novita_model.py +2 -2
- camel/models/nvidia_model.py +2 -2
- camel/models/ollama_model.py +2 -2
- camel/models/openai_audio_models.py +2 -2
- camel/models/openai_compatible_model.py +4 -28
- camel/models/openai_model.py +4 -43
- camel/models/openrouter_model.py +2 -2
- camel/models/ppio_model.py +2 -2
- camel/models/qianfan_model.py +2 -2
- camel/models/qwen_model.py +2 -2
- camel/models/reka_model.py +4 -30
- 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 +4 -30
- camel/models/sglang_model.py +4 -30
- camel/models/siliconflow_model.py +2 -2
- camel/models/stub_model.py +2 -2
- camel/models/togetherai_model.py +2 -2
- camel/models/vllm_model.py +2 -2
- camel/models/volcano_model.py +147 -4
- camel/models/watsonx_model.py +4 -30
- camel/models/yi_model.py +2 -2
- camel/models/zhipuai_model.py +2 -2
- camel/parsers/__init__.py +2 -2
- camel/parsers/mcp_tool_call_parser.py +2 -2
- 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 +2 -2
- camel/prompts/prompt_templates.py +2 -2
- camel/prompts/role_description_prompt_template.py +2 -2
- camel/prompts/solution_extraction.py +2 -2
- camel/prompts/task_prompt_template.py +2 -2
- camel/prompts/translation.py +2 -2
- camel/prompts/video_description_prompt.py +2 -2
- camel/responses/__init__.py +2 -2
- camel/responses/agent_responses.py +2 -2
- camel/retrievers/__init__.py +2 -2
- camel/retrievers/auto_retriever.py +2 -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/__init__.py +2 -2
- camel/runtimes/api.py +2 -2
- camel/runtimes/base.py +2 -2
- camel/runtimes/configs.py +2 -2
- camel/runtimes/daytona_runtime.py +2 -2
- camel/runtimes/docker_runtime.py +2 -2
- camel/runtimes/llm_guard_runtime.py +2 -2
- camel/runtimes/remote_http_runtime.py +2 -2
- 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 +2 -2
- camel/schemas/outlines_converter.py +2 -2
- camel/services/agent_openapi_server.py +2 -2
- camel/societies/__init__.py +2 -2
- camel/societies/babyagi_playing.py +2 -2
- camel/societies/role_playing.py +2 -2
- camel/societies/workforce/__init__.py +2 -2
- camel/societies/workforce/base.py +2 -2
- camel/societies/workforce/events.py +4 -2
- camel/societies/workforce/prompts.py +9 -8
- camel/societies/workforce/role_playing_worker.py +2 -2
- camel/societies/workforce/single_agent_worker.py +2 -2
- camel/societies/workforce/structured_output_handler.py +2 -2
- camel/societies/workforce/task_channel.py +2 -2
- camel/societies/workforce/utils.py +2 -2
- camel/societies/workforce/worker.py +2 -2
- camel/societies/workforce/workflow_memory_manager.py +2 -2
- camel/societies/workforce/workforce.py +132 -71
- camel/societies/workforce/workforce_callback.py +2 -2
- camel/societies/workforce/workforce_logger.py +2 -2
- camel/societies/workforce/workforce_metrics.py +2 -2
- camel/storages/__init__.py +2 -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 +2 -2
- camel/storages/graph_storages/neo4j_graph.py +2 -2
- 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 +2 -2
- camel/storages/key_value_storages/mem0_cloud.py +2 -2
- 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 +2 -2
- camel/storages/vectordb_storages/__init__.py +2 -2
- camel/storages/vectordb_storages/base.py +2 -2
- camel/storages/vectordb_storages/chroma.py +2 -2
- camel/storages/vectordb_storages/faiss.py +2 -2
- camel/storages/vectordb_storages/milvus.py +2 -2
- camel/storages/vectordb_storages/oceanbase.py +2 -2
- camel/storages/vectordb_storages/pgvector.py +2 -2
- camel/storages/vectordb_storages/qdrant.py +2 -2
- camel/storages/vectordb_storages/surreal.py +2 -2
- camel/storages/vectordb_storages/tidb.py +2 -2
- camel/storages/vectordb_storages/weaviate.py +2 -2
- camel/tasks/__init__.py +2 -2
- camel/tasks/task.py +2 -2
- camel/tasks/task_prompt.py +2 -2
- 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 +6 -3
- camel/toolkits/aci_toolkit.py +2 -2
- camel/toolkits/arxiv_toolkit.py +2 -2
- camel/toolkits/ask_news_toolkit.py +2 -2
- camel/toolkits/async_browser_toolkit.py +2 -2
- camel/toolkits/audio_analysis_toolkit.py +2 -2
- camel/toolkits/base.py +47 -5
- camel/toolkits/bohrium_toolkit.py +2 -2
- camel/toolkits/browser_toolkit.py +2 -2
- camel/toolkits/browser_toolkit_commons.py +2 -2
- camel/toolkits/code_execution.py +2 -2
- camel/toolkits/context_summarizer_toolkit.py +2 -2
- camel/toolkits/craw4ai_toolkit.py +2 -2
- camel/toolkits/dappier_toolkit.py +2 -2
- camel/toolkits/data_commons_toolkit.py +2 -2
- camel/toolkits/dingtalk.py +2 -2
- camel/toolkits/earth_science_toolkit.py +2 -2
- camel/toolkits/edgeone_pages_mcp_toolkit.py +2 -2
- camel/toolkits/excel_toolkit.py +2 -2
- camel/toolkits/file_toolkit.py +2 -2
- camel/toolkits/function_tool.py +95 -25
- camel/toolkits/github_toolkit.py +2 -2
- camel/toolkits/gmail_toolkit.py +2 -2
- camel/toolkits/google_calendar_toolkit.py +2 -2
- camel/toolkits/google_drive_mcp_toolkit.py +2 -2
- camel/toolkits/google_maps_toolkit.py +2 -2
- camel/toolkits/google_scholar_toolkit.py +2 -2
- camel/toolkits/human_toolkit.py +2 -2
- camel/toolkits/hybrid_browser_toolkit/__init__.py +2 -2
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +2 -2
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +2 -2
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +89 -104
- camel/toolkits/hybrid_browser_toolkit/installer.py +2 -2
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +25 -14
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +6 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +2 -2
- camel/toolkits/hybrid_browser_toolkit_py/__init__.py +2 -2
- camel/toolkits/hybrid_browser_toolkit_py/actions.py +2 -2
- camel/toolkits/hybrid_browser_toolkit_py/agent.py +2 -2
- camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +2 -2
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +2 -2
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2 -2
- camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +2 -2
- camel/toolkits/image_analysis_toolkit.py +2 -2
- camel/toolkits/image_generation_toolkit.py +2 -2
- camel/toolkits/jina_reranker_toolkit.py +2 -2
- camel/toolkits/klavis_toolkit.py +2 -2
- camel/toolkits/linkedin_toolkit.py +2 -2
- camel/toolkits/markitdown_toolkit.py +2 -2
- camel/toolkits/math_toolkit.py +2 -2
- camel/toolkits/mcp_toolkit.py +2 -2
- camel/toolkits/memory_toolkit.py +2 -2
- camel/toolkits/meshy_toolkit.py +2 -2
- camel/toolkits/message_agent_toolkit.py +2 -2
- camel/toolkits/message_integration.py +6 -2
- camel/toolkits/microsoft_outlook_mail_toolkit.py +1885 -0
- camel/toolkits/mineru_toolkit.py +2 -2
- camel/toolkits/minimax_mcp_toolkit.py +2 -2
- camel/toolkits/networkx_toolkit.py +2 -2
- camel/toolkits/note_taking_toolkit.py +2 -2
- camel/toolkits/notion_mcp_toolkit.py +2 -2
- camel/toolkits/notion_toolkit.py +2 -2
- camel/toolkits/open_api_specs/biztoc/__init__.py +2 -2
- 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/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/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 +2 -2
- camel/toolkits/origene_mcp_toolkit.py +2 -2
- camel/toolkits/playwright_mcp_toolkit.py +2 -2
- camel/toolkits/pptx_toolkit.py +2 -2
- 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 +2 -2
- camel/toolkits/retrieval_toolkit.py +2 -2
- camel/toolkits/screenshot_toolkit.py +2 -2
- camel/toolkits/search_toolkit.py +70 -13
- camel/toolkits/searxng_toolkit.py +2 -2
- camel/toolkits/semantic_scholar_toolkit.py +2 -2
- camel/toolkits/slack_toolkit.py +2 -2
- camel/toolkits/sql_toolkit.py +2 -2
- camel/toolkits/stripe_toolkit.py +2 -2
- camel/toolkits/sympy_toolkit.py +2 -2
- camel/toolkits/task_planning_toolkit.py +2 -2
- camel/toolkits/terminal_toolkit/__init__.py +2 -2
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +323 -112
- camel/toolkits/terminal_toolkit/utils.py +179 -52
- camel/toolkits/thinking_toolkit.py +2 -2
- camel/toolkits/twitter_toolkit.py +2 -2
- camel/toolkits/vertex_ai_veo_toolkit.py +2 -2
- camel/toolkits/video_analysis_toolkit.py +2 -2
- camel/toolkits/video_download_toolkit.py +2 -2
- camel/toolkits/weather_toolkit.py +2 -2
- camel/toolkits/web_deploy_toolkit.py +2 -2
- camel/toolkits/wechat_official_toolkit.py +2 -2
- camel/toolkits/whatsapp_toolkit.py +2 -2
- camel/toolkits/wolfram_alpha_toolkit.py +2 -2
- camel/toolkits/zapier_toolkit.py +2 -2
- camel/types/__init__.py +2 -2
- camel/types/agents/__init__.py +2 -2
- camel/types/agents/tool_calling_record.py +2 -2
- camel/types/enums.py +5 -4
- camel/types/mcp_registries.py +2 -2
- camel/types/openai_types.py +2 -2
- camel/types/unified_model_type.py +10 -6
- 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 +2 -2
- camel/utils/constants.py +2 -2
- camel/utils/context_utils.py +2 -2
- camel/utils/deduplication.py +2 -2
- camel/utils/filename.py +2 -2
- camel/utils/langfuse.py +18 -10
- camel/utils/mcp.py +2 -2
- camel/utils/mcp_client.py +2 -2
- camel/utils/message_summarizer.py +2 -2
- camel/utils/response_format.py +2 -2
- camel/utils/token_counting.py +2 -2
- camel/utils/tool_result.py +2 -2
- 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.82.dist-info → camel_ai-0.2.83a6.dist-info}/METADATA +34 -29
- camel_ai-0.2.83a6.dist-info/RECORD +511 -0
- camel_ai-0.2.82.dist-info/RECORD +0 -507
- {camel_ai-0.2.82.dist-info → camel_ai-0.2.83a6.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.82.dist-info → camel_ai-0.2.83a6.dist-info}/licenses/LICENSE +0 -0
camel/agents/chat_agent.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ========= Copyright 2023-
|
|
1
|
+
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
2
2
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
3
|
# you may not use this file except in compliance with the License.
|
|
4
4
|
# You may obtain a copy of the License at
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
|
-
# ========= Copyright 2023-
|
|
13
|
+
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
@@ -70,7 +70,6 @@ from camel.logger import get_logger
|
|
|
70
70
|
from camel.memories import (
|
|
71
71
|
AgentMemory,
|
|
72
72
|
ChatHistoryMemory,
|
|
73
|
-
ContextRecord,
|
|
74
73
|
MemoryRecord,
|
|
75
74
|
ScoreBasedContextCreator,
|
|
76
75
|
)
|
|
@@ -105,16 +104,7 @@ from camel.utils import (
|
|
|
105
104
|
)
|
|
106
105
|
from camel.utils.commons import dependencies_required
|
|
107
106
|
from camel.utils.context_utils import ContextUtility
|
|
108
|
-
|
|
109
|
-
TOKEN_LIMIT_ERROR_MARKERS = (
|
|
110
|
-
"context_length_exceeded",
|
|
111
|
-
"prompt is too long",
|
|
112
|
-
"exceeded your current quota",
|
|
113
|
-
"tokens must be reduced",
|
|
114
|
-
"context length",
|
|
115
|
-
"token count",
|
|
116
|
-
"context limit",
|
|
117
|
-
)
|
|
107
|
+
from camel.utils.tool_result import ToolResult
|
|
118
108
|
|
|
119
109
|
if TYPE_CHECKING:
|
|
120
110
|
from camel.terminators import ResponseTerminator
|
|
@@ -397,6 +387,10 @@ class ChatAgent(BaseAgent):
|
|
|
397
387
|
window that triggers summarization. If `None`, will trigger
|
|
398
388
|
summarization when the context window is full.
|
|
399
389
|
(default: :obj:`None`)
|
|
390
|
+
token_limit (int, optional): The maximum number of tokens allowed for
|
|
391
|
+
the context window. If `None`, uses the model's default token
|
|
392
|
+
limit. This can be used to restrict the context size below the
|
|
393
|
+
model's maximum capacity. (default: :obj:`None`)
|
|
400
394
|
output_language (str, optional): The language to be output by the
|
|
401
395
|
agent. (default: :obj:`None`)
|
|
402
396
|
tools (Optional[List[Union[FunctionTool, Callable]]], optional): List
|
|
@@ -416,7 +410,10 @@ class ChatAgent(BaseAgent):
|
|
|
416
410
|
directly return the request instead of processing it.
|
|
417
411
|
(default: :obj:`None`)
|
|
418
412
|
response_terminators (List[ResponseTerminator], optional): List of
|
|
419
|
-
:obj:`ResponseTerminator`
|
|
413
|
+
:obj:`ResponseTerminator` to check if task is complete. When set,
|
|
414
|
+
the agent will keep prompting the model until a terminator signals
|
|
415
|
+
completion. Note: You must define the termination signal (e.g.,
|
|
416
|
+
a keyword) in your system prompt so the model knows what to output.
|
|
420
417
|
(default: :obj:`None`)
|
|
421
418
|
scheduling_strategy (str): name of function that defines how to select
|
|
422
419
|
the next model in ModelManager. (default: :str:`round_robin`)
|
|
@@ -454,10 +451,12 @@ class ChatAgent(BaseAgent):
|
|
|
454
451
|
step_timeout (Optional[float], optional): Timeout in seconds for the
|
|
455
452
|
entire step operation. If None, no timeout is applied.
|
|
456
453
|
(default: :obj:`None`)
|
|
457
|
-
stream_accumulate (bool, optional): When True, partial
|
|
458
|
-
updates return accumulated content
|
|
459
|
-
|
|
460
|
-
|
|
454
|
+
stream_accumulate (Optional[bool], optional): When True, partial
|
|
455
|
+
streaming updates return accumulated content. When False, partial
|
|
456
|
+
updates return only the incremental delta (recommended).
|
|
457
|
+
If None, defaults to False with a deprecation warning for users
|
|
458
|
+
who previously relied on the old default (True).
|
|
459
|
+
(default: :obj:`None`, which behaves as :obj:`False`)
|
|
461
460
|
summary_window_ratio (float, optional): Maximum fraction of the total
|
|
462
461
|
context window that can be occupied by summary information. Used
|
|
463
462
|
to limit how much of the model's context is reserved for
|
|
@@ -507,7 +506,7 @@ class ChatAgent(BaseAgent):
|
|
|
507
506
|
retry_attempts: int = 3,
|
|
508
507
|
retry_delay: float = 1.0,
|
|
509
508
|
step_timeout: Optional[float] = Constants.TIMEOUT_THRESHOLD,
|
|
510
|
-
stream_accumulate: bool =
|
|
509
|
+
stream_accumulate: Optional[bool] = None,
|
|
511
510
|
summary_window_ratio: float = 0.6,
|
|
512
511
|
) -> None:
|
|
513
512
|
if isinstance(model, ModelManager):
|
|
@@ -528,10 +527,16 @@ class ChatAgent(BaseAgent):
|
|
|
528
527
|
self._tool_output_history: List[_ToolOutputHistoryEntry] = []
|
|
529
528
|
|
|
530
529
|
# Set up memory
|
|
530
|
+
if token_limit is not None:
|
|
531
|
+
effective_token_limit = token_limit
|
|
532
|
+
else:
|
|
533
|
+
effective_token_limit = self.model_backend.token_limit
|
|
531
534
|
context_creator = ScoreBasedContextCreator(
|
|
532
535
|
self.model_backend.token_counter,
|
|
533
|
-
|
|
536
|
+
effective_token_limit,
|
|
534
537
|
)
|
|
538
|
+
self._token_limit = effective_token_limit
|
|
539
|
+
self._summary_token_count = 0
|
|
535
540
|
|
|
536
541
|
self._memory: AgentMemory = memory or ChatHistoryMemory(
|
|
537
542
|
context_creator,
|
|
@@ -568,7 +573,6 @@ class ChatAgent(BaseAgent):
|
|
|
568
573
|
f"{summarize_threshold}% of the total token limit."
|
|
569
574
|
)
|
|
570
575
|
self.summarize_threshold = summarize_threshold
|
|
571
|
-
self._reset_summary_state()
|
|
572
576
|
|
|
573
577
|
# Set up role name and role type
|
|
574
578
|
self.role_name: str = (
|
|
@@ -616,20 +620,48 @@ class ChatAgent(BaseAgent):
|
|
|
616
620
|
self.step_timeout = step_timeout
|
|
617
621
|
self._context_utility: Optional[ContextUtility] = None
|
|
618
622
|
self._context_summary_agent: Optional["ChatAgent"] = None
|
|
619
|
-
|
|
623
|
+
|
|
624
|
+
# Store whether user explicitly set stream_accumulate
|
|
625
|
+
# Warning will be issued only when streaming is actually used
|
|
626
|
+
self._stream_accumulate_explicit = stream_accumulate is not None
|
|
627
|
+
self.stream_accumulate = (
|
|
628
|
+
stream_accumulate if stream_accumulate is not None else False
|
|
629
|
+
)
|
|
620
630
|
self._last_tool_call_record: Optional[ToolCallingRecord] = None
|
|
621
631
|
self._last_tool_call_signature: Optional[str] = None
|
|
622
|
-
self._last_token_limit_tool_signature: Optional[str] = None
|
|
623
632
|
self.summary_window_ratio = summary_window_ratio
|
|
624
633
|
|
|
625
634
|
def reset(self):
|
|
626
635
|
r"""Resets the :obj:`ChatAgent` to its initial state."""
|
|
627
636
|
self.terminated = False
|
|
628
637
|
self.init_messages()
|
|
629
|
-
self._reset_summary_state()
|
|
630
638
|
for terminator in self.response_terminators:
|
|
631
639
|
terminator.reset()
|
|
632
640
|
|
|
641
|
+
def _update_token_cache(
|
|
642
|
+
self,
|
|
643
|
+
usage_dict: Dict[str, Any],
|
|
644
|
+
message_count: int,
|
|
645
|
+
) -> None:
|
|
646
|
+
r"""Update the token count cache from LLM response usage.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
usage_dict (Dict[str, Any]): Usage dictionary from LLM response.
|
|
650
|
+
message_count (int): Number of messages sent to the LLM.
|
|
651
|
+
"""
|
|
652
|
+
prompt_tokens = usage_dict.get("prompt_tokens", 0)
|
|
653
|
+
completion_tokens = usage_dict.get("completion_tokens", 0)
|
|
654
|
+
|
|
655
|
+
if prompt_tokens == 0:
|
|
656
|
+
return
|
|
657
|
+
|
|
658
|
+
total_tokens = prompt_tokens + completion_tokens
|
|
659
|
+
context_creator = self.memory.get_context_creator()
|
|
660
|
+
if hasattr(context_creator, 'set_cached_token_count'):
|
|
661
|
+
context_creator.set_cached_token_count(
|
|
662
|
+
total_tokens, message_count + 1
|
|
663
|
+
)
|
|
664
|
+
|
|
633
665
|
def _resolve_models(
|
|
634
666
|
self,
|
|
635
667
|
model: Optional[
|
|
@@ -774,6 +806,11 @@ class ChatAgent(BaseAgent):
|
|
|
774
806
|
r"""Returns a dictionary of internal tools."""
|
|
775
807
|
return self._internal_tools
|
|
776
808
|
|
|
809
|
+
@property
|
|
810
|
+
def token_limit(self) -> int:
|
|
811
|
+
r"""Returns the token limit for the agent's context window."""
|
|
812
|
+
return self._token_limit
|
|
813
|
+
|
|
777
814
|
@property
|
|
778
815
|
def output_language(self) -> Optional[str]:
|
|
779
816
|
r"""Returns the output language for the agent."""
|
|
@@ -833,122 +870,6 @@ class ChatAgent(BaseAgent):
|
|
|
833
870
|
for func_tool in self._internal_tools.values()
|
|
834
871
|
]
|
|
835
872
|
|
|
836
|
-
@staticmethod
|
|
837
|
-
def _is_token_limit_error(error: Exception) -> bool:
|
|
838
|
-
r"""Return True when the exception message indicates a token limit."""
|
|
839
|
-
error_message = str(error).lower()
|
|
840
|
-
return any(
|
|
841
|
-
marker in error_message for marker in TOKEN_LIMIT_ERROR_MARKERS
|
|
842
|
-
)
|
|
843
|
-
|
|
844
|
-
@staticmethod
|
|
845
|
-
def _is_tool_related_record(record: MemoryRecord) -> bool:
|
|
846
|
-
r"""Determine whether the given memory record
|
|
847
|
-
belongs to a tool call."""
|
|
848
|
-
if record.role_at_backend in {
|
|
849
|
-
OpenAIBackendRole.TOOL,
|
|
850
|
-
OpenAIBackendRole.FUNCTION,
|
|
851
|
-
}:
|
|
852
|
-
return True
|
|
853
|
-
|
|
854
|
-
if (
|
|
855
|
-
record.role_at_backend == OpenAIBackendRole.ASSISTANT
|
|
856
|
-
and isinstance(record.message, FunctionCallingMessage)
|
|
857
|
-
):
|
|
858
|
-
return True
|
|
859
|
-
|
|
860
|
-
return False
|
|
861
|
-
|
|
862
|
-
def _find_indices_to_remove_for_last_tool_pair(
|
|
863
|
-
self, recent_records: List[ContextRecord]
|
|
864
|
-
) -> List[int]:
|
|
865
|
-
"""Find indices of records that should be removed to clean up the most
|
|
866
|
-
recent incomplete tool interaction pair.
|
|
867
|
-
|
|
868
|
-
This method identifies tool call/result pairs by tool_call_id and
|
|
869
|
-
returns the exact indices to remove, allowing non-contiguous deletions.
|
|
870
|
-
|
|
871
|
-
Logic:
|
|
872
|
-
- If the last record is a tool result (TOOL/FUNCTION) with a
|
|
873
|
-
tool_call_id, find the matching assistant call anywhere in history
|
|
874
|
-
and return both indices.
|
|
875
|
-
- If the last record is an assistant tool call without a result yet,
|
|
876
|
-
return just that index.
|
|
877
|
-
- For normal messages (non tool-related): remove just the last one.
|
|
878
|
-
- Fallback: If no tool_call_id is available, use heuristic (last 2 if
|
|
879
|
-
tool-related, otherwise last 1).
|
|
880
|
-
|
|
881
|
-
Returns:
|
|
882
|
-
List[int]: Indices to remove (may be non-contiguous).
|
|
883
|
-
"""
|
|
884
|
-
if not recent_records:
|
|
885
|
-
return []
|
|
886
|
-
|
|
887
|
-
last_idx = len(recent_records) - 1
|
|
888
|
-
last_record = recent_records[last_idx].memory_record
|
|
889
|
-
|
|
890
|
-
# Case A: Last is an ASSISTANT tool call with no result yet
|
|
891
|
-
if (
|
|
892
|
-
last_record.role_at_backend == OpenAIBackendRole.ASSISTANT
|
|
893
|
-
and isinstance(last_record.message, FunctionCallingMessage)
|
|
894
|
-
and last_record.message.result is None
|
|
895
|
-
):
|
|
896
|
-
return [last_idx]
|
|
897
|
-
|
|
898
|
-
# Case B: Last is TOOL/FUNCTION result, try id-based pairing
|
|
899
|
-
if last_record.role_at_backend in {
|
|
900
|
-
OpenAIBackendRole.TOOL,
|
|
901
|
-
OpenAIBackendRole.FUNCTION,
|
|
902
|
-
}:
|
|
903
|
-
tool_id = None
|
|
904
|
-
if isinstance(last_record.message, FunctionCallingMessage):
|
|
905
|
-
tool_id = last_record.message.tool_call_id
|
|
906
|
-
|
|
907
|
-
if tool_id:
|
|
908
|
-
for idx in range(len(recent_records) - 2, -1, -1):
|
|
909
|
-
rec = recent_records[idx].memory_record
|
|
910
|
-
if rec.role_at_backend != OpenAIBackendRole.ASSISTANT:
|
|
911
|
-
continue
|
|
912
|
-
|
|
913
|
-
# Check if this assistant message contains the tool_call_id
|
|
914
|
-
matched = False
|
|
915
|
-
|
|
916
|
-
# Case 1: FunctionCallingMessage (single tool call)
|
|
917
|
-
if isinstance(rec.message, FunctionCallingMessage):
|
|
918
|
-
if rec.message.tool_call_id == tool_id:
|
|
919
|
-
matched = True
|
|
920
|
-
|
|
921
|
-
# Case 2: BaseMessage with multiple tool_calls in meta_dict
|
|
922
|
-
elif (
|
|
923
|
-
hasattr(rec.message, "meta_dict")
|
|
924
|
-
and rec.message.meta_dict
|
|
925
|
-
):
|
|
926
|
-
tool_calls_list = rec.message.meta_dict.get(
|
|
927
|
-
"tool_calls", []
|
|
928
|
-
)
|
|
929
|
-
if isinstance(tool_calls_list, list):
|
|
930
|
-
for tc in tool_calls_list:
|
|
931
|
-
if (
|
|
932
|
-
isinstance(tc, dict)
|
|
933
|
-
and tc.get("id") == tool_id
|
|
934
|
-
):
|
|
935
|
-
matched = True
|
|
936
|
-
break
|
|
937
|
-
|
|
938
|
-
if matched:
|
|
939
|
-
# Return both assistant call and tool result indices
|
|
940
|
-
return [idx, last_idx]
|
|
941
|
-
|
|
942
|
-
# Fallback: no tool_call_id, use heuristic
|
|
943
|
-
if self._is_tool_related_record(last_record):
|
|
944
|
-
# Remove last 2 (assume they are paired)
|
|
945
|
-
return [last_idx - 1, last_idx] if last_idx > 0 else [last_idx]
|
|
946
|
-
else:
|
|
947
|
-
return [last_idx]
|
|
948
|
-
|
|
949
|
-
# Default: non tool-related tail => remove last one
|
|
950
|
-
return [last_idx]
|
|
951
|
-
|
|
952
873
|
@staticmethod
|
|
953
874
|
def _serialize_tool_args(args: Dict[str, Any]) -> str:
|
|
954
875
|
try:
|
|
@@ -991,39 +912,6 @@ class ChatAgent(BaseAgent):
|
|
|
991
912
|
signature = None
|
|
992
913
|
self._last_tool_call_signature = signature
|
|
993
914
|
|
|
994
|
-
def _format_tool_limit_notice(self) -> Optional[str]:
|
|
995
|
-
record = self._last_tool_call_record
|
|
996
|
-
description = self._describe_tool_call(record)
|
|
997
|
-
if description is None:
|
|
998
|
-
return None
|
|
999
|
-
notice_lines = [
|
|
1000
|
-
"[Tool Call Causing Token Limit]",
|
|
1001
|
-
description,
|
|
1002
|
-
]
|
|
1003
|
-
|
|
1004
|
-
if record is not None:
|
|
1005
|
-
result = record.result
|
|
1006
|
-
if isinstance(result, bytes):
|
|
1007
|
-
result_repr = result.decode(errors="replace")
|
|
1008
|
-
elif isinstance(result, str):
|
|
1009
|
-
result_repr = result
|
|
1010
|
-
else:
|
|
1011
|
-
try:
|
|
1012
|
-
result_repr = json.dumps(
|
|
1013
|
-
result, ensure_ascii=False, sort_keys=True
|
|
1014
|
-
)
|
|
1015
|
-
except (TypeError, ValueError):
|
|
1016
|
-
result_repr = str(result)
|
|
1017
|
-
|
|
1018
|
-
result_length = len(result_repr)
|
|
1019
|
-
notice_lines.append(f"Tool result length: {result_length}")
|
|
1020
|
-
if self.model_backend.token_limit != 999999999:
|
|
1021
|
-
notice_lines.append(
|
|
1022
|
-
f"Token limit: {self.model_backend.token_limit}"
|
|
1023
|
-
)
|
|
1024
|
-
|
|
1025
|
-
return "\n".join(notice_lines)
|
|
1026
|
-
|
|
1027
915
|
@staticmethod
|
|
1028
916
|
def _append_user_messages_section(
|
|
1029
917
|
summary_content: str, user_messages: List[str]
|
|
@@ -1051,21 +939,104 @@ class ChatAgent(BaseAgent):
|
|
|
1051
939
|
def _reset_summary_state(self) -> None:
|
|
1052
940
|
self._summary_token_count = 0 # Total tokens in summary messages
|
|
1053
941
|
|
|
942
|
+
def _get_context_with_summarization(
|
|
943
|
+
self,
|
|
944
|
+
) -> Tuple[List[OpenAIMessage], int]:
|
|
945
|
+
r"""Get context and trigger summarization if needed."""
|
|
946
|
+
openai_messages, num_tokens = self.memory.get_context()
|
|
947
|
+
|
|
948
|
+
if self.summarize_threshold is None or num_tokens > self.token_limit:
|
|
949
|
+
return openai_messages, num_tokens
|
|
950
|
+
|
|
951
|
+
summary_token_count = self._summary_token_count
|
|
952
|
+
|
|
953
|
+
if summary_token_count > self.token_limit * self.summary_window_ratio:
|
|
954
|
+
logger.warning(
|
|
955
|
+
f"Summary tokens ({summary_token_count}) "
|
|
956
|
+
f"exceed limit, full compression."
|
|
957
|
+
)
|
|
958
|
+
summary = self.summarize(include_summaries=True)
|
|
959
|
+
self._update_memory_with_summary(
|
|
960
|
+
summary.get("summary", ""), include_summaries=True
|
|
961
|
+
)
|
|
962
|
+
return self.memory.get_context()
|
|
963
|
+
|
|
964
|
+
threshold = self._calculate_next_summary_threshold()
|
|
965
|
+
if num_tokens > threshold:
|
|
966
|
+
logger.warning(
|
|
967
|
+
f"Token count ({num_tokens}) exceed threshold "
|
|
968
|
+
f"({threshold}). Triggering summarization."
|
|
969
|
+
)
|
|
970
|
+
summary = self.summarize(include_summaries=False)
|
|
971
|
+
self._update_memory_with_summary(
|
|
972
|
+
summary.get("summary", ""), include_summaries=False
|
|
973
|
+
)
|
|
974
|
+
return self.memory.get_context()
|
|
975
|
+
|
|
976
|
+
return openai_messages, num_tokens
|
|
977
|
+
|
|
978
|
+
async def _get_context_with_summarization_async(
|
|
979
|
+
self,
|
|
980
|
+
) -> Tuple[List[OpenAIMessage], int]:
|
|
981
|
+
r"""Async version: get context and trigger summarization if needed."""
|
|
982
|
+
openai_messages, num_tokens = self.memory.get_context()
|
|
983
|
+
|
|
984
|
+
if self.summarize_threshold is None or num_tokens > self.token_limit:
|
|
985
|
+
return openai_messages, num_tokens
|
|
986
|
+
|
|
987
|
+
summary_token_count = self._summary_token_count
|
|
988
|
+
|
|
989
|
+
if summary_token_count > self.token_limit * self.summary_window_ratio:
|
|
990
|
+
logger.warning(
|
|
991
|
+
f"Summary tokens ({summary_token_count}) "
|
|
992
|
+
f"exceed limit, full compression."
|
|
993
|
+
)
|
|
994
|
+
summary = await self.asummarize(include_summaries=True)
|
|
995
|
+
self._update_memory_with_summary(
|
|
996
|
+
summary.get("summary", ""), include_summaries=True
|
|
997
|
+
)
|
|
998
|
+
return self.memory.get_context()
|
|
999
|
+
|
|
1000
|
+
threshold = self._calculate_next_summary_threshold()
|
|
1001
|
+
if num_tokens > threshold:
|
|
1002
|
+
logger.warning(
|
|
1003
|
+
f"Token count ({num_tokens}) exceed threshold "
|
|
1004
|
+
f"({threshold}). Triggering summarization."
|
|
1005
|
+
)
|
|
1006
|
+
summary = await self.asummarize(include_summaries=False)
|
|
1007
|
+
self._update_memory_with_summary(
|
|
1008
|
+
summary.get("summary", ""), include_summaries=False
|
|
1009
|
+
)
|
|
1010
|
+
return self.memory.get_context()
|
|
1011
|
+
|
|
1012
|
+
return openai_messages, num_tokens
|
|
1013
|
+
|
|
1054
1014
|
def _calculate_next_summary_threshold(self) -> int:
|
|
1055
1015
|
r"""Calculate the next token threshold that should trigger
|
|
1056
1016
|
summarization.
|
|
1057
1017
|
|
|
1058
1018
|
The threshold calculation follows a progressive strategy:
|
|
1059
|
-
- First time
|
|
1060
|
-
|
|
1019
|
+
- First time (or after full compression):
|
|
1020
|
+
token_limit * (summarize_threshold / 100)
|
|
1021
|
+
- After progressive compression:
|
|
1022
|
+
(token_limit - summary_tokens) * (summarize_threshold / 100)
|
|
1023
|
+
+ summary_tokens
|
|
1061
1024
|
|
|
1062
|
-
This ensures that as summaries accumulate
|
|
1063
|
-
to maintain a reasonable balance
|
|
1025
|
+
This ensures that as summaries accumulate through progressive
|
|
1026
|
+
compression, the threshold adapts to maintain a reasonable balance
|
|
1027
|
+
between context and summaries. After full compression, the threshold
|
|
1028
|
+
resets to the initial value to prevent frequent re-summarization.
|
|
1064
1029
|
|
|
1065
1030
|
Returns:
|
|
1066
1031
|
int: The token count threshold for next summarization.
|
|
1067
1032
|
"""
|
|
1068
|
-
|
|
1033
|
+
if self.summarize_threshold is None:
|
|
1034
|
+
raise ValueError(
|
|
1035
|
+
"Cannot calculate summary threshold when "
|
|
1036
|
+
"summarize_threshold is None"
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
token_limit = self.token_limit
|
|
1069
1040
|
summary_token_count = self._summary_token_count
|
|
1070
1041
|
|
|
1071
1042
|
# First summarization: use the percentage threshold
|
|
@@ -1094,17 +1065,21 @@ class ChatAgent(BaseAgent):
|
|
|
1094
1065
|
summary_content: str = summary
|
|
1095
1066
|
|
|
1096
1067
|
existing_summaries = []
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1068
|
+
last_user_message: Optional[str] = None
|
|
1069
|
+
messages, _ = self.memory.get_context()
|
|
1070
|
+
for msg in messages:
|
|
1071
|
+
content = msg.get('content', '')
|
|
1072
|
+
role = msg.get('role', '')
|
|
1073
|
+
if role == 'user' and isinstance(content, str) and content:
|
|
1074
|
+
last_user_message = content
|
|
1075
|
+
if (
|
|
1076
|
+
not include_summaries
|
|
1077
|
+
and isinstance(content, str)
|
|
1078
|
+
and content.startswith('[CONTEXT_SUMMARY]')
|
|
1079
|
+
):
|
|
1080
|
+
existing_summaries.append(msg)
|
|
1105
1081
|
|
|
1106
|
-
|
|
1107
|
-
self.clear_memory()
|
|
1082
|
+
self.clear_memory(reset_summary_state=False)
|
|
1108
1083
|
|
|
1109
1084
|
# Restore old summaries (for progressive compression)
|
|
1110
1085
|
for old_summary in existing_summaries:
|
|
@@ -1121,16 +1096,24 @@ class ChatAgent(BaseAgent):
|
|
|
1121
1096
|
role_name="assistant", content=summary_content
|
|
1122
1097
|
)
|
|
1123
1098
|
self.update_memory(new_summary_msg, OpenAIBackendRole.ASSISTANT)
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1099
|
+
|
|
1100
|
+
# Restore last user message to maintain conversation structure
|
|
1101
|
+
# The summary already contains all user messages, but we keep the
|
|
1102
|
+
# latest one so the model knows what to respond to
|
|
1103
|
+
if last_user_message:
|
|
1104
|
+
# Avoid duplicate prefix - check if already prefixed
|
|
1105
|
+
context_prefix = (
|
|
1106
|
+
"Based on the previous CONTEXT_SUMMARY, "
|
|
1107
|
+
"continue with my current message: "
|
|
1108
|
+
)
|
|
1109
|
+
if not last_user_message.startswith(context_prefix):
|
|
1110
|
+
last_user_message = f"{context_prefix}{last_user_message}"
|
|
1111
|
+
user_msg = BaseMessage.make_user_message(
|
|
1112
|
+
role_name="user",
|
|
1113
|
+
content=last_user_message,
|
|
1114
|
+
)
|
|
1115
|
+
self.update_memory(user_msg, OpenAIBackendRole.USER)
|
|
1116
|
+
|
|
1134
1117
|
# Update token count
|
|
1135
1118
|
try:
|
|
1136
1119
|
summary_tokens = (
|
|
@@ -1139,13 +1122,15 @@ class ChatAgent(BaseAgent):
|
|
|
1139
1122
|
)
|
|
1140
1123
|
)
|
|
1141
1124
|
|
|
1142
|
-
if
|
|
1125
|
+
if (
|
|
1126
|
+
include_summaries
|
|
1127
|
+
): # Full compression - reset and set to new summary tokens only
|
|
1143
1128
|
self._summary_token_count = summary_tokens
|
|
1144
1129
|
logger.info(
|
|
1145
1130
|
f"Full compression: Summary with {summary_tokens} tokens. "
|
|
1146
|
-
f"Total summary tokens
|
|
1131
|
+
f"Total summary tokens set to: {summary_tokens}"
|
|
1147
1132
|
)
|
|
1148
|
-
else: # Progressive compression - accumulate
|
|
1133
|
+
else: # Progressive compression - accumulate on existing count
|
|
1149
1134
|
self._summary_token_count += summary_tokens
|
|
1150
1135
|
logger.info(
|
|
1151
1136
|
f"Progressive compression: New summary "
|
|
@@ -1178,6 +1163,50 @@ class ChatAgent(BaseAgent):
|
|
|
1178
1163
|
except (TypeError, ValueError):
|
|
1179
1164
|
return str(result)
|
|
1180
1165
|
|
|
1166
|
+
def _truncate_tool_result(
|
|
1167
|
+
self, func_name: str, result: Any
|
|
1168
|
+
) -> Tuple[Any, bool]:
|
|
1169
|
+
r"""Truncate tool result if it exceeds the maximum token limit.
|
|
1170
|
+
|
|
1171
|
+
Args:
|
|
1172
|
+
func_name (str): The name of the tool function called.
|
|
1173
|
+
result (Any): The result returned by the tool execution.
|
|
1174
|
+
|
|
1175
|
+
Returns:
|
|
1176
|
+
Tuple[Any, bool]: A tuple containing:
|
|
1177
|
+
- The (possibly truncated) result
|
|
1178
|
+
- A boolean indicating whether truncation occurred
|
|
1179
|
+
"""
|
|
1180
|
+
serialized = self._serialize_tool_result(result)
|
|
1181
|
+
# Use summarize_threshold if set, otherwise default to 90%
|
|
1182
|
+
threshold_ratio = (
|
|
1183
|
+
min(0.9, self.summarize_threshold / 100)
|
|
1184
|
+
if self.summarize_threshold is not None
|
|
1185
|
+
else 0.9
|
|
1186
|
+
)
|
|
1187
|
+
max_tokens = int(self.token_limit * threshold_ratio)
|
|
1188
|
+
result_tokens = self._get_token_count(serialized)
|
|
1189
|
+
|
|
1190
|
+
if result_tokens <= max_tokens:
|
|
1191
|
+
return result, False
|
|
1192
|
+
|
|
1193
|
+
# Reserve ~100 tokens for notice, use char-based truncation directly
|
|
1194
|
+
target_tokens = max(max_tokens - 100, 100)
|
|
1195
|
+
truncated = serialized[: target_tokens * 3]
|
|
1196
|
+
|
|
1197
|
+
notice = (
|
|
1198
|
+
f"\n\n[TRUNCATED] Tool '{func_name}' output truncated "
|
|
1199
|
+
f"({result_tokens} > {max_tokens} tokens). "
|
|
1200
|
+
f"Tool executed successfully."
|
|
1201
|
+
)
|
|
1202
|
+
|
|
1203
|
+
logger.warning(
|
|
1204
|
+
f"Tool '{func_name}' result truncated: "
|
|
1205
|
+
f"{result_tokens} -> ~{target_tokens} tokens"
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
return notice + truncated, True
|
|
1209
|
+
|
|
1181
1210
|
def _clean_snapshot_line(self, line: str) -> str:
|
|
1182
1211
|
r"""Clean a single snapshot line by removing prefixes and references.
|
|
1183
1212
|
|
|
@@ -1702,6 +1731,7 @@ class ChatAgent(BaseAgent):
|
|
|
1702
1731
|
),
|
|
1703
1732
|
model=self.model_backend,
|
|
1704
1733
|
agent_id=f"{self.agent_id}_context_summarizer",
|
|
1734
|
+
token_limit=self.token_limit,
|
|
1705
1735
|
summarize_threshold=None,
|
|
1706
1736
|
)
|
|
1707
1737
|
else:
|
|
@@ -1994,6 +2024,8 @@ class ChatAgent(BaseAgent):
|
|
|
1994
2024
|
),
|
|
1995
2025
|
model=self.model_backend,
|
|
1996
2026
|
agent_id=f"{self.agent_id}_context_summarizer",
|
|
2027
|
+
token_limit=self.token_limit,
|
|
2028
|
+
summarize_threshold=None,
|
|
1997
2029
|
)
|
|
1998
2030
|
else:
|
|
1999
2031
|
self._context_summary_agent.reset()
|
|
@@ -2137,6 +2169,7 @@ class ChatAgent(BaseAgent):
|
|
|
2137
2169
|
),
|
|
2138
2170
|
model=self.model_backend,
|
|
2139
2171
|
agent_id=f"{self.agent_id}_context_summarizer",
|
|
2172
|
+
token_limit=self.token_limit,
|
|
2140
2173
|
summarize_threshold=None,
|
|
2141
2174
|
)
|
|
2142
2175
|
else:
|
|
@@ -2280,14 +2313,24 @@ class ChatAgent(BaseAgent):
|
|
|
2280
2313
|
result["status"] = error_message
|
|
2281
2314
|
return result
|
|
2282
2315
|
|
|
2283
|
-
def clear_memory(self
|
|
2316
|
+
def clear_memory(self, reset_summary_state: bool = True):
|
|
2284
2317
|
r"""Clear the agent's memory and reset to initial state.
|
|
2285
2318
|
|
|
2286
|
-
|
|
2287
|
-
|
|
2319
|
+
Args:
|
|
2320
|
+
reset_summary_state (bool): Whether to reset the summary token
|
|
2321
|
+
count. Set to False when preserving summary state during
|
|
2322
|
+
summarization. Defaults to True for full memory clearing.
|
|
2288
2323
|
"""
|
|
2289
2324
|
self.memory.clear()
|
|
2290
2325
|
|
|
2326
|
+
if reset_summary_state:
|
|
2327
|
+
self._reset_summary_state()
|
|
2328
|
+
|
|
2329
|
+
# Reset token cache when memory is cleared
|
|
2330
|
+
context_creator = self.memory.get_context_creator()
|
|
2331
|
+
if hasattr(context_creator, 'clear_cache'):
|
|
2332
|
+
context_creator.clear_cache()
|
|
2333
|
+
|
|
2291
2334
|
if self.system_message is not None:
|
|
2292
2335
|
self.memory.write_record(
|
|
2293
2336
|
MemoryRecord(
|
|
@@ -2327,7 +2370,6 @@ class ChatAgent(BaseAgent):
|
|
|
2327
2370
|
r"""Initializes the stored messages list with the current system
|
|
2328
2371
|
message.
|
|
2329
2372
|
"""
|
|
2330
|
-
self._reset_summary_state()
|
|
2331
2373
|
self.clear_memory()
|
|
2332
2374
|
|
|
2333
2375
|
def update_system_message(
|
|
@@ -2655,7 +2697,6 @@ class ChatAgent(BaseAgent):
|
|
|
2655
2697
|
# Explicitly set the tools to empty list to avoid calling tools
|
|
2656
2698
|
response = self._get_model_response(
|
|
2657
2699
|
openai_messages=[openai_message],
|
|
2658
|
-
num_tokens=0,
|
|
2659
2700
|
response_format=response_format,
|
|
2660
2701
|
tool_schemas=[],
|
|
2661
2702
|
prev_num_openai_messages=0,
|
|
@@ -2687,7 +2728,6 @@ class ChatAgent(BaseAgent):
|
|
|
2687
2728
|
openai_message: OpenAIMessage = {"role": "user", "content": prompt}
|
|
2688
2729
|
response = await self._aget_model_response(
|
|
2689
2730
|
openai_messages=[openai_message],
|
|
2690
|
-
num_tokens=0,
|
|
2691
2731
|
response_format=response_format,
|
|
2692
2732
|
tool_schemas=[],
|
|
2693
2733
|
prev_num_openai_messages=0,
|
|
@@ -2755,6 +2795,11 @@ class ChatAgent(BaseAgent):
|
|
|
2755
2795
|
response_format: Optional[Type[BaseModel]] = None,
|
|
2756
2796
|
) -> ChatAgentResponse:
|
|
2757
2797
|
r"""Implementation of non-streaming step logic."""
|
|
2798
|
+
# Set agent_id in context-local storage for logging
|
|
2799
|
+
from camel.utils.agent_context import set_current_agent_id
|
|
2800
|
+
|
|
2801
|
+
set_current_agent_id(self.agent_id)
|
|
2802
|
+
|
|
2758
2803
|
# Set Langfuse session_id using agent_id for trace grouping
|
|
2759
2804
|
try:
|
|
2760
2805
|
from camel.utils.langfuse import set_current_agent_session_id
|
|
@@ -2807,122 +2852,24 @@ class ChatAgent(BaseAgent):
|
|
|
2807
2852
|
time.sleep(0.001)
|
|
2808
2853
|
|
|
2809
2854
|
try:
|
|
2810
|
-
openai_messages, num_tokens =
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
summary_token_count = self._summary_token_count
|
|
2814
|
-
token_limit = self.model_backend.token_limit
|
|
2815
|
-
|
|
2816
|
-
if num_tokens <= token_limit:
|
|
2817
|
-
if (
|
|
2818
|
-
summary_token_count
|
|
2819
|
-
> token_limit * self.summary_window_ratio
|
|
2820
|
-
):
|
|
2821
|
-
logger.info(
|
|
2822
|
-
f"Summary tokens ({summary_token_count}) "
|
|
2823
|
-
f"exceed limit, full compression."
|
|
2824
|
-
)
|
|
2825
|
-
# Summarize everything (including summaries)
|
|
2826
|
-
summary = self.summarize(include_summaries=True)
|
|
2827
|
-
self._update_memory_with_summary(
|
|
2828
|
-
summary.get("summary", ""),
|
|
2829
|
-
include_summaries=True,
|
|
2830
|
-
)
|
|
2831
|
-
elif num_tokens > threshold:
|
|
2832
|
-
logger.info(
|
|
2833
|
-
f"Token count ({num_tokens}) exceed threshold "
|
|
2834
|
-
f"({threshold}). Triggering summarization."
|
|
2835
|
-
)
|
|
2836
|
-
# Only summarize non-summary content
|
|
2837
|
-
summary = self.summarize(include_summaries=False)
|
|
2838
|
-
self._update_memory_with_summary(
|
|
2839
|
-
summary.get("summary", ""),
|
|
2840
|
-
include_summaries=False,
|
|
2841
|
-
)
|
|
2855
|
+
openai_messages, num_tokens = (
|
|
2856
|
+
self._get_context_with_summarization()
|
|
2857
|
+
)
|
|
2842
2858
|
accumulated_context_tokens += num_tokens
|
|
2843
2859
|
except RuntimeError as e:
|
|
2844
2860
|
return self._step_terminate(
|
|
2845
2861
|
e.args[1], tool_call_records, "max_tokens_exceeded"
|
|
2846
2862
|
)
|
|
2847
|
-
# Get response from model backend
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
prev_num_openai_messages=prev_num_openai_messages,
|
|
2858
|
-
)
|
|
2859
|
-
except Exception as exc:
|
|
2860
|
-
logger.exception("Model error: %s", exc)
|
|
2861
|
-
|
|
2862
|
-
if self._is_token_limit_error(exc):
|
|
2863
|
-
tool_signature = self._last_tool_call_signature
|
|
2864
|
-
if (
|
|
2865
|
-
tool_signature is not None
|
|
2866
|
-
and tool_signature
|
|
2867
|
-
== self._last_token_limit_tool_signature
|
|
2868
|
-
):
|
|
2869
|
-
description = self._describe_tool_call(
|
|
2870
|
-
self._last_tool_call_record
|
|
2871
|
-
)
|
|
2872
|
-
repeated_msg = (
|
|
2873
|
-
"Context exceeded again by the same tool call."
|
|
2874
|
-
)
|
|
2875
|
-
if description:
|
|
2876
|
-
repeated_msg += f" {description}"
|
|
2877
|
-
raise RuntimeError(repeated_msg) from exc
|
|
2878
|
-
|
|
2879
|
-
user_message_count = sum(
|
|
2880
|
-
1
|
|
2881
|
-
for msg in openai_messages
|
|
2882
|
-
if getattr(msg, "role", None) == "user"
|
|
2883
|
-
)
|
|
2884
|
-
if (
|
|
2885
|
-
user_message_count == 1
|
|
2886
|
-
and getattr(openai_messages[-1], "role", None)
|
|
2887
|
-
== "user"
|
|
2888
|
-
):
|
|
2889
|
-
raise RuntimeError(
|
|
2890
|
-
"The provided user input alone exceeds the "
|
|
2891
|
-
"context window. Please shorten the input."
|
|
2892
|
-
) from exc
|
|
2893
|
-
|
|
2894
|
-
logger.warning(
|
|
2895
|
-
"Token limit exceeded error detected. "
|
|
2896
|
-
"Summarizing context."
|
|
2897
|
-
)
|
|
2898
|
-
|
|
2899
|
-
recent_records: List[ContextRecord]
|
|
2900
|
-
try:
|
|
2901
|
-
recent_records = self.memory.retrieve()
|
|
2902
|
-
except Exception: # pragma: no cover - defensive guard
|
|
2903
|
-
recent_records = []
|
|
2904
|
-
|
|
2905
|
-
indices_to_remove = (
|
|
2906
|
-
self._find_indices_to_remove_for_last_tool_pair(
|
|
2907
|
-
recent_records
|
|
2908
|
-
)
|
|
2909
|
-
)
|
|
2910
|
-
self.memory.remove_records_by_indices(indices_to_remove)
|
|
2911
|
-
|
|
2912
|
-
summary = self.summarize(include_summaries=False)
|
|
2913
|
-
tool_notice = self._format_tool_limit_notice()
|
|
2914
|
-
summary_messages = summary.get("summary", "")
|
|
2915
|
-
|
|
2916
|
-
if tool_notice:
|
|
2917
|
-
summary_messages += "\n\n" + tool_notice
|
|
2918
|
-
|
|
2919
|
-
self._update_memory_with_summary(
|
|
2920
|
-
summary_messages, include_summaries=False
|
|
2921
|
-
)
|
|
2922
|
-
self._last_token_limit_tool_signature = tool_signature
|
|
2923
|
-
return self._step_impl(input_message, response_format)
|
|
2924
|
-
|
|
2925
|
-
raise
|
|
2863
|
+
# Get response from model backend
|
|
2864
|
+
response = self._get_model_response(
|
|
2865
|
+
openai_messages,
|
|
2866
|
+
current_iteration=iteration_count,
|
|
2867
|
+
response_format=response_format,
|
|
2868
|
+
tool_schemas=[]
|
|
2869
|
+
if disable_tools
|
|
2870
|
+
else self._get_full_tool_schemas(),
|
|
2871
|
+
prev_num_openai_messages=prev_num_openai_messages,
|
|
2872
|
+
)
|
|
2926
2873
|
|
|
2927
2874
|
prev_num_openai_messages = len(openai_messages)
|
|
2928
2875
|
iteration_count += 1
|
|
@@ -2932,6 +2879,9 @@ class ChatAgent(BaseAgent):
|
|
|
2932
2879
|
step_token_usage, response.usage_dict
|
|
2933
2880
|
)
|
|
2934
2881
|
|
|
2882
|
+
# Update token cache from LLM response
|
|
2883
|
+
self._update_token_cache(response.usage_dict, len(openai_messages))
|
|
2884
|
+
|
|
2935
2885
|
# Terminate Agent if stop_event is set
|
|
2936
2886
|
if self.stop_event and self.stop_event.is_set():
|
|
2937
2887
|
# Use the _step_terminate to terminate the agent with reason
|
|
@@ -2981,6 +2931,43 @@ class ChatAgent(BaseAgent):
|
|
|
2981
2931
|
# If we're still here, continue the loop
|
|
2982
2932
|
continue
|
|
2983
2933
|
|
|
2934
|
+
# No tool calls - check if we should terminate based on terminators
|
|
2935
|
+
if self.response_terminators:
|
|
2936
|
+
# Check terminators to see if task is complete
|
|
2937
|
+
termination_results = [
|
|
2938
|
+
terminator.is_terminated(response.output_messages)
|
|
2939
|
+
for terminator in self.response_terminators
|
|
2940
|
+
]
|
|
2941
|
+
should_terminate = any(
|
|
2942
|
+
terminated for terminated, _ in termination_results
|
|
2943
|
+
)
|
|
2944
|
+
|
|
2945
|
+
if should_terminate:
|
|
2946
|
+
# Task is complete, exit the loop
|
|
2947
|
+
break
|
|
2948
|
+
|
|
2949
|
+
# Task not complete - prompt the model to continue
|
|
2950
|
+
if (
|
|
2951
|
+
self.max_iteration is not None
|
|
2952
|
+
and iteration_count >= self.max_iteration
|
|
2953
|
+
):
|
|
2954
|
+
logger.warning(
|
|
2955
|
+
f"Max iteration {self.max_iteration} reached without "
|
|
2956
|
+
"termination signal"
|
|
2957
|
+
)
|
|
2958
|
+
break
|
|
2959
|
+
|
|
2960
|
+
# Add a continuation prompt to memory as a user message
|
|
2961
|
+
continue_message = BaseMessage(
|
|
2962
|
+
role_name="user",
|
|
2963
|
+
role_type=RoleType.USER,
|
|
2964
|
+
content="Please continue.",
|
|
2965
|
+
meta_dict={},
|
|
2966
|
+
)
|
|
2967
|
+
self.update_memory(continue_message, OpenAIBackendRole.USER)
|
|
2968
|
+
continue
|
|
2969
|
+
|
|
2970
|
+
# No terminators configured, use original behavior
|
|
2984
2971
|
break
|
|
2985
2972
|
|
|
2986
2973
|
self._format_response_if_needed(response, response_format)
|
|
@@ -3044,6 +3031,10 @@ class ChatAgent(BaseAgent):
|
|
|
3044
3031
|
asyncio.TimeoutError: If the step operation exceeds the configured
|
|
3045
3032
|
timeout.
|
|
3046
3033
|
"""
|
|
3034
|
+
# Set agent_id in context-local storage for logging
|
|
3035
|
+
from camel.utils.agent_context import set_current_agent_id
|
|
3036
|
+
|
|
3037
|
+
set_current_agent_id(self.agent_id)
|
|
3047
3038
|
|
|
3048
3039
|
try:
|
|
3049
3040
|
from camel.utils.langfuse import set_current_agent_session_id
|
|
@@ -3081,6 +3072,10 @@ class ChatAgent(BaseAgent):
|
|
|
3081
3072
|
response_format: Optional[Type[BaseModel]] = None,
|
|
3082
3073
|
) -> ChatAgentResponse:
|
|
3083
3074
|
r"""Internal async method for non-streaming astep logic."""
|
|
3075
|
+
# Set agent_id in context-local storage for logging
|
|
3076
|
+
from camel.utils.agent_context import set_current_agent_id
|
|
3077
|
+
|
|
3078
|
+
set_current_agent_id(self.agent_id)
|
|
3084
3079
|
|
|
3085
3080
|
try:
|
|
3086
3081
|
from camel.utils.langfuse import set_current_agent_session_id
|
|
@@ -3128,128 +3123,25 @@ class ChatAgent(BaseAgent):
|
|
|
3128
3123
|
loop = asyncio.get_event_loop()
|
|
3129
3124
|
await loop.run_in_executor(None, self.pause_event.wait)
|
|
3130
3125
|
try:
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
token_limit = self.model_backend.token_limit
|
|
3136
|
-
|
|
3137
|
-
if num_tokens <= token_limit:
|
|
3138
|
-
if (
|
|
3139
|
-
summary_token_count
|
|
3140
|
-
> token_limit * self.summary_window_ratio
|
|
3141
|
-
):
|
|
3142
|
-
logger.info(
|
|
3143
|
-
f"Summary tokens ({summary_token_count}) "
|
|
3144
|
-
f"exceed limit, full compression."
|
|
3145
|
-
)
|
|
3146
|
-
# Summarize everything (including summaries)
|
|
3147
|
-
summary = await self.asummarize(
|
|
3148
|
-
include_summaries=True
|
|
3149
|
-
)
|
|
3150
|
-
self._update_memory_with_summary(
|
|
3151
|
-
summary.get("summary", ""),
|
|
3152
|
-
include_summaries=True,
|
|
3153
|
-
)
|
|
3154
|
-
elif num_tokens > threshold:
|
|
3155
|
-
logger.info(
|
|
3156
|
-
f"Token count ({num_tokens}) exceed threshold "
|
|
3157
|
-
"({threshold}). Triggering summarization."
|
|
3158
|
-
)
|
|
3159
|
-
# Only summarize non-summary content
|
|
3160
|
-
summary = await self.asummarize(
|
|
3161
|
-
include_summaries=False
|
|
3162
|
-
)
|
|
3163
|
-
self._update_memory_with_summary(
|
|
3164
|
-
summary.get("summary", ""),
|
|
3165
|
-
include_summaries=False,
|
|
3166
|
-
)
|
|
3126
|
+
(
|
|
3127
|
+
openai_messages,
|
|
3128
|
+
num_tokens,
|
|
3129
|
+
) = await self._get_context_with_summarization_async()
|
|
3167
3130
|
accumulated_context_tokens += num_tokens
|
|
3168
3131
|
except RuntimeError as e:
|
|
3169
3132
|
return self._step_terminate(
|
|
3170
3133
|
e.args[1], tool_call_records, "max_tokens_exceeded"
|
|
3171
3134
|
)
|
|
3172
|
-
# Get response from model backend
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
prev_num_openai_messages=prev_num_openai_messages,
|
|
3183
|
-
)
|
|
3184
|
-
except Exception as exc:
|
|
3185
|
-
logger.exception("Model error: %s", exc)
|
|
3186
|
-
|
|
3187
|
-
if self._is_token_limit_error(exc):
|
|
3188
|
-
tool_signature = self._last_tool_call_signature
|
|
3189
|
-
if (
|
|
3190
|
-
tool_signature is not None
|
|
3191
|
-
and tool_signature
|
|
3192
|
-
== self._last_token_limit_tool_signature
|
|
3193
|
-
):
|
|
3194
|
-
description = self._describe_tool_call(
|
|
3195
|
-
self._last_tool_call_record
|
|
3196
|
-
)
|
|
3197
|
-
repeated_msg = (
|
|
3198
|
-
"Context exceeded again by the same tool call."
|
|
3199
|
-
)
|
|
3200
|
-
if description:
|
|
3201
|
-
repeated_msg += f" {description}"
|
|
3202
|
-
raise RuntimeError(repeated_msg) from exc
|
|
3203
|
-
|
|
3204
|
-
user_message_count = sum(
|
|
3205
|
-
1
|
|
3206
|
-
for msg in openai_messages
|
|
3207
|
-
if getattr(msg, "role", None) == "user"
|
|
3208
|
-
)
|
|
3209
|
-
if (
|
|
3210
|
-
user_message_count == 1
|
|
3211
|
-
and getattr(openai_messages[-1], "role", None)
|
|
3212
|
-
== "user"
|
|
3213
|
-
):
|
|
3214
|
-
raise RuntimeError(
|
|
3215
|
-
"The provided user input alone exceeds the"
|
|
3216
|
-
"context window. Please shorten the input."
|
|
3217
|
-
) from exc
|
|
3218
|
-
|
|
3219
|
-
logger.warning(
|
|
3220
|
-
"Token limit exceeded error detected. "
|
|
3221
|
-
"Summarizing context."
|
|
3222
|
-
)
|
|
3223
|
-
|
|
3224
|
-
recent_records: List[ContextRecord]
|
|
3225
|
-
try:
|
|
3226
|
-
recent_records = self.memory.retrieve()
|
|
3227
|
-
except Exception: # pragma: no cover - defensive guard
|
|
3228
|
-
recent_records = []
|
|
3229
|
-
|
|
3230
|
-
indices_to_remove = (
|
|
3231
|
-
self._find_indices_to_remove_for_last_tool_pair(
|
|
3232
|
-
recent_records
|
|
3233
|
-
)
|
|
3234
|
-
)
|
|
3235
|
-
self.memory.remove_records_by_indices(indices_to_remove)
|
|
3236
|
-
|
|
3237
|
-
summary = await self.asummarize()
|
|
3238
|
-
|
|
3239
|
-
tool_notice = self._format_tool_limit_notice()
|
|
3240
|
-
summary_messages = summary.get("summary", "")
|
|
3241
|
-
|
|
3242
|
-
if tool_notice:
|
|
3243
|
-
summary_messages += "\n\n" + tool_notice
|
|
3244
|
-
self._update_memory_with_summary(
|
|
3245
|
-
summary_messages, include_summaries=False
|
|
3246
|
-
)
|
|
3247
|
-
self._last_token_limit_tool_signature = tool_signature
|
|
3248
|
-
return await self._astep_non_streaming_task(
|
|
3249
|
-
input_message, response_format
|
|
3250
|
-
)
|
|
3251
|
-
|
|
3252
|
-
raise
|
|
3135
|
+
# Get response from model backend
|
|
3136
|
+
response = await self._aget_model_response(
|
|
3137
|
+
openai_messages,
|
|
3138
|
+
current_iteration=iteration_count,
|
|
3139
|
+
response_format=response_format,
|
|
3140
|
+
tool_schemas=[]
|
|
3141
|
+
if disable_tools
|
|
3142
|
+
else self._get_full_tool_schemas(),
|
|
3143
|
+
prev_num_openai_messages=prev_num_openai_messages,
|
|
3144
|
+
)
|
|
3253
3145
|
|
|
3254
3146
|
prev_num_openai_messages = len(openai_messages)
|
|
3255
3147
|
iteration_count += 1
|
|
@@ -3259,6 +3151,9 @@ class ChatAgent(BaseAgent):
|
|
|
3259
3151
|
step_token_usage, response.usage_dict
|
|
3260
3152
|
)
|
|
3261
3153
|
|
|
3154
|
+
# Update token cache from LLM response
|
|
3155
|
+
self._update_token_cache(response.usage_dict, len(openai_messages))
|
|
3156
|
+
|
|
3262
3157
|
# Terminate Agent if stop_event is set
|
|
3263
3158
|
if self.stop_event and self.stop_event.is_set():
|
|
3264
3159
|
# Use the _step_terminate to terminate the agent with reason
|
|
@@ -3311,6 +3206,43 @@ class ChatAgent(BaseAgent):
|
|
|
3311
3206
|
# If we're still here, continue the loop
|
|
3312
3207
|
continue
|
|
3313
3208
|
|
|
3209
|
+
# No tool calls - check if we should terminate based on terminators
|
|
3210
|
+
if self.response_terminators:
|
|
3211
|
+
# Check terminators to see if task is complete
|
|
3212
|
+
termination_results = [
|
|
3213
|
+
terminator.is_terminated(response.output_messages)
|
|
3214
|
+
for terminator in self.response_terminators
|
|
3215
|
+
]
|
|
3216
|
+
should_terminate = any(
|
|
3217
|
+
terminated for terminated, _ in termination_results
|
|
3218
|
+
)
|
|
3219
|
+
|
|
3220
|
+
if should_terminate:
|
|
3221
|
+
# Task is complete, exit the loop
|
|
3222
|
+
break
|
|
3223
|
+
|
|
3224
|
+
# Task not complete - prompt the model to continue
|
|
3225
|
+
if (
|
|
3226
|
+
self.max_iteration is not None
|
|
3227
|
+
and iteration_count >= self.max_iteration
|
|
3228
|
+
):
|
|
3229
|
+
logger.warning(
|
|
3230
|
+
f"Max iteration {self.max_iteration} reached without "
|
|
3231
|
+
"termination signal"
|
|
3232
|
+
)
|
|
3233
|
+
break
|
|
3234
|
+
|
|
3235
|
+
# Add a continuation prompt to memory as a user message
|
|
3236
|
+
continue_message = BaseMessage(
|
|
3237
|
+
role_name="user",
|
|
3238
|
+
role_type=RoleType.USER,
|
|
3239
|
+
content="Please continue.",
|
|
3240
|
+
meta_dict={},
|
|
3241
|
+
)
|
|
3242
|
+
self.update_memory(continue_message, OpenAIBackendRole.USER)
|
|
3243
|
+
continue
|
|
3244
|
+
|
|
3245
|
+
# No terminators configured, use original behavior
|
|
3314
3246
|
break
|
|
3315
3247
|
|
|
3316
3248
|
await self._aformat_response_if_needed(response, response_format)
|
|
@@ -3327,8 +3259,6 @@ class ChatAgent(BaseAgent):
|
|
|
3327
3259
|
if self.prune_tool_calls_from_memory and tool_call_records:
|
|
3328
3260
|
self.memory.clean_tool_calls()
|
|
3329
3261
|
|
|
3330
|
-
self._last_token_limit_user_signature = None
|
|
3331
|
-
|
|
3332
3262
|
return self._convert_to_chatagent_response(
|
|
3333
3263
|
response,
|
|
3334
3264
|
tool_call_records,
|
|
@@ -3356,9 +3286,11 @@ class ChatAgent(BaseAgent):
|
|
|
3356
3286
|
tracker (Dict[str, int]): The token usage tracker to update.
|
|
3357
3287
|
usage_dict (Dict[str, int]): The usage dictionary with new values.
|
|
3358
3288
|
"""
|
|
3359
|
-
tracker["prompt_tokens"] += usage_dict.get("prompt_tokens"
|
|
3360
|
-
tracker["completion_tokens"] +=
|
|
3361
|
-
|
|
3289
|
+
tracker["prompt_tokens"] += usage_dict.get("prompt_tokens") or 0
|
|
3290
|
+
tracker["completion_tokens"] += (
|
|
3291
|
+
usage_dict.get("completion_tokens") or 0
|
|
3292
|
+
)
|
|
3293
|
+
tracker["total_tokens"] += usage_dict.get("total_tokens") or 0
|
|
3362
3294
|
|
|
3363
3295
|
def _convert_to_chatagent_response(
|
|
3364
3296
|
self,
|
|
@@ -3398,17 +3330,21 @@ class ChatAgent(BaseAgent):
|
|
|
3398
3330
|
r"""Log final messages or warnings about multiple responses."""
|
|
3399
3331
|
if len(output_messages) == 1:
|
|
3400
3332
|
self.record_message(output_messages[0])
|
|
3333
|
+
elif len(output_messages) == 0:
|
|
3334
|
+
logger.warning(
|
|
3335
|
+
"No messages returned in `step()`. The model returned an "
|
|
3336
|
+
"empty response."
|
|
3337
|
+
)
|
|
3401
3338
|
else:
|
|
3402
3339
|
logger.warning(
|
|
3403
|
-
"
|
|
3404
|
-
"selected message manually using `record_message()`."
|
|
3340
|
+
f"{len(output_messages)} messages returned in `step()`. "
|
|
3341
|
+
"Record selected message manually using `record_message()`."
|
|
3405
3342
|
)
|
|
3406
3343
|
|
|
3407
3344
|
@observe()
|
|
3408
3345
|
def _get_model_response(
|
|
3409
3346
|
self,
|
|
3410
3347
|
openai_messages: List[OpenAIMessage],
|
|
3411
|
-
num_tokens: int,
|
|
3412
3348
|
current_iteration: int = 0,
|
|
3413
3349
|
response_format: Optional[Type[BaseModel]] = None,
|
|
3414
3350
|
tool_schemas: Optional[List[Dict[str, Any]]] = None,
|
|
@@ -3425,8 +3361,6 @@ class ChatAgent(BaseAgent):
|
|
|
3425
3361
|
if response:
|
|
3426
3362
|
break
|
|
3427
3363
|
except RateLimitError as e:
|
|
3428
|
-
if self._is_token_limit_error(e):
|
|
3429
|
-
raise
|
|
3430
3364
|
last_error = e
|
|
3431
3365
|
if attempt < self.retry_attempts - 1:
|
|
3432
3366
|
delay = min(self.retry_delay * (2**attempt), 60.0)
|
|
@@ -3473,7 +3407,6 @@ class ChatAgent(BaseAgent):
|
|
|
3473
3407
|
async def _aget_model_response(
|
|
3474
3408
|
self,
|
|
3475
3409
|
openai_messages: List[OpenAIMessage],
|
|
3476
|
-
num_tokens: int,
|
|
3477
3410
|
current_iteration: int = 0,
|
|
3478
3411
|
response_format: Optional[Type[BaseModel]] = None,
|
|
3479
3412
|
tool_schemas: Optional[List[Dict[str, Any]]] = None,
|
|
@@ -3490,8 +3423,6 @@ class ChatAgent(BaseAgent):
|
|
|
3490
3423
|
if response:
|
|
3491
3424
|
break
|
|
3492
3425
|
except RateLimitError as e:
|
|
3493
|
-
if self._is_token_limit_error(e):
|
|
3494
|
-
raise
|
|
3495
3426
|
last_error = e
|
|
3496
3427
|
if attempt < self.retry_attempts - 1:
|
|
3497
3428
|
delay = min(self.retry_delay * (2**attempt), 60.0)
|
|
@@ -3873,26 +3804,31 @@ class ChatAgent(BaseAgent):
|
|
|
3873
3804
|
func_name = tool_call_request.tool_name
|
|
3874
3805
|
args = tool_call_request.args
|
|
3875
3806
|
tool_call_id = tool_call_request.tool_call_id
|
|
3876
|
-
tool = self._internal_tools
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
self._secure_result_store[tool_call_id] = raw_result
|
|
3882
|
-
result = (
|
|
3883
|
-
"[The tool has been executed successfully, but the output"
|
|
3884
|
-
" from the tool is masked. You can move forward]"
|
|
3885
|
-
)
|
|
3886
|
-
mask_flag = True
|
|
3887
|
-
else:
|
|
3888
|
-
result = raw_result
|
|
3889
|
-
mask_flag = False
|
|
3890
|
-
except Exception as e:
|
|
3891
|
-
# Capture the error message to prevent framework crash
|
|
3892
|
-
error_msg = f"Error executing tool '{func_name}': {e!s}"
|
|
3807
|
+
tool = self._internal_tools.get(func_name)
|
|
3808
|
+
mask_flag = False
|
|
3809
|
+
|
|
3810
|
+
if tool is None:
|
|
3811
|
+
error_msg = f"Tool '{func_name}' not found in registered tools"
|
|
3893
3812
|
result = f"Tool execution failed: {error_msg}"
|
|
3894
|
-
|
|
3895
|
-
|
|
3813
|
+
logger.warning(error_msg)
|
|
3814
|
+
else:
|
|
3815
|
+
try:
|
|
3816
|
+
raw_result = tool(**args)
|
|
3817
|
+
if self.mask_tool_output:
|
|
3818
|
+
with self._secure_result_store_lock:
|
|
3819
|
+
self._secure_result_store[tool_call_id] = raw_result
|
|
3820
|
+
result = (
|
|
3821
|
+
"[The tool has been executed successfully, but the "
|
|
3822
|
+
"output from the tool is masked. You can move forward]"
|
|
3823
|
+
)
|
|
3824
|
+
mask_flag = True
|
|
3825
|
+
else:
|
|
3826
|
+
result = raw_result
|
|
3827
|
+
except Exception as e:
|
|
3828
|
+
# Capture the error message to prevent framework crash
|
|
3829
|
+
error_msg = f"Error executing tool '{func_name}': {e!s}"
|
|
3830
|
+
result = f"Tool execution failed: {error_msg}"
|
|
3831
|
+
logger.warning(f"{error_msg} with result: {result}")
|
|
3896
3832
|
|
|
3897
3833
|
return self._record_tool_calling(
|
|
3898
3834
|
func_name,
|
|
@@ -3907,50 +3843,69 @@ class ChatAgent(BaseAgent):
|
|
|
3907
3843
|
self,
|
|
3908
3844
|
tool_call_request: ToolCallRequest,
|
|
3909
3845
|
) -> ToolCallingRecord:
|
|
3846
|
+
import asyncio
|
|
3847
|
+
|
|
3910
3848
|
func_name = tool_call_request.tool_name
|
|
3911
3849
|
args = tool_call_request.args
|
|
3912
3850
|
tool_call_id = tool_call_request.tool_call_id
|
|
3913
|
-
tool = self._internal_tools
|
|
3914
|
-
|
|
3851
|
+
tool = self._internal_tools.get(func_name)
|
|
3852
|
+
mask_flag = False
|
|
3915
3853
|
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3854
|
+
if tool is None:
|
|
3855
|
+
error_msg = f"Tool '{func_name}' not found in registered tools"
|
|
3856
|
+
result = f"Tool execution failed: {error_msg}"
|
|
3857
|
+
logger.warning(error_msg)
|
|
3858
|
+
else:
|
|
3859
|
+
try:
|
|
3860
|
+
# Try different invocation paths in order of preference
|
|
3861
|
+
if hasattr(tool, 'func') and hasattr(tool.func, 'async_call'):
|
|
3862
|
+
# Case: FunctionTool wrapping an MCP tool
|
|
3863
|
+
raw_result = await tool.func.async_call(**args)
|
|
3921
3864
|
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3865
|
+
elif hasattr(tool, 'async_call') and callable(tool.async_call):
|
|
3866
|
+
# Case: tool itself has async_call
|
|
3867
|
+
raw_result = await tool.async_call(**args)
|
|
3925
3868
|
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3869
|
+
elif hasattr(tool, 'func') and asyncio.iscoroutinefunction(
|
|
3870
|
+
tool.func
|
|
3871
|
+
):
|
|
3872
|
+
# Case: tool wraps a direct async function
|
|
3873
|
+
raw_result = await tool.func(**args)
|
|
3931
3874
|
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3875
|
+
elif asyncio.iscoroutinefunction(tool):
|
|
3876
|
+
# Case: tool is itself a coroutine function
|
|
3877
|
+
raw_result = await tool(**args)
|
|
3935
3878
|
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3879
|
+
else:
|
|
3880
|
+
# Fallback: synchronous call
|
|
3881
|
+
# Use functools.partial to properly capture args
|
|
3882
|
+
loop = asyncio.get_running_loop()
|
|
3883
|
+
raw_result = await loop.run_in_executor(
|
|
3884
|
+
None, functools.partial(tool, **args)
|
|
3885
|
+
)
|
|
3943
3886
|
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3887
|
+
if self.mask_tool_output:
|
|
3888
|
+
with self._secure_result_store_lock:
|
|
3889
|
+
self._secure_result_store[tool_call_id] = raw_result
|
|
3890
|
+
result = (
|
|
3891
|
+
"[The tool has been executed successfully, but the "
|
|
3892
|
+
"output from the tool is masked. You can move forward]"
|
|
3893
|
+
)
|
|
3894
|
+
mask_flag = True
|
|
3895
|
+
else:
|
|
3896
|
+
result = raw_result
|
|
3897
|
+
|
|
3898
|
+
except Exception as e:
|
|
3899
|
+
# Capture the error message to prevent framework crash
|
|
3900
|
+
error_msg = f"Error executing async tool '{func_name}': {e!s}"
|
|
3901
|
+
result = f"Tool execution failed: {error_msg}"
|
|
3902
|
+
logger.warning(f"{error_msg} with result: {result}")
|
|
3949
3903
|
return self._record_tool_calling(
|
|
3950
3904
|
func_name,
|
|
3951
3905
|
args,
|
|
3952
3906
|
result,
|
|
3953
3907
|
tool_call_id,
|
|
3908
|
+
mask_output=mask_flag,
|
|
3954
3909
|
extra_content=tool_call_request.extra_content,
|
|
3955
3910
|
)
|
|
3956
3911
|
|
|
@@ -3982,6 +3937,13 @@ class ChatAgent(BaseAgent):
|
|
|
3982
3937
|
ToolCallingRecord: A struct containing information about
|
|
3983
3938
|
this tool call.
|
|
3984
3939
|
"""
|
|
3940
|
+
# Truncate tool result if it exceeds the maximum token limit
|
|
3941
|
+
# This prevents single tool calls from exceeding context window
|
|
3942
|
+
truncated_result, was_truncated = self._truncate_tool_result(
|
|
3943
|
+
func_name, result
|
|
3944
|
+
)
|
|
3945
|
+
result_for_memory = truncated_result if was_truncated else result
|
|
3946
|
+
|
|
3985
3947
|
assist_msg = FunctionCallingMessage(
|
|
3986
3948
|
role_name=self.role_name,
|
|
3987
3949
|
role_type=self.role_type,
|
|
@@ -3998,7 +3960,7 @@ class ChatAgent(BaseAgent):
|
|
|
3998
3960
|
meta_dict=None,
|
|
3999
3961
|
content="",
|
|
4000
3962
|
func_name=func_name,
|
|
4001
|
-
result=
|
|
3963
|
+
result=result_for_memory,
|
|
4002
3964
|
tool_call_id=tool_call_id,
|
|
4003
3965
|
mask_output=mask_output,
|
|
4004
3966
|
extra_content=extra_content,
|
|
@@ -4028,7 +3990,7 @@ class ChatAgent(BaseAgent):
|
|
|
4028
3990
|
|
|
4029
3991
|
# Register tool output for snapshot cleaning if enabled
|
|
4030
3992
|
if self._enable_snapshot_clean and not mask_output and func_records:
|
|
4031
|
-
serialized_result = self._serialize_tool_result(
|
|
3993
|
+
serialized_result = self._serialize_tool_result(result_for_memory)
|
|
4032
3994
|
self._register_tool_output_for_cache(
|
|
4033
3995
|
func_name,
|
|
4034
3996
|
tool_call_id,
|
|
@@ -4036,14 +3998,74 @@ class ChatAgent(BaseAgent):
|
|
|
4036
3998
|
cast(List[MemoryRecord], func_records),
|
|
4037
3999
|
)
|
|
4038
4000
|
|
|
4001
|
+
if isinstance(result, ToolResult) and result.images:
|
|
4002
|
+
try:
|
|
4003
|
+
import base64
|
|
4004
|
+
import io
|
|
4005
|
+
|
|
4006
|
+
try:
|
|
4007
|
+
from PIL import Image
|
|
4008
|
+
except ImportError:
|
|
4009
|
+
logger.warning(
|
|
4010
|
+
f"Tool '{func_name}' returned images but PIL "
|
|
4011
|
+
"is not installed. Install with: pip install "
|
|
4012
|
+
"Pillow. Skipping visual context injection."
|
|
4013
|
+
)
|
|
4014
|
+
# Continue without injecting images
|
|
4015
|
+
result = (
|
|
4016
|
+
result.text if hasattr(result, 'text') else str(result)
|
|
4017
|
+
)
|
|
4018
|
+
else:
|
|
4019
|
+
logger.info(
|
|
4020
|
+
f"Tool '{func_name}' returned ToolResult with "
|
|
4021
|
+
f"{len(result.images)} image(s), injecting into "
|
|
4022
|
+
"context"
|
|
4023
|
+
)
|
|
4024
|
+
|
|
4025
|
+
# Convert base64 images to PIL Image objects
|
|
4026
|
+
pil_images: List[Union[Image.Image, str]] = []
|
|
4027
|
+
for img_data in result.images:
|
|
4028
|
+
if img_data.startswith('data:image/'):
|
|
4029
|
+
# Extract base64 data
|
|
4030
|
+
base64_str = img_data.split(',', 1)[1]
|
|
4031
|
+
img_bytes = base64.b64decode(base64_str)
|
|
4032
|
+
pil_img = Image.open(io.BytesIO(img_bytes))
|
|
4033
|
+
pil_images.append(pil_img)
|
|
4034
|
+
|
|
4035
|
+
if pil_images:
|
|
4036
|
+
# Create a user message with the image(s)
|
|
4037
|
+
visual_msg = BaseMessage.make_user_message(
|
|
4038
|
+
role_name="Tool",
|
|
4039
|
+
content=f"[Visual output from {func_name}]",
|
|
4040
|
+
image_list=pil_images,
|
|
4041
|
+
)
|
|
4042
|
+
|
|
4043
|
+
# Inject into conversation context with slight
|
|
4044
|
+
# timestamp increment
|
|
4045
|
+
self.update_memory(
|
|
4046
|
+
visual_msg,
|
|
4047
|
+
OpenAIBackendRole.USER,
|
|
4048
|
+
timestamp=base_timestamp + 2e-6,
|
|
4049
|
+
return_records=False,
|
|
4050
|
+
)
|
|
4051
|
+
logger.info(
|
|
4052
|
+
f"Successfully injected {len(pil_images)} "
|
|
4053
|
+
"image(s) into agent context"
|
|
4054
|
+
)
|
|
4055
|
+
except Exception as e:
|
|
4056
|
+
logger.error(
|
|
4057
|
+
f"Failed to inject visual content from {func_name}: {e}"
|
|
4058
|
+
)
|
|
4059
|
+
|
|
4039
4060
|
# Record information about this tool call
|
|
4061
|
+
# Note: tool_record contains the original result for the caller,
|
|
4062
|
+
# while result_for_memory (possibly truncated) is stored in memory
|
|
4040
4063
|
tool_record = ToolCallingRecord(
|
|
4041
4064
|
tool_name=func_name,
|
|
4042
4065
|
args=args,
|
|
4043
4066
|
result=result,
|
|
4044
4067
|
tool_call_id=tool_call_id,
|
|
4045
4068
|
)
|
|
4046
|
-
|
|
4047
4069
|
self._update_last_tool_call_state(tool_record)
|
|
4048
4070
|
return tool_record
|
|
4049
4071
|
|
|
@@ -4077,7 +4099,9 @@ class ChatAgent(BaseAgent):
|
|
|
4077
4099
|
|
|
4078
4100
|
# Get context for streaming
|
|
4079
4101
|
try:
|
|
4080
|
-
openai_messages, num_tokens =
|
|
4102
|
+
openai_messages, num_tokens = (
|
|
4103
|
+
self._get_context_with_summarization()
|
|
4104
|
+
)
|
|
4081
4105
|
except RuntimeError as e:
|
|
4082
4106
|
yield self._step_terminate(e.args[1], [], "max_tokens_exceeded")
|
|
4083
4107
|
return
|
|
@@ -4090,9 +4114,36 @@ class ChatAgent(BaseAgent):
|
|
|
4090
4114
|
def _get_token_count(self, content: str) -> int:
|
|
4091
4115
|
r"""Get token count for content with fallback."""
|
|
4092
4116
|
if hasattr(self.model_backend, 'token_counter'):
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4117
|
+
try:
|
|
4118
|
+
return len(self.model_backend.token_counter.encode(content))
|
|
4119
|
+
except BaseException as e:
|
|
4120
|
+
logger.debug(
|
|
4121
|
+
f"Token counting failed, using char fallback: {e}"
|
|
4122
|
+
)
|
|
4123
|
+
# Conservative estimate: ~3 chars per token
|
|
4124
|
+
return len(content) // 3
|
|
4125
|
+
|
|
4126
|
+
def _warn_stream_accumulate_deprecation(self) -> None:
|
|
4127
|
+
r"""Issue deprecation warning for stream_accumulate default change.
|
|
4128
|
+
|
|
4129
|
+
Only warns once per agent instance, and only if the user didn't
|
|
4130
|
+
explicitly set stream_accumulate.
|
|
4131
|
+
"""
|
|
4132
|
+
if not self._stream_accumulate_explicit:
|
|
4133
|
+
import warnings
|
|
4134
|
+
|
|
4135
|
+
warnings.warn(
|
|
4136
|
+
"The default value of 'stream_accumulate' has changed from "
|
|
4137
|
+
"True to False. In streaming mode, each chunk now returns "
|
|
4138
|
+
"only the incremental delta instead of accumulated content. "
|
|
4139
|
+
"To suppress this warning, explicitly set "
|
|
4140
|
+
"stream_accumulate=False (recommended) or stream_accumulate="
|
|
4141
|
+
"True if you need the old behavior.",
|
|
4142
|
+
DeprecationWarning,
|
|
4143
|
+
stacklevel=5,
|
|
4144
|
+
)
|
|
4145
|
+
# Only warn once per agent instance
|
|
4146
|
+
self._stream_accumulate_explicit = True
|
|
4096
4147
|
|
|
4097
4148
|
def _stream_response(
|
|
4098
4149
|
self,
|
|
@@ -4102,6 +4153,8 @@ class ChatAgent(BaseAgent):
|
|
|
4102
4153
|
) -> Generator[ChatAgentResponse, None, None]:
|
|
4103
4154
|
r"""Internal method to handle streaming responses with tool calls."""
|
|
4104
4155
|
|
|
4156
|
+
self._warn_stream_accumulate_deprecation()
|
|
4157
|
+
|
|
4105
4158
|
tool_call_records: List[ToolCallingRecord] = []
|
|
4106
4159
|
accumulated_tool_calls: Dict[str, Any] = {}
|
|
4107
4160
|
step_token_usage = self._create_token_usage_tracker()
|
|
@@ -4136,12 +4189,22 @@ class ChatAgent(BaseAgent):
|
|
|
4136
4189
|
return
|
|
4137
4190
|
|
|
4138
4191
|
# Handle streaming response
|
|
4139
|
-
|
|
4192
|
+
# Check for Stream, generator, or third-party wrappers
|
|
4193
|
+
if (
|
|
4194
|
+
isinstance(response, Stream)
|
|
4195
|
+
or inspect.isgenerator(response)
|
|
4196
|
+
or (
|
|
4197
|
+
hasattr(response, '__iter__')
|
|
4198
|
+
and hasattr(response, '__enter__')
|
|
4199
|
+
and not hasattr(response, 'get_final_completion')
|
|
4200
|
+
and not isinstance(response, ChatCompletion)
|
|
4201
|
+
)
|
|
4202
|
+
):
|
|
4140
4203
|
(
|
|
4141
4204
|
stream_completed,
|
|
4142
4205
|
tool_calls_complete,
|
|
4143
4206
|
) = yield from self._process_stream_chunks_with_accumulator(
|
|
4144
|
-
response,
|
|
4207
|
+
response, # type: ignore[arg-type]
|
|
4145
4208
|
content_accumulator,
|
|
4146
4209
|
accumulated_tool_calls,
|
|
4147
4210
|
tool_call_records,
|
|
@@ -4180,11 +4243,9 @@ class ChatAgent(BaseAgent):
|
|
|
4180
4243
|
# Stream completed without tool calls
|
|
4181
4244
|
accumulated_tool_calls.clear()
|
|
4182
4245
|
break
|
|
4183
|
-
elif hasattr(response, '
|
|
4184
|
-
response, '__exit__'
|
|
4185
|
-
):
|
|
4246
|
+
elif hasattr(response, 'get_final_completion'):
|
|
4186
4247
|
# Handle structured output stream (ChatCompletionStreamManager)
|
|
4187
|
-
with response as stream:
|
|
4248
|
+
with response as stream: # type: ignore[union-attr]
|
|
4188
4249
|
parsed_object = None
|
|
4189
4250
|
|
|
4190
4251
|
for event in stream:
|
|
@@ -4273,7 +4334,9 @@ class ChatAgent(BaseAgent):
|
|
|
4273
4334
|
return
|
|
4274
4335
|
else:
|
|
4275
4336
|
# Handle non-streaming response (fallback)
|
|
4276
|
-
model_response = self._handle_batch_response(
|
|
4337
|
+
model_response = self._handle_batch_response(
|
|
4338
|
+
response # type: ignore[arg-type]
|
|
4339
|
+
)
|
|
4277
4340
|
yield self._convert_to_chatagent_response(
|
|
4278
4341
|
model_response,
|
|
4279
4342
|
tool_call_records,
|
|
@@ -4410,12 +4473,20 @@ class ChatAgent(BaseAgent):
|
|
|
4410
4473
|
content_accumulator.get_full_reasoning_content()
|
|
4411
4474
|
or None
|
|
4412
4475
|
)
|
|
4476
|
+
# In delta mode, final response content should be empty
|
|
4477
|
+
# since all content was already yielded incrementally
|
|
4478
|
+
display_content = (
|
|
4479
|
+
final_content if self.stream_accumulate else ""
|
|
4480
|
+
)
|
|
4481
|
+
display_reasoning = (
|
|
4482
|
+
final_reasoning if self.stream_accumulate else None
|
|
4483
|
+
)
|
|
4413
4484
|
final_message = BaseMessage(
|
|
4414
4485
|
role_name=self.role_name,
|
|
4415
4486
|
role_type=self.role_type,
|
|
4416
4487
|
meta_dict={},
|
|
4417
|
-
content=
|
|
4418
|
-
reasoning_content=
|
|
4488
|
+
content=display_content,
|
|
4489
|
+
reasoning_content=display_reasoning,
|
|
4419
4490
|
)
|
|
4420
4491
|
|
|
4421
4492
|
if response_format:
|
|
@@ -4466,13 +4537,52 @@ class ChatAgent(BaseAgent):
|
|
|
4466
4537
|
bool: True if any tool call is complete, False otherwise.
|
|
4467
4538
|
"""
|
|
4468
4539
|
|
|
4540
|
+
index_map_key = '_index_to_key_map'
|
|
4541
|
+
if index_map_key not in accumulated_tool_calls:
|
|
4542
|
+
accumulated_tool_calls[index_map_key] = {}
|
|
4543
|
+
index_map = accumulated_tool_calls[index_map_key]
|
|
4544
|
+
|
|
4469
4545
|
for delta_tool_call in tool_call_deltas:
|
|
4470
|
-
index = delta_tool_call
|
|
4546
|
+
index = getattr(delta_tool_call, 'index', None)
|
|
4471
4547
|
tool_call_id = getattr(delta_tool_call, 'id', None)
|
|
4472
4548
|
|
|
4549
|
+
# Determine entry key
|
|
4550
|
+
if index is not None:
|
|
4551
|
+
index_str = str(index)
|
|
4552
|
+
if tool_call_id:
|
|
4553
|
+
# New ID provided: check if it differs from current mapping
|
|
4554
|
+
current_key = index_map.get(index_str)
|
|
4555
|
+
if current_key is None:
|
|
4556
|
+
# First time seeing this index, use tool_call_id as key
|
|
4557
|
+
entry_key = tool_call_id
|
|
4558
|
+
elif current_key in accumulated_tool_calls:
|
|
4559
|
+
existing_id = accumulated_tool_calls[current_key].get(
|
|
4560
|
+
'id'
|
|
4561
|
+
)
|
|
4562
|
+
if existing_id and existing_id != tool_call_id:
|
|
4563
|
+
# ID changed: use new ID as key
|
|
4564
|
+
entry_key = tool_call_id
|
|
4565
|
+
else:
|
|
4566
|
+
# No existing ID or same ID: keep current key
|
|
4567
|
+
entry_key = current_key
|
|
4568
|
+
else:
|
|
4569
|
+
entry_key = current_key
|
|
4570
|
+
# Update mapping
|
|
4571
|
+
index_map[index_str] = entry_key
|
|
4572
|
+
else:
|
|
4573
|
+
# No ID in this chunk: use existing mapping or index as
|
|
4574
|
+
# string
|
|
4575
|
+
entry_key = index_map.get(index_str, index_str)
|
|
4576
|
+
if index_str not in index_map:
|
|
4577
|
+
index_map[index_str] = entry_key
|
|
4578
|
+
elif tool_call_id is not None:
|
|
4579
|
+
entry_key = tool_call_id
|
|
4580
|
+
else:
|
|
4581
|
+
entry_key = '0' # Default fallback as string
|
|
4582
|
+
|
|
4473
4583
|
# Initialize tool call entry if not exists
|
|
4474
|
-
if
|
|
4475
|
-
accumulated_tool_calls[
|
|
4584
|
+
if entry_key not in accumulated_tool_calls:
|
|
4585
|
+
accumulated_tool_calls[entry_key] = {
|
|
4476
4586
|
'id': '',
|
|
4477
4587
|
'type': 'function',
|
|
4478
4588
|
'function': {'name': '', 'arguments': ''},
|
|
@@ -4480,7 +4590,7 @@ class ChatAgent(BaseAgent):
|
|
|
4480
4590
|
'complete': False,
|
|
4481
4591
|
}
|
|
4482
4592
|
|
|
4483
|
-
tool_call_entry = accumulated_tool_calls[
|
|
4593
|
+
tool_call_entry = accumulated_tool_calls[entry_key]
|
|
4484
4594
|
|
|
4485
4595
|
# Accumulate tool call data
|
|
4486
4596
|
if tool_call_id:
|
|
@@ -4512,6 +4622,9 @@ class ChatAgent(BaseAgent):
|
|
|
4512
4622
|
# Check if any tool calls are complete
|
|
4513
4623
|
any_complete = False
|
|
4514
4624
|
for _index, tool_call_entry in accumulated_tool_calls.items():
|
|
4625
|
+
# Skip internal mapping key
|
|
4626
|
+
if _index == '_index_to_key_map':
|
|
4627
|
+
continue
|
|
4515
4628
|
if (
|
|
4516
4629
|
tool_call_entry['id']
|
|
4517
4630
|
and tool_call_entry['function']['name']
|
|
@@ -4539,6 +4652,9 @@ class ChatAgent(BaseAgent):
|
|
|
4539
4652
|
|
|
4540
4653
|
tool_calls_to_execute = []
|
|
4541
4654
|
for _tool_call_index, tool_call_data in accumulated_tool_calls.items():
|
|
4655
|
+
# Skip internal mapping key
|
|
4656
|
+
if _tool_call_index == '_index_to_key_map':
|
|
4657
|
+
continue
|
|
4542
4658
|
if tool_call_data.get('complete', False):
|
|
4543
4659
|
tool_calls_to_execute.append(tool_call_data)
|
|
4544
4660
|
|
|
@@ -4618,6 +4734,27 @@ class ChatAgent(BaseAgent):
|
|
|
4618
4734
|
tool = self._internal_tools[function_name]
|
|
4619
4735
|
try:
|
|
4620
4736
|
result = tool(**args)
|
|
4737
|
+
|
|
4738
|
+
# Handle mask_tool_output
|
|
4739
|
+
if self.mask_tool_output:
|
|
4740
|
+
with self._secure_result_store_lock:
|
|
4741
|
+
self._secure_result_store[tool_call_id] = result
|
|
4742
|
+
result = (
|
|
4743
|
+
"[The tool has been executed successfully, but the"
|
|
4744
|
+
" output from the tool is masked. You can move"
|
|
4745
|
+
" forward]"
|
|
4746
|
+
)
|
|
4747
|
+
|
|
4748
|
+
# Truncate tool result if it exceeds the maximum token
|
|
4749
|
+
# limit. This prevents single tool calls from exceeding
|
|
4750
|
+
# context window
|
|
4751
|
+
truncated_result, was_truncated = (
|
|
4752
|
+
self._truncate_tool_result(function_name, result)
|
|
4753
|
+
)
|
|
4754
|
+
result_for_memory = (
|
|
4755
|
+
truncated_result if was_truncated else result
|
|
4756
|
+
)
|
|
4757
|
+
|
|
4621
4758
|
# First, create and record the assistant message with tool
|
|
4622
4759
|
# call
|
|
4623
4760
|
assist_msg = FunctionCallingMessage(
|
|
@@ -4638,8 +4775,9 @@ class ChatAgent(BaseAgent):
|
|
|
4638
4775
|
meta_dict=None,
|
|
4639
4776
|
content="",
|
|
4640
4777
|
func_name=function_name,
|
|
4641
|
-
result=
|
|
4778
|
+
result=result_for_memory,
|
|
4642
4779
|
tool_call_id=tool_call_id,
|
|
4780
|
+
mask_output=self.mask_tool_output,
|
|
4643
4781
|
extra_content=extra_content,
|
|
4644
4782
|
)
|
|
4645
4783
|
|
|
@@ -4675,7 +4813,7 @@ class ChatAgent(BaseAgent):
|
|
|
4675
4813
|
f"Error executing tool '{function_name}': {e!s}"
|
|
4676
4814
|
)
|
|
4677
4815
|
result = {"error": error_msg}
|
|
4678
|
-
logger.warning(error_msg)
|
|
4816
|
+
logger.warning(f"{error_msg} with result: {result}")
|
|
4679
4817
|
|
|
4680
4818
|
# Record error response
|
|
4681
4819
|
func_msg = FunctionCallingMessage(
|
|
@@ -4700,10 +4838,32 @@ class ChatAgent(BaseAgent):
|
|
|
4700
4838
|
self._update_last_tool_call_state(tool_record)
|
|
4701
4839
|
return tool_record
|
|
4702
4840
|
else:
|
|
4703
|
-
|
|
4704
|
-
f"Tool '{function_name}' not found in
|
|
4841
|
+
error_msg = (
|
|
4842
|
+
f"Tool '{function_name}' not found in registered tools"
|
|
4843
|
+
)
|
|
4844
|
+
result = {"error": error_msg}
|
|
4845
|
+
logger.warning(error_msg)
|
|
4846
|
+
|
|
4847
|
+
func_msg = FunctionCallingMessage(
|
|
4848
|
+
role_name=self.role_name,
|
|
4849
|
+
role_type=self.role_type,
|
|
4850
|
+
meta_dict=None,
|
|
4851
|
+
content="",
|
|
4852
|
+
func_name=function_name,
|
|
4853
|
+
result=result,
|
|
4854
|
+
tool_call_id=tool_call_id,
|
|
4855
|
+
extra_content=extra_content,
|
|
4705
4856
|
)
|
|
4706
|
-
|
|
4857
|
+
self.update_memory(func_msg, OpenAIBackendRole.FUNCTION)
|
|
4858
|
+
|
|
4859
|
+
tool_record = ToolCallingRecord(
|
|
4860
|
+
tool_name=function_name,
|
|
4861
|
+
args=args,
|
|
4862
|
+
result=result,
|
|
4863
|
+
tool_call_id=tool_call_id,
|
|
4864
|
+
)
|
|
4865
|
+
self._update_last_tool_call_state(tool_record)
|
|
4866
|
+
return tool_record
|
|
4707
4867
|
|
|
4708
4868
|
except Exception as e:
|
|
4709
4869
|
logger.error(f"Error processing tool call: {e}")
|
|
@@ -4772,6 +4932,26 @@ class ChatAgent(BaseAgent):
|
|
|
4772
4932
|
None, functools.partial(tool, **args)
|
|
4773
4933
|
)
|
|
4774
4934
|
|
|
4935
|
+
# Handle mask_tool_output
|
|
4936
|
+
if self.mask_tool_output:
|
|
4937
|
+
with self._secure_result_store_lock:
|
|
4938
|
+
self._secure_result_store[tool_call_id] = result
|
|
4939
|
+
result = (
|
|
4940
|
+
"[The tool has been executed successfully, but the"
|
|
4941
|
+
" output from the tool is masked. You can move"
|
|
4942
|
+
" forward]"
|
|
4943
|
+
)
|
|
4944
|
+
|
|
4945
|
+
# Truncate tool result if it exceeds the maximum token
|
|
4946
|
+
# limit. This prevents single tool calls from exceeding
|
|
4947
|
+
# context window
|
|
4948
|
+
truncated_result, was_truncated = (
|
|
4949
|
+
self._truncate_tool_result(function_name, result)
|
|
4950
|
+
)
|
|
4951
|
+
result_for_memory = (
|
|
4952
|
+
truncated_result if was_truncated else result
|
|
4953
|
+
)
|
|
4954
|
+
|
|
4775
4955
|
# Create the tool response message
|
|
4776
4956
|
func_msg = FunctionCallingMessage(
|
|
4777
4957
|
role_name=self.role_name,
|
|
@@ -4779,8 +4959,9 @@ class ChatAgent(BaseAgent):
|
|
|
4779
4959
|
meta_dict=None,
|
|
4780
4960
|
content="",
|
|
4781
4961
|
func_name=function_name,
|
|
4782
|
-
result=
|
|
4962
|
+
result=result_for_memory,
|
|
4783
4963
|
tool_call_id=tool_call_id,
|
|
4964
|
+
mask_output=self.mask_tool_output,
|
|
4784
4965
|
extra_content=extra_content,
|
|
4785
4966
|
)
|
|
4786
4967
|
func_ts = time.time_ns() / 1_000_000_000
|
|
@@ -4804,7 +4985,7 @@ class ChatAgent(BaseAgent):
|
|
|
4804
4985
|
f"Error executing async tool '{function_name}': {e!s}"
|
|
4805
4986
|
)
|
|
4806
4987
|
result = {"error": error_msg}
|
|
4807
|
-
logger.warning(error_msg)
|
|
4988
|
+
logger.warning(f"{error_msg} with result: {result}")
|
|
4808
4989
|
|
|
4809
4990
|
# Record error response
|
|
4810
4991
|
func_msg = FunctionCallingMessage(
|
|
@@ -4833,10 +5014,32 @@ class ChatAgent(BaseAgent):
|
|
|
4833
5014
|
self._update_last_tool_call_state(tool_record)
|
|
4834
5015
|
return tool_record
|
|
4835
5016
|
else:
|
|
4836
|
-
|
|
4837
|
-
f"Tool '{function_name}' not found in
|
|
5017
|
+
error_msg = (
|
|
5018
|
+
f"Tool '{function_name}' not found in registered tools"
|
|
5019
|
+
)
|
|
5020
|
+
result = {"error": error_msg}
|
|
5021
|
+
logger.warning(error_msg)
|
|
5022
|
+
|
|
5023
|
+
func_msg = FunctionCallingMessage(
|
|
5024
|
+
role_name=self.role_name,
|
|
5025
|
+
role_type=self.role_type,
|
|
5026
|
+
meta_dict=None,
|
|
5027
|
+
content="",
|
|
5028
|
+
func_name=function_name,
|
|
5029
|
+
result=result,
|
|
5030
|
+
tool_call_id=tool_call_id,
|
|
5031
|
+
extra_content=extra_content,
|
|
5032
|
+
)
|
|
5033
|
+
self.update_memory(func_msg, OpenAIBackendRole.FUNCTION)
|
|
5034
|
+
|
|
5035
|
+
tool_record = ToolCallingRecord(
|
|
5036
|
+
tool_name=function_name,
|
|
5037
|
+
args=args,
|
|
5038
|
+
result=result,
|
|
5039
|
+
tool_call_id=tool_call_id,
|
|
4838
5040
|
)
|
|
4839
|
-
|
|
5041
|
+
self._update_last_tool_call_state(tool_record)
|
|
5042
|
+
return tool_record
|
|
4840
5043
|
|
|
4841
5044
|
except Exception as e:
|
|
4842
5045
|
logger.error(f"Error processing async tool call: {e}")
|
|
@@ -4882,7 +5085,10 @@ class ChatAgent(BaseAgent):
|
|
|
4882
5085
|
|
|
4883
5086
|
# Get context for streaming
|
|
4884
5087
|
try:
|
|
4885
|
-
|
|
5088
|
+
(
|
|
5089
|
+
openai_messages,
|
|
5090
|
+
num_tokens,
|
|
5091
|
+
) = await self._get_context_with_summarization_async()
|
|
4886
5092
|
except RuntimeError as e:
|
|
4887
5093
|
yield self._step_terminate(e.args[1], [], "max_tokens_exceeded")
|
|
4888
5094
|
return
|
|
@@ -4910,6 +5116,8 @@ class ChatAgent(BaseAgent):
|
|
|
4910
5116
|
) -> AsyncGenerator[ChatAgentResponse, None]:
|
|
4911
5117
|
r"""Async method to handle streaming responses with tool calls."""
|
|
4912
5118
|
|
|
5119
|
+
self._warn_stream_accumulate_deprecation()
|
|
5120
|
+
|
|
4913
5121
|
tool_call_records: List[ToolCallingRecord] = []
|
|
4914
5122
|
accumulated_tool_calls: Dict[str, Any] = {}
|
|
4915
5123
|
step_token_usage = self._create_token_usage_tracker()
|
|
@@ -4945,11 +5153,16 @@ class ChatAgent(BaseAgent):
|
|
|
4945
5153
|
return
|
|
4946
5154
|
|
|
4947
5155
|
# Handle streaming response
|
|
4948
|
-
#
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
5156
|
+
# Check for AsyncStream, async generator, or third-party wrappers
|
|
5157
|
+
if (
|
|
5158
|
+
isinstance(response, AsyncStream)
|
|
5159
|
+
or inspect.isasyncgen(response)
|
|
5160
|
+
or (
|
|
5161
|
+
hasattr(response, '__aiter__')
|
|
5162
|
+
and hasattr(response, '__aenter__')
|
|
5163
|
+
and not hasattr(response, 'get_final_completion')
|
|
5164
|
+
and not isinstance(response, ChatCompletion)
|
|
5165
|
+
)
|
|
4953
5166
|
):
|
|
4954
5167
|
stream_completed = False
|
|
4955
5168
|
tool_calls_complete = False
|
|
@@ -4958,7 +5171,7 @@ class ChatAgent(BaseAgent):
|
|
|
4958
5171
|
async for (
|
|
4959
5172
|
item
|
|
4960
5173
|
) in self._aprocess_stream_chunks_with_accumulator(
|
|
4961
|
-
response,
|
|
5174
|
+
response, # type: ignore[arg-type]
|
|
4962
5175
|
content_accumulator,
|
|
4963
5176
|
accumulated_tool_calls,
|
|
4964
5177
|
tool_call_records,
|
|
@@ -5005,12 +5218,10 @@ class ChatAgent(BaseAgent):
|
|
|
5005
5218
|
# Stream completed without tool calls
|
|
5006
5219
|
accumulated_tool_calls.clear()
|
|
5007
5220
|
break
|
|
5008
|
-
elif hasattr(response, '
|
|
5009
|
-
response, '__aexit__'
|
|
5010
|
-
):
|
|
5221
|
+
elif hasattr(response, 'get_final_completion'):
|
|
5011
5222
|
# Handle structured output stream
|
|
5012
5223
|
# (AsyncChatCompletionStreamManager)
|
|
5013
|
-
async with response as stream:
|
|
5224
|
+
async with response as stream: # type: ignore[union-attr]
|
|
5014
5225
|
parsed_object = None
|
|
5015
5226
|
|
|
5016
5227
|
async for event in stream:
|
|
@@ -5101,7 +5312,9 @@ class ChatAgent(BaseAgent):
|
|
|
5101
5312
|
return
|
|
5102
5313
|
else:
|
|
5103
5314
|
# Handle non-streaming response (fallback)
|
|
5104
|
-
model_response = self._handle_batch_response(
|
|
5315
|
+
model_response = self._handle_batch_response(
|
|
5316
|
+
response # type: ignore[arg-type]
|
|
5317
|
+
)
|
|
5105
5318
|
yield self._convert_to_chatagent_response(
|
|
5106
5319
|
model_response,
|
|
5107
5320
|
tool_call_records,
|
|
@@ -5279,12 +5492,20 @@ class ChatAgent(BaseAgent):
|
|
|
5279
5492
|
content_accumulator.get_full_reasoning_content()
|
|
5280
5493
|
or None
|
|
5281
5494
|
)
|
|
5495
|
+
# In delta mode, final response content should be empty
|
|
5496
|
+
# since all content was already yielded incrementally
|
|
5497
|
+
display_content = (
|
|
5498
|
+
final_content if self.stream_accumulate else ""
|
|
5499
|
+
)
|
|
5500
|
+
display_reasoning = (
|
|
5501
|
+
final_reasoning if self.stream_accumulate else None
|
|
5502
|
+
)
|
|
5282
5503
|
final_message = BaseMessage(
|
|
5283
5504
|
role_name=self.role_name,
|
|
5284
5505
|
role_type=self.role_type,
|
|
5285
5506
|
meta_dict={},
|
|
5286
|
-
content=
|
|
5287
|
-
reasoning_content=
|
|
5507
|
+
content=display_content,
|
|
5508
|
+
reasoning_content=display_reasoning,
|
|
5288
5509
|
)
|
|
5289
5510
|
|
|
5290
5511
|
if response_format:
|
|
@@ -5332,6 +5553,9 @@ class ChatAgent(BaseAgent):
|
|
|
5332
5553
|
# statuses immediately
|
|
5333
5554
|
tool_tasks = []
|
|
5334
5555
|
for _tool_call_index, tool_call_data in accumulated_tool_calls.items():
|
|
5556
|
+
# Skip internal mapping key
|
|
5557
|
+
if _tool_call_index == '_index_to_key_map':
|
|
5558
|
+
continue
|
|
5335
5559
|
if tool_call_data.get('complete', False):
|
|
5336
5560
|
function_name = tool_call_data['function']['name']
|
|
5337
5561
|
try:
|