Raza Rizwan

Worked example: a first-reply support agent, start to URL

The book's own Tier-1 support agent taken through every page of this room: the three gates, the spec, the first test, the agent and its two tools, the harness, the eval that catches the wrong-account reply, and the done-when checks at the live URL.

Status
stable
Trust
human-reviewedHuman: Raza Rizwan ·
5 min read
Owner
Human: Raza Rizwan
Approved
Human: Raza Rizwan ·

The book carries one agent through its deployment course: Maya, a Tier-1 support agent for a software product. She answers a customer's first message: she looks up the account, then drafts a reply. Her two tools are deliberately simple stand-ins so the harness runs without any external system behind it. 1 This page takes that same agent through this room's pages in order, so you can see the shape of each step on one small case before you do it on yours.

Gates

Gate 1. Customer messages arrive in different words about the same thing; there is no fixed rule that maps a message to its answer, so this is AI work, not a tool. And the work is not only to reply in kind: it has to look something up and then act on what it found, which is what makes it an agent shape rather than a chatbot. 2

Gate 2. The companion runs Maya one message at a time, with a person reading the draft; that is Mode 1, with a human at the end of every run. The third time the same task is done the same way, run Gate 2: if all three dials are up, often, same shape, worth it, that is the cue to cross it into a worker. 2

Gate 3. Works from: the customer's first message, and the account id when the message names one. Want at the end: a short reply in the requested tone. Done when: the draft exists, and when an account was named, the draft rests on that account's plan and status rather than on a guess. 2

Spec, in the six sections

# spec.md, First-reply support agent

## Goal
Answer a customer's first message with a correct, kind draft so a person
can send it instead of writing it.

## User Scenarios
- A message naming a known account gets a draft that reflects that
  account's plan and status.
- A message naming an unknown account gets a draft that says no account
  was found for that id.
- A message naming no account gets a draft without any lookup.

## Functional Requirements
FR-1 When a message references an account, look it up before drafting.
FR-2 The draft names what was found: plan and status, or "no account".
FR-3 The draft is short and in the requested tone; the default is friendly.

## Edge Cases & Rules
- The lookup returns more than one match: do not proceed with action
  tools until the matching account is identified; if it cannot be, stop
  and hand the message to a person.

## Out of Scope
- Sending the reply. Refunds, plan changes, or any change to an account.

## Acceptance Criteria
- [ ] acct_1001 in the message: the draft mentions plan Pro and status active.
- [ ] An unknown id: the draft says no account was found.
- [ ] No account in the message: no lookup call appears in the run.
- [ ] Two matches: no draft is produced; the run stops for a person.

The multiple-match rule is the book's own, from the eval walkthrough on the evals page: the fix for the wrong-customer refund was one targeted rule, "when the lookup returns multiple results, do not proceed with action tools until you have identified which account matches; if you cannot, escalate to a human". 3 Here it is written into the spec before any code exists.

The first test, before the tool body

Keep the lookup a plain function with a tiny interface, so the test calls only that and never touches the network, as on the tests page. The two tests come straight from the first two acceptance criteria:

# tests/test_accounts.py, written FIRST
from chat_agent.accounts import find_account


def test_known_account_reports_plan_and_status():
    assert "plan Pro" in find_account("acct_1001")
    assert "status active" in find_account("acct_1001")


def test_unknown_account_says_so():
    assert find_account("acct_9999") == "No account found for id acct_9999."

Watch both fail, write the function, watch both pass. The @function_tool below is a thin wrapper over that function.

The agent and its two tools

The companion's agent, as shipped: 1

# src/maya_harness/agent.py, from the book's companion
from agents import Agent, function_tool

from .settings import settings

# A tiny in-memory account table. In production this would be a CRM call.
_ACCOUNTS = {
    "acct_1001": {"name": "Ada Lovelace", "plan": "Pro", "status": "active"},
    "acct_1002": {"name": "Alan Turing", "plan": "Free", "status": "past_due"},
}


@function_tool
def lookup_account(account_id: str) -> str:
    """Look up a customer account by id and return its plan and status."""
    account = _ACCOUNTS.get(account_id)
    if account is None:
        return f"No account found for id {account_id}."
    return (
        f"Account {account_id}: {account['name']}, plan {account['plan']}, "
        f"status {account['status']}."
    )


@function_tool
def draft_reply(customer_message: str, tone: str = "friendly") -> str:
    """Draft a short support reply to a customer message in the given tone."""
    return (
        f"[{tone} draft]\n"
        f"Hi there, thanks for reaching out. About: '{customer_message}'. "
        f"Here is what I can do to help."
    )


def build_agent() -> Agent:
    """Construct Maya. Model defaults to gpt-5.4-mini (the SDK default)."""
    return Agent(
        name="Maya",
        instructions=(
            "You are Maya, a Tier-1 support agent. Be brief and kind. "
            "When a customer references an account, call lookup_account first. "
            "Then call draft_reply to compose your answer."
        ),
        model=settings.model,
        tools=[lookup_account, draft_reply],
    )

One line in that file has aged: the docstring names gpt-5.4-mini as the SDK default, which was true for the SDK release the companion was written against; the default has since changed, and the probe on the build page prints the one your installed SDK will use. Leave the model unset, as the previous pages do, and the code stays correct across releases.

Two things to notice against the earlier pages. The instructions carry the order of operations in words, and the spec's multiple-match rule belongs there too as one more sentence; both are prompts, written under the prompting guide. And neither tool changes anything in the world, so nothing here needs needs_approval; the moment a tool that sends the reply or changes the account is added, that tool gets the gate.

The harness and the container

Nothing changes from the FastAPI page: the same main.py with GET /health and POST /runs, the same Dockerfile, the same Dockerfile.vercel copy, PORT set to 8000 and OPENAI_API_KEY on the project. The book's boot condition holds here too: with no key, /health still answers and /runs answers 503 with the message to add one. 1

The eval that the tests cannot write

The golden set starts with the four acceptance criteria as four examples, tagged easy (known account), easy (no account), medium (unknown id) and hard (two matches), with a few reworded variants of each so the set is not one sentence per case. The hard case is the one that matters: a run can produce a perfect-looking draft for the wrong account, and only a tool-use eval that asks "was the right argument passed?" catches it. 3 Grade every example at two levels, output and tool calls, as a pass rate across several runs, and hold the change until the whole set passes, not just the case you were fixing. 3

At the URL

The end-to-end page in one line each: the smoke test with BODY set to "Hi, I cannot log in to account acct_1001." prints a draft that mentions Pro and active; the same test passes inside the container; /health answers ok from your phone at the deployed URL; after the quiet window the first request is slower and then ok again; the golden set passes against the live /runs; and a message with two matching accounts stops without a draft. 4

That is the whole room on one small agent. Yours will have different tools and a different spec, and exactly the same steps. Where this agent needs more than a session file, give it a record says what it grows into; how to write the skill that runs it is on write a skill.

Footnotes

  1. deploying-agents-base companion: src/maya_harness/agent.py, src/maya_harness/main.py, README.md. Panaversity, 2026. ↩ ↩2 ↩3

  2. Is This an Agent Problem?, the three gates. The AI Agent Factory, Panaversity, 2026. ↩ ↩2 ↩3

  3. Eval-Driven Development for AI Employees: A Multi-Track Crash Course, Concepts 3, 11 and 12. The AI Agent Factory, Panaversity, 2026. ↩ ↩2 ↩3

  4. Deploy Your Agent Harness to the Cloud: A Crash Course, Decisions 1 to 3. The AI Agent Factory, Panaversity, 2026. ↩

On this page