ag2 0.9.1__py3-none-any.whl → 0.9.1.post0__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.1.dist-info → ag2-0.9.1.post0.dist-info}/METADATA +264 -73
- ag2-0.9.1.post0.dist-info/RECORD +392 -0
- {ag2-0.9.1.dist-info → ag2-0.9.1.post0.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 +4020 -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 +1010 -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 +113 -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 +379 -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/mcp_client.py +208 -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 +1435 -0
- autogen/oai/client_utils.py +169 -0
- autogen/oai/cohere.py +479 -0
- autogen/oai/gemini.py +990 -0
- autogen/oai/gemini_types.py +129 -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 +43 -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/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
- ag2-0.9.1.dist-info/RECORD +0 -6
- ag2-0.9.1.dist-info/top_level.txt +0 -1
- {ag2-0.9.1.dist-info → ag2-0.9.1.post0.dist-info/licenses}/LICENSE +0 -0
- {ag2-0.9.1.dist-info → ag2-0.9.1.post0.dist-info/licenses}/NOTICE.md +0 -0
|
@@ -0,0 +1,1694 @@
|
|
|
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
|
+
import copy
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import random
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Callable, Literal, Optional, Union
|
|
15
|
+
|
|
16
|
+
from ..code_utils import content_str
|
|
17
|
+
from ..doc_utils import export_module
|
|
18
|
+
from ..events.agent_events import (
|
|
19
|
+
ClearAgentsHistoryEvent,
|
|
20
|
+
GroupChatResumeEvent,
|
|
21
|
+
GroupChatRunChatEvent,
|
|
22
|
+
SelectSpeakerEvent,
|
|
23
|
+
SelectSpeakerInvalidInputEvent,
|
|
24
|
+
SelectSpeakerTryCountExceededEvent,
|
|
25
|
+
SpeakerAttemptFailedMultipleAgentsEvent,
|
|
26
|
+
SpeakerAttemptFailedNoAgentsEvent,
|
|
27
|
+
SpeakerAttemptSuccessfulEvent,
|
|
28
|
+
TerminationEvent,
|
|
29
|
+
)
|
|
30
|
+
from ..exception_utils import AgentNameConflictError, NoEligibleSpeakerError, UndefinedNextAgentError
|
|
31
|
+
from ..graph_utils import check_graph_validity, invert_disallowed_to_allowed
|
|
32
|
+
from ..io.base import IOStream
|
|
33
|
+
from ..llm_config import LLMConfig
|
|
34
|
+
from ..oai.client import ModelClient
|
|
35
|
+
from ..runtime_logging import log_new_agent, logging_enabled
|
|
36
|
+
from .agent import Agent
|
|
37
|
+
from .contrib.capabilities import transform_messages
|
|
38
|
+
from .conversable_agent import ConversableAgent
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
SELECT_SPEAKER_PROMPT_TEMPLATE = (
|
|
43
|
+
"Read the above conversation. Then select the next role from {agentlist} to play. Only return the role."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
@export_module("autogen")
|
|
49
|
+
class GroupChat:
|
|
50
|
+
"""(In preview) A group chat class that contains the following data fields:
|
|
51
|
+
- agents: a list of participating agents.
|
|
52
|
+
- messages: a list of messages in the group chat.
|
|
53
|
+
- max_round: the maximum number of rounds.
|
|
54
|
+
- admin_name: the name of the admin agent if there is one. Default is "Admin".
|
|
55
|
+
KeyBoardInterrupt will make the admin agent take over.
|
|
56
|
+
- func_call_filter: whether to enforce function call filter. Default is True.
|
|
57
|
+
When set to True and when a message is a function call suggestion,
|
|
58
|
+
the next speaker will be chosen from an agent which contains the corresponding function name
|
|
59
|
+
in its `function_map`.
|
|
60
|
+
- select_speaker_message_template: customize the select speaker message (used in "auto" speaker selection), which appears first in the message context and generally includes the agent descriptions and list of agents. If the string contains "`{roles}`" it will replaced with the agent's and their role descriptions. If the string contains "`{agentlist}`" it will be replaced with a comma-separated list of agent names in square brackets. The default value is:
|
|
61
|
+
"You are in a role play game. The following roles are available:
|
|
62
|
+
`{roles}`.
|
|
63
|
+
Read the following conversation.
|
|
64
|
+
Then select the next role from `{agentlist}` to play. Only return the role."
|
|
65
|
+
- select_speaker_prompt_template: customize the select speaker prompt (used in "auto" speaker selection), which appears last in the message context and generally includes the list of agents and guidance for the LLM to select the next agent. If the string contains "`{agentlist}`" it will be replaced with a comma-separated list of agent names in square brackets. The default value is:
|
|
66
|
+
"Read the above conversation. Then select the next role from `{agentlist}` to play. Only return the role."
|
|
67
|
+
To ignore this prompt being used, set this to None. If set to None, ensure your instructions for selecting a speaker are in the select_speaker_message_template string.
|
|
68
|
+
- select_speaker_auto_multiple_template: customize the follow-up prompt used when selecting a speaker fails with a response that contains multiple agent names. This prompt guides the LLM to return just one agent name. Applies only to "auto" speaker selection method. If the string contains "`{agentlist}`" it will be replaced with a comma-separated list of agent names in square brackets. The default value is:
|
|
69
|
+
"You provided more than one name in your text, please return just the name of the next speaker. To determine the speaker use these prioritised rules:
|
|
70
|
+
1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name
|
|
71
|
+
2. If it refers to the "next" speaker name, choose that name
|
|
72
|
+
3. Otherwise, choose the first provided speaker's name in the context
|
|
73
|
+
The names are case-sensitive and should not be abbreviated or changed.
|
|
74
|
+
Respond with ONLY the name of the speaker and DO NOT provide a reason."
|
|
75
|
+
- select_speaker_auto_none_template: customize the follow-up prompt used when selecting a speaker fails with a response that contains no agent names. This prompt guides the LLM to return an agent name and provides a list of agent names. Applies only to "auto" speaker selection method. If the string contains "`{agentlist}`" it will be replaced with a comma-separated list of agent names in square brackets. The default value is:
|
|
76
|
+
"You didn't choose a speaker. As a reminder, to determine the speaker use these prioritised rules:
|
|
77
|
+
1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name
|
|
78
|
+
2. If it refers to the "next" speaker name, choose that name
|
|
79
|
+
3. Otherwise, choose the first provided speaker's name in the context
|
|
80
|
+
The names are case-sensitive and should not be abbreviated or changed.
|
|
81
|
+
The only names that are accepted are `{agentlist}`.
|
|
82
|
+
Respond with ONLY the name of the speaker and DO NOT provide a reason."
|
|
83
|
+
- speaker_selection_method: the method for selecting the next speaker. Default is "auto".
|
|
84
|
+
Could be any of the following (case insensitive), will raise ValueError if not recognized:
|
|
85
|
+
- "auto": the next speaker is selected automatically by LLM.
|
|
86
|
+
- "manual": the next speaker is selected manually by user input.
|
|
87
|
+
- "random": the next speaker is selected randomly.
|
|
88
|
+
- "round_robin": the next speaker is selected in a round robin fashion, i.e., iterating in the same order as provided in `agents`.
|
|
89
|
+
- a customized speaker selection function (Callable): the function will be called to select the next speaker.
|
|
90
|
+
The function should take the last speaker and the group chat as input and return one of the following:
|
|
91
|
+
1. an `Agent` class, it must be one of the agents in the group chat.
|
|
92
|
+
2. a string from ['auto', 'manual', 'random', 'round_robin'] to select a default method to use.
|
|
93
|
+
3. None, which would terminate the conversation gracefully.
|
|
94
|
+
```python
|
|
95
|
+
def custom_speaker_selection_func(
|
|
96
|
+
last_speaker: Agent, groupchat: GroupChat
|
|
97
|
+
) -> Union[Agent, str, None]:
|
|
98
|
+
```
|
|
99
|
+
- max_retries_for_selecting_speaker: the maximum number of times the speaker selection requery process will run.
|
|
100
|
+
If, during speaker selection, multiple agent names or no agent names are returned by the LLM as the next agent, it will be queried again up to the maximum number
|
|
101
|
+
of times until a single agent is returned or it exhausts the maximum attempts.
|
|
102
|
+
Applies only to "auto" speaker selection method.
|
|
103
|
+
Default is 2.
|
|
104
|
+
- select_speaker_transform_messages: (optional) the message transformations to apply to the nested select speaker agent-to-agent chat messages.
|
|
105
|
+
Takes a TransformMessages object, defaults to None and is only utilised when the speaker selection method is "auto".
|
|
106
|
+
- select_speaker_auto_verbose: whether to output the select speaker responses and selections
|
|
107
|
+
If set to True, the outputs from the two agents in the nested select speaker chat will be output, along with
|
|
108
|
+
whether the responses were successful, or not, in selecting an agent
|
|
109
|
+
Applies only to "auto" speaker selection method.
|
|
110
|
+
- allow_repeat_speaker: whether to allow the same speaker to speak consecutively.
|
|
111
|
+
Default is True, in which case all speakers are allowed to speak consecutively.
|
|
112
|
+
If `allow_repeat_speaker` is a list of Agents, then only those listed agents are allowed to repeat.
|
|
113
|
+
If set to False, then no speakers are allowed to repeat.
|
|
114
|
+
`allow_repeat_speaker` and `allowed_or_disallowed_speaker_transitions` are mutually exclusive.
|
|
115
|
+
- allowed_or_disallowed_speaker_transitions: dict.
|
|
116
|
+
The keys are source agents, and the values are agents that the key agent can/can't transit to,
|
|
117
|
+
depending on speaker_transitions_type. Default is None, which means all agents can transit to all other agents.
|
|
118
|
+
`allow_repeat_speaker` and `allowed_or_disallowed_speaker_transitions` are mutually exclusive.
|
|
119
|
+
- speaker_transitions_type: whether the speaker_transitions_type is a dictionary containing lists of allowed agents or disallowed agents.
|
|
120
|
+
"allowed" means the `allowed_or_disallowed_speaker_transitions` is a dictionary containing lists of allowed agents.
|
|
121
|
+
If set to "disallowed", then the `allowed_or_disallowed_speaker_transitions` is a dictionary containing lists of disallowed agents.
|
|
122
|
+
Must be supplied if `allowed_or_disallowed_speaker_transitions` is not None.
|
|
123
|
+
- enable_clear_history: enable possibility to clear history of messages for agents manually by providing
|
|
124
|
+
"clear history" phrase in user prompt. This is experimental feature.
|
|
125
|
+
See description of GroupChatManager.clear_agents_history function for more info.
|
|
126
|
+
- send_introductions: send a round of introductions at the start of the group chat, so agents know who they can speak to (default: False)
|
|
127
|
+
- select_speaker_auto_model_client_cls: Custom model client class for the internal speaker select agent used during 'auto' speaker selection (optional)
|
|
128
|
+
- select_speaker_auto_llm_config: LLM config for the internal speaker select agent used during 'auto' speaker selection (optional)
|
|
129
|
+
- role_for_select_speaker_messages: sets the role name for speaker selection when in 'auto' mode, typically 'user' or 'system'. (default: 'system')
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
agents: list[Agent]
|
|
133
|
+
messages: list[dict[str, Any]] = field(default_factory=list)
|
|
134
|
+
max_round: int = 10
|
|
135
|
+
admin_name: str = "Admin"
|
|
136
|
+
func_call_filter: bool = True
|
|
137
|
+
speaker_selection_method: Union[Literal["auto", "manual", "random", "round_robin"], Callable[..., Any]] = "auto"
|
|
138
|
+
max_retries_for_selecting_speaker: int = 2
|
|
139
|
+
allow_repeat_speaker: Optional[Union[bool, list[Agent]]] = None
|
|
140
|
+
allowed_or_disallowed_speaker_transitions: Optional[dict[str, Any]] = None
|
|
141
|
+
speaker_transitions_type: Literal["allowed", "disallowed", None] = None
|
|
142
|
+
enable_clear_history: bool = False
|
|
143
|
+
send_introductions: bool = False
|
|
144
|
+
select_speaker_message_template: str = """You are in a role play game. The following roles are available:
|
|
145
|
+
{roles}.
|
|
146
|
+
Read the following conversation.
|
|
147
|
+
Then select the next role from {agentlist} to play. Only return the role."""
|
|
148
|
+
select_speaker_prompt_template: str = SELECT_SPEAKER_PROMPT_TEMPLATE
|
|
149
|
+
select_speaker_auto_multiple_template: str = """You provided more than one name in your text, please return just the name of the next speaker. To determine the speaker use these prioritised rules:
|
|
150
|
+
1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name
|
|
151
|
+
2. If it refers to the "next" speaker name, choose that name
|
|
152
|
+
3. Otherwise, choose the first provided speaker's name in the context
|
|
153
|
+
The names are case-sensitive and should not be abbreviated or changed.
|
|
154
|
+
Respond with ONLY the name of the speaker and DO NOT provide a reason."""
|
|
155
|
+
select_speaker_auto_none_template: str = """You didn't choose a speaker. As a reminder, to determine the speaker use these prioritised rules:
|
|
156
|
+
1. If the context refers to themselves as a speaker e.g. "As the..." , choose that speaker's name
|
|
157
|
+
2. If it refers to the "next" speaker name, choose that name
|
|
158
|
+
3. Otherwise, choose the first provided speaker's name in the context
|
|
159
|
+
The names are case-sensitive and should not be abbreviated or changed.
|
|
160
|
+
The only names that are accepted are {agentlist}.
|
|
161
|
+
Respond with ONLY the name of the speaker and DO NOT provide a reason."""
|
|
162
|
+
select_speaker_transform_messages: Optional[transform_messages.TransformMessages] = None
|
|
163
|
+
select_speaker_auto_verbose: Optional[bool] = False
|
|
164
|
+
select_speaker_auto_model_client_cls: Optional[Union[ModelClient, list[ModelClient]]] = None
|
|
165
|
+
select_speaker_auto_llm_config: Optional[Union[LLMConfig, dict[str, Any], Literal[False]]] = None
|
|
166
|
+
role_for_select_speaker_messages: Optional[str] = "system"
|
|
167
|
+
|
|
168
|
+
_VALID_SPEAKER_SELECTION_METHODS = ["auto", "manual", "random", "round_robin"]
|
|
169
|
+
_VALID_SPEAKER_TRANSITIONS_TYPE = ["allowed", "disallowed", None]
|
|
170
|
+
|
|
171
|
+
# Define a class attribute for the default introduction message
|
|
172
|
+
DEFAULT_INTRO_MSG = (
|
|
173
|
+
"Hello everyone. We have assembled a great team today to answer questions and solve tasks. In attendance are:"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
allowed_speaker_transitions_dict: dict[str, list[Agent]] = field(init=False)
|
|
177
|
+
|
|
178
|
+
def __post_init__(self):
|
|
179
|
+
# Post init steers clears of the automatically generated __init__ method from dataclass
|
|
180
|
+
|
|
181
|
+
if self.allow_repeat_speaker is not None and not isinstance(self.allow_repeat_speaker, (bool, list)):
|
|
182
|
+
raise ValueError("GroupChat allow_repeat_speaker should be a bool or a list of Agents.")
|
|
183
|
+
|
|
184
|
+
# Here, we create allowed_speaker_transitions_dict from the supplied allowed_or_disallowed_speaker_transitions and speaker_transitions_type, and lastly checks for validity.
|
|
185
|
+
|
|
186
|
+
# Check input
|
|
187
|
+
if self.speaker_transitions_type is not None:
|
|
188
|
+
self.speaker_transitions_type = self.speaker_transitions_type.lower()
|
|
189
|
+
|
|
190
|
+
if self.speaker_transitions_type not in self._VALID_SPEAKER_TRANSITIONS_TYPE:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"GroupChat speaker_transitions_type is set to '{self.speaker_transitions_type}'. "
|
|
193
|
+
f"It should be one of {self._VALID_SPEAKER_TRANSITIONS_TYPE} (case insensitive). "
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# If both self.allowed_or_disallowed_speaker_transitions is None and self.allow_repeat_speaker is None, set allow_repeat_speaker to True to ensure backward compatibility
|
|
197
|
+
# Discussed in https://github.com/microsoft/autogen/pull/857#discussion_r1451541204
|
|
198
|
+
if self.allowed_or_disallowed_speaker_transitions is None and self.allow_repeat_speaker is None:
|
|
199
|
+
self.allow_repeat_speaker = True
|
|
200
|
+
|
|
201
|
+
# self.allowed_or_disallowed_speaker_transitions and self.allow_repeat_speaker are mutually exclusive parameters.
|
|
202
|
+
# Discussed in https://github.com/microsoft/autogen/pull/857#discussion_r1451266661
|
|
203
|
+
if self.allowed_or_disallowed_speaker_transitions is not None and self.allow_repeat_speaker is not None:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
"Don't provide both allowed_or_disallowed_speaker_transitions and allow_repeat_speaker in group chat. "
|
|
206
|
+
"Please set one of them to None."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Asks the user to specify whether the speaker_transitions_type is allowed or disallowed if speaker_transitions_type is supplied
|
|
210
|
+
# Discussed in https://github.com/microsoft/autogen/pull/857#discussion_r1451259524
|
|
211
|
+
if self.allowed_or_disallowed_speaker_transitions is not None and self.speaker_transitions_type is None:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
"GroupChat allowed_or_disallowed_speaker_transitions is not None, but speaker_transitions_type is None. "
|
|
214
|
+
"Please set speaker_transitions_type to either 'allowed' or 'disallowed'."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Inferring self.allowed_speaker_transitions_dict
|
|
218
|
+
# Create self.allowed_speaker_transitions_dict if allowed_or_disallowed_speaker_transitions is None, using allow_repeat_speaker
|
|
219
|
+
if self.allowed_or_disallowed_speaker_transitions is None:
|
|
220
|
+
self.allowed_speaker_transitions_dict = {}
|
|
221
|
+
|
|
222
|
+
# Create a fully connected allowed_speaker_transitions_dict not including self loops
|
|
223
|
+
for agent in self.agents:
|
|
224
|
+
self.allowed_speaker_transitions_dict[agent] = [
|
|
225
|
+
other_agent for other_agent in self.agents if other_agent != agent
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
# If self.allow_repeat_speaker is True, add self loops to all agents
|
|
229
|
+
if self.allow_repeat_speaker is True:
|
|
230
|
+
for agent in self.agents:
|
|
231
|
+
self.allowed_speaker_transitions_dict[agent].append(agent)
|
|
232
|
+
|
|
233
|
+
# Else if self.allow_repeat_speaker is a list of Agents, add self loops to the agents in the list
|
|
234
|
+
elif isinstance(self.allow_repeat_speaker, list):
|
|
235
|
+
for agent in self.allow_repeat_speaker:
|
|
236
|
+
self.allowed_speaker_transitions_dict[agent].append(agent)
|
|
237
|
+
|
|
238
|
+
# Create self.allowed_speaker_transitions_dict if allowed_or_disallowed_speaker_transitions is not None, using allowed_or_disallowed_speaker_transitions
|
|
239
|
+
else:
|
|
240
|
+
# Process based on speaker_transitions_type
|
|
241
|
+
if self.speaker_transitions_type == "allowed":
|
|
242
|
+
self.allowed_speaker_transitions_dict = self.allowed_or_disallowed_speaker_transitions
|
|
243
|
+
else:
|
|
244
|
+
# Logic for processing disallowed allowed_or_disallowed_speaker_transitions to allowed_speaker_transitions_dict
|
|
245
|
+
self.allowed_speaker_transitions_dict = invert_disallowed_to_allowed(
|
|
246
|
+
self.allowed_or_disallowed_speaker_transitions, self.agents
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Check for validity
|
|
250
|
+
check_graph_validity(
|
|
251
|
+
allowed_speaker_transitions_dict=self.allowed_speaker_transitions_dict,
|
|
252
|
+
agents=self.agents,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Check select speaker messages, prompts, roles, and retries have values
|
|
256
|
+
if self.select_speaker_message_template is None or len(self.select_speaker_message_template) == 0:
|
|
257
|
+
raise ValueError("select_speaker_message_template cannot be empty or None.")
|
|
258
|
+
|
|
259
|
+
if self.select_speaker_prompt_template is not None and len(self.select_speaker_prompt_template) == 0:
|
|
260
|
+
self.select_speaker_prompt_template = None
|
|
261
|
+
|
|
262
|
+
if self.role_for_select_speaker_messages is None or len(self.role_for_select_speaker_messages) == 0:
|
|
263
|
+
raise ValueError("role_for_select_speaker_messages cannot be empty or None.")
|
|
264
|
+
|
|
265
|
+
if self.select_speaker_auto_multiple_template is None or len(self.select_speaker_auto_multiple_template) == 0:
|
|
266
|
+
raise ValueError("select_speaker_auto_multiple_template cannot be empty or None.")
|
|
267
|
+
|
|
268
|
+
if self.select_speaker_auto_none_template is None or len(self.select_speaker_auto_none_template) == 0:
|
|
269
|
+
raise ValueError("select_speaker_auto_none_template cannot be empty or None.")
|
|
270
|
+
|
|
271
|
+
if self.max_retries_for_selecting_speaker is None or len(self.role_for_select_speaker_messages) == 0:
|
|
272
|
+
raise ValueError("role_for_select_speaker_messages cannot be empty or None.")
|
|
273
|
+
|
|
274
|
+
# Validate max select speakers retries
|
|
275
|
+
if self.max_retries_for_selecting_speaker is None or not isinstance(
|
|
276
|
+
self.max_retries_for_selecting_speaker, int
|
|
277
|
+
):
|
|
278
|
+
raise ValueError("max_retries_for_selecting_speaker cannot be None or non-int")
|
|
279
|
+
elif self.max_retries_for_selecting_speaker < 0:
|
|
280
|
+
raise ValueError("max_retries_for_selecting_speaker must be greater than or equal to zero")
|
|
281
|
+
|
|
282
|
+
# Load message transforms here (load once for the Group Chat so we don't have to re-initiate it and it maintains the cache across subsequent select speaker calls)
|
|
283
|
+
if self.select_speaker_transform_messages is not None:
|
|
284
|
+
if isinstance(self.select_speaker_transform_messages, transform_messages.TransformMessages):
|
|
285
|
+
self._speaker_selection_transforms = self.select_speaker_transform_messages
|
|
286
|
+
else:
|
|
287
|
+
raise ValueError("select_speaker_transform_messages must be None or MessageTransforms.")
|
|
288
|
+
else:
|
|
289
|
+
self._speaker_selection_transforms = None
|
|
290
|
+
|
|
291
|
+
# Validate select_speaker_auto_verbose
|
|
292
|
+
if self.select_speaker_auto_verbose is None or not isinstance(self.select_speaker_auto_verbose, bool):
|
|
293
|
+
raise ValueError("select_speaker_auto_verbose cannot be None or non-bool")
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def agent_names(self) -> list[str]:
|
|
297
|
+
"""Return the names of the agents in the group chat."""
|
|
298
|
+
return [agent.name for agent in self.agents]
|
|
299
|
+
|
|
300
|
+
def reset(self):
|
|
301
|
+
"""Reset the group chat."""
|
|
302
|
+
self.messages.clear()
|
|
303
|
+
|
|
304
|
+
def append(self, message: dict[str, Any], speaker: Agent):
|
|
305
|
+
"""Append a message to the group chat.
|
|
306
|
+
We cast the content to str here so that it can be managed by text-based
|
|
307
|
+
model.
|
|
308
|
+
"""
|
|
309
|
+
# set the name to speaker's name if the role is not function
|
|
310
|
+
# if the role is tool, it is OK to modify the name
|
|
311
|
+
if message["role"] != "function":
|
|
312
|
+
message["name"] = speaker.name
|
|
313
|
+
if not isinstance(message["content"], str) and not isinstance(message["content"], list):
|
|
314
|
+
message["content"] = str(message["content"])
|
|
315
|
+
message["content"] = content_str(message["content"])
|
|
316
|
+
self.messages.append(message)
|
|
317
|
+
|
|
318
|
+
def agent_by_name(
|
|
319
|
+
self, name: str, recursive: bool = False, raise_on_name_conflict: bool = False
|
|
320
|
+
) -> Optional[Agent]:
|
|
321
|
+
"""Returns the agent with a given name. If recursive is True, it will search in nested teams."""
|
|
322
|
+
agents = self.nested_agents() if recursive else self.agents
|
|
323
|
+
filtered_agents = [agent for agent in agents if agent.name == name]
|
|
324
|
+
|
|
325
|
+
if raise_on_name_conflict and len(filtered_agents) > 1:
|
|
326
|
+
raise AgentNameConflictError()
|
|
327
|
+
|
|
328
|
+
return filtered_agents[0] if filtered_agents else None
|
|
329
|
+
|
|
330
|
+
def nested_agents(self) -> list[Agent]:
|
|
331
|
+
"""Returns all agents in the group chat manager."""
|
|
332
|
+
agents = self.agents.copy()
|
|
333
|
+
for agent in agents:
|
|
334
|
+
if isinstance(agent, GroupChatManager):
|
|
335
|
+
# Recursive call for nested teams
|
|
336
|
+
agents.extend(agent.groupchat.nested_agents())
|
|
337
|
+
return agents
|
|
338
|
+
|
|
339
|
+
def next_agent(self, agent: Agent, agents: Optional[list[Agent]] = None) -> Agent:
|
|
340
|
+
"""Return the next agent in the list."""
|
|
341
|
+
if agents is None:
|
|
342
|
+
agents = self.agents
|
|
343
|
+
|
|
344
|
+
# Ensure the provided list of agents is a subset of self.agents
|
|
345
|
+
if not set(agents).issubset(set(self.agents)):
|
|
346
|
+
raise UndefinedNextAgentError()
|
|
347
|
+
|
|
348
|
+
# What index is the agent? (-1 if not present)
|
|
349
|
+
idx = self.agent_names.index(agent.name) if agent.name in self.agent_names else -1
|
|
350
|
+
|
|
351
|
+
# Return the next agent
|
|
352
|
+
if agents == self.agents:
|
|
353
|
+
return agents[(idx + 1) % len(agents)]
|
|
354
|
+
else:
|
|
355
|
+
offset = idx + 1
|
|
356
|
+
for i in range(len(self.agents)):
|
|
357
|
+
if self.agents[(offset + i) % len(self.agents)] in agents:
|
|
358
|
+
return self.agents[(offset + i) % len(self.agents)]
|
|
359
|
+
|
|
360
|
+
# Explicitly handle cases where no valid next agent exists in the provided subset.
|
|
361
|
+
raise UndefinedNextAgentError()
|
|
362
|
+
|
|
363
|
+
def select_speaker_msg(self, agents: Optional[list[Agent]] = None) -> str:
|
|
364
|
+
"""Return the system message for selecting the next speaker. This is always the *first* message in the context."""
|
|
365
|
+
if agents is None:
|
|
366
|
+
agents = self.agents
|
|
367
|
+
|
|
368
|
+
roles = self._participant_roles(agents)
|
|
369
|
+
agentlist = f"{[agent.name for agent in agents]}"
|
|
370
|
+
|
|
371
|
+
return_msg = self.select_speaker_message_template.format(roles=roles, agentlist=agentlist)
|
|
372
|
+
return return_msg
|
|
373
|
+
|
|
374
|
+
def select_speaker_prompt(self, agents: Optional[list[Agent]] = None) -> str:
|
|
375
|
+
"""Return the floating system prompt selecting the next speaker.
|
|
376
|
+
This is always the *last* message in the context.
|
|
377
|
+
Will return None if the select_speaker_prompt_template is None.
|
|
378
|
+
"""
|
|
379
|
+
if self.select_speaker_prompt_template is None:
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
if agents is None:
|
|
383
|
+
agents = self.agents
|
|
384
|
+
|
|
385
|
+
agentlist = f"{[agent.name for agent in agents]}"
|
|
386
|
+
|
|
387
|
+
return_prompt = f"{self.select_speaker_prompt_template}".replace("{agentlist}", agentlist)
|
|
388
|
+
return return_prompt
|
|
389
|
+
|
|
390
|
+
def introductions_msg(self, agents: Optional[list[Agent]] = None) -> str:
|
|
391
|
+
"""Return the system message for selecting the next speaker. This is always the *first* message in the context."""
|
|
392
|
+
if agents is None:
|
|
393
|
+
agents = self.agents
|
|
394
|
+
|
|
395
|
+
# Use the class attribute instead of a hardcoded string
|
|
396
|
+
intro_msg = self.DEFAULT_INTRO_MSG
|
|
397
|
+
participant_roles = self._participant_roles(agents)
|
|
398
|
+
|
|
399
|
+
return f"{intro_msg}\n\n{participant_roles}"
|
|
400
|
+
|
|
401
|
+
def manual_select_speaker(self, agents: Optional[list[Agent]] = None) -> Union[Agent, None]:
|
|
402
|
+
"""Manually select the next speaker."""
|
|
403
|
+
iostream = IOStream.get_default()
|
|
404
|
+
|
|
405
|
+
if agents is None:
|
|
406
|
+
agents = self.agents
|
|
407
|
+
|
|
408
|
+
iostream.send(SelectSpeakerEvent(agents=agents))
|
|
409
|
+
|
|
410
|
+
try_count = 0
|
|
411
|
+
# Assume the user will enter a valid number within 3 tries, otherwise use auto selection to avoid blocking.
|
|
412
|
+
while try_count <= 3:
|
|
413
|
+
try_count += 1
|
|
414
|
+
if try_count >= 3:
|
|
415
|
+
iostream.send(SelectSpeakerTryCountExceededEvent(try_count=try_count, agents=agents))
|
|
416
|
+
break
|
|
417
|
+
try:
|
|
418
|
+
i = iostream.input(
|
|
419
|
+
"Enter the number of the next speaker (enter nothing or `q` to use auto selection): "
|
|
420
|
+
)
|
|
421
|
+
if i == "" or i == "q":
|
|
422
|
+
break
|
|
423
|
+
i = int(i)
|
|
424
|
+
if i > 0 and i <= len(agents):
|
|
425
|
+
return agents[i - 1]
|
|
426
|
+
else:
|
|
427
|
+
raise ValueError
|
|
428
|
+
except ValueError:
|
|
429
|
+
iostream.send(SelectSpeakerInvalidInputEvent(agents=agents))
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
def random_select_speaker(self, agents: Optional[list[Agent]] = None) -> Union[Agent, None]:
|
|
433
|
+
"""Randomly select the next speaker."""
|
|
434
|
+
if agents is None:
|
|
435
|
+
agents = self.agents
|
|
436
|
+
return random.choice(agents)
|
|
437
|
+
|
|
438
|
+
def _prepare_and_select_agents(
|
|
439
|
+
self,
|
|
440
|
+
last_speaker: Agent,
|
|
441
|
+
) -> tuple[Optional[Agent], list[Agent], Optional[list[dict[str, Any]]]]:
|
|
442
|
+
# If self.speaker_selection_method is a callable, call it to get the next speaker.
|
|
443
|
+
# If self.speaker_selection_method is a string, return it.
|
|
444
|
+
speaker_selection_method = self.speaker_selection_method
|
|
445
|
+
if isinstance(self.speaker_selection_method, Callable):
|
|
446
|
+
selected_agent = self.speaker_selection_method(last_speaker, self)
|
|
447
|
+
if selected_agent is None:
|
|
448
|
+
raise NoEligibleSpeakerError(
|
|
449
|
+
"Custom speaker selection function returned None. Terminating conversation."
|
|
450
|
+
)
|
|
451
|
+
elif isinstance(selected_agent, Agent):
|
|
452
|
+
if selected_agent in self.agents:
|
|
453
|
+
return selected_agent, self.agents, None
|
|
454
|
+
else:
|
|
455
|
+
raise ValueError(
|
|
456
|
+
f"Custom speaker selection function returned an agent {selected_agent.name} not in the group chat."
|
|
457
|
+
)
|
|
458
|
+
elif isinstance(selected_agent, str):
|
|
459
|
+
# If returned a string, assume it is a speaker selection method
|
|
460
|
+
speaker_selection_method = selected_agent
|
|
461
|
+
else:
|
|
462
|
+
raise ValueError(
|
|
463
|
+
f"Custom speaker selection function returned an object of type {type(selected_agent)} instead of Agent or str."
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if speaker_selection_method.lower() not in self._VALID_SPEAKER_SELECTION_METHODS:
|
|
467
|
+
raise ValueError(
|
|
468
|
+
f"GroupChat speaker_selection_method is set to '{speaker_selection_method}'. "
|
|
469
|
+
f"It should be one of {self._VALID_SPEAKER_SELECTION_METHODS} (case insensitive). "
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
# If provided a list, make sure the agent is in the list
|
|
473
|
+
allow_repeat_speaker = (
|
|
474
|
+
self.allow_repeat_speaker
|
|
475
|
+
if isinstance(self.allow_repeat_speaker, bool) or self.allow_repeat_speaker is None
|
|
476
|
+
else last_speaker in self.allow_repeat_speaker
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
agents = self.agents
|
|
480
|
+
n_agents = len(agents)
|
|
481
|
+
# Warn if GroupChat is underpopulated
|
|
482
|
+
if n_agents < 2:
|
|
483
|
+
raise ValueError(
|
|
484
|
+
f"GroupChat is underpopulated with {n_agents} agents. "
|
|
485
|
+
"Please add more agents to the GroupChat or use direct communication instead."
|
|
486
|
+
)
|
|
487
|
+
elif n_agents == 2 and speaker_selection_method.lower() != "round_robin" and allow_repeat_speaker:
|
|
488
|
+
logger.warning(
|
|
489
|
+
f"GroupChat is underpopulated with {n_agents} agents. "
|
|
490
|
+
"Consider setting speaker_selection_method to 'round_robin' or allow_repeat_speaker to False, "
|
|
491
|
+
"or use direct communication, unless repeated speaker is desired."
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if (
|
|
495
|
+
self.func_call_filter
|
|
496
|
+
and self.messages
|
|
497
|
+
and ("function_call" in self.messages[-1] or "tool_calls" in self.messages[-1])
|
|
498
|
+
):
|
|
499
|
+
funcs = []
|
|
500
|
+
if "function_call" in self.messages[-1]:
|
|
501
|
+
funcs += [self.messages[-1]["function_call"]["name"]]
|
|
502
|
+
if "tool_calls" in self.messages[-1]:
|
|
503
|
+
funcs += [
|
|
504
|
+
tool["function"]["name"] for tool in self.messages[-1]["tool_calls"] if tool["type"] == "function"
|
|
505
|
+
]
|
|
506
|
+
|
|
507
|
+
# find agents with the right function_map which contains the function name
|
|
508
|
+
agents = [agent for agent in self.agents if agent.can_execute_function(funcs)]
|
|
509
|
+
if len(agents) == 1:
|
|
510
|
+
# only one agent can execute the function
|
|
511
|
+
return agents[0], agents, None
|
|
512
|
+
elif not agents:
|
|
513
|
+
# find all the agents with function_map
|
|
514
|
+
agents = [agent for agent in self.agents if agent.function_map]
|
|
515
|
+
if len(agents) == 1:
|
|
516
|
+
return agents[0], agents, None
|
|
517
|
+
elif not agents:
|
|
518
|
+
raise ValueError(
|
|
519
|
+
f"No agent can execute the function {', '.join(funcs)}. "
|
|
520
|
+
"Please check the function_map of the agents."
|
|
521
|
+
)
|
|
522
|
+
# remove the last speaker from the list to avoid selecting the same speaker if allow_repeat_speaker is False
|
|
523
|
+
agents = [agent for agent in agents if agent != last_speaker] if allow_repeat_speaker is False else agents
|
|
524
|
+
|
|
525
|
+
# Filter agents with allowed_speaker_transitions_dict
|
|
526
|
+
|
|
527
|
+
is_last_speaker_in_group = last_speaker in self.agents
|
|
528
|
+
|
|
529
|
+
# this condition means last_speaker is a sink in the graph, then no agents are eligible
|
|
530
|
+
if last_speaker not in self.allowed_speaker_transitions_dict and is_last_speaker_in_group:
|
|
531
|
+
raise NoEligibleSpeakerError(
|
|
532
|
+
f"Last speaker {last_speaker.name} is not in the allowed_speaker_transitions_dict."
|
|
533
|
+
)
|
|
534
|
+
# last_speaker is not in the group, so all agents are eligible
|
|
535
|
+
elif last_speaker not in self.allowed_speaker_transitions_dict and not is_last_speaker_in_group:
|
|
536
|
+
graph_eligible_agents = []
|
|
537
|
+
else:
|
|
538
|
+
# Extract agent names from the list of agents
|
|
539
|
+
graph_eligible_agents = [
|
|
540
|
+
agent for agent in agents if agent in self.allowed_speaker_transitions_dict[last_speaker]
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
# If there is only one eligible agent, just return it to avoid the speaker selection prompt
|
|
544
|
+
if len(graph_eligible_agents) == 1:
|
|
545
|
+
return graph_eligible_agents[0], graph_eligible_agents, None
|
|
546
|
+
|
|
547
|
+
# If there are no eligible agents, return None, which means all agents will be taken into consideration in the next step
|
|
548
|
+
if len(graph_eligible_agents) == 0:
|
|
549
|
+
graph_eligible_agents = None
|
|
550
|
+
|
|
551
|
+
# Use the selected speaker selection method
|
|
552
|
+
select_speaker_messages = None
|
|
553
|
+
if speaker_selection_method.lower() == "manual":
|
|
554
|
+
selected_agent = self.manual_select_speaker(graph_eligible_agents)
|
|
555
|
+
elif speaker_selection_method.lower() == "round_robin":
|
|
556
|
+
selected_agent = self.next_agent(last_speaker, graph_eligible_agents)
|
|
557
|
+
elif speaker_selection_method.lower() == "random":
|
|
558
|
+
selected_agent = self.random_select_speaker(graph_eligible_agents)
|
|
559
|
+
else: # auto
|
|
560
|
+
selected_agent = None
|
|
561
|
+
select_speaker_messages = self.messages.copy()
|
|
562
|
+
# If last message is a tool call or function call, blank the call so the api doesn't throw
|
|
563
|
+
if select_speaker_messages[-1].get("function_call", False):
|
|
564
|
+
select_speaker_messages[-1] = dict(select_speaker_messages[-1], function_call=None)
|
|
565
|
+
if select_speaker_messages[-1].get("tool_calls", False):
|
|
566
|
+
select_speaker_messages[-1] = dict(select_speaker_messages[-1], tool_calls=None)
|
|
567
|
+
return selected_agent, graph_eligible_agents, select_speaker_messages
|
|
568
|
+
|
|
569
|
+
def select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent:
|
|
570
|
+
"""Select the next speaker (with requery)."""
|
|
571
|
+
# Prepare the list of available agents and select an agent if selection method allows (non-auto)
|
|
572
|
+
selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker)
|
|
573
|
+
if selected_agent:
|
|
574
|
+
return selected_agent
|
|
575
|
+
elif self.speaker_selection_method == "manual":
|
|
576
|
+
# An agent has not been selected while in manual mode, so move to the next agent
|
|
577
|
+
return self.next_agent(last_speaker)
|
|
578
|
+
|
|
579
|
+
# auto speaker selection with 2-agent chat
|
|
580
|
+
return self._auto_select_speaker(last_speaker, selector, messages, agents)
|
|
581
|
+
|
|
582
|
+
async def a_select_speaker(self, last_speaker: Agent, selector: ConversableAgent) -> Agent:
|
|
583
|
+
"""Select the next speaker (with requery), asynchronously."""
|
|
584
|
+
selected_agent, agents, messages = self._prepare_and_select_agents(last_speaker)
|
|
585
|
+
if selected_agent:
|
|
586
|
+
return selected_agent
|
|
587
|
+
elif self.speaker_selection_method == "manual":
|
|
588
|
+
# An agent has not been selected while in manual mode, so move to the next agent
|
|
589
|
+
return self.next_agent(last_speaker)
|
|
590
|
+
|
|
591
|
+
# auto speaker selection with 2-agent chat
|
|
592
|
+
return await self.a_auto_select_speaker(last_speaker, selector, messages, agents)
|
|
593
|
+
|
|
594
|
+
def _finalize_speaker(self, last_speaker: Agent, final: bool, name: str, agents: Optional[list[Agent]]) -> Agent:
|
|
595
|
+
if not final:
|
|
596
|
+
# the LLM client is None, thus no reply is generated. Use round robin instead.
|
|
597
|
+
return self.next_agent(last_speaker, agents)
|
|
598
|
+
|
|
599
|
+
# If exactly one agent is mentioned, use it. Otherwise, leave the OAI response unmodified
|
|
600
|
+
mentions = self._mentioned_agents(name, agents)
|
|
601
|
+
if len(mentions) == 1:
|
|
602
|
+
name = next(iter(mentions))
|
|
603
|
+
else:
|
|
604
|
+
logger.warning(
|
|
605
|
+
f"GroupChat select_speaker failed to resolve the next speaker's name. This is because the speaker selection OAI call returned:\n{name}"
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# Return the result
|
|
609
|
+
agent = self.agent_by_name(name)
|
|
610
|
+
return agent if agent else self.next_agent(last_speaker, agents)
|
|
611
|
+
|
|
612
|
+
def _register_client_from_config(self, agent: Agent, config: dict):
|
|
613
|
+
model_client_cls_to_match = config.get("model_client_cls")
|
|
614
|
+
if model_client_cls_to_match:
|
|
615
|
+
if not self.select_speaker_auto_model_client_cls:
|
|
616
|
+
raise ValueError(
|
|
617
|
+
"A custom model was detected in the config but no 'model_client_cls' "
|
|
618
|
+
"was supplied for registration in GroupChat."
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
if isinstance(self.select_speaker_auto_model_client_cls, list):
|
|
622
|
+
# Register the first custom model client class matching the name specified in the config
|
|
623
|
+
matching_model_cls = [
|
|
624
|
+
client_cls
|
|
625
|
+
for client_cls in self.select_speaker_auto_model_client_cls
|
|
626
|
+
if client_cls.__name__ == model_client_cls_to_match
|
|
627
|
+
]
|
|
628
|
+
if len(set(matching_model_cls)) > 1:
|
|
629
|
+
raise RuntimeError(
|
|
630
|
+
f"More than one unique 'model_client_cls' with __name__ '{model_client_cls_to_match}'."
|
|
631
|
+
)
|
|
632
|
+
if not matching_model_cls:
|
|
633
|
+
raise ValueError(
|
|
634
|
+
"No model's __name__ matches the model client class "
|
|
635
|
+
f"'{model_client_cls_to_match}' specified in select_speaker_auto_llm_config."
|
|
636
|
+
)
|
|
637
|
+
select_speaker_auto_model_client_cls = matching_model_cls[0]
|
|
638
|
+
else:
|
|
639
|
+
# Register the only custom model client
|
|
640
|
+
select_speaker_auto_model_client_cls = self.select_speaker_auto_model_client_cls
|
|
641
|
+
|
|
642
|
+
agent.register_model_client(select_speaker_auto_model_client_cls)
|
|
643
|
+
|
|
644
|
+
def _register_custom_model_clients(self, agent: ConversableAgent):
|
|
645
|
+
if not self.select_speaker_auto_llm_config:
|
|
646
|
+
return
|
|
647
|
+
|
|
648
|
+
config_format_is_list = "config_list" in self.select_speaker_auto_llm_config
|
|
649
|
+
if config_format_is_list:
|
|
650
|
+
for config in self.select_speaker_auto_llm_config["config_list"]:
|
|
651
|
+
self._register_client_from_config(agent, config)
|
|
652
|
+
elif not config_format_is_list:
|
|
653
|
+
self._register_client_from_config(agent, self.select_speaker_auto_llm_config)
|
|
654
|
+
|
|
655
|
+
def _create_internal_agents(
|
|
656
|
+
self, agents, max_attempts, messages, validate_speaker_name, selector: Optional[ConversableAgent] = None
|
|
657
|
+
):
|
|
658
|
+
checking_agent = ConversableAgent("checking_agent", default_auto_reply=max_attempts)
|
|
659
|
+
|
|
660
|
+
# Register the speaker validation function with the checking agent
|
|
661
|
+
checking_agent.register_reply(
|
|
662
|
+
[ConversableAgent, None],
|
|
663
|
+
reply_func=validate_speaker_name, # Validate each response
|
|
664
|
+
remove_other_reply_funcs=True,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
# Override the selector's config if one was passed as a parameter to this class
|
|
668
|
+
speaker_selection_llm_config = self.select_speaker_auto_llm_config or selector.llm_config
|
|
669
|
+
|
|
670
|
+
if speaker_selection_llm_config is False:
|
|
671
|
+
raise ValueError(
|
|
672
|
+
"The group chat's internal speaker selection agent does not have an LLM configuration. Please provide a valid LLM config to the group chat's GroupChatManager or set it with the select_speaker_auto_llm_config parameter."
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
# Agent for selecting a single agent name from the response
|
|
676
|
+
speaker_selection_agent = ConversableAgent(
|
|
677
|
+
"speaker_selection_agent",
|
|
678
|
+
system_message=self.select_speaker_msg(agents),
|
|
679
|
+
chat_messages={checking_agent: messages},
|
|
680
|
+
llm_config=speaker_selection_llm_config,
|
|
681
|
+
human_input_mode="NEVER",
|
|
682
|
+
# Suppresses some extra terminal outputs, outputs will be handled by select_speaker_auto_verbose
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
# Register any custom model passed in select_speaker_auto_llm_config with the speaker_selection_agent
|
|
686
|
+
self._register_custom_model_clients(speaker_selection_agent)
|
|
687
|
+
|
|
688
|
+
return checking_agent, speaker_selection_agent
|
|
689
|
+
|
|
690
|
+
def _auto_select_speaker(
|
|
691
|
+
self,
|
|
692
|
+
last_speaker: Agent,
|
|
693
|
+
selector: ConversableAgent,
|
|
694
|
+
messages: Optional[list[dict[str, Any]]],
|
|
695
|
+
agents: Optional[list[Agent]],
|
|
696
|
+
) -> Agent:
|
|
697
|
+
"""Selects next speaker for the "auto" speaker selection method. Utilises its own two-agent chat to determine the next speaker and supports requerying.
|
|
698
|
+
|
|
699
|
+
Speaker selection for "auto" speaker selection method:
|
|
700
|
+
1. Create a two-agent chat with a speaker selector agent and a speaker validator agent, like a nested chat
|
|
701
|
+
2. Inject the group messages into the new chat
|
|
702
|
+
3. Run the two-agent chat, evaluating the result of response from the speaker selector agent:
|
|
703
|
+
- If a single agent is provided then we return it and finish. If not, we add an additional message to this nested chat in an attempt to guide the LLM to a single agent response
|
|
704
|
+
4. Chat continues until a single agent is nominated or there are no more attempts left
|
|
705
|
+
5. If we run out of turns and no single agent can be determined, the next speaker in the list of agents is returned
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
last_speaker: The previous speaker in the group chat
|
|
709
|
+
selector: The ConversableAgent that initiated the speaker selection
|
|
710
|
+
messages: Current chat messages
|
|
711
|
+
agents: Valid list of agents for speaker selection
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
A counter for mentioned agents.
|
|
715
|
+
"""
|
|
716
|
+
# If no agents are passed in, assign all the group chat's agents
|
|
717
|
+
if agents is None:
|
|
718
|
+
agents = self.agents
|
|
719
|
+
|
|
720
|
+
# The maximum number of speaker selection attempts (including requeries)
|
|
721
|
+
# is the initial speaker selection attempt plus the maximum number of retries.
|
|
722
|
+
# We track these and use them in the validation function as we can't
|
|
723
|
+
# access the max_turns from within validate_speaker_name.
|
|
724
|
+
max_attempts = 1 + self.max_retries_for_selecting_speaker
|
|
725
|
+
attempts_left = max_attempts
|
|
726
|
+
attempt = 0
|
|
727
|
+
|
|
728
|
+
# Registered reply function for checking_agent, checks the result of the response for agent names
|
|
729
|
+
def validate_speaker_name(
|
|
730
|
+
recipient, messages, sender, config
|
|
731
|
+
) -> tuple[bool, Optional[Union[str, dict[str, Any]]]]:
|
|
732
|
+
# The number of retries left, starting at max_retries_for_selecting_speaker
|
|
733
|
+
nonlocal attempts_left
|
|
734
|
+
nonlocal attempt
|
|
735
|
+
|
|
736
|
+
attempt = attempt + 1
|
|
737
|
+
attempts_left = attempts_left - 1
|
|
738
|
+
|
|
739
|
+
return self._validate_speaker_name(recipient, messages, sender, config, attempts_left, attempt, agents)
|
|
740
|
+
|
|
741
|
+
# Two-agent chat for speaker selection
|
|
742
|
+
|
|
743
|
+
# Agent for checking the response from the speaker_select_agent
|
|
744
|
+
checking_agent, speaker_selection_agent = self._create_internal_agents(
|
|
745
|
+
agents, max_attempts, messages, validate_speaker_name, selector
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
# Create the starting message
|
|
749
|
+
if self.select_speaker_prompt_template is not None:
|
|
750
|
+
start_message = {
|
|
751
|
+
"content": self.select_speaker_prompt(agents),
|
|
752
|
+
"name": "checking_agent",
|
|
753
|
+
"override_role": self.role_for_select_speaker_messages,
|
|
754
|
+
}
|
|
755
|
+
else:
|
|
756
|
+
start_message = messages[-1]
|
|
757
|
+
|
|
758
|
+
# Add the message transforms, if any, to the speaker selection agent
|
|
759
|
+
if self._speaker_selection_transforms is not None:
|
|
760
|
+
self._speaker_selection_transforms.add_to_agent(speaker_selection_agent)
|
|
761
|
+
|
|
762
|
+
# Run the speaker selection chat
|
|
763
|
+
result = checking_agent.initiate_chat(
|
|
764
|
+
speaker_selection_agent,
|
|
765
|
+
cache=None, # don't use caching for the speaker selection chat
|
|
766
|
+
message=start_message,
|
|
767
|
+
max_turns=2
|
|
768
|
+
* max(1, max_attempts), # Limiting the chat to the number of attempts, including the initial one
|
|
769
|
+
clear_history=False,
|
|
770
|
+
silent=not self.select_speaker_auto_verbose, # Base silence on the verbose attribute
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
return self._process_speaker_selection_result(result, last_speaker, agents)
|
|
774
|
+
|
|
775
|
+
async def a_auto_select_speaker(
|
|
776
|
+
self,
|
|
777
|
+
last_speaker: Agent,
|
|
778
|
+
selector: ConversableAgent,
|
|
779
|
+
messages: Optional[list[dict[str, Any]]],
|
|
780
|
+
agents: Optional[list[Agent]],
|
|
781
|
+
) -> Agent:
|
|
782
|
+
"""(Asynchronous) Selects next speaker for the "auto" speaker selection method. Utilises its own two-agent chat to determine the next speaker and supports requerying.
|
|
783
|
+
|
|
784
|
+
Speaker selection for "auto" speaker selection method:
|
|
785
|
+
1. Create a two-agent chat with a speaker selector agent and a speaker validator agent, like a nested chat
|
|
786
|
+
2. Inject the group messages into the new chat
|
|
787
|
+
3. Run the two-agent chat, evaluating the result of response from the speaker selector agent:
|
|
788
|
+
- If a single agent is provided then we return it and finish. If not, we add an additional message to this nested chat in an attempt to guide the LLM to a single agent response
|
|
789
|
+
4. Chat continues until a single agent is nominated or there are no more attempts left
|
|
790
|
+
5. If we run out of turns and no single agent can be determined, the next speaker in the list of agents is returned
|
|
791
|
+
|
|
792
|
+
Args:
|
|
793
|
+
last_speaker: The previous speaker in the group chat
|
|
794
|
+
selector: The ConversableAgent that initiated the speaker selection
|
|
795
|
+
messages: Current chat messages
|
|
796
|
+
agents: Valid list of agents for speaker selection
|
|
797
|
+
|
|
798
|
+
Returns:
|
|
799
|
+
A counter for mentioned agents.
|
|
800
|
+
"""
|
|
801
|
+
# If no agents are passed in, assign all the group chat's agents
|
|
802
|
+
if agents is None:
|
|
803
|
+
agents = self.agents
|
|
804
|
+
|
|
805
|
+
# The maximum number of speaker selection attempts (including requeries)
|
|
806
|
+
# We track these and use them in the validation function as we can't
|
|
807
|
+
# access the max_turns from within validate_speaker_name
|
|
808
|
+
max_attempts = 1 + self.max_retries_for_selecting_speaker
|
|
809
|
+
attempts_left = max_attempts
|
|
810
|
+
attempt = 0
|
|
811
|
+
|
|
812
|
+
# Registered reply function for checking_agent, checks the result of the response for agent names
|
|
813
|
+
def validate_speaker_name(
|
|
814
|
+
recipient, messages, sender, config
|
|
815
|
+
) -> tuple[bool, Optional[Union[str, dict[str, Any]]]]:
|
|
816
|
+
# The number of retries left, starting at max_retries_for_selecting_speaker
|
|
817
|
+
nonlocal attempts_left
|
|
818
|
+
nonlocal attempt
|
|
819
|
+
|
|
820
|
+
attempt = attempt + 1
|
|
821
|
+
attempts_left = attempts_left - 1
|
|
822
|
+
|
|
823
|
+
return self._validate_speaker_name(recipient, messages, sender, config, attempts_left, attempt, agents)
|
|
824
|
+
|
|
825
|
+
# Two-agent chat for speaker selection
|
|
826
|
+
|
|
827
|
+
# Agent for checking the response from the speaker_select_agent
|
|
828
|
+
checking_agent, speaker_selection_agent = self._create_internal_agents(
|
|
829
|
+
agents, max_attempts, messages, validate_speaker_name, selector
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
# Create the starting message
|
|
833
|
+
if self.select_speaker_prompt_template is not None:
|
|
834
|
+
start_message = {
|
|
835
|
+
"content": self.select_speaker_prompt(agents),
|
|
836
|
+
"override_role": self.role_for_select_speaker_messages,
|
|
837
|
+
}
|
|
838
|
+
else:
|
|
839
|
+
start_message = messages[-1]
|
|
840
|
+
|
|
841
|
+
# Add the message transforms, if any, to the speaker selection agent
|
|
842
|
+
if self._speaker_selection_transforms is not None:
|
|
843
|
+
self._speaker_selection_transforms.add_to_agent(speaker_selection_agent)
|
|
844
|
+
|
|
845
|
+
# Run the speaker selection chat
|
|
846
|
+
result = await checking_agent.a_initiate_chat(
|
|
847
|
+
speaker_selection_agent,
|
|
848
|
+
cache=None, # don't use caching for the speaker selection chat
|
|
849
|
+
message=start_message,
|
|
850
|
+
max_turns=2
|
|
851
|
+
* max(1, max_attempts), # Limiting the chat to the number of attempts, including the initial one
|
|
852
|
+
clear_history=False,
|
|
853
|
+
silent=not self.select_speaker_auto_verbose, # Base silence on the verbose attribute
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
return self._process_speaker_selection_result(result, last_speaker, agents)
|
|
857
|
+
|
|
858
|
+
def _validate_speaker_name(
|
|
859
|
+
self, recipient, messages, sender, config, attempts_left, attempt, agents
|
|
860
|
+
) -> tuple[bool, Optional[Union[str, dict[str, Any]]]]:
|
|
861
|
+
"""Validates the speaker response for each round in the internal 2-agent
|
|
862
|
+
chat within the auto select speaker method.
|
|
863
|
+
|
|
864
|
+
Used by auto_select_speaker and a_auto_select_speaker.
|
|
865
|
+
"""
|
|
866
|
+
# Validate the speaker name selected
|
|
867
|
+
select_name = messages[-1]["content"].strip()
|
|
868
|
+
|
|
869
|
+
mentions = self._mentioned_agents(select_name, agents)
|
|
870
|
+
|
|
871
|
+
# Output the query and requery results
|
|
872
|
+
if self.select_speaker_auto_verbose:
|
|
873
|
+
iostream = IOStream.get_default()
|
|
874
|
+
no_of_mentions = len(mentions)
|
|
875
|
+
if no_of_mentions == 1:
|
|
876
|
+
# Success on retry, we have just one name mentioned
|
|
877
|
+
iostream.send(
|
|
878
|
+
SpeakerAttemptSuccessfulEvent(
|
|
879
|
+
mentions=mentions,
|
|
880
|
+
attempt=attempt,
|
|
881
|
+
attempts_left=attempts_left,
|
|
882
|
+
select_speaker_auto_verbose=self.select_speaker_auto_verbose,
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
elif no_of_mentions == 1:
|
|
886
|
+
iostream.send(
|
|
887
|
+
SpeakerAttemptFailedMultipleAgentsEvent(
|
|
888
|
+
mentions=mentions,
|
|
889
|
+
attempt=attempt,
|
|
890
|
+
attempts_left=attempts_left,
|
|
891
|
+
select_speaker_auto_verbose=self.select_speaker_auto_verbose,
|
|
892
|
+
)
|
|
893
|
+
)
|
|
894
|
+
else:
|
|
895
|
+
iostream.send(
|
|
896
|
+
SpeakerAttemptFailedNoAgentsEvent(
|
|
897
|
+
mentions=mentions,
|
|
898
|
+
attempt=attempt,
|
|
899
|
+
attempts_left=attempts_left,
|
|
900
|
+
select_speaker_auto_verbose=self.select_speaker_auto_verbose,
|
|
901
|
+
)
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
if len(mentions) == 1:
|
|
905
|
+
# Success on retry, we have just one name mentioned
|
|
906
|
+
selected_agent_name = next(iter(mentions))
|
|
907
|
+
|
|
908
|
+
# Add the selected agent to the response so we can return it
|
|
909
|
+
messages.append({"role": "user", "content": f"[AGENT SELECTED]{selected_agent_name}"})
|
|
910
|
+
|
|
911
|
+
elif len(mentions) > 1:
|
|
912
|
+
# More than one name on requery so add additional reminder prompt for next retry
|
|
913
|
+
|
|
914
|
+
if attempts_left:
|
|
915
|
+
# Message to return to the chat for the next attempt
|
|
916
|
+
agentlist = f"{[agent.name for agent in agents]}"
|
|
917
|
+
|
|
918
|
+
return True, {
|
|
919
|
+
"content": self.select_speaker_auto_multiple_template.format(agentlist=agentlist),
|
|
920
|
+
"name": "checking_agent",
|
|
921
|
+
"override_role": self.role_for_select_speaker_messages,
|
|
922
|
+
}
|
|
923
|
+
else:
|
|
924
|
+
# Final failure, no attempts left
|
|
925
|
+
messages.append({
|
|
926
|
+
"role": "user",
|
|
927
|
+
"content": f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it returned multiple names.",
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
else:
|
|
931
|
+
# No names at all on requery so add additional reminder prompt for next retry
|
|
932
|
+
|
|
933
|
+
if attempts_left:
|
|
934
|
+
# Message to return to the chat for the next attempt
|
|
935
|
+
agentlist = f"{[agent.name for agent in agents]}"
|
|
936
|
+
|
|
937
|
+
return True, {
|
|
938
|
+
"content": self.select_speaker_auto_none_template.format(agentlist=agentlist),
|
|
939
|
+
"name": "checking_agent",
|
|
940
|
+
"override_role": self.role_for_select_speaker_messages,
|
|
941
|
+
}
|
|
942
|
+
else:
|
|
943
|
+
# Final failure, no attempts left
|
|
944
|
+
messages.append({
|
|
945
|
+
"role": "user",
|
|
946
|
+
"content": f"[AGENT SELECTION FAILED]Select speaker attempt #{attempt} of {attempt + attempts_left} failed as it did not include any agent names.",
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
return True, None
|
|
950
|
+
|
|
951
|
+
def _process_speaker_selection_result(self, result, last_speaker: ConversableAgent, agents: Optional[list[Agent]]):
|
|
952
|
+
"""Checks the result of the auto_select_speaker function, returning the
|
|
953
|
+
agent to speak.
|
|
954
|
+
|
|
955
|
+
Used by auto_select_speaker and a_auto_select_speaker.
|
|
956
|
+
"""
|
|
957
|
+
if len(result.chat_history) > 0:
|
|
958
|
+
# Use the final message, which will have the selected agent or reason for failure
|
|
959
|
+
final_message = result.chat_history[-1]["content"]
|
|
960
|
+
|
|
961
|
+
if "[AGENT SELECTED]" in final_message:
|
|
962
|
+
# Have successfully selected an agent, return it
|
|
963
|
+
return self.agent_by_name(final_message.replace("[AGENT SELECTED]", ""))
|
|
964
|
+
|
|
965
|
+
else: # "[AGENT SELECTION FAILED]"
|
|
966
|
+
# Failed to select an agent, so we'll select the next agent in the list
|
|
967
|
+
next_agent = self.next_agent(last_speaker, agents)
|
|
968
|
+
|
|
969
|
+
# No agent, return the failed reason
|
|
970
|
+
return next_agent
|
|
971
|
+
|
|
972
|
+
def _participant_roles(self, agents: list[Agent] = None) -> str:
|
|
973
|
+
# Default to all agents registered
|
|
974
|
+
if agents is None:
|
|
975
|
+
agents = self.agents
|
|
976
|
+
|
|
977
|
+
roles = []
|
|
978
|
+
for agent in agents:
|
|
979
|
+
if agent.description.strip() == "":
|
|
980
|
+
logger.warning(
|
|
981
|
+
f"The agent '{agent.name}' has an empty description, and may not work well with GroupChat."
|
|
982
|
+
)
|
|
983
|
+
roles.append(f"{agent.name}: {agent.description}".strip())
|
|
984
|
+
return "\n".join(roles)
|
|
985
|
+
|
|
986
|
+
def _mentioned_agents(self, message_content: Union[str, list], agents: Optional[list[Agent]]) -> dict:
|
|
987
|
+
"""Counts the number of times each agent is mentioned in the provided message content.
|
|
988
|
+
Agent names will match under any of the following conditions (all case-sensitive):
|
|
989
|
+
- Exact name match
|
|
990
|
+
- If the agent name has underscores it will match with spaces instead (e.g. 'Story_writer' == 'Story writer')
|
|
991
|
+
- If the agent name has underscores it will match with '\\_' instead of '_' (e.g. 'Story_writer' == 'Story\\_writer')
|
|
992
|
+
|
|
993
|
+
Args:
|
|
994
|
+
message_content (Union[str, List]): The content of the message, either as a single string or a list of strings.
|
|
995
|
+
agents (List[Agent]): A list of Agent objects, each having a 'name' attribute to be searched in the message content.
|
|
996
|
+
|
|
997
|
+
Returns:
|
|
998
|
+
Dict: a counter for mentioned agents.
|
|
999
|
+
"""
|
|
1000
|
+
if agents is None:
|
|
1001
|
+
agents = self.agents
|
|
1002
|
+
|
|
1003
|
+
# Cast message content to str
|
|
1004
|
+
if isinstance(message_content, dict):
|
|
1005
|
+
message_content = message_content["content"]
|
|
1006
|
+
message_content = content_str(message_content)
|
|
1007
|
+
|
|
1008
|
+
mentions = dict()
|
|
1009
|
+
for agent in agents:
|
|
1010
|
+
# Finds agent mentions, taking word boundaries into account,
|
|
1011
|
+
# accommodates escaping underscores and underscores as spaces
|
|
1012
|
+
regex = (
|
|
1013
|
+
r"(?<=\W)("
|
|
1014
|
+
+ re.escape(agent.name)
|
|
1015
|
+
+ r"|"
|
|
1016
|
+
+ re.escape(agent.name.replace("_", " "))
|
|
1017
|
+
+ r"|"
|
|
1018
|
+
+ re.escape(agent.name.replace("_", r"\_"))
|
|
1019
|
+
+ r")(?=\W)"
|
|
1020
|
+
)
|
|
1021
|
+
count = len(re.findall(regex, f" {message_content} ")) # Pad the message to help with matching
|
|
1022
|
+
if count > 0:
|
|
1023
|
+
mentions[agent.name] = count
|
|
1024
|
+
return mentions
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
@export_module("autogen")
|
|
1028
|
+
class GroupChatManager(ConversableAgent):
|
|
1029
|
+
"""(In preview) A chat manager agent that can manage a group chat of multiple agents."""
|
|
1030
|
+
|
|
1031
|
+
def __init__(
|
|
1032
|
+
self,
|
|
1033
|
+
groupchat: GroupChat,
|
|
1034
|
+
name: Optional[str] = "chat_manager",
|
|
1035
|
+
# unlimited consecutive auto reply by default
|
|
1036
|
+
max_consecutive_auto_reply: Optional[int] = sys.maxsize,
|
|
1037
|
+
human_input_mode: Literal["ALWAYS", "NEVER", "TERMINATE"] = "NEVER",
|
|
1038
|
+
system_message: Optional[Union[str, list]] = "Group chat manager.",
|
|
1039
|
+
silent: bool = False,
|
|
1040
|
+
**kwargs: Any,
|
|
1041
|
+
):
|
|
1042
|
+
if (
|
|
1043
|
+
kwargs.get("llm_config")
|
|
1044
|
+
and isinstance(kwargs["llm_config"], dict)
|
|
1045
|
+
and (kwargs["llm_config"].get("functions") or kwargs["llm_config"].get("tools"))
|
|
1046
|
+
):
|
|
1047
|
+
raise ValueError(
|
|
1048
|
+
"GroupChatManager is not allowed to make function/tool calls. Please remove the 'functions' or 'tools' config in 'llm_config' you passed in."
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
super().__init__(
|
|
1052
|
+
name=name,
|
|
1053
|
+
max_consecutive_auto_reply=max_consecutive_auto_reply,
|
|
1054
|
+
human_input_mode=human_input_mode,
|
|
1055
|
+
system_message=system_message,
|
|
1056
|
+
**kwargs,
|
|
1057
|
+
)
|
|
1058
|
+
if logging_enabled():
|
|
1059
|
+
log_new_agent(self, locals())
|
|
1060
|
+
# Store groupchat
|
|
1061
|
+
self._groupchat = groupchat
|
|
1062
|
+
|
|
1063
|
+
self._last_speaker = None
|
|
1064
|
+
self._silent = silent
|
|
1065
|
+
|
|
1066
|
+
# Order of register_reply is important.
|
|
1067
|
+
# Allow sync chat if initiated using initiate_chat
|
|
1068
|
+
self.register_reply(Agent, GroupChatManager.run_chat, config=groupchat, reset_config=GroupChat.reset)
|
|
1069
|
+
# Allow async chat if initiated using a_initiate_chat
|
|
1070
|
+
self.register_reply(
|
|
1071
|
+
Agent,
|
|
1072
|
+
GroupChatManager.a_run_chat,
|
|
1073
|
+
config=groupchat,
|
|
1074
|
+
reset_config=GroupChat.reset,
|
|
1075
|
+
ignore_async_in_sync_chat=True,
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
@property
|
|
1079
|
+
def groupchat(self) -> GroupChat:
|
|
1080
|
+
"""Returns the group chat managed by the group chat manager."""
|
|
1081
|
+
return self._groupchat
|
|
1082
|
+
|
|
1083
|
+
def chat_messages_for_summary(self, agent: Agent) -> list[dict[str, Any]]:
|
|
1084
|
+
"""The list of messages in the group chat as a conversation to summarize.
|
|
1085
|
+
The agent is ignored.
|
|
1086
|
+
"""
|
|
1087
|
+
return self._groupchat.messages
|
|
1088
|
+
|
|
1089
|
+
def _prepare_chat(
|
|
1090
|
+
self,
|
|
1091
|
+
recipient: ConversableAgent,
|
|
1092
|
+
clear_history: bool,
|
|
1093
|
+
prepare_recipient: bool = True,
|
|
1094
|
+
reply_at_receive: bool = True,
|
|
1095
|
+
) -> None:
|
|
1096
|
+
super()._prepare_chat(recipient, clear_history, prepare_recipient, reply_at_receive)
|
|
1097
|
+
|
|
1098
|
+
if clear_history:
|
|
1099
|
+
self._groupchat.reset()
|
|
1100
|
+
|
|
1101
|
+
for agent in self._groupchat.agents:
|
|
1102
|
+
if (recipient != agent or prepare_recipient) and isinstance(agent, ConversableAgent):
|
|
1103
|
+
agent._prepare_chat(self, clear_history, False, reply_at_receive)
|
|
1104
|
+
|
|
1105
|
+
@property
|
|
1106
|
+
def last_speaker(self) -> Agent:
|
|
1107
|
+
"""Return the agent who sent the last message to group chat manager.
|
|
1108
|
+
|
|
1109
|
+
In a group chat, an agent will always send a message to the group chat manager, and the group chat manager will
|
|
1110
|
+
send the message to all other agents in the group chat. So, when an agent receives a message, it will always be
|
|
1111
|
+
from the group chat manager. With this property, the agent receiving the message can know who actually sent the
|
|
1112
|
+
message.
|
|
1113
|
+
|
|
1114
|
+
Example:
|
|
1115
|
+
```python
|
|
1116
|
+
from autogen import ConversableAgent
|
|
1117
|
+
from autogen import GroupChat, GroupChatManager
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def print_messages(recipient, messages, sender, config):
|
|
1121
|
+
# Print the message immediately
|
|
1122
|
+
print(f"Sender: {sender.name} | Recipient: {recipient.name} | Message: {messages[-1].get('content')}")
|
|
1123
|
+
print(f"Real Sender: {sender.last_speaker.name}")
|
|
1124
|
+
assert sender.last_speaker.name in messages[-1].get("content")
|
|
1125
|
+
return False, None # Required to ensure the agent communication flow continues
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
agent_a = ConversableAgent("agent A", default_auto_reply="I'm agent A.")
|
|
1129
|
+
agent_b = ConversableAgent("agent B", default_auto_reply="I'm agent B.")
|
|
1130
|
+
agent_c = ConversableAgent("agent C", default_auto_reply="I'm agent C.")
|
|
1131
|
+
for agent in [agent_a, agent_b, agent_c]:
|
|
1132
|
+
agent.register_reply([ConversableAgent, None], reply_func=print_messages, config=None)
|
|
1133
|
+
group_chat = GroupChat(
|
|
1134
|
+
[agent_a, agent_b, agent_c],
|
|
1135
|
+
messages=[],
|
|
1136
|
+
max_round=6,
|
|
1137
|
+
speaker_selection_method="random",
|
|
1138
|
+
allow_repeat_speaker=True,
|
|
1139
|
+
)
|
|
1140
|
+
chat_manager = GroupChatManager(group_chat)
|
|
1141
|
+
groupchat_result = agent_a.initiate_chat(chat_manager, message="Hi, there, I'm agent A.")
|
|
1142
|
+
```
|
|
1143
|
+
"""
|
|
1144
|
+
return self._last_speaker
|
|
1145
|
+
|
|
1146
|
+
def run_chat(
|
|
1147
|
+
self,
|
|
1148
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1149
|
+
sender: Optional[Agent] = None,
|
|
1150
|
+
config: Optional[GroupChat] = None,
|
|
1151
|
+
) -> tuple[bool, Optional[str]]:
|
|
1152
|
+
"""Run a group chat."""
|
|
1153
|
+
iostream = IOStream.get_default()
|
|
1154
|
+
|
|
1155
|
+
if messages is None:
|
|
1156
|
+
messages = self._oai_messages[sender]
|
|
1157
|
+
message = messages[-1]
|
|
1158
|
+
speaker = sender
|
|
1159
|
+
groupchat = config
|
|
1160
|
+
send_introductions = getattr(groupchat, "send_introductions", False)
|
|
1161
|
+
silent = getattr(self, "_silent", False)
|
|
1162
|
+
termination_reason = None
|
|
1163
|
+
|
|
1164
|
+
if send_introductions:
|
|
1165
|
+
# Broadcast the intro
|
|
1166
|
+
intro = groupchat.introductions_msg()
|
|
1167
|
+
for agent in groupchat.agents:
|
|
1168
|
+
self.send(intro, agent, request_reply=False, silent=True)
|
|
1169
|
+
# NOTE: We do not also append to groupchat.messages,
|
|
1170
|
+
# since groupchat handles its own introductions
|
|
1171
|
+
|
|
1172
|
+
if self.client_cache is not None:
|
|
1173
|
+
for a in groupchat.agents:
|
|
1174
|
+
a.previous_cache = a.client_cache
|
|
1175
|
+
a.client_cache = self.client_cache
|
|
1176
|
+
for i in range(groupchat.max_round):
|
|
1177
|
+
self._last_speaker = speaker
|
|
1178
|
+
groupchat.append(message, speaker)
|
|
1179
|
+
# broadcast the message to all agents except the speaker
|
|
1180
|
+
for agent in groupchat.agents:
|
|
1181
|
+
if agent != speaker:
|
|
1182
|
+
self.send(message, agent, request_reply=False, silent=True)
|
|
1183
|
+
if self._is_termination_msg(message):
|
|
1184
|
+
# The conversation is over
|
|
1185
|
+
termination_reason = f"Termination message condition on the GroupChatManager '{self.name}' met"
|
|
1186
|
+
break
|
|
1187
|
+
elif i == groupchat.max_round - 1:
|
|
1188
|
+
# It's the last round
|
|
1189
|
+
termination_reason = f"Maximum rounds ({groupchat.max_round}) reached"
|
|
1190
|
+
break
|
|
1191
|
+
try:
|
|
1192
|
+
# select the next speaker
|
|
1193
|
+
speaker = groupchat.select_speaker(speaker, self)
|
|
1194
|
+
if not silent:
|
|
1195
|
+
iostream = IOStream.get_default()
|
|
1196
|
+
iostream.send(GroupChatRunChatEvent(speaker=speaker, silent=silent))
|
|
1197
|
+
# let the speaker speak
|
|
1198
|
+
reply = speaker.generate_reply(sender=self)
|
|
1199
|
+
except KeyboardInterrupt:
|
|
1200
|
+
# let the admin agent speak if interrupted
|
|
1201
|
+
if groupchat.admin_name in groupchat.agent_names:
|
|
1202
|
+
# admin agent is one of the participants
|
|
1203
|
+
speaker = groupchat.agent_by_name(groupchat.admin_name)
|
|
1204
|
+
reply = speaker.generate_reply(sender=self)
|
|
1205
|
+
else:
|
|
1206
|
+
# admin agent is not found in the participants
|
|
1207
|
+
raise
|
|
1208
|
+
except NoEligibleSpeakerError:
|
|
1209
|
+
# No eligible speaker, terminate the conversation
|
|
1210
|
+
termination_reason = "No next speaker selected"
|
|
1211
|
+
break
|
|
1212
|
+
|
|
1213
|
+
if reply is None:
|
|
1214
|
+
# no reply is generated, exit the chat
|
|
1215
|
+
termination_reason = "No reply generated"
|
|
1216
|
+
break
|
|
1217
|
+
|
|
1218
|
+
# check for "clear history" phrase in reply and activate clear history function if found
|
|
1219
|
+
if (
|
|
1220
|
+
groupchat.enable_clear_history
|
|
1221
|
+
and isinstance(reply, dict)
|
|
1222
|
+
and reply["content"]
|
|
1223
|
+
and "CLEAR HISTORY" in reply["content"].upper()
|
|
1224
|
+
):
|
|
1225
|
+
reply["content"] = self.clear_agents_history(reply, groupchat)
|
|
1226
|
+
|
|
1227
|
+
# The speaker sends the message without requesting a reply
|
|
1228
|
+
speaker.send(reply, self, request_reply=False, silent=silent)
|
|
1229
|
+
message = self.last_message(speaker)
|
|
1230
|
+
if self.client_cache is not None:
|
|
1231
|
+
for a in groupchat.agents:
|
|
1232
|
+
a.client_cache = a.previous_cache
|
|
1233
|
+
a.previous_cache = None
|
|
1234
|
+
|
|
1235
|
+
if termination_reason:
|
|
1236
|
+
iostream.send(TerminationEvent(termination_reason=termination_reason))
|
|
1237
|
+
|
|
1238
|
+
return True, None
|
|
1239
|
+
|
|
1240
|
+
async def a_run_chat(
|
|
1241
|
+
self,
|
|
1242
|
+
messages: Optional[list[dict[str, Any]]] = None,
|
|
1243
|
+
sender: Optional[Agent] = None,
|
|
1244
|
+
config: Optional[GroupChat] = None,
|
|
1245
|
+
):
|
|
1246
|
+
"""Run a group chat asynchronously."""
|
|
1247
|
+
iostream = IOStream.get_default()
|
|
1248
|
+
|
|
1249
|
+
if messages is None:
|
|
1250
|
+
messages = self._oai_messages[sender]
|
|
1251
|
+
message = messages[-1]
|
|
1252
|
+
speaker = sender
|
|
1253
|
+
groupchat = config
|
|
1254
|
+
send_introductions = getattr(groupchat, "send_introductions", False)
|
|
1255
|
+
silent = getattr(self, "_silent", False)
|
|
1256
|
+
termination_reason = None
|
|
1257
|
+
|
|
1258
|
+
if send_introductions:
|
|
1259
|
+
# Broadcast the intro
|
|
1260
|
+
intro = groupchat.introductions_msg()
|
|
1261
|
+
for agent in groupchat.agents:
|
|
1262
|
+
await self.a_send(intro, agent, request_reply=False, silent=True)
|
|
1263
|
+
# NOTE: We do not also append to groupchat.messages,
|
|
1264
|
+
# since groupchat handles its own introductions
|
|
1265
|
+
|
|
1266
|
+
if self.client_cache is not None:
|
|
1267
|
+
for a in groupchat.agents:
|
|
1268
|
+
a.previous_cache = a.client_cache
|
|
1269
|
+
a.client_cache = self.client_cache
|
|
1270
|
+
for i in range(groupchat.max_round):
|
|
1271
|
+
groupchat.append(message, speaker)
|
|
1272
|
+
self._last_speaker = speaker
|
|
1273
|
+
|
|
1274
|
+
if self._is_termination_msg(message):
|
|
1275
|
+
# The conversation is over
|
|
1276
|
+
termination_reason = f"Termination message condition on the GroupChatManager '{self.name}' met"
|
|
1277
|
+
break
|
|
1278
|
+
|
|
1279
|
+
# broadcast the message to all agents except the speaker
|
|
1280
|
+
for agent in groupchat.agents:
|
|
1281
|
+
if agent != speaker:
|
|
1282
|
+
await self.a_send(message, agent, request_reply=False, silent=True)
|
|
1283
|
+
if i == groupchat.max_round - 1:
|
|
1284
|
+
# the last round
|
|
1285
|
+
termination_reason = f"Maximum rounds ({groupchat.max_round}) reached"
|
|
1286
|
+
break
|
|
1287
|
+
try:
|
|
1288
|
+
# select the next speaker
|
|
1289
|
+
speaker = await groupchat.a_select_speaker(speaker, self)
|
|
1290
|
+
# let the speaker speak
|
|
1291
|
+
reply = await speaker.a_generate_reply(sender=self)
|
|
1292
|
+
except KeyboardInterrupt:
|
|
1293
|
+
# let the admin agent speak if interrupted
|
|
1294
|
+
if groupchat.admin_name in groupchat.agent_names:
|
|
1295
|
+
# admin agent is one of the participants
|
|
1296
|
+
speaker = groupchat.agent_by_name(groupchat.admin_name)
|
|
1297
|
+
reply = await speaker.a_generate_reply(sender=self)
|
|
1298
|
+
else:
|
|
1299
|
+
# admin agent is not found in the participants
|
|
1300
|
+
raise
|
|
1301
|
+
except NoEligibleSpeakerError:
|
|
1302
|
+
# No eligible speaker, terminate the conversation
|
|
1303
|
+
termination_reason = "No next speaker selected"
|
|
1304
|
+
break
|
|
1305
|
+
|
|
1306
|
+
if reply is None:
|
|
1307
|
+
# no reply is generated, exit the chat
|
|
1308
|
+
termination_reason = "No reply generated"
|
|
1309
|
+
break
|
|
1310
|
+
|
|
1311
|
+
# The speaker sends the message without requesting a reply
|
|
1312
|
+
await speaker.a_send(reply, self, request_reply=False, silent=silent)
|
|
1313
|
+
message = self.last_message(speaker)
|
|
1314
|
+
if self.client_cache is not None:
|
|
1315
|
+
for a in groupchat.agents:
|
|
1316
|
+
a.client_cache = a.previous_cache
|
|
1317
|
+
a.previous_cache = None
|
|
1318
|
+
|
|
1319
|
+
if termination_reason:
|
|
1320
|
+
iostream.send(TerminationEvent(termination_reason=termination_reason))
|
|
1321
|
+
|
|
1322
|
+
return True, None
|
|
1323
|
+
|
|
1324
|
+
def resume(
|
|
1325
|
+
self,
|
|
1326
|
+
messages: Union[list[dict[str, Any]], str],
|
|
1327
|
+
remove_termination_string: Optional[Union[str, Callable[[str], str]]] = None,
|
|
1328
|
+
silent: Optional[bool] = False,
|
|
1329
|
+
) -> tuple[ConversableAgent, dict[str, Any]]:
|
|
1330
|
+
"""Resumes a group chat using the previous messages as a starting point. Requires the agents, group chat, and group chat manager to be established
|
|
1331
|
+
as per the original group chat.
|
|
1332
|
+
|
|
1333
|
+
Args:
|
|
1334
|
+
messages: The content of the previous chat's messages, either as a Json string or a list of message dictionaries.
|
|
1335
|
+
remove_termination_string: Remove the termination string from the last message to prevent immediate termination
|
|
1336
|
+
If a string is provided, this string will be removed from last message.
|
|
1337
|
+
If a function is provided, the last message will be passed to this function.
|
|
1338
|
+
silent: (Experimental) whether to print the messages for this conversation. Default is False.
|
|
1339
|
+
|
|
1340
|
+
Returns:
|
|
1341
|
+
A tuple containing the last agent who spoke and their message
|
|
1342
|
+
"""
|
|
1343
|
+
# Convert messages from string to messages list, if needed
|
|
1344
|
+
if isinstance(messages, str):
|
|
1345
|
+
messages = self.messages_from_string(messages)
|
|
1346
|
+
elif isinstance(messages, list) and all(isinstance(item, dict) for item in messages):
|
|
1347
|
+
messages = copy.deepcopy(messages)
|
|
1348
|
+
else:
|
|
1349
|
+
raise Exception("Messages is not of type str or List[Dict]")
|
|
1350
|
+
|
|
1351
|
+
# Clean up the objects, ensuring there are no messages in the agents and group chat
|
|
1352
|
+
|
|
1353
|
+
# Clear agent message history
|
|
1354
|
+
for agent in self._groupchat.agents:
|
|
1355
|
+
if isinstance(agent, ConversableAgent):
|
|
1356
|
+
agent.clear_history()
|
|
1357
|
+
|
|
1358
|
+
# Clear Manager message history
|
|
1359
|
+
self.clear_history()
|
|
1360
|
+
|
|
1361
|
+
# Clear GroupChat messages
|
|
1362
|
+
self._groupchat.reset()
|
|
1363
|
+
|
|
1364
|
+
# Validation of message and agents
|
|
1365
|
+
|
|
1366
|
+
try:
|
|
1367
|
+
self._valid_resume_messages(messages)
|
|
1368
|
+
except:
|
|
1369
|
+
raise
|
|
1370
|
+
|
|
1371
|
+
# Load the messages into the group chat
|
|
1372
|
+
for i, message in enumerate(messages):
|
|
1373
|
+
if "name" in message:
|
|
1374
|
+
message_speaker_agent = self._groupchat.agent_by_name(message["name"])
|
|
1375
|
+
else:
|
|
1376
|
+
# If there's no name, assign the group chat manager (this is an indication the ChatResult messages was used instead of groupchat.messages as state)
|
|
1377
|
+
message_speaker_agent = self
|
|
1378
|
+
message["name"] = self.name
|
|
1379
|
+
|
|
1380
|
+
# If it wasn't an agent speaking, it may be the manager
|
|
1381
|
+
if not message_speaker_agent and message["name"] == self.name:
|
|
1382
|
+
message_speaker_agent = self
|
|
1383
|
+
|
|
1384
|
+
# Add previous messages to each agent (except the last message, as we'll kick off the conversation with it)
|
|
1385
|
+
if i != len(messages) - 1:
|
|
1386
|
+
for agent in self._groupchat.agents:
|
|
1387
|
+
if agent.name == message["name"]:
|
|
1388
|
+
# An agent`s message is sent to the Group Chat Manager
|
|
1389
|
+
agent.send(message, self, request_reply=False, silent=True)
|
|
1390
|
+
else:
|
|
1391
|
+
# Otherwise, messages are sent from the Group Chat Manager to the agent
|
|
1392
|
+
self.send(message, agent, request_reply=False, silent=True)
|
|
1393
|
+
|
|
1394
|
+
# Add previous message to the new groupchat, if it's an admin message the name may not match so add the message directly
|
|
1395
|
+
if message_speaker_agent:
|
|
1396
|
+
self._groupchat.append(message, message_speaker_agent)
|
|
1397
|
+
else:
|
|
1398
|
+
self._groupchat.messages.append(message)
|
|
1399
|
+
|
|
1400
|
+
# Last speaker agent
|
|
1401
|
+
last_speaker_name = message["name"]
|
|
1402
|
+
|
|
1403
|
+
# Last message to check for termination (we could avoid this by ignoring termination check for resume in the future)
|
|
1404
|
+
last_message = message
|
|
1405
|
+
|
|
1406
|
+
# Get last speaker as an agent
|
|
1407
|
+
previous_last_agent = self._groupchat.agent_by_name(name=last_speaker_name)
|
|
1408
|
+
|
|
1409
|
+
# If we didn't match a last speaker agent, we check that it's the group chat's admin name and assign the manager, if so
|
|
1410
|
+
if not previous_last_agent and (
|
|
1411
|
+
last_speaker_name == self._groupchat.admin_name or last_speaker_name == self.name
|
|
1412
|
+
):
|
|
1413
|
+
previous_last_agent = self
|
|
1414
|
+
|
|
1415
|
+
# Termination removal and check
|
|
1416
|
+
self._process_resume_termination(remove_termination_string, messages)
|
|
1417
|
+
|
|
1418
|
+
if not silent:
|
|
1419
|
+
iostream = IOStream.get_default()
|
|
1420
|
+
iostream.send(GroupChatResumeEvent(last_speaker_name=last_speaker_name, events=messages, silent=silent))
|
|
1421
|
+
|
|
1422
|
+
# Update group chat settings for resuming
|
|
1423
|
+
self._groupchat.send_introductions = False
|
|
1424
|
+
|
|
1425
|
+
return previous_last_agent, last_message
|
|
1426
|
+
|
|
1427
|
+
async def a_resume(
|
|
1428
|
+
self,
|
|
1429
|
+
messages: Union[list[dict[str, Any]], str],
|
|
1430
|
+
remove_termination_string: Optional[Union[str, Callable[[str], str]]] = None,
|
|
1431
|
+
silent: Optional[bool] = False,
|
|
1432
|
+
) -> tuple[ConversableAgent, dict[str, Any]]:
|
|
1433
|
+
"""Resumes a group chat using the previous messages as a starting point, asynchronously. Requires the agents, group chat, and group chat manager to be established
|
|
1434
|
+
as per the original group chat.
|
|
1435
|
+
|
|
1436
|
+
Args:
|
|
1437
|
+
messages: The content of the previous chat's messages, either as a Json string or a list of message dictionaries.
|
|
1438
|
+
remove_termination_string: Remove the termination string from the last message to prevent immediate termination
|
|
1439
|
+
If a string is provided, this string will be removed from last message.
|
|
1440
|
+
If a function is provided, the last message will be passed to this function, and the function returns the string after processing.
|
|
1441
|
+
silent: (Experimental) whether to print the messages for this conversation. Default is False.
|
|
1442
|
+
|
|
1443
|
+
Returns:
|
|
1444
|
+
A tuple containing the last agent who spoke and their message
|
|
1445
|
+
"""
|
|
1446
|
+
# Convert messages from string to messages list, if needed
|
|
1447
|
+
if isinstance(messages, str):
|
|
1448
|
+
messages = self.messages_from_string(messages)
|
|
1449
|
+
elif isinstance(messages, list) and all(isinstance(item, dict) for item in messages):
|
|
1450
|
+
messages = copy.deepcopy(messages)
|
|
1451
|
+
else:
|
|
1452
|
+
raise Exception("Messages is not of type str or List[Dict]")
|
|
1453
|
+
|
|
1454
|
+
# Clean up the objects, ensuring there are no messages in the agents and group chat
|
|
1455
|
+
|
|
1456
|
+
# Clear agent message history
|
|
1457
|
+
for agent in self._groupchat.agents:
|
|
1458
|
+
if isinstance(agent, ConversableAgent):
|
|
1459
|
+
agent.clear_history()
|
|
1460
|
+
|
|
1461
|
+
# Clear Manager message history
|
|
1462
|
+
self.clear_history()
|
|
1463
|
+
|
|
1464
|
+
# Clear GroupChat messages
|
|
1465
|
+
self._groupchat.reset()
|
|
1466
|
+
|
|
1467
|
+
# Validation of message and agents
|
|
1468
|
+
|
|
1469
|
+
try:
|
|
1470
|
+
self._valid_resume_messages(messages)
|
|
1471
|
+
except:
|
|
1472
|
+
raise
|
|
1473
|
+
|
|
1474
|
+
# Load the messages into the group chat
|
|
1475
|
+
for i, message in enumerate(messages):
|
|
1476
|
+
if "name" in message:
|
|
1477
|
+
message_speaker_agent = self._groupchat.agent_by_name(message["name"])
|
|
1478
|
+
else:
|
|
1479
|
+
# If there's no name, assign the group chat manager (this is an indication the ChatResult messages was used instead of groupchat.messages as state)
|
|
1480
|
+
message_speaker_agent = self
|
|
1481
|
+
message["name"] = self.name
|
|
1482
|
+
|
|
1483
|
+
# If it wasn't an agent speaking, it may be the manager
|
|
1484
|
+
if not message_speaker_agent and message["name"] == self.name:
|
|
1485
|
+
message_speaker_agent = self
|
|
1486
|
+
|
|
1487
|
+
# Add previous messages to each agent (except the last message, as we'll kick off the conversation with it)
|
|
1488
|
+
if i != len(messages) - 1:
|
|
1489
|
+
for agent in self._groupchat.agents:
|
|
1490
|
+
if agent.name == message["name"]:
|
|
1491
|
+
# An agent`s message is sent to the Group Chat Manager
|
|
1492
|
+
agent.a_send(message, self, request_reply=False, silent=True)
|
|
1493
|
+
else:
|
|
1494
|
+
# Otherwise, messages are sent from the Group Chat Manager to the agent
|
|
1495
|
+
self.a_send(message, agent, request_reply=False, silent=True)
|
|
1496
|
+
|
|
1497
|
+
# Add previous message to the new groupchat, if it's an admin message the name may not match so add the message directly
|
|
1498
|
+
if message_speaker_agent:
|
|
1499
|
+
self._groupchat.append(message, message_speaker_agent)
|
|
1500
|
+
else:
|
|
1501
|
+
self._groupchat.messages.append(message)
|
|
1502
|
+
|
|
1503
|
+
# Last speaker agent
|
|
1504
|
+
last_speaker_name = message["name"]
|
|
1505
|
+
|
|
1506
|
+
# Last message to check for termination (we could avoid this by ignoring termination check for resume in the future)
|
|
1507
|
+
last_message = message
|
|
1508
|
+
|
|
1509
|
+
# Get last speaker as an agent
|
|
1510
|
+
previous_last_agent = self._groupchat.agent_by_name(name=last_speaker_name)
|
|
1511
|
+
|
|
1512
|
+
# If we didn't match a last speaker agent, we check that it's the group chat's admin name and assign the manager, if so
|
|
1513
|
+
if not previous_last_agent and (
|
|
1514
|
+
last_speaker_name == self._groupchat.admin_name or last_speaker_name == self.name
|
|
1515
|
+
):
|
|
1516
|
+
previous_last_agent = self
|
|
1517
|
+
|
|
1518
|
+
# Termination removal and check
|
|
1519
|
+
self._process_resume_termination(remove_termination_string, messages)
|
|
1520
|
+
|
|
1521
|
+
if not silent:
|
|
1522
|
+
iostream = IOStream.get_default()
|
|
1523
|
+
iostream.send(GroupChatResumeEvent(last_speaker_name=last_speaker_name, events=messages, silent=silent))
|
|
1524
|
+
|
|
1525
|
+
# Update group chat settings for resuming
|
|
1526
|
+
self._groupchat.send_introductions = False
|
|
1527
|
+
|
|
1528
|
+
return previous_last_agent, last_message
|
|
1529
|
+
|
|
1530
|
+
def _valid_resume_messages(self, messages: list[dict[str, Any]]):
|
|
1531
|
+
"""Validates the messages used for resuming
|
|
1532
|
+
|
|
1533
|
+
Args:
|
|
1534
|
+
messages (List[Dict]): list of messages to resume with
|
|
1535
|
+
|
|
1536
|
+
Returns:
|
|
1537
|
+
- bool: Whether they are valid for resuming
|
|
1538
|
+
"""
|
|
1539
|
+
# Must have messages to start with, otherwise they should run run_chat
|
|
1540
|
+
if not messages:
|
|
1541
|
+
raise Exception(
|
|
1542
|
+
"Cannot resume group chat as no messages were provided. Use GroupChatManager.run_chat or ConversableAgent.initiate_chat to start a new chat."
|
|
1543
|
+
)
|
|
1544
|
+
|
|
1545
|
+
# Check that all agents in the chat messages exist in the group chat
|
|
1546
|
+
for message in messages:
|
|
1547
|
+
if message.get("name") and (
|
|
1548
|
+
not self._groupchat.agent_by_name(message["name"])
|
|
1549
|
+
and not message["name"] == self._groupchat.admin_name # ignore group chat's name
|
|
1550
|
+
and not message["name"] == self.name # ignore group chat manager's name
|
|
1551
|
+
):
|
|
1552
|
+
raise Exception(f"Agent name in message doesn't exist as agent in group chat: {message['name']}")
|
|
1553
|
+
|
|
1554
|
+
def _process_resume_termination(
|
|
1555
|
+
self, remove_termination_string: Union[str, Callable[[str], str]], messages: list[dict[str, Any]]
|
|
1556
|
+
):
|
|
1557
|
+
"""Removes termination string, if required, and checks if termination may occur.
|
|
1558
|
+
|
|
1559
|
+
Args:
|
|
1560
|
+
remove_termination_string: Remove the termination string from the last message to prevent immediate termination
|
|
1561
|
+
If a string is provided, this string will be removed from last message.
|
|
1562
|
+
If a function is provided, the last message will be passed to this function, and the function returns the string after processing.
|
|
1563
|
+
messages: List of chat messages
|
|
1564
|
+
|
|
1565
|
+
Returns:
|
|
1566
|
+
None
|
|
1567
|
+
"""
|
|
1568
|
+
last_message = messages[-1]
|
|
1569
|
+
|
|
1570
|
+
# Replace any given termination string in the last message
|
|
1571
|
+
if isinstance(remove_termination_string, str):
|
|
1572
|
+
|
|
1573
|
+
def _remove_termination_string(content: str) -> str:
|
|
1574
|
+
return content.replace(remove_termination_string, "")
|
|
1575
|
+
|
|
1576
|
+
else:
|
|
1577
|
+
_remove_termination_string = remove_termination_string
|
|
1578
|
+
|
|
1579
|
+
if _remove_termination_string and messages[-1].get("content"):
|
|
1580
|
+
messages[-1]["content"] = _remove_termination_string(messages[-1]["content"])
|
|
1581
|
+
|
|
1582
|
+
# Check if the last message meets termination (if it has one)
|
|
1583
|
+
if self._is_termination_msg and self._is_termination_msg(last_message):
|
|
1584
|
+
logger.warning("WARNING: Last message meets termination criteria and this may terminate the chat.")
|
|
1585
|
+
|
|
1586
|
+
def messages_from_string(self, message_string: str) -> list[dict[str, Any]]:
|
|
1587
|
+
"""Reads the saved state of messages in Json format for resume and returns as a messages list
|
|
1588
|
+
|
|
1589
|
+
Args:
|
|
1590
|
+
message_string: Json string, the saved state
|
|
1591
|
+
|
|
1592
|
+
Returns:
|
|
1593
|
+
A list of messages
|
|
1594
|
+
"""
|
|
1595
|
+
try:
|
|
1596
|
+
state = json.loads(message_string)
|
|
1597
|
+
except json.JSONDecodeError:
|
|
1598
|
+
raise Exception("Messages string is not a valid JSON string")
|
|
1599
|
+
|
|
1600
|
+
return state
|
|
1601
|
+
|
|
1602
|
+
def messages_to_string(self, messages: list[dict[str, Any]]) -> str:
|
|
1603
|
+
"""Converts the provided messages into a Json string that can be used for resuming the chat.
|
|
1604
|
+
The state is made up of a list of messages
|
|
1605
|
+
|
|
1606
|
+
Args:
|
|
1607
|
+
messages: set of messages to convert to a string
|
|
1608
|
+
|
|
1609
|
+
Returns:
|
|
1610
|
+
A JSON representation of the messages which can be persisted for resuming later
|
|
1611
|
+
"""
|
|
1612
|
+
return json.dumps(messages)
|
|
1613
|
+
|
|
1614
|
+
def _raise_exception_on_async_reply_functions(self) -> None:
|
|
1615
|
+
"""Raise an exception if any async reply functions are registered.
|
|
1616
|
+
|
|
1617
|
+
Raises:
|
|
1618
|
+
RuntimeError: if any async reply functions are registered.
|
|
1619
|
+
"""
|
|
1620
|
+
super()._raise_exception_on_async_reply_functions()
|
|
1621
|
+
|
|
1622
|
+
for agent in self._groupchat.agents:
|
|
1623
|
+
agent._raise_exception_on_async_reply_functions()
|
|
1624
|
+
|
|
1625
|
+
def clear_agents_history(self, reply: dict[str, Any], groupchat: GroupChat) -> str:
|
|
1626
|
+
"""Clears history of messages for all agents or selected one. Can preserve selected number of last messages.
|
|
1627
|
+
That function is called when user manually provide "clear history" phrase in his reply.
|
|
1628
|
+
When "clear history" is provided, the history of messages for all agents is cleared.
|
|
1629
|
+
When "clear history `<agent_name>`" is provided, the history of messages for selected agent is cleared.
|
|
1630
|
+
When "clear history `<nr_of_messages_to_preserve>`" is provided, the history of messages for all agents is cleared
|
|
1631
|
+
except last `<nr_of_messages_to_preserve>` messages.
|
|
1632
|
+
When "clear history `<agent_name>` `<nr_of_messages_to_preserve>`" is provided, the history of messages for selected
|
|
1633
|
+
agent is cleared except last `<nr_of_messages_to_preserve>` messages.
|
|
1634
|
+
Phrase "clear history" and optional arguments are cut out from the reply before it passed to the chat.
|
|
1635
|
+
|
|
1636
|
+
Args:
|
|
1637
|
+
reply (dict): reply message dict to analyze.
|
|
1638
|
+
groupchat (GroupChat): GroupChat object.
|
|
1639
|
+
"""
|
|
1640
|
+
iostream = IOStream.get_default()
|
|
1641
|
+
|
|
1642
|
+
reply_content = reply["content"]
|
|
1643
|
+
# Split the reply into words
|
|
1644
|
+
words = reply_content.split()
|
|
1645
|
+
# Find the position of "clear" to determine where to start processing
|
|
1646
|
+
clear_word_index = next(i for i in reversed(range(len(words))) if words[i].upper() == "CLEAR")
|
|
1647
|
+
# Extract potential agent name and steps
|
|
1648
|
+
words_to_check = words[clear_word_index + 2 : clear_word_index + 4]
|
|
1649
|
+
nr_messages_to_preserve = None
|
|
1650
|
+
nr_messages_to_preserve_provided = False
|
|
1651
|
+
agent_to_memory_clear = None
|
|
1652
|
+
|
|
1653
|
+
for word in words_to_check:
|
|
1654
|
+
if word.isdigit():
|
|
1655
|
+
nr_messages_to_preserve = int(word)
|
|
1656
|
+
nr_messages_to_preserve_provided = True
|
|
1657
|
+
elif word[:-1].isdigit(): # for the case when number of messages is followed by dot or other sign
|
|
1658
|
+
nr_messages_to_preserve = int(word[:-1])
|
|
1659
|
+
nr_messages_to_preserve_provided = True
|
|
1660
|
+
else:
|
|
1661
|
+
for agent in groupchat.agents:
|
|
1662
|
+
if agent.name == word or agent.name == word[:-1]:
|
|
1663
|
+
agent_to_memory_clear = agent
|
|
1664
|
+
break
|
|
1665
|
+
# preserve last tool call message if clear history called inside of tool response
|
|
1666
|
+
if "tool_responses" in reply and not nr_messages_to_preserve:
|
|
1667
|
+
nr_messages_to_preserve = 1
|
|
1668
|
+
logger.warning(
|
|
1669
|
+
"The last tool call message will be saved to prevent errors caused by tool response without tool call."
|
|
1670
|
+
)
|
|
1671
|
+
# clear history
|
|
1672
|
+
iostream.send(
|
|
1673
|
+
ClearAgentsHistoryEvent(agent=agent_to_memory_clear, nr_events_to_preserve=nr_messages_to_preserve)
|
|
1674
|
+
)
|
|
1675
|
+
if agent_to_memory_clear:
|
|
1676
|
+
agent_to_memory_clear.clear_history(nr_messages_to_preserve=nr_messages_to_preserve)
|
|
1677
|
+
else:
|
|
1678
|
+
if nr_messages_to_preserve:
|
|
1679
|
+
# clearing history for groupchat here
|
|
1680
|
+
temp = groupchat.messages[-nr_messages_to_preserve:]
|
|
1681
|
+
groupchat.messages.clear()
|
|
1682
|
+
groupchat.messages.extend(temp)
|
|
1683
|
+
else:
|
|
1684
|
+
# clearing history for groupchat here
|
|
1685
|
+
groupchat.messages.clear()
|
|
1686
|
+
# clearing history for agents
|
|
1687
|
+
for agent in groupchat.agents:
|
|
1688
|
+
agent.clear_history(nr_messages_to_preserve=nr_messages_to_preserve)
|
|
1689
|
+
|
|
1690
|
+
# Reconstruct the reply without the "clear history" command and parameters
|
|
1691
|
+
skip_words_number = 2 + int(bool(agent_to_memory_clear)) + int(nr_messages_to_preserve_provided)
|
|
1692
|
+
reply_content = " ".join(words[:clear_word_index] + words[clear_word_index + skip_words_number :])
|
|
1693
|
+
|
|
1694
|
+
return reply_content
|