Duel Verbal : Barbie vs l’Âne de Shrek

# Parameters
BATCH_MODE = True

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 :

  1. Contrainte — un style linguistique (rime, Shakespeare, chanson) est tiré au sort et imposé aux deux agents via leurs instructions système ;
  2. Agents — deux ChatCompletionAgent aux personnalités opposées, chacun sur son propre kernel ;
  3. Plugin — un ImagePlugin branché sur l’Âne illustre chaque réplique ;
  4. 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 .env local (voir OPENAI_API_KEY utilisé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 depuis OPENAI_CHAT_MODEL_ID) ;
  • dalle : OpenAITextToImage — le modèle de génération d’images gpt-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é dans create_kernel() par son service_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 de gpt-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_TURNS est une constante de classe, pas un champ d’instance — l’annotation ClassVar évite le conflit avec le mécanisme d’annotations de Semantic Kernel ;
  • le contrat d’async : should_terminate est async 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/1 alternent Barbie puis l’Âne de Shrek, conformément à AgentGroupChat avec 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 Rime tiré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 (CompletionUsage visible dans les logs) ; chaque image DALL-E gpt-image-1 consomme 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_Shrek déclenche image_gen-generate_image, conformément au câblage kernel_ane.add_plugin(...) de la configuration des agents.
  • La terminaison fonctionne : après 5 messages d’historique, DebateTerminationStrategy.should_terminate coupe 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 logs Selected agent at index N traç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_image est le goulot d’étranglement du débat.
  • La stratégie de terminaison fonctionne : le débat s’arrête après MAX_TURNS = 5 messages 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
Retour au sommet