Aller au contenu
Développement medium

Chainlit : une interface web pour vos agents IA

14 min de lecture

logo chainlit

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.

  • 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.
  • 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.

É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.

Fenêtre de terminal
python3 -m venv .venv
source .venv/bin/activate
pip install chainlit==2.11.1

Vérifiez l'installation — la commande doit afficher un numéro de version :

Fenêtre de terminal
chainlit --version

Créez un fichier app.py avec l'application minimale : une fonction qui renvoie le message reçu.

import chainlit as cl
@cl.on_message
async def repondre(message: cl.Message):
await cl.Message(content=f"Vous avez écrit : {message.content}").send()

Lancez le serveur de développement :

Fenêtre de terminal
chainlit run app.py -w

L'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.

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écorateurQuand 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_stopQuand 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.

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 cl
from 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_message
async 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.

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_start
async def demarrer():
cl.user_session.set("historique", [{"role": "system", "content": SYSTEME}])
await cl.Message(content="Bonjour. Posez votre question technique.").send()
@cl.on_message
async 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.

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_message
async 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.

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 :

Fenêtre de terminal
chainlit create-secret
# copiez la valeur produite dans CHAINLIT_AUTH_SECRET
export 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_callback
def verifier(identifiant: str, mot_de_passe: str):
if identifiant == "equipe" and mot_de_passe == attendu():
return cl.User(identifier=identifiant)
return None

Une 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").

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.

SymptômeCause probableSolution
command not found: chainlitEnvironnement virtuel non activésource .venv/bin/activate puis réinstaller
L'interface reste vide après un messageRéponse non envoyéeVérifier l'appel à .send() ou .update()
L'assistant oublie le contexteHistorique non conservéInitialiser et réutiliser cl.user_session
Connection refused sur le port 11434Ollama n'est pas démarréLancer ollama serve et vérifier le modèle
L'écran de connexion ne s'affiche pasCHAINLIT_AUTH_SECRET absentGénérer le secret et l'exporter avant chainlit run
Les modifications ne sont pas prises en compteServeur lancé sans rechargementRelancer avec chainlit run app.py -w
  • Chainlit donne à un modèle ou à un agent une interface web de chat sans écrire de frontend.
  • Les décorateurs de cycle de vieon_chat_start, on_message — structurent une conversation.
  • Le streaming affiche la réponse token par token : message vide, stream_token(), update().
  • cl.user_session garde l'historique par connexion — indispensable pour suivre le contexte, mais volatile.
  • @cl.on_message est 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_callback et CHAINLIT_AUTH_SECRET la protègent.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn