Learnixo

AutoGen Essentials · Lesson 9 of 11

GroupChatManager: Selecting the Next Speaker

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 2

The 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.

Python
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.

Python
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.

Python
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

Python
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.

Python
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:

Python
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_agent

Sample log output:

[ROUTER] After patient → selecting triage (turn 1)
[ROUTER] After triage → selecting cardiologist (turn 2)

Common Routing Patterns

Pattern 1: Topic-Based Routing

Python
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:

Python
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

Python
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:

Python
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

  • GroupChatManager controls who speaks after every message
  • Speaker selection methods: auto (LLM), round_robin, random, or custom function
  • Custom functions receive last_speaker and the groupchat object — 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 auto mode, override the manager's system_message with explicit routing rules

Next: we look at termination conditions — how to make sure conversations end reliably and cost-efficiently.