Module 11 · AI Agents & MCP · Drills

Drills: AI Agents & Tool Calling

Reading the loop is not knowing the loop. Type every one of these — in a scratch file or a real DocChat branch — before you reveal the solution. The agent loop only sticks when your fingers know it.

How to use this page Each drill is a small task. Attempt it first, run it against the Messages API (or stub the client), then click "Show solution" to compare. If yours works differently but correctly — great, that's fluency. Tick each box as you go; your progress is saved in this browser.

A · Warm-up reps Basic

Drill 1 first call

Make a basic messages.create call to claude-sonnet-4-6 with a system prompt and one user message, then print only the text blocks of the response.

Show solution
import anthropic
client = anthropic.Anthropic()

resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=512,
    system="You are DocChat, a precise document assistant.",
    messages=[{"role": "user", "content": "Say hello in one line."}],
)
for block in resp.content:
    if block.type == "text":
        print(block.text)

Remember: content is a list of blocks, not a string. Always guard on block.type.

Drill 2 tool schema

Define a tool dict named get_weather that takes a required city (string). Give it a description that says when to use it.

Show solution
weather_tool = {
    "name": "get_weather",
    "description": (
        "Get the current weather for a city. Use this whenever the "
        "user asks about weather or temperature."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string", "description": "City name, e.g. Dubai."},
        },
        "required": ["city"],
        "additionalProperties": False,
    },
}

Drill 3 tool_use block

Given a resp with stop_reason == "tool_use", pull out the first tool_use block and print its id, name, and input.

Show solution
tool_calls = [b for b in resp.content if b.type == "tool_use"]
if tool_calls:
    block = tool_calls[0]
    print(block.id)      # toolu_01A... — echo this back as tool_use_id
    print(block.name)    # get_weather
    print(block.input)   # {"city": "Dubai"} — already parsed, a dict

block.input is already a parsed dict — never raw-string-match the serialized JSON.

B · Stretch Intermediate

Drill 4 the loop

Write the agent loop: call the model, break on end_turn, otherwise append the assistant turn, run each tool, append a single user message of tool_result blocks, and repeat — with a 5-step cap.

Show solution
def agent(question, tools, run_tool):
    messages = [{"role": "user", "content": question}]

    for _ in range(5):                       # max-iteration guard
        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            tools=tools,
            messages=messages,
        )
        if resp.stop_reason == "end_turn":
            return next(b.text for b in resp.content if b.type == "text")

        messages.append({"role": "assistant", "content": resp.content})
        results = [
            {"type": "tool_result", "tool_use_id": b.id,
             "content": run_tool(b.name, b.input)}
            for b in resp.content if b.type == "tool_use"
        ]
        messages.append({"role": "user", "content": results})

    return "Hit the step limit."

Two non-negotiables: append resp.content before the results, and return all results in one user message.

Drill 5 structured JSON

Force the model to return JSON with name (string) and urgent (boolean), then parse it. Use output_config.

Show solution
import json

resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=256,
    messages=[{"role": "user",
               "content": "Ticket: Ada says the server is down NOW."}],
    output_config={"format": {
        "type": "json_schema",
        "schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "urgent": {"type": "boolean"},
            },
            "required": ["name", "urgent"],
            "additionalProperties": False,
        },
    }},
)
data = json.loads(next(b.text for b in resp.content if b.type == "text"))
print(data["name"], data["urgent"])   # Ada True

Don't prefill the assistant turn with { — that's a 400 on current models. output_config.format is the supported way.

C · Build challenge Build

Mini-project Build a 2-tool DocChat agent: search_documents (stub it to return a fixed string) and calculator (evaluate a simple arithmetic expression safely). Run the full loop with a max-iteration guard and ask a question that needs both — "Find our seat count in the office doc and tell me the cost at 1200 AED each."

Build · two-tool agent

Wire two tools, a run_tool dispatcher, and the capped loop. Validate the calculator input — never eval arbitrary strings.

Show solution
import anthropic, ast, operator
client = anthropic.Anthropic()

TOOLS = [
    {"name": "search_documents",
     "description": "Search DocChat for relevant passages. Use for document questions.",
     "input_schema": {"type": "object",
        "properties": {"query": {"type": "string"}},
        "required": ["query"], "additionalProperties": False}},
    {"name": "calculator",
     "description": "Evaluate an arithmetic expression. Use for any math.",
     "input_schema": {"type": "object",
        "properties": {"expr": {"type": "string"}},
        "required": ["expr"], "additionalProperties": False}},
]

_OPS = {ast.Add: operator.add, ast.Sub: operator.sub,
        ast.Mult: operator.mul, ast.Div: operator.truediv}

def safe_calc(expr):
    def ev(node):
        if isinstance(node, ast.Constant): return node.value
        if isinstance(node, ast.BinOp): return _OPS[type(node.op)](ev(node.left), ev(node.right))
        raise ValueError("unsupported expression")
    return str(ev(ast.parse(expr, mode="eval").body))

def run_tool(name, args):
    if name == "search_documents":
        return "[office.pdf] The Dubai office seats 40 people."   # stub
    if name == "calculator":
        return safe_calc(args["expr"])
    return f"Unknown tool: {name}"

def agent(question, max_steps=6):
    messages = [{"role": "user", "content": question}]
    for _ in range(max_steps):
        resp = client.messages.create(
            model="claude-sonnet-4-6", max_tokens=1024,
            system="You are DocChat. Use tools, then answer grounded in results.",
            tools=TOOLS, messages=messages)
        if resp.stop_reason == "end_turn":
            return next(b.text for b in resp.content if b.type == "text")
        messages.append({"role": "assistant", "content": resp.content})
        messages.append({"role": "user", "content": [
            {"type": "tool_result", "tool_use_id": b.id,
             "content": run_tool(b.name, b.input)}
            for b in resp.content if b.type == "tool_use"]})
    return "Hit the step limit."

print(agent("How many seats in the office, and the cost at 1200 AED each?"))

The model searches first (40 seats), then calls the calculator (40 × 1200) — two tools, one question, all its own decision. Note the AST-based safe_calc: never eval() model-supplied strings.

D · Rapid recall Flashcards

Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.

One-line definition of an agent?
An LLM in a loop that can call tools and act on the results.
click to flip
Three parts of a tool definition?
name, description, and input_schema (JSON Schema).
click to flip
How does the model "call" a tool?
It emits a tool_use block with stop_reason == "tool_use"; your code runs it.
click to flip
How do you return a tool's output?
A tool_result block in a user message, with tool_use_id matching the call's id.
click to flip
Current model IDs (June 2026)?
claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5.
click to flip
When NOT to build an agent?
When the steps are fixed — a workflow or plain RAG is cheaper and more reliable.
click to flip

E · Self-check before moving on

Tick each only if you can do it without looking:

Next All ticked? You can build agents — the thing most candidates can't. Next we standardise how agents discover tools and how you prove they actually work: Lesson 11.2 — MCP & Evals.