# Exercices — Niveau Avancé, Session 1
# « Claude API : plongée en profondeur »

**Programme :** Applied AI — Yann Isola
**Public :** Architectes solutions (préparation *Claude Certified Architect*)
**Durée totale estimée :** 2 h 30 – 3 h (hors classe ou en atelier encadré)
**Prérequis :** Python 3.10+, SDK (SDK = Software Development Kit) `anthropic` installé, clé API (API = Application Programming Interface) de test.

> ⚠ Tous les noms de modèles, tarifs et limites cités sont **volatils** : vérifiez la documentation officielle (docs.anthropic.com) avant de coder en dur quoi que ce soit.

---

## Exercice 1 — Construction de requête API : le client robuste (60 min)

### Contexte

Vous êtes architecte chez un courtier en instruments tokenisés. L'équipe produit veut un module Python `claude_client.py` réutilisable par tous les services internes. Votre mission : concevoir la fonction d'appel de référence, avec tous les paramètres maîtrisés et tous les `stop_reason` gérés.

### Consignes

**Partie A — La requête complète (20 min)**

Écrivez une fonction `appeler_claude()` qui :

1. Accepte : `question: str`, `persona: str`, `format_json: bool = False`, `max_tokens: int = 1024`.
2. Construit la requête avec :
   - un **system prompt** (canal privilégié : persona + règles de sortie — jamais dans un message `user`) ;
   - `temperature=0.2` (tâche technique) ;
   - si `format_json=True` : un **prefilling** `assistant` avec `"{"` pour forcer l'ouverture JSON (JSON = JavaScript Object Notation), et une `stop_sequences=["```"]` de sécurité.
3. Retourne un objet structuré `ReponseClaude(texte, stop_reason, input_tokens, output_tokens, cout_estime)`.

Squelette de départ :

```python
from dataclasses import dataclass
import anthropic

PRIX_INPUT_PAR_MTOK = 3.00    # $ / Mtok ⚠ volatil
PRIX_OUTPUT_PAR_MTOK = 15.00  # $ / Mtok ⚠ volatil

@dataclass
class ReponseClaude:
    texte: str
    stop_reason: str
    input_tokens: int
    output_tokens: int
    cout_estime: float

def appeler_claude(question: str, persona: str,
                   format_json: bool = False,
                   max_tokens: int = 1024) -> ReponseClaude:
    client = anthropic.Anthropic()
    # ... à compléter ...
```

**Partie B — La garde stop_reason (20 min)**

Complétez la fonction pour traiter **chacun** des quatre `stop_reason` :

- `end_turn` → retour nominal ;
- `max_tokens` → lever `ReponseTronqueeError` en incluant le texte partiel ET le compte de tokens (le client appelant décidera de relancer) ;
- `stop_sequence` → retour nominal + log de la séquence rencontrée (`response.stop_sequence`) ;
- `tool_use` → lever `NotImplementedError("tool loop hors périmètre session 1")`.

N'oubliez pas : si `format_json=True`, **re-préfixez** le `"{"` du prefill (il n'est pas inclus dans la réponse).

**Partie C — Le pré-comptage (20 min)**

Avant l'appel réel, utilisez l'endpoint `client.messages.count_tokens(...)` pour :

1. Compter les tokens d'entrée de la requête complète (system + messages).
2. Si `input_tokens + max_tokens > 200_000` ⚠ (fenêtre de contexte), lever `ContexteDebordeError` **sans consommer d'appel de génération**.
3. Logger l'estimation de coût **avant** l'appel.

### Livrables

- `claude_client.py` complet et exécutable.
- Un bloc de tests manuels (`if __name__ == "__main__":`) démontrant : un appel nominal, un appel JSON avec prefill, une troncature provoquée (`max_tokens=20`).

### Critères d'évaluation

| Critère | Points |
|---------|--------|
| Requête complète et paramètres justifiés (commentaires) | /6 |
| Les 4 `stop_reason` traités correctement | /6 |
| Prefill JSON re-préfixé côté client | /3 |
| Pré-comptage + garde de fenêtre de contexte | /3 |
| Qualité du code (typage, dataclass, logs) | /2 |
| **Total** | **/20** |

### Piège à éviter (indice)

`stop_reason: "max_tokens"` arrive avec un **HTTP 200**. Si votre gestion d'erreur ne regarde que les exceptions HTTP, vous livrerez des JSON tronqués en production.

---

## Exercice 2 — Implémentation du streaming SSE (50 min)

### Contexte

Le front-end de votre plateforme affiche les réponses de Claude en temps réel. Vous devez implémenter le consommateur de flux côté serveur, en traitant les **événements bruts** SSE (SSE = Server-Sent Events) — pas seulement le helper haut niveau — car la certification l'exige et votre équipe front a besoin des métadonnées fines.

### Consignes

**Partie A — Le consommateur d'événements (25 min)**

Implémentez `streamer_reponse()` qui consomme le flux bas niveau et maintient un état complet :

```python
def streamer_reponse(client, question: str) -> dict:
    """Consomme le flux SSE et retourne l'état final :
    {texte, stop_reason, output_tokens, evenements_recus (liste des types), ttft_ms}
    """
    import time
    etat = {"texte": "", "stop_reason": None, "output_tokens": None,
            "evenements_recus": [], "ttft_ms": None}
    debut = time.monotonic()

    with client.messages.stream(
        model="claude-sonnet-4-5",   # ⚠
        max_tokens=800,
        messages=[{"role": "user", "content": question}],
    ) as stream:
        for event in stream:
            etat["evenements_recus"].append(event.type)
            # ... à compléter : traiter chaque type d'événement ...
    return etat
```

Exigences :

1. `message_start` → capturer l'`id` du message.
2. `content_block_delta` (sous-type `text_delta`) → accumuler le texte ; au **premier** delta, enregistrer le TTFT (TTFT = Time To First Token) en millisecondes.
3. `message_delta` → capturer `stop_reason` et `output_tokens` (rappel : ils n'arrivent QUE dans cet événement).
4. `message_stop` → clore proprement.
5. Ignorer les `ping` sans planter ; sur un événement `error`, lever une exception avec le détail.

**Partie B — L'assertion de séquence (15 min)**

Écrivez `verifier_sequence(evenements: list[str]) -> bool` qui valide l'ordre canonique :

```
message_start
  → (content_block_start → content_block_delta* → content_block_stop)+
  → message_delta
  → message_stop
```

Testez-la sur la liste `evenements_recus` de la partie A. Cette fonction sert de test d'intégration : si Anthropic change le protocole ou si votre parseur perd des événements, elle échoue bruyamment.

**Partie C — Question d'architecture (10 min, rédigé)**

En 10 lignes max : votre front-end est derrière un proxy inverse (nginx) qui **bufferise** les réponses HTTP. Quel est l'impact sur votre streaming, quel symptôme observera l'utilisateur, et quelles directives de configuration corrigent le problème ? (Indice : `proxy_buffering`, `X-Accel-Buffering`, `text/event-stream`.)

### Critères d'évaluation

| Critère | Points |
|---------|--------|
| Tous les types d'événements traités (y compris ping/error) | /7 |
| TTFT mesuré au bon endroit (premier text_delta) | /3 |
| stop_reason + output_tokens extraits de message_delta | /4 |
| Validateur de séquence correct | /4 |
| Question proxy : buffering identifié + correctifs | /2 |
| **Total** | **/20** |

### Piège à éviter (indice)

Chercher `stop_reason` dans `message_start` ou `message_stop` = 0 point sur le critère 3. Relisez la séquence.

---

## Exercice 3 — Conception d'un traitement par lots (Batches API) (60 min)

### Contexte

Votre société doit classifier **80 000 courriels clients** archivés (conformité) : catégorie, sentiment, présence de réclamation réglementaire. Pas d'exigence de latence — le rapport est mensuel. Budget serré. C'est un cas d'école pour la **Batches API** : traitement asynchrone, **−50 % ⚠** sur les coûts, SLA (SLA = Service Level Agreement) de **24 h ⚠**, jusqu'à **100 000 requêtes ⚠** par batch.

### Consignes

**Partie A — Document de conception (25 min, rédigé)**

Produisez une note d'architecture (1–2 pages) couvrant :

1. **Découpage** : un seul batch de 80k ou plusieurs ? Justifiez (limites ⚠ : 100k requêtes / ~256 Mo ⚠ par batch ; granularité de reprise sur erreur).
2. **Schéma de `custom_id`** : proposez un format traçable (ex. `mail-{lot}-{id_source}`) et expliquez pourquoi la corrélation par `custom_id` est **obligatoire** (les résultats ne reviennent pas dans l'ordre).
3. **Choix de modèle** : quel modèle pour de la classification simple à 80k exemplaires, et pourquoi ? (coût vs capacité)
4. **Cumul caching + batch** : votre prompt de classification contient 3 000 tokens de taxonomie identiques pour les 80k requêtes. Expliquez comment `cache_control` se combine avec le batch et estimez l'économie supplémentaire (hits non garantis ⚠).
5. **Gestion des états finaux** : `succeeded`, `errored`, `expired`, `canceled` — politique de rejeu pour chacun.
6. **Estimation de coût complète** : avec entrée ~3 300 tokens/requête et sortie ~150 tokens/requête, chiffrez le coût total avec et sans batch, avec et sans cache (tarifs ⚠ de la grille du jour, montrez vos calculs).

**Partie B — Implémentation du pipeline (35 min)**

Codez `pipeline_batch.py` avec quatre fonctions :

```python
def construire_requetes(emails: list[dict]) -> list[dict]:
    """Génère les requêtes batch avec custom_id traçables,
    system prompt de taxonomie marqué cache_control,
    et prefill assistant '{' pour forcer le JSON."""

def soumettre(client, requetes: list[dict]) -> str:
    """Crée le batch, retourne son id. Découpe en plusieurs
    batches si > 100_000 requêtes."""  # ⚠

def surveiller(client, batch_id: str, intervalle_s: int = 60) -> None:
    """Polling de processing_status jusqu'à 'ended'.
    Logge request_counts à chaque itération.
    Backoff : ne pas marteler l'API."""

def recolter(client, batch_id: str) -> tuple[list, list]:
    """Itère les résultats JSONL (JSONL = JSON Lines).
    Retourne (succes, echecs). Les echecs incluent custom_id
    + type d'erreur pour rejeu ciblé."""
```

Exigences :

1. Les résultats `errored` sont écrits dans `rejeu.jsonl`, prêts à être resoumis dans un batch correctif.
2. Chaque réponse `succeeded` est validée : JSON parsable ET `stop_reason == "end_turn"` (une classification tronquée par `max_tokens` est un **échec silencieux** à intercepter).
3. Le polling utilise un intervalle croissant (60 s → 120 s → 300 s max) : un batch peut durer des heures, inutile d'interroger chaque seconde.

### Critères d'évaluation

| Critère | Points |
|---------|--------|
| Note d'architecture : les 6 points traités avec chiffres | /8 |
| custom_id traçable + corrélation correcte | /3 |
| cache_control combiné au batch | /3 |
| Validation stop_reason sur chaque résultat | /3 |
| Pipeline de rejeu des échecs | /3 |
| **Total** | **/20** |

### Piège à éviter (indice)

Deux pièges classiques : (1) supposer que les résultats reviennent dans l'ordre de soumission ; (2) compter uniquement les erreurs HTTP et laisser passer les classifications tronquées (`stop_reason: "max_tokens"`) comme des succès.

---

## Barème global

| Exercice | Poids |
|----------|-------|
| 1 — Client robuste | 35 % |
| 2 — Streaming SSE | 30 % |
| 3 — Conception batch | 35 % |

**Seuil de validation session :** 60 %. Les corrigés types sont fournis par le formateur après remise.
