Navigation : Index

Notebook de conception de Notebook

Ce Notebook .Net interactive a pour objectif de permettre la création assistée d’autres notebooks .Net interactive en confiant à Semantic-Kernel la responsabilité de les écrire, de les exécuter et de les corriger. C’est un cas d’usage méta : le LLM ne produit pas un texte pour l’humain, il produit et raffine un livrable exécutable — un notebook complet, du Markdown initial jusqu’au graphique final conforme.

Le notebook déroule un pipeline en quatre étapes, chacune portée par une section numérotée :

  1. Fourniture des informations (section 1) — le mode d’entrée de la description de tâche : variable en dur, questions interactives, ou fichier.
  2. Recueil des informations (section 2) — la collecte effective de la description selon le mode choisi.
  3. Personnalisation du notebook de travail (section 3) — la génération du chemin du notebook cible depuis un template.
  4. Exécution et mise à jour itérative — la boucle AutoInvokeSKAgentsNotebookUpdater, qui charge le template, laisse les agents SK compléter les cellules, exécute, et recommence jusqu’à convergence.

Chaque section conceptuelle est suivie d’un exercice (mode URL, validation de description, nom de fichier descriptif) : le notebook est lui-même un support pédagogique sur l’orchestration d’agents qui créent des supports pédagogiques.

Prérequis : kernel .NET (C#), les packages Nuget chargés dans la première cellule, et les fichiers compagnons .cs (DisplayLogger, NotebookExecutor…) importés dans la deuxième. La description d’exemple cible DBPedia via dotNetRDF et XPlot.Plotly.Interactive — un cas assez riche pour que la correction itérative ait du travail.

Note d’honnêteté : dans cette copie committée, les sorties d’affichage display() du kernel .NET ne sont pas conservées — les lectures ci-dessous s’anchorent sur le source committé des cellules, cité verbatim.

#r "nuget: Microsoft.DotNet.Interactive, 1.0.0-beta.25323.1"
#r "nuget: Microsoft.DotNet.Interactive.CSharp, 1.0.0-beta.25323.1"
#r "nuget: Microsoft.DotNet.Interactive.Documents, 1.0.0-beta.25323.1"
#r "nuget: Microsoft.DotNet.Interactive.PackageManagement, 1.0.0-beta.25323.1"
#r "nuget: Microsoft.Extensions.Logging"
#r "nuget: Microsoft.SemanticKernel, *-*"
#r "nuget: Microsoft.SemanticKernel.Planners.OpenAI, *-*"
#r "nuget: Microsoft.SemanticKernel.Agents.Core, *-*"
Installed Packages
  • Microsoft.DotNet.Interactive, 1.0.0-beta.25323.1
  • Microsoft.DotNet.Interactive.CSharp, 1.0.0-beta.25323.1
  • Microsoft.DotNet.Interactive.Documents, 1.0.0-beta.25323.1
  • Microsoft.DotNet.Interactive.PackageManagement, 1.0.0-beta.25323.1
  • Microsoft.Extensions.Logging, 10.0.11
  • Microsoft.SemanticKernel, 1.80.0
  • Microsoft.SemanticKernel.Agents.Core, 1.80.0
  • Microsoft.SemanticKernel.Planners.OpenAI, 1.47.0-preview

Lecture de la cellule : la machinerie en deux couches

La liste Nuget superpose deux mondes que ce notebook doit faire dialoguer :

  • Microsoft.DotNet.Interactive.* (CSharp, Documents, PackageManagement) — l’API du kernel .NET Interactive lui-même : compiler des cellules, lire et écrire des documents .ipynb, gérer les packages. C’est elle qui permet de manipuler le notebook comme un objet.
  • Microsoft.SemanticKernel + Planners.OpenAI + Agents.Core — l’orchestration LLM : planification, agents, invocations.

L’exercice n’est pas anodin : les deux couches exposent chacune un symbole Kernel (voir la section des using plus bas, qui pose les alias). Un notebook qui écrit des notebooks doit instancier les deux — l’un comme substrat, l’autre comme auteur.

Importation des fichiers .cs

Un certain nombre de classes ont été conçues pour fournir l’exécution et la mise à jour de notebook .Net interactive à l’aide de Semantic-Kernel.

#!import ../../Config/Settings.cs

#!import DisplayLogger.cs
#!import DisplayLoggerProvider.cs
#!import NotebookExecutor.cs
#!import WorkbookInteractionBase.cs
#!import WorkbookUpdateInteraction.cs
#!import WorkbookValidation.cs
#!import NotebookUpdaterBase.cs
#!import AutoInvokeSKAgentsNotebookUpdater.cs

// #!import NotebookPlannerUpdater.cs  // variante historique — voir conclusion
// #!import AutoGenNotebookUpdater.cs   // variante historique — voir conclusion

Lecture de la cellule : les classes compagnons

Les imports #!import chargent les classes d’infrastructure qui ne sont pas dans ce fichier — elles vivent à côté, dans le dossier de la série :

  • ../../Config/Settings.cs — la configuration partagée (endpoint, clé de modèle), même source que tous les notebooks SK de la série.
  • DisplayLogger / DisplayLoggerProvider — un ILogger qui restitue la trace des agents dans le notebook au lieu de la noyer dans la console : chaque requête du planificateur devient visible en place.
  • NotebookExecutor — le cœur méta : exécute un notebook .NET Interactive par programme et rapporte les erreurs de compilation/exécution cellule par cellule.

Les imports en commentaire (WorkbookUpdateInteraction, NotebookPlannerUpdater, AutoGenNotebookUpdater…) sont les variantes historiques de la stratégie de mise à jour — ce notebook active AutoInvokeSKAgentsNotebookUpdater, les autres restent disponibles pour comparaison.

Lecture de la cellule : denamespace des helpers .cs (#12194)

Les huit fichiers .cs compagnons (DisplayLogger, DisplayLoggerProvider, NotebookExecutor, WorkbookInteractionBase, WorkbookUpdateInteraction, WorkbookValidation, NotebookUpdaterBase, AutoInvokeSKAgentsNotebookUpdater) ont été dénamespace-ifiés dans le même PR : le namespace MyNotebookLib; fichier-scoped déclarait les types dans une enveloppe que le kernel .NET Interactive rejette en mode script avec CS7021 (Impossible de déclarer un espace de noms dans le code de script). En passant les types au namespace global, le #!import les charge proprement et les cellules suivantes les voient comme types racine — cohérent avec les références non qualifiées déjà présentes dans le code du notebook (new DisplayLogger(...), var updater = new AutoInvokeSKAgentsNotebookUpdater(...), etc.).

Deux commentaires résiduels restent commentés pour des raisons distinctes :

  • NotebookPlannerUpdater.cs / AutoGenNotebookUpdater.cs — variantes historiques de la stratégie de mise à jour, alternatives à AutoInvokeSKAgentsNotebookUpdater que ce notebook n’active pas (les charger en plus créerait des conflits de symboles).
  • La cellule d’orchestration finale (UpdateNotebookWithAutoInvokeSKAgents) — stubée : la boucle appelle un LLM via Settings.LoadFromFile() qui lit config/settings.json. Sans credential OpenAI/Azure OpenAI configuré, l’invocation échoue à l’appel réseau. Les types sont chargés et utilisables ; l’invocation est hors-périmètre d’un notebook reproductible.

Blast radius vérifié : grep -rn '#!import DisplayLogger\|NotebookExecutor\|NotebookUpdaterBase\|AutoInvoke\|Workbook' MyIA.AI.Notebooks --include='*.ipynb' retourne uniquement Semantic-kernel-AutoInteractive.ipynb et sa copie _output.ipynb. Aucun autre notebook ne dépend de ces helpers.

  • Imports des espaces de noms

On prend soin de distinguer le kernel d’exécution de notebook .Net interactive, et le kernel de semantic-kernel.

using Microsoft.DotNet.Interactive;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Planning;
using Microsoft.SemanticKernel.Connectors.OpenAI;
  
using System;
using System.IO;
using System.Threading.Tasks;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;

using SKernel = Microsoft.SemanticKernel.Kernel;
using IKernel = Microsoft.DotNet.Interactive.Kernel;

Lecture de la cellule : deux kernels, deux alias

La dernière ligne porte tout le sens de la superposition :

using SKernel = Microsoft.SemanticKernel.Kernel;
using IKernel  = Microsoft.DotNet.Interactive.Kernel;

SKernel est l’orchestrateur LLM (plugins, agents, planners) ; IKernel est le kernel hôte — celui qui exécute cette cellule et sait interroger l’utilisateur (IKernel.GetInputAsync, utilisé par le mode Prompt de la section 2). Sans les alias, le nom Kernel serait ambigu et le compilateur refuserait. C’est le point de jonction exact du cas d’usage méta : le notebook utilise le kernel hôte pour interagir avec vous, et instancie le kernel SK pour engendrer ses pairs.

1. Mode de Fourniture des Informations

On permet à l’utilisateur de saisir les informations décrivant la tâche à accomplir dans le notebook de travail de plusieurs façons différentes.

L’enum InformationMode ci-dessous fixe la stratégie d’entrée : Variable (la description est en dur dans ce notebook — reproductible, idéal pour les tests), Prompt (quatre questions posées interactivement via le kernel hôte), File (la description vit dans un fichier). Le mode est un paramètre d’expérience, pas un détail : il détermine qui fournit l’intention initiale — le développeur, l’utilisateur au clavier, ou un artefact existant.

public enum InformationMode
{
    Variable,
    Prompt,
    File
}

Lecture de la cellule : un enum comme point d’extension

Trois valeurs, trois stratégies — et l’exercice ci-dessous en propose une quatrième (Url). Le pattern mérite d’être nommé : plutôt qu’un switch dispersé, la sélection de source est une donnée (var mode = InformationMode.Variable; dans la section 2), lue par un seul bloc conditionnel. Ajouter un mode = ajouter une valeur + une branche, sans toucher au reste.

Exercice : Ajouter un mode de fourniture URL

L’enum InformationMode définit actuellement trois modes (Variable, Prompt, File). Vous pouvez ajouter un quatrieme mode pour recuperer la description depuis une URL distante.

Objectif : Completez le code ci-dessous pour ajouter le mode Url a l’enum et ecrire une méthode qui simule le chargement d’une description depuis une URL (affichez simplement l’URL que vous utiliseriez).

Indices : - # Étape 1 : Ajoutez la valeur Url dans l’enum InformationMode - # Étape 2 : Completez la méthode FetchDescriptionFromUrl qui prend une URL et retourne un message indiquant l’URL qui serait chargee - # Indice : En production, on utiliserait HttpClient.GetStringAsync(url), ici contentez-vous de simuler le comportement

Indice d’architecture : en production, FetchDescriptionFromUrl ferait un HttpClient().GetStringAsync(url) — mais pour l’exercice, une simulation suffit : l’objectif est de comprendre où se raccorde une nouvelle source d’information, pas d’écrire un client HTTP. Observez que le test en bas de la cellule (simulated ?? "Exercice a completer") échoue doucement tant que le stub retourne null — pattern C.1, le notebook reste exécutable de bout en bout.

// Exercice : Ajouter un mode de fourniture URL
// 1. Ajoutez la valeur Url a l'enum InformationMode
// 2. Completez la methode de simulation

// TODO etudiant : ajoutez Url a l'enum ci-dessus (dans la cellule precedente)
// public enum InformationMode { Variable, Prompt, File, Url }

string FetchDescriptionFromUrl(string url)
{
    // TODO etudiant : simulez le chargement depuis l'URL
    // En production : var content = await new HttpClient().GetStringAsync(url);
    return null;  // TODO etudiant : retournez un message simulant le chargement
}

// Test
var simulated = FetchDescriptionFromUrl("https://example.com/task-description.txt");
Console.WriteLine(simulated ?? "Exercice a completer");
Exercice a completer

2. Recueil des informations

Selon le mode de fourniture des informations choisi, on récupère la tâche à accomplir dans le notebook de travail.

La cellule ci-dessous matérialise le choix : elle instancie un display d’avancement (Collecte d'informations en cours...), fixe mode = InformationMode.Variable, puis porte la description de référence — une commande de cahier des charges complète en une phrase : requêter DBPedia avec dotNetRDF et XPlot.Plotly.Interactive, commencer par le Markdown, choisir une requête complexe « manipulant des aggrégats », et finir par la conformité de la sortie graphique. C’est cette richesse qui donnera au LLM une tâche digne de sa boucle de correction.

var infoCollectionDisplay = display("Collecte d'informations en cours...");

var mode = InformationMode.Variable;

string taskDescription = "Créer un notebook .Net interactive permettant de requêter DBPedia, utilisant les package Nuget dotNetRDF et XPlot.Plotly.Interactive. Commencer par éditer les cellules de Markdown pour définir et affiner l'exemple de requête dont le graphique final sera le plus pertinent. Choisir un exemple de requête complexe, manipulant des aggrégats, qui pourront être synthétisés dans un graphique. Une fois les cellules de code alimentées et les bugs corrigés, s'assurer que la sortie de la cellule générant le graphique est conforme et pertinente.";

if (mode == InformationMode.Variable)
{
    display("Utilisation de la variable pour la description de la tâche.");
}
else if (mode == InformationMode.Prompt)
{
    var questions = new[]
    {
        "Bonjour! Veuillez fournir une brève description de la tâche à accomplir.",
        "Quels sont les principaux objectifs de cette tâche?",
        "Y a-t-il des contraintes ou des conditions spécifiques à prendre en compte?",
        "Des informations supplémentaires que vous souhaitez ajouter?"
    };

    taskDescription = string.Empty;
    foreach (var question in questions)
    {
        var response = await IKernel.GetInputAsync(question);
        taskDescription += $"{question}\\n{response}\\n\\n";
    }
}


display("Informations recueillies :\\n" + taskDescription);
Collecte d'informations en cours...
Utilisation de la variable pour la description de la tâche.
Informations recueillies :\nCréer un notebook .Net interactive permettant de requêter DBPedia, utilisant les package Nuget dotNetRDF et XPlot.Plotly.Interactive. Commencer par éditer les cellules de Markdown pour définir et affiner l'exemple de requête dont le graphique final sera le plus pertinent. Choisir un exemple de requête complexe, manipulant des aggrégats, qui pourront être synthétisés dans un graphique. Une fois les cellules de code alimentées et les bugs corrigés, s'assurer que la sortie de la cellule générant le graphique est conforme et pertinente.

Lecture de la cellule : trois branches, un seul flux de sortie

Le bloc conditionnel se lit comme une table des modes :

  • Variable — branche active ici : un simple display de confirmation, la description en dur fait foi.
  • Prompt — quatre questions posées à l’utilisateur (IKernel.GetInputAsync) : « Veuillez fournir une brève description », « principaux objectifs », « contraintes ou conditions spécifiques », « informations supplémentaires ». Chaque réponse est concaténée avec sa question dans taskDescription — le LLM recevra le dialogue complet, pas seulement les réponses : les questions servent de squelette au prompt.
  • File — laissée en exercice de lecture au lecteur (et à l’exercice Url ci-dessus pour la variante réseau).

Point de détail qui compte : taskDescription est réassignée par le mode Prompt (elle démarre vide), alors que le mode Variable la consomme telle quelle. La variable est le contrat entre cette section et la boucle de mise à jour finale — peu importe son origine, la suite du pipeline n’en verra qu’une chaîne.

Exercice : Valider la description de la tâche

Avant de lancer la generation du notebook, il est utile de verifier que la description fournie est suffisamment precise pour guider le modèle.

Objectif : Completez la méthode ValidateTaskDescription qui verifie que la description contient au moins 10 mots, mentionne au moins un package NuGet ou une technologie, et retourne un message de validation ou d’erreur.

Indices : - # Étape 1 : Comptez le nombre de mots avec description.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length - # Étape 2 : Cherchez des mots cles techniques (package, NuGet, using, requête, graphique, etc.) avec description.Contains() - # Étape 3 : Retournez un tuple (bool isValid, string message) indiquant le résultat - # Indice : Utilisez une liste de mots cles et LINQ .Any(k => description.Contains(k)) pour la detection

// Exercice : Valider la description de la tache
// Completez la methode pour verifier la qualite de la description

(bool IsValid, string Message) ValidateTaskDescription(string description)
{
    // TODO etudiant : verifiez que la description contient au moins 10 mots
    // TODO etudiant : cherchez des mots cles techniques
    // TODO etudiant : retournez un tuple (isValid, message)
    return (false, "Exercice a completer");  // TODO etudiant : remplacez par l'implementation
}

// Test avec la description actuelle
var validation = ValidateTaskDescription(taskDescription);
Console.WriteLine($"Validation : {validation.IsValid} - {validation.Message}");
Validation : False - Exercice a completer

3. Personnalisation du Notebook de Travail

On charge un notebook template contenant des parties de Markdown et de code à compléter, et on injecte la tâche dans la partie descriptive en entête du notebook (méthode SetStartingNotebookFromTemplate de la section 4).

La cellule ci-dessous génère le chemin cible : ./Workbooks/Workbook-{DateTime.Now.ToFileTime()}.ipynb. Deux choix se lisent dans cette seule ligne : le dossier Workbooks/ sépare les notebooks engendrés du notebook concepteur (jamais de mutation du fichier source), et le timestamp ToFileTime() garantit l’unicité — chaque exécution produit un exemplaire neuf, comparable au précédent sans l’écraser.


var notebookPath = @$"./Workbooks/Workbook-{DateTime.Now.ToFileTime()}.ipynb";

Lecture de la cellule : pourquoi un timestamp plutôt qu’un nom fixe

ToFileTime() rend le nom triable chronologiquement (c’est un entier croissant, formaté sans séparateurs). L’exercice suivant propose l’alternative lisible (SK-<mots-cle>-<date>.ipynb) — notez le compromis que chaque option achète : le timestamp est unique par construction mais illisible ; le nom descriptif est lisible mais exige une stratégie de collision (deux descriptions semblables le même jour produiraient le même nom). Une boucle de génération automatique qui écrase ses propres artefacts est un bug silencieux ; c’est le critère que votre implémentation de l’exercice doit tenir.

Exercice : Generer un nom de notebook personnalise

Le chemin du notebook est actuellement genere avec un timestamp. Vous pouvez enrichir cette logique pour produire un nom plus descriptif.

Objectif : Completez la méthode GenerateNotebookName qui construit un nom de fichier incluant un prefix fourni, la date du jour et un identifiant court derive de la description de la tâche.

Indices : - # Étape 1 : Extrayez les 3 premiers mots significatifs de taskDescription (supprimez les mots vides comme “un”, “de”, “le”, “la”, “des”) - # Étape 2 : Formatez la date avec DateTime.Now.ToString("yyyy-MM-dd") - # Étape 3 : Concatenez prefix + date + mots extraits avec des underscores et ajoutez l’extension .ipynb - # Indice : Utilisez string.Join("_", mots.Take(3)) pour assembler les mots cles

// Exercice : Generer un nom de notebook personnalise
// Completez la methode pour construire un nom de fichier descriptif

string GenerateNotebookName(string prefix, string description)
{
    // TODO etudiant : extrayez les mots significatifs de la description
    // TODO etudiant : filtrez les mots vides (un, de, le, la, des, du, etc.)
    // TODO etudiant : formatez la date et assemblez le nom
    return null;  // TODO etudiant : remplacez par l'implementation
}

// Test (decommentez apres implementation)
// var customName = GenerateNotebookName("SK", taskDescription);
// Console.WriteLine($"Nom genere : {customName}");
Console.WriteLine("Exercice a completer");
Exercice a completer

Exécution et Mise à Jour Itérative avec AutoInvokeSKAgentsNotebookUpdater

La dernière cellule assemble tout : un DisplayLogger en verbosité Trace (chaque décision d’agent sera affichée), l’updater branché sur le chemin généré, l’injection de la description via SetStartingNotebookFromTemplate(taskDescription), puis l’appel asynchrone UpdateNotebookWithAutoInvokeSKAgents() — la boucle complète : compléter les cellules du template, exécuter via NotebookExecutor, lire les erreurs, corriger, recommencer.

// Cellule 25 désactivée : la boucle AutoInvokeSKAgentsNotebookUpdater requiert
// un credential OpenAI/Azure OpenAI configuré dans config/settings.json.
// Les types sont désormais chargés (post-#12194, denamespace + #!import actifs),
// mais l'invocation réseau est hors-périmètre d'un notebook reproductible sans clé.
// Pour activer : décommenter le bloc ci-dessous et configurer Settings.cs (endpoint + apikey).

// var logger = new DisplayLogger("NotebookUpdater", LogLevel.Trace);
// var updater = new AutoInvokeSKAgentsNotebookUpdater(notebookPath, logger);
// updater.SetStartingNotebookFromTemplate(taskDescription);
// display("Appel à UpdateNotebookWithAutoInvokeSKAgents...");
// await updater.UpdateNotebookWithAutoInvokeSKAgents();
// display("Mise à jour du notebook terminée.");

display("Cellule 25 stubée — credentials SK requis pour la boucle AutoInvoke. Voir commentaire ci-dessus.");
Cellule 25 stubée — credentials SK requis pour la boucle AutoInvoke. Voir commentaire ci-dessus.

Lecture de la cellule : ce que l’await masque

Les trois display encadrent l’appel (Appel à UpdateNotebookWithAutoInvokeSKAgents..., Mise à jour du notebook terminée.) : tout ce qui se passe entre les deux est caché dans un seul await — et c’est précisément là que vit l’intérêt du notebook. Selon la verbosité Trace du DisplayLogger, l’exécution réelle déroulera : la planification SK (choix des cellules à traiter), les invocations d’agents (rédaction Markdown, complétion C#), l’exécution du notebook engendré par NotebookExecutor, le rapport d’erreurs cellule par cellule, et la ré-injection de ces erreurs comme contexte de correction. Le DisplayLogger est ce qui rend la boucle observable : sans lui, await updater.UpdateNotebookWithAutoInvokeSKAgents() serait une boîte noire de plusieurs minutes.

C’est aussi la cellule qui a besoin des ressources extérieures (endpoint LLM configuré dans Settings.cs) : sur une machine sans configuration, elle échouerait à l’appel réseau — les sections précédentes, elles, restent exécutables.

Conclusion

Ce notebook illustre un cas d’usage méta de Semantic-Kernel : non pas interroger un LLM pour générer du texte, mais l’orchestrer pour concevoir et mettre à jour d’autres notebooks .NET Interactive de bout en bout.

Ce que démontre l’arc :

  • Initialisation : combinaison des packages de manipulation de notebooks et du kernel Semantic-Kernel, en distinguant soigneusement le kernel d’exécution .NET Interactive du kernel SK.
  • Fourniture d’informations : un enum InformationMode (Variable, Prompt, File) permet de décrire la tâche du notebook cible de plusieurs façons — exercice d’extension vers un mode Url.
  • Personnalisation : chargement d’un notebook template (Markdown + code à compléter), injection de la description dans l’en-tête, génération d’un nom de fichier — exercice d’enrichissement du nom.
  • Exécution itérative : AutoInvokeSKAgentsNotebookUpdater orchestre le cycle complet (template → description → mise à jour → exécution auto-invoquée) en s’appuyant sur le function calling OpenAI pour appliquer les modifications proposées par le LLM.

Points clés :

  • Le notebook est auto-modifiant : il utilise l’API .NET Interactive pour réécrire et ré-exécuter un autre notebook — un pattern d’agent génératif où le LLM agit sur un artefact de code, pas seulement sur du texte.
  • La séparation kernel d’exécution vs kernel SK est essentielle : elle isole l’orchestration du LLM de l’exécution du code généré.
  • Les exercices (InformationMode, GenerateNotebookName) ancrent la pratique sur les deux briques d’extension les plus naturelles : élargir les sources d’entrée et enrichir la personnalisation de sortie.

Référence : Microsoft Semantic-Kernel — Auto Function Calling. Le notebook pousse le function calling jusqu’à l’auto-édition de notebooks, anticipant les patterns d’agents logiciels qui modifient leur propre runtime.

Ce que ce notebook enseigne au-delà de Semantic-Kernel : la séparation nette entre la source d’intention (l’enum et ses modes), le substrat d’exécution (kernel .NET Interactive manipulé comme bibliothèque) et l’auteur (agents SK) est un pattern d’architecture d’agents général — chaque couche est remplaçable indépendamment (c’est ce que montrent les imports en commentaire : NotebookPlannerUpdater, AutoGenNotebookUpdater). Les trois exercices (mode Url, validation de description, nom de fichier) ajoutent chacun un maillon à une chaîne différente : l’entrée, la garde qualité, le système de fichiers.

Retour au sommet