Navigation : Index | Suivant >>

Conception Automatique de Notebook par Agents IA

Bienvenue dans ce démonstrateur d’intelligence collective !
Trois agents spécialisés collaborent pour développer et valider votre notebook :

  • 🧑💻 Admin : élabore et finalise les spécifications dans les cellules Markdown.
  • 🤖 Codeur : implémente le code dans les cellules de type code.
  • 🔍 Relecteur : vérifie le travail du codeur et valide (ou non) ses modifications.

Comment démarrer

  1. Configurez votre tâche ci-dessous ⤵
  2. Lancez l’orchestration avec le bouton « Play » ou « Exécuter »
  3. Observez la collaboration en direct : chaque agent intervient à tour de rôle
  4. Les cellules suivantes s’exécuteront automatiquement après validation

Conseil : Dans votre explorateur de fichiers, ouvrez le notebook cible (nommé “Notebook-Generated.ipynb” si vous avez choisi une tâche, ou bien le notebook téléversé) pour voir ses cellules évoluer en temps réel !

Configuration du Projet

Dans cette section, plusieurs modes vous sont proposés pour définir la tâche à réaliser :

  • Aléatoire : choisit une tâche de manière aléatoire parmi une liste préétablie.
  • Bibliothèque : vous sélectionnez la tâche désirée dans un menu déroulant.
  • Personnalisé : vous décrivez librement votre tâche.
  • Importer : vous téléversez votre propre notebook .ipynb.

Cliquez sur Valider pour confirmer votre choix. Les cellules suivantes prendront automatiquement en compte cette configuration.

# Cellule Code : Installation des dépendances pour l'interface utilisateur
# ----------------------------------------------------------------------
# Exécutez cette cellule EN PREMIER, puis redémarrez le noyau si nécessaire
# avant d'exécuter la cellule suivante.

print("Dépendances UI installées. Redémarrez le noyau si vous rencontrez des problèmes d'affichage des widgets.")
Dépendances UI installées. Redémarrez le noyau si vous rencontrez des problèmes d'affichage des widgets.
# Parameters cell for Papermill execution
# Tagged with "parameters" for Papermill to inject values

# Notebook topic (the main subject/task for the notebook)
notebook_topic = "Simple notebook creation"  # Valeur par défaut

# Notebook complexity level
notebook_complexity = "simple"  # simple|complex

# Additional requirements or specifications
additional_requirements = ""

# Target framework for the notebook
target_framework = "python"  # python|dotnet|hybrid

# Skip interactive widgets (for batch execution)
skip_widgets = True

# Legacy compatibility mapping
task_description = notebook_topic  # Map to existing logic
config_mode = 2  # Force Custom mode to use task_description

Paramètres d’exécution Papermill

La cellule précédente définit les paramètres par defaut du notebook. Ces variables (notebook_topic, notebook_complexity, target_framework, skip_widgets, config_mode) peuvent etre surcharges par Papermill lors d’une exécution en batch, permettant ainsi de piloter le comportement du notebook sans modifier son code source.

Points cles sur les paramètres :

Paramètre Valeur par defaut Rôle
notebook_topic “Simple notebook creation” Sujet principal du notebook a generer
skip_widgets True Desactive l’UI interactive (mode batch)
config_mode 2 Sélection du mode de configuration (0=Aleatoire, 1=Bibliotheque, 2=Personnalise)

Les cellules suivantes definissent le mode batch et verifient que toutes les dependances necessaires sont disponibles avant de lancer la configuration du projet.

# Parameters
BATCH_MODE = True
# Verification des dependances (import guards)
try:
    from semantic_kernel import Kernel
    semantic_kernel_AVAILABLE = True
except ImportError:
    semantic_kernel_AVAILABLE = False
    print(f'WARNING: semantic-kernel non installe. Installez avec: pip install semantic-kernel')

try:
    import nbformat
    nbformat_AVAILABLE = True
except ImportError:
    nbformat_AVAILABLE = False
    print(f'WARNING: nbformat non installe. Installez avec: pip install nbformat')

try:
    import papermill as pm
    papermill_AVAILABLE = True
except ImportError:
    papermill_AVAILABLE = False
    print(f'WARNING: papermill non installe. Installez avec: pip install papermill')

try:
    from dotenv import load_dotenv
    dotenv_AVAILABLE = True
except ImportError:
    dotenv_AVAILABLE = False
    print(f'WARNING: python-dotenv non installe. Installez avec: pip install python-dotenv')

try:
    import ipywidgets as widgets
    ipywidgets_AVAILABLE = True
except ImportError:
    ipywidgets_AVAILABLE = False
    print(f'WARNING: ipywidgets non installe. Installez avec: pip install ipywidgets')


# %% Configuration du projet - Batch Mode

import time
import random

import ipywidgets as widgets
from IPython.display import display
from jupyter_ui_poll import ui_events

# ---- Tâches prédéfinies proposées ----
POSSIBLE_TASKS = [
    "Créer un notebook Python qui génère aléatoirement un DataFrame de ventes mensuelles (12 mois), affiche des graphiques d'évolution et exporte un rapport PDF.",
    "Créer un notebook Python qui crée un dossier local avec quelques fichiers, puis compresse ce dossier en ZIP, et vérifie la taille et l'intégrité après décompression.",
    "Créer un notebook Python qui implémente un mini jeu console (Snake ou Pong) en mode 'demo' et se termine après un certain nombre de 'ticks'.",
    "Créer un notebook Python utilisant openpyxl (ou xlsxwriter) pour générer deux fichiers Excel puis les fusionner avec un résumé global.",
    "Créer un notebook Python qui télécharge un flux RSS public (p.ex. CNN), stocke les titres dans un CSV, puis génère un nuage de mots (WordCloud).",
    "Créer un notebook Python qui requête DBpedia (SPARQL) et affiche un graphique final (Plotly).",
    "Créer un notebook Python qui charge le dataset Titanic depuis une URL, effectue une analyse basique et un court modèle de classification.",
    "Construire un notebook scikit-learn sur le dataset IRIS et réaliser un court modèle de classification."
]

# ---- Stockage de la configuration ----
config = {
    'mode': None,
    'task_description': None,
    'uploaded_file': None
}

# Flag qui indique si la config est prête
config_ready = False

# ---- MODE BATCH : Configuration automatique via paramètres ----
if skip_widgets:
    print("🤖 MODE BATCH ACTIVÉ - Configuration automatique sans UI")
    config['mode'] = config_mode
    
    if config_mode == 0:  # Aléatoire
        config['task_description'] = random.choice(POSSIBLE_TASKS)
        print(f"Mode: Aléatoire")
    elif config_mode == 1:  # Bibliothèque (utilise la première tâche par défaut)
        config['task_description'] = POSSIBLE_TASKS[0]
        print(f"Mode: Bibliothèque")
    elif config_mode == 2:  # Personnalisé
        config['task_description'] = task_description
        print(f"Mode: Personnalisé")
    elif config_mode == 3:  # Upload (non supporté en batch)
        print("⚠️ Mode Upload non supporté en mode batch, utilisation mode Aléatoire")
        config['mode'] = 0
        config['task_description'] = random.choice(POSSIBLE_TASKS)
    
    print(f"Tâche sélectionnée: {config['task_description'][:100]}...")
    config_ready = True

# ---- MODE INTERACTIF : UI avec widgets (code original) ----
else:
    # ---- Widgets de configuration ----
    task_selector = widgets.Dropdown(
        options=POSSIBLE_TASKS, 
        description='Tâche :', 
        style={'description_width': 'initial'}
    )
    custom_task = widgets.Textarea(
        placeholder="Décrivez votre projet en détail...", 
        layout={'width': '90%', 'height': '120px'}
    )
    uploader    = widgets.FileUpload(accept='.ipynb', multiple=False)
    submit_btn  = widgets.Button(description="Valider", button_style='success', icon='rocket')

    tabs = widgets.Tab()
    tabs.children = [
        widgets.VBox([widgets.HTML("<i>Une tâche aléatoire sera générée</i>")]),
        widgets.VBox([widgets.Label("Choisissez une tâche type :"), task_selector]),
        widgets.VBox([widgets.Label("Écrivez vos instructions :"), custom_task]),
        widgets.VBox([widgets.Label("Uploader votre notebook :"), uploader])
    ]
    tabs.set_title(0, '🎲 Aléatoire')
    tabs.set_title(1, '📚 Bibliothèque')
    tabs.set_title(2, '✨ Personnalisé')
    tabs.set_title(3, '📤 Importer')

    def on_submit(_):
        """Callback déclenché au clic du bouton."""
        global config_ready
        try:
            config['mode'] = tabs.selected_index
            
            if config['mode'] == 3:
                if uploader.value:
                    config['uploaded_file'] = uploader.value[0]
            else:
                if config['mode'] == 0:
                    config['task_description'] = random.choice(POSSIBLE_TASKS)
                elif config['mode'] == 1:
                    config['task_description'] = task_selector.value
                elif config['mode'] == 2:
                    config['task_description'] = custom_task.value

            submit_btn.disabled = True
            print("Configuration validée !")
            
        except Exception as e:
            print(f"Erreur pendant la configuration : {e}")

        config_ready = True

    submit_btn.on_click(on_submit)

    # ---- Affichage ----
    display(widgets.HTML("<h3>🔧 Configuration du Projet</h3>"))
    display(tabs)
    display(submit_btn)

    # ---- Boucle bloquante synchrone ----
    print("En attente du clic sur Valider...")

    with ui_events() as poll:
        while not config_ready:
            poll(10)
            time.sleep(0.1)

print("✅ Config terminée, vous pouvez exécuter les cellules suivantes !")
🤖 MODE BATCH ACTIVÉ - Configuration automatique sans UI
Mode: Personnalisé
Tâche sélectionnée: Simple notebook creation...
✅ Config terminée, vous pouvez exécuter les cellules suivantes !

Configuration du LLM (.env)

Dans cette section, nous allons :

  1. Vérifier si un fichier .env est présent (et déjà configuré) ou non.
  2. Vous proposer une interface pour saisir ou rappeler :
    • La clé d’API (OPENAI_API_KEY),
    • L’URL d’un endpoint compatible OpenAI (OPENAI_BASE_URL),
    • Le modèle à utiliser (OPENAI_CHAT_MODEL_ID).
  3. Mettre à jour ou créer le fichier .env une fois la configuration validée.

Les cellules ultérieures se baseront sur ces informations pour orchestrer les agents.


import os
import time
import requests

from dotenv import load_dotenv
import ipywidgets as widgets
from IPython.display import display
from jupyter_ui_poll import ui_events

# -------------------------------------------------------------------------
# Fonctions utilitaires
# -------------------------------------------------------------------------
def list_models(api_base, api_key):
    """Retourne un dict avec la liste des modèles ou un champ 'error'."""
    url = f"{api_base}/models"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }
    try:
        resp = requests.get(url, headers=headers, timeout=20)
        if resp.status_code == 200:
            return resp.json()
        else:
            return {"error": f"status={resp.status_code}", "text": resp.text}
    except Exception as e:
        return {"error": str(e)}

# -------------------------------------------------------------------------
# Lecture .env
# -------------------------------------------------------------------------
load_dotenv("../.env")

sd_fake = "sk-proj-1234567890"
openai_api_key       = os.getenv("OPENAI_API_KEY", sd_fake).strip()
openai_base_url      = os.getenv("OPENAI_BASE_URL", "").strip()
openai_chat_model_id = os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-5.6-luna").strip()

already_configured = (
    openai_api_key != sd_fake
    or (openai_base_url not in ["", "https://api.openai.com/v1"])
)

# Flag indiquant quand la config est OK
env_config_ready = False

# ---- MODE BATCH : Lecture directe du .env ----
if skip_widgets:
    print("🤖 MODE BATCH ACTIVÉ - Lecture configuration depuis .env")
    
    if already_configured:
        print("✅ Configuration LLM détectée dans .env :")
        print(f"   - OPENAI_API_KEY       = {'configuree' if openai_api_key and openai_api_key != sd_fake else '(non configuree)'}")
        print(f"   - OPENAI_BASE_URL      = {openai_base_url or '(API officielle)'}")
        print(f"   - OPENAI_CHAT_MODEL_ID = {openai_chat_model_id}")
        env_config_ready = True
    else:
        print("⚠️ Configuration LLM non détectée dans .env")
        print("Veuillez configurer les variables d'environnement :")
        print("  - OPENAI_API_KEY")
        print("  - OPENAI_BASE_URL (optionnel)")
        print("  - OPENAI_CHAT_MODEL_ID")
        raise ValueError("Configuration LLM manquante pour mode batch")

# ---- MODE INTERACTIF : UI avec widgets (code original) ----
else:
    # -------------------------------------------------------------------------
    # Widgets 
    # -------------------------------------------------------------------------
    message_output = widgets.Output()

    api_key_input = widgets.Text(
        value=openai_api_key if already_configured else "",
        placeholder=f"Ex: {sd_fake}",
        description="Clé API :",
        layout={'width': '80%'}
    )

    base_url_input = widgets.Text(
        value=openai_base_url if already_configured else "https://api.openai.com/v1",
        placeholder="ex: https://api.my-llm.com/v1",
        description="Endpoint :",
        layout={'width': '80%'}
    )

    model_dropdown = widgets.Dropdown(
        options=[],
        description="Modèle :",
        layout={'width': '80%', 'display': 'none'}
    )

    list_models_btn = widgets.Button(
        description="Lister modèles",
        button_style='info',
        icon='search'
    )

    validate_llm_btn = widgets.Button(
        description="Enregistrer .env",
        button_style='success',
        icon='save'
    )

    ui_box = widgets.VBox([
        api_key_input,
        base_url_input,
        model_dropdown,
        widgets.HBox([list_models_btn, validate_llm_btn])
    ])

    # -------------------------------------------------------------------------
    # Callbacks
    # -------------------------------------------------------------------------
    def on_list_models_click(_):
        new_base_url = base_url_input.value.strip()
        new_api_key  = api_key_input.value.strip()
        with message_output:
            message_output.clear_output()
            if not new_base_url or not new_api_key:
                print("⚠️ Veuillez saisir un Endpoint et une clé API avant de lister les modèles.")
                return

            info = list_models(new_base_url, new_api_key)
            if "error" in info:
                print(f"Erreur /models: {info['error']} - {info.get('text','')}")
            else:
                data_list = info.get("data", [])
                if not data_list:
                    print("Aucun modèle n'a été retourné (data=[]).")
                else:
                    model_ids = [m.get("id", "(inconnu)") for m in data_list]
                    model_dropdown.options = model_ids

                    if openai_chat_model_id in model_ids:
                        model_dropdown.value = openai_chat_model_id
                    else:
                        model_dropdown.value = model_ids[0]

                    model_dropdown.layout.display = 'block'
                    print(f"✅ {len(model_ids)} modèle(s) trouvé(s).")

    def on_validate_llm_click(_):
        global env_config_ready
        new_api_key  = api_key_input.value.strip() or "sk-fake"
        new_base_url = base_url_input.value.strip()

        chosen_model = "gpt-5.6-luna"
        if model_dropdown.options and (model_dropdown.layout.display != 'none'):
            chosen_model = model_dropdown.value.strip()

        with message_output:
            message_output.clear_output()
            try:
                with open('.env', 'w', encoding='utf-8') as f:
                    f.write(f"OPENAI_API_KEY={new_api_key}\n")
                    f.write(f"OPENAI_BASE_URL={new_base_url}\n")
                    f.write(f"OPENAI_CHAT_MODEL_ID={chosen_model}\n")

                print("✅ Fichier .env créé/mis à jour avec :")
                print(f"   - OPENAI_API_KEY       = {'configuree' if new_api_key and new_api_key != 'sk-fake' else '(non configuree)'}")
                print(f"   - OPENAI_BASE_URL      = {new_base_url or '(API OpenAI officiel)'}")
                print(f"   - OPENAI_CHAT_MODEL_ID = {chosen_model}")

                env_config_ready = True

            except Exception as e:
                print(f"❌ Erreur lors de l'écriture du fichier .env: {str(e)}")

    # -------------------------------------------------------------------------
    # Suppression des anciens callbacks
    # -------------------------------------------------------------------------
    list_models_btn._click_handlers.callbacks = []
    validate_llm_btn._click_handlers.callbacks = []

    list_models_btn.on_click(on_list_models_click)
    validate_llm_btn.on_click(on_validate_llm_click)

    # -------------------------------------------------------------------------
    # Affichage 
    # -------------------------------------------------------------------------
    if already_configured:
        print("✅ Configuration LLM détectée dans .env :")
        print(f"   - OPENAI_API_KEY       = {'configuree' if openai_api_key and openai_api_key != sd_fake else '(non configuree)'}")
        print(f"   - OPENAI_BASE_URL      = {openai_base_url or '(API officielle)'}")
        print(f"   - OPENAI_CHAT_MODEL_ID = {openai_chat_model_id}")
        print("Aucune saisie supplémentaire n'est requise.")
        env_config_ready = True
    else:
        print("Veuillez :\n1) Saisir votre Endpoint et clé API")
        print("2) Cliquer sur [Lister modèles] (pour un endpoint custom)")
        print("3) Cliquer sur [Enregistrer .env] pour finaliser la configuration")
        display(ui_box)
        display(message_output)
        
        # ---- Boucle bloquante: attend le clic sur "Enregistrer .env" ----
        with ui_events() as poll:
            while not env_config_ready:
                poll(10)
                time.sleep(0.1)

        ui_box.layout.display = 'none'

    print("✅ Configuration LLM terminée, vous pouvez exécuter la suite !")
🤖 MODE BATCH ACTIVÉ - Lecture configuration depuis .env
✅ Configuration LLM détectée dans .env :
   - OPENAI_API_KEY       = configuree
   - OPENAI_BASE_URL      = (API officielle)
   - OPENAI_CHAT_MODEL_ID = gpt-5.6-terra

▶ Démarrage du Processus

La configuration étant terminée, les étapes suivantes vont se lancer :

  1. Installation des dépendances : Nous vérifions et installons papermill, nbformat, semantic-kernel, etc.
  2. Gestion d’état : nous utilisons la classe NotebookState pour piloter le cycle de vie du notebook.
  3. Plugins : chaque agent aura un plugin lui permettant de lire ou modifier le contenu du notebook.
  4. Stratégies d’orchestration : nous définissons quelles actions lancer en fonction de l’état (specified, implemented, tested, validated).
  5. Agents : définition et configuration des trois agents (AdminAgent, CoderAgent, ReviewerAgent).
  6. Conversation multi-agents : la conversation s’enchaîne jusqu’à validation du notebook (ou dépassement du nombre maximal d’itérations).

Objectif final : Obtenir un notebook entièrement fonctionnel et validé.

1. Installation des dépendances

Nous installons ici les bibliothèques nécessaires :

  • papermill : pour exécuter des notebooks et injecter des variables,
  • nbformat : pour manipuler la structure interne d’un notebook,
  • semantic-kernel : pour orchestrer la collaboration entre plusieurs agents (LLM).
# Cellule Code : Installation des packages requis
# ------------------------------------------------
# Nous installons ici les packages indispensables au pipeline.

print("Installation terminée. Si nécessaire, redémarrez le kernel pour activer les nouveaux packages.")
Installation terminée. Si nécessaire, redémarrez le kernel pour activer les nouveaux packages.

2. Import des bibliothèques et configuration

Nous importons :

  • Les bibliothèques standard (os, json, logging, etc.).
  • Les bibliothèques notebook (nbformat, papermill).
  • semantic-kernel pour la gestion de nos agents conversationnels.
  • Un logger coloré pour améliorer la lisibilité et le suivi de l’exécution.
# Cellule Code : Imports et configuration du logger
# -------------------------------------------------
import os
import json
import hashlib
import logging
import nbformat
import papermill as pm
import random
from datetime import datetime

# Imports liés à Semantic Kernel
from semantic_kernel import Kernel
from semantic_kernel.agents import ChatCompletionAgent, AgentGroupChat
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.functions import kernel_function
from semantic_kernel.agents.strategies.selection.selection_strategy import SelectionStrategy
from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.functions.kernel_arguments import KernelArguments

class ColorFormatter(logging.Formatter):
    """
    Un formatter coloré pour rendre les logs plus lisibles.
    """
    colors = {
        'DEBUG': '\033[94m',
        'INFO': '\033[92m',
        'WARNING': '\033[93m',
        'ERROR': '\033[91m',
        'CRITICAL': '\033[91m\033[1m'
    }
    reset = '\033[0m'

    def format(self, record: logging.LogRecord) -> str:
        msg = super().format(record)
        return f"{self.colors.get(record.levelname, '')}{msg}{self.reset}"

logger = logging.getLogger("Orchestration")
logger.setLevel(logging.DEBUG)  # Peut être paramétré via .env ou variable

if not logger.handlers:
    handler = logging.StreamHandler()
    handler.setLevel(logging.DEBUG)
    formatter = ColorFormatter(
        fmt="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
        datefmt="%H:%M:%S"
    )
    handler.setFormatter(formatter)
    logger.addHandler(handler)

logger.info("Configuration initiale terminée.")

3. Classe NotebookState : gestion de l’état du notebook

Cette classe regroupe :

  • La lecture et l’écriture du fichier .ipynb,
  • L’exécution via Papermill pour détecter d’éventuelles erreurs,
  • Les transitions d’états du notebook : specified, implemented, tested, validated,
  • Les mises à jour ciblées d’une cellule déterminée (recherche par contenu).

Elle centralise toutes les opérations afin que chaque agent puisse y accéder.

import re as _re_redact
_REDACT_PATH_PATTERNS = [
    # Windows: prefix 'C:\\Users\\' then user-name then '\\...' remainder
    _re_redact.compile(r'(C:\\Users\\)([^\\/\s"\']+)(\\[^\s"\']*)'),
    # Windows JSON-echappe : le dump log_notebook_state (json.dumps) double les antislashes
    _re_redact.compile(r'(C:\\\\Users\\\\)([^\\/\s"\']+)(\\\\[^\s"\']*)'),
    # Unix: /home/<name>/... and /Users/<name>/...
    _re_redact.compile(r'(/home/)([^/\s"\']+)(/[^\s"\']*)'),
    _re_redact.compile(r'(/Users/)([^/\s"\']+)(/[^\s"\']*)'),
]


def _redact_paths(text):
    """Remplace les chemins machine (Windows ``C:\\Users\\<name>\\...`` et Unix
    ``/home/<name>/...`` / ``/Users/<name>/...``) par le placeholder ``<USER>`` avant
    journalisation. Représentation volontairement conservative et content-based
    (cf secrets-hygiene règle 6, Stop & Repair type C : on corrige la source,
    jamais la sortie)."""
    if not text:
        return text
    redacted = text
    for pat in _REDACT_PATH_PATTERNS:
        redacted = pat.sub(lambda m: f"{m.group(1)}<USER>{m.group(3)}", redacted)
    return redacted


class NotebookState:
    """Gère le statut et le contenu d'un notebook."""

    VALID_STATES = ["specified", "implemented", "tested", "validated"]

    def __init__(self, notebook_path: str) -> None:
        self.notebook_path = notebook_path
        self._cached_notebook = None
        self._status = "specified"
        self._previous_states = []
        self._revision = 0
        self._load_notebook()

    def _load_notebook(self) -> None:
        """Charge le notebook depuis le chemin spécifié avec nbformat."""
        if not os.path.exists(self.notebook_path):
            raise FileNotFoundError(f"Notebook introuvable: {self.notebook_path}")
        with open(self.notebook_path, "r", encoding="utf-8") as f:
            self._cached_notebook = nbformat.read(f, as_version=4)
        logger.debug(
            f"[NotebookState] Chargé '{self.notebook_path}' avec "
            f"{len(self._cached_notebook.cells)} cellules."
        )

    def save_notebook(self, path: str = "") -> None:
        """Enregistre le notebook sur disque, par défaut au même chemin."""
        if not path:
            path = self.notebook_path
        try:
            with open(path, "w", encoding="utf-8") as f:
                nbformat.write(self._cached_notebook, f)
            logger.info(f"[NotebookState] Notebook sauvegardé sous {path}")
        except Exception as e:
            logger.error(f"[save_notebook] Erreur de sauvegarde: {e}")
            raise

    def get_notebook_json(self) -> str:
        """Retourne la représentation JSON complète du notebook en mémoire."""
        return json.dumps(self._cached_notebook, indent=2, ensure_ascii=False)

    @staticmethod
    def _output_summaries(cell, max_chars: int = 500) -> list[str]:
        """Résume les sorties textuelles sans inclure les rendus volumineux."""
        summaries = []
        for output in cell.get("outputs", []):
            if output.get("output_type") == "error":
                summary = (
                    f"{output.get('ename', 'Error')}: "
                    f"{output.get('evalue', '')}"
                )
            else:
                summary = output.get("text", "")
                if isinstance(summary, list):
                    summary = "".join(summary)
            if summary:
                summaries.append(str(summary)[:max_chars])
        return summaries

    def get_agent_context(self, preview_chars: int = 300) -> str:
        """Retourne l'inventaire JSON borné de toutes les cellules."""
        cells = []
        for index, cell in enumerate(self._cached_notebook.cells):
            source = cell.source
            item = {
                "index": index,
                "cell_type": cell.cell_type,
                "source_preview": source[:preview_chars],
                "source_chars": len(source),
            }
            if cell.cell_type == "code":
                item["execution_count"] = cell.get("execution_count")
                item["output_summaries"] = self._output_summaries(cell)
            cells.append(item)
        payload = {"status": self._status, "cells": cells}
        return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))

    def get_cell_context(self, cell_index: int, max_chars: int = 8000) -> str:
        """Retourne une cellule ciblée complète, bornée indépendamment."""
        if not 0 <= cell_index < len(self._cached_notebook.cells):
            return json.dumps(
                {"error": f"index invalide: {cell_index}"},
                ensure_ascii=False,
            )
        cell = self._cached_notebook.cells[cell_index]
        source = cell.source
        payload = {
            "index": cell_index,
            "cell_type": cell.cell_type,
            "source": source[:max_chars],
            "source_chars": len(source),
            "source_truncated": len(source) > max_chars,
            "execution_count": cell.get("execution_count"),
            "output_summaries": self._output_summaries(cell),
        }
        return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))

    def log_notebook_state(self, max_length: int = 10000) -> None:
        """Affiche une partie bornée du JSON complet pour diagnostic local."""
        notebook_json = self.get_notebook_json()
        snippet = notebook_json[:max_length]
        if len(notebook_json) > max_length:
            snippet += ">>> TRUNCATED <<<"
        sanitized = _redact_paths(snippet)
        logger.debug(f"[NotebookState] Current notebook state (truncated, chemins rediges):\n{sanitized}")

    def path_leak_report(self) -> str:
        """Detection mecanique de fuite de chemin machine (issue #6443).

        Retourne une ligne par cellule dont un output contient un chemin que
        `_redact_paths` reecrit (Windows ou Unix) — vide si aucun. C'est le
        signal que validate_notebook consulte avant d'approuver : la garde
        ne repose pas seulement sur le prompt du Reviewer (cécité #6443).
        """
        lines = []
        for i, c in enumerate(self._cached_notebook.cells):
            flag = False
            for out in (c.get("outputs") or []):
                text = out.get("text", "")
                if isinstance(text, list):
                    text = "".join(text)
                if text and _redact_paths(text) != text:
                    flag = True
                    break
            if flag:
                lines.append(f"cellule {i:02d} : LEAK-CHEMIN")
        return "\n".join(lines)

    def get_status(self) -> str:
        """Renvoie l'état courant du notebook."""
        return self._status

    def set_status(self, new_status: str) -> None:
        """Met à jour l'état courant si la transition cible est valide."""
        if new_status not in self.VALID_STATES:
            logger.error(f"[NotebookState] État invalide: {new_status}")
            return
        logger.info(f"[NotebookState] Passage de l'état {self._status} → {new_status}")
        self._previous_states.append(self._status)
        self._status = new_status

    def reset_outputs(self) -> None:
        """Réinitialise les sorties et compteurs des cellules du notebook cible."""
        for cell in self._cached_notebook["cells"]:
            if "outputs" in cell:
                cell["outputs"] = []
            if "execution_count" in cell:
                cell["execution_count"] = None
        logger.debug("[NotebookState] Sorties réinitialisées dans le notebook.")

    def execute_notebook(self) -> bool:
        """Exécute réellement le notebook cible via Papermill."""
        logger.info(f"[NotebookState] Exécution Papermill sur {self.notebook_path}.")
        self.save_notebook()
        success = True
        try:
            pm.execute_notebook(
                input_path=self.notebook_path,
                output_path=self.notebook_path,
                kernel_name="python3",
                progress_bar=False,
                log_output=True,
            )
        except Exception as e:
            logger.error(f"[execute_notebook] Erreur lors de l'exécution: {e}")
            success = False
        finally:
            try:
                self._load_notebook()
                self.save_notebook()
                logger.info("[NotebookState] Notebook mis à jour après exécution.")
            except Exception as e:
                logger.error(f"[execute_notebook] Rechargement impossible après exécution: {e}")
                success = False
        return success

    def update_cell(self, cell_index: int, new_source: str) -> None:
        """Met à jour la source d'une cellule ciblée et sauvegarde le notebook."""
        old_src = self._cached_notebook.cells[cell_index].source
        self._cached_notebook.cells[cell_index].source = new_source
        self._revision += 1
        logger.info(f"[NotebookState] Mise à jour de la cellule {cell_index}")
        logger.debug(f"[NotebookState] Ancien contenu:\n{old_src}")
        logger.debug(f"[NotebookState] Nouveau contenu:\n{new_source}")
        self.save_notebook()

    def find_cell_indices_by_content(self, content_pattern: str) -> list:
        """Retourne les indices des cellules contenant le motif demandé."""
        return [
            index
            for index, cell in enumerate(self._cached_notebook.cells)
            if content_pattern in cell.source
        ]

    def is_approved(self) -> bool:
        """Renvoie True lorsque le notebook a atteint l'état validated."""
        return self._status == "validated"

Exercice 1 : Ajout d’une méthode de resume du notebook

La classe NotebookState permet de lire et modifier un notebook. Il manque une méthode qui genere un resume textuel utile pour le diagnostic.

Objectif : Ajouter une méthode get_summary() a NotebookState qui retourne un resume structure du notebook.

Indices : - # Étape 1 : Compter le nombre de cellules par type (code vs markdown) - # Étape 2 : Calculer la taille totale (en caractères) des cellules - # Étape 3 : Identifier la cellule la plus longue et la plus courte - # Indice : Utiliser len(cell.source) pour la taille et cell.cell_type pour le type

def get_notebook_summary(notebook_state: NotebookState) -> dict:
    """
    TODO etudiant : Generer un resume structure du notebook.
    
    Returns:
        dict avec les cles: total_cells, code_cells, markdown_cells,
        total_chars, longest_cell_index, shortest_cell_index, current_status
    """
    # TODO etudiant : analyser notebook_state._cached_notebook.cells
    return None

# Test avec le notebook charge :
# summary = get_notebook_summary(notebook_state)
# print(f"Cellules: {summary['total_cells']} ({summary['code_cells']} code, {summary['markdown_cells']} markdown)")
print("Exercice a completer")
Exercice a completer

4. Création (ou chargement) du Notebook cible + test de validité

Voici les étapes automatisées :

  1. Nous utilisons un template Notebook-Template.ipynb, ou bien le fichier .ipynb téléversé,
  2. Nous injectons une description de tâche (si elle est choisie aléatoirement, prédéfinie ou personnalisée),
  3. Nous exécutons le notebook pour vérifier qu’aucune erreur fatale n’apparaît,
  4. Nous validons l’intégrité pour confirmer que tout est correctement initialisé.

Si tout se passe bien, la phase d’orchestration multi-agents peut commencer.


# Cellule Code : Initialisation du notebook ciblé et injection de la tâche
# -----------------------------------------------------------------------
import shutil
import os
import requests

TEMPLATE_URL = "https://raw.githubusercontent.com/jsboige/CoursIA/refs/heads/main/MyIA.AI.Notebooks/GenAI/SemanticKernel/Notebook-Template.ipynb"
TEMPLATE_FILE = "Notebook-Template.ipynb"

def apply_task_description(notebook_state: NotebookState, task_description: str) -> bool:
    """
    Remplace le placeholder {{TASK_DESCRIPTION}} dans la première cellule Markdown appropriée.
    Retourne True si le placeholder a été trouvé et remplacé, False sinon.
    """
    for idx, cell in enumerate(notebook_state._cached_notebook.cells):
        if "{{TASK_DESCRIPTION}}" in cell.source:
            new_source = cell.source.replace("{{TASK_DESCRIPTION}}", task_description)
            notebook_state.update_cell(idx, new_source)
            return True
    return False

DEST_NOTEBOOK = "Notebook-Generated.ipynb"

# Gestion différenciée selon le mode sélectionné
if config['mode'] == 3:  # Mode upload
    if config['uploaded_file']:
        DEST_NOTEBOOK = config['uploaded_file'].name
        with open(DEST_NOTEBOOK, 'wb') as f:
            f.write(config['uploaded_file'].content)
        logger.info(f"Notebook uploadé : {DEST_NOTEBOOK}")
else:  # Modes template (aléatoire, bibliothèque, personnalisé)
    if not os.path.exists(TEMPLATE_FILE):
        print(f"{TEMPLATE_FILE} introuvable, téléchargement depuis {TEMPLATE_URL}")
        try:
            response = requests.get(TEMPLATE_URL, timeout=10)
            response.raise_for_status()  # Gère les erreurs HTTP
            with open(TEMPLATE_FILE, "wb") as f:
                f.write(response.content)
            print(f"Téléchargement terminé, fichier {TEMPLATE_FILE} créé.")
        except Exception as e:
            print(f"Échec du téléchargement : {e}")
    else:
        print(f"Le fichier {TEMPLATE_FILE} existe déjà, aucune action nécessaire.")
    
    shutil.copy2(TEMPLATE_FILE, DEST_NOTEBOOK)
    logger.info(f"Création depuis le template : {TEMPLATE_FILE}")

# Instanciation du notebook state
notebook_state = NotebookState(DEST_NOTEBOOK)
notebook_state.log_notebook_state()

# Injection de la tâche pour les modes template
if config['mode'] != 3:
    changed = apply_task_description(notebook_state, config['task_description'])
    
    if changed:
        logger.info("Placeholder {{TASK_DESCRIPTION}} remplacé avec succès.")
        logger.info("Exécution du notebook pour validation initiale...")
        notebook_state.execute_notebook()
        logger.info("Notebook ré-exécuté après injection de la tâche.")
        notebook_state.log_notebook_state()
    else:
        logger.warning("Aucun placeholder détecté. Vérifiez la cellule Markdown contenant {{TASK_DESCRIPTION}}.")
else:
    logger.info("Mode upload - Aucune injection de tâche nécessaire")
Le fichier Notebook-Template.ipynb existe déjà, aucune action nécessaire.

5. Architecture de plugins : extension du NotebookState

Nous définissons des plugins pour manipuler NotebookState :

  • BaseNotebookPlugin : expose en lecture le notebook (méthode get_notebook_content()).
  • CoderNotebookPlugin : permet de modifier des cellules de code et de signaler la fin d’implémentation.
  • ReviewerNotebookPlugin : exécute le notebook et décide d’approuver ou de refuser.
  • AdminNotebookPlugin : finalise ou rejette le notebook, et peut éditer les cellules Markdown.

Chaque plugin est un ensemble de fonctions décorées (@kernel_function), utilisables par les agents via Semantic Kernel.

# ================================
# Plugins actualisés (avec logs)
# ================================
class BaseNotebookPlugin:
    """Plugin de lecture fournissant un contexte borné aux agents."""

    def __init__(self, state: NotebookState) -> None:
        self.state = state
        self._get_content_counter = 0

    @kernel_function(
        name="get_notebook_content",
        description=(
            "Renvoie l'inventaire borné du notebook. Avec cell_index >= 0, "
            "renvoie la cellule ciblée complète et ses sorties résumées."
        )
    )
    def get_notebook_content(self, cell_index: int = -1) -> str:
        """Retourne l'inventaire ou une cellule ciblée, sans notebook brut."""
        self._get_content_counter += 1
        if cell_index >= 0:
            content = self.state.get_cell_context(cell_index)
            scope = f"cellule {cell_index}"
        else:
            content = self.state.get_agent_context()
            scope = "inventaire"
        logger.info(
            "[BaseNotebookPlugin] get_notebook_content() "
            f"appel n°{self._get_content_counter}, {scope}, "
            f"{len(content)} caractères"
        )
        return content


class NotebookEditingMixin:
    """Mixin fournissant l'édition ciblée de cellules."""

    def update_cell_anyway(
        self,
        pattern: str,
        new_source: str,
        cell_type: str = None,
    ) -> str:
        """Remplace une cellule unique correspondant au motif et au type."""
        indices = []
        for idx, cell in enumerate(self.state._cached_notebook.cells):
            content_match = pattern in cell.source
            type_match = (cell_type is None) or (cell.cell_type == cell_type)
            if content_match and type_match:
                indices.append(idx)

        if not indices:
            return_message = (
                f"Aucune cellule ({cell_type or 'tout type'}) ne contient '{pattern}'."
            )
        elif len(indices) > 1:
            return_message = (
                f"Plusieurs cellules ({cell_type or 'tout type'}) contiennent "
                f"'{pattern}'."
            )
        elif self.state.get_status() == "validated":
            return_message = "Édition impossible (notebook déjà validé)."
        else:
            self.state.update_cell(indices[0], new_source)
            return_message = (
                f"Cellule {cell_type} contenant '{pattern}' mise à jour."
            )

        logger.info(
            f"[NotebookEditingMixin] update_cell_anyway -> {return_message}"
        )
        return return_message


class CoderNotebookPlugin(BaseNotebookPlugin, NotebookEditingMixin):
    """Plugin de lecture et d'édition des cellules de code."""

    @kernel_function(
        name="update_cell_by_content",
        description="Modifie la première cellule de code contenant content_pattern.",
    )
    def update_cell_by_content(
        self,
        content_pattern: str,
        new_source: str,
    ) -> str:
        logger.info("[CoderNotebookPlugin] update_cell_by_content()")
        status = self.state.get_status()
        if status not in ["specified", "implemented"]:
            msg = f"Erreur: état '{status}' => modifications bloquées."
        else:
            msg = self.update_cell_anyway(
                pattern=content_pattern,
                new_source=new_source,
                cell_type="code",
            )
        logger.info(f"[CoderNotebookPlugin] update_cell_by_content -> {msg}")
        return msg

    @kernel_function(
        name="finish_implementation",
        description="Déclare le notebook implemented lorsque le code est prêt.",
    )
    def finish_implementation(self) -> str:
        logger.info("[CoderNotebookPlugin] finish_implementation()")
        status = self.state.get_status()
        if status == "specified":
            self.state.set_status("implemented")
            msg = "Le notebook passe à l'état 'implemented'."
        elif status == "implemented":
            msg = "Le notebook est déjà en état 'implemented'."
        else:
            msg = f"Impossible de passer en 'implemented' depuis '{status}'."
        logger.info(f"[CoderNotebookPlugin] finish_implementation -> {msg}")
        return msg


class ReviewerNotebookPlugin(BaseNotebookPlugin):
    """Plugin de lecture et d'exécution réelle pour le Reviewer."""

    @kernel_function(
        name="validate_notebook",
        description="Exécute le notebook et l'approuve ou le refuse.",
    )
    def validate_notebook(self, approve: bool = True) -> str:
        logger.info(
            f"[ReviewerNotebookPlugin] validate_notebook(approve={approve})"
        )
        status = self.state.get_status()
        if status != "implemented":
            msg = f"Le reviewer ne peut pas valider, état actuel = '{status}'."
        else:
            success = self.state.execute_notebook()
            if not success:
                self.state.set_status("specified")
                msg = (
                    "Erreur d'exécution dans le notebook (voir logs). "
                    "Retour à l'état 'specified' pour corrections."
                )
            elif approve:
                leak = self.state.path_leak_report()
                if leak:
                    self.state.set_status("specified")
                    msg = (
                        "Refus LEAK-CHEMIN : output contenant un chemin machine "
                        "détecté. Retour à 'specified' pour nettoyage. Détail : "
                        + leak
                    )
                else:
                    self.state.set_status("tested")
                    msg = "Le reviewer approuve => état 'tested'."
            else:
                self.state.set_status("specified")
                msg = "Le reviewer refuse => retour à 'specified'."
        logger.info(f"[ReviewerNotebookPlugin] validate_notebook -> {msg}")
        return msg


class AdminNotebookPlugin(BaseNotebookPlugin, NotebookEditingMixin):
    """Plugin d'administration : édition Markdown et validation finale."""

    @kernel_function(
        name="update_markdown_cell",
        description="Modifie la première cellule Markdown contenant content_pattern.",
    )
    def admin_edit_markdown_cell(
        self,
        content_pattern: str,
        new_source: str,
    ) -> str:
        logger.info("[AdminNotebookPlugin] admin_edit_markdown_cell()")
        msg = self.update_cell_anyway(
            pattern=content_pattern,
            new_source=new_source,
            cell_type="markdown",
        )
        self.state.set_status("specified")
        logger.info(f"[AdminNotebookPlugin] admin_edit_markdown_cell -> {msg}")
        return msg + " -> Revert à 'specified'."

    @kernel_function(
        name="approve_notebook",
        description="Valide définitivement le notebook si son état est tested.",
    )
    def approve_notebook(self, admin_ok: bool = True) -> str:
        logger.info(
            f"[AdminNotebookPlugin] approve_notebook(admin_ok={admin_ok})"
        )
        status = self.state.get_status()
        if status != "tested":
            msg = f"Impossible d'approuver: état '{status}' (attendu: 'tested')."
        elif admin_ok:
            self.state.set_status("validated")
            msg = "Notebook validé => état 'validated'."
        else:
            self.state.set_status("specified")
            msg = "Admin refuse => retour à 'specified'."
        logger.info(f"[AdminNotebookPlugin] approve_notebook -> {msg}")
        return msg

Exercice 2 : Plugin de validation syntaxique

Le ReviewerNotebookPlugin execute le notebook via Papermill pour detecter les erreurs. Cependant, une validation syntaxique prealable pourrait eviter des exécutions inutiles.

Objectif : Créer une méthode check_syntax() dans une classe SyntaxCheckerPlugin qui verifie la syntaxe Python des cellules de code sans les executer.

Indices : - # Étape 1 : Utiliser ast.parse() du module standard ast pour verifier la syntaxe - # Étape 2 : Iterer sur les cellules du notebook et tester uniquement les cellules de type “code” - # Étape 3 : Retourner un rapport listant les cellules avec erreurs (index + message d’erreur) - # Indice : Capturer SyntaxError et IndentationError pour chaque cellule

import ast

class SyntaxCheckerPlugin(BaseNotebookPlugin):
    """
    TODO etudiant : Plugin de validation syntaxique.
    """
    
    @kernel_function(
        name="check_syntax",
        description="Verifie la syntaxe Python de toutes les cellules de code."
    )
    def check_syntax(self) -> str:
        """
        TODO etudiant : Verifier la syntaxe de chaque cellule de code.
        Retourner un rapport avec les erreurs detectees.
        """
        # TODO etudiant : iterer sur self.state._cached_notebook.cells
        # et utiliser ast.parse() sur chaque cellule de type "code"
        return None

print("Exercice a completer")
Exercice a completer

6. Stratégies d’orchestration

Deux stratégies principales :

  1. ApprovedBasedTerminationStrategy
    • Met fin à la conversation dès que le notebook est validé, ou si un nombre maximal d’itérations est atteint.
  2. NotebookAwareSelectionStrategy
    • Sélectionne l’agent en fonction de l’état courant du notebook :
      • specified ⇒ CoderAgent
      • implemented ⇒ ReviewerAgent
      • tested ⇒ AdminAgent
      • validated ⇒ plus d’agent (arrêt de la conversation)
    • Tient aussi compte de la première intervention pour laisser l’Admin faire ses modifications initiales.
from pydantic import PrivateAttr


class ApprovedBasedTerminationStrategy(TerminationStrategy):
    """Arrête la conversation après validation ou épuisement du budget."""

    _state: NotebookState = PrivateAttr()
    _max_steps: int = PrivateAttr(default=24)

    def __init__(self, state: NotebookState, max_steps: int = 24):
        super().__init__()
        self._state = state
        self._max_steps = max_steps
        self._current_step = 0

    async def should_agent_terminate(self, agent, history) -> bool:
        self._current_step += 1
        is_approved = self._state.is_approved()
        logger.debug(
            f"[TerminationStrategy] Step={self._current_step}/"
            f"{self._max_steps}, IsApproved={is_approved}"
        )
        if is_approved:
            logger.info("[TerminationStrategy] Notebook approuvé => arrêt.")
            return True
        if self._current_step >= self._max_steps:
            logger.warning(
                f"[TerminationStrategy] max_steps={self._max_steps} atteint => arrêt."
            )
            return True
        return False


class NotebookAwareSelectionStrategy(SelectionStrategy):
    """Sélectionne l'agent à partir de l'état courant du notebook."""

    def __init__(self, state: NotebookState):
        super().__init__()
        self._state = state
        self._has_first_agent_run = False
        self._reviewed_revision = -1

    def reset(self) -> None:
        self._has_first_agent_run = False
        self._reviewed_revision = -1

    def _has_code_placeholders(self) -> bool:
        """Détecte les cellules code encore réduites au marqueur du template."""
        for cell in self._state._cached_notebook.cells:
            source = cell.source.strip()
            if (
                cell.cell_type == "code"
                and source.startswith("# Cellule ")
                and "\n" not in source
            ):
                return True
        return False

    async def select_agent(self, agents, history):
        current_status = self._state.get_status()
        logger.debug(
            f"[SelectionStrategy] nb_agents={len(agents)}, "
            f"statut={current_status}, "
            f"first_run={not self._has_first_agent_run}, "
            f"revision={self._state._revision}, "
            f"reviewed_revision={self._reviewed_revision}"
        )

        if current_status == "validated":
            logger.info("[SelectionStrategy] Notebook déjà validé => fin.")
            return None

        coder = next((a for a in agents if a.name == "CoderAgent"), None)
        reviewer = next(
            (a for a in agents if a.name == "ReviewerAgent"),
            None,
        )
        admin = next((a for a in agents if a.name == "AdminAgent"), None)

        if not self._has_first_agent_run and current_status == "specified" and admin:
            self._has_first_agent_run = True
            logger.info("Première intervention : AdminAgent.")
            return admin

        if (
            current_status == "specified"
            and self._has_first_agent_run
            and not self._has_code_placeholders()
            and self._state._revision > self._reviewed_revision
        ):
            self._state.set_status("implemented")
            current_status = "implemented"
            logger.info(
                "Une édition réelle est disponible depuis la dernière revue : "
                "transition déterministe vers implemented."
            )

        if current_status == "specified" and coder:
            selected = coder
        elif current_status == "implemented" and reviewer:
            self._reviewed_revision = self._state._revision
            selected = reviewer
        elif current_status == "tested" and admin:
            selected = admin
        else:
            logger.warning(
                f"[SelectionStrategy] Aucun agent pour état='{current_status}'."
            )
            selected = None

        if selected and not self._has_first_agent_run:
            self._has_first_agent_run = True
        if selected:
            logger.info(
                f"[SelectionStrategy] Agent sélectionné : {selected.name}"
            )
        return selected

7. Création des 3 agents (Coder, Reviewer, Admin)

  • Chaque agent dispose d’un kernel indépendant, relié à un plugin dédié,
  • Le service ChatCompletion (OpenAI) ou un endpoint custom est configuré via le .env,
  • Nous activons le comportement « Auto » pour le choix et l’appel des fonctions,
  • Les rôles :
    • CoderAgent : implémente les cellules de code,
    • ReviewerAgent : exécute et valide (ou non) après relecture,
    • AdminAgent : valide, invalide, ou réédite les spécifications dans le Markdown.
# Cellule Code : Création des 3 agents et configuration avec prise en compte du .env
# ---------------------------------------------------------------------------------
from openai import AsyncOpenAI
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
import os
from dotenv import load_dotenv

load_dotenv("../.env")
openai_api_key = os.getenv("OPENAI_API_KEY")
openai_base_url_from_env = os.getenv("OPENAI_BASE_URL", "").strip()
openai_chat_model_id = os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-5.6-luna")


def create_chat_completion_service(service_id: str = "default"):
    """
    Crée une instance de ChatCompletion (OpenAI) en fonction
    des variables d'environnement. Gère explicitement l'URL par défaut pour éviter
    les erreurs de protocole non supporté.

    Variables .env utilisées :
     - OPENAI_API_KEY       : Clé API (obligatoire).
     - OPENAI_CHAT_MODEL_ID : Nom du modèle (ex: "gpt-5.6-terra", "gpt-5.6-luna").
     - OPENAI_BASE_URL      : URL personnalisée (optionnelle). Si vide, utilise l'API OpenAI officielle.
    """

    if not openai_api_key:
        # Il est crucial d'avoir une clé API
        logger.error("La variable d'environnement OPENAI_API_KEY n'est pas définie !")
        raise ValueError("OPENAI_API_KEY is not set in the environment.")

    # Déterminer l'URL de base finale à utiliser
    if openai_base_url_from_env:
        # Un endpoint personnalisé est défini dans le .env
        final_base_url = openai_base_url_from_env
        logger.info(f"Utilisation d'un endpoint compatible OpenAI : {final_base_url}")
    else:
        # Pas d'endpoint personnalisé, utiliser l'API officielle avec URL explicite
        final_base_url = "https://api.openai.com/v1"
        logger.info(f"Utilisation du service OpenAI officiel (URL explicite: {final_base_url}).")

    # Créer un client AsyncOpenAI configuré avec l'URL finale et la clé
    # Ce client sera passé au service Semantic Kernel
    try:
        openai_async_client = AsyncOpenAI(
            api_key=openai_api_key,
            base_url=final_base_url
            # Vous pouvez ajouter d'autres paramètres ici si nécessaire (ex: timeout)
        )
    except Exception as client_error:
        logger.error(f"Erreur lors de la création du client AsyncOpenAI pour base_url='{final_base_url}': {client_error}")
        raise

    # Instancier le service Semantic Kernel en lui passant le client pré-configuré
    try:
        sk_service = OpenAIChatCompletion(
            service_id=service_id,
            ai_model_id=openai_chat_model_id,
            async_client=openai_async_client # Important: passer le client configuré
            # api_key=... n'est pas nécessaire ici car géré par le client
        )
        logger.debug(f"Service Semantic Kernel '{service_id}' créé pour le modèle '{openai_chat_model_id}' pointant vers '{final_base_url}'")
        return sk_service
    except Exception as sk_service_error:
        logger.error(f"Erreur lors de la création du service OpenAIChatCompletion de Semantic Kernel: {sk_service_error}")
        raise

def create_kernel_for_agent(agent_id: str, plugin_instance) -> Kernel:
    """
    Instancie un Kernel SemanticKernel pour l'agent donné,
    en y ajoutant le plugin associé (Coder, Reviewer ou Admin).
    Utilise create_chat_completion_service() pour choisir le backend OpenAI
    (officiel ou endpoint custom) en fonction du .env.
    """
    k = Kernel()

    # Instancier le service de ChatCompletion en fonction du .env
    chat_service = create_chat_completion_service(service_id="default")

    # Ajouter ce service au kernel
    k.add_service(chat_service)

    # Ajouter le plugin de l'agent (Coder, Reviewer, Admin)
    k.add_plugin(plugin_instance, plugin_name=f"{agent_id}_plugin")

    return k

# Instancier les plugins (basés sur NotebookState)
coder_plugin = CoderNotebookPlugin(notebook_state)
reviewer_plugin = ReviewerNotebookPlugin(notebook_state)
admin_plugin = AdminNotebookPlugin(notebook_state)

# Créer un kernel dédié par agent (en tenant compte du .env)
coder_kernel = create_kernel_for_agent("coder_kernel", coder_plugin)
reviewer_kernel = create_kernel_for_agent("reviewer_kernel", reviewer_plugin)
admin_kernel = create_kernel_for_agent("admin_kernel", admin_plugin)

# Configurer l'auto-appel de fonctions
coder_settings = coder_kernel.get_prompt_execution_settings_from_service_id("default")
coder_settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
# Famille gpt-5.6 : le reasoning par defaut est incompatible avec les function tools
# sur /v1/chat/completions -- l API exige une desactivation explicite (reasoning_effort=none).
coder_settings.extra_body = {"reasoning_effort": "none"}

reviewer_settings = reviewer_kernel.get_prompt_execution_settings_from_service_id("default")
reviewer_settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
# Famille gpt-5.6 : le reasoning par defaut est incompatible avec les function tools
# sur /v1/chat/completions -- l API exige une desactivation explicite (reasoning_effort=none).
reviewer_settings.extra_body = {"reasoning_effort": "none"}

admin_settings = admin_kernel.get_prompt_execution_settings_from_service_id("default")
admin_settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
# Famille gpt-5.6 : le reasoning par defaut est incompatible avec les function tools
# sur /v1/chat/completions -- l API exige une desactivation explicite (reasoning_effort=none).
admin_settings.extra_body = {"reasoning_effort": "none"}

# Définition des 3 agents conversationnels
coder_agent = ChatCompletionAgent(
    kernel=coder_kernel,
    name="CoderAgent",
    instructions=(
        "Vous êtes le **Coder**. Votre rôle : Implémenter avec le plus grand soin dans les cellules de code (pas la markdown) les instructions établies dans les cellules de markdown du notebook\n"
        "1) Consultez d’abord l’inventaire via get_notebook_content().\n"
        "2) Avant de modifier une cellule, relisez-la intégralement avec get_notebook_content(cell_index). Ne reconstruisez jamais une cellule depuis source_preview.\n"
        "3) Modifiez uniquement les cellules de code avec update_cell_by_content(), en préservant imports, variables et marqueurs existants.\n"
        "4) Préservez les marqueurs importants afin de permettre les éditions futures.\n"
        "5) Quand le code est prêt, appelez finish_implementation().\n"
        "6) Si le Reviewer refuse, relisez la cellule signalée avant toute nouvelle édition.\n"
        "7) INTERDICTION d'installation pip : ne générez JAMAIS de cellule d'installation de packages (`%pip install`, `pip install`, `_ensure_package`, appel subprocess) — les packages standard (numpy, pandas, matplotlib, seaborn, scikit-learn…) sont pré-installés dans l'environnement d'exécution. Utilisez leurs fonctionnalités natives (ex. PdfPages de matplotlib pour un PDF, zipfile/os/shutil pour l'archivage). Une installation inline rend le notebook non reproductible et fait fuiter des chemins machine dans les outputs (issue #12478).\n"
        "8) Chemins : n'imprimez JAMAIS de chemins absolus dans les sorties du notebook généré (fuite du nom d'utilisateur, issue #6443). Utilisez des chemins relatifs au répertoire de travail, ou n'affichez que les noms de fichiers (os.path.basename).\n"
    ),
    arguments=KernelArguments(settings=coder_settings)
)

reviewer_agent = ChatCompletionAgent(
    kernel=reviewer_kernel,
    name="ReviewerAgent",
    instructions=(
        "Vous êtes le **Reviewer**. Votre rôle: vérifier avec le plus grand soin le travail du codeur, l'aiguiller pour d'éventuelles corrections, et valider son travail quand il est parfait.\n"
        "1) Consultez l’inventaire avec get_notebook_content(), puis relisez avec get_notebook_content(cell_index) chaque cellule utile à votre verdict.\n"
        "2) Appelez validate_notebook(approve=True/False) selon la qualité.\n"
        "   - True => passage à 'tested'\n"
        "   - False => retour à 'specified'\n"
        "3) Vérifiez les erreurs d’exécution et l’alignement code/Markdown sans demander le notebook brut.\n"
        "4) Hygiène outputs (issue #6443) : refusez (approve=False) toute installation inline (`%pip`, subprocess) et tout output contenant un chemin machine absolu (répertoire utilisateur Windows ou Unix, lettre de lecteur) — validate_notebook refuse automatiquement (marqueur LEAK-CHEMIN) tant qu'un chemin machine apparaît dans un output. Donnez au Coder un message explicite : « un chemin machine est présent dans les outputs — imprime des chemins relatifs ou des basenames, jamais des chemins absolus, et re-itère ».\n"
    ),
    arguments=KernelArguments(settings=reviewer_settings)
)

admin_agent = ChatCompletionAgent(
    kernel=admin_kernel,
    name="AdminAgent",
    instructions=(
        "Vous êtes l'**Admin**. Vous êtes en charge de la réalisation du notebook et vous faites preuve d'un grand niveau d'exigence avec votre équipe pour assurer un rendu de la plus grande qualité.\n"
        "1) Au début, consultez l’inventaire borné. Ne modifiez le Markdown que si une ambiguïté réelle l’exige ; relisez alors la cellule ciblée avec get_notebook_content(cell_index).\n"
        "Corrigez, reformulez, Détaillez les explications dans toutes les cellules de Markdown (pas le code) où c'est possible de sorte que les cellules de code pourront être implémentées, corrigées ou finalisées sans ambiguïté. Vous pouvez éditer les cellules de markdown en utilisant la fonction admin_edit_cell_by_content. Les explications fournies dans le Markdown doivent être didactiques, détaillées et limpides. \n"
        "2) Après chaque édition de votre part, l'agent codeur est solicité pour l'implémentation des cellules de code.\n"
        "3) Quand l'agent reviewer a validé les éditions du codeur, vous pouvez à nouveau l'éditer ou le valider ou l'invalider via la fonction approve_notebook(admin_ok=True/False). Soyez intransigeant et n'hésitez pas à dédire le reviewer en affinant les instructions pour le codeur tant que le résultat n'est pas de très haute qualité.\n"
        "4) Une fois satisfait, validez définitivement (admin_ok=True) pour passer l'état à 'validated'.\n"
    ),
    arguments=KernelArguments(settings=admin_settings)
)



# Mettre en place la stratégie de sélection & la stratégie d'arrêt
termination_strategy = ApprovedBasedTerminationStrategy(notebook_state, max_steps=24)
selection_strategy = NotebookAwareSelectionStrategy(notebook_state)

# GroupChat pour orchestrer la conversation
group_chat = AgentGroupChat(
    agents=[coder_agent, reviewer_agent, admin_agent],
    selection_strategy=selection_strategy,
    termination_strategy=termination_strategy
)

logger.info("Agents créés et group_chat initialisé avec instructions mises à jour.")

8. Boucle de conversation

Nous lançons enfin la conversation multi-agents :

  • À chaque itération, l’agent sélectionné dépend de l’état (specified, implemented, tested, validated).
  • Les agents peuvent appeler leurs plugins (ex. : update_cell_by_content, validate_notebook, approve_notebook, etc.).
  • La conversation s’arrête dès que le notebook est validé ou si le quota d’itérations est dépassé.

Le statut final du notebook (approuvé ou non) est alors visible dans les logs et dans son contenu.

import asyncio


class StepBufferHandler(logging.Handler):
    """
    Tamponne les records du logger et les émet en un seul bloc par step.

    Avec le StreamHandler par défaut, chaque record de log devient un objet
    de sortie distinct : une conversation multi-agents en produit plus de
    200, ce qui fait régresser le ratchet output-flood à chaque
    re-exécution. Ce handler conserve chaque ligne, dans l'ordre, mais
    les regroupe en un bloc par step (les appels d'outils d'un agent
    arrivent avant son marqueur [STEP n] et restent donc dans son bloc).
    """

    def __init__(self, formatter: logging.Formatter):
        super().__init__()
        self.setLevel(logging.DEBUG)
        self.setFormatter(formatter)
        self._buffer = []

    def emit(self, record: logging.LogRecord) -> None:
        self._buffer.append(self.format(record))

    def flush_block(self) -> None:
        if self._buffer:
            print("\n".join(self._buffer))
            self._buffer = []


async def run_conversation():
    """Lance la conversation avec un contexte initial borné et un budget de tours."""
    stream_handler = logger.handlers[0]
    step_handler = StepBufferHandler(stream_handler.formatter)
    logger.removeHandler(stream_handler)
    logger.addHandler(step_handler)
    try:
        logger.info("Version initiale du notebook :")
        notebook_state.log_notebook_state()

        initial_context = notebook_state.get_agent_context()
        logger.info(
            f"Contexte initial borné : {len(initial_context)} caractères "
            "(sans métadonnées Papermill ni sorties complètes)."
        )
        group_chat.history.add_system_message(
            f"NOTEBOOK CONTEXT (snapshot borné):\n{initial_context}"
        )
        group_chat.history.add_user_message(
            "Finalisez ce notebook de manière entièrement autonome. Remplissez "
            "toutes les cellules code encore réduites à un placeholder '# Cellule N'. "
            "Après chaque lot d'éditions, appelez finish_implementation(). Si le "
            "Reviewer refuse ou si Papermill trouve une erreur, corrigez la cellule "
            "signalée puis rappelez finish_implementation() sans demander de "
            "confirmation humaine ni attendre une réexécution externe. Continuez "
            "jusqu'à la validation finale par l'Admin."
        )

        logger.info("=== Début de la conversation entre agents ===")
        step_handler.flush_block()  # bloc d'introduction (version initiale + contexte borné)
        iteration = 0

        async for message in group_chat.invoke():
            iteration += 1
            # Les appels de plugins journalisent déjà chaque action utile. Ne pas recopier
            # ici le contenu LLM : il peut contenir du code généré, des chemins ou des
            # messages d'erreur hypothétiques qui seraient alors confondus avec le résultat
            # réel de l'exécution. On conserve une trace structurelle vérifiable.
            content_length = len(str(message.content))
            logger.info(
                f"[STEP {iteration} - {message.name}] réponse reçue "
                f"({content_length} caractères)."
            )
            step_handler.flush_block()  # un bloc par step : outils de l'agent + marqueur

            if notebook_state.is_approved():
                logger.info("Notebook approuvé => fin de la conversation.")
                break

        logger.info("Version finale du notebook, après la conversation :")
        notebook_state.log_notebook_state(max_length=20000)

    except Exception as e:
        logger.error(f"Erreur inattendue: {str(e)}")
    finally:
        logger.info(f"Statut final - Approuvé: {notebook_state.is_approved()}")
        logger.info("=== Fin de la conversation ===")
        step_handler.flush_block()  # bloc de clôture (version finale + statut)
        logger.removeHandler(step_handler)
        logger.addHandler(stream_handler)


await run_conversation()
10:35:35 [INFO] Orchestration - Version initiale du notebook :

10:35:35 [DEBUG] Orchestration - [NotebookState] Current notebook state (truncated, chemins rediges):

{

  "cells": [

    {

      "cell_type": "markdown",

      "id": "516d2854",

      "metadata": {

        "papermill": {

          "duration": 0.001617,

          "end_time": "2026-09-09T08:35:34.588848",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.587231",

          "status": "completed"

        },

        "tags": []

      },

      "source": "**Navigation** : [Index](README.md)\n\n# Notebook de travail\n\nCe notebook est généré de façon incrémentale pour accomplir la tâche décrite ci-dessous.\n\n## Objectif du Notebook\n\nDans cette section, nous décrivons l'objectif du notebook, sa fonction, les outils à utiliser et les buts à atteindre.\n\n### Tâche originale\n\nVoilà la tâche telle qu'elle a été initialement formulée :\n\nSimple notebook creation\n\n\n### Interprétation et sous-objectifs\n\nDécrire ici l'interprétation de la tâche et les étapes prévues pour la réaliser.\n\n## 0. Installation des dépendances\n\nVérifiez l'environnement et utilisez '%pip install --quiet' dans la cellule de code suivante pour installer les packages nécessaires. "

    },

    {

      "cell_type": "code",

      "execution_count": 1,

      "id": "10e85dc0",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:35:34.593189Z",

          "iopub.status.busy": "2026-09-09T08:35:34.592842Z",

          "iopub.status.idle": "2026-09-09T08:35:34.596804Z",

          "shell.execute_reply": "2026-09-09T08:35:34.596166Z"

        },

        "papermill": {

          "duration": 0.007168,

          "end_time": "2026-09-09T08:35:34.597671",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.590503",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [],

      "source": "# Cellule 0"

    },

    {

      "cell_type": "markdown",

      "id": "df046193",

      "metadata": {

        "papermill": {

          "duration": 0.001445,

          "end_time": "2026-09-09T08:35:34.600897",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.599452",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 1. Préparation de l'environnement\n\nNous allons importer les composants nécessaires, et configurer la journalisation et l'instrumentation de l'environnement.\n\n"

    },

    {

      "cell_type": "code",

      "execution_count": 2,

      "id": "a22abc06",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:35:34.604862Z",

          "iopub.status.busy": "2026-09-09T08:35:34.604465Z",

          "iopub.status.idle": "2026-09-09T08:35:34.607604Z",

          "shell.execute_reply": "2026-09-09T08:35:34.606973Z"

        },

        "papermill": {

          "duration": 0.006101,

          "end_time": "2026-09-09T08:35:34.608557",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.602456",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [],

      "source": "# Cellule 1"

    },

    {

      "cell_type": "markdown",

      "id": "4f066e84",

      "metadata": {

        "papermill": {

          "duration": 0.001428,

          "end_time": "2026-09-09T08:35:34.611854",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.610426",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 2. Initialisation\n\nDans cette partie, nous créons les composants et importons les données nécessaires pour réaliser la tâche.\n\n"

    },

    {

      "cell_type": "code",

      "execution_count": 3,

      "id": "33440af5",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:35:34.615651Z",

          "iopub.status.busy": "2026-09-09T08:35:34.615437Z",

          "iopub.status.idle": "2026-09-09T08:35:34.618189Z",

          "shell.execute_reply": "2026-09-09T08:35:34.617687Z"

        },

        "papermill": {

          "duration": 0.005353,

          "end_time": "2026-09-09T08:35:34.618967",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.613614",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [],

      "source": "# Cellule 2"

    },

    {

      "cell_type": "markdown",

      "id": "8bca0c51",

      "metadata": {

        "papermill": {

          "duration": 0.003034,

          "end_time": "2026-09-09T08:35:34.623557",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.620523",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 3. Traitement\n\nIci, nous effectuons les manipulations et traitements nécessaires pour atteindre les résultats escomptés."

    },

    {

      "cell_type": "code",

      "execution_count": 4,

      "id": "bcee28ce",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:35:34.627495Z",

          "iopub.status.busy": "2026-09-09T08:35:34.627184Z",

          "iopub.status.idle": "2026-09-09T08:35:34.630730Z",

          "shell.execute_reply": "2026-09-09T08:35:34.630289Z"

        },

        "papermill": {

          "duration": 0.006295,

          "end_time": "2026-09-09T08:35:34.631442",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.625147",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [],

      "source": "# Cellule 3"

    },

    {

      "cell_type": "markdown",

      "id": "5957b743",

      "metadata": {

        "papermill": {

          "duration": 0.001349,

          "end_time": "2026-09-09T08:35:34.634352",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.633003",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 4. Analyse\n\nNous effectuons ici les vérifications, agrégats et visualisations nécessaires pour valider les résultats obtenus."

    },

    {

      "cell_type": "code",

      "execution_count": 5,

      "id": "9dfda7dc",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:35:34.638011Z",

          "iopub.status.busy": "2026-09-09T08:35:34.637809Z",

          "iopub.status.idle": "2026-09-09T08:35:34.640173Z",

          "shell.execute_reply": "2026-09-09T08:35:34.639782Z"

        },

        "papermill": {

          "duration": 0.00508,

          "end_time": "2026-09-09T08:35:34.640970",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.635890",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [],

      "source": "# Cellule 4"

    },

    {

      "cell_type": "markdown",

      "id": "332d027e",

      "metadata": {

        "papermill": {

          "duration": 0.002082,

          "end_time": "2026-09-09T08:35:34.644814",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.642732",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 5. Conclusion\n\nConcluons ici l'exécution et la validation de la tâche.\nRécapitulons les résultats obtenus. La cellule suivante peut servir de synthèse."

    },

    {

      "cell_type": "code",

      "execution_count": 6,

      "id": "fc01300b",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:35:34.648984Z",

          "iopub.status.busy": "2026-09-09T08:35:34.648624Z",

          "iopub.status.idle": "2026-09-09T08:35:34.651111Z",

          "shell.execute_reply": "2026-09-09T08:35:34.650660Z"

        },

        "papermill": {

          "duration": 0.005218,

          "end_time": "2026-09-09T08:35:34.651835",

          "exception": false,

          "start_time": "2026-09-09T08:35:34.646617",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [],

      "source": "# Cellule 5"

    }

  ],

  "metadata": {

    "cost": {

      "api_provider": "none",

      "api_usd_est": 0.0,

      "cpu_min": 0,

      "external_account": null,

      "free_alternative": "self",

      "gpu_min": 0,

      "gpu_required": false,

      "metadata_written": "2026-07-28",

      "network": false,

      "notes": "Template squelette (5 cellules de stub) pour instanciation par NotebookMaker. Pas d'API ni CPU effectif : squelette a remplir. Provenance: SK-10. Provenance: myia-po-2023:CoursIA-2 (c.946).",

      "qcc_tokens_est": 0,

      "reduced_pedagogical": null,

      "reproducibility": "MED",

      "validator": "manual",

      "vram_gb": 0,

      "vram_tier": "LITE"

    },

    "kernelspec": {

      "display_name": "Python 3",

      "language": "python",

      "name": "python3"

    },

    "language_info": {

      "codemirror_mode": {

        "name": "ipython",

        "version": 3

      },

      "file_extension": ".py",

      "mimetype": "text/x-python",

      "name": "python",

      "nbconvert_exporter": "python",

      "pygments_lexer": "ipython3",

      "version": "3.13.3"

    },

    "papermill": {

      "default_parameters": {},

      "duration": 1.688845,

      "end_time": "2026-09-09T08:35:34.884370",

      "environment_variables": {},

      "exception": null,

      "input_path": "Notebook-Generated.ipynb",

      "output_path": "Notebook-Generated.ipynb",

      "parameters": {},

      "start_time": "2026-09-09T08:35:33.195525",

      "version": "2.6.0"

    },

    "polyglot_notebook": {

      "kernelInfo": {

        "defaultKernelName": "csharp",

        "items": [

          {

            "aliases": [],

            "name": "csharp"

          }

        ]

      }

    }

  },

  "nbformat": 4,

  "nbformat_minor": 5

}

10:35:35 [INFO] Orchestration - Contexte initial borné : 2231 caractères (sans métadonnées Papermill ni sorties complètes).

10:35:35 [INFO] Orchestration - === Début de la conversation entre agents ===

10:35:35 [DEBUG] Orchestration - [SelectionStrategy] nb_agents=3, statut=specified, first_run=True, revision=1, reviewed_revision=-1

10:35:35 [INFO] Orchestration - Première intervention : AdminAgent.

10:35:38 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°1, inventaire, 2231 caractères

10:35:41 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°2, cellule 0, 851 caractères

10:35:41 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°3, cellule 2, 300 caractères

10:35:41 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°4, cellule 4, 269 caractères

10:35:41 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°5, cellule 6, 260 caractères

10:35:41 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°6, cellule 8, 265 caractères

10:35:41 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°7, cellule 10, 293 caractères

10:35:46 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell()

10:35:46 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 0

10:35:46 [DEBUG] Orchestration - [NotebookState] Ancien contenu:

**Navigation** : [Index](README.md)



# Notebook de travail



Ce notebook est généré de façon incrémentale pour accomplir la tâche décrite ci-dessous.



## Objectif du Notebook



Dans cette section, nous décrivons l'objectif du notebook, sa fonction, les outils à utiliser et les buts à atteindre.



### Tâche originale



Voilà la tâche telle qu'elle a été initialement formulée :



Simple notebook creation





### Interprétation et sous-objectifs



Décrire ici l'interprétation de la tâche et les étapes prévues pour la réaliser.



## 0. Installation des dépendances



Vérifiez l'environnement et utilisez '%pip install --quiet' dans la cellule de code suivante pour installer les packages nécessaires. 

10:35:46 [DEBUG] Orchestration - [NotebookState] Nouveau contenu:

**Navigation** : [Index](README.md)



# Notebook de travail minimal et reproductible



## Objectif du notebook



La demande (« *Simple notebook creation* ») ne prescrit ni jeu de données ni analyse métier. Ce notebook met donc en place un exemple autonome, déterministe et sans dépendance externe : il construit un petit jeu de données en mémoire, applique une transformation explicite, contrôle des invariants élémentaires, puis affiche une synthèse. Son exécution complète doit réussir dans un noyau Python standard.



## Déroulé



1. vérifier la version de Python et importer uniquement la bibliothèque standard ;

2. créer un jeu de données d'exemple ;

3. calculer un montant par ligne et des agrégats par catégorie ;

4. vérifier que les agrégats sont cohérents avec les données détaillées ;

5. présenter une conclusion lisible.



## 0. Installation des dépendances



Aucune installation n'est nécessaire : les cellules suivantes utilisent exclusivement la bibliothèque standard de Python. La première cellule de code doit simplement afficher les informations d'environnement utiles à la reproductibilité.

10:35:46 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb

10:35:46 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule markdown contenant '**Navigation** : [Index](README.md)



# Notebook de travail' mise à jour.

10:35:46 [INFO] Orchestration - [NotebookState] Passage de l'état specified → specified

10:35:46 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell -> Cellule markdown contenant '**Navigation** : [Index](README.md)



# Notebook de travail' mise à jour.

10:35:49 [DEBUG] Orchestration - [TerminationStrategy] Step=1/24, IsApproved=False

10:35:49 [INFO] Orchestration - [STEP 1 - AdminAgent] réponse reçue (254 caractères).

10:35:49 [DEBUG] Orchestration - [SelectionStrategy] nb_agents=3, statut=specified, first_run=False, revision=2, reviewed_revision=-1

10:35:49 [INFO] Orchestration - [SelectionStrategy] Agent sélectionné : CoderAgent

10:35:50 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°1, inventaire, 2228 caractères

10:35:52 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°2, cellule 1, 138 caractères

10:35:52 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°3, cellule 3, 138 caractères

10:35:52 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°4, cellule 5, 138 caractères

10:35:52 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°5, cellule 7, 138 caractères

10:35:52 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°6, cellule 9, 138 caractères

10:35:52 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°7, cellule 11, 139 caractères

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()

10:36:03 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 1

10:36:03 [DEBUG] Orchestration - [NotebookState] Ancien contenu:

# Cellule 0

10:36:03 [DEBUG] Orchestration - [NotebookState] Nouveau contenu:

# Cellule 0 — environnement et dépendances standard

import platform

from collections import defaultdict

from decimal import Decimal



PYTHON_VERSION = platform.python_version()

print(f"Environnement prêt : Python {PYTHON_VERSION} | dépendances externes : aucune")

10:36:03 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb

10:36:03 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 0' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 0' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()

10:36:03 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 3

10:36:03 [DEBUG] Orchestration - [NotebookState] Ancien contenu:

# Cellule 1

10:36:03 [DEBUG] Orchestration - [NotebookState] Nouveau contenu:

# Cellule 1 — constantes de l'exemple

CURRENCY = "EUR"

CATEGORY_LABELS = {

    "livres": "Livres",

    "papeterie": "Papeterie",

    "accessoires": "Accessoires",

}

print(f"Configuration chargée : devise={CURRENCY}, catégories={len(CATEGORY_LABELS)}")

10:36:03 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb

10:36:03 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 1' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 1' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()

10:36:03 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 5

10:36:03 [DEBUG] Orchestration - [NotebookState] Ancien contenu:

# Cellule 2

10:36:03 [DEBUG] Orchestration - [NotebookState] Nouveau contenu:

# Cellule 2 — jeu de données déterministe en mémoire

orders = [

    {"id": "CMD-001", "category": "livres", "quantity": 2, "unit_price": Decimal("12.50")},

    {"id": "CMD-002", "category": "papeterie", "quantity": 5, "unit_price": Decimal("1.80")},

    {"id": "CMD-003", "category": "livres", "quantity": 1, "unit_price": Decimal("24.00")},

    {"id": "CMD-004", "category": "accessoires", "quantity": 3, "unit_price": Decimal("7.25")},

]



assert all(order["category"] in CATEGORY_LABELS for order in orders)

assert all(order["quantity"] > 0 and order["unit_price"] >= 0 for order in orders)

print(f"Jeu de données créé : {len(orders)} commandes")

10:36:03 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb

10:36:03 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 2' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 2' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()

10:36:03 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 7

10:36:03 [DEBUG] Orchestration - [NotebookState] Ancien contenu:

# Cellule 3

10:36:03 [DEBUG] Orchestration - [NotebookState] Nouveau contenu:

# Cellule 3 — calcul des montants et agrégation par catégorie

for order in orders:

    order["amount"] = order["quantity"] * order["unit_price"]



amount_by_category = defaultdict(lambda: Decimal("0.00"))

quantity_by_category = defaultdict(int)

for order in orders:

    amount_by_category[order["category"]] += order["amount"]

    quantity_by_category[order["category"]] += order["quantity"]



summary_rows = [

    {

        "category": category,

        "label": CATEGORY_LABELS[category],

        "quantity": quantity_by_category[category],

        "amount": amount_by_category[category],

    }

    for category in sorted(amount_by_category)

]

print(f"Traitement terminé : {len(summary_rows)} catégories agrégées")

10:36:03 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb

10:36:03 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 3' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 3' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()

10:36:03 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 9

10:36:03 [DEBUG] Orchestration - [NotebookState] Ancien contenu:

# Cellule 4

10:36:03 [DEBUG] Orchestration - [NotebookState] Nouveau contenu:

# Cellule 4 — contrôles de cohérence et restitution analytique

detail_total = sum((order["amount"] for order in orders), Decimal("0.00"))

summary_total = sum((row["amount"] for row in summary_rows), Decimal("0.00"))



assert detail_total == summary_total, "Le total détaillé doit être égal au total agrégé."

assert len({order["id"] for order in orders}) == len(orders), "Les identifiants de commande doivent être uniques."

assert all(row["amount"] >= 0 for row in summary_rows)



print("Synthèse par catégorie")

for row in summary_rows:

    print(f"- {row['label']}: {row['quantity']} unités, {row['amount']:.2f} {CURRENCY}")

print(f"Total contrôlé : {summary_total:.2f} {CURRENCY}")

10:36:03 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb

10:36:03 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 4' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 4' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()

10:36:03 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 11

10:36:03 [DEBUG] Orchestration - [NotebookState] Ancien contenu:

# Cellule 5

10:36:03 [DEBUG] Orchestration - [NotebookState] Nouveau contenu:

# Cellule 5 — conclusion

result = {

    "status": "validé",

    "orders_processed": len(orders),

    "categories_processed": len(summary_rows),

    "total_amount": summary_total,

    "currency": CURRENCY,

}



print(

    "Exécution validée : "

    f"{result['orders_processed']} commandes, "

    f"{result['categories_processed']} catégories, "

    f"total de {result['total_amount']:.2f} {result['currency']}."

)

result

10:36:03 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb

10:36:03 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 5' mise à jour.

10:36:03 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 5' mise à jour.

10:36:04 [INFO] Orchestration - [CoderNotebookPlugin] finish_implementation()

10:36:04 [INFO] Orchestration - [NotebookState] Passage de l'état specified → implemented

10:36:04 [INFO] Orchestration - [CoderNotebookPlugin] finish_implementation -> Le notebook passe à l'état 'implemented'.

10:36:06 [DEBUG] Orchestration - [TerminationStrategy] Step=2/24, IsApproved=False

10:36:06 [INFO] Orchestration - [STEP 2 - CoderAgent] réponse reçue (201 caractères).

10:36:06 [DEBUG] Orchestration - [SelectionStrategy] nb_agents=3, statut=implemented, first_run=False, revision=8, reviewed_revision=-1

10:36:06 [INFO] Orchestration - [SelectionStrategy] Agent sélectionné : ReviewerAgent

10:36:07 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°1, inventaire, 4013 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°2, cellule 0, 1255 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°3, cellule 1, 398 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°4, cellule 2, 300 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°5, cellule 3, 402 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°6, cellule 4, 269 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°7, cellule 5, 850 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°8, cellule 6, 260 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°9, cellule 7, 887 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°10, cellule 8, 265 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°11, cellule 9, 842 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°12, cellule 10, 293 caractères

10:36:11 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() appel n°13, cellule 11, 582 caractères

10:36:12 [INFO] Orchestration - [ReviewerNotebookPlugin] validate_notebook(approve=True)

10:36:12 [INFO] Orchestration - [NotebookState] Exécution Papermill sur Notebook-Generated.ipynb.

10:36:12 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb

10:36:14 [DEBUG] Orchestration - [NotebookState] Chargé 'Notebook-Generated.ipynb' avec 12 cellules.

10:36:14 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb

10:36:14 [INFO] Orchestration - [NotebookState] Notebook mis à jour après exécution.

10:36:14 [INFO] Orchestration - [NotebookState] Passage de l'état implemented → tested

10:36:14 [INFO] Orchestration - [ReviewerNotebookPlugin] validate_notebook -> Le reviewer approuve => état 'tested'.

10:36:15 [DEBUG] Orchestration - [TerminationStrategy] Step=3/24, IsApproved=False

10:36:15 [INFO] Orchestration - [STEP 3 - ReviewerAgent] réponse reçue (131 caractères).

10:36:15 [DEBUG] Orchestration - [SelectionStrategy] nb_agents=3, statut=tested, first_run=False, revision=8, reviewed_revision=8

10:36:15 [INFO] Orchestration - [SelectionStrategy] Agent sélectionné : AdminAgent

10:36:17 [INFO] Orchestration - [AdminNotebookPlugin] approve_notebook(admin_ok=True)

10:36:17 [INFO] Orchestration - [NotebookState] Passage de l'état tested → validated

10:36:17 [INFO] Orchestration - [AdminNotebookPlugin] approve_notebook -> Notebook validé => état 'validated'.

10:36:18 [DEBUG] Orchestration - [TerminationStrategy] Step=4/24, IsApproved=True

10:36:18 [INFO] Orchestration - [TerminationStrategy] Notebook approuvé => arrêt.

10:36:18 [INFO] Orchestration - [STEP 4 - AdminAgent] réponse reçue (43 caractères).

10:36:18 [INFO] Orchestration - Notebook approuvé => fin de la conversation.

10:36:18 [INFO] Orchestration - Version finale du notebook, après la conversation :

10:36:18 [DEBUG] Orchestration - [NotebookState] Current notebook state (truncated, chemins rediges):

{

  "cells": [

    {

      "cell_type": "markdown",

      "id": "516d2854",

      "metadata": {

        "papermill": {

          "duration": 0.00245,

          "end_time": "2026-09-09T08:36:14.155828",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.153378",

          "status": "completed"

        },

        "tags": []

      },

      "source": "**Navigation** : [Index](README.md)\n\n# Notebook de travail minimal et reproductible\n\n## Objectif du notebook\n\nLa demande (« *Simple notebook creation* ») ne prescrit ni jeu de données ni analyse métier. Ce notebook met donc en place un exemple autonome, déterministe et sans dépendance externe : il construit un petit jeu de données en mémoire, applique une transformation explicite, contrôle des invariants élémentaires, puis affiche une synthèse. Son exécution complète doit réussir dans un noyau Python standard.\n\n## Déroulé\n\n1. vérifier la version de Python et importer uniquement la bibliothèque standard ;\n2. créer un jeu de données d'exemple ;\n3. calculer un montant par ligne et des agrégats par catégorie ;\n4. vérifier que les agrégats sont cohérents avec les données détaillées ;\n5. présenter une conclusion lisible.\n\n## 0. Installation des dépendances\n\nAucune installation n'est nécessaire : les cellules suivantes utilisent exclusivement la bibliothèque standard de Python. La première cellule de code doit simplement afficher les informations d'environnement utiles à la reproductibilité."

    },

    {

      "cell_type": "code",

      "execution_count": 1,

      "id": "10e85dc0",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:36:14.159988Z",

          "iopub.status.busy": "2026-09-09T08:36:14.159799Z",

          "iopub.status.idle": "2026-09-09T08:36:14.164919Z",

          "shell.execute_reply": "2026-09-09T08:36:14.164509Z"

        },

        "papermill": {

          "duration": 0.007944,

          "end_time": "2026-09-09T08:36:14.165832",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.157888",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [

        {

          "name": "stdout",

          "output_type": "stream",

          "text": "Environnement prêt : Python 3.13.3 | dépendances externes : aucune\n"

        }

      ],

      "source": "# Cellule 0 — environnement et dépendances standard\nimport platform\nfrom collections import defaultdict\nfrom decimal import Decimal\n\nPYTHON_VERSION = platform.python_version()\nprint(f\"Environnement prêt : Python {PYTHON_VERSION} | dépendances externes : aucune\")"

    },

    {

      "cell_type": "markdown",

      "id": "df046193",

      "metadata": {

        "papermill": {

          "duration": 0.001603,

          "end_time": "2026-09-09T08:36:14.169366",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.167763",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 1. Préparation de l'environnement\n\nNous allons importer les composants nécessaires, et configurer la journalisation et l'instrumentation de l'environnement.\n\n"

    },

    {

      "cell_type": "code",

      "execution_count": 2,

      "id": "a22abc06",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:36:14.174142Z",

          "iopub.status.busy": "2026-09-09T08:36:14.173931Z",

          "iopub.status.idle": "2026-09-09T08:36:14.177675Z",

          "shell.execute_reply": "2026-09-09T08:36:14.176765Z"

        },

        "papermill": {

          "duration": 0.00694,

          "end_time": "2026-09-09T08:36:14.178548",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.171608",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [

        {

          "name": "stdout",

          "output_type": "stream",

          "text": "Configuration chargée : devise=EUR, catégories=3\n"

        }

      ],

      "source": "# Cellule 1 — constantes de l'exemple\nCURRENCY = \"EUR\"\nCATEGORY_LABELS = {\n    \"livres\": \"Livres\",\n    \"papeterie\": \"Papeterie\",\n    \"accessoires\": \"Accessoires\",\n}\nprint(f\"Configuration chargée : devise={CURRENCY}, catégories={len(CATEGORY_LABELS)}\")"

    },

    {

      "cell_type": "markdown",

      "id": "4f066e84",

      "metadata": {

        "papermill": {

          "duration": 0.001692,

          "end_time": "2026-09-09T08:36:14.182175",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.180483",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 2. Initialisation\n\nDans cette partie, nous créons les composants et importons les données nécessaires pour réaliser la tâche.\n\n"

    },

    {

      "cell_type": "code",

      "execution_count": 3,

      "id": "33440af5",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:36:14.186657Z",

          "iopub.status.busy": "2026-09-09T08:36:14.186380Z",

          "iopub.status.idle": "2026-09-09T08:36:14.190646Z",

          "shell.execute_reply": "2026-09-09T08:36:14.190203Z"

        },

        "papermill": {

          "duration": 0.007548,

          "end_time": "2026-09-09T08:36:14.191448",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.183900",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [

        {

          "name": "stdout",

          "output_type": "stream",

          "text": "Jeu de données créé : 4 commandes\n"

        }

      ],

      "source": "# Cellule 2 — jeu de données déterministe en mémoire\norders = [\n    {\"id\": \"CMD-001\", \"category\": \"livres\", \"quantity\": 2, \"unit_price\": Decimal(\"12.50\")},\n    {\"id\": \"CMD-002\", \"category\": \"papeterie\", \"quantity\": 5, \"unit_price\": Decimal(\"1.80\")},\n    {\"id\": \"CMD-003\", \"category\": \"livres\", \"quantity\": 1, \"unit_price\": Decimal(\"24.00\")},\n    {\"id\": \"CMD-004\", \"category\": \"accessoires\", \"quantity\": 3, \"unit_price\": Decimal(\"7.25\")},\n]\n\nassert all(order[\"category\"] in CATEGORY_LABELS for order in orders)\nassert all(order[\"quantity\"] > 0 and order[\"unit_price\"] >= 0 for order in orders)\nprint(f\"Jeu de données créé : {len(orders)} commandes\")"

    },

    {

      "cell_type": "markdown",

      "id": "8bca0c51",

      "metadata": {

        "papermill": {

          "duration": 0.001568,

          "end_time": "2026-09-09T08:36:14.194924",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.193356",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 3. Traitement\n\nIci, nous effectuons les manipulations et traitements nécessaires pour atteindre les résultats escomptés."

    },

    {

      "cell_type": "code",

      "execution_count": 4,

      "id": "bcee28ce",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:36:14.199291Z",

          "iopub.status.busy": "2026-09-09T08:36:14.199088Z",

          "iopub.status.idle": "2026-09-09T08:36:14.203529Z",

          "shell.execute_reply": "2026-09-09T08:36:14.202924Z"

        },

        "papermill": {

          "duration": 0.007502,

          "end_time": "2026-09-09T08:36:14.204338",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.196836",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [

        {

          "name": "stdout",

          "output_type": "stream",

          "text": "Traitement terminé : 3 catégories agrégées\n"

        }

      ],

      "source": "# Cellule 3 — calcul des montants et agrégation par catégorie\nfor order in orders:\n    order[\"amount\"] = order[\"quantity\"] * order[\"unit_price\"]\n\namount_by_category = defaultdict(lambda: Decimal(\"0.00\"))\nquantity_by_category = defaultdict(int)\nfor order in orders:\n    amount_by_category[order[\"category\"]] += order[\"amount\"]\n    quantity_by_category[order[\"category\"]] += order[\"quantity\"]\n\nsummary_rows = [\n    {\n        \"category\": category,\n        \"label\": CATEGORY_LABELS[category],\n        \"quantity\": quantity_by_category[category],\n        \"amount\": amount_by_category[category],\n    }\n    for category in sorted(amount_by_category)\n]\nprint(f\"Traitement terminé : {len(summary_rows)} catégories agrégées\")"

    },

    {

      "cell_type": "markdown",

      "id": "5957b743",

      "metadata": {

        "papermill": {

          "duration": 0.001611,

          "end_time": "2026-09-09T08:36:14.207859",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.206248",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 4. Analyse\n\nNous effectuons ici les vérifications, agrégats et visualisations nécessaires pour valider les résultats obtenus."

    },

    {

      "cell_type": "code",

      "execution_count": 5,

      "id": "9dfda7dc",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:36:14.215636Z",

          "iopub.status.busy": "2026-09-09T08:36:14.215185Z",

          "iopub.status.idle": "2026-09-09T08:36:14.221187Z",

          "shell.execute_reply": "2026-09-09T08:36:14.220320Z"

        },

        "papermill": {

          "duration": 0.012746,

          "end_time": "2026-09-09T08:36:14.223651",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.210905",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [

        {

          "name": "stdout",

          "output_type": "stream",

          "text": "Synthèse par catégorie\n- Accessoires: 3 unités, 21.75 EUR\n- Livres: 3 unités, 49.00 EUR\n- Papeterie: 5 unités, 9.00 EUR\nTotal contrôlé : 79.75 EUR\n"

        }

      ],

      "source": "# Cellule 4 — contrôles de cohérence et restitution analytique\ndetail_total = sum((order[\"amount\"] for order in orders), Decimal(\"0.00\"))\nsummary_total = sum((row[\"amount\"] for row in summary_rows), Decimal(\"0.00\"))\n\nassert detail_total == summary_total, \"Le total détaillé doit être égal au total agrégé.\"\nassert len({order[\"id\"] for order in orders}) == len(orders), \"Les identifiants de commande doivent être uniques.\"\nassert all(row[\"amount\"] >= 0 for row in summary_rows)\n\nprint(\"Synthèse par catégorie\")\nfor row in summary_rows:\n    print(f\"- {row['label']}: {row['quantity']} unités, {row['amount']:.2f} {CURRENCY}\")\nprint(f\"Total contrôlé : {summary_total:.2f} {CURRENCY}\")"

    },

    {

      "cell_type": "markdown",

      "id": "332d027e",

      "metadata": {

        "papermill": {

          "duration": 0.002854,

          "end_time": "2026-09-09T08:36:14.230442",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.227588",

          "status": "completed"

        },

        "tags": []

      },

      "source": "## 5. Conclusion\n\nConcluons ici l'exécution et la validation de la tâche.\nRécapitulons les résultats obtenus. La cellule suivante peut servir de synthèse."

    },

    {

      "cell_type": "code",

      "execution_count": 6,

      "id": "fc01300b",

      "metadata": {

        "execution": {

          "iopub.execute_input": "2026-09-09T08:36:14.239390Z",

          "iopub.status.busy": "2026-09-09T08:36:14.239041Z",

          "iopub.status.idle": "2026-09-09T08:36:14.244878Z",

          "shell.execute_reply": "2026-09-09T08:36:14.244253Z"

        },

        "papermill": {

          "duration": 0.011914,

          "end_time": "2026-09-09T08:36:14.246022",

          "exception": false,

          "start_time": "2026-09-09T08:36:14.234108",

          "status": "completed"

        },

        "tags": []

      },

      "outputs": [

        {

          "name": "stdout",

          "output_type": "stream",

          "text": "Exécution validée : 4 commandes, 3 catégories, total de 79.75 EUR.\n"

        },

        {

          "data": {

            "text/plain": "{'status': 'validé',\n 'orders_processed': 4,\n 'categories_processed': 3,\n 'total_amount': Decimal('79.75'),\n 'currency': 'EUR'}"

          },

          "execution_count": 6,

          "metadata": {},

          "output_type": "execute_result"

        }

      ],

      "source": "# Cellule 5 — conclusion\nresult = {\n    \"status\": \"validé\",\n    \"orders_processed\": len(orders),\n    \"categories_processed\": len(summary_rows),\n    \"total_amount\": summary_total,\n    \"currency\": CURRENCY,\n}\n\nprint(\n    \"Exécution validée : \"\n    f\"{result['orders_processed']} commandes, \"\n    f\"{result['categories_processed']} catégories, \"\n    f\"total de {result['total_amount']:.2f} {result['currency']}.\"\n)\nresult"

    }

  ],

  "metadata": {

    "cost": {

      "api_provider": "none",

      "api_usd_est": 0.0,

      "cpu_min": 0,

      "external_account": null,

      "free_alternative": "self",

      "gpu_min": 0,

      "gpu_required": false,

      "metadata_written": "2026-07-28",

      "network": false,

      "notes": "Template squelette (5 cellules de stub) pour instanciation par NotebookMaker. Pas d'API ni CPU effectif : squelette a remplir. Provenance: SK-10. Provenance: myia-po-2023:CoursIA-2 (c.946).",

      "qcc_tokens_est": 0,

      "reduced_pedagogical": null,

      "reproducibility": "MED",

      "validator": "manual",

      "vram_gb": 0,

      "vram_tier": "LITE"

    },

    "kernelspec": {

      "display_name": "Python 3",

      "language": "python",

      "name": "python3"

    },

    "language_info": {

      "codemirror_mode": {

        "name": "ipython",

        "version": 3

      },

      "file_extension": ".py",

      "mimetype": "text/x-python",

      "name": "python",

      "nbconvert_exporter": "python",

      "pygments_lexer": "ipython3",

      "version": "3.13.3"

    },

    "papermill": {

      "default_parameters": {},

      "duration": 1.633245,

      "end_time": "2026-09-09T08:36:14.483090",

      "environment_variables": {},

      "exception": null,

      "input_path": "Notebook-Generated.ipynb",

      "output_path": "Notebook-Generated.ipynb",

      "parameters": {},

      "start_time": "2026-09-09T08:36:12.849845",

      "version": "2.6.0"

    },

    "polyglot_notebook": {

      "kernelInfo": {

        "defaultKernelName": "csharp",

        "items": [

          {

            "aliases": [],

            "name": "csharp"

          }

        ]

      }

    }

  },

  "nbformat": 4,

  "nbformat_minor": 5

}

10:36:18 [INFO] Orchestration - Statut final - Approuvé: True

10:36:18 [INFO] Orchestration - === Fin de la conversation ===

Exercice 3 : Analyse de la conversation multi-agents

La conversation entre les 3 agents produit une serie de messages. Analyser ces echanges permet de comprendre le comportement du système.

Objectif : Implementer une fonction analyze_conversation() qui extrait des statistiques de la conversation des agents.

Indices : - # Étape 1 : La conversation est stockee dans group_chat.history (ChatHistory SK) - # Étape 2 : Compter le nombre de messages par agent (CoderAgent, ReviewerAgent, AdminAgent) - # Étape 3 : Calculer la longueur moyenne des messages et le ratio messages/itérations - # Indice : Chaque message a un attribut name (nom de l’agent) et content (texte)

def analyze_conversation(group_chat_instance) -> dict:
    """
    TODO etudiant : Analyser les statistiques de la conversation multi-agents.
    
    Returns:
        dict avec les cles: total_messages, messages_per_agent (dict),
        avg_message_length, iterations_used
    """
    # TODO etudiant : parcourir l'historique et calculer les stats
    return None

# Pour tester apres execution de la conversation :
# stats = analyze_conversation(group_chat)
# print(f"Messages par agent: {stats['messages_per_agent']}")
print("Exercice a completer")
Exercice a completer
Retour au sommet