Aller au contenu

Langgraph et Gradio

Nous allons ici refaire un exemple simple, sans utiliser de LLM mais en intégrant Gradio.

from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from langgraph.graph.message import add_messages
import gradio as gr
import random

Nous allons créer un faux chatbot qui va répondre n'importe quoi aux demandes de l'utilisateur. Nous allons avoir besoin de 2 listes Python qui nous permettront de construire ces phrases aléatoires.

list_sujets = ["Le manège", "Le véhicule", "Le casque", "Le téléphone", "Le clavier", "Le zombie", "L'ordinateur"]
list_adjectifs = ["rapide", "lent", "vert", "rouge", "grand", "petit"] 
Mettons en place l'état :

class State(BaseModel):
    messages : Annotated[list, add_messages]
Nous avons une seule variable dans notre état : messages. messages est de type list. Nous utilisons Annotated afin d'associer la fonction add_messages à notre variable messages. add_messages est une fonction proposée par Langgraph, elle va permettre d'ajouter des messages à la liste messagestrès simplement comme nous le verrons un peu plus loin. Petite parenthèse : en fait add_messages est ce que l'on appelle un reducer, mais nous ne rentrerons pas dans ces détails ici, l'explication fournie ci-dessus devrait suffire.

graph_builder = StateGraph(State)

def chatbot(state : State):
    if state.messages[-1].content == "bonjour" or state.messages[-1].content == "hello":
        return {"messages" : [{'role':'assistant', 'content':"Bonjour, comment puis-je vous aider aujourd'hui ?"}]}
    else :
        sujet = random.choice(list_sujets)
        adj = random.choice(list_adjectifs)
        return {'messages': [{'role':'assistant', 'content': f"{sujet} est {adj}"}]}
Beaucoup de choses à dire sur la fonction chatbot:

Cette fonction va nous servir à créer un noeud pour notre graphe. En la listant, vous vous serez sans doute rendu compte par vous-même qu'elle gère 2 situations : - 1er cas : l'utilisateur envoie au "chatbot" "bonjour" ou "hello", la fonction renvoie alors {"messages" : [{'role':'assistant', 'content':"Bonjour, comment puis-je vous aider aujourd'hui ?"}]} - 2e cas : pour toutes les autres saisies de l'utilisateur, la fonction tire au sort un sujet et un adjectif et renvoie {'messages': [{'role':'assistant', 'content': f"{sujet} est {adj}"}]} (une phrase est constituée à partir des 2 mots tirés au sort)

Entrons un peu dans les détails avec les valeurs renvoyées par la fonction : prenons pour exemple le {"messages" : [{'role':'assistant', 'content':"Bonjour, comment puis-je vous aider aujourd'hui ?"}]}. Comme déjà évoqué dans le chapitre précédent, nous renvoyons une nouvelle valeur de l'état, cette valeur est un dictionnaire avec une seule clé messages (puisque l'on trouve une seule variable dans la classe State). La valeur associée à cette clé est une liste qui contient un dictionnaire. Normalement vous devriez reconnaître un message, comme déjà vu dans le chapitre 2 avec une clé role et une clé content.

On pourrait penser que désormais la valeur de la clé messages sera une liste contenant cet unique dictionnaire {"messages" : [{'role':'assistant', 'content':"Bonjour, comment puis-je vous aider aujourd'hui ?"}]}. En fait, c'est un peu plus compliqué que ça : en effet, grâce au add_messages ajouté avec le Annotated, les dictionnaires vont automatiquement s'accumuler dans la liste messages.

Imaginons que l'état d'origine est le suivant :

{"messages" : [{'role':'user', 'content':"hello"}]}
après le nœud 'chatbot' on aura :
{"messages" : [{'role':'user', 'content':"hello"}, {'role':'assistant', 'content':"Bonjour, comment puis-je vous aider aujourd'hui ?"}]}
Le deuxième message sera ajouté à la liste sans effacer le premier (on a l'équivalent d'un append). Les messages vont donc s'ajouter les uns aux autres.

Nous pouvons maintenant étudier le if de la fonction chatbot: nous avons un state.messages[-1]['content'], le state.messages correspond à la variable messages de l'état, state.messages est donc une liste contenant des dictionnaires. Le [-1] nous permet de récupérer le dernier élément ajouté de cette liste. Comme les éléments de cette liste sont des dictionnaires, le .content correspond à la valeur de la clé content (en fait ce n'est pas vraiment un dictionnaire mais une instance de la classe HumanMessage (d'où le .content à la place du ['content'] ), mais cela ne change pas grand chose à notre raisonnement).

Pour {"messages" : [{'role':'user', 'content':"hello"}, {'role':'assistant', 'content':"Bonjour, comment puis-je vous aider aujourd'hui ?"}]} cela correspondra donc à "Bonjour, comment puis-je vous aider aujourd'hui ?"

Nous construisons ensuite notre graphe :

graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, 'chatbot')
graph_builder.add_edge('chatbot', END)
graph = graph_builder.compile()
La fonction qui sera utilisée par Gradio :

def chat(user_input: str, history):
    result = graph.invoke({'messages' : [{"role": "user", "content": user_input}]})
    return result["messages"][-1].content
Rien de nouveau ci-dessus, n'hésitez pas à prendre bien votre temps pour analyser les 2 lignes

gr.ChatInterface(chat, type="messages").launch()
Vous pouvez vérifier que notre faux chatbot fonctionne parfaitement, nous allons pouvoir nous intéresser à un vrai chatbot dans le prochain chapitre.

Arrivé à ce stade, il est très important de bien comprendre une chose :

à chaque interaction entre l'utilisateur et l'assistant, nous repartons de zéro et nous parcourons de nouveau le graphe en entier. Voici un exemple pour vous aider à comprendre ce point essentiel. Imaginons le dialogue suivant :

  1. utilisateur : bonjour
  2. assistant : Bonjour, comment puis-je vous aider aujourd'hui ?
  3. utilisateur : j'aimerais réviser mon bac de français
  4. assitant : Le manège est rouge
  5. utilisateur : ?
  6. assistant : Le casque est petit

étapes 1 et 2

on parcourt une première fois le graphe (START -> chatbot -> END), arrivé au nœud END l'état est le suivant : {"messages" : [{'role':'user', 'content':"bonjour"}, {'role':'assistant', 'content':"Bonjour, comment puis-je vous aider aujourd'hui ?"}]}

étape 3 et 4

on parcourt une deuxième fois le graphe (START -> chatbot -> END), arrivé au nœud END l'état est le suivant : {"messages" : [{'role':'user', 'content':"j'aimerais réviser mon bac de français"}, {'role':'assistant', 'content':"Le manège est rouge"}]}

étape 5 et 6

on parcourt une troisième fois le graphe (START -> chatbot -> END), arrivé au nœud END l'état est le suivant : {"messages" : [{'role':'user', 'content':"?"}, {'role':'assistant', 'content':"Le casque est petit"}]}

Comme vous pouvez le constater, aucune trace de la conversation n'est conservée, il ne faudra pas perdre cela de vue lorsque nous voudrons réaliser un véritable chatbot avec Langgraph.

Exercice : modifiez le faux chatbot afin qu'il ait une conversation un peu plus intéressante.

Code complet :

from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from langgraph.graph.message import add_messages
import gradio as gr
import random

list_sujets = ["Le manège", "Le véhicule", "Le casque", "Le téléphone", "Le clavier", "Le zombie", "L'ordinateur"]
list_adjectifs = ["rapide", "lent", "vert", "rouge", "grand", "petit"]

class State(BaseModel):
    messages : Annotated[list, add_messages]

graph_builder = StateGraph(State)

def chatbot(state : State):
    if state.messages[-1].content == "bonjour" or state.messages[-1].content == "hello":
        return {"messages" : [{'role':'assistant', 'content':"Bonjour, comment puis-je vous aider aujourd'hui ?"}]}
    else :
        sujet = random.choice(list_sujets)
        adj = random.choice(list_adjectifs)
        return {'messages': [{'role':'assistant', 'content': f"{sujet} est {adj}"}]}

graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, 'chatbot')
graph_builder.add_edge('chatbot', END)
graph = graph_builder.compile()

def chat(user_input: str, history):
    result = graph.invoke({'messages' : [{"role": "user", "content": user_input}]})
    return result["messages"][-1].content

gr.ChatInterface(chat, type="messages").launch()