SK-3-Agents : Agent Framework Semantic Kernel

Navigation : << 02-Functions | Index | 04-Filters >>


Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Créer un ChatCompletionAgent simple avec instructions 2. Integrer des plugins avec Function Calling automatique 3. Orchestrer plusieurs agents via AgentGroupChat 4. Définir des stratégies de terminaison personnalisees 5. Comprendre les bases d’OpenAIAssistantAgent (Code Interpreter, File Search)

Prerequis

  • Python 3.10+
  • Notebooks 01 et 02 completes
  • Cle API OpenAI configuree (.env)

Duree estimee : 55 minutes


Sommaire

Section Contenu Concepts cles
1 Installation semantic-kernel, imports
2 Agent Simple ChatCompletionAgent, instructions, invoke
3 Agent + Plugins MenuPlugin, FunctionChoiceBehavior.Auto()
4 Group Chat AgentGroupChat, TerminationStrategy
5 OpenAIAssistantAgent Code Interpreter, File Search, Threads
6 Conclusion Resume, exercices, navigation

Qu’est-ce que l’Agent Framework ? Introduit avec SK 1.0, il permet de créer des agents autonomes capables de raisonner, utiliser des outils, et collaborer entre eux. C’est l’evolution naturelle des plugins vers des entites plus intelligentes.

# ============================
# Bloc 1 : Installation semantic-kernel et imports
# ============================

# A n'executer qu'une fois
import asyncio
import logging
import os
from dotenv import load_dotenv

# Chargement du fichier .env (cles API)
load_dotenv("../.env")

# Verification de la configuration
api_key = os.getenv("OPENAI_API_KEY")
model_id = os.getenv("OPENAI_CHAT_MODEL_ID", "gpt-5")
print(f"Configuration chargee:")
print(f"  - API Key: {'OK' if api_key else 'MANQUANTE'}")
print(f"  - Modele: {model_id}")
print("semantic-kernel installe.")
Configuration chargee:
  - API Key: OK
  - Modele: gpt-5.2
semantic-kernel installe.

1. Installation et imports

Cette cellule installe le SDK Semantic Kernel et importe les modules necessaires pour créer des agents conversationnels.

Concepts cles : - ChatCompletionAgent : Un agent base sur un modèle de chat completion - AgentGroupChat : Orchestrateur pour faire collaborer plusieurs agents - ChatHistory : Historique de conversation partage

Bloc 2 : Simple Agent (Parrot)

Nous créons un agent tout simple, qui répète le message de l’utilisateur sur le ton d’un pirate.

import logging
from semantic_kernel import Kernel
from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.contents import ChatHistory

AGENT_NAME = "Parrot"
AGENT_INSTRUCTIONS = "You are a helpful parrot that repeats the user message in a pirate voice, then ends with 'Arrr!'"

# Création du Kernel
kernel = Kernel()
# On suppose que vous avez défini ou récupéré des clés d'API :
# kernel.add_service(OpenAIChatCompletion(...)) ou AzureChatCompletion(...)
kernel.add_service(OpenAIChatCompletion(service_id="agent"))

agent = ChatCompletionAgent(
    kernel=kernel,
    name=AGENT_NAME,
    instructions=AGENT_INSTRUCTIONS
)
user_inputs = [
    "Fortune favors the bold.",
    "I came, I saw, I conquered.",
    "Practice makes perfect.",
]

async def simple_agent_demo():
    chat_history = ChatHistory()
    # On ajoute les instructions de l'agent en tant que 'developer' ou 'system'
    chat_history.add_developer_message(AGENT_INSTRUCTIONS)

    for user_input in user_inputs:
        chat_history.add_user_message(user_input)
        print(f"# User: '{user_input}'")
        async for content in agent.invoke(chat_history):
            # CORRECTION DÉFINITIVE : Toujours utiliser add_assistant_message
            # L'API SemanticKernel agents retourne des objets incompatibles avec add_message()
            if hasattr(content, 'content'):
                # Extraire le contenu et le convertir en string
                content_str = str(content.content) if content.content else str(content)
                chat_history.add_assistant_message(content_str)
                print(f"# Agent - {content.name or AGENT_NAME}: '{content_str}'")
            else:
                # Fallback: convertir tout l'objet en string
                content_str = str(content)
                chat_history.add_assistant_message(content_str)
                print(f"# Agent - {AGENT_NAME}: '{content_str}'")

await simple_agent_demo()
# User: 'Fortune favors the bold.'
# Agent - Parrot: 'Fortune be favorin’ the bold, matey. Arrr!'
# User: 'I came, I saw, I conquered.'
# Agent - Parrot: 'I came, I saw, I conquered, matey. Arrr!'
# User: 'Practice makes perfect.'
# Agent - Parrot: 'Practice be makin’ perfect, matey. Arrr!'

Exercice 1 : Agent avec personnalite parametrable

Objectif : Créer une fonction qui genere un agent ChatCompletionAgent avec une personnalite configurable, et tester 3 personnalites différentes sur la même question.

L’agent Parrot utilise des instructions fixees en dur. En production, on veut généralement créer des agents dynamiquement avec différentes personnalites (support client, expert technique, tuteur, etc.) sans dupliquer le code.

Indices : - # Étape 1 : Définir create_personality_agent(name, personality, domain) qui retourne un agent configure - # Étape 2 : Configurer le Kernel et le service dans la fonction (ou les recevoir en paramètre) - # Étape 3 : Tester avec 3 personnalites : “formel”, “humoristique”, “pedagogique” - # Indice : Injecter la personnalite dans les instructions de l’agent

def create_personality_agent(name: str, personality: str, domain: str = "technologie") -> ChatCompletionAgent:
    """
    Cree un agent avec une personnalite parametrable.
    
    # Etape 1 : Creer un Kernel avec un service OpenAI
    # Etape 2 : Construire les instructions en combinant personnalite et domaine
    # Etape 3 : Retourner un ChatCompletionAgent configure
    
    # Indice : Utilisez un template d'instructions comme :
    #   f"Tu es un assistant specialise en {domain}. Tu reponds de maniere {personality}."
    
    Args:
        name: Nom de l'agent
        personality: Style de reponse (formel, humoristique, pedagogique...)
        domain: Domaine d'expertise
    
    Returns:
        ChatCompletionAgent configure
    """
    # TODO etudiant : implementer la creation d'agent parametrable
    agent_kernel = Kernel()
    agent_kernel.add_service(OpenAIChatCompletion(service_id=f"personality_{name}"))
    
    instructions = f"Tu es un assistant."  # TODO etudiant : personnaliser avec personality et domain
    
    return ChatCompletionAgent(
        kernel=agent_kernel,
        name=name,
        instructions=instructions
    )

# Test : creez 3 agents avec des personnalites differentes
personalities = {
    "FormelBot": "formel et professionnel",
    "FunBot": "humoristique et decontracte",
    "ProfBot": "pedagogique avec des exemples concrets",
}

test_question = "Explique-moi ce qu'est Semantic Kernel en 2 phrases."

for agent_name, personality in personalities.items():
    agent = create_personality_agent(agent_name, personality, domain="IA et Semantic Kernel")
    print(f"--- {agent_name} (personnalite: {personality}) ---")
    # TODO etudiant : invoquer l'agent avec la question de test
    # history = ChatHistory()
    # history.add_user_message(test_question)
    # async for content in agent.invoke(history):
    #     print(content.content if hasattr(content, 'content') else str(content))
    print("Exercice a completer\n")
--- FormelBot (personnalite: formel et professionnel) ---
Exercice a completer

--- FunBot (personnalite: humoristique et decontracte) ---
Exercice a completer

--- ProfBot (personnalite: pedagogique avec des exemples concrets) ---
Exercice a completer

Interprétation : Anatomie d’un Agent Simple

Sortie obtenue : L’agent Parrot transforme 3 proverbes en voix de pirate avec la signature “Arrr!”

Aspect Valeur Signification
Instructions “repeat in a pirate voice” Définit la personnalité de l’agent
Invocation agent.invoke(chat_history) Traite les messages de manière asynchrone
Streaming async for content in ... Réception progressive des tokens
Historique ChatHistory() Maintient le contexte conversationnel

Points clés :

  1. Séparation des responsabilités : Le Kernel fournit les services (modèle LLM), l’agent ajoute la logique comportementale
  2. Instructions = System Message : Les instructions sont injectées comme message développeur, invisibles pour l’utilisateur
  3. Asynchrone par défaut : SK utilise async/await pour toutes les opérations I/O (appels API)
  4. Historique explicite : Contrairement à l’API OpenAI brute, SK gère l’historique via ChatHistory

Composants d’initialisation :

Kernel() ─→ add_service(OpenAIChatCompletion) ─→ ChatCompletionAgent(kernel, instructions)

Note technique : La correction add_assistant_message() est nécessaire car invoke() retourne des objets StreamingChatMessageContent incompatibles avec add_message() générique.

Exécution de l’agent Parrot

L’agent “Parrot” illustre le cas le plus simple : - Instructions système : Definissent le comportement (repeter en voix de pirate) - Invocation : Chaque message utilisateur est transforme par l’agent - Historique : Les reponses sont ajoutees a ChatHistory pour le contexte

Point technique : La méthode invoke() est asynchrone et retourne un flux (streaming) de contenus.

Bloc 3 : Agent simple avec Plugins

Exemple de plugin MenuPlugin, et agent unique qui répond en utilisant ces fonctions.

import asyncio
from typing import TYPE_CHECKING, Annotated
from semantic_kernel import Kernel
from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.functions import KernelArguments, kernel_function
from semantic_kernel.contents import ChatHistory

class MenuPlugin:
    """Plugin pour gérer un menu"""
    @kernel_function(description="Liste les specials")
    def get_specials(self) -> Annotated[str, "Describes specials"]:
        # print function call
        print("get_specials called")
        return "Special Soup: Clam Chowder\nSpecial Salad: Cobb Salad\nSpecial Drink: Chai Tea"
    @kernel_function(description="Donne le prix d'un item")
    def get_item_price(self, menu_item: Annotated[str, "nom de l'item"]) -> str:
         # print function call
        print("get_item_price called")
        return "$9.99"
# Créer kernel
kernel2 = Kernel()
# Ajout du plugin
kernel2.add_plugin(MenuPlugin(), plugin_name="menu")
# Ajout du service
kernel2.add_service(OpenAIChatCompletion(service_id="agent2"))
# On configure l'auto function-calling
settings2 = kernel2.get_prompt_execution_settings_from_service_id(service_id="agent2")
from semantic_kernel.connectors.ai import FunctionChoiceBehavior
settings2.function_choice_behavior = FunctionChoiceBehavior.Auto()

AGENT2_NAME = "Host"
AGENT2_INSTRUCTIONS = "Answer questions about the menu."
agent2 = ChatCompletionAgent(
    kernel=kernel2,
    name=AGENT2_NAME,
    instructions=AGENT2_INSTRUCTIONS,
    arguments=KernelArguments(settings=settings2),
)
async def plugin_agent_demo():
    chat_history = ChatHistory()
    user_msgs = [
        "Hello",
        "What is the special soup?",
        "What does it cost?",
        "Thanks",
    ]
    for user_input in user_msgs:
        chat_history.add_user_message(user_input)
        print(f"# User: '{user_input}'")
        agent_name = None
        full_response = ""  # Accumulateur pour la réponse complète
        
        async for content in agent2.invoke_stream(chat_history):
            if not agent_name:
                agent_name = content.name or AGENT2_NAME
                print(f"# {agent_name}: '", end="")
            
            # CORRECTION: Gestion appropriée du StreamingChatMessageContent
            if hasattr(content, 'content'):
                # Conversion sécurisée du contenu
                content_str = str(content.content) if content.content else ""
                # Accumulation de la réponse
                full_response += content_str
                # Affichage incrémental
                if content_str:
                    print(content_str, end="", flush=True)
        
        print("'")
        
        # Ajout sécurisé du message complet à l'historique
        if full_response:
            chat_history.add_assistant_message(full_response)
        else:
            # Si pas de contenu, ajouter un message par défaut
            chat_history.add_assistant_message("[No response generated]")

await plugin_agent_demo()
# User: 'Hello'
# Host: 'Hello! I can help with the menu—are you looking for today’s specials or the price of a specific item?'
# User: 'What is the special soup?'
# Host: 'get_specials called
The special soup is **Clam Chowder**.'
# User: 'What does it cost?'
# Host: 'get_item_price called
The **Clam Chowder** costs **$9.99**.'
# User: 'Thanks'
# Host: 'You’re welcome!'

Exercice 2 : Plugin de recherche simulate avec Function Calling

Objectif : Créer un plugin ResearchPlugin avec une fonction de recherche simulee, puis configurer un agent qui l’utilise automatiquement via Function Calling.

L’exercice precede demontre comment le MenuPlugin est appele automatiquement. Vous allez reproduire ce pattern avec un plugin de recherche qui simule des résultats pour un agent “Researcher”.

Indices : - # Étape 1 : Créer une classe ResearchPlugin avec une méthode search decoree de @kernel_function - # Étape 2 : Ajouter le plugin au kernel et configurer FunctionChoiceBehavior.Auto() - # Étape 3 : Créer un agent ChatCompletionAgent avec ce kernel et tester une question de recherche - # Indice : Simulez les résultats avec un dictionnaire {sujet: "Résultats simules pour..."}

class ResearchPlugin:
    """
    Plugin de recherche simulant des resultats.
    
    # Etape 1 : Definir la methode search() avec @kernel_function et une description claire
    # Etape 2 : Utiliser un parametre `topic` annote avec Annotated[str, "sujet de recherche"]
    # Etape 3 : Retourner une string simulant des resultats de recherche
    """
    
    @kernel_function(description="Recherche des informations sur un sujet")  # TODO etudiant : completer
    def search(self, topic: Annotated[str, "Sujet de la recherche"]) -> str:
        """
        Simule une recherche et retourne des resultats.
        
        # Indice : Retournez une string realiste simulant des resultats de recherche
        # Indice : Incluez le topic dans les resultats pour que l'agent puisse les exploiter
        """
        # TODO etudiant : implementer la recherche simulee
        return "Exercice a completer"  # TODO etudiant : retourner des resultats simules

# Test du plugin seul
research = ResearchPlugin()
print(research.search("intelligence artificielle"))

# TODO etudiant : creer le kernel, ajouter le plugin, configurer l'agent
# kernel_research = Kernel()
# kernel_research.add_service(OpenAIChatCompletion(service_id="research"))
# kernel_research.add_plugin(ResearchPlugin(), plugin_name="research")
# settings = kernel_research.get_prompt_execution_settings_from_service_id("research")
# settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
# 
# researcher = ChatCompletionAgent(
#     kernel=kernel_research,
#     name="Researcher",
#     instructions="Tu es un chercheur qui utilise l'outil de recherche pour repondre.",
#     arguments=KernelArguments(settings=settings),
# )
# Test avec une question
print("Exercice a completer : configurez l'agent Researcher avec le plugin")
Exercice a completer
Exercice a completer : configurez l'agent Researcher avec le plugin

Interprétation : Function Calling Automatique (ReAct Pattern)

Sortie obtenue : L’agent Host répond aux questions sur le menu en appelant automatiquement les bonnes fonctions du plugin

Question Fonction appelée Résultat
“What is the special soup?” get_specials() Liste des spéciaux → extraction “Clam Chowder”
“What does it cost?” get_item_price("Clam Chowder") “$9.99” (contexte conservé)
“Hello” / “Thanks” Aucune Réponse conversationnelle directe

Le graphe Mermaid de la cellule suivante détaille l’architecture du pattern ReAct : première passe du LLM, appel de MenuPlugin.get_specials(), puis synthèse de la réponse.

Comparaison avec les Plugins simples :

Aspect Plugin sans Agent Agent + Plugin
Appel de fonction Manuel (kernel.invoke("plugin-func")) Automatique (LLM décide)
Extraction paramètres Développeur spécifie LLM extrait du contexte
Contexte conversationnel Absent Conservé dans ChatHistory
Raisonnement Aucun LLM raisonne avant d’agir

Points clés :

  1. FunctionChoiceBehavior.Auto() : Active le mode “tool use” du LLM (équivalent à tools dans l’API OpenAI brute)
  2. Inférence de paramètres : Le LLM extrait “Clam Chowder” du contexte précédent pour get_item_price()
  3. Transparence : Les appels de fonction sont invisibles pour l’utilisateur final
  4. Streaming multi-phases : Le flux contient à la fois les tool calls et la réponse textuelle finale

Note technique : Le pattern invoke_stream() + accumulation de full_response permet d’afficher progressivement la réponse tout en maintenant l’historique complet.

Le diagramme ci-dessous rend cette sequence d’appel de fonction sous forme de graphe : le LLM detecte qu’il faut un outil, l’invoque, puis synthetise la reponse finale a partir du résultat.

flowchart TD
    U(["User : What is the special soup ?"]) --> L1["LLM (gpt-5)<br/>1. Analyse 2. Identifie get_specials() 3. Tool call"]
    L1 --> P["MenuPlugin.get_specials()<br/>retourne : Special Soup: Clam Chowder..."]
    P --> L2["LLM (gpt-5)<br/>4. Synthetise la reponse"]
    L2 --> A(["The special soup is..."])
    classDef llm fill:#fff3cd,stroke:#b8860b,color:#5c4400
    classDef tool fill:#cfe2ff,stroke:#084298,color:#052c65
    classDef io fill:#d1e7dd,stroke:#0f5132,color:#0a3622
    class L1,L2 llm
    class P tool
    class U,A io

Lecture. Le function calling se deroule en deux passes du LLM autour d’un appel d’outil : a la première passe, le modèle analyse la question et decide d’appeler get_specials() (il ne repond pas directement) ; le plugin s’execute et renvoie une donnee factuelle ; a la seconde passe, le LLM integre ce résultat pour formuler une reponse en langage naturel. C’est ce cycle raisonner -> agir -> observer -> repondre qui distingue un agent d’un simple appel de completion.

Analyse des appels de fonction automatiques

Dans l’exécution ci-dessus, observez la sequence d’appels :

  1. User : “What is the special soup?”
  2. Agent interne : Appelle get_specials() (affiche “get_specials called”)
  3. Agent repond : “The special soup is Clam Chowder”
  4. User : “What does it cost?”
  5. Agent interne : Appelle get_item_price("Clam Chowder") (affiche “get_item_price called”)
  6. Agent repond : “It costs $9.99”

Le paramètre FunctionChoiceBehavior.Auto() permet a l’agent de : - Analyser la question de l’utilisateur - Determiner quelle fonction appeler - Extraire les paramètres (ex: “Clam Chowder”) - Integrer le résultat dans sa reponse

C’est la base du ReAct pattern (Reasoning + Acting).

Exécution de l’agent avec plugins

Le plugin MenuPlugin expose des fonctions que l’agent peut appeler automatiquement : - get_specials() : Retourne les plats du jour - get_item_price() : Retourne le prix d’un article

Function Calling Auto : Le paramètre FunctionChoiceBehavior.Auto() permet a l’agent de decider lui-même quand appeler ces fonctions en fonction du contexte de la conversation.

Observez les appels get_specials called et get_item_price called dans la sortie.

Bloc 4 : Group Chat

Exemple d’un chat groupé : un agent CopyWriter, un agent ArtDirector, etc. On utilise la AgentGroupChat.

import asyncio
from semantic_kernel.agents import AgentGroupChat, ChatCompletionAgent
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.contents import AuthorRole, ChatMessageContent
from semantic_kernel.agents.strategies import TerminationStrategy
from semantic_kernel import Kernel

class ApprovalTerminationStrategy(TerminationStrategy):
    async def should_agent_terminate(self, agent, history):
        return "approved" in history[-1].content.lower()

# On crée un kernel par agent, ou le même kernel + service differencié.
def create_kernel_for(name):
    k = Kernel()
    # on admet qu'on a paramétré un service openAI.
    k.add_service(OpenAIChatCompletion(service_id=name))
    return k

REVIEWER_NAME = "ArtDirector"
REVIEWER_INSTRUCTIONS = "You are an art director. If the copy is good, say 'Approved'. Otherwise, propose improvements."
reviewer_agent = ChatCompletionAgent(
    kernel=create_kernel_for(REVIEWER_NAME),
    name=REVIEWER_NAME,
    instructions=REVIEWER_INSTRUCTIONS,
)
COPYWRITER_NAME = "CopyWriter"
COPYWRITER_INSTRUCTIONS = "You are a copywriter. Provide short but strong marketing copy."
writer_agent = ChatCompletionAgent(
    kernel=create_kernel_for(COPYWRITER_NAME),
    name=COPYWRITER_NAME,
    instructions=COPYWRITER_INSTRUCTIONS,
)
group_chat = AgentGroupChat(
    agents=[reviewer_agent, writer_agent],
    termination_strategy=ApprovalTerminationStrategy(agents=[reviewer_agent], maximum_iterations=6)
)

async def group_chat_demo():
    user_msg = "I need a slogan for a new line of electric bikes"
    await group_chat.add_chat_message(ChatMessageContent(role=AuthorRole.USER, content=user_msg))
    print(f"# User: '{user_msg}'")
    async for content in group_chat.invoke():
        # Gestion sécurisée du contenu pour éviter ContentInitializationError
        if hasattr(content, 'content') and content.content:
            print(f"# Agent - {content.name or '*'}: '{content.content}'")
        else:
            print(f"# Agent - {content.name or '*'}: '{str(content)}'")

    print(f"# IS COMPLETE: {group_chat.is_complete}")

await group_chat_demo()
# User: 'I need a slogan for a new line of electric bikes'
# Agent - ArtDirector: 'Charge Your Ride.

Options if you want alternates:
- Ride Far. Ride Electric.
- Power the Pedal.
- Silent Speed, Serious Range.
- Plug In. Go Anywhere.
- Clean Power. Pure Fun.

To tailor it, tell me: commuter vs. adventure, premium vs. budget, and your key differentiator (range, speed, design, light weight).'
# Agent - CopyWriter: 'Charge Your Ride.

More slogan options:
- Ride Far. Ride Electric.
- Power the Pedal.
- Silent Speed, Serious Range.
- Plug In. Go Anywhere.
- Clean Power. Pure Fun.
- Pedal Less. Live More.
- City Ready. Trail Hungry.
- Range That Keeps Up.

Tell me your vibe (commuter/adventure), price point (premium/value), and your #1 edge (range, speed, weight, design), and I’ll tailor 10 punchy picks.'
# Agent - ArtDirector: 'Not approved — “Charge Your Ride” is catchy, but it’s generic and could fit any electric product. It doesn’t signal bikes, benefit, or brand personality.

Improvements (more ownable, e-bike specific):
- **Electric, Meet Effortless.**
- **Go Far. Feel Fresh.**
- **Make Every Mile Easy.**
- **Commute, Upgraded.**
- **More Miles. Less Sweat.**
- **Your City. Supercharged.**
- **Power When You Want It.**
- **Quiet Power. Big Range.**

Tell me: commuter vs. trail, premium vs. value, and your standout (range/weight/speed/design), and I’ll tighten these into a single best-line direction.'
# IS COMPLETE: True

Exercice 3 : Stratégie de terminaison par mot-cle amelioree

Objectif : Implementer une stratégie de terminaison KeywordTerminationStrategy qui arrete le group chat quand un mot-cle spécifique est detecte dans le dernier message d’un agent designe.

La ApprovalTerminationStrategy utilisee precedemment est basique et peut echouer si le mot exact n’apparait pas. Vous allez créer une version plus robuste qui supporte plusieurs mots-cles et insensibilite a la casse.

Indices : - # Étape 1 : Heriter de TerminationStrategy et implementer should_agent_terminate - # Étape 2 : Verifier si le dernier message contient l’un des mots-cles (case-insensitive) - # Étape 3 : Limiter la verification aux agents specifies dans agents (comme ApprovalTerminationStrategy) - # Indice : any(kw in history[-1].content.lower() for kw in self.keywords)

class KeywordTerminationStrategy(TerminationStrategy):
    """
    Strategie de terminaison basee sur des mots-cles configurables.
    
    # Etape 1 : Definir __init__ avec une liste de keywords (ex: ["approved", "done", "final"])
    # Etape 2 : Implementer should_agent_terminate() qui verifie le dernier message
    # Etape 3 : Convertir le contenu en minuscules avant la comparaison
    """
    # SK strategies sont des modeles pydantic : keywords doit etre un champ declare,
    # passe via super().__init__ (l'assignation directe self.keywords est rejetee).
    keywords: list[str] = None

    def __init__(self, keywords: list[str] = None, **kwargs):
        super().__init__(keywords=keywords or ["approved"], **kwargs)
        # TODO etudiant : personnaliser les mots-cles par defaut
    
    async def should_agent_terminate(self, agent, history) -> bool:
        """
        Verifie si le dernier message contient un mot-cle.
        
        # Indice : Acceder au dernier message avec history[-1].content
        # Indice : Convertir en minuscules avec .lower()
        """
        # TODO etudiant : implementer la verification
        return False  # TODO etudiant : remplacer par la logique de detection

# Test unitaire de la strategie
strategy = KeywordTerminationStrategy(
    keywords=["approved", "done", "finalise"],
    agents=[],  # sera configure avec les vrais agents
    maximum_iterations=10
)

# Simulation avec des messages fictifs
from semantic_kernel.contents import ChatMessageContent

test_messages = [
    ChatMessageContent(role=AuthorRole.ASSISTANT, content="Le slogan est pret."),
    ChatMessageContent(role=AuthorRole.ASSISTANT, content="APPROVED - version finale."),
]

result = await strategy.should_agent_terminate(agent=None, history=test_messages)
print(f"Terminaison detectee : {result}")
print("Exercice a completer")
Terminaison detectee : False
Exercice a completer

Interprétation : Orchestration Multi-Agents et Stratégies

Sortie obtenue : Dialogue itératif entre CopyWriter (propose des slogans) et ArtDirector (critique), mais incomplet (is_complete=False)

Itération Agent Message État
1 ArtDirector “Ride the Future: Electrify Your Journey” Suggestion initiale
2 CopyWriter “Unleash the Power of Green: Ride Beyond” Proposition
3 ArtDirector (répète slogan #1) Problème de contexte
4 CopyWriter “Unchain Your Commute…” Nouvelle proposition
5 ArtDirector “I’m looking for organic skincare” Dérapage (hors sujet)
6 CopyWriter “Purely You: Embrace Nature’s Glow” Répond au dérapage

Analyse du problème de terminaison :

ApprovalTerminationStrategy attend "approved" dans le message
    ↓
Aucun message ne contient "approved"
    ↓
Maximum 6 itérations atteint → STOP (sans validation)
    ↓
is_complete = False (terminaison par limite, pas par succès)

Comparaison des stratégies de terminaison :

Stratégie Condition de succès Cas d’échec
ApprovalTerminationStrategy Mot-clé “approved” détecté Itérations épuisées sans mot-clé
DefaultTerminationStrategy Toujours success à la limite Aucun (accepte l’état final)
KernelFunctionTerminationStrategy LLM juge l’objectif atteint LLM désynchronisé du contexte
AggregatorTerminationStrategy ALL/ANY conditions remplies Conditions contradictoires

Flux d’orchestration AgentGroupChat :

┌─────────────────────────────────────────────────────────┐
│              AgentGroupChat                             │
│  ┌────────────────────┐  ┌────────────────────────┐    │
│  │ SelectionStrategy  │  │ TerminationStrategy    │    │
│  │ (qui parle ?)      │  │ (quand arrêter ?)      │    │
│  └────────────────────┘  └────────────────────────┘    │
│           ↓                        ↓                    │
│  ┌──────────────────────────────────────────────┐      │
│  │  Invoke Loop                                 │      │
│  │  1. Sélectionne agent (ex: round-robin)     │      │
│  │  2. Agent génère réponse                     │      │
│  │  3. Ajoute à l'historique                    │      │
│  │  4. Vérifie terminaison                      │      │
│  │  5. Si non-terminé → retour à 1              │      │
│  └──────────────────────────────────────────────┘      │
└─────────────────────────────────────────────────────────┘

Points clés :

  1. Limite d’itérations cruciale : Sans maximum_iterations, risque de boucle infinie si le mot-clé n’apparaît jamais
  2. Prompt engineering pour terminaison : Les instructions de l’ArtDirector devraient explicitement mentionner “Say ‘Approved’ when satisfied”
  3. Dérapage de contexte : L’itération 5 montre que les agents peuvent perdre le fil (problème connu avec les longs historiques)
  4. is_complete vs succès : False signifie que la stratégie n’a pas validé, pas nécessairement un échec fonctionnel

Améliorations possibles :

# Instructions plus explicites
REVIEWER_INSTRUCTIONS = """You are an art director. 
Review the copywriter's slogan. If it's good, respond with exactly 'APPROVED'.
Otherwise, suggest ONE specific improvement."""

# Stratégie hybride
class HybridTerminationStrategy(TerminationStrategy):
    async def should_agent_terminate(self, agent, history):
        last_msg = history[-1].content.lower()
        # Condition 1: Mot-clé explicite
        if "approved" in last_msg:
            return True
        # Condition 2: LLM juge l'objectif atteint
        judgment = await llm_judge(history, objective="Create bike slogan")
        return judgment == "SUCCESS"

Cas d’usage production : Le notebook 10-NotebookMaker implémente un système 3-agents (Admin, Coder, Reviewer) avec stratégies robustes pour éviter ces problèmes.

Bloc 5 : OpenAIAssistantAgent (Apercu)

L’API Assistants d’OpenAI offre des capacites avancees que ChatCompletionAgent n’a pas nativement :

Fonctionnalite Description Cas d’usage
Code Interpreter Execute du Python dans un sandbox Calculs, graphiques, analyse de données
File Search RAG integre sur fichiers uploades Questions sur documents PDF, DOCX
Threads persistants Historique conserve cote serveur Conversations longues, reprise de session

Architecture OpenAIAssistantAgent

┌─────────────────────────────────────────────┐
│          OpenAIAssistantAgent               │
│  ┌─────────────────────────────────────┐   │
│  │         OpenAI Assistants API       │   │
│  │  ┌───────────┐  ┌───────────────┐  │   │
│  │  │   Code    │  │  File Search  │  │   │
│  │  │Interpreter│  │  (RAG integre)│  │   │
│  │  └───────────┘  └───────────────┘  │   │
│  │         ↓              ↓           │   │
│  │       Thread (persistent state)    │   │
│  └─────────────────────────────────────┘   │
└─────────────────────────────────────────────┘

Exemple conceptuel (non execute)

from semantic_kernel.agents.open_ai import OpenAIAssistantAgent

# Creation de l'agent avec Code Interpreter
agent = await OpenAIAssistantAgent.create(
    kernel=kernel,
    name="DataAnalyst",
    instructions="Tu es un analyste de données. Utilise Python pour les calculs.",
    enable_code_interpreter=True,
    enable_file_search=True
)

# Creation d'un thread (session persistante)
thread = await agent.create_thread()

# Ajout d'un fichier pour analyse
await agent.add_file(thread_id=thread.id, file_path="data.csv")

# Invocation avec contexte fichier
response = await agent.invoke(thread_id=thread.id, message="Analyse ce CSV et genere un graphique")

Points cles : - enable_code_interpreter=True : L’agent peut executer du Python - enable_file_search=True : L’agent peut rechercher dans les fichiers - thread : Session persistante cote OpenAI (pas de ChatHistory local) - Les fichiers sont uploades vers OpenAI et indexes automatiquement

Note : Cette API necessite un compte OpenAI avec acces aux Assistants (payant). Pour des alternatives locales, voir les Vector Stores dans le notebook 05.

Exécution du Group Chat

Le AgentGroupChat orchestre plusieurs agents qui collaborent : - ArtDirector : Valide ou demande des ameliorations du texte - CopyWriter : Propose du contenu marketing

Stratégie de terminaison : La classe ApprovalTerminationStrategy arrete la conversation quand l’ArtDirector dit “approved”. Cela illustre comment définir des conditions d’arret personnalisees.

La limite maximum_iterations=6 protege contre les boucles infinies.

Conclusion

Resume des concepts

Concept Description Code cle
ChatCompletionAgent Agent de base SK ChatCompletionAgent(kernel, name, instructions)
Instructions Personnalite de l’agent Message système definissant le comportement
Plugins + Agent Outils pour l’agent FunctionChoiceBehavior.Auto()
AgentGroupChat Orchestration multi-agents AgentGroupChat(agents, termination_strategy)
TerminationStrategy Condition d’arret ApprovalTerminationStrategy, custom
OpenAIAssistantAgent API Assistants Code Interpreter, File Search, Threads

Points cles a retenir

  1. Un agent = Kernel + Instructions - Le Kernel fournit les capacites, les instructions definissent le comportement
  2. Function Calling automatique - FunctionChoiceBehavior.Auto() permet a l’agent de decider quand utiliser les outils
  3. AgentGroupChat pour la collaboration - Plusieurs agents peuvent dialoguer et iterer
  4. Stratégies configurables - Sélection et terminaison sont personnalisables
  5. OpenAIAssistantAgent pour les cas avances - Code Interpreter et RAG integres

Exercice suggere

Créez un groupe de 2 agents : - Researcher : Recherche des informations (simule avec un plugin) - Writer : Redige un article base sur les recherches

## Squelette de depart
class ResearchPlugin:
    @kernel_function(description="Recherche sur un sujet")
    def search(self, topic: str) -> str:
        return f"Résultats de recherche pour: {topic}..."

## A vous de créer les agents et le group chat !

Pour aller plus loin

Notebook Contenu
04-Filters Intercepter et modifier les appels
05-VectorStores RAG avec Qdrant
05-NotebookMaker Système 3-agents en production

Navigation : << 02-Functions | Index | 04-Filters >>

Synthèse : Taxonomie Complète des Agents Semantic Kernel

1. Types d’agents et leurs capacités

Type Backend Capacités natives Persistance Coût
ChatCompletionAgent Chat Completion API Streaming, Function Calling, Vision Locale (ChatHistory) Token/token
OpenAIAssistantAgent Assistants API + Code Interpreter, File Search, Vector Store Serveur (Threads) Token + stockage
AzureAIAgent Azure AI Foundry + Enterprise security, compliance Azure Storage Enterprise pricing

2. Patterns d’utilisation

Pattern 1 : Agent solo avec outils (ReAct)

Agent + Kernel(Plugins) + FunctionChoiceBehavior.Auto()
    → Raisonne et agit de manière autonome

Cas d’usage : Chatbot avec accès à des APIs, assistant de recherche

Pattern 2 : Multi-agents collaboratifs (AutoGen-like)

AgentGroupChat([Agent1, Agent2, Agent3])
    + SelectionStrategy (qui parle ?)
    + TerminationStrategy (quand arrêter ?)
    → Dialogue itératif jusqu'à validation

Cas d’usage : Peer review (Coder + Reviewer), brainstorming (Ideator + Critic), pipeline (Research → Write → Edit)

Pattern 3 : Agent avec état persistant (Assistants API)

OpenAIAssistantAgent + Thread (stocké serveur OpenAI)
    → Conversations longues, reprise de session

Cas d’usage : Support client, tuteurs éducatifs, assistants personnels

3. Composants clés de l’orchestration

Composant Rôle Options courantes
SelectionStrategy Qui parle ensuite ? Sequential, KernelFunction, Custom
TerminationStrategy Quand arrêter ? Approval, MaxIterations, KernelFunction, Aggregator
ChatHistory Contexte conversationnel Partagé entre agents, trimming automatique
FunctionChoiceBehavior Quand utiliser les outils ? Auto, Required, None

4. Pièges courants et solutions

Problème Cause Solution
Boucle infinie Pas de limite d’itérations maximum_iterations dans TerminationStrategy
Agent ne termine pas Mot-clé “approved” jamais prononcé Instructions explicites + prompt engineering
Dérapage de contexte Historique trop long Trimming automatique (max_tokens dans ChatHistory)
Tool calls ignorés FunctionChoiceBehavior non configuré Définir dans KernelArguments de l’agent
StreamingChatMessageContent error Mauvaise méthode d’ajout à l’historique Utiliser add_assistant_message(str(content.content))

5. Progression recommandée

1. Agent simple (Parrot)
    → Comprendre Instructions + Kernel + invoke()
    
2. Agent + Plugins (Menu)
    → Maîtriser FunctionChoiceBehavior.Auto()
    
3. AgentGroupChat (CopyWriter + ArtDirector)
    → Orchestrer des agents, gérer les stratégies
    
4. OpenAIAssistantAgent (Code Interpreter)
    → Cas avancés : code exécution, RAG intégré
    
5. Production (NotebookMaker)
    → Système robuste 3+ agents avec gestion d'erreurs

6. Comparaison avec d’autres frameworks

Framework Philosophie Forces Faiblesses
Semantic Kernel “Kernel-centric” (services partagés) Intégration .NET, plugins réutilisables Moins mature qu’AutoGen
AutoGen “Agent-centric” (agents autonomes) Patterns multi-agents éprouvés, communauté active Principalement Python
LangGraph “Graph-centric” (workflows as graphs) Contrôle fin, debugging visuel Courbe d’apprentissage
CrewAI “Rôle-centric” (agents = métiers) Abstractions haut niveau Moins flexible

Positionnement SK : Meilleur choix pour écosystème Microsoft (.NET, Azure), plugins partagés entre agents, et migration progressive depuis des applications SK existantes.


Prochaine étape : Notebook 04 - Filters & Observability pour instrumenter et intercepter les agents en production.

Retour au sommet