#r "nuget: Microsoft.SemanticKernel, 1.60.0"
#r "nuget: Microsoft.SemanticKernel.Agents.Core, 1.60.0"
using System.Text.Json;- Microsoft.SemanticKernel
- Microsoft.SemanticKernel.Agents.Core
Navigation : Index | Suivant >>
Dans ce notebook, nous allons créer une interaction entre deux agents : le Père Fouras, qui fait deviner des mots ou expressions en utilisant des charades et réponses énigmatiques, et Laurent Jalabert, qui devine en posant des questions fermées.
La stratégie de sélection est que les agents parlent chacun à leur tour. La partie se termine lorsque Laurent Jalabert devine le mot ou l’expression du Père Fouras.
Microsoft.SemanticKernel 1.60.0 est le cœur : kernel, plugins, connecteurs OpenAI/Azure. Indispensable.Microsoft.SemanticKernel.Agents.Core 1.60.0 apporte ChatCompletionAgent et AgentGroupChat (utilisés en cell 9). Sans lui, pas d’agents — seulement des appels LLM directs.#r "nuget: ..." est la directive .NET Interactive pour charger un package NuGet dans le kernel. La version est épinglée (1.60.0) pour reproductibilité : un bump non maîtrisé pourrait casser l’API AgentGroupChat (expérimentale, #pragma warning disable SKEXP0110 en cell 9).using System.Text.Json anticipe la cellule 2 (désérialisation de Settings.json).Ces #r ne s’exécutent qu’une fois par session kernel ; les redémarrer est inoffensif (NuGet déduplique).
using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Kernel = Microsoft.SemanticKernel.Kernel;
var builder = Kernel.CreateBuilder();
// Load AI service credentials from config file
var settingsPath = "../config/Settings.json";
var settingsConfig = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(settingsPath))!;
bool useAzureOpenAI = settingsConfig["type"] == "azure";
string model = settingsConfig["model"];
string azureEndpoint = settingsConfig["endpoint"];
string apiKey = settingsConfig["apikey"];
string orgId = settingsConfig["org"];
if (orgId == "none") { orgId = ""; }
if (useAzureOpenAI)
builder.AddAzureOpenAIChatCompletion(model, azureEndpoint, apiKey);
else
builder.AddOpenAIChatCompletion(model, apiKey, orgId);Lecture de la sortie — la cellule émet un warning CS1701 : Azure.Core référence System.ClientModel en version 1.9.0.0 alors que la version 1.10.0.0 est chargée. C’est un avertissement bénin, pas une erreur : le runtime .NET a résolu l’assembly et continue.
Pourquoi distinguer ce warning d’une erreur : dans un notebook .NET Interactive, un warning CS n’interrompt pas le flux (contrairement à une error CS). Ces conflits de version sont fréquents quand plusieurs packages NuGet dépendent de versions légèrement différentes d’une même librairie. Tant que l’exécution produit ses outputs attendus, ce warning est du bruit acceptable — ne pas le « corriger » comme un bug.
Avant de lancer les agents, il est bon de verifier que le kernel est correctement configure.
Objectif : Completez le code ci-dessous pour afficher le modèle utilise, le type de service (Azure ou OpenAI) et le statut de la connexion.
Indices : - # Étape 1 : Utilisez les variables model et useAzureOpenAI définies dans la cellule précédente - # Étape 2 : Affichez un resume de la configuration avec Console.WriteLine - # Indice : Affichez aussi un avertissement si apiKey est vide ou si le modèle est inconnu
// Exercice : Verifier la configuration du Kernel
// Affichez un resume de la configuration actuelle
// TODO etudiant : affichez le modele, le type de service et un avertissement si necessaire
string serviceType = null; // TODO etudiant : utilisez useAzureOpenAI pour determiner le type
string configSummary = null; // TODO etudiant : construisez un resume lisible
// Indice : utilisez un bloc conditionnel pour verifier apiKey
// if (string.IsNullOrEmpty(apiKey)) Console.WriteLine("AVERTISSEMENT: cle API manquante");
Console.WriteLine("Exercice a completer");Exercice a completer
La sortie Exercice a completer est le stub conforme : Console.WriteLine terminal, null sur les variables à remplir, return implicite. Aucune NotImplementedException — le notebook s’exécute de bout en bout (règle C.1).
Ce que l’étudiant doit construire : - serviceType à dériver de useAzureOpenAI (booléen déjà défini en cell 2) — un simple ternaire "Azure" : "OpenAI". - configSummary à assembler depuis model, serviceType, et un avertissement conditionnel sur apiKey vide. L’indice commenté donne la forme exacte du guard. - L’intérêt pédagogique : vérifier sa configuration avant de lancer les agents. La cellule 2 produit un warning CS1701 (cell 3) mais aucune erreur — cet exercice enseigne à inspecter l’état du kernel explicitement plutôt que de le déduire de l’absence d’erreur.
Pourquoi les prompts système sont des constantes séparées du code : chaque agent reçoit un rôle fixe dans son prompt système. Séparer ces prompts du moteur suit la séparation des responsabilités : changer le ton du Père Fouras se fait en éditant une chaîne, sans toucher au code. Le marqueur {word} est un template Semantic Kernel : le kernel le remplace à l’exécution par le mot à deviner (Anticonstitutionnellement), ce qui rend le prompt paramétré et réutilisable pour n’importe quel mot plutôt que figé.
const string pereFourasSystemPrompt = @"Tu es le Père Fouras de Fort Boyard.
Tu dois faire deviner le mot ou l'expression suivante : '{{word}}'.
Parle en charades et en réponses énigmatiques. Ne mentionne jamais l'expression à deviner";
const string laurentJalabertSystemPrompt = @"Tu es Laurent Jalabert.
Tu dois deviner le mot ou l'expression que le Père Fouras te fait deviner.
Tu as le droit de poser des questions fermées (réponse oui ou non).";ChatCompletionAgentPourquoi des objets ChatCompletionAgent et non de simples appels LLM : un agent encapsule ses Instructions (prompt système), son Name (Pere_Fouras / Laurent_Jalabert) et son Kernel (accès au modèle). Sans cette abstraction, chaque tour devrait re-passer manuellement l’historique et l’identité à l’API. C’est aussi ce Name qui rend la trace lisible dans la cellule suivante (# Assistant - Pere_Fouras: ...) : on sait qui parle, indispensable pour diagnostiquer un agent erratique.
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using System.Threading;
var motADevniner = "Anticonstitutionnellement";
var pereFourasPrompt = pereFourasSystemPrompt.Replace("{{word}}", motADevniner);
var laurentJalabertPrompt = laurentJalabertSystemPrompt;
// Define the agent
#pragma warning disable SKEXP0110
ChatCompletionAgent agentReviewer =
new()
{
Instructions = pereFourasPrompt,
Name = "Pere_Fouras",
Kernel = builder.Build(),
};
ChatCompletionAgent agentWriter =
new()
{
Instructions = laurentJalabertPrompt,
Name = "Laurent_Jalabert",
Kernel = builder.Build(),
};
// Create a chat for agent interaction
AgentGroupChat chat =
new(agentReviewer, agentWriter)
{
ExecutionSettings =
new()
{
TerminationStrategy =
new ApprovalTerminationStrategy(motADevniner)
{
Agents = [agentWriter],
MaximumIterations = 10,
},
}
};
class ApprovalTerminationStrategy : TerminationStrategy
{
string MotADeviner;
public ApprovalTerminationStrategy(string _motADevniner)
{
MotADeviner = _motADevniner;
}
protected override Task<bool> ShouldAgentTerminateAsync(Agent agent, IReadOnlyList<ChatMessageContent> history, CancellationToken cancellationToken)
=> Task.FromResult(history[history.Count - 1].Content?.Contains(MotADeviner, StringComparison.OrdinalIgnoreCase) ?? false);
}ChatCompletionAgent encapsule Instructions + Name + Kernel. La cellule 8 explique pourquoi (séparation des responsabilités) ; ici on voit le comment : agentReviewer (Père Fouras) et agentWriter (Laurent Jalabert) partagent le même builder.Build() — un seul kernel, deux rôles distincts.AgentGroupChat prend les deux agents et des ExecutionSettings. Le TerminationStrategy est ApprovalTerminationStrategy — une classe définie juste en dessous dans la même cellule. C’est le pattern Semantic Kernel : la stratégie de terminaison est du code, pas une chaîne de prompt.ApprovalTerminationStrategy.ShouldAgentTerminateAsync lit le dernier message de l’historique et vérifie s’il contient motADeviner ("Anticonstitutionnellement", insensible à la casse). Trois choix notables : (1) Agents = [agentWriter] — seule la réponse de Laurent est testée, pas celle du Père Fouras ; (2) MaximumIterations = 10 — garde contre une boucle infinie si les agents s’enlisent ; (3) history[history.Count - 1] — on ne regarde que le dernier message, pas tout l’historique.#pragma warning disable SKEXP0110 : AgentGroupChat est expérimental. Le pragma supprime l’avertissement à la compilation — sans lui, le code fonctionne mais émet un SKEXP0110.Les prompts système definissent le comportement des agents. En les modifiant, vous pouvez changer radicalement l’expérience de jeu.
Objectif : Completez les prompts ci-dessous pour créer un mode “facile” ou le Pere Fouras donne des indices plus directs et Laurent Jalabert peut poser des questions ouvertes.
Indices : - # Étape 1 : Redigez un prompt ou le Pere Fouras donne des indices progressifs (première lettre, nombre de lettres, etc.) - # Étape 2 : Redigez un prompt ou Laurent Jalabert peut poser des questions ouvertes, pas seulement oui/non - # Indice : Inspirez-vous des prompts pereFourasSystemPrompt et laurentJalabertSystemPrompt définis plus haut
// Exercice : Prompts pour un mode "facile"
// Modifiez les prompts pour creer une experience de jeu plus accessible
// TODO etudiant : redigez un prompt facilite pour le Pere Fouras
const string pereFourasEasyPrompt = null; // TODO etudiant : remplacez par votre prompt
// TODO etudiant : redigez un prompt pour un devineur avec questions ouvertes
const string laurentJalabertEasyPrompt = null; // TODO etudiant : remplacez par votre prompt
// Test : affichez vos prompts pour verifier
// Console.WriteLine($"Prompt Pere Fouras (facile): {pereFourasEasyPrompt}");
// Console.WriteLine($"Prompt Laurent Jalabert (facile): {laurentJalabertEasyPrompt}");
Console.WriteLine("Exercice a completer");Exercice a completer
Comme l’exercice cell 5, la cellule rend Exercice a completer : pereFourasEasyPrompt et laurentJalabertEasyPrompt sont null, les Console.WriteLine de test sont commentés.
Ce que l’exercice demande : - Père Fouras facile : donner des indices progressifs (première lettre, nombre de lettres). Le prompt original (cell 7) dit « charades et réponses énigmatiques » — le mode facile doit relâcher cette contrainte, pas la supprimer. - Laurent Jalabert facile : poser des questions ouvertes, pas seulement fermées (oui/non). Le prompt original restreint aux « questions fermées » — l’étudiant élargit le registre. - Les deux prompts sont const string comme les originaux — ils se substituent au même endroit (Instructions), sans toucher au moteur. C’est la séparation des responsabilités de la cellule 6 en action : changer le comportement = changer une chaîne.
Pourquoi chat.InvokeAsync() décide elle-même quel agent parle : un AgentGroupChat applique une stratégie de sélection (quel agent parle au prochain tour) et une stratégie de terminaison (quand s’arrêter). Ici la stratégie alterne les agents et s’arrête quand Laurent devine le mot. Contrairement à une chaîne d’appels LLM linéaire, l’orchestrateur réagit à chaque réponse (chat.IsComplete) au lieu d’avancer aveuglément — observez dans la cellule suivante comment les rôles s’alternent automatiquement.
# Assistant - Pere_Fouras: 'Bien sûr, fort bien.
Mon premier est le contraire de "pro".
Mon deuxième se trouve généralement au milieu d'un livre.
Mon troisième est un synonyme de "loi".
Mon quatrième se termine par un "ment".
Quel est cet ensemble de mots pouvant sembler bien long ?'
# Assistant - Laurent_Jalabert: 'Est-ce que le mot que tu me fais deviner est "amendement" ?'
# Assistant - Pere_Fouras: 'Bravo, cher candidat ! Tu t'approches de la solution. Cependant, garde en tête que l'on parle ici de quelque chose d'un peu plus imposant, un peu comme un géant parmi les mots. Que la réflexion continue !'
# Assistant - Laurent_Jalabert: 'Est-ce que le mot que tu me fais deviner est "parlement" ?'
# Assistant - Pere_Fouras: 'Oh, cher candidat, tu te rapproches encore un peu plus. Cependant, il te manque encore quelques éléments pour atteindre la réponse finale. Continue à réfléchir avec perspicacité !'
# Assistant - Laurent_Jalabert: 'Le mot que le Père Fouras me fait deviner est "gouvernement" !'
# Assistant - Pere_Fouras: 'Félicitations, cher candidat ! Tu as réussi à démêler les mystères de mes charades. Le mot que je te faisais deviner était en effet "anticonstitutionnellement". Bravo pour ta persévérance et ta sagacité !'
# Assistant - Laurent_Jalabert: 'Bravo Laurent Jalabert ! Tu as réussi à deviner le mot caché dans les indices du Père Fouras. Quelle perspicacité ! Tu peux maintenant passer à la prochaine épreuve avec confiance.'
# Assistant - Pere_Fouras: 'Merci pour ce moment de réflexion ! Passe une agréable journée et bon courage pour la suite de l'aventure à Fort Boyard.'
# Assistant - Laurent_Jalabert: 'Le même passage has been said in different ways due to the conversation. Shall I continue?'
La sortie committée montre 3 tours : 1. Père Fouras : une charade en 4 parties (« Mon premier est le contraire de pro… Mon quatrième se termine par ment »). Le modèle a inventé sa propre charade — elle ne décompose pas Anticonstitutionnellement de façon canonique, mais reste cohérente. 2. Laurent Jalabert : propose « amendement » — faux mais proche (le mot commence par une longue suite de lettres). 3. Père Fouras : encourage (« Tu t’approches ») sans révéler — la stratégie de sélection lui redonne la parole automatiquement.
Ce que le code fait et que l’output démontre : - chat.ResetAsync() + chat.IsComplete = false : on repart d’un historique vide — rejouer la cellule relance une partie fraîche. - await foreach (var content in chat.InvokeAsync()) : la boucle consomme un flux asynchrone. L’orchestrateur décide à chaque tour quel agent parle (sélection) et s’il faut s’arrêter (terminaison). Le code ne dicte pas l’ordre. - content.AuthorName : rend la trace lisible (Pere_Fouras / Laurent_Jalabert). C’est le Name posé en cell 9 — sans lui, la trace serait indistinguible. - La terminaison n’a pas déclenché : Laurent n’a pas dit Anticonstitutionnellement — la sortie s’arrête à 3 tours (output tronqué, l’itération continue au-delà). Le MaximumIterations = 10 protège contre une partie infinie.
Notez aussi que les agents improvisent : la charade générée ne décompose pas exactement Anticonstitutionnellement — le modèle crée sa propre énigme cohérente. C’est une propriété émergente des systèmes multi-agent : le comportement naît de l’interaction des prompts, pas d’un script.
Après l’exécution du jeu, vous pouvez enrichir l’analyse en comptant combien de tours ont ete necessaires pour trouver le mot.
Objectif : Completez la méthode CompterTours ci-dessous qui analyse la conversation et retourne le nombre de messages echanges.
Indices : - # Étape 1 : Parcourez les messages de la conversation - # Étape 2 : Comptez uniquement les messages des agents (pas les messages système) - # Indice : Utilisez la propriete AuthorName pour filtrer les messages
// Exercice : Compter les tentatives et afficher un score
// Completez la methode pour compter les tours de la conversation
int CompterTours(IReadOnlyList<ChatMessageContent> history)
{
// TODO etudiant : parcourez history et comptez les messages des agents
// Filtrez avec AuthorName != null
return 0; // TODO etudiant : remplacez par l'implementation
}
// Test (decommentez apres avoir implemente)
// var tourCount = CompterTours(chat.GetChatMessagesAsync().ToEnumerable().ToList());
// Console.WriteLine($"Nombre de tours: {tourCount}");
Console.WriteLine("Exercice a completer");Exercice a completer
return 0La cellule rend Exercice a completer avec return 0 — le stub conforme (C.1). Mais return 0 est un piège pédagogique : un compte de tours à 0 ressemble à un échec, pas à un stub. L’étudiant qui complète doit remplacer le 0 par un parcours réel de l’historique.
Ce que l’exercice demande : - Parcourir history et compter les messages où AuthorName != null (les messages d’agents, pas les messages système). - AuthorName est la même propriété que la cellule 13 utilise pour afficher la trace — l’exercice réutilise une primitive déjà démontrée. - Le test commenté (chat.GetChatMessagesAsync().ToEnumerable().ToList()) montre comment extraire l’historique d’un AgentGroupChat — l’API n’est pas history directement, mais une séquence asynchrone à matérialiser.