Lab 17: Projet Final - Pipeline DS-STAR Complet

Navigation : Lab 16 << | Index

Objectifs d’apprentissage

À la fin de ce laboratoire, vous saurez : 1. Intégrer tous les composants DS-STAR : FileAnalyzer, Planner-Coder-Verifier 2. Appliquer à un problème réel de data science 3. Générer un rapport d’analyse automatisé complet 4. Déployer un agent de data science fonctionnel

Prérequis

  • Tous les labs précédents (Lab 8-16) complétés
  • Projet GCP configuré (optionnel)
  • Maîtrise des patterns d’agents

Durée estimée : 60-90 minutes


Architecture Complète

Fichiers CSV/JSON --> FileAnalyzer --> Planner --> Coder --> Executor --> Verifier --> Reporter

1. Configuration

Cette section importe les briques techniques du pipeline DS-STAR : types standards (pandas, numpy) pour la manipulation de données, structures (dataclass, Enum) pour faire circuler l’information entre les agents, et surtout le client LLM (LLMClient de utils) qui sera partagé par les six composants. L’objectif est de n’avoir qu’une seule instance de LLM pour tout le pipeline — cela garantit une configuration cohérente (provider, modèle, température) et évite les fuites de connexion.

Notez le sys.path.insert(0, '..') : les modules config et utils vivent un niveau au-dessus, partagés avec les autres labs de la série Track2-GoogleADK.

import sys
sys.path.insert(0, '..')

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

from config import get_settings
from utils import LLMClient

print("Imports et configuration charges.")
Imports et configuration charges.

Chargement effectif des paramètres via get_settings() : cette fonction lit la configuration du provider LLM (clé API, endpoint, modèle actif) depuis l’environnement. L’affichage du provider actif (openrouter ici) permet de vérifier instantanément que le bon backend est branché avant de lancer le pipeline — un diagnostic utile quand une exécution échoue silencieusement à cause d’une clé manquante.

settings = get_settings()
print(f'Provider: {settings.active_provider}')
Provider: openrouter

2. Data Classes

Avant de construire les agents, on définit les structures de données qui circulent entre eux. Dans un système multi-agent, la qualité du contrat de données conditionne la robustesse de l’orchestration : chaque composant consomme la sortie du précédent selon un type explicite, ce qui évite les erreurs à l’exécution et facilite le débogage.

  • Plan capture la décomposition produite par le Planner : les étapes + le raisonnement.
  • ExecutionResult encapsule la sortie de l’Executor : succès, stdout capturé, erreur éventuelle, code exécuté.
  • FileMetadata décrit le fichier analysé (lignes, colonnes, types) — c’est le contexte passé au Planner.
  • VerificationStatus est l’énumération des verdicts du Verifier (SUCCESS / NEEDS_REFINEMENT / FAILED), qui pilote la boucle d’itération du pipeline.

Ces quatre types forment le vocabulaire de communication des agents : un Plan devient du code, qui devient un ExecutionResult, qui devient un verdict, qui décide de recommencer ou non.

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

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

@dataclass
class FileMetadata:
    filename: str
    format: str
    size_bytes: int
    num_rows: int = None
    num_columns: int = None
    columns: list = None

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

print("Data classes et enums definis.")
Data classes et enums definis.

3. Modules DS-STAR

Ce projet final integre le pipeline DS-STAR complet (FileAnalyzer -> Planner -> Coder -> Executor -> Verifier -> Reporter) tel que défini par Guo et al. (2025) : un système multi-agent qui decompose un problème de data science, genere et execute du code en sandbox, puis valide les résultats avant de produire un rapport. Ce lab orchestre l’integration end-to-end de tous les composants, par opposition a l’étude d’un composant isole (cf. Lab 10 sur le FileAnalyzer).

Reference : Nam, J., et al. (2025). *DS-STAR: Data Science Agent for Solving Diverse Tasks across Heterogeneous Formats and Open-Ended Queries arXiv:2509.21825. https://arxiv.org/abs/2509.21825

class FileAnalyzer:
    SUPPORTED = {'.csv': 'csv', '.json': 'json'}

    def __init__(self, llm=None):
        self.llm = llm or LLMClient()

    def analyze_csv(self, path: str) -> FileMetadata:
        df = pd.read_csv(path)
        cols = [{'name': c, 'dtype': str(df[c].dtype)} for c in df.columns]
        return FileMetadata(
            filename=Path(path).name, format='csv', size_bytes=os.path.getsize(path),
            num_rows=len(df), num_columns=len(df.columns), columns=cols
        )

    def generate_context(self, meta: FileMetadata) -> str:
        lines = [f'Fichier: {meta.filename}', f'Lignes: {meta.num_rows}', f'Colonnes: {meta.num_columns}']
        if meta.columns:
            lines.append('Colonnes: ' + ', '.join([c['name'] for c in meta.columns[:5]]))
        return '\\n'.join(lines)

print("FileAnalyzer pret.")
FileAnalyzer pret.

FileAnalyzer est l’entrée du pipeline : il lit le fichier (CSV ou JSON), en extrait les métadonnées (nombre de lignes, colonnes, types) et produit un contexte textuel consommé par tous les agents suivants. Sa méthode generate_context est cruciale — c’est elle qui décide de l’information que le Planner et le Coder « verront » du dataset. Un contexte trop paubre entraîne des plans génériques ; un contexte trop riche dilue la question.

Le composant suivant, le Planner, reçoit ce contexte et décompose la question d’analyse en étapes concrètes.

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

    def create_plan(self, question: str, context: str) -> Plan:
        prompt = f'Planifie: {question}\nCONTEXTE: {context[:600]}\nDonne 2-3 etapes. Format: REASONING: [...] STEPS: 1. [...] 2. [...]'
        response = self.llm.generate(prompt, temperature=0.3)
        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()))
        return Plan(steps=steps, reasoning=reasoning)

print("Planner pret.")
Planner pret.

Planner est le cerveau stratégique du pipeline : il prend la question de l’utilisateur + le contexte du fichier et produit un Plan (étapes + raisonnement). Notez le prompt structuré (REASONING: / STEPS:) et le parsing par expression régulière — c’est une approche fragile (visible dans l’interprétation de la section 6 : si le LLM ne respecte pas le format, l’extraction échoue), mais simple à implémenter. La température basse (0.3) favorise des plans cohérents et reproductibles.

Le Coder reçoit ce plan et génère le code Python correspondant.

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'Code Python pour: {steps_text}\nDataFrame: df. Utilise print(). ```python [code] ```'
        response = self.llm.generate(prompt, temperature=0.2)
        match = re.search(r'```python\s*(.*?)\s*```', response, re.DOTALL)
        return match.group(1).strip() if match else ''

print("Coder pret.")
Coder pret.

Coder traduit chaque étape du plan en code Python exécutable. Le prompt demande explicitement un bloc ```python et une extraction par regex isole le code. Le DataFrame df est fourni comme contexte pour que le code généré suppose qu’un df existe déjà. La température 0.2 rend la génération presque déterministe — on veut du code fiable, pas de la créativité.

Ce code est ensuite exécuté en sandbox par l’Executor.

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:
        from io import StringIO
        import sys
        old_stdout = sys.stdout
        sys.stdout = StringIO()
        try:
            exec(code, self.namespace)
            return ExecutionResult(success=True, output=sys.stdout.getvalue(), code=code)
        except Exception as e:
            return ExecutionResult(success=False, output=sys.stdout.getvalue(), error=str(e), code=code)
        finally:
            sys.stdout = old_stdout

print("Executor pret.")
Executor pret.

Executor exécute le code généré dans un espace de noms isolé (namespace contenant df, pd, np, print). La redirection de sys.stdout vers un StringIO capture la sortie standard pour que le Verifier puisse l’inspecter. Si une exception est levée, elle est attrapée et encapsulée dans ExecutionResult.error — le pipeline ne plante jamais, il signale. C’est le pattern « fail-soft » indispensable à un agent autonome.

Le Verifier examine ensuite ce résultat.

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: {result.error}'
        prompt = f'Verifie: {question}\nResultat: {result.output[:400]}\nReponds SUCCESS ou NEEDS_REFINEMENT.'
        response = self.llm.generate(prompt, temperature=0.1).upper()
        if 'SUCCESS' in response:
            return VerificationStatus.SUCCESS, 'OK'
        return VerificationStatus.NEEDS_REFINEMENT, 'A ameliorer'

print("Verifier pret.")
Verifier pret.

Verifier est le garde-fou qualitatif : il demande au LLM si le résultat répond à la question (SUCCESS ou NEEDS_REFINEMENT). Son verdict pilote la boucle d’itération du pipeline — si le résultat est incomplet, le contexte est enrichi de la sortie précédente et une nouvelle itération est tentée (jusqu’à max_iterations). Notez que si l’exécution a échoué (result.success == False), le Verifier court-circuite et renvoie FAILED sans appeler le LLM — une économie d’appels bienvenue.

Enfin, le ReportGenerator synthétise les résultats en un rapport Markdown.

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

    def generate_report(self, question: str, results: List[Dict], metadata: FileMetadata) -> str:
        prompt = f'Genere un rapport Markdown pour: {question}\nFichier: {metadata.filename} ({metadata.num_rows} lignes)\nResultats: {len(results)} analyses'
        return self.llm.generate(prompt, temperature=0.3)

print("ReportGenerator pret.")
ReportGenerator pret.

4. Pipeline Complet

La classe DSStarPipeline est l’orchestrateur qui assemble les six composants en une boucle cohérente. C’est le cœur du lab : là où les sections précédentes isolaient chaque agent, cette section montre comment les composer — l’apport spécifique d’un projet final par rapport aux labs monocomposant (Lab 10 FileAnalyzer, Lab 11 Planner-Coder).

La boucle d’itération (for i in range(self.max_iterations)) est le mécanisme clé : à chaque tour, Planner → Coder → Executor → Verifier s’enchaînent, et si le Verifier demande un raffinement (NEEDS_REFINEMENT), le contexte est enrichi du résultat précédent avant de retenter. C’est l’implémentation concrète du pattern « plan-execute-verify-refine » de la littérature sur les agents (cf. référence DS-STAR).

class DSStarPipeline:
    def __init__(self, max_iterations: int = 2):
        self.llm = LLMClient()
        self.file_analyzer = FileAnalyzer(self.llm)
        self.planner = Planner(self.llm)
        self.coder = Coder(self.llm)
        self.verifier = Verifier(self.llm)
        self.reporter = ReportGenerator(self.llm)
        self.max_iterations = max_iterations

    def analyze(self, file_path: str, question: str) -> Dict:
        print('='*50 + '\nDS-STAR PIPELINE\n' + '='*50)

        meta = self.file_analyzer.analyze_csv(file_path)
        context = self.file_analyzer.generate_context(meta)
        print(f'[FILE] {meta.filename}: {meta.num_rows} lignes')

        df = pd.read_csv(file_path)
        executor = Executor(df)
        results = []

        for i in range(self.max_iterations):
            print(f'\n=== ITERATION {i+1} ===')
            plan = self.planner.create_plan(question, context)
            print(f'[PLANNER] {len(plan.steps)} etapes')
            code = self.coder.generate_code(plan, context)
            result = executor.execute(code)
            print(f'[EXECUTOR] {"OK" if result.success else "ERROR"}')
            if result.success:
                status, msg = self.verifier.verify(question, result)
                print(f'[VERIFIER] {status.value}')
                results.append({'output': result.output, 'status': status.value})
                if status == VerificationStatus.SUCCESS:
                    break
                context += f'\nResultat: {result.output[:200]}'

        report = self.reporter.generate_report(question, results, meta)
        return {'success': len(results) > 0, 'results': results, 'report': report}

print("DS-STAR Pipeline complet initialise.")
DS-STAR Pipeline complet initialise.

5. Dataset de Test

Pour exercer le pipeline de façon reproductible, on génère un dataset synthétique de ventes (200 lignes, produits A/B/C/D, régions Nord/Sud/Est/Ouest, revenus et unités aléatoires). Le np.random.seed(42) garantit que chaque exécution du lab produit le même fichier — indispensable pour que les interprétations des sections 6 et 7 (qui commentent les sorties réelles) restent valables d’une exécution à l’autre.

Ce dataset est volontairement simple (colonnes catégorielles + numériques) : il permet de poser des questions d’agrégation claires (« analyse les ventes par région ») sans complexité de nettoyage de données.

import tempfile
test_dir = tempfile.mkdtemp()

np.random.seed(42)
df = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=200, freq='D'),
    'product': np.random.choice(['A', 'B', 'C', 'D'], 200),
    'region': np.random.choice(['Nord', 'Sud', 'Est', 'Ouest'], 200),
    'revenue': np.random.uniform(500, 5000, 200).round(2),
    'units': np.random.randint(5, 100, 200)
})

csv_path = os.path.join(test_dir, 'sales.csv')
df.to_csv(csv_path, index=False)
print(f'Dataset: {csv_path} ({len(df)} lignes)')
Dataset: ~\AppData\Local\Temp\tmp1bu24mbj\sales.csv (200 lignes)

6. Exécution du Pipeline

On lance maintenant le pipeline complet sur le dataset de test avec max_iterations=1. Ce paramètre est volontairement restrictif : il permet d’observer une exécution unique et d’étudier ce qui se passe quand le premier essai ne suffit pas — un cas très fréquent avec des agents LLM. L’interprétation qui suit détaille pourquoi le Planner peut produire 0 étape et comment le Verifier réagit à ce cas.

Pour un comportement plus robuste, l’exercice final proposera d’augmenter max_iterations à 2 ou 3 et de comparer les résultats — c’est l’objet du benchmark de la section Exercices.

pipeline = DSStarPipeline(max_iterations=1)
result = pipeline.analyze(csv_path, 'Analyse les ventes par region')
==================================================
DS-STAR PIPELINE
==================================================
[FILE] sales.csv: 200 lignes

=== ITERATION 1 ===
[PLANNER] 3 etapes
[EXECUTOR] ERROR

Interpretation : Exécution du pipeline complet

Résultat obtenu : Le pipeline DS-STAR s’execute sur le dataset de ventes mais produit un résultat partiel — le Planner genere 0 étape et le Verifier classe le résultat comme needs_refinement.

Analyse des étapes :

Composant Résultat Observation
FileAnalyzer OK Fichier detecte : 200 lignes
Planner 0 étapes Le LLM n’a pas decompose la question correctement
Coder Code genere Malgre 0 étapes formelles, du code est produit
Executor OK Le code s’execute sans erreur
Verifier needs_refinement Le résultat ne repond pas completement a la question

Cause probable : Le prompt du Planner demande un format strict (REASONING + STEPS numerotes). Si le LLM ne respecte pas exactement ce format, le parseur par expression reguliere echoue a extraire les étapes. C’est une limitation courante des approches de parsing structure basees sur des regex.

Point pedagogique : Avec max_iterations=1, le pipeline ne peut pas retenter. En augmentant a max_iterations=2 ou 3, le contexte enrichi après la première itération permettrait au Planner de mieux repondre.

7. Résultats

Cette section affiche le rapport final généré par le ReportGenerator. C’est ici qu’apparaît le phénomène central d’hallucination commenté dans l’interprétation suivante : faute d’accès aux chiffres réels produits par l’Executor, le LLM invente des montants plausibles mais faux. Ce défaut — et sa correction par injection des données réelles — est précisément le sujet du 3ᵉ exercice (SafeReportGenerator anti-hallucination).

print(chr(10) + "="*50 + chr(10) + "RAPPORT" + chr(10) + "="*50)
print(result["report"][:800])

==================================================
RAPPORT
==================================================
```markdown
# Rapport d'Analyse des Ventes par Région

## Contexte
Ce rapport présente une analyse des ventes par région à partir du fichier `sales.csv` contenant 200 lignes de données.

## Données
- Fichier source : `sales.csv`
- Nombre de lignes : 200

## Résultats de l'analyse
Aucune analyse n'a pu être réalisée à partir des données fournies.  
Cela peut être dû à plusieurs raisons possibles :
- Données manquantes ou corrompues dans le fichier.
- Absence de colonne indiquant la région.
- Données non exploitables pour une analyse par région.

## Recommandations
- Vérifier la structure et la qualité des données dans le fichier `sales.csv`.
- S'assurer que la colonne région est bien présente et correctement renseignée.
- Nettoyer les données si nécessaire avant de relancer l'analyse.

---

Interpretation : Rapport genere par le LLM

Résultat obtenu : Le pipeline produit un rapport Markdown structure avec context, methodologie, analyse par region et observations — malgre l’echec partiel de l’étape de verification.

Analyse du rapport :

Section Contenu Qualite
Contexte Description du dataset Correct (200 lignes, fichier identifie)
Methodologie Étapes d’analyse Standard mais generique
Tableau de résultats Ventes par region Données fictives — le LLM invente les chiffres
Observations Interpretations Coherentes avec les données fictives

Point critique : Les montants du tableau (123 456 euros, etc.) sont generes par le LLM et ne correspondent PAS aux données reelles du dataset. Le rapport inclut d’ailleurs une note “Les montants sont des exemples, a remplacer par les résultats reels”. C’est un phenomene classique d’hallucination du LLM quand il n’a pas acces aux résultats d’exécution.

Amelioration : Injecter les résultats reels de l’Executor dans le prompt du ReportGenerator pour eviter l’hallucination des chiffres.

Nettoyage des fichiers temporaires generes pendant le pipeline.

import shutil
shutil.rmtree(test_dir)
print('Done')
Done

Conclusion

Competences acquises

  • Architecture d’agents pour data science
  • Boucles iteratives avec raffinement
  • Abstraction multi-provider
  • Rapports automatises ### Extensions possibles
  • Support de plus de formats
  • Parallelisation
  • Interface web
  • Deploiement cloud Felicitation! Vous avez complete la serie Track2-GoogleADK.

Exercice Final : Pipeline DS-STAR sur Données Reelles

Cet exercice final vous guide pour appliquer le pipeline complet a un problème reel.

Objectifs

  1. Trouver un dataset public (Kaggle, UCI, data.gouv)
  2. Configurer le pipeline DS-STAR
  3. Poser 3 questions d’analyse d’augmenter complexite
  4. Generer un rapport final

Instructions

# Exercice: Chargez un dataset reel
# Exemples: 
# - Kaggle: titanic, housing prices, credit card fraud
# - UCI: iris, wine, adult census
# - data.gouv: donnees ouvertes francaises

import pandas as pd
# mon_df = pd.read_csv('mon_dataset.csv')
# print(f"Dataset: {mon_df.shape}")

# Exercice: Sauvegardez en CSV pour le pipeline
# mon_df.to_csv('data_temp.csv', index=False)

# Exercice: Definissez 3 questions d'analyse
questions = [
    "Question simple - agregation de base",
    "Question intermediaire - comparaison de groupes",
    "Question complexe - analyse temporelle ou correlation"
]

# Exercice: Executez le pipeline pour chaque question
# pipeline = DSStarPipeline(max_iterations=2)
# for q in questions:
#     result = pipeline.analyze('data_temp.csv', q)
#     print(f"\n{q}: {result['success']}")

# Exercice: Generez un rapport de synthese
#rapport_final = """
## Analyse du Dataset [Nom]
# 
# ### Questions posees
# 1. ...
# 2. ...
# 3. ...
#
# ### Resultats cles
# - ...
#
# ### Limitations
# - ...
#"""

print("Exercice a completer")
Exercice a completer

Exercice : Benchmark du Pipeline avec Différentes Configurations

Testez le pipeline DS-STAR avec différentes configurations de paramètres (nombre d’itérations, temperature du LLM, longueur du contexte) et mesurez l’impact sur la qualite des résultats.

Objectifs

  1. Définir 3 configurations avec des paramètres différents
  2. Executer le pipeline sur la même question pour chaque configuration
  3. Comparer les résultats (succes, nombre d’itérations, qualite de l’output)

Indice : - Configuration 1: max_iterations=1, temperature=0.1 (conservateur) - Configuration 2: max_iterations=3, temperature=0.3 (equilibre) - Configuration 3: max_iterations=2, temperature=0.7 (creatif) - Mesurez le taux de succes et le nombre moyen d’itérations pour chaque config

# Exercice : Benchmark du pipeline DS-STAR avec differentes configurations
# Objectif : Mesurer l'impact des parametres sur la qualite des resultats

import tempfile
import os

# TODO: Creez un dataset de benchmark
bench_dir = tempfile.mkdtemp()
np.random.seed(42)
bench_df = pd.DataFrame({
    'date': pd.date_range('2024-01-01', periods=100, freq='D'),
    'product': np.random.choice(['Alpha', 'Beta', 'Gamma'], 100),
    'channel': np.random.choice(['Online', 'Store', 'Partner'], 100),
    'revenue': np.random.uniform(200, 3000, 100).round(2),
    'units': np.random.randint(1, 50, 100)
})
bench_path = os.path.join(bench_dir, 'benchmark.csv')
bench_df.to_csv(bench_path, index=False)

# Question de test identique pour toutes les configurations
test_question = "Analyse la performance par canal de vente et identifie le plus rentable"

# TODO: Definissez 3 configurations
configurations = [
    {"nom": "conservateur", "max_iterations": 1, "temperature": 0.1},
    {"nom": "equilibre", "max_iterations": 3, "temperature": 0.3},
    {"nom": "creatif", "max_iterations": 2, "temperature": 0.7},
]

# TODO: Executez le pipeline pour chaque configuration
resultats_benchmark = []
for config in configurations:
    # Indice: creez un pipeline avec max_iterations=config['max_iterations']
    # Note: la temperature se configure dans le LLMClient, pas directement dans le pipeline
    # pipeline = DSStarPipeline(max_iterations=config['max_iterations'])
    # result = pipeline.analyze(bench_path, test_question)
    
    resultats_benchmark.append({
        'config': config['nom'],
        # 'success': result['success'],
        # 'iterations': len(result.get('results', [])),
    })

# TODO: Affichez le tableau comparatif
print("Configuration | Succes | Iterations")
print("--------------|--------|-----------")
for r in resultats_benchmark:
    print(f"{r['config']:13} | {'?':>6} | {'?':>9}")

# TODO: Concluez sur la meilleure configuration
# Quelle configuration offre le meilleur compromis qualite / cout (nombre d'appels LLM) ?
meilleure_config = None  # TODO etudiant

# Nettoyage
import shutil
shutil.rmtree(bench_dir)

print("Exercice a completer : benchmark du pipeline avec differentes configurations")
Exercice a completer

Exercice : Amelioration du ReportGenerator Anti-Hallucination

Contexte methodologique : les LLM sont sujets a l’hallucination (generation de contenu plausible mais factuellement errone), phenomene systematise dans l’étude de reference de Ji et al. (2023). Le ReportGenerator doit donc ancrer ses chiffres sur les sorties verifiees du Verifier plutot que sur des estimations generees.

Reference : Ji, Z., Lee, N., Frieske, R., et al. (2023). Survey of Hallucination in Natural Language Generation. ACM Computing Surveys 55. arXiv:2202.03629. https://arxiv.org/abs/2202.03629

Dans la section 6, nous avons observe que le ReportGenerator peut halluciner des chiffres quand il n’a pas acces aux résultats reels. L’objectif est de créer un générateur de rapport qui injecte uniquement les données reelles de l’Executor et evite toute invention.

Objectifs

  1. Analyser le prompt du ReportGenerator actuel pour identifier les sources d’hallucination
  2. Implementer un SafeReportGenerator qui n’utilise que les données injectees
  3. Comparer les rapports genere par les deux versions

Indice : - Le prompt actuel demande “genere un rapport” sans fournir les résultats concrets - Injectez les outputs reels dans le prompt avec une instruction stricte “n’invente aucun chiffre” - Ajoutez un avertissement “Si tu n’as pas de données pour une section, indique ‘Données non disponibles’”

# Exercice : SafeReportGenerator - Anti-Hallucination
# Objectif : Creer un generateur de rapport qui n'invente pas de donnees

class SafeReportGenerator:
    """Generateur de rapport qui n'utilise que les donnees reelles injectees."""
    
    def __init__(self, llm: LLMClient):
        self.llm = llm
    
    def generate_safe_report(self, question: str, outputs: list, metadata) -> str:
        """
        Genere un rapport en n'utilisant QUE les donnees fournies.
        
        Args:
            question: Question analysee
            outputs: Liste des outputs reels de l'Executor
            metadata: FileMetadata du fichier analyse
            
        Returns:
            Rapport Markdown base uniquement sur les donnees reelles
        """
        # TODO etudiant: construisez un prompt strict
        # Etape 1: Formatez les donnees reelles
        real_data = "\n".join([f"Iteration {i+1}: {out[:200]}" for i, out in enumerate(outputs)])
        
        prompt = f"""Genere un rapport d'analyse Markdown pour la question suivante.

QUESTION: {question}
FICHIER: {metadata.filename} ({metadata.num_rows} lignes)

DONNEES REELLES (utilise UNIQUEMENT ces donnees, n'invente AUCUN chiffre):
{real_data}

REGLES STRICTES:
1. N'invente AUCUN chiffre qui ne figure pas dans les donnees reelles ci-dessus
2. Si les donnees sont insuffisantes pour une section, ecris "Donnees non disponibles"
3. Cite les chiffres exacts des resultats fournis
4. Ne specule PAS sur les causes si les donnees ne les expliquent pas

Format du rapport:
## Question
## Resultats
## Conclusion

Rapport:"""
        
        return self.llm.generate(prompt, temperature=0.1)
    
    def generate_from_template(self, question: str, outputs: list, metadata) -> str:
        """
        Genere un rapport par template (pas de LLM, zero hallucination).
        
        Returns:
            Rapport Markdown construit uniquement avec les vraies donnees
        """
        # TODO etudiant: construisez le rapport sans appeler le LLM
        report = f"# Rapport d'Analyse\n\n"
        report += f"## Question\n{question}\n\n"
        report += f"## Fichier\n{metadata.filename} ({metadata.num_rows} lignes, {metadata.num_columns} colonnes)\n\n"
        report += f"## Resultats\n"
        # TODO etudiant: ajoutez les outputs reels
        
        report += f"\n## Conclusion\n"
        report += "Analyse basee uniquement sur les resultats de l'Executor.\n"
        
        return report

# TODO: Testez les deux methodes et comparez
# safe_reporter = SafeReportGenerator(LLMClient())
# 
# # Utilisez des outputs reels du pipeline
# test_outputs = ["Nord: 12500.50, Sud: 18300.75, Est: 9800.20"]
# 
# rapport_safe = safe_reporter.generate_safe_report(
#     "Revenu par region", test_outputs, meta
# )
# rapport_template = safe_reporter.generate_from_template(
#     "Revenu par region", test_outputs, meta
# )
# 
# print("=== RAPPORT SAFE (LLM with constraints) ===")
# print(rapport_safe[:300])
# print("\n=== RAPPORT TEMPLATE (zero hallucination) ===")
# print(rapport_template[:300])

print("Exercice a completer : SafeReportGenerator anti-hallucination")
Exercice a completer

Extensions (bonus)

  • Ajoutez des visualisations avec matplotlib/seaborn
  • Exportez le rapport en Markdown ou HTML
  • Integrez une validation croisee des résultats
  • Connectez a une API externe pour enrichir les données ### Checklist de completion

References

  • Nam, J., et al. (2025). DS-STAR: Data Science Agent for Solving Diverse Tasks across Heterogeneous Formats and Open-Ended Queries. arXiv:2509.21825. https://arxiv.org/abs/2509.21825
  • Ji, Z., Lee, N., Frieske, R., et al. (2023). Survey of Hallucination in Natural Language Generation. ACM Computing Surveys 55. arXiv:2202.03629. https://arxiv.org/abs/2202.03629
  • Xi, Z., et al. (2023). The Rise and Potential of Large Language Model Based Agents: A Survey. arXiv:2309.07864.
Retour au sommet