You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_group_chat_graph.py 61 kB

Enable concurrent execution of agents in GraphFlow (#6545) Support concurrent execution in `GraphFlow`: - Updated `BaseGroupChatManager.select_speaker` to return a union of a single string or a list of speaker name strings and added logics to check for currently activated speakers and only proceed to select next speakers when all activated speakers have finished. - Updated existing teams (e.g., `SelectorGroupChat`) with the new signature, while still returning a single speaker in their implementations. - Updated `GraphFlow` to support multiple speakers selected. - Refactored `GraphFlow` for less dictionary gymnastic by using a queue and update using `update_message_thread`. Example: a fan out graph: ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent("A", model_client=model_client, system_message="You are a helpful assistant.") agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to Chinese.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Japanese.") # Create a directed graph with fan-out flow A -> (B, C). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) builder.add_edge(agent_a, agent_b).add_edge(agent_a, agent_c) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, ) # Run the team and print the events. async for event in team.run_stream(task="Write a short story about a cat."): print(event) asyncio.run(main()) ``` Resolves: #6541 #6533
1 year ago
Enable concurrent execution of agents in GraphFlow (#6545) Support concurrent execution in `GraphFlow`: - Updated `BaseGroupChatManager.select_speaker` to return a union of a single string or a list of speaker name strings and added logics to check for currently activated speakers and only proceed to select next speakers when all activated speakers have finished. - Updated existing teams (e.g., `SelectorGroupChat`) with the new signature, while still returning a single speaker in their implementations. - Updated `GraphFlow` to support multiple speakers selected. - Refactored `GraphFlow` for less dictionary gymnastic by using a queue and update using `update_message_thread`. Example: a fan out graph: ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent("A", model_client=model_client, system_message="You are a helpful assistant.") agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to Chinese.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Japanese.") # Create a directed graph with fan-out flow A -> (B, C). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) builder.add_edge(agent_a, agent_b).add_edge(agent_a, agent_c) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, ) # Run the team and print the events. async for event in team.run_stream(task="Write a short story about a cat."): print(event) asyncio.run(main()) ``` Resolves: #6541 #6533
1 year ago
FIX: GraphFlow serialize/deserialize and adding test (#6434) ## Why are these changes needed? ❗ Before Previously, GraphFlow.__init__() modified the inner_chats and termination_condition for internal execution logic (e.g., constructing _StopAgent or composing OrTerminationCondition). However, these modified values were also used during dump_component(), meaning the serialized config no longer matched the original inputs. As a result: 1. dump_component() → load_component() → dump_component() produced non-idempotent configs. 2. Internal-only constructs like _StopAgent were mistakenly serialized, even though they should only exist in runtime. ⸻ ✅ After This patch changes the behavior to: • Store original inner_chats and termination_condition as-is at initialization. • During to_config(), serialize only the original unmodified versions. • Avoid serializing _StopAgent or other dynamically built agents. • Ensure deserialization (from_config) produces a logically equivalent object without additional nesting or duplication. This ensures that: • GraphFlow.dump_component() → load_component() round-trip produces consistent, minimal configs. • Internal execution logic and serialized component structure are properly separated. <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #6431 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [x] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [x] I've made sure all auto checks have passed.
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Fix GraphFlow to support multiple task execution without explicit reset (#6747) ## Problem When using GraphFlow with a termination condition, the second task execution would immediately terminate without running any agents. The first task would run successfully, but subsequent tasks would skip all agents and go directly to the stop agent. This was demonstrated by the following issue: ```python # First task runs correctly result1 = await team.run(task="First task") # ✅ Works fine # Second task fails immediately result2 = await team.run(task="Second task") # ❌ Only user + stop messages ``` ## Root Cause The `GraphFlowManager` was not resetting its execution state when termination occurred. After the first task completed: 1. The `_ready` queue was empty (all nodes had been processed) 2. The `_remaining` and `_enqueued_any` tracking structures remained in "completed" state 3. The `_message_thread` retained history from the previous task This left the graph in a "completed" state, causing subsequent tasks to immediately trigger the stop agent instead of executing the workflow. ## Solution Added an override of the `_apply_termination_condition` method in `GraphFlowManager` to automatically reset the graph execution state when termination occurs: ```python async def _apply_termination_condition( self, delta: Sequence[BaseAgentEvent | BaseChatMessage], increment_turn_count: bool = False ) -> bool: # Call the base implementation first terminated = await super()._apply_termination_condition(delta, increment_turn_count) # If terminated, reset the graph execution state and message thread for the next task if terminated: self._remaining = {target: Counter(groups) for target, groups in self._graph.get_remaining_map().items()} self._enqueued_any = {n: {g: False for g in self._enqueued_any[n]} for n in self._enqueued_any} self._ready = deque([n for n in self._graph.get_start_nodes()]) # Clear the message thread to start fresh for the next task self._message_thread.clear() return terminated ``` This ensures that when a task completes (termination condition is met), the graph is automatically reset to its initial state ready for the next task. ## Testing Added a comprehensive test case `test_digraph_group_chat_multiple_task_execution` that validates: - Multiple tasks can be run sequentially without explicit reset calls - All agents are executed the expected number of times - Both tasks produce the correct number of messages - The fix works with various termination conditions (MaxMessageTermination, TextMentionTermination) ## Result GraphFlow now works like SelectorGroupChat where multiple tasks can be run sequentially without explicit resets between them: ```python # Both tasks now work correctly result1 = await team.run(task="First task") # ✅ 5 messages, all agents called result2 = await team.run(task="Second task") # ✅ 5 messages, all agents called again ``` Fixes #6746. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `esm.ubuntu.com` > - Triggering command: `/usr/lib/apt/methods/https` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to my [firewall allow list](https://gh.io/copilot/firewall-config) > > </details> <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
FIX: GraphFlow serialize/deserialize and adding test (#6434) ## Why are these changes needed? ❗ Before Previously, GraphFlow.__init__() modified the inner_chats and termination_condition for internal execution logic (e.g., constructing _StopAgent or composing OrTerminationCondition). However, these modified values were also used during dump_component(), meaning the serialized config no longer matched the original inputs. As a result: 1. dump_component() → load_component() → dump_component() produced non-idempotent configs. 2. Internal-only constructs like _StopAgent were mistakenly serialized, even though they should only exist in runtime. ⸻ ✅ After This patch changes the behavior to: • Store original inner_chats and termination_condition as-is at initialization. • During to_config(), serialize only the original unmodified versions. • Avoid serializing _StopAgent or other dynamically built agents. • Ensure deserialization (from_config) produces a logically equivalent object without additional nesting or duplication. This ensures that: • GraphFlow.dump_component() → load_component() round-trip produces consistent, minimal configs. • Internal execution logic and serialized component structure are properly separated. <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #6431 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [x] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [x] I've made sure all auto checks have passed.
1 year ago
FIX: GraphFlow serialize/deserialize and adding test (#6434) ## Why are these changes needed? ❗ Before Previously, GraphFlow.__init__() modified the inner_chats and termination_condition for internal execution logic (e.g., constructing _StopAgent or composing OrTerminationCondition). However, these modified values were also used during dump_component(), meaning the serialized config no longer matched the original inputs. As a result: 1. dump_component() → load_component() → dump_component() produced non-idempotent configs. 2. Internal-only constructs like _StopAgent were mistakenly serialized, even though they should only exist in runtime. ⸻ ✅ After This patch changes the behavior to: • Store original inner_chats and termination_condition as-is at initialization. • During to_config(), serialize only the original unmodified versions. • Avoid serializing _StopAgent or other dynamically built agents. • Ensure deserialization (from_config) produces a logically equivalent object without additional nesting or duplication. This ensures that: • GraphFlow.dump_component() → load_component() round-trip produces consistent, minimal configs. • Internal execution logic and serialized component structure are properly separated. <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #6431 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [x] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [x] I've made sure all auto checks have passed.
1 year ago
FIX: GraphFlow serialize/deserialize and adding test (#6434) ## Why are these changes needed? ❗ Before Previously, GraphFlow.__init__() modified the inner_chats and termination_condition for internal execution logic (e.g., constructing _StopAgent or composing OrTerminationCondition). However, these modified values were also used during dump_component(), meaning the serialized config no longer matched the original inputs. As a result: 1. dump_component() → load_component() → dump_component() produced non-idempotent configs. 2. Internal-only constructs like _StopAgent were mistakenly serialized, even though they should only exist in runtime. ⸻ ✅ After This patch changes the behavior to: • Store original inner_chats and termination_condition as-is at initialization. • During to_config(), serialize only the original unmodified versions. • Avoid serializing _StopAgent or other dynamically built agents. • Ensure deserialization (from_config) produces a logically equivalent object without additional nesting or duplication. This ensures that: • GraphFlow.dump_component() → load_component() round-trip produces consistent, minimal configs. • Internal execution logic and serialized component structure are properly separated. <!-- Please give a short summary of the change and the problem this solves. --> ## Related issue number <!-- For example: "Closes #1234" --> Closes #6431 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [x] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [x] I've made sure all auto checks have passed.
1 year ago
Enable concurrent execution of agents in GraphFlow (#6545) Support concurrent execution in `GraphFlow`: - Updated `BaseGroupChatManager.select_speaker` to return a union of a single string or a list of speaker name strings and added logics to check for currently activated speakers and only proceed to select next speakers when all activated speakers have finished. - Updated existing teams (e.g., `SelectorGroupChat`) with the new signature, while still returning a single speaker in their implementations. - Updated `GraphFlow` to support multiple speakers selected. - Refactored `GraphFlow` for less dictionary gymnastic by using a queue and update using `update_message_thread`. Example: a fan out graph: ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent("A", model_client=model_client, system_message="You are a helpful assistant.") agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to Chinese.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Japanese.") # Create a directed graph with fan-out flow A -> (B, C). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) builder.add_edge(agent_a, agent_b).add_edge(agent_a, agent_c) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, ) # Run the team and print the events. async for event in team.run_stream(task="Write a short story about a cat."): print(event) asyncio.run(main()) ``` Resolves: #6541 #6533
1 year ago
Enable concurrent execution of agents in GraphFlow (#6545) Support concurrent execution in `GraphFlow`: - Updated `BaseGroupChatManager.select_speaker` to return a union of a single string or a list of speaker name strings and added logics to check for currently activated speakers and only proceed to select next speakers when all activated speakers have finished. - Updated existing teams (e.g., `SelectorGroupChat`) with the new signature, while still returning a single speaker in their implementations. - Updated `GraphFlow` to support multiple speakers selected. - Refactored `GraphFlow` for less dictionary gymnastic by using a queue and update using `update_message_thread`. Example: a fan out graph: ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent("A", model_client=model_client, system_message="You are a helpful assistant.") agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to Chinese.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Japanese.") # Create a directed graph with fan-out flow A -> (B, C). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) builder.add_edge(agent_a, agent_b).add_edge(agent_a, agent_c) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, ) # Run the team and print the events. async for event in team.run_stream(task="Write a short story about a cat."): print(event) asyncio.run(main()) ``` Resolves: #6541 #6533
1 year ago
Enable concurrent execution of agents in GraphFlow (#6545) Support concurrent execution in `GraphFlow`: - Updated `BaseGroupChatManager.select_speaker` to return a union of a single string or a list of speaker name strings and added logics to check for currently activated speakers and only proceed to select next speakers when all activated speakers have finished. - Updated existing teams (e.g., `SelectorGroupChat`) with the new signature, while still returning a single speaker in their implementations. - Updated `GraphFlow` to support multiple speakers selected. - Refactored `GraphFlow` for less dictionary gymnastic by using a queue and update using `update_message_thread`. Example: a fan out graph: ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent("A", model_client=model_client, system_message="You are a helpful assistant.") agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to Chinese.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Japanese.") # Create a directed graph with fan-out flow A -> (B, C). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) builder.add_edge(agent_a, agent_b).add_edge(agent_a, agent_c) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, ) # Run the team and print the events. async for event in team.run_stream(task="Write a short story about a cat."): print(event) asyncio.run(main()) ``` Resolves: #6541 #6533
1 year ago
Add callable condition for GraphFlow edges (#6623) This PR adds callable as an option to specify conditional edges in GraphFlow. ```python import asyncio from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.conditions import MaxMessageTermination from autogen_agentchat.teams import DiGraphBuilder, GraphFlow from autogen_ext.models.openai import OpenAIChatCompletionClient async def main(): # Initialize agents with OpenAI model clients. model_client = OpenAIChatCompletionClient(model="gpt-4.1-nano") agent_a = AssistantAgent( "A", model_client=model_client, system_message="Detect if the input is in Chinese. If it is, say 'yes', else say 'no', and nothing else.", ) agent_b = AssistantAgent("B", model_client=model_client, system_message="Translate input to English.") agent_c = AssistantAgent("C", model_client=model_client, system_message="Translate input to Chinese.") # Create a directed graph with conditional branching flow A -> B ("yes"), A -> C (otherwise). builder = DiGraphBuilder() builder.add_node(agent_a).add_node(agent_b).add_node(agent_c) # Create conditions as callables that check the message content. builder.add_edge(agent_a, agent_b, condition=lambda msg: "yes" in msg.to_model_text()) builder.add_edge(agent_a, agent_c, condition=lambda msg: "yes" not in msg.to_model_text()) graph = builder.build() # Create a GraphFlow team with the directed graph. team = GraphFlow( participants=[agent_a, agent_b, agent_c], graph=graph, termination_condition=MaxMessageTermination(5), ) # Run the team and print the events. async for event in team.run_stream(task="AutoGen is a framework for building AI agents."): print(event) asyncio.run(main()) ``` --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
1 year ago
Fix GraphFlow to support multiple task execution without explicit reset (#6747) ## Problem When using GraphFlow with a termination condition, the second task execution would immediately terminate without running any agents. The first task would run successfully, but subsequent tasks would skip all agents and go directly to the stop agent. This was demonstrated by the following issue: ```python # First task runs correctly result1 = await team.run(task="First task") # ✅ Works fine # Second task fails immediately result2 = await team.run(task="Second task") # ❌ Only user + stop messages ``` ## Root Cause The `GraphFlowManager` was not resetting its execution state when termination occurred. After the first task completed: 1. The `_ready` queue was empty (all nodes had been processed) 2. The `_remaining` and `_enqueued_any` tracking structures remained in "completed" state 3. The `_message_thread` retained history from the previous task This left the graph in a "completed" state, causing subsequent tasks to immediately trigger the stop agent instead of executing the workflow. ## Solution Added an override of the `_apply_termination_condition` method in `GraphFlowManager` to automatically reset the graph execution state when termination occurs: ```python async def _apply_termination_condition( self, delta: Sequence[BaseAgentEvent | BaseChatMessage], increment_turn_count: bool = False ) -> bool: # Call the base implementation first terminated = await super()._apply_termination_condition(delta, increment_turn_count) # If terminated, reset the graph execution state and message thread for the next task if terminated: self._remaining = {target: Counter(groups) for target, groups in self._graph.get_remaining_map().items()} self._enqueued_any = {n: {g: False for g in self._enqueued_any[n]} for n in self._enqueued_any} self._ready = deque([n for n in self._graph.get_start_nodes()]) # Clear the message thread to start fresh for the next task self._message_thread.clear() return terminated ``` This ensures that when a task completes (termination condition is met), the graph is automatically reset to its initial state ready for the next task. ## Testing Added a comprehensive test case `test_digraph_group_chat_multiple_task_execution` that validates: - Multiple tasks can be run sequentially without explicit reset calls - All agents are executed the expected number of times - Both tasks produce the correct number of messages - The fix works with various termination conditions (MaxMessageTermination, TextMentionTermination) ## Result GraphFlow now works like SelectorGroupChat where multiple tasks can be run sequentially without explicit resets between them: ```python # Both tasks now work correctly result1 = await team.run(task="First task") # ✅ 5 messages, all agents called result2 = await team.run(task="Second task") # ✅ 5 messages, all agents called again ``` Fixes #6746. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `esm.ubuntu.com` > - Triggering command: `/usr/lib/apt/methods/https` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to my [firewall allow list](https://gh.io/copilot/firewall-config) > > </details> <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
1 year ago
Fix GraphFlow to support multiple task execution without explicit reset (#6747) ## Problem When using GraphFlow with a termination condition, the second task execution would immediately terminate without running any agents. The first task would run successfully, but subsequent tasks would skip all agents and go directly to the stop agent. This was demonstrated by the following issue: ```python # First task runs correctly result1 = await team.run(task="First task") # ✅ Works fine # Second task fails immediately result2 = await team.run(task="Second task") # ❌ Only user + stop messages ``` ## Root Cause The `GraphFlowManager` was not resetting its execution state when termination occurred. After the first task completed: 1. The `_ready` queue was empty (all nodes had been processed) 2. The `_remaining` and `_enqueued_any` tracking structures remained in "completed" state 3. The `_message_thread` retained history from the previous task This left the graph in a "completed" state, causing subsequent tasks to immediately trigger the stop agent instead of executing the workflow. ## Solution Added an override of the `_apply_termination_condition` method in `GraphFlowManager` to automatically reset the graph execution state when termination occurs: ```python async def _apply_termination_condition( self, delta: Sequence[BaseAgentEvent | BaseChatMessage], increment_turn_count: bool = False ) -> bool: # Call the base implementation first terminated = await super()._apply_termination_condition(delta, increment_turn_count) # If terminated, reset the graph execution state and message thread for the next task if terminated: self._remaining = {target: Counter(groups) for target, groups in self._graph.get_remaining_map().items()} self._enqueued_any = {n: {g: False for g in self._enqueued_any[n]} for n in self._enqueued_any} self._ready = deque([n for n in self._graph.get_start_nodes()]) # Clear the message thread to start fresh for the next task self._message_thread.clear() return terminated ``` This ensures that when a task completes (termination condition is met), the graph is automatically reset to its initial state ready for the next task. ## Testing Added a comprehensive test case `test_digraph_group_chat_multiple_task_execution` that validates: - Multiple tasks can be run sequentially without explicit reset calls - All agents are executed the expected number of times - Both tasks produce the correct number of messages - The fix works with various termination conditions (MaxMessageTermination, TextMentionTermination) ## Result GraphFlow now works like SelectorGroupChat where multiple tasks can be run sequentially without explicit resets between them: ```python # Both tasks now work correctly result1 = await team.run(task="First task") # ✅ 5 messages, all agents called result2 = await team.run(task="Second task") # ✅ 5 messages, all agents called again ``` Fixes #6746. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `esm.ubuntu.com` > - Triggering command: `/usr/lib/apt/methods/https` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to my [firewall allow list](https://gh.io/copilot/firewall-config) > > </details> <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
1 year ago
Fix GraphFlow to support multiple task execution without explicit reset (#6747) ## Problem When using GraphFlow with a termination condition, the second task execution would immediately terminate without running any agents. The first task would run successfully, but subsequent tasks would skip all agents and go directly to the stop agent. This was demonstrated by the following issue: ```python # First task runs correctly result1 = await team.run(task="First task") # ✅ Works fine # Second task fails immediately result2 = await team.run(task="Second task") # ❌ Only user + stop messages ``` ## Root Cause The `GraphFlowManager` was not resetting its execution state when termination occurred. After the first task completed: 1. The `_ready` queue was empty (all nodes had been processed) 2. The `_remaining` and `_enqueued_any` tracking structures remained in "completed" state 3. The `_message_thread` retained history from the previous task This left the graph in a "completed" state, causing subsequent tasks to immediately trigger the stop agent instead of executing the workflow. ## Solution Added an override of the `_apply_termination_condition` method in `GraphFlowManager` to automatically reset the graph execution state when termination occurs: ```python async def _apply_termination_condition( self, delta: Sequence[BaseAgentEvent | BaseChatMessage], increment_turn_count: bool = False ) -> bool: # Call the base implementation first terminated = await super()._apply_termination_condition(delta, increment_turn_count) # If terminated, reset the graph execution state and message thread for the next task if terminated: self._remaining = {target: Counter(groups) for target, groups in self._graph.get_remaining_map().items()} self._enqueued_any = {n: {g: False for g in self._enqueued_any[n]} for n in self._enqueued_any} self._ready = deque([n for n in self._graph.get_start_nodes()]) # Clear the message thread to start fresh for the next task self._message_thread.clear() return terminated ``` This ensures that when a task completes (termination condition is met), the graph is automatically reset to its initial state ready for the next task. ## Testing Added a comprehensive test case `test_digraph_group_chat_multiple_task_execution` that validates: - Multiple tasks can be run sequentially without explicit reset calls - All agents are executed the expected number of times - Both tasks produce the correct number of messages - The fix works with various termination conditions (MaxMessageTermination, TextMentionTermination) ## Result GraphFlow now works like SelectorGroupChat where multiple tasks can be run sequentially without explicit resets between them: ```python # Both tasks now work correctly result1 = await team.run(task="First task") # ✅ 5 messages, all agents called result2 = await team.run(task="Second task") # ✅ 5 messages, all agents called again ``` Fixes #6746. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `esm.ubuntu.com` > - Triggering command: `/usr/lib/apt/methods/https` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to my [firewall allow list](https://gh.io/copilot/firewall-config) > > </details> <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
1 year ago
Fix GraphFlow to support multiple task execution without explicit reset (#6747) ## Problem When using GraphFlow with a termination condition, the second task execution would immediately terminate without running any agents. The first task would run successfully, but subsequent tasks would skip all agents and go directly to the stop agent. This was demonstrated by the following issue: ```python # First task runs correctly result1 = await team.run(task="First task") # ✅ Works fine # Second task fails immediately result2 = await team.run(task="Second task") # ❌ Only user + stop messages ``` ## Root Cause The `GraphFlowManager` was not resetting its execution state when termination occurred. After the first task completed: 1. The `_ready` queue was empty (all nodes had been processed) 2. The `_remaining` and `_enqueued_any` tracking structures remained in "completed" state 3. The `_message_thread` retained history from the previous task This left the graph in a "completed" state, causing subsequent tasks to immediately trigger the stop agent instead of executing the workflow. ## Solution Added an override of the `_apply_termination_condition` method in `GraphFlowManager` to automatically reset the graph execution state when termination occurs: ```python async def _apply_termination_condition( self, delta: Sequence[BaseAgentEvent | BaseChatMessage], increment_turn_count: bool = False ) -> bool: # Call the base implementation first terminated = await super()._apply_termination_condition(delta, increment_turn_count) # If terminated, reset the graph execution state and message thread for the next task if terminated: self._remaining = {target: Counter(groups) for target, groups in self._graph.get_remaining_map().items()} self._enqueued_any = {n: {g: False for g in self._enqueued_any[n]} for n in self._enqueued_any} self._ready = deque([n for n in self._graph.get_start_nodes()]) # Clear the message thread to start fresh for the next task self._message_thread.clear() return terminated ``` This ensures that when a task completes (termination condition is met), the graph is automatically reset to its initial state ready for the next task. ## Testing Added a comprehensive test case `test_digraph_group_chat_multiple_task_execution` that validates: - Multiple tasks can be run sequentially without explicit reset calls - All agents are executed the expected number of times - Both tasks produce the correct number of messages - The fix works with various termination conditions (MaxMessageTermination, TextMentionTermination) ## Result GraphFlow now works like SelectorGroupChat where multiple tasks can be run sequentially without explicit resets between them: ```python # Both tasks now work correctly result1 = await team.run(task="First task") # ✅ 5 messages, all agents called result2 = await team.run(task="Second task") # ✅ 5 messages, all agents called again ``` Fixes #6746. > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `esm.ubuntu.com` > - Triggering command: `/usr/lib/apt/methods/https` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to my [firewall allow list](https://gh.io/copilot/firewall-config) > > </details> <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey.alchemer.com/s3/8343779/Copilot-Coding-agent) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
1 year ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723
  1. import asyncio
  2. import re
  3. from typing import AsyncGenerator, List, Sequence
  4. from unittest.mock import patch
  5. import pytest
  6. import pytest_asyncio
  7. from autogen_agentchat.agents import (
  8. AssistantAgent,
  9. BaseChatAgent,
  10. MessageFilterAgent,
  11. MessageFilterConfig,
  12. PerSourceFilter,
  13. )
  14. from autogen_agentchat.base import Response, TaskResult
  15. from autogen_agentchat.conditions import MaxMessageTermination, SourceMatchTermination
  16. from autogen_agentchat.messages import BaseChatMessage, ChatMessage, MessageFactory, StopMessage, TextMessage
  17. from autogen_agentchat.teams import (
  18. DiGraphBuilder,
  19. GraphFlow,
  20. )
  21. from autogen_agentchat.teams._group_chat._events import ( # type: ignore[attr-defined]
  22. BaseAgentEvent,
  23. GroupChatTermination,
  24. )
  25. from autogen_agentchat.teams._group_chat._graph._digraph_group_chat import (
  26. DiGraph,
  27. DiGraphEdge,
  28. DiGraphNode,
  29. GraphFlowManager,
  30. )
  31. from autogen_core import AgentRuntime, CancellationToken, Component, SingleThreadedAgentRuntime
  32. from autogen_ext.models.replay import ReplayChatCompletionClient
  33. from pydantic import BaseModel
  34. from utils import compare_message_lists, compare_task_results
  35. def test_create_digraph() -> None:
  36. """Test creating a simple directed graph."""
  37. graph = DiGraph(
  38. nodes={
  39. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  40. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  41. "C": DiGraphNode(name="C", edges=[]),
  42. }
  43. )
  44. assert "A" in graph.nodes
  45. assert "B" in graph.nodes
  46. assert "C" in graph.nodes
  47. assert len(graph.nodes["A"].edges) == 1
  48. assert len(graph.nodes["B"].edges) == 1
  49. assert len(graph.nodes["C"].edges) == 0
  50. def test_get_parents() -> None:
  51. """Test computing parent relationships."""
  52. graph = DiGraph(
  53. nodes={
  54. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  55. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  56. "C": DiGraphNode(name="C", edges=[]),
  57. }
  58. )
  59. parents = graph.get_parents()
  60. assert parents["A"] == []
  61. assert parents["B"] == ["A"]
  62. assert parents["C"] == ["B"]
  63. def test_get_start_nodes() -> None:
  64. """Test retrieving start nodes (nodes with no incoming edges)."""
  65. graph = DiGraph(
  66. nodes={
  67. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  68. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  69. "C": DiGraphNode(name="C", edges=[]),
  70. }
  71. )
  72. start_nodes = graph.get_start_nodes()
  73. assert start_nodes == set(["A"])
  74. def test_get_leaf_nodes() -> None:
  75. """Test retrieving leaf nodes (nodes with no outgoing edges)."""
  76. graph = DiGraph(
  77. nodes={
  78. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  79. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  80. "C": DiGraphNode(name="C", edges=[]),
  81. }
  82. )
  83. leaf_nodes = graph.get_leaf_nodes()
  84. assert leaf_nodes == set(["C"])
  85. def test_serialization() -> None:
  86. """Test serializing and deserializing the graph."""
  87. # Use a string condition instead of a lambda
  88. graph = DiGraph(
  89. nodes={
  90. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B", condition="trigger1")]),
  91. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  92. "C": DiGraphNode(name="C", edges=[]),
  93. }
  94. )
  95. serialized = graph.model_dump_json()
  96. deserialized_graph = DiGraph.model_validate_json(serialized)
  97. assert deserialized_graph.nodes["A"].edges[0].target == "B"
  98. assert deserialized_graph.nodes["A"].edges[0].condition == "trigger1"
  99. assert deserialized_graph.nodes["B"].edges[0].target == "C"
  100. # Test the original condition works
  101. test_msg = TextMessage(content="this has trigger1 in it", source="test")
  102. # Manually check if the string is in the message text
  103. assert "trigger1" in test_msg.to_model_text()
  104. def test_invalid_graph_no_start_node() -> None:
  105. """Test validation failure when there is no start node."""
  106. graph = DiGraph(
  107. nodes={
  108. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  109. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="B")]), # Forms a cycle
  110. }
  111. )
  112. start_nodes = graph.get_start_nodes()
  113. assert len(start_nodes) == 0 # Now it correctly fails when no start nodes exist
  114. def test_invalid_graph_no_leaf_node() -> None:
  115. """Test validation failure when there is no leaf node."""
  116. graph = DiGraph(
  117. nodes={
  118. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  119. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  120. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="A")]), # Circular reference
  121. }
  122. )
  123. leaf_nodes = graph.get_leaf_nodes()
  124. assert len(leaf_nodes) == 0 # No true endpoint because of cycle
  125. def test_condition_edge_execution() -> None:
  126. """Test conditional edge execution support."""
  127. # Use string condition
  128. graph = DiGraph(
  129. nodes={
  130. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B", condition="TRIGGER")]),
  131. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  132. "C": DiGraphNode(name="C", edges=[]),
  133. }
  134. )
  135. # Check the condition manually
  136. test_message = TextMessage(content="This has TRIGGER in it", source="test")
  137. non_match_message = TextMessage(content="This doesn't match", source="test")
  138. # Check if the string condition is in each message text
  139. assert "TRIGGER" in test_message.to_model_text()
  140. assert "TRIGGER" not in non_match_message.to_model_text()
  141. # Check the condition itself
  142. assert graph.nodes["A"].edges[0].condition == "TRIGGER"
  143. assert graph.nodes["B"].edges[0].condition is None
  144. def test_graph_with_multiple_paths() -> None:
  145. """Test a graph with multiple execution paths."""
  146. graph = DiGraph(
  147. nodes={
  148. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B"), DiGraphEdge(target="C")]),
  149. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="D")]),
  150. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="D")]),
  151. "D": DiGraphNode(name="D", edges=[]),
  152. }
  153. )
  154. parents = graph.get_parents()
  155. assert parents["B"] == ["A"]
  156. assert parents["C"] == ["A"]
  157. assert parents["D"] == ["B", "C"]
  158. start_nodes = graph.get_start_nodes()
  159. assert start_nodes == set(["A"])
  160. leaf_nodes = graph.get_leaf_nodes()
  161. assert leaf_nodes == set(["D"])
  162. def test_cycle_detection_no_cycle() -> None:
  163. """Test that a valid acyclic graph returns False for cycle check."""
  164. graph = DiGraph(
  165. nodes={
  166. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  167. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  168. "C": DiGraphNode(name="C", edges=[]),
  169. }
  170. )
  171. assert not graph.has_cycles_with_exit()
  172. def test_cycle_detection_with_exit_condition() -> None:
  173. """Test a graph with cycle and conditional exit passes validation."""
  174. # Use a string condition
  175. graph = DiGraph(
  176. nodes={
  177. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  178. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  179. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="A", condition="exit")]), # Cycle with condition
  180. }
  181. )
  182. assert graph.has_cycles_with_exit()
  183. # Use a lambda condition
  184. graph_with_lambda = DiGraph(
  185. nodes={
  186. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  187. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  188. "C": DiGraphNode(
  189. name="C", edges=[DiGraphEdge(target="A", condition=lambda msg: "test" in msg.to_model_text())]
  190. ), # Cycle with lambda
  191. }
  192. )
  193. assert graph_with_lambda.has_cycles_with_exit()
  194. def test_cycle_detection_without_exit_condition() -> None:
  195. """Test that cycle without exit condition raises an error."""
  196. graph = DiGraph(
  197. nodes={
  198. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  199. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  200. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="A")]), # Cycle without condition
  201. "D": DiGraphNode(name="D", edges=[DiGraphEdge(target="E")]),
  202. "E": DiGraphNode(name="E", edges=[]),
  203. }
  204. )
  205. with pytest.raises(ValueError, match="Cycle detected without exit condition: A -> B -> C -> A"):
  206. graph.has_cycles_with_exit()
  207. def test_different_activation_groups_detection() -> None:
  208. """Test different activation groups."""
  209. graph = DiGraph(
  210. nodes={
  211. "A": DiGraphNode(
  212. name="A",
  213. edges=[
  214. DiGraphEdge(target="B"),
  215. DiGraphEdge(target="C"),
  216. ],
  217. ),
  218. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="D", activation_condition="all")]),
  219. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="D", activation_condition="any")]),
  220. "D": DiGraphNode(name="D", edges=[]),
  221. }
  222. )
  223. with pytest.raises(
  224. ValueError,
  225. match=re.escape(
  226. "Conflicting activation conditions for target 'D' group 'D': "
  227. "'all' (from node 'B') and 'any' (from node 'C')"
  228. ),
  229. ):
  230. graph.graph_validate()
  231. def test_validate_graph_success() -> None:
  232. """Test successful validation of a valid graph."""
  233. graph = DiGraph(
  234. nodes={
  235. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  236. "B": DiGraphNode(name="B", edges=[]),
  237. }
  238. )
  239. # No error should be raised
  240. graph.graph_validate()
  241. assert not graph.get_has_cycles()
  242. # Use a lambda condition
  243. graph_with_lambda = DiGraph(
  244. nodes={
  245. "A": DiGraphNode(
  246. name="A", edges=[DiGraphEdge(target="B", condition=lambda msg: "test" in msg.to_model_text())]
  247. ),
  248. "B": DiGraphNode(name="B", edges=[]),
  249. }
  250. )
  251. # No error should be raised
  252. graph_with_lambda.graph_validate()
  253. assert not graph_with_lambda.get_has_cycles()
  254. def test_validate_graph_missing_start_node() -> None:
  255. """Test validation failure when no start node exists."""
  256. graph = DiGraph(
  257. nodes={
  258. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  259. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="A")]), # Cycle
  260. }
  261. )
  262. with pytest.raises(ValueError, match="Graph must have at least one start node"):
  263. graph.graph_validate()
  264. def test_validate_graph_missing_leaf_node() -> None:
  265. """Test validation failure when no leaf node exists."""
  266. graph = DiGraph(
  267. nodes={
  268. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  269. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  270. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="B")]), # Cycle
  271. }
  272. )
  273. with pytest.raises(ValueError, match="Graph must have at least one leaf node"):
  274. graph.graph_validate()
  275. def test_validate_graph_mixed_conditions() -> None:
  276. """Test validation failure when node has mixed conditional and unconditional edges."""
  277. # Use string for condition
  278. graph = DiGraph(
  279. nodes={
  280. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B", condition="cond"), DiGraphEdge(target="C")]),
  281. "B": DiGraphNode(name="B", edges=[]),
  282. "C": DiGraphNode(name="C", edges=[]),
  283. }
  284. )
  285. with pytest.raises(ValueError, match="Node 'A' has a mix of conditional and unconditional edges"):
  286. graph.graph_validate()
  287. # Use lambda for condition
  288. graph_with_lambda = DiGraph(
  289. nodes={
  290. "A": DiGraphNode(
  291. name="A",
  292. edges=[
  293. DiGraphEdge(target="B", condition=lambda msg: "test" in msg.to_model_text()),
  294. DiGraphEdge(target="C"),
  295. ],
  296. ),
  297. "B": DiGraphNode(name="B", edges=[]),
  298. "C": DiGraphNode(name="C", edges=[]),
  299. }
  300. )
  301. with pytest.raises(ValueError, match="Node 'A' has a mix of conditional and unconditional edges"):
  302. graph_with_lambda.graph_validate()
  303. @pytest.mark.asyncio
  304. async def test_invalid_digraph_manager_cycle_without_termination() -> None:
  305. """Test GraphManager raises error for cyclic graph without termination condition."""
  306. # Create a cyclic graph A → B → A
  307. graph = DiGraph(
  308. nodes={
  309. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  310. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="A")]),
  311. }
  312. )
  313. output_queue: asyncio.Queue[BaseAgentEvent | BaseChatMessage | GroupChatTermination] = asyncio.Queue()
  314. with patch(
  315. "autogen_agentchat.teams._group_chat._base_group_chat_manager.BaseGroupChatManager.__init__",
  316. return_value=None,
  317. ):
  318. manager = GraphFlowManager.__new__(GraphFlowManager)
  319. with pytest.raises(ValueError, match="Graph must have at least one start node"):
  320. manager.__init__( # type: ignore[misc]
  321. name="test_manager",
  322. group_topic_type="topic",
  323. output_topic_type="topic",
  324. participant_topic_types=["topic1", "topic2"],
  325. participant_names=["A", "B"],
  326. participant_descriptions=["Agent A", "Agent B"],
  327. output_message_queue=output_queue,
  328. termination_condition=None,
  329. max_turns=None,
  330. message_factory=MessageFactory(),
  331. graph=graph,
  332. )
  333. class _EchoAgent(BaseChatAgent):
  334. def __init__(self, name: str, description: str) -> None:
  335. super().__init__(name, description)
  336. self._last_message: str | None = None
  337. self._total_messages = 0
  338. @property
  339. def produced_message_types(self) -> Sequence[type[BaseChatMessage]]:
  340. return (TextMessage,)
  341. @property
  342. def total_messages(self) -> int:
  343. return self._total_messages
  344. async def on_messages(self, messages: Sequence[BaseChatMessage], cancellation_token: CancellationToken) -> Response:
  345. if len(messages) > 0:
  346. assert isinstance(messages[0], TextMessage) or isinstance(messages[0], StopMessage)
  347. self._last_message = messages[0].content
  348. self._total_messages += 1
  349. return Response(chat_message=TextMessage(content=messages[0].content, source=self.name))
  350. else:
  351. assert self._last_message is not None
  352. self._total_messages += 1
  353. return Response(chat_message=TextMessage(content=self._last_message, source=self.name))
  354. async def on_reset(self, cancellation_token: CancellationToken) -> None:
  355. self._last_message = None
  356. @pytest_asyncio.fixture(params=["single_threaded", "embedded"]) # type: ignore
  357. async def runtime(request: pytest.FixtureRequest) -> AsyncGenerator[AgentRuntime | None, None]:
  358. if request.param == "single_threaded":
  359. runtime = SingleThreadedAgentRuntime()
  360. runtime.start()
  361. yield runtime
  362. await runtime.stop()
  363. elif request.param == "embedded":
  364. yield None
  365. TaskType = str | List[ChatMessage] | ChatMessage
  366. @pytest.mark.asyncio
  367. async def test_digraph_group_chat_sequential_execution(runtime: AgentRuntime | None) -> None:
  368. # Create agents A → B → C
  369. agent_a = _EchoAgent("A", description="Echo agent A")
  370. agent_b = _EchoAgent("B", description="Echo agent B")
  371. agent_c = _EchoAgent("C", description="Echo agent C")
  372. # Define graph A → B → C
  373. graph = DiGraph(
  374. nodes={
  375. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  376. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  377. "C": DiGraphNode(name="C", edges=[]),
  378. }
  379. )
  380. # Create team using Graph
  381. team = GraphFlow(
  382. participants=[agent_a, agent_b, agent_c],
  383. graph=graph,
  384. runtime=runtime,
  385. termination_condition=MaxMessageTermination(5),
  386. )
  387. # Run the chat
  388. result: TaskResult = await team.run(task="Hello from User")
  389. assert len(result.messages) == 4
  390. assert isinstance(result.messages[0], TextMessage)
  391. assert result.messages[0].source == "user"
  392. assert result.messages[1].source == "A"
  393. assert result.messages[2].source == "B"
  394. assert result.messages[3].source == "C"
  395. assert all(isinstance(m, TextMessage) for m in result.messages)
  396. assert result.stop_reason is not None
  397. @pytest.mark.asyncio
  398. async def test_digraph_group_chat_parallel_fanout(runtime: AgentRuntime | None) -> None:
  399. agent_a = _EchoAgent("A", description="Echo agent A")
  400. agent_b = _EchoAgent("B", description="Echo agent B")
  401. agent_c = _EchoAgent("C", description="Echo agent C")
  402. graph = DiGraph(
  403. nodes={
  404. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B"), DiGraphEdge(target="C")]),
  405. "B": DiGraphNode(name="B", edges=[]),
  406. "C": DiGraphNode(name="C", edges=[]),
  407. }
  408. )
  409. team = GraphFlow(
  410. participants=[agent_a, agent_b, agent_c],
  411. graph=graph,
  412. runtime=runtime,
  413. termination_condition=MaxMessageTermination(5),
  414. )
  415. result: TaskResult = await team.run(task="Start")
  416. assert len(result.messages) == 4
  417. assert result.messages[0].source == "user"
  418. assert result.messages[1].source == "A"
  419. assert set(m.source for m in result.messages[2:]) == {"B", "C"}
  420. assert result.stop_reason is not None
  421. @pytest.mark.asyncio
  422. async def test_digraph_group_chat_parallel_join_all(runtime: AgentRuntime | None) -> None:
  423. agent_a = _EchoAgent("A", description="Echo agent A")
  424. agent_b = _EchoAgent("B", description="Echo agent B")
  425. agent_c = _EchoAgent("C", description="Echo agent C")
  426. graph = DiGraph(
  427. nodes={
  428. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="C")]),
  429. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  430. "C": DiGraphNode(name="C", edges=[], activation="all"),
  431. }
  432. )
  433. team = GraphFlow(
  434. participants=[agent_a, agent_b, agent_c],
  435. graph=graph,
  436. runtime=runtime,
  437. termination_condition=MaxMessageTermination(5),
  438. )
  439. result: TaskResult = await team.run(task="Go")
  440. assert len(result.messages) == 4
  441. assert result.messages[0].source == "user"
  442. assert set([result.messages[1].source, result.messages[2].source]) == {"A", "B"}
  443. assert result.messages[3].source == "C"
  444. assert result.stop_reason is not None
  445. @pytest.mark.asyncio
  446. async def test_digraph_group_chat_parallel_join_any(runtime: AgentRuntime | None) -> None:
  447. agent_a = _EchoAgent("A", description="Echo agent A")
  448. agent_b = _EchoAgent("B", description="Echo agent B")
  449. agent_c = _EchoAgent("C", description="Echo agent C")
  450. graph = DiGraph(
  451. nodes={
  452. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="C")]),
  453. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  454. "C": DiGraphNode(name="C", edges=[], activation="any"),
  455. }
  456. )
  457. team = GraphFlow(
  458. participants=[agent_a, agent_b, agent_c],
  459. graph=graph,
  460. runtime=runtime,
  461. termination_condition=MaxMessageTermination(5),
  462. )
  463. result: TaskResult = await team.run(task="Start")
  464. assert len(result.messages) == 4
  465. assert result.messages[0].source == "user"
  466. sources = [m.source for m in result.messages[1:]]
  467. # C must be last
  468. assert sources[-1] == "C"
  469. # A and B must both execute
  470. assert {"A", "B"}.issubset(set(sources))
  471. # One of A or B must execute before C
  472. index_a = sources.index("A")
  473. index_b = sources.index("B")
  474. index_c = sources.index("C")
  475. assert index_c > min(index_a, index_b)
  476. assert result.stop_reason is not None
  477. @pytest.mark.asyncio
  478. async def test_digraph_group_chat_multiple_start_nodes(runtime: AgentRuntime | None) -> None:
  479. agent_a = _EchoAgent("A", description="Echo agent A")
  480. agent_b = _EchoAgent("B", description="Echo agent B")
  481. graph = DiGraph(
  482. nodes={
  483. "A": DiGraphNode(name="A", edges=[]),
  484. "B": DiGraphNode(name="B", edges=[]),
  485. }
  486. )
  487. team = GraphFlow(
  488. participants=[agent_a, agent_b],
  489. graph=graph,
  490. runtime=runtime,
  491. termination_condition=MaxMessageTermination(5),
  492. )
  493. result: TaskResult = await team.run(task="Start")
  494. assert len(result.messages) == 3
  495. assert result.messages[0].source == "user"
  496. assert set(m.source for m in result.messages[1:]) == {"A", "B"}
  497. assert result.stop_reason is not None
  498. @pytest.mark.asyncio
  499. async def test_digraph_group_chat_disconnected_graph(runtime: AgentRuntime | None) -> None:
  500. agent_a = _EchoAgent("A", description="Echo agent A")
  501. agent_b = _EchoAgent("B", description="Echo agent B")
  502. agent_c = _EchoAgent("C", description="Echo agent C")
  503. agent_d = _EchoAgent("D", description="Echo agent D")
  504. graph = DiGraph(
  505. nodes={
  506. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  507. "B": DiGraphNode(name="B", edges=[]),
  508. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="D")]),
  509. "D": DiGraphNode(name="D", edges=[]),
  510. }
  511. )
  512. team = GraphFlow(
  513. participants=[agent_a, agent_b, agent_c, agent_d],
  514. graph=graph,
  515. runtime=runtime,
  516. termination_condition=MaxMessageTermination(10),
  517. )
  518. result: TaskResult = await team.run(task="Go")
  519. assert len(result.messages) == 5
  520. assert result.messages[0].source == "user"
  521. assert {"A", "C"} == set([result.messages[1].source, result.messages[2].source])
  522. assert {"B", "D"} == set([result.messages[3].source, result.messages[4].source])
  523. assert result.stop_reason is not None
  524. @pytest.mark.asyncio
  525. async def test_digraph_group_chat_conditional_branch(runtime: AgentRuntime | None) -> None:
  526. agent_a = _EchoAgent("A", description="Echo agent A")
  527. agent_b = _EchoAgent("B", description="Echo agent B")
  528. agent_c = _EchoAgent("C", description="Echo agent C")
  529. # Use string conditions
  530. graph = DiGraph(
  531. nodes={
  532. "A": DiGraphNode(
  533. name="A", edges=[DiGraphEdge(target="B", condition="yes"), DiGraphEdge(target="C", condition="no")]
  534. ),
  535. "B": DiGraphNode(name="B", edges=[], activation="any"),
  536. "C": DiGraphNode(name="C", edges=[], activation="any"),
  537. }
  538. )
  539. team = GraphFlow(
  540. participants=[agent_a, agent_b, agent_c],
  541. graph=graph,
  542. runtime=runtime,
  543. termination_condition=MaxMessageTermination(5),
  544. )
  545. result = await team.run(task="Trigger yes")
  546. assert result.messages[2].source == "B"
  547. # Use lambda conditions
  548. graph_with_lambda = DiGraph(
  549. nodes={
  550. "A": DiGraphNode(
  551. name="A",
  552. edges=[
  553. DiGraphEdge(target="B", condition=lambda msg: "yes" in msg.to_model_text()),
  554. DiGraphEdge(target="C", condition=lambda msg: "no" in msg.to_model_text()),
  555. ],
  556. ),
  557. "B": DiGraphNode(name="B", edges=[], activation="any"),
  558. "C": DiGraphNode(name="C", edges=[], activation="any"),
  559. }
  560. )
  561. team_with_lambda = GraphFlow(
  562. participants=[agent_a, agent_b, agent_c],
  563. graph=graph_with_lambda,
  564. runtime=runtime,
  565. termination_condition=MaxMessageTermination(5),
  566. )
  567. result_with_lambda = await team_with_lambda.run(task="Trigger no")
  568. assert result_with_lambda.messages[2].source == "C"
  569. @pytest.mark.asyncio
  570. async def test_digraph_group_chat_loop_with_exit_condition(runtime: AgentRuntime | None) -> None:
  571. # Agents A and C: Echo Agents
  572. agent_a = _EchoAgent("A", description="Echo agent A")
  573. agent_c = _EchoAgent("C", description="Echo agent C")
  574. # Replay model client for agent B
  575. model_client = ReplayChatCompletionClient(
  576. chat_completions=[
  577. "loop", # First time B will ask to loop
  578. "loop", # Second time B will ask to loop
  579. "exit", # Third time B will say exit
  580. ]
  581. )
  582. # Agent B: Assistant Agent using Replay Client
  583. agent_b = AssistantAgent("B", description="Decision agent B", model_client=model_client)
  584. # DiGraph: A → B → C (conditional back to A or terminate)
  585. graph = DiGraph(
  586. nodes={
  587. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  588. "B": DiGraphNode(
  589. name="B", edges=[DiGraphEdge(target="C", condition="exit"), DiGraphEdge(target="A", condition="loop")]
  590. ),
  591. "C": DiGraphNode(name="C", edges=[]),
  592. },
  593. default_start_node="A",
  594. )
  595. team = GraphFlow(
  596. participants=[agent_a, agent_b, agent_c],
  597. graph=graph,
  598. runtime=runtime,
  599. termination_condition=MaxMessageTermination(20),
  600. )
  601. # Run
  602. result = await team.run(task="Start")
  603. # Assert message order
  604. expected_sources = [
  605. "user",
  606. "A",
  607. "B", # 1st loop
  608. "A",
  609. "B", # 2nd loop
  610. "A",
  611. "B",
  612. "C",
  613. ]
  614. actual_sources = [m.source for m in result.messages]
  615. assert actual_sources == expected_sources
  616. assert result.stop_reason is not None
  617. assert result.messages[-1].source == "C"
  618. assert any(m.content == "exit" for m in result.messages) # type: ignore[attr-defined,union-attr]
  619. @pytest.mark.asyncio
  620. async def test_digraph_group_chat_loop_with_self_cycle(runtime: AgentRuntime | None) -> None:
  621. # Agents A and C: Echo Agents
  622. agent_a = _EchoAgent("A", description="Echo agent A")
  623. agent_c = _EchoAgent("C", description="Echo agent C")
  624. # Replay model client for agent B
  625. model_client = ReplayChatCompletionClient(
  626. chat_completions=[
  627. "loop", # First time B will ask to loop
  628. "loop", # Second time B will ask to loop
  629. "exit", # Third time B will say exit
  630. ]
  631. )
  632. # Agent B: Assistant Agent using Replay Client
  633. agent_b = AssistantAgent("B", description="Decision agent B", model_client=model_client)
  634. # DiGraph: A → B(self loop) → C (conditional back to A or terminate)
  635. graph = DiGraph(
  636. nodes={
  637. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  638. "B": DiGraphNode(
  639. name="B",
  640. edges=[
  641. DiGraphEdge(target="C", condition="exit"),
  642. DiGraphEdge(target="B", condition="loop", activation_group="B_loop"),
  643. ],
  644. ),
  645. "C": DiGraphNode(name="C", edges=[]),
  646. },
  647. default_start_node="A",
  648. )
  649. team = GraphFlow(
  650. participants=[agent_a, agent_b, agent_c],
  651. graph=graph,
  652. runtime=runtime,
  653. termination_condition=MaxMessageTermination(20),
  654. )
  655. # Run
  656. result = await team.run(task="Start")
  657. # Assert message order
  658. expected_sources = [
  659. "user",
  660. "A",
  661. "B", # 1st loop
  662. "B", # 2nd loop
  663. "B",
  664. "C",
  665. ]
  666. actual_sources = [m.source for m in result.messages]
  667. assert actual_sources == expected_sources
  668. assert result.stop_reason is not None
  669. assert result.messages[-1].source == "C"
  670. assert any(m.content == "exit" for m in result.messages) # type: ignore[attr-defined,union-attr]
  671. @pytest.mark.asyncio
  672. async def test_digraph_group_chat_loop_with_two_cycles(runtime: AgentRuntime | None) -> None:
  673. # Agents A and C: Echo Agents
  674. agent_a = _EchoAgent("A", description="Echo agent A")
  675. agent_b = _EchoAgent("B", description="Echo agent B")
  676. agent_c = _EchoAgent("C", description="Echo agent C")
  677. agent_e = _EchoAgent("E", description="Echo agent E")
  678. # Replay model client for agent B
  679. model_client = ReplayChatCompletionClient(
  680. chat_completions=[
  681. "to_x", # First time O will branch to B
  682. "to_o", # X will go back to O
  683. "to_y", # Second time O will branch to C
  684. "to_o", # Y will go back to O
  685. "exit", # Third time O will say exit
  686. ]
  687. )
  688. # Agent o, b, c: Assistant Agent using Replay Client
  689. agent_o = AssistantAgent("O", description="Decision agent o", model_client=model_client)
  690. agent_x = AssistantAgent("X", description="Decision agent x", model_client=model_client)
  691. agent_y = AssistantAgent("Y", description="Decision agent y", model_client=model_client)
  692. # DiGraph:
  693. #
  694. # A
  695. # / \
  696. # B C
  697. # \ |
  698. # X = O = Y (bidirectional)
  699. # |
  700. # E(exit)
  701. graph = DiGraph(
  702. nodes={
  703. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B"), DiGraphEdge(target="C")]),
  704. "B": DiGraphNode(
  705. name="B", edges=[DiGraphEdge(target="O")]
  706. ), # default activation group name is same as target node name "O"
  707. "C": DiGraphNode(
  708. name="C", edges=[DiGraphEdge(target="O")]
  709. ), # default activation group name is same as target node name "O"
  710. "O": DiGraphNode(
  711. name="O",
  712. edges=[
  713. DiGraphEdge(target="X", condition="to_x"),
  714. DiGraphEdge(target="Y", condition="to_y"),
  715. DiGraphEdge(target="E", condition="exit"),
  716. ],
  717. ),
  718. "X": DiGraphNode(name="X", edges=[DiGraphEdge(target="O", condition="to_o", activation_group="x_o_loop")]),
  719. "Y": DiGraphNode(name="Y", edges=[DiGraphEdge(target="O", condition="to_o", activation_group="y_o_loop")]),
  720. "E": DiGraphNode(name="E", edges=[]),
  721. },
  722. default_start_node="A",
  723. )
  724. team = GraphFlow(
  725. participants=[agent_a, agent_o, agent_b, agent_c, agent_x, agent_y, agent_e],
  726. graph=graph,
  727. runtime=runtime,
  728. termination_condition=MaxMessageTermination(20),
  729. )
  730. # Run
  731. result = await team.run(task="Start")
  732. # Assert message order
  733. expected_sources = [
  734. "user",
  735. "A",
  736. "B",
  737. "C",
  738. "O",
  739. "X", # O -> X
  740. "O", # X -> O
  741. "Y", # O -> Y
  742. "O", # Y -> O
  743. "E", # O -> E
  744. ]
  745. actual_sources = [m.source for m in result.messages]
  746. assert actual_sources == expected_sources
  747. assert result.stop_reason is not None
  748. assert result.messages[-1].source == "E"
  749. assert any(m.content == "exit" for m in result.messages) # type: ignore[attr-defined,union-attr]
  750. @pytest.mark.asyncio
  751. async def test_digraph_group_chat_parallel_join_any_1(runtime: AgentRuntime | None) -> None:
  752. agent_a = _EchoAgent("A", description="Echo agent A")
  753. agent_b = _EchoAgent("B", description="Echo agent B")
  754. agent_c = _EchoAgent("C", description="Echo agent C")
  755. agent_d = _EchoAgent("D", description="Echo agent D")
  756. graph = DiGraph(
  757. nodes={
  758. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B"), DiGraphEdge(target="C")]),
  759. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="D", activation_group="any")]),
  760. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="D", activation_group="any")]),
  761. "D": DiGraphNode(name="D", edges=[]),
  762. }
  763. )
  764. team = GraphFlow(
  765. participants=[agent_a, agent_b, agent_c, agent_d],
  766. graph=graph,
  767. runtime=runtime,
  768. termination_condition=MaxMessageTermination(10),
  769. )
  770. result = await team.run(task="Run parallel join")
  771. sequence = [msg.source for msg in result.messages if isinstance(msg, TextMessage)]
  772. assert sequence[0] == "user"
  773. # B and C should both run
  774. assert "B" in sequence
  775. assert "C" in sequence
  776. # D should trigger twice → once after B and once after C (order depends on runtime)
  777. d_indices = [i for i, s in enumerate(sequence) if s == "D"]
  778. assert len(d_indices) == 1
  779. # Each D trigger must be after corresponding B or C
  780. b_index = sequence.index("B")
  781. c_index = sequence.index("C")
  782. assert any(d > b_index for d in d_indices)
  783. assert any(d > c_index for d in d_indices)
  784. assert result.stop_reason is not None
  785. @pytest.mark.asyncio
  786. async def test_digraph_group_chat_chained_parallel_join_any(runtime: AgentRuntime | None) -> None:
  787. agent_a = _EchoAgent("A", description="Echo agent A")
  788. agent_b = _EchoAgent("B", description="Echo agent B")
  789. agent_c = _EchoAgent("C", description="Echo agent C")
  790. agent_d = _EchoAgent("D", description="Echo agent D")
  791. agent_e = _EchoAgent("E", description="Echo agent E")
  792. graph = DiGraph(
  793. nodes={
  794. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B"), DiGraphEdge(target="C")]),
  795. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="D")]),
  796. "C": DiGraphNode(name="C", edges=[DiGraphEdge(target="D")]),
  797. "D": DiGraphNode(name="D", edges=[DiGraphEdge(target="E")], activation="any"),
  798. "E": DiGraphNode(name="E", edges=[], activation="any"),
  799. }
  800. )
  801. team = GraphFlow(
  802. participants=[agent_a, agent_b, agent_c, agent_d, agent_e],
  803. graph=graph,
  804. runtime=runtime,
  805. termination_condition=MaxMessageTermination(20),
  806. )
  807. result = await team.run(task="Run chained parallel join-any")
  808. sequence = [msg.source for msg in result.messages if isinstance(msg, TextMessage)]
  809. # D should trigger twice
  810. d_indices = [i for i, s in enumerate(sequence) if s == "D"]
  811. assert len(d_indices) == 1
  812. # Each D trigger must be after corresponding B or C
  813. b_index = sequence.index("B")
  814. c_index = sequence.index("C")
  815. assert any(d > b_index for d in d_indices)
  816. assert any(d > c_index for d in d_indices)
  817. # E should also trigger twice → once after each D
  818. e_indices = [i for i, s in enumerate(sequence) if s == "E"]
  819. assert len(e_indices) == 1
  820. assert e_indices[0] > d_indices[0]
  821. assert result.stop_reason is not None
  822. @pytest.mark.asyncio
  823. async def test_digraph_group_chat_multiple_conditional(runtime: AgentRuntime | None) -> None:
  824. agent_a = _EchoAgent("A", description="Echo agent A")
  825. agent_b = _EchoAgent("B", description="Echo agent B")
  826. agent_c = _EchoAgent("C", description="Echo agent C")
  827. agent_d = _EchoAgent("D", description="Echo agent D")
  828. # Use string conditions
  829. graph = DiGraph(
  830. nodes={
  831. "A": DiGraphNode(
  832. name="A",
  833. edges=[
  834. DiGraphEdge(target="B", condition="apple"),
  835. DiGraphEdge(target="C", condition="banana"),
  836. DiGraphEdge(target="D", condition="cherry"),
  837. ],
  838. ),
  839. "B": DiGraphNode(name="B", edges=[]),
  840. "C": DiGraphNode(name="C", edges=[]),
  841. "D": DiGraphNode(name="D", edges=[]),
  842. }
  843. )
  844. team = GraphFlow(
  845. participants=[agent_a, agent_b, agent_c, agent_d],
  846. graph=graph,
  847. runtime=runtime,
  848. termination_condition=MaxMessageTermination(5),
  849. )
  850. # Test banana branch
  851. result = await team.run(task="banana")
  852. assert result.messages[2].source == "C"
  853. # Use lambda conditions
  854. graph_with_lambda = DiGraph(
  855. nodes={
  856. "A": DiGraphNode(
  857. name="A",
  858. edges=[
  859. DiGraphEdge(target="B", condition=lambda msg: "apple" in msg.to_model_text()),
  860. DiGraphEdge(target="C", condition=lambda msg: "banana" in msg.to_model_text()),
  861. DiGraphEdge(target="D", condition=lambda msg: "cherry" in msg.to_model_text()),
  862. ],
  863. ),
  864. "B": DiGraphNode(name="B", edges=[]),
  865. "C": DiGraphNode(name="C", edges=[]),
  866. "D": DiGraphNode(name="D", edges=[]),
  867. }
  868. )
  869. team_with_lambda = GraphFlow(
  870. participants=[agent_a, agent_b, agent_c, agent_d],
  871. graph=graph_with_lambda,
  872. runtime=runtime,
  873. termination_condition=MaxMessageTermination(5),
  874. )
  875. result_with_lambda = await team_with_lambda.run(task="cherry")
  876. assert result_with_lambda.messages[2].source == "D"
  877. class _TestMessageFilterAgentConfig(BaseModel):
  878. name: str
  879. description: str = "Echo test agent"
  880. class _TestMessageFilterAgent(BaseChatAgent, Component[_TestMessageFilterAgentConfig]):
  881. component_config_schema = _TestMessageFilterAgentConfig
  882. component_provider_override = "test_group_chat_graph._TestMessageFilterAgent"
  883. def __init__(self, name: str, description: str = "Echo test agent") -> None:
  884. super().__init__(name=name, description=description)
  885. self.received_messages: list[BaseChatMessage] = []
  886. @property
  887. def produced_message_types(self) -> Sequence[type[BaseChatMessage]]:
  888. return (TextMessage,)
  889. async def on_messages(self, messages: Sequence[BaseChatMessage], cancellation_token: CancellationToken) -> Response:
  890. self.received_messages.extend(messages)
  891. return Response(chat_message=TextMessage(content="ACK", source=self.name))
  892. async def on_reset(self, cancellation_token: CancellationToken) -> None:
  893. self.received_messages.clear()
  894. def _to_config(self) -> _TestMessageFilterAgentConfig:
  895. return _TestMessageFilterAgentConfig(name=self.name, description=self.description)
  896. @classmethod
  897. def _from_config(cls, config: _TestMessageFilterAgentConfig) -> "_TestMessageFilterAgent":
  898. return cls(name=config.name, description=config.description)
  899. @pytest.mark.asyncio
  900. async def test_message_filter_agent_empty_filter_blocks_all() -> None:
  901. inner_agent = _TestMessageFilterAgent("inner")
  902. wrapper = MessageFilterAgent(
  903. name="wrapper",
  904. wrapped_agent=inner_agent,
  905. filter=MessageFilterConfig(per_source=[]),
  906. )
  907. messages = [
  908. TextMessage(source="user", content="Hello"),
  909. TextMessage(source="system", content="System msg"),
  910. ]
  911. await wrapper.on_messages(messages, CancellationToken())
  912. assert len(inner_agent.received_messages) == 0
  913. @pytest.mark.asyncio
  914. async def test_message_filter_agent_with_position_none_gets_all() -> None:
  915. inner_agent = _TestMessageFilterAgent("inner")
  916. wrapper = MessageFilterAgent(
  917. name="wrapper",
  918. wrapped_agent=inner_agent,
  919. filter=MessageFilterConfig(per_source=[PerSourceFilter(source="user", position=None, count=None)]),
  920. )
  921. messages = [
  922. TextMessage(source="user", content="A"),
  923. TextMessage(source="user", content="B"),
  924. TextMessage(source="system", content="Ignore this"),
  925. ]
  926. await wrapper.on_messages(messages, CancellationToken())
  927. assert len(inner_agent.received_messages) == 2
  928. assert {m.content for m in inner_agent.received_messages} == {"A", "B"} # type: ignore[attr-defined]
  929. @pytest.mark.asyncio
  930. async def test_digraph_group_chat() -> None:
  931. inner_agent = _TestMessageFilterAgent("agent")
  932. wrapper = MessageFilterAgent(
  933. name="agent",
  934. wrapped_agent=inner_agent,
  935. filter=MessageFilterConfig(
  936. per_source=[
  937. PerSourceFilter(source="user", position="last", count=2),
  938. PerSourceFilter(source="system", position="first", count=1),
  939. ]
  940. ),
  941. )
  942. config = wrapper.dump_component()
  943. loaded = MessageFilterAgent.load_component(config)
  944. assert loaded.name == "agent"
  945. assert loaded._filter == wrapper._filter # pyright: ignore[reportPrivateUsage]
  946. assert loaded._wrapped_agent.name == wrapper._wrapped_agent.name # pyright: ignore[reportPrivateUsage]
  947. # Run on_messages and validate filtering still works
  948. messages = [
  949. TextMessage(source="user", content="u1"),
  950. TextMessage(source="user", content="u2"),
  951. TextMessage(source="user", content="u3"),
  952. TextMessage(source="system", content="s1"),
  953. TextMessage(source="system", content="s2"),
  954. ]
  955. await loaded.on_messages(messages, CancellationToken())
  956. received = loaded._wrapped_agent.received_messages # type: ignore[attr-defined]
  957. assert {m.content for m in received} == {"u2", "u3", "s1"} # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
  958. @pytest.mark.asyncio
  959. async def test_message_filter_agent_in_digraph_group_chat(runtime: AgentRuntime | None) -> None:
  960. inner_agent = _TestMessageFilterAgent("filtered")
  961. filtered = MessageFilterAgent(
  962. name="filtered",
  963. wrapped_agent=inner_agent,
  964. filter=MessageFilterConfig(
  965. per_source=[
  966. PerSourceFilter(source="user", position="last", count=1),
  967. ]
  968. ),
  969. )
  970. graph = DiGraph(
  971. nodes={
  972. "filtered": DiGraphNode(name="filtered", edges=[]),
  973. }
  974. )
  975. team = GraphFlow(
  976. participants=[filtered],
  977. graph=graph,
  978. runtime=runtime,
  979. termination_condition=MaxMessageTermination(3),
  980. )
  981. result = await team.run(task="only last user message matters")
  982. assert result.stop_reason is not None
  983. assert any(msg.source == "filtered" for msg in result.messages)
  984. assert any(msg.content == "ACK" for msg in result.messages if msg.source == "filtered") # type: ignore[attr-defined,union-attr]
  985. @pytest.mark.asyncio
  986. async def test_message_filter_agent_loop_graph_visibility(runtime: AgentRuntime | None) -> None:
  987. agent_a_inner = _TestMessageFilterAgent("A")
  988. agent_a = MessageFilterAgent(
  989. name="A",
  990. wrapped_agent=agent_a_inner,
  991. filter=MessageFilterConfig(
  992. per_source=[
  993. PerSourceFilter(source="user", position="first", count=1),
  994. PerSourceFilter(source="B", position="last", count=1),
  995. ]
  996. ),
  997. )
  998. from autogen_agentchat.agents import AssistantAgent
  999. from autogen_ext.models.replay import ReplayChatCompletionClient
  1000. model_client = ReplayChatCompletionClient(["loop", "loop", "exit"])
  1001. agent_b_inner = AssistantAgent("B", model_client=model_client)
  1002. agent_b = MessageFilterAgent(
  1003. name="B",
  1004. wrapped_agent=agent_b_inner,
  1005. filter=MessageFilterConfig(
  1006. per_source=[
  1007. PerSourceFilter(source="user", position="first", count=1),
  1008. PerSourceFilter(source="A", position="last", count=1),
  1009. PerSourceFilter(source="B", position="last", count=10),
  1010. ]
  1011. ),
  1012. )
  1013. agent_c_inner = _TestMessageFilterAgent("C")
  1014. agent_c = MessageFilterAgent(
  1015. name="C",
  1016. wrapped_agent=agent_c_inner,
  1017. filter=MessageFilterConfig(
  1018. per_source=[
  1019. PerSourceFilter(source="user", position="first", count=1),
  1020. PerSourceFilter(source="B", position="last", count=1),
  1021. ]
  1022. ),
  1023. )
  1024. graph = DiGraph(
  1025. nodes={
  1026. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  1027. "B": DiGraphNode(
  1028. name="B",
  1029. edges=[
  1030. DiGraphEdge(target="C", condition="exit"),
  1031. DiGraphEdge(target="A", condition="loop"),
  1032. ],
  1033. ),
  1034. "C": DiGraphNode(name="C", edges=[]),
  1035. },
  1036. default_start_node="A",
  1037. )
  1038. team = GraphFlow(
  1039. participants=[agent_a, agent_b, agent_c],
  1040. graph=graph,
  1041. runtime=runtime,
  1042. termination_condition=MaxMessageTermination(20),
  1043. )
  1044. result = await team.run(task="Start")
  1045. assert result.stop_reason is not None
  1046. # Check A received: 1 user + 2 from B
  1047. assert [m.source for m in agent_a_inner.received_messages].count("user") == 1
  1048. assert [m.source for m in agent_a_inner.received_messages].count("B") == 2
  1049. # Check C received: 1 user + 1 from B
  1050. assert [m.source for m in agent_c_inner.received_messages].count("user") == 1
  1051. assert [m.source for m in agent_c_inner.received_messages].count("B") == 1
  1052. # Check B received: 1 user + multiple from A + own messages
  1053. model_msgs = await agent_b_inner.model_context.get_messages()
  1054. sources = [m.source for m in model_msgs] # type: ignore[union-attr]
  1055. assert sources.count("user") == 1 # pyright: ignore[reportUnknownMemberType]
  1056. assert sources.count("A") >= 3 # pyright: ignore[reportUnknownMemberType]
  1057. assert sources.count("B") >= 2 # pyright: ignore[reportUnknownMemberType]
  1058. # Test Graph Builder
  1059. def test_add_node() -> None:
  1060. client = ReplayChatCompletionClient(["response"])
  1061. agent = AssistantAgent("A", model_client=client)
  1062. builder = DiGraphBuilder()
  1063. builder.add_node(agent)
  1064. assert "A" in builder.nodes
  1065. assert "A" in builder.agents
  1066. assert builder.nodes["A"].activation == "all"
  1067. def test_add_edge() -> None:
  1068. client = ReplayChatCompletionClient(["1", "2"])
  1069. a = AssistantAgent("A", model_client=client)
  1070. b = AssistantAgent("B", model_client=client)
  1071. builder = DiGraphBuilder()
  1072. builder.add_node(a).add_node(b)
  1073. builder.add_edge(a, b)
  1074. assert builder.nodes["A"].edges[0].target == "B"
  1075. assert builder.nodes["A"].edges[0].condition is None
  1076. def test_add_conditional_edges() -> None:
  1077. client = ReplayChatCompletionClient(["1", "2"])
  1078. a = AssistantAgent("A", model_client=client)
  1079. b = AssistantAgent("B", model_client=client)
  1080. c = AssistantAgent("C", model_client=client)
  1081. builder = DiGraphBuilder()
  1082. builder.add_node(a).add_node(b).add_node(c)
  1083. builder.add_conditional_edges(a, {"yes": b, "no": c})
  1084. edges = builder.nodes["A"].edges
  1085. assert len(edges) == 2
  1086. # Extract the condition strings to compare them
  1087. conditions = [e.condition for e in edges]
  1088. assert "yes" in conditions
  1089. assert "no" in conditions
  1090. # Match edge targets with conditions
  1091. yes_edge = next(e for e in edges if e.condition == "yes")
  1092. no_edge = next(e for e in edges if e.condition == "no")
  1093. assert yes_edge.target == "B"
  1094. assert no_edge.target == "C"
  1095. def test_set_entry_point() -> None:
  1096. client = ReplayChatCompletionClient(["ok"])
  1097. a = AssistantAgent("A", model_client=client)
  1098. builder = DiGraphBuilder().add_node(a).set_entry_point(a)
  1099. graph = builder.build()
  1100. assert graph.default_start_node == "A"
  1101. def test_build_graph_validation() -> None:
  1102. client = ReplayChatCompletionClient(["1", "2", "3"])
  1103. a = AssistantAgent("A", model_client=client)
  1104. b = AssistantAgent("B", model_client=client)
  1105. c = AssistantAgent("C", model_client=client)
  1106. builder = DiGraphBuilder()
  1107. builder.add_node(a).add_node(b).add_node(c)
  1108. builder.add_edge("A", "B").add_edge("B", "C")
  1109. builder.set_entry_point("A")
  1110. graph = builder.build()
  1111. assert isinstance(graph, DiGraph)
  1112. assert set(graph.nodes.keys()) == {"A", "B", "C"}
  1113. assert graph.get_start_nodes() == {"A"}
  1114. assert graph.get_leaf_nodes() == {"C"}
  1115. def test_build_fan_out() -> None:
  1116. client = ReplayChatCompletionClient(["hi"] * 3)
  1117. a = AssistantAgent("A", model_client=client)
  1118. b = AssistantAgent("B", model_client=client)
  1119. c = AssistantAgent("C", model_client=client)
  1120. builder = DiGraphBuilder()
  1121. builder.add_node(a).add_node(b).add_node(c)
  1122. builder.add_edge(a, b).add_edge(a, c)
  1123. builder.set_entry_point(a)
  1124. graph = builder.build()
  1125. assert graph.get_start_nodes() == {"A"}
  1126. assert graph.get_leaf_nodes() == {"B", "C"}
  1127. def test_build_parallel_join() -> None:
  1128. client = ReplayChatCompletionClient(["go"] * 3)
  1129. a = AssistantAgent("A", model_client=client)
  1130. b = AssistantAgent("B", model_client=client)
  1131. c = AssistantAgent("C", model_client=client)
  1132. builder = DiGraphBuilder()
  1133. builder.add_node(a).add_node(b).add_node(c, activation="all")
  1134. builder.add_edge(a, c).add_edge(b, c)
  1135. builder.set_entry_point(a)
  1136. builder.add_edge(b, c)
  1137. builder.nodes["B"] = DiGraphNode(name="B", edges=[DiGraphEdge(target="C")])
  1138. graph = builder.build()
  1139. assert graph.nodes["C"].activation == "all"
  1140. assert graph.get_leaf_nodes() == {"C"}
  1141. def test_build_conditional_loop() -> None:
  1142. client = ReplayChatCompletionClient(["loop", "loop", "exit"])
  1143. a = AssistantAgent("A", model_client=client)
  1144. b = AssistantAgent("B", model_client=client)
  1145. c = AssistantAgent("C", model_client=client)
  1146. builder = DiGraphBuilder()
  1147. builder.add_node(a).add_node(b).add_node(c)
  1148. builder.add_edge(a, b)
  1149. builder.add_conditional_edges(b, {"loop": a, "exit": c})
  1150. builder.set_entry_point(a)
  1151. graph = builder.build()
  1152. # Check that edges have the right conditions and targets
  1153. edges = graph.nodes["B"].edges
  1154. assert len(edges) == 2
  1155. # Find edges by their conditions
  1156. loop_edge = next(e for e in edges if e.condition == "loop")
  1157. exit_edge = next(e for e in edges if e.condition == "exit")
  1158. assert loop_edge.target == "A"
  1159. assert exit_edge.target == "C"
  1160. assert graph.has_cycles_with_exit()
  1161. @pytest.mark.asyncio
  1162. async def test_graph_builder_sequential_execution(runtime: AgentRuntime | None) -> None:
  1163. a = _EchoAgent("A", description="Echo A")
  1164. b = _EchoAgent("B", description="Echo B")
  1165. c = _EchoAgent("C", description="Echo C")
  1166. builder = DiGraphBuilder()
  1167. builder.add_node(a).add_node(b).add_node(c)
  1168. builder.add_edge(a, b).add_edge(b, c)
  1169. team = GraphFlow(
  1170. participants=builder.get_participants(),
  1171. graph=builder.build(),
  1172. runtime=runtime,
  1173. termination_condition=MaxMessageTermination(5),
  1174. )
  1175. result = await team.run(task="Start")
  1176. assert [m.source for m in result.messages[1:]] == ["A", "B", "C"]
  1177. assert result.stop_reason is not None
  1178. @pytest.mark.asyncio
  1179. async def test_graph_builder_fan_out(runtime: AgentRuntime | None) -> None:
  1180. a = _EchoAgent("A", description="Echo A")
  1181. b = _EchoAgent("B", description="Echo B")
  1182. c = _EchoAgent("C", description="Echo C")
  1183. builder = DiGraphBuilder()
  1184. builder.add_node(a).add_node(b).add_node(c)
  1185. builder.add_edge(a, b).add_edge(a, c)
  1186. team = GraphFlow(
  1187. participants=builder.get_participants(),
  1188. graph=builder.build(),
  1189. runtime=runtime,
  1190. termination_condition=MaxMessageTermination(5),
  1191. )
  1192. result = await team.run(task="Start")
  1193. sources = [m.source for m in result.messages if isinstance(m, TextMessage)]
  1194. assert set(sources[1:]) == {"A", "B", "C"}
  1195. assert result.stop_reason is not None
  1196. @pytest.mark.asyncio
  1197. async def test_graph_builder_conditional_execution(runtime: AgentRuntime | None) -> None:
  1198. a = _EchoAgent("A", description="Echo A")
  1199. b = _EchoAgent("B", description="Echo B")
  1200. c = _EchoAgent("C", description="Echo C")
  1201. builder = DiGraphBuilder()
  1202. builder.add_node(a).add_node(b).add_node(c)
  1203. builder.add_conditional_edges(a, {"yes": b, "no": c})
  1204. team = GraphFlow(
  1205. participants=builder.get_participants(),
  1206. graph=builder.build(),
  1207. runtime=runtime,
  1208. termination_condition=MaxMessageTermination(5),
  1209. )
  1210. # Input "no" should trigger the edge to C
  1211. result = await team.run(task="no")
  1212. sources = [m.source for m in result.messages]
  1213. assert "C" in sources
  1214. assert result.stop_reason is not None
  1215. @pytest.mark.asyncio
  1216. async def test_digraph_group_chat_callable_condition(runtime: AgentRuntime | None) -> None:
  1217. """Test that string conditions work correctly in edge transitions."""
  1218. agent_a = _EchoAgent("A", description="Echo agent A")
  1219. agent_b = _EchoAgent("B", description="Echo agent B")
  1220. agent_c = _EchoAgent("C", description="Echo agent C")
  1221. graph = DiGraph(
  1222. nodes={
  1223. "A": DiGraphNode(
  1224. name="A",
  1225. edges=[
  1226. # Will go to B if "long" is in message
  1227. DiGraphEdge(target="B", condition="long"),
  1228. # Will go to C if "short" is in message
  1229. DiGraphEdge(target="C", condition="short"),
  1230. ],
  1231. ),
  1232. "B": DiGraphNode(name="B", edges=[]),
  1233. "C": DiGraphNode(name="C", edges=[]),
  1234. }
  1235. )
  1236. team = GraphFlow(
  1237. participants=[agent_a, agent_b, agent_c],
  1238. graph=graph,
  1239. runtime=runtime,
  1240. termination_condition=MaxMessageTermination(5),
  1241. )
  1242. # Test with a message containing "long" - should go to B
  1243. result = await team.run(task="This is a long message")
  1244. assert result.messages[2].source == "B"
  1245. # Reset for next test
  1246. await team.reset()
  1247. # Test with a message containing "short" - should go to C
  1248. result = await team.run(task="This is a short message")
  1249. assert result.messages[2].source == "C"
  1250. @pytest.mark.asyncio
  1251. async def test_graph_flow_serialize_deserialize() -> None:
  1252. client_a = ReplayChatCompletionClient(list(map(str, range(10))))
  1253. client_b = ReplayChatCompletionClient(list(map(str, range(10))))
  1254. a = AssistantAgent("A", model_client=client_a)
  1255. b = AssistantAgent("B", model_client=client_b)
  1256. builder = DiGraphBuilder()
  1257. builder.add_node(a).add_node(b)
  1258. builder.add_edge(a, b)
  1259. builder.set_entry_point(a)
  1260. team = GraphFlow(
  1261. participants=builder.get_participants(),
  1262. graph=builder.build(),
  1263. runtime=None,
  1264. )
  1265. serialized = team.dump_component()
  1266. deserialized_team = GraphFlow.load_component(serialized)
  1267. serialized_deserialized = deserialized_team.dump_component()
  1268. results = await team.run(task="Start")
  1269. de_results = await deserialized_team.run(task="Start")
  1270. assert serialized == serialized_deserialized
  1271. assert compare_task_results(results, de_results)
  1272. assert results.stop_reason is not None
  1273. assert results.stop_reason == de_results.stop_reason
  1274. assert compare_message_lists(results.messages, de_results.messages)
  1275. assert isinstance(results.messages[0], TextMessage)
  1276. assert results.messages[0].source == "user"
  1277. assert results.messages[0].content == "Start"
  1278. assert isinstance(results.messages[1], TextMessage)
  1279. assert results.messages[1].source == "A"
  1280. assert results.messages[1].content == "0"
  1281. assert isinstance(results.messages[2], TextMessage)
  1282. assert results.messages[2].source == "B"
  1283. assert results.messages[2].content == "0"
  1284. # No stop agent message should appear in the conversation
  1285. assert all(not isinstance(m, StopMessage) for m in results.messages)
  1286. assert results.stop_reason is not None
  1287. @pytest.mark.asyncio
  1288. async def test_graph_flow_stateful_pause_and_resume_with_termination() -> None:
  1289. client_a = ReplayChatCompletionClient(["A1", "A2"])
  1290. client_b = ReplayChatCompletionClient(["B1"])
  1291. a = AssistantAgent("A", model_client=client_a)
  1292. b = AssistantAgent("B", model_client=client_b)
  1293. builder = DiGraphBuilder()
  1294. builder.add_node(a).add_node(b)
  1295. builder.add_edge(a, b)
  1296. builder.set_entry_point(a)
  1297. team = GraphFlow(
  1298. participants=builder.get_participants(),
  1299. graph=builder.build(),
  1300. runtime=None,
  1301. termination_condition=SourceMatchTermination(sources=["A"]),
  1302. )
  1303. result = await team.run(task="Start")
  1304. assert len(result.messages) == 2
  1305. assert result.messages[0].source == "user"
  1306. assert result.messages[1].source == "A"
  1307. assert result.stop_reason is not None and result.stop_reason == "'A' answered"
  1308. # Export state.
  1309. state = await team.save_state()
  1310. # Load state into a new team.
  1311. new_team = GraphFlow(
  1312. participants=builder.get_participants(),
  1313. graph=builder.build(),
  1314. runtime=None,
  1315. )
  1316. await new_team.load_state(state)
  1317. # Resume.
  1318. result = await new_team.run()
  1319. assert len(result.messages) == 1
  1320. assert result.messages[0].source == "B"
  1321. @pytest.mark.asyncio
  1322. async def test_builder_with_lambda_condition(runtime: AgentRuntime | None) -> None:
  1323. """Test that DiGraphBuilder supports string conditions."""
  1324. agent_a = _EchoAgent("A", description="Echo agent A")
  1325. agent_b = _EchoAgent("B", description="Echo agent B")
  1326. agent_c = _EchoAgent("C", description="Echo agent C")
  1327. builder = DiGraphBuilder()
  1328. builder.add_node(agent_a).add_node(agent_b).add_node(agent_c)
  1329. # Using callable conditions
  1330. builder.add_edge(agent_a, agent_b, lambda msg: "even" in msg.to_model_text())
  1331. builder.add_edge(agent_a, agent_c, lambda msg: "odd" in msg.to_model_text())
  1332. team = GraphFlow(
  1333. participants=builder.get_participants(),
  1334. graph=builder.build(),
  1335. runtime=runtime,
  1336. termination_condition=MaxMessageTermination(5),
  1337. )
  1338. # Test with "even" in message - should go to B
  1339. result = await team.run(task="even length")
  1340. assert result.messages[2].source == "B"
  1341. # Reset for next test
  1342. await team.reset()
  1343. # Test with "odd" in message - should go to C
  1344. result = await team.run(task="odd message")
  1345. assert result.messages[2].source == "C"
  1346. @pytest.mark.asyncio
  1347. async def test_digraph_group_chat_multiple_task_execution(runtime: AgentRuntime | None) -> None:
  1348. """Test that GraphFlow can run multiple tasks sequentially after resetting execution state."""
  1349. # Create agents A → B → C
  1350. agent_a = _EchoAgent("A", description="Echo agent A")
  1351. agent_b = _EchoAgent("B", description="Echo agent B")
  1352. agent_c = _EchoAgent("C", description="Echo agent C")
  1353. # Define graph A → B → C
  1354. graph = DiGraph(
  1355. nodes={
  1356. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  1357. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  1358. "C": DiGraphNode(name="C", edges=[]),
  1359. }
  1360. )
  1361. # Create team using Graph
  1362. team = GraphFlow(
  1363. participants=[agent_a, agent_b, agent_c],
  1364. graph=graph,
  1365. runtime=runtime,
  1366. termination_condition=MaxMessageTermination(5),
  1367. )
  1368. # Run the first task
  1369. result1: TaskResult = await team.run(task="First task")
  1370. assert len(result1.messages) == 4
  1371. assert isinstance(result1.messages[0], TextMessage)
  1372. assert result1.messages[0].source == "user"
  1373. assert result1.messages[0].content == "First task"
  1374. assert result1.messages[1].source == "A"
  1375. assert result1.messages[2].source == "B"
  1376. assert result1.messages[3].source == "C"
  1377. assert result1.stop_reason is not None
  1378. # Run the second task - should work without explicit reset
  1379. result2: TaskResult = await team.run(task="Second task")
  1380. assert len(result2.messages) == 4
  1381. assert isinstance(result2.messages[0], TextMessage)
  1382. assert result2.messages[0].source == "user"
  1383. assert result2.messages[0].content == "Second task"
  1384. assert result2.messages[1].source == "A"
  1385. assert result2.messages[2].source == "B"
  1386. assert result2.messages[3].source == "C"
  1387. assert result2.stop_reason is not None
  1388. # Verify agents were properly reset and executed again
  1389. assert agent_a.total_messages == 2 # Once for each task
  1390. assert agent_b.total_messages == 2 # Once for each task
  1391. assert agent_c.total_messages == 2 # Once for each task
  1392. @pytest.mark.asyncio
  1393. async def test_digraph_group_chat_resume_with_termination_condition(runtime: AgentRuntime | None) -> None:
  1394. """Test that GraphFlow can be resumed with the same execution state when a termination condition is reached."""
  1395. # Create agents A → B → C
  1396. agent_a = _EchoAgent("A", description="Echo agent A")
  1397. agent_b = _EchoAgent("B", description="Echo agent B")
  1398. agent_c = _EchoAgent("C", description="Echo agent C")
  1399. # Define graph A → B → C
  1400. graph = DiGraph(
  1401. nodes={
  1402. "A": DiGraphNode(name="A", edges=[DiGraphEdge(target="B")]),
  1403. "B": DiGraphNode(name="B", edges=[DiGraphEdge(target="C")]),
  1404. "C": DiGraphNode(name="C", edges=[]),
  1405. }
  1406. )
  1407. # Create team with MaxMessageTermination that will stop before completion
  1408. team = GraphFlow(
  1409. participants=[agent_a, agent_b, agent_c],
  1410. graph=graph,
  1411. runtime=runtime,
  1412. termination_condition=MaxMessageTermination(3), # Stop after user + A + B
  1413. )
  1414. # Run the graph flow until termination condition is reached
  1415. result1: TaskResult = await team.run(task="Start execution")
  1416. # Should have stopped at termination condition (user + A + B messages)
  1417. assert len(result1.messages) == 3
  1418. assert result1.messages[0].source == "user"
  1419. assert result1.messages[1].source == "A"
  1420. assert result1.messages[2].source == "B"
  1421. assert result1.stop_reason is not None
  1422. # Verify A and B ran, but C did not
  1423. assert agent_a.total_messages == 1
  1424. assert agent_b.total_messages == 1
  1425. assert agent_c.total_messages == 0
  1426. # Resume the graph flow with no task to continue where it left off
  1427. result2: TaskResult = await team.run()
  1428. # Should continue and execute C, then complete without stop agent message
  1429. assert len(result2.messages) == 1
  1430. assert result2.messages[0].source == "C"
  1431. assert result2.stop_reason is not None
  1432. # Verify C now ran and the execution state was preserved
  1433. assert agent_a.total_messages == 1 # Still only ran once
  1434. assert agent_b.total_messages == 1 # Still only ran once
  1435. assert agent_c.total_messages == 1 # Now ran once