# Exercises — Advanced Level, Session 2
# “Advanced tool use”

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

⚠ The model names used in the answers (`claude-sonnet-4-5`) are volatile — check the official documentation at the time of the 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,
- consult the maintenance history of a vehicle.

An intern produced this first version — it is **deliberately bad**:```json
{
  "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 — Critical (10 min).** List at least **6 defects** of this definition, each relating them to a design principle seen in class (single responsibility, naming, description-prompt, documentation of fields, borderline cases, minimum required fields).

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

- noun in `snake_case`, verb + object;
- description of 3-4 sentences: what / when to use it / when not to use it / what is returned (including the case “no results”);
- `input_schema` with a `description` for **each** field, `enum` where relevant, and justified use of `required`.

Business constraints to integrate:
- possible vehicle statuses: `disponible`, `en_mission`, `en_maintenance`, `hors_service`;
- intervention types: `revision`, `pneus`, `freins`, `carrosserie`, `controle_technique`;
- an intervention is planned for a slot (ISO 8601 date — international date format standard — + agency);
- the 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 `webpage/index.html` validator.

### Success criteria

- [ ] 3 tools with single responsibility, no “catch-all” settings.
- [ ] Each field in each schema has a `description`.
- [ ] The statuses and intervention types are `enum`, not free chains.
- [ ] The registration format is documented (description + possibly `pattern`).
- [ ] On the ambiguous query, the observed behavior is explainable by your descriptions.

### Fix items (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 `snake_case` and does not describe an action.
3. **Useless 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`:```python
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 **syntactic/semantic/protocol** classification.

**2.2 — Correction (20 min).** Rewrite the script with:
- a `while` loop managing an arbitrary number of tool revolutions;
- iteration on all blocks `tool_use`;
- management of the non-existent order via `is_error: true` (actionable message for the model);
- a check of `stop_reason` before 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 `webpage/index.html` debugger, injecting the "command not found" error.

### Fixed — the 5 bugs

| # | Line | Bug | Classification | Symptom |
|---|---|---|---|---|
| 1 | `max_tokens=50` | Too low: the JSON of the `tool_use` block may be truncated (`stop_reason: "max_tokens"`) | Syntactic | `input` incomplete or missing, parsing fails |
| 2 | `response.content[0]` | Assume the first block is `tool_use`; there may be text before, and HERE are **two** calls expected (two commands requested) | Protocol | `AttributeError` (text block without `.input`) or second call ignored |
| 3 | `messages.append(...)` without adding the helper response | Sent history contains a `tool_result` without the corresponding `tool_use` | Protocol | API Error 400 (Unknown `tool_use_id`) |
| 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 `stop_reason` test | If the model responds in text (no tool) or makes a 2nd turn of the tool, the script is false | Protocol / semantic | Erratic behavior depending on the answer |

Expected correction skeleton:```python
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 above budget?” »

USD = United States Dollar.

### The three tools to implement

1. `get_team_expenses(team, month)` → expense list `[{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: Tool 1's outputs 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 `while` with guardrail `max_iterations = 10` (classic certification question: what to do if the model loops? → hard limit + clean exit);
- multi-block management (the model can call `convert_currency` and `get_team_budget` in the same round);
- **results cache**: a `{(tool_name, params_frozen): result}` dictionary that bypasses repeated identical calls — log cache hits;
- logging of each round: `stop_reason`, tools called, parameters.

**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 for “in Swiss francs” (CHF, not supported by the enum). Observe: does the model still call the tool? Return `is_error: true` ("unsupported currency, valid currencies: EUR, USD") and check the catch-up.
3. **Cache**: ask two successive questions requiring the same expenses. Check the cache hit in the second round.

### Success criteria

- [ ] The final verdict (above/below 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_iterations` properly tests and interrupts a loop that is too long.
- [ ] Each `tool_use` received has exactly one `tool_result` returned.

### Answer key (structure)```python
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 **never** cache a `is_error: true` result? (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"}` to the first round to ensure that the agent starts by collecting data rather than responding from memory.