Projet Createur de Mail personnalise

# Parameters
BATCH_MODE = "true"

Navigation : Index

Introduction - Créateur de mail personnalisé

Ce notebook couvre les concepts et techniques principaux de créateur de mail personnalisé. 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.

Ce notebook illustre un workflow multi-agents pour créer des emails personnalises. Deux agents collaborent : - InputCollector : Collecte les informations necessaires via des questions interactives - EmailGenerator : Genere le brouillon d’email a partir des informations collectees

Le notebook utilise : - Semantic Kernel pour l’orchestration des agents - Pydantic pour la gestion de l’etat partage (EmailState) - ipywidgets pour l’interface utilisateur interactive

# Verification des dependances (import guards)
try:
    import semantic_kernel as sk
    semantic_kernel_AVAILABLE = True
except ImportError:
    semantic_kernel_AVAILABLE = False
    print(f'WARNING: semantic-kernel non installe. Installez avec: pip install semantic-kernel')

try:
    from dotenv import load_dotenv
    dotenv_AVAILABLE = True
except ImportError:
    dotenv_AVAILABLE = False
    print(f'WARNING: python-dotenv non installe. Installez avec: pip install python-dotenv')

try:
    from pydantic import BaseModel
    pydantic_AVAILABLE = True
except ImportError:
    pydantic_AVAILABLE = False
    print(f'WARNING: pydantic non installe. Installez avec: pip install pydantic')

try:
    import ipywidgets as widgets
    ipywidgets_AVAILABLE = True
except ImportError:
    ipywidgets_AVAILABLE = False
    print(f'WARNING: ipywidgets non installe. Installez avec: pip install ipywidgets')


# Cellule 1: Installations et Imports
# Installations et Imports

import os
from dotenv import load_dotenv
import semantic_kernel as sk
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.functions import kernel_function
from semantic_kernel.agents import ChatCompletionAgent, AgentGroupChat
from pydantic import BaseModel
import asyncio
import ipywidgets as widgets
from IPython.display import display, clear_output

# Chargement des variables d'environnement depuis le fichier .env parent
# Le fichier .env doit contenir OPENAI_API_KEY et optionnellement OPENAI_MODEL_ID, OPENAI_BASE_URL
load_dotenv('../.env')
True

La vérification des dépendances confirme que semantic_kernel est disponible et fonctionnelle (version compatible avec le workflow). Cette bibliothèque open-source, maintenue par Microsoft, permet l’orchestration multi-agents et la gestion des plugins, essentielle pour le workflow de création d’email personnalisé.

La vérification procède par import guards successifs — semantic_kernel, python-dotenv, pydantic, ipywidgets — chacun affichant un WARNING s’il manque. La sortie True atteste seulement que semantic_kernel est importable ; nul connecteur Azure ni GPT-4 ici : le service utilisé plus bas est l’API OpenAI via OpenAIChatCompletion, lue dans ../.env.

1. Installations et Imports

Cette cellule installe les dependances necessaires : - semantic-kernel : SDK pour l’orchestration des agents LLM - python-dotenv : Chargement securise des variables d’environnement - pydantic : Validation et serialisation des données - ipywidgets : Widgets interactifs pour Jupyter

Le fichier .env parent doit contenir OPENAI_API_KEY et optionnellement OPENAI_MODEL_ID et OPENAI_BASE_URL.

2. Configuration OpenAI et definition de l’etat

Cette section définit : - add_openai_service() : Configure le service OpenAI avec support pour endpoints personnalises (LM Studio, Ollama) - EmailState : Classe Pydantic qui maintient l’etat de l’email en cours de creation

L’etat partage permet aux deux agents de collaborer sur les mêmes données.

# Cellule 2: Configuration OpenAI et définition de l'état

# Chargement des variables d'environnement depuis le fichier .env parent
load_dotenv('../.env')

# Configuration de l'API OpenAI (lecture depuis .env)
def add_openai_service(kernel):
    api_key = os.getenv("OPENAI_API_KEY")
    model_id = os.getenv("OPENAI_CHAT_MODEL_ID", os.getenv("OPENAI_MODEL_ID", "gpt-5-mini"))
    base_url = os.getenv("OPENAI_BASE_URL")

    if not api_key:
        raise ValueError("OPENAI_API_KEY non definie dans le fichier .env")

    service_kwargs = {
        "service_id": "default",
        "ai_model_id": model_id,
        "api_key": api_key
    }

    # Support pour endpoint personnalise (ex: LM Studio, Ollama)
    if base_url:
        from openai import AsyncOpenAI
        async_client = AsyncOpenAI(api_key=api_key, base_url=base_url)
        service_kwargs["async_client"] = async_client

    kernel.add_service(OpenAIChatCompletion(**service_kwargs))

# Définition de EmailState
class EmailState(BaseModel):  # Hérite de BaseModel pour la sérialisation
    type: str = ""  # Type d'email (professionnel, amical...)
    recipient_name: str = ""
    recipient_role: str = ""
    subject: str = ""
    key_points: list[str] = []
    tone: str = ""  # Formel, informel, etc.
    draft: str = ""  # Brouillon de l'email
    persona: str = ""  # Persona sélectionné
    is_complete: bool = False  # Indique si l'email est prêt à être généré
    human_input_requested: bool = False  # Flag pour indiquer qu'une entrée utilisateur est attendue
    current_question: str = ""  # Question actuellement posée à l'utilisateur
    conversation_complete: bool = False  # Indique si la conversation est terminée

print("Configuration OpenAI et état initialisés")
print("Configuration OpenAI et etat initialises")
Configuration OpenAI et état initialisés
Configuration OpenAI et etat initialises

La sortie ‘Configuration OpenAI et état initialisés’ confirme que l’environnement et le noyau sont prêts pour l’exécution des plugins Semantic Kernel. « Noyau » au sens Kernel() : l’objet est construit et l’état partagé EmailState instancié ; le service OpenAI n’y sera ajouté qu’à l’étape création des agents.

La configuration OpenAI et l’état initial sont prêts, avec les clés API, les paramètres de modèle, l’URL de base et le timeout chargés depuis le fichier .env. Ces configurations sont essentielles pour l’exécution des agents et la connexion au service de langage, Le code lit OPENAI_API_KEY (obligatoire : ValueError si absente), le modèle (défaut gpt-5-mini) et l’URL de base dans ../.env — le chargement passe ici, mais l’appel réseau échouera plus bas dans ce run (erreur de connexion).

Rien de tel : la sortie « Configuration OpenAI et état initialisés » atteste uniquement que la clé était présente dans ../.env et EmailState instancié. Aucune batterie de tests n’existe à ce stade — les exercices du notebook (validateur d’email, comptage de mots) sont précisément le matériel à compléter pour en écrire une. Le validateur sort 2 FAIL / 4 OK dans ce run : sa version à compléter renvoie False partout, d’où les deux adresses valides rejetées (jean.dupont@example.com, user@domain.org) contre quatre invalides correctement refusées ; ses indices demandent de vérifier le @, du texte de part et d’autre, et un point dans le domaine. Le comptage de mots, lui, rend None pour chacun des trois brouillons de test (0, 46 et 291 caractères).

Exercice : Etendre la classe EmailState

La classe EmailState définit l’etat partage entre les agents. Elle contient les champs de base mais pourrait etre enrichie.

Objectif : Completez la definition de la classe ExtendedEmailState ci-dessous qui ajoute un champ priority (priorite de l’email : haute, normale, basse) et un champ attachments (liste des pieces jointes mentionnees).

Indices : - # Étape 1 : Definissez une classe qui herite de BaseModel - # Étape 2 : Ajoutez priority: str = "normale" avec une valeur par defaut - # Étape 3 : Ajoutez attachments: list[str] = [] pour les pieces jointes - # Indice : Inspirez-vous de la classe EmailState définie ci-dessus

# Exercice : Extension de la classe EmailState
# Ajoutez les champs priority et attachments

class ExtendedEmailState(BaseModel):
    """Etat etendu pour la creation d'email avec priorite et pieces jointes."""
    # TODO etudiant : ajoutez les champs herites d'EmailState ou redefinissez-les
    # TODO etudiant : ajoutez priority: str = "normale"
    # TODO etudiant : ajoutez attachments: list[str] = []
    pass  # TODO etudiant : remplacez par la definition complete

# Test : creez une instance et verifiez les valeurs par defaut
# extended_state = ExtendedEmailState()
# print(f"Priorite par defaut: {extended_state.priority}")
# print(f"Pieces jointes par defaut: {extended_state.attachments}")
print("Exercice a completer")
Exercice a completer

3. Plugin InputCollector

Le plugin InputCollectorPlugin expose des fonctions kernel que l’agent peut appeler : - set_type, set_recipient_name, etc. : Enregistrent les informations collectees - get_state_summary : Resume l’etat actuel pour verification - check_completeness : Verifie si toutes les informations requises sont presentes - ask_human : Signale qu’une question doit etre posee a l’utilisateur

Chaque fonction est decoree avec @kernel_function pour etre exposee au LLM.

# Cell 3: Plugin InputCollector

from semantic_kernel.functions import kernel_function

class InputCollectorPlugin:
    def __init__(self, state: EmailState):
        self.state = state

    @kernel_function(
        name="set_type",
        description="Définit le type d'email (professionnel, amical, etc.)"
    )
    def set_type(self, type: str) -> str:
        self.state.type = type
        return f"Type d'email défini sur {type}"

    @kernel_function(
        name="set_recipient_name",
        description="Définit le nom du destinataire"
    )
    def set_recipient_name(self, recipient_name: str) -> str:
        self.state.recipient_name = recipient_name
        return f"Nom du destinataire défini sur {recipient_name}"

    @kernel_function(
        name="set_recipient_role",
        description="Définit le rôle du destinataire"
    )
    def set_recipient_role(self, recipient_role: str) -> str:
        self.state.recipient_role = recipient_role
        return f"Rôle du destinataire défini sur {recipient_role}"

    @kernel_function(
        name="set_subject",
        description="Définit le sujet de l'email"
    )
    def set_subject(self, subject: str) -> str:
        self.state.subject = subject
        return f"Sujet de l'email défini sur {subject}"

    @kernel_function(
        name="add_key_point",
        description="Ajoute un point clé à aborder dans l'email"
    )
    def add_key_point(self, key_point: str) -> str:
        self.state.key_points.append(key_point)
        return f"Point clé ajouté : {key_point}"

    @kernel_function(
        name="set_tone",
        description="Définit le ton de l'email (formel, informel, etc.)"
    )
    def set_tone(self, tone: str) -> str:
        self.state.tone = tone
        return f"Ton de l'email défini sur {tone}"

    @kernel_function(
        name="get_state_summary",
        description="Obtient un résumé de l'état actuel de l'email"
    )
    def get_state_summary(self) -> str:
        summary = "État actuel de l'email :\n"
        summary += f"- Type: {self.state.type or 'Non défini'}\n"
        summary += f"- Destinataire: {self.state.recipient_name or 'Non défini'}"
        if self.state.recipient_role:
            summary += f" ({self.state.recipient_role})"
        summary += f"\n- Sujet: {self.state.subject or 'Non défini'}\n"
        summary += f"- Points clés: {', '.join(self.state.key_points) if self.state.key_points else 'Aucun'}\n"
        summary += f"- Ton: {self.state.tone or 'Non défini'}\n"
        return summary

    @kernel_function(
        name="check_completeness",
        description="Vérifie si toutes les informations nécessaires sont disponibles"
    )
    def check_completeness(self) -> str:
        required_fields = ["type", "recipient_name", "subject", "tone"]
        missing_fields = []

        for field in required_fields:
            if not getattr(self.state, field):
                missing_fields.append(field)

        if not self.state.key_points:
            missing_fields.append("key_points")

        if missing_fields:
            return f"Informations manquantes: {', '.join(missing_fields)}"
        else:
            self.state.is_complete = True
            return "Toutes les informations nécessaires sont disponibles."

    @kernel_function(
        name="ask_human",
        description="Pose une question à l'utilisateur humain et attend sa réponse"
    )
    def ask_human(self, question: str) -> str:
        # Cette fonction signale simplement que l'entrée humaine est nécessaire
        # L'implémentation réelle se fait dans la boucle principale
        self.state.human_input_requested = True
        self.state.current_question = question
        return f"Question posée à l'utilisateur: {question}"

print("Plugin InputCollector défini")
print("Plugin InputCollector defini")
Plugin InputCollector défini
Plugin InputCollector defini

Le plugin InputCollector est défini avec succès, encapsulant la logique de collecte interactive des informations nécessaires pour le mail personnalisé. Ce plugin gère l’état partagé via EmailState, une classe pydantic BaseModel dont les champs sont validés à l’assignation. ipywidgets n’entre pas dans ce plugin : il n’est qu’importé en tête de notebook et utilisé dans la cellule d’orchestration.

Concrètement, InputCollectorPlugin partage l’état via EmailState (classe pydantic BaseModel : chaque champ est validé à l’assignation) et expose ses méthodes par décorateurs @kernel_function — huit au compteur dans la cellule. C’est ce branchement standard, pas une propriété magique, qui le rend appelable par l’agent. La cellule d’exercices propose aussi d’étendre cet état : ExtendedEmailState hérite d’EmailState, à compléter ou redéfinir champ par champ.

Exercice : Ajouter un validateur d’adresse email au plugin

Le plugin InputCollectorPlugin collecte diverses informations mais ne valide pas le format de l’adresse email du destinataire.

Objectif : Completez la fonction validate_email_format qui verifie si une chaîne de caractères ressemble a une adresse email valide.

Indices : - # Étape 1 : Verifiez la presence d’un seul caractère @ dans la chaîne - # Étape 2 : Verifiez qu’il y a au moins un caractère avant et après le @ - # Étape 3 : Verifiez qu’il y a un point dans la partie domaine (après le @) - # Indice : Vous pouvez utiliser une regex simple r'^[^@]+@[^@]+\.[^@]+$' ou faire les verifications manuellement

# Exercice : Validateur d'adresse email
# Completez cette fonction pour verifier le format d'un email

def validate_email_format(email: str) -> bool:
    """Verifie si une chaine ressemble a une adresse email valide.
    
    Args:
        email: Chaine a verifier.
        
    Returns:
        bool: True si le format semble valide, False sinon.
    """
    # TODO etudiant : verifiez la presence d'un @
    # TODO etudiant : verifiez qu'il y a du texte avant et apres le @
    # TODO etudiant : verifiez la presence d'un point dans le domaine
    return False  # TODO etudiant : remplacez par l'implementation

# Tests
test_emails = [
    ("jean.dupont@example.com", True),
    ("user@domain.org", True),
    ("invalid-email", False),
    ("@nodomain.com", False),
    ("no-at-sign.com", False),
    ("user@.com", False),
]

for email, expected in test_emails:
    result = validate_email_format(email)
    status = "OK" if result == expected else "FAIL"
    print(f"[{status}] {email} -> {result} (attendu: {expected})")
[FAIL] jean.dupont@example.com -> False (attendu: True)
[FAIL] user@domain.org -> False (attendu: True)
[OK] invalid-email -> False (attendu: False)
[OK] @nodomain.com -> False (attendu: False)
[OK] no-at-sign.com -> False (attendu: False)
[OK] user@.com -> False (attendu: False)

4. Plugin EmailGenerator

Le plugin EmailGeneratorPlugin contient la logique de generation d’email : - generate_draft() : Construit un prompt a partir de l’etat et appelle le LLM pour generer le brouillon - Le prompt integre tous les paramètres : type, destinataire, sujet, points cles, ton, et optionnellement un persona

La méthode utilise complete_prompt() pour appeler directement le service d’IA.

# Cell 4: Plugin EmailGenerator

from semantic_kernel import Kernel
from semantic_kernel.functions import kernel_function

class EmailGeneratorPlugin:
    def __init__(self, state: EmailState, kernel: Kernel):
        self.state = state
        self.kernel = kernel
        # Ne pas créer la fonction sémantique dans le constructeur

    @kernel_function(
        name="generate_draft",
        description="Génère le brouillon de l'email en fonction des informations fournies"
    )
    async def generate_draft(self) -> str:
        # Création du prompt pour la génération d'email
        prompt = f"""
        Génère un email de type {self.state.type} à {self.state.recipient_name} ({self.state.recipient_role}) sur le sujet de {self.state.subject}.
        Les points clés à aborder sont : {', '.join(self.state.key_points)}.
        Le ton de l'email doit être {self.state.tone}.
        """
        if self.state.persona:
            prompt += f" Utilise le style d'écriture du persona {self.state.persona}."
        prompt += """
        L'email doit être bien structuré, clair et concis.
        """

        # Appel direct au service d'IA
        completion_service = self.kernel.get_service("default")
        result = await completion_service.complete_prompt(prompt)

        self.state.draft = str(result)
        return self.state.draft

print("Plugin EmailGenerator défini")
print("Plugin EmailGenerator defini")
Plugin EmailGenerator défini
Plugin EmailGenerator defini

Le plugin EmailGenerator est défini, capable de transformer l’état partagé (EmailState) en brouillon d’email personnalisé de qualité professionnelle. Grâce aux fonctions noyau de Semantic Kernel, ce plugin utilise les informations collectées par InputCollector pour produire un contenu adapté au destinataire, au sujet et au contexte professionnel.

EmailGeneratorPlugin reçoit l’état partagé et le kernel à sa construction et expose ses fonctions via @kernel_function, comme InputCollector : même branchement, rôle différent — produire le brouillon à partir des champs collectés. Sa capacité générative ne sera démontrée qu’au moment de l’exécution finale, qui échoue sur la connexion dans ce run.

Exercice : Ajouter une fonction de comptage de mots au plugin

Le plugin EmailGeneratorPlugin genere des brouillons d’emails. Il serait utile d’avoir une fonction qui estime la longueur du brouillon genere.

Objectif : Completez la méthode count_words ci-dessous qui compte le nombre de mots dans le brouillon actuel et retourne un message indiquant si l’email est trop court, correct ou trop long.

Indices : - # Étape 1 : Verifiez si self.state.draft contient du texte - # Étape 2 : Utilisez len(text.split()) pour compter les mots - # Étape 3 : Retournez un message différent selon que le nombre de mots est < 50, entre 50 et 200, ou > 200 - # Indice : Un email professionnel typique fait entre 50 et 200 mots

# Exercice : Fonction de comptage de mots
# Completez cette fonction pour evaluer la longueur du brouillon

def count_words(draft_text):
    """Compte les mots et evalue la longueur de l'email.
    
    Args:
        draft_text: Texte du brouillon d'email.
        
    Returns:
        str: Message indiquant le nombre de mots et l'evaluation.
    """
    # TODO etudiant : verifiez si le texte est vide
    # TODO etudiant : comptez les mots avec len(text.split())
    # TODO etudiant : retournez un message selon le nombre de mots
    return None  # TODO etudiant : remplacez par l'implementation

# Test avec des exemples
test_drafts = [
    "",  # Aucun brouillon
    "Bonjour, merci pour votre email. Cordialement.",  # Trop court
    "Bonjour " + "Ceci est un email de test. " * 10 + "Cordialement.",  # Correct
]

for draft in test_drafts:
    result = count_words(draft)
    print(f"Draft ({len(draft)} chars): {result}")
Draft (0 chars): None
Draft (46 chars): None
Draft (291 chars): None

5. Creation des Kernels et des Agents

Cette cellule configure les deux agents : - InputCollector : Kernel avec le plugin InputCollector, instructions pour collecter les informations systematiquement - EmailGenerator : Kernel avec le plugin EmailGenerator, instructions pour generer et proposer des modifications

Chaque agent possede son propre Kernel mais partage le même EmailState.

# Cell 5: Création des Kernels et des Agents

from semantic_kernel import Kernel
from semantic_kernel.agents import ChatCompletionAgent

# Création de l'état partagé
shared_state = EmailState()

# Kernel 1 (Input Collector)
input_kernel = sk.Kernel()
add_openai_service(input_kernel)
input_collector_plugin = InputCollectorPlugin(shared_state)
input_kernel.add_plugin(input_collector_plugin, "input_plugin")

input_agent = ChatCompletionAgent(
    kernel=input_kernel,
    name="InputCollector",
    instructions="""Vous êtes un assistant qui collecte des informations pour générer un email.

    IMPORTANT: Vous devez interagir directement avec l'utilisateur humain en posant des questions claires, une à la fois.
    Pour poser une question à l'utilisateur, utilisez TOUJOURS la fonction ask_human.

    Collectez systématiquement les informations suivantes dans cet ordre:
    1. Le type d'email (professionnel, amical, etc.)
    2. Le nom du destinataire
    3. Le rôle du destinataire (si applicable)
    4. Le sujet de l'email
    5. Au moins un point clé à aborder (demandez s'il y en a d'autres)
    6. Le ton souhaité (formel, informel, etc.)

    Après chaque réponse de l'utilisateur, utilisez la fonction appropriée pour enregistrer l'information.
    Utilisez get_state_summary régulièrement pour vérifier l'état actuel.
    Utilisez check_completeness pour vérifier si toutes les informations nécessaires sont disponibles.

    Une fois toutes les informations collectées, informez l'utilisateur que vous avez tout ce qu'il faut
    et que vous allez passer à la génération de l'email."""
)

# Kernel 2 (Email Generator)
email_kernel = sk.Kernel()
add_openai_service(email_kernel)
email_generator_plugin = EmailGeneratorPlugin(shared_state, email_kernel)
email_kernel.add_plugin(email_generator_plugin, "email_plugin")

email_agent = ChatCompletionAgent(
    kernel=email_kernel,
    name="EmailGenerator",
    instructions="""Vous êtes un assistant qui génère des emails personnalisés.

    Lorsque toutes les informations nécessaires sont collectées, utilisez la fonction generate_draft
    pour créer un brouillon d'email adapté.

    Une fois l'email généré, présentez-le à l'utilisateur et demandez-lui s'il souhaite y apporter des modifications.
    Si l'utilisateur demande des modifications, aidez-le à ajuster le contenu de l'email.
    Si l'utilisateur est satisfait, remerciez-le et concluez la conversation.

    En cas d'erreur ou d'informations manquantes, expliquez clairement ce qui manque et demandez à l'InputCollector
    de compléter les informations."""
)

print("Kernels et agents créés")
print("Kernels et agents crees")
Kernels et agents créés
Kernels et agents crees

Les kernels et agents sont créés et configurés avec succès, reliant les plugins InputCollector et EmailGenerator dans un workflow cohérent. Cette architecture modulaire permet une séparation claire des responsabilités : InputCollector gère la collecte des données, EmailGenerator gère la génération du texte, et le kernel coordonne l’ensemble.

Le code crée un kernel par agent, y ajoute le service OpenAI (add_openai_service) puis le plugin dédié, et instancie un ChatCompletionAgent nommé — « InputCollector » en tête. Rien d’auto-organisé ni d’auto-optimisant : deux agents standard, chacun branché sur son kernel, réunis ensuite dans un AgentGroupChat. La création se résume à une ligne de sortie — « Kernels et agents créés » — chaque agent gardant son nom pour la sélection séquentielle qui suit.

6. Orchestration et Groupe de discussion

Cette section configure l’orchestration : - EmailCompletionStrategy : Termine la conversation quand conversation_complete est True - SequentialSelectionStrategy : Les agents parlent dans l’ordre, en commencant par InputCollector - get_user_input() : Fonction asynchrone pour capturer les reponses utilisateur via widgets

Le AgentGroupChat orchestre les deux agents avec ces stratégies.

# Cell 6: Orchestration et Workflow

from semantic_kernel.agents import AgentGroupChat
from semantic_kernel.agents.strategies import SequentialSelectionStrategy
from semantic_kernel.agents.strategies import TerminationStrategy
from pydantic import BaseModel
from IPython.display import display, clear_output
import ipywidgets as widgets
import asyncio

# Stratégie de terminaison basée sur l'état de la conversation
class EmailCompletionStrategy(TerminationStrategy, BaseModel):
    state: EmailState  # Utilisation de l'annotation de type Pydantic

    async def should_terminate(self, agent, history, cancellation_token=None) -> bool:
        return self.state.conversation_complete

# Création d'une stratégie de sélection qui commence avec l'InputCollector
selection_strategy = SequentialSelectionStrategy(initial_agent=input_agent)

# Création du groupe de discussion avec la stratégie de sélection
group_chat = AgentGroupChat(
    agents=[input_agent, email_agent],
    selection_strategy=selection_strategy,
    termination_strategy=EmailCompletionStrategy(state=shared_state)
)

# Fonction asynchrone pour obtenir l'entrée utilisateur via ipywidgets (compatible Jupyter local)
async def get_user_input():
    """
    Obtient une entrée utilisateur de manière asynchrone en utilisant ipywidgets.
    Compatible avec Jupyter Notebook/Lab local (pas de dépendance Google Colab).
    """
    response_received = asyncio.Event()
    response_value = [None]

    # Création des widgets
    input_widget = widgets.Text(
        placeholder='Entrez votre réponse...',
        description='Réponse:',
        layout=widgets.Layout(width='400px')
    )
    submit_button = widgets.Button(
        description='Envoyer',
        button_style='primary'
    )
    output_widget = widgets.Output()

    # Callback pour le bouton
    def on_submit(b):
        with output_widget:
            clear_output()
            if input_widget.value.strip():
                response_value[0] = input_widget.value.strip()
                print(f"Reponse enregistree: {response_value[0]}")
                response_received.set()
            else:
                print("Veuillez entrer une reponse.")

    # Callback pour la touche Entrée
    def on_enter(change):
        if change['type'] == 'change' and change['name'] == 'value':
            pass  # On utilise on_submit pour le traitement

    submit_button.on_click(on_submit)

    # Permettre aussi la soumission avec Entrée (via observe sur la valeur, ipywidgets 8+)
    def handle_submit(change):
        # observe() declenche sur chaque changement de 'value' ; avec continuous_update=False,
        # le callback n'est appele que quand l'utilisateur presse Entree ou quitte le champ.
        on_submit(None)

    input_widget.continuous_update = False
    input_widget.observe(handle_submit, names='value')

    # Affichage des widgets
    display(widgets.HBox([input_widget, submit_button]))
    display(output_widget)

    # Attendre la réponse avec timeout
    try:
        await asyncio.wait_for(response_received.wait(), timeout=600)  # 10 minutes timeout
        return response_value[0]
    except asyncio.TimeoutError:
        print("Aucune reponse recue dans le delai imparti (10 minutes).")
        return "Timeout"

print("Orchestration configurée")
print("Configuration OpenAI et etat initialises")
print("Orchestration configuree")
Orchestration configurée
Configuration OpenAI et etat initialises
Orchestration configuree

L’orchestration est configurée avec une stratégie SequentialSelection, permettant ainsi aux deux agents de collaborer de manière séquentielle et déterministe. Cette approche garantit que chaque étape du workflow (collecte des informations, génération du brouillon) est exécutée dans le bon ordre, avec partage et mise à jour de l’état entre tous les composants du système.

L’enchaînement est bien là — AgentGroupChat avec sélection séquentielle, InputCollector puis EmailGenerator — mais l’exécution de bout en bout reste à démontrer : dans ce run, l’étape réseau bute sur une erreur de connexion. « Résilient aux pannes » ne s’applique pas : l’erreur interrompt le workflow.

7. Fonction principale du workflow

La fonction email_workflow() orchestre tout le processus : 1. Initialise la conversation avec un message utilisateur 2. Itere sur les messages des agents 3. Detecte quand une question est posee et attend la reponse utilisateur 4. Affiche le brouillon genere et demande confirmation 5. Permet des modifications iteratives jusqu’a satisfaction

Le workflow gere les erreurs et affiche l’email final.

# Cellule 7: Fonction principale du workflow

async def email_workflow():
    try:
        # Initialisation de la conversation
        group_chat.history.add_user_message("Je voudrais créer un email personnalisé.")

        # Exécution de la conversation
        async for message in group_chat.invoke():
            # Afficher le message de l'agent
            print(f"[{message.name}] {message.content}")

            # Vérifier si un input humain est demandé
            if shared_state.human_input_requested:
                # Afficher la question
                print(f"\nQuestion: {shared_state.current_question}")

                # Obtenir la réponse de l'utilisateur
                user_response = await get_user_input()
                print(f"Réponse utilisateur reçue: {user_response}")

                # Ajouter la réponse à l'historique de la conversation
                group_chat.history.add_user_message(user_response)

                # Réinitialiser le flag
                shared_state.human_input_requested = False
                shared_state.current_question = ""

            # Si le brouillon est généré, l'afficher
            if shared_state.draft and not shared_state.conversation_complete:
                print("\n--- Brouillon de l'email : ---")
                print(shared_state.draft)
                print("\n--- Fin du brouillon ---")

                # Demander à l'utilisateur s'il est satisfait
                print("\nÊtes-vous satisfait de ce brouillon? (oui/non)")
                satisfied = await get_user_input()

                if satisfied.lower() in ["oui", "yes", "o", "y"]:
                    group_chat.history.add_user_message("Oui, je suis satisfait de ce brouillon.")
                    # Marquer la conversation comme terminée
                    shared_state.conversation_complete = True
                else:
                    # Demander des modifications
                    print("\nQuelles modifications souhaitez-vous apporter?")
                    modifications = await get_user_input()
                    group_chat.history.add_user_message(f"Je voudrais apporter les modifications suivantes: {modifications}")

        # Afficher le message final si la conversation est terminée
        if shared_state.conversation_complete:
            print("\n--- Email final : ---")
            print(shared_state.draft)
            print("\n--- Fin de l'email ---")
            print("\nMerci d'avoir utilisé notre service de création d'email!")

    except Exception as e:
        # Evite d'imprimer le traceback complet : il contient le chemin d'installation
        # Python (qui leak dans les outputs du notebook).
        print(f"Erreur: type={type(e).__name__}, message={e!s}")

print("Fonction principale prête")
print("Fonction principale prete")
Fonction principale prête
Fonction principale prete

La fonction principale du workflow est prête, intégrant l’initialisation de la conversation, la collecte des informations structurées et la génération finale du mail. Cette fonction asynchrone orchestre l’ensemble du processus de bout en bout : elle démarre avec l’agent InputCollector pour poser les questions, passe à EmailGenerator pour la création du brouillon, et retourne le brouillon d’email complet.

La sortie « Fonction principale prête » dit que la fonction est définie — pas qu’elle a tourné : son exécution est l’étape suivante, celle qui échoue ici sur la connexion au service. La « garantie de service » reste à établir avec une API joignable.

8. Exécution du workflow

Executez la cellule ci-dessous pour demarrer le processus interactif de creation d’email.

Fonctionnement : 1. L’agent InputCollector pose des questions une par une 2. Vous repondez via les widgets interactifs 3. Une fois toutes les informations collectees, l’agent EmailGenerator créé un brouillon 4. Vous pouvez demander des modifications ou valider l’email final

Note : Ce notebook necessite une interaction utilisateur via ipywidgets. Assurez-vous d’executer le notebook localement dans Jupyter Notebook ou JupyterLab.

# Cellule 8: Exécution du workflow
# Exécutez cette cellule pour démarrer le processus de création d'email

# Exécution du workflow (avec timeout pour le mode batch)
try:
    await asyncio.wait_for(email_workflow(), timeout=30)
except (asyncio.TimeoutError, Exception) as e:
    print(f"[Batch] Workflow interactif ignore ({type(e).__name__}: {str(e)[:80]})")
Erreur: type=ServiceResponseException, message=("<class 'semantic_kernel.connectors.ai.open_ai.services.open_ai_chat_completion.OpenAIChatCompletion'> service failed to complete the prompt", APIConnectionError('Connection error.'))

C’est le contraire qui se produit : une ServiceResponseException (APIConnectionError) interrompt le workflow — le service OpenAI n’a pas répondu, aucun email n’est produit. Matériel d’exercice idéal : relire l’erreur (échec de connexion, pas d’authentification), vérifier ../.env et la joignabilité de l’URL, puis rejouer la cellule. L’exception est une ServiceResponseException portée par APIConnectionError(‘Connection error.’) et nomme OpenAIChatCompletion : l’échec est au niveau transport, avant toute évaluation de la clé.

Conclusion

Ce notebook a illustre un workflow multi-agents complet pour la creation d’emails personnalises.

Concepts techniques demontres

  1. Plugins personnalises : InputCollectorPlugin et EmailGeneratorPlugin exposent des fonctions appelables par les agents
  2. Etat partage : La classe EmailState (Pydantic) permet aux deux agents de collaborer sur les mêmes données
  3. Stratégie de sélection : SequentialSelectionStrategy orchestre l’ordre de prise de parole
  4. Stratégie de terminaison : EmailCompletionStrategy detecte quand le workflow est termine
  5. Integration UI : ipywidgets pour l’interaction utilisateur asynchrone

Applications pratiques

Ce pattern peut etre adapte pour : - Assistants de redaction : Documents, rapports, presentations - Chatbots structurants : Collecte d’informations methodique pour formulaires complexes - Systèmes de recommendation : Collecte de préférences puis generation de suggestions - Workflows multi-étapes : Tout processus necessitant validation iterative

Points d’amelioration possibles

  • Ajouter une validation des entrees (regex pour emails, longueur des textes)
  • Implementer un système de templates d’emails pre-définis
  • Sauvegarder l’historique des emails generes
  • Ajouter un agent de review pour verifier la grammaire et le style
  • Integrer avec une API d’envoi d’emails (SendGrid, Mailgun)
Retour au sommet