Chatbot de réservation en clinique avec LangGraph, SQLite et Streamlit, étape par étape
Créer un assistant de réservation de rendez-vous pour une clinique : tables SQLite, fonctions de service, un flux de travail LangGraph à neuf nœuds avec gpt-4o-mini, une interface de chat Streamlit et un mode notebook.
Sources: Agentic AI Project: Build a Customer Service Chatbot for a Clinic
Gérer une clinique exige une coordination fastidieuse : associer les patients aux médecins, suivre qui est disponible à quel moment et éviter les doubles réservations. Un assistant conversationnel peut prendre en charge cette partie liée aux rendez-vous. Ce projet en construit un à partir de quatre éléments : LangGraph pour gérer une conversation en plusieurs étapes comme une machine à états, gpt-4o-mini d’OpenAI pour comprendre ce que les gens tapent, SQLite pour stocker les données, et Streamlit pour la fenêtre de chat.
Le problème et le plan
De nombreuses cliniques continuent de compter sur des processus manuels ou un mélange hétéroclite d’outils. Les conséquences sont des délais longs, des conflits d’horaires, une charge de travail inégale pour le personnel et une expérience frustrante pour les patients, qui ont du mal à trouver un médecin disponible ou la spécialité adéquate. Ce dont a besoin une clinique, c’est d’un système automatisé qui prend les rendez-vous grâce à une conversation naturelle tout en conservant des registres précis des médecins, des patients et des réservations.
Le chatbot guide l’utilisateur à travers un bref script :
- Il salue l’utilisateur et lui demande s’il souhaite prendre un rendez-vous.
- Il énumère les spécialités médicales disponibles.
- Il affiche le médecin ainsi que les créneaux libres.
- Il collecte le nom et le numéro de téléphone du patient.
- Il confirme la réservation et l’enregistre dans la base de données.
Cela permet de réduire la charge administrative, d’accélérer les réservations, d’éviter les conflits de rendez-vous et d’offrir une expérience plus fluide aux patients. Le code final est organisé en dossiers pour les données, les services, l’agent et l’interface utilisateur. Chaque composant a une fonction bien définie : LangGraph décide de l’étape suivante de la conversation, le modèle interprète les textes libres, SQLite enregistre qui et quand, tandis que Streamlit affiche tout cela à l’utilisateur dans un navigateur. En séparant ces fonctions, il est possible de modifier une partie, par exemple l’interface, sans affecter le reste.
Mise en place
Créez un environnement virtuel. La commande est la même sous Mac, Linux et Windows :
python -m venv .venv
Activez-le sur Mac ou Linux :
source .venv/bin/activate
Au Windows avec PowerShell :
.venv\Scripts\Activate.ps1
Dans l’invite de commandes Windows :
.venv\Scripts\activate
Lorsqu’il est actif, l’invite de terminal commence par le nom de l’environnement, comme ceci :
(.venv) your-folder-name %
Viennent ensuite les dépendances. Le fichier requirements.txt indique Streamlit, python-dotenv, pydantic, pandas ainsi que les autres bibliothèques nécessaires :
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
Installez-les depuis le dossier du projet :
cd clinic-agent
pip install -r requirements.txt
Finalement, créez un fichier .env dans le répertoire clinic-agent contenant votre clé OpenAI :
OPENAI_API_KEY=your_openai_api_key_here
Partie 1 : la base de données (data/db.py)
Un système de réservation repose sur sa couche de données. La clinique utilise SQLite avec trois tables pour les médecins, les clients et les réservations. Le schéma est relationnel et facile à interroger au cours d’une conversation : une réservation fait référence à un médecin et à un client, de sorte qu’une seule recherche vous indique qui voit qui et quand. Les numéros de téléphone servent d’identifiant pratique pour un patient, car un appelant régulier ne doit pas se retrouver avec un enregistrement dupliqué.
Médecins. Chaque ligne correspond à un médecin avec sa spécialité et ses horaires de travail, ce qui permet au chatbot de suggérer la bonne personne une fois que la spécialité a été choisie. Les champs sont doctor_id (identifiant unique), doctor_name, speciality (comme Dermatologue ou Pédiatre) et office_timing (les heures pendant lesquelles le médecin travaille). La démonstration couvre cinq spécialités : Dermatologue, Orthopédiste, Médecin généraliste, Pédiatre et Spécialiste ORL, avec des noms et des horaires fictifs pour chacune. Ajouter d’autres spécialités est très simple.
Clients. Ce tableau contient les informations nécessaires pour effectuer une réservation. Chaque fois que quelqu’un réserve, le système recherche le client par numéro de téléphone et crée un nouveau dossier s’il n’en existe pas déjà. Les champs sont customer_id, name et phone.
Réservations. Le tableau central. Chaque ligne relie un médecin et un client à une date et heure précises. Ses champs sont booking_id, doctor_id et customer_id (références aux deux autres tableaux), appointment_date, appointment_time et status, tel que Confirmé ou Annulé.
Le script définit ces tableaux et crée le fichier clinic.db :
# 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.")
Exécutez-le depuis le dossier du projet :
cd clinic-agent
python data/db.py
Cela génère clinic.db contenant les trois tables. Relancer le script est un moyen rapide de réinitialiser les données d’exemple lors des expérimentations.
Partie 2 : la couche de service
Les services contiennent la logique métier entre l’agent et la base de données. L’agent LangGraph ne consulte pas directement les tables ; il appelle des fonctions de service pour obtenir des informations sur les médecins, générer des créneaux horaires et confirmer les réservations. Dans une conception d’agent, ces fonctions agissent comme des outils, permettant à l’agent de se concentrer sur le déroulement de la conversation. Cette séparation permet également de tester la logique indépendamment. Vous pouvez vérifier la génération des créneaux et les réservations sans jamais appeler le modèle de langage, ce qui est précisément ce que fait le petit script suivant.
Le service de médecins (services/doctor_service.py) gère les spécialités, les médecins et les créneaux horaires. Il récupère la liste des spécialités depuis la base de données, obtient les médecins correspondant à une spécialité choisie, crée des créneaux à partir des horaires de consultation d’un médecin, et convertit les heures affichées comme 14:00 en format 24 heures pour le stockage.
# 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}"
Le service de réservation (services/booking_service.py) gère les clients et les réservations. Il trouve un client existant par numéro de téléphone ou en crée un nouveau, consulte les créneaux déjà pris pour un médecin à une date donnée, les filtre pour ne conserver que les créneaux libres, génère un identifiant de réservation unique et enregistre les détails finaux du rendez-vous : médecin, patient, date et heure.
# 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
Vérification des services
Un petit script met à l’épreuve les deux services :
# 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}")
Exécutez-le :
cd clinic-agent
python test_service.py
La console affiche, entre autres, la liste des spécialités et les créneaux disponibles pour un médecin :
(.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
Si tout cela semble correct, que les services fonctionnent bien, alors la partie intéressante peut commencer. Voir des spécialités réelles et des créneaux réels dans les résultats confirme que la base de données, la logique médicale et la logique de réservation sont en accord entre elles avant que toute logique de conversation ne soit ajoutée par-dessus.
Partie 3 : l’agent
Le cœur du chatbot est un agent LangGraph qui fait avancer une réservation à travers plusieurs étapes.
État de la réservation
L’objet d’état constitue une mémoire structurée pour la conversation. Il enregistre l’étape actuelle, comme la salutation ou le choix d’un médecin, mémorise les choix tels que la spécialité, le médecin et le créneau, et conserve les détails du client et de la réservation afin que l’agent puisse finaliser le rendez-vous.
# 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": []
}
L’aide LLM
Une seule fonction call_llm centralise la communication avec le modèle OpenAI. Elle envoie les prompts système et utilisateur, renvoie le texte généré et assure que chaque appel au modèle reste cohérent et réutilisable.
# 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
Chaque nœud représente une étape dans le flux de travail de réservation. Ils gèrent l’étape qu’ils représentent, lisent les entrées de l’utilisateur et mettent à jour l’état, ce qui rend le flux de travail modulaire. Il y en a neuf :
greeting_nodeouvre la conversation et demande à l’utilisateur s’il souhaite prendre un rendez-vous.select_speciality_nodeliste les spécialités proposées.select_doctor_nodeaffiche les médecins de la spécialité choisie.select_date_nodedemande quel jour convient à l’utilisateur.select_slot_nodepropose les créneaux libres de la journée.confirm_nodedemande à l’utilisateur d’approuver son choix.collect_details_nodedemande un nom et un numéro de téléphone.completed_nodeclôture une réservation réussie.
cancelled_node gère le cas d’un utilisateur qui se retire.Construction du graphe
La fonction build_booking_graph ajoute ces nœuds, définit les transitions conditionnelles entre eux et détermine ainsi le parcours global de la conversation.
# 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()
Un petit script affiche le graphique sous forme d’image :
#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()
Exécutez-le :
cd clinic-agent
python save_langgraph_flow.py
Celui-ci enregistre le diagramme dans le dossier agents, ce qui constitue un moyen pratique de visualiser d’un coup d’œil les étapes et leurs connexions avant de les confier à des utilisateurs réels.
Traitement des messages
process_message reçoit les données depuis l’interface, les fait passer par le graphe, et renvoie l’état mis à jour ainsi que la réponse de l’assistant. Il accepte un identifiant de thread, avec un nom de session par défaut, afin que chaque session de navigateur ou exécution de notebook dispose de son propre suivi enregistré au sein du flux de réservation.
# 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
Un script de test pilote l’agent depuis la terminale :
# 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"])
Exécutez-le :
cd clinic-agent
python test/test_agent.py
Cela devrait afficher les options disponibles, ce qui montre que le graphique répond. À ce stade, toute la conversation fonctionne depuis le terminal, il ne manque que la présentation.
Partie 4 : l’interface Streamlit
L’interface utilisateur propose une fenêtre de chat en temps réel, des boutons d’option cliquables et des sessions identifiées par des IDs uniques. Les boutons sont importants dans un tel flux, car choisir une spécialité ou un créneau horaire d’un seul clic est plus rapide et moins sujet aux erreurs que de le taper manuellement.
# 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 : le point d’entrée
app.py est le point de départ qui lance toute l’application :
# app.py - Main entry point for the application
from ui.chat_ui import run_chat_ui
if __name__ == "__main__":
run_chat_ui()
Deux manières de l’exécuter
En tant qu’application web Streamlit, ce qui représente la méthode la plus simple :
streamlit run app.py
Il s’ouvre à http://localhost:8501.
Dans un notebook Jupyter, qui convient au développement et aux expérimentations. Importez d’abord les fonctions :
# 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
)
Créez le graphe et affichez sa structure :
# 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))
Puis lancez une session de chat interactive à l’intérieur du cahier :
# 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)
L’aide de cahier de notes lance une session de réservation sur un thread nommé et peut être réinitialisée pour recommencer. Dans le cahier de notes, la boîte de saisie apparaît en haut, au-dessus des cellules. Le dépôt contient un cahier de notes clinic-agent.ipynb qui exécute l’ensemble du processus.
Ce que le bot final vous offre
Le résultat est un chatbot de réservation développé à partir de LangGraph, OpenAI et SQLite, avec une interface Streamlit. Un flux de travail piloté par l’état guide les utilisateurs depuis le choix d’une spécialité jusqu’à la confirmation d’un créneau, et les différentes couches (base de données, services, logique de l’agent et interface utilisateur) restent séparées, ce qui rend l’application modulaire et facile à étendre. Comme ces couches sont clairement distinctes, chaque amélioration ne concerne qu’une d’entre elles. Parmi les prochaines étapes possibles figurent l’intégration avec un calendrier, des notifications, le soutien multilingue et des plateformes de messagerie. C’est un exemple concret de la combinaison de frameworks d’IA modernes et d’outils web simples pour créer une application utile dans le domaine de la santé.