InfoWok
Intermediate

Build a Customer Support AI Agent in Python (2026)

A complete 2026 walkthrough for building a customer support AI agent in Python with the OpenAI Agents SDK. You wire an order-lookup tool, specialist agents for orders, refunds, and FAQs, a triage router that hands off between them, a human escalation path, and session memory — every snippet runnable.

SK
Sukhveer Kaur
Published July 4, 2026
5 min read
Title card 'Build a Customer Support AI Agent in Python' — a 2026 tutorial building a triage-and-handoff customer support AI agent with the OpenAI Agents SDKAI Engineering
HANDS-ON PROJECT
On this page +

A good support bot is not one giant prompt. It is a small team: a front desk that reads the message, a specialist for order status, another for refunds, one for FAQs, and a clean path to a human when things get hard. Build it that way and each part stays simple and testable.

In this tutorial you build exactly that — a customer support AI agent in Python, end to end, on the OpenAI Agents SDK. You wire an order-lookup tool, three specialist agents, a triage router that hands off between them, a human-escalation path, and memory so the chat remembers context. Every snippet runs as-is. By the end you will have a working support agent and a clear map of where to plug in your real systems.

🟡 Intermediate⏱️ 25 minStack: Python 3.10+, openai-agents
Before you start
🎯 Key takeaways
  • Model support as a team, not a mega-prompt: a triage router plus small specialist agents.
  • Tools connect the agent to reality — an order-lookup function tool returns live status.
  • Handoffs do the routing; a human-escalation handoff gives you a safe exit hatch.
  • A session gives the bot memory, so “I want a refund for it” resolves to the right order.

What your customer support AI agent will do#

The design is a triage router in front of specialists. The triage agent never answers directly. It reads the message and hands off to the specialist that fits: order status, refunds, or FAQs. When nothing fits or the customer is upset, it escalates to a human path.

Each specialist stays small and focused, which makes the whole system easy to test and cheap to run. That is the payoff of the team model over one sprawling prompt.

Step 1: The order-lookup tool#

Specialists need real data. Start with a tool that looks up an order. Here it reads a dictionary; in your app it would call your database or API.

python
from agents import function_tool
 
# Stand-in for your real orders database.
ORDERS = {
    "A1001": {"status": "shipped", "eta": "2 days"},
    "A1002": {"status": "processing", "eta": "5 days"},
}
 
@function_tool
def lookup_order(order_id: str) -> str:
    """Look up the status of a customer order.
 
    Args:
        order_id: The order ID, for example "A1001".
    """
    order = ORDERS.get(order_id)
    if not order:
        return f"No order found with ID {order_id}."
    return f"Order {order_id} is {order['status']} (ETA {order['eta']})."
💡 Tip

The docstring is the tool’s spec — the model reads it to decide when to call lookup_order and what to pass. Keep it specific. To connect real data, swap the dictionary for your database call and leave the rest unchanged.

Step 2: The specialist agents#

Now define the specialists. Each gets a focused instruction and a handoff_description the router uses to route. The order agent gets the tool; the others do not need it.

python
from agents import Agent
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
 
order_agent = Agent(
    name="Order Status Agent",
    handoff_description="Checks order status and shipping ETA.",
    instructions=f"{RECOMMENDED_PROMPT_PREFIX}\n"
                 "Help with order status. Use lookup_order for the latest status.",
    tools=[lookup_order],
)
 
refund_agent = Agent(
    name="Refund Agent",
    handoff_description="Handles refunds and billing disputes.",
    instructions=f"{RECOMMENDED_PROMPT_PREFIX}\n"
                 "Resolve refund and billing questions. Be clear about timelines.",
)
 
faq_agent = Agent(
    name="FAQ Agent",
    handoff_description="Answers general product and policy questions.",
    instructions=f"{RECOMMENDED_PROMPT_PREFIX}\n"
                 "Answer common product and policy questions concisely.",
)

The RECOMMENDED_PROMPT_PREFIX primes each agent to understand handoffs, which noticeably improves routing. Skip it and agents sometimes answer when they should transfer.

Step 3: The triage router#

The triage agent ties it together. It gets the specialists in handoffs and one job: route.

python
import asyncio
from agents import Agent, Runner
from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX
 
triage_agent = Agent(
    name="Support Triage",
    instructions=f"{RECOMMENDED_PROMPT_PREFIX}\n"
                 "You are front-line support. Route each message to the right specialist.",
    handoffs=[order_agent, refund_agent, faq_agent],
)
 
async def main():
    result = await Runner.run(triage_agent, "Where is my order A1001?")
    print(result.final_output)
    print("Handled by:", result.last_agent.name)
 
if __name__ == "__main__":
    asyncio.run(main())

Run it and the router transfers to the order agent, which calls the tool and answers. result.last_agent.name tells you which specialist replied — log it so you can see how traffic splits. The handoffs deep-dive covers customizing this routing further.

Step 4: Escalate to a human#

Some messages should not be auto-resolved. Add an escalation handoff that captures a reason and priority, then fires a callback where you would page a human.

python
from pydantic import BaseModel
from agents import Agent, handoff, RunContextWrapper
 
class Escalation(BaseModel):
    reason: str
    priority: str  # "low" | "normal" | "high"
 
human_agent = Agent(
    name="Human Escalation",
    instructions="Summarize the issue for a teammate and reassure the customer.",
)
 
async def on_escalate(ctx: RunContextWrapper[None], data: Escalation):
    # Push to your ticketing system or on-call queue here.
    print(f"[ESCALATION] {data.priority.upper()}: {data.reason}")
 
escalation = handoff(human_agent, on_handoff=on_escalate, input_type=Escalation)

Add escalation to the triage agent’s handoffs list and the router can now escalate with structured context. The input_type forces the model to state a reason and priority, so your on-call queue gets a real ticket, not a shrug. New to Pydantic models? The BaseModel primer is a quick read.

Step 5: Give it memory#

A real chat has more than one turn. Wrap runs in a session and the agent remembers earlier messages, so a vague follow-up still resolves.

python
import asyncio
from agents import Runner, SQLiteSession
 
async def main():
    session = SQLiteSession("customer_A1001", "support.db")
    await Runner.run(triage_agent, "My order is A1001.", session=session)
    result = await Runner.run(triage_agent, "Actually, I want a refund for it.", session=session)
    print(result.final_output)
    print("Handled by:", result.last_agent.name)
 
if __name__ == "__main__":
    asyncio.run(main())

The second message says “it,” not the order ID — but the session remembers A1001 from the first turn, and the router sends it to refunds. Sessions turn disconnected calls into a real conversation with almost no extra code.

Guardrails and going to production#

A demo that works is not a bot you trust with customers. Two additions matter most before launch. First, add guardrails so the agent refuses out-of-scope or unsafe requests — the SDK runs input guardrails on the first agent and output guardrails on the final one. Second, ground the FAQ agent in your real help center with retrieval (RAG) instead of the model’s general knowledge.

⚠️ Warning

Never let a support agent take an irreversible action — issuing a refund, cancelling an order — without a check. Keep the money-moving step behind a human approval or a tool that only proposes the action. The agent drafts; a person or a guarded tool commits.

Treat the demo as the skeleton and your tools, guardrails, and knowledge base as the muscle. That is the line between a toy and something you would put in front of customers. Before you ship, add evals so a routing or answer regression fails a test, not a customer.

Common mistakes#

  • One overloaded agent. Cramming orders, refunds, and FAQs into a single prompt makes it mediocre at all three. Split into specialists.
  • Vague handoff descriptions. The router routes on them. Name each specialist’s domain precisely.
  • Auto-committing money. Refunds and cancellations need a guard or a human. Let the agent draft, not execute.
  • No memory. Without a session, “it” and “that order” lose their meaning between turns.
  • Shipping without evals. A prompt tweak can silently break routing. Catch it in a test.

Summary#

You built a customer support AI agent as a small team: a triage router, specialists for orders, refunds, and FAQs, a tool for real data, a human-escalation path with structured context, and session memory. The structure is the point — each piece is simple, and you extend it by swapping in your own tools and knowledge base, not by growing one giant prompt. Add guardrails and evals, and this skeleton is ready for real customers.

🧭 Where to go from here

Building one for your own product? Tell me your specialist list — orders, refunds, shipping, whatever — and I’ll tell you where the routing tends to get confused.

Frequently asked questions

Do I need a framework, or can I just call the model directly? +
You can start with raw model calls, but a support bot quickly needs tool calls, routing between specialists, and memory. A framework like the OpenAI Agents SDK gives you handoffs, function tools, and sessions out of the box, so you write far less plumbing.
How does the agent decide which specialist handles a message? +
A triage agent routes with handoffs. It reads each specialist's description and transfers the conversation to the best match — order status, refunds, or FAQs. You control routing quality by writing clear handoff descriptions.
Can the agent remember earlier messages in a conversation? +
Yes. Wrap each run in a session (SQLiteSession) and pass the same session across turns. The agent then remembers context — like the order ID from an earlier message — without you managing history by hand.
How do I connect it to my real orders and help docs? +
Replace the demo lookup_order tool with a call to your real database or API, and ground the FAQ agent's answers in your help center with retrieval (RAG). The agent structure stays the same; only the tools change.

References

  1. OpenAI Agents SDK — Quickstart (official docs)
  2. OpenAI Agents SDK — Handoffs (official docs)
  3. openai/openai-agents-python (GitHub)
Written by
Sukhveer Kaur
Sukhveer KaurSoftware Developer & AI Engineer

Sukhveer is a software developer specialising in AI systems and backend engineering. She has hands-on experience designing agentic AI applications, working with large language model pipelines, autonomous agent frameworks, and cloud-native services in Java and Python. At InfoWok, she bridges the gap between cutting-edge AI research and practical implementation — helping developers understand and apply emerging technologies through clear, experience-backed writing.

New AI engineering guides, the day they ship

Real Python, production depth. No digest spam.

Comments