Lab 11: Planner-Coder-Verifier Loop (DS-STAR Core)

Navigation : Lab 10 << | Index | >> Lab 12

Objectifs d’apprentissage

À la fin de ce laboratoire, vous saurez : 1. Implémenter la boucle itérative Planner-Coder-Verifier de DS-STAR 2. Créer un système multi-agents pour l’analyse de données 3. Gérer les échecs et raffinements automatiques 4. Orchestrer plusieurs composants LLM en pipeline

Prérequis

  • Lab 10 (File Analyzer) complété
  • Connaissance des patterns d’agents
  • Configuration multi-provider active

Durée estimée : 45-60 minutes

1. Architecture Planner-Coder-Verifier

Repères bibliographiques. Cette boucle itérative Planner → Coder → Executor → Verifier est un cas particulier du paradigme ReAct (Reasoning + Acting) de S. Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, arXiv:2210.03629, ICLR 2023, où l’agent alterne raisonnement et actions observables. L’étape de Verifier qui renvoie vers le Coder en cas d’échec (raffinement automatique) suit le principe d’auto-réflexion formalisé par N. Shinn et al., Reflexion: Language Agents with Verbal Reinforcement Learning, arXiv:2303.11366, NeurIPS 2023. Le découpage en rôles spécialisés (Planner / Coder / Verifier) est l’architecture multi-agent déterministe retenue par DS-STAR (Nam et al., arXiv:2509.21825, 2025).

Question --> [PLANNER] --> Plan
                      |
                      v
                [CODER] --> Code
                      |
                      v
                [EXECUTOR] --> Result
                      |
                      v
                [VERIFIER] --> Success/Retry

Schema : la boucle Planner-Coder-Verifier

Chaque étape transforme l’entree de la précédente ; le Verifier conclut sur un succes ou declenche un nouvel essai (retry).

flowchart TD
    Q["Question"] --> P["PLANNER (genere le Plan)"]
    P --> C["CODER (genere le Code)"]
    C --> E["EXECUTOR (produit le Result)"]
    E --> V["VERIFIER"]
    V --> SR["Success / Retry"]

2. Configuration

Même socle multi-provider qu’au Lab 10 : get_settings() expose un fournisseur actif unique, et les quatre agents (Planner, Coder, Executor, Verifier) reçoivent un LLMClient qui sait parler à n’importe quel backend compatible. L’orchestrateur DSStarAgent instancie ces quatre composants — changer de modèle est un changement de configuration, pas de code.

import sys
import warnings
from pathlib import Path

sys.path.insert(0, str(Path().resolve().parent))

import json
import re
import pandas as pd
import numpy as np
from typing import Optional, Dict, List, Tuple
from dataclasses import dataclass
from enum import Enum

from config import get_settings
from utils import LLMClient
from utils.adk_runtime import build_agent, run_agent_turn

print("Imports OK : pandas, numpy, LLMClient et runtime Google ADK réel")
Imports OK : pandas, numpy, LLMClient et runtime Google ADK réel
settings = get_settings()
print(f'Provider: {settings.active_provider}')
Provider: openai

Lecture. L’output précédent donne le provider réellement actif pour cette exécution. Les étapes LLM historiques de la première boucle passent par LLMClient; la section Google ADK plus bas réutilise la même configuration pour construire de vrais Agent, sessions et Runner. Un provider unique simplifie le laboratoire, même si une architecture de production pourrait affecter un modèle différent à chaque rôle.

3. Data Classes

La boucle fait circuler des données structurées entre ses étapes, pas du texte libre. Trois dataclasses matérialisent ce contrat : Plan (les étapes + le raisonnement qui les justifie), ExecutionResult (succès/échec + stdout + erreur éventuelle), et VerificationStatus (un enum à trois valeurs : SUCCESS, NEEDS_REFINEMENT, FAILED). C’est ce typage qui rend le raffinement automatique possible — le Verifier ne renvoie pas une chaîne ambiguë, mais un verdict actionnable.

@dataclass
class Plan:
    steps: List[str]
    reasoning: str

@dataclass
class ExecutionResult:
    success: bool
    output: str
    error: Optional[str] = None
    code: Optional[str] = None

class VerificationStatus(Enum):
    SUCCESS = 'success'
    NEEDS_REFINEMENT = 'needs_refinement'
    FAILED = 'failed'

print("Dataclasses definies : Plan, ExecutionResult, VerificationStatus (SUCCESS, NEEDS_REFINEMENT, FAILED)")
Dataclasses definies : Plan, ExecutionResult, VerificationStatus (SUCCESS, NEEDS_REFINEMENT, FAILED)

Lecture. Trois structures définies : Plan porte la décomposition en étapes et le raisonnement du Planner ; ExecutionResult capture le stdout, l’erreur et le succès de l’exécution ; VerificationStatus est l’enum qui pilote la boucle. Le point décisif est NEEDS_REFINEMENT : c’est ce verdict qui renvoie le Coder au travail avec le contexte de l’échec, au lieu d’un simple échec terminal. Sans cette troisième valeur, la boucle ne pourrait pas corriger — elle ne ferait que constater.

4. Planner Agent

Première étape de la boucle : le Planner reçoit la question et les métadonnées du dataset (le FileMetadata du Lab 10), et produit un Plan — une liste ordonnée d’étapes d’analyse, accompagnée du raisonnement qui les justifie. C’est l’étape où le LLM « pense » à haute voix avant de coder : décomposer le problème avant de le résoudre.

class Planner:
    def __init__(self, llm: LLMClient):
        self.llm = llm

    def create_plan(self, question: str, context: str) -> Plan:
        prompt = f"""Tu es un planificateur d'analyse de donnees.

CONTEXTE DES FICHIERS:
{context}

QUESTION: {question}

Genere un plan d'analyse en 3-5 etapes. Pour chaque etape, decris:
1. L'objectif
2. La methode
3. Le resultat attendu

Format de sortie:
REASONING: [ton raisonnement]
STEPS:
1. [etape 1]
2. [etape 2]
...

Plan:"""

        response = self.llm.generate(prompt, temperature=0.3)

        # Parse response
        steps = []
        reasoning = ""
        lines = response.split('\n')
        in_steps = False

        for line in lines:
            if line.startswith('REASONING:'):
                reasoning = line.replace('REASONING:', '').strip()
            elif line.startswith('STEPS:'):
                in_steps = True
            elif in_steps and re.match(r'^\d+\.', line.strip()):
                steps.append(re.sub(r'^\d+\.\s*', '', line.strip()))

        return Plan(steps=steps, reasoning=reasoning)

print("Classe Planner definie : decomposition de questions en plans d'analyse (3-5 etapes)")
Classe Planner definie : decomposition de questions en plans d'analyse (3-5 etapes)

Lecture. Le Planner est un wrapper autour du LLM qui force la sortie en un Plan structuré : il extrait les étapes via un parseur (expressions régulières sur un format attendu). C’est précisément ce parseur qui est fragile — l’Exercice 2 du lab vous demande d’ailleurs d’améliorer sa robustesse, car un Planner qui extrait 0 étapes fait échouer toute la boucle en aval.

5. Coder Agent

Deuxième étape : le Coder reçoit le Plan et génère du code Python qui l’exécute. Il produit une chaîne de code (pas un fichier), destinée à être passée à l’Executor. C’est l’étape la plus exposée aux échecs : un LLM peut générer du code qui appelle des colonnes inexistantes, oublie un import, ou se trompe de nom de variable — d’où la nécessité du Verifier en aval.

class Coder:
    def __init__(self, llm: LLMClient):
        self.llm = llm

    def generate_code(self, plan: Plan, context: str) -> str:
        steps_text = '\n'.join(f"{i+1}. {s}" for i, s in enumerate(plan.steps))

        prompt = f"""Tu es un programmeur Python expert. Genere du code pour executer ce plan.

CONTEXTE:
{context}

PLAN:
{steps_text}

Genere UNIQUEMENT du code Python executable entre balises ```python ... ```
Le DataFrame est disponible dans la variable 'df'.
Utilise print() pour afficher les resultats.

Code:"""

        response = self.llm.generate(prompt, temperature=0.2)

        # Extract code
        match = re.search(r'```python\s*(.*?)\s*```', response, re.DOTALL)
        if match:
            return match.group(1).strip()
        return response

print("Classe Coder definie : generation de code Python a partir d'un plan d'analyse")
Classe Coder definie : generation de code Python a partir d'un plan d'analyse

Lecture. Le Coder transforme un Plan abstrait en code concret exécutable. Sa fragilité est intrinsèque : il infère les noms de colonnes et la API Pandas depuis le contexte, et se trompe parfois (le test plus bas le montrera avec un ['revenu'] au lieu de 'revenue'). C’est exactement la défaillance que la boucle Planner-Coder-Verifier est conçue pour rattraper — à condition que max_iterations laisse assez de tentatives.

6. Executor

Troisième étape : l’Executor prend le code généré et l’exécute dans un namespace contrôlé (df, pd, np exposés), en capturant stdout et exceptions. Il ne raisonne pas — il exécute et rapporte. La capture de l’erreur (traceback Python) est ce qui permettra au Verifier de diagnostiquer, puis au Coder de corriger.

class Executor:
    def __init__(self, df: pd.DataFrame):
        self.df = df
        self.namespace = {'df': df, 'pd': pd, 'np': np, 'print': print}

    def execute(self, code: str) -> ExecutionResult:
        import sys
        from io import StringIO

        old_stdout = sys.stdout
        sys.stdout = StringIO()

        try:
            exec(code, self.namespace)
            output = sys.stdout.getvalue()
            return ExecutionResult(success=True, output=output, code=code)
        except Exception as e:
            output = sys.stdout.getvalue()
            return ExecutionResult(success=False, output=output, error=str(e), code=code)
        finally:
            sys.stdout = old_stdout

print("Classe Executor definie : execution securisee de code Python avec capture stdout")
Classe Executor definie : execution securisee de code Python avec capture stdout

Lecture. L’Executor sandboxe l’exécution : un namespace restreint avec df/pd/np, et la capture systématique du stdout et des exceptions. Le point critique est qu’il ne valide rien — un code qui plante renvoie une ExecutionResult avec success=False et le message d’erreur. Ce message remonte au Verifier, qui décide s’il mérite un retry (erreur corrigeable) ou un échec définitif.

7. Verifier Agent

Quatrième étape : le Verifier examine le résultat de l’exécution à la lumière de la question initiale. Trois verdicts possibles : SUCCESS (la question est répondue), NEEDS_REFINEMENT (le résultat est partiel ou faux, mais l’erreur est corrigeable — on relance le Coder avec le contexte de l’échec), FAILED (erreur non récupérable). C’est la décision qui pilote la boucle.

class Verifier:
    def __init__(self, llm: LLMClient):
        self.llm = llm

    def verify(self, question: str, result: ExecutionResult) -> Tuple[VerificationStatus, str]:
        if not result.success:
            return VerificationStatus.FAILED, f"Erreur d'execution: {result.error}"

        prompt = f"""Verifie si ce resultat repond a la question.

QUESTION: {question}

RESULTAT:
{result.output[:1000]}

Reponds par:
- SUCCESS si le resultat est complet et correct
- NEEDS_REFINEMENT si le resultat est partiel mais prometteur
- FAILED si le resultat ne repond pas du tout

Verdict:"""

        response = self.llm.generate(prompt, temperature=0.1).upper()

        if 'SUCCESS' in response:
            return VerificationStatus.SUCCESS, "Resultat valide"
        elif 'REFINEMENT' in response:
            return VerificationStatus.NEEDS_REFINEMENT, "Necessite des ajustements"
        return VerificationStatus.FAILED, "Resultat insuffisant"

print("Classe Verifier definie : validation des resultats (SUCCESS / NEEDS_REFINEMENT / FAILED)")
Classe Verifier definie : validation des resultats (SUCCESS / NEEDS_REFINEMENT / FAILED)

Lecture. Le Verifier est le juge de la boucle : il compare le résultat produit à la question posée, et émet un VerificationStatus. La distinction NEEDS_REFINEMENT vs FAILED est subtile et importante — une KeyError sur un nom de colonne est raffinable (le Coder peut corriger), une erreur de logique métier peut ne pas l’être. C’est ce verdict qui détermine si l’itération suivante a lieu.

8. DS-STAR Orchestrator

L’orchestrateur assemble les quatre composants en une boucle : Plan → Code → Execute → Verify, répétée jusqu’à SUCCESS ou max_iterations. À chaque itération, le contexte s’enrichit — le Coder reçoit le code précédent et l’erreur observée, de sorte que la tentative suivante est informée de l’échec précédent. C’est ce mécanisme qui distingue DS-STAR d’un simple pipeline linéaire.

class DSStarAgent:
    def __init__(self, df: pd.DataFrame, max_iterations: int = 3):
        self.llm = LLMClient()
        self.planner = Planner(self.llm)
        self.coder = Coder(self.llm)
        self.executor = Executor(df)
        self.verifier = Verifier(self.llm)
        self.max_iterations = max_iterations

    def analyze(self, question: str, file_context: str = None) -> Dict:
        context = file_context or "DataFrame 'df' disponible"

        for iteration in range(self.max_iterations):
            print(f"\n=== ITERATION {iteration + 1}/{self.max_iterations} ===")

            # Step 1: Plan
            print("[PLANNER] Creation du plan...")
            plan = self.planner.create_plan(question, context)
            print(f"Etapes: {len(plan.steps)}")

            # Step 2: Generate code
            print("[CODER] Generation du code...")
            code = self.coder.generate_code(plan, context)

            # Step 3: Execute
            print("[EXECUTOR] Execution...")
            result = self.executor.execute(code)

            if not result.success:
                print(f"[ERROR] {result.error}")
                context += f"\nErreur precedente: {result.error}\nCorrige le code."
                continue

            print(f"[OUTPUT] {result.output[:200]}...")

            # Step 4: Verify
            print("[VERIFIER] Verification...")
            status, message = self.verifier.verify(question, result)
            print(f"[STATUS] {status.value}: {message}")

            if status == VerificationStatus.SUCCESS:
                return {
                    'success': True,
                    'output': result.output,
                    'code': code,
                    'iterations': iteration + 1
                }

            # Raffinement
            context += f"\nResultat partiel: {result.output[:500]}\nAmeliore l'analyse."

        return {
            'success': False,
            'output': result.output if 'result' in dir() else '',
            'error': 'Max iterations atteint',
            'iterations': self.max_iterations
        }

print("Classe DSStarAgent definie : orchestrateur Planner-Coder-Executor-Verifier avec boucle iterative")
Classe DSStarAgent definie : orchestrateur Planner-Coder-Executor-Verifier avec boucle iterative

Lecture. DSStarAgent est la colle : il instancie les quatre agents et pilote la boucle. Le paramètre max_iterations borne le nombre de tentatives — un compromis entre coût (chaque itération appelle le LLM plusieurs fois) et qualité (plus d’itérations = plus de chances de rattraper un échec). À max_iterations=1, aucune retry n’est possible : un seul échec du Coder condamne la question, comme le montrera le test.

9. Test avec un Dataset

Dataset synthétique : 200 lignes de ventes (date, produit, région, revenu, unités). La question posée à l’agent (« Quelle est la région la plus rentable ? ») demande une agrégation par région — une tâche typique que DS-STAR doit pouvoir résoudre en une boucle Plan → Code → Execute → Verify.

# Dataset de test reproductible
rng = np.random.default_rng(42)

df = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=200, freq='D'),
    'product': rng.choice(['A', 'B', 'C', 'D'], 200),
    'region': rng.choice(['Nord', 'Sud', 'Est', 'Ouest'], 200),
    'revenue': rng.uniform(100, 5000, 200).round(2),
    'units': rng.integers(1, 100, 200)
})
print(f'Dataset reproductible : {len(df)} lignes, {len(df.columns)} colonnes')
df.head()
Dataset reproductible : 200 lignes, 5 colonnes
date product region revenue units
0 2024-01-01 A Sud 3909.28 2
1 2024-01-02 D Ouest 4861.95 30
2 2024-01-03 C Sud 2553.63 52
3 2024-01-04 B Est 805.10 91
4 2024-01-05 B Sud 168.29 74

La cellule suivante exécute la boucle historique LLMClient avec une seule itération. Sa trace permet d’observer le plan produit, le code exécuté et le verdict réellement obtenu, sans présupposer un échec particulier du modèle.

# Test de l'agent DS-STAR avec 1 iteration pour rapidite
agent = DSStarAgent(df, max_iterations=1)

question = "Quelle est la region avec le plus grand revenu moyen?"
result = agent.analyze(question)

print("\n" + "="*50)
print("RESULTAT FINAL:")
print("="*50)
if result['success']:
    print(result['output'])
else:
    print(f"Erreur: {result.get('error', 'Inconnu')}")

=== ITERATION 1/1 ===
[PLANNER] Creation du plan...
Etapes: 5
[CODER] Generation du code...
[EXECUTOR] Execution...
[ERROR] ['revenu']

==================================================
RESULTAT FINAL:
==================================================
Erreur: Max iterations atteint

Lecture du résultat historique. La trace précédente montre le comportement réellement obtenu par LLMClient sur ce dataset seedé : elle indique le nombre d’étapes, l’output de l’Executor et le verdict du Verifier. Cette observation sert de point de comparaison à la jambe ADK, sans généraliser un succès ou un échec stochastique à toutes les exécutions.

10. La même boucle sur le vrai runtime Google ADK

La première implémentation rend les contrats Planner-Coder-Executor-Verifier explicites, mais ses rôles LLM sont des wrappers maison autour de LLMClient. Cette seconde jambe conserve exactement la boucle pédagogique tout en remplaçant chaque rôle LLM par un vrai google.adk.agents.Agent exécuté dans une session par un Runner.

L’Executor reste volontairement local et déterministe : ADK orchestre le raisonnement et les événements, tandis que le code Pandas généré reste visible, exécutable et inspectable. Le Planner reçoit un outil Python réel pour lire le schéma au lieu de l’inventer.

def get_dataset_schema() -> Dict:
    """Retourne au Planner ADK le schéma réellement disponible."""
    return {
        "rows": int(len(df)),
        "columns": {name: str(dtype) for name, dtype in df.dtypes.items()},
    }

print(
    "Outil ADK prêt : get_dataset_schema expose "
    f"{len(df)} lignes et {len(df.columns)} colonnes"
)
Outil ADK prêt : get_dataset_schema expose 200 lignes et 5 colonnes

Le schéma est désormais obtenu par un outil déterministe. La cellule suivante construit les trois rôles ADK et garde explicitement la boucle d’orchestration dans le notebook.

async def adk_analyze(question: str, max_iterations: int = 2) -> Dict:
    """Exécute la boucle visible Planner → Coder → Executor → Verifier."""
    schema = get_dataset_schema()
    context = (
        "DataFrame df disponible. Schéma exact: "
        f"{json.dumps(schema, ensure_ascii=False)}. "
        "Le Planner doit appeler get_dataset_schema."
    )
    executor = Executor(df)
    evidence = {}
    last_result = ExecutionResult(False, "", "Aucune exécution")
    last_verdict = "FAILED"

    planner = build_agent(
        "lab11_planner",
        "Planifie une analyse de données à partir du schéma réel.",
        (
            "Appelle obligatoirement get_dataset_schema. Réponds ensuite avec "
            "REASONING: sur une ligne, puis STEPS: et 3 étapes numérotées."
        ),
        tools=(get_dataset_schema,),
    )
    coder = build_agent(
        "lab11_coder",
        "Transforme un plan d'analyse en code Pandas exécutable.",
        (
            "Réponds uniquement avec un bloc ```python```. Le DataFrame s'appelle "
            "df. Respecte exactement le schéma fourni et la métrique demandée: "
            "revenue désigne le revenu, units le nombre d'unités. Utilise print()."
        ),
    )
    verifier = build_agent(
        "lab11_verifier",
        "Vérifie qu'une sortie répond à la question initiale.",
        (
            "Commence impérativement par SUCCESS, NEEDS_REFINEMENT ou FAILED, "
            "puis justifie en une phrase. Vérifie le nom exact de la métrique."
        ),
    )

    for iteration in range(max_iterations):
        planner_session = f"lab11-planner-{iteration}"
        plan_turn = await run_agent_turn(
            planner,
            f"QUESTION: {question}\nCONTEXTE: {context}",
            session_id=planner_session,
        )
        evidence[f"planner-{iteration}"] = {
            "session_id": planner_session,
            "events": plan_turn.event_count,
            "tool_calls": ", ".join(plan_turn.tool_calls),
            "tool_responses": ", ".join(plan_turn.tool_responses),
        }

        coder_session = f"lab11-coder-{iteration}"
        code_turn = await run_agent_turn(
            coder,
            f"QUESTION: {question}\nPLAN:\n{plan_turn.response_text}\nCONTEXTE: {context}",
            session_id=coder_session,
        )
        evidence[f"coder-{iteration}"] = {
            "session_id": coder_session,
            "events": code_turn.event_count,
            "tool_calls": ", ".join(code_turn.tool_calls),
            "tool_responses": ", ".join(code_turn.tool_responses),
        }
        match = re.search(
            r"```(?:python)?\s*(.*?)\s*```",
            code_turn.response_text,
            re.DOTALL,
        )
        code = match.group(1).strip() if match else code_turn.response_text.strip()
        last_result = executor.execute(code)

        verifier_session = f"lab11-verifier-{iteration}"
        verify_turn = await run_agent_turn(
            verifier,
            (
                f"QUESTION: {question}\nSCHÉMA: {json.dumps(schema, ensure_ascii=False)}\n"
                f"SUCCÈS EXÉCUTION: {last_result.success}\n"
                f"ERREUR: {last_result.error}\nSORTIE:\n{last_result.output[:1200]}"
            ),
            session_id=verifier_session,
        )
        evidence[f"verifier-{iteration}"] = {
            "session_id": verifier_session,
            "events": verify_turn.event_count,
            "tool_calls": ", ".join(verify_turn.tool_calls),
            "tool_responses": ", ".join(verify_turn.tool_responses),
        }
        last_verdict = verify_turn.response_text.strip()
        verdict_match = re.match(
            r"^(SUCCESS|NEEDS_REFINEMENT|FAILED)\b",
            last_verdict.upper(),
        )
        verdict_token = verdict_match.group(1) if verdict_match else "FAILED"
        if last_result.success and verdict_token == "SUCCESS":
            return {
                "success": True,
                "output": last_result.output,
                "code": code,
                "iterations": iteration + 1,
                "verdict": last_verdict,
                "planner_tool_round_trip": plan_turn.tool_was_invoked,
                "evidence": evidence,
            }

        context += (
            f"\nTentative {iteration + 1}: erreur={last_result.error}; "
            f"verdict={last_verdict}; corrige le code."
        )

    return {
        "success": False,
        "output": last_result.output,
        "error": last_result.error or "Max iterations atteint",
        "iterations": max_iterations,
        "verdict": last_verdict,
        "planner_tool_round_trip": any(
            item["tool_calls"] and item["tool_responses"]
            for role, item in evidence.items()
            if role.startswith("planner-")
        ),
        "evidence": evidence,
    }

print("Boucle ADK définie : Planner, Coder et Verifier utilisent de vrais Agent/Runner")
Boucle ADK définie : Planner, Coder et Verifier utilisent de vrais Agent/Runner

La fonction est définie sans encore appeler le provider. L’exécution suivante lance réellement les trois rôles et imprime les preuves issues de leurs flux d’événements.

# Exécution réelle : trois Agent ADK, trois sessions, trois Runner et un outil
with warnings.catch_warnings():
    warnings.filterwarnings(
        "ignore",
        message=(
            r"\[EXPERIMENTAL\] feature "
            r"FeatureName\.JSON_SCHEMA_FOR_FUNC_DECL is enabled\."
        ),
        category=UserWarning,
        module=r"google\.adk\.models\.llm_request",
    )
    adk_result = await adk_analyze(question)

expected_region = df.groupby("region")["revenue"].mean().idxmax()
assert adk_result["planner_tool_round_trip"], "Le Planner ADK n'a pas appelé l'outil"
assert adk_result["success"], f"La boucle ADK a échoué: {adk_result['verdict']}"
assert expected_region in adk_result["output"], (
    "La sortie LLM/Executor ne nomme pas la région calculée avec revenue"
)

print("\n" + "=" * 50)
print("PREUVE GOOGLE ADK RÉELLE")
print("=" * 50)
for role, evidence in adk_result["evidence"].items():
    print(
        f"{role}: session={evidence['session_id']}, "
        f"events={evidence['events']}, "
        f"tool_calls={evidence['tool_calls'] or 'aucun'}, "
        f"tool_responses={evidence['tool_responses'] or 'aucune'}"
    )
print(f"Planner tool round-trip: {adk_result['planner_tool_round_trip']}")
print(f"Succès: {adk_result['success']}")
print(f"Itérations: {adk_result['iterations']}")
print(f"Région attendue (calcul déterministe): {expected_region}")
print(f"Verdict LLM: {adk_result['verdict']}")
print(f"Sortie Executor:\n{adk_result['output']}")

==================================================
PREUVE GOOGLE ADK RÉELLE
==================================================
planner-0: session=lab11-planner-0, events=3, tool_calls=get_dataset_schema, tool_responses=get_dataset_schema
coder-0: session=lab11-coder-0, events=1, tool_calls=aucun, tool_responses=aucune
verifier-0: session=lab11-verifier-0, events=1, tool_calls=aucun, tool_responses=aucune
Planner tool round-trip: True
Succès: True
Itérations: 1
Région attendue (calcul déterministe): Ouest
Verdict LLM: SUCCESS, la sortie répond correctement à la question initiale en identifiant la région avec le plus grand revenu moyen.
Sortie Executor:
La région avec le plus grand revenu moyen est Ouest avec un revenu moyen de 2800.6191304347826.

Lecture du résultat ADK. La trace précédente est la preuve d’exécution : chaque rôle LLM possède une session nommée et émet des événements via un Runner. Le Planner réalise en plus un aller-retour d’outil (get_dataset_schema) avant que le Coder ne produise le code, que l’Executor local ne l’exécute et que le Verifier ne rende son verdict. Cette jambe n’est donc ni une classe maison appelée « ADK », ni une réponse simulée.

11. Résumé du Lab

Ce que nous avons implémenté

  1. Planner : décompose la question en étapes d’analyse, avec raisonnement.
  2. Coder : génère le code Python qui exécute le plan, à partir du contexte.
  3. Executor : exécute le code dans un namespace contrôlé et capture stdout + erreurs.
  4. Verifier : valide le résultat (SUCCESS), demande un raffinement (NEEDS_REFINEMENT) ou déclare l’échec (FAILED).
  5. Google ADK réel : instancie les rôles LLM comme de vrais Agent, crée une session et un Runner par rôle, puis observe les événements et le round-trip d’outil du Planner.

Points clés

  • La boucle itérative est la valeur ajoutée de DS-STAR : une erreur observée peut être réinjectée dans le contexte de l’itération suivante au lieu de devenir un échec silencieux.
  • Le Verifier est le point de décision : son verdict pilote le succès, le raffinement ou l’arrêt.
  • max_iterations est un compromis coût/qualité : il borne explicitement le nombre d’appels LLM.
  • ADK orchestre, l’Executor reste déterministe : le runtime agentique porte les rôles LLM et leurs événements; l’exécution Pandas reste visible et contrôlée dans le notebook.

Prochaine étape

  • Lab 12 : workshop DS-STAR complet sur fichiers réels, avec davantage d’itérations et de cas d’échec à raffiner.

Exercice : Analysez vos propres données

En utilisant l’architecture DS-STAR que vous venez d’apprendre, créez un agent capable d’analyser un dataset de votre choix.

Objectifs

  1. Créer un dataset personnalise (ou utiliser un dataset publique)
  2. Configurer l’agent DS-STAR avec ce dataset
  3. Poser 3 questions d’analyse différentes
  4. Observer le comportement iteratif de l’agent
# TODO: Creez votre propre dataset
# Exemples : donnees de ventes, meteo, scores sportifs, etc.
mon_dataset = pd.DataFrame({
    # Definissez vos colonnes ici
})

# TODO: Instanciez l'agent DS-STAR
mon_agent = DSStarAgent(mon_dataset, max_iterations=2)

# TODO: Posez 3 questions d'analyse progressives
question_1 = "..."  # Question simple (agregation)
question_2 = "..."  # Question intermediaire (comparaison)
question_3 = "..."  # Question complexe (analyse temporelle ou correlation)

# TODO: Executez et analysez les resultats
resultat_1 = mon_agent.analyze(question_1)
print(f"Reussite: {resultat_1['success']}, Iterations: {resultat_1['iterations']}")

=== ITERATION 1/2 ===
[PLANNER] Creation du plan...
Etapes: 5
[CODER] Generation du code...
[EXECUTOR] Execution...
[ERROR] Cannot describe a DataFrame without columns

=== ITERATION 2/2 ===
[PLANNER] Creation du plan...
Etapes: 5
[CODER] Generation du code...
[EXECUTOR] Execution...
[OUTPUT] Le DataFrame est vide.

Aperçu du DataFrame après nettoyage :
Empty DataFrame
Columns: []
Index: []
...
[VERIFIER] Verification...
[STATUS] failed: Resultat insuffisant
Reussite: False, Iterations: 2

Exercice : Amelioration du Prompt du Planner

Le parseur du Planner utilise des expressions regulieres pour extraire les étapes. L’objectif est d’ameliorer le prompt et/ou le parseur pour augmenter le taux de succes de l’extraction.

Objectifs

  1. Analyser pourquoi le Planner extrait parfois 0 étapes
  2. Proposer un prompt plus robuste avec des instructions plus strictes
  3. Implementer un parseur de fallback (ex: detecter les puces - ou *)

Indice : - Le format actuel exige REASONING: et STEPS: avec des numéros 1. - Un fallback pourrait chercher des lignes commencant par - ou * - Testez votre version amelioree sur les mêmes questions que le Planner original

# Exercice : Amelioration du prompt et du parseur du Planner
# Objectif : Rendre l'extraction des etapes plus robuste

# TODO: Analysez le prompt actuel du Planner (cell-8) et identifiez les faiblesses
# Le prompt demande "REASONING:" et "STEPS:" avec des numeros
# Que se passe-t-il si le LLM repond differemment ?

# TODO: Creez une classe RobustPlanner qui herite de Planner
class RobustPlanner(Planner):
    """Planner avec prompt ameliore et parseur de fallback."""
    
    def create_plan(self, question: str, context: str) -> Plan:
        # Etape 1: Modifiez le prompt pour etre plus explicite
        prompt = f"""Tu es un planificateur d'analyse de donnees.
CONTEXTE: {context[:600]}
QUESTION: {question}

IMPORTANT: Reponds OBLIGATOIREMENT dans ce format exact:
REASONING: [ton raisonnement en une phrase]
STEPS:
1. [premiere etape]
2. [deuxieme etape]
3. [troisieme etape]"""
        
        response = self.llm.generate(prompt, temperature=0.3)
        
        # Etape 2: Parse avec le regex standard
        steps = []
        reasoning = ""
        in_steps = False
        for line in response.split('\n'):
            if line.startswith('REASONING:'):
                reasoning = line.replace('REASONING:', '').strip()
            elif line.startswith('STEPS:'):
                in_steps = True
            elif in_steps and re.match(r'^\d+\.', line.strip()):
                steps.append(re.sub(r'^\d+\.\s*', '', line.strip()))
        
        # Etape 3: Fallback si aucune etape trouvee
        # Indice: cherchez des lignes commencant par "- " ou "* "
        if not steps:
            pass  # TODO etudiant : implementez le fallback
        
        return Plan(steps=steps, reasoning=reasoning)

# TODO: Testez et comparez avec le Planner original
# robust_planner = RobustPlanner(LLMClient())
# test_question = "Quelle region genere le plus de revenus?"
# test_context = "DataFrame avec colonnes: date, product, region, revenue, units"
# plan = robust_planner.create_plan(test_question, test_context)
# print(f"Etapes trouvees: {len(plan.steps)}")
# for i, step in enumerate(plan.steps):
#     print(f"  {i+1}. {step}")

print("Exercice a completer : amelioration du prompt et parseur du Planner")
Exercice a completer : amelioration du prompt et parseur du Planner

Exercice : Analyseur d’Erreurs pour l’Executor

L’Executor retourne des messages d’erreur Python (KeyError, NameError, etc.). L’objectif est de créer un composant ErrorAnalyzer qui classifie les erreurs et genere un contexte de correction automatique pour la boucle iterative.

Objectifs

  1. Classifier les erreurs les plus frequentes du code genere par le LLM
  2. Créer un dictionnaire de correspondance erreur -> correction
  3. Integrer l’ErrorAnalyzer dans la boucle du DSStarAgent

Indice : - Erreurs frequentes : KeyError (mauvais nom de colonne), NameError (variable non définie), AttributeError (méthode inexistante) - Pour chaque type, proposez une correction automatique (ex: suggerer les noms de colonnes proches) - Utilisez difflib.get_close_matches() pour suggerer des noms de colonnes similaires

# Exercice : Analyseur d'erreurs automatique pour l'Executor
# Objectif : Classifier et corriger automatiquement les erreurs du code genere

import difflib

class ErrorAnalyzer:
    """Analyse les erreurs d'execution et suggere des corrections."""
    
    def __init__(self, available_columns: list = None):
        self.available_columns = available_columns or []
    
    def classify_error(self, error_message: str) -> str:
        """
        Classifie le type d'erreur.
        
        Args:
            error_message: message d'erreur Python
            
        Returns:
            Type d'erreur: 'key_error', 'name_error', 'attribute_error', 'type_error', 'other'
        """
        # TODO: Implementez la classification
        # Indice: utilisez des mots-cles comme "KeyError", "NameError", etc.
        error_lower = error_message.lower()
        
        if 'keyerror' in error_lower:
            return 'key_error'
        # TODO etudiant : ajoutez les autres types d'erreurs
        
        return 'other'
    
    def suggest_correction(self, error_message: str, error_type: str) -> str:
        """
        Suggere une correction basee sur le type d'erreur.
        
        Args:
            error_message: message d'erreur original
            error_type: type classifie
            
        Returns:
            Message de correction pour enrichir le contexte
        """
        if error_type == 'key_error':
            # Etape 1: Extrayez le nom de colonne errone
            # Indice: cherchez entre guillemets dans le message d'erreur
            wrong_col = None  # TODO etudiant : extrayez avec un regex
            
            # Etape 2: Suggerez des colonnes proches
            # Indice: difflib.get_close_matches(wrong_col, self.available_columns, n=3)
            suggestions = []
            
            return f"La colonne '{wrong_col}' n'existe pas. Colonnes disponibles: {self.available_columns}. Suggestions proches: {suggestions}"
        
        # TODO etudiant : ajoutez des corrections pour name_error, attribute_error, etc.
        return f"Erreur de type {error_type}: {error_message}"

# TODO: Testez l'ErrorAnalyzer
# analyzer = ErrorAnalyzer(available_columns=['date', 'product', 'region', 'revenue', 'units'])
# 
# # Test avec une erreur KeyError typique
# error = "KeyError: 'revenu'"
# error_type = analyzer.classify_error(error)
# correction = analyzer.suggest_correction(error, error_type)
# print(f"Type: {error_type}")
# print(f"Correction: {correction}")

print("Exercice a completer : analyseur d'erreurs automatique pour l'Executor")
Exercice a completer : analyseur d'erreurs automatique pour l'Executor

Points de reflexion

  • Combien d’itérations sont necessaires pour chaque type de question ?
  • Quelles erreurs l’agent rencontre-t-il et comment les corrige-t-il ?
  • Comment pourriez-vous ameliorer le verifier ?

Références

  1. S. Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, arXiv:2210.03629, ICLR 2023. Paradigme Reasoning+Acting : l’agent alterne raisonnement et actions observables — fondement de la boucle Planner-Coder-Verifier.
  2. N. Shinn et al., Reflexion: Language Agents with Verbal Reinforcement Learning, arXiv:2303.11366, NeurIPS 2023. Auto-réflexion verbale et raffinement itératif après échec — principe de l’étape Verifier → Coder.
  3. Nam et al., DS-STAR: Data Science Agent for Solving Diverse Tasks across Heterogeneous Formats and Open-Ended Queries, arXiv:2509.21825, 2025. Agent DS-STAR dont ce lab implémente la boucle cœur (suite Lab 10).
  4. Z. Xi et al., The Rise and Potential of Large Language Model Based Agents: A Survey, arXiv:2309.07864, 2023. Cadre conceptuel des agents LLM (suite Labs 8-10).
Retour au sommet