MathieuGAL commited on
Commit
87bce6b
·
verified ·
1 Parent(s): 1e3774b

FULL ACCESS

Browse files
Files changed (1) hide show
  1. app.py +399 -377
app.py CHANGED
@@ -1,378 +1,400 @@
1
- import os
2
- import pandas as pd
3
- import chromadb
4
- from google import genai
5
- from sentence_transformers import SentenceTransformer, CrossEncoder
6
- from typing import List, Dict
7
- from flask import Flask, request, jsonify
8
- from flask_cors import CORS
9
- from datetime import datetime
10
-
11
- # ======================================================================
12
- # CONFIGURATION
13
- # ======================================================================
14
-
15
- DATA_FILE_PATH = "data/QR.csv"
16
- CHROMA_DB_PATH = "data/bdd_ChromaDB"
17
- COLLECTION_NAME = "qr_data_dual_embeddings"
18
-
19
- Q_COLUMN_NAME = "Question"
20
- R_COLUMN_NAME = "Reponse"
21
- SYSTEM_PROMPT_PATH = "data/system_prompt.txt"
22
-
23
- SRC_CROSS_ENCODER = "models/mmarco-mMiniLMv2-L12-H384-v1"
24
- SRC_PARAPHRASE = "models/paraphrase-mpnet-base-v2"
25
-
26
- N_RESULTS_RETRIEVAL = 10
27
- N_RESULTS_RERANK = 3
28
-
29
- GEMINI_API_KEY = "AIzaSyDXXY7uSXryTxZ51jQFsSLcPnC_Ivt9V1g"
30
- GEMINI_MODEL = "gemini-2.5-flash"
31
-
32
- MAX_CONVERSATION_HISTORY = 10
33
-
34
- # ======================================================================
35
- # VARIABLES GLOBALES
36
- # ======================================================================
37
-
38
- model_cross_encoder: CrossEncoder = None
39
- model_paraphrase: SentenceTransformer = None
40
- collection: chromadb.Collection = None
41
- system_prompt: str = None
42
- gemini_client: genai.Client = None
43
-
44
- conversation_histories: Dict[str, List[Dict[str, str]]] = {}
45
- conversation_start_times: Dict[str, str] = {}
46
-
47
- # ======================================================================
48
- # CHARGEMENT DES RESSOURCES
49
- # ======================================================================
50
-
51
- def load_models():
52
- """Charge les modèles SentenceTransformer et CrossEncoder."""
53
- print("⏳ Chargement des modèles...")
54
- try:
55
- cross_encoder = CrossEncoder(
56
- SRC_CROSS_ENCODER if os.path.exists(SRC_CROSS_ENCODER)
57
- else "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
58
- )
59
- paraphrase = SentenceTransformer(
60
- SRC_PARAPHRASE if os.path.exists(SRC_PARAPHRASE)
61
- else "sentence-transformers/paraphrase-mpnet-base-v2"
62
- )
63
- print("✅ Modèles chargés avec succès.")
64
- return cross_encoder, paraphrase
65
- except Exception as e:
66
- print(f"❌ Erreur chargement modèles: {e}")
67
- raise
68
-
69
- def load_data():
70
- """Charge le DataFrame depuis le CSV."""
71
- try:
72
- if not os.path.exists(DATA_FILE_PATH):
73
- print(f"⚠️ Fichier {DATA_FILE_PATH} non trouvé. Utilisation d'exemple.")
74
- df = pd.DataFrame({
75
- Q_COLUMN_NAME: ["Où est le soleil?", "Qui est l'IA?"],
76
- R_COLUMN_NAME: ["Le soleil est une étoile.", "L'IA est l'intelligence artificielle."]
77
- })
78
- else:
79
- df = pd.read_csv(DATA_FILE_PATH)
80
- print(f" {len(df)} lignes chargées depuis {DATA_FILE_PATH}.")
81
- return df
82
- except Exception as e:
83
- print(f"❌ Erreur chargement données: {e}")
84
- raise
85
-
86
- def load_system_prompt():
87
- """Charge le system prompt."""
88
- try:
89
- with open(SYSTEM_PROMPT_PATH, 'r', encoding='utf-8') as f:
90
- return f.read().strip()
91
- except FileNotFoundError:
92
- default = "Tu es un assistant utile et concis. Réponds à la requête de l'utilisateur."
93
- print(f"⚠️ System prompt non trouvé. Utilisation du prompt par défaut.")
94
- return default
95
-
96
- def initialize_gemini_client():
97
- """Initialise le client Google Gemini."""
98
- try:
99
- return genai.Client(api_key=GEMINI_API_KEY)
100
- except Exception as e:
101
- print(f" Erreur Gemini: {e}")
102
- raise
103
-
104
- # ======================================================================
105
- # CHROMADB SETUP
106
- # ======================================================================
107
-
108
- def setup_chromadb_collection(client, df, model_paraphrase):
109
- """Configure et remplit la collection ChromaDB."""
110
- total_docs = len(df) * 2
111
-
112
- try:
113
- collection = client.get_or_create_collection(name=COLLECTION_NAME)
114
- except Exception as e:
115
- print(f"❌ Erreur ChromaDB: {e}")
116
- raise
117
-
118
- if collection.count() == total_docs and total_docs > 0:
119
- print(f" Collection déjà remplie ({collection.count()} docs).")
120
- return collection
121
-
122
- if total_docs == 0:
123
- print("⚠️ DataFrame vide.")
124
- return collection
125
-
126
- print(f"⏳ Remplissage de ChromaDB ({len(df)} lignes)...")
127
-
128
- docs, metadatas, ids = [], [], []
129
-
130
- for i, row in df.iterrows():
131
- question = str(row[Q_COLUMN_NAME])
132
- reponse = str(row[R_COLUMN_NAME])
133
- meta = {Q_COLUMN_NAME: question, R_COLUMN_NAME: reponse, "source_row": i}
134
-
135
- docs.append(question)
136
- metadatas.append({**meta, "type": "question"})
137
- ids.append(f"id_{i}_Q")
138
-
139
- docs.append(reponse)
140
- metadatas.append({**meta, "type": "reponse"})
141
- ids.append(f"id_{i}_R")
142
-
143
- embeddings = model_paraphrase.encode(docs, show_progress_bar=False).tolist()
144
-
145
- try:
146
- client.delete_collection(name=COLLECTION_NAME)
147
- except:
148
- pass
149
-
150
- collection = client.get_or_create_collection(name=COLLECTION_NAME)
151
- collection.add(embeddings=embeddings, documents=docs, metadatas=metadatas, ids=ids)
152
-
153
- print(f"✅ Collection remplie: {collection.count()} documents.")
154
- return collection
155
-
156
- # ======================================================================
157
- # RAG - RETRIEVAL & RERANKING
158
- # ======================================================================
159
-
160
- def retrieve_and_rerank(query_text, collection, model_paraphrase, model_cross_encoder):
161
- """Récupère et rerank les résultats."""
162
- print(f"🔍 Récupération pour: '{query_text[:40]}...'")
163
-
164
- query_emb = model_paraphrase.encode([query_text]).tolist()
165
- results = collection.query(
166
- query_embeddings=query_emb,
167
- n_results=N_RESULTS_RETRIEVAL,
168
- include=['documents', 'metadatas', 'distances']
169
- )
170
-
171
- if not results['ids'][0]:
172
- print("⚠️ Aucun résultat trouvé.")
173
- return pd.DataFrame()
174
-
175
- candidates = []
176
- cross_input = []
177
-
178
- for i, doc in enumerate(results['documents'][0]):
179
- meta = results['metadatas'][0][i]
180
- candidates.append({
181
- 'question': meta[Q_COLUMN_NAME],
182
- 'reponse': meta[R_COLUMN_NAME],
183
- 'doc_type': meta.get('type'),
184
- 'text_reranked': doc,
185
- 'initial_distance': results['distances'][0][i]
186
- })
187
- cross_input.append([query_text, doc])
188
-
189
- scores = model_cross_encoder.predict(cross_input)
190
- for i, score in enumerate(scores):
191
- candidates[i]['rerank_score'] = score
192
-
193
- df = pd.DataFrame(candidates).sort_values('rerank_score', ascending=False)
194
- df = df.drop_duplicates(subset=['question', 'reponse'], keep='first')
195
-
196
- return df.head(N_RESULTS_RERANK)
197
-
198
- def generate_rag_prompt(query_text, df_results, conversation_history):
199
- """Génère le prompt RAG final."""
200
- context = []
201
- if not df_results.empty:
202
- for _, row in df_results.iterrows():
203
- context.append(f"Q: {row['question']}\nR: {row['reponse']}")
204
-
205
- context_str = "\n---\n".join(context)
206
-
207
- history_str = ""
208
- if conversation_history:
209
- history_str = "HISTORIQUE:\n"
210
- for msg in conversation_history:
211
- role = "USER" if msg["role"] == "user" else "ASSISTANT"
212
- history_str += f"{role}: {msg['content']}\n"
213
- history_str += "\n"
214
-
215
- return f"""{history_str}UTILISATEUR: {query_text}
216
-
217
- CONTEXTE (si utile):
218
- [{context_str}]
219
-
220
- INSTRUCTIONS:
221
- - Réponds brièvement et clairement, en un minimum de 2 paragraphe et un maximum de 3 paragraphe, entre chaque paragraphe tu écrira un #
222
- - Reste professionnel, met des emojis dans ta réponse tout en restant profesionnel
223
- - Réponds uniquement à la question posée
224
- - Ne mentionne NOVA que si l'utilisateur l'évoque clairement
225
- - Mentionne obligatoirement Mathieu dans ta réponse"""
226
-
227
- # ======================================================================
228
- # GESTION HISTORIQUE
229
- # ======================================================================
230
-
231
- def get_conversation_history(session_id):
232
- """Récupère l'historique d'une session."""
233
- return conversation_histories.get(session_id, [])
234
-
235
- def add_to_history(session_id, role, content):
236
- """Ajoute un message à l'historique."""
237
- if session_id not in conversation_histories:
238
- conversation_histories[session_id] = []
239
-
240
- conversation_histories[session_id].append({"role": role, "content": content})
241
-
242
- if len(conversation_histories[session_id]) > MAX_CONVERSATION_HISTORY * 2:
243
- conversation_histories[session_id] = conversation_histories[session_id][-(MAX_CONVERSATION_HISTORY * 2):]
244
-
245
- def clear_history(session_id):
246
- """Efface l'historique d'une session."""
247
- conversation_histories[session_id] = []
248
-
249
- # ======================================================================
250
- # CALL GEMINI
251
- # ======================================================================
252
-
253
- def call_gemini(rag_prompt, system_prompt, gemini_client):
254
- """Appelle Google Gemini."""
255
- try:
256
- response = gemini_client.models.generate_content(
257
- model=GEMINI_MODEL,
258
- contents=f"{system_prompt}\n\n{rag_prompt}"
259
- )
260
- return response.text
261
- except Exception as e:
262
- print(f"❌ Erreur Gemini: {e}")
263
- return f"Erreur: {str(e)}"
264
-
265
- # ======================================================================
266
- # ANSWER PROCESS
267
- # ======================================================================
268
-
269
- def get_answer(query_text, collection, model_paraphrase, model_cross_encoder, conversation_history):
270
- """Exécute le processus RAG complet."""
271
- print(f"\n{'='*50}")
272
- print(f"🚀 Traitement: '{query_text}'")
273
- print(f"{'='*50}")
274
-
275
- df_results = retrieve_and_rerank(query_text, collection, model_paraphrase, model_cross_encoder)
276
- final_prompt = generate_rag_prompt(query_text, df_results, conversation_history)
277
-
278
- return final_prompt
279
-
280
- # ======================================================================
281
- # INITIALISATION GLOBALE
282
- # ======================================================================
283
-
284
- def initialize_global_resources():
285
- """Initialise tous les modèles et ressources."""
286
- global model_cross_encoder, model_paraphrase, collection, system_prompt, gemini_client
287
-
288
- print("\n" + "="*50)
289
- print("⚙️ INITIALISATION RAG")
290
- print("="*50)
291
-
292
- os.makedirs(CHROMA_DB_PATH, exist_ok=True)
293
-
294
- try:
295
- model_cross_encoder, model_paraphrase = load_models()
296
- df = load_data()
297
- system_prompt = load_system_prompt()
298
- gemini_client = initialize_gemini_client()
299
- except Exception:
300
- return False
301
-
302
- try:
303
- chroma_client = chromadb.PersistentClient(path=CHROMA_DB_PATH)
304
- collection = setup_chromadb_collection(chroma_client, df, model_paraphrase)
305
- print("✅ INITIALISATION COMPLÈTE\n")
306
- return True
307
- except Exception:
308
- return False
309
-
310
- # ======================================================================
311
- # FLASK API
312
- # ======================================================================
313
-
314
- app = Flask(__name__)
315
- CORS(app)
316
-
317
- @app.route('/api/get_answer', methods=['POST'])
318
- def api_get_answer():
319
- """Endpoint principal pour obtenir une réponse."""
320
- if any(x is None for x in [model_cross_encoder, model_paraphrase, collection, system_prompt, gemini_client]):
321
- return jsonify({"error": "Ressources non chargées"}), 500
322
-
323
- try:
324
- data = request.get_json()
325
- query_text = data.get('query_text')
326
- session_id = data.get('session_id', 'archive')
327
-
328
- if not query_text:
329
- return jsonify({"error": "Champ 'query_text' manquant"}), 400
330
-
331
- # Log utilisateur
332
- log_conversation(session_id, "user", query_text)
333
-
334
- # Récupère historique
335
- history = get_conversation_history(session_id)
336
-
337
- # Génère prompt RAG
338
- rag_prompt = get_answer(query_text, collection, model_paraphrase, model_cross_encoder, history)
339
-
340
- # Appelle Gemini
341
- response = call_gemini(rag_prompt, system_prompt, gemini_client)
342
-
343
- # Sauvegarde réponse
344
- add_to_history(session_id, "user", query_text)
345
- add_to_history(session_id, "assistant", response)
346
- log_conversation(session_id, "assistant", response)
347
-
348
- return jsonify({"generated_response": response})
349
-
350
- except Exception as e:
351
- print(f" Erreur: {e}")
352
- return jsonify({"error": str(e)}), 500
353
-
354
- @app.route('/api/clear_history', methods=['POST'])
355
- def api_clear_history():
356
- """Efface l'historique d'une session."""
357
- try:
358
- data = request.get_json()
359
- session_id = data.get('session_id', 'archive')
360
- clear_history(session_id)
361
-
362
- return jsonify({"message": f"Historique effacé: {session_id}"})
363
- except Exception as e:
364
- return jsonify({"error": str(e)}), 500
365
-
366
- # ======================================================================
367
- # MAIN
368
- # ======================================================================
369
-
370
- if __name__ == '__main__':
371
- print("start app.py")
372
- if initialize_global_resources():
373
- print("\n" + "="*50)
374
- print("🌐 SERVEUR DÉMARRÉ")
375
- print("="*50 + "\n")
376
- app.run(host='0.0.0.0', port=1212, debug=False)
377
- else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  print("❌ Impossible de démarrer le serveur")
 
1
+ import os
2
+ import pandas as pd
3
+ import chromadb
4
+ from google import genai
5
+ from sentence_transformers import SentenceTransformer, CrossEncoder
6
+ from typing import List, Dict
7
+ from flask import Flask, request, jsonify
8
+ from flask_cors import CORS
9
+ from datetime import datetime
10
+
11
+ # ======================================================================
12
+ # CONFIGURATION
13
+ # ======================================================================
14
+
15
+ DATA_FILE_PATH = "data/QR.csv"
16
+ CHROMA_DB_PATH = "data/bdd_ChromaDB"
17
+ COLLECTION_NAME = "qr_data_dual_embeddings"
18
+
19
+ Q_COLUMN_NAME = "Question"
20
+ R_COLUMN_NAME = "Reponse"
21
+ SYSTEM_PROMPT_PATH = "data/system_prompt.txt"
22
+
23
+ SRC_CROSS_ENCODER = "models/mmarco-mMiniLMv2-L12-H384-v1"
24
+ SRC_PARAPHRASE = "models/paraphrase-mpnet-base-v2"
25
+
26
+ N_RESULTS_RETRIEVAL = 10
27
+ N_RESULTS_RERANK = 3
28
+
29
+ GEMINI_API_KEY = "AIzaSyDXXY7uSXryTxZ51jQFsSLcPnC_Ivt9V1g"
30
+ GEMINI_MODEL = "gemini-2.5-flash"
31
+
32
+ MAX_CONVERSATION_HISTORY = 10
33
+
34
+ # Configuration pour l'accès externe (host et port)
35
+ API_HOST = '0.0.0.0'
36
+ API_PORT = 1212
37
+
38
+ # ======================================================================
39
+ # VARIABLES GLOBALES
40
+ # ======================================================================
41
+
42
+ model_cross_encoder: CrossEncoder = None
43
+ model_paraphrase: SentenceTransformer = None
44
+ collection: chromadb.Collection = None
45
+ system_prompt: str = None
46
+ gemini_client: genai.Client = None
47
+
48
+ conversation_histories: Dict[str, List[Dict[str, str]]] = {}
49
+ conversation_start_times: Dict[str, str] = {}
50
+
51
+ # ======================================================================
52
+ # CHARGEMENT DES RESSOURCES
53
+ # ======================================================================
54
+
55
+ def load_models():
56
+ """Charge les modèles SentenceTransformer et CrossEncoder."""
57
+ print("⏳ Chargement des modèles...")
58
+ try:
59
+ cross_encoder = CrossEncoder(
60
+ SRC_CROSS_ENCODER if os.path.exists(SRC_CROSS_ENCODER)
61
+ else "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
62
+ )
63
+ paraphrase = SentenceTransformer(
64
+ SRC_PARAPHRASE if os.path.exists(SRC_PARAPHRASE)
65
+ else "sentence-transformers/paraphrase-mpnet-base-v2"
66
+ )
67
+ print("✅ Modèles chargés avec succès.")
68
+ return cross_encoder, paraphrase
69
+ except Exception as e:
70
+ print(f" Erreur chargement modèles: {e}")
71
+ raise
72
+
73
+ def load_data():
74
+ """Charge le DataFrame depuis le CSV."""
75
+ try:
76
+ if not os.path.exists(DATA_FILE_PATH):
77
+ print(f"⚠️ Fichier {DATA_FILE_PATH} non trouvé. Utilisation d'exemple.")
78
+ df = pd.DataFrame({
79
+ Q_COLUMN_NAME: ["Où est le soleil?", "Qui est l'IA?"],
80
+ R_COLUMN_NAME: ["Le soleil est une étoile.", "L'IA est l'intelligence artificielle."]
81
+ })
82
+ else:
83
+ df = pd.read_csv(DATA_FILE_PATH)
84
+ print(f"✅ {len(df)} lignes chargées depuis {DATA_FILE_PATH}.")
85
+ return df
86
+ except Exception as e:
87
+ print(f" Erreur chargement données: {e}")
88
+ raise
89
+
90
+ def load_system_prompt():
91
+ """Charge le system prompt."""
92
+ try:
93
+ with open(SYSTEM_PROMPT_PATH, 'r', encoding='utf-8') as f:
94
+ return f.read().strip()
95
+ except FileNotFoundError:
96
+ default = "Tu es un assistant utile et concis. Réponds à la requête de l'utilisateur."
97
+ print(f"⚠️ System prompt non trouvé. Utilisation du prompt par défaut.")
98
+ return default
99
+
100
+ def initialize_gemini_client():
101
+ """Initialise le client Google Gemini."""
102
+ try:
103
+ return genai.Client(api_key=GEMINI_API_KEY)
104
+ except Exception as e:
105
+ print(f"❌ Erreur Gemini: {e}")
106
+ raise
107
+
108
+ # ======================================================================
109
+ # CHROMADB SETUP
110
+ # ======================================================================
111
+
112
+ def setup_chromadb_collection(client, df, model_paraphrase):
113
+ """Configure et remplit la collection ChromaDB."""
114
+ total_docs = len(df) * 2
115
+
116
+ try:
117
+ collection = client.get_or_create_collection(name=COLLECTION_NAME)
118
+ except Exception as e:
119
+ print(f" Erreur ChromaDB: {e}")
120
+ raise
121
+
122
+ if collection.count() == total_docs and total_docs > 0:
123
+ print(f" Collection déjà remplie ({collection.count()} docs).")
124
+ return collection
125
+
126
+ if total_docs == 0:
127
+ print("⚠️ DataFrame vide.")
128
+ return collection
129
+
130
+ print(f"⏳ Remplissage de ChromaDB ({len(df)} lignes)...")
131
+
132
+ docs, metadatas, ids = [], [], []
133
+
134
+ for i, row in df.iterrows():
135
+ question = str(row[Q_COLUMN_NAME])
136
+ reponse = str(row[R_COLUMN_NAME])
137
+ meta = {Q_COLUMN_NAME: question, R_COLUMN_NAME: reponse, "source_row": i}
138
+
139
+ docs.append(question)
140
+ metadatas.append({**meta, "type": "question"})
141
+ ids.append(f"id_{i}_Q")
142
+
143
+ docs.append(reponse)
144
+ metadatas.append({**meta, "type": "reponse"})
145
+ ids.append(f"id_{i}_R")
146
+
147
+ embeddings = model_paraphrase.encode(docs, show_progress_bar=False).tolist()
148
+
149
+ try:
150
+ client.delete_collection(name=COLLECTION_NAME)
151
+ except:
152
+ pass
153
+
154
+ collection = client.get_or_create_collection(name=COLLECTION_NAME)
155
+ collection.add(embeddings=embeddings, documents=docs, metadatas=metadatas, ids=ids)
156
+
157
+ print(f"✅ Collection remplie: {collection.count()} documents.")
158
+ return collection
159
+
160
+ # ======================================================================
161
+ # RAG - RETRIEVAL & RERANKING
162
+ # ======================================================================
163
+
164
+ def retrieve_and_rerank(query_text, collection, model_paraphrase, model_cross_encoder):
165
+ """Récupère et rerank les résultats."""
166
+ print(f"🔍 Récupération pour: '{query_text[:40]}...'")
167
+
168
+ query_emb = model_paraphrase.encode([query_text]).tolist()
169
+ results = collection.query(
170
+ query_embeddings=query_emb,
171
+ n_results=N_RESULTS_RETRIEVAL,
172
+ include=['documents', 'metadatas', 'distances']
173
+ )
174
+
175
+ if not results['ids'][0]:
176
+ print("⚠️ Aucun résultat trouvé.")
177
+ return pd.DataFrame()
178
+
179
+ candidates = []
180
+ cross_input = []
181
+
182
+ for i, doc in enumerate(results['documents'][0]):
183
+ meta = results['metadatas'][0][i]
184
+ candidates.append({
185
+ 'question': meta[Q_COLUMN_NAME],
186
+ 'reponse': meta[R_COLUMN_NAME],
187
+ 'doc_type': meta.get('type'),
188
+ 'text_reranked': doc,
189
+ 'initial_distance': results['distances'][0][i]
190
+ })
191
+ cross_input.append([query_text, doc])
192
+
193
+ scores = model_cross_encoder.predict(cross_input)
194
+ for i, score in enumerate(scores):
195
+ candidates[i]['rerank_score'] = score
196
+
197
+ df = pd.DataFrame(candidates).sort_values('rerank_score', ascending=False)
198
+ df = df.drop_duplicates(subset=['question', 'reponse'], keep='first')
199
+
200
+ return df.head(N_RESULTS_RERANK)
201
+
202
+ def generate_rag_prompt(query_text, df_results, conversation_history):
203
+ """Génère le prompt RAG final."""
204
+ context = []
205
+ if not df_results.empty:
206
+ for _, row in df_results.iterrows():
207
+ context.append(f"Q: {row['question']}\nR: {row['reponse']}")
208
+
209
+ context_str = "\n---\n".join(context)
210
+
211
+ history_str = ""
212
+ if conversation_history:
213
+ history_str = "HISTORIQUE:\n"
214
+ for msg in conversation_history:
215
+ role = "USER" if msg["role"] == "user" else "ASSISTANT"
216
+ history_str += f"{role}: {msg['content']}\n"
217
+ history_str += "\n"
218
+
219
+ return f"""{history_str}UTILISATEUR: {query_text}
220
+
221
+ CONTEXTE (si utile):
222
+ [{context_str}]
223
+
224
+ INSTRUCTIONS:
225
+ - Réponds brièvement et clairement, en un minimum de 2 paragraphe et un maximum de 3 paragraphe, entre chaque paragraphe tu écrira un #
226
+ - Reste professionnel, met des emojis dans ta réponse tout en restant profesionnel
227
+ - Réponds uniquement à la question posée
228
+ - Ne mentionne NOVA que si l'utilisateur l'évoque clairement
229
+ - Mentionne obligatoirement Mathieu dans ta réponse"""
230
+
231
+ # ======================================================================
232
+ # GESTION HISTORIQUE
233
+ # ======================================================================
234
+
235
+ def get_conversation_history(session_id):
236
+ """Récupère l'historique d'une session."""
237
+ return conversation_histories.get(session_id, [])
238
+
239
+ def add_to_history(session_id, role, content):
240
+ """Ajoute un message à l'historique."""
241
+ if session_id not in conversation_histories:
242
+ conversation_histories[session_id] = []
243
+
244
+ conversation_histories[session_id].append({"role": role, "content": content})
245
+
246
+ if len(conversation_histories[session_id]) > MAX_CONVERSATION_HISTORY * 2:
247
+ conversation_histories[session_id] = conversation_histories[session_id][-(MAX_CONVERSATION_HISTORY * 2):]
248
+
249
+ def clear_history(session_id):
250
+ """Efface l'historique d'une session."""
251
+ conversation_histories[session_id] = []
252
+
253
+ # ======================================================================
254
+ # CALL GEMINI
255
+ # ======================================================================
256
+
257
+ def call_gemini(rag_prompt, system_prompt, gemini_client):
258
+ """Appelle Google Gemini."""
259
+ try:
260
+ response = gemini_client.models.generate_content(
261
+ model=GEMINI_MODEL,
262
+ contents=f"{system_prompt}\n\n{rag_prompt}"
263
+ )
264
+ return response.text
265
+ except Exception as e:
266
+ print(f"❌ Erreur Gemini: {e}")
267
+ return f"Erreur: {str(e)}"
268
+
269
+ # ======================================================================
270
+ # ANSWER PROCESS
271
+ # ======================================================================
272
+
273
+ def get_answer(query_text, collection, model_paraphrase, model_cross_encoder, conversation_history):
274
+ """Exécute le processus RAG complet."""
275
+ print(f"\n{'='*50}")
276
+ print(f"🚀 Traitement: '{query_text}'")
277
+ print(f"{'='*50}")
278
+
279
+ df_results = retrieve_and_rerank(query_text, collection, model_paraphrase, model_cross_encoder)
280
+ final_prompt = generate_rag_prompt(query_text, df_results, conversation_history)
281
+
282
+ return final_prompt
283
+
284
+ # ======================================================================
285
+ # INITIALISATION GLOBALE
286
+ # ======================================================================
287
+
288
+ def initialize_global_resources():
289
+ """Initialise tous les modèles et ressources."""
290
+ global model_cross_encoder, model_paraphrase, collection, system_prompt, gemini_client
291
+
292
+ print("\n" + "="*50)
293
+ print("⚙️ INITIALISATION RAG")
294
+ print("="*50)
295
+
296
+ os.makedirs(CHROMA_DB_PATH, exist_ok=True)
297
+
298
+ try:
299
+ model_cross_encoder, model_paraphrase = load_models()
300
+ df = load_data()
301
+ system_prompt = load_system_prompt()
302
+ gemini_client = initialize_gemini_client()
303
+ except Exception:
304
+ return False
305
+
306
+ try:
307
+ chroma_client = chromadb.PersistentClient(path=CHROMA_DB_PATH)
308
+ collection = setup_chromadb_collection(chroma_client, df, model_paraphrase)
309
+ print("✅ INITIALISATION COMPLÈTE\n")
310
+ return True
311
+ except Exception:
312
+ return False
313
+
314
+ # ======================================================================
315
+ # FLASK API
316
+ # ======================================================================
317
+
318
+ app = Flask(__name__)
319
+ # CORS activé, permet les requêtes depuis n'importe quelle origine
320
+ CORS(app)
321
+
322
+ @app.route('/status', methods=['GET'])
323
+ def api_status():
324
+ """Route de ping pour vérifier l'état de l'API."""
325
+ return jsonify({"status": "everything is good"}), 200
326
+
327
+ @app.route('/api/get_answer', methods=['POST'])
328
+ def api_get_answer():
329
+ """Endpoint principal pour obtenir une réponse."""
330
+ if any(x is None for x in [model_cross_encoder, model_paraphrase, collection, system_prompt, gemini_client]):
331
+ return jsonify({"error": "Ressources non chargées"}), 500
332
+
333
+ try:
334
+ data = request.get_json()
335
+ query_text = data.get('query_text')
336
+ session_id = data.get('session_id', 'archive')
337
+
338
+ if not query_text:
339
+ return jsonify({"error": "Champ 'query_text' manquant"}), 400
340
+
341
+ # Récupère historique
342
+ history = get_conversation_history(session_id)
343
+
344
+ # Génère prompt RAG
345
+ rag_prompt = get_answer(query_text, collection, model_paraphrase, model_cross_encoder, history)
346
+
347
+ # Appelle Gemini
348
+ response = call_gemini(rag_prompt, system_prompt, gemini_client)
349
+
350
+ # Sauvegarde réponse
351
+ add_to_history(session_id, "user", query_text)
352
+ add_to_history(session_id, "assistant", response)
353
+
354
+ return jsonify({"generated_response": response})
355
+
356
+ except Exception as e:
357
+ print(f"❌ Erreur: {e}")
358
+ return jsonify({"error": str(e)}), 500
359
+
360
+ @app.route('/api/clear_history', methods=['POST'])
361
+ def api_clear_history():
362
+ """Efface l'historique d'une session."""
363
+ try:
364
+ data = request.get_json()
365
+ session_id = data.get('session_id', 'archive')
366
+ clear_history(session_id)
367
+
368
+ return jsonify({"message": f"Historique effacé: {session_id}"})
369
+ except Exception as e:
370
+ return jsonify({"error": str(e)}), 500
371
+
372
+ # ======================================================================
373
+ # MAIN
374
+ # ======================================================================
375
+
376
+ if __name__ == '__main__':
377
+ print("start app.py")
378
+ if initialize_global_resources():
379
+
380
+ # Récupération de l'adresse IP si possible (pour l'affichage)
381
+ try:
382
+ import socket
383
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
384
+ s.connect(("8.8.8.8", 80)) # Connecte à un serveur externe pour trouver l'IP locale utilisée
385
+ local_ip = s.getsockname()[0]
386
+ s.close()
387
+ except Exception:
388
+ local_ip = "127.0.0.1" # Fallback si échec
389
+
390
+ print("\n" + "="*50)
391
+ print("🌐 SERVEUR DÉMARRÉ")
392
+ print(f"✅ API accessible à l'URL (via l'interface réseau locale): http://{local_ip}:{API_PORT}")
393
+ print(f"✅ Route Status: http://{local_ip}:{API_PORT}/status")
394
+ print(f"💡 Pour un accès depuis l'extérieur, utilisez l'adresse IP publique de votre machine et assurez-vous que le port {API_PORT} est ouvert.")
395
+ print("="*50 + "\n")
396
+
397
+ # L'utilisation de host='0.0.0.0' dans app.run() permet l'accès depuis l'extérieur
398
+ app.run(host=API_HOST, port=API_PORT, debug=False)
399
+ else:
400
  print("❌ Impossible de démarrer le serveur")