Aller au contenu principal

Module 4 — Interfaces conversationnelles

Le fil rouge bascule ici sur son troisième et dernier chantier, qui occupera tous les modules restants : un assistant conversationnel branché sur un modèle de langage. Cette bascule n'est pas cosmétique. Une interface conversationnelle change la nature de l'interaction : elle n'est plus « je téléverse, je reçois », mais « nous échangeons », avec un historique qui s'accumule, un contexte qui pèse et une latence qui se voit à chaque tour. gr.ChatInterface est le composant de plus haut niveau de Gradio, taillé pour ce cas d'usage.

Ce que gr.ChatInterface encapsule

En interne, gr.ChatInterface est un gr.Blocks préconstruit : un gr.Chatbot pour l'affichage des messages, un gr.Textbox pour la saisie, un bouton « Envoyer », un bouton « Réessayer » qui rejoue le dernier tour, un bouton « Annuler » qui supprime le dernier échange, et un bouton « Vider » qui remet l'historique à zéro. Toute cette machinerie tient dans deux lignes de code utilisateur, contre une bonne cinquantaine si l'on devait la reconstruire à la main avec Blocks.

La contrainte, en échange, porte sur la signature de votre fonction. gr.ChatInterface appelle la fonction avec deux arguments dans cet ordre : le message que l'utilisateur vient d'envoyer (chaîne) et l'historique de la conversation. L'historique est une liste de tuples [(message_utilisateur, réponse_assistant), ...] dans le format classique (dit "tuples"), ou une liste de dictionnaires {"role": "user"|"assistant", "content": "..."} dans le format récent (dit "messages"), qui reprend la convention d'OpenAI et de Hugging Face. Votre fonction retourne uniquement la réponse de l'assistant, une chaîne, et Gradio se charge de l'ajouter à l'historique.

Un premier assistant, écho amélioré

Le squelette minimal tient en cinq lignes et sert de test.

import gradio as gr

def repondre(message, historique):
tours = len(historique)
return f"Tour {tours + 1}. Vous avez dit : {message}"

demo = gr.ChatInterface(fn=repondre, type="messages", title="Assistant écho")
demo.launch()

Le paramètre type="messages" sélectionne le format récent (dictionnaires avec role et content), qui est celui de toutes les API modernes. Le format "tuples" reste disponible pour la compatibilité, mais tout code nouveau doit utiliser "messages" : c'est le format que les modèles de la famille Hugging Face et OpenAI attendent nativement, et il évite une conversion à la main dans presque tous les cas.

Brancher un modèle local

Pour un premier assistant réel, on utilise un petit modèle instruct local, exécuté avec la bibliothèque transformers. Le cours 29 sur les modèles de langage locaux détaille l'installation ; ici nous prenons TinyLlama-1.1B-Chat, assez léger pour un CPU récent.

import gradio as gr
from transformers import AutoModelForCausalLM, AutoTokenizer

nom = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
tokenizer = AutoTokenizer.from_pretrained(nom)
modele = AutoModelForCausalLM.from_pretrained(nom)

CONSIGNE_SYSTEME = (
"Vous êtes un assistant francophone poli et concis. "
"Vous répondez en français, en trois phrases au plus, "
"et vous dites 'je ne sais pas' plutôt que d'inventer."
)

def repondre(message, historique):
conversation = [{"role": "system", "content": CONSIGNE_SYSTEME}]
for tour in historique:
conversation.append(tour)
conversation.append({"role": "user", "content": message})

entree = tokenizer.apply_chat_template(
conversation, add_generation_prompt=True, return_tensors="pt"
)
sortie = modele.generate(entree, max_new_tokens=200, do_sample=False)
reponse = tokenizer.decode(sortie[0][entree.shape[1]:], skip_special_tokens=True)
return reponse.strip()

demo = gr.ChatInterface(
fn=repondre,
type="messages",
title="Assistant TinyLlama",
description="Un petit modèle instruct exécuté localement, sans clé d'API.",
)
demo.launch()

La conversion apply_chat_template est la pièce à ne pas manquer. Chaque famille de modèles a son format de gabarit — TinyLlama, Llama 3, Qwen, Mistral en ont des différents — et le tokenizer fait la traduction automatique à partir du format messages. Sans cet appel, le modèle voit un bloc de texte non structuré et ses réponses deviennent incohérentes.

Brancher une API

Le cas d'usage inverse consiste à appeler une API distante (OpenAI, Anthropic, Mistral, un serveur local exposant l'API OpenAI). Le squelette est identique, seul le corps de la fonction change.

import os
import gradio as gr
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

CONSIGNE_SYSTEME = "Vous êtes un assistant francophone poli et concis."

def repondre(message, historique):
messages = [{"role": "system", "content": CONSIGNE_SYSTEME}] + historique
messages.append({"role": "user", "content": message})

reponse = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0.2,
)
return reponse.choices[0].message.content

demo = gr.ChatInterface(fn=repondre, type="messages", title="Assistant OpenAI")
demo.launch()

Deux points de vigilance. Le format messages de Gradio est déjà celui d'OpenAI, donc aucune conversion n'est nécessaire, ce qui rend le code particulièrement compact. La clé d'API est lue depuis une variable d'environnement — jamais écrite en dur, jamais visible dans le code de la démonstration — et ce point deviendra critique au module 9 quand nous publierons l'espace : les secrets s'y déposent au même endroit et se lisent avec le même os.environ.

La consigne système, pièce oubliée par les débutants

Une consigne système claire fait la différence entre un assistant utile et un assistant qui hallucine. Elle définit le rôle (« vous êtes un expert en fiscalité française »), la langue de réponse quand le modèle est multilingue, la longueur attendue (« en trois phrases au plus ») et le comportement en cas de doute (« dites que vous ne savez pas plutôt que d'inventer »). Sans ces quatre éléments, le modèle prend des décisions par défaut souvent inadaptées à votre cas.

La consigne système n'est pas un endroit où stocker des informations confidentielles. Elle est incluse dans chaque requête, ce qui augmente les jetons consommés, et un utilisateur suffisamment insistant peut souvent la faire répéter par l'assistant. Le secret d'API va dans la variable d'environnement, la consigne dans la constante Python.

Un mot sur la version arabe

Une démonstration multilingue doit prévoir l'affichage de droite à gauche pour l'arabe. Le composant gr.Chatbot détecte automatiquement la direction du texte selon les caractères, mais l'aspect général de la page reste orienté à gauche. Pour une démonstration destinée principalement à un public arabophone, on injecte un peu de CSS via le paramètre css=".gradio-container { direction: rtl; }" de gr.Blocks. La version francophone n'a rien à faire de ce côté-là — Gradio est de gauche à droite par défaut.

L'historique grossit à chaque tour

Chaque tour ajoute deux messages à l'historique, et cet historique est renvoyé au modèle en entier à chaque appel. À dix tours, une conversation banale peut atteindre plusieurs milliers de jetons, et la latence comme le coût suivent linéairement. Le module 7 verra comment tronquer proprement l'historique quand il devient trop long, mais garder l'idée en tête dès maintenant évite les mauvaises surprises la première fois qu'on publie une démonstration payante.

En résumé

  • gr.ChatInterface(fn, type="messages") encapsule chatbot, saisie, boutons Réessayer et Vider en deux lignes ; la fonction reçoit (message, historique) et retourne la réponse.
  • Le format "messages" (liste de dictionnaires role/content) est celui d'OpenAI, Anthropic et Hugging Face ; à utiliser systématiquement pour tout nouveau code.
  • Pour un modèle local, tokenizer.apply_chat_template traduit le format messages dans le gabarit propre à la famille du modèle ; sans lui, le modèle voit un bloc de texte incohérent.
  • La consigne système fixe rôle, langue, longueur et comportement en cas de doute ; elle ne remplace pas un secret d'API, qui reste dans la variable d'environnement.

Le module suivant s'attaque au principal grief des utilisateurs face à un assistant : l'attente. Les générateurs Python et le mode stream de Gradio permettent d'afficher la réponse jeton par jeton, comme les assistants publics le font.