Build the support desk
Everything in this course, in one program, on the agent lesson 0 promised.
It is built in four pieces: two tools, a guard, a specialist, and the agent the customer talks to. Each one is something you wrote alone in an earlier lesson.
The two tools
def lookup_order(order_id: str, tool_context: ToolContext) -> dict:
"""Look up the delivery status of an existing order by its id."""
tool_context.state["last_order"] = order_id
return {"status": "success", "order_id": order_id, "state": "shipped", "date": "3 March"}The lookup writes the order id into state, so nothing later has to ask the customer again, and no model has to remember it.
def refund(amount: int, tool_context: ToolContext) -> dict:
"""Refund an amount in rupees for the order this customer is asking about."""
return {"status": "success", "refunded": amount,
"order_id": tool_context.state.get("last_order")}The refund reads that id back out of state rather than taking it as a parameter. Lesson 10's rule, applied where it matters: the model chooses the amount, never the account.
The guard
def needs_a_person(tool, args, tool_context):
"""Refuse anything expensive until a human has approved it."""
if tool.name == "refund" and args.get("amount", 0) > 100:
if not tool_context.state.get("approved_by_human"):
return {"status": "refused", "reason": "refunds over 100 need a person"}Four lines, from lesson 14. It returns a refusal for a big refund and nothing at all otherwise, which lets every other call through untouched.
Two agents
billing = LlmAgent(
name="billing",
description="Handles refunds and charges for a customer's order.",
model=PretendModel(replies=[call("refund", amount=500),
say("That refund needs a person to approve it.")]),
instruction="Handle billing for {customer?}. Use the refund tool.",
tools=[refund],
before_tool_callback=needs_a_person,
)The specialist owns the refund tool and the guard. Nothing else in the desk can issue money.
support = LlmAgent(
name="support",
description="Answers questions about orders.",
model=PretendModel(replies=[call("lookup_order", order_id="A17"),
say("Order A17 shipped on 3 March."),
call("transfer_to_agent", agent_name="billing")]),
instruction="Help {customer?} with orders. Send billing questions to the billing agent.",
tools=[lookup_order],
sub_agents=[billing],
)The agent the customer talks to. It answers order questions itself and hands billing over, which is lesson 16.
Running two turns
runner = InMemoryRunner(agent=support, app_name="desk")
session = await runner.session_service.create_session(
app_name="desk", user_id="u1", state={"customer": "Asha"})A session seeded with the customer's name, which both instructions read through their templates.
async def turn(text):
message = types.Content(role="user", parts=[types.Part(text=text)])
async for event in runner.run_async(user_id="u1", session_id=session.id,
new_message=message,
run_config=RunConfig(max_llm_calls=20)):
for part in (event.content.parts if event.content else []):
if part.text:
print(f"{event.author:<8} {part.text.strip()}")
elif part.function_response:
print(f"{event.author:<8} tool -> {part.function_response.response}")The session is seeded with the customer's name, which both instructions read through their templates, and every run carries the call limit from lesson 15.
print("--- turn 1")
await turn("Where is order A17?")
print("--- turn 2")
await turn("I want my money back, 500 rupees")
done = await runner.session_service.get_session(
app_name="desk", user_id="u1", session_id=session.id)
print("--- state")
print(dict(done.state))The first turn is one agent and one tool. The second is the interesting one: support handed over, billing asked for the refund, and the guard refused it before the tool ran. The customer got an explanation rather than an error, and no money moved.
Where each piece came from
| Piece | Lesson |
|---|---|
| Tools with docstrings the model reads | 5, 6, 7 |
ToolContext, so the refund reads the order from state | 8 |
An instruction template, {customer?} | 10 |
A before_tool_callback that can refuse | 12 |
max_llm_calls, so a stuck run stops | 13 |
| A sub agent and a transfer | 14 |
| Session state, seeded and written to | 19, 20 |
What to change first
- Set
approved_by_humanin state and run turn two again. The refund goes through, and nothing else changes. - Give billing its own
disallow_transfer_to_parentand see what happens when it tries to hand back. - Swap one model for a real one and read the events. The scripted answers become decisions.
- Add the monthly summary as a workflow, with the counting done in a function node.
What this course left out
| Left out | What it is |
|---|---|
| Deployment | Cloud Run, GKE and the managed agent runtime, plus adk deploy |
| Evaluation | adk eval, eval sets, criteria and simulated users |
| Live and voice agents | The bidirectional streaming API, audio and video |
| A2A | The agent to agent protocol, for agents from different vendors |
| Artifacts | Files an agent produces or is given, saved through the artifact service |
| Plugins and callbacks in depth | Reusable cross-cutting behaviour across a whole app |
| The integrations catalogue | More than a hundred connectors, from BigQuery to Slack |
| Other languages | The same ADK in TypeScript, Go, Java and Kotlin |
None of those change what you learned. They are the same agents with more around them, and the documentation reads like a manual once the loop makes sense.
You are done
You can explain what happens between a question and an answer in ADK, write tools a model can choose between, shape the reply into something your code can use, split work across agents and workflows, keep what matters in state, and stop the things that should not happen without a person. That is the whole toolkit, minus the parts you will read when you need them.
- Run the capstone, then set
approved_by_humanand run it again. - Put your own project's first agent together this week, with one tool and one guard.
You understood something today that you didn't yesterday.