Accueil / Articles / Notes pratiques : Projet d’IA agente : Créer un chatbot de service client pour

Notes pratiques : Projet d’IA agente : Créer un chatbot de service client pour

Guide pas à pas fonctionnel des notes pratiques : Projet d’IA agente – Créer un chatbot de service client pour les contrats, les virements et des espaces de code prêts à l’emploi destinés aux équipes utilisant ce modèle.

5381 mots

Les notes suivantes reconstituent une approche pratique pour le « Projet d’IA agente : Créer un chatbot de service client pour une clinique ». L’accent est mis sur les contrats, les vérifications et les placeholders de code à insérer, plutôt que sur une présentation motivante. Lors de la phase d’aperçu, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Documentez à la fois le parcours normal et les scénarios de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure.

Introduction

La phase d’introduction fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Recueillez un transcript exemplaire, un cas d’échec et la note de réversion avant d’élargir le périmètre. Préférez des unités petites et testables aux scripts complexes. Lorsqu’une étape échoue, l’échec doit pointer vers une seule responsabilité plutôt que vers un processus embrouillé. Maintenez l’état des graphes simple et typé. Les blocs imbriqués masquent le fait que tel nœud a écrit telle champ et perturbent la reprise après interruption.

Énoncé du problème

La phase d’énoncé du problème fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Recueillez un transcript exemplaire, un cas d’échec et la note de réversion avant d’élargir le périmètre. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des vérifications de succès et refusez toute complétion partielle silencieuse. Maintenez l’état du graphe plat et typé. Les blocs imbriqués masquent le fait que tel nœud a écrit tel champ et perturbent la reprise après interruption.

Solution

La phase de Solution fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe de la démonstration aux environnements partagés. Maintenez l’état du graphe plat et typé. Les blocs imbriqués masquent le fait que tel nœud a écrit telle champ et perturbent la reprise après interruption. La phase de Solution fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Documentez ensemble le parcours réussi et le parcours de récupération. Les tentatives répétées, les contrôles humains et le traitement des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure.

Mise en place

Pendant l’étape de configuration, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Préférez des unités petites et testables aux scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Faites approuver par un humain les étapes qui entraînent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas la complétude du processus métier.

Mac / Linux / Windows

Pour l’étape Mac/Linux/Windows, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des vérifications de succès et refusez les terminations partielles silencieuses. Faites approuver par un humain les cas où de l’argent est dépensé ou où des données de production sont modifiées. La connexion en temps de compilation ne revient pas à une complétude opérationnelle.

python -m venv .venv
source .venv/bin/activate
.venv\Scripts\Activate.ps1
.venv\Scripts\activate
(.venv) your-folder-name %
streamlit>=1.50.0
python-dotenv==1.0.0
pydantic==2.12.5
pandas==2.3.3
python-dateutil==2.8.2
langgraph>=1.0.7
openai>=2.16.0
pygraphviz==1.14
cd clinic-agent
pip install -r requirements.txt
OPENAI_API_KEY=your_openai_api_key_here

Partie 1 : Configuration de la base de données (data/db.py)

Pour l’étape 1 de configuration de la base de données, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Imposez une approbation humaine pour les actions qui entraînent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas une complétude fonctionnelle pour l’entreprise. Pour l’étape 1 de configuration de la base de données, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez conjointement le parcours normal et les scénarios de récupération. Les tentatives de réexécution, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’améliorations apportées ultérieurement.

1. Table des médecins

Lors de la phase 1 « Table des médecins », notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Préférez des unités petites et testables plutôt que des scripts complexes. Lorsqu’une étape échoue, l’échec doit pointer vers une seule responsabilité et non vers un processus embrouillé. Faites des points de contrôle après les étapes coûteuses. Le système de reprise ne doit pas facturer à nouveau la même appel d’LLM lorsque l’opérateur réessaie un nœud ultérieur.

2. Table des clients

Lors de l’étape relative à la table des 2 clients, notez d’abord le contrat : les données requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Considérez cette étape comme un contrat entre les données d’entrée et les résultats validés. Donnez des noms aux éléments générés, définez des vérifications de succès, et refusez toute exécution partielle silencieuse. Faites un point après les étapes coûteuses. Le système de reprise ne doit pas facturer à nouveau la même appel du LLM lorsque l’opérateur réessaie un nœud ultérieur.

3. Table des réservations

Lors de la réalisation des 3 étapes liées à la table de réservation, notez d’abord les conditions du contrat : entrées requises, signal de succès et conséquences en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe de l’environnement de démonstration à des environnements partagés. Instaurez un point de contrôle après chaque étape coûteuse. Le système de reprise ne doit pas facturer à nouveau la même appel du LLM lorsque l’opérateur réessaie un nœud ultérieur. Lors de la réalisation des 3 étapes liées à la table de réservation, notez d’abord les conditions du contrat : entrées requises, signal de succès et conséquences en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Documentez conjointement le parcours normal et les scénarios de récupération. Les tentatives de réessai, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations apportées ultérieurement.

4. Création de la base de données

La phase 4 « Création de la base de données » fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un exemplaire idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Préférez des unités petites et testables à des scripts complexes. Lorsqu’une étape échoue, l’erreur doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Maintenez l’état du graphe simple et typé. Les blocs imbriqués masquent le fait que tel nœud a écrit tel champ et perturbent la reprise après interruption.

# data/db.py - Database initialization and operations

import sqlite3
import os
from datetime import datetime, timedelta

DB_PATH = os.path.join(os.path.dirname(__file__), "clinic.db")

def get_connection():
    """Get a database connection."""
    return sqlite3.connect(DB_PATH)

def init_db():
    """Initialize the database with tables and sample data."""
    conn = get_connection()
    cursor = conn.cursor()

    # Doctors table
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS doctors (
            doctor_id TEXT PRIMARY KEY,
            doctor_name TEXT NOT NULL,
            speciality TEXT NOT NULL,
            office_timing TEXT NOT NULL
        )
    """)

    # Customers table
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS customers (
            customer_id TEXT PRIMARY KEY,
            name TEXT NOT NULL,
            phone TEXT NOT NULL
        )
    """)

    # Bookings table
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS bookings (
            booking_id TEXT PRIMARY KEY,
            doctor_id TEXT NOT NULL,
            customer_id TEXT NOT NULL,
            appointment_date TEXT NOT NULL,
            appointment_time TEXT NOT NULL,
            status TEXT NOT NULL,
            FOREIGN KEY (doctor_id) REFERENCES doctors (doctor_id),
            FOREIGN KEY (customer_id) REFERENCES customers (customer_id)
        )
    """)

    # Insert sample doctors
    doctors = [
        ("D1", "Dr. Anil Sharma", "General Physician", "10:00-14:00"),
        ("D2", "Dr. Neha Verma", "Dermatologist", "11:00-16:00"),
        ("D3", "Dr. Rohit Mehta", "Orthopedic", "09:00-13:00"),
        ("D4", "Dr. Kavita Rao", "Pediatrician", "10:00-15:00"),
        ("D5", "Dr. Sanjay Iyer", "ENT Specialist", "12:00-17:00"),
    ]

    for doctor in doctors:
        cursor.execute(
            "INSERT OR IGNORE INTO doctors (doctor_id, doctor_name, speciality, office_timing) VALUES (?, ?, ?, ?)",
            doctor
        )

    conn.commit()
    conn.close()

if __name__ == "__main__":
    print("Initializing database...")
    init_db()
    print("Database initialized successfully.")
cd clinic-agent
python data/db.py

Partie 2 : Couche de service (Outils pour l’agent)

La phase de la couche de service, partie 2, fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des vérifications de succès et refusez toute mise en œuvre partielle silencieuse. Exposez des outils dotés de schémas restreints et de labels explicites indiquant les effets secondaires. Les hôtes doivent savoir quels appels modifient l’état avant d’approuver automatiquement.

Service Doctor (services/doctor_service.py)

Le service Doctor Service fonctionne le mieux lorsqu’il est considéré comme une surface mesurable. Capturez un enregistrement idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Notez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe de l’environnement de démonstration à des environnements partagés. Gardez l’état du graphique simple et bien typé ; les blocs imbriqués masquent l’identité du nœud qui a modifié tel champ et perturbent la reprise après interruption. Le service Doctor Service fonctionne le mieux lorsqu’il est considéré comme une surface mesurable. Capturez un enregistrement idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Documentez ensemble le parcours réussi et le parcours de récupération. Les tentatives de réessai, les contrôles humains et le traitement des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure.

# services/doctor_service.py - Doctor operations

from data.db import get_all_doctors, get_doctor_by_speciality, get_doctor_by_id

def get_specialities_list():
    """Get list of all specialities."""
    doctors = get_all_doctors()
    # Return unique specialities
    return list(dict.fromkeys([doc[2] for doc in doctors]))

def get_doctor_info(speciality):
    """Get doctor information by speciality."""
    doctor = get_doctor_by_speciality(speciality)
    if doctor:
        return {
            "doctor_id": doctor[0],
            "doctor_name": doctor[1],
            "speciality": doctor[2],
            "office_timing": doctor[3]
        }
    return None

def generate_time_slots(office_timing):
    """Generate hourly time slots from office timing string.

    Args:
        office_timing: String like "11:00-16:00"

    Returns:
        List of time slots like ["11:00 AM", "12:00 PM", ...]
    """
    start_time, end_time = office_timing.split("-")
    start_hour = int(start_time.split(":")[0])
    end_hour = int(end_time.split(":")[0])

    slots = []
    for hour in range(start_hour, end_hour):
        if hour < 12:
            suffix = "AM"
            display_hour = hour if hour > 0 else 12
        elif hour == 12:
            suffix = "PM"
            display_hour = 12
        else:
            suffix = "PM"
            display_hour = hour - 12
        slots.append(f"{display_hour}:00 {suffix}")

    return slots

def parse_time_slot(slot_str):
    """Parse time slot string to 24-hour format.

    Args:
        slot_str: String like "1:00 PM"

    Returns:
        String like "13:00"
    """
    time_part, suffix = slot_str.split(" ")
    hour, minute = time_part.split(":")
    hour = int(hour)

    if suffix == "PM" and hour != 12:
        hour += 12
    elif suffix == "AM" and hour == 12:
        hour = 0

    return f"{hour:02d}:{minute}"

Service de réservation (services/booking_service.py)

Pour l’étape bookingservice du Service de réservation, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Préférez des unités petites et testables à des scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Faites approuver par un humain les étapes qui engagent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas la complétude du processus métier.

# services/booking_service.py - Booking operations

import uuid
from datetime import datetime
from data.db import (
    create_customer,
    create_booking,
    get_customer_by_phone,
    get_bookings_by_doctor_and_date,
    get_booking_by_id
)
from services.doctor_service import parse_time_slot

def get_or_create_customer(name, phone):
    """Get existing customer or create new one."""
    customer = get_customer_by_phone(phone)
    if customer:
        return customer[0]  # Return customer_id

    customer_id = f"CUST-{uuid.uuid4().hex[:6].upper()}"
    create_customer(customer_id, name, phone)
    return customer_id

def get_available_slots(doctor_id, office_timing):
    """Get available time slots for a doctor for today.

    Args:
        doctor_id: Doctor ID
        office_timing: Office timing string like "11:00-16:00"

    Returns:
        List of available time slots
    """
    from services.doctor_service import generate_time_slots

    today = datetime.now().strftime("%Y-%m-%d")
    all_slots = generate_time_slots(office_timing)

    # Get booked slots
    booked_times = get_bookings_by_doctor_and_date(doctor_id, today)

    # Filter out booked slots
    available = []
    for slot in all_slots:
        slot_24h = parse_time_slot(slot)
        if slot_24h not in booked_times:
            available.append(slot)

    return available

def confirm_booking(doctor_id, customer_name, customer_phone, time_slot, appointment_date=None):
    """Confirm a booking.

    Args:
        doctor_id: Doctor ID
        customer_name: Customer name
        customer_phone: Customer phone
        time_slot: Time slot like "1:00 PM"
        appointment_date: Optional date in YYYY-MM-DD format. Defaults to today.

    Returns:
        Booking ID
    """
    # Get or create customer
    customer_id = get_or_create_customer(customer_name, customer_phone)

    # Generate booking ID
    booking_id = f"BKG-{uuid.uuid4().hex[:6].upper()}"

    # Format appointment time
    if not appointment_date:
        appointment_date = datetime.now().strftime("%Y-%m-%d")
    appointment_time = parse_time_slot(time_slot)

    # Create booking
    create_booking(booking_id, doctor_id, customer_id, appointment_date, appointment_time)

    return booking_id

Testage du service

Pendant la phase de test du service, définissez les entrées, le responsable de l’étape et les critères d’achèvement avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu, sans avoir à deviner l’état caché. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des vérifications de succès et refusez les terminaisons partielles silencieuses. Faites approuver par un humain les cas où de l’argent est dépensé ou où des données de production sont modifiées. La connexion en temps de compilation ne revient pas à une complétude opérationnelle.

# test/test_service.py - to test the services created

from pathlib import Path
import sys

# Allow running this file directly: `python test/test_service.py`.
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from services.doctor_service import get_specialities_list, generate_time_slots
from services.booking_service import confirm_booking

# Get all specialities
specialities = get_specialities_list()
print("Available specialities:", specialities)

# Generate time slots for a doctor (11:00 AM - 4:00 PM)
slots = generate_time_slots("11:00-16:00")
print("Available slots:", slots)

# Confirm a booking
booking_id = confirm_booking(
    doctor_id="D1",
    customer_name="John Doe",
    customer_phone="9876543210",
    time_slot="2:00 PM"
)
print(f"Booking confirmed: {booking_id}")
cd clinic-agent
python test_service.py
(.venv) (base) my-mac clinic-agent % python test_service.py
Available specialities: ['General Physician', 'Dermatologist', 'Orthopedic', 'Pediatrician', 'ENT Specialist']
Available slots: ['11:00 AM', '12:00 PM', '1:00 PM', '2:00 PM', '3:00 PM']
Booking confirmed: BKG-C4F60A

Partie 3 : Couche agente

Pour l’étape de la couche agente de la Partie 3, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût en tokens ou en requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Faites approuver par un humain les étapes qui entraînent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas une complétude fonctionnelle pour l’entreprise. Pour l’étape de la couche agente de la Partie 3, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez conjointement le parcours idéal et le parcours de récupération. Les tentatives de réexécution, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure.

État de la réservation :

Lors du traitement de l’étape concernant l’état de la réservation, notez d’abord les éléments requis pour le contrat : les données nécessaires, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de maintenir l’honnêteté des modifications ultérieures du code. Préférez des unités petites et testables plutôt que des scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité et non un processus embrouillé. Instaurez des points de contrôle après les étapes coûteuses. Le mécanisme de reprise ne doit pas facturer à nouveau la même appel du LLM lorsque l’opérateur réessaie un nœud ultérieur.

# agents/booking_agent.py - LangGraph agent implementation

from typing import TypedDict, Annotated, List, Optional
from langgraph.graph import StateGraph, END
from openai import OpenAI
import os
from dotenv import load_dotenv

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

class BookingState(TypedDict):
    """State for the booking conversation."""
    messages: List[dict]                    # Chat history
    stage: str                               # greeting, select_speciality, select_doctor, etc.
    selected_speciality: Optional[str]      # Chosen medical specialty
    selected_doctor: Optional[dict]         # Selected doctor details
    selected_date: Optional[str]            # Appointment date
    selected_slot: Optional[str]            # Time slot
    customer_name: Optional[str]            # Customer name
    customer_phone: Optional[str]           # Customer phone
    booking_id: Optional[str]               # Confirmation ID
    available_options: List[str]            # UI options

def create_initial_state():
    """Create initial state for the conversation."""
    return {
        "messages": [],
        "stage": "greeting",
        "selected_speciality": None,
        "selected_doctor": None,
        "selected_date": None,
        "selected_slot": None,
        "customer_name": None,
        "customer_phone": None,
        "booking_id": None,
        "available_options": []
    }

Fonction d’aide du LLM

Lors de la phase relative à la fonction d’aide pour les LLM, notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des vérifications de succès, et refusez les terminations partielles silencieuses. Cachez les instructions système stables ainsi que les schémas des outils. L’envoi répété d’un préambule identique est une cause fréquente de gaspillage.

# agents/booking_agent.py - LangGraph agent implementation
def call_llm(
    system_prompt: str,
    user_prompt: str,
    *,
    model: str = "gpt-4o-mini",
    temperature: float = 0,
    max_tokens: int = 50,
) -> str:
    """
    Centralized helper for all LLM calls.
    Returns the assistant's response
    """
    try:
        response = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt},
            ],
            temperature=temperature,
            max_tokens=max_tokens,
        )
        return response
    except Exception as e:
        print(f"LLM call error: {e}")
        return ""

Nœuds d’agent

Lors de la phase des nœuds d’agent, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications de code ultérieures. Enregistrez les temps d’exécution ainsi que le coût en tokens ou en requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite des factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Créez un point de contrôle après chaque étape coûteuse. La reprise du processus ne doit pas facturer à nouveau la même appel de LLM lorsque l’opérateur réessaie un nœud ultérieur. Lors de la phase des nœuds d’agent, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications de code ultérieures. Documentez en même temps le parcours optimal et les procédures de récupération. Les tentatives de réessai, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations apportées ultérieurement.

Construction du graphe

La phase de construction du graphe fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript exemplaire, un cas d’échec et la note de réversion avant d’élargir le périmètre. Préférez des unités petites et testables aux scripts complexes. Lorsqu’une étape échoue, l’erreur doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Maintenez l’état du graphe simple et typé. Les blocs imbriqués masquent le fait que tel nœud a écrit telle champ et perturbent la reprise après interruption.

# agents/booking_agent.py - LangGraph agent implementation
from langgraph.checkpoint.memory import MemorySaver

def build_booking_graph():
    """Build the LangGraph workflow."""
    workflow = StateGraph(BookingState)

    # Add all nodes
    workflow.add_node("greeting", greeting_node)
    workflow.add_node("select_speciality", select_speciality_node)
    workflow.add_node("select_doctor", select_doctor_node)
    workflow.add_node("select_date", select_date_node)
    workflow.add_node("select_slot", select_slot_node)
    workflow.add_node("confirm", confirm_node)
    workflow.add_node("collect_details", collect_details_node)
    workflow.add_node("completed", completed_node)
    workflow.add_node("cancelled", cancelled_node)

    # Set entry point
    workflow.set_entry_point("greeting")

    # Add conditional edges based on routing
    workflow.add_conditional_edges(
        "greeting",
        llm_router,
        {
            "greeting": "greeting",
            "select_speciality": "select_speciality",
            "cancelled": "cancelled"
        }
    )

    # Similar conditional edges for other nodes...

    # Final edges to END
    workflow.add_edge("completed", END)
    workflow.add_edge("cancelled", END)

    # Compile with checkpointer for session management
    return workflow.compile(checkpointer=MemorySaver())

# Create the compiled graph
booking_graph = build_booking_graph()
#agents/save_langgraph_flow.py
"""Save the clinic booking LangGraph flow to png format in this folder."""


from pathlib import Path
import sys


# Ensure imports work whether the script is run from project root or this folder.
AGENTS_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = AGENTS_DIR.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from agents.booking_agent import booking_graph  # noqa: E402


def save_graph_files() -> None:
    """Export graph as PNG."""
    graph = booking_graph.get_graph()

    png_path = AGENTS_DIR / "langgraph_flow.png"
    png_data = graph.draw_mermaid_png()
    png_path.write_bytes(png_data)
    print(f"Saved PNG flow to: {png_path}")


if __name__ == "__main__":
    save_graph_files()
cd clinic-agent
python save_langgraph_flow.py

Traiter le message :

La phase « Message du processus » fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des vérifications de succès et refusez toute complétion partielle silencieuse. Maintenez l’état du graphe plat et typé. Les blocs imbriqués masquent le fait que tel nœud a écrit telle champ et perturbent la reprise après interruption.

# agents/booking_agent.py - LangGraph agent implementation
def process_message(state: BookingState, user_message: str, thread_id: str = "default_session") -> BookingState:
    """Process a user message through the booking graph."""
    config = {"configurable": {"thread_id": thread_id}}

    # Check if the graph is currently interrupted
    current_state = booking_graph.get_state(config)

    if current_state.tasks and current_state.tasks[0].interrupts:
        # Resume the graph with the user's message
        result = booking_graph.invoke(Command(resume=user_message), config=config)
    else:
        # No interrupt, so start/continue normally
        # Add user message to state (unless it's an initial trigger)
        if user_message.lower() != "hi" or state["messages"]:
            # Avoid duplicate user messages if already added
            if not state["messages"] or state["messages"][-1].get("content") != user_message:
                state["messages"].append({
                    "role": "user",
                    "content": user_message
                })
        # Run the graph
        result = booking_graph.invoke(state, config=config)

    # Update available_options and ensure message is in history
    snapshot = booking_graph.get_state(config)
    if snapshot.tasks and snapshot.tasks[0].interrupts:
        interrupt_value = snapshot.tasks[0].interrupts[0].value

        # Handle both dict and string interrupt values
        msg_content = ""
        options = []
        if isinstance(interrupt_value, dict):
            msg_content = interrupt_value.get("content", "")
            options = interrupt_value.get("available_options", [])
        else:
            msg_content = str(interrupt_value)

        # Ensure the interrupt message is in the chat history
        if msg_content:
            # Check if it was already added by the node
            last_msg_content = result["messages"][-1].get("content", "") if result["messages"] else ""
            if last_msg_content != msg_content:
                result["messages"].append({
                    "role": "assistant",
                    "content": msg_content,
                    "options": options
                })
            else:
                # If already added, just update it with options if missing
                result["messages"][-1]["options"] = options

        result["available_options"] = options
    else:
        # If not interrupted, use whatever set in state, or default to empty
        if "available_options" not in result:
            result["available_options"] = []

    return result

Test de l’agent

La phase de test de l’agent fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe de la démonstration aux environnements partagés. Maintenez l’état du graphe simple et typé. Les blocs imbriqués masquent le fait quel nœud a écrit quel champ et perturbent la reprise après interruption. La phase de test de l’agent fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Documentez ensemble le parcours réussi et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure.

# test/test_agent.py - test agent
# Initialize state

from pathlib import Path
import sys

# Allow running this file directly: `python test/test_agent.py`.
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
 sys.path.insert(0, str(PROJECT_ROOT))

from agents.booking_agent import create_initial_state, process_message


state = create_initial_state()

# Process messages
state = process_message(state, "Hi", thread_id="session_1")
print(state["messages"][-1]["content"])

state = process_message(state, "I want to book", thread_id="session_1")
print(state["available_options"])
cd clinic-agent
python test/test_agent.py

Partie 4 : Interface utilisateur Streamlit

Pour l’étape d’interface utilisateur Streamlit de la partie 4, définissez les entrées, le responsable de l’étape et les critères de sortie avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Préférez des unités petites et testables à des scripts volumineux. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un pipeline embrouillé. Faites approuver par des humains les étapes qui entraînent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas la complétude du processus métier.

# ui/chat_ui.py - Streamlit chatbot interface

"""Streamlit UI for the clinic booking chatbot."""

import streamlit as st
from agents.booking_agent import create_initial_state, process_message
from data.db import init_db


def initialize_session():
    """Initialize session state."""
    if "state" not in st.session_state:
        st.session_state.state = create_initial_state()
    if "initialized" not in st.session_state:
        st.session_state.initialized = False
    if "session_id" not in st.session_state:
        import uuid
        st.session_state.session_id = str(uuid.uuid4())


def display_chat_history():
    """Display the chat history with persistent options and styling."""
    messages = st.session_state.state.get("messages", [])
    for i, message in enumerate(messages):
        if message["role"] == "assistant":
            with st.chat_message("assistant"):
                st.markdown(message["content"])

                # Show options if they exist
                options = message.get("options", [])
                if options:
                    # If this is the last message in history, show as clickable buttons
                    if i == len(messages) - 1 and st.session_state.state["stage"] not in ["completed", "cancelled"]:
                        st.markdown("---")
                        # Create columns for buttons
                        cols = st.columns(min(len(options), 3))
                        for idx, option in enumerate(options):
                            col_idx = idx % 3
                            with cols[col_idx]:
                                if st.button(option, key=f"btn_{i}_{idx}", use_container_width=True):
                                    handle_user_input(option)
                    else:
                        # For older messages, show options as pills/text to keep history
                        options_str = "  ".join([f"`{opt}`" for opt in options])
                        st.markdown(f"**Available options:** {options_str}")
        else:
            with st.chat_message("user"):
                st.markdown(message["content"])


def handle_user_input(user_input: str):
    """Handle user input and process through agent."""
    # Process the message
    st.session_state.state = process_message(
        st.session_state.state,
        user_input,
        thread_id=st.session_state.session_id
    )

    # Rerun to update UI
    st.rerun()


def run_chat_ui():
    """Run the chat UI."""
    # Page config
    st.set_page_config(
        page_title="CarePlus Clinic - Book Appointment",
        page_icon="🏥",
        layout="centered"
    )

    # Custom CSS for distinction between messages
    st.markdown("""
        <style>
        [data-testid="stChatMessageUser"] {
            flex-direction: row-reverse;
            text-align: right;
            background-color: #e0f2f1;
            border-radius: 15px 15px 0px 15px;
        }
        [data-testid="stChatMessageAssistant"] {
            background-color: #f5f5f5;
            border-radius: 15px 15px 15px 0px;
        }
        </style>
    """, unsafe_allow_html=True)

    # Initialize database
    init_db()

    # Initialize session
    initialize_session()

    # Header
    st.title("🏥 CarePlus Clinic")
    st.markdown("*Book your doctor appointment easily*")
    st.markdown("---")

    # Send initial greeting if not initialized
    if not st.session_state.initialized:
        st.session_state.state = process_message(
            st.session_state.state,
            "Hi",
            thread_id=st.session_state.session_id
        )
        st.session_state.initialized = True
        st.rerun()

    # Display chat history
    display_chat_history()

    # Chat input (only show if not completed)
    if st.session_state.state["stage"] not in ["completed", "cancelled"]:
        if prompt := st.chat_input("Type your message here..."):
            handle_user_input(prompt)
    else:
        # Show restart button after completion
        st.markdown("---")
        if st.button("🔄 Start New Booking", use_container_width=True):
            st.session_state.state = create_initial_state()
            st.session_state.initialized = False
            st.rerun()

Partie 5 : Point d’entrée de l’application

Pour l’étape d’entrée de la demande, partie 5, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des vérifications de succès et refusez les terminaisons partielles silencieuses. Faites approuver par un humain les cas où de l’argent est dépensé ou où des données de production sont modifiées. La connexion en temps de compilation ne revient pas à une complétude opérationnelle.

# app.py - Main entry point for the application

from ui.chat_ui import run_chat_ui

if __name__ == "__main__":
    run_chat_ui()

Exécution du chatbot : deux approches

Pour la phase deux du fonctionnement du chatbot, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût en tokens ou en requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Mettez en place une approbation humaine pour les actions qui entraînent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas une complétude opérationnelle.

Méthode 1 : Application web Streamlit

Pour l’étape Web Streamlit de l’Approche 1, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les stocks de secrets et les flags fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans avoir à lire l’ensemble du schéma. Mettez en place une approbation humaine pour les étapes qui engendrent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas la complétude du processus métier.

streamlit run app.py

Approche 2 : Jupyter Notebook

Pour l’étape du notebook Jupyter d’Approach 2, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez conjointement le parcours normal et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’une mise en forme ultérieure. Imposez une approbation humaine pour les actions qui entraînent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas la complétude du processus métier.

# clinic-agent.ipynb
from services.doctor_service import get_specialities_list, get_doctor_info, generate_time_slots
from services.booking_service import confirm_booking
from agents.booking_agent import (
    BookingState,
    create_initial_state,
    build_booking_graph,
    process_message
)
# clinic-agent.ipynb
# Initialize the booking graph
booking_graph = build_booking_graph()

## Visualize the booking graph structure
from IPython.display import Image, display

png_bytes = booking_graph.get_graph().draw_mermaid_png()
display(Image(png_bytes))

Démarrer la session du chatbot :

Pour l’étape « Exécuter la session du chatbot », définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Préférez des unités petites et testables à des scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Faites approuver par un humain les actions qui entraînent des dépenses ou modifient des données de production. Une connexion en temps de compilation ne garantit pas la complétude du processus métier.

# clinic-agent.ipynb
from langgraph.types import Command

def run_booking_session(graph, thread_id="notebook_session", reset=False):
    config = {"configurable": {"thread_id": thread_id}}

    # 1. Start or Reset logic
    current_state = graph.get_state(config)
    if reset or not current_state.values:
        print(f"--- {'🔄 Resetting' if reset else '🆕 Initializing'} Session ---")
        # Using invoke() here kicks off the 'greeting' node immediately
        graph.invoke(create_initial_state(), config=config)

    print("---⚕⚕ Starting CarePlus Booking Session ---")

    last_displayed_message_idx = -1  # Track which messages have been displayed

    while True:
        state = graph.get_state(config)

        # Display any new assistant messages that haven't been shown yet
        # (This handles guardrail/off-topic responses)
        if state.values and state.values.get('messages'):
            messages = state.values['messages']
            for idx in range(last_displayed_message_idx + 1, len(messages)):
                msg = messages[idx]
                if msg.get("role") == "assistant":
                    print(f"\n[AI]: {msg['content']}")
            last_displayed_message_idx = len(messages) - 1

        # 2. Check for Interrupts
        if state.tasks and state.tasks[0].interrupts:
            interrupt_info = state.tasks[0].interrupts[0].value

            # --- FIX: Safely handle both String and Dict interrupts ---
            if isinstance(interrupt_info, dict):
                ai_message = interrupt_info.get('content', 'No message content')
                options = interrupt_info.get('available_options', [])
            else:
                ai_message = interrupt_info
                options = []

            print(f"\n[AI]: {ai_message}")
            if options:
                print(f"Options: {', '.join(options)}")
            # -------------------------------------------------------

            user_input = input("\n[YOU]: ")
            print(f"[YOU]: {user_input}")

            # Resume the graph with the user's input
            graph.invoke(Command(resume=user_input), config=config)

        # 3. Check if the graph has finished
        elif not state.next:
            # Before ending, check if there's a final assistant message to print
            if state.values and state.values.get('messages') and state.values['messages'][-1]["role"] == "assistant":
                if last_displayed_message_idx < len(state.values['messages']) - 1:
                    print(f"\n[AI]: {state.values['messages'][-1]['content']}")
            print("\n--- ⚑⚑ Session Ended ---")
            break

        # 4. If nodes are pending but no interrupt, let them run (the gas pedal)
        else:
            graph.invoke(None, config=config)

# IMPORTANT: Set reset=True only when you want to wipe the history.
# Set it to False to actually continue the conversation!
run_booking_session(booking_graph, reset=True)

Conclusion :

Pour l’étape de conclusion, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des vérifications de succès et refusez les terminaisons partielles silencieuses. Faites approuver par un humain les cas où de l’argent est dépensé ou où des données de production sont modifiées. La connexion en temps de compilation ne revient pas à une complétude opérationnelle.

Références :

Pour l’étape des références, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Faites approuver par un humain les étapes qui entraînent des dépenses ou modifient des données de production. La configuration en temps de compilation ne garantit pas la complétude du processus métier. Pour l’étape des références, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez ensemble le parcours idéal et le parcours de récupération. Les tentatives de répétition, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure.

Liste de contrôle opérationnelle

Pour l’étape de la liste de contrôle opérationnelle, définissez les entrées, le responsable de chaque étape ainsi que les critères d’achèvement avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché.

Gardez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système.

Mettez en place une approbation humaine pour les actions qui entraînent des dépenses ou modifient des données de production. Une connexion réalisée en temps de compilation ne garantit pas une couverture complète des besoins métier.

Rédigez un petit manuel d’utilisation : comment rotationner les clés, comment vider la file d’attente, comment annuler la dernière ingestion.

Dokumentez à la fois le parcours normal et les procédures de récupération. Les tentatives répétées, les contrôles humains et le traitement des messages échoués font partie intégrante du produit, et non d’améliorations ultérieures.

Apportez une validation humaine pour les étapes qui engagent des dépenses ou modifient les données de production. La connexion en temps de compilation ne garantit pas une couverture complète des besoins métier.

Au préalable de promouvoir la pile logicielle, figez les versions, conservez une transcription exemplaire pour le chemin critique, et vérifiez les étapes de réversion. Les environnements partagés nécessitent des limites de fréquence, des contrôles d’attribution et un responsable clair pour la rotation des secrets. Préférez une fiabilité banale à des démonstrations ingénieuses ponctuelles.

Note de batch pour 9744ef4a5b25 : gardez les clés du fournisseur hors du répertoire, fixez un plafond pour les tokens par session, et stockez les transcriptions à côté des fichiers de configuration d’évaluation afin que les remplacements ultérieurs de modèles restent comparables.