Français
Applied AI · Advanced 🔴 · Session 2
✏️ Exercises
← Return to program 📄 Source .md

Exercises — Advanced Level, Session 2

“Advanced tool use”

Program : Applied AI — Yann Isola Level : Advanced (Claude Certified Architect preparation) Estimated total duration: 2h30 (in TD or at home) Technical prerequisites: Python ≥ 3.10, pip install anthropic pydantic , API key in ANTHROPIC_API_KEY

⚠ Model names used in fixes (claude-sonnet-4-5) are volatile — check official documentation at the time of exercise.


Exercise 1 — Tool Layout Design Challenge (45 min)

Context

You are an architect for an automobile fleet management platform. The AI ​​(Artificial Intelligence) assistant must allow managers to:

An intern produced this first version — it is intentionally bad :

{
  "name": "FleetManager",
  "description": "Gère la flotte",
  "input_schema": {
    "type": "object",
    "properties": {
      "action": {"type": "string"},
      "data": {"type": "object"}
    },
    "required": ["action", "data"]
  }
}

Work requested

1.1 — Review (10 min). List at least 6 defects of this definition, by linking each to a design principle seen in progress (single responsibility, naming, description-prompt, documentation of fields, borderline cases, minimum required fields).

1.2 — Redesign (25 min). Replace this single tool with 3 tools well designed. For each, produce the full JSON definition with:

Business constraints to integrate:

1.3 — Cross-test (10 min). Exchange your diagrams with a pair. Everyone writes 3 user requests in natural language (one of which is ambiguous) and predicts which tool will be triggered and with what parameters. Then test against the real API with tool_choice: {"type": "auto"} and compare. Finally pass your schemas through the validator webpage/index.html .

Success criteria

Answer key (1.1)

  1. Multiple responsibility : a tool that does everything via action → the model must guess the valid values ​​of action , never listed.
  2. Naming : FleetManager is not in snake_case and does not describe an action.
  3. Unnecessary description : “Manage the fleet” does not indicate when to trigger or what to expect in return.
  4. data opaque : "type": "object" without properties = zero constraints, the model invents the structure.
  5. No documented borderline cases (vehicle not found, slot unavailable, etc.).
  6. required misused : everything is required but nothing is defined — the worst of both worlds.

Exercise 2 — Debug a broken tool flow (45 min)

Context

The script below is supposed to answer questions about e-commerce orders. It contains 5 bugs (some cause API errors, others silently wrong behavior). It is provided to you in exo2_broken.py :

import anthropic, json

client = anthropic.Anthropic()

tools = [{
    "name": "get_order",
    "description": "Récupère une commande par son identifiant. Retourne statut, montant et date. Utiliser dès qu'une question porte sur une commande précise.",
    "input_schema": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string",
                         "description": "Identifiant de commande, format ORD-XXXXX."}
        },
        "required": ["order_id"],
    },
}]

DB = {"ORD-10042": {"statut": "expédiée", "montant": 129.90,
                    "date": "2026-06-28"}}

def get_order(order_id):
    return json.dumps(DB[order_id])          # BUG ?

messages = [{"role": "user",
             "content": "Où en est ma commande ORD-10042 ? Et la ORD-99999 ?"}]

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=50,                            # BUG ?
    tools=tools,
    messages=messages,
)

tool_block = response.content[0]              # BUG ?

result = get_order(tool_block.input["order_id"])

messages.append({"role": "user", "content": [{   # BUG ?
    "type": "tool_result",
    "tool_use_id": tool_block.id,
    "content": result,
}]})

final = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    messages=messages,
)
print(final.content[0].text)

Work requested

2.1 — Static audit (15 min). Without running, identify the 5 bugs. For each: the line, the expected symptom (400 error? Python exception? JSON truncated? incomplete response?), and the classification syntactic/semantic/protocol .

2.2 — Correction (20 min). Rewrite the script with:

2.3 — Validation (10 min). Run and verify that the model: (a) gives the status of ORD-10042, (b) cleanly explains that ORD-99999 is not found — without crashing. Replay the scenario in the debugger webpage/index.html by injecting the “command not found” error.

Fixed — the 5 bugs

# Line Bug Classification Symptom
1 max_tokens=50 Too low: the JSON of the block tool_use risk of being truncated (stop_reason: "max_tokens") Syntax input incomplete or missing, parsing fails
2 response.content[0] Suppose the first block is the tool_use ; there may be text before, and there is HERE two calls expected (two commands requested) Protocol AttributeError (text block without .input ) or second call ignored
3 messages.append(...) without adding the helper response The sent history contains a tool_result without the tool_use corresponding Protocol API error 400 (tool_use_id unknown)
4 DB[order_id] KeyError on ORD-99999: runtime error is not converted to tool_result with is_error: true Execution / protocol Python crash, broken loop
5 No loop + no test stop_reason If the model responds in text (no tool) or makes a 2nd turn of the tool, the script is false Protocol / semantics Erratic behavior depending on response

Expected correction skeleton:

messages = [...]
while True:
    response = client.messages.create(model="claude-sonnet-4-5",
                                      max_tokens=1024, tools=tools,
                                      messages=messages)
    if response.stop_reason != "tool_use":
        break
    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type != "tool_use":
            continue
        try:
            out = get_order(**block.input)
            results.append({"type": "tool_result",
                            "tool_use_id": block.id, "content": out})
        except KeyError:
            results.append({"type": "tool_result",
                            "tool_use_id": block.id,
                            "content": f"Commande {block.input.get('order_id')} "
                                       "introuvable. Vérifier l'identifiant "
                                       "(format ORD-XXXXX).",
                            "is_error": True})
    messages.append({"role": "user", "content": results})

print(next(b.text for b in response.content if b.type == "text"))

Exercise 3 — Multi-tool pipeline (60 min)

Context

Build a mini-agent “expense analyst” which combines three tools to respond to:

“What is the Data team's total spend as of June 2026, converted to USD, and is it over budget? »

USD = United States Dollar.

The three tools to implement

  1. get_team_expenses(team, month) → list of expenses [{label, amount_eur}] (hard simulated data).
  2. convert_currency(amount, from_currency, to_currency) → converted amount (simulated rate, e.g. 1 EUR = 1.08 USD ⚠ fictitious rate).
  3. get_team_budget(team, currency) → monthly team budget in the requested currency.

Work requested

3.1 — Design (15 min). Write the 3 tool definitions. Key chaining point: the outputs of tool 1 must provide Exactly what tool 2 expects (amounts + explicit currency). Document the supported currencies in enum (EUR , USD ).

3.2 — Implementation (30 min). Write the complete agentic loop:

3.3 — Robustness test (15 min). Three scenarios:

  1. Nominal : the question above. Check the complete chaining (expenses → conversion → budget → verdict).
  2. Semantic error caused : ask “in Swiss francs” (CHF, not supported by the enum). Observe: does the model still call the tool? Resend is_error: true (“currency not supported, valid currencies: EUR, USD”) and check the catch-up.
  3. Hidden : ask two successive questions requiring the same expenses. Check the cache hit in the second round.

Success criteria

Answer key (structure)

import anthropic, json

client = anthropic.Anthropic()
cache: dict[tuple, str] = {}

def call_tool(name: str, args: dict) -> tuple[str, bool]:
    """Retourne (contenu, is_error). Passe par le cache."""
    key = (name, json.dumps(args, sort_keys=True))
    if key in cache:
        print(f"[cache HIT] {name}{args}")
        return cache[key], False
    try:
        out = TOOL_IMPLS[name](**args)      # dict name -> fonction
        cache[key] = out
        return out, False
    except ToolError as e:
        return str(e), True                  # jamais mis en cache

messages = [{"role": "user", "content": QUESTION}]
for turn in range(10):                       # garde-fou
    resp = client.messages.create(model="claude-sonnet-4-5",
                                  max_tokens=2048, tools=TOOLS,
                                  messages=messages)
    print(f"[tour {turn}] stop_reason={resp.stop_reason}")
    if resp.stop_reason != "tool_use":
        break
    messages.append({"role": "assistant", "content": resp.content})
    results = []
    for b in resp.content:
        if b.type == "tool_use":
            content, is_err = call_tool(b.name, b.input)
            r = {"type": "tool_result", "tool_use_id": b.id,
                 "content": content}
            if is_err:
                r["is_error"] = True
            results.append(r)
    messages.append({"role": "user", "content": results})
else:
    raise RuntimeError("max_iterations atteint — boucle interrompue")

Discussion points in collective correction: