ag2 0.9.1a1__py3-none-any.whl → 0.9.2__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 ag2 might be problematic. Click here for more details.
- {ag2-0.9.1a1.dist-info → ag2-0.9.2.dist-info}/METADATA +272 -75
- ag2-0.9.2.dist-info/RECORD +406 -0
- {ag2-0.9.1a1.dist-info → ag2-0.9.2.dist-info}/WHEEL +1 -2
- autogen/__init__.py +89 -0
- autogen/_website/__init__.py +3 -0
- autogen/_website/generate_api_references.py +427 -0
- autogen/_website/generate_mkdocs.py +1174 -0
- autogen/_website/notebook_processor.py +476 -0
- autogen/_website/process_notebooks.py +656 -0
- autogen/_website/utils.py +412 -0
- autogen/agentchat/__init__.py +44 -0
- autogen/agentchat/agent.py +182 -0
- autogen/agentchat/assistant_agent.py +85 -0
- autogen/agentchat/chat.py +309 -0
- autogen/agentchat/contrib/__init__.py +5 -0
- autogen/agentchat/contrib/agent_eval/README.md +7 -0
- autogen/agentchat/contrib/agent_eval/agent_eval.py +108 -0
- autogen/agentchat/contrib/agent_eval/criterion.py +43 -0
- autogen/agentchat/contrib/agent_eval/critic_agent.py +44 -0
- autogen/agentchat/contrib/agent_eval/quantifier_agent.py +39 -0
- autogen/agentchat/contrib/agent_eval/subcritic_agent.py +45 -0
- autogen/agentchat/contrib/agent_eval/task.py +42 -0
- autogen/agentchat/contrib/agent_optimizer.py +429 -0
- autogen/agentchat/contrib/capabilities/__init__.py +5 -0
- autogen/agentchat/contrib/capabilities/agent_capability.py +20 -0
- autogen/agentchat/contrib/capabilities/generate_images.py +301 -0
- autogen/agentchat/contrib/capabilities/teachability.py +393 -0
- autogen/agentchat/contrib/capabilities/text_compressors.py +66 -0
- autogen/agentchat/contrib/capabilities/tools_capability.py +22 -0
- autogen/agentchat/contrib/capabilities/transform_messages.py +93 -0
- autogen/agentchat/contrib/capabilities/transforms.py +566 -0
- autogen/agentchat/contrib/capabilities/transforms_util.py +122 -0
- autogen/agentchat/contrib/capabilities/vision_capability.py +214 -0
- autogen/agentchat/contrib/captainagent/__init__.py +9 -0
- autogen/agentchat/contrib/captainagent/agent_builder.py +790 -0
- autogen/agentchat/contrib/captainagent/captainagent.py +512 -0
- autogen/agentchat/contrib/captainagent/tool_retriever.py +335 -0
- autogen/agentchat/contrib/captainagent/tools/README.md +44 -0
- autogen/agentchat/contrib/captainagent/tools/__init__.py +5 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/calculate_correlation.py +40 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/calculate_skewness_and_kurtosis.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/detect_outlier_iqr.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/detect_outlier_zscore.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/explore_csv.py +21 -0
- autogen/agentchat/contrib/captainagent/tools/data_analysis/shapiro_wilk_test.py +30 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/arxiv_download.py +27 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/arxiv_search.py +53 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/extract_pdf_image.py +53 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/extract_pdf_text.py +38 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/get_wikipedia_text.py +21 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/get_youtube_caption.py +34 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/image_qa.py +60 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/optical_character_recognition.py +61 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/perform_web_search.py +47 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/scrape_wikipedia_tables.py +33 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/transcribe_audio_file.py +21 -0
- autogen/agentchat/contrib/captainagent/tools/information_retrieval/youtube_download.py +35 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_circle_area_from_diameter.py +21 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_day_of_the_week.py +18 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_fraction_sum.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_matrix_power.py +31 -0
- autogen/agentchat/contrib/captainagent/tools/math/calculate_reflected_point.py +16 -0
- autogen/agentchat/contrib/captainagent/tools/math/complex_numbers_product.py +25 -0
- autogen/agentchat/contrib/captainagent/tools/math/compute_currency_conversion.py +23 -0
- autogen/agentchat/contrib/captainagent/tools/math/count_distinct_permutations.py +27 -0
- autogen/agentchat/contrib/captainagent/tools/math/evaluate_expression.py +28 -0
- autogen/agentchat/contrib/captainagent/tools/math/find_continuity_point.py +34 -0
- autogen/agentchat/contrib/captainagent/tools/math/fraction_to_mixed_numbers.py +39 -0
- autogen/agentchat/contrib/captainagent/tools/math/modular_inverse_sum.py +23 -0
- autogen/agentchat/contrib/captainagent/tools/math/simplify_mixed_numbers.py +36 -0
- autogen/agentchat/contrib/captainagent/tools/math/sum_of_digit_factorials.py +15 -0
- autogen/agentchat/contrib/captainagent/tools/math/sum_of_primes_below.py +15 -0
- autogen/agentchat/contrib/captainagent/tools/requirements.txt +10 -0
- autogen/agentchat/contrib/captainagent/tools/tool_description.tsv +34 -0
- autogen/agentchat/contrib/gpt_assistant_agent.py +526 -0
- autogen/agentchat/contrib/graph_rag/__init__.py +9 -0
- autogen/agentchat/contrib/graph_rag/document.py +29 -0
- autogen/agentchat/contrib/graph_rag/falkor_graph_query_engine.py +170 -0
- autogen/agentchat/contrib/graph_rag/falkor_graph_rag_capability.py +103 -0
- autogen/agentchat/contrib/graph_rag/graph_query_engine.py +53 -0
- autogen/agentchat/contrib/graph_rag/graph_rag_capability.py +63 -0
- autogen/agentchat/contrib/graph_rag/neo4j_graph_query_engine.py +268 -0
- autogen/agentchat/contrib/graph_rag/neo4j_graph_rag_capability.py +83 -0
- autogen/agentchat/contrib/graph_rag/neo4j_native_graph_query_engine.py +210 -0
- autogen/agentchat/contrib/graph_rag/neo4j_native_graph_rag_capability.py +93 -0
- autogen/agentchat/contrib/img_utils.py +397 -0
- autogen/agentchat/contrib/llamaindex_conversable_agent.py +117 -0
- autogen/agentchat/contrib/llava_agent.py +187 -0
- autogen/agentchat/contrib/math_user_proxy_agent.py +464 -0
- autogen/agentchat/contrib/multimodal_conversable_agent.py +125 -0
- autogen/agentchat/contrib/qdrant_retrieve_user_proxy_agent.py +324 -0
- autogen/agentchat/contrib/rag/__init__.py +10 -0
- autogen/agentchat/contrib/rag/chromadb_query_engine.py +272 -0
- autogen/agentchat/contrib/rag/llamaindex_query_engine.py +198 -0
- autogen/agentchat/contrib/rag/mongodb_query_engine.py +329 -0
- autogen/agentchat/contrib/rag/query_engine.py +74 -0
- autogen/agentchat/contrib/retrieve_assistant_agent.py +56 -0
- autogen/agentchat/contrib/retrieve_user_proxy_agent.py +703 -0
- autogen/agentchat/contrib/society_of_mind_agent.py +199 -0
- autogen/agentchat/contrib/swarm_agent.py +1425 -0
- autogen/agentchat/contrib/text_analyzer_agent.py +79 -0
- autogen/agentchat/contrib/vectordb/__init__.py +5 -0
- autogen/agentchat/contrib/vectordb/base.py +232 -0
- autogen/agentchat/contrib/vectordb/chromadb.py +315 -0
- autogen/agentchat/contrib/vectordb/couchbase.py +407 -0
- autogen/agentchat/contrib/vectordb/mongodb.py +550 -0
- autogen/agentchat/contrib/vectordb/pgvectordb.py +928 -0
- autogen/agentchat/contrib/vectordb/qdrant.py +320 -0
- autogen/agentchat/contrib/vectordb/utils.py +126 -0
- autogen/agentchat/contrib/web_surfer.py +303 -0
- autogen/agentchat/conversable_agent.py +4023 -0
- autogen/agentchat/group/__init__.py +64 -0
- autogen/agentchat/group/available_condition.py +91 -0
- autogen/agentchat/group/context_condition.py +77 -0
- autogen/agentchat/group/context_expression.py +238 -0
- autogen/agentchat/group/context_str.py +41 -0
- autogen/agentchat/group/context_variables.py +192 -0
- autogen/agentchat/group/group_tool_executor.py +202 -0
- autogen/agentchat/group/group_utils.py +591 -0
- autogen/agentchat/group/handoffs.py +244 -0
- autogen/agentchat/group/llm_condition.py +93 -0
- autogen/agentchat/group/multi_agent_chat.py +237 -0
- autogen/agentchat/group/on_condition.py +58 -0
- autogen/agentchat/group/on_context_condition.py +54 -0
- autogen/agentchat/group/patterns/__init__.py +18 -0
- autogen/agentchat/group/patterns/auto.py +159 -0
- autogen/agentchat/group/patterns/manual.py +176 -0
- autogen/agentchat/group/patterns/pattern.py +288 -0
- autogen/agentchat/group/patterns/random.py +106 -0
- autogen/agentchat/group/patterns/round_robin.py +117 -0
- autogen/agentchat/group/reply_result.py +26 -0
- autogen/agentchat/group/speaker_selection_result.py +41 -0
- autogen/agentchat/group/targets/__init__.py +4 -0
- autogen/agentchat/group/targets/group_chat_target.py +132 -0
- autogen/agentchat/group/targets/group_manager_target.py +151 -0
- autogen/agentchat/group/targets/transition_target.py +413 -0
- autogen/agentchat/group/targets/transition_utils.py +6 -0
- autogen/agentchat/groupchat.py +1694 -0
- autogen/agentchat/realtime/__init__.py +3 -0
- autogen/agentchat/realtime/experimental/__init__.py +20 -0
- autogen/agentchat/realtime/experimental/audio_adapters/__init__.py +8 -0
- autogen/agentchat/realtime/experimental/audio_adapters/twilio_audio_adapter.py +148 -0
- autogen/agentchat/realtime/experimental/audio_adapters/websocket_audio_adapter.py +139 -0
- autogen/agentchat/realtime/experimental/audio_observer.py +42 -0
- autogen/agentchat/realtime/experimental/clients/__init__.py +15 -0
- autogen/agentchat/realtime/experimental/clients/gemini/__init__.py +7 -0
- autogen/agentchat/realtime/experimental/clients/gemini/client.py +274 -0
- autogen/agentchat/realtime/experimental/clients/oai/__init__.py +8 -0
- autogen/agentchat/realtime/experimental/clients/oai/base_client.py +220 -0
- autogen/agentchat/realtime/experimental/clients/oai/rtc_client.py +243 -0
- autogen/agentchat/realtime/experimental/clients/oai/utils.py +48 -0
- autogen/agentchat/realtime/experimental/clients/realtime_client.py +190 -0
- autogen/agentchat/realtime/experimental/function_observer.py +85 -0
- autogen/agentchat/realtime/experimental/realtime_agent.py +158 -0
- autogen/agentchat/realtime/experimental/realtime_events.py +42 -0
- autogen/agentchat/realtime/experimental/realtime_observer.py +100 -0
- autogen/agentchat/realtime/experimental/realtime_swarm.py +475 -0
- autogen/agentchat/realtime/experimental/websockets.py +21 -0
- autogen/agentchat/realtime_agent/__init__.py +21 -0
- autogen/agentchat/user_proxy_agent.py +111 -0
- autogen/agentchat/utils.py +206 -0
- autogen/agents/__init__.py +3 -0
- autogen/agents/contrib/__init__.py +10 -0
- autogen/agents/contrib/time/__init__.py +8 -0
- autogen/agents/contrib/time/time_reply_agent.py +73 -0
- autogen/agents/contrib/time/time_tool_agent.py +51 -0
- autogen/agents/experimental/__init__.py +27 -0
- autogen/agents/experimental/deep_research/__init__.py +7 -0
- autogen/agents/experimental/deep_research/deep_research.py +52 -0
- autogen/agents/experimental/discord/__init__.py +7 -0
- autogen/agents/experimental/discord/discord.py +66 -0
- autogen/agents/experimental/document_agent/__init__.py +19 -0
- autogen/agents/experimental/document_agent/chroma_query_engine.py +316 -0
- autogen/agents/experimental/document_agent/docling_doc_ingest_agent.py +118 -0
- autogen/agents/experimental/document_agent/document_agent.py +461 -0
- autogen/agents/experimental/document_agent/document_conditions.py +50 -0
- autogen/agents/experimental/document_agent/document_utils.py +380 -0
- autogen/agents/experimental/document_agent/inmemory_query_engine.py +220 -0
- autogen/agents/experimental/document_agent/parser_utils.py +130 -0
- autogen/agents/experimental/document_agent/url_utils.py +426 -0
- autogen/agents/experimental/reasoning/__init__.py +7 -0
- autogen/agents/experimental/reasoning/reasoning_agent.py +1178 -0
- autogen/agents/experimental/slack/__init__.py +7 -0
- autogen/agents/experimental/slack/slack.py +73 -0
- autogen/agents/experimental/telegram/__init__.py +7 -0
- autogen/agents/experimental/telegram/telegram.py +77 -0
- autogen/agents/experimental/websurfer/__init__.py +7 -0
- autogen/agents/experimental/websurfer/websurfer.py +62 -0
- autogen/agents/experimental/wikipedia/__init__.py +7 -0
- autogen/agents/experimental/wikipedia/wikipedia.py +90 -0
- autogen/browser_utils.py +309 -0
- autogen/cache/__init__.py +10 -0
- autogen/cache/abstract_cache_base.py +75 -0
- autogen/cache/cache.py +203 -0
- autogen/cache/cache_factory.py +88 -0
- autogen/cache/cosmos_db_cache.py +144 -0
- autogen/cache/disk_cache.py +102 -0
- autogen/cache/in_memory_cache.py +58 -0
- autogen/cache/redis_cache.py +123 -0
- autogen/code_utils.py +596 -0
- autogen/coding/__init__.py +22 -0
- autogen/coding/base.py +119 -0
- autogen/coding/docker_commandline_code_executor.py +268 -0
- autogen/coding/factory.py +47 -0
- autogen/coding/func_with_reqs.py +202 -0
- autogen/coding/jupyter/__init__.py +23 -0
- autogen/coding/jupyter/base.py +36 -0
- autogen/coding/jupyter/docker_jupyter_server.py +167 -0
- autogen/coding/jupyter/embedded_ipython_code_executor.py +182 -0
- autogen/coding/jupyter/import_utils.py +82 -0
- autogen/coding/jupyter/jupyter_client.py +231 -0
- autogen/coding/jupyter/jupyter_code_executor.py +160 -0
- autogen/coding/jupyter/local_jupyter_server.py +172 -0
- autogen/coding/local_commandline_code_executor.py +405 -0
- autogen/coding/markdown_code_extractor.py +45 -0
- autogen/coding/utils.py +56 -0
- autogen/doc_utils.py +34 -0
- autogen/events/__init__.py +7 -0
- autogen/events/agent_events.py +1013 -0
- autogen/events/base_event.py +99 -0
- autogen/events/client_events.py +167 -0
- autogen/events/helpers.py +36 -0
- autogen/events/print_event.py +46 -0
- autogen/exception_utils.py +73 -0
- autogen/extensions/__init__.py +5 -0
- autogen/fast_depends/__init__.py +16 -0
- autogen/fast_depends/_compat.py +80 -0
- autogen/fast_depends/core/__init__.py +14 -0
- autogen/fast_depends/core/build.py +225 -0
- autogen/fast_depends/core/model.py +576 -0
- autogen/fast_depends/dependencies/__init__.py +15 -0
- autogen/fast_depends/dependencies/model.py +29 -0
- autogen/fast_depends/dependencies/provider.py +39 -0
- autogen/fast_depends/library/__init__.py +10 -0
- autogen/fast_depends/library/model.py +46 -0
- autogen/fast_depends/py.typed +6 -0
- autogen/fast_depends/schema.py +66 -0
- autogen/fast_depends/use.py +280 -0
- autogen/fast_depends/utils.py +187 -0
- autogen/formatting_utils.py +83 -0
- autogen/function_utils.py +13 -0
- autogen/graph_utils.py +178 -0
- autogen/import_utils.py +526 -0
- autogen/interop/__init__.py +22 -0
- autogen/interop/crewai/__init__.py +7 -0
- autogen/interop/crewai/crewai.py +88 -0
- autogen/interop/interoperability.py +71 -0
- autogen/interop/interoperable.py +46 -0
- autogen/interop/langchain/__init__.py +8 -0
- autogen/interop/langchain/langchain_chat_model_factory.py +155 -0
- autogen/interop/langchain/langchain_tool.py +82 -0
- autogen/interop/litellm/__init__.py +7 -0
- autogen/interop/litellm/litellm_config_factory.py +179 -0
- autogen/interop/pydantic_ai/__init__.py +7 -0
- autogen/interop/pydantic_ai/pydantic_ai.py +168 -0
- autogen/interop/registry.py +69 -0
- autogen/io/__init__.py +15 -0
- autogen/io/base.py +151 -0
- autogen/io/console.py +56 -0
- autogen/io/processors/__init__.py +12 -0
- autogen/io/processors/base.py +21 -0
- autogen/io/processors/console_event_processor.py +56 -0
- autogen/io/run_response.py +293 -0
- autogen/io/thread_io_stream.py +63 -0
- autogen/io/websockets.py +213 -0
- autogen/json_utils.py +43 -0
- autogen/llm_config.py +382 -0
- autogen/logger/__init__.py +11 -0
- autogen/logger/base_logger.py +128 -0
- autogen/logger/file_logger.py +261 -0
- autogen/logger/logger_factory.py +42 -0
- autogen/logger/logger_utils.py +57 -0
- autogen/logger/sqlite_logger.py +523 -0
- autogen/math_utils.py +339 -0
- autogen/mcp/__init__.py +7 -0
- autogen/mcp/__main__.py +78 -0
- autogen/mcp/mcp_client.py +208 -0
- autogen/mcp/mcp_proxy/__init__.py +19 -0
- autogen/mcp/mcp_proxy/fastapi_code_generator_helpers.py +63 -0
- autogen/mcp/mcp_proxy/mcp_proxy.py +581 -0
- autogen/mcp/mcp_proxy/operation_grouping.py +158 -0
- autogen/mcp/mcp_proxy/operation_renaming.py +114 -0
- autogen/mcp/mcp_proxy/patch_fastapi_code_generator.py +98 -0
- autogen/mcp/mcp_proxy/security.py +400 -0
- autogen/mcp/mcp_proxy/security_schema_visitor.py +37 -0
- autogen/messages/__init__.py +7 -0
- autogen/messages/agent_messages.py +948 -0
- autogen/messages/base_message.py +107 -0
- autogen/messages/client_messages.py +171 -0
- autogen/messages/print_message.py +49 -0
- autogen/oai/__init__.py +53 -0
- autogen/oai/anthropic.py +714 -0
- autogen/oai/bedrock.py +628 -0
- autogen/oai/cerebras.py +299 -0
- autogen/oai/client.py +1444 -0
- autogen/oai/client_utils.py +169 -0
- autogen/oai/cohere.py +479 -0
- autogen/oai/gemini.py +998 -0
- autogen/oai/gemini_types.py +155 -0
- autogen/oai/groq.py +305 -0
- autogen/oai/mistral.py +303 -0
- autogen/oai/oai_models/__init__.py +11 -0
- autogen/oai/oai_models/_models.py +16 -0
- autogen/oai/oai_models/chat_completion.py +87 -0
- autogen/oai/oai_models/chat_completion_audio.py +32 -0
- autogen/oai/oai_models/chat_completion_message.py +86 -0
- autogen/oai/oai_models/chat_completion_message_tool_call.py +37 -0
- autogen/oai/oai_models/chat_completion_token_logprob.py +63 -0
- autogen/oai/oai_models/completion_usage.py +60 -0
- autogen/oai/ollama.py +643 -0
- autogen/oai/openai_utils.py +881 -0
- autogen/oai/together.py +370 -0
- autogen/retrieve_utils.py +491 -0
- autogen/runtime_logging.py +160 -0
- autogen/token_count_utils.py +267 -0
- autogen/tools/__init__.py +20 -0
- autogen/tools/contrib/__init__.py +9 -0
- autogen/tools/contrib/time/__init__.py +7 -0
- autogen/tools/contrib/time/time.py +41 -0
- autogen/tools/dependency_injection.py +254 -0
- autogen/tools/experimental/__init__.py +48 -0
- autogen/tools/experimental/browser_use/__init__.py +7 -0
- autogen/tools/experimental/browser_use/browser_use.py +161 -0
- autogen/tools/experimental/crawl4ai/__init__.py +7 -0
- autogen/tools/experimental/crawl4ai/crawl4ai.py +153 -0
- autogen/tools/experimental/deep_research/__init__.py +7 -0
- autogen/tools/experimental/deep_research/deep_research.py +328 -0
- autogen/tools/experimental/duckduckgo/__init__.py +7 -0
- autogen/tools/experimental/duckduckgo/duckduckgo_search.py +109 -0
- autogen/tools/experimental/google/__init__.py +14 -0
- autogen/tools/experimental/google/authentication/__init__.py +11 -0
- autogen/tools/experimental/google/authentication/credentials_hosted_provider.py +43 -0
- autogen/tools/experimental/google/authentication/credentials_local_provider.py +91 -0
- autogen/tools/experimental/google/authentication/credentials_provider.py +35 -0
- autogen/tools/experimental/google/drive/__init__.py +9 -0
- autogen/tools/experimental/google/drive/drive_functions.py +124 -0
- autogen/tools/experimental/google/drive/toolkit.py +88 -0
- autogen/tools/experimental/google/model.py +17 -0
- autogen/tools/experimental/google/toolkit_protocol.py +19 -0
- autogen/tools/experimental/google_search/__init__.py +8 -0
- autogen/tools/experimental/google_search/google_search.py +93 -0
- autogen/tools/experimental/google_search/youtube_search.py +181 -0
- autogen/tools/experimental/messageplatform/__init__.py +17 -0
- autogen/tools/experimental/messageplatform/discord/__init__.py +7 -0
- autogen/tools/experimental/messageplatform/discord/discord.py +288 -0
- autogen/tools/experimental/messageplatform/slack/__init__.py +7 -0
- autogen/tools/experimental/messageplatform/slack/slack.py +391 -0
- autogen/tools/experimental/messageplatform/telegram/__init__.py +7 -0
- autogen/tools/experimental/messageplatform/telegram/telegram.py +275 -0
- autogen/tools/experimental/perplexity/__init__.py +7 -0
- autogen/tools/experimental/perplexity/perplexity_search.py +260 -0
- autogen/tools/experimental/reliable/__init__.py +10 -0
- autogen/tools/experimental/reliable/reliable.py +1316 -0
- autogen/tools/experimental/tavily/__init__.py +7 -0
- autogen/tools/experimental/tavily/tavily_search.py +183 -0
- autogen/tools/experimental/web_search_preview/__init__.py +7 -0
- autogen/tools/experimental/web_search_preview/web_search_preview.py +114 -0
- autogen/tools/experimental/wikipedia/__init__.py +7 -0
- autogen/tools/experimental/wikipedia/wikipedia.py +287 -0
- autogen/tools/function_utils.py +411 -0
- autogen/tools/tool.py +187 -0
- autogen/tools/toolkit.py +86 -0
- autogen/types.py +29 -0
- autogen/version.py +7 -0
- templates/client_template/main.jinja2 +69 -0
- templates/config_template/config.jinja2 +7 -0
- templates/main.jinja2 +61 -0
- ag2-0.9.1a1.dist-info/RECORD +0 -6
- ag2-0.9.1a1.dist-info/top_level.txt +0 -1
- {ag2-0.9.1a1.dist-info → ag2-0.9.2.dist-info/licenses}/LICENSE +0 -0
- {ag2-0.9.1a1.dist-info → ag2-0.9.2.dist-info/licenses}/NOTICE.md +0 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
# Portions derived from https://github.com/microsoft/autogen are under the MIT License.
|
|
6
|
+
# SPDX-License-Identifier: MIT
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import contextlib
|
|
10
|
+
import copy
|
|
11
|
+
import functools
|
|
12
|
+
import inspect
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
import warnings
|
|
17
|
+
from typing import Annotated, Any, Callable, Dict, List, Optional, Tuple, Type, Union
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
20
|
+
|
|
21
|
+
from ....agentchat import ChatResult, initiate_group_chat
|
|
22
|
+
from ....agentchat.agent import Agent
|
|
23
|
+
from ....agentchat.conversable_agent import ConversableAgent
|
|
24
|
+
from ....agentchat.group import AgentTarget, ReplyResult, TerminateTarget
|
|
25
|
+
from ....agentchat.group.context_variables import ContextVariables
|
|
26
|
+
from ....agentchat.group.patterns import DefaultPattern
|
|
27
|
+
from ....doc_utils import export_module
|
|
28
|
+
from ....llm_config import LLMConfig
|
|
29
|
+
from ....tools.dependency_injection import Field as AG2Field
|
|
30
|
+
from ....tools.tool import Tool
|
|
31
|
+
|
|
32
|
+
__all__ = ("ReliableTool", "ReliableToolError", "SuccessfulExecutionParameters", "ToolExecutionDetails")
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
HYPOTHESIS_DESCRIPTION = (
|
|
37
|
+
"A clear, concise statement about the expected outcome or result format of the function call "
|
|
38
|
+
"based on the provided inputs. This helps in assessing the relevance and potential success "
|
|
39
|
+
"of the call, and guides validation."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ValidationResult(BaseModel):
|
|
44
|
+
"""Represents the outcome of a single validation step."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(extra="forbid")
|
|
47
|
+
validation_result: bool
|
|
48
|
+
justification: str
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
status = "Passed" if self.validation_result else "Failed"
|
|
52
|
+
return f"Validation Result: {status}\nJustification: {self.justification}"
|
|
53
|
+
|
|
54
|
+
def format(self) -> str:
|
|
55
|
+
"""Returns the JSON representation for AutoGen compatibility."""
|
|
56
|
+
return self.model_dump_json()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ExecutionAttempt(BaseModel):
|
|
60
|
+
"""Stores the state of a single attempt to execute and validate the function."""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
63
|
+
timestamp: float = Field(default_factory=time.time)
|
|
64
|
+
attempt_args: List[Any] = Field(default_factory=list)
|
|
65
|
+
attempt_kwargs: Dict[str, Any] = Field(default_factory=dict)
|
|
66
|
+
hypothesis: Optional[str] = None
|
|
67
|
+
error: Optional[str] = None
|
|
68
|
+
result_data: Optional[Any] = None
|
|
69
|
+
result_str: Optional[str] = None
|
|
70
|
+
validation: Optional[ValidationResult] = None
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def did_execute_successfully(self) -> bool:
|
|
74
|
+
"""Check if the attempt executed without raising an error."""
|
|
75
|
+
return self.error is None
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def did_validate_successfully(self) -> bool:
|
|
79
|
+
"""Check if the attempt passed validation."""
|
|
80
|
+
return self.validation is not None and self.validation.validation_result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ReliableToolContext(BaseModel):
|
|
84
|
+
"""Main context object holding the overall state and history of attempts."""
|
|
85
|
+
|
|
86
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
87
|
+
task: str
|
|
88
|
+
reliable_tool_name: str
|
|
89
|
+
start_time: float = Field(default_factory=time.time)
|
|
90
|
+
dynamic_validation_input: Optional[str] = None
|
|
91
|
+
attempts: List[ExecutionAttempt] = Field(default_factory=list)
|
|
92
|
+
initial_messages: Optional[List[dict[str, Any]]] = Field(
|
|
93
|
+
default=None, description="Initial messages provided to the tool run."
|
|
94
|
+
)
|
|
95
|
+
initial_ground_truth: Optional[List[str]] = Field(
|
|
96
|
+
default=None, description="Initial ground truth strings provided."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def attempt_count(self) -> int:
|
|
101
|
+
"""Return the number of attempts made."""
|
|
102
|
+
return len(self.attempts)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def latest_attempt(self) -> Optional[ExecutionAttempt]:
|
|
106
|
+
"""Return the most recent attempt, if any."""
|
|
107
|
+
return self.attempts[-1] if self.attempts else None
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def is_complete_and_successful(self) -> bool:
|
|
111
|
+
"""Check if the process finished with a validated successful attempt."""
|
|
112
|
+
latest = self.latest_attempt
|
|
113
|
+
return latest is not None and latest.did_execute_successfully and latest.did_validate_successfully
|
|
114
|
+
|
|
115
|
+
def get_final_result_data(self) -> Any:
|
|
116
|
+
"""Return the result_data from the successful and validated attempt."""
|
|
117
|
+
if self.is_complete_and_successful and self.latest_attempt:
|
|
118
|
+
return self.latest_attempt.result_data
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def get_final_result_str(self) -> Any:
|
|
122
|
+
"""Return the result_str from the successful and validated attempt."""
|
|
123
|
+
if self.is_complete_and_successful and self.latest_attempt:
|
|
124
|
+
return self.latest_attempt.result_str
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
def get_failure_summary(self) -> str:
|
|
128
|
+
"""Provide a summary of why the overall execution failed."""
|
|
129
|
+
latest = self.latest_attempt
|
|
130
|
+
if latest is None:
|
|
131
|
+
return "No execution attempts were made."
|
|
132
|
+
if not latest.did_execute_successfully:
|
|
133
|
+
return f"Execution failed: {latest.error}"
|
|
134
|
+
if not latest.did_validate_successfully:
|
|
135
|
+
justification = (
|
|
136
|
+
latest.validation.justification if latest.validation else "Validation result missing or invalid"
|
|
137
|
+
)
|
|
138
|
+
return f"Execution succeeded but failed validation (Justification: {justification})"
|
|
139
|
+
return "Execution completed but overall status indicates failure (Internal inconsistency)."
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class SuccessfulExecutionParameters(BaseModel):
|
|
143
|
+
"""Holds the arguments of a successful tool function execution."""
|
|
144
|
+
|
|
145
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
146
|
+
attempt_args: List[Any]
|
|
147
|
+
attempt_kwargs: Dict[str, Any]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ToolExecutionDetails(BaseModel):
|
|
151
|
+
"""Provides detailed information about a ReliableTool execution."""
|
|
152
|
+
|
|
153
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
154
|
+
task: str
|
|
155
|
+
is_overall_successful: bool
|
|
156
|
+
failure_reason: Optional[str] = None
|
|
157
|
+
successful_parameters: Optional[SuccessfulExecutionParameters] = None
|
|
158
|
+
final_tool_context: ReliableToolContext
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _configure_llm_for_structured_output(
|
|
162
|
+
llm_config: Optional[Union[LLMConfig, dict[str, Any]]], structured_output_type: Type[BaseModel]
|
|
163
|
+
) -> Union[LLMConfig, dict[str, Any]]: # Return type changed, False is no longer a valid return
|
|
164
|
+
"""Configure LLM config for structured output using a Pydantic model."""
|
|
165
|
+
if llm_config is None or llm_config is False:
|
|
166
|
+
raise ValueError("LLMConfig cannot be None or False for structured output.")
|
|
167
|
+
if not issubclass(structured_output_type, BaseModel):
|
|
168
|
+
raise TypeError(f"{structured_output_type} must be a Pydantic BaseModel subclass.")
|
|
169
|
+
|
|
170
|
+
llm_config_obj = ConversableAgent._validate_llm_config(llm_config)
|
|
171
|
+
|
|
172
|
+
if llm_config_obj is False: # Should not happen if input llm_config is not False
|
|
173
|
+
raise ValueError("Validated LLMConfig resolved to False unexpectedly.")
|
|
174
|
+
|
|
175
|
+
response_format_set = False
|
|
176
|
+
|
|
177
|
+
def _set_format_and_remove_conflicts(config_item: Union[LLMConfig, Dict[str, Any]]) -> None:
|
|
178
|
+
nonlocal response_format_set
|
|
179
|
+
conflicting_keys = ["tools", "tool_choice", "functions"]
|
|
180
|
+
removed_keys = []
|
|
181
|
+
|
|
182
|
+
if isinstance(config_item, dict):
|
|
183
|
+
config_item["response_format"] = structured_output_type
|
|
184
|
+
response_format_set = True
|
|
185
|
+
for key in conflicting_keys:
|
|
186
|
+
if key in config_item:
|
|
187
|
+
del config_item[key]
|
|
188
|
+
removed_keys.append(key)
|
|
189
|
+
elif hasattr(config_item, "response_format"): # LLMConfig object
|
|
190
|
+
setattr(config_item, "response_format", structured_output_type)
|
|
191
|
+
response_format_set = True
|
|
192
|
+
for key in conflicting_keys:
|
|
193
|
+
if hasattr(config_item, key) and getattr(config_item, key, None):
|
|
194
|
+
# Try setting to None or empty list/dict as appropriate
|
|
195
|
+
default_empty: Optional[List[str]] = [] if key in ["tools", "functions"] else None
|
|
196
|
+
setattr(config_item, key, default_empty)
|
|
197
|
+
removed_keys.append(key)
|
|
198
|
+
else:
|
|
199
|
+
# This case implies llm_config_obj is an object not fitting LLMConfig ducktype for response_format
|
|
200
|
+
# or not a dict, which should be caught by _validate_llm_config or earlier checks.
|
|
201
|
+
raise TypeError(f"Unsupported LLM config item type for structured output: {type(config_item)}")
|
|
202
|
+
|
|
203
|
+
if removed_keys:
|
|
204
|
+
logger.debug(
|
|
205
|
+
"Removed conflicting keys %s from LLM config for structured output (response_format=%s)",
|
|
206
|
+
removed_keys,
|
|
207
|
+
structured_output_type.__name__,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
_set_format_and_remove_conflicts(llm_config_obj)
|
|
211
|
+
|
|
212
|
+
if not response_format_set and not isinstance(llm_config_obj, dict): # Double check if it's an object
|
|
213
|
+
# if it's an object and response_format could not be set, it's an issue.
|
|
214
|
+
# For dicts, it's assumed to be set by _set_format_and_remove_conflicts.
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"LLMConfig object type ({type(llm_config_obj).__name__}) "
|
|
217
|
+
"could not have 'response_format' set. Structured output may fail."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Handle config_list if present
|
|
221
|
+
config_list_attr_name = "config_list"
|
|
222
|
+
original_config_list = None
|
|
223
|
+
|
|
224
|
+
if isinstance(llm_config_obj, dict):
|
|
225
|
+
original_config_list = llm_config_obj.get(config_list_attr_name)
|
|
226
|
+
elif hasattr(llm_config_obj, config_list_attr_name):
|
|
227
|
+
original_config_list = getattr(llm_config_obj, config_list_attr_name, None)
|
|
228
|
+
|
|
229
|
+
if isinstance(original_config_list, list):
|
|
230
|
+
new_config_list = []
|
|
231
|
+
for item in original_config_list:
|
|
232
|
+
item_copy = copy.deepcopy(item)
|
|
233
|
+
# Assuming items in config_list are dicts or LLMConfig-like objects
|
|
234
|
+
_set_format_and_remove_conflicts(item_copy)
|
|
235
|
+
new_config_list.append(item_copy)
|
|
236
|
+
|
|
237
|
+
if isinstance(llm_config_obj, dict):
|
|
238
|
+
llm_config_obj[config_list_attr_name] = new_config_list
|
|
239
|
+
else: # Must be an object if hasattr was true
|
|
240
|
+
setattr(llm_config_obj, config_list_attr_name, new_config_list)
|
|
241
|
+
|
|
242
|
+
logger.debug("Prepared LLM config for validator (response_format=%s)", structured_output_type.__name__)
|
|
243
|
+
return llm_config_obj
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _get_last_non_empty_message_content(messages: Optional[List[dict[str, Any]]]) -> Optional[str]:
|
|
247
|
+
"""Get content of the last message with non-empty content."""
|
|
248
|
+
if not messages:
|
|
249
|
+
return None
|
|
250
|
+
for message in reversed(messages):
|
|
251
|
+
content = message.get("content")
|
|
252
|
+
if isinstance(content, str) and content.strip():
|
|
253
|
+
return content.strip()
|
|
254
|
+
if isinstance(content, list) and content: # Handle multimodal content
|
|
255
|
+
# Prioritize text parts
|
|
256
|
+
text_parts = [
|
|
257
|
+
item["text"].strip()
|
|
258
|
+
for item in content
|
|
259
|
+
if isinstance(item, dict)
|
|
260
|
+
and item.get("type") == "text"
|
|
261
|
+
and isinstance(item.get("text"), str)
|
|
262
|
+
and item["text"].strip()
|
|
263
|
+
]
|
|
264
|
+
if text_parts:
|
|
265
|
+
return "\n".join(text_parts)
|
|
266
|
+
|
|
267
|
+
# If no text parts, serialize the first non-empty item
|
|
268
|
+
for item in content:
|
|
269
|
+
if item: # Ensure item is not None or empty
|
|
270
|
+
if isinstance(item, dict):
|
|
271
|
+
return json.dumps(item)
|
|
272
|
+
else:
|
|
273
|
+
return str(item).strip()
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _get_reliable_tool_context(context_variables: ContextVariables, context_key: str) -> ReliableToolContext:
|
|
278
|
+
"""Retrieve and validate the ReliableToolContext from ContextVariables."""
|
|
279
|
+
context_data = context_variables.get(context_key)
|
|
280
|
+
if context_data is None:
|
|
281
|
+
raise KeyError(f"ReliableToolContext key '{context_key}' not found in ContextVariables.")
|
|
282
|
+
try:
|
|
283
|
+
if isinstance(context_data, str):
|
|
284
|
+
return ReliableToolContext.model_validate_json(context_data)
|
|
285
|
+
raise TypeError(
|
|
286
|
+
f"Unexpected type {type(context_data)} for context key '{context_key}'. Expected ReliableToolContext, str, or dict."
|
|
287
|
+
)
|
|
288
|
+
except (ValidationError, json.JSONDecodeError, TypeError) as e:
|
|
289
|
+
preview = f" Preview: '{str(context_data)[:100]}...'" if isinstance(context_data, (str, dict)) else ""
|
|
290
|
+
# Logged error level changed to warning as this function re-raises.
|
|
291
|
+
logger.warning(
|
|
292
|
+
"Failed loading ReliableToolContext '%s'. Error: %s. Type: %s.%s",
|
|
293
|
+
context_key,
|
|
294
|
+
e,
|
|
295
|
+
type(context_data).__name__,
|
|
296
|
+
preview,
|
|
297
|
+
)
|
|
298
|
+
raise ValueError(f"Failed loading ReliableToolContext key '{context_key}': {e}") from e
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _set_reliable_tool_context(
|
|
302
|
+
context_variables: ContextVariables, context_key: str, context: ReliableToolContext
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Serialize and store the ReliableToolContext in ContextVariables."""
|
|
305
|
+
if not isinstance(context, ReliableToolContext):
|
|
306
|
+
raise TypeError(f"Object to set must be a ReliableToolContext, got {type(context)}.")
|
|
307
|
+
try:
|
|
308
|
+
context_variables[context_key] = context.model_dump_json(warnings="warn")
|
|
309
|
+
except (ValidationError, TypeError) as e: # More specific exceptions
|
|
310
|
+
context_dict_str = "N/A"
|
|
311
|
+
try: # Best effort to get some context info for logging
|
|
312
|
+
context_dict_str = str(context.model_dump(warnings="warn", exclude={"attempts"}))[:500]
|
|
313
|
+
except Exception:
|
|
314
|
+
contextlib.suppress(Exception)
|
|
315
|
+
logger.error( # Log as error as this is a critical serialization failure
|
|
316
|
+
"Failed serializing ReliableToolContext key '%s': %s. Context (partial): %s",
|
|
317
|
+
context_key,
|
|
318
|
+
e,
|
|
319
|
+
context_dict_str,
|
|
320
|
+
)
|
|
321
|
+
raise ValueError(f"Critical error serializing ReliableToolContext: {e}") from e
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def get_runner_prompt(task: str, agent_system_message: str, internal_tool_name: str) -> str:
|
|
325
|
+
"""Generate the system prompt for the internal runner agent."""
|
|
326
|
+
return f"""
|
|
327
|
+
You are an AI assistant responsible for invoking a specific function based on the user's task and conversation history.
|
|
328
|
+
Function to call: '{internal_tool_name}'
|
|
329
|
+
Analyze the previous attempt's outcome (if any, visible in history) and adjust the function arguments accordingly for this retry. If this is the first attempt, determine the best initial arguments based on the task and initial context.
|
|
330
|
+
|
|
331
|
+
You MUST invoke the function '{internal_tool_name}' exactly one time per response using a tool call format that the system can execute.
|
|
332
|
+
Do NOT just output text explaining what you would do, or asking for confirmation. Directly make the tool call.
|
|
333
|
+
Analyze the task description and *full conversation history* carefully to determine the correct arguments for the function call.
|
|
334
|
+
You MUST provide a 'hypothesis' argument summarizing the expected outcome or result format of the function call based on the inputs.
|
|
335
|
+
|
|
336
|
+
Base Instructions:
|
|
337
|
+
{agent_system_message}
|
|
338
|
+
|
|
339
|
+
Current Task:
|
|
340
|
+
{task}
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_validator_prompt(
|
|
345
|
+
task: str, base_validator_system_message: str, dynamic_validation_addition: Optional[str] = None
|
|
346
|
+
) -> str:
|
|
347
|
+
"""Generate the system prompt for the internal validator agent."""
|
|
348
|
+
dynamic_section = (
|
|
349
|
+
f"\n\nAdditional Dynamic Requirements for This Specific Run:\n{dynamic_validation_addition.strip()}"
|
|
350
|
+
if dynamic_validation_addition and dynamic_validation_addition.strip()
|
|
351
|
+
else ""
|
|
352
|
+
)
|
|
353
|
+
return f"""
|
|
354
|
+
You are an AI validation assistant. You will receive a curated message list containing:
|
|
355
|
+
1. Initial context messages (original request, potentially prior conversation).
|
|
356
|
+
2. Provided ground truth information (if any).
|
|
357
|
+
3. The final result of a function call intended to accomplish the task.
|
|
358
|
+
|
|
359
|
+
Your goal is to validate if the *final function call result* meets ALL requirements based on the *entire context provided in the message list*. Consider the base task description, base validation rules, initial context/ground truth, and any dynamic requirements below.
|
|
360
|
+
|
|
361
|
+
Evaluate the *final function call result* (presented at the end of the message list) based on *all* information provided.
|
|
362
|
+
|
|
363
|
+
Base Validation Rules/Context:
|
|
364
|
+
{base_validator_system_message}{dynamic_section}
|
|
365
|
+
|
|
366
|
+
Base Task Description (for reference):
|
|
367
|
+
{task}
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def reliable_function_wrapper(
|
|
372
|
+
tool_function: Callable[..., Any], validator: ConversableAgent, runner: ConversableAgent, context_variables_key: str
|
|
373
|
+
) -> Callable[..., Any]:
|
|
374
|
+
"""Wraps the target function, returning a sync or async wrapper.
|
|
375
|
+
|
|
376
|
+
Adds 'hypothesis' and 'context_variables' keyword-only arguments.
|
|
377
|
+
Returns a ReplyResult targeting the validator.
|
|
378
|
+
"""
|
|
379
|
+
is_original_func_async = inspect.iscoroutinefunction(tool_function)
|
|
380
|
+
tool_sig = inspect.signature(tool_function)
|
|
381
|
+
wrapper_func: Callable[..., Any] # Declare type for wrapper_func
|
|
382
|
+
|
|
383
|
+
def _handle_execution_error(
|
|
384
|
+
attempt: ExecutionAttempt, context_vars: ContextVariables, context: ReliableToolContext, e: Exception
|
|
385
|
+
) -> ReplyResult:
|
|
386
|
+
"""Shared logic to handle tool_function execution error."""
|
|
387
|
+
err_msg = f"{type(e).__name__}: {e}"
|
|
388
|
+
logger.error( # Log the error from the wrapped function
|
|
389
|
+
"Wrapped function '%s' execution error: %s",
|
|
390
|
+
getattr(tool_function, "__name__", "unknown_func"),
|
|
391
|
+
err_msg,
|
|
392
|
+
exc_info=True, # Include traceback for wrapped function error
|
|
393
|
+
)
|
|
394
|
+
attempt.error = err_msg
|
|
395
|
+
if attempt not in context.attempts:
|
|
396
|
+
context.attempts.append(attempt)
|
|
397
|
+
|
|
398
|
+
_set_reliable_tool_context(context_vars, context_variables_key, context)
|
|
399
|
+
|
|
400
|
+
# Go to runner in this scenario because an error can just be handled by the runner again
|
|
401
|
+
return ReplyResult(
|
|
402
|
+
context_variables=context_vars,
|
|
403
|
+
target=AgentTarget(runner),
|
|
404
|
+
message=f"Function execution failed with error: {err_msg}.",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def _process_successful_execution(
|
|
408
|
+
attempt: ExecutionAttempt, result: Any, context_vars: ContextVariables, context: ReliableToolContext
|
|
409
|
+
) -> ReplyResult:
|
|
410
|
+
value_to_stringify: Any = None
|
|
411
|
+
|
|
412
|
+
if isinstance(result, tuple):
|
|
413
|
+
if len(result) >= 2:
|
|
414
|
+
attempt.result_data = result[0]
|
|
415
|
+
value_to_stringify = result[1]
|
|
416
|
+
elif len(result) == 1:
|
|
417
|
+
attempt.result_data = result[0]
|
|
418
|
+
value_to_stringify = result[0]
|
|
419
|
+
else:
|
|
420
|
+
attempt.result_data = None
|
|
421
|
+
value_to_stringify = ""
|
|
422
|
+
|
|
423
|
+
else:
|
|
424
|
+
attempt.result_data = result
|
|
425
|
+
value_to_stringify = result
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
attempt.result_str = str(value_to_stringify) if value_to_stringify is not None else ""
|
|
429
|
+
except Exception as str_e:
|
|
430
|
+
logger.warning(
|
|
431
|
+
"Could not convert result string part to string, using repr() \n %s",
|
|
432
|
+
str_e,
|
|
433
|
+
)
|
|
434
|
+
attempt.result_str = repr(value_to_stringify)
|
|
435
|
+
|
|
436
|
+
if attempt not in context.attempts:
|
|
437
|
+
context.attempts.append(attempt)
|
|
438
|
+
|
|
439
|
+
_set_reliable_tool_context(context_vars, context_variables_key, context)
|
|
440
|
+
|
|
441
|
+
return ReplyResult(
|
|
442
|
+
context_variables=context_vars,
|
|
443
|
+
target=AgentTarget(validator),
|
|
444
|
+
message=attempt.result_str,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if not is_original_func_async:
|
|
448
|
+
|
|
449
|
+
@functools.wraps(tool_function)
|
|
450
|
+
def sync_wrapper(
|
|
451
|
+
*args: Any, hypothesis: str, context_variables: ContextVariables, **kwargs: Any
|
|
452
|
+
) -> ReplyResult:
|
|
453
|
+
context = _get_reliable_tool_context(context_variables, context_variables_key)
|
|
454
|
+
attempt = ExecutionAttempt(attempt_args=list(args), attempt_kwargs=kwargs, hypothesis=hypothesis)
|
|
455
|
+
try:
|
|
456
|
+
result = tool_function(*args, **kwargs)
|
|
457
|
+
return _process_successful_execution(attempt, result, context_variables, context)
|
|
458
|
+
except Exception as e:
|
|
459
|
+
return _handle_execution_error(attempt, context_variables, context, e)
|
|
460
|
+
|
|
461
|
+
wrapper_func = sync_wrapper
|
|
462
|
+
else:
|
|
463
|
+
|
|
464
|
+
@functools.wraps(tool_function)
|
|
465
|
+
async def async_wrapper(
|
|
466
|
+
*args: Any, hypothesis: str, context_variables: ContextVariables, **kwargs: Any
|
|
467
|
+
) -> ReplyResult:
|
|
468
|
+
context = _get_reliable_tool_context(context_variables, context_variables_key)
|
|
469
|
+
attempt = ExecutionAttempt(attempt_args=list(args), attempt_kwargs=kwargs, hypothesis=hypothesis)
|
|
470
|
+
try:
|
|
471
|
+
result = await tool_function(*args, **kwargs)
|
|
472
|
+
return _process_successful_execution(attempt, result, context_variables, context)
|
|
473
|
+
except Exception as e:
|
|
474
|
+
return _handle_execution_error(attempt, context_variables, context, e)
|
|
475
|
+
|
|
476
|
+
wrapper_func = async_wrapper
|
|
477
|
+
|
|
478
|
+
params = list(tool_sig.parameters.values())
|
|
479
|
+
pos_or_kw_params, kw_only_params, var_pos_param, var_kw_param = [], [], None, None
|
|
480
|
+
for p in params:
|
|
481
|
+
if p.kind == inspect.Parameter.POSITIONAL_ONLY or p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
|
482
|
+
pos_or_kw_params.append(p)
|
|
483
|
+
elif p.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
484
|
+
var_pos_param = p
|
|
485
|
+
elif p.kind == inspect.Parameter.KEYWORD_ONLY:
|
|
486
|
+
kw_only_params.append(p)
|
|
487
|
+
elif p.kind == inspect.Parameter.VAR_KEYWORD:
|
|
488
|
+
var_kw_param = p
|
|
489
|
+
|
|
490
|
+
new_kw_only_params = [
|
|
491
|
+
inspect.Parameter(
|
|
492
|
+
"hypothesis",
|
|
493
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
494
|
+
annotation=Annotated[str, AG2Field(description=HYPOTHESIS_DESCRIPTION)],
|
|
495
|
+
default=inspect.Parameter.empty,
|
|
496
|
+
),
|
|
497
|
+
inspect.Parameter(
|
|
498
|
+
"context_variables",
|
|
499
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
500
|
+
annotation=ContextVariables,
|
|
501
|
+
default=inspect.Parameter.empty,
|
|
502
|
+
),
|
|
503
|
+
]
|
|
504
|
+
|
|
505
|
+
wrapper_params = (
|
|
506
|
+
pos_or_kw_params
|
|
507
|
+
+ ([var_pos_param] if var_pos_param else [])
|
|
508
|
+
+ kw_only_params
|
|
509
|
+
+ new_kw_only_params
|
|
510
|
+
+ ([var_kw_param] if var_kw_param else [])
|
|
511
|
+
)
|
|
512
|
+
setattr(wrapper_func, "__signature__", inspect.Signature(parameters=wrapper_params, return_annotation=ReplyResult))
|
|
513
|
+
return wrapper_func
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
@export_module("autogen.tools.experimental")
|
|
517
|
+
class ReliableToolError(Exception):
|
|
518
|
+
"""Custom exception for errors during ReliableTool execution."""
|
|
519
|
+
|
|
520
|
+
def __init__(self, message: str, final_context: Optional[ReliableToolContext] = None):
|
|
521
|
+
super().__init__(message)
|
|
522
|
+
self.final_context = final_context
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@export_module("autogen.tools.experimental")
|
|
526
|
+
class ReliableTool(Tool):
|
|
527
|
+
INTERNAL_TOOL_NAME_PREFIX = "execute_"
|
|
528
|
+
|
|
529
|
+
def __init__(
|
|
530
|
+
self,
|
|
531
|
+
name: str,
|
|
532
|
+
func_or_tool: Union[Callable[..., Any], Tool],
|
|
533
|
+
runner_llm_config: Union[LLMConfig, dict[str, Any]],
|
|
534
|
+
validator_llm_config: Union[LLMConfig, dict[str, Any]],
|
|
535
|
+
description: Optional[str] = None,
|
|
536
|
+
system_message_addition_for_tool_calling: str = "",
|
|
537
|
+
system_message_addition_for_result_validation: str = "",
|
|
538
|
+
max_tool_invocations: int = 3,
|
|
539
|
+
enable_dynamic_validation: bool = False,
|
|
540
|
+
messages: Optional[List[dict[str, Any]]] = None,
|
|
541
|
+
ground_truth: Optional[List[str]] = None,
|
|
542
|
+
) -> None:
|
|
543
|
+
"""
|
|
544
|
+
A ReliableTool wraps an existing function or tool.
|
|
545
|
+
When the ReliableTool is invoked, it kicks off an internal Group Chat where a Runner
|
|
546
|
+
and Validator agent will iteratively invoke the wrapped function or tool until
|
|
547
|
+
*the output of a single invocation of the original function or tool satisfies the provided validation criteria.*
|
|
548
|
+
Reliable Tools are best used when the LLM used or the function or tool itself is unreliable.
|
|
549
|
+
Commonly this happens when using small, local LLMs, <32b params
|
|
550
|
+
Or when functions/tools are used to "explore" (doing many web searches, exploring a database with SQL)
|
|
551
|
+
The Reliable Tool allows the user to bake a result validation strategy into the tool itself
|
|
552
|
+
so that the broader group chat/agentic system can be built more clearly around the intended flow
|
|
553
|
+
instead of needing to focus so much on retry and validation loops.
|
|
554
|
+
|
|
555
|
+
Additionally, the .run() and .a_run() methods serve as a way to use LLMs to invoke a specific tool outside
|
|
556
|
+
of a Group Chat or similar structure to provide a more traditional programming method of using LLMs and tools in code.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
name (str):
|
|
560
|
+
A unique and descriptive name for this ReliableTool instance.
|
|
561
|
+
This name is used for logging, internal context management, and can be
|
|
562
|
+
how other agents or systems refer to this specific reliable capability.
|
|
563
|
+
Example: `"AccurateWeatherForecaster"`, `"ValidatedCustomerLookup"`
|
|
564
|
+
|
|
565
|
+
func_or_tool (Union[Callable[..., Any], Tool]):
|
|
566
|
+
The core Python function or an existing AG2 `Tool` instance that this
|
|
567
|
+
`ReliableTool` will manage and execute. This is the underlying capability
|
|
568
|
+
you want to enhance with reliability features like retries and validation.
|
|
569
|
+
The `ReliableTool` will handle calling this function with arguments
|
|
570
|
+
determined by its internal Runner Agent based on the provided `task`.
|
|
571
|
+
Example: `my_api_call_function`, `existing_search_tool_instance`
|
|
572
|
+
|
|
573
|
+
runner_llm_config (Union[LLMConfig, dict[str, Any]]):
|
|
574
|
+
The LLM configuration for the internal "Runner Agent". This agent is
|
|
575
|
+
responsible for interpreting the high-level `task` provided when the
|
|
576
|
+
`ReliableTool` is invoked, deciding the appropriate arguments for the
|
|
577
|
+
`func_or_tool`, and initiating its execution.
|
|
578
|
+
This configuration dictates the model, API keys, temperature, etc., for
|
|
579
|
+
the LLM that attempts to call your function. It must support tool/function calling.
|
|
580
|
+
Example: `LLMConfig(config_list=oai_config_list, model="gpt-4o-mini")`
|
|
581
|
+
`{"config_list": [{"model": "gpt-3.5-turbo", "api_key": "..."}], "temperature": 0.5}`
|
|
582
|
+
|
|
583
|
+
validator_llm_config (Union[LLMConfig, dict[str, Any]]):
|
|
584
|
+
The LLM configuration for the internal "Validator Agent". After the
|
|
585
|
+
`func_or_tool` executes successfully, this agent receives its string output
|
|
586
|
+
and assesses whether it meets defined validation criteria. It is
|
|
587
|
+
configured for structured output (Pydantic model `ValidationResult`)
|
|
588
|
+
to provide a boolean validation status and a justification.
|
|
589
|
+
This configuration dictates the model, etc., for the LLM that validates
|
|
590
|
+
the function's result. It can be the same as `runner_llm_config` or different.
|
|
591
|
+
Example: `LLMConfig(config_list=oai_config_list, model="gpt-4o-mini")`
|
|
592
|
+
|
|
593
|
+
description (Optional[str], default: None):
|
|
594
|
+
A human-readable description of what this `ReliableTool` achieves.
|
|
595
|
+
If `None`, the description is inferred from the docstring of the
|
|
596
|
+
provided `func_or_tool`. This description is primarily for the public-facing
|
|
597
|
+
`ReliableTool` (e.g., when registered with an outer agent for it to decide
|
|
598
|
+
when to use this tool).
|
|
599
|
+
Example: `"Reliably fetches and validates current weather information for a specified city."`
|
|
600
|
+
|
|
601
|
+
system_message_addition_for_tool_calling (str, default: ""):
|
|
602
|
+
Additional text appended to the system message of the internal "Runner Agent".
|
|
603
|
+
This allows you to provide specific instructions, context, or constraints
|
|
604
|
+
to the LLM responsible for deciding *how* to call your underlying `func_or_tool`.
|
|
605
|
+
Use this when the Runner Agent needs more guidance than just the task
|
|
606
|
+
description and the function's signature to correctly formulate arguments.
|
|
607
|
+
Example: `"When calling 'search_products', if the task mentions 'budget', ensure the 'max_price' argument is set accordingly. Prioritize items in stock."`
|
|
608
|
+
|
|
609
|
+
system_message_addition_for_result_validation (str, default: ""):
|
|
610
|
+
Additional text appended to the system message of the internal "Validator Agent".
|
|
611
|
+
This is where you define the *base* or *static* criteria for validating the
|
|
612
|
+
*result* (string representation) of your `func_or_tool`. These criteria
|
|
613
|
+
are applied on every validation attempt unless overridden or supplemented by
|
|
614
|
+
dynamic validation.
|
|
615
|
+
Example: `"The stock price must be a positive number. The company name in the result must match the one in the task. If data is unavailable, the result should explicitly state 'Data not found'."`
|
|
616
|
+
|
|
617
|
+
max_tool_invocations (int, default: 3):
|
|
618
|
+
The maximum number of times the internal "Runner Agent" can attempt to
|
|
619
|
+
call the underlying `func_or_tool`. This limit includes the initial attempt
|
|
620
|
+
and any subsequent retries that occur due to:
|
|
621
|
+
1. Direct execution errors from `func_or_tool`.
|
|
622
|
+
2. The Runner Agent failing to generate a valid tool call.
|
|
623
|
+
3. The Validator Agent deeming a successful execution's result as invalid.
|
|
624
|
+
Adjust this to control retries and prevent excessive LLM calls, considering
|
|
625
|
+
the potential flakiness of the `func_or_tool` or complexity of parameterization.
|
|
626
|
+
Example: `max_tool_invocations=2` (allows one initial attempt and one retry if needed).
|
|
627
|
+
|
|
628
|
+
enable_dynamic_validation (bool, default: False):
|
|
629
|
+
If `True`, the public-facing `run` (or `a_run`) method of this `ReliableTool`
|
|
630
|
+
(accessible via its `func` attribute after initialization) will accept an
|
|
631
|
+
additional optional argument: `validation_prompt_addition: Optional[str]`.
|
|
632
|
+
If a string is provided for this argument during a call, it will be appended
|
|
633
|
+
to the Validator Agent's system message *for that specific run*, allowing
|
|
634
|
+
validation criteria to be tailored on-the-fly based on the task.
|
|
635
|
+
Example: If `True`, `my_tool.func(task="search for AG2 examples", validation_prompt_addition="Result must include Python code snippets.")`
|
|
636
|
+
|
|
637
|
+
messages (Optional[List[dict[str, Any]]], default: None):
|
|
638
|
+
A list of initial messages (e.g., from a prior conversation history) to
|
|
639
|
+
provide context to the internal Runner and Validator agents. These messages
|
|
640
|
+
are prepended to the message history seen by these agents during their
|
|
641
|
+
internal chat, helping them understand the `task` in a broader context.
|
|
642
|
+
Use when the `task` for the `ReliableTool` might refer to entities or
|
|
643
|
+
intentions established in preceding turns of a conversation.
|
|
644
|
+
Example: `messages=[{"role": "user", "content": "I'm interested in large-cap tech stocks."}, {"role": "assistant", "content": "Okay, any specific ones?"}]`
|
|
645
|
+
(Then a task like "Fetch the latest price for 'the one we just discussed'.")
|
|
646
|
+
|
|
647
|
+
ground_truth (Optional[List[str]], default: None):
|
|
648
|
+
A list of strings representing factual information, examples, or specific
|
|
649
|
+
constraints that should be considered by the internal Runner and Validator
|
|
650
|
+
agents. These are injected into the conversation history as distinct user
|
|
651
|
+
messages (e.g., "[[Provided Ground Truth 1]]: ...").
|
|
652
|
+
Use to provide specific, factual data or strong hints that might not fit
|
|
653
|
+
naturally into system messages or prior conversation history, guiding the
|
|
654
|
+
agents towards correct interpretation or validation.
|
|
655
|
+
Example: `ground_truth=["The API rate limit is 10 requests per minute.", "User preference: only show results from the last 7 days."]`
|
|
656
|
+
"""
|
|
657
|
+
self._original_func, original_name, original_description = self._extract_func_details(func_or_tool)
|
|
658
|
+
self._is_original_func_async = inspect.iscoroutinefunction(self._original_func)
|
|
659
|
+
|
|
660
|
+
self._runner_llm_config = ConversableAgent._validate_llm_config(runner_llm_config)
|
|
661
|
+
if self._runner_llm_config is False:
|
|
662
|
+
raise ValueError("Runner LLM config failed validation.")
|
|
663
|
+
# Validate validator_llm_config and store it. It can be LLMConfig | dict | False.
|
|
664
|
+
self._validator_llm_config = ConversableAgent._validate_llm_config(validator_llm_config)
|
|
665
|
+
if self._validator_llm_config is False: # Check before use in _setup_validator_agent
|
|
666
|
+
raise ValueError("Validator LLM config failed validation.")
|
|
667
|
+
|
|
668
|
+
self._runner_system_message_addition = system_message_addition_for_tool_calling
|
|
669
|
+
self._validator_system_message_addition = system_message_addition_for_result_validation
|
|
670
|
+
self.max_tool_invocations = max_tool_invocations
|
|
671
|
+
self._context_variables_key = f"{name}_ReliableToolContext_{id(self)}"
|
|
672
|
+
|
|
673
|
+
self._original_func_name = original_name
|
|
674
|
+
self.enable_dynamic_validation = enable_dynamic_validation
|
|
675
|
+
|
|
676
|
+
self._init_messages = copy.deepcopy(messages) if messages is not None else None
|
|
677
|
+
self._init_ground_truth = copy.deepcopy(ground_truth) if ground_truth else None
|
|
678
|
+
|
|
679
|
+
self._tool_description = description if description is not None else original_description
|
|
680
|
+
|
|
681
|
+
public_entry_point_func = self._define_public_entry_point(
|
|
682
|
+
self._is_original_func_async, self.enable_dynamic_validation
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
super().__init__(
|
|
686
|
+
name=name,
|
|
687
|
+
description=self._tool_description,
|
|
688
|
+
func_or_tool=public_entry_point_func,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
self._validator_name = f"{self.name}_Validator"
|
|
692
|
+
self._runner_name = f"{self.name}_Runner"
|
|
693
|
+
|
|
694
|
+
self._validator = self._setup_validator_agent()
|
|
695
|
+
self._runner = self._setup_runner_agent()
|
|
696
|
+
self._reliable_func_wrapper = reliable_function_wrapper(
|
|
697
|
+
self._original_func, self._validator, self._runner, self._context_variables_key
|
|
698
|
+
)
|
|
699
|
+
self._setup_runner_tool()
|
|
700
|
+
self._register_internal_hooks()
|
|
701
|
+
|
|
702
|
+
def _define_public_entry_point(self, is_async: bool, enable_dynamic: bool) -> Callable[..., Any]:
|
|
703
|
+
if not is_async:
|
|
704
|
+
if enable_dynamic:
|
|
705
|
+
|
|
706
|
+
def sync_entry_point_with_validation(
|
|
707
|
+
task: str, validation_prompt_addition: Optional[str] = None
|
|
708
|
+
) -> Any:
|
|
709
|
+
return self.run(task=task, validation_prompt_addition=validation_prompt_addition)
|
|
710
|
+
|
|
711
|
+
return sync_entry_point_with_validation
|
|
712
|
+
else:
|
|
713
|
+
|
|
714
|
+
def sync_entry_point_without_validation(task: str) -> Any:
|
|
715
|
+
return self.run(task=task, validation_prompt_addition=None)
|
|
716
|
+
|
|
717
|
+
return sync_entry_point_without_validation
|
|
718
|
+
else:
|
|
719
|
+
if enable_dynamic:
|
|
720
|
+
|
|
721
|
+
async def async_entry_point_with_validation(
|
|
722
|
+
task: str, validation_prompt_addition: Optional[str] = None
|
|
723
|
+
) -> Any:
|
|
724
|
+
return await self.a_run(task=task, validation_prompt_addition=validation_prompt_addition)
|
|
725
|
+
|
|
726
|
+
return async_entry_point_with_validation
|
|
727
|
+
else:
|
|
728
|
+
|
|
729
|
+
async def async_entry_point_without_validation(task: str) -> Any:
|
|
730
|
+
return await self.a_run(task=task, validation_prompt_addition=None)
|
|
731
|
+
|
|
732
|
+
return async_entry_point_without_validation
|
|
733
|
+
|
|
734
|
+
def _extract_func_details(
|
|
735
|
+
self, func_or_tool: Union[Callable[..., Any], Tool]
|
|
736
|
+
) -> Tuple[Callable[..., Any], str, str]:
|
|
737
|
+
default_desc_template = "Executes the '{name}' function."
|
|
738
|
+
if isinstance(func_or_tool, Tool):
|
|
739
|
+
func = getattr(func_or_tool, "func", None)
|
|
740
|
+
if not callable(func):
|
|
741
|
+
raise TypeError(
|
|
742
|
+
f"Tool '{func_or_tool.name}' provided but its 'func' attribute is not callable or missing."
|
|
743
|
+
)
|
|
744
|
+
name = func_or_tool.name
|
|
745
|
+
desc = func_or_tool.description
|
|
746
|
+
if not desc or desc == f"Tool '{name}'." or desc == "No description provided.":
|
|
747
|
+
func_doc = inspect.getdoc(func)
|
|
748
|
+
desc = func_doc.strip() if func_doc else f"{default_desc_template.format(name=name)}"
|
|
749
|
+
return func, name, desc
|
|
750
|
+
elif callable(func_or_tool):
|
|
751
|
+
name = getattr(func_or_tool, "__name__", "callable_function")
|
|
752
|
+
doc = inspect.getdoc(func_or_tool)
|
|
753
|
+
desc = doc.strip() if doc else f"{default_desc_template.format(name=name)}"
|
|
754
|
+
# For raw callables, we don't have a pre-computed schema like Tool object might
|
|
755
|
+
return func_or_tool, name, desc
|
|
756
|
+
raise TypeError(
|
|
757
|
+
"Input 'func_or_tool' must be a callable or an autogen.Tool instance with a callable 'func' attribute."
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
def _setup_validator_agent(self) -> ConversableAgent:
|
|
761
|
+
# _configure_llm_for_structured_output will raise ValueError if config is bad
|
|
762
|
+
# Use a local variable for type narrowing after the False check.
|
|
763
|
+
current_validator_config = self._validator_llm_config
|
|
764
|
+
if current_validator_config is False:
|
|
765
|
+
# This case should have been caught in __init__, but as a safeguard:
|
|
766
|
+
raise ValueError("Validator LLM config is False, cannot proceed.")
|
|
767
|
+
|
|
768
|
+
structured_llm_config = _configure_llm_for_structured_output(
|
|
769
|
+
copy.deepcopy(current_validator_config), # current_validator_config is not False here
|
|
770
|
+
ValidationResult,
|
|
771
|
+
)
|
|
772
|
+
return ConversableAgent(
|
|
773
|
+
name=self._validator_name,
|
|
774
|
+
system_message="[Validator Prompt Updated Per Run]",
|
|
775
|
+
llm_config=structured_llm_config,
|
|
776
|
+
human_input_mode="NEVER",
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
def _setup_runner_agent(self) -> ConversableAgent:
|
|
780
|
+
runner_llm_config_copy = copy.deepcopy(self._runner_llm_config)
|
|
781
|
+
runner = ConversableAgent(
|
|
782
|
+
name=self._runner_name,
|
|
783
|
+
system_message="[Runner Prompt Updated Per Run]",
|
|
784
|
+
llm_config=runner_llm_config_copy,
|
|
785
|
+
human_input_mode="NEVER",
|
|
786
|
+
)
|
|
787
|
+
return runner
|
|
788
|
+
|
|
789
|
+
def _setup_runner_tool(self) -> None:
|
|
790
|
+
internal_tool_name = f"{self.INTERNAL_TOOL_NAME_PREFIX}{self._original_func_name}"
|
|
791
|
+
internal_tool = Tool(
|
|
792
|
+
name=internal_tool_name, description=self._tool_description, func_or_tool=self._reliable_func_wrapper
|
|
793
|
+
)
|
|
794
|
+
internal_tool.register_tool(self._runner)
|
|
795
|
+
logger.info(
|
|
796
|
+
"Successfully registered internal tool '%s' with runner '%s'", internal_tool_name, self._runner.name
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
def _register_internal_hooks(self) -> None:
|
|
800
|
+
self._validator.register_hook(
|
|
801
|
+
hookable_method="process_message_before_send", hook=self._validator_structured_output_hook
|
|
802
|
+
)
|
|
803
|
+
self._validator.register_hook(
|
|
804
|
+
hookable_method="process_all_messages_before_reply", hook=self._validator_construct_context_hook
|
|
805
|
+
)
|
|
806
|
+
self._runner.register_hook(hookable_method="process_message_before_send", hook=self._ensure_function_call_hook)
|
|
807
|
+
|
|
808
|
+
def _validator_structured_output_hook(
|
|
809
|
+
self, sender: Agent, message: Union[dict[str, Any], str], recipient: Agent, silent: bool
|
|
810
|
+
) -> Union[dict[str, Any], str]:
|
|
811
|
+
if not isinstance(message, str):
|
|
812
|
+
logger.error(
|
|
813
|
+
f"Validator Hook: Expected a JSON string message from LLM, but got {type(message)}. Content: {str(message)[:200]}"
|
|
814
|
+
)
|
|
815
|
+
# This indicates a misconfiguration or unexpected LLM output format.
|
|
816
|
+
raise TypeError(f"Validator hook expected str from LLM, got {type(message)}")
|
|
817
|
+
|
|
818
|
+
validation_result_obj: ValidationResult = ValidationResult.model_validate_json(message)
|
|
819
|
+
status = "PASSED" if validation_result_obj.validation_result else "FAILED"
|
|
820
|
+
log_level = logging.INFO if status == "PASSED" else logging.WARNING
|
|
821
|
+
logger.log(
|
|
822
|
+
log_level,
|
|
823
|
+
f"Validator Hook: Parsed Validation - {status}. Justification: {validation_result_obj.justification}",
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
self._try_update_context_validation(sender, validation_result_obj)
|
|
827
|
+
# sender is self._validator in this hook context
|
|
828
|
+
self._set_validator_handoff(self._validator, validation_result_obj.validation_result)
|
|
829
|
+
return validation_result_obj.format() # Return JSON string
|
|
830
|
+
|
|
831
|
+
def _set_validator_handoff(self, validator_agent: ConversableAgent, validation_passed: bool) -> None:
|
|
832
|
+
if not validation_passed:
|
|
833
|
+
logger.info("Validation failed, setting handoff to runner: %s", self._runner_name)
|
|
834
|
+
validator_agent.handoffs.set_after_work(target=AgentTarget(self._runner))
|
|
835
|
+
else:
|
|
836
|
+
logger.info("Validation passed, setting handoff to TerminateTarget.")
|
|
837
|
+
validator_agent.handoffs.set_after_work(target=TerminateTarget())
|
|
838
|
+
|
|
839
|
+
def _try_update_context_validation(self, sender: Agent, validation_result: ValidationResult) -> None:
|
|
840
|
+
"""Helper to attempt updating the validation state in the ReliableToolContext."""
|
|
841
|
+
context_vars = getattr(sender, "context_variables")
|
|
842
|
+
|
|
843
|
+
tool_context = _get_reliable_tool_context(context_vars, self._context_variables_key)
|
|
844
|
+
latest_attempt = tool_context.latest_attempt
|
|
845
|
+
|
|
846
|
+
if not latest_attempt:
|
|
847
|
+
# This implies a logical error in the execution flow.
|
|
848
|
+
raise RuntimeError(
|
|
849
|
+
f"Validator hook: No execution attempt found in context '{self._context_variables_key}' to update validation for."
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
latest_attempt.validation = validation_result
|
|
853
|
+
_set_reliable_tool_context(context_vars, self._context_variables_key, tool_context)
|
|
854
|
+
logger.info(
|
|
855
|
+
"Validator hook: Updated validation status in context: %s",
|
|
856
|
+
"Passed" if validation_result.validation_result else "Failed",
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
def _validator_construct_context_hook(self, messages: list[dict[str, Any]], **kwargs: Any) -> list[dict[str, Any]]:
|
|
860
|
+
sender = self._validator # Assuming self._validator is the agent instance
|
|
861
|
+
logger.debug("Validator Construct Context Hook running for agent %s.", sender.name)
|
|
862
|
+
|
|
863
|
+
context_vars = getattr(sender, "context_variables")
|
|
864
|
+
|
|
865
|
+
tool_context = _get_reliable_tool_context(context_vars, self._context_variables_key)
|
|
866
|
+
initial_messages_to_inject = (
|
|
867
|
+
copy.deepcopy(tool_context.initial_messages) if tool_context.initial_messages else []
|
|
868
|
+
)
|
|
869
|
+
|
|
870
|
+
ground_truth_messages_to_inject = []
|
|
871
|
+
if tool_context.initial_ground_truth:
|
|
872
|
+
for i, gt in enumerate(tool_context.initial_ground_truth):
|
|
873
|
+
ground_truth_messages_to_inject.append({
|
|
874
|
+
"role": "user",
|
|
875
|
+
"content": f"[[Provided Ground Truth {i + 1}]]:\n{gt}",
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
last_content = _get_last_non_empty_message_content(messages)
|
|
879
|
+
result_message_dict = {
|
|
880
|
+
"role": "user",
|
|
881
|
+
"content": f"--- Function Result to Validate ---\n```\n{last_content}\n```\n--- End of Result ---",
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
final_messages = initial_messages_to_inject + ground_truth_messages_to_inject + [result_message_dict]
|
|
885
|
+
return final_messages
|
|
886
|
+
|
|
887
|
+
def _ensure_function_call_hook(
|
|
888
|
+
self, sender: Agent, message: Union[dict[str, Any], str], recipient: Agent, silent: bool
|
|
889
|
+
) -> Union[dict[str, Any], str]:
|
|
890
|
+
if sender.name != self._runner_name:
|
|
891
|
+
return message
|
|
892
|
+
|
|
893
|
+
tool_calls_list = None
|
|
894
|
+
if isinstance(message, dict):
|
|
895
|
+
tool_calls_list = message.get("tool_calls")
|
|
896
|
+
|
|
897
|
+
tool_name_expected = f"{self.INTERNAL_TOOL_NAME_PREFIX}{self._original_func_name}"
|
|
898
|
+
correct_tool_called = False
|
|
899
|
+
if isinstance(tool_calls_list, list):
|
|
900
|
+
for call in tool_calls_list:
|
|
901
|
+
if (
|
|
902
|
+
isinstance(call, dict)
|
|
903
|
+
and call.get("type") == "function"
|
|
904
|
+
and isinstance(call.get("function"), dict)
|
|
905
|
+
and call["function"].get("name") == tool_name_expected
|
|
906
|
+
):
|
|
907
|
+
correct_tool_called = True
|
|
908
|
+
break
|
|
909
|
+
|
|
910
|
+
if not correct_tool_called:
|
|
911
|
+
if not hasattr(self._runner, "handoffs"):
|
|
912
|
+
raise AttributeError(f"Runner agent '{self._runner.name}' missing 'handoffs' attribute for reminder.")
|
|
913
|
+
self._runner.handoffs.set_after_work(target=AgentTarget(self._runner)) # Retry with runner
|
|
914
|
+
|
|
915
|
+
logger.warning(
|
|
916
|
+
"Runner '%s' did not generate required tool call for '%s'. Appending reminder.",
|
|
917
|
+
self._runner_name,
|
|
918
|
+
tool_name_expected,
|
|
919
|
+
)
|
|
920
|
+
reminder = (
|
|
921
|
+
f"\n\n[[System Reminder: You MUST invoke the function '{tool_name_expected}' using a tool call. "
|
|
922
|
+
"Provide all required arguments including 'hypothesis'.]]\n"
|
|
923
|
+
"Correct your mistake and make a new attempt at invoking the tool."
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
current_content = ""
|
|
927
|
+
if isinstance(message, str):
|
|
928
|
+
current_content = message
|
|
929
|
+
elif isinstance(message, dict):
|
|
930
|
+
current_content = message.get("content") or ""
|
|
931
|
+
|
|
932
|
+
# Return a new message dict to ensure it's processed correctly by the agent
|
|
933
|
+
return {
|
|
934
|
+
"role": "assistant", # The LLM's previous turn was as assistant
|
|
935
|
+
"content": (current_content or "") + reminder,
|
|
936
|
+
"tool_calls": [] if isinstance(message, dict) else None,
|
|
937
|
+
}
|
|
938
|
+
return message
|
|
939
|
+
|
|
940
|
+
def _execute_internal_group_chat(
|
|
941
|
+
self,
|
|
942
|
+
task: str,
|
|
943
|
+
initial_context_vars: ContextVariables, # Renamed for clarity
|
|
944
|
+
dynamic_validation_str: Optional[str] = None,
|
|
945
|
+
) -> Tuple[ChatResult, ContextVariables, Agent]:
|
|
946
|
+
internal_tool_name = f"{self.INTERNAL_TOOL_NAME_PREFIX}{self._original_func_name}"
|
|
947
|
+
|
|
948
|
+
# update_system_message should not fail if agent is properly initialized
|
|
949
|
+
runner_prompt = get_runner_prompt(task, self._runner_system_message_addition, internal_tool_name)
|
|
950
|
+
self._runner.update_system_message(runner_prompt)
|
|
951
|
+
|
|
952
|
+
validator_prompt = get_validator_prompt(task, self._validator_system_message_addition, dynamic_validation_str)
|
|
953
|
+
self._validator.update_system_message(validator_prompt)
|
|
954
|
+
|
|
955
|
+
# Store context ref on agents for hooks. Crucial for hooks to access shared state.
|
|
956
|
+
self._validator.context_variables = initial_context_vars
|
|
957
|
+
self._runner.context_variables = initial_context_vars
|
|
958
|
+
|
|
959
|
+
messages_for_runner_history = []
|
|
960
|
+
# Retrieve tool_context again to build runner history with potentially updated initial messages/GT
|
|
961
|
+
# This is vital if _process_run (the caller) modifies them in initial_context_vars.
|
|
962
|
+
tool_context = _get_reliable_tool_context(initial_context_vars, self._context_variables_key)
|
|
963
|
+
|
|
964
|
+
if tool_context.initial_messages:
|
|
965
|
+
messages_for_runner_history.extend(copy.deepcopy(tool_context.initial_messages))
|
|
966
|
+
if tool_context.initial_ground_truth:
|
|
967
|
+
for i, gt in enumerate(tool_context.initial_ground_truth):
|
|
968
|
+
messages_for_runner_history.append({
|
|
969
|
+
"role": "user",
|
|
970
|
+
"content": f"[[Provided Ground Truth {i + 1}]]:\n{gt}",
|
|
971
|
+
})
|
|
972
|
+
|
|
973
|
+
task_message = {
|
|
974
|
+
"role": "user",
|
|
975
|
+
"content": f"[[Task Kickoff]]: Please execute the required function call for the task: {task}",
|
|
976
|
+
}
|
|
977
|
+
final_initial_messages_for_runner = messages_for_runner_history + [task_message]
|
|
978
|
+
|
|
979
|
+
agent_pattern = DefaultPattern(
|
|
980
|
+
agents=[self._runner, self._validator],
|
|
981
|
+
initial_agent=self._runner,
|
|
982
|
+
context_variables=initial_context_vars,
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
max_internal_rounds = 1 + (self.max_tool_invocations * 3)
|
|
986
|
+
logger.debug(
|
|
987
|
+
f"Setting max internal chat rounds to {max_internal_rounds} for {self.max_tool_invocations} tool invocations."
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
logger.info(
|
|
991
|
+
f"--- Starting ReliableTool '{self.name}' Internal Chat (Max Invocations: {self.max_tool_invocations}) ---"
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
last_reply, final_context_vars, last_agent = initiate_group_chat(
|
|
995
|
+
pattern=agent_pattern,
|
|
996
|
+
messages=final_initial_messages_for_runner,
|
|
997
|
+
max_rounds=max_internal_rounds,
|
|
998
|
+
)
|
|
999
|
+
logger.info(
|
|
1000
|
+
f"--- ReliableTool '{self.name}' Internal Chat Finished (Last Agent: {getattr(last_agent, 'name', 'N/A')}) ---"
|
|
1001
|
+
)
|
|
1002
|
+
if not isinstance(final_context_vars, ContextVariables):
|
|
1003
|
+
# This would be an unexpected issue with initiate_group_chat or pattern
|
|
1004
|
+
raise TypeError(f"Internal chat returned invalid context_variables type: {type(final_context_vars)}")
|
|
1005
|
+
return last_reply, final_context_vars, last_agent
|
|
1006
|
+
|
|
1007
|
+
def _prepare_tool_context(
|
|
1008
|
+
self,
|
|
1009
|
+
task: str,
|
|
1010
|
+
current_context_variables: ContextVariables,
|
|
1011
|
+
validation_prompt_addition: Optional[str] = None,
|
|
1012
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1013
|
+
ground_truth: Optional[List[str]] = None,
|
|
1014
|
+
) -> ReliableToolContext:
|
|
1015
|
+
"""Initializes or updates the ReliableToolContext for the current run."""
|
|
1016
|
+
effective_messages = copy.deepcopy(messages) if messages is not None else self._init_messages
|
|
1017
|
+
effective_ground_truth = copy.deepcopy(ground_truth) if ground_truth is not None else self._init_ground_truth
|
|
1018
|
+
|
|
1019
|
+
tool_context = ReliableToolContext(task=task, reliable_tool_name=self.name)
|
|
1020
|
+
|
|
1021
|
+
tool_context.task = task
|
|
1022
|
+
tool_context.dynamic_validation_input = validation_prompt_addition
|
|
1023
|
+
tool_context.initial_messages = effective_messages
|
|
1024
|
+
tool_context.initial_ground_truth = effective_ground_truth
|
|
1025
|
+
|
|
1026
|
+
_set_reliable_tool_context(current_context_variables, self._context_variables_key, tool_context)
|
|
1027
|
+
return tool_context
|
|
1028
|
+
|
|
1029
|
+
def _process_run(
|
|
1030
|
+
self,
|
|
1031
|
+
task: str,
|
|
1032
|
+
context_variables: Optional[ContextVariables] = None,
|
|
1033
|
+
validation_prompt_addition: Optional[str] = None,
|
|
1034
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1035
|
+
ground_truth: Optional[List[str]] = None,
|
|
1036
|
+
) -> Any:
|
|
1037
|
+
current_context_variables = context_variables if context_variables is not None else ContextVariables()
|
|
1038
|
+
if not isinstance(current_context_variables, ContextVariables):
|
|
1039
|
+
raise TypeError(f"Expected context_variables as ContextVariables or None, got {type(context_variables)}")
|
|
1040
|
+
|
|
1041
|
+
self._prepare_tool_context(task, current_context_variables, validation_prompt_addition, messages, ground_truth)
|
|
1042
|
+
|
|
1043
|
+
final_tool_context: ReliableToolContext
|
|
1044
|
+
_, chat_context_variables, _ = self._execute_internal_group_chat(
|
|
1045
|
+
task=task,
|
|
1046
|
+
initial_context_vars=current_context_variables,
|
|
1047
|
+
dynamic_validation_str=validation_prompt_addition,
|
|
1048
|
+
)
|
|
1049
|
+
current_context_variables = chat_context_variables
|
|
1050
|
+
|
|
1051
|
+
final_tool_context = _get_reliable_tool_context(current_context_variables, self._context_variables_key)
|
|
1052
|
+
latest_attempt_obj = final_tool_context.latest_attempt
|
|
1053
|
+
|
|
1054
|
+
if not latest_attempt_obj:
|
|
1055
|
+
raise ReliableToolError(
|
|
1056
|
+
"Critical internal error: No execution attempt recorded after chat cycle.",
|
|
1057
|
+
final_context=final_tool_context,
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
# If execution was successful BUT validation is missing (e.g. validator hook failed to set it)
|
|
1061
|
+
if latest_attempt_obj.did_execute_successfully and latest_attempt_obj.validation is None:
|
|
1062
|
+
logger.warning(
|
|
1063
|
+
"[%s]: Validation result missing after successful execution. Assuming validation failed.", self.name
|
|
1064
|
+
)
|
|
1065
|
+
latest_attempt_obj.validation = ValidationResult(
|
|
1066
|
+
validation_result=False,
|
|
1067
|
+
justification="Validation result was not recorded after successful execution. Usually due to group chat reaching maximum runs",
|
|
1068
|
+
)
|
|
1069
|
+
_set_reliable_tool_context(current_context_variables, self._context_variables_key, final_tool_context)
|
|
1070
|
+
|
|
1071
|
+
if final_tool_context.is_complete_and_successful:
|
|
1072
|
+
logger.info("ReliableTool '%s' succeeded.", self.name)
|
|
1073
|
+
return final_tool_context.get_final_result_data()
|
|
1074
|
+
else:
|
|
1075
|
+
failure_reason = final_tool_context.get_failure_summary()
|
|
1076
|
+
logger.warning("ReliableTool '%s' failed. Reason: %s", self.name, failure_reason)
|
|
1077
|
+
raise ReliableToolError(
|
|
1078
|
+
f"ReliableTool '{self.name}' failed. Last failure: {failure_reason}",
|
|
1079
|
+
final_context=final_tool_context,
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
def run(
|
|
1083
|
+
self,
|
|
1084
|
+
task: str,
|
|
1085
|
+
context_variables: Optional[ContextVariables] = None,
|
|
1086
|
+
validation_prompt_addition: Optional[str] = None,
|
|
1087
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1088
|
+
ground_truth: Optional[List[str]] = None,
|
|
1089
|
+
) -> Any:
|
|
1090
|
+
if self._is_original_func_async:
|
|
1091
|
+
raise TypeError(f"Sync 'run()' called for async tool '{self.name}'. Use 'a_run()'.")
|
|
1092
|
+
return self._process_run(
|
|
1093
|
+
task=task,
|
|
1094
|
+
context_variables=context_variables,
|
|
1095
|
+
validation_prompt_addition=validation_prompt_addition,
|
|
1096
|
+
messages=messages,
|
|
1097
|
+
ground_truth=ground_truth,
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
async def a_run(
|
|
1101
|
+
self,
|
|
1102
|
+
task: str,
|
|
1103
|
+
context_variables: Optional[ContextVariables] = None,
|
|
1104
|
+
validation_prompt_addition: Optional[str] = None,
|
|
1105
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1106
|
+
ground_truth: Optional[List[str]] = None,
|
|
1107
|
+
) -> Any:
|
|
1108
|
+
if not self._is_original_func_async:
|
|
1109
|
+
warnings.warn(
|
|
1110
|
+
f"Running sync function '{self._original_func_name}' wrapped by ReliableTool '{self.name}' "
|
|
1111
|
+
f"asynchronously using 'a_run()'. The underlying execution of _process_run will be synchronous "
|
|
1112
|
+
f"within an executor.",
|
|
1113
|
+
UserWarning,
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
loop = asyncio.get_running_loop()
|
|
1117
|
+
func_call = functools.partial(
|
|
1118
|
+
self._process_run,
|
|
1119
|
+
task=task,
|
|
1120
|
+
context_variables=context_variables,
|
|
1121
|
+
validation_prompt_addition=validation_prompt_addition,
|
|
1122
|
+
messages=messages,
|
|
1123
|
+
ground_truth=ground_truth,
|
|
1124
|
+
)
|
|
1125
|
+
return await loop.run_in_executor(None, func_call)
|
|
1126
|
+
|
|
1127
|
+
def _process_run_with_details(
|
|
1128
|
+
self,
|
|
1129
|
+
task: str,
|
|
1130
|
+
context_variables: Optional[ContextVariables] = None,
|
|
1131
|
+
validation_prompt_addition: Optional[str] = None,
|
|
1132
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1133
|
+
ground_truth: Optional[List[str]] = None,
|
|
1134
|
+
) -> ToolExecutionDetails:
|
|
1135
|
+
current_context_variables = context_variables if context_variables is not None else ContextVariables()
|
|
1136
|
+
if not isinstance(current_context_variables, ContextVariables):
|
|
1137
|
+
err_msg = f"Invalid ContextVariables type: {type(context_variables)}"
|
|
1138
|
+
# Create a minimal context for reporting
|
|
1139
|
+
err_ctx = ReliableToolContext(task=task, reliable_tool_name=self.name)
|
|
1140
|
+
err_ctx.attempts.append(ExecutionAttempt(error=f"Initialization error: {err_msg}"))
|
|
1141
|
+
return ToolExecutionDetails(
|
|
1142
|
+
task=task, is_overall_successful=False, failure_reason=err_msg, final_tool_context=err_ctx
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
tool_context_for_run: ReliableToolContext
|
|
1146
|
+
try:
|
|
1147
|
+
# Initialize or update tool context state. Raises on ser/de errors.
|
|
1148
|
+
tool_context_for_run = self._prepare_tool_context(
|
|
1149
|
+
task, current_context_variables, validation_prompt_addition, messages, ground_truth
|
|
1150
|
+
)
|
|
1151
|
+
except (ValueError, TypeError) as e_ctx_setup:
|
|
1152
|
+
err_msg = f"Error during ReliableToolContext setup: {e_ctx_setup}"
|
|
1153
|
+
logger.error("[%s] %s", self.name, err_msg, exc_info=True)
|
|
1154
|
+
err_ctx = ReliableToolContext(task=task, reliable_tool_name=self.name)
|
|
1155
|
+
err_ctx.attempts.append(ExecutionAttempt(error=f"Context setup error: {e_ctx_setup}"))
|
|
1156
|
+
return ToolExecutionDetails(
|
|
1157
|
+
task=task, is_overall_successful=False, failure_reason=err_msg, final_tool_context=err_ctx
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
# Variables for ToolExecutionDetails
|
|
1161
|
+
is_successful_val = False
|
|
1162
|
+
failure_reason_val = None
|
|
1163
|
+
successful_params_val = None
|
|
1164
|
+
final_tool_context_val: ReliableToolContext = tool_context_for_run # Start with prepared context
|
|
1165
|
+
|
|
1166
|
+
try:
|
|
1167
|
+
_, chat_context_variables, _ = self._execute_internal_group_chat(
|
|
1168
|
+
task=task,
|
|
1169
|
+
initial_context_vars=current_context_variables, # This contains the prepared tool_context_for_run
|
|
1170
|
+
dynamic_validation_str=validation_prompt_addition,
|
|
1171
|
+
)
|
|
1172
|
+
current_context_variables = chat_context_variables # Update with context from chat
|
|
1173
|
+
|
|
1174
|
+
final_tool_context_val = _get_reliable_tool_context(current_context_variables, self._context_variables_key)
|
|
1175
|
+
latest_attempt = final_tool_context_val.latest_attempt
|
|
1176
|
+
|
|
1177
|
+
if not latest_attempt:
|
|
1178
|
+
failure_reason_val = "Critical internal error: No execution attempt recorded after chat cycle."
|
|
1179
|
+
# final_tool_context_val already reflects this state if attempts list is empty
|
|
1180
|
+
elif latest_attempt.did_execute_successfully and latest_attempt.validation is None:
|
|
1181
|
+
logger.warning(
|
|
1182
|
+
"[%s]: Validation result missing after successful execution. Assuming validation failed.", self.name
|
|
1183
|
+
)
|
|
1184
|
+
latest_attempt.validation = ValidationResult(
|
|
1185
|
+
validation_result=False,
|
|
1186
|
+
justification="Validation result was not recorded after successful execution.",
|
|
1187
|
+
)
|
|
1188
|
+
_set_reliable_tool_context(
|
|
1189
|
+
current_context_variables, self._context_variables_key, final_tool_context_val
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
if final_tool_context_val.is_complete_and_successful:
|
|
1193
|
+
is_successful_val = True
|
|
1194
|
+
# latest_attempt must exist if is_complete_and_successful is true
|
|
1195
|
+
# Re-fetch or assert to help Mypy understand it's not None
|
|
1196
|
+
confirmed_latest_attempt = final_tool_context_val.latest_attempt
|
|
1197
|
+
assert confirmed_latest_attempt is not None, (
|
|
1198
|
+
"Internal logic error: is_complete_and_successful is True but latest_attempt is None"
|
|
1199
|
+
)
|
|
1200
|
+
successful_params_val = SuccessfulExecutionParameters(
|
|
1201
|
+
attempt_args=confirmed_latest_attempt.attempt_args,
|
|
1202
|
+
attempt_kwargs=confirmed_latest_attempt.attempt_kwargs,
|
|
1203
|
+
)
|
|
1204
|
+
else:
|
|
1205
|
+
failure_reason_val = final_tool_context_val.get_failure_summary()
|
|
1206
|
+
|
|
1207
|
+
except ReliableToolError as e:
|
|
1208
|
+
is_successful_val = False
|
|
1209
|
+
failure_reason_val = f"ReliableTool execution failed: {e}"
|
|
1210
|
+
logger.warning("[%s] %s", self.name, failure_reason_val) # Log the failure reason from ReliableToolError
|
|
1211
|
+
final_tool_context_val = e.final_context or final_tool_context_val # Use context from error if available
|
|
1212
|
+
if not final_tool_context_val.attempts: # Ensure some attempt is logged if context is minimal
|
|
1213
|
+
final_tool_context_val.attempts.append(ExecutionAttempt(error=str(e)))
|
|
1214
|
+
|
|
1215
|
+
except (KeyError, ValueError, TypeError) as e_ctx_final: # Context errors after chat
|
|
1216
|
+
is_successful_val = False
|
|
1217
|
+
failure_reason_val = f"Critical error involving context after chat: {e_ctx_final}"
|
|
1218
|
+
logger.error("[%s] %s", self.name, failure_reason_val, exc_info=True)
|
|
1219
|
+
try: # Try to get the latest context, otherwise use what we had
|
|
1220
|
+
final_tool_context_val = _get_reliable_tool_context(
|
|
1221
|
+
current_context_variables, self._context_variables_key
|
|
1222
|
+
)
|
|
1223
|
+
except (
|
|
1224
|
+
Exception
|
|
1225
|
+
): # If still fails, final_tool_context_val remains as tool_context_for_run or from a prior partial update
|
|
1226
|
+
if not final_tool_context_val.attempts or final_tool_context_val.attempts[-1].error is None:
|
|
1227
|
+
final_tool_context_val.attempts.append(ExecutionAttempt(error=failure_reason_val))
|
|
1228
|
+
|
|
1229
|
+
except Exception as e_unexp: # Unexpected errors during the process
|
|
1230
|
+
is_successful_val = False
|
|
1231
|
+
failure_reason_val = f"Unexpected error during reliable execution: {e_unexp}"
|
|
1232
|
+
logger.error("[%s] %s", self.name, failure_reason_val, exc_info=True)
|
|
1233
|
+
try: # Try to get the latest context
|
|
1234
|
+
final_tool_context_val = _get_reliable_tool_context(
|
|
1235
|
+
current_context_variables, self._context_variables_key
|
|
1236
|
+
)
|
|
1237
|
+
except (
|
|
1238
|
+
Exception
|
|
1239
|
+
): # If still fails, final_tool_context_val remains as tool_context_for_run or from a prior partial update
|
|
1240
|
+
if not final_tool_context_val.attempts or final_tool_context_val.attempts[-1].error is None:
|
|
1241
|
+
final_tool_context_val.attempts.append(ExecutionAttempt(error=failure_reason_val))
|
|
1242
|
+
|
|
1243
|
+
return ToolExecutionDetails(
|
|
1244
|
+
task=task,
|
|
1245
|
+
is_overall_successful=is_successful_val,
|
|
1246
|
+
failure_reason=failure_reason_val,
|
|
1247
|
+
successful_parameters=successful_params_val,
|
|
1248
|
+
final_tool_context=final_tool_context_val,
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
def run_and_get_details(
|
|
1252
|
+
self,
|
|
1253
|
+
task: str,
|
|
1254
|
+
context_variables: Optional[ContextVariables] = None,
|
|
1255
|
+
validation_prompt_addition: Optional[str] = None,
|
|
1256
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1257
|
+
ground_truth: Optional[List[str]] = None,
|
|
1258
|
+
) -> ToolExecutionDetails:
|
|
1259
|
+
if self._is_original_func_async:
|
|
1260
|
+
raise TypeError(
|
|
1261
|
+
f"Synchronous 'run_and_get_details()' called for an async tool '{self.name}'. "
|
|
1262
|
+
f"Use 'a_run_and_get_details()' instead."
|
|
1263
|
+
)
|
|
1264
|
+
return self._process_run_with_details(
|
|
1265
|
+
task=task,
|
|
1266
|
+
context_variables=context_variables,
|
|
1267
|
+
validation_prompt_addition=validation_prompt_addition,
|
|
1268
|
+
messages=messages,
|
|
1269
|
+
ground_truth=ground_truth,
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
async def a_run_and_get_details(
|
|
1273
|
+
self,
|
|
1274
|
+
task: str,
|
|
1275
|
+
context_variables: Optional[ContextVariables] = None,
|
|
1276
|
+
validation_prompt_addition: Optional[str] = None,
|
|
1277
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1278
|
+
ground_truth: Optional[List[str]] = None,
|
|
1279
|
+
) -> ToolExecutionDetails:
|
|
1280
|
+
if not self._is_original_func_async:
|
|
1281
|
+
warnings.warn(
|
|
1282
|
+
f"Running sync function '{self._original_func_name}' (wrapped by ReliableTool '{self.name}') "
|
|
1283
|
+
f"asynchronously using 'a_run_and_get_details()'. The underlying execution will be synchronous "
|
|
1284
|
+
f"within an executor.",
|
|
1285
|
+
UserWarning,
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
loop = asyncio.get_running_loop()
|
|
1289
|
+
try:
|
|
1290
|
+
func_call = functools.partial(
|
|
1291
|
+
self._process_run_with_details,
|
|
1292
|
+
task=task,
|
|
1293
|
+
context_variables=context_variables,
|
|
1294
|
+
validation_prompt_addition=validation_prompt_addition,
|
|
1295
|
+
messages=messages,
|
|
1296
|
+
ground_truth=ground_truth,
|
|
1297
|
+
)
|
|
1298
|
+
details: ToolExecutionDetails = await loop.run_in_executor(None, func_call)
|
|
1299
|
+
return details
|
|
1300
|
+
except Exception as e:
|
|
1301
|
+
logger.critical(
|
|
1302
|
+
"[%s] a_run_and_get_details encountered an unhandled exception from executor: %s",
|
|
1303
|
+
self.name,
|
|
1304
|
+
e,
|
|
1305
|
+
exc_info=True,
|
|
1306
|
+
)
|
|
1307
|
+
fallback_ctx = ReliableToolContext(task=task, reliable_tool_name=self.name)
|
|
1308
|
+
fallback_ctx.attempts.append(
|
|
1309
|
+
ExecutionAttempt(error=f"Unhandled executor/process error: {type(e).__name__}: {e}")
|
|
1310
|
+
)
|
|
1311
|
+
return ToolExecutionDetails(
|
|
1312
|
+
task=task,
|
|
1313
|
+
is_overall_successful=False,
|
|
1314
|
+
failure_reason=f"Critical unhandled exception during async execution: {type(e).__name__}: {e}",
|
|
1315
|
+
final_tool_context=fallback_ctx,
|
|
1316
|
+
)
|