
Les guides sur les agents IA produisent des programmes qui s'utilisent en ligne de commande — pratique pour développer, inutilisable pour un collègue non technique. Chainlit comble ce manque : c'est une bibliothèque Python open source qui donne à un modèle ou à un agent une interface web de chat en quelques lignes, sans écrire une seule ligne de frontend. Vous construirez une application de chat branchée sur un modèle servi par Ollama, avec réponse en streaming et mémoire de conversation. Au passage : le cycle de vie d'une session, le stockage par utilisateur et la protection par mot de passe. Public visé : développeur Python à l'aise avec async/await, qui veut exposer un assistant IA derrière une vraie interface.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Installer Chainlit et lancer une première application web.
- Réagir aux événements d'une conversation avec les décorateurs de cycle de vie.
- Afficher une réponse token par token avec le streaming.
- Garder le fil d'un échange grâce à la mémoire de session.
- Brancher un agent à la place d'un simple modèle.
- Restreindre l'accès avec l'authentification par mot de passe.
Prérequis
Section intitulée « Prérequis »- Python 3.10+ et l'aise avec la programmation asynchrone (
async/await). - Une instance Ollama avec le modèle
qwen2.5. - Avoir lu Comprendre les agents IA aide à situer Chainlit, sans être obligatoire.
Ce que Chainlit prend en charge à votre place
Section intitulée « Ce que Chainlit prend en charge à votre place »Écrire une interface de chat à la main, c'est gérer un serveur web, un protocole temps réel pour le streaming, l'état de chaque connexion, le rendu Markdown, l'affichage du code. Beaucoup de plomberie avant la première réponse affichée.
Chainlit absorbe toute cette couche. Vous écrivez des fonctions Python décorées ; la bibliothèque fournit l'interface web responsive, le streaming, le rendu Markdown et code, l'historique visuel et même une authentification prête à brancher. Votre code ne s'occupe que d'une chose : recevoir un message, produire une réponse.
C'est ce découpage qui rend Chainlit pertinent pour la section agentique. Les guides précédents ont construit la logique d'un agent — la boucle, les outils, l'état. Chainlit fournit la façade par laquelle un utilisateur s'en sert. Les deux se branchent en un point unique, que ce guide identifie précisément.
Installer Chainlit et lancer une première application
Section intitulée « Installer Chainlit et lancer une première application »Chainlit s'installe comme n'importe quel paquet Python. Travaillez toujours dans un environnement virtuel pour isoler les dépendances du projet.
python3 -m venv .venvsource .venv/bin/activatepip install chainlit==2.11.1Vérifiez l'installation — la commande doit afficher un numéro de version :
chainlit --versionCréez un fichier app.py avec l'application minimale : une fonction qui renvoie le message reçu.
import chainlit as cl
@cl.on_messageasync def repondre(message: cl.Message): await cl.Message(content=f"Vous avez écrit : {message.content}").send()Lancez le serveur de développement :
chainlit run app.py -wL'interface s'ouvre sur http://localhost:8000. L'option -w (watch) active le rechargement automatique : à chaque sauvegarde du fichier, le serveur se met à jour sans redémarrage manuel. Cette première application ne fait qu'un écho, mais l'interface — champ de saisie, fil de discussion, rendu Markdown — est déjà complète.
Le cycle de vie d'une conversation
Section intitulée « Le cycle de vie d'une conversation »Une conversation Chainlit traverse des étapes, et la bibliothèque expose un décorateur pour chacune. Vous n'implémentez que celles dont vous avez besoin.
| Décorateur | Quand il s'exécute |
|---|---|
@cl.on_chat_start | À l'ouverture d'une session, avant tout message |
@cl.on_message | À chaque message envoyé par l'utilisateur |
@cl.on_stop | Quand l'utilisateur clique sur « stop » pendant une génération |
@cl.on_chat_end | À la fermeture de la session ou au démarrage d'un nouveau chat |
Le couple le plus utilisé est on_chat_start et on_message. Le premier prépare le terrain — typiquement, initialiser l'historique de conversation. Le second traite chaque message. Les deux sont des fonctions asynchrones : Chainlit les exécute dans une boucle d'événements, ce qui permet de servir plusieurs utilisateurs en parallèle sans les bloquer les uns les autres.
Répondre en streaming
Section intitulée « Répondre en streaming »Faire patienter l'utilisateur devant un écran figé pendant que le modèle génère sa réponse donne une impression de lenteur. Le streaming affiche la réponse au fur et à mesure de sa production — la même expérience que les assistants grand public.
Le principe tient en trois temps. On crée un message vide, on le remplit token par token avec stream_token(), puis on le fige avec update().
import chainlit as clfrom openai import AsyncOpenAI
# Ollama expose une API compatible OpenAI sur /v1. La clé est factice :# Ollama ne la vérifie pas, mais le client OpenAI exige un champ non vide.CLIENT = AsyncOpenAI(base_url="http://localhost:11434/v1", api_key="ollama")MODELE = "qwen2.5"
@cl.on_messageasync def repondre(message: cl.Message): reponse = cl.Message(content="") flux = await CLIENT.chat.completions.create( model=MODELE, messages=[{"role": "user", "content": message.content}], stream=True, ) async for fragment in flux: token = fragment.choices[0].delta.content or "" if token: await reponse.stream_token(token) await reponse.update()Deux points méritent attention. On interroge Ollama via son API compatible OpenAI : le client AsyncOpenAI fonctionne tel quel, il suffit de pointer base_url sur Ollama. Et stream=True transforme la réponse en un flux asynchrone de fragments, que la boucle async for consomme à mesure qu'ils arrivent. Cette application répond, mais elle a un défaut majeur : elle oublie tout entre deux messages.
Garder le fil : la mémoire de session
Section intitulée « Garder le fil : la mémoire de session »Sans mémoire, chaque question est traitée isolément — l'assistant ne peut pas tenir compte de ce qui précède. Pour conserver le fil, il faut accumuler l'historique et le renvoyer au modèle à chaque tour.
Chainlit fournit pour cela cl.user_session : un stockage propre à chaque connexion. Deux utilisateurs sur la même application n'y partagent rien — chacun a son historique. On l'initialise dans on_chat_start, on le lit et le complète dans on_message.
SYSTEME = ( "Tu es un assistant technique concis. Tu réponds en français, " "avec des exemples concrets quand c'est utile.")
@cl.on_chat_startasync def demarrer(): cl.user_session.set("historique", [{"role": "system", "content": SYSTEME}]) await cl.Message(content="Bonjour. Posez votre question technique.").send()
@cl.on_messageasync def repondre(message: cl.Message): historique = cl.user_session.get("historique") historique.append({"role": "user", "content": message.content})
reponse = cl.Message(content="") flux = await CLIENT.chat.completions.create( model=MODELE, messages=historique, stream=True ) async for fragment in flux: token = fragment.choices[0].delta.content or "" if token: await reponse.stream_token(token) await reponse.update()
# On conserve la réponse pour que le tour suivant garde le contexte. historique.append({"role": "assistant", "content": reponse.content})La logique est désormais complète. Le message système fixe le rôle de l'assistant à l'ouverture. Chaque question et chaque réponse sont ajoutées à l'historique, transmis intégralement au modèle au tour suivant. L'assistant suit le contexte : on peut enchaîner « et en Python ? » après une première question, il comprend de quoi on parle.
Brancher un agent à la place du modèle
Section intitulée « Brancher un agent à la place du modèle »L'application interroge ici un modèle brut. Or le point clé est que @cl.on_message n'impose rien sur la façon de produire la réponse : tout ce qui prend une question et renvoie un texte convient. C'est l'unique point d'intégration entre Chainlit et la logique des guides précédents.
Pour brancher un agent PydanticAI — celui du guide sur les agents typés —, on remplace l'appel au modèle par un appel à l'agent :
@cl.on_messageasync def repondre(message: cl.Message): resultat = await agent.run(message.content) await cl.Message(content=str(resultat.output)).send()Le même principe vaut pour un graphe LangGraph (await graphe.ainvoke(...)) ou pour la boucle d'un agent écrite à la main. Chainlit ne s'occupe que de l'interface ; la logique métier — outils, mémoire, validation — reste dans l'agent, testée séparément. Cette séparation nette est ce qui rend l'ensemble maintenable : on fait évoluer l'agent sans toucher à l'interface, et l'inverse.
Restreindre l'accès avec l'authentification
Section intitulée « Restreindre l'accès avec l'authentification »Une application Chainlit est publique par défaut : quiconque connaît l'URL y accède. Dès qu'un assistant touche à des données internes, il faut une authentification.
La méthode la plus simple est le mot de passe. Chainlit a besoin d'un secret pour signer les jetons de session — générez-le une fois et placez-le dans une variable d'environnement :
chainlit create-secret# copiez la valeur produite dans CHAINLIT_AUTH_SECRETexport CHAINLIT_AUTH_SECRET="la-valeur-générée"Ajoutez ensuite un callback décoré par @cl.password_auth_callback. Il reçoit l'identifiant et le mot de passe saisis, et renvoie un objet cl.User si la vérification réussit, None sinon.
@cl.password_auth_callbackdef verifier(identifiant: str, mot_de_passe: str): if identifiant == "equipe" and mot_de_passe == attendu(): return cl.User(identifier=identifiant) return NoneUne fois le callback en place, Chainlit affiche un écran de connexion avant tout accès. L'utilisateur authentifié est ensuite disponible dans la session via cl.user_session.get("user").
Personnaliser l'accueil et l'apparence
Section intitulée « Personnaliser l'accueil et l'apparence »Au premier lancement, Chainlit crée un fichier chainlit.md à la racine du projet : son contenu s'affiche comme écran d'accueil. Le vider masque cet écran ; le remplir permet d'expliquer à l'utilisateur ce que l'assistant sait faire et ses limites.
La configuration plus fine — nom de l'application, thème de couleurs, logo — passe par le fichier .chainlit/config.toml, lui aussi généré automatiquement. Pour un premier projet, les valeurs par défaut conviennent : l'effort se concentre d'abord sur la qualité des réponses, l'habillage vient ensuite.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
command not found: chainlit | Environnement virtuel non activé | source .venv/bin/activate puis réinstaller |
| L'interface reste vide après un message | Réponse non envoyée | Vérifier l'appel à .send() ou .update() |
| L'assistant oublie le contexte | Historique non conservé | Initialiser et réutiliser cl.user_session |
Connection refused sur le port 11434 | Ollama n'est pas démarré | Lancer ollama serve et vérifier le modèle |
| L'écran de connexion ne s'affiche pas | CHAINLIT_AUTH_SECRET absent | Générer le secret et l'exporter avant chainlit run |
| Les modifications ne sont pas prises en compte | Serveur lancé sans rechargement | Relancer avec chainlit run app.py -w |
À retenir
Section intitulée « À retenir »- Chainlit donne à un modèle ou à un agent une interface web de chat sans écrire de frontend.
- Les décorateurs de cycle de vie —
on_chat_start,on_message— structurent une conversation. - Le streaming affiche la réponse token par token : message vide,
stream_token(),update(). cl.user_sessiongarde l'historique par connexion — indispensable pour suivre le contexte, mais volatile.@cl.on_messageest l'unique point d'intégration : on y branche un modèle brut ou n'importe quel agent.- L'application est publique par défaut ;
@cl.password_auth_callbacketCHAINLIT_AUTH_SECRETla protègent.
Prochaines étapes
Section intitulée « Prochaines étapes »Pour aller plus loin
Section intitulée « Pour aller plus loin »- Documentation Chainlit — la référence de la bibliothèque, dont l'authentification et la persistance.
- Cycle de vie d'une conversation — le détail de chaque décorateur.