Doctor vs ChatGPT: Multi-Agent Medical Chatbot

# Parameters
BATCH_MODE = True

Navigation: Index

Estimated duration: 30 minutes | Prerequisites: OpenAI account with API key, basic knowledge of Python

In this use case, we build a simulated medical consultation system where three AI agents cooperate: a general practitioner who asks questions, a medical AI that analyzes symptoms, and a pharmacist who recommends treatments. The dialogue is orchestrated by Semantic Kernel via AgentGroupChat, with an automatic termination strategy.

Warning: this notebook is an educational exercise. Diagnoses and medication recommendations are simulated and in no way replace a real medical consultation.

Adapted from an EPF student production: Louise and Jeanne Céline. Universalization: refactor #890.

Overview: a 3-role multi-agent medical chatbot

This notebook implements a simulated medical consultation between three LLM agents: a general practitioner who asks the patient follow-up questions, a medical analysis AI that proposes diagnostic hypotheses, and a pharmacist who checks drug interactions. The whole thing is orchestrated by a Semantic Kernel AgentGroupChat with an exchange-limit termination strategy (6 messages) — detecting a final diagnosis by keyword is left as an exercise. The notebook covers the patterns: Kernel + native plugins, ChatCompletionAgent, AgentGroupChat, TerminationStrategy, and auto function calling. It is a typical use case for synthetic clinical reasoning where several LLM agents simulate a real consultation.

# Import guards - availability flags for external dependencies

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')

Importing libraries

We import the Semantic Kernel building blocks needed for multi-agent orchestration: the Kernel (container for services and plugins), ChatCompletionAgent and AgentGroupChat (agents and group orchestrator), TerminationStrategy (end of dialogue), the @kernel_function decorator (to expose functions as tools) and FunctionChoiceBehavior (for automatic plugin invocation by the LLM). The Annotated type documents the parameters of these plugin functions.

This use case implements a multi-agent conversation in the medical field. Three agents cooperate: a general practitioner, an AI specialized in diagnosis, and a pharmacist. Each agent has its own system prompt and dedicated plugins via @kernel_function.

Educational objectives: - Understand multi-agent orchestration with AgentGroupChat - Use @kernel_function plugins to equip agents with specific capabilities - Implement a TerminationStrategy to control the end of the dialogue - Configure FunctionChoiceBehavior.Auto() for automatic plugin calls

import os
import logging
import asyncio
from dotenv import load_dotenv
from semantic_kernel import Kernel
from semantic_kernel.agents import ChatCompletionAgent, AgentGroupChat
from semantic_kernel.agents.strategies import TerminationStrategy
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
from semantic_kernel.contents import ChatHistory
from semantic_kernel.functions import kernel_function, KernelArguments
from semantic_kernel.connectors.ai import FunctionChoiceBehavior
from typing import Annotated
print("Imports OK")
print("Imports et configuration OK")
Imports OK
Imports et configuration OK

Reading the result: imports and initial configuration

The "Imports OK" output validates the full python-dotenv + semantic-kernel + pydantic chain: three critical dependencies for this use case, and a failure in any single one would have short-circuited the imports cell above (load_dotenv()). The notebook uses guarded imports (try/except ImportError) in the imports cell above — a defensive pattern that documents the availability of external services before failing downstream. The student should reproduce this reflex: a bare import semantic_kernel in a fresh clone without the dependency installed would return an unhelpful ModuleNotFoundError.

# Charger les variables d'environnement
load_dotenv()
True

Reading the result: environment variables loaded

load_dotenv() returns True when a .env file is found and parsed. For this notebook, only one variable matters: OPENAI_API_KEY — the key for the gpt-5-mini model that create_kernel() (below) passes to OpenAIChatCompletion. It is the only one the code reads: no Azure backend and no ComfyUI/Qwen endpoint is wired in here. A False — as in the output above — only means that no .env file was found: the recorded run used the key exported into the process environment (hence the 5 successful API calls at the end of the notebook). If OPENAI_API_KEY is missing from both the .env file and the environment, the service configuration or the first LLM call fails explicitly; the final try/except does not mask that failure.

Log configuration

The logging module makes it possible to trace exchanges between agents in real time. Each message will be timestamped and identified by the name of the sending agent, which makes debugging multi-agent conversations easier.

# Configuration des logs
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[logging.StreamHandler()]
)
logger = logging.getLogger('MedicalAI')
print("Configuration des logs OK")
Configuration des logs OK

Reading the result: structured logging for multi-agent traceability

"Configuration des logs OK" validates the %(asctime)s [%(levelname)s] %(message)s format at level INFO. This is the format the consultation execution cell (at the end of the notebook) will display as output (timestamp + [INFO] Début de la consultation médicale IA when the consultation starts). Structured logging is not a pedagogical whim: without it, the student cannot tell who is speaking in a 3-agent chat — doctor, medical AI, or pharmacist. The distinction does not come from the format (which displays neither the logger name nor the sender) but from the explicit log calls inside run_medical_chat(): logger.info(f"[{message.role}] {message.name}: {message.content}") identifies the sender of each message, turn by turn. The student should note the timestamp (the date changes on every run) and the order of events: this trace is what validates the orchestral sequence after the fact.

Creating the Semantic Kernel kernel

The Kernel is Semantic Kernel’s central container: it brings together the AI service and the plugins. The create_kernel() function instantiates an empty kernel, then registers an OpenAIChatCompletion service on it (model gpt-5-mini, API key read from the environment — never hard-coded). Factoring this construction into a function makes it possible to create several kernels, for example one per agent if you want to isolate them.

# Création du kernel Semantic Kernel
def create_kernel():
    kernel = Kernel()
    kernel.add_service(OpenAIChatCompletion(
        service_id="openai",
        ai_model_id="gpt-5-mini",  # Modifier si besoin
        api_key=os.getenv("OPENAI_API_KEY")
    ))
    return kernel
# La cellule ne fait que DEFINIR la factory : aucun Kernel() n'existe encore.
print("Fonction create_kernel definie — le kernel sera instancie plus bas (kernel = create_kernel())")
Fonction create_kernel definie — le kernel sera instancie plus bas (kernel = create_kernel())

Reading the result: create_kernel factory function

"Fonction create_kernel definie…" describes exactly what this cell did: define the factory, nothing more. No Kernel() exists yet — instantiation only happens later in the notebook (kernel = create_kernel()). This definition/instantiation separation is a testability pattern: the instantiation cell can be rerun several times without redefining the function. In an educational notebook, this allows the student to reinstantiate a fresh kernel if the previous one has accumulated conflicting plugins (e.g., after an error in a previous cell). In a production project, this is the equivalent of a classic factory pattern (GoF).

Reading the result: Semantic Kernel kernel instantiated

The cell above defines create_kernel() — the factory of Semantic Kernel’s central container. The kernel aggregates LLM services, plugins (doctor/pharmacist/allergies) and filters. In this notebook, each conversational agent (doctor, medical AI, pharmacist) shares the same kernel but accesses different plugins via kernel.add_plugin(DoctorPlugin(), plugin_name="doctor"). Sharing the kernel instead of duplicating it per agent is an architecture choice: one LLM service, N personas.

Medical plugins: equipping agents with specific skills

Each agent has a dedicated plugin containing functions annotated with @kernel_function. These functions act as tools that the LLM can automatically invoke when it needs them:

  • DoctorPlugin: asks follow-up questions based on the mentioned symptom
  • MedicalAIPlugin: assesses the severity of a symptom (Mild, Moderate, Severe, Critical)
  • PharmacistPlugin: recommends an appropriate medication with dosage and precautions

The kernel is the central element of Semantic Kernel. The create_kernel() function instantiates a kernel and registers an OpenAIChatCompletion service in it, which will provide access to the GPT-4o-mini model. Each agent will use this shared kernel to generate its responses.

class DoctorPlugin:
    """Plugin permettant au médecin de poser des questions complémentaires sur les symptômes."""
    @kernel_function(description="Pose des questions supplémentaires pour affiner le diagnostic.")
    def ask_followup_questions(self, symptom: Annotated[str, "Symptôme décrit par l'utilisateur"]) -> str:
        """Retourne une question en fonction du symptôme mentionné."""
        questions_map = {
            "fièvre": "Depuis combien de temps avez-vous de la fièvre ?",
            "maux de tête": "Avez-vous une sensibilité à la lumière ou au bruit ?",
            "douleur thoracique": "La douleur est-elle aiguë ou diffuse ?",
        }
        return questions_map.get(symptom.lower(), "Pouvez-vous donner plus de détails sur vos symptômes ?")

class MedicalAIPlugin:
    """Plugin qui analyse la gravité des symptômes."""
    @kernel_function(description="Vérifie la gravité d'un symptôme médical.")
    def check_symptom_severity(self, symptom: Annotated[str, "Symptôme décrit par l'utilisateur"]) -> str:
        """Retourne une évaluation de la gravité du symptôme."""
        severity_map = {
            "fièvre": "Modérée",
            "maux de tête": "Léger",
            "douleur thoracique": "Sévère",
            "perte de connaissance": "Critique"
        }
        return severity_map.get(symptom.lower(), "Inconnu - consultez un médecin.")

class PharmacistPlugin:
    """Plugin qui recommande des médicaments en fonction du diagnostic."""
    @kernel_function(description="Recommande un médicament adapté à un symptôme.")
    def recommend_medication(self, symptom: Annotated[str, "Symptôme décrit par l'utilisateur"]) -> str:
        """Retourne une suggestion de médicament (avec précautions)."""
        medication_map = {
            "fièvre": "Paracétamol (500mg, toutes les 6h, max 3 jours)",
            "maux de tête": "Ibuprofène (200mg, toutes les 8h, avec précaution si problème gastrique)",
            "douleur thoracique": "Aucun médicament recommandé - Consultez un médecin immédiatement",
        }
        return medication_map.get(symptom.lower(), "Aucun médicament recommandé - Consultez un pharmacien.")
print("Classe DoctorPlugin définie")
print("Classes DoctorPlugin, MedicalAIPlugin, PharmacistPlugin definies")
Classe DoctorPlugin définie
Classes DoctorPlugin, MedicalAIPlugin, PharmacistPlugin definies

Reading the result: medical plugins defined

"Classe DoctorPlugin définie" validates that DoctorPlugin (patient follow-up questions), MedicalAIPlugin (AI analysis), and PharmacistPlugin (drug interaction checks) are instantiated in memory. Each plugin exposes Python native functions decorated with @kernel.function_description() that agents can invoke via SK’s auto function calling mechanism. The notebook uses 3 medical plugins + 1 allergy plugin (exercise, below) — the student must complete the AllergyPlugin (see the exercise below). Plugins are the medical equivalent of tools in LangChain: skill calling becomes function_call on the LLM side.

Exercise 1: Allergy Verification Plugin

The current plugins cover follow-up questions, severity, and medications. A crucial plugin is missing: allergy verification. The objective is to create AllergyPlugin, which allows the pharmacist agent to verify whether a recommended medication is compatible with the patient’s declared allergies.

Objective: implement a class AllergyPlugin with a check_allergy method annotated with @kernel_function that returns a warning if the medication is incompatible with a known allergy.

Hints: - # Step 1: Define a dictionary mapping allergies to contraindicated medications - # Step 2: Implement the check_allergy method with Annotated type annotations - # Hint: use the pattern of the existing plugins (DoctorPlugin, PharmacistPlugin)

class AllergyPlugin:
    """Plugin de verification des allergies medicamenteuses."""
    # TODO etudiant : implementer le plugin
    
    @kernel_function(description="Verifie si un medicament est compatible avec les allergies du patient.")
    def check_allergy(
        self, 
        medication: Annotated[str, "Nom du medicament"], 
        allergy: Annotated[str, "Allergie declaree du patient"]
    ) -> str:
        # Etape 1 : definir les contre-indications connues
        contraindications = {}  # TODO etudiant : dictionnaire allergie -> medicaments
        
        # Etape 2 : verifier et retourner le resultat
        result = None  # TODO etudiant : logique de verification
        return result  # TODO etudiant : retourner le message approprie

print("Exercice a completer : AllergyPlugin")
Exercice a completer : AllergyPlugin

Kernel Creation

Here we instantiate the shared kernel that the three agents and their plugins will use. Every add_plugin call and every agent creation that follows relies on this same instance: this is what lets the medical AI invoke, through the kernel, the functions exposed by the doctor’s and the pharmacist’s plugins.

# Création du kernel
kernel = create_kernel()
print("Kernel instancie")
Kernel instancie

Reading the result: system prompts configured

"Prompts medicaux configures" covers the 3 roles: DOCTOR_PROMPT (general practitioner, follow-up questions), AI_MEDICAL_PROMPT (AI analysis assistant), PHARMACIST_PROMPT (interaction checks). Each prompt structures the persona + expected behavior: professional tone, refusal to prescribe, asking for clarifications. This is an application of the role prompting pattern (cf. OpenAI Best Practices). The prompt exercise (further down) invites the student to write a pediatric prompt — adding a specialized persona is a classic persona engineering exercise.

Reading the result: from skeleton to execution

With the kernel instantiation cell above (kernel = create_kernel()) and the plugin-addition cell further down (kernel.add_plugin(...)), the notebook leaves the definition phase for the instantiation phase. Before: we defined classes and functions. After: we use them. This demarcation is a classic pedagogical signal in notebooks: the second half of the notebook is executable (the student should see the consultation unfold), while the first half is structural (the student can read it as production code). The print("Kernel instancie") that accompanies the instantiation is the written trace of this transition.

Adding plugins for each agent

kernel.add_plugin() registers a plugin instance under a name. Each plugin exposes its @kernel_function methods to the kernel, which makes them discoverable by the LLM. Here we register the three medical plugins (doctor, medical, pharmacist) on the shared kernel — they will be invocable automatically thanks to the FunctionChoiceBehavior.Auto() configured further down.

# Ajout des plugins pour chaque agent
kernel.add_plugin(DoctorPlugin(), plugin_name="doctor")
kernel.add_plugin(MedicalAIPlugin(), plugin_name="medical")
kernel.add_plugin(PharmacistPlugin(), plugin_name="pharmacist")
print("Kernel configuré avec le plugin médical")
print("Classes plugins medicaux definies")
print("Kernel configure avec le plugin medical")
Kernel configuré avec le plugin médical
Classes plugins medicaux definies
Kernel configure avec le plugin medical

Reading the result: kernel and plugins operational

"Kernel instancie" then "Kernel configuré avec le plugin médical" validate the sequence kernel = create_kernel() → kernel.add_plugin(DoctorPlugin(), plugin_name="doctor"). This second step is what distinguishes Semantic Kernel from a simple OpenAI wrapper: the kernel registers plugins as tools invocable by the LLM via the auto function calling layer. Without add_plugin, the Python native functions of DoctorPlugin would be unreachable by the agents, and the LLM would answer without medical grounding.

System prompts: defining the behavior of each agent

System prompts are the instructions that guide the behavior of each agent. They establish the role, constraints, and expected response style. Good prompt design is essential to avoid undesirable behaviors (premature diagnosis, unfounded recommendation).

DOCTOR_PROMPT = """
Vous êtes un médecin généraliste. Vous posez d'abord des questions pour mieux comprendre les symptômes de l'utilisateur,
puis vous donnez un diagnostic probable basé sur votre expertise médicale. 
Ne donnez jamais de diagnostic sans avoir recueilli assez d'informations.
"""

AI_MEDICAL_PROMPT = """
Vous êtes une IA médicale spécialisée en diagnostic. Analysez les symptômes fournis et proposez un diagnostic basé sur des statistiques et des études médicales. 
Soyez clair et donnez plusieurs hypothèses si nécessaire.
"""

PHARMACIST_PROMPT = """
Vous êtes un pharmacien qualifié. En fonction du diagnostic fourni, vous recommandez les médicaments appropriés. 
Mentionnez toujours les précautions d'utilisation et la nécessité d'une consultation médicale avant la prise de médicaments.
"""
print("Prompt médical configuré")
print("Prompts medicaux configures")
Prompt médical configuré
Prompts medicaux configures

Reading the result: medical termination strategy

"Stratégie de terminaison médicale définie" marks the instantiation of MedicalTerminationStrategy — a subclass of TerminationStrategy that detects the end of the multi-agent chat. In this notebook, the termination criterion is an exchange limit: should_terminate returns True as soon as the history reaches 6 messages (len(history) >= 6) — i.e. 1 patient message + 5 agent replies. Without an explicit strategy, AgentGroupChat loops forever. Detecting a diagnosis by keyword is precisely the exercise left to the student further down (DiagnosticTerminationStrategy). The student should observe this separation between orchestration and business logic: the termination strategy is a behavioral plugin, not a rule hard-coded into the chat.

Exercise 2: Adapt the prompts for a pediatric scenario

The current prompts are configured for an adult. The objective is to adapt the system for a pediatric consultation: the doctor must use language suited to children, the medical AI must take pediatric specifics into account (different dosage, specific vital signs), and the pharmacist must mention precautions for children.

Objective: write the three system prompts adapted to the pediatric context.

Hints: - # Step 1: Modify the doctor’s prompt for language accessible to children - # Step 2: Adapt the medical AI prompt for pediatric dosages and signs - # Hint: add constraints such as “always mention the recommended weight for dosage”

# Exercice 2 : Prompts adaptes au contexte pediatric
# TODO etudiant : rediger les trois prompts pediatric

PEDIATRIC_DOCTOR_PROMPT = ""    # Etape 1 : langage enfantin, questions adaptees
PEDIATRIC_AI_PROMPT = ""        # Etape 2 : dosages enfant, signes specific
PEDIATRIC_PHARMACIST_PROMPT = ""  # TODO etudiant : precautions enfant, posologie poids

print("Exercice a completer : prompts pediatric")
Exercice a completer : prompts pediatric

Creating Agents

The three system prompts define the behavior and constraints of each agent. The doctor must ask questions before diagnosing, the medical AI analyzes symptoms using a statistical approach, and the pharmacist recommends treatments while reminding users of the precautions for use. This separation of roles is fundamental in a multi-agent architecture.

# Création des agents
doctor_agent = ChatCompletionAgent(
    kernel=kernel,
    name="Docteur_Humain",
    instructions=DOCTOR_PROMPT,
)

ai_medical_agent = ChatCompletionAgent(
    kernel=kernel,
    name="IA_Medicale",
    instructions=AI_MEDICAL_PROMPT,
)
print("Agents de conversation créés")
print("Kernel configure avec plugin medical")
print("Agents de conversation crees")
Agents de conversation créés
Kernel configure avec plugin medical
Agents de conversation crees

Reading the result: conversational agents created

"Agents de conversation créés" marks the instantiation of three ChatCompletionAgent (doctor, medical AI, pharmacist) with their respective roles and the kernel’s ServiceId="openai". This is where the group model (AgentGroupChat, instantiated further down in the notebook) takes shape: three personas, one chat orchestrator, one medical termination strategy. The student should observe the agent/kernel distinction: an agent uses a kernel but does not own it. For scalability, adding a 4th agent (e.g. a nurse) does not require re-instantiating the kernel — only a new ChatCompletionAgent sharing the same one.

Configuration so that the medical agent automatically calls the plugins

By default, an agent relies only on its textual instructions. So that it can invoke the plugins registered in the kernel, we enable FunctionChoiceBehavior.Auto() in its KernelArguments: the LLM then decides by itself, depending on the context, to call one @kernel_function or another (severity of a symptom, medication recommendation, and so on). We apply this setting to the IA_Medicale agent, then create the pharmacist on the same model.

settings = kernel.get_prompt_execution_settings_from_service_id("openai")
settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
ai_medical_agent.arguments = KernelArguments(settings=settings)

pharmacist_agent = ChatCompletionAgent(
    kernel=kernel,
    name="Pharmacien",
    instructions=PHARMACIST_PROMPT,
)
print("Paramètres d'exécution configurés")
print("Parametres d execution configures")
Paramètres d'exécution configurés
Parametres d execution configures

Reading the result: LLM execution settings

"Paramètres d'exécution configurés" marks kernel.get_prompt_execution_settings_from_service_id("openai") which configures the temperature, max_tokens, and top_p for the kernel’s LLM calls. These settings are shared by the 3 agents through the kernel — not configured agent by agent. It is an orchestration choice: for a medical consultation, we want reproducible and conservative answers (low temperature, typically 0.2-0.5), not creative ones. The student can observe the effect by changing temperature (from 0.0 = deterministic to 1.0 = exploratory) and re-running the settings cell above.

Definition of a termination strategy

The FunctionChoiceBehavior.Auto() setting allows the IA_Medicale agent to automatically invoke the plugins registered in the kernel (symptom severity, recommendations). Without this configuration, the agent would only have its textual instructions to formulate its responses.

class MedicalTerminationStrategy(TerminationStrategy):
    async def should_terminate(self, agent, history):
        return len(history) >= 6  # On limite à 6 échanges
print("Stratégie de terminaison médicale définie")
print("Strategie de terminaison medicale definie")
Stratégie de terminaison médicale définie
Strategie de terminaison medicale definie

Exercise 3: Termination strategy based on diagnosis

The current strategy stops after 6 messages. The objective is to implement a smarter strategy that detects when a probable diagnosis has been formulated by the medical AI (presence of words such as “diagnostic”, “hypothese”, “probablement” in the last message).

Objective: create DiagnosticTerminationStrategy that stops the conversation as soon as a diagnosis is issued or after a maximum of 8 exchanges.

Hints: - # Step 1: Define a list of keywords indicative of a diagnosis - # Step 2: Check for their presence in the content of the last message - # Hint: combine the diagnosis condition with a maximum exchange counter

from typing import ClassVar

class DiagnosticTerminationStrategy(TerminationStrategy):
    # TODO etudiant : implementer la detection de diagnostic
    DIAGNOSTIC_KEYWORDS: ClassVar[list] = []  # Etape 1 : mots-cles de diagnostic
    MAX_EXCHANGES: ClassVar[int] = 8
    
    async def should_terminate(self, agent, history):
        # Etape 2 : verifier diagnostic OU limite d'echanges
        result = False  # TODO etudiant : remplacer par la logique
        return result

print("Exercice a completer : DiagnosticTerminationStrategy")
Exercice a completer : DiagnosticTerminationStrategy

Creating the group chat with a termination strategy

The MedicalTerminationStrategy class inherits from TerminationStrategy and implements should_terminate. Here, the condition is simple: after 6 messages in the history, the dialogue stops. This approach avoids infinite loops while allowing enough exchanges for a complete diagnosis.

chat = AgentGroupChat(
    agents=[doctor_agent, ai_medical_agent, pharmacist_agent],
    termination_strategy=MedicalTerminationStrategy()  # Ajout de la stratégie
)
print("Chat de groupe médical configuré")
print("Prompts medicaux configures")
print("Chat de groupe medical configure")
Chat de groupe médical configuré
Prompts medicaux configures
Chat de groupe medical configure

Reading the result: medical group chat configured

"Chat de groupe médical configuré" validates the chain AgentGroupChat(agents=[...], termination_strategy=MedicalTerminationStrategy(), selection_strategy=DefaultSelectionStrategy()). The group chat orchestrates the multi-agent conversation: at each turn, the selection_strategy picks the active agent (round-robin by default, or based on the last message), the termination_strategy detects the end (limit of 6 messages in the history). This is the heart of the multi-agent pattern: without explicit orchestration, the agents would speak in parallel. This notebook is a typical use case of synthetic clinical reasoning where 3 LLM agents simulate a real consultation.

Reading the result: complete multi-agent skeleton, ready for execution

At this point in the notebook, the entire multi-agent machinery is in place: shared kernel (1), 3 registered medical plugins (doctor/AI/pharmacist — the AllergyPlugin skeleton from exercise 1 is not registered and remains to be completed), 3 conversational agents with their respective prompts, 1 medical termination strategy, 1 orchestrated group chat, 1 async run_medical_chat() function. What separates this notebook from a dead skeleton is the execution cell (try/except await run_medical_chat(), below): the asynchronous call actually triggers the orchestration and the student sees the conversation unfold turn by turn. Key pattern: the complexity of a multi-agent system is hidden in the setup (everything above) and visible in the execution (the last cells). The student must understand this distinction to script their own use cases.

Function to run the dialogue

The run_medical_chat function orchestrates the complete dialogue: it seeds the consultation with the patient’s symptoms, passes them to the AgentGroupChat, then iterates over the agents’ responses until the termination strategy declares the consultation complete (limit of 6 exchanges set by MedicalTerminationStrategy).

Two seeding modes:

  • Batch (BATCH_MODE = True, the Papermill/CI mode): a fixed clinical case (SYMPTOMES_DEMO) starts the consultation. Under headless execution stdin does not exist — calling input() raises StdinNotImplementedError before any agent has spoken at all, and the consultation never takes place. This is the same pattern as the two other case studies in this directory: Fort-Boyard seeds with a fixed word to guess, Barbie-Schreck with a randomly drawn style constraint.
  • Interactive (BATCH_MODE = False): the initial symptoms are typed in at the keyboard via input().

The consultation is then agent-driven: the three agents reply to one another without intervention. The try/except in the execution cell now catches only EOFError/KeyboardInterrupt — an orchestration failure (API key, network, quota) must remain visible, not silently absorbed.

from semantic_kernel.contents import ChatMessageContent, AuthorRole

SYMPTOMES_DEMO = ("Depuis trois jours, j'ai une fievre a 38,5 degres avec des maux de tete "
                  "et une legere toux seche. Je n'ai pas de douleur thoracique.")

async def run_medical_chat():
    logger.info("Début de la consultation médicale IA")

    # En mode batch (Papermill/CI), stdin n'existe pas : on amorce la consultation
    # avec un cas clinique fixe -- meme patron que Fort-Boyard et Barbie-Schreck.
    if BATCH_MODE:
        symptoms = SYMPTOMES_DEMO
        print(f"[Batch] Cas clinique démo : {symptoms}")
    else:
        symptoms = input("Décrivez vos symptômes : ")

    # Le message patient amorce l'historique INTERNE de l'AgentGroupChat (via
    # add_chat_message) : sans lui, la premiere invocation n'a aucun message
    # auquel repondre et echoue.
    await chat.add_chat_message(ChatMessageContent(role=AuthorRole.USER, content=symptoms))

    # Consultation agent-driven : les trois agents se repondent jusqu'a la limite
    # de 6 echanges posee par MedicalTerminationStrategy.
    while True:
        async for message in chat.invoke():
            logger.info(f"[{message.role}] {message.name}: {message.content}")
            print(f"{message.name}: {message.content}")

        if chat.is_complete:
            break

    logger.info("Consultation terminée.")

print("Fonction de chat médical prête")
Fonction de chat médical prête

Reading the result: medical chat function ready

"Fonction de chat médical prête" marks the culmination of the skeleton: the async function run_medical_chat() can be called to start the simulated consultation. The logger.info("Début de la consultation médicale IA") that opens run_medical_chat() is the first execution signal when the student runs the consultation execution cell (below). At this point, the notebook has instantiated 3 agents, 1 shared kernel, 3 registered plugins, 1 termination strategy, 1 group chat — the whole multi-agent machinery is in place. The execution cell launches await run_medical_chat() which will loop until MedicalTerminationStrategy.should_terminate() returns True (limit of 6 messages in the group history).

# except resserre : plus d'Exception nue. EOFError/KeyboardInterrupt = session
# interactive interrompue par l'utilisateur ; tout autre echec (API, cle, reseau)
# doit rester VISIBLE dans la sortie, pas etre absorbe.
try:
    await run_medical_chat()
except (EOFError, KeyboardInterrupt) as e:
    print(f"[Session interactive interrompue ({type(e).__name__}) - la consultation n'a pas eu lieu, relancez la cellule.]")
[Batch] Cas clinique démo : Depuis trois jours, j'ai une fievre a 38,5 degres avec des maux de tete et une legere toux seche. Je n'ai pas de douleur thoracique.
Docteur_Humain: Merci — j'aimerais poser quelques questions pour mieux comprendre votre état avant de proposer un diagnostic probable.

Questions (répondez autant que possible) :
1. Avez-vous des frissons, sueurs ou fatigue importante ?  
2. Avez-vous des courbatures, douleurs musculaires ou articulaires ?  
3. Votre toux produit-elle des expectorations (crachats) ou est-elle entièrement sèche ?  
4. Avez-vous un mal de gorge, une congestion nasale ou un écoulement nasal ?  
5. Avez-vous des difficultés à respirer, une respiration rapide ou un essoufflement à l'effort ?  
6. Avez-vous des nausées, vomissements, diarrhée ou perte d'appétit ?  
7. Avez-vous vos antécédents vaccinaux à jour (grippe, COVID-19) ou été en contact récent avec une personne malade ?  
8. Prenez-vous des médicaments actuellement (y compris antalgiques) ? Avez-vous déjà pris du paracétamol/ibuprofène pour faire baisser la fièvre et cela a-t-il aidé ?  
9. Avez-vous des conditions médicales chroniques (diabète, maladies pulmonaires, immunosuppression, cœur, etc.) ?  
10. Avez-vous remarqué une évolution : la fièvre augmente-t-elle, diminue-t-elle ou est-elle stable depuis 3 jours ?

Répondez à ces questions et j'indiquerai un diagnostic probable et les conseils à suivre (soins à domicile, signes d'alerte, nécessité de test/santé publique, et si besoin prise en charge médicale urgente).
IA_Medicale: Merci — avec les éléments fournis (fièvre 38,5 °C depuis 3 jours, maux de tête, toux sèche légère, pas de douleur thoracique) voici une analyse et des conseils pragmatiques.

Hypothèses probables (par ordre de probabilité)
- Infection virale des voies respiratoires hautes (rhume/bronchite virale) — la cause la plus fréquente chez un adulte ambulatoire avec fièvre modérée et toux sèche.  
- Infection à SARS‑CoV‑2 (COVID‑19) — présente fréquemment par fièvre, céphalées et toux sèche ; reste une possibilité importante à tester.  
- Grippe (influenza) — peut donner fièvre élevée, céphalées et toux; plus probable si début brutal et courbatures importantes ou épidémie saisonnière en cours.  
- Bronchite aiguë non compliquée — souvent virale, toux prédominante.  
- Pneumonie bactérienne — moins probable en absence de douleur thoracique, de dyspnée ou de signes respiratoires focaux, mais à envisager si aggravation (fièvre persistante, dyspnée, expectorations purulentes).  

Contexte statistique succinct
- Dans les consultations ambulatoires pour toux/fièvre sans détresse respiratoire, la majorité (la plupart des séries, ≈70–90%) sont d’origine virale (rhinovirus, influenza, SARS‑CoV‑2, etc.). Les pneumonies bactériennes sont relativement rares chez des patients sans signes de gravité (<10% dans ce type de population), mais doivent être recherchées si signes d’aggravation.

Conseils immédiats (soins à domicile)
- Testez-vous pour le COVID‑19 dès que possible (test antigénique rapide ou PCR selon disponibilité). Si positif, suivez les recommandations locales d’isolement et de traitement.  
- Repos, hydratation, nutrition légère.  
- Antipyrétique/analgésique : paracétamol 500–1000 mg toutes les 4–6 h si nécessaire, maximum environ 3 g/24 h (suivre posologie locale et conditions personnelles). Les AINS (ibuprofène) peuvent être utilisés si absence de contre‑indication, mais paracétamol est souvent préféré pour la fièvre.  
- Soulagement de la toux : inhalations de vapeur, boissons chaudes, miel (si >1 an), pastilles pour la gorge.  
- Évitez les antibiotiques sauf si diagnostic de surinfection bactérienne confirmé ou très probable (prescrit par médecin).

Quand consulter en urgence / signes d’alerte
Consultez ou rendez‑vous aux urgences immédiatement si apparition de l’un des signes suivants :
- Difficulté à respirer, essoufflement important, respiration rapide.  
- Saturation en oxygène < 94% (si vous avez un oxymètre) ou coloration bleutée des lèvres/visage.  
- Douleur thoracique intense ou oppression.  
- Confusion, somnolence marquée, incapacité à boire/tenir les liquides.  
- Fièvre très élevée (>40 °C) ou fièvre qui s’aggrave malgré antipyrétiques, ou fièvre persistante au-delà de 72 heures sans amélioration.

Quand voir son médecin / faire un test complémentaire
- Si vous êtes âgé(e) > 65 ans, immunodéprimé(e), ou avez une maladie chronique (diabète, insuffisance cardiaque, BPCO, etc.), contactez votre médecin rapidement pour évaluation et éventuels tests/traitement.  
- Si la fièvre ne baisse pas après 48–72 heures ou que les symptômes empirent, consulter pour examen clinique, éventuelle radiographie pulmonaire et examen sanguin.

Informations que j’ai besoin pour affiner le diagnostic
Merci de me dire, si possible :
- Votre âge et antécédents médicaux (diabète, BPCO, immunosuppression, grossesse, etc.).  
- Avez‑vous des courbatures, frissons, perte d’appétit, nausées, ou diarrhée ?  
- La toux est‑elle sèche sans expectoration ? Avez‑vous une perte d’odorat/goût ?  
- Avez‑vous été en contact récent avec une personne malade ou pas vacciné contre la grippe/COVID récemment ?  
- Avez‑vous pris des antipyrétiques et ont‑ils diminué la fièvre ?

Répondez à ces points et je préciserai la probabilité des diagnostics et les actions à suivre (test(s) recommandés, besoin d’antibiotiques ou d’examens complémentaires, etc.). Si vous avez des signes de gravité décrits ci‑dessus, rendez‑vous sans délai aux urgences.
Pharmacien: Merci — d’après vos signes (fièvre 38,5 °C depuis 3 jours, maux de tête, toux sèche légère, pas de douleur thoracique) voici des recommandations pharmaceutiques pratiques. Avant toute prise de médicament, consultez un médecin ou votre pharmacien si vous avez des facteurs de risque (âge avancé, grossesse, maladies chroniques, prise d’autres traitements).

Médicaments pouvant aider (adultes)
- Paracétamol (antipyrétique/antalgique)  
  - Posologie usuelle : 500–1000 mg toutes les 4–6 heures si besoin, maximum ≈ 3 g/24 h.  
  - Précautions : attention en cas d’insuffisance hépatique, consommation régulière d’alcool (dans ce cas demander conseil, dose maximale plus basse). Ne pas dépasser la dose recommandée.

- Anti‑inflammatoires non stéroïdiens (AINS) — ex. ibuprofène  
  - Posologie OTC : 200–400 mg toutes les 4–6 h si besoin, maximum ~1200 mg/24 h (selon recommandation locale).  
  - Précautions : éviter si antécédent d’ulcère, saignement digestif, insuffisance rénale sévère, insuffisance cardiaque, prise d’anticoagulant, asthme sensible aux AINS, ou grossesse (surtout 3e trimestre). Si doute, préférez paracétamol.

- Pour la toux sèche (si elle vous gêne)  
  - Dextrométhorphane (antitussif) : posologie adulte ~10–20 mg toutes les 4–6 h, max ~120 mg/24 h (respecter notice).  
  - Précautions : contre‑indiqué avec inhibiteurs de la MAO, à utiliser prudemment si vous prenez certains antidépresseurs (risque de syndrome sérotoninergique).  
  - Alternatives non médicamenteuses : miel (si >1 an), boissons chaudes, inhalations de vapeur, pastilles adoucissantes.

- Si toux productive (expectoration)  
  - Expectorant comme guaifenesin (posologie selon notice) ou simplement hydratation + humidification. Antibiotique non indiqué sans confirmation d’infection bactérienne.

Soins non médicamenteux utiles
- Repos, hydratation régulière, humidification de l’air, lavages nasaux au sérum physiologique.  
- Si suspicion COVID‑19 : test antigénique ou PCR et isolement selon règles locales.

Contre‑indications et interactions importantes (à vérifier avant prise)
- Paracétamol : prudence si maladie du foie ou consommation importante d’alcool.  
- Ibuprofène/AINS : éviter selon antécédents cardiaques, rénaux, digestifs, traitement anticoagulant ou certains médicaments antihypertenseurs.  
- Dextrométhorphane : éviter avec IMAO et surveiller avec certains antidépresseurs/psychotropes.  
- Si vous êtes enceinte, allaitante, âgé(e) ou immunodéprimé(e) : demandez conseil médical avant toute automédication.

Antibiotiques
- Ne pas prendre d’antibiotique sans avis médical et sans signe de surinfection bactérienne (expectorations purulentes persistantes, fièvre qui s’aggrave, examen médical).

Signes d’alerte — consultez sans délai ou allez aux urgences si l’un des signes suivants apparaît
- Difficultés respiratoires, essoufflement marquant.  
- Douleur thoracique, oppression.  
- Confusion, somnolence importante, incapacité à boire.  
- Saturation en O2 < 94 % si vous avez un oxymètre.  
- Fièvre très élevée ou fièvre qui s’aggrave malgré antipyrétiques et persiste >72 h.

Pour affiner la recommandation
Merci de préciser : âge, grossesse possible, antécédents (insuffisance hépatique/ rénale, ulcère, hypertension, maladies cardiaques, asthme), traitements en cours (anticoagulants, antidépresseurs, etc.), consommation d’alcool, et si vous avez déjà pris paracétamol/ibuprofène et si cela a aidé. Je pourrai alors vous proposer une posologie personnalisée et vérifier les interactions/contre‑indications.

Rappel légal et sécurité : ces conseils ne remplacent pas une consultation médicale. En cas de doute ou de signes de gravité, consultez rapidement un professionnel de santé.
Docteur_Humain: Merci — quelques questions rapides pour affiner et pouvoir poser un diagnostic probable et des conseils adaptés. Répondez autant que possible :

1. Quel âge avez‑vous ? Êtes‑vous enceinte ?  
2. Avez‑vous des antécédents : diabète, hypertension, maladie cardiaque, BPCO/asthme, insuffisance rénale, immunodépression ?  
3. Avez‑vous été en contact récent avec quelqu’un malade (COVID/grippe) ? Avez‑vous eu un test COVID (antigénique ou PCR) ? résultat ?  
4. Avez‑vous des courbatures, frissons ou une fatigue importante ? Perte d’odorat/goût ?  
5. La toux est‑elle complètement sèche ? y a‑t‑il des crachats (couleur, quantité) ?  
6. Avez‑vous mal de gorge, nez bouché ou écoulement nasal ?  
7. Avez‑vous des nausées, vomissements, diarrhée ?  
8. Avez‑vous pris des médicaments (paracétamol, ibuprofène, antitussifs) ? la fièvre a‑t‑elle baissé ?  
9. Fumez‑vous ? avez‑vous un oxymètre à domicile (SpO2) et si oui quelle saturation ?  
10. Depuis ce début de symptômes, la situation s’améliore, stagne ou s’aggrave ?

Une fois que j’ai ces informations je vous indiquerai le diagnostic le plus probable, les mesures à prendre immédiatement (soins à domicile, tests), et les signes qui nécessitent une consultation urgente. Si vous avez déjà des signes de gravité (essoufflement, confusion, lèvres bleues, fièvre très élevée), dites‑le tout de suite ou consultez les urgences.
IA_Medicale: Merci — avec les éléments que vous avez donnés (fièvre 38,5 °C depuis 3 jours, maux de tête, toux sèche légère, pas de douleur thoracique) voici une synthèse rapide et des recommandations pratiques.

Hypothèses probables (par ordre de probabilité)
- Infection virale des voies respiratoires hautes (rhume/bronchite virale) — la plus probable.  
- COVID‑19 — possibilité importante (tousse sèche + fièvre + céphalée) ; à tester pour le dépister.  
- Grippe (influenza) — possible surtout en période épidémique ou si début brutal et courbatures intenses.  
- Bronchite aiguë non compliquée (souvent virale).  
- Pneumonie bactérienne — moins probable en l’absence de douleur thoracique, d’essoufflement ou d’expectoration purulente, mais à garder en tête si aggravation.

Probabilités générales (estimation)
- Dans ce contexte ambulatoire sans signes de gravité, la majorité (~70–90%) des cas sont d’origine virale. Les causes bactériennes sévères restent rares.

Mesures immédiates (à domicile)
- Faites un test COVID‑19 (antigénique ou PCR) dès que possible. Isolement si test positif.  
- Repos et hydratation.  
- Antipyrétique : paracétamol 500–1000 mg toutes les 4–6 h si besoin, sans dépasser ≈ 3 g/24 h (adapter si problèmes hépatiques).  
- AINS (ibuprofène) possible si pas de contre‑indication (ulcère, insuffisance rénale, insuffisance cardiaque, asthme sensible, anticoagulants, grossesse) — sinon préférez paracétamol.  
- Pour la toux sèche : inhalations de vapeur, miel (si >1 an), pastilles, éventuellement déxtrométhorphane si gênante et pas de contre‑indication (IMAO, certains antidépresseurs).  
- Évitez les antibiotiques sans avis médical.

Quand consulter en urgence
Rendez‑vous aux urgences ou appelez le 112/15 si apparition de l’un des signes suivants :
- Difficultés respiratoires ou essoufflement important.  
- Saturation en O2 < 94 % (si oxymètre).  
- Douleur ou oppression thoracique.  
- Confusion, somnolence anormale, incapacité à boire.  
- Fièvre très élevée (>40 °C) ou fièvre qui s’aggrave malgré antipyrétiques, ou persistance >72 h sans amélioration.

Quand voir son médecin
- Si vous êtes âgé(e) > 65 ans, immunodéprimé(e), enceinte, ou avez une maladie chronique (diabète, BPCO, insuffisance cardiaque, etc.), contactez votre médecin rapidement.  
- Si pas d’amélioration sous 48–72 h ou symptôme qui s’aggrave (essoufflement, expectorations purulentes, fièvre persistante).

Questions pour affiner le diagnostic (répondez si vous pouvez)
1) Quel âge avez‑vous et avez‑vous des antécédents (diabète, cœur, poumons, immunosuppression, etc.) ?  
2) Avez‑vous pris paracétamol/ibuprofène et la fièvre a‑t‑elle baissé ?  
3) Avez‑vous des courbatures, frissons, perte d’odorat/goût, mal de gorge ou nez bouché ?  
4) La toux est‑elle strictement sèche ? y a‑t‑il des crachats (couleur) ?  
5) Avez‑vous fait un test COVID et quel est le résultat ? Êtes‑vous vacciné(e) contre la grippe/COVID récemment ?  
6) Les symptômes s’améliorent, stagnent ou s’aggravent depuis 3 jours ?

Si vous répondez à ces questions je préciserai la probabilité des diagnostics et vous indiquerai les examens ou traitements à envisager. Si vous avez déjà un signe de gravité, consultez sans délai.

Reading the result: actual execution of the simulated consultation

The output above is a real consultation (5 gpt-5-mini calls counted in the HTTP logs). The Début de la consultation médicale IA log opens the trace, then the demo clinical case seeds the group history. The student observes:

  1. The round-robin rotation of the agents — the Selected agent at index 0/1/2 logs show the default selection_strategy passing the baton: Docteur_Humain → IA_Medicale → Pharmacien → Docteur_Humain → IA_Medicale (5 agent turns).
  2. Auto function calling in action — from its very first turn, the doctor invokes 2 plugins in parallel (doctor-ask_followup_questions + medical-check_symptom_severity): FunctionChoiceBehavior.Auto() really executes, it is not a description.
  3. Termination by exchange limit — 1 patient message + 5 agent replies = 6 messages: MedicalTerminationStrategy ends the consultation (Consultation terminée.). Detecting a final diagnosis by keyword did not happen: that is the exercise left to the student (DiagnosticTerminationStrategy, stub above).

This replayable trace is what makes the notebook pedagogical: the student can re-read the async sequence to understand the agent-selection mechanism, instead of reading a black box.

What we built

This use case illustrates several advanced Semantic Kernel concepts:

Concept Implementation
Specialized agents Three distinct roles (Doctor, Medical AI, Pharmacist) with dedicated system instructions
Plugins @kernel_function Each agent has a plugin with specific functions (follow-up questions, severity assessment, medication recommendations)
AgentGroupChat Multi-agent orchestration with automatic handoff between the three participants
Termination strategy MedicalTerminationStrategy stops the dialogue after 6 exchanges to avoid infinite loops
FunctionChoiceBehavior.Auto() The Medical AI agent can automatically call kernel plugins to enrich its responses

Key points to remember

  1. Separation of responsibilities: each agent has a specific role, its own instructions, and its own tools. This modularity makes it easier to debug and evolve the system.

  2. Plugins as tools: @kernel_function exposes capabilities that agents can invoke via FunctionChoiceBehavior.Auto(). The LLM decides when and which plugin to call based on the context.

  3. Control strategies: the TerminationStrategy is essential in an AgentGroupChat to prevent agents from conversing indefinitely. Other strategies exist (agent selection, message filtering).

Going further

  • Add a simulated Patient agent that describes symptoms realistically
  • Implement a persistent consultation history (storage in a database)
  • Use a vector model to search for similar medical cases in a knowledge base
  • Add ethical guardrails (refusal to diagnose certain conditions, systematic referral to a healthcare professional)
Retour au sommet