Prise en main de Chainlit
Mise à jour :
Chainlit est une bibliothèque Python open-source conçue pour créer facilement des interfaces web conversationnelles autour des LLMs comme GPT, Mistral ou Llama. Son objectif ? Permettre aux développeurs de concevoir des chatbots ou assistants interactifs sans se soucier du frontend. En quelques lignes de code, vous lancez une application web responsive, avec support natif du streaming, d’actions personnalisées, d’authentification OAuth, et même d’intégration avec des frameworks comme LangChain ou LlamaIndex. Chainlit est donc une solution idéale pour tester, prototyper, ou même déployer en production des assistants IA avec une interface moderne et réactive.
Installation de Chainlit
Chainlit est conçu pour être simple à installer et rapide à démarrer. En quelques commandes, vous pouvez lancer une application conversationnelle fonctionnelle avec interface web.
Commencez par créer un environnement virtuel Python pour isoler votre projet :
python -m venv venvsource venv/bin/activate # Sur Windows, utilisez `venv\Scripts\activate`
Installez la bibliothèque avec pip :
pip install chainlit
Pendant l’installation, Chainlit va télécharger toutes les dépendances nécessaires, y compris les bibliothèques pour le support du streaming et des actions interactives, mais aussi les outils pour l’interface web.
Vérifiez que la commande chainlit
est bien disponible :
chainlit --help
Normalement, vous devriez voir l’aide de Chainlit s’afficher, indiquant que l’installation s’est bien déroulée. Mais aussi la création de deux dossiers importants :
.chainlit/
: contient le fichier de configuration et les fichiers de traduction..files/
: utilisé pour stocker les fichiers uploadés par les utilisateurs.chainlit.md
: le fichier de documentation de Chainlit.
Premier script minimal
Créez un fichier app.py
avec ce contenu :
import chainlit as cl
@cl.on_chat_startasync def start(): await cl.Message(content="Bienvenue !").send()
Lancez ensuite l’application avec :
chainlit run app.py -w
Vous devriez voir un message de bienvenue s’afficher dans votre navigateur à
l’adresse http://localhost:8000
comme dans l’aperçu ci-dessous :
Concepts clés
Chainlit repose sur un modèle d’événements asynchrones. Il fournit une série de décorateurs Python qui vous permettent de réagir aux actions de l’utilisateur et de structurer le comportement de votre application conversationnelle.
Le cycle de vie d’une session utilisateur
Voici les principaux événements gérés par Chainlit :
@cl.on_chat_start
: appelé à l’ouverture d’une nouvelle session ou au rechargement.@cl.on_message
: déclenché à chaque message utilisateur.@cl.on_stop
: optionnel, déclenché lors de l’arrêt de la session.@cl.action_callback
: pour gérer les actions interactives (boutons).- …
Exemple de base :
import chainlit as cl
@cl.on_chat_startasync def start(): await cl.Message(content="Bienvenue !").send()
@cl.on_messageasync def handle_message(message: cl.Message): await cl.Message(content=f"Vous avez dit : {message.content}").send()
Si vous lancez ce code avec chainlit run app.py
, vous verrez un message de
Les objets fournis par Chainlit
Les fonctions déclenchées par les hooks reçoivent ou manipulent différents objets natifs. Voici les principaux :
cl.Message
: représente un message dans la conversation. Vous pouvez envoyer du texte, des actions, des fichiers, etc.cl.Action
: représente une action interactive (bouton) que l’utilisateur peut cliquer. Chaque action doit avoir unname
, unvalue
et unlabel
.cl.UserSession
: permet de stocker des données persistantes spécifiques à l’utilisateur, comme son nom ou ses préférences.
Exemple d’utilisation des actions :
import chainlit as cl
@cl.on_chat_startasync def start(): await cl.Message(content="Bienvenue ! Tapez 'Bonjour' pour commencer.").send()
@cl.on_messageasync def handle_message(message: cl.Message): if message.content == "Bonjour": # Créer des actions avec la syntaxe correcte (payload requis) actions = [ cl.Action(name="help", value="help", label="🆘 Aide", payload={"action": "help"}), cl.Action(name="bye", value="bye", label="👋 Au revoir", payload={"action": "bye"}) ]
await cl.Message( content="Comment puis-je vous aider ?", actions=actions ).send() elif message.content == "Aide": await cl.Message(content="Voici comment je peux vous aider...\n- Tapez 'Bonjour' pour commencer\n- Tapez 'Aide' pour cette aide\n- Tapez 'Au revoir' pour quitter").send() elif message.content == "Au revoir": await cl.Message(content="À bientôt ! Merci d'avoir utilisé cette application.").send() else: await cl.Message(content="Je ne comprends pas votre message. Tapez 'Bonjour' pour commencer ou 'Aide' pour obtenir de l'aide.").send()
@cl.action_callback("help")async def on_help_action(action: cl.Action): await cl.Message(content="Voici comment je peux vous aider...\n- Tapez 'Bonjour' pour commencer\n- Cliquez sur 'Aide' pour cette aide\n- Cliquez sur 'Au revoir' pour quitter").send()
@cl.action_callback("bye")async def on_bye_action(action: cl.Action): await cl.Message(content="À bientôt ! Merci d'avoir utilisé cette application.").send()
Quelques explications sur le code ci-dessus :
- Quand l’utilisateur tape “Bonjour”, l’application envoie un message avec deux actions : “Aide” et “Au revoir”.
- Si l’utilisateur clique sur “Aide”, l’application répond avec un message détaillant les options disponibles.
- Si l’utilisateur clique sur “Au revoir”, l’application envoie un message de remerciement et de fin de session (fictif).
Authentification dans Chainlit
Par défaut, une application Chainlit est publique, accessible à tous. Pour restreindre l’accès, il est possible d’activer trois méthodes d’authentification : mot de passe, header HTTP, et OAuth. Avant tout, commencez par définir une clé secrète pour signer les tokens utilisateur :
chainlit create-secret
Cette clé permet de sécuriser les sessions : la modifier déconnectera
automatiquement tous les utilisateurs. Vous devez la définir dans votre fichier
.env
.
CHAINLIT_AUTH_SECRET="la_clé_secrète_que_vous_avez_générée"
N’oubliez pas d’ajouter ce fichier .env
à votre .gitignore
pour ne pas le
partager publiquement.
La méthode la plus simple consiste à utiliser un mot de passe. Vous pouvez
configurer un callback @cl.password_auth_callback
qui sera appelé à chaque
connexion. Ce callback doit vérifier les identifiants et retourner un objet
cl.User
si la connexion est réussie, ou None
en cas d’échec.
import chainlit as clfrom typing import Optional
@cl.password_auth_callbackdef auth_callback(username: str, password: str) -> Optional[cl.User]: # Vérification des identifiants (exemple simple) # En production, utilisez une base de données et des mots de passe hachés if (username, password) == ("admin", "admin"): return cl.User( identifier="admin", metadata={"role": "admin", "provider": "credentials"} ) elif (username, password) == ("user", "user"): return cl.User( identifier="user", metadata={"role": "user", "provider": "credentials"} ) else: return None
@cl.on_chat_startasync def start(): # Récupérer l'utilisateur connecté user = cl.user_session.get("user") if user: username = user.identifier await cl.Message( content=f"Bienvenue {username} ! Tapez 'Bonjour' pour commencer." ).send() else: await cl.Message(content="Bienvenue ! Tapez 'Bonjour' pour commencer.").send()
@cl.on_messageasync def handle_message(message: cl.Message): if message.content == "Bonjour": # Créer des actions avec la syntaxe correcte (payload requis) actions = [ cl.Action(name="help", value="help", label="🆘 Aide", payload={"action": "help"}), cl.Action(name="bye", value="bye", label="👋 Au revoir", payload={"action": "bye"}) ]
await cl.Message( content="Comment puis-je vous aider ?", actions=actions ).send() elif message.content == "Aide": await cl.Message(content="Voici comment je peux vous aider...\n- Tapez 'Bonjour' pour commencer\n- Tapez 'Aide' pour cette aide\n- Tapez 'Au revoir' pour quitter").send() elif message.content == "Au revoir": await cl.Message(content="À bientôt ! Merci d'avoir utilisé cette application.").send() else: await cl.Message(content="Je ne comprends pas votre message. Tapez 'Bonjour' pour commencer ou 'Aide' pour obtenir de l'aide.").send()
@cl.action_callback("help")async def on_help_action(action: cl.Action): await cl.Message(content="Voici comment je peux vous aider...\n- Tapez 'Bonjour' pour commencer\n- Cliquez sur 'Aide' pour cette aide\n- Cliquez sur 'Au revoir' pour quitter").send()
@cl.action_callback("bye")async def on_bye_action(action: cl.Action): await cl.Message(content="À bientôt ! Merci d'avoir utilisé cette application.").send()
-
Si la fonction retourne
None
➜ échec de connexion. -
Sinon, un objet
cl.User
est associé à la session, accessible viacl.user_session.get("user")
. -
Vous pouvez personnaliser l’objet
cl.User
avec des métadonnées (rôle, provider, etc.) pour gérer les permissions ou personnaliser l’expérience utilisateur.
Plus d’informations sur l’authentification sont disponibles dans la documentation officielle de Chainlit ↗.
Personnalisation de l’interface Chainlit
Avec Chainlit, vous pouvez transformer une interface générique en application à l’image de votre entreprise. Cela inclut l’apparence visuelle, les comportements dynamiques, les avatars, ainsi que la traduction des éléments d’interface. Toutes les personnalisations se font sans modifier le cœur de la bibliothèque, en déposant simplement des fichiers dans des répertoires dédiés.
Configuration de l’interface
Pour personnaliser l’interface, modifiez le fichier config.toml
dans le
dossier .chainlit/
. Vous pouvez y définir des options globales comme le nom du
projet, la description, et les paramètres d’interface utilisateur. Voici un
exemple de configuration :
[UI]name = "Ton assistant"
default_theme = "light"
Cette configuration permet de définir le nom de l’application et le thème par défaut. Vous pouvez aussi choisir entre les thèmes “light” et “dark”.
Langue de l’interface
Pour ajouter une langue spécifique, créez un fichier de traduction dans le
dossier .chainlit/translations/
. Par exemple, pour le français, créez un
fichier fr-FR.json
:
{ "common": { "actions": { "cancel": "Annuler", "confirm": "Confirmer", "continue": "Continuer", "goBack": "Retour", "reset": "Réinitialiser", "submit": "Soumettre" }, "status": { "loading": "Chargement...", "error": { "default": "Une erreur s'est produite", "serverConnection": "Impossible de contacter le serveur" } } }, "auth": { "login": { "title": "Connectez-vous pour accéder à l'application", "form": { "email": { "label": "Adresse e-mail", "required": "l'e-mail est un champ obligatoire" }, "password": { "label": "Mot de passe", "required": "le mot de passe est un champ obligatoire" }, "actions": { "signin": "Se connecter" }, "alternativeText": { "or": "OU" } }, "errors": { "default": "Impossible de se connecter", "signin": "Essayez de vous connecter avec un compte différent", "oauthSignin": "Essayez de vous connecter avec un compte différent", "redirectUriMismatch": "L'URI de redirection ne correspond pas à la configuration de l'application OAuth", "oauthCallback": "Essayez de vous connecter avec un compte différent", "oauthCreateAccount": "Essayez de vous connecter avec un compte différent", "emailCreateAccount": "Essayez de vous connecter avec un compte différent", "callback": "Essayez de vous connecter avec un compte différent", "oauthAccountNotLinked": "Pour confirmer votre identité, connectez-vous avec le même compte que vous avez utilisé initialement", "emailSignin": "L'e-mail n'a pas pu être envoyé", "emailVerify": "Veuillez vérifier votre e-mail, un nouvel e-mail a été envoyé", "credentialsSignin": "Échec de la connexion. Vérifiez que les détails fournis sont corrects", "sessionRequired": "Veuillez vous connecter pour accéder à cette page" } }, "provider": { "continue": "Continuer avec {{provider}}" } }, "chat": { "input": { "placeholder": "Tapez votre message ici...", "actions": { "send": "Envoyer le message", "stop": "Arrêter la tâche", "attachFiles": "Joindre des fichiers" } }, "speech": { "start": "Commencer l'enregistrement", "stop": "Arrêter l'enregistrement", "connecting": "Connexion en cours" }, "fileUpload": { "dragDrop": "Glisser-déposer les fichiers ici", "browse": "Parcourir les fichiers", "sizeLimit": "Limite :", "errors": { "failed": "Échec du téléchargement", "cancelled": "Téléchargement annulé de" } }, "messages": { "status": { "using": "Utilise", "used": "Utilisé" }, "actions": { "copy": { "button": "Copier dans le presse-papiers", "success": "Copié !" } }, "feedback": { "positive": "Utile", "negative": "Pas utile", "edit": "Modifier le commentaire", "dialog": { "title": "Ajouter un commentaire", "submit": "Soumettre le commentaire" }, "status": { "updating": "Mise à jour", "updated": "Commentaire mis à jour" } } }, "history": { "title": "Dernières saisies", "empty": "Rien ici...", "show": "Afficher l'historique" }, "settings": { "title": "Panneau de paramètres" }, "watermark": "Développé avec" }, "threadHistory": { "sidebar": { "title": "Conversations précédentes", "filters": { "search": "Rechercher", "placeholder": "Rechercher des conversations..." }, "timeframes": { "today": "Aujourd'hui", "yesterday": "Hier", "previous7days": "7 derniers jours", "previous30days": "30 derniers jours" }, "empty": "Aucune conversation trouvée", "actions": { "close": "Fermer la barre latérale", "open": "Ouvrir la barre latérale" } }, "thread": { "untitled": "Conversation sans titre", "menu": { "rename": "Renommer", "delete": "Supprimer" }, "actions": { "delete": { "title": "Confirmer la suppression", "description": "Cela supprimera la conversation ainsi que ses messages et éléments. Cette action ne peut pas être annulée", "success": "Conversation supprimée", "inProgress": "Suppression de la conversation" }, "rename": { "title": "Renommer la conversation", "description": "Saisissez un nouveau nom pour cette conversation", "form": { "name": { "label": "Nom", "placeholder": "Saisissez le nouveau nom" } }, "success": "Conversation renommée !", "inProgress": "Renommage de la conversation" } } } }, "navigation": { "header": { "chat": "Chat", "readme": "Lisez-moi", "theme": { "light": "Thème clair", "dark": "Thème sombre", "system": "Suivre le système" } }, "newChat": { "button": "Nouvelle conversation", "dialog": { "title": "Créer une nouvelle conversation", "description": "Cela effacera votre historique de conversation actuel. Êtes-vous sûr de vouloir continuer ?", "tooltip": "Nouvelle conversation" } }, "user": { "menu": { "settings": "Paramètres", "settingsKey": "S", "apiKeys": "Clés API", "logout": "Se déconnecter" } } }, "apiKeys": { "title": "Clés API requises", "description": "Pour utiliser cette application, les clés API suivantes sont requises. Les clés sont stockées dans le stockage local de votre appareil.", "success": { "saved": "Enregistré avec succès" } }, "alerts": { "info": "Info", "note": "Note", "tip": "Astuce", "important": "Important", "warning": "Avertissement", "caution": "Attention", "debug": "Débogage", "example": "Exemple", "success": "Succès", "help": "Aide", "idea": "Idée", "pending": "En attente", "security": "Sécurité", "beta": "Bêta", "best-practice": "Bonne pratique" }}
Si vous lancez l’application avec cette traduction, l’interface sera affichée en
français. Vous pouvez ajouter d’autres langues en créant des fichiers de
traduction similaires, par exemple es-ES.json
pour l’espagnol.
Créer un fichier d’aide personnalisé
Pour ajouter un fichier d’aide personnalisé, éditez le fichier chainlit.md
présent à la racine de votre projet. Ce fichier sera affiché dans l’onglet
“Documentation” de l’interface. Vous pouvez y inclure des instructions, des
guides d’utilisation, ou toute autre information utile pour les utilisateurs de
votre application.
Un exemple de contenu pour chainlit.md
:
# Bienvenue dans l'application Chainlit
Cette application est conçue pour vous aider à interagir avec notre assistant IA.
## Comment utiliser l'application
1. Tapez votre message dans la zone de saisie en bas de l'écran.2. Cliquez sur "Envoyer" ou appuyez sur Entrée pour envoyer votre message.3. Vous pouvez également utiliser les boutons d'action pour interagir avec l'assistant.
Logo, favicon et visuel de connexion
Pour intégrer votre identité visuelle, déposez les fichiers suivants dans le
dossier public/
:
logo_light.png
etlogo_dark.png
: adaptés au thème clair ou sombre.favicon.ico
,.png
ou.svg
: affiché dans l’onglet du navigateur.
Vous pouvez aussi ajouter une image de fond pour la page de connexion :
[UI]login_page_image = "/public/custom-bg.png"login_page_image_filter = "brightness-50 grayscale"login_page_image_dark_filter = "contrast-200 blur-sm"
Le filtre CSS applique un effet visuel directement dans l’interface (grayscale, sepia, blur, etc.).
Thème personnalisé
Le fichier theme.json
dans le dossier public/
vous permet de redéfinir
l’apparence globale de Chainlit. Exemple :
{ "custom_fonts": ["https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"], "variables": { "light": { "--primary": "220 90% 56%", "--background": "0 0% 100%", "--foreground": "0 0% 10%" }, "dark": { "--primary": "220 90% 56%", "--background": "0 0% 10%", "--foreground": "0 0% 90%" } }}
Ce fichier permet de définir :
- des polices personnalisées (Google Fonts, etc.)
- des variables CSS pour les couleurs principales, arrière-plan, texte, etc.
Pour un contrôle total, ajoutez un fichier CSS et référencez-le dans
chainlit.toml
:
[UI]custom_css = "/public/style.css"
Ce fichier vous permet de redéfinir les bordures, polices, marges, tailles d’éléments, ou masquer certains composants. Vous pouvez, par exemple, supprimer le champ “README” ou changer la disposition des messages.
#theme-toggle,#new-chat-button { display: none !important;}
.watermark { display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; height: 0px !important; width: 0px !important; overflow: hidden !important;}
#chat-input:empty::before { content: 'Demandez-moi n’importe quoi';}
Dans cet exemple, nous masquons le bouton de changement de thème et le bouton de nouvelle conversation, ainsi que le watermark. Nous modifions aussi le placeholder du champ de saisie pour afficher “Ask anything” au lieu de “Type your message here…”.
Astuce : utilisez l’inspecteur de votre navigateur pour identifier les sélecteurs CSS à surcharger.
JavaScript personnalisé
Vous pouvez injecter du JavaScript dans l’interface :
[UI]custom_js = "/public/script.js"
Cela permet d’ajouter :
- du tracking analytics (Google Tag, Matomo…)
- des interactions client dynamiques
- des appels à des APIs externes
- ou des événements personnalisés (logique métier, switch UI, etc.)
Avatars personnalisés
Pour remplacer les avatars par défaut :
- Déposez vos images dans
public/avatars/
- Nommez les fichiers selon le nom de l’auteur du message, ex.
my_bot.png
pourname="My Bot"
Chainlit détectera automatiquement le bon avatar dans les conversations.
Intégration avec LangChain
Associer LangChain à Chainlit permet de bâtir des assistants IA puissants, capables de suivre une logique conversationnelle avancée tout en offrant une interface utilisateur réactive et interactive.
LangChain fournit la logique métier et le chaînage des outils LLM, tandis que Chainlit sert d’interface frontale permettant :
- d’afficher les messages utilisateurs et IA,
- de visualiser les étapes internes des chaînes,
- de gérer les interactions UI (boutons, fichiers, sliders…),
- de streamer la réponse au fil de son élaboration.
Cette complémentarité vous permet de créer des assistants bien plus interactifs qu’une simple API ou CLI.
Prérequis
Avant de commencer, assurez-vous d’avoir installé les bibliothèques. Pour cela,
créer le fichier requirements.txt
avec le contenu suivant :
# Chainlit - Framework de chat UIchainlit>=2.6.2
# OpenAI et LangChainopenai>=1.0.0langchain>=0.1.0langchain-openai>=0.1.0langchain-community>=0.1.0
Installez-les avec :
pip install -r requirements.txt
Ajoutez dans le fichier .env
la clé de votre API OpenAI :
OPENAI_API_KEY=your_api_key_here
Voici un exemple simple d’intégration de LangChain avec Chainlit pour créer un chatbot capable de répondre à des questions sur un sujet donné :
import chainlit as clfrom typing import Dict, Optionalfrom langchain_openai import ChatOpenAIfrom langchain.prompts import ChatPromptTemplatefrom langchain.schema import StrOutputParserfrom langchain.schema.runnable import Runnablefrom langchain.schema.runnable.config import RunnableConfigfrom typing import cast
@cl.oauth_callbackdef oauth_callback( provider_id: str, token: str, raw_user_data: Dict[str, str], default_user: cl.User,) -> Optional[cl.User]: # Autoriser tous les utilisateurs qui ont passé l'authentification GitHub if provider_id == "github": return default_user return None
@cl.on_chat_startasync def on_chat_start(): # Récupérer l'utilisateur connecté user = cl.user_session.get("user") if user: username = user.identifier provider = user.metadata.get("provider", "inconnu") welcome_message = f"Bienvenue {username} ! Vous êtes connecté via {provider}." else: welcome_message = "Bienvenue !"
# Initialiser le modèle OpenAI avec streaming model = ChatOpenAI( model="gpt-3.5-turbo", temperature=0.7, streaming=True )
# Créer le prompt template prompt = ChatPromptTemplate.from_messages([ ( "system", "Vous êtes un assistant IA très compétent et serviable. " "Répondez de manière claire, précise et utile aux questions des utilisateurs. " "Utilisez un ton amical et professionnel." ), ("human", "{question}"), ])
# Créer la chaîne runnable runnable = prompt | model | StrOutputParser()
# Stocker la chaîne dans la session utilisateur cl.user_session.set("runnable", runnable)
# Envoyer le message de bienvenue await cl.Message(content=f"{welcome_message}\n\nJe suis prêt à répondre à vos questions !").send()
@cl.on_messageasync def on_message(message: cl.Message): # Récupérer la chaîne runnable de la session runnable = cast(Runnable, cl.user_session.get("runnable"))
# Créer un message vide qui sera rempli progressivement msg = cl.Message(content="")
try: # Utiliser le streaming pour une réponse progressive async for chunk in runnable.astream( {"question": message.content}, config=RunnableConfig(callbacks=[cl.LangchainCallbackHandler()]), ): await msg.stream_token(chunk)
# Envoyer le message final await msg.send()
except Exception as e: # Gérer les erreurs error_message = f"Désolé, une erreur s'est produite : {str(e)}" await cl.Message(content=error_message).send()
Ce code crée un assistant IA qui répond aux questions des utilisateurs en utilisant un modèle OpenAI. Il utilise le streaming pour envoyer la réponse au fur et à mesure qu’elle est générée, offrant ainsi une expérience utilisateur fluide et interactive.
Conclusion
Ce guide est une introduction complète à Chainlit, conçue pour vous aider à prendre en main rapidement cette bibliothèque, de l’installation à l’intégration avec LangChain. Mais ce n’est que le début.
Dans les prochaines semaines, je publierai un tutoriel avancé basé sur ce socle, incluant :
- La mise en place de l’historique des conversations,
- une architecture complète avec RAG (Retrieval-Augmented Generation),
- l’ingestion de documents PDF/Markdown dans une base vectorielle (OpenSearch, PostgreSQL Vector, etc.),
- l’extraction contextuelle et le chaînage via LangChain,
- la persistance des sessions utilisateurs et des métadonnées,
- une interface Chainlit customisée, interactive et sécurisée.
L’objectif : créer un chatbot documenté et industrialisable, capable de répondre à des questions sur une base de connaissances métier, avec logs, permissions, et outils en production.