Docfile commited on
Commit
b212844
·
verified ·
1 Parent(s): a08ef55

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +303 -325
app.py CHANGED
@@ -1,209 +1,171 @@
1
  import os
2
  import json
3
  import mimetypes
4
- import time
5
- import re # Pour la détection d'URL YouTube
6
- import traceback # Ajout pour afficher les tracebacks complets
7
-
8
  from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template
9
  from dotenv import load_dotenv
10
- # Importe genai et types séparément pour plus de clarté
11
- from google import genai
12
- from google.genai import types # Important pour Part, FileData, SafetySetting etc.
13
- # L'exception StopCandidateException est directement sous genai
14
- # from google.generativeai.types import StopCandidateException # Ne fonctionne pas
15
-
16
  import requests
17
  from werkzeug.utils import secure_filename
18
- import markdown # Pour convertir la réponse Markdown en HTML
19
 
20
  # --- Configuration Initiale ---
21
-
22
- load_dotenv() # Charge les variables depuis .env
23
 
24
  app = Flask(__name__)
25
 
26
- # Clé secrète FORTEMENT recommandée pour la sécurité des sessions
27
- app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-cle-secrete-tres-difficile-a-deviner')
28
 
29
  # Configuration pour les uploads
30
  UPLOAD_FOLDER = 'temp'
31
-
32
- # Extensions autorisées (incluant vidéo)
33
- ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'mp4', 'mov', 'avi', 'mkv', 'webm'}
34
- VIDEO_EXTENSIONS = {'mp4', 'mov', 'avi', 'mkv', 'webm'} # Pour identifier les vidéos
35
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
 
36
 
37
- # Augmenter la limite pour les vidéos (ex: 100MB) - Ajustez si nécessaire
38
- app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
39
-
40
- # Crée le dossier temporaire s'il n'existe pas
41
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
42
  print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
43
 
44
  # --- Configuration de l'API Gemini ---
45
- # Utilisez les noms de modèles spécifiés
46
- MODEL_FLASH = 'gemini-2.0-flash'
47
- MODEL_PRO = 'gemini-2.5-pro-exp-03-25' # Pro est souvent nécessaire/meilleur pour la vidéo
48
-
49
- # Instruction système pour le modèle
50
- SYSTEM_INSTRUCTION = "Tu es un assistant intelligent et amical nommé Mariam. Tu assistes les utilisateurs au mieux de tes capacités, y compris dans l'analyse de texte, d'images et de vidéos (via upload ou lien YouTube). Tu as été créé par Aenir."
51
-
52
- # Paramètres de sécurité (structure correcte pour GenerateContentConfig)
53
- SAFETY_SETTINGS_CONFIG = [
54
- types.SafetySetting(
55
- category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
56
- threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
57
- ),
58
- types.SafetySetting(
59
- category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
60
- threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
61
- ),
62
- types.SafetySetting(
63
- category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
64
- threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
65
- ),
66
- types.SafetySetting(
67
- category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
68
- threshold=types.HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
69
- )
70
  ]
71
-
72
-
73
  GEMINI_CONFIGURED = False
74
- gemini_client = None # Client API
75
-
76
  try:
77
  gemini_api_key = os.getenv("GOOGLE_API_KEY")
78
  if not gemini_api_key:
79
- raise ValueError("Clé API GOOGLE_API_KEY manquante dans le fichier .env")
80
-
81
- # Initialise le client
82
- gemini_client = genai.Client(api_key=gemini_api_key)
83
-
84
- # Vérifie si les modèles requis sont disponibles en utilisant le client
85
- print("Vérification des modèles Gemini disponibles...")
86
- available_models_full_names = [m.name for m in gemini_client.models.list()] # Correction: client.models.list()
87
- required_models_prefixes = [MODEL_FLASH, MODEL_PRO]
88
- models_found = {req: False for req in required_models_prefixes}
89
- for available in available_models_full_names:
90
- for req in required_models_prefixes:
91
- if available.startswith(f'models/{req}'):
92
- models_found[req] = True
93
-
94
- if all(models_found.values()):
95
- print(f"Configuration Gemini effectuée. Modèles requis ({', '.join(required_models_prefixes)}) trouvés.")
96
- print(f"Instruction Système: {SYSTEM_INSTRUCTION}")
97
- GEMINI_CONFIGURED = True
98
  else:
99
- missing = [req for req, found in models_found.items() if not found]
100
- raise ValueError(f"Les modèles Gemini requis suivants (ou commençant par) sont manquants : {missing}")
101
-
 
 
 
 
 
 
 
 
102
 
103
  except Exception as e:
104
  print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
105
- traceback.print_exc() # Affiche le traceback pour l'erreur d'init
106
  print("L'application fonctionnera sans les fonctionnalités IA.")
107
- gemini_client = None
108
- GEMINI_CONFIGURED = False
109
 
110
  # --- Fonctions Utilitaires ---
111
 
112
- def get_file_extension(filename):
113
- """Retourne l'extension du fichier en minuscules, ou None."""
114
- return filename.rsplit('.', 1)[1].lower() if '.' in filename else None
115
-
116
  def allowed_file(filename):
117
- """Vérifie si l'extension du fichier est dans la liste autorisée."""
118
- return get_file_extension(filename) in ALLOWED_EXTENSIONS
119
-
120
- def is_video_file(filename):
121
- """Vérifie si l'extension correspond à un type vidéo connu."""
122
- return get_file_extension(filename) in VIDEO_EXTENSIONS
123
-
124
- def is_youtube_url(url):
125
- """Vérifie si la chaîne ressemble à une URL YouTube valide."""
126
- if not url: return False
127
- youtube_regex = re.compile(
128
- r'(https?://)?(www.)?'
129
- r'(youtube|youtu|youtube-nocookie).(com|be)/'
130
- r'(watch?v=|embed/|v/|.+?v=)?'
131
- r'([^&=%\?]{11})')
132
- return youtube_regex.match(url) is not None
133
-
134
- # --- Fonction d'Upload Vidéo avec Polling ---
135
-
136
- def upload_video_with_polling(filepath, mime_type, max_wait_seconds=300, poll_interval=10):
137
- """Upload une vidéo via client.files.upload et attend son traitement."""
138
- if not gemini_client:
139
- raise ConnectionError("Le client Gemini n'est pas initialisé.")
140
-
141
- print(f"Début de l'upload vidéo via client.files: {filepath} ({mime_type})")
142
- video_file = None
143
- try:
144
- video_file = gemini_client.files.upload(path=filepath, mime_type=mime_type)
145
- print(f"Upload initialisé. Nom du fichier distant: {video_file.name}. Attente du traitement...")
146
-
147
- start_time = time.time()
148
- while video_file.state == types.FileState.PROCESSING:
149
- elapsed_time = time.time() - start_time
150
- if elapsed_time > max_wait_seconds:
151
- raise TimeoutError(f"Le traitement de la vidéo a dépassé le délai de {max_wait_seconds} secondes.")
152
- print(f"Vidéo en cours de traitement... (État: {video_file.state.name}, {int(elapsed_time)}s écoulées)")
153
- time.sleep(poll_interval)
154
- video_file = gemini_client.files.get(name=video_file.name)
155
-
156
- if video_file.state == types.FileState.FAILED:
157
- print(f"ERREUR: Le traitement de la vidéo a échoué. État: {video_file.state.name}")
158
- raise ValueError("Le traitement de la vidéo a échoué côté serveur.")
159
- if video_file.state == types.FileState.ACTIVE:
160
- print(f"Traitement vidéo terminé avec succès: {video_file.uri}")
161
- return video_file
162
- else:
163
- print(f"AVERTISSEMENT: État inattendu du fichier vidéo après traitement: {video_file.state.name}")
164
- raise RuntimeError(f"État inattendu du fichier vidéo: {video_file.state.name}")
165
-
166
- except Exception as e:
167
- print(f"Erreur lors de l'upload/traitement vidéo via client.files: {e}")
168
- if video_file and hasattr(video_file, 'name'):
169
- try:
170
- gemini_client.files.delete(name=video_file.name)
171
- print(f"Tentative de nettoyage du fichier distant {video_file.name} après erreur.")
172
- except Exception as delete_err:
173
- print(f"Échec du nettoyage du fichier distant {video_file.name} après erreur: {delete_err}")
174
- raise
175
-
176
- # --- Fonctions de Recherche Web (inchangées - implémentez si nécessaire) ---
177
 
178
  def perform_web_search(query):
 
179
  serper_api_key = os.getenv("SERPER_API_KEY")
180
  if not serper_api_key:
181
  print("AVERTISSEMENT: Clé API SERPER_API_KEY manquante. Recherche web désactivée.")
182
  return None
183
- print(f"Recherche Web (simulation) pour : {query}")
184
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  def format_search_results(data):
187
- if not data: return "Aucun résultat de recherche web pertinent."
188
- results = ["Résultats Web:"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  if data.get('organic'):
190
- for item in data['organic'][:3]:
191
- results.append(f"- {item.get('title', '')}: {item.get('snippet', '')}")
192
- return "\n".join(results)
193
- return "Aucun résultat organique trouvé."
 
 
 
 
 
 
 
 
194
 
195
- # --- Préparation Historique (Corrigé pour utiliser types.Part) ---
 
 
 
 
196
 
197
  def prepare_gemini_history(chat_history):
198
- """Convertit l'historique de session pour l'API Gemini (texte seulement)."""
199
  gemini_history = []
200
  for message in chat_history:
201
  role = 'user' if message['role'] == 'user' else 'model'
202
- raw_text_content = message.get('raw_text', '')
203
- if raw_text_content:
204
- # Correction: Créer un objet Part pour le texte
205
- text_part_object = types.Part(text=raw_text_content)
206
- gemini_history.append({'role': role, 'parts': [text_part_object]})
 
 
 
207
  return gemini_history
208
 
209
  # --- Routes Flask ---
@@ -211,236 +173,252 @@ def prepare_gemini_history(chat_history):
211
  @app.route('/')
212
  def root():
213
  """Sert la page HTML principale."""
214
- try:
215
- return render_template('index.html')
216
- except Exception as e:
217
- print(f"Erreur lors du rendu du template index.html: {e}")
218
- return "Erreur: Impossible de charger la page principale. Vérifiez que 'templates/index.html' existe.", 500
219
 
220
  @app.route('/api/history', methods=['GET'])
221
  def get_history():
222
- """Fournit l'historique de chat (formaté pour affichage) en JSON."""
223
  if 'chat_history' not in session:
224
  session['chat_history'] = []
 
 
225
  display_history = [
226
  {'role': msg.get('role', 'unknown'), 'text': msg.get('text', '')}
227
  for msg in session.get('chat_history', [])
228
  ]
 
229
  return jsonify({'success': True, 'history': display_history})
230
 
231
  @app.route('/api/chat', methods=['POST'])
232
  def chat_api():
233
- """Gère les requêtes de chat (texte, fichier/vidéo uploadé, URL YouTube)."""
234
- if not GEMINI_CONFIGURED or not gemini_client:
235
  print("API ERREUR: Tentative d'appel à /api/chat sans configuration Gemini valide.")
236
- return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503
237
 
 
238
  prompt = request.form.get('prompt', '').strip()
239
- youtube_url = request.form.get('youtube_url', '').strip()
240
- use_web_search = request.form.get('web_search', 'false').lower() == 'true'
241
- use_advanced = request.form.get('advanced_reasoning', 'false').lower() == 'true'
242
  file = request.files.get('file')
 
 
243
 
244
- if not file and not youtube_url and not prompt:
245
- return jsonify({'success': False, 'error': 'Veuillez fournir un message, un fichier/vidéo ou un lien YouTube.'}), 400
 
246
 
247
  print(f"\n--- Nouvelle requête /api/chat ---")
248
- print(f" Prompt: '{prompt[:50]}...'")
249
- print(f" Fichier: {file.filename if file else 'Non'}")
250
- print(f" URL YouTube: {youtube_url if youtube_url else 'Non'}")
251
- print(f" Web Search: {use_web_search}, Advanced: {use_advanced}")
252
 
 
253
  if 'chat_history' not in session:
254
  session['chat_history'] = []
255
 
256
- uploaded_media_part = None
257
- uploaded_filename_for_display = None
258
- filepath_to_delete = None
259
- is_media_request = False
260
- media_type = None
261
 
262
- try:
263
- if file and file.filename != '':
264
- is_media_request = True
265
- media_type = 'file'
266
- uploaded_filename_for_display = secure_filename(file.filename)
267
- if not allowed_file(uploaded_filename_for_display):
268
- raise ValueError(f"Type de fichier non autorisé: {uploaded_filename_for_display}")
269
-
270
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], uploaded_filename_for_display)
271
- file.save(filepath)
272
- filepath_to_delete = filepath
273
- print(f" Fichier '{uploaded_filename_for_display}' sauvegardé -> '{filepath}'")
274
- mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
275
-
276
- if is_video_file(uploaded_filename_for_display):
277
- media_type = 'video'
278
- print(" Traitement VIDÉO Uploadée (avec polling)...")
279
- processed_media_file = upload_video_with_polling(filepath, mime_type)
280
- uploaded_media_part = types.Part(file_data=processed_media_file)
281
- else:
282
- print(" Traitement FICHIER standard via client.files...")
283
- processed_media_file = gemini_client.files.upload(path=filepath, mime_type=mime_type)
284
- time.sleep(2) # Attente simplifiée
285
- processed_media_file = gemini_client.files.get(name=processed_media_file.name)
286
- if processed_media_file.state != types.FileState.ACTIVE:
287
- print(f"AVERTISSEMENT: Fichier non-vidéo '{processed_media_file.name}' n'est pas ACTIF ({processed_media_file.state.name}).")
288
- uploaded_media_part = types.Part(file_data=processed_media_file)
289
- print(f" Part Média ({media_type}) créé: {processed_media_file.uri}")
290
-
291
- elif youtube_url:
292
- if not is_youtube_url(youtube_url):
293
- print(f" AVERTISSEMENT: '{youtube_url}' n'est pas un lien YouTube valide.")
294
- media_type = 'text'
295
- else:
296
- is_media_request = True
297
- media_type = 'youtube'
298
- print(" Traitement LIEN YouTube...")
299
- uploaded_filename_for_display = youtube_url
300
- youtube_uri = youtube_url
301
- uploaded_media_part = types.Part(
302
- file_data=types.FileData(file_uri=youtube_uri, mime_type="video/mp4")
303
- )
304
- print(f" Part YouTube créé pour: {youtube_uri}")
305
- if not prompt:
306
- prompt = "Décris ou analyse le contenu de cette vidéo YouTube."
307
- print(f" Prompt par défaut ajouté pour YouTube: '{prompt}'")
308
-
309
- elif prompt:
310
- media_type = 'text'
311
- print(" Traitement PROMPT texte seul.")
312
  else:
313
- raise ValueError("Aucune entrée valide fournie.")
314
-
315
- display_user_text = prompt
316
- if media_type == 'file' or media_type == 'video':
317
- display_user_text = f"[{uploaded_filename_for_display}]" + (f" {prompt}" if prompt else "")
318
- elif media_type == 'youtube':
319
- display_user_text = f"[YouTube]" + (f" {prompt}" if prompt else "") + f"\n{uploaded_filename_for_display}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
- user_history_entry = {'role': 'user', 'text': display_user_text, 'raw_text': prompt}
322
- session['chat_history'].append(user_history_entry)
323
- session.modified = True
324
 
325
- current_gemini_parts = []
326
- if uploaded_media_part:
327
- current_gemini_parts.append(uploaded_media_part)
328
-
329
- final_prompt_for_gemini = prompt
330
- if use_web_search and prompt and media_type == 'text':
331
- print(" Activation Recherche Web...")
332
- search_data = perform_web_search(prompt)
333
- if search_data:
334
- formatted_results = format_search_results(search_data)
335
- final_prompt_for_gemini = f"""Basé sur la question suivante et les informations web ci-dessous, fournis une réponse complète.\n\nQuestion Originale:\n"{prompt}"\n\nInformations Web Pertinentes:\n--- DEBUT RESULTATS WEB ---\n{formatted_results}\n--- FIN RESULTATS WEB ---\n\nRéponse:"""
336
- print(" Prompt enrichi avec les résultats web.")
337
- else:
338
- print(" Aucun résultat de recherche web trouvé ou pertinent.")
339
-
340
- if final_prompt_for_gemini:
341
- current_gemini_parts.append(types.Part(text=final_prompt_for_gemini)) # Correction: Toujours Part
342
-
343
- if not current_gemini_parts:
344
- raise ValueError("Impossible de traiter la requête : contenu vide.")
345
-
346
- gemini_history = prepare_gemini_history(session['chat_history'][:-1])
347
- # Structure correcte pour contents (liste de dictionnaires role/parts)
348
  contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]
349
 
350
- selected_model_api_name = f'models/{MODEL_PRO}' if is_media_request or use_advanced else f'models/{MODEL_FLASH}'
351
- print(f" Modèle sélectionné pour l'API: {selected_model_api_name}")
352
-
353
- generation_config = types.GenerationConfig() # Config vide par défaut, ajouter des params si besoin
354
 
355
- # Correction: Appel via client.models.generate_content
356
- print(f" Envoi de la requête à {selected_model_api_name} ({len(contents_for_gemini)} messages/tours)...")
357
- response = gemini_client.models.generate_content(
358
- model=selected_model_api_name, # Utilise 'model'
359
- contents=contents_for_gemini,
360
- generation_config=generation_config,
361
- safety_settings=SAFETY_SETTINGS_CONFIG,
362
- system_instruction=types.Content(parts=[types.Part(text=SYSTEM_INSTRUCTION)], role="system")
363
  )
364
 
365
- response_text_raw = ""
366
- response_html = ""
 
 
 
 
 
 
367
  try:
368
  response_text_raw = response.text
369
- except ValueError as ve:
370
- print(f" ERREUR: La réponse de Gemini a été bloquée (ValueError): {ve}")
371
- try:
372
- print(f" Détails du blocage (Prompt Feedback): {response.prompt_feedback}")
373
- block_reason = response.prompt_feedback.block_reason_message or "Raison non spécifiée"
374
- response_text_raw = f"Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité ({block_reason})."
375
- except Exception as feedback_err:
376
- print(f" Impossible de récupérer les détails du blocage: {feedback_err}")
377
- response_text_raw = "Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité."
378
- except Exception as resp_err:
379
- print(f" ERREUR inattendue lors de l'accès à response.text : {resp_err}")
380
- print(f" Réponse brute complète : {response}")
381
- response_text_raw = "Désolé, une erreur interne s'est produite lors de la réception de la réponse."
382
-
383
- print(f" Réponse reçue (début): '{response_text_raw[:100]}...'")
384
- response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
385
- print(" Réponse convertie en HTML.")
386
-
387
- assistant_history_entry = {'role': 'assistant', 'text': response_html, 'raw_text': response_text_raw}
 
 
 
 
 
388
  session['chat_history'].append(assistant_history_entry)
389
  session.modified = True
390
 
391
- print(" Envoi de la réponse HTML au client.")
 
392
  return jsonify({'success': True, 'message': response_html})
393
 
394
- # Correction: Utiliser genai.StopCandidateException
395
- except (TimeoutError, ValueError, ConnectionError, FileNotFoundError, genai.StopCandidateException) as e:
396
- error_message = f"Erreur lors du traitement de la requête: {e}"
397
- print(f"ERREUR (Traitement/Appel API): {error_message}")
398
- # Correction: Utiliser genai.StopCandidateException pour la vérification
399
- if isinstance(e, genai.StopCandidateException):
400
- error_message = "La génération a été stoppée, probablement à cause du contenu."
401
- print(f" StopCandidateException: {e}")
402
-
403
- if session.get('chat_history') and session['chat_history'][-1]['role'] == 'user':
404
- session['chat_history'].pop()
405
- session.modified = True
406
- print(" Dernier message utilisateur retiré de l'historique après erreur.")
407
- return jsonify({'success': False, 'error': error_message}), 500
408
-
409
  except Exception as e:
410
- error_message = f"Une erreur interne inattendue est survenue: {e}"
411
- print(f"ERREUR CRITIQUE INATTENDUE: {error_message}")
412
- traceback.print_exc() # Correction: Utilise traceback pour détails
413
- if session.get('chat_history') and session['chat_history'][-1]['role'] == 'user':
 
414
  session['chat_history'].pop()
415
  session.modified = True
416
- print(" Dernier message utilisateur retiré de l'historique après erreur inattendue.")
417
- return jsonify({'success': False, 'error': "Une erreur interne inattendue est survenue."}), 500
 
 
 
 
418
 
419
  finally:
 
420
  if filepath_to_delete and os.path.exists(filepath_to_delete):
421
  try:
422
  os.remove(filepath_to_delete)
423
- print(f" Fichier temporaire '{filepath_to_delete}' supprimé avec succès.")
424
  except OSError as e:
425
- print(f" ERREUR lors de la suppression du fichier temporaire '{filepath_to_delete}': {e}")
 
426
 
427
  @app.route('/clear', methods=['POST'])
428
  def clear_chat():
429
  """Efface l'historique de chat dans la session."""
430
  session.pop('chat_history', None)
 
431
  print("API: Historique de chat effacé via /clear.")
432
- is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' or \
433
- 'application/json' in request.headers.get('Accept', '')
 
 
 
 
434
  if is_ajax:
435
  return jsonify({'success': True, 'message': 'Historique effacé.'})
436
  else:
 
437
  flash("Conversation effacée.", "info")
438
- return redirect(url_for('root'))
439
 
440
- # --- Démarrage de l'application Flask ---
441
 
 
442
  if __name__ == '__main__':
443
  print("Démarrage du serveur Flask...")
444
- port = int(os.environ.get('PORT', 7860))
445
- # Mettre debug=False pour la production
 
 
 
446
  app.run(debug=True, host='0.0.0.0', port=port)
 
1
  import os
2
  import json
3
  import mimetypes
 
 
 
 
4
  from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template
5
  from dotenv import load_dotenv
6
+ import google.generativeai as genai
 
 
 
 
 
7
  import requests
8
  from werkzeug.utils import secure_filename
9
+ import markdown # Pour convertir la réponse en HTML
10
 
11
  # --- Configuration Initiale ---
12
+ load_dotenv()
 
13
 
14
  app = Flask(__name__)
15
 
16
+ # Clé secrète FORTEMENT recommandée pour les sessions
17
+ app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key-replace-in-prod')
18
 
19
  # Configuration pour les uploads
20
  UPLOAD_FOLDER = 'temp'
21
+ ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'} # Extensions autorisées
 
 
 
22
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
23
+ app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024 # Limite de taille (ex: 25MB)
24
 
25
+ # Créer le dossier temp s'il n'existe pas
 
 
 
26
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
27
  print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
28
 
29
  # --- Configuration de l'API Gemini ---
30
+ MODEL_FLASH = 'gemini-2.0-flash' # Default model
31
+ MODEL_PRO = 'gemini-2.5-pro-exp-03-25' # Advanced model
32
+ SYSTEM_INSTRUCTION = "Tu es un assistant intelligent et amical nommé Mariam. Tu assistes les utilisateurs au mieux de tes capacités. Tu as été créé par Aenir."
33
+ SAFETY_SETTINGS = [
34
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
35
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
36
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
37
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  ]
 
 
39
  GEMINI_CONFIGURED = False
 
 
40
  try:
41
  gemini_api_key = os.getenv("GOOGLE_API_KEY")
42
  if not gemini_api_key:
43
+ print("ERREUR: Clé API GOOGLE_API_KEY manquante dans le fichier .env")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  else:
45
+ genai.configure(api_key=gemini_api_key)
46
+ # Just configure, don't create model instance yet
47
+ # Check if we can list models as a basic configuration test
48
+ models_list = [m.name for m in genai.list_models()]
49
+ if f'models/{MODEL_FLASH}' in models_list and f'models/{MODEL_PRO}' in models_list:
50
+ print(f"Configuration Gemini effectuée. Modèles requis ({MODEL_FLASH}, {MODEL_PRO}) disponibles.")
51
+ print(f"System instruction: {SYSTEM_INSTRUCTION}")
52
+ GEMINI_CONFIGURED = True
53
+ else:
54
+ print(f"ERREUR: Les modèles requis ({MODEL_FLASH}, {MODEL_PRO}) ne sont pas tous disponibles via l'API.")
55
+ print(f"Modèles trouvés: {models_list}")
56
 
57
  except Exception as e:
58
  print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
 
59
  print("L'application fonctionnera sans les fonctionnalités IA.")
 
 
60
 
61
  # --- Fonctions Utilitaires ---
62
 
 
 
 
 
63
  def allowed_file(filename):
64
+ """Vérifie si l'extension du fichier est autorisée."""
65
+ return '.' in filename and \
66
+ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  def perform_web_search(query):
69
+ """Effectue une recherche web via l'API Serper."""
70
  serper_api_key = os.getenv("SERPER_API_KEY")
71
  if not serper_api_key:
72
  print("AVERTISSEMENT: Clé API SERPER_API_KEY manquante. Recherche web désactivée.")
73
  return None
74
+
75
+ search_url = "https://google.serper.dev/search"
76
+ headers = {
77
+ 'X-API-KEY': serper_api_key,
78
+ 'Content-Type': 'application/json'
79
+ }
80
+ payload = json.dumps({"q": query, "gl": "fr", "hl": "fr"}) # Ajout localisation FR
81
+
82
+ try:
83
+ print(f"Recherche Serper pour: '{query}'")
84
+ response = requests.post(search_url, headers=headers, data=payload, timeout=10)
85
+ response.raise_for_status() # Lève une exception pour les erreurs HTTP (4xx, 5xx)
86
+ data = response.json()
87
+ print("Résultats de recherche Serper obtenus.")
88
+ # print(json.dumps(data, indent=2)) # Décommenter pour voir les résultats bruts
89
+ return data
90
+ except requests.exceptions.Timeout:
91
+ print("Erreur lors de la recherche web : Timeout")
92
+ return None
93
+ except requests.exceptions.RequestException as e:
94
+ print(f"Erreur lors de la recherche web : {e}")
95
+ # Essayer de lire le corps de la réponse d'erreur si possible
96
+ try:
97
+ error_details = e.response.json()
98
+ print(f"Détails de l'erreur Serper: {error_details}")
99
+ except:
100
+ pass # Ignorer si le corps n'est pas JSON ou n'existe pas
101
+ return None
102
+ except json.JSONDecodeError as e:
103
+ print(f"Erreur lors du décodage de la réponse JSON de Serper : {e}")
104
+ print(f"Réponse reçue (texte brut) : {response.text}")
105
+ return None
106
 
107
  def format_search_results(data):
108
+ """Met en forme les résultats de recherche (format Markdown)."""
109
+ if not data:
110
+ return "Aucun résultat de recherche web trouvé pertinent."
111
+
112
+ results = []
113
+
114
+ # Réponse directe (Answer Box)
115
+ if data.get('answerBox'):
116
+ ab = data['answerBox']
117
+ title = ab.get('title', '')
118
+ snippet = ab.get('snippet') or ab.get('answer', '')
119
+ if snippet:
120
+ results.append(f"**Réponse rapide : {title}**\n{snippet}\n")
121
+
122
+ # Knowledge Graph
123
+ if data.get('knowledgeGraph'):
124
+ kg = data['knowledgeGraph']
125
+ title = kg.get('title', '')
126
+ type = kg.get('type', '')
127
+ description = kg.get('description', '')
128
+ if title and description:
129
+ results.append(f"**{title} ({type})**\n{description}\n")
130
+ if kg.get('attributes'):
131
+ for attr, value in kg['attributes'].items():
132
+ results.append(f"- {attr}: {value}")
133
+
134
+
135
+ # Résultats organiques
136
  if data.get('organic'):
137
+ results.append("**Pages web pertinentes :**")
138
+ for i, item in enumerate(data['organic'][:3], 1): # Top 3
139
+ title = item.get('title', 'Sans titre')
140
+ link = item.get('link', '#')
141
+ snippet = item.get('snippet', 'Pas de description.')
142
+ results.append(f"{i}. **[{title}]({link})**\n {snippet}\n")
143
+
144
+ # People Also Ask
145
+ if data.get('peopleAlsoAsk'):
146
+ results.append("**Questions liées :**")
147
+ for i, item in enumerate(data['peopleAlsoAsk'][:2], 1): # Top 2
148
+ results.append(f"- {item.get('question', '')}")
149
 
150
+
151
+ if not results:
152
+ return "Aucun résultat structuré trouvé dans la recherche web."
153
+
154
+ return "\n".join(results)
155
 
156
  def prepare_gemini_history(chat_history):
157
+ """Convertit l'historique stocké en session au format attendu par Gemini API."""
158
  gemini_history = []
159
  for message in chat_history:
160
  role = 'user' if message['role'] == 'user' else 'model'
161
+ # Utiliser le 'raw_text' stocké pour Gemini
162
+ text_part = message.get('raw_text', '') # Fallback au cas où
163
+ parts = [text_part]
164
+ # NOTE: La gestion des fichiers des tours PRÉCÉDENTS n'est pas gérée ici.
165
+ # L'API generate_content se concentre généralement sur le fichier du tour ACTUEL.
166
+ # Si une référence de fichier passée était nécessaire, il faudrait la stocker
167
+ # et la ré-attacher ici (potentiellement plus complexe).
168
+ gemini_history.append({'role': role, 'parts': parts})
169
  return gemini_history
170
 
171
  # --- Routes Flask ---
 
173
  @app.route('/')
174
  def root():
175
  """Sert la page HTML principale."""
176
+ return render_template('index.html')
 
 
 
 
177
 
178
  @app.route('/api/history', methods=['GET'])
179
  def get_history():
180
+ """Fournit l'historique de chat stocké en session au format JSON."""
181
  if 'chat_history' not in session:
182
  session['chat_history'] = []
183
+
184
+ # Préparer l'historique pour l'affichage (contient déjà le HTML pour l'assistant)
185
  display_history = [
186
  {'role': msg.get('role', 'unknown'), 'text': msg.get('text', '')}
187
  for msg in session.get('chat_history', [])
188
  ]
189
+ print(f"API: Récupération de l'historique ({len(display_history)} messages)")
190
  return jsonify({'success': True, 'history': display_history})
191
 
192
  @app.route('/api/chat', methods=['POST'])
193
  def chat_api():
194
+ """Gère les nouvelles requêtes de chat via AJAX."""
195
+ if not GEMINI_CONFIGURED:
196
  print("API ERREUR: Tentative d'appel à /api/chat sans configuration Gemini valide.")
197
+ return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503 # Service Unavailable
198
 
199
+ # Récupération des données du formulaire
200
  prompt = request.form.get('prompt', '').strip()
201
+ use_web_search_str = request.form.get('web_search', 'false') # 'true' ou 'false'
202
+ use_web_search = use_web_search_str.lower() == 'true'
 
203
  file = request.files.get('file')
204
+ use_advanced_str = request.form.get('advanced_reasoning', 'false') # Get the new flag
205
+ use_advanced = use_advanced_str.lower() == 'true'
206
 
207
+ # Validation simple
208
+ if not prompt and not file:
209
+ return jsonify({'success': False, 'error': 'Veuillez fournir un message ou un fichier.'}), 400
210
 
211
  print(f"\n--- Nouvelle requête /api/chat ---")
212
+ print(f"Prompt reçu: '{prompt[:50]}...'")
213
+ print(f"Recherche Web activée: {use_web_search}")
214
+ print(f"Raisonnement avancé demandé: {use_advanced}")
215
+ print(f"Fichier reçu: {file.filename if file else 'Aucun'}")
216
 
217
+ # Initialiser l'historique de session si nécessaire
218
  if 'chat_history' not in session:
219
  session['chat_history'] = []
220
 
221
+ uploaded_gemini_file = None # L'objet fichier retourné par genai.upload_file
222
+ uploaded_filename = None # Juste le nom du fichier pour référence/affichage
223
+ filepath_to_delete = None # Chemin du fichier local à supprimer après traitement
 
 
224
 
225
+ # --- Gestion de l'upload de fichier ---
226
+ if file and file.filename != '':
227
+ if allowed_file(file.filename):
228
+ try:
229
+ filename = secure_filename(file.filename)
230
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
231
+ file.save(filepath)
232
+ filepath_to_delete = filepath # Marquer pour suppression
233
+ uploaded_filename = filename
234
+ print(f"Fichier '{filename}' sauvegardé temporairement dans '{filepath}'")
235
+
236
+ # Détecter le MimeType pour Gemini
237
+ mime_type = mimetypes.guess_type(filepath)[0]
238
+ if not mime_type:
239
+ mime_type = 'application/octet-stream' # Fallback
240
+ print(f"AVERTISSEMENT: Impossible de deviner le MimeType pour '{filename}', utilisation de '{mime_type}'.")
241
+
242
+ # Uploader vers Google AI (peut prendre du temps)
243
+ print(f"Upload du fichier vers Google AI (MimeType: {mime_type})...")
244
+ # Note: L'API upload_file est générique et ne dépend pas du modèle Flash/Pro
245
+ uploaded_gemini_file = genai.upload_file(path=filepath, mime_type=mime_type)
246
+ print(f"Fichier '{uploaded_gemini_file.name}' uploadé avec succès vers Google AI.")
247
+
248
+ except Exception as e:
249
+ print(f"ERREUR Critique lors du traitement/upload du fichier '{filename}': {e}")
250
+ # Supprimer le fichier local même en cas d'erreur d'upload Gemini
251
+ if filepath_to_delete and os.path.exists(filepath_to_delete):
252
+ try:
253
+ os.remove(filepath_to_delete)
254
+ print(f"Fichier temporaire '{filepath_to_delete}' supprimé après erreur.")
255
+ except OSError as del_e:
256
+ print(f"Erreur lors de la suppression du fichier temporaire après erreur: {del_e}")
257
+ # Renvoyer une erreur claire au client
258
+ return jsonify({'success': False, 'error': f"Erreur lors du traitement du fichier: {e}"}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  else:
260
+ print(f"ERREUR: Type de fichier non autorisé: {file.filename}")
261
+ return jsonify({'success': False, 'error': f"Type de fichier non autorisé. Extensions permises: {', '.join(ALLOWED_EXTENSIONS)}"}), 400
262
+
263
+ # --- Préparation du message utilisateur pour l'historique et Gemini ---
264
+ # Texte brut pour Gemini (et pour l'historique interne)
265
+ raw_user_text = prompt
266
+ # Texte pour l'affichage dans l'interface (peut inclure le nom de fichier)
267
+ display_user_text = f"[{uploaded_filename}] {prompt}" if uploaded_filename and prompt else (prompt or f"[{uploaded_filename}]")
268
+
269
+ # Ajout à l'historique de session
270
+ user_history_entry = {
271
+ 'role': 'user',
272
+ 'text': display_user_text, # Pour get_history et potentiellement debug
273
+ 'raw_text': raw_user_text, # Pour l'envoi à Gemini via prepare_gemini_history
274
+ # On ne stocke PAS l'objet 'uploaded_gemini_file' dans la session
275
+ }
276
+ session['chat_history'].append(user_history_entry)
277
+ session.modified = True # Indiquer que la session a été modifiée
278
+
279
+ # --- Préparation des 'parts' pour l'appel Gemini ACTUEL ---
280
+ current_gemini_parts = []
281
+ if uploaded_gemini_file:
282
+ current_gemini_parts.append(uploaded_gemini_file) # L'objet fichier uploadé
283
+
284
+ final_prompt_for_gemini = raw_user_text # Commencer avec le texte brut
285
+
286
+ # --- Recherche Web (si activée et si un prompt textuel existe) ---
287
+ if use_web_search and raw_user_text:
288
+ print("Activation de la recherche web...")
289
+ search_data = perform_web_search(raw_user_text)
290
+ if search_data:
291
+ formatted_results = format_search_results(search_data)
292
+ # Construire un prompt enrichi pour Gemini
293
+ final_prompt_for_gemini = f"""Voici la question originale de l'utilisateur:
294
+ "{raw_user_text}"
295
+
296
+ J'ai effectué une recherche web et voici les informations pertinentes trouvées:
297
+ --- DEBUT RESULTATS WEB ---
298
+ {formatted_results}
299
+ --- FIN RESULTATS WEB ---
300
+
301
+ En te basant sur ces informations ET sur ta connaissance générale, fournis une réponse complète et bien structurée à la question originale de l'utilisateur."""
302
+ print("Prompt enrichi avec les résultats de recherche web.")
303
+ else:
304
+ print("Aucun résultat de recherche web pertinent trouvé ou erreur, utilisation du prompt original.")
305
+ # final_prompt_for_gemini reste raw_user_text
306
 
307
+ # Ajouter le texte (potentiellement enrichi) aux parts pour Gemini
308
+ current_gemini_parts.append(final_prompt_for_gemini)
 
309
 
310
+ # --- Appel à l'API Gemini ---
311
+ try:
312
+ # Préparer l'historique des messages PRÉCÉDENTS
313
+ gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le message actuel
314
+ print(f"Préparation de l'appel Gemini avec {len(gemini_history)} messages d'historique.")
315
+ # Construire le contenu complet pour l'appel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]
317
 
318
+ # Choisir le nom du modèle à utiliser
319
+ selected_model_name = MODEL_PRO if use_advanced else MODEL_FLASH
320
+ print(f"Utilisation du modèle Gemini: {selected_model_name}")
 
321
 
322
+ # Créer l'instance du modèle spécifique pour cette requête
323
+ # Réutiliser les paramètres globaux (safety, system instruction)
324
+ active_model = genai.GenerativeModel(
325
+ model_name=selected_model_name,
326
+ safety_settings=SAFETY_SETTINGS, # defined globally
327
+ system_instruction=SYSTEM_INSTRUCTION # defined globally
 
 
328
  )
329
 
330
+ # Appel API
331
+ print(f"Envoi de la requête à {selected_model_name}...")
332
+ # Utilisation de generate_content en mode non-streamé
333
+ response = active_model.generate_content(contents_for_gemini)
334
+ # print(response) # Décommenter pour voir la réponse brute de l'API
335
+
336
+ # Extraire le texte de la réponse (gestion d'erreur potentielle ici si la réponse est bloquée etc.)
337
+ # Gérer le cas où la réponse est bloquée par les safety settings
338
  try:
339
  response_text_raw = response.text
340
+ except ValueError:
341
+ # Si response.text échoue, la réponse a probablement été bloquée.
342
+ print("ERREUR: La réponse de Gemini a été bloquée (probablement par les safety settings).")
343
+ print(f"Détails du blocage : {response.prompt_feedback}")
344
+ # Vous pouvez décider quoi renvoyer au client ici.
345
+ # Soit une erreur spécifique, soit un message générique.
346
+ response_text_raw = "Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité."
347
+ # Convertir ce message d'erreur en HTML aussi pour la cohérence
348
+ response_html = markdown.markdown(response_text_raw)
349
+
350
+ else:
351
+ # Si response.text réussit, continuer normalement
352
+ print(f"Réponse reçue de Gemini (brute, début): '{response_text_raw[:100]}...'")
353
+ # Convertir la réponse Markdown en HTML pour l'affichage
354
+ response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
355
+ print("Réponse convertie en HTML.")
356
+
357
+
358
+ # Ajouter la réponse de l'assistant à l'historique de session
359
+ assistant_history_entry = {
360
+ 'role': 'assistant',
361
+ 'text': response_html, # HTML pour l'affichage via get_history
362
+ 'raw_text': response_text_raw # Texte brut pour les futurs appels Gemini
363
+ }
364
  session['chat_history'].append(assistant_history_entry)
365
  session.modified = True
366
 
367
+ # Renvoyer la réponse HTML au frontend
368
+ print("Envoi de la réponse HTML au client.")
369
  return jsonify({'success': True, 'message': response_html})
370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  except Exception as e:
372
+ print(f"ERREUR Critique lors de l'appel à Gemini ou du traitement de la réponse : {e}")
373
+ # En cas d'erreur, retirer le dernier message utilisateur de l'historique
374
+ # pour éviter les boucles d'erreur si le message lui-même pose problème.
375
+ # Vérifier si l'historique n'est pas vide avant de pop
376
+ if session.get('chat_history'):
377
  session['chat_history'].pop()
378
  session.modified = True
379
+ print("Le dernier message utilisateur a été retiré de l'historique suite à l'erreur.")
380
+ else:
381
+ print("L'historique était déjà vide lors de l'erreur.")
382
+
383
+ # Renvoyer une erreur générique mais informative
384
+ return jsonify({'success': False, 'error': f"Une erreur interne est survenue lors de la génération de la réponse. Détails: {e}"}), 500
385
 
386
  finally:
387
+ # --- Nettoyage du fichier temporaire ---
388
  if filepath_to_delete and os.path.exists(filepath_to_delete):
389
  try:
390
  os.remove(filepath_to_delete)
391
+ print(f"Fichier temporaire '{filepath_to_delete}' supprimé avec succès.")
392
  except OSError as e:
393
+ print(f"ERREUR lors de la suppression du fichier temporaire '{filepath_to_delete}': {e}")
394
+
395
 
396
  @app.route('/clear', methods=['POST'])
397
  def clear_chat():
398
  """Efface l'historique de chat dans la session."""
399
  session.pop('chat_history', None)
400
+ # session.pop('web_search', None) # On ne stocke pas ça en session
401
  print("API: Historique de chat effacé via /clear.")
402
+
403
+ # Adapter la réponse selon si c'est une requête AJAX (fetch) ou une soumission classique
404
+ # Vérification si la requête vient probablement de fetch (simple)
405
+ is_ajax = 'XMLHttpRequest' == request.headers.get('X-Requested-With') or \
406
+ 'application/json' in request.headers.get('Accept', '') # Plus robuste
407
+
408
  if is_ajax:
409
  return jsonify({'success': True, 'message': 'Historique effacé.'})
410
  else:
411
+ # Comportement pour une soumission de formulaire classique (si jamais utilisé)
412
  flash("Conversation effacée.", "info")
413
+ return redirect(url_for('root')) # Redirige vers la racine
414
 
 
415
 
416
+ # --- Démarrage de l'application ---
417
  if __name__ == '__main__':
418
  print("Démarrage du serveur Flask...")
419
+ # Utiliser host='0.0.0.0' pour rendre accessible sur le réseau local
420
+ # debug=True est pratique pour le développement, mais à désactiver en production !
421
+ # Changer le port si nécessaire (ex: 5000, 5001, 8080)
422
+ # Utiliser un port différent si le port 5000 est déjà pris
423
+ port = int(os.environ.get('PORT', 5001))
424
  app.run(debug=True, host='0.0.0.0', port=port)