GroupChatManager: Selecting the Next Speaker
How GroupChatManager orchestrates multi-agent conversations, speaker selection strategies including custom routing functions, and a domain-routing medical specialist example.
The Manager's Role
In a GroupChat, individual agents do not choose when to speak ā the GroupChatManager decides. After every message, the manager looks at the conversation history and selects the next speaker.
This separation of concerns is powerful: you can change how routing works without modifying any agent's logic. The agents focus on their domain; the manager focuses on orchestration.
What GroupChatManager Does
1. Receives a message from an agent (or the initial message from the executor)
2. Appends it to group_chat.messages
3. Checks: is_termination_msg ā end if True
4. Checks: has max_round been reached ā end if True
5. Selects next speaker using speaker_selection_method
6. Passes the full conversation history to the selected agent
7. Receives that agent's reply
8. Loop back to step 2The manager is itself backed by an LLM (in auto mode). When it needs to select the next speaker, it sends the conversation history plus a system prompt asking "who should speak next?" to its configured LLM.
Speaker Selection Strategies
Strategy 1: auto ā LLM Picks the Next Speaker
The manager sends the conversation history to its LLM with a prompt like:
You are in a role-play game. The following roles are available:
- researcher: Clarifies requirements
- coder: Writes implementation
- executor: Runs code
- reviewer: Reviews output
Read the following conversation history and select the most appropriate next role.
Only return the name of the role.The LLM replies with a single name, and that agent speaks next.
import autogen
import os
llm_config = {
"config_list": [{"model": "gpt-4o-mini", "api_key": os.environ["OPENAI_API_KEY"]}],
"temperature": 0,
}
group_chat = autogen.GroupChat(
agents=[executor, researcher, coder, reviewer],
messages=[],
max_round=20,
speaker_selection_method="auto", # LLM decides
)
manager = autogen.GroupChatManager(
groupchat=group_chat,
llm_config=llm_config,
)Pros: Context-aware, handles non-linear workflows
Cons: Costs extra LLM tokens per turn, occasional selection errors
Strategy 2: round_robin ā Agents Take Fixed Turns
Agents speak in the order they are listed in agents, cycling indefinitely until termination.
group_chat = autogen.GroupChat(
agents=[researcher, coder, executor, reviewer],
messages=[],
max_round=12,
speaker_selection_method="round_robin",
)
# Turn order: researcher ā coder ā executor ā reviewer
# ā researcher ā coder ā executor ā reviewer ā ...Pros: Deterministic, predictable, zero extra LLM calls
Cons: Rigid ā cannot adapt to what happened in the conversation
Strategy 3: Custom Function ā You Write the Router
This is the most powerful option. You provide a Python function that receives the GroupChat object and returns the next agent.
def my_speaker_selector(last_speaker: autogen.Agent, groupchat: autogen.GroupChat) -> autogen.Agent:
"""
Custom speaker selection function.
Args:
last_speaker: The agent that just spoke.
groupchat: The GroupChat object (access .agents and .messages).
Returns:
The next agent to speak.
"""
# ... your logic here ...
return next_agent
group_chat = autogen.GroupChat(
agents=[executor, researcher, coder, reviewer],
messages=[],
max_round=20,
speaker_selection_method=my_speaker_selector, # pass the function
)Writing a Custom Speaker Selection Function
Example 1: Simple Sequential Flow
def sequential_selector(
last_speaker: autogen.Agent,
groupchat: autogen.GroupChat,
) -> autogen.Agent:
"""
Enforce a fixed workflow: executor ā researcher ā coder ā executor ā reviewer
"""
agents_by_name = {a.name: a for a in groupchat.agents}
# Define the fixed sequence
sequence = {
"executor": "researcher",
"researcher": "coder",
"coder": "executor",
"executor_after_code": "reviewer", # special case handled below
}
last_name = last_speaker.name
messages = groupchat.messages
# Special case: if executor just ran code, go to reviewer instead of researcher
if last_name == "executor":
last_content = messages[-1].get("content", "")
if "exitcode:" in last_content: # execution happened
return agents_by_name["reviewer"]
else:
return agents_by_name["researcher"]
next_name = sequence.get(last_name, "researcher")
return agents_by_name.get(next_name, groupchat.agents[0])Example 2: Content-Based Routing (Medical Specialist)
This is a realistic example: a triage agent routes questions to the appropriate specialist based on message content.
import autogen
import os
llm_config = {
"config_list": [{"model": "gpt-4o-mini", "api_key": os.environ["OPENAI_API_KEY"]}],
"temperature": 0,
}
# āāā Define Specialist Agents āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
triage = autogen.AssistantAgent(
name="triage",
llm_config=llm_config,
system_message="""You are a medical triage coordinator.
When you receive a patient question:
1. Identify the medical domain (cardiology, neurology, general)
2. Briefly acknowledge the question
3. State which specialist should answer
Do NOT provide medical advice yourself.
Format: "Routing to [specialist]. [one sentence why]"
""",
)
cardiologist = autogen.AssistantAgent(
name="cardiologist",
llm_config=llm_config,
system_message="""You are a consultant cardiologist providing educational information.
Answer questions about heart conditions, blood pressure, and cardiovascular health.
Always remind users that this is educational content only and they should see a doctor.
End your response with TERMINATE.""",
)
neurologist = autogen.AssistantAgent(
name="neurologist",
llm_config=llm_config,
system_message="""You are a consultant neurologist providing educational information.
Answer questions about the nervous system, headaches, seizures, and brain health.
Always remind users that this is educational content only and they should see a doctor.
End your response with TERMINATE.""",
)
general_practitioner = autogen.AssistantAgent(
name="general_practitioner",
llm_config=llm_config,
system_message="""You are a general practitioner providing educational information.
Answer general health questions that don't require specialist knowledge.
Always remind users that this is educational content only and they should see a doctor.
End your response with TERMINATE.""",
)
patient_proxy = autogen.UserProxyAgent(
name="patient",
human_input_mode="NEVER",
max_consecutive_auto_reply=5,
is_termination_msg=lambda msg: "TERMINATE" in msg.get("content", ""),
code_execution_config=False,
)
# āāā Custom Speaker Selection Function āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
CARDIOLOGY_KEYWORDS = {
"heart", "cardiac", "chest pain", "arrhythmia", "blood pressure",
"hypertension", "palpitations", "tachycardia", "bradycardia",
"angina", "coronary", "ecg", "ekg", "aorta",
}
NEUROLOGY_KEYWORDS = {
"headache", "migraine", "seizure", "stroke", "neurological",
"brain", "nerve", "tremor", "numbness", "tingling", "dizziness",
"vertigo", "epilepsy", "memory", "alzheimer", "parkinson",
}
def medical_router(
last_speaker: autogen.Agent,
groupchat: autogen.GroupChat,
) -> autogen.Agent:
"""
Route medical questions to the appropriate specialist.
Flow:
1. patient asks question ā triage receives it
2. triage acknowledges ā route to appropriate specialist
3. specialist answers with TERMINATE ā conversation ends
"""
agents_by_name = {a.name: a for a in groupchat.agents}
messages = groupchat.messages
# Step 1: Patient just sent a message ā always go to triage first
if last_speaker.name == "patient":
return agents_by_name["triage"]
# Step 2: Triage just spoke ā route to specialist based on content analysis
if last_speaker.name == "triage":
# Look at the original patient question (message before triage's response)
patient_messages = [
msg for msg in messages
if msg.get("name") == "patient"
]
if not patient_messages:
return agents_by_name["general_practitioner"]
patient_question = patient_messages[-1]["content"].lower()
# Check for cardiology keywords
if any(kw in patient_question for kw in CARDIOLOGY_KEYWORDS):
print("[Router] Routing to cardiologist")
return agents_by_name["cardiologist"]
# Check for neurology keywords
if any(kw in patient_question for kw in NEUROLOGY_KEYWORDS):
print("[Router] Routing to neurologist")
return agents_by_name["neurologist"]
# Default to general practitioner
print("[Router] Routing to general practitioner")
return agents_by_name["general_practitioner"]
# If specialist just spoke with TERMINATE, the is_termination_msg will catch it
# As a fallback, return triage
return agents_by_name["triage"]
# āāā Set Up GroupChat with Custom Router āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
medical_group_chat = autogen.GroupChat(
agents=[patient_proxy, triage, cardiologist, neurologist, general_practitioner],
messages=[],
max_round=8,
speaker_selection_method=medical_router, # use our custom function
)
medical_manager = autogen.GroupChatManager(
groupchat=medical_group_chat,
llm_config=llm_config,
)
# āāā Test with Different Questions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
test_cases = [
"I've been experiencing palpitations and chest tightness for the past week.",
"I get severe migraines with visual aura about twice a month.",
"I have a persistent cough and mild fever for three days.",
]
for question in test_cases:
print(f"\n{'='*60}")
print(f"Patient: {question}")
print(f"{'='*60}")
# Reset for each question
medical_group_chat.messages.clear()
patient_proxy.initiate_chat(
medical_manager,
message=question,
clear_history=True,
)Inspecting the Router's Decisions
Add logging to understand what your custom router is doing:
def logged_medical_router(
last_speaker: autogen.Agent,
groupchat: autogen.GroupChat,
) -> autogen.Agent:
"""Medical router with decision logging."""
next_agent = medical_router(last_speaker, groupchat)
print(
f"[ROUTER] After {last_speaker.name} ā selecting {next_agent.name} "
f"(turn {len(groupchat.messages)})"
)
return next_agentSample log output:
[ROUTER] After patient ā selecting triage (turn 1)
[ROUTER] After triage ā selecting cardiologist (turn 2)Common Routing Patterns
Pattern 1: Topic-Based Routing
def topic_router(last_speaker, groupchat):
"""Route based on keywords in the last message."""
agents_by_name = {a.name: a for a in groupchat.agents}
messages = groupchat.messages
if not messages:
return agents_by_name.get("coordinator", groupchat.agents[0])
last_content = messages[-1].get("content", "").lower()
if "database" in last_content or "sql" in last_content:
return agents_by_name["db_expert"]
elif "security" in last_content or "auth" in last_content:
return agents_by_name["security_expert"]
elif "performance" in last_content or "optimization" in last_content:
return agents_by_name["performance_expert"]
else:
return agents_by_name["generalist"]Pattern 2: Handoff Protocol
Agents explicitly name who should speak next:
def handoff_router(last_speaker, groupchat):
"""Agents say 'Handoff to [name]' to select the next speaker."""
agents_by_name = {a.name: a for a in groupchat.agents}
messages = groupchat.messages
if messages:
last_content = messages[-1].get("content", "")
for name, agent in agents_by_name.items():
if f"handoff to {name.lower()}" in last_content.lower():
return agent
# Default to round robin if no explicit handoff
if last_speaker in groupchat.agents:
idx = groupchat.agents.index(last_speaker)
return groupchat.agents[(idx + 1) % len(groupchat.agents)]
return groupchat.agents[0]Pattern 3: State Machine
from enum import Enum
class WorkflowState(Enum):
RESEARCH = "research"
IMPLEMENT = "implement"
TEST = "test"
REVIEW = "review"
DONE = "done"
# Keep state outside the function (closure)
current_state = {"value": WorkflowState.RESEARCH}
def state_machine_router(last_speaker, groupchat):
"""Strict state machine for a software development workflow."""
agents_by_name = {a.name: a for a in groupchat.agents}
messages = groupchat.messages
state = current_state["value"]
last_content = messages[-1]["content"] if messages else ""
if state == WorkflowState.RESEARCH:
current_state["value"] = WorkflowState.IMPLEMENT
return agents_by_name["coder"]
elif state == WorkflowState.IMPLEMENT:
current_state["value"] = WorkflowState.TEST
return agents_by_name["executor"]
elif state == WorkflowState.TEST:
if "exitcode: 0" in last_content:
current_state["value"] = WorkflowState.REVIEW
return agents_by_name["reviewer"]
else:
# Tests failed ā go back to coder
current_state["value"] = WorkflowState.IMPLEMENT
return agents_by_name["coder"]
elif state == WorkflowState.REVIEW:
if "LGTM" in last_content or "TERMINATE" in last_content:
current_state["value"] = WorkflowState.DONE
return agents_by_name["executor"]
return groupchat.agents[0]Debugging Speaker Selection
When auto mode makes unexpected speaker selections, add a system message override:
manager = autogen.GroupChatManager(
groupchat=group_chat,
llm_config=llm_config,
system_message="""You are a conversation coordinator.
Select the next speaker following these strict rules:
1. After executor sends the initial task ā select researcher
2. After researcher speaks ā select coder
3. After coder writes code ā select executor (to run the code)
4. After executor runs code and reports success ā select reviewer
5. After reviewer approves ā end conversation
ONLY reply with the agent name. Nothing else.""",
)Summary
GroupChatManagercontrols who speaks after every message- Speaker selection methods:
auto(LLM),round_robin,random, or custom function - Custom functions receive
last_speakerand thegroupchatobject ā return the next agent - Content-based routing: inspect message keywords to route to domain specialists
- Handoff protocol: let agents explicitly name their successor
- State machine routing: enforce a strict workflow sequence with state tracking
- For debugging
automode, override the manager'ssystem_messagewith explicit routing rules
Next: we look at termination conditions ā how to make sure conversations end reliably and cost-efficiently.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.