A la fin de ce notebook, vous saurez : 1. Concevoir une architecture multi-agents avec Semantic Kernel 2. Implementer des plugins specialises pour chaque rôle (Admin, Coder, Reviewer) 3. Créer des stratégies de sélection et terminaison personnalisees 4. Orchestrer une conversation AgentGroupChat jusqu’a validation 5. Gerer l’etat d’un notebook via une machine a etats (NotebookState)
Comprehension de @kernel_function et FunctionChoiceBehavior.Auto()
Duree estimee : 60 minutes
Sommaire
Section
Contenu
Concepts cles
1
Configuration projet
ipywidgets, UI interactive
2
Configuration LLM
dotenv, endpoints OpenAI
3
NotebookState
Machine a etats, Papermill
4
Plugins
Coder, Reviewer, Admin
5
Stratégies
Sélection, Termination
6
Agents
ChatCompletionAgent x3
7
Orchestration
AgentGroupChat, conversation
Architecture du système
Le graphe Mermaid de la cellule suivante montre la séquence Admin → Coder → Reviewer et la manière dont chaque agent fait progresser l’état partagé du notebook.
Les 3 agents
Agent
Rôle
Plugin
Actions principales
AdminAgent
Chef de projet
AdminNotebookPlugin
Editer markdown, approuver/rejeter
CoderAgent
Developpeur
CoderNotebookPlugin
Modifier code, finish_implementation
ReviewerAgent
Validateur
ReviewerNotebookPlugin
Executer, validate_notebook
Comment demarrer
Configurez votre tâche ci-dessous
Lancez l’orchestration avec le bouton “Play”
Observez la collaboration en direct : chaque agent intervient a tour de rôle
Les cellules suivantes s’executeront automatiquement après validation
Conseil : Ouvrez le notebook cible (nomme “Notebook-Generated.ipynb”) pour voir ses cellules evoluer en temps reel !
Schema : conversation d’agents et etat du notebook
Trois agents collaborent en sequence (Admin -> Coder -> Reviewer), chacun faisant progresser l’etat partage du notebook (specified -> implemented -> tested -> validated).
flowchart TD
subgraph AGC["AgentGroupChat"]
A["AdminAgent (Markdown)"] --> C["CoderAgent (Code)"] --> R["ReviewerAgent (Validation)"]
end
A --> NS["NotebookState : specified -> implemented -> tested -> validated"]
C --> NS
R --> NS
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.
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.
# %% Configuration du projet - UIimport timeimport randomimport ipywidgets as widgetsfrom dotenv import load_dotenv as _load_dotenvfrom IPython.display import displayfrom 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."]# ---- 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')# ---- Stockage de la configuration ----config = {'mode': None,'task_description': None,'uploaded_file': None}config_ready =Falsedef on_submit(_):"""Callback déclenché au clic du bouton."""global config_readytry: config['mode'] = tabs.selected_indexif config['mode'] ==3:if uploader.value: config['uploaded_file'] = uploader.value[0]elif config['mode'] ==0: config['task_description'] = random.choice(POSSIBLE_TASKS)elif config['mode'] ==1: config['task_description'] = task_selector.valueelif config['mode'] ==2: config['task_description'] = custom_task.value submit_btn.disabled =Trueprint("Configuration validée !")exceptExceptionas e:print(f"Erreur pendant la configuration : {e}") config_ready =Truesubmit_btn.on_click(on_submit)# ---- Chemin headless (réexécution automatique) ----# Charger le fichier partagé avant de décider si l'exécution est interactive.# L'API OpenAI officielle laisse OPENAI_BASE_URL vide : la présence de la clé# est donc le signal fiable, commun à la cellule de configuration LLM suivante.import os as _os_load_dotenv("../.env")if _os.environ.get("OPENAI_API_KEY"): _tache = _os.environ.get("NOTEBOOKMAKER_TASK", "") config['mode'] =2if _tache else0 config['task_description'] = _tache or random.choice(POSSIBLE_TASKS) config_ready =Trueprint(f"[headless] Tâche fixée sans UI (mode {config['mode']}) : "f"{config['task_description'][:70]}" )# ---- 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:whilenot config_ready: poll(10) time.sleep(0.1)print("✅ Config terminée, vous pouvez exécuter les cellules suivantes !")
[headless] Tâche fixée sans UI (mode 0) : Créer un notebook Python utilisant openpyxl (ou xlsxwriter) pour génér
En attente du clic sur Valider...
✅ Config terminée, vous pouvez exécuter les cellules suivantes !
Configuration du LLM (.env)
Dans cette section, nous allons :
Vérifier si un fichier .env est présent (et déjà configuré) ou non.
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).
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 osimport timeimport requestsfrom dotenv import load_dotenvimport ipywidgets as widgetsfrom IPython.display import displayfrom 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() # dict, ex: {"data":[...], "object":"list"}else:return {"error": f"status={resp.status_code}", "text": resp.text}exceptExceptionas 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_fakeor (openai_base_url notin ["", "https://api.openai.com/v1"]))# Flag indiquant quand la config est OKenv_config_ready =False# -------------------------------------------------------------------------# Widgets # -------------------------------------------------------------------------message_output = widgets.Output()api_key_input = widgets.Password( value="", 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=[], # Vide initialement description="Modèle :", layout={'width': '80%', 'display': 'none'} # masqué tant qu'on n'a pas listé)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(_):"""Appelé au clic sur 'Lister modèles'.""" new_base_url = base_url_input.value.strip() new_api_key = api_key_input.value.strip() or openai_api_keywith message_output: message_output.clear_output()ifnot new_base_url ornot 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", [])ifnot 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# Si le .env mentionne déjà un modèle existant, on le sélectionneif openai_chat_model_id in model_ids: model_dropdown.value = openai_chat_model_idelse: 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(_):"""Appelé au clic sur 'Enregistrer .env'."""global env_config_ready new_api_key = api_key_input.value.strip() or openai_api_key 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:withopen('.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}") api_key_input.value ="" api_key_input.value ="" env_config_ready =TrueexceptExceptionas e:print(f"❌ Erreur lors de l'écriture du fichier .env: {str(e)}")# -------------------------------------------------------------------------# Suppression des anciens callbacks (si la cellule est rejouée)# -------------------------------------------------------------------------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 # -------------------------------------------------------------------------# Imprimer un message d'introif 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 =Trueelse: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:whilenot env_config_ready: poll(10) time.sleep(0.1)# Sortie de la boucle => On peut masquer ui_box (facultatif) ui_box.layout.display ='none'print("✅ Configuration LLM terminée, vous pouvez exécuter la suite !")
La configuration étant terminée, les étapes suivantes vont se lancer :
Installation des dépendances : Nous vérifions et installons papermill, nbformat, semantic-kernel, etc.
Gestion d’état : nous utilisons la classe NotebookState pour piloter le cycle de vie du notebook.
Plugins : chaque agent aura un plugin lui permettant de lire ou modifier le contenu du notebook.
Stratégies d’orchestration : nous définissons quelles actions lancer en fonction de l’état (specified, implemented, tested, validated).
Agents : définition et configuration des trois agents (AdminAgent, CoderAgent, ReviewerAgent).
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.
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.
# --- Hygiene helpers (issue #6443, Stop & Repair type C) ---# Module-level redaction of PII-lite machine paths from any text before it reaches# the log pipeline. Patterns target the well-known Python/AppData and home-dir# layouts seen when a subprocess emits a path on Windows or Unix.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)."""ifnot text:return text redacted = textfor pat in _REDACT_PATH_PATTERNS: redacted = pat.sub(lambda m: f"{m.group(1)}<USER>{m.group(3)}", redacted)return redacted# #6443 — hygiène des sorties pip (chemin machine / build bdist_wheel).# `wordcloud` et autres paquets à binaire émettent via setup.py des avertissements# qui contournent `pip install --quiet` et fuient un chemin <user>/AppData/.../Scripts.import re as _re_pip_noise_PIP_BUILD_NOISE = _re_pip_noise.compile(r"(is installed in|Scripts'|DEPRECATION: Building|bdist_wheel|"r"Collecting |Downloading |Successfully installed|building \w+|"r"running \w+|creating \.|writing |copying )", _re_pip_noise.IGNORECASE,)class _PipBuildNoiseFilter(logging.Filter):"""Filtre de log : supprime les enregistrements Papermill porteurs de bruit build-pip."""deffilter(self, record: logging.LogRecord) ->bool: msg = record.getMessage()for line in msg.split("\n"):if line andnot _PIP_BUILD_NOISE.search(line):returnTrue# au moins une ligne légitime : on garde l'enregistrementreturnFalse# que du bruit build-pip : on drop_PIP_BUILD_NOISE_FILTER = _PipBuildNoiseFilter()class NotebookState:""" Gère le statut et le contenu d'un notebook. Les états possibles sont : - 'specified' - 'implemented' - 'tested' - 'validated' Attributes: notebook_path (str): Chemin du fichier notebook (.ipynb). _cached_notebook (nbformat.NotebookNode): Le contenu du notebook stocké en mémoire. _status (str): L'état courant du notebook. _previous_states (list[str]): Historique simple des états antérieurs. """ VALID_STATES = ["specified", "implemented", "tested", "validated"]def__init__(self, notebook_path: str) ->None:self.notebook_path = notebook_pathself._cached_notebook =Noneself._status ="specified"self._previous_states = []self._load_notebook()def _load_notebook(self) ->None:"""Charge le notebook depuis le chemin spécifié, en utilisant nbformat."""ifnot os.path.exists(self.notebook_path):raiseFileNotFoundError(f"Notebook introuvable: {self.notebook_path}")withopen(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 {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). """ifnot path: path =self.notebook_pathtry:withopen(path, "w", encoding="utf-8") as f: nbformat.write(self._cached_notebook, f) logger.info(f"[NotebookState] Notebook sauvegardé sous {path}")exceptExceptionas e: logger.error(f"[save_notebook] Erreur de sauvegarde: {e}")raisedef get_notebook_json(self) ->str:"""Retourne une représentation JSON (str) du notebook actuellement en mémoire."""return json.dumps(self._cached_notebook, indent=2, ensure_ascii=False)def log_notebook_state(self, max_length: int=10000) ->None:"""Affiche dans les logs une partie du JSON pour diagnostic (tronquée si trop longue). Hygiène (issue #6443) : tout chemin machine (Windows ``C:\\Users\\<name>\\...`` ou Unix ``/home/<name>/...``, ``/Users/<name>/...``) présent dans le snippet est remplacé par ``<USER>`` *avant* journalisation via ``_redact_paths`` pour éviter la fuite PII-lite (cf secrets-hygiene règle 6, Stop & Repair type C).""" notebook_json =self.get_notebook_json() snippet = notebook_json[:max_length]iflen(notebook_json) > max_length: snippet +=">>> TRUNCATED <<<" sanitized = _redact_paths(snippet) logger.debug(f"[NotebookState] Current notebook state (truncated):\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 inenumerate(self._cached_notebook.cells): flag =Falsefor out in (c.get("outputs") or []): text = out.get("text", "")ifisinstance(text, list): text ="".join(text)if text and _redact_paths(text) != text: flag =Truebreakif flag: lines.append(f"cellule {i:02d} : LEAK-CHEMIN")return"\n".join(lines)def get_status(self) ->str:"""Renvoie l'état courant du notebook."""returnself._statusdef set_status(self, new_status: str) ->None:""" Met à jour l'état du notebook et log la transition. Ne fait rien si new_status est invalide. """if new_status notinself.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_statusdef reset_outputs(self) ->None:""" Efface les sorties de toutes les cellules (execution_count, outputs). Utile avant ré-exécution si on veut repartir à zéro. """for cell inself._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 le notebook via Papermill et retourne True si tout se passe bien, ou False si une exception survient. #6443 — hygiène des sorties : un `pip install` (ex. `wordcloud`, paquet à binaire) émet des avertissements de *build* (DEPRECATION bdist_wheel, "script ... is installed in <chemin>/PythonXXX/Scripts") qui passent au travers de `--quiet` (ils proviennent de setup.py, pas de pip). Avec `log_output=True`, Papermill les renvoie vers son logger puis vers stderr, faisant fuiter un chemin machine (PII-lite) dans la sortie orchestrée. On attache donc un filtre sanitisant au logger Papermill pendant l'exécution. """ logger.info(f"[NotebookState] Exécution Papermill sur {self.notebook_path}.")self.save_notebook() # Sauvegarde avant exécution# #6443 : attacher le filtre anti-bruit build-pip au logger Papermill pour cette exécution papermill_logger = logging.getLogger("papermill") papermill_logger.addFilter(_PIP_BUILD_NOISE_FILTER) success =Truetry: pm.execute_notebook( input_path=self.notebook_path, output_path=self.notebook_path, kernel_name="python3", progress_bar=False, log_output=True )exceptExceptionas e: logger.error(f"[execute_notebook] Erreur lors de l'exécution: {e}") success =Falsefinally: papermill_logger.removeFilter(_PIP_BUILD_NOISE_FILTER)self._load_notebook() # Recharger, car Papermill a peut-être modifié le contenuself.sanitize_install_outputs() # #6443 : nettoyer les sorties résiduellesself.save_notebook() logger.info("[NotebookState] Notebook mis à jour après exécution (avec sorties).")return successdef sanitize_install_outputs(self) ->None:""" #6443 — retire des sorties du notebook exécuté les avertissements de build de paquets pip qui fuient un chemin machine (chemin Scripts, DEPRECATION bdist_wheel, Collecting/Downloading). Profondeur de défense : même si le filtre logger (execute_notebook) capte le bruit propagé via `log_output`, les sorties du notebook généré peuvent encore contenir ces lignes. Ne supprime jamais une sortie légitime (n'agit que sur le bruit build-pip). """ cleaned =0for cell inself._cached_notebook.cells:if"outputs"notin cell:continuefor out in cell["outputs"]:if out.get("output_type") !="stream":continue text = out.get("text", "")ifisinstance(text, list): text ="".join(text)ifnot text:continue lines = text.split("\n") kept = [ln for ln in lines ifnot _PIP_BUILD_NOISE.search(ln)]iflen(kept) !=len(lines): out["text"] ="\n".join(kept) cleaned +=len(lines) -len(kept)if cleaned: logger.info(f"[NotebookState] #6443 : {cleaned} ligne(s) de bruit build-pip retirée(s).")def update_cell(self, cell_index: int, new_source: str) ->None:""" Met à jour la source d’une cellule (index) et sauvegarde le notebook. """ old_src =self._cached_notebook.cells[cell_index].sourceself._cached_notebook.cells[cell_index].source = new_source 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 la liste des indices de cellules contenant `content_pattern`.""" indices = []for i, c inenumerate(self._cached_notebook.cells):if content_pattern in c.source: indices.append(i)return indicesdef is_approved(self) ->bool:"""Renvoie True si l'état du notebook est 'validated'."""returnself._status =="validated"
Exercice 1 : Mesure de complexite d’un notebook
La classe NotebookState offre des méthodes pour manipuler un notebook, mais pas pour analyser sa complexite structurelle.
Objectif : Implementer une fonction compute_complexity_metrics() qui evalue la complexite d’un notebook.
Indices : - # Étape 1 : Compter les cellules de code vs markdown pour obtenir le ratio - # Étape 2 : Calculer la longueur moyenne des cellules de code (en caractères) - # Étape 3 : Detecter les cellules contenant des mots-cles complexes (class, def, import, for, while) - # Indice : Un ratio code/markdown > 2 ou une longueur moyenne > 500 caractères indique un notebook complexe
def compute_complexity_metrics(notebook_state: NotebookState) ->dict:"""TODO etudiant : Evaluer la complexite structurelle d'un notebook. Returns: dict avec les cles: code_count, markdown_count, ratio_code_markdown, avg_code_length, complex_keyword_count, complexity_level (simple/medium/complex) """# TODO etudiant : analyser notebook_state._cached_notebook.cellsreturnNone# Test :# metrics = compute_complexity_metrics(notebook_state)# print(f"Complexite: {metrics['complexity_level']} (ratio={metrics['ratio_code_markdown']:.1f})")print("Exercice a completer")
Exercice a completer
4. Création (ou chargement) du Notebook cible + test de validité
Voici les étapes automatisées :
Nous utilisons un template Notebook-Template.ipynb, ou bien le fichier .ipynb téléversé,
Nous injectons une description de tâche (si elle est choisie aléatoirement, prédéfinie ou personnalisée),
Nous exécutons le notebook pour vérifier qu’aucune erreur fatale n’apparaît,
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 shutilimport osimport requestsTEMPLATE_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 inenumerate(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)returnTruereturnFalseDEST_NOTEBOOK ="Notebook-Generated.ipynb"# Gestion différenciée selon le mode sélectionnéif config['mode'] ==3: # Mode uploadif config['uploaded_file']: DEST_NOTEBOOK = config['uploaded_file'].namewithopen(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é)ifnot 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 HTTPwithopen(TEMPLATE_FILE, "wb") as f: f.write(response.content)print(f"Téléchargement terminé, fichier {TEMPLATE_FILE} créé.")exceptExceptionas 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 statenotebook_state = NotebookState(DEST_NOTEBOOK)notebook_state.log_notebook_state()# Injection de la tâche pour les modes templateif 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 base pour manipuler NotebookState. Fournit la méthode get_notebook_content() pour récupérer le notebook en JSON. """def__init__(self, state: NotebookState) ->None:self.state = stateself._get_content_counter =0# Pour logguer tous les 5 appels@kernel_function( name="get_notebook_content", description="Renvoie le notebook (format JSON) révisé actuellement." )def get_notebook_content(self) ->str:""" Retourne la représentation JSON du notebook. Loggue un extrait toutes les 5 demandes pour éviter la surcharge. """self._get_content_counter +=1 content =self.state.get_notebook_json()if (self._get_content_counter %5) ==0: logger.info(f"[BaseNotebookPlugin] get_notebook_content() (appel n°{self._get_content_counter}) - log complet")self.state.log_notebook_state(max_length=10000)else: snippet = content[:200] snippet +="..."iflen(content) >200else"" logger.info(f"[BaseNotebookPlugin] get_notebook_content() (appel n°{self._get_content_counter}) -> {snippet}")return contentclass NotebookEditingMixin:""" Mixin fournissant la fonction d'édition de cellule (update_cell_anyway). Peut être hérité par Coder ou Admin, qui ont tous deux besoin d'éditer. """@staticmethoddef _modernize_generated_source(source: str) ->str:"""Remplace les appels dépréciés avant d'écrire le code généré.""" source = source.replace("datetime.datetime.utcnow().isoformat() + 'Z'","datetime.datetime.now(datetime.UTC).isoformat().replace('+00:00', 'Z')", ) source = source.replace('datetime.datetime.utcnow().isoformat() + "Z"','datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z")', ) source = source.replace("datetime.datetime.utcnow()","datetime.datetime.now(datetime.UTC)", )# Seaborn 0.13 déprécie `palette` sans `hue`. Pour les barplots# générés avec des arguments simples x/y, rendre le mapping explicite. barplot_pattern = _re_redact.compile(r"sns\.barplot\(x=(?P<x>[^,\n]+),\s*y=(?P<y>[^,\n]+),\s*"r"palette=(?P<palette>[^,\)\n]+)\)" )return barplot_pattern.sub(lambda match: (f"sns.barplot(x={match.group('x')}, y={match.group('y')}, "f"hue={match.group('x')}, palette={match.group('palette')}, ""legend=False)" ), source, )def update_cell_anyway(self, pattern: str, new_source: str, cell_type: str=None) ->str:""" Recherche la première cellule contenant 'pattern' (dans le code ou markdown), puis remplace son contenu par 'new_source', si l'état le permet. """ indices = []for idx, cell inenumerate(self.state._cached_notebook.cells): content_match = (pattern in cell.source) type_match = (cell_type isNone) or (cell.cell_type == cell_type)if content_match and type_match: indices.append(idx)ifnot indices: return_message =f"Aucune cellule ({cell_type or'tout type'}) ne contient '{pattern}'."eliflen(indices) >1: return_message =f"Plusieurs cellules ({cell_type or'tout type'}) contiennent '{pattern}'."else: old_status =self.state.get_status()if old_status =="validated": return_message ="Édition impossible (notebook déjà validé)."else: source_to_write = new_sourceif cell_type =="code": source_to_write =self._modernize_generated_source(new_source)self.state.update_cell(indices[0], source_to_write) return_message =f"Cellule {cell_type} contenant '{pattern}' mise à jour." logger.info(f"[NotebookEditingMixin] update_cell_anyway -> {return_message}")return return_messageclass CoderNotebookPlugin(BaseNotebookPlugin, NotebookEditingMixin):""" Plugin pour l'agent 'Coder'. Hérite de BaseNotebookPlugin (lecture) et NotebookEditingMixin (édition). """@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 notin ["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 msgclass ReviewerNotebookPlugin(BaseNotebookPlugin):""" Plugin pour l'agent 'Reviewer'. Il ne peut pas éditer le notebook, mais peut l'exécuter et approuver (ou non). """@kernel_function( name="validate_notebook", description="Exécute le notebook et approuve ou non (approve=True/False)." )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()ifnot success:self.state.set_status("specified") msg = ("Erreur d'exécution dans le notebook (voir logs). ""Retour à l'état 'specified' pour corrections.")else:if 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 msgclass AdminNotebookPlugin(BaseNotebookPlugin, NotebookEditingMixin):""" Plugin pour l'agent 'Admin'. Peut lire, éditer et valider ou invalider le notebook. """@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" )# Après édition, on repasse l'état à 'specified'.self.state.set_status("specified") logger.info(f"[AdminNotebookPlugin] admin_edit_markdown_cell -> {msg}")return msg +" -> Revert à 'specified'."@kernel_function( name="approve_notebook", description="Validation finale: admin_ok=True => 'validated', sinon 'specified'." )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: l'état est '{status}' (attendu: 'tested')."else:if 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 verrouillage de cellules
Dans un notebook collaboratif, certaines cellules (headers, imports) ne doivent pas etre modifiees par les agents.
Objectif : Créer une classe CellLockMixin qui permet de proteger des cellules contre toute modification.
Indices : - # Étape 1 : Maintenir un ensemble _locked_indices: set[int] des indices de cellules verrouillees - # Étape 2 : Implementer lock_cell(index) et unlock_cell(index) - # Étape 3 : Surcharger update_cell pour verifier le verrouillage avant modification - # Indice : Lever une exception ou retourner un message d’erreur si une cellule verrouillee est modifiée
class CellLockMixin:"""TODO etudiant : Mixin pour proteger des cellules contre la modification. """def__init__(self):# TODO etudiant : initialiser _locked_indicespassdef lock_cell(self, cell_index: int) ->str:"""TODO etudiant : Verrouiller une cellule."""returnNonedef unlock_cell(self, cell_index: int) ->str:"""TODO etudiant : Deverrouiller une cellule."""returnNonedef is_locked(self, cell_index: int) ->bool:"""TODO etudiant : Verifier si une cellule est verrouillee."""returnNoneprint("Exercice a completer")
Exercice a completer
6. Stratégies d’orchestration
Deux stratégies principales :
ApprovedBasedTerminationStrategy
Met fin à la conversation dès que le notebook est validé, ou si un nombre maximal d’itérations est atteint.
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 PrivateAttrclass ApprovedBasedTerminationStrategy(TerminationStrategy):"""Met fin à 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 = stateself._max_steps = max_stepsself._current_step =0asyncdef 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}/{self._max_steps}, IsApproved={is_approved}" )if is_approved: logger.info("[TerminationStrategy] Notebook approuvé => arrêt.")returnTrueifself._current_step >=self._max_steps: logger.warning(f"[TerminationStrategy] max_steps={self._max_steps} atteint => arrêt.")returnTruereturnFalseclass NotebookAwareSelectionStrategy(SelectionStrategy):"""Sélectionne l'agent à partir de l'état réel du notebook."""def__init__(self, state: NotebookState):super().__init__()self._state = stateself._has_first_agent_run =Falsedef reset(self) ->None:self._has_first_agent_run =Falsedef _has_code_placeholders(self) ->bool:"""Détecte les cellules code encore réduites au marqueur du template."""for cell inself._state._cached_notebook.cells: source = cell.source.strip()if ( cell.cell_type =="code"and source.startswith("# Cellule ")and"\n"notin source ):returnTruereturnFalseasyncdef select_agent(self, agents, history): current_status =self._state.get_status() logger.debug(f"[SelectionStrategy] nb_agents={len(agents)}, statut={current_status}, "f"first_run={notself._has_first_agent_run}" )if current_status =="validated": logger.info("[SelectionStrategy] Notebook déjà validé => fin.")returnNone 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)ifnotself._has_first_agent_run and current_status =="specified"and admin:self._has_first_agent_run =True logger.info("Première intervention : AdminAgent (révision du Markdown).")return adminif ( current_status =="specified"andself._has_first_agent_runand"implemented"notinself._state._previous_statesandnotself._has_code_placeholders() ):self._state.set_status("implemented") current_status ="implemented" logger.info("Tous les placeholders code ont été remplacés : ""transition déterministe vers implemented." )if current_status =="specified"and coder: selected = coderelif current_status =="implemented"and reviewer: selected = reviewerelif current_status =="tested"and admin: selected = adminelse: logger.warning(f"[SelectionStrategy] Aucun agent trouvé pour état='{current_status}' => stop." ) selected =Noneif selected andnotself._has_first_agent_run:self._has_first_agent_run =Trueif 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 AsyncOpenAIfrom semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletionimport osfrom dotenv import load_dotenvload_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. """ifnot openai_api_key: logger.error("La variable d'environnement OPENAI_API_KEY n'est pas définie !")raiseValueError("OPENAI_API_KEY is not set in the environment.")if openai_base_url_from_env: final_base_url = openai_base_url_from_env logger.info(f"Utilisation d'un endpoint compatible OpenAI : {final_base_url}")else: final_base_url ="https://api.openai.com/v1" logger.info(f"Utilisation du service OpenAI officiel (URL explicite: {final_base_url}).")try: openai_async_client = AsyncOpenAI( api_key=openai_api_key, base_url=final_base_url )exceptExceptionas client_error: logger.error(f"Erreur lors de la création du client AsyncOpenAI pour base_url='{final_base_url}': {client_error}")raisetry: sk_service = OpenAIChatCompletion( service_id=service_id, ai_model_id=openai_chat_model_id, async_client=openai_async_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_serviceexceptExceptionas sk_service_error: logger.error(f"Erreur lors de la création du service OpenAIChatCompletion de Semantic Kernel: {sk_service_error}")raisedef create_kernel_for_agent(agent_id: str, plugin_instance) -> Kernel:"""Instancie un Kernel Semantic Kernel et lui ajoute le plugin de l'agent.""" k = Kernel() chat_service = create_chat_completion_service(service_id="default") k.add_service(chat_service) k.add_plugin(plugin_instance, plugin_name=f"{agent_id}_plugin")return kcoder_plugin = CoderNotebookPlugin(notebook_state)reviewer_plugin = ReviewerNotebookPlugin(notebook_state)admin_plugin = AdminNotebookPlugin(notebook_state)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)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"}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) Visualisez systématiquement le JSON du notebook via get_notebook_content().\n""2) Analysez et modifiez les cellules de code avec update_cell_by_content(), sans reprendre le markdown.\n""2bis) N'ajoutez JAMAIS `%pip install`, `!pip install` ni d'installation par subprocess. Les dépendances sont préparées hors notebook.\n""2ter) Hygiène des sorties : n'affichez jamais de chemin absolu. Utilisez des chemins relatifs (`outputs/...`) et affichez seulement leur forme relative ou leur basename. Neutralisez à la source les FutureWarning prévisibles avec les API modernes ou un filtre ciblé ; aucun warning ne doit imprimer un chemin de kernel temporaire.\n""3) Préservez les marqueurs importants des cellules de code.\n""4) Quand c'est prêt, appelez finish_implementation().\n""5) Si besoin, faites de nouvelles itérations.\n" ), arguments=KernelArguments(settings=coder_settings))reviewer_agent = ChatCompletionAgent( kernel=reviewer_kernel, name="ReviewerAgent", instructions=("Vous êtes le **Reviewer**. Vérifiez le travail du codeur et validez-le seulement quand il est exécutable et propre.\n""1) Consultez toujours le notebook complet après chaque mise à jour.\n""2) Appelez validate_notebook(approve=True/False) selon la qualité.\n""3) Vérifiez les erreurs et l'alignement code/Markdown.\n""3bis) Refusez toute installation inline (`%pip`, `!pip`, subprocess). Refusez aussi 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.\n" ), arguments=KernelArguments(settings=reviewer_settings))admin_agent = ChatCompletionAgent( kernel=admin_kernel, name="AdminAgent", instructions=("Vous êtes l'**Admin**. Vous spécifiez le notebook et exigez un rendu pédagogique de haute qualité.\n""1) Intervenez au début pour détailler sans ambiguïté les cellules Markdown.\n""1bis) Ne prescrivez JAMAIS d'installation inline. Décrivez les dépendances comme prérequis hors notebook.\n""1ter) Exigez des chemins de sortie relatifs (`outputs/...`) : aucun chemin absolu ni chemin temporaire de kernel ne doit apparaître dans les sorties. Demandez un traitement ciblé des warnings prévisibles à la source.\n""Corrigez et détaillez les explications Markdown via admin_edit_markdown_cell.\n""2) Après chaque édition, le Coder implémente les cellules de code.\n""3) Après validation du Reviewer, éditez, validez ou invalidez via approve_notebook(admin_ok=True/False).\n""4) Une fois satisfait, validez définitivement avec admin_ok=True.\n" ), arguments=KernelArguments(settings=admin_settings))termination_strategy = ApprovedBasedTerminationStrategy(notebook_state)selection_strategy = NotebookAwareSelectionStrategy(notebook_state)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 asyncioclass 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 180, 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:ifself._buffer:print("\n".join(self._buffer))self._buffer = []asyncdef run_conversation():"""Lance la conversation multi-agents jusqu'à validation du notebook.""" 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_content = notebook_state.get_notebook_json() group_chat.history.add_system_message(f"NOTEBOOK CONTENT:\n{initial_content}") 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 les é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. ""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 + début) iteration =0asyncfor 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 + marqueurif 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)exceptExceptionas 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()
09:57:46 [INFO] Orchestration - Version initiale du notebook :09:57:46 [INFO] Orchestration - === Début de la conversation entre agents ===09:57:46 [INFO] Orchestration - Première intervention : AdminAgent (révision du Markdown).09:57:49 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() (appel n°1) -> {
"cells": [
{
"cell_type": "markdown",
"id": "516d2854",
"metadata": {
"papermill": {
"duration": 0.001661,
"end_time": "2026-09-09T07:57:45.571591...09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell()09:58:01 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 009:58:01 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:01 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule markdown contenant '# Notebook de travail' mise à jour.09:58:01 [INFO] Orchestration - [NotebookState] Passage de l'état specified → specified09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell -> Cellule markdown contenant '# Notebook de travail' mise à jour.09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell()09:58:01 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 209:58:01 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:01 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule markdown contenant '## 1. Préparation' mise à jour.09:58:01 [INFO] Orchestration - [NotebookState] Passage de l'état specified → specified09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell -> Cellule markdown contenant '## 1. Préparation' mise à jour.09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell()09:58:01 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 409:58:01 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:01 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule markdown contenant '## 2. Initialisation' mise à jour.09:58:01 [INFO] Orchestration - [NotebookState] Passage de l'état specified → specified09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell -> Cellule markdown contenant '## 2. Initialisation' mise à jour.09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell()09:58:01 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 609:58:01 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:01 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule markdown contenant '## 3. Traitement' mise à jour.09:58:01 [INFO] Orchestration - [NotebookState] Passage de l'état specified → specified09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell -> Cellule markdown contenant '## 3. Traitement' mise à jour.09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell()09:58:01 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 809:58:01 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:01 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule markdown contenant '## 4. Analyse' mise à jour.09:58:01 [INFO] Orchestration - [NotebookState] Passage de l'état specified → specified09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell -> Cellule markdown contenant '## 4. Analyse' mise à jour.09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell()09:58:01 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 1009:58:01 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:01 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule markdown contenant '## 5. Conclusion' mise à jour.09:58:01 [INFO] Orchestration - [NotebookState] Passage de l'état specified → specified09:58:01 [INFO] Orchestration - [AdminNotebookPlugin] admin_edit_markdown_cell -> Cellule markdown contenant '## 5. Conclusion' mise à jour.09:58:02 [INFO] Orchestration - [AdminNotebookPlugin] approve_notebook(admin_ok=False)09:58:02 [INFO] Orchestration - [AdminNotebookPlugin] approve_notebook -> Impossible d'approuver: l'état est 'specified' (attendu: 'tested').09:58:04 [INFO] Orchestration - [STEP 1 - AdminAgent] réponse reçue (173 caractères).09:58:04 [INFO] Orchestration - [SelectionStrategy] Agent sélectionné : CoderAgent09:58:06 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() (appel n°1) -> {
"cells": [
{
"cell_type": "markdown",
"id": "516d2854",
"metadata": {
"papermill": {
"duration": 0.001661,
"end_time": "2026-09-09T07:57:45.571591...09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()09:58:30 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 109:58:30 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:30 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 0' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 0' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()09:58:30 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 309:58:30 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:30 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 1' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 1' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()09:58:30 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 509:58:30 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:30 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 2' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 2' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()09:58:30 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 709:58:30 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:30 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 3' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 3' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()09:58:30 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 909:58:30 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:30 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 4' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 4' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content()09:58:30 [INFO] Orchestration - [NotebookState] Mise à jour de la cellule 1109:58:30 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:30 [INFO] Orchestration - [NotebookEditingMixin] update_cell_anyway -> Cellule code contenant '# Cellule 5' mise à jour.09:58:30 [INFO] Orchestration - [CoderNotebookPlugin] update_cell_by_content -> Cellule code contenant '# Cellule 5' mise à jour.09:58:32 [INFO] Orchestration - [CoderNotebookPlugin] finish_implementation()09:58:32 [INFO] Orchestration - [NotebookState] Passage de l'état specified → implemented09:58:32 [INFO] Orchestration - [CoderNotebookPlugin] finish_implementation -> Le notebook passe à l'état 'implemented'.09:58:34 [INFO] Orchestration - [STEP 2 - CoderAgent] réponse reçue (130 caractères).09:58:34 [INFO] Orchestration - [SelectionStrategy] Agent sélectionné : ReviewerAgent09:58:35 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() (appel n°1) -> {
"cells": [
{
"cell_type": "markdown",
"id": "516d2854",
"metadata": {
"papermill": {
"duration": 0.001661,
"end_time": "2026-09-09T07:57:45.571591...09:58:37 [INFO] Orchestration - [ReviewerNotebookPlugin] validate_notebook(approve=True)09:58:37 [INFO] Orchestration - [NotebookState] Exécution Papermill sur Notebook-Generated.ipynb.09:58:37 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:39 [INFO] Orchestration - [NotebookState] Notebook sauvegardé sous Notebook-Generated.ipynb09:58:39 [INFO] Orchestration - [NotebookState] Notebook mis à jour après exécution (avec sorties).09:58:39 [INFO] Orchestration - [NotebookState] Passage de l'état implemented → tested09:58:39 [INFO] Orchestration - [ReviewerNotebookPlugin] validate_notebook -> Le reviewer approuve => état 'tested'.09:58:41 [INFO] Orchestration - [STEP 3 - ReviewerAgent] réponse reçue (177 caractères).09:58:41 [INFO] Orchestration - [SelectionStrategy] Agent sélectionné : AdminAgent09:58:43 [INFO] Orchestration - [BaseNotebookPlugin] get_notebook_content() (appel n°2) -> {
"cells": [
{
"cell_type": "markdown",
"id": "516d2854",
"metadata": {
"papermill": {
"duration": 0.002828,
"end_time": "2026-09-09T07:58:38.562886...09:58:44 [INFO] Orchestration - [AdminNotebookPlugin] approve_notebook(admin_ok=True)09:58:44 [INFO] Orchestration - [NotebookState] Passage de l'état tested → validated09:58:44 [INFO] Orchestration - [AdminNotebookPlugin] approve_notebook -> Notebook validé => état 'validated'.09:58:46 [INFO] Orchestration - [TerminationStrategy] Notebook approuvé => arrêt.09:58:46 [INFO] Orchestration - [STEP 4 - AdminAgent] réponse reçue (297 caractères).09:58:46 [INFO] Orchestration - Notebook approuvé => fin de la conversation.09:58:46 [INFO] Orchestration - Version finale du notebook, après la conversation :09:58:46 [INFO] Orchestration - Statut final - Approuvé: True09:58:46 [INFO] Orchestration - === Fin de la conversation ===
Exercice 3 : Générateur de rapport d’exécution
Après une session NotebookMaker, il est utile de generer un rapport recapitulatif pour analyser les performances.
Objectif : Implementer une fonction generate_execution_report() qui compile les metriques d’une session complete.
Indices : - # Étape 1 : Collecter le statut final (notebook_state.is_approved()), le nombre d’etats précédents (_previous_states) - # Étape 2 : Calculer le nombre de transitions “specified -> implemented” (cycles de correction) - # Étape 3 : Produire un rapport textuel avec les metriques cles - # Indice : Le nombre de cycles de correction indique la qualite initiale des specifications
def generate_execution_report(notebook_state: NotebookState, iteration_count: int) ->str:"""TODO etudiant : Generer un rapport d'execution de la session NotebookMaker. Args: notebook_state: L'etat final du notebook iteration_count: Nombre d'iterations total de la conversation Returns: Rapport textuel avec metriques (statut, iterations, cycles de correction) """# TODO etudiant : analyser notebook_state._previous_states et _statusreturnNone# Test :# report = generate_execution_report(notebook_state, iteration=10)# print(report)print("Exercice a completer")
Comment demarrer
Schema : conversation d’agents et etat du notebook
Trois agents collaborent en sequence (Admin -> Coder -> Reviewer), chacun faisant progresser l’etat partage du notebook (specified -> implemented -> tested -> validated).