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:
- search for vehicles (by registration, status, agency),
- plan a maintenance intervention,
- view a vehicle’s maintenance history.
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:
- name in
snake_case, verb + object; - 3-4 sentence description: what / when to use it / when not to use it / what is returned (including the “no result” case);
input_schemawith adescriptionFor each field,enumwhere relevant, and justified use ofrequired.
Business constraints to integrate:
- possible vehicle statuses:
disponible,en_mission,en_maintenance,hors_service; - types of intervention:
revision,pneus,freins,carrosserie,controle_technique; - an intervention is planned for a slot (ISO 8601 date — international date format standard — + agency);
- registration follows the French format
AA-123-BB.
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
- 3 single-responsibility tools, no “catch-all” settings.
- Each field in each schema has a
description. - The statuses and types of intervention are
enum, not free channels. - The registration format is documented (description + possibly
pattern). - On the ambiguous query, the observed behavior is explainable by your descriptions.
Answer key (1.1)
- Multiple responsibility : a tool that does everything via
action→ the model must guess the valid values ofaction, never listed. - Naming :
FleetManageris not insnake_caseand does not describe an action. - Unnecessary description : “Manage the fleet” does not indicate when to trigger or what to expect in return.
dataopaque :"type": "object"withoutproperties= zero constraints, the model invents the structure.- No documented borderline cases (vehicle not found, slot unavailable, etc.).
requiredmisused : 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:
- a loop
whilemanaging an arbitrary number of tool revolutions; - iteration on all blocks
tool_use; - non-existent order management via
is_error: true(actionable message for the model); - a check of
stop_reasonbefore any analysis.
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
get_team_expenses(team, month)→ list of expenses[{label, amount_eur}](hard simulated data).convert_currency(amount, from_currency, to_currency)→ converted amount (simulated rate, e.g. 1 EUR = 1.08 USD ⚠ fictitious rate).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:
- loop
whilewith guardrailmax_iterations = 10(classic certification question: what to do if the model loops? → hard limit + clean exit); - multi-block management (the model can call
convert_currencyAndget_team_budgetin the same turn); - results cache : a dictionary
{(tool_name, params_frozen): result}which bypasses repeated identical calls — log cache hits; - logging of each round:
stop_reason, tools called, parameters.
3.3 — Robustness test (15 min). Three scenarios:
- Nominal : the question above. Check the complete chaining (expenses → conversion → budget → verdict).
- 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. - Hidden : ask two successive questions requiring the same expenses. Check the cache hit in the second round.
Success criteria
- The final verdict (over/under budget) is correct compared to the simulated data.
- No crash on the CHF scenario; the model explains the limitation or converts to USD by reporting it.
- At least one cache hit logged on scenario 3.
-
max_iterationscleanly tests and interrupts a loop that is too long. - Each
tool_usereceived exactly onetool_resultreturned.
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:
- Why don't you Never cache a result
is_error: true? (The error may be transient; and we want the model to retry with better parameters.) - Where to place the conversion: dedicated tool (composable, traceable) vs letting the model calculate (fast but unreliable for finance)? Architect's response: always a tool for monetary arithmetic .
- Extension variant: add
tool_choice: {"type": "any"}in the first round to ensure that the agent starts by collecting data rather than responding from memory.