Aller au contenu

chatbot anglais

Dans ce chapitre nous allons refaire le chatbot anglais déjà vu au chapitre 6 mais en utilisant langgraph.

from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from langgraph.graph.message import add_messages
from langchain.prompts import ChatPromptTemplate
from langgraph.checkpoint.memory import MemorySaver
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage
from dotenv import load_dotenv
import os
import gradio as gr
4 nouveautés dans les import ci-dessus : ChatPromptTemplate, MemorySaver, ChatGoogleGenerativeAI et HumanMessage :

  • ChatPromptTemplate permet de créer un prompt pour notre chatbot. Vous avez peut-être remarqué qu'au niveau du from on a langchain. Langchain est une bibliothèque développée par la même entreprise que langgraph, et langgraph utilise de nombreux éléments de langchain
  • MemorySaver va nous permettre de gérer la "mémoire" de notre chatbot. Nous avons vu dans le chapitre précédent que l'état (state) est remis zéro à chaque passage dans le graphe. Grâce à ce système de mémoire, cela ne sera plus le cas, les messages vont s'accumuler au niveau de l'état.
  • ChatGoogleGenerativeAI va nous permettre d'utiliser Gemini avec langgraph
  • HumanMessage nous l'avons déjà évoqué dans le chapitre précédent, mais dans l'état (variable state) les messages ne sont pas stockés sous la forme de simples dictionnaires mais sous la forme d'instances de classe : de la classe HumanMessage si c'est un message de l'utilisateur, de la classe AIMessage si c'est un message du LLM et SystemMessage si c'est un message système.  Nous n’aurons besoin ici que de la classe HumanMessage.

Commençons à développer notre chatbot :

load_dotenv()

API_KEY = os.getenv('GOOGLE_API_KEY')

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

memory = MemorySaver()

graph_builder = StateGraph(State)
Seule chose nouvelle ci-dessus, la création d'une instance de la classe MemorySaver qui va gérer la mémoire.

Passons au prompt :

prompt_system = """
You are an assistant who must help French students improve their level in English by discussing different topics with them. 
You must only answer in English, even if the student speaks to you in French.
If the student makes mistakes when speaking in English, you must point out their mistakes.
When the student succeeds in making sentences without mistakes, you can congratulate them on the quality of their expression.
Keep in mind that your responses should be adapted to the student you are chatting with, as they are learning both to read and to write.
Use their first questions to rate their English level and answer appropriately. However, while adapting to the student's level, 
make sure to gradually introduce more complex vocabulary and sentence structures to encourage continuous improvement in reading comprehension.
When the student asks a question, analyse their language to determine their English level. Consider the following:
Sentence structure: Are the sentences simple or complex? Does the student use a variety of grammatical structures?
Vocabulary: Is the vocabulary basic or advanced? Does the student use idiomatic expressions?
Grammar: Are there any grammatical errors? If so, what kind of errors are they?
Based on your analysis, categorize the student's level as beginner, intermediate, or advanced.
Once you have determined the student's level, adapt your responses accordingly:
Beginner: Use simple vocabulary and sentence structures. Focus on basic grammar and vocabulary. Provide clear and concise explanations.
Intermediate: Use a wider range of vocabulary and sentence structures. Introduce more complex grammar concepts. Provide more detailed explanations.
Advanced: Use advanced vocabulary and sentence structures. Focus on nuanced grammar and vocabulary. Provide in-depth explanations and examples.
While adapting to the student's level, gradually introduce more complex vocabulary and sentence structures. For example:
Beginner: Start by using simple vocabulary and sentence structures. Then, gradually introduce new vocabulary words and more complex sentence structures.
Intermediate: Start by using a wider range of vocabulary and sentence structures.
Then, gradually introduce more advanced vocabulary words and more complex grammar concepts.
Advanced: Start by using advanced vocabulary and sentence structures. Then, gradually introduce more nuanced vocabulary and grammar concepts.
Provide constructive feedback to the student on their understanding and writing. For example:
'Your answer is correct, but you could have used a more advanced vocabulary word here.''
'Your sentence structure is a bit confusing. Try simplifying it.'
'You made a small grammatical error here. Here is the correction.'
Be encouraging and supportive in your feedback. Help the student to learn from their mistakes and improve their English skills.
"""

prompt = ChatPromptTemplate([
    ("system", prompt_system),
    ("human", "Here is the conversation history :\n {historical}"),
    ("human", "{student_intervention}")

])
Nous utilisons ici ChatPromptTemplate. En paramètre nous avons une liste des différents message qui va constituer notre prompt (cette liste est constituée de tuples): - ("system", prompt_system) c'est un message de type SystemMessage, le contenu de ce message est simplement le contenu de la variable prompt_system définie juste au-dessus. - ("human", "Here is the conversation history :\n {historical}") va permettre d'intégrer l'historique de la conversation. {historical} sera remplacé au moment du invoke dans notre nœud chatbot(voir plus loin dans ce chapitre) par une chaine de caractères reprenant tout l'historique de la conversation. - ("human", "{student_intervention}")on y trouvera l'intervention de l'élève. student_intervention sera remplacé au moment du invoke dans notre nœud chatbot(voir plus loin dans ce chapitre) par une chaine de caractères contenant l'intervention de l'élève.

Nous utilisons ensuite ChatGoogleGenerativeAI afin de pouvoir travailler avec Gemini 2.0 flash (ici aussi, rien de compliqué):

llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
Ensuite nous allons coupler notre prompt et notre llm grâce à une chaine (d'où le nom langchain) :

llm_prompt = prompt | llm
Cela signifie que l'on va d'abord passer les éléments au prompt et ensuite passer le tout au llm.

La fonction ci-dessous va transformer la liste des messages présents dans l'état (state) en chaine de caractères. Si le message est de type HumanMessage, on commencera la ligne par "STUDENT : ". Dans le cas contraire (sous-entendu c'est un AIMessage), on commencera la ligne par un "ASSISTANT : ". Cette distinction dans l'historique de ce qui a été dit par l'élève et de ce qui a été dit par l'assistant devrait rendre l'historique de la conversation plus clair et donc faciliter le travail du LLM. On notera que cette fonction conversation_history prend en paramètre la liste de tous les messages de la conversation en cours.

def conversation_history(h_list):
    txt = ""
    for d in h_list:
        if isinstance(d, HumanMessage):
            txt += f"STUDENT : {d.content}\n"
        else :
            txt += f"ASSISTANT : {d.content}\n"
    return txt
Nous allons ensuite construire notre nœud comme nous l'avons déjà fait au chapitre précédent :

def chatbot(state : State):
    q = state.messages[-1].content
    h = conversation_history(state.messages[:-1])
    response = llm_prompt.invoke({"historical": h,"student_intervention" : q})
    return {'messages': [response]}

q va contenir la question (dernier message de l'état (state)). h va contenir l'historique de conversation sous forme de chaine de caractères (utilisation de la fonction conversation_history). Le [:-1] permet de récupérer tous les messages sauf le dernier (puisque le dernier a déjà été récupéré juste au-dessus).

Quand on a une chaine prompt+llm (prompt | llm), le invoke prend en paramètre un dictionnaire qui a pour clés les différents paramètres attendus dans le prompt (ici historical et student_intervention).

La fonction renvoie, comme vu au chapitre précédent, le nouvel état.

graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, 'chatbot')
graph_builder.add_edge('chatbot', END)
graph = graph_builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "1"}}
Petites nouveautés ci-dessus : - au moment de la compilation, nous passons l'instance de la classe MemorySaver (memory) à la méthode compile. - nous définissons un dictionnaire config qui va permettre de créer plusieurs threads de notre chatbot, chaque thread ayant sa propre mémoire (son propre historique de conversation). Si au niveau de la clé thread_id nous changeons le 1 en 2 par exemple, nous aurons un nouveau thread et donc un nouvel historique. Tout ceci sera important si votre chatbot est amené à gérer plusieurs conversations en même temps (il faudra donner à chaque conversation un thread_id différent afin de ne pas mélanger les différents historiques de conversation).

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


gr.ChatInterface(chat, type="messages").launch()
Exercice : Créez votre propre chatbot en vous aidant de ce qui a été fait ici

Ce chapitre est terminé. Je pense que l'on peut se poser une question après ce chapitre : pourquoi s'embêter avec Langgraph alors que l'on avait obtenu exactement le même résultat avec un code moins complexe au chapitre 6 ? La réponse est simple : cet exemple n'a servi qu'à introduire langgraph, dans les prochains chapitres, maintenant que nous maîtrisons bien les bases de langgraph, nous allons pouvoir utiliser sa puissance en concevant de véritables agents.

Code complet :

from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from typing import Annotated
from langgraph.graph.message import add_messages
from langchain.prompts import ChatPromptTemplate
from langgraph.checkpoint.memory import MemorySaver
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.messages import HumanMessage
from dotenv import load_dotenv
import os
import gradio as gr

load_dotenv()

API_KEY = os.getenv('GOOGLE_API_KEY')

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

memory = MemorySaver()

graph_builder = StateGraph(State)

prompt_system = """
You are an assistant who must help French students improve their level in English by discussing different topics with them. 
You must only answer in English, even if the student speaks to you in French.
If the student makes mistakes when speaking in English, you must point out their mistakes.
When the student succeeds in making sentences without mistakes, you can congratulate them on the quality of their expression.
Keep in mind that your responses should be adapted to the student you are chatting with, as they are learning both to read and to write.
Use their first questions to rate their English level and answer appropriately. However, while adapting to the student's level, 
make sure to gradually introduce more complex vocabulary and sentence structures to encourage continuous improvement in reading comprehension.
When the student asks a question, analyse their language to determine their English level. Consider the following:
Sentence structure: Are the sentences simple or complex? Does the student use a variety of grammatical structures?
Vocabulary: Is the vocabulary basic or advanced? Does the student use idiomatic expressions?
Grammar: Are there any grammatical errors? If so, what kind of errors are they?
Based on your analysis, categorize the student's level as beginner, intermediate, or advanced.
Once you have determined the student's level, adapt your responses accordingly:
Beginner: Use simple vocabulary and sentence structures. Focus on basic grammar and vocabulary. Provide clear and concise explanations.
Intermediate: Use a wider range of vocabulary and sentence structures. Introduce more complex grammar concepts. Provide more detailed explanations.
Advanced: Use advanced vocabulary and sentence structures. Focus on nuanced grammar and vocabulary. Provide in-depth explanations and examples.
While adapting to the student's level, gradually introduce more complex vocabulary and sentence structures. For example:
Beginner: Start by using simple vocabulary and sentence structures. Then, gradually introduce new vocabulary words and more complex sentence structures.
Intermediate: Start by using a wider range of vocabulary and sentence structures.
Then, gradually introduce more advanced vocabulary words and more complex grammar concepts.
Advanced: Start by using advanced vocabulary and sentence structures. Then, gradually introduce more nuanced vocabulary and grammar concepts.
Provide constructive feedback to the student on their understanding and writing. For example:
'Your answer is correct, but you could have used a more advanced vocabulary word here.''
'Your sentence structure is a bit confusing. Try simplifying it.'
'You made a small grammatical error here. Here is the correction.'
Be encouraging and supportive in your feedback. Help the student to learn from their mistakes and improve their English skills.
"""

prompt = ChatPromptTemplate([
    ("system", prompt_system),
    ("human", "Here is the conversation history :\n {historical}"),
    ("human", "{student_intervention}")

])

llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")

llm_prompt = prompt | llm

def conversation_history(h_list):
    txt = ""
    for d in h_list:
        if isinstance(d, HumanMessage):
            txt += f"STUDENT : {d.content}\n"
        else :
            txt += f"ASSISTANT : {d.content}\n"
    return txt

def chatbot(state : State):
    q = state.messages[-1].content
    h = conversation_history(state.messages[:-1])
    response = llm_prompt.invoke({"historical": h,"student_intervention" : q})
    return {'messages': [response]}

graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, 'chatbot')
graph_builder.add_edge('chatbot', END)
graph = graph_builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "1"}}

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

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