CI5 : IA agentique
Dans ce TP, vous allez transformer votre pipeline RAG (réalisé au TP précédent sur votre boîte mail + PDFs administratifs) en un agent orchestré capable de prendre des décisions (triage), d’appeler des outils (votre RAG comme tool), et de gérer un état (state) tout au long d’un graphe d’exécution LangGraph. L’objectif n’est pas de construire un “agent autonome”, mais un système testable, observable et sûr, proche de ce qu’on attendrait en ingénierie logicielle.
Le TP est modérément guidé : la majorité du code est fournie, et vous compléterez des zones marquées _______. Vous travaillerez sur des emails réels (les vôtres), mais l’exécution sera conçue pour éviter la friction pendant la séance : vous constituerez un petit jeu de tests (8 à 12 emails) et vous évaluerez l’agent sur ce jeu, en produisant un rapport Markdown au fil de l’eau (captures d’écran, commandes, extraits de logs, observations).
- Mettre en place un agent orchestré avec LangGraph : nœuds, transitions conditionnelles, et boucle contrôlée.
- Modéliser un state typé avec Pydantic pour transporter décisions, preuves (evidence), brouillons, actions (mock), erreurs et budgets.
- Intégrer votre RAG (TP précédent) comme un tool : appel, récupération de preuves, citations, et “evidence gating”.
- Implémenter un routing robuste (LLM + validation) entre reply, ask_clarification, escalate, ignore.
- Ajouter des mécanismes de robustesse : validation JSON, fallback/repair, timeouts, et dégradation sûre (safe mode).
- Mettre en place une observabilité minimale via des logs JSONL (événements par run) exploitables dans votre rapport.
- Évaluer pragmatiquement l’agent sur 8–12 emails : trajectoires, citations, erreurs, et limites.
Mise en place de TP5 et copie du RAG (base Chroma incluse)
- la question
- la réponse
- la liste des sources récupérées
Constituer un jeu de test (8–12 emails) pour piloter le développement
- administratif (procédure, inscription, attestation, etc.)
- enseignement (devoirs, notes, organisation)
- recherche (projet, paper, meeting)
- au moins 1 email ambigu (besoin de clarification)
- au moins 1 email “à risque” (PII, demande sensible, ou potentiel prompt injection)
- la liste des fichiers emails (E01…)
- une capture d’écran du répertoire TP5/data/test_emails/ (liste des fichiers)
- un court paragraphe (3–5 lignes) expliquant la diversité de votre jeu de test
- charger tous les fichiers .md/.txt du dossier
- extraire email_id, subject, from, et le corps entre <<< ... >>>
- retourner une liste de dictionnaires Python
- le nombre d’emails chargés
- la liste (email_id + subject) affichée par le script
Implémenter le State typé (Pydantic) et un logger JSONL (run events)
- TP5/agent/
- TP5/agent/nodes/
- TP5/runs/ (pour les logs JSONL)
- Decision : intent/category/priority/risk/needs_retrieval/retrieval_query (+ rationale courte)
- RetrievalSpec : query/k/filters
- EvidenceDoc : doc_id/source/doc_type/snippet/score
- ToolCallRecord : tool_name/args_hash/status/latency_ms/error
- Budget : max_steps/steps_used/max_tool_calls/tool_calls_used/max_retrieval_attempts/retrieval_attempts
- AgentState : email + decision + evidence + drafts + actions + errors + budget + run_id
- le fichier TP5/runs/<run_id>.jsonl créé
- un extrait du contenu (par exemple tail -n 5)
Router LLM : produire une Decision JSON validée (avec fallback/repair)
- Sortie : JSON uniquement (pas de Markdown)
- intent dans un set fermé
- category dans un set fermé
- priority dans 1..5
- risk_level dans low/med/high
- retrieval_query courte et vide si needs_retrieval=false
- rationale : 1 phrase max, pas de données sensibles
- appeler le LLM (Ollama) avec le prompt routeur
- parser la sortie JSON
- valider via Decision (Pydantic)
- en cas d’échec de parsing/validation : appeler un repair prompt puis re-parser
- logguer node_start et node_end en JSONL
- charge le premier email de votre jeu de test (E01)
- instancie un AgentState
- appelle classify_email(state)
- affiche la décision
- une capture d’écran de la décision JSON affichée
- une capture d’écran d’un extrait de TP5/runs/<run_id>.jsonl montrant l’événement classify_email
LangGraph : routing déterministe et graphe minimal (MVP)
- logguer node_start et node_end (JSONL)
- écrire un message minimal dans state.draft_v1 (ou ajouter une action mockée)
- utiliser AgentState comme state
- avoir un nœud d’entrée classify_email
- router vers les stubs selon route(state)
- terminer après le stub (pas de boucle pour l’instant)
- charger un email de test (ex: E01)
- créer un AgentState avec un run_id
- exécuter le graphe
- afficher decision + draft_v1 + actions
- une capture d’écran montrant la décision + la sortie (draft/actions)
- une capture d’écran d’un extrait du fichier TP5/runs/<run_id>.jsonl (au moins 4 événements)
Tool use : intégrer votre RAG comme outil (retrieval + evidence)
- se connecter à Chroma depuis TP5/chroma_db
- utiliser la même COLLECTION_NAME que votre TP précédent
- utiliser Ollama (port modifiable via constante PORT)
- retourner une liste de EvidenceDoc (doc_id, doc_type, source, snippet, score)
- logguer un événement tool_call (JSONL) avec latence + status
- logguer node_start et node_end
- si needs_retrieval=false : ne rien faire
- sinon : construire un RetrievalSpec et appeler rag_search_tool
- mettre à jour state.retrieval_spec et state.evidence
- incrémenter state.budget.retrieval_attempts et state.budget.tool_calls_used
- classify_email → (route)
- si reply : maybe_retrieve → stub_reply → END
- sinon : stubs directs → END
- une capture d’écran montrant que evidence n’est pas vide (au moins 1 doc)
- un extrait JSONL montrant un événement tool_call pour rag_search
Génération : rédiger une réponse institutionnelle avec citations (remplacer le stub reply)
- logguer node_start et node_end
- construire un contexte à partir de state.evidence (format citable)
- appeler le LLM (Ollama) pour produire une sortie JSON : {"reply_text": "...", "citations": ["doc_1", ...]}
- vérifier que citations ⊆ evidence.doc_id
- si citations invalides ou evidence vide : produire une réponse prudente (safe mode)
- mettre le résultat dans state.draft_v1
- un cas reply avec evidence non vide
- un cas où l’evidence est vide ou citations invalides (safe mode)
- la réponse finale (draft_v1)
- un extrait JSONL montrant draft_reply (status ok vs safe_mode)
Boucle contrôlée : réécriture de requête et 2e tentative de retrieval (max 2)
- evidence_ok: bool = False
- last_draft_had_valid_citations: bool = False
- si la génération produit des citations valides (subset des doc_id présents dans state.evidence) : mettez state.last_draft_had_valid_citations = True
- si la génération bascule en safe mode (evidence vide, JSON invalide, citations invalides) : mettez state.last_draft_had_valid_citations = False
- evidence_ok = True si state.last_draft_had_valid_citations est vrai
- Sinon, evidence_ok = False et on tente un rewrite (si budget le permet)
- logguer start/end
- réécrire la requête si l’evidence est insuffisante
- mettre à jour state.decision.retrieval_query (courte)
- ne faire qu’UNE proposition (une seule query)
- classify_email → (reply) → maybe_retrieve → draft_reply → check_evidence
- si evidence_ok : END
- si evidence_ok=false et tentatives < 2 : rewrite_query → retour à maybe_retrieve
- si evidence_ok=false et tentatives = 2 : END
- une capture d’écran montrant au moins 2 tentatives de retrieval (via logs)
- un extrait JSONL montrant draft_reply en safe mode puis un second tool_call
Finalize + Escalade (mock) : sortie propre, actionnable, et traçable
- si intent=reply : réponse institutionnelle (déjà) + rappel des citations (si dispo)
- si intent=ask_clarification : questions précises
- si intent=escalate : création d’un HandoffPacket (mock) avec résumé + evidence + run_id
- si intent=ignore : raison concise
- final_text: str = ""
- final_kind: str = "" (ex: reply / clarification / handoff / ignore)
- logguer start/end
- produire state.final_text et state.final_kind
- si reply : reprendre draft_v1 et ajouter une ligne “Sources: [doc_…]” (si citations détectables)
- si ask_clarification : produire 1–3 questions (si draft_v1 vide, générer un fallback)
- si escalate : créer une action mockée handoff_packet (résumé + evidence IDs + run_id)
- si ignore : produire un texte minimal
- final_kind et final_text
- si escalade : le contenu de l’action mockée handoff_packet
- un extrait JSONL montrant l’événement finalize
Robustesse & sécurité : budgets, allow-list tools, et cas “prompt injection”
- budgets (steps, tool calls, retrieval attempts)
- allow-list des tools (éviter les appels hors périmètre)
- un mini détecteur de prompt injection (heuristique simple) pour déclencher risk_level=high ou une escalade
- risk_level="high"
- intent="escalate"
- needs_retrieval=false
- au début du nœud : si not state.budget.can_step(), logguez et retournez l’état sans faire de traitement
- sinon : incrémentez state.budget.steps_used += 1
- le tool doit refuser (retourner []) si k > 10 ou si la query est vide
- le tool doit logguer un tool_call avec status="error" dans ce cas
- la décision est forcée en intent=escalate et risk_level=high
- il n’y a pas d’appel rag_search dans les logs
- un handoff_packet est produit par finalize
Évaluation pragmatique : exécuter 8–12 emails, produire un tableau de résultats et un extrait de trajectoires
- charger tous les emails de TP5/data/test_emails/
- exécuter le graphe pour chaque email (un run_id par email)
- écrire un tableau Markdown dans TP5/batch_results.md
- ne pas écrire de gros fichiers : uniquement le tableau et des pointeurs vers les logs JSONL
- une capture d’écran du terminal (script OK)
- une capture d’écran du fichier TP5/batch_results.md (au moins 5 lignes)
- quels intents dominent ?
- combien d’escalades ?
- combien de safe modes (si vous les avez) ?
- un exemple de trajectoire intéressante (ex: rewrite + 2e retrieval)
- ajoutez une capture d’écran d’un extrait de chaque TP5/runs/<run_id>.jsonl (10–20 lignes)
- expliquez en 4–6 lignes la trajectoire (nodes, tool calls, boucle)
Rédaction finale du rapport (1–2 pages) : synthèse, preuves, et réflexion courte
- les commandes utilisées pour lancer rag_answer_tp5.py, test_graph_minimal.py, et run_batch.py
- au moins 2 captures d’écran (terminal) montrant un run reply et un run escalate ou ignore
- un extrait du tableau batch_results.md (ou une capture)
- un commentaire de 5–8 lignes sur les tendances observées
- ce qui marche bien (2 points)
- ce qui est fragile (2 points)
- une amélioration prioritaire si vous aviez 2h de plus (1 point)