Route customer support requests between specialist agents using OpenAI's experimental Swarm framework, with a manual topic guardrail in front of it.
What you'll build #
A small customer support system: a triage agent reads an incoming message, decides whether it's a billing or technical issue, and hands off to the right specialist agent. A lightweight classifier agent acts as a topic guardrail, blocking anything unrelated to support before the triage agent even sees it. You'll run all of this locally with OpenAI's Swarm framework and confirm the handoff actually happened by checking which agent produced the final reply.
Prerequisites #
- Python 3.10 or newer (Swarm relies on typing features that don't work on older versions)
- An OpenAI API key with active billing (platform.openai.com/api-keys)
- Basic comfort with Python functions and the OpenAI chat completions message format (
{"role": ..., "content": ...}
) pip
,git
, and a terminal
A note on the framework: Swarm is OpenAI's own experimental, educational multi-agent framework (openai/swarm
on GitHub). It's explicitly not meant for production, there's no built-in tracing dashboard, no persistent sessions, no guardrail primitives. It's a thin, readable layer over the standard openai
client that demonstrates one core idea well: an agent can "hand off" a conversation to another agent by returning that agent from a function call. That's exactly what we need here, and it's small enough that you can read the entire source in twenty minutes if you want to know what's happening under the hood.
Important: Swarm is not published on PyPI. There's an unrelated package called swarm
on PyPI (a distributed job queue tool), and pip install swarm
will install that instead and quietly break everything downstream. Install straight from GitHub.
1. Set up the project #
mkdir agent-handoffs && cd agent-handoffs
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install git+https://github.com/openai/swarm.git
Set your API key as an environment variable rather than hardcoding it:
export OPENAI_API_KEY="sk-..." # macOS/Linux
Swarm doesn't take the key directly, it builds an openai.OpenAI()
client internally, which reads OPENAI_API_KEY
from the environment automatically.
Create support_system.py
. Everything below goes in that one file, in order.
2. Build the specialist agents #
Start with two agents that each know how to do one job and nothing else. In Swarm, Agent
is a simple object with a name, a model, an instructions string, and a list of callable functions
the model can invoke as tools.
from swarm import Swarm, Agent
client = Swarm()
billing_agent = Agent(
name="Billing Agent",
instructions=(
"You help customers with billing issues: invoices, charges, and "
"subscription changes. Be concise and specific."
),
)
technical_agent = Agent(
name="Technical Support Agent",
instructions=(
"You help customers troubleshoot technical problems with the product. "
"Ask clarifying questions if you need more detail."
),
)
3. Build the topic guardrail #
Swarm has no built-in guardrail concept, so we build one the plain way: a cheap classifier agent that runs before triage and decides whether the message is worth routing at all. It replies with JSON, which we parse ourselves.
import json
topic_guardrail_agent = Agent(
name="Topic Guardrail",
model="gpt-4o-mini",
instructions=(
"Decide whether the user's message is a customer support question "
"about billing or technical issues. Jokes, homework, and general "
"chit-chat are not support related. Reply with ONLY raw JSON, no "
'markdown fences, in this exact shape: {"is_support_related": true, '
'"reasoning": "short reason"}'
),
)
def check_support_topic(user_message: str) -> bool:
response = client.run(
agent=topic_guardrail_agent,
messages=[{"role": "user", "content": user_message}],
)
raw = response.messages[-1]["content"].strip()
raw = raw.removeprefix("``` json").removeprefix("```").removesuffix("```").strip()
try:
parsed = json.loads(raw)
return bool(parsed.get("is_support_related", False))
except json.JSONDecodeError:
return False
gpt-4o-mini
is a reasonable default here: it's cheap and fast, and a yes/no classification doesn't need a frontier model. The .removeprefix()
/.removesuffix()
cleanup exists because small models occasionally wrap JSON in a markdown code fence even when told not to, don't skip it, it's the most common way this guardrail silently breaks.
4. Build the triage agent with handoffs #
In Swarm, a handoff is nothing special, it's just a Python function that returns an Agent
instance instead of a string. When the model calls that function, Swarm swaps the active agent and keeps running the conversation against it. Define the specialists first (already done above), then the transfer functions, then the triage agent that uses them.
def transfer_to_billing():
"""Call this when the customer's issue is about billing, invoices, or charges."""
return billing_agent
def transfer_to_technical():
"""Call this when the customer has a technical problem with the product."""
return technical_agent
triage_agent = Agent(
name="Triage Agent",
instructions=(
"Determine whether the user needs billing help or technical support, "
"then call the matching transfer function. Don't try to solve the "
"problem yourself."
),
functions=[transfer_to_billing, transfer_to_technical],
)
The docstring on each transfer function isn't decoration, Swarm sends it to the model as the function's description, same as any other tool. Vague docstrings are the number one reason triage picks the wrong specialist or doesn't hand off at all.
5. Run it #
def handle_message(user_message: str):
if not check_support_topic(user_message):
print("Blocked: this doesn't look like a support question.")
return
response = client.run(
agent=triage_agent,
messages=[{"role": "user", "content": user_message}],
)
print(f"Handled by: {response.agent.name}")
print(response.messages[-1]["content"])
if __name__ == "__main__":
handle_message("My last invoice charged me twice, can you fix it?")
Run it:
python support_system.py
Verify it works #
You should see something like:
Handled by: Billing Agent
I'm sorry about the duplicate charge. I can start a refund for the extra charge...
response.agent.name
confirms the handoff actually happened, triage routed to billing, not itself. Swap the call at the bottom to handle_message("my app crashes when I upload a file")
and confirm it routes to the Technical Support Agent instead. Then try handle_message("write me a poem about the ocean")
, it should print the guardrail block message and never touch the triage agent at all.
If you want to see it handle a few cases in one run, loop over a list of test messages instead of calling handle_message
once, it's cheap since the guardrail model is small.
Troubleshooting #
or import errors that don't match anything in this tutorial: you likely ranModuleNotFoundError: No module named 'swarm'
pip install swarm
, which grabs an unrelated PyPI package. Uninstall it (pip uninstall swarm
) and install from GitHub as shown above.:AuthenticationError: Incorrect API key provided
OPENAI_API_KEY
isn't set in the shell that's running the script. Confirm withecho $OPENAI_API_KEY
(macOS/Linux) before running, env vars set in one terminal tab don't carry to another.: the guardrail model wrapped its answer in a code fence or added a stray sentence before the JSON. Tighten the instructions further ("reply with nothing but the JSON object") or, if it keeps happening, printjson.JSONDecodeError
fromcheck_support_topic
raw
before parsing so you can see exactly what came back.Triage agent answers the question directly instead of transferring: this usually means the transfer function docstrings are too vague or the triage instructions don't clearly forbid answering directly. Make both more explicit, and double checkfunctions=[...]
on the triageAgent
actually lists both transfer functions.
Next steps #
Swarm is deliberately minimal, there's no persistent session across separate script runs, no built-in tracing, no output guardrails. If you outgrow it, the next stop is OpenAI's Assistants API (via the standard openai
package), which adds persistent threads, built-in file retrieval, and hosted tool execution at the cost of more moving parts to configure. For now, try adding a third specialist agent (say, a "refunds" agent) and a third transfer function, or add a second guardrail that checks the specialist's reply before it's printed, useful if you're worried about a specialist agent giving out information it shouldn't. The openai/swarm
repo's examples/
directory has a few more handoff patterns worth reading, including one with shared context_variables
passed between agents.
Mariana Souza· Senior Editor
Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.
Discussion 0 #
No comments yet
Be the first to weigh in.