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"]
class State(BaseModel):
messages : Annotated[list, add_messages]
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 messages
trè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}"}]}
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"}]}
{"messages" : [{'role':'user', 'content':"hello"}, {'role':'assistant', 'content':"Bonjour, comment puis-je vous aider aujourd'hui ?"}]}
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()
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()
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 :
- utilisateur : bonjour
- assistant : Bonjour, comment puis-je vous aider aujourd'hui ?
- utilisateur : j'aimerais réviser mon bac de français
- assitant : Le manège est rouge
- utilisateur : ?
- 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()