MathieuGAL commited on
Commit
c2d7780
·
verified ·
1 Parent(s): e6291e3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +285 -285
app.py CHANGED
@@ -59,182 +59,182 @@ conversation_start_times: Dict[str, str] = {}
59
  # ======================================================================
60
 
61
  def load_models():
62
-     """Charge les modèles SentenceTransformer et CrossEncoder."""
63
-     print("⏳ Chargement des modèles...")
64
-     try:
65
-         # Tente de charger localement, sinon télécharge (le cache se fera dans /tmp)
66
-         cross_encoder = CrossEncoder(
67
-             SRC_CROSS_ENCODER if os.path.exists(SRC_CROSS_ENCODER)
68
-             else "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
69
-         )
70
-         paraphrase = SentenceTransformer(
71
-             SRC_PARAPHRASE if os.path.exists(SRC_PARAPHRASE)
72
-             else "sentence-transformers/paraphrase-mpnet-base-v2"
73
-         )
74
-         print("✅ Modèles chargés avec succès.")
75
-         return cross_encoder, paraphrase
76
-     except Exception as e:
77
-         print(f"❌ Erreur chargement modèles: {e}")
78
-         # Note: L'erreur de PermissionError est maintenant gérée par le Dockerfile
79
-         raise
80
 
81
  def load_data():
82
-     """Charge le DataFrame depuis le CSV."""
83
-     try:
84
-         if not os.path.exists(DATA_FILE_PATH):
85
-             print(f"⚠️ Fichier {DATA_FILE_PATH} non trouvé. Utilisation d'exemple.")
86
-             df = pd.DataFrame({
87
-                 Q_COLUMN_NAME: ["Où est le soleil?", "Qui est l'IA?"],
88
-                 R_COLUMN_NAME: ["Le soleil est une étoile.", "L'IA est l'intelligence artificielle."]
89
-             })
90
-         else:
91
-             df = pd.read_csv(DATA_FILE_PATH)
92
-             print(f"✅ {len(df)} lignes chargées depuis {DATA_FILE_PATH}.")
93
-         return df
94
-     except Exception as e:
95
-         print(f"❌ Erreur chargement données: {e}")
96
-         raise
97
 
98
  def load_system_prompt():
99
-     """Charge le system prompt."""
100
-     try:
101
-         with open(SYSTEM_PROMPT_PATH, 'r', encoding='utf-8') as f:
102
-             return f.read().strip()
103
-     except FileNotFoundError:
104
-         default = "Tu es un assistant utile et concis. Réponds à la requête de l'utilisateur."
105
-         print(f"⚠️ System prompt non trouvé à {SYSTEM_PROMPT_PATH}. Utilisation du prompt par défaut.")
106
-         return default
107
 
108
  def initialize_gemini_client():
109
-     """Initialise le client Google Gemini."""
110
-     if GEMINI_API_KEY == "AIzaSyDXXY7uSXryTxZ51jQFsSLcPnC_Ivt9V1g":
111
-         print("⚠️ AVIS: Clé Gemini par défaut/placeholder détectée. Veuillez la remplacer par un secret d'environnement nommé 'GEMINI_API_KEY' pour la production.")
112
-     try:
113
-         return genai.Client(api_key=GEMINI_API_KEY)
114
-     except Exception as e:
115
-         print(f"❌ Erreur lors de l'initialisation du client Gemini: {e}")
116
-         raise
117
 
118
  # ======================================================================
119
  # CHROMADB SETUP
120
  # ======================================================================
121
 
122
  def setup_chromadb_collection(client, df, model_paraphrase):
123
-     """Configure et remplit la collection ChromaDB."""
124
-     total_docs = len(df) * 2
125
-     
126
-     # S'assurer que le répertoire de la DB existe
127
-     os.makedirs(CHROMA_DB_PATH, exist_ok=True)
128
-     
129
-     try:
130
-         collection = client.get_or_create_collection(name=COLLECTION_NAME)
131
-     except Exception as e:
132
-         print(f"❌ Erreur lors de l'accès à la collection ChromaDB: {e}")
133
-         raise
134
-     
135
-     if collection.count() == total_docs and total_docs > 0:
136
-         print(f"✅ Collection déjà remplie ({collection.count()} docs) dans {CHROMA_DB_PATH}.")
137
-         return collection
138
-     
139
-     if total_docs == 0:
140
-         print("⚠️ DataFrame vide. Collection non remplie.")
141
-         return collection
142
-     
143
-     print(f"⏳ Remplissage de ChromaDB ({len(df)} lignes) à l'emplacement: {CHROMA_DB_PATH}...")
144
-     
145
-     docs, metadatas, ids = [], [], []
146
-     
147
-     for i, row in df.iterrows():
148
-         question = str(row[Q_COLUMN_NAME])
149
-         reponse = str(row[R_COLUMN_NAME])
150
-         meta = {Q_COLUMN_NAME: question, R_COLUMN_NAME: reponse, "source_row": i}
151
-         
152
-         docs.append(question)
153
-         metadatas.append({**meta, "type": "question"})
154
-         ids.append(f"id_{i}_Q")
155
-         
156
-         docs.append(reponse)
157
-         metadatas.append({**meta, "type": "reponse"})
158
-         ids.append(f"id_{i}_R")
159
-     
160
-     embeddings = model_paraphrase.encode(docs, show_progress_bar=False).tolist()
161
-     
162
-     # Nettoyage et recréation (pour le cas où les données CSV ont changé)
163
-     try:
164
-         client.delete_collection(name=COLLECTION_NAME)
165
-     except:
166
-         pass
167
-     
168
-     collection = client.get_or_create_collection(name=COLLECTION_NAME)
169
-     collection.add(embeddings=embeddings, documents=docs, metadatas=metadatas, ids=ids)
170
-     
171
-     print(f"✅ Collection remplie: {collection.count()} documents.")
172
-     return collection
173
 
174
  # ======================================================================
175
  # RAG - RETRIEVAL & RERANKING
176
  # ======================================================================
177
 
178
  def retrieve_and_rerank(query_text, collection, model_paraphrase, model_cross_encoder):
179
-     """Récupère et rerank les résultats."""
180
-     print(f"🔍 Récupération pour: '{query_text[:40]}...'")
181
-     
182
-     query_emb = model_paraphrase.encode([query_text]).tolist()
183
-     results = collection.query(
184
-         query_embeddings=query_emb,
185
-         n_results=N_RESULTS_RETRIEVAL,
186
-         include=['documents', 'metadatas', 'distances']
187
-     )
188
-     
189
-     if not results['ids'][0]:
190
-         print("⚠️ Aucun résultat trouvé.")
191
-         return pd.DataFrame()
192
-     
193
-     candidates = []
194
-     cross_input = []
195
-     
196
-     for i, doc in enumerate(results['documents'][0]):
197
-         meta = results['metadatas'][0][i]
198
-         candidates.append({
199
-             'question': meta[Q_COLUMN_NAME],
200
-             'reponse': meta[R_COLUMN_NAME],
201
-             'doc_type': meta.get('type'),
202
-             'text_reranked': doc,
203
-             'initial_distance': results['distances'][0][i]
204
-         })
205
-         cross_input.append([query_text, doc])
206
-     
207
-     scores = model_cross_encoder.predict(cross_input)
208
-     for i, score in enumerate(scores):
209
-         candidates[i]['rerank_score'] = score
210
-     
211
-     df = pd.DataFrame(candidates).sort_values('rerank_score', ascending=False)
212
-     df = df.drop_duplicates(subset=['question', 'reponse'], keep='first')
213
-     
214
-     return df.head(N_RESULTS_RERANK)
215
 
216
  def generate_rag_prompt(query_text, df_results, conversation_history):
217
-     """Génère le prompt RAG final."""
218
-     context = []
219
-     if not df_results.empty:
220
-         for _, row in df_results.iterrows():
221
-             context.append(f"Q: {row['question']}\nR: {row['reponse']}")
222
-     
223
-     context_str = "\n---\n".join(context)
224
-     
225
-     history_str = ""
226
-     if conversation_history:
227
-         history_str = "HISTORIQUE:\n"
228
-         # Ajout du contexte pour le LLM, mais on ne veut pas l'historique complet
229
-         # On va limiter l'historique à l'affichage si on dépasse MAX_CONVERSATION_HISTORY
230
-         display_history = conversation_history[-(MAX_CONVERSATION_HISTORY * 2):]
231
-         for msg in display_history:
232
-             role = "USER" if msg["role"] == "user" else "ASSISTANT"
233
-             # On utilise 'content' pour le texte du message
234
-             history_str += f"{role}: {msg['content']}\n"
235
-         history_str += "\n"
236
-     
237
-     return f"""{history_str}UTILISATEUR: {query_text}
238
 
239
  CONTEXTE (si utile):
240
  [{context_str}]
@@ -251,89 +251,89 @@ INSTRUCTIONS:
251
  # ======================================================================
252
 
253
  def get_conversation_history(session_id):
254
-     """Récupère l'historique d'une session."""
255
-     return conversation_histories.get(session_id, [])
256
 
257
  def add_to_history(session_id, role, content):
258
-     """Ajoute un message à l'historique."""
259
-     if session_id not in conversation_histories:
260
-         conversation_histories[session_id] = []
261
-     
262
-     conversation_histories[session_id].append({"role": role, "content": content})
263
-     
264
-     # Limiter la taille de l'historique conservé en mémoire
265
-     if len(conversation_histories[session_id]) > MAX_CONVERSATION_HISTORY * 2:
266
-         conversation_histories[session_id] = conversation_histories[session_id][-(MAX_CONVERSATION_HISTORY * 2):]
267
 
268
  def clear_history(session_id):
269
-     """Efface l'historique d'une session."""
270
-     conversation_histories[session_id] = []
271
 
272
  # ======================================================================
273
  # CALL GEMINI
274
  # ======================================================================
275
 
276
  def call_gemini(rag_prompt, system_prompt, gemini_client):
277
-     """Appelle Google Gemini."""
278
-     try:
279
-         response = gemini_client.models.generate_content(
280
-             model=GEMINI_MODEL,
281
-             contents=f"{system_prompt}\n\n{rag_prompt}"
282
-         )
283
-         return response.text.replace("*", "")
284
-     except Exception as e:
285
-         print(f"❌ Erreur Gemini: {e}")
286
-         return f"Erreur: {str(e)}"
287
 
288
  # ======================================================================
289
  # ANSWER PROCESS
290
  # ======================================================================
291
 
292
  def get_answer(query_text, collection, model_paraphrase, model_cross_encoder, conversation_history):
293
-     """Exécute le processus RAG complet."""
294
-     print(f"\n{'='*50}")
295
-     print(f"🚀 Traitement: '{query_text}'")
296
-     print(f"{'='*50}")
297
-     
298
-     df_results = retrieve_and_rerank(query_text, collection, model_paraphrase, model_cross_encoder)
299
-     final_prompt = generate_rag_prompt(query_text, df_results, conversation_history)
300
-     
301
-     # On retourne le prompt final RAG pour référence, mais l'appel Gemini est fait après
302
-     return final_prompt
303
 
304
  # ======================================================================
305
  # INITIALISATION GLOBALE
306
  # ======================================================================
307
 
308
  def initialize_global_resources():
309
-     """Initialise tous les modèles et ressources."""
310
-     global model_cross_encoder, model_paraphrase, collection, system_prompt, gemini_client
311
-     
312
-     print("\n" + "="*50)
313
-     print("⚙️  INITIALISATION RAG")
314
-     print("="*50)
315
-     
316
-     # Le répertoire /tmp est géré par la variable CHROMA_DB_PATH
317
-     
318
-     try:
319
-         model_cross_encoder, model_paraphrase = load_models()
320
-         df = load_data()
321
-         system_prompt = load_system_prompt()
322
-         gemini_client = initialize_gemini_client()
323
-     except Exception:
324
-         # L'erreur est déjà print dans les fonctions de chargement
325
-         return False
326
-     
327
-     try:
328
-         print(f"⏳ Initialisation de ChromaDB à l'emplacement: {CHROMA_DB_PATH}")
329
-         # Le PersistentClient créera les fichiers dans le chemin spécifié (maintenant dans /tmp)
330
-         chroma_client = chromadb.PersistentClient(path=CHROMA_DB_PATH)
331
-         collection = setup_chromadb_collection(chroma_client, df, model_paraphrase)
332
-         print("✅ INITIALISATION COMPLÈTE\n")
333
-         return True
334
-     except Exception as e:
335
-         print(f"❌ Erreur lors de l'initialisation de ChromaDB ou du remplissage: {e}")
336
-         return False
337
 
338
  # ======================================================================
339
  # FLASK API
@@ -345,83 +345,83 @@ CORS(app)
345
 
346
  @app.route('/status', methods=['GET'])
347
  def api_status():
348
-     """Route de ping pour vérifier l'état de l'API."""
349
-     return jsonify({"status": "everything is good"}), 200
350
 
351
  @app.route('/api/get_answer', methods=['POST'])
352
  def api_get_answer():
353
-     """Endpoint principal pour obtenir une réponse."""
354
-     if any(x is None for x in [model_cross_encoder, model_paraphrase, collection, system_prompt, gemini_client]):
355
-         return jsonify({"error": "Ressources non chargées. Veuillez vérifier les logs d'initialisation."}), 500
356
-     
357
-     try:
358
-         data = request.get_json()
359
-         query_text = data.get('query_text')
360
-         session_id = data.get('session_id', 'archive')
361
-         
362
-         if not query_text:
363
-             generic_message = "Problème avec l'API, veuillez réessayer plus tard."
364
-             return jsonify({"error": generic_message}), 500
365
-         
366
-         # Récupère historique
367
-         history = get_conversation_history(session_id)
368
-         
369
-         # Génère prompt RAG
370
-         rag_prompt = get_answer(query_text, collection, model_paraphrase, model_cross_encoder, history)
371
-         
372
-         # Appelle Gemini
373
-         response = call_gemini(rag_prompt, system_prompt, gemini_client)
374
-         
375
-         # Sauvegarde réponse
376
-         add_to_history(session_id, "user", query_text)
377
-         add_to_history(session_id, "assistant", response)
378
-         
379
-         return jsonify({"generated_response": response})
380
-     
381
-     except Exception as e:
382
-         print(f"❌ Erreur générale de l'API: {e}")
383
-         generic_message = "Problème avec l'API, veuillez réessayer plus tard."
384
-         return jsonify({"error": generic_message}), 500
385
 
386
  @app.route('/api/clear_history', methods=['POST'])
387
  def api_clear_history():
388
-     """Efface l'historique d'une session."""
389
-     try:
390
-         data = request.get_json()
391
-         session_id = data.get('session_id', 'archive')
392
-         clear_history(session_id)
393
-         
394
-         return jsonify({"message": f"Historique effacé: {session_id}"})
395
-     except Exception as e:
396
-         generic_message = "Problème avec l'API, veuillez réessayer plus tard."
397
-         return jsonify({"error": generic_message}), 500
398
 
399
  # ======================================================================
400
  # MAIN
401
  # ======================================================================
402
 
403
  if __name__ == '__main__':
404
-     print("start app.py")
405
-     if initialize_global_resources():
406
-         
407
-         # Récupération de l'adresse IP si possible (pour l'affichage)
408
-         try:
409
-             import socket
410
-             s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
411
-             s.connect(("8.8.8.8", 80)) # Connecte à un serveur externe pour trouver l'IP locale utilisée
412
-             local_ip = s.getsockname()[0]
413
-             s.close()
414
-         except Exception:
415
-             local_ip = "127.0.0.1" # Fallback si échec
416
-         
417
-         print("\n" + "="*50)
418
-         print("🌐 SERVEUR DÉMARRÉ")
419
-         print(f"✅ API accessible à l'URL (via l'interface réseau locale): http://{local_ip}:{API_PORT}")
420
-         print(f"✅ Route Status: http://{local_ip}:{API_PORT}/status")
421
-         print(f"💡 N'oubliez pas de configurer 'app_port: 1212' et 'sdk: docker' dans votre README.md !")
422
-         print("="*50 + "\n")
423
-         
424
-         # L'utilisation de host='0.0.0.0' dans app.run() permet l'accès depuis l'extérieur
425
-         app.run(host=API_HOST, port=API_PORT, debug=False)
426
-     else:
427
-         print("❌ Impossible de démarrer le serveur. Veuillez vérifier les logs pour les erreurs d'initialisation.")
 
59
  # ======================================================================
60
 
61
  def load_models():
62
+ """Charge les modèles SentenceTransformer et CrossEncoder."""
63
+ print("⏳ Chargement des modèles...")
64
+ try:
65
+ # Tente de charger localement, sinon télécharge (le cache se fera dans /tmp)
66
+ cross_encoder = CrossEncoder(
67
+ SRC_CROSS_ENCODER if os.path.exists(SRC_CROSS_ENCODER)
68
+ else "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
69
+ )
70
+ paraphrase = SentenceTransformer(
71
+ SRC_PARAPHRASE if os.path.exists(SRC_PARAPHRASE)
72
+ else "sentence-transformers/paraphrase-mpnet-base-v2"
73
+ )
74
+ print("✅ Modèles chargés avec succès.")
75
+ return cross_encoder, paraphrase
76
+ except Exception as e:
77
+ print(f"❌ Erreur chargement modèles: {e}")
78
+ # Note: L'erreur de PermissionError est maintenant gérée par le Dockerfile
79
+ raise
80
 
81
  def load_data():
82
+ """Charge le DataFrame depuis le CSV."""
83
+ try:
84
+ if not os.path.exists(DATA_FILE_PATH):
85
+ print(f"⚠️ Fichier {DATA_FILE_PATH} non trouvé. Utilisation d'exemple.")
86
+ df = pd.DataFrame({
87
+ Q_COLUMN_NAME: ["Où est le soleil?", "Qui est l'IA?"],
88
+ R_COLUMN_NAME: ["Le soleil est une étoile.", "L'IA est l'intelligence artificielle."]
89
+ })
90
+ else:
91
+ df = pd.read_csv(DATA_FILE_PATH)
92
+ print(f"✅ {len(df)} lignes chargées depuis {DATA_FILE_PATH}.")
93
+ return df
94
+ except Exception as e:
95
+ print(f"❌ Erreur chargement données: {e}")
96
+ raise
97
 
98
  def load_system_prompt():
99
+ """Charge le system prompt."""
100
+ try:
101
+ with open(SYSTEM_PROMPT_PATH, 'r', encoding='utf-8') as f:
102
+ return f.read().strip()
103
+ except FileNotFoundError:
104
+ default = "Tu es un assistant utile et concis. Réponds à la requête de l'utilisateur."
105
+ print(f"⚠️ System prompt non trouvé à {SYSTEM_PROMPT_PATH}. Utilisation du prompt par défaut.")
106
+ return default
107
 
108
  def initialize_gemini_client():
109
+ """Initialise le client Google Gemini."""
110
+ if GEMINI_API_KEY == "AIzaSyDXXY7uSXryTxZ51jQFsSLcPnC_Ivt9V1g":
111
+ print("⚠️ AVIS: Clé Gemini par défaut/placeholder détectée. Veuillez la remplacer par un secret d'environnement nommé 'GEMINI_API_KEY' pour la production.")
112
+ try:
113
+ return genai.Client(api_key=GEMINI_API_KEY)
114
+ except Exception as e:
115
+ print(f"❌ Erreur lors de l'initialisation du client Gemini: {e}")
116
+ raise
117
 
118
  # ======================================================================
119
  # CHROMADB SETUP
120
  # ======================================================================
121
 
122
  def setup_chromadb_collection(client, df, model_paraphrase):
123
+ """Configure et remplit la collection ChromaDB."""
124
+ total_docs = len(df) * 2
125
+
126
+ # S'assurer que le répertoire de la DB existe
127
+ os.makedirs(CHROMA_DB_PATH, exist_ok=True)
128
+
129
+ try:
130
+ collection = client.get_or_create_collection(name=COLLECTION_NAME)
131
+ except Exception as e:
132
+ print(f"❌ Erreur lors de l'accès à la collection ChromaDB: {e}")
133
+ raise
134
+
135
+ if collection.count() == total_docs and total_docs > 0:
136
+ print(f"✅ Collection déjà remplie ({collection.count()} docs) dans {CHROMA_DB_PATH}.")
137
+ return collection
138
+
139
+ if total_docs == 0:
140
+ print("⚠️ DataFrame vide. Collection non remplie.")
141
+ return collection
142
+
143
+ print(f"⏳ Remplissage de ChromaDB ({len(df)} lignes) à l'emplacement: {CHROMA_DB_PATH}...")
144
+
145
+ docs, metadatas, ids = [], [], []
146
+
147
+ for i, row in df.iterrows():
148
+ question = str(row[Q_COLUMN_NAME])
149
+ reponse = str(row[R_COLUMN_NAME])
150
+ meta = {Q_COLUMN_NAME: question, R_COLUMN_NAME: reponse, "source_row": i}
151
+
152
+ docs.append(question)
153
+ metadatas.append({**meta, "type": "question"})
154
+ ids.append(f"id_{i}_Q")
155
+
156
+ docs.append(reponse)
157
+ metadatas.append({**meta, "type": "reponse"})
158
+ ids.append(f"id_{i}_R")
159
+
160
+ embeddings = model_paraphrase.encode(docs, show_progress_bar=False).tolist()
161
+
162
+ # Nettoyage et recréation (pour le cas où les données CSV ont changé)
163
+ try:
164
+ client.delete_collection(name=COLLECTION_NAME)
165
+ except:
166
+ pass
167
+
168
+ collection = client.get_or_create_collection(name=COLLECTION_NAME)
169
+ collection.add(embeddings=embeddings, documents=docs, metadatas=metadatas, ids=ids)
170
+
171
+ print(f"✅ Collection remplie: {collection.count()} documents.")
172
+ return collection
173
 
174
  # ======================================================================
175
  # RAG - RETRIEVAL & RERANKING
176
  # ======================================================================
177
 
178
  def retrieve_and_rerank(query_text, collection, model_paraphrase, model_cross_encoder):
179
+ """Récupère et rerank les résultats."""
180
+ print(f"🔍 Récupération pour: '{query_text[:40]}...'")
181
+
182
+ query_emb = model_paraphrase.encode([query_text]).tolist()
183
+ results = collection.query(
184
+ query_embeddings=query_emb,
185
+ n_results=N_RESULTS_RETRIEVAL,
186
+ include=['documents', 'metadatas', 'distances']
187
+ )
188
+
189
+ if not results['ids'][0]:
190
+ print("⚠️ Aucun résultat trouvé.")
191
+ return pd.DataFrame()
192
+
193
+ candidates = []
194
+ cross_input = []
195
+
196
+ for i, doc in enumerate(results['documents'][0]):
197
+ meta = results['metadatas'][0][i]
198
+ candidates.append({
199
+ 'question': meta[Q_COLUMN_NAME],
200
+ 'reponse': meta[R_COLUMN_NAME],
201
+ 'doc_type': meta.get('type'),
202
+ 'text_reranked': doc,
203
+ 'initial_distance': results['distances'][0][i]
204
+ })
205
+ cross_input.append([query_text, doc])
206
+
207
+ scores = model_cross_encoder.predict(cross_input)
208
+ for i, score in enumerate(scores):
209
+ candidates[i]['rerank_score'] = score
210
+
211
+ df = pd.DataFrame(candidates).sort_values('rerank_score', ascending=False)
212
+ df = df.drop_duplicates(subset=['question', 'reponse'], keep='first')
213
+
214
+ return df.head(N_RESULTS_RERANK)
215
 
216
  def generate_rag_prompt(query_text, df_results, conversation_history):
217
+ """Génère le prompt RAG final."""
218
+ context = []
219
+ if not df_results.empty:
220
+ for _, row in df_results.iterrows():
221
+ context.append(f"Q: {row['question']}\nR: {row['reponse']}")
222
+
223
+ context_str = "\n---\n".join(context)
224
+
225
+ history_str = ""
226
+ if conversation_history:
227
+ history_str = "HISTORIQUE:\n"
228
+ # Ajout du contexte pour le LLM, mais on ne veut pas l'historique complet
229
+ # On va limiter l'historique à l'affichage si on dépasse MAX_CONVERSATION_HISTORY
230
+ display_history = conversation_history[-(MAX_CONVERSATION_HISTORY * 2):]
231
+ for msg in display_history:
232
+ role = "USER" if msg["role"] == "user" else "ASSISTANT"
233
+ # On utilise 'content' pour le texte du message
234
+ history_str += f"{role}: {msg['content']}\n"
235
+ history_str += "\n"
236
+
237
+ return f"""{history_str}UTILISATEUR: {query_text}
238
 
239
  CONTEXTE (si utile):
240
  [{context_str}]
 
251
  # ======================================================================
252
 
253
  def get_conversation_history(session_id):
254
+ """Récupère l'historique d'une session."""
255
+ return conversation_histories.get(session_id, [])
256
 
257
  def add_to_history(session_id, role, content):
258
+ """Ajoute un message à l'historique."""
259
+ if session_id not in conversation_histories:
260
+ conversation_histories[session_id] = []
261
+
262
+ conversation_histories[session_id].append({"role": role, "content": content})
263
+
264
+ # Limiter la taille de l'historique conservé en mémoire
265
+ if len(conversation_histories[session_id]) > MAX_CONVERSATION_HISTORY * 2:
266
+ conversation_histories[session_id] = conversation_histories[session_id][-(MAX_CONVERSATION_HISTORY * 2):]
267
 
268
  def clear_history(session_id):
269
+ """Efface l'historique d'une session."""
270
+ conversation_histories[session_id] = []
271
 
272
  # ======================================================================
273
  # CALL GEMINI
274
  # ======================================================================
275
 
276
  def call_gemini(rag_prompt, system_prompt, gemini_client):
277
+ """Appelle Google Gemini."""
278
+ try:
279
+ response = gemini_client.models.generate_content(
280
+ model=GEMINI_MODEL,
281
+ contents=f"{system_prompt}\n\n{rag_prompt}"
282
+ )
283
+ return response.text.replace("*", "")
284
+ except Exception as e:
285
+ print(f"❌ Erreur Gemini: {e}")
286
+ return f"Erreur: {str(e)}"
287
 
288
  # ======================================================================
289
  # ANSWER PROCESS
290
  # ======================================================================
291
 
292
  def get_answer(query_text, collection, model_paraphrase, model_cross_encoder, conversation_history):
293
+ """Exécute le processus RAG complet."""
294
+ print(f"\n{'='*50}")
295
+ print(f"🚀 Traitement: '{query_text}'")
296
+ print(f"{'='*50}")
297
+
298
+ df_results = retrieve_and_rerank(query_text, collection, model_paraphrase, model_cross_encoder)
299
+ final_prompt = generate_rag_prompt(query_text, df_results, conversation_history)
300
+
301
+ # On retourne le prompt final RAG pour référence, mais l'appel Gemini est fait après
302
+ return final_prompt
303
 
304
  # ======================================================================
305
  # INITIALISATION GLOBALE
306
  # ======================================================================
307
 
308
  def initialize_global_resources():
309
+ """Initialise tous les modèles et ressources."""
310
+ global model_cross_encoder, model_paraphrase, collection, system_prompt, gemini_client
311
+
312
+ print("\n" + "="*50)
313
+ print("⚙️ INITIALISATION RAG")
314
+ print("="*50)
315
+
316
+ # Le répertoire /tmp est géré par la variable CHROMA_DB_PATH
317
+
318
+ try:
319
+ model_cross_encoder, model_paraphrase = load_models()
320
+ df = load_data()
321
+ system_prompt = load_system_prompt()
322
+ gemini_client = initialize_gemini_client()
323
+ except Exception:
324
+ # L'erreur est déjà print dans les fonctions de chargement
325
+ return False
326
+
327
+ try:
328
+ print(f"⏳ Initialisation de ChromaDB à l'emplacement: {CHROMA_DB_PATH}")
329
+ # Le PersistentClient créera les fichiers dans le chemin spécifié (maintenant dans /tmp)
330
+ chroma_client = chromadb.PersistentClient(path=CHROMA_DB_PATH)
331
+ collection = setup_chromadb_collection(chroma_client, df, model_paraphrase)
332
+ print("✅ INITIALISATION COMPLÈTE\n")
333
+ return True
334
+ except Exception as e:
335
+ print(f"❌ Erreur lors de l'initialisation de ChromaDB ou du remplissage: {e}")
336
+ return False
337
 
338
  # ======================================================================
339
  # FLASK API
 
345
 
346
  @app.route('/status', methods=['GET'])
347
  def api_status():
348
+ """Route de ping pour vérifier l'état de l'API."""
349
+ return jsonify({"status": "everything is good"}), 200
350
 
351
  @app.route('/api/get_answer', methods=['POST'])
352
  def api_get_answer():
353
+ """Endpoint principal pour obtenir une réponse."""
354
+ if any(x is None for x in [model_cross_encoder, model_paraphrase, collection, system_prompt, gemini_client]):
355
+ return jsonify({"error": "Ressources non chargées. Veuillez vérifier les logs d'initialisation."}), 500
356
+
357
+ try:
358
+ data = request.get_json()
359
+ query_text = data.get('query_text')
360
+ session_id = data.get('session_id', 'archive')
361
+
362
+ if not query_text:
363
+ generic_message = "Problème avec l'API, veuillez réessayer plus tard."
364
+ return jsonify({"error": generic_message}), 500
365
+
366
+ # Récupère historique
367
+ history = get_conversation_history(session_id)
368
+
369
+ # Génère prompt RAG
370
+ rag_prompt = get_answer(query_text, collection, model_paraphrase, model_cross_encoder, history)
371
+
372
+ # Appelle Gemini
373
+ response = call_gemini(rag_prompt, system_prompt, gemini_client)
374
+
375
+ # Sauvegarde réponse
376
+ add_to_history(session_id, "user", query_text)
377
+ add_to_history(session_id, "assistant", response)
378
+
379
+ return jsonify({"generated_response": response})
380
+
381
+ except Exception as e:
382
+ print(f"❌ Erreur générale de l'API: {e}")
383
+ generic_message = "Problème avec l'API, veuillez réessayer plus tard."
384
+ return jsonify({"error": generic_message}), 500
385
 
386
  @app.route('/api/clear_history', methods=['POST'])
387
  def api_clear_history():
388
+ """Efface l'historique d'une session."""
389
+ try:
390
+ data = request.get_json()
391
+ session_id = data.get('session_id', 'archive')
392
+ clear_history(session_id)
393
+
394
+ return jsonify({"message": f"Historique effacé: {session_id}"})
395
+ except Exception as e:
396
+ generic_message = "Problème avec l'API, veuillez réessayer plus tard."
397
+ return jsonify({"error": generic_message}), 500
398
 
399
  # ======================================================================
400
  # MAIN
401
  # ======================================================================
402
 
403
  if __name__ == '__main__':
404
+ print("start app.py")
405
+ if initialize_global_resources():
406
+
407
+ # Récupération de l'adresse IP si possible (pour l'affichage)
408
+ try:
409
+ import socket
410
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
411
+ s.connect(("8.8.8.8", 80)) # Connecte à un serveur externe pour trouver l'IP locale utilisée
412
+ local_ip = s.getsockname()[0]
413
+ s.close()
414
+ except Exception:
415
+ local_ip = "127.0.0.1" # Fallback si échec
416
+
417
+ print("\n" + "="*50)
418
+ print("🌐 SERVEUR DÉMARRÉ")
419
+ print(f"✅ API accessible à l'URL (via l'interface réseau locale): http://{local_ip}:{API_PORT}")
420
+ print(f"✅ Route Status: http://{local_ip}:{API_PORT}/status")
421
+ print(f"💡 N'oubliez pas de configurer 'app_port: 1212' et 'sdk: docker' dans votre README.md !")
422
+ print("="*50 + "\n")
423
+
424
+ # L'utilisation de host='0.0.0.0' dans app.run() permet l'accès depuis l'extérieur
425
+ app.run(host=API_HOST, port=API_PORT, debug=False)
426
+ else:
427
+ print("❌ Impossible de démarrer le serveur. Veuillez vérifier les logs pour les erreurs d'initialisation.")