By Sagar Shankaran, Founder of CallSphere
Build a complete customer support multi-agent system with four specialized agents — a router, FAQ handler, billing specialist, and escalation agent — using the OpenAI Agents SDK with shared context and graceful fallbacks.
Key takeaways
We are building a customer support system with four agents:
This is a practical, production-oriented architecture. Each agent has a clear responsibility boundary, its own tools, and explicit handoff rules.
All agents share a context object that tracks the customer, the conversation state, and escalation metadata:
flowchart TD
INPUT(["Task input"])
SUPER["Supervisor agent<br/>plans plus monitors"]
W1["Worker 1<br/>research"]
W2["Worker 2<br/>code"]
W3["Worker 3<br/>writing"]
CRITIC{"Output meets<br/>rubric?"}
REWORK["Rework or<br/>retry path"]
SHARED[("Shared scratchpad<br/>and memory")]
OUT(["Final result"])
INPUT --> SUPER
SUPER --> W1 --> CRITIC
SUPER --> W2 --> CRITIC
SUPER --> W3 --> CRITIC
W1 --> SHARED
W2 --> SHARED
W3 --> SHARED
SHARED --> SUPER
CRITIC -->|Pass| OUT
CRITIC -->|Fail| REWORK --> SUPER
style SUPER fill:#4f46e5,stroke:#4338ca,color:#fff
style CRITIC fill:#f59e0b,stroke:#d97706,color:#1f2937
style OUT fill:#059669,stroke:#047857,color:#fff
style SHARED fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class SupportContext:
# Customer info (populated by router)
customer_id: str = ""
customer_name: str = ""
customer_email: str = ""
subscription_plan: str = ""
# Interaction tracking
issue_category: str = ""
resolution_status: str = "open" # open, resolved, escalated
interaction_log: list[str] = field(default_factory=list)
# Escalation data
escalation_reason: str = ""
escalation_priority: str = "normal" # low, normal, high, urgent
def log(self, agent_name: str, action: str):
timestamp = datetime.now().strftime("%H:%M:%S")
self.interaction_log.append(f"[{timestamp}] {agent_name}: {action}")
The FAQ agent searches a knowledge base and returns answers. It is the simplest specialist:
from agents import Agent, RunContextWrapper, function_tool
FAQ_DATABASE = {
"password_reset": "To reset your password, go to Settings > Security > Reset Password. You will receive an email with a reset link.",
"supported_browsers": "We support Chrome 90+, Firefox 88+, Safari 15+, and Edge 90+.",
"data_export": "Go to Settings > Data > Export. Select your date range and format (CSV or JSON). Exports are ready within 5 minutes.",
"api_rate_limits": "Free plans: 100 requests/hour. Pro plans: 10,000 requests/hour. Enterprise: custom limits.",
"two_factor_auth": "Go to Settings > Security > Two-Factor Authentication. We support authenticator apps and SMS codes.",
}
@function_tool
def search_faq(
ctx: RunContextWrapper[SupportContext],
query: str,
) -> str:
"""Search the FAQ knowledge base for answers."""
ctx.context.log("FAQ Agent", f"Searched FAQ: {query}")
query_lower = query.lower()
results = []
for key, answer in FAQ_DATABASE.items():
if any(word in query_lower for word in key.split("_")):
results.append(f"**{key.replace('_', ' ').title()}**: {answer}")
if results:
return "\n\n".join(results)
return "No FAQ articles found matching your query."
@function_tool
def mark_resolved(
ctx: RunContextWrapper[SupportContext],
) -> str:
"""Mark the current issue as resolved."""
ctx.context.resolution_status = "resolved"
ctx.context.log("FAQ Agent", "Marked issue as resolved")
return "Issue marked as resolved."
faq_agent = Agent(
name="FAQ Agent",
model="gpt-4o-mini",
instructions="""You answer common customer questions using the FAQ
knowledge base. Search for relevant articles and provide clear,
helpful answers.
Rules:
- Always search the FAQ before answering
- If the FAQ has the answer, provide it and mark the issue resolved
- If the FAQ does not have the answer, say so clearly and suggest
the user might need billing help or escalation
- Never make up information not in the FAQ""",
tools=[search_faq, mark_resolved],
)
The billing agent handles financial operations. It has access to tools the FAQ agent does not:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
@function_tool
def get_invoices(
ctx: RunContextWrapper[SupportContext],
customer_id: str,
) -> str:
"""Retrieve recent invoices for a customer."""
ctx.context.log("Billing Agent", f"Retrieved invoices for {customer_id}")
return f"""Invoices for {customer_id}:
- INV-2024-001: $49.00 (Pro Plan - March 2026) - PAID
- INV-2024-002: $49.00 (Pro Plan - February 2026) - PAID
- INV-2024-003: $49.00 (Pro Plan - January 2026) - PAID"""
@function_tool
def process_refund(
ctx: RunContextWrapper[SupportContext],
invoice_id: str,
reason: str,
) -> str:
"""Process a refund for a specific invoice."""
ctx.context.log("Billing Agent", f"Processed refund for {invoice_id}: {reason}")
ctx.context.resolution_status = "resolved"
return f"Refund of $49.00 initiated for {invoice_id}. Will appear in 5-10 business days."
@function_tool
def change_subscription(
ctx: RunContextWrapper[SupportContext],
new_plan: str,
) -> str:
"""Change the customer's subscription plan."""
ctx.context.log("Billing Agent", f"Changed plan to {new_plan}")
ctx.context.subscription_plan = new_plan
return f"Subscription changed to {new_plan}. New billing starts next cycle."
billing_agent = Agent(
name="Billing Agent",
instructions="""You handle billing, payments, invoices, refunds,
and subscription changes.
Rules:
- Always verify the customer ID from context before making changes
- For refunds, confirm the invoice ID and reason with the user
- For plan changes, explain the pricing difference before proceeding
- Maximum refund without escalation: $200. For larger amounts,
hand off to Escalation Agent
- Never discuss other customers' billing information""",
tools=[get_invoices, process_refund, change_subscription],
)
The escalation agent handles cases that automated agents cannot resolve. It collects information and creates a support ticket:
@function_tool
def create_support_ticket(
ctx: RunContextWrapper[SupportContext],
summary: str,
priority: str,
details: str,
) -> str:
"""Create a support ticket for human review."""
ctx.context.escalation_reason = summary
ctx.context.escalation_priority = priority
ctx.context.resolution_status = "escalated"
ctx.context.log("Escalation Agent", f"Created ticket: {summary} [{priority}]")
ticket_id = f"TKT-{datetime.now().strftime('%Y%m%d%H%M%S')}"
return f"Support ticket {ticket_id} created (Priority: {priority}). A human agent will respond within {'1 hour' if priority == 'urgent' else '24 hours'}."
@function_tool
def get_interaction_history(
ctx: RunContextWrapper[SupportContext],
) -> str:
"""Get the full interaction log for this session."""
return "\n".join(ctx.context.interaction_log) or "No interactions logged."
escalation_agent = Agent(
name="Escalation Agent",
instructions="""You handle cases that automated agents cannot
resolve. Your job is to:
1. Acknowledge the customer's frustration
2. Summarize what has been tried so far (check interaction history)
3. Collect any additional details needed
4. Create a support ticket with appropriate priority
Priority guidelines:
- urgent: service outage, security issue, payment processing failure
- high: significant feature broken, large refund request
- normal: general issues requiring human judgment
- low: feature requests, minor cosmetic issues
Always give the customer the ticket ID and expected response time.""",
tools=[create_support_ticket, get_interaction_history],
)
The router ties everything together. It identifies the customer and routes to the right specialist:
from agents import handoff
@function_tool
def identify_customer(
ctx: RunContextWrapper[SupportContext],
email: str,
) -> str:
"""Look up a customer by email address."""
# Simulated lookup
ctx.context.customer_id = "cust_88421"
ctx.context.customer_name = "Sarah Chen"
ctx.context.customer_email = email
ctx.context.subscription_plan = "Pro"
ctx.context.log("Router", f"Identified customer: Sarah Chen ({email})")
return "Customer found: Sarah Chen (Pro plan)"
router = Agent(
name="Support Router",
model="gpt-4o-mini",
instructions="""You are the first point of contact for customer
support. For every conversation:
1. If you know the customer's email, identify them first
2. Classify the request:
- FAQ: general how-to questions, feature questions, documentation
- Billing: payments, invoices, refunds, plan changes
- Escalation: complaints, complex issues, anything you are unsure about
3. Hand off to the appropriate specialist
If a customer seems frustrated or mentions wanting to cancel, route
to Escalation regardless of the topic.
Never try to resolve issues yourself. Route immediately.""",
tools=[identify_customer],
handoffs=[
handoff(faq_agent, tool_description_override="Route to FAQ for general questions and how-to help"),
handoff(billing_agent, tool_description_override="Route to Billing for payments, invoices, and subscriptions"),
handoff(escalation_agent, tool_description_override="Route to Escalation for complaints, complex issues, or frustrated customers"),
],
)
Specialists need to escalate or redirect when a request does not match their expertise:
# FAQ agent can escalate or redirect to billing
faq_agent.handoffs = [
handoff(billing_agent, tool_description_override="Transfer to Billing if the user has a payment or subscription question"),
handoff(escalation_agent, tool_description_override="Escalate if you cannot find the answer in the FAQ"),
]
# Billing agent can escalate
billing_agent.handoffs = [
handoff(escalation_agent, tool_description_override="Escalate for refunds over $200 or complex billing disputes"),
]
from agents import Runner
context = SupportContext()
result = Runner.run_sync(
router,
"Hi, my email is sarah@example.com. I was charged twice this month and I want a refund.",
context=context,
)
print(result.final_output)
print("\n--- Interaction Log ---")
for entry in context.interaction_log:
print(entry)
print(f"Status: {context.resolution_status}")
This produces a natural conversation flow: Router identifies the customer, recognizes a billing issue, hands off to the Billing Agent, which retrieves invoices, confirms the duplicate charge, and processes the refund.
Timeout protection. Set max_turns=15 on the Runner to prevent infinite agent loops.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Logging. The interaction_log in the context captures every agent action, creating an audit trail for compliance and quality review.
Graceful degradation. If any specialist fails, the escalation agent is always available as a backstop. Every agent has an escalation handoff.
Create the new agent with its tools and instructions. Add a handoff to it from the router. Add the routing criteria to the router's instructions. No other agents need to change unless they should be able to redirect to the new specialist.
Persist the SupportContext to your session store (Redis or database) keyed by customer ID. When the customer returns, load the context and pass it to the Runner. The agents will see the interaction history and previous resolution status.
Each specialist agent has handoffs to other agents and the escalation path. If the FAQ agent receives a billing question, it can hand off to the Billing Agent directly. The system self-corrects through inter-specialist handoffs.
#CustomerSupport #MultiAgentSystems #OpenAIAgentsSDK #ProductionArchitecture #AgentDesign #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Ukrainian online stores use CallSphere AI voice and chat agents to answer order, delivery and returns questions 24/7 in Ukrainian and Russian, recover abandoned carts and scale support without hiring.
Orange Money and MTN MoMo agents in Liberia field endless customer questions and support calls. See how CallSphere AI voice and chat agents answer 24/7 in English, cut support pressure, and capture leads from about $50 a month.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
Five proven multi-agent architecture patterns built on A2A — orchestrator, peer mesh, hub-and-spoke, marketplace, and tiered specialist.
How to build a safety eval pipeline that runs known jailbreak corpora, prompt-injection attacks, and tool-misuse scenarios on every release — and gates merges on it.
OpenAI's parallel function calling can cut latency in half — or burn money on dependent calls. The architecture, code, and an eval that proves the win.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI