Agent Playbook, build tutorial

How to Build an AI Agent to Cut Support Tickets

A walkthrough of how to build a support agent that answers customers from your own knowledge base, resolves the routine tickets instantly, reads the room and hands an angry customer straight to a human, takes safe actions like checking an order, and drafts replies to public reviews. Every code step is shown in three frameworks, Google ADK, the Claude SDK, and LangGraph, so you can build it on the stack your team already knows. The code here is the agent's core logic; standing it up in production also means adding the setup, integrations, and hardening around it.

Why manual support does not scale, and bad bots make it worse

Most support volume is the same questions asked again, and answering them by hand is expensive: Gartner puts a live contact on phone, chat, or email at about $8.01, versus roughly $0.10 for a self-service resolution, and service leaders estimate 20 to 40 percent of live volume could be self-served. Customers want this too: a Harvard Business Review figure has 81 percent of them trying to solve a problem themselves before contacting you. The catch is that self-service rarely works, with Gartner finding only about 14 percent of issues fully resolved that way.

A rule-tree chatbot does not close that gap; it just frustrates people into demanding an agent. A grounded agent is different: it answers from your actual help content, cites where the answer came from, does real tasks like looking up an order, and escalates the hard or emotional cases to a human with full context instead of arguing with them.

Pick a framework: ADK, Claude SDK, or LangGraph

You do not build the agent loop from scratch. A framework gives you the moving parts: a step is a model plus an instruction plus a few typed tools. The code tabs on the build steps show the same triage, retrieve, and resolve flow three ways.

Google ADK gives the tools to an agent and runs it with a Runner, the Claude SDK drives a tool-use loop, and LangGraph builds an explicit graph that branches to escalate on anger. The retrieval, the grounding rule, and the tools are the same code in all three; only the orchestration and the model calls differ.

The architecture

Every ticket flows through one loop: receive and triage, retrieve from the knowledge base, draft an answer, then either resolve when it is confident and safe or escalate to a human otherwise. Resolved and escalated tickets feed a learning loop, and guardrails for grounding, PII, and approvals wrap the whole thing.

1 to 2. Receive and triage channel, intent, sentiment 3. Retrieve from the KB RAG, grounded 4. Draft the answer with sources, confidence 5. Confident and safe? grounded, not angry? 6. Resolve reply, read-only actions Escalate human + draft 7. Learn tickets become FAQs Guardrails grounding PII redact approve writes yes, confident unsure or angry human resolves feedback loop

Before you build: prerequisites

Content

Your help docs, policies, product info, and past resolved tickets. The richer and cleaner this is, the more the agent can answer.

Systems to connect

A model API key (Gemini, Claude, or both), your channels (WhatsApp, email, web), your helpdesk (Zendesk, Freshdesk), and order or CRM APIs.

Policy

Confidence threshold for auto-reply, what may be auto-actioned versus approved, escalation rules, and PII handling.

Setup: install and keys

Install the packages and set your Gemini key. That is enough to run the runnable file below, which answers from a tiny in-memory knowledge base. For production you add a vector store (pgvector, Pinecone) with an embedding model for retrieval, your channels (WhatsApp, email, a web widget), and your helpdesk (Zendesk, Freshdesk); those are provider-specific.

Shell (install and key)
# Python 3.10 or newer
python -m venv venv && source venv/bin/activate

# The runnable path below uses Google ADK + Gemini:
pip install google-adk google-genai

# Your Gemini key from aistudio.google.com/apikey:
export GEMINI_API_KEY="your-key-here"

The stack, and why each piece

Layer Pick Why
Channels WhatsApp Cloud API, email, and a web widget Meet customers where they already ask. In India that is WhatsApp first, so the agent has to be fluent there.
Knowledge base + retrieval A vector store (pgvector, Pinecone, Weaviate) with RAG The agent answers from your help docs, not the open internet. Retrieval is what keeps answers grounded and current.
Model Gemini 2.5 Flash, Claude, or any model behind LangChain Triages intent and sentiment and writes a grounded answer from the retrieved context, in Hindi or English. The code tabs show all three.
Agent framework Google ADK, the Claude SDK, or LangGraph Drives each ticket through triage, retrieve, resolve or escalate. Pick the one your team runs; the logic is identical.
Tools + helpdesk Your helpdesk API (Zendesk, Freshdesk) and order or CRM APIs Look up an order, create a ticket, and hand escalations to a human with full context, instead of starting from scratch.
Data + monitoring Postgres plus a dashboard Store conversations, sources cited, and outcomes, and track deflection, CSAT, and escalation over time.

Build it, step by step

The model and orchestration steps show the same logic in Google ADK, the Claude SDK, and LangGraph. Use the tabs to switch. The retrieval, routing, and guardrail steps are plain Python and identical everywhere. These are the building blocks, and they call your own systems (the vector store, the helpdesk, the channels) by name; the complete file you can run today is in "Put it together" below.

1

Index your knowledge base for retrieval

The agent's answers are only as good as what it can look up, so the first job is to turn your help docs, policies, and past tickets into searchable memory. You split each document into overlapping chunks, convert them to embeddings, and store them in a vector database. This is retrieval-augmented generation, or RAG. The embedding model is your choice (Gemini, Voyage, or an open model); the indexing itself is the same in every framework.

Python (RAG indexing)
from langchain_text_splitters import RecursiveCharacterTextSplitter

# embed() wraps your embedding model (Gemini embeddings, Voyage, or open-source).
def index_kb(docs):
    splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=120)
    rows = []
    for d in docs:
        for chunk in splitter.split_text(d.text):
            rows.append((chunk, embed(chunk), d.source))   # text, vector, source
    store.upsert(rows)
2

Answer only from what you retrieve

This single rule is the difference between a useful bot and an embarrassing one. The agent retrieves the top matching chunks, and if nothing is relevant enough it says it does not know and escalates, rather than inventing an answer. When it does answer, it writes strictly from the retrieved context and returns the sources it used. The tabs show the same grounded generation in Gemini, Claude, and LangChain.

from google import genai
client = genai.Client()

def answer(question: str) -> dict:
    ctx = store.search(embed(question), k=5)
    if not ctx or top_score(ctx) < 0.75:
        return {"answer": None, "escalate": True}          # we genuinely do not know
    resp = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=[GROUNDED_PROMPT,
                  "Context:\n" + "\n".join(c.text for c in ctx),
                  "Question: " + question])
    return {"answer": resp.text, "sources": [c.source for c in ctx]}
from anthropic import Anthropic
client = Anthropic()

def answer(question: str) -> dict:
    ctx = store.search(embed(question), k=5)
    if not ctx or top_score(ctx) < 0.75:
        return {"answer": None, "escalate": True}
    msg = client.messages.create(
        model="claude-opus-4-8", max_tokens=512, system=GROUNDED_PROMPT,
        messages=[{"role": "user", "content":
            "Context:\n" + "\n".join(c.text for c in ctx) + "\n\nQ: " + question}])
    return {"answer": msg.content[0].text, "sources": [c.source for c in ctx]}
from langchain_google_genai import ChatGoogleGenerativeAI
llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash")    # or ChatAnthropic(...)

def answer(question: str) -> dict:
    ctx = store.search(embed(question), k=5)
    if not ctx or top_score(ctx) < 0.75:
        return {"answer": None, "escalate": True}
    prompt = (GROUNDED_PROMPT + "\nContext:\n" + "\n".join(c.text for c in ctx)
              + "\n\nQ: " + question)
    return {"answer": llm.invoke(prompt).content, "sources": [c.source for c in ctx]}
3

Triage intent, sentiment, and urgency

Not every message wants the same handling, so before answering the agent classifies it into a small, typed shape: what the customer wants, how they feel, and how urgent it is. An angry or high-urgency message skips the bot entirely and reaches a human at once. Forcing structured output keeps this routing reliable. The tabs show the same classification three ways.

from google.genai import types
from pydantic import BaseModel

class Triage(BaseModel):
    intent: str        # how_to | order_status | refund | complaint | other
    sentiment: str     # happy | neutral | angry
    urgency: str       # low | normal | high

def triage(text: str) -> Triage:
    resp = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=["Triage this support message.", text],
        config=types.GenerateContentConfig(
            response_mime_type="application/json", response_schema=Triage))
    return resp.parsed
# Triage is the same Pydantic schema as the Gemini tab.
def triage(text: str) -> Triage:
    msg = client.messages.parse(
        model="claude-opus-4-8", max_tokens=64,
        messages=[{"role": "user", "content": f"Triage this support message: {text}"}],
        output_config={"format": Triage})
    return msg.parsed_output
# Triage is the same Pydantic schema as the Gemini tab.
triager = ChatGoogleGenerativeAI(
    model="gemini-2.5-flash").with_structured_output(Triage)

def triage(text: str) -> Triage:
    return triager.invoke(f"Triage this support message: {text}")
4

Resolve or escalate, with the human handed a draft

Now the loop decides. An angry or urgent message escalates straight to a person. Otherwise the agent answers from the KB, and if it is not confident or the answer was not found, it still escalates, but it hands the human a draft reply and the full context. A confident, grounded answer goes back to the customer with its sources. This routing is plain deterministic code.

Python (routing)
def handle(text: str):
    t = triage(text)
    if t.sentiment == "angry" or t.urgency == "high":
        return escalate(text, t, reason="needs_a_human")   # never bot an angry user
    a = answer(text)
    if a["escalate"] or a.get("confidence", 1) < policy.min_confidence:
        return escalate(text, t, draft=a["answer"])        # hand the human a draft
    return reply(text, a["answer"], sources=a["sources"])
5

Let it take actions, safely

Answering questions is half the value; doing things is the other half. Read-only actions such as looking up an order run automatically. Anything that changes state or moves money, such as a refund, is never executed by the agent: it drafts the action and routes it for a human to approve. These are plain typed functions the agent calls as tools.

Python (tools)
def get_order_status(order_id: str) -> dict:
    """Look up an order's status by id. Read-only, safe to run automatically."""
    o = erp.order(order_id)
    return {"status": o.status, "eta": o.eta}

def request_refund(order_id: str, amount: float) -> dict:
    """Refunds move money, so never auto-run: draft for human approval."""
    review_queue.add_action("refund", order_id, amount)
    return {"queued": True}
6

Draft replies to public reviews

Reviews are support in public, and tone matters more than speed. The agent reads a Google or app store review, judges the sentiment, and drafts an on-brand reply: apologise and offer to fix for an unhappy one, a warm thank-you for a happy one. Public replies are never auto-posted, because one tone-deaf automated response does real brand damage. A human approves and posts.

Python (draft, human posts)
def draft_review_reply(review) -> None:
    t = triage(review.text)
    tone = "apologise_and_fix" if t.sentiment == "angry" else "thank"
    # draft() wraps the same model call as answer(), in your chosen framework.
    draft = draft(review=review.text, tone=tone, brand=BRAND_VOICE)
    approvals.add(draft, channel=review.platform)        # human posts it
7

Close the loop: turn tickets into knowledge

Every escalation is a gap in the knowledge base, and the agent uses them to improve itself. On a schedule it gathers the questions it had to escalate, clusters the similar ones, and drafts a new help article for each common cluster. A human reviews each draft before it is indexed, so the agent answers next month what it could not answer this month, and deflection rises over time.

Python (learning loop)
def learn_from_resolved(window="7d") -> None:
    misses = tickets.where(escalated=True, period=window)
    clusters = cluster(misses, by="question_embedding")
    for c in top(clusters, n=10):
        article = draft_faq(examples=c.tickets)          # a new help article
        kb.propose(article)                              # human reviews, then index
8

Guardrails: grounding, PII, approvals, monitoring

In support, the safety rails are not optional. The agent redacts personal data such as card or Aadhaar numbers so it never echoes them back, and any write action, refund, or public post waits for human approval. Every interaction is written to an append-only audit log, and a dashboard tracks deflection rate, CSAT, escalation rate, and first response time, with an alert when deflection drops.

Python (guardrails)
def safe_reply(text: str, action=None) -> str:
    if contains_pii(text):
        text = redact(text)                  # never echo card / Aadhaar numbers
    if action and action.is_write and not action.approved:
        hold(action)                         # refunds and public posts need a human
    return text

# audit.append(ticket, action); metrics.track(deflection_rate, csat,
#   escalation_rate, first_response_time); alert_if(deflection_rate < target)
9

Assemble and run the support agent

Now wire triage, retrieval, and the tools into one runnable agent. ADK gives the tools to an LlmAgent and runs it with a Runner, the Claude SDK drives a tool-use loop, and LangGraph builds a graph that branches to escalate on anger and otherwise answers from the KB. The grounding, the schema, and the tools are the same in all three.

from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService

support_agent = LlmAgent(
    name="support_agent", model="gemini-2.5-flash",
    instruction="Triage the message. If angry or urgent, call escalate. Otherwise "
                "answer from the KB with answer(); if it returns escalate, call "
                "escalate. Use get_order_status for order questions.",
    tools=[triage, answer, get_order_status, request_refund, escalate])

runner = Runner(agent=support_agent, app_name="support",
                session_service=InMemorySessionService())
from anthropic import Anthropic
client = Anthropic()
TOOLS = [triage_tool, answer_tool, order_tool, refund_tool, escalate_tool]

def run_support(messages: list):
    while True:
        resp = client.messages.create(model="claude-opus-4-8", max_tokens=1024,
                                       tools=TOOLS, messages=messages)
        if resp.stop_reason != "tool_use":
            return resp
        messages.append({"role": "assistant", "content": resp.content})
        for b in resp.content:
            if b.type == "tool_use":
                out = dispatch(b.name, b.input)
                messages.append({"role": "user", "content": [
                    {"type": "tool_result", "tool_use_id": b.id, "content": str(out)}]})
from langgraph.graph import StateGraph, START, END

def do_triage(s): return {"t": triage(s["text"])}
def do_answer(s): return {"a": answer(s["text"])}
def route(s):
    t = s["t"]
    return "escalate" if t.sentiment == "angry" or t.urgency == "high" else "answer"

g = StateGraph(dict)
g.add_node("triage", do_triage); g.add_node("answer", do_answer)
g.add_node("escalate", lambda s: {"escalated": escalate(s["text"], s["t"])})
g.add_edge(START, "triage")
g.add_conditional_edges("triage", route)
g.add_conditional_edges("answer",
    lambda s: "escalate" if s["a"]["escalate"] else END)
g.add_edge("escalate", END)
support_agent = g.compile()

Put it together: run it end to end

Here is a runnable file. It answers one question strictly from a tiny in-memory knowledge base and prints the grounded reply with its source. Replace KB and search with your vector store and embeddings. Save it as main.py and run python main.py "how do refunds work?".

Python (full runnable file, ADK)
# main.py  --  run:  python main.py "how do refunds work?"
# A runnable reference: answers a question strictly from a tiny in-memory
# knowledge base. Replace KB and search() with your vector store + embeddings.
import sys, asyncio
from google import genai
from google.genai import types
from google.adk.agents import LlmAgent
from google.adk.runners import InMemoryRunner

client = genai.Client()                       # reads GEMINI_API_KEY

# ---- a tiny knowledge base (replace with your vector store) ----
KB = [
    "Orders ship within 2 business days. Track them from the link in your confirmation email.",
    "Returns are accepted within 7 days of delivery for unused items in original packaging.",
    "Refunds go back to the original payment method within 5 to 7 business days.",
]
def search(query: str, k: int = 2) -> list[str]:
    # Demo retrieval ranks by shared words. Swap for embeddings + a vector store.
    q = set(query.lower().split())
    return sorted(KB, key=lambda d: len(q & set(d.lower().split())), reverse=True)[:k]
# ---------------------------------------------------------------

def answer(question: str) -> dict:
    """Answer strictly from the retrieved passages, or escalate if not covered."""
    ctx = search(question)
    if not ctx:
        return {"answer": None, "escalate": True}
    resp = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=["Answer ONLY from the context. If it is not covered, say you do "
                  "not know.\nContext:\n" + "\n".join(ctx) + "\n\nQ: " + question])
    return {"answer": resp.text, "sources": ctx}

agent = LlmAgent(
    name="support_agent", model="gemini-2.5-flash",
    instruction="Call answer on the user's question and return the grounded reply.",
    tools=[answer])

async def main(question: str):
    runner = InMemoryRunner(agent=agent, app_name="support")
    session = await runner.session_service.create_session(app_name="support", user_id="demo")
    msg = types.Content(role="user", parts=[types.Part(text=question)])
    async for event in runner.run_async(user_id="demo", session_id=session.id, new_message=msg):
        if event.is_final_response():
            print(event.content.parts[0].text)

if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit('usage: python main.py "<a customer question>"')
    asyncio.run(main(sys.argv[1]))

You should see a grounded answer drawn only from the matching passage, with its source. Ask something the knowledge base does not cover and it declines rather than inventing an answer. Triage, safe actions, and review replies stay in the build steps above, which add your channels and helpdesk.

The agent console

Your team sees one screen: the queue, the conversation, and the agent's suggested answer with its sources, ready to send or escalate.

Support QUEUE (3) Order queryBOT, resolved Refund, angryESCALATED How-toBOT, draft CONVERSATION Where is my order #4471? Shipped, arriving Tue.Tracking: BLR-882... Thanks! SUGGESTED ANSWER Grounded reply draftedfrom 2 help articlesconfidence 0.91 Sourcesshipping-policy Sentimentneutral Send Escalate

Build, buy, or no-code

Approach Best for Effort Cost Control
Custom agent (build in-house) You want answers grounded on your own KB, your tools, and full control High Build cost + model usage Full
Helpdesk AI add-on (Zendesk AI, Freshdesk Freddy, Intercom Fin) Standard deflection on an existing helpdesk, fast start Low Per-resolution or seat subscription Medium
No-code chatbot (rule trees) A handful of fixed FAQs, a quick pilot Low Cheap, but brittle and cannot really understand Low

Trust, grounding, and the escalation boundary

The whole reason a support agent can be trusted is that it answers from your content and admits when it cannot. Retrieval gives it the relevant passages, a strict prompt keeps it inside them, every answer carries its sources, and a confidence floor sends anything weak to a human. That is what prevents the confident, wrong answer that destroys trust faster than any slow reply ever could.

The escalation boundary is just as important. Angry or urgent messages, low-confidence answers, anything that moves money, and any public reply all cross to a human, with the agent handing over a draft and full context. Personal data is redacted so it is never echoed back, every action is logged, and the DPDP Act 2023 obligations on the customer data in tickets are handled in the build, not afterwards.

The numbers

Benchmarks vary by source and should be treated as ranges, not promises. These are from neutral research, not vendor marketing.

Metric Manual Agent
Cost per contact ~$8.01 per live contact on phone, chat, or email (Gartner) ~$0.10 per self-service contact the agent resolves (Gartner)
Live volume that is deflectable Service leaders estimate 20 to 40 percent could be self-served (Gartner) Captured, with routine tickets answered instantly
Issues fully resolved in self-service today Only about 14 percent (Gartner, 2024) Higher, because answers are grounded in your KB, not a generic page
Customers who try self-help first 81 percent attempt it before contacting you (Harvard Business Review) Met with a real answer instead of a dead FAQ page

Sources: Gartner Customer Service and Support research; Harvard Business Review. Figures are industry ranges.

What makes it fail

  • !Answering from the model's general knowledge instead of your KB, so it is confidently wrong.
  • !Botting an angry customer instead of escalating to a human at once.
  • !Auto-posting public review replies or auto-refunding with no human gate.
  • !A stale knowledge base, so the bot is confidently out of date.
  • !No escalation path, so hard tickets dead-end, or PII leaks into a reply.

A safe rollout

  1. Start suggest-only: the agent drafts answers for your team, never replying to customers directly.
  2. Turn on auto-reply for high-confidence, grounded answers on one channel; escalate the rest.
  3. Add actions, read-only first and then gated writes, plus review-reply drafting.
  4. Turn on the FAQ-generation loop and scale to all channels with deflection and CSAT dashboards.

FAQs

General FAQs

Everything you need to know about the service and how it works. Can’t find an answer? Mail us at info@galific.com

  • How does it avoid making up answers?
    By grounding. The agent retrieves the most relevant chunks from your knowledge base and answers strictly from them, returning the sources it used. If nothing relevant is found, it says it does not know and escalates rather than inventing a response. This retrieval-augmented approach is the single most important safeguard, because a support bot that guesses confidently does more harm than no bot at all.
  • Which framework should I use: ADK, the Claude SDK, or LangGraph?
    The right framework depends on your team, and the agent logic is the same in all three. The code tabs let you read the identical triage, retrieve, and resolve flow in Google ADK, the Anthropic SDK, and LangGraph and pick the one you already know. ADK and LangGraph are model-agnostic, so you can run Gemini or Claude under either; the retrieval and the grounding rule are the same code regardless.
  • What happens when a customer is angry?
    It does not get a bot. The triage step detects anger and high urgency and escalates straight to a human, with the conversation and a suggested response attached. Making a frustrated customer argue with a chatbot is the fastest way to lose them, so the agent is deliberately built to step aside in exactly those moments and let a person take over immediately.
  • Can it handle Hindi and other Indian languages?
    Yes. It understands and replies in Hindi, English, and common mixes, and can be extended to other Indian languages, while still grounding every answer in your knowledge base. Customers ask in whatever they are comfortable with, including over WhatsApp, and get a correct, sourced answer rather than being forced into English or a rigid menu.
  • Can it actually do things, like check an order or refund?
    Yes, safely. Read-only actions such as looking up an order or delivery status run automatically through typed tools. Anything that changes state or moves money, such as a refund, is never executed by the agent: it drafts the action and a human approves it. Wrapping each capability as a validated tool means the agent cannot improvise a dangerous one.
  • Does it reply to Google and app store reviews?
    It drafts replies, and a human posts them. The agent reads the review, judges the tone, and writes an on-brand response, apologising and offering to fix an unhappy one or thanking a happy one. Public replies are never auto-posted, because a single tone-deaf automated reply can do real brand damage, so you keep the speed of a draft with the safety of human approval.
  • How does it keep the knowledge base from going stale?
    Through a learning loop. On a schedule the agent gathers the questions it had to escalate, clusters the common ones, and drafts new help articles for them, which a human reviews before they are indexed. So the knowledge base grows from real customer questions, and the agent answers next month what it could not answer this month, which is why deflection rises over time rather than plateauing.
  • Does it integrate with Zendesk, Freshdesk, or WhatsApp?
    Yes, through their APIs. It works on WhatsApp, email, and a web widget, creates and updates tickets in your helpdesk such as Zendesk or Freshdesk, and looks up orders in your ERP or CRM. Escalations land in your existing helpdesk with full context, so your team keeps the tools they already use rather than learning a new one.
  • How does escalation to a human work?
    Cleanly and with context. When the agent is unsure, the answer is not in the KB, or the customer is angry or urgent, it creates or routes a ticket to your team with the full conversation, the customer's intent and sentiment, and a draft reply where it has one. The human starts from a prepared draft rather than a blank box, so even escalated tickets are faster to close.
  • How do you measure deflection, and does it hurt CSAT?
    Deflection is the share of contacts the agent resolves without a human, and it is tracked alongside CSAT, escalation rate, and response time on a dashboard. Because answers are grounded and angry customers are escalated immediately, satisfaction typically holds or improves rather than dropping. An alert fires if deflection falls or satisfaction dips, so a quietly degrading bot is caught early.
  • Should I build a custom agent or buy a helpdesk AI add-on?
    Buy an add-on if you are already on a helpdesk and want standard deflection quickly with little engineering. Build, or have it built, when you need answers grounded on your own knowledge base, custom tools and actions, tight integration, and control of the data. See the comparison table above; the right answer depends on how custom your support and your systems are.
  • What does it cost to run, and how fast is payback?
    Running cost is mostly model and retrieval usage plus hosting, which is small against the labour it removes. With live contacts costing around $8.01 each versus about $0.10 for self-service, and 20 to 40 percent of live volume estimated to be deflectable, even a modest deflection rate usually pays back the build within a few months while freeing your team for the hard tickets.

Want this built and running in your stack?

Galific designs, builds, and runs support agents like this on ADK, the Claude SDK, or LangGraph, grounded on your knowledge base and wired into your helpdesk. Or explore the ready-made versions, from the L1 Support Bot to the FAQ Generator and Review Reply Assistant, in our agent suite.