File size: 10,300 Bytes
8ddbeb8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d217f1b
 
 
 
 
 
8ddbeb8
 
 
 
 
 
 
 
 
 
 
 
 
 
fdbdd19
 
 
 
dcbec3a
8ddbeb8
 
 
 
20fe53d
 
 
8ddbeb8
20fe53d
 
 
8ddbeb8
20fe53d
 
 
8ddbeb8
d217f1b
 
8ddbeb8
e39a8bb
 
 
 
 
 
 
 
 
 
 
 
 
62d0cf5
8ddbeb8
 
 
 
 
 
20fe53d
 
 
 
 
 
 
 
8ddbeb8
d217f1b
8ddbeb8
 
 
 
 
 
 
 
 
 
 
d217f1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68ee069
 
d217f1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ddbeb8
 
 
 
d217f1b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8ddbeb8
d217f1b
 
 
 
 
 
8ddbeb8
 
d217f1b
8ddbeb8
d217f1b
 
 
 
 
8ddbeb8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d217f1b
 
fdbdd19
 
d217f1b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
from langchain_core.prompts import ChatPromptTemplate
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_community.document_loaders import DirectoryLoader
from langchain_community.document_loaders import PyPDFLoader
from typing import List
from typing_extensions import TypedDict
from typing import Annotated
from langgraph.graph.message import AnyMessage, add_messages
from langchain_core.messages import HumanMessage, AIMessage
from langgraph.graph import END, StateGraph, START
from langgraph.checkpoint.memory import MemorySaver
from fastapi import FastAPI, UploadFile, Form
from fastapi.middleware.cors import CORSMiddleware
from typing import Optional
from PIL import Image
import base64
from io import BytesIO
import os 
import logging
import sys
import matplotlib
matplotlib.use('Agg')  # Configuration du backend avant d'importer pyplot
import matplotlib.pyplot as plt
import numpy as np
import re
import json

logger = logging.getLogger('uvicorn.error')
logger.setLevel(logging.DEBUG)

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

logging.basicConfig(
    format="%(asctime)s - %(levelname)s - %(message)s",
    level=logging.INFO
)
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", temperature=0.7)

memory = MemorySaver()


file_path_sec = "./documents/seconde.pdf"
loader_sec = PyPDFLoader(file_path_sec)
sec = loader_sec.load()

file_path_prem = "./documents/premiere.pdf"
loader_prem = PyPDFLoader(file_path_prem)
prem = loader_prem.load()

file_path_term = "./documents/term.pdf"
loader_term = PyPDFLoader(file_path_term)
term = loader_term.load()

plot_graph = False

system = """
Tu es un chatbot spécialisé dans l’aide aux élèves de lycée en mathématiques (niveau seconde, première spécialité, terminale spécialité).
Ton rôle est d’aider les élèves à :
1- Comprendre le cours : Tu dois être capable d’expliquer une notion du programme de manière claire, rigoureuse, précise et adaptée au niveau lycée. Utilise des exemples concrets et du vocabulaire accessible, tout en restant mathématiquement correct.
2- S’exercer : Tu peux proposer des exercices sur la notion demandée, avec des niveaux de difficulté variés. Commence par un ou deux exercices simples, puis propose des questions un peu plus complexes si l’élève se sent à l’aise.
3- Être guidé dans un exercice : Si un élève te donne un énoncé, ne donne jamais directement la réponse. Pose-lui des questions guidées, propose-lui des pistes de réflexion, et encourage-le à progresser pas à pas pour qu’il trouve la solution par lui-même.

Avant de répondre à une question, vérifie toujours que la notion évoquée fait bien partie du programme de mathématiques du lycée :
- Si c’est le cas, réponds normalement.
- Si ce n’est pas au programme du lycée, informe poliment l’élève que la notion dépasse le cadre du lycée, mais tu peux tout de même lui donner une explication simplifiée pour nourrir sa curiosité.

Si jamais tu n’es pas absolument certain de la réponse, ne tente pas de deviner. Dis-le honnêtement à l’élève et conseille-lui de poser la question à son professeur.

**Important** : Tu dois toujours rester centré sur les mathématiques. N’aborde aucun sujet qui n’a pas de lien avec les mathématiques, même si l’élève te le demande.
Tu dois obligatoirement utiliser le format Latex pour écrire les formules mathématiques
"""

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system),
        ("human", """
        Voici les différents programme officiel qui te permettront d'aider l'élève :
        Le programme de la classe de seconde :
        {sec}
        Le programme de la spécialité mathématiques en classe de première :
        {prem}
         Le programme de la spécialité mathématiques en classe de terminale :
        {term}
        Tu trouveras aussi l'historique conversation que tu as eu avec l'élève : \n {historical}
        Et enfin l'intervention de l'élève : {question}"),
        {graph}
        """)
    ]
)

def format_historical(hist):
    historical = []
    for i in range(0,len(hist)-2,2):
        historical.append("Utilisateur : "+hist[i].content[0]['text'])
        historical.append("Assistant : "+hist[i+1].content[0]['text'])
    return "\n".join(historical[-20:])

def generate_function_plot(expression, x_range=(-10, 10), num_points=1000):
    try:
        # Créer les points x
        x = np.linspace(x_range[0], x_range[1], num_points)
        
        # Évaluer la fonction
        # Remplacer les expressions mathématiques courantes
        expression = expression.replace('^', '**')
        expression = expression.replace('sin', 'np.sin')
        expression = expression.replace('cos', 'np.cos')
        expression = expression.replace('tan', 'np.tan')
        expression = expression.replace('exp', 'np.exp')
        expression = expression.replace('log', 'np.log')
        expression = expression.replace('ln', 'np.log')
        expression = expression.replace('sqrt', 'np.sqrt')
        expression = expression.replace('e', 'np.exp')
        expression = expression.replace('exp', 'np.exp')
        
        # Évaluer l'expression
        y = eval(expression)
        
        # Créer le graphique
        plt.figure(figsize=(10, 6))
        plt.plot(x, y)
        plt.grid(True)
        plt.axhline(y=0, color='k', linestyle='-', alpha=0.3)
        plt.axvline(x=0, color='k', linestyle='-', alpha=0.3)
        
        # Sauvegarder le graphique en mémoire
        img_buffer = BytesIO()
        plt.savefig(img_buffer, format='PNG')
        plt.close()
        img_buffer.seek(0)
        
        # Convertir en base64
        base64_img = base64.b64encode(img_buffer.getvalue()).decode('utf-8')
        return base64_img
    except Exception as e:
        return None

class GraphState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]

def should_plot(state: GraphState):
    system_prompt = """Tu es un assistant expert en pédagogie et en mathématiques. Ta spécialité est l'enseignement de mathématiques au lycée.
    Ta tâche est de déterminer si la demande de l'utilisateur nécessite la représentation graphique d'une fonction.
    Si c'est le cas, tu dois extraire l'expression de la fonction.
    
    Réponds sur une seule ligne avec le format suivant :
    OUI:expression si un graphique est nécessaire
    NON si aucun graphique n'est nécessaire
    
    Exemples de réponses :
    OUI:x**2
    OUI:sin(x)
    NON
    """
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", system_prompt),
        ("human", "Analyse cette demande : {question}")
    ])
    
    question = state['messages'][-1].content[0]['text']
    response = llm.invoke(prompt.format_messages(question=question))
    
    try:
        response_text = response.content.strip()
        if response_text.startswith("OUI:"):
            plot_graph = True
            expression = response_text[4:].strip()
            return {"should_plot": True, "expression": expression}
        else :
            plot_graph = False
            return {"should_plot": False, "expression": None}
    except Exception as e:
        return {"should_plot": False, "expression": None}

def chatbot(state : GraphState):
    plot_decision = should_plot(state) 
    if plot_graph :
        msg_graph = "Une représentation graphique de la fonction a été fournis à l'élève, tu dois préciser dans ta réponse que cette représentation graphique a été fournis à l'élève"
    else :
        msg_graph = ""
    question = prompt.invoke({'historical': format_historical(state['messages']),'sec':sec, 'prem':prem, 'term':term, 'graph':msg_graph, 'question' : state['messages'][-1].content[0]['text']})
    q = question.messages[0].content + question.messages[1].content
    if len(state['messages'][-1].content) > 1 :
        response = llm.invoke([HumanMessage(content=[{"type": "text", "text": q},state['messages'][-1].content[1]])])
    else :
        response = llm.invoke([HumanMessage(content=[{"type": "text", "text": q}])])
    if plot_decision["should_plot"] and plot_decision["expression"]:
        plot_base64 = generate_function_plot(plot_decision["expression"])
        if plot_base64:
            return {"messages": [AIMessage(content=[{'type': 'text', 'text': response.content},{'type': 'image_url', 'image_url': {"url": f"data:image/png;base64,{plot_base64}"}}])]}
    return {"messages": [AIMessage(content=[{'type': 'text', 'text': response.content}])]}

workflow = StateGraph(GraphState)
workflow.add_node('chatbot', chatbot)

workflow.add_edge(START, 'chatbot')
workflow.add_edge('chatbot', END)

app_chatbot = workflow.compile(checkpointer=memory)

@app.post('/request')
def request(id:Annotated[str, Form()], query:Annotated[str, Form()], image:Optional[UploadFile] = None):
    config = {"configurable": {"thread_id": id}}
    if image:
        try:
            img = Image.open(image.file)
            img_buffer = BytesIO()
            img.save(img_buffer, format='PNG')
            byte_data = img_buffer.getvalue()
            base64_img = base64.b64encode(byte_data).decode("utf-8")
            message = HumanMessage(
            content=[
                {'type': 'text', 'text': query},
                {'type': 'image_url', 'image_url': {"url": f"data:image/jpeg;base64,{base64_img}"}}
            ])
        except:
            return {"response":"Attention, vous m'avez fourni autre chose qu'une image. Renouvelez votre demande avec une image."}
        rep = app_chatbot.invoke({"messages": message},config, stream_mode="values")
    else :
        rep = app_chatbot.invoke({"messages": [HumanMessage(content=[{'type': 'text', 'text': query}])]},config, stream_mode="values")
    if len(rep['messages'][-1].content) > 1 and rep['messages'][-1].content[1].get('type') == 'image_url':
        return {"response": rep['messages'][-1].content[0]['text'],"image": rep['messages'][-1].content[1]['image_url']['url']}
    logging.info(query)
    logging.info(rep['messages'][-1].content)
    return {"response": rep['messages'][-1].content[0]['text']}