# Parameters
BATCH_MODE = TrueDuel Verbal : Barbie vs l’Âne de Shrek
Introduction - barbie schreck
Navigation : Index
Ce notebook couvre les concepts et techniques principaux de barbie schreck. Les objectifs pedagogiques incluent la comprehension des fondamentaux, la mise en pratique via des exercices, et analyse de résultats.
Prerequis : notions de base en Python et en algorithmique.
Adapté d’une production étudiante EPF : Carole & Cléo. Universalisation : refactor #890.
Notebook utilisant Semantic Kernel pour organiser un débat contraint entre deux agents — Barbie et l’Âne de Shrek — avec génération d’images DALL-E à chaque réplique de l’Âne.
Le fil conducteur pédagogique en 4 actes :
- Contrainte — un style linguistique (rime, Shakespeare, chanson) est tiré au sort et imposé aux deux agents via leurs instructions système ;
- Agents — deux
ChatCompletionAgentaux personnalités opposées, chacun sur son propre kernel ; - Plugin — un
ImagePluginbranché sur l’Âne illustre chaque réplique ; - Orchestration — un
AgentGroupChat+ une stratégie de terminaison font tourner le duel jusqu’àMAX_TURNS.
Les trois exercices font évoluer ce socle : nouvelles contraintes, troisième agent arbitre, terminaison dynamique par mots-clés.
# Import guards - availability flags for external dependencies
try:
from IPython.display import display, Audio, Image as IPImage
IPYTHON_AVAILABLE = True
except ImportError:
IPYTHON_AVAILABLE = False
print(f' IPython non disponible - certaines fonctionnalites seront limitees')
try:
from dotenv import load_dotenv
DOTENV_AVAILABLE = True
except ImportError:
DOTENV_AVAILABLE = False
print(f' dotenv non disponible - certaines fonctionnalites seront limitees')
try:
import semantic_kernel
SEMANTIC_KERNEL_AVAILABLE = True
except ImportError:
SEMANTIC_KERNEL_AVAILABLE = False
print(f' semantic_kernel non disponible - certaines fonctionnalites seront limitees')Configuration initiale
Cette cellule importe toutes les briques nécessaires et prépare l’environnement :
os,random,logging— utilitaires standard (variables d’env, tirage de contrainte, logs) ;dotenv—load_dotenv()charge les clés API depuis le fichier.envlocal (voirOPENAI_API_KEYutilisée plus bas) ;semantic_kernel— le cœur :Kernel,ChatCompletionAgent,AgentGroupChat, les connecteurs OpenAI (chat + texte-à-image),kernel_function,ChatMessageContent/AuthorRole.
Le logging.basicConfig(level=logging.INFO) active les logs INFO qui rendront l’exécution du débat lisible (sélection d’agent, usage API). L’output Imports et configuration OK confirme que toutes les dépendances sont présentes sur la machine d’exécution.
La cellule # Import guards précédente joue un rôle de pré-vol : elle vérifie la disponibilité de IPython, dotenv et semantic_kernel et définit des flags (IPYTHON_AVAILABLE, etc.) sans interrompre le notebook si une dépendance manque.
import os
import random
import logging
from dotenv import load_dotenv
from semantic_kernel import Kernel
from semantic_kernel.agents import ChatCompletionAgent, AgentGroupChat
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.connectors.ai.open_ai.services.open_ai_text_to_image import OpenAITextToImage
from semantic_kernel.functions import kernel_function, KernelArguments
from semantic_kernel.contents import ChatMessageContent, AuthorRole
load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('BarbieVsAne')
print("Imports et configuration OK")Imports et configuration OK
Définition des contraintes linguistiques
Le débat ne gagne en intérêt que si les répliques obéissent à une règle. La liste CONTRAINTES définit trois styles possibles, chacun sous la forme d’un tuple (nom, description) :
| Nom | Règle |
|---|---|
Rime |
Chaque réplique contient une rime parfaite |
Shakespeare |
Imiter le style théâtral de Shakespeare |
Chanson |
Répondre sur l’air d’I’m a Believer |
random.choice tire une contrainte au sort à chaque exécution — l’output montre que la Rime a été choisie, et les répliques du débat riment effectivement. La structure en tuples (nom, description) est celle que l’Exercice 1 demande d’étendre : la description est interpolée dans les instructions des agents, donc toute nouvelle entrée de la liste se répercute automatiquement sur le comportement des deux duellistes.
CONTRAINTES = [
("Rime", "Chaque réplique doit contenir une rime parfaite"),
("Shakespeare", "Imiter le style théâtral de Shakespeare"),
("Chanson", "Répondre sur l'air de 'I'm a Believer'")
]
contrainte_choisie = random.choice(CONTRAINTES)
print(f"Contrainte choisie : {contrainte_choisie[0]}")
print("Contraintes de debate definies")Contrainte choisie : Rime
Contraintes de debate definies
Exercice 1 : Ajout de contraintes linguistiques personnalisees
La liste CONTRAINTES contient actuellement trois styles (Rime, Shakespeare, Chanson). L’objectif est d’enrichir cette liste avec de nouvelles contraintes originales pour varier les debats.
Objectif : ajouter au moins deux nouvelles contraintes a la liste, par exemple un style “Haiku japonais” (3 lignes, 5-7-5 syllabes) ou “Code Python” (repondre en utilisant la syntaxe Python).
Indices : - # Étape 1 : Définir le nom et la description de chaque nouvelle contrainte - # Étape 2 : Ajouter les tuples (nom, description) a la liste CONTRAINTES - # Indice : respecter le format existant ("Nom", "Description de la règle")
# Exercice 1 : Ajout de contraintes linguistiques personnalisees
# TODO etudiant : ajouter de nouvelles contraintes a la liste
NOUVELLES_CONTRAINTES = [] # Etape 1 : definir les nouvelles contraintes
# Etape 2 : les ajouter a CONTRAINTES
# CONTRAINTES.extend(NOUVELLES_CONTRAINTES)
print("Exercice a completer : nouvelles contraintes")Exercice a completer : nouvelles contraintes
Création du kernel
create_kernel() instancie un Kernel Semantic Kernel et y enregistre deux services OpenAI distincts, identifiés par leur service_id :
chat:OpenAIChatCompletion— le modèle de conversation (id lu depuisOPENAI_CHAT_MODEL_ID) ;dalle:OpenAITextToImage— le modèle de génération d’imagesgpt-image-1, codé en dur ici.
Le pattern service_id est central : c’est lui qui permet ensuite à kernel.get_service("dalle", OpenAITextToImage) (dans ImagePlugin) et aux agents de retrouver le bon service, même quand un kernel en porte plusieurs.
Les clés API sont lues depuis l’environnement (os.getenv) — jamais en dur dans le notebook — conformément à l’hygiène des secrets du dépôt. La fonction est appelée deux fois (un kernel par agent) : chaque duelliste dispose de sa propre pile de services, ce qui isole leurs appels.
def create_kernel():
kernel = Kernel()
kernel.add_service(OpenAIChatCompletion(
service_id="chat",
ai_model_id=os.getenv("OPENAI_CHAT_MODEL_ID"),
api_key=os.getenv("OPENAI_API_KEY")
))
kernel.add_service(OpenAITextToImage(
service_id="dalle",
ai_model_id="gpt-image-1",
api_key=os.getenv("OPENAI_API_KEY")
))
return kernel
print("Fonction create_kernel définie")
print("Fonction create_kernel definie")Fonction create_kernel définie
Fonction create_kernel definie
Plugin de génération d’images
ImagePlugin encapsule l’appel au service DALL-E du kernel sous forme de fonction invocable Semantic Kernel. Le décorateur @kernel_function(name="generate_image", description="...") expose la méthode generate comme une capacité que le kernel peut invoquer par nom.
Détails de l’implémentation :
- récupération du service :
kernel.get_service("dalle", OpenAITextToImage)retrouve le service ajouté danscreate_kernel()par sonservice_id; - style appliqué : chaque image est générée avec le préfixe
"Style cartoon comique - "+ la réplique de l’Âne en contexte — c’est ce qui produit des illustrations cohérentes avec le duel ; - dimensions :
1024x1024(carré), le format natif degpt-image-1; - robustesse : en cas d’échec, l’exception est loggée (
logger.error) et la méthode retourne une chaîne vide — le débat continue sans illustration plutôt que de crasher.
L’output de la cellule montre la classe définie avec succès, prête à être branchée sur un kernel.
class ImagePlugin:
def __init__(self, kernel):
self.text_to_image = kernel.get_service("dalle", OpenAITextToImage)
@kernel_function(name="generate_image", description="Genere une image via DALL-E")
async def generate(self, context: str) -> str:
try:
response = await self.text_to_image.generate_image(
description=f"Style cartoon comique - {context}",
width=1024,
height=1024
)
return str(response)
except Exception as e:
logger.error(f"Erreur generation image: {e}")
return ""
print("Classe ImagePlugin définie")
print("Debat configure")
print("Classe ImagePlugin definie")Classe ImagePlugin définie
Debat configure
Classe ImagePlugin definie
Configuration des agents
Chaque duelliste est un ChatCompletionAgent avec son propre kernel (kernel_barbie, kernel_ane) et des instructions système distinctes :
- Barbie : « Défends des positions optimistes avec élégance » — le ton soigné ;
- L’Âne de Shrek : « Utilise l’humour absurde et décalé » — le ton débridé.
Les deux instructions embarquent la contrainte linguistique choisie (contrainte_choisie[1]), interpolée dans le prompt système : c’est ce qui garantit que chaque réplique, quel que soit l’agent, respecte la règle du débat.
Point de câblage important : le plugin ImagePlugin est ajouté uniquement au kernel de l’Âne (kernel_ane.add_plugin(...)). Barbie parle, l’Âne parle et illustre — la dissymétrie est volontaire et montre que les plugins sont attachés par kernel, pas globalement.
# Création des kernels séparés
kernel_barbie = create_kernel()
kernel_ane = create_kernel()
image_Plugin = ImagePlugin(kernel_ane)
# Ajout du plugin uniquement à l'Âne
kernel_ane.add_plugin(image_Plugin, plugin_name="image_gen")
barbie = ChatCompletionAgent(
kernel=kernel_barbie,
name="Barbie",
instructions=f"""
Tu es Barbie. Défends des positions optimistes avec élégance.
Contrainte obligatoire : {contrainte_choisie[1]}
"""
)
ane = ChatCompletionAgent(
kernel=kernel_ane,
name="Ane_Shrek",
instructions=f"""
Tu es l'Âne de Shrek. Utilise l'humour absurde et décalé.
Contrainte obligatoire : {contrainte_choisie[1]}
"""
)
print("Kernels et agents Barbie/Ane créés")
print("Kernels et agents Barbie/Ane crees")Kernels et agents Barbie/Ane créés
Kernels et agents Barbie/Ane crees
Exercice 2 : Creation d’un troisieme agent
Le debat oppose actuellement Barbie et l’Ane de Shrek. L’objectif est d’ajouter un troisieme personnage au debat : un Arbitre qui commente les performances des deux duellistes a chaque tour.
Objectif : créer un agent ChatCompletionAgent nomme “Arbitre” avec un kernel separe et des instructions système qui lui demandent de juger la qualite des repliques (respect de la contrainte, creativite, humour).
Indices : - # Étape 1 : Créer un kernel dedie pour l’Arbitre avec create_kernel() - # Étape 2 : Définir les instructions de l’Arbitre (rôle, comportement attendu) - # Indice : s’inspirer des instructions de Barbie et l’Ane pour le format du prompt
# Exercice 2 : Creation d'un troisieme agent (Arbitre)
# TODO etudiant : creer le kernel et l'agent Arbitre
kernel_arbitre = None # Etape 1 : creer un kernel avec create_kernel()
ARBITRE_INSTRUCTIONS = "" # Etape 2 : definir les instructions de l'arbitre
arbitre = None # TODO etudiant : creer le ChatCompletionAgent
print("Exercice a completer : agent Arbitre")Exercice a completer : agent Arbitre
Stratégie de terminaison
Sans règle d’arrêt, un AgentGroupChat tournerait indéfiniment. DebateTerminationStrategy sous-classe TerminationStrategy et implémente should_terminate : le débat s’arrête dès que l’historique atteint MAX_TURNS = 5 messages.
Deux détails d’implémentation à noter :
ClassVar[int]:MAX_TURNSest une constante de classe, pas un champ d’instance — l’annotationClassVarévite le conflit avec le mécanisme d’annotations de Semantic Kernel ;- le contrat d’
async:should_terminateestasync def, ce qui permettrait plus tard une terminaison dynamique (interroger le contenu du dernier message, un service externe, etc.) — c’est exactement la piste de l’Exercice 3.
La stratégie est passée à l’AgentGroupChat au moment de sa construction, et est consultée après chaque message ajouté à l’historique.
# %% [code]
from typing import ClassVar
from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy
class DebateTerminationStrategy(TerminationStrategy):
MAX_TURNS: ClassVar[int] = 5 # Annotation correcte avec ClassVar
async def should_terminate(self, agent, history, cancellation_token=None) -> bool:
return len(history) >= self.MAX_TURNS
print("Stratégie de terminaison du débat configurée")
print("Fonction create_kernel definie")
print("Strategie de terminaison du debate configuree")Stratégie de terminaison du débat configurée
Fonction create_kernel definie
Strategie de terminaison du debate configuree
Lancement du débat
L’AgentGroupChat rassemble les deux agents (barbie, ane) et la stratégie de terminaison définie plus haut. L’appel add_chat_message amorce la conversation — Semantic Kernel exige un historique non vide — puis group_chat.invoke() fait tourner le duel :
- chaque agent produit sa réplique à tour de rôle, conformément à la stratégie de sélection séquentielle ;
- spécificité pédagogique : à chaque réplique de l’Âne de Shrek, le code invoque le plugin
image_gen(generate_image) avec le contenu de la réplique en contexte, puis affiche l’image DALL-E générée (width=300).
C’est ici que se matérialise la boucle « parler puis illustrer » : le texte d’un agent devient l’argument d’un appel d’image, reliant les deux services OpenAI (chat + génération) dans le même débat.
async def run_debate():
logger.info(f"Contrainte active : {contrainte_choisie[0]}")
group_chat = AgentGroupChat(
agents=[barbie, ane],
termination_strategy=DebateTerminationStrategy()
)
# Add initial topic message (SK requires non-empty chat history)
await group_chat.add_chat_message(
ChatMessageContent(role=AuthorRole.USER, content="Commencez le duel verbal !")
)
async for msg in group_chat.invoke():
print(f"\n{msg.name}: {msg.content}")
if msg.name == "Ane_Shrek":
try:
image_result = await ane.kernel.invoke(
function_name="generate_image",
plugin_name="image_gen",
arguments=KernelArguments(context=msg.content)
)
if image_result:
import base64
from IPython.display import display, Image
img_str = str(image_result)
if img_str.startswith("http"):
display(Image(url=img_str, width=300))
else:
display(Image(data=base64.b64decode(img_str), width=300))
except Exception as e:
logger.warning(f"Image generation skipped: {e}")
await run_debate()
Barbie: J’entre en scène, sourire intact, prête à te mener vers la **victoire**,
Dans ce duel verbal, je garde le cœur léger et je vise la **victoire**.
Ane_Shrek: Je débarque en braillant, je veux du bruit et du **fromage**,
Duel verbal en plein marais : prépare ta langue au **fromage**.

Barbie: Je monte sur scène, talons bien posés, cap sur la **victoire**,
À toi de lancer ta meilleure pique, je répondrai par la **victoire**.
Ane_Shrek: Je débarque en trombe, j’te défie sans sommation, c’est la **bagarre**,
Crache ta meilleure vanne, moi j’te réponds au galop, c’est la **bagarre**.

Lecture du résultat : le duel verbal réel
L’exécution montre le protocole complet en action, tour par tour :
- Sélection séquentielle des agents : les logs
Selected agent at index 0/1alternent Barbie puis l’Âne de Shrek, conformément àAgentGroupChatavec sa stratégie de sélection par défaut. - Contrainte de rime respectée : Barbie répond « sourire intact, prête à te mener vers la victoire » puis « talons bien posés, cap sur la victoire » ; l’Âne rime « je veux du bruit et du fromage » puis « j’te réponds au galop, c’est la bagarre ». Chaque paire de vers obéit à la contrainte
Rimetirée au sort — l’instruction système portée par l’agent suffit à contraindre la génération. - Consommation API mesurée : chaque réplique textuelle consomme entre 95 et 512 tokens (
CompletionUsagevisible dans les logs) ; chaque image DALL-Egpt-image-1consomme 4160 output tokens et ~37-40 s de génération (durées 37.48 s et 39.95 s observées). - Le plugin d’image n’est branché que sur l’Âne : seul
Ane_Shrekdéclencheimage_gen-generate_image, conformément au câblagekernel_ane.add_plugin(...)de la configuration des agents. - La terminaison fonctionne : après 5 messages d’historique,
DebateTerminationStrategy.should_terminatecoupe la boucle.
La sortie est donc la preuve de bout en bout du montage : deux agents contraints, un plugin d’image, une stratégie de terminaison — le tout orchestré par Semantic Kernel.
Conclusion
Ce notebook a démontré un débat multi-agents contraint avec Semantic Kernel : deux ChatCompletionAgent (Barbie et l’Âne de Shrek) s’affrontent en respectant une contrainte linguistique tirée au sort (ici la Rime), et l’Âne illustre chacune de ses répliques par une image DALL-E générée à la volée.
Points clés observés dans l’exécution réelle :
- Le protocole de sélection séquentielle alterne les agents (Barbie → Âne → Barbie → Âne) via
SequentialSelectionStrategy, les logsSelected agent at index Ntraçant chaque tour. - La contrainte linguistique est respectée : les répliques produites riment (« je vise la victoire » / « prépare ta langue au fromage », « cap sur la victoire » / « c’est la bagarre »), preuve que l’instruction système portant la contrainte est suivie par le modèle.
- La génération d’images est coûteuse mais fiable : chaque illustration consomme 4160 output tokens DALL-E (usage observé) et prend ~37-40 s — l’appel
text_to_imageest le goulot d’étranglement du débat. - La stratégie de terminaison fonctionne : le débat s’arrête après
MAX_TURNS = 5messages d’historique.
Pour aller plus loin : les trois exercices proposent d’étendre le système — nouvelles contraintes (Exercice 1), un troisième agent arbitre (Exercice 2), et une terminaison dynamique par mots-clés (Exercice 3). L’architecture (kernel + plugin + stratégie de terminaison) est réutilisable telle quelle pour d’autres formats de joute (débat politique, concours de poésie, improvisation théâtrale).
Exercice 3 : Stratégie de terminaison personnalisee
L’implementation actuelle arrete le debat après un nombre fixe de tours (MAX_TURNS = 5). Imaginez un scénario ou le debat doit s’arreter dynamiquement selon le contenu des messages.
Objectif : implementer une stratégie de terminaison qui detecte quand un des deux personnages utilise un mot spécifique (par exemple “capituler” ou “abandon”) dans sa reponse, et arrete le debat immediatement.
Indices : - # Étape 1 : Définir une liste de mots-cles declencheurs - # Étape 2 : Dans should_terminate, verifier si le dernier message contient un mot-cle - # Indice : utiliser str(history[-1].content).lower() pour lire le dernier message
from typing import ClassVar
from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy
class SurrenderTerminationStrategy(TerminationStrategy):
# TODO etudiant : completer la strategy de terminaison par mots-cles
TRIGGER_WORDS: ClassVar[list[str]] = [] # Etape 1 : mots-cles declencheurs
async def should_terminate(self, agent, history, cancellation_token=None) -> bool:
# Etape 2 : verifier si le dernier message contient un mot-cle
result = False # TODO etudiant : remplacer par la logique de detection
return result
print("Exercice a completer : SurrenderTerminationStrategy")Exercice a completer : SurrenderTerminationStrategy