Claude CLI - Agents et Subagents

Navigation : Index | << Précédent | Suivant >>

Module : Vibe-Coding / Claude Code / Notebooks CLI
Niveau : Intermediaire
Duree : 30 min
Prerequis : Notebooks 01-03 completes

Objectifs d’Apprentissage


Pourquoi les Agents ?

Les agents permettent de deleguer des tâches complexes a des instances specialisees de Claude, chacune optimisee pour un type de travail spécifique :

  • Efficacite : Un agent Explore utilise un modèle leger (Haiku) pour des recherches rapides
  • Securite : Les agents en lecture seule ne peuvent pas modifier votre code accidentellement
  • Parallelisation : Jusqu’a 10 agents peuvent travailler simultanement sur des sous-tâches

Cas d’usage typique : Vous demandez “Refactorise ce projet”. Claude lance automatiquement des agents Explore pour comprendre la structure, puis un agent Plan pour définir les étapes, avant d’effectuer les modifications.

1. Configuration

import sys
import subprocess
import json
import os

sys.path.insert(0, 'helpers')
from claude_cli import run_claude, verify_installation, print_response

EXAMPLES_DIR = os.path.join(os.getcwd(), 'examples', 'sample_project')
print(f"Claude CLI pret: {verify_installation()}")
Claude CLI pret: True

Lecture de la configuration

La cellule precedente realise trois actions distinctes qu’il faut dissocier pour comprendre la sortie :

  1. Import des helpers depuis helpers/claude_cli.py (run_claude, verify_installation, print_response) – ce module encapsule les appels subprocess a la CLI Claude pour eviter de reinventer la roue dans chaque cellule.
  2. Calcul du chemin des exemples : EXAMPLES_DIR pointe vers examples/sample_project (un mini-projet Python utilise pour les demonstrations suivantes). Le chemin depend du cwd du notebook au moment de l’execution.
  3. Verification de l’installation : verify_installation() teste l’executabilite reelle de la CLI (un appel claude --version sous le capot), pas seulement sa presence sur le PATH. Sous Windows, un shim npm .CMD serait trouve par shutil.which mais refuse par CreateProcess – le garde distingue donc introuvable, non-executable et executable (voir installation_status()).

L’output "Claude CLI pret: True" signifie ici que la CLI est installee, executable et que claude --version repond. C’est ce feu vert qui autorise les appels ulterieurs via run_claude(...) : les cellules suivantes obtiennent donc de vraies reponses de la CLI, avec leurs durees et contenus reels.

Prérequis d’environnement d’exécution : ce notebook invoque réellement la CLI Claude via helpers/claude_cli.py. La précondition n’est pas seulement que claude figure dans le PATH : la CLI doit être exécutable et authentifiée. Le helper distingue les trois états (introuvable / non-executable / executable) — sous Windows notamment, une installation npm fournit un shim claude.CMD que shutil.which trouve mais que CreateProcess refuse : c’est le piège classique que verify_installation() détecte désormais en testant réellement claude --version. L’import du helper réussit toujours (sys.path.insert(0, 'helpers') est fait par le code) ; aucune ModuleNotFoundError n’est attendue, et rien n’est simulé (SIMULATION_MODE = False).

2. Concept d’Agents

Claude Code dispose d’agents specialises qui peuvent effectuer des tâches complexes de maniere autonome :

Agent Fonction Outils disponibles
Explore Recherche rapide en lecture seule Read, Glob, Grep
Plan Planification et architecture Read, Glob, Grep + analyse
General Tâches générales Tous les outils
┌─────────────────────────────────────┐
│          Claude Principal           │
│  (Coordonne les tâches complexes)   │
└───────────────┬─────────────────────┘
                │
    ┌───────────┼───────────┐
    ▼           ▼           ▼
┌───────┐  ┌───────┐  ┌───────┐
│Explore│  │ Plan  │  │General│
│ Agent │  │ Agent │  │ Agent │
└───────┘  └───────┘  └───────┘

Note importante : Les agents Explore et Plan sont automatiquement utilises par Claude en mode interactif (claude sans -p). En mode CLI one-shot (claude -p "..."), nous simulons leur comportement pour comprendre leur logique.

Schema : delegation aux sous-agents

Le Claude principal coordonne et delegue les sous-tâches a des sous-agents specialises.

flowchart TD
    P["Claude Principal (coordonne les tâches complexes)"]
    P --> E["Explore Agent"]
    P --> Pl["Plan Agent"]
    P --> G["General Agent"]

3. Agent Explore

L’agent Explore est optimise pour la recherche rapide dans un codebase :

Caractéristique Detail
Mode Lecture seule (ne modifie jamais de fichiers)
Modèle Haiku (rapide, economique)
Outils Glob (recherche fichiers), Grep (recherche contenu), Read (lecture)
Timeout Plus court que l’agent principal

Quand Claude lance-t-il un agent Explore ?

  • Quand vous posez une question sur la structure du projet
  • Quand vous demandez “trouve tous les fichiers qui…”
  • Quand Claude a besoin de contexte avant une modification

Différence avec une recherche manuelle

# Recherche manuelle (vous)
grep -r "def calculate" *.py

# Avec agent Explore (Claude en mode interactif)
"Trouve toutes les fonctions de calcul dans le projet"
→ Claude lance Explore qui utilise Grep intelligemment
→ Explore synthetise les résultats
→ Claude recoit un resume structure

En mode interactif, Claude lance automatiquement des agents Explore. En CLI, on peut simuler ce comportement.

# Simuler une recherche de type Explore
# En pratique, Claude utilise l'agent Explore automatiquement

stdout, stderr, code = run_claude(
    f"""Explore ce projet et reponds a ces questions:
1. Quelles sont les principales fonctions ?
2. Y a-t-il des tests ?
3. Quelles dependances externes sont utilisees ?

Projet:
- main.py : point d'entree
- utils.py : fonctions utilitaires
- tests/test_utils.py : tests unitaires""",
    model="haiku"  # Modele rapide comme un agent Explore
)
print_response(stdout, stderr, code)
=== Reponse Claude ===
## Analyse du Projet

### 1️⃣ Principales Fonctions

**utils.py** (module utilitaire) :
- `calculate_statistics(data)` — Calcule moyenne, médiane, écart-type, min/max d'une liste de nombres
- `format_report(stats)` — Formate les statistiques en rapport lisible
- `validate_data(data)` — Valide et nettoie les données (convertit en float, exclut NaN/Inf)
- `normalize_data(data)` — Normalise les valeurs entre 0 et 1

**main.py** (point d'entrée) :
- `main()` — Exécution principale : charge des données d'exemple, calcule les stats, affiche le rapport
- `process_batch(items)` — Traite un lot d'éléments et retourne un résumé (total, valides, invalides, taux de succès)

### 2️⃣ Tests

**Oui**, tests complets avec **pytest** dans `tests/test_utils.py` :
- 4 classes de tests couvrant chaque fonction
- **TestCalculateStatistics** (5 tests) : cas basique, médiane paire, un élément, liste vide, nombres négatifs
- **TestFormatReport** (2 tests) : formatage et valeurs manquantes
- **TestValidateData** (3 tests) : données mixtes, invalides, valeurs spéciales (NaN/Inf)
- **TestNormalizeData** (3 tests) : normalisation basique, données constantes, liste vide

Lancer les tests : `pytest tests/test_utils.py -v`

### 3️⃣ Dépendances Externes

- **pytest** — Framework de test (installé via `pip`)
- **math** — Module standard Python (écart-type, isnan, isinf)
- **typing** — Module standard Python (type hints)

Pas de dépendances tierces pour le code principal — code pur Python 3.9+.

Deux facettes de l’agent Explore

La cellule précédente simulait une vue d’ensemble du projet (structure, fonctions, dependances). La cellule suivante illustre une recherche ciblee : trouver des fonctions avec des signatures spécifiques (prenant une liste, retournant un dictionnaire). Ces deux approches correspondent aux outils Glob (trouver des fichiers) et Grep (chercher dans le contenu) que l’agent Explore utilise en mode interactif.

# Recherche de patterns specifiques (comme Grep)
with open(os.path.join(EXAMPLES_DIR, 'utils.py'), 'r') as f:
    utils_content = f.read()

stdout, stderr, code = run_claude(
    f"""Dans ce code, trouve toutes les fonctions qui:
1. Prennent une liste en parametre
2. Retournent un dictionnaire

Code:
{utils_content}""",
    model="haiku"
)
print_response(stdout, stderr, code)
=== Reponse Claude ===
Dans ce code, il y a **une seule fonction** qui prend une liste en paramètre ET retourne un dictionnaire:

## **`calculate_statistics`**

```python
def calculate_statistics(data: List[float]) -> Dict[str, float]:
```

- **Paramètre :** `data: List[float]` ✓
- **Retour :** `Dict[str, float]` ✓
- **Contenu retourné :** `{"mean": ..., "median": ..., "std_dev": ..., "min": ..., "max": ..., "count": ...}`

---

### Pourquoi les autres ne correspondent pas:

| Fonction | Prend une liste? | Retourne un dict? | Verdict |
|----------|---|---|---|
| `calculate_statistics` | ✓ | ✓ | **OUI** |
| `format_report` | ✗ (prend un dict) | ✗ (retourne str) | Non |
| `validate_data` | ✓ | ✗ (retourne List) | Non |
| `normalize_data` | ✓ | ✗ (retourne List) | Non |

Ce que fait l’agent Explore en pratique

Les deux cellules précédentes illustrent les deux facettes de l’agent Explore :

  1. Vue d’ensemble : La première requête demande une synthese globale du projet (fonctions, tests, dependances). C’est l’equivalent d’un find + grep intelligent qui comprend le sens du code.
  2. Recherche ciblee : La seconde cherche des patterns spécifiques (fonctions prenant une liste, retournant un dict). C’est l’equivalent d’un grep structure qui comprend les signatures.

Dans un vrai usage, vous n’ecrivez pas ces requêtes vous-même. En mode interactif, vous demandez simplement “Quelles fonctions retournent un dictionnaire ?” et Claude decide automatiquement de lancer un agent Explore avec les outils adaptes (Grep pour chercher, Read pour lire les fichiers trouves).

4. Agent Plan

L’agent Plan aide a structurer des tâches complexes avant de les executer :

Caractéristique Detail
Rôle Decomposer une tâche en étapes executables
Modèle Sonnet (raisonnement avance)
Outils Read, Glob, Grep + capacite d’analyse
Sortie Plan structure avec étapes, risques, dependances

Workflow typique avec Plan

┌──────────────────────────────────────────────────────────┐
│  Utilisateur: "Ajoute une API REST au projet"            │
└──────────────────────┬───────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────┐
│  1. Agent Explore → Analyse structure existante          │
│  2. Agent Plan → Créé plan d'implementation              │
│     - Fichiers a créer                                   │
│     - Ordre des étapes                                   │
│     - Risques identifies                                 │
│  3. Utilisateur → Valide ou ajuste le plan               │
│  4. Agent Principal → Execute le plan valide             │
└──────────────────────────────────────────────────────────┘

Quand utiliser Plan explicitement ?

  • Refactoring majeur : Avant de toucher a plusieurs fichiers
  • Nouvelle fonctionnalite : Pour définir l’architecture
  • Migration : Pour planifier les étapes sans rien casser
# Demander un plan d'implementation
stdout, stderr, code = run_claude(
    """Je veux ajouter les fonctionnalites suivantes au projet sample_project:

1. Export des resultats en CSV
2. Graphiques avec matplotlib
3. Interface en ligne de commande (CLI)

Cree un plan d'implementation detaille avec:
- Les fichiers a creer/modifier
- L'ordre des etapes
- Les dependances a ajouter
- Les risques potentiels""",
    timeout=300
)
print_response(stdout, stderr, code)
=== Reponse Claude ===
Voici le plan d'implémentation basé sur l'état actuel de `examples/sample_project/` (seulement `main.py`, `utils.py`, `tests/test_utils.py` — pas de `requirements.txt`, aucune dépendance externe pour l'instant).

## 1. Fichiers à créer / modifier

| Fichier | Action | Contenu |
|---|---|---|
| `sample_project/export.py` | **Créer** | `export_stats_to_csv(stats: Dict[str, Any], filepath: str) -> None` et `export_records_to_csv(data: List[float], filepath: str) -> None`, via `csv` (stdlib, aucune dépendance) |
| `sample_project/visualization.py` | **Créer** | `plot_distribution(data: List[float], output_path: str) -> None` (histogramme) et `plot_summary(stats: Dict[str, Any], output_path: str) -> None` (barres min/mean/median/max) ; backend `matplotlib.use("Agg")` forcé en tête de fichier pour fonctionner sans affichage |
| `sample_project/cli.py` | **Créer** | Point d'entrée `argparse` avec sous-commandes `analyze`, `export`, `plot` ; réutilise `utils.calculate_statistics`/`format_report`, `export.py`, `visualization.py` |
| `sample_project/main.py` | **Modifier** | Extraire les données d'exemple (`data = [...]`, ligne 14) dans une fonction `get_sample_data()` réutilisable par `cli.py`, sans changer le comportement de `main()` |
| `sample_project/tests/test_export.py` | **Créer** | Tests CSV : contenu, en-têtes, fichier vide/dossier inexistant |
| `sample_project/tests/test_visualization.py` | **Créer** | Tests génération de fichier (existence, taille > 0) — pas de comparaison pixel par pixel |
| `sample_project/tests/test_cli.py` | **Créer** | Tests via `subprocess`/`capsys` sur les sous-commandes et codes de sortie |
| `sample_project/requirements.txt` | **Créer** | `matplotlib>=3.7` (seule dépendance externe réelle ; `csv` et `argparse` sont stdlib) |
| `examples/CLAUDE.md` | **Modifier** | Sections "Structure", "Architecture", "Commandes Utiles", "Dependances" mises à jour |

Point à trancher avec toi : les données restent-elles hardcodées (`main.py:14`) et le CLI ne fait qu'exporter/tracer ces données d'exemple, ou le CLI doit-il aussi lire un fichier d'entrée (`--input data.csv`) ? Je recommande de **rester sur les données hardcodées** pour ce premier lot (le projet est pédagogique, cf. `examples/CLAUDE.md`), et d'ajouter la lecture de fichier dans un lot ultérieur si besoin — évite le scope creep.

## 2. Ordre des étapes

1. `requirements.txt` + installation matplotlib (valider l'environnement avant d'écrire du code qui en dépend)
2. `export.py` + `test_export.py` (le plus autonome, aucune dépendance externe)
3. `visualization.py` + `test_visualization.py` (dépend de matplotlib, backend `Agg`)
4. Extraction `get_sample_data()` dans `main.py` (petit refactor, ne casse pas `test_utils.py`)
5. `cli.py` + `test_cli.py` (assemble 2, 3, 4)
6. Mise à jour `examples/CLAUDE.md`
7. `pytest tests/ -v` complet + `python -m sample_project.cli --help` en fumée manuelle

## 3. Dépendances à ajouter

- **matplotlib** (`requirements.txt`) — seule dépendance externe nécessaire
- `csv`, `argparse` : stdlib, rien à ajouter
- Pas de dépendance CLI framework (`click`/`typer`) : `argparse` suffit pour 3 sous-commandes et respecte "pas de dépendances externes pour le code principal" autant que possible

## 4. Risques potentiels

- **Backend matplotlib sans affichage** : sans `matplotlib.use("Agg")` avant tout `import pyplot`, les tests peuvent échouer ou tenter d'ouvrir une fenêtre sur CI/Windows headless — à fixer dès la création de `visualization.py`, pas après coup.
- **Fichiers de sortie côté tests** : les tests CSV/PNG doivent écrire dans un répertoire temporaire (`tmp_path` pytest), jamais dans le repo — sinon pollution du dépôt à chaque run.
- **Rupture des tests existants** : le refactor de `main.py` (extraction `get_sample_data()`) ne doit pas changer la signature de `main()` utilisée potentiellement par d'autres exercices des notebooks — vérifier qu'aucun notebook n'importe `main.data` directement avant de toucher au fichier.
- **Encodage CSV sur Windows** : `csv.writer` avec `newline=""` requis explicitivement, sinon lignes vides parasites sous Windows (`\r\n` doublé).
- **Divergence conventions projet** : `examples/CLAUDE.md` exige docstrings Google-style + type hints obligatoires + couverture tests > 80% — à respecter dans les 3 nouveaux modules pour rester cohérent avec le style existant de `utils.py`.

Dis-moi si tu veux que je lance l'implémentation dans cet ordre, ou si tu veux d'abord trancher la question data hardcodée vs fichier d'entrée.

Ce que produit l’agent Plan

Un bon plan d’implementation produit par l’agent Plan devrait contenir :

  1. Inventaire des fichiers a créer et modifier (avec chemins précis)
  2. Ordre d’exécution justifie (dependances entre étapes)
  3. Risques identifies (breaking changes, regressions possibles)
  4. Dependances techniques (nouveaux packages, configs)

Conseil pratique : Avant de demander a Claude d’executer un refactoring ou une nouvelle fonctionnalite, demandez toujours un plan d’abord. Cela vous permet de valider l’approche et d’eviter les modifications inutiles. En mode interactif, vous pouvez utiliser la commande /plan ou simplement dire “Planifie avant de modifier quoi que ce soit”.

# Plan de refactoring
stdout, stderr, code = run_claude(
    f"""Propose un plan de refactoring pour ce code:

{utils_content}

Objectifs:
- Ameliorer la lisibilite
- Ajouter la gestion d'erreurs
- Permettre l'extension future

Format: etapes numerotees avec justification""",
    timeout=180
)
print_response(stdout, stderr, code)
=== Reponse Claude ===
Voici un plan de refactoring en 10 étapes, classées par ordre d'application logique (fondations d'abord, extensions ensuite).

## 1. Introduire des exceptions métier dédiées
Remplacer `raise ValueError("La liste...")` par une hiérarchie `StatisticsError` / `EmptyDataError(StatisticsError)`.
**Justification** : un `ValueError` générique ne se distingue pas d'une erreur de type accidentelle ailleurs dans l'appli ; une exception typée permet au code appelant de réagir spécifiquement (ex. afficher "aucune donnée" vs planter).

## 2. Typer le résultat avec une `dataclass` plutôt qu'un `Dict[str, float]`
```python
@dataclass(frozen=True)
class Statistics:
    mean: float
    median: float
    std_dev: float
    min: float
    max: float
    count: int
```
**Justification** : le `Dict[str, Any]` en sortie de `calculate_statistics` et en entrée de `format_report` n'est pas auto-documenté — le pattern `stats.get('mean', 'N/A')` dans `format_report` masque en fait des bugs (si `calculate_statistics` est la seule source, la clé est *toujours* présente ; le fallback `'N/A'` cache un vrai problème au lieu de le signaler).

## 3. Séparer calcul et arrondi
Ne plus arrondir (`round(mean, 2)`) dans `calculate_statistics` ; arrondir uniquement à l'affichage dans `format_report`.
**Justification** : arrondir dans le calcul détruit la précision pour tout usage ultérieur (agrégation, tests, normalisation) — c'est un choix de présentation qui n'a rien à faire dans la couche calcul.

## 4. Extraire les sous-calculs en fonctions pures
`_mean(data)`, `_median(sorted_data)`, `_std_dev(data, mean)` séparées, appelées par `calculate_statistics`.
**Justification** : chacune devient testable isolément et réutilisable ; ajouter demain un percentile ou un mode ne demande pas de retoucher une fonction monolithique — c'est le point d'extension le plus direct.

## 5. Paramétrer l'écart-type (population vs échantillon)
Ajouter `ddof: int = 0` à `calculate_statistics` (0 = population actuelle, 1 = échantillon).
**Justification** : le choix `/n` (population) est actuellement implicite et non documenté ; le rendre explicite évite un bug silencieux si l'appli traite un jour un échantillon plutôt qu'une population complète.

## 6. Renforcer `validate_data` pour ne plus avaler les erreurs silencieusement
Retourner `(valides, rejetés)` ou logger chaque exclusion (`logging.debug`) au lieu du `except ... continue` muet.
**Justification** : perdre silencieusement des données invalides rend le débogage impossible en production — on ne sait jamais combien d'éléments ont été jetés ni pourquoi.

## 7. Valider les types d'entrée sur les fonctions publiques
Vérifier que `data` est bien une liste/itérable avant de traiter (`TypeError` explicite sinon), pour `calculate_statistics` et `normalize_data`.
**Justification** : aujourd'hui une entrée invalide (ex. `None`, générateur épuisé) produit une erreur Python bas niveau (`TypeError: object of type 'NoneType' has no len()`) peu lisible pour l'appelant.

## 8. Découpler `format_report` du format texte fixe
Extraire un renderer (`format_report_text`, `format_report_json`, …) derrière une interface commune, `format_report` devenant un simple dispatcher par paramètre `fmt`.
**Justification** : répond directement à l'objectif "extension future" — ajouter un export JSON/CSV pour une API ne doit pas dupliquer la logique d'agrégation des champs.

## 9. Ajouter des tests unitaires couvrant les cas limites
Liste vide, un seul élément, valeurs toutes identiques (`std_dev == 0`, `normalize_data` avec `range_val == 0`), données avec `NaN`/`Inf` à filtrer.
**Justification** : ce sont précisément les branches à risque déjà visibles dans le code (`range_val == 0`, division par `n`) ; sans tests, un refactoring futur les casse silencieusement.

## 10. Documenter les invariants dans les docstrings existantes
Compléter les docstrings avec les cas limites gérés (ex. "retourne `[0.5, 0.5, ...]` si toutes les valeurs sont égales") plutôt que de les laisser implicites dans le code.
**Justification** : les docstrings actuelles décrivent la signature mais pas le comportement aux limites — c'est justement là que les futurs contributeurs se trompent.

---
**Ordre d'exécution recommandé** : 1 → 2 → 3 (fondations sans changer le comportement observable) → 4 → 5 (extraction, testable dès cette étape) → 6 → 7 (durcissement) → 9 (verrouiller par tests avant d'aller plus loin) → 8 → 10.

5. Subagents Personnalises

Claude Code ne permet pas (encore) de définir des subagents externes via un fichier de configuration. Cependant, vous pouvez simuler des agents specialises en utilisant des system prompts spécifiques.

Approche par System Prompts

L’idee est de définir des “personas” avec des instructions spécifiques :

agents = {
    "security_reviewer": {
        "system_prompt": "Tu es un expert en securite. Analyse uniquement les vulnerabilites.",
        "model": "sonnet"  # Plus précis pour la securite
    },
    "quick_explainer": {
        "system_prompt": "Tu es un tuteur. Explique simplement en 3 phrases max.",
        "model": "haiku"   # Rapide pour les explications courtes
    }
}

Alternatives pour de vrais agents personnalises

Méthode Description
Skills (.claude/commands/) Commandes slash personnalisees
MCP Servers Outils externes via Model Context Protocol
Hooks Scripts executes avant/après certaines actions

Note : Les exemples ci-dessous montrent comment structurer des prompts specialises, pas de vrais subagents Claude.

Vous pouvez définir vos propres agents specialises :

# Definition d'agents personnalises
custom_agents = {
    "security_reviewer": {
        "description": "Expert en securite",
        "system_prompt": "Tu es un expert en securite applicative. Analyse le code pour identifier les vulnerabilites potentielles (injection, XSS, etc.).",
        "model": "sonnet"
    },
    "performance_analyst": {
        "description": "Expert en performance",
        "system_prompt": "Tu es un expert en optimisation de performance. Identifie les goulots d'etranglement et propose des ameliorations.",
        "model": "sonnet"
    },
    "documentation_writer": {
        "description": "Redacteur de documentation",
        "system_prompt": "Tu es un technical writer. Redige une documentation claire et complete.",
        "model": "haiku"
    }
}

print("Agents definis:")
for name, config in custom_agents.items():
    print(f"  - {name}: {config['description']}")
Agents definis:
  - security_reviewer: Expert en securite
  - performance_analyst: Expert en performance
  - documentation_writer: Redacteur de documentation

Lecture de la definition des agents

L’output "Agents definis:" suivi des trois lignes - security_reviewer: Expert en securite / - performance_analyst: Expert en performance / - documentation_writer: Redacteur de documentation confirme que trois personas distincts sont enregistres dans le dictionnaire custom_agents. Trois observations sur ce pattern :

  1. Le role est explicite : chaque entree porte une description en une ligne qui resume le domaine d’expertise. C’est le system_prompt qui porte la specialisation complete (plusieurs phrases instruisant le modele sur ce qu’il doit faire), mais la description sert d’etiquette humaine dans les logs et l’UI.
  2. Le modele n’est pas uniforme : deux agents utilisent sonnet (securite, performance – raisonnement technique exigeant) et un utilise haiku (redaction de documentation – tache plus directe ou un modele leger suffit, divisant cout et temps de reponse par un facteur important). C’est un arbitrage cout/qualite explicite : on ne reserve Sonnet qu’aux taches ou le raisonnement avance apporte une valeur mesurable.
  3. Le pattern est extensible : ajouter un agent = ajouter une entree au dictionnaire avec la meme triple {description, system_prompt, model}. L’exercice « Créer un agent d’audit de code », à la fin de la section 6, fait structurer un quatrième agent (code_auditor).

Cette cellule prepare la suite : les deux cellules « Simuler l’agent » qui suivent montrent comment invoquer un agent spécifique en passant son system_prompt a run_claude(...) – le mecanisme sous-jacent est le meme qu’un appel direct a la CLI, mais avec l’instruction systeme personnalisee.

Structure du dictionnaire d’agents

Le dictionnaire custom_agents suit un pattern reproductible : chaque entree contient un description (identifiant lisible), un system_prompt (les instructions specialisees) et un model (le modèle LLM cible). La sortie confirme que trois personas sont définis avec des rôles distincts :

  • security_reviewer (Sonnet) : analyse les vulnerabilites, necessite un raisonnement précis
  • performance_analyst (Sonnet) : identifie les goulots d’etranglement, necessite du raisonnement technique
  • documentation_writer (Haiku) : redige de la documentation, tâche plus directe, Haiku suffit

Le choix du modèle (Sonnet vs Haiku) n’est pas arbitraire : les tâches d’analyse critique beneficient du raisonnement avance de Sonnet, tandis que les tâches redactionnelles simples fonctionnent bien avec Haiku, reduisant le cout et le temps de reponse.

# Simuler l'agent security_reviewer
agent_config = custom_agents["security_reviewer"]

stdout, stderr, code = run_claude(
    f"""{agent_config['system_prompt']}

Analyse ce code:
{utils_content}

Identifie les problemes de securite potentiels.""",
    model=agent_config['model'],
    timeout=180
)
print("=== Agent: security_reviewer ===")
print_response(stdout, stderr, code)
=== Agent: security_reviewer ===
=== Reponse Claude ===
# Analyse de sécurité

Ce module est purement computationnel (stats + formatage) — **aucune vulnérabilité d'injection classique** (pas de SQL, pas de commande shell, pas d'`eval`/`exec`, pas de désérialisation, pas d'accès fichier/réseau).

## Points relevés

1. **`format_report` — XSS potentiel en aval (pas dans ce code)**
   Le rapport est construit par f-strings sans échappement. Si cette chaîne est un jour injectée dans du HTML (page web, email HTML, rapport PDF via template) sans échappement côté consommateur, un `stats` contenant des valeurs contrôlées par un utilisateur (peu probable ici vu les clés numériques, mais `stats` est typé `Dict[str, Any]`) pourrait produire du HTML/JS non désiré. Ce n'est pas exploitable *dans* ce module, mais à surveiller si le retour est rendu tel quel côté frontend.

2. **`validate_data` — gestion d'erreur correcte**
   `float(item)` est sûr (pas d'`eval`), et les exceptions `TypeError`/`ValueError` sont bien catchées. Pas de risque de déni de service via des valeurs `inf`/`nan` puisqu'elles sont explicitement filtrées.

3. **`calculate_statistics` — absence de validation de type**
   La fonction suppose `data: List[float]` mais ne le vérifie pas (pas d'appel à `validate_data` en interne). Une liste contenant des types non numériques lèvera une `TypeError` non gérée — c'est un problème de robustesse, pas une faille de sécurité (pas de fuite d'info, pas d'exécution de code).

4. **Pas de secrets, pas d'I/O externe** — surface d'attaque nulle pour ce fichier isolément.

## Conclusion

Aucune vulnérabilité exploitable dans ce module en l'état. Le seul point d'attention réel est architectural : si `format_report` alimente une sortie HTML ailleurs dans l'application, s'assurer que l'échappement est fait côté consommateur (ou l'ajouter ici avec `html.escape()` si la destination est connue et fixe).

Analyse specialisee : securite vs performance

Les deux cellules suivantes appliquent le même mécanisme – un system prompt specialise – mais avec des perspectives radicalement différentes sur le même code source. L’agent security_reviewer cherche des vulnerabilites (injection, fuites de données), tandis que l’agent performance_analyst cherche des goulots d’etranglement (complexite algorithmique, allocations inutiles). C’est l’equivalent d’une revue de code en équipe ou chaque reviewer apporte son expertise metier.

# Simuler l'agent performance_analyst
agent_config = custom_agents["performance_analyst"]

stdout, stderr, code = run_claude(
    f"""{agent_config['system_prompt']}

Analyse ce code:
{utils_content}

Identifie les problemes de performance et propose des optimisations.""",
    model=agent_config['model'],
    timeout=180
)
print("=== Agent: performance_analyst ===")
print_response(stdout, stderr, code)
=== Agent: performance_analyst ===
=== Reponse Claude ===
## Analyse des goulots d'étranglement

### 1. `calculate_statistics` — passes redondantes sur `data` (le plus impactant)

- `min(data)` et `max(data)` sont recalculés en O(n) chacun **alors que `sorted_data` existe déjà** → `sorted_data[0]` / `sorted_data[-1]` donnent le résultat en O(1).
- `mean` (via `sum(data)`) et `variance` (via une seconde génératrice sur `data`) font **deux passes séparées** alors qu'une seule suffit (somme + somme des carrés en une boucle).
- `sorted(data)` est O(n log n) et domine déjà la complexité — s'il ne sert qu'à la médiane, un `numpy.partition` (sélection, O(n) en moyenne) est nettement moins cher pour de gros volumes.

```python
def calculate_statistics(data: List[float]) -> Dict[str, float]:
    if not data:
        raise ValueError("La liste de donnees ne peut pas etre vide")

    n = len(data)
    sorted_data = sorted(data)

    total = 0.0
    total_sq = 0.0
    for x in data:
        total += x
        total_sq += x * x
    mean = total / n
    variance = total_sq / n - mean * mean  # une seule passe au lieu de deux

    mid = n // 2
    median = (sorted_data[mid - 1] + sorted_data[mid]) / 2 if n % 2 == 0 else sorted_data[mid]

    return {
        "mean": round(mean, 2),
        "median": round(median, 2),
        "std_dev": round(math.sqrt(max(variance, 0.0)), 2),  # max(...,0) contre l'instabilite numerique
        "min": sorted_data[0],   # au lieu d'un min(data) redondant
        "max": sorted_data[-1],  # au lieu d'un max(data) redondant
        "count": n
    }
```
⚠️ Trade-off : `total_sq/n - mean²` est numériquement moins stable que la formule à deux passes (annulation catastrophique si les valeurs sont grandes avec une faible variance). Acceptable ici vu le `round(.., 2)` final, mais à garder en tête si précision critique.

### 2. `normalize_data` — deux passes pour min/max
`min(data)` et `max(data)` sont deux itérations distinctes. Sur une petite liste ça ne change rien, mais sur un gros volume une seule boucle (ou numpy) est préférable :
```python
min_val, max_val = math.inf, -math.inf
for x in data:
    if x < min_val: min_val = x
    if x > max_val: max_val = x
```

### 3. `validate_data` — try/except par élément
Correct et idiomatique pour de petites listes (les exceptions non levées sont peu coûteuses en Python), mais devient un vrai goulot sur de gros volumes car chaque `float(item)` + vérif `isnan`/`isinf` est interprété élément par élément.

### 4. Le vrai levier si les volumes sont grands : **numpy**
Toutes ces fonctions sont des boucles Python pures. Pour n de l'ordre de 10⁴-10⁶+, le gain principal ne vient pas des micro-optimisations ci-dessus mais du passage à des opérations vectorisées C :
```python
import numpy as np

def calculate_statistics(data):
    arr = np.asarray(data, dtype=float)
    if arr.size == 0:
        raise ValueError("La liste de donnees ne peut pas etre vide")
    return {
        "mean": round(float(arr.mean()), 2),
        "median": round(float(np.median(arr)), 2),  # np.partition en interne, pas de tri complet
        "std_dev": round(float(arr.std()), 2),
        "min": float(arr.min()),
        "max": float(arr.max()),
        "count": int(arr.size),
    }

def validate_data(data):
    arr = pd.to_numeric(pd.Series(data), errors="coerce").to_numpy()
    return arr[np.isfinite(arr)].tolist()

def normalize_data(data):
    arr = np.asarray(data, dtype=float)
    if arr.size == 0:
        return []
    rng = arr.max() - arr.min()
    return (np.full(arr.size, 0.5) if rng == 0 else (arr - arr.min()) / rng).tolist()
```

### 5. `format_report` — aucun problème
Fonction déjà optimale (concaténation via `"\n".join`, pas de boucle sur de grosses données).

## Priorisation
1. **Numpy** si les listes peuvent être grandes (>~10k éléments) — gain d'ordres de grandeur, remplace toutes les boucles Python.
2. **Réutiliser `sorted_data[0]`/`[-1]`** au lieu de `min()`/`max()` redondants — gratuit, zéro risque.
3. **Fusionner mean/variance en une seule passe** — gain mineur mais sans coût, à condition d'accepter le compromis de stabilité numérique.
4. Le reste (`validate_data` par exception, `normalize_data` deux passes) n'est à traiter que si le profilage montre que ces fonctions sont réellement sur le chemin critique.
# Témoin K08 (#18737) : la suggestion `total_sq/n - mean**2` est numériquement instable
# Le code existant utils.py utilise la formule à deux passes (stable).
# L'agent performance_analyst suggère une passe unique.
# Ce témoin montre sur un cas réel que la suggestion produit un écart-type FAUX.

import math

temoin = [1e9, 1e9 + 1, 1e9 + 2]  # 3 valeurs proches autour de 1 milliard
n = len(temoin)

# Méthode A : deux passes (code existant dans utils.py)
mean_A = sum(temoin) / n
var_A = sum((x - mean_A) ** 2 for x in temoin) / n
std_A = math.sqrt(var_A)

# Méthode B : une passe (suggestion de l'agent performance_analyst)
total = 0.0
total_sq = 0.0
for x in temoin:
    total += x
    total_sq += x * x
mean_B = total / n
var_B = total_sq / n - mean_B * mean_B
std_B = math.sqrt(max(var_B, 0.0))  # max(..., 0) masque la perte d'information

# Méthode C : Welford (une passe stable, référence)
count = 0
avg = 0.0
m2 = 0.0
for x in temoin:
    count += 1
    delta = x - avg
    avg += delta / count
    m2 += delta * (x - avg)
std_C = math.sqrt(m2 / n)

print(f"témoin : {temoin}")
print(f"écarts attendus : 1 entre valeurs consécutives -> std théorique = {math.sqrt(2/3):.4f}")
print()
print(f"A (deux passes, code existant) : std = {std_A:.4f}  -> round2 = {round(std_A, 2)}")
print(f"B (une passe, suggestion agent) : std = {std_B:.4f}  -> round2 = {round(std_B, 2)}")
print(f"C (Welford, référence stable)    : std = {std_C:.4f}  -> round2 = {round(std_C, 2)}")
print()

# Test de sensibilité à la translation
# Si les données sont translatées (x + K), la variance ne change PAS.
# Méthode B échoue ce test quand K est grand : annulation catastrophique.

def sensibilite_translation(data, K):
    """Applique une translation x+K et compare les 3 méthodes."""
    shift = [x + K for x in data]
    n2 = len(shift)
    m = sum(shift) / n2
    vA = sum((x - m) ** 2 for x in shift) / n2
    t = sum(shift); tq = sum(x*x for x in shift)
    mB = t / n2
    vB = tq / n2 - mB * mB
    return math.sqrt(vA), math.sqrt(max(vB, 0.0))

print("Sensibilité à la translation (données + 10^9) :")
for K in [0, 10**6, 10**9, 10**12]:
    a, b = sensibilite_translation([1.0, 2.0, 3.0], K)
    flag = "  <-- B effondre" if abs(b - a) > 0.01 else ""
    print(f"  K=10^{int(math.log10(K)) if K>0 else 0:<2}  A={a:.4f}  B={b:.4f}{flag}")

# Conclusion du témoin
print()
print("VERDICT TÉMOIN : la suggestion de l'agent (méthode B) est RÉFUTÉE.")
print("Le round(.., 2) final ne répare pas l'annulation numérique.")
print("Le max(var, 0) masque la perte d'information au lieu de la signaler.")
témoin : [1000000000.0, 1000000001.0, 1000000002.0]
écarts attendus : 1 entre valeurs consécutives -> std théorique = 0.8165

A (deux passes, code existant) : std = 0.8165  -> round2 = 0.82
B (une passe, suggestion agent) : std = 0.0000  -> round2 = 0.0
C (Welford, référence stable)    : std = 0.8165  -> round2 = 0.82

Sensibilité à la translation (données + 10^9) :
  K=10^0   A=0.8165  B=0.8165
  K=10^6   A=0.8165  B=0.8165
  K=10^9   A=0.8165  B=0.0000  <-- B effondre
  K=10^12  A=0.8165  B=11585.2375  <-- B effondre

VERDICT TÉMOIN : la suggestion de l'agent (méthode B) est RÉFUTÉE.
Le round(.., 2) final ne répare pas l'annulation numérique.
Le max(var, 0) masque la perte d'information au lieu de la signaler.

Lecture critique : l’agent a tort sur la variance (témoin K08)

La suggestion de l’agent performance_analyst — remplacer la variance à deux passes par total_sq/n - mean² — est numériquement fausse et le témoin ci-dessus le démontre.

Sur le témoin [1e9, 1e9+1, 1e9+2] : - Méthode A (deux passes, code existant) : écart-type 0,82 — correct. - Méthode B (une passe, suggestion de l’agent) : écart-type 0,00 — faux. - Méthode C (Welford, référence) : écart-type 0,82 — correct.

Pourquoi B échoue : la formule E[x²] − E[x]² soustrait deux nombres très proches quand les valeurs sont grandes avec une faible variance. En arithmétique flottante, cette soustraction annule les chiffres significatifs (catastrophic cancellation). L’arrondi final à 2 décimales ne répare pas la perte : le résultat arrondi est 0.00 au lieu de 0.82. Le max(variance, 0.0) masque le symptôme (variance négative due aux erreurs d’arrondi) au lieu de signaler le problème.

Seuil d’instabilité, mesuré sur 4 translations : la méthode B est correcte tant que la translation reste petite (K=10⁰ et K=10⁶ donnent tous deux B=0.8165). À K=10⁹, B s’effondre à 0.0000 — la cancellation catastrophique fait passer E[x²] − E[x]² sous zéro, et le max(var, 0) masque le signe. À K=10¹², B explose à 11585.2375 — le résidu ne tient plus en double, et l’amplitude explose avant d’être masquée. Le titre « données + 10⁹ » du tableau reflète la valeur du témoin canonique (translation par 10⁹), pas une promesse d’une seule translation : le balayage va jusqu’à K=10¹² et c’est là que le signe de l’erreur change.

Leçon pour le vibe coding : un LLM peut formuler une optimisation algorithmique classique (une passe au lieu de deux) sans en mesurer la stabilité numérique sur les données cibles. C’est exactement le type d’erreur qu’un test sensibilité-translation détecte : la variance ne doit pas changer quand on ajoute une constante aux données, et la méthode B échoue ce test à partir de K=10⁹ (effondrement) et jusqu’à K=10¹² (explosion).

Bon réflexe : quand un agent propose une optimisation numérique, demander un témoin qui fait varier la magnitude des données sur plusieurs ordres de grandeur. Si la méthode proposée est instable, la variance calculée change avec la translation — c’est le signal d’alarme. La détection est d’autant plus robuste que le balayage inclut un K où la sous-estimation (variance négative masquée) ET un K où la sur-estimation (explosion) — les deux modes sont visibles dans la sortie de ce témoin.

Références : - Welford, B. P. (1962). « Note on a method for calculating corrected sums of squares and products ». Technometrics 4(3): 419–420. L’algorithme de la méthode C. - Goldberg, D. (1991). « What Every Computer Scientist Should Know About Floating-Point Arithmetic ». ACM Computing Surveys 23(1): 5–48. La référence sur l’arithmétique flottante. - Issue #18737 (fiche K08 de l’audit Astra du 2026-10-01, CONFIRMED pedagogy).

Comparaison des deux agents specialises

Observez la différence de perspective entre les deux analyses du même code :

Aspect security_reviewer performance_analyst
Focus Vulnerabilites, injections, fuites de données Complexite algorithmique, allocations memoire
Questions cle “Y a-t-il des entrees non validees ?” “Cette boucle est-elle necessaire ?”
Modèle Sonnet (precision critique) Sonnet (raisonnement technique)

Principe fondamental : Le même code, analyse par deux “personas” différents, produit des insights complementaires. C’est exactement ce que font les équipes de développement avec les revues de code specialisees (secu + perf + style). L’approche par system prompts permet de reproduire ce mécanisme avec un LLM.

6. Parallelisation (Concept)

Claude Code peut lancer jusqu’a 10 agents en parallele pour accelerer les tâches complexes.

En mode interactif, cela se fait automatiquement. En script, on peut simuler avec des threads :

Attention aux couts ! Chaque appel parallele consomme des tokens. Avec 10 agents en parallele, vous multipliez par 10 la consommation. Utilisez model="haiku" pour les tâches paralleles.

Bonnes pratiques pour la parallelisation

A faire A eviter
Tâches independantes (analyse de fichiers différents) Tâches dependantes (résultat A requis pour B)
Modèle Haiku pour chaque sous-tâche Modèle Opus pour tout
Timeout court (30s) par tâche Pas de timeout
Agregation des résultats a la fin Traitement séquentiel deguise
from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def run_agent_task(agent_name, prompt):
    """Execute une tache d'agent et retourne le resultat."""
    start = time.time()
    stdout, stderr, code = run_claude(prompt, model="haiku", timeout=30)
    duration = time.time() - start
    return {
        "agent": agent_name,
        "response": stdout[:500] + "..." if len(stdout) > 500 else stdout,
        "success": code == 0,
        "duration": round(duration, 2)
    }

# Definir les taches
tasks = [
    ("Analyse structure", "Resume la structure de ce code en 3 points"),
    ("Liste fonctions", "Liste toutes les fonctions avec leur signature"),
    ("Identifie types", "Identifie tous les types de donnees utilises")
]

print("Taches definies:")
for name, _ in tasks:
    print(f"  - {name}")
Taches definies:
  - Analyse structure
  - Liste fonctions
  - Identifie types

Exercice : Créer un agent d’audit de code

Les agents personnalises permettent de specialiser l’analyse selon des critères précis. Vous allez définir un nouvel agent dedie a l’audit de qualite du code.

Objectif : Completez la definition de l’agent code_auditor dans le dictionnaire custom_agents avec un system prompt qui analyse la qualite du code (lisibilite, respect des conventions PEP 8, complexite cyclomatique).

Indices : - # Étape 1 : Ajoutez une entree "code_auditor" au dictionnaire custom_agents défini plus haut - # Étape 2 : Redigez un system_prompt qui demande l’analyse de la lisibilite, des conventions PEP 8, et de la complexite - # Étape 3 : Choisissez le modèle adapte (Haiku pour une analyse rapide, Sonnet pour une analyse approfondie) - # Indice : Inspirez-vous des definitions de security_reviewer et performance_analyst déjà presentes

# Exercice : Agent d'audit de qualite du code
# Completez la definition de l'agent code_auditor

# TODO etudiant : ajoutez une entree "code_auditor" au dictionnaire custom_agents
# custom_agents["code_auditor"] = {
#     "description": "...",
#     "system_prompt": "...",
#     "model": "..."
# }

# TODO etudiant : simulez l'appel a cet agent avec le code utils_content
# agent_config = custom_agents["code_auditor"]
# stdout, stderr, code = run_claude(
#     f"{agent_config['system_prompt']}\n\nAnalyse ce code:\n{utils_content}",
#     model=agent_config['model']
# )
# print_response(stdout, stderr, code)

print("Exercice a completer")
Exercice a completer

Analyse du pattern de parallelisation

La cellule précédente définit trois tâches independantes qui pourraient s’executer en parallele. Le pattern ThreadPoolExecutor est la méthode standard en Python pour paralleliser des appels I/O (comme des requêtes API) :

  • Tâche 1 (“Analyse structure”) : Vision macro du code, equivalent a un Read rapide
  • Tâche 2 (“Liste fonctions”) : Extraction des signatures, equivalent a un Grep sur def
  • Tâche 3 (“Identifie types”) : Analyse des types de données, equivalent a un Grep sur les annotations

Chaque tâche cible un aspect différent du même code, et leurs résultats sont independants – ce qui rend la parallelisation pertinente. Si la tâche 2 avait besoin du résultat de la tâche 1, la parallelisation serait contre-productive.

# Executer en parallele (ATTENTION: consomme des tokens)
# Decommentez pour executer

# with ThreadPoolExecutor(max_workers=3) as executor:
#     futures = {
#         executor.submit(run_agent_task, name, f"{prompt}\n\nCode:\n{utils_content}"): name
#         for name, prompt in tasks
#     }
#     
#     for future in as_completed(futures):
#         result = future.result()
#         print(f"\n=== {result['agent']} ({result['duration']}s) ===")
#         print(result['response'])

print("(Execution parallele desactivee - decommentez pour tester)")
(Execution parallele desactivee - decommentez pour tester)

7. Exemple guidé et exercices

Exemple guidé 1 : Agent de generation de tests unitaires

Contribution étudiante de Nathan KLIEBER (@nathan138), PR #18565, intégrée comme exemple guidé.

Nous allons creer un agent test_generator specialise dans l’ecriture de tests pytest, puis l’appliquer a la fonction normalize_data de utils.py. Les tests couvrent trois categories : le cas nominal, les cas limites (liste vide, None, valeurs identiques) et les cas d’erreur.

Un test genere par un LLM n’a de valeur que s’il s’execute : la cellule qui suit l’appel a l’agent execute donc reellement les tests produits.

# EXEMPLE GUIDE 1 : agent "test_generator" (contribution etudiante)
# qui genere des tests unitaires pour une fonction donnee

test_generator = {
    "system_prompt": "Tu es un expert en tests unitaires Python avec pytest. "
                     "Pour chaque fonction, genere des tests couvrant: "
                     "cas nominal, cas limites (vide, None), et cas d'erreur. "
                     # Sans cette consigne, l'agent ecrit un import generique
                     # ("from your_module import ...") : les tests ne s'executent pas.
                     "La fonction est definie dans le module utils : importe-la avec "
                     "'from utils import normalize_data'. "
                     "Reponds avec un seul bloc de code python complet et executable.",
    "model": "sonnet"
}

# Fonction a tester (de utils.py)
function_to_test = """
def normalize_data(data):
    if not data:
        return []
    min_val = min(data)
    max_val = max(data)
    range_val = max_val - min_val
    if range_val == 0:
        return [0.5] * len(data)
    return [(x - min_val) / range_val for x in data]
"""

# Appel a l'agent : role (system prompt) + consigne + code a tester
stdout, stderr, code = run_claude(
    f"{test_generator['system_prompt']}\n\nGenere des tests pour:\n{function_to_test}",
    model=test_generator['model'],
    timeout=180,
)
print_response(stdout, stderr, code)

# On conserve la reponse pour la cellule de verification
reponse_tests = stdout if code == 0 else ""
=== Reponse Claude ===
```python
import pytest
from utils import normalize_data


class TestNormalizeDataNominal:
    def test_typical_list_of_positive_numbers(self):
        result = normalize_data([1, 2, 3, 4, 5])
        assert result == [0.0, 0.25, 0.5, 0.75, 1.0]

    def test_typical_list_with_negative_numbers(self):
        result = normalize_data([-10, 0, 10])
        assert result == pytest.approx([0.0, 0.5, 1.0])

    def test_floats(self):
        result = normalize_data([1.5, 2.5, 3.5])
        assert result == pytest.approx([0.0, 0.5, 1.0])

    def test_unordered_input_preserves_order(self):
        result = normalize_data([5, 1, 3])
        assert result == pytest.approx([1.0, 0.0, 0.5])

    def test_result_length_matches_input_length(self):
        data = [3, 7, 2, 9, 4]
        result = normalize_data(data)
        assert len(result) == len(data)

    def test_min_maps_to_zero_and_max_maps_to_one(self):
        data = [4, 8, 15, 16, 23, 42]
        result = normalize_data(data)
        assert result[data.index(min(data))] == pytest.approx(0.0)
        assert result[data.index(max(data))] == pytest.approx(1.0)


class TestNormalizeDataEdgeCases:
    def test_empty_list_returns_empty_list(self):
        assert normalize_data([]) == []

    def test_none_returns_empty_list(self):
        assert normalize_data(None) == []

    def test_single_element_list(self):
        assert normalize_data([42]) == [0.5]

    def test_all_identical_values(self):
        assert normalize_data([7, 7, 7, 7]) == [0.5, 0.5, 0.5, 0.5]

    def test_two_identical_values(self):
        assert normalize_data([3, 3]) == [0.5, 0.5]

    def test_empty_tuple_returns_empty_list(self):
        assert normalize_data(()) == []

    def test_zero_falsy_value_in_list_not_treated_as_empty(self):
        # data = [0] is truthy (non-empty list) even though it contains a falsy element
        result = normalize_data([0])
        assert result == [0.5]

    def test_large_range_of_values(self):
        result = normalize_data([0, 1_000_000])
        assert result == pytest.approx([0.0, 1.0])

    def test_small_float_range(self):
        result = normalize_data([0.0001, 0.0002])
        assert result == pytest.approx([0.0, 1.0])


class TestNormalizeDataErrorCases:
    def test_non_numeric_elements_raise_type_error(self):
        with pytest.raises(TypeError):
            normalize_data(["a", "b", "c"])

    def test_mixed_numeric_and_non_numeric_raises_type_error(self):
        with pytest.raises(TypeError):
            normalize_data([1, "two", 3])

    def test_dict_input_raises_error(self):
        # min()/max() on a dict operate on keys; mixing incompatible key types raises
        with pytest.raises(TypeError):
            normalize_data({"a": 1, "b": 2})

    def test_unorderable_mixed_types_raise_type_error(self):
        with pytest.raises(TypeError):
            normalize_data([1, "2", None])


if __name__ == "__main__":
    import sys
    sys.exit(pytest.main([__file__, "-v"]))
```
# Verification : execution reelle des tests generes par l'agent
import re, shutil, tempfile

blocs = re.findall(r"```python\n(.*?)```", reponse_tests, re.DOTALL)
if not blocs:
    print("Aucun bloc de code python dans la reponse de l'agent : rien a executer.")
else:
    # Dossier temporaire : utils.py + tests generes, sans modifier le projet
    with tempfile.TemporaryDirectory() as tmp:
        shutil.copy(os.path.join(EXAMPLES_DIR, "utils.py"), tmp)
        chemin = os.path.join(tmp, "test_normalize.py")
        with open(chemin, "w", encoding="utf-8") as f:
            f.write("\n\n".join(blocs))
        resultat = subprocess.run(
            [sys.executable, "-m", "pytest", chemin, "-q", "--color=no", "-p", "no:cacheprovider"],
            capture_output=True, text=True, cwd=tmp,
        )
    # Si pytest manque ou ne peut pas collecter les tests, stdout reste vide :
    # l'explication est alors dans stderr.
    print(resultat.stdout[-2500:] or resultat.stderr[-2500:])
    print(f"Code de retour de pytest : {resultat.returncode}")
...................                                                      [100%]
19 passed in 0.05s

Code de retour de pytest : 0

Lecture du resultat : faut-il croire les tests generes ?

La cellule precedente ne se contente pas d’afficher les tests : elle les execute avec pytest sur le vrai utils.py, dans un dossier temporaire. La ligne finale (N passed, ou N failed, M passed) est le seul verdict qui compte.

  • Import : sans la consigne from utils import normalize_data dans le system prompt, l’agent ecrivait from your_module import normalize_data et aucun test ne pouvait s’executer. Un prompt precis rend la sortie directement utilisable.
  • Piege None : normalize_data(None) ne leve pas d’erreur, elle renvoie [] (car not None vaut True). Un test qui attend une exception sur None serait faux.
  • Un test en echec n’accuse pas forcement la fonction : l’agent peut se tromper dans la valeur attendue. Avant de corriger le code, recalculer a la main la valeur du test fautif avec la formule (x - min) / (max - min).

À cette exécution : l’agent a produit 19 tests, rangés en trois classes (6 cas nominaux, 9 cas limites, 4 cas d’erreur), et pytest rend 19 passed. Le piège None est bien évité : le test test_none_returns_empty_list attend [], pas une exception. Parmi les cas limites, l’agent a pensé à test_zero_falsy_value_in_list_not_treated_as_empty, qui vérifie qu’une liste contenant 0 n’est pas confondue avec une liste vide. Ce détail vient de la condition if not data de la fonction.

Ce que « 19 passed » ne prouve pas : que ces tests détecteraient un bug. Des tests qui passent sur un code sain peuvent aussi passer sur un code faux. L’exercice 1 prend le problème par l’autre bout : on part d’une version cassée de la même fonction.

Si la sortie est vide : pytest n’est peut-être pas installé dans l’environnement du kernel (pip install pytest). La cellule affiche alors le message d’erreur de pytest et son code de retour, au lieu de rester muette.

Moralite : un LLM produit des tests plausibles, mais seule leur execution dit s’ils sont justes.

Exercice 1 : Agent de réparation d’un bug

L’exemple guidé ci-dessus générait des tests pour un code sain. Le problème inverse est tout aussi utile : un agent qui répare un code défaillant. La fonction normalize_data ci-dessous a été cassée volontairement : elle divise chaque valeur par max_val au lieu de ramener l’intervalle [min_val, max_val] sur [0, 1], si bien que les valeurs négatives sortent de l’intervalle. Un test qui échoue sur cette version est fourni.

Objectif : créez un agent bug_fixer qui reçoit la fonction cassée et le test en échec, et renvoie la fonction corrigée.

Indices : - # Étape 1 : rédigez un system_prompt qui demande la fonction corrigée seule, dans un unique bloc de code python complet et exécutable (pas de texte autour) - # Étape 2 : appelez run_claude avec le code cassé et le test en échec, en conservant la réponse dans une variable pour la vérification - # Indice : inspirez-vous de l’exemple guidé — le bloc de vérification commenté en fin de cellule rejoue le test fourni contre la réponse de l’agent, dans un dossier temporaire

# EXERCICE 1 : Creez un agent "bug_fixer" qui repare la fonction cassee
# La verification (commentee) rejoue le test fourni contre la reponse de l'agent

# Fonction cassee volontairement (elle divise par max_val sans soustraire min_val)
broken_normalize_data = '''
def normalize_data(data):
    if not data:
        return []
    min_val = min(data)
    max_val = max(data)
    range_val = max_val - min_val
    return [x / max_val for x in data]
'''

# Test qui echoue sur la version cassee ([-10, 0, 10] doit donner [0.0, 0.5, 1.0])
failing_test = '''
from utils import normalize_data

def test_normalize_negative_values():
    assert normalize_data([-10, 0, 10]) == [0.0, 0.5, 1.0]
'''

# TODO etudiant : definissez l'agent bug_fixer (system_prompt + model)
# bug_fixer = {
#     "system_prompt": "...",
#     "model": "sonnet"
# }

# TODO etudiant : appelez run_claude avec le code casse ET le test en echec
# stdout, stderr, code = run_claude(
#     f"{bug_fixer['system_prompt']}\n\nCorrige cette fonction pour que le test passe:\n"
#     f"{broken_normalize_data}\n\nTest en echec:\n{failing_test}",
#     model=bug_fixer['model'],
#     timeout=180,
# )
# print_response(stdout, stderr, code)
# reponse_fix = stdout if code == 0 else ""

# TODO etudiant : verifiez que la fonction corrigee fait passer le test
# (meme schema que l'exemple guide : dossier temporaire + pytest)
# import re, tempfile
# blocs = re.findall(r"```python\n(.*?)```", reponse_fix, re.DOTALL)
# with tempfile.TemporaryDirectory() as tmp:
#     with open(os.path.join(tmp, "utils.py"), "w", encoding="utf-8") as f:
#         f.write("\n\n".join(blocs))
#     with open(os.path.join(tmp, "test_fix.py"), "w", encoding="utf-8") as f:
#         f.write(failing_test)
#     resultat = subprocess.run(
#         [sys.executable, "-m", "pytest", os.path.join(tmp, "test_fix.py"),
#          "-q", "--color=no", "-p", "no:cacheprovider"],
#         capture_output=True, text=True, cwd=tmp,
#     )
#     print(resultat.stdout[-2500:])

print("Exercice a completer")
Exercice a completer

Exercice 2 : Planification avec l’agent Plan

Dans le second exercice, vous utilisez l’agent Plan pour structurer l’ajout d’une fonctionnalite complete. Contrairement a l’exercice 1 ou vous definissiez un agent personnalise, ici vous devez rediger le prompt de planification qui guidera l’agent vers un plan d’implementation coherent.

# EXERCICE 2 : Utilisez l'agent Plan pour planifier
# l'ajout d'une fonctionnalite de votre choix

# Choisissez UNE fonctionnalite parmi:
# - Ajout d'une CLI avec argparse
# - Export des resultats en JSON/CSV
# - Ajout de logging avec le module logging

feature = "Export des resultats en CSV"  # Changez selon votre choix

# TODO: Completez le prompt de planification
# plan_prompt = f"""
# Je veux ajouter cette fonctionnalite au projet sample_project: {feature}
# 
# Contexte du projet:
# - main.py: point d'entree avec fonction main()
# - utils.py: fonctions calculate_statistics(), format_report(), validate_data()
# - tests/test_utils.py: tests existants
#
# Cree un plan d'implementation avec:
# 1. Fichiers a creer/modifier
# 2. Ordre des etapes
# 3. Risques potentiels
# """

# stdout, stderr, code = run_claude(plan_prompt)
# print_response(stdout, stderr, code)

print("Decommentez le code pour executer l'exercice")
Decommentez le code pour executer l'exercice

8. Resume

Dans ce notebook, nous avons appris :

Concept Description Modèle
Agent Explore Recherche rapide en lecture seule Haiku
Agent Plan Planification et decomposition de tâches Sonnet
Subagents personnalises System prompts specialises Variable
Parallelisation Jusqu’a 10 agents simultanement Haiku recommande

Points cles a retenir

  1. Les agents sont automatiques en mode interactif - vous n’avez pas a les appeler explicitement
  2. Explore = lecture seule - ne peut jamais modifier de fichiers
  3. Plan avant exécution - pour les tâches complexes, demandez d’abord un plan
  4. Parallelisation = cout x N - utilisez Haiku pour les sous-tâches

Auto-evaluation

Pouvez-vous repondre a ces questions ?

Prochaine étape

Dans le notebook final, nous verrons l’automatisation avancee : pipelines, hooks, et integration CI/CD.

-> 05-Claude-CLI-Automatisation.ipynb

Retour au sommet