Docfile commited on
Commit
10ad7a5
·
verified ·
1 Parent(s): c960c5d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +266 -402
app.py CHANGED
@@ -1,440 +1,304 @@
1
- from flask import Flask, render_template, request, jsonify, session
2
- import google.generativeai as genai
3
  import os
4
- from dotenv import load_dotenv
5
- import http.client
6
  import json
 
 
 
 
7
  from werkzeug.utils import secure_filename
8
- import uuid
9
- from PIL import Image # Required for checking image files if needed, or directly by genai
10
- import traceback # For detailed error logging
11
-
12
- # --- Configuration ---
13
- load_dotenv() # Charge les variables depuis le fichier .env
14
 
15
- # Check for essential environment variables
16
- GEMINI_API_KEY = os.getenv("GOOGLE_API_KEY")
17
- SERPER_API_KEY = "9b90a274d9e704ff5b21c0367f9ae1161779b573" # Clé pour la recherche web (optionnelle)
18
- FLASK_SECRET_KEY = os.getenv("FLASK_SECRET_KEY")
19
- # Validation critique des clés au démarrage
20
 
21
- # Configure Flask App
22
  app = Flask(__name__)
23
- app.secret_key = FLASK_SECRET_KEY
24
- app.config['UPLOAD_FOLDER'] = 'temp_uploads' # Dossier pour les uploads temporaires
25
- app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # Limite la taille des uploads à 100 Mo
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- # Assurer l'existence du dossier d'upload
28
- os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
 
 
 
 
29
 
30
- # Configure Google AI
31
- try:
32
- genai.configure(api_key=GEMINI_API_KEY)
33
- except Exception as e:
34
- raise RuntimeError(f"Échec de la configuration de l'API Google AI: {e}. Vérifiez votre GEMINI_API_KEY.")
35
-
36
-
37
- # Paramètres de Sécurité Gemini (Ajustez si nécessaire)
38
- safety_settings = [
39
- {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
40
- {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
41
- {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
42
- {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
43
- ]
44
-
45
- # Configuration de la Génération Gemini
46
- generation_config = {
47
- "temperature": 0.7,
48
- "top_p": 0.95,
49
- "top_k": 64,
50
- "max_output_tokens": 8192,
51
- "response_mime_type": "text/plain", # Demande une réponse texte simple
52
- }
53
-
54
- # Prompt Système (Instructions pour l'IA)
55
- SYSTEM_PROMPT = """
56
- Vous êtes Mariam, une assistante IA conversationnelle polyvalente conçue par Youssouf.
57
- Votre objectif est d'aider les utilisateurs de manière informative, créative et engageante.
58
- Répondez en français. Soyez concise mais complète.
59
- Si l'utilisateur télécharge des fichiers, prenez-les en compte dans votre réponse si pertinent pour la question posée.
60
- Si la recherche web est activée et que des résultats sont fournis via le prompt, utilisez-les pour enrichir votre réponse en citant brièvement les points clés trouvés.
61
- Formattez vos réponses en Markdown lorsque cela améliore la lisibilité (listes, blocs de code, gras, italique, etc.).
62
- Ne mentionnez pas explicitement les "Résultats de recherche web" dans votre réponse finale à moins que ce ne soit naturel dans le contexte. Intégrez l'information trouvée.
63
- """
64
-
65
- # Initialisation du Modèle Gemini
66
- try:
67
  model = genai.GenerativeModel(
68
- model_name="gemini-1.5-flash-latest", # Utilisation de la dernière version flash
 
69
  safety_settings=safety_settings,
70
- generation_config=generation_config,
71
- system_instruction=SYSTEM_PROMPT
 
72
  )
73
- print("Modèle Gemini initialisé avec succès.")
74
  except Exception as e:
75
- raise RuntimeError(f"Échec de l'initialisation du modèle Gemini: {e}")
76
-
77
 
78
- # Stockage en mémoire des sessions de chat (Pour la production, envisagez une solution persistante comme Redis ou une base de données)
79
- chat_sessions = {}
80
-
81
- # Extensions de fichiers autorisées pour l'upload
82
- ALLOWED_EXTENSIONS = {
83
- 'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp', # Texte et Images
84
- 'heic', 'heif', # Formats d'image Apple
85
- 'mp3', 'wav', 'ogg', 'flac', # Audio
86
- 'mp4', 'mov', 'avi', 'mkv', 'webm' # Vidéo
87
- }
88
 
89
  def allowed_file(filename):
90
- """Vérifie si l'extension du fichier est autorisée."""
91
  return '.' in filename and \
92
  filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
93
 
94
- # --- Fonctions Utilitaires ---
95
-
96
- def get_chat_session(session_id):
97
- """Récupère ou crée une session de chat Gemini pour l'ID de session Flask donné."""
98
- if session_id not in chat_sessions:
99
- print(f"Création d'une nouvelle session de chat Gemini pour Flask session ID: {session_id}")
100
- # L'historique est géré dynamiquement lors de l'envoi des messages
101
- chat_sessions[session_id] = model.start_chat(history=[])
102
- return chat_sessions[session_id]
103
-
104
  def perform_web_search(query):
105
- """Effectue une recherche web via l'API Serper.dev."""
106
- if not SERPER_API_KEY:
107
- print("Recherche web ignorée : SERPER_API_KEY non configurée.")
108
- return None # Retourne None si la clé n'est pas dispo
109
-
110
- conn = http.client.HTTPSConnection("google.serper.dev")
111
- # Payload pour la recherche en français
112
- payload = json.dumps({"q": query, "gl": "fr", "hl": "fr"})
113
  headers = {
114
- 'X-API-KEY': SERPER_API_KEY,
115
  'Content-Type': 'application/json'
116
  }
 
117
  try:
118
- print(f"Serper: Envoi de la requête pour '{query}'...")
119
- conn.request("POST", "/search", payload, headers)
120
- res = conn.getresponse()
121
- data_bytes = res.read()
122
- data_str = data_bytes.decode("utf-8")
123
- print(f"Serper: Réponse reçue - Statut: {res.status}")
124
-
125
- if res.status == 200:
126
- print("Serper: Recherche réussie.")
127
- return json.loads(data_str)
128
- else:
129
- print(f"Serper: Erreur API - Statut {res.status}, Réponse: {data_str}")
130
- return {"error": f"L'API de recherche a échoué avec le statut {res.status}"}
131
- except http.client.HTTPException as http_err:
132
- print(f"Erreur HTTP lors de la recherche web: {http_err}")
133
- return {"error": f"Erreur de connexion HTTP: {str(http_err)}"}
134
- except json.JSONDecodeError as json_err:
135
- print(f"Erreur de décodage JSON de la réponse Serper: {json_err}")
136
- print(f"Réponse brute reçue: {data_str}")
137
- return {"error": "Impossible de lire la réponse de l'API de recherche."}
138
- except Exception as e:
139
- print(f"Erreur inattendue lors de la recherche web: {e}")
140
- traceback.print_exc()
141
- return {"error": f"Exception lors de la recherche web: {str(e)}"}
142
- finally:
143
- conn.close()
144
-
145
- def format_search_results_for_prompt(data):
146
- """Formate les résultats de recherche de manière concise pour les injecter dans le prompt de l'IA."""
147
- if not data or data.get("searchParameters", {}).get("q") is None:
148
- # Si data est vide ou ne semble pas être une réponse valide
149
- print("Formatage recherche: Données invalides ou vides reçues.")
150
- return "La recherche web n'a pas retourné de résultats exploitables."
151
- if "error" in data:
152
- print(f"Formatage recherche: Erreur détectée dans les données - {data['error']}")
153
- return f"Note : La recherche web a rencontré une erreur ({data['error']})."
154
-
155
- results = []
156
- query = data.get("searchParameters", {}).get("q", "Terme inconnu")
157
- results.append(f"Résultats de recherche web pour '{query}':")
158
-
159
- # Boîte de réponse / Knowledge Graph (si présents)
160
- if 'answerBox' in data:
161
- ab = data['answerBox']
162
- answer = ab.get('answer') or ab.get('snippet') or ab.get('title')
163
- if answer:
164
- results.append(f"- Réponse directe trouvée : {answer}")
165
- elif 'knowledgeGraph' in data:
166
  kg = data['knowledgeGraph']
167
- description = kg.get('description')
168
- if description:
169
- results.append(f"- Info (Knowledge Graph) : {kg.get('title', '')} - {description}")
170
-
171
- # Résultats Organiques (naturels)
172
- if 'organic' in data and data['organic']:
173
- results.append("- Résultats principaux :")
174
- for i, item in enumerate(data['organic'][:3], 1): # Limite aux 3 premiers
175
- title = item.get('title', 'Sans titre')
176
- snippet = item.get('snippet', 'Pas de description')
177
- link = item.get('link', '#')
178
- results.append(f" {i}. {title}: {snippet} (Source: {link})")
179
- elif not results: # Si ni answerbox/KG ni organic n'ont donné qqch
180
- return f"Aucun résultat de recherche web pertinent trouvé pour '{query}'."
181
 
 
 
 
 
182
 
183
- print(f"Formatage recherche: {len(results)-1} éléments formatés pour le prompt.")
184
- return "\n".join(results)
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
  # --- Routes Flask ---
188
 
189
- @app.route('/')
190
- def home():
191
- """Affiche l'interface de chat principale."""
192
- # Initialise la session Flask si elle n'existe pas
193
- if 'session_id' not in session:
194
- session_id = str(uuid.uuid4())
195
- session['session_id'] = session_id
196
- session['messages'] = [] # Historique pour l'affichage client
197
- session['uploaded_files_gemini'] = [] # Stocke les objets File retournés par genai.upload_file
198
- session.modified = True # Marque la session comme modifiée
199
- print(f"Nouvelle session Flask créée: {session_id}")
200
- else:
201
- # Assure que les clés nécessaires existent même si la session existe déjà
202
- if 'messages' not in session: session['messages'] = []
203
- if 'uploaded_files_gemini' not in session: session['uploaded_files_gemini'] = []
204
-
205
-
206
- # Récupère les messages de la session pour les passer au template
207
- messages_for_template = session.get('messages', [])
208
- return render_template('index.html', messages=messages_for_template)
209
-
210
- @app.route('/send_message', methods=['POST'])
211
- def send_message():
212
- """Gère l'envoi d'un message à l'IA et retourne sa réponse."""
213
- # Vérifie si la session utilisateur est valide
214
- if 'session_id' not in session:
215
- print("Erreur send_message: ID de session manquant.")
216
- return jsonify({'error': 'Session expirée ou invalide. Veuillez rafraîchir la page.'}), 400
217
-
218
- session_id = session['session_id']
219
- print(f"\n--- Requête /send_message reçue (Session: {session_id}) ---")
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  try:
222
- data = request.json
223
- user_message_text = data.get('message', '').strip()
224
- web_search_enabled = data.get('web_search', False)
225
-
226
- # Vérifie si le message est vide (sauf si des fichiers sont attachés)
227
- uploaded_files = session.get('uploaded_files_gemini', [])
228
- if not user_message_text and not uploaded_files:
229
- print("Avertissement send_message: Message vide et aucun fichier attaché.")
230
- return jsonify({'error': 'Veuillez entrer un message ou joindre un fichier.'}), 400
231
-
232
- # Récupère ou crée la session de chat Gemini associée
233
- chat_gemini = get_chat_session(session_id)
234
-
235
- # --- Préparation du contenu pour Gemini ---
236
- prompt_parts = [] # Liste pour contenir texte, fichiers, résultats web
237
-
238
- # 1. Ajouter les fichiers uploadés (depuis la session)
239
- # On utilise directement les objets File retournés par genai.upload_file
240
- if uploaded_files:
241
- prompt_parts.extend(uploaded_files) # Ajoute les objets File à la liste
242
- file_names = [f.display_name for f in uploaded_files] # Noms pour log
243
- print(f"Ajout au prompt des fichiers Gemini: {', '.join(file_names)}")
244
- # Vider la liste des fichiers après les avoir ajoutés au prompt de CE message
245
- # Permet d'associer les fichiers au message en cours uniquement
246
- session['uploaded_files_gemini'] = []
247
- session.modified = True
248
- print("Liste des fichiers uploadés vidée pour le prochain message.")
249
-
250
-
251
- # 2. Ajouter les résultats de recherche web (si activé et clé API dispo)
252
- search_prompt_text = "" # Texte à ajouter au prompt textuel
253
- if web_search_enabled and SERPER_API_KEY:
254
- print(f"Recherche web activée pour: '{user_message_text}'")
255
- web_results_data = perform_web_search(user_message_text)
256
- if web_results_data:
257
- formatted_results = format_search_results_for_prompt(web_results_data)
258
- search_prompt_text = f"\n\n--- Informations issues de la recherche web ---\n{formatted_results}\n--- Fin des informations de recherche ---\n"
259
- print("Résultats de recherche formatés ajoutés au contexte.")
260
- else:
261
- search_prompt_text = "\n\n(Note: La recherche web a été tentée mais n'a pas retourné de résultats ou a échoué.)\n"
262
- print("Aucun résultat de recherche web ou échec.")
263
-
264
-
265
- # 3. Ajouter le message texte de l'utilisateur (incluant le contexte de recherche si existant)
266
- final_user_text = user_message_text + search_prompt_text
267
- prompt_parts.append(final_user_text) # Ajoute le texte (potentiellement enrichi)
268
- print(f"Texte final envoyé à Gemini:\n{final_user_text[:500]}...") # Log tronqué
269
-
270
- # --- Envoi à l'API Gemini ---
271
- print(f"Envoi de {len(prompt_parts)} partie(s) à l'API Gemini...")
272
- ai_response_text = ""
273
- try:
274
- # Envoie la liste complète des parties (fichiers + texte)
275
- response = chat_gemini.send_message(prompt_parts)
276
- ai_response_text = response.text # Récupère le texte de la réponse
277
- print("Réponse reçue de Gemini.")
278
-
279
- # Vérifier si la réponse a été bloquée par les filtres de sécurité
280
- if response.prompt_feedback.block_reason:
281
- block_reason = response.prompt_feedback.block_reason
282
- print(f"AVERTISSEMENT: Réponse bloquée par Gemini. Raison: {block_reason}")
283
- ai_response_text = f"⚠️ Ma réponse a été bloquée en raison des filtres de sécurité (Raison: {block_reason}). Veuillez reformuler votre demande."
284
-
285
- except Exception as e:
286
- print(f"ERREUR lors de l'appel à Gemini API: {e}")
287
- traceback.print_exc() # Log détaillé de l'erreur serveur
288
- # Essaye de donner une erreur plus spécifique si possible
289
- error_details = str(e)
290
- ai_response_text = f"❌ Désolé, une erreur est survenue lors de la communication avec l'IA. Détails techniques: {error_details}"
291
-
292
-
293
- # --- Mise à jour de l'historique de session Flask (pour l'affichage) ---
294
- # Stocke le message *original* de l'utilisateur et la réponse de l'IA
295
- current_messages = session.get('messages', [])
296
- # !! Ne pas stocker le prompt enrichi dans l'historique affiché !!
297
- current_messages.append({'role': 'user', 'content': user_message_text}) # Message original
298
- current_messages.append({'role': 'assistant', 'content': ai_response_text}) # Réponse IA
299
- session['messages'] = current_messages
300
  session.modified = True
301
- print("Historique de session Flask mis à jour.")
302
 
303
- # --- Retourner la réponse de l'IA au client ---
304
- print(f"--- Fin Requête /send_message (Session: {session_id}) ---")
305
- return jsonify({'response': ai_response_text})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  except Exception as e:
308
- print(f"ERREUR Inattendue dans /send_message: {e}")
309
- traceback.print_exc()
310
- return jsonify({'error': f'Erreur interne du serveur: {str(e)}'}), 500
311
-
312
-
313
- @app.route('/upload', methods=['POST'])
314
- def upload_file():
315
- """Gère l'upload de fichiers, les enregistre temporairement et les prépare pour Gemini."""
316
- if 'session_id' not in session:
317
- print("Erreur upload: ID de session manquant.")
318
- return jsonify({'error': 'Session expirée ou invalide. Veuillez rafraîchir la page.'}), 400
319
-
320
- session_id = session['session_id']
321
- print(f"\n--- Requête /upload reçue (Session: {session_id}) ---")
322
-
323
- if 'file' not in request.files:
324
- print("Erreur upload: 'file' manquant dans request.files.")
325
- return jsonify({'error': 'Aucun fichier trouvé dans la requête.'}), 400
326
-
327
- file = request.files['file']
328
- if file.filename == '':
329
- print("Erreur upload: Nom de fichier vide.")
330
- return jsonify({'error': 'Aucun fichier sélectionné.'}), 400
331
-
332
- if file and allowed_file(file.filename):
333
- filename = secure_filename(file.filename)
334
- # Crée un chemin de sauvegarde temporaire unique (évite écrasement si même nom)
335
- temp_filename = f"{session_id}_{uuid.uuid4().hex}_{filename}"
336
- save_path = os.path.join(app.config['UPLOAD_FOLDER'], temp_filename)
337
-
338
- try:
339
- print(f"Sauvegarde du fichier '{filename}' vers '{save_path}'...")
340
- file.save(save_path)
341
- print(f"Fichier sauvegardé localement.")
342
-
343
- # Uploader le fichier vers Google AI Studio (API Gemini)
344
- print(f"Upload de '{filename}' vers Google AI Studio...")
345
- # display_name est optionnel mais utile pour le débogage ou l'affichage
346
- gemini_file_object = genai.upload_file(path=save_path, display_name=filename)
347
- print(f"Fichier uploadé vers Gemini. Référence obtenue: {gemini_file_object.name}") # L'objet a un attribut 'name'
348
-
349
- # Stocker l'objet File retourné par Gemini dans la session Flask
350
- # C'est cet objet qui sera utilisé directement dans send_message
351
- current_uploads = session.get('uploaded_files_gemini', [])
352
- current_uploads.append(gemini_file_object)
353
- session['uploaded_files_gemini'] = current_uploads
354
- session.modified = True
355
- print(f"Objet Fichier Gemini ajouté à la session Flask (Total: {len(current_uploads)}).")
356
-
357
- # Supprimer le fichier local temporaire après l'upload réussi vers Gemini
358
- try:
359
- os.remove(save_path)
360
- print(f"Fichier local temporaire '{save_path}' supprimé.")
361
- except OSError as e:
362
- print(f"AVERTISSEMENT: Échec de la suppression du fichier local '{save_path}': {e}")
363
-
364
- print(f"--- Fin Requête /upload (Session: {session_id}) - Succès ---")
365
- return jsonify({'success': True, 'filename': filename}) # Renvoie le nom original pour l'UI
366
-
367
- except Exception as e:
368
- print(f"ERREUR lors de l'upload ou du traitement Gemini: {e}")
369
- traceback.print_exc()
370
- # Nettoyer le fichier local s'il existe encore en cas d'erreur
371
- if os.path.exists(save_path):
372
- try:
373
- os.remove(save_path)
374
- print(f"Nettoyage: Fichier local '{save_path}' supprimé après erreur.")
375
- except OSError as rm_err:
376
- print(f"Erreur lors du nettoyage du fichier '{save_path}': {rm_err}")
377
- print(f"--- Fin Requête /upload (Session: {session_id}) - Échec ---")
378
- return jsonify({'error': f'Échec de l\'upload ou du traitement du fichier: {str(e)}'}), 500
379
- else:
380
- print(f"Erreur upload: Type de fichier non autorisé - '{file.filename}'")
381
- return jsonify({'error': 'Type de fichier non autorisé.'}), 400
382
-
383
- @app.route('/clear_chat', methods=['POST'])
384
  def clear_chat():
385
- """Efface l'historique de chat et les fichiers uploadés pour la session en cours."""
386
- if 'session_id' not in session:
387
- print("Avertissement clear_chat: Pas de session à effacer.")
388
- return jsonify({'success': True}) # Pas d'erreur si pas de session
389
-
390
- session_id = session['session_id']
391
- print(f"\n--- Requête /clear_chat reçue (Session: {session_id}) ---")
392
-
393
- # 1. Effacer la session de chat Gemini en mémoire (si elle existe)
394
- if session_id in chat_sessions:
395
- del chat_sessions[session_id]
396
- print(f"Session de chat Gemini pour {session_id} effacée de la mémoire.")
397
-
398
- # 2. Effacer les données pertinentes de la session Flask
399
- session['messages'] = []
400
- files_to_delete = session.get('uploaded_files_gemini', [])
401
- session['uploaded_files_gemini'] = []
402
- session.modified = True
403
- print("Historique des messages et références de fichiers effacés de la session Flask.")
404
-
405
- # 3. (Optionnel mais recommandé) Supprimer les fichiers uploadés correspondants depuis Google AI Studio
406
- # Cela nécessite de stocker les 'name' (IDs) des fichiers Gemini et d'appeler genai.delete_file()
407
- # Pour l'instant, on efface juste la référence dans la session Flask.
408
- # L'API delete_file est importante pour gérer l'espace de stockage Gemini.
409
- if files_to_delete:
410
- print(f"Tentative de suppression de {len(files_to_delete)} fichier(s) sur Google AI Studio...")
411
- for file_obj in files_to_delete:
412
- try:
413
- print(f" Suppression de {file_obj.display_name} (ID: {file_obj.name})...")
414
- genai.delete_file(file_obj.name)
415
- print(f" Fichier {file_obj.name} supprimé de Google AI.")
416
- except Exception as e:
417
- print(f" ERREUR lors de la suppression du fichier {file_obj.name} de Google AI: {e}")
418
- # Continuer même si un fichier ne peut être supprimé
419
-
420
-
421
- print(f"--- Fin Requête /clear_chat (Session: {session_id}) ---")
422
- return jsonify({'success': True})
423
-
424
-
425
- # --- Démarrage de l'Application ---
426
  if __name__ == '__main__':
427
- # Rappel des dépendances:
428
- # pip install Flask python-dotenv google-generativeai Pillow werkzeug requests gunicorn
429
- print("\n" + "="*40)
430
- print(" Démarrage du serveur Flask Assistant IA ")
431
- print("="*40)
432
- print("Vérifiez que votre fichier .env contient :")
433
- print(" - GEMINI_API_KEY=VOTRE_CLE_GEMINI")
434
- print(" - FLASK_SECRET_KEY=VOTRE_CLE_SECRET_FLASK")
435
- print(" - SERPER_API_KEY=VOTRE_CLE_SERPER (Optionnel, pour recherche web)")
436
- print("-"*40)
437
- # Utiliser host='0.0.0.0' pour rendre accessible sur le réseau local
438
- # debug=True active le rechargement automatique et le débogueur (NE PAS UTILISER EN PRODUCTION)
439
- # Utiliser Gunicorn ou un autre serveur WSGI pour la production
440
- app.run(debug=True, host='0.0.0.0', port=5000)
 
 
 
1
  import os
 
 
2
  import json
3
+ from flask import Flask, render_template, request, session, redirect, url_for, flash
4
+ from dotenv import load_dotenv
5
+ import google.generativeai as genai
6
+ import requests # Pour Serper API
7
  from werkzeug.utils import secure_filename
8
+ import mimetypes # Pour vérifier le type de fichier
 
 
 
 
 
9
 
10
+ load_dotenv()
 
 
 
 
11
 
 
12
  app = Flask(__name__)
13
+ # Très important pour utiliser les sessions Flask !
14
+ app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-clé-secrète-par-défaut-pour-dev')
15
+ # Configuration pour les uploads
16
+ UPLOAD_FOLDER = 'temp'
17
+ ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'} # Extensions autorisées
18
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
19
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # Limite de taille (ex: 16MB)
20
+
21
+ # Créer le dossier temp s'il n'existe pas
22
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
23
+
24
+ # Configuration de l'API Gemini
25
+ try:
26
+ genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
27
 
28
+ safety_settings = [
29
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
30
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
31
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
32
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
33
+ ]
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  model = genai.GenerativeModel(
36
+ 'gemini-1.5-flash', # Utiliser un modèle stable recommandé si 'gemini-2.0-flash-exp' cause problème
37
+ # 'gemini-1.5-pro-latest', # Alternative plus puissante
38
  safety_settings=safety_settings,
39
+ system_instruction="Tu es un assistant intelligent. ton but est d'assister au mieux que tu peux. tu as été créé par Aenir et tu t'appelles Mariam"
40
+ # Note: 'tools' n'est pas directement utilisé ici comme dans Streamlit,
41
+ # la logique de recherche web est gérée manuellement.
42
  )
43
+ print("Modèle Gemini chargé.")
44
  except Exception as e:
45
+ print(f"Erreur lors de la configuration de Gemini : {e}")
46
+ model = None # Empêche l'app de crasher si l'API key est mauvaise
47
 
48
+ # --- Fonctions Utilitaires ---
 
 
 
 
 
 
 
 
 
49
 
50
  def allowed_file(filename):
 
51
  return '.' in filename and \
52
  filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
53
 
 
 
 
 
 
 
 
 
 
 
54
  def perform_web_search(query):
55
+ """Effectue une recherche web via l'API Serper."""
56
+ conn_key = "9b90a274d9e704ff5b21c0367f9ae1161779b573"
57
+ if not conn_key:
58
+ print("Clé API SERPER manquante dans .env")
59
+ return None
60
+ search_url = "https://google.serper.dev/search"
 
 
61
  headers = {
62
+ 'X-API-KEY': conn_key,
63
  'Content-Type': 'application/json'
64
  }
65
+ payload = json.dumps({"q": query})
66
  try:
67
+ response = requests.post(search_url, headers=headers, data=payload, timeout=10)
68
+ response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP
69
+ data = response.json()
70
+ print("Résultats de recherche obtenus.")
71
+ return data
72
+ except requests.exceptions.RequestException as e:
73
+ print(f"Erreur lors de la recherche web : {e}")
74
+ return None
75
+ except json.JSONDecodeError as e:
76
+ print(f"Erreur lors du décodage de la réponse JSON de Serper : {e}")
77
+ print(f"Réponse reçue : {response.text}")
78
+ return None
79
+
80
+ def format_search_results(data):
81
+ """Met en forme les résultats de recherche pour le prompt Gemini."""
82
+ if not data:
83
+ return "Aucun résultat de recherche trouvé."
84
+
85
+ result = "Résultats de recherche web :\n"
86
+
87
+ if 'knowledgeGraph' in data:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  kg = data['knowledgeGraph']
89
+ result += f"\n## Graphe de connaissances :\n"
90
+ result += f"### {kg.get('title', '')} ({kg.get('type', '')})\n"
91
+ result += f"{kg.get('description', '')}\n"
92
+ if 'attributes' in kg:
93
+ for attr, value in kg['attributes'].items():
94
+ result += f"- {attr}: {value}\n"
 
 
 
 
 
 
 
 
95
 
96
+ if 'answerBox' in data:
97
+ ab = data['answerBox']
98
+ result += f"\n## Réponse directe :\n"
99
+ result += f"{ab.get('title','')}\n{ab.get('snippet') or ab.get('answer','')}\n"
100
 
 
 
101
 
102
+ if 'organic' in data and data['organic']:
103
+ result += "\n## Résultats principaux :\n"
104
+ for i, item in enumerate(data['organic'][:3], 1): # Limite aux 3 premiers
105
+ result += f"{i}. **{item.get('title', 'N/A')}**\n"
106
+ result += f" {item.get('snippet', 'N/A')}\n"
107
+ result += f" Lien : {item.get('link', '#')}\n\n"
108
+
109
+ if 'peopleAlsoAsk' in data and data['peopleAlsoAsk']:
110
+ result += "## Questions fréquentes :\n"
111
+ for i, item in enumerate(data['peopleAlsoAsk'][:2], 1): # Limite aux 2 premières
112
+ result += f"{i}. **{item.get('question', 'N/A')}**\n"
113
+ # result += f" {item.get('snippet', 'N/A')}\n\n" # Le snippet est souvent redondant
114
+
115
+ return result
116
+
117
+ def prepare_gemini_history(chat_history):
118
+ """Convertit l'historique stocké en session au format attendu par Gemini API."""
119
+ gemini_history = []
120
+ for message in chat_history:
121
+ role = 'user' if message['role'] == 'user' else 'model'
122
+ parts = [message['text']]
123
+ if message.get('gemini_file'): # Ajoute la référence au fichier si présente
124
+ parts.insert(0, message['gemini_file']) # Met le fichier avant le texte
125
+ gemini_history.append({'role': role, 'parts': parts})
126
+ return gemini_history
127
 
128
  # --- Routes Flask ---
129
 
130
+ @app.route('/', methods=['GET'])
131
+ def index():
132
+ """Affiche la page principale du chat."""
133
+ if 'chat_history' not in session:
134
+ session['chat_history'] = [] # Initialise l'historique
135
+ if 'web_search' not in session:
136
+ session['web_search'] = False # Initialise l'état de la recherche web
137
+
138
+ return render_template(
139
+ 'index.html',
140
+ chat_history=session['chat_history'],
141
+ web_search_active=session['web_search'],
142
+ error=session.pop('error', None), # Récupère et supprime l'erreur s'il y en a une
143
+ processing_message=session.pop('processing', False) # Indicateur de traitement
144
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
+ @app.route('/chat', methods=['POST'])
147
+ def chat():
148
+ """Gère la soumission du formulaire de chat."""
149
+ if not model:
150
+ session['error'] = "Le modèle Gemini n'a pas pu être chargé. Vérifiez la clé API et la configuration."
151
+ return redirect(url_for('index'))
152
+
153
+ prompt = request.form.get('prompt', '').strip()
154
+ # Met à jour l'état de la recherche web depuis la checkbox
155
+ session['web_search'] = 'web_search' in request.form
156
+ file = request.files.get('file')
157
+ uploaded_gemini_file = None
158
+ user_message_content = {'role': 'user', 'text': prompt}
159
+
160
+ if not prompt and not file:
161
+ session['error'] = "Veuillez entrer un message ou uploader un fichier."
162
+ return redirect(url_for('index'))
163
+
164
+ # --- Gestion de l'upload de fichier ---
165
+ if file and file.filename != '':
166
+ if allowed_file(file.filename):
167
+ try:
168
+ # Sécurise le nom du fichier et détermine le chemin complet
169
+ filename = secure_filename(file.filename)
170
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
171
+ file.save(filepath)
172
+ print(f"Fichier sauvegardé temporairement : {filepath}")
173
+
174
+ # Essayer d'uploader vers Gemini
175
+ print("Upload vers Gemini en cours...")
176
+ # Détecter le mime type pour l'upload Gemini
177
+ mime_type = mimetypes.guess_type(filepath)[0]
178
+ if not mime_type:
179
+ # Fallback si la détection échoue
180
+ mime_type = 'application/octet-stream'
181
+ print(f"Impossible de deviner le mime_type pour {filename}, utilisation de {mime_type}")
182
+
183
+ gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type)
184
+ uploaded_gemini_file = gemini_file_obj # Garder l'objet fichier pour l'API
185
+ user_message_content['gemini_file'] = uploaded_gemini_file # Stocke la référence pour l'historique Gemini
186
+ user_message_content['text'] = f"[Fichier: {filename}]\n\n{prompt}" # Modifie le texte affiché
187
+ print(f"Fichier {filename} uploadé avec succès vers Gemini. MimeType: {mime_type}")
188
+
189
+ # Optionnel: Supprimer le fichier local après upload vers Gemini
190
+ # try:
191
+ # os.remove(filepath)
192
+ # print(f"Fichier temporaire {filepath} supprimé.")
193
+ # except OSError as e:
194
+ # print(f"Erreur lors de la suppression du fichier temporaire {filepath}: {e}")
195
+
196
+
197
+ except Exception as e:
198
+ print(f"Erreur lors du traitement ou de l'upload du fichier : {e}")
199
+ session['error'] = f"Erreur lors du traitement du fichier : {e}"
200
+ # Ne pas bloquer si l'upload échoue, on peut continuer sans le fichier
201
+ # return redirect(url_for('index')) # Décommentez si l'upload doit être bloquant
202
+ else:
203
+ session['error'] = "Type de fichier non autorisé."
204
+ return redirect(url_for('index'))
205
+ elif file and file.filename == '':
206
+ # Si un champ fichier existe mais est vide, on l'ignore simplement
207
+ pass
208
+
209
+ # --- Ajout du message utilisateur à l'historique de session ---
210
+ if prompt or uploaded_gemini_file: # N'ajoute que s'il y a du contenu
211
+ # Version simplifiée pour l'affichage HTML (sans l'objet gemini_file)
212
+ display_history_message = {'role': 'user', 'text': user_message_content['text']}
213
+ session['chat_history'].append(display_history_message)
214
+ session.modified = True # Important car on modifie une liste dans la session
215
+
216
+ # --- Préparation et Envoi à Gemini ---
217
  try:
218
+ # Indiquer qu'un traitement est en cours
219
+ session['processing'] = True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  session.modified = True
 
221
 
222
+ # Construire le prompt final (avec recherche web si activée)
223
+ final_prompt_parts = []
224
+ if uploaded_gemini_file:
225
+ final_prompt_parts.append(uploaded_gemini_file)
226
+
227
+ current_prompt_text = prompt
228
+
229
+ # Effectuer la recherche web si activée ET si un prompt textuel existe
230
+ if session['web_search'] and prompt:
231
+ print("Recherche web activée pour le prompt:", prompt)
232
+ # Afficher l'indicateur de recherche
233
+ # Note: dans Flask simple, l'indicateur ne sera visible qu'APRES le rechargement.
234
+ # Pour un affichage dynamique, il faudrait JS/AJAX.
235
+ # On peut passer une variable au template pour l'afficher avant la réponse.
236
+ session['processing_web_search'] = True # Indicateur spécifique
237
+ session.modified = True
238
+
239
+ web_results = perform_web_search(prompt)
240
+ if web_results:
241
+ formatted_results = format_search_results(web_results)
242
+ current_prompt_text = f"Question originale: {prompt}\n\n{formatted_results}\n\nBasé sur ces informations et ta connaissance générale, réponds à la question originale."
243
+ print("Prompt modifié avec les résultats de recherche.")
244
+ else:
245
+ print("Aucun résultat de recherche web obtenu ou erreur.")
246
+ # On continue avec le prompt original
247
+
248
+ final_prompt_parts.append(current_prompt_text)
249
+
250
+ # Préparer l'historique complet pour Gemini
251
+ gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le dernier message utilisateur (qui est dans final_prompt_parts)
252
+
253
+ print(f"\n--- Envoi à Gemini ---")
254
+ print(f"Historique envoyé: {len(gemini_history)} messages")
255
+ print(f"Parties du prompt actuel: {len(final_prompt_parts)}")
256
+ # print(f"Prompt textuel final: {current_prompt_text[:200]}...") # Afficher début du prompt
257
+ # print("----------------------\n")
258
+
259
+ # Appel à Gemini API
260
+ # Utiliser generate_content qui prend l'historique + le nouveau message
261
+ full_conversation = gemini_history + [{'role': 'user', 'parts': final_prompt_parts}]
262
+ response = model.generate_content(full_conversation)
263
+
264
+ # --- Traitement de la réponse ---
265
+ response_text = response.text
266
+ print(f"\n--- Réponse de Gemini ---")
267
+ print(response_text[:500] + ('...' if len(response_text) > 500 else ''))
268
+ # print("-------------------------\n")
269
+
270
+ # Ajout de la réponse à l'historique de session
271
+ session['chat_history'].append({'role': 'assistant', 'text': response_text})
272
+ session.modified = True
273
 
274
  except Exception as e:
275
+ print(f"Erreur lors de l'appel à Gemini : {e}")
276
+ session['error'] = f"Une erreur s'est produite lors de la communication avec l'IA : {e}"
277
+ # Si une erreur survient, on retire le message utilisateur qui a causé l'erreur
278
+ # pour éviter de le renvoyer indéfiniment.
279
+ if session['chat_history'] and session['chat_history'][-1]['role'] == 'user':
280
+ session['chat_history'].pop()
281
+ session.modified = True
282
+
283
+ finally:
284
+ # Indiquer que le traitement est terminé
285
+ session['processing'] = False
286
+ session.pop('processing_web_search', None) # Nettoyer l'indicateur de recherche
287
+ session.modified = True
288
+
289
+
290
+ return redirect(url_for('index')) # Redirige vers la page principale pour afficher le nouvel état
291
+
292
+ @app.route('/clear', methods=['POST'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  def clear_chat():
294
+ """Efface l'historique de la conversation."""
295
+ session.pop('chat_history', None) # Supprime l'historique de la session
296
+ session.pop('web_search', None) # Réinitialise aussi le toggle web
297
+ print("Historique de chat effacé.")
298
+ return redirect(url_for('index')) # Redirige vers la page d'accueil
299
+
300
+ # --- Démarrage de l'application ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  if __name__ == '__main__':
302
+ # Utiliser host='0.0.0.0' pour rendre l'app accessible sur le réseau local
303
+ # Debug=True est utile pour le développement, mais à désactiver en production
304
+ app.run(debug=True, host='0.0.0.0', port=5001)