Chapter 22 of 22 · advanced
Capstone: an end-to-end app
What this session covers
There's nothing new this week, and that's the point. A real AI application isn't a new technique — it's the techniques you already have, wired together with judgment. So the capstone builds a working customer-support assistant out of nothing but parts from earlier chapters: a moderation guardrail on the input, an agent loop that decides what to do next, grounded tools that search a knowledge base and look up an order, and instructions that pin every answer to what the tools actually returned. Read the code and you'll recognize every piece. Assemble the pieces and you've got something you could ship.
That recognition is the whole course landing at once. Twenty chapters ago, "build an AI-powered support bot" was a vague, slightly intimidating ask. Now it breaks down into parts you've each built on their own — retrieve, ground, call tools, loop, moderate. What separates the people who ship from the people who demo is exactly this: seeing a product as a composition of known pieces, and knowing which piece each requirement is asking for.
OpenAI
The whole application is one function. Moderate the input, run a grounded agent loop over two tools, return the answer. Notice how little glue it takes to hold together — the parts were built to compose in the first place.
# Grounding sources as tools: a knowledge base to search and an order system to
# query. The agent decides which it needs.
_KB = [
{"topic": "returns", "text": "Items can be returned within 30 days; opened items get store credit only."},
{"topic": "shipping", "text": "Standard shipping is free over $50 and takes 3-5 business days."},
{"topic": "warranty", "text": "Electronics have a 1-year warranty covering defects, not accidental damage."},
]
_ORDERS = {
"1001": {"status": "shipped", "carrier": "UPS", "eta": "Tuesday"},
"1002": {"status": "processing", "eta": "ships within 24 hours"},
}
def search_kb(query: str) -> dict:
words = [w for w in query.lower().split() if len(w) > 2]
return {"results": [e for e in _KB if any(w in (e["text"] + e["topic"]).lower() for w in words)][:3]}
def order_status(order_id: str) -> dict:
return _ORDERS.get(order_id.strip(), {"error": f"no order {order_id!r}"})
_IMPL = {"search_kb": search_kb, "order_status": order_status}
_TOOLS = [
{"type": "function", "name": "search_kb", "description": "Search help articles (returns, shipping, warranty).",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}},
{"type": "function", "name": "order_status", "description": "Look up an order's status by its id.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]}},
]
The tools are grounding sources the agent picks between — search the knowledge base for a policy question, look up an order for an order question. The instructions do the tying: answer only from what the tools return, and escalate when the tools come up short.
# The whole app in one function: moderate, then run the grounded agent loop.
_GOAL = ("You are a customer support assistant. Use search_kb for policy questions and "
"order_status for order questions. Answer ONLY from tool results; if the tools don't "
"cover it, say you'll escalate to a human. Be warm and brief.")
def _flagged(text: str) -> bool:
return client.moderations.create(model="omni-moderation-latest", input=text).results[0].flagged
def assist(message: str) -> str:
if _flagged(message):
return "[message blocked by moderation]"
input_list = [{"role": "user", "content": message}]
response = None
for _ in range(6):
response = client.responses.create(model=_MODEL, instructions=_GOAL, input=input_list, tools=_TOOLS)
calls = [i for i in response.output if i.type == "function_call"]
if not calls:
break
input_list += response.output
for c in calls:
result = _IMPL[c.name](**json.loads(c.arguments))
input_list.append({"type": "function_call_output", "call_id": c.call_id, "output": json.dumps(result)})
return response.output_text if response else ""
Gemini
The same assembly, on Gemini. That it ports with only the API calls changing is the last bit of proof of the course's throughline — you learned patterns, not a vendor. Swap in a new provider, or a model that ships next month, and the architecture survives the swap.
def _flagged(text: str) -> bool:
r = client.models.generate_content(
model=_MODEL, contents=[types.Content(role="user", parts=[types.Part(text=text)])],
config=types.GenerateContentConfig(system_instruction=(
"Reply 'flag' if this text is hateful, harassing, sexual, violent, or dangerous; "
"otherwise reply 'ok'. One word only.")),
)
return "flag" in (r.text or "").lower()
def assist(message: str) -> str:
if _flagged(message):
return "[message blocked by moderation]"
contents = [types.Content(role="user", parts=[types.Part(text=message)])]
response = None
for _ in range(6):
response = client.models.generate_content(model=_MODEL, contents=contents, config=_CONFIG)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = [types.Part.from_function_response(name=fc.name, response=_IMPL[fc.name](**dict(fc.args)))
for fc in response.function_calls]
contents.append(types.Content(role="user", parts=parts))
return (response.text or "") if response else ""
Two closing thoughts for when you take this past a demo. First, everything you learned about production applies here at once: log token usage, measure quality with an eval harness, cache the stable parts of the prompt, watch latency, moderate both ends. A capstone is where those habits stop being separate lessons and turn into how you build by default. Second, keep it as simple as the problem allows — reach for a single agent before a multi-agent swarm, prompting before fine-tuning, long context before a RAG pipeline when the document already fits. The best AI applications aren't the ones with the most machinery. They're the ones where each piece is there because some requirement actually demanded it.
Put it to work
Three docker-compose apps under code/showcase/<slug>/, each one composing
several chapters into a single feature. Same drill as always: bash bootstrap-secrets.sh, docker compose up --build, http://localhost:3000.
Showcase 1 — Support assistant
The full stack — moderation, agent, grounded KB search, order lookup. Ask about an order or a policy and watch it pick a tool, ground its answer, and stay inside its guardrails, or escalate when it can't help. This is the reference architecture for a whole class of real products.
Showcase 2 — Content pipeline
Multi-agent orchestration with a safety gate on the output — writer drafts, critic sharpens, writer revises, moderation clears it. A content-generation feature that comes out better for the critique pass and safer for the gate than any single call would.
Showcase 3 — Q&A over docs
Long context, grounding, and moderation working together — paste a document, ask a question, get an answer anchored in the text. The "chat with your PDF" feature that launched a hundred startups, in a few dozen lines you understand completely.
Three products, zero new concepts. That's the course.
Run it
The README in this folder lists the Python version, the install line, the two
environment variables, and the exact commands. Keys come from the untracked
.env at the course root. The basic scripts run the assistant on an order
question and a policy question, so you can see it choose tools and ground its
answers.
Takeaways
A real AI application is a composition of things you already know. The capstone's support assistant, content pipeline, and document Q&A introduce nothing new — they wire together moderation, agents, tools, retrieval, grounding, and long context, each one from an earlier week. That's the skill the whole course was building toward: look at a product requirement, see which known piece it's asking for, then assemble the pieces with the production habits — measure, cache, moderate, log — that keep them honest. And here's the throughline that outlasts any model: you learned patterns, not a provider, so the architectures you built here port to whatever ships next. You started twenty chapters ago with a single API call. You can now build the thing that call was always leading up to. Go build it.