skip to content

In AutoGen's SelectorGroupChat, how is the next speaker chosen and how do you override it?

level: seniorimportance: should knowfreq 56%

answer

  1. a model call decides each turn
  2. the prompt template has slots
  3. a function can pre-empt the model
  4. returning None defers to the model
  5. the previous speaker is excluded by default

basics

~20 s

SelectorGroupChat calls its model_client every turn with a rendered selector_prompt and asks it to name the next participant. Supplying selector_func short-circuits that: return a participant name to force the choice, or None to fall back to the model.

solid answer

~50 s

`SelectorGroupChat(participants, model_client=..., selector_prompt=..., selector_func=..., allow_repeated_speaker=False)` puts a model in charge of turn-taking. Each turn it renders `selector_prompt` — whose default template exposes `{roles}`, `{participants}` and `{history}` — sends it to `model_client`, and expects a participant name back. That is one extra model call per turn on top of the agents' own calls, and the prompt carries conversation history, so selection cost grows with the run. `allow_repeated_speaker` defaults to False, which drops the previous speaker from the candidates. If the model returns an invalid or ambiguous name, the team retries up to `max_selector_attempts` (default 3) and then falls back to the previous speaker, or the first participant. `selector_func` is the escape hatch: a callable over the message thread that returns a participant name to decide deterministically, or `None` to defer to the model. `candidate_func` narrows the set the model may choose from.

code

python · 21 lines
python
from typing import Sequence

from autogen_agentchat.messages import BaseAgentEvent, BaseChatMessage
from autogen_agentchat.teams import SelectorGroupChat


def selector_func(messages: Sequence[BaseAgentEvent | BaseChatMessage]) -> str | None:
    if not messages:
        return "planner"
    if messages[-1].source == "planner":
        return "researcher"
    return None


team = SelectorGroupChat(
    [planner, researcher, writer],
    model_client=model_client,
    selector_func=selector_func,
    allow_repeated_speaker=True,
    max_selector_attempts=3,
)

go deeper

for a junior

Know that this team asks a model which agent should speak next, and that you configure it with a model_client plus each participant's description.

for a middle

Explain the per-turn selector call, the placeholders in selector_prompt, what allow_repeated_speaker does, and how selector_func returning a name or None switches between code and model selection.

for a senior

Show the production trade: an extra model call and non-determinism per turn, deterministic hops pushed into selector_func, phase gating via candidate_func, and monitoring the actual speaker sequence for silent degradation.

for a principal

Own the choice between model-driven selection, positional order and handoff-based routing for a workload, and be explicit about what each buys in cost, reproducibility and blast radius when routing goes wrong.

## The selection loop `SelectorGroupChat` is the team type where routing is a model decision. On every turn the group-chat manager builds a selector prompt, calls the configured `model_client`, parses a participant name out of the reply, and gives that participant the turn. Everything else — broadcast context, termination conditions, `max_turns`, `TaskResult` — behaves as in other AgentChat teams. The default `selector_prompt` is a template with placeholders the team fills in: `{roles}` (each participant's name and description), `{participants}` (the legal names to choose from) and `{history}` (the conversation so far). Overriding `selector_prompt` is how you inject routing policy in natural language — "prefer the planner when the task is not yet decomposed", "never pick the reviewer before a draft exists". A detail that surprises people: the quality of selection depends heavily on each agent's `description`, not its `system_message`. The description is what the selector sees in `{roles}`; the system message is private to the agent. Vague descriptions produce erratic routing, and the fix is a one-line, decision-shaped description per agent. ## What selection costs Selection is not free. Compared with a positional team, you pay: - one extra model call per turn, with its own latency on the critical path; - a prompt containing conversation history, so selection cost rises as the run grows; - non-determinism — the same task can route differently on two runs, which makes reproducing a bug harder. Those costs are the reason `selector_func` exists. Anything you can decide with code, decide with code. ## selector_func and candidate_func `selector_func` receives the message sequence so far and returns either a participant name or `None`: ``` def selector_func(messages): if messages[-1].source == "planner": return "researcher" # deterministic hop, no model call return None # let the model decide ``` Returning a name skips the selector model call entirely for that turn — cheaper, faster, reproducible. Returning `None` restores model selection, so you can encode the handful of rules you are sure about and leave genuinely ambiguous turns to the model. This hybrid is the pattern to reach for in production: hard-code the deterministic spine, delegate only the branches that need judgment. `candidate_func` is the softer knob: rather than picking the speaker, it restricts the candidate list the model may choose from on a given turn — useful for phase gating, where only certain agents are legal in the current stage. ## allow_repeated_speaker and retries `allow_repeated_speaker` defaults to `False`, meaning the previous speaker is excluded from candidates for the next turn. That prevents the common failure where one talkative agent monopolises the conversation, but it also forces a switch even when the same agent genuinely should continue — for a research agent that needs several consecutive tool-calling turns, set it to `True`. When the model's reply does not resolve to exactly one participant name, the team retries selection up to `max_selector_attempts` (default 3). If all attempts fail it falls back to the previous speaker when there is one, otherwise the first participant. This makes selection robust, but it also means a poorly specified roster can degrade into near-round-robin behaviour while still paying for a selector call every turn. Watching who actually speaks, rather than trusting that the selector works, is a real operational task. ## When to pick this team SelectorGroupChat earns its cost when the next speaker genuinely depends on content that you cannot enumerate: open-ended research, triage across many specialists, work whose stages are not known in advance. When the flow is a fixed pipeline, a positional team is cheaper and reproducible; when the deciding knowledge is local to the current speaker, a handoff-based team removes the central call altogether. The strongest answer in an interview names the cost of the extra call and says which parts of the flow you would move into `selector_func`.

  • What happens when selector_func returns None?
    The team falls back to model-based selection for that turn: it renders the selector_prompt with roles, participants and history, calls the model client and parses a participant name. That is what makes the hybrid pattern work — encode the deterministic hops in the function, return None everywhere else and pay for a model call only on the genuinely ambiguous turns.
  • Why does an agent's description matter more than its system_message for selection quality?
    The selector prompt exposes participants' descriptions in its roles slot; system messages are private to each agent and never reach the selector. So routing quality is bounded by how decision-shaped your descriptions are. Write each description as a statement of when this agent should be chosen, not as a general biography, and erratic routing usually improves without touching the prompt template.
  • When would you set allow_repeated_speaker=True?
    When an agent legitimately needs consecutive turns — for example a research agent making several tool calls before it can report, or a coder iterating on an error. The default False excludes the previous speaker to stop one agent monopolising the conversation, but on those workloads it forces a pointless hop to another agent that has nothing to add and still costs a model call.
  • How do you make a SelectorGroupChat run reproducible enough to debug?
    Move the deterministic parts of the flow into selector_func so those turns no longer depend on a model, narrow the legal set per phase with candidate_func, and log the chosen speaker per turn alongside the message thread. What remains non-deterministic is then a small, identified surface rather than the entire control flow.

saying these in an interview costs you the question

  • Thinks selection is free and adds no model call
  • Believes selector_func returning None halts the run
  • Assumes the selector reads each agent's system_message
  • Thinks allow_repeated_speaker=False bans an agent for the whole run
  • Expects invalid model output to raise instead of retrying and falling back

context