Docfile commited on
Commit
b293b19
·
verified ·
1 Parent(s): 254ce1e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -279
app.py CHANGED
@@ -1,5 +1,3 @@
1
- # --- START OF FILE app.py ---
2
-
3
  import os
4
  import json
5
  import mimetypes
@@ -11,62 +9,66 @@ import requests
11
  from werkzeug.utils import secure_filename
12
  import markdown # Pour convertir la réponse en HTML
13
  from flask_session import Session
14
- import pprint # Pour un affichage plus lisible (optionnel)
 
 
15
 
16
  # --- Configuration Initiale ---
17
  load_dotenv()
18
 
19
  app = Flask(__name__)
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # --- Configuration Flask Standard ---
22
  app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-super-cle-secrete-a-changer')
23
 
24
  # Configuration pour les uploads
25
  UPLOAD_FOLDER = 'temp'
26
- ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'}
 
27
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
28
- app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024 # Exemple : 25MB
29
 
30
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
31
- print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
32
 
33
- # --- Configuration pour Flask-Session (Backend Filesystem) ---
34
  app.config['SESSION_TYPE'] = 'filesystem'
35
  app.config['SESSION_PERMANENT'] = False
36
  app.config['SESSION_USE_SIGNER'] = True
37
  app.config['SESSION_FILE_DIR'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'flask_session')
38
  app.config['SESSION_COOKIE_SAMESITE'] = 'None'
39
  app.config['SESSION_COOKIE_SECURE'] = True
40
-
41
  os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
42
- print(f"Dossier pour les sessions serveur configuré : {app.config['SESSION_FILE_DIR']}")
43
 
44
- # --- Initialisation de Flask-Session ---
45
  server_session = Session(app)
46
 
47
  # --- Configuration de l'API Gemini ---
48
- MODEL_FLASH = 'gemini-2.0-flash'
49
- MODEL_PRO = 'gemini-2.5-pro-exp-03-25'
50
- SYSTEM_INSTRUCTION = ("Tu es un assistant intelligent et amical nommé Mariam. "
51
- "Tu assistes les utilisateurs au mieux de tes capacités. Tu as été créé par Aenir.")
52
 
53
  SAFETY_SETTINGS = [
54
- types.SafetySetting(
55
- category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
56
- threshold=types.HarmBlockThreshold.BLOCK_NONE,
57
- ),
58
- types.SafetySetting(
59
- category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
60
- threshold=types.HarmBlockThreshold.BLOCK_NONE,
61
- ),
62
- types.SafetySetting(
63
- category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
64
- threshold=types.HarmBlockThreshold.BLOCK_NONE,
65
- ),
66
- types.SafetySetting(
67
- category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
68
- threshold=types.HarmBlockThreshold.BLOCK_NONE,
69
- )
70
  ]
71
 
72
  GEMINI_CONFIGURED = False
@@ -74,341 +76,271 @@ genai_client = None
74
  try:
75
  gemini_api_key = os.getenv("GOOGLE_API_KEY")
76
  if not gemini_api_key:
77
- print("ERREUR: Clé API GOOGLE_API_KEY manquante dans le fichier .env")
78
  else:
79
- # Initialisation du client avec le SDK
80
  genai_client = genai.Client(api_key=gemini_api_key)
81
-
82
- # Vérification de la disponibilité des modèles
83
  try:
84
  models = genai_client.list_models()
85
  models_list = [model.name for model in models]
86
  if any(MODEL_FLASH in model for model in models_list) and any(MODEL_PRO in model for model in models_list):
87
- print(f"Configuration Gemini effectuée. Modèles requis ({MODEL_FLASH}, {MODEL_PRO}) disponibles.")
88
- print(f"System instruction: {SYSTEM_INSTRUCTION}")
89
  GEMINI_CONFIGURED = True
90
  else:
91
- print(f"ERREUR: Les modèles requis ({MODEL_FLASH}, {MODEL_PRO}) ne sont pas tous disponibles.")
92
- print(f"Modèles trouvés: {models_list}")
93
  except Exception as e_models:
94
- print(f"ERREUR lors de la vérification des modèles: {e_models}")
95
- print("Tentative de continuer sans vérification des modèles disponibles.")
96
  GEMINI_CONFIGURED = True
97
-
98
  except Exception as e:
99
- print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
100
- print("L'application fonctionnera sans les fonctionnalités IA.")
101
 
102
  # --- Fonctions Utilitaires ---
103
 
104
  def allowed_file(filename):
105
  """Vérifie si l'extension du fichier est autorisée."""
106
- return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
107
-
108
- def perform_web_search(query, client, model_id):
109
- """
110
- Réalise un appel minimal pour la recherche web en utilisant le format documenté.
111
- """
112
- try:
113
- print(f"--- LOG WEBSEARCH: Recherche Google pour: '{query}'")
114
- response = client.models.generate_content(
115
- model=model_id,
116
- contents=query,
117
- config={"tools": [{"google_search": {}}]},
118
- )
119
- print("--- LOG WEBSEARCH: Résultats de recherche obtenus.")
120
- return response
121
- except Exception as e:
122
- print(f"--- LOG WEBSEARCH: Erreur lors de la recherche web : {e}")
123
- return None
124
 
125
- def format_search_response(response):
126
- """Extrait le texte de la réponse de recherche web."""
127
- if not response:
128
- return ""
129
- try:
130
- return response.text
131
- except Exception as e:
132
- print(f"--- LOG WEBSEARCH: Erreur lors de l'extraction du texte: {e}")
133
- return ""
134
 
135
  def prepare_gemini_history(chat_history):
136
- """Convertit l'historique stocké en session au format attendu par l'API Gemini."""
137
- print(f"--- DEBUG [prepare_gemini_history]: Entrée avec {len(chat_history)} messages")
138
  gemini_history = []
139
- for i, message in enumerate(list(chat_history)):
140
  role = message.get('role')
141
  text_part = message.get('raw_text')
142
- print(f" [prepare_gemini_history] Message {i} (rôle: {role}): raw_text présent? {'Oui' if text_part else 'NON'}")
143
  if text_part:
144
- if role == 'user':
145
- gemini_history.append({
146
- "role": "user",
147
- "parts": [{"text": text_part}]
148
- })
149
- else: # assistant ou autre
150
- gemini_history.append({
151
- "role": "model",
152
- "parts": [{"text": text_part}]
153
- })
154
- else:
155
- print(f" AVERTISSEMENT [prepare_gemini_history]: Message {i} sans texte, ignoré.")
156
- print(f"--- DEBUG [prepare_gemini_history]: Sortie avec {len(gemini_history)} messages")
157
  return gemini_history
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  # --- Routes Flask ---
160
 
161
  @app.route('/')
162
  def root():
163
- print("--- LOG: Appel route '/' ---")
 
164
  return render_template('index.html')
165
 
166
  @app.route('/api/history', methods=['GET'])
167
  def get_history():
168
- print("\n--- DEBUG [/api/history]: Début requête GET ---")
169
  if 'chat_history' not in session:
170
  session['chat_history'] = []
171
- print(" [/api/history]: Session 'chat_history' initialisée (vide).")
172
- display_history = []
173
- current_history = session.get('chat_history', [])
174
- print(f" [/api/history]: Historique récupéré: {len(current_history)} messages.")
175
- for i, msg in enumerate(current_history):
176
- if isinstance(msg, dict) and 'role' in msg and 'text' in msg:
177
- display_history.append({
178
- 'role': msg.get('role'),
179
- 'text': msg.get('text')
180
- })
181
- else:
182
- print(f" AVERTISSEMENT [/api/history]: Format invalide pour le message {i}: {msg}")
183
- print(f" [/api/history]: Historique préparé pour le frontend: {len(display_history)} messages.")
184
- return jsonify({'success': True, 'history': display_history})
185
 
186
  @app.route('/api/chat', methods=['POST'])
187
  def chat_api():
188
- print("\n---===================================---")
189
- print("--- DEBUG [/api/chat]: Nouvelle requête POST ---")
 
190
  if not GEMINI_CONFIGURED or not genai_client:
191
- print("--- ERREUR [/api/chat]: Service IA non configuré.")
192
  return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503
193
 
194
  prompt = request.form.get('prompt', '').strip()
195
  use_web_search = request.form.get('web_search', 'false').lower() == 'true'
196
- file = request.files.get('file')
197
  use_advanced = request.form.get('advanced_reasoning', 'false').lower() == 'true'
198
-
199
- print(f" [/api/chat]: Prompt reçu: '{prompt[:50]}...'")
200
- print(f" [/api/chat]: Recherche Web: {use_web_search}, Raisonnement Avancé: {use_advanced}")
201
- print(f" [/api/chat]: Fichier: {file.filename if file else 'Aucun'}")
202
-
203
- if not prompt and not file:
204
- print("--- ERREUR [/api/chat]: Prompt et fichier vides.")
205
- return jsonify({'success': False, 'error': 'Veuillez fournir un message ou un fichier.'}), 400
 
 
 
 
206
 
207
  if 'chat_history' not in session:
208
  session['chat_history'] = []
209
- history_before_user_add = list(session.get('chat_history', []))
210
- print(f"--- DEBUG [/api/chat]: Historique avant ajout: {len(history_before_user_add)} messages")
211
-
212
- uploaded_file_part = None
213
- uploaded_filename = None
214
- filepath_to_delete = None
215
-
216
- if file and file.filename != '':
217
- print(f"--- LOG [/api/chat]: Traitement du fichier '{file.filename}'")
218
- if allowed_file(file.filename):
219
- try:
220
- filename = secure_filename(file.filename)
221
- filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
222
- file.save(filepath)
223
- filepath_to_delete = filepath
224
- uploaded_filename = filename
225
- print(f" [/api/chat]: Fichier '{filename}' sauvegardé dans '{filepath}'")
226
- mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
227
- with open(filepath, "rb") as f:
228
- file_data = f.read()
229
- uploaded_file_part = {
230
- "inline_data": {
231
- "mime_type": mime_type,
232
- "data": file_data
233
- }
234
- }
235
- print(f" [/api/chat]: Fichier préparé pour Gemini.")
236
- except Exception as e:
237
- print(f"--- ERREUR [/api/chat]: Échec traitement fichier '{filename}': {e}")
238
- if filepath_to_delete and os.path.exists(filepath_to_delete):
239
- try:
240
- os.remove(filepath_to_delete)
241
- except OSError as del_e:
242
- print(f" Erreur suppression fichier temp: {del_e}")
243
- return jsonify({'success': False, 'error': f"Erreur traitement fichier: {e}"}), 500
244
- else:
245
- print(f"--- ERREUR [/api/chat]: Type de fichier non autorisé: {file.filename}")
246
- return jsonify({'success': False, 'error': "Type de fichier non autorisé."}), 400
247
 
248
  raw_user_text = prompt
249
- display_user_text = f"[{uploaded_filename}] {prompt}" if uploaded_filename and prompt else (prompt or f"[{uploaded_filename}]")
250
- user_history_entry = {
251
- 'role': 'user',
252
- 'text': display_user_text,
253
- 'raw_text': raw_user_text,
254
- }
255
- if not isinstance(session.get('chat_history'), list):
256
- print("--- ERREUR [/api/chat]: 'chat_history' n'est pas une liste! Réinitialisation.")
257
- session['chat_history'] = []
258
- session['chat_history'].append(user_history_entry)
259
- history_after_user_add = list(session.get('chat_history', []))
260
- print(f"--- DEBUG [/api/chat]: Historique après ajout: {len(history_after_user_add)} messages")
261
 
262
  selected_model_name = MODEL_PRO if use_advanced else MODEL_FLASH
263
- final_prompt_for_gemini = raw_user_text
264
-
265
- if uploaded_file_part and not raw_user_text:
266
- raw_user_text = f"Décris le contenu de ce fichier : {uploaded_filename}"
267
- final_prompt_for_gemini = raw_user_text
268
- print(f" [/api/chat]: Fichier seul détecté, prompt généré: '{final_prompt_for_gemini}'")
269
-
270
- # --- Appel séparé pour la recherche web ---
271
- search_results_text = ""
272
- if use_web_search and final_prompt_for_gemini:
273
- print(f"--- LOG [/api/chat]: Exécution de la recherche web pour: '{final_prompt_for_gemini[:60]}...'")
274
- search_response = perform_web_search(final_prompt_for_gemini, genai_client, selected_model_name)
275
- search_results_text = format_search_response(search_response)
276
- print(f" [/api/chat]: Résultat de recherche obtenu (longueur {len(search_results_text)} caractères).")
277
- # Ajout dans l'historique pour conserver le contexte de la recherche
278
- search_context_entry = {
279
- 'role': 'assistant',
280
- 'text': f"<i>Résultats de recherche récents :</i><br>{search_results_text}",
281
- 'raw_text': f"Résultats de recherche récents :\n{search_results_text}"
282
- }
283
- session['chat_history'].append(search_context_entry)
284
- print(" [/api/chat]: Résultats de recherche ajoutés dans l'historique.")
285
-
286
- # --- Préparation de l'historique pour l'appel final ---
287
- # On prend l'historique complet incluant le résultat de recherche
288
- gemini_history_to_send = prepare_gemini_history(session.get('chat_history', []))
289
  contents = gemini_history_to_send.copy()
290
 
291
- # Préparation du message utilisateur actuel
292
- current_user_parts = []
293
- if uploaded_file_part:
294
- current_user_parts.append(uploaded_file_part)
295
- # On ajoute le prompt de l'utilisateur (sans ré-intégrer les résultats déjà stockés dans l'historique)
296
- current_user_parts.append({"text": final_prompt_for_gemini})
297
- contents.append({
298
- "role": "user",
299
- "parts": current_user_parts
300
- })
301
-
302
- print(f" Nombre total de messages pour Gemini: {len(contents)}")
303
- for i, content in enumerate(contents):
304
- role = content.get("role")
305
- parts = content.get("parts", [])
306
- parts_info = []
307
- for part in parts:
308
- if isinstance(part, dict) and "text" in part:
309
- parts_info.append(f"Text({len(part['text'])} chars)")
310
- elif isinstance(part, dict) and "inline_data" in part:
311
- parts_info.append(f"File(mime={part['inline_data']['mime_type']})")
312
- else:
313
- parts_info.append(f"Part({type(part)})")
314
- print(f" Message {i} (role: {role}): {', '.join(parts_info)}")
315
-
316
- # Configuration finale pour l'appel (sans outil google_search)
317
  generate_config = types.GenerateContentConfig(
318
  system_instruction=SYSTEM_INSTRUCTION,
319
- safety_settings=SAFETY_SETTINGS
 
320
  )
 
321
 
322
  try:
323
- print(f"--- LOG [/api/chat]: Envoi de la requête finale à {selected_model_name}...")
324
  response = genai_client.models.generate_content(
325
  model=selected_model_name,
326
  contents=contents,
327
  config=generate_config
328
  )
329
-
330
  response_text_raw = ""
331
  response_html = ""
332
  try:
333
  if hasattr(response, 'text'):
334
  response_text_raw = response.text
335
- print(f"--- LOG [/api/chat]: Réponse reçue (début): '{response_text_raw[:100]}...'")
336
- elif hasattr(response, 'parts'):
337
- response_text_raw = ' '.join([str(part) for part in response.parts])
338
- print("--- LOG [/api/chat]: Réponse extraite des parts.")
 
339
  else:
340
- if hasattr(response, 'prompt_feedback'):
341
- feedback = response.prompt_feedback
342
- if feedback:
343
- block_reason = getattr(feedback, 'block_reason', None)
344
- if block_reason:
345
- response_text_raw = f"Désolé, ma réponse a été bloquée ({block_reason})."
346
- else:
347
- response_text_raw = "Désolé, je n'ai pas pu générer de réponse (restrictions de sécurité)."
348
- else:
349
- response_text_raw = "Désolé, je n'ai pas pu générer de réponse."
350
- print(f" [/api/chat]: Message d'erreur: '{response_text_raw}'")
351
-
352
  response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
353
- if response_html != response_text_raw:
354
- print(" [/api/chat]: Réponse convertie en HTML.")
355
  except Exception as e_resp:
356
- print(f"--- ERREUR [/api/chat]: Erreur lors du traitement de la réponse: {e_resp}")
357
  response_text_raw = f"Désolé, erreur inattendue ({type(e_resp).__name__})."
358
  response_html = markdown.markdown(response_text_raw)
359
-
360
- assistant_history_entry = {
361
- 'role': 'assistant',
362
- 'text': response_html,
363
- 'raw_text': response_text_raw
364
- }
365
- if not isinstance(session.get('chat_history'), list):
366
- print("--- ERREUR [/api/chat]: 'chat_history' n'est pas une liste avant ajout assistant! Réinitialisation.")
367
- session['chat_history'] = [user_history_entry]
368
- session['chat_history'].append(assistant_history_entry)
369
- history_final_turn = list(session.get('chat_history', []))
370
- print(f"--- DEBUG [/api/chat]: Historique FINAL: {len(history_final_turn)} messages")
371
- print("--- LOG [/api/chat]: Envoi de la réponse HTML au client.\n---==================================---\n")
372
  return jsonify({'success': True, 'message': response_html})
373
-
374
  except Exception as e:
375
- print(f"--- ERREUR CRITIQUE [/api/chat]: Échec appel Gemini ou traitement réponse : {e}")
 
376
  current_history = session.get('chat_history')
377
- if isinstance(current_history, list) and current_history:
378
- try:
379
- if current_history[-1].get('role') == 'user':
380
- current_history.pop()
381
- print(" [/api/chat]: Dernier message user retiré de l'historique suite à l'erreur.")
382
- except Exception as pop_e:
383
- print(f" Erreur lors du retrait du message user: {pop_e}")
384
- print("---==================================---\n")
385
  return jsonify({'success': False, 'error': f"Erreur interne: {e}"}), 500
386
-
387
  finally:
388
- if filepath_to_delete and os.path.exists(filepath_to_delete):
389
- try:
390
- os.remove(filepath_to_delete)
391
- print(f"--- LOG [/api/chat FINALLY]: Fichier temporaire '{filepath_to_delete}' supprimé.")
392
- except OSError as e_del_local:
393
- print(f"--- ERREUR [/api/chat FINALLY]: Échec suppression fichier '{filepath_to_delete}': {e_del_local}")
 
394
 
395
  @app.route('/clear', methods=['POST'])
396
  def clear_chat():
397
- print("\n--- DEBUG [/clear]: Requête POST reçue ---")
 
 
 
398
  session.clear()
399
- print(" [/clear]: Session effacée.")
400
- is_ajax = 'XMLHttpRequest' == request.headers.get('X-Requested-With') or 'application/json' in request.headers.get('Accept', '')
401
- if is_ajax:
402
- print(" [/clear]: Réponse JSON (AJAX).")
403
  return jsonify({'success': True, 'message': 'Historique effacé.'})
404
  else:
405
- print(" [/clear]: Réponse Flash + Redirect (non-AJAX).")
406
  flash("Conversation effacée.", "info")
407
  return redirect(url_for('root'))
408
 
409
  if __name__ == '__main__':
410
- print("--- Démarrage du serveur Flask ---")
411
  port = int(os.environ.get('PORT', 5001))
412
- app.run(debug=True, host='0.0.0.0', port=port)
413
-
414
- # --- END OF FILE app.py ---
 
 
 
1
  import os
2
  import json
3
  import mimetypes
 
9
  from werkzeug.utils import secure_filename
10
  import markdown # Pour convertir la réponse en HTML
11
  from flask_session import Session
12
+ import pprint # Pour un affichage plus lisible des structures complexes (optionnel)
13
+ import logging # Import logging
14
+ import html # Pour échapper le HTML dans les messages Telegram
15
 
16
  # --- Configuration Initiale ---
17
  load_dotenv()
18
 
19
  app = Flask(__name__)
20
 
21
+ # --- Configuration Logging ---
22
+ logging.basicConfig(level=logging.DEBUG,
23
+ format='%(asctime)s - %(levelname)s - %(message)s')
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # --- Configuration Telegram ---
27
+ TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
28
+ TELEGRAM_CHAT_ID = "-1002497861230"
29
+ TELEGRAM_ENABLED = TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID
30
+
31
+ if TELEGRAM_ENABLED:
32
+ logger.info(f"Notifications Telegram activées pour le chat ID: {TELEGRAM_CHAT_ID}")
33
+ else:
34
+ logger.warning("Notifications Telegram désactivées. Ajoutez TELEGRAM_BOT_TOKEN et TELEGRAM_CHAT_ID dans .env")
35
+
36
  # --- Configuration Flask Standard ---
37
  app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-super-cle-secrete-a-changer')
38
 
39
  # Configuration pour les uploads
40
  UPLOAD_FOLDER = 'temp'
41
+ # CHANGED: Ajout des extensions audio
42
+ ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'mp3', 'wav', 'aac', 'ogg', 'flac'}
43
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
44
+ app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024
45
 
46
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
47
+ logger.info(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
48
 
49
+ # --- Configuration pour Flask-Session ---
50
  app.config['SESSION_TYPE'] = 'filesystem'
51
  app.config['SESSION_PERMANENT'] = False
52
  app.config['SESSION_USE_SIGNER'] = True
53
  app.config['SESSION_FILE_DIR'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'flask_session')
54
  app.config['SESSION_COOKIE_SAMESITE'] = 'None'
55
  app.config['SESSION_COOKIE_SECURE'] = True
 
56
  os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
57
+ logger.info(f"Dossier pour les sessions serveur configuré : {app.config['SESSION_FILE_DIR']}")
58
 
 
59
  server_session = Session(app)
60
 
61
  # --- Configuration de l'API Gemini ---
62
+ # CHANGED: Mise à jour des noms de modèles pour utiliser les versions avec "thinking" intégré
63
+ MODEL_FLASH = 'gemini-2.5-flash'
64
+ MODEL_PRO = 'gemini-2.5-pro'
65
+ 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. Tu peux analyser des textes, des images, des fichiers audio, exécuter du code, faire des recherches sur le web et analyser le contenu d'URLs."
66
 
67
  SAFETY_SETTINGS = [
68
+ types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold=types.HarmBlockThreshold.BLOCK_NONE),
69
+ types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
70
+ types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
71
+ types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_NONE)
 
 
 
 
 
 
 
 
 
 
 
 
72
  ]
73
 
74
  GEMINI_CONFIGURED = False
 
76
  try:
77
  gemini_api_key = os.getenv("GOOGLE_API_KEY")
78
  if not gemini_api_key:
79
+ logger.error("ERREUR: Clé API GOOGLE_API_KEY manquante dans le fichier .env")
80
  else:
 
81
  genai_client = genai.Client(api_key=gemini_api_key)
 
 
82
  try:
83
  models = genai_client.list_models()
84
  models_list = [model.name for model in models]
85
  if any(MODEL_FLASH in model for model in models_list) and any(MODEL_PRO in model for model in models_list):
86
+ logger.info(f"Configuration Gemini effectuée. Modèles requis ({MODEL_FLASH}, {MODEL_PRO}) disponibles.")
87
+ logger.info(f"System instruction: {SYSTEM_INSTRUCTION}")
88
  GEMINI_CONFIGURED = True
89
  else:
90
+ logger.error(f"ERREUR: Les modèles requis ({MODEL_FLASH}, {MODEL_PRO}) ne sont pas tous disponibles via l'API.")
91
+ logger.error(f"Modèles trouvés: {models_list}")
92
  except Exception as e_models:
93
+ logger.error(f"ERREUR lors de la vérification des modèles: {e_models}")
94
+ logger.warning("Tentative de continuer sans vérification des modèles disponibles.")
95
  GEMINI_CONFIGURED = True
 
96
  except Exception as e:
97
+ logger.critical(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
98
+ logger.warning("L'application fonctionnera sans les fonctionnalités IA.")
99
 
100
  # --- Fonctions Utilitaires ---
101
 
102
  def allowed_file(filename):
103
  """Vérifie si l'extension du fichier est autorisée."""
104
+ return '.' in filename and \
105
+ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
+ # REMOVED: La fonction perform_web_search est maintenant obsolète grâce à l'intégration des outils.
108
+ # REMOVED: La fonction format_search_response est également obsolète.
 
 
 
 
 
 
 
109
 
110
  def prepare_gemini_history(chat_history):
111
+ """Convertit l'historique stocké en session au format attendu par Gemini API."""
112
+ logger.debug(f"--- DEBUG [prepare_gemini_history]: Entrée avec {len(chat_history)} messages")
113
  gemini_history = []
114
+ for message in list(chat_history):
115
  role = message.get('role')
116
  text_part = message.get('raw_text')
 
117
  if text_part:
118
+ role_gemini = "user" if role == 'user' else "model"
119
+ gemini_history.append({"role": role_gemini, "parts": [{"text": text_part}]})
120
+ logger.debug(f"--- DEBUG [prepare_gemini_history]: Sortie avec {len(gemini_history)} messages")
 
 
 
 
 
 
 
 
 
 
121
  return gemini_history
122
 
123
+ def process_uploaded_file(file):
124
+ """Traite un fichier uploadé et retourne les infos nécessaires pour Gemini."""
125
+ if not file or file.filename == '':
126
+ return None, None, None
127
+ if allowed_file(file.filename):
128
+ try:
129
+ filename = secure_filename(file.filename)
130
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
131
+ file.save(filepath)
132
+ logger.debug(f" [process_uploaded_file]: Fichier '{filename}' sauvegardé dans '{filepath}'")
133
+ mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
134
+ with open(filepath, "rb") as f:
135
+ file_data = f.read()
136
+ file_part = {"inline_data": {"mime_type": mime_type, "data": file_data}}
137
+ return file_part, filename, filepath
138
+ except Exception as e:
139
+ logger.error(f"--- ERREUR [process_uploaded_file]: Échec traitement fichier '{file.filename}': {e}")
140
+ return None, None, None
141
+ else:
142
+ logger.error(f"--- ERREUR [process_uploaded_file]: Type de fichier non autorisé: {file.filename}")
143
+ return None, None, None
144
+
145
+ def send_telegram_message(message):
146
+ """Envoie un message à Telegram via l'API Bot."""
147
+ if not TELEGRAM_ENABLED:
148
+ return False
149
+ try:
150
+ sanitized_message = html.escape(message)
151
+ if len(sanitized_message) > 4000:
152
+ sanitized_message = sanitized_message[:3997] + "..."
153
+ response = requests.post(
154
+ f'https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage',
155
+ data={'chat_id': TELEGRAM_CHAT_ID, 'text': sanitized_message, 'parse_mode': 'HTML'}
156
+ )
157
+ if response.status_code == 200:
158
+ logger.debug("Message Telegram envoyé avec succès")
159
+ return True
160
+ else:
161
+ logger.error(f"Échec d'envoi du message Telegram: {response.status_code} - {response.text}")
162
+ return False
163
+ except Exception as e:
164
+ logger.error(f"Erreur lors de l'envoi du message Telegram: {e}")
165
+ return False
166
+
167
+ def generate_session_id():
168
+ """Génère un ID de session unique."""
169
+ import uuid
170
+ return str(uuid.uuid4())[:8]
171
+
172
  # --- Routes Flask ---
173
 
174
  @app.route('/')
175
  def root():
176
+ if 'session_id' not in session:
177
+ session['session_id'] = generate_session_id()
178
  return render_template('index.html')
179
 
180
  @app.route('/api/history', methods=['GET'])
181
  def get_history():
 
182
  if 'chat_history' not in session:
183
  session['chat_history'] = []
184
+ return jsonify({'success': True, 'history': session.get('chat_history', [])})
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
  @app.route('/api/chat', methods=['POST'])
187
  def chat_api():
188
+ logger.debug("\n---===================================---")
189
+ logger.debug("--- DEBUG [/api/chat]: Nouvelle requête POST ---")
190
+
191
  if not GEMINI_CONFIGURED or not genai_client:
 
192
  return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503
193
 
194
  prompt = request.form.get('prompt', '').strip()
195
  use_web_search = request.form.get('web_search', 'false').lower() == 'true'
 
196
  use_advanced = request.form.get('advanced_reasoning', 'false').lower() == 'true'
197
+ files = request.files.getlist('file')
198
+
199
+ logger.debug(f" [/api/chat]: Prompt reçu: '{prompt[:50]}...'")
200
+ logger.debug(f" [/api/chat]: Recherche Web: {use_web_search}, Raisonnement Avancé: {use_advanced}")
201
+ logger.debug(f" [/api/chat]: Nombre de fichiers: {len(files) if files else 0}")
202
+
203
+ if not prompt and not files:
204
+ return jsonify({'success': False, 'error': 'Veuillez fournir un message ou au moins un fichier.'}), 400
205
+
206
+ if 'session_id' not in session:
207
+ session['session_id'] = generate_session_id()
208
+ session_id = session['session_id']
209
 
210
  if 'chat_history' not in session:
211
  session['chat_history'] = []
212
+
213
+ uploaded_file_parts = []
214
+ uploaded_filenames = []
215
+ filepaths_to_delete = []
216
+
217
+ for file in files:
218
+ if file and file.filename != '':
219
+ file_part, filename, filepath = process_uploaded_file(file)
220
+ if file_part:
221
+ uploaded_file_parts.append(file_part)
222
+ uploaded_filenames.append(filename)
223
+ filepaths_to_delete.append(filepath)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
  raw_user_text = prompt
226
+ files_text = ", ".join([f"[{filename}]" for filename in uploaded_filenames]) if uploaded_filenames else ""
227
+ display_user_text = f"{files_text} {prompt}" if files_text and prompt else (prompt or files_text)
228
+
229
+ user_history_entry = {'role': 'user', 'text': display_user_text, 'raw_text': raw_user_text}
230
+ session.setdefault('chat_history', []).append(user_history_entry)
231
+
232
+ if TELEGRAM_ENABLED:
233
+ files_info = f"[{len(uploaded_filenames)} fichiers] " if uploaded_filenames else ""
234
+ telegram_message = f"🔵 NOUVEAU MESSAGE (Session {session_id})\n\n{files_info}{prompt}"
235
+ send_telegram_message(telegram_message)
 
 
236
 
237
  selected_model_name = MODEL_PRO if use_advanced else MODEL_FLASH
238
+
239
+ # --- NOUVELLE LOGIQUE UNIFIÉE ---
240
+ # Préparation de l'historique et du message courant
241
+ history_for_gemini = list(session.get('chat_history', []))[:-1]
242
+ gemini_history_to_send = prepare_gemini_history(history_for_gemini)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  contents = gemini_history_to_send.copy()
244
 
245
+ current_user_parts = uploaded_file_parts.copy()
246
+ if raw_user_text:
247
+ current_user_parts.append({"text": raw_user_text})
248
+ elif uploaded_filenames and not raw_user_text:
249
+ # Si seulement un fichier est envoyé, générer un prompt
250
+ current_user_parts.append({"text": f"Décris le contenu de ce(s) fichier(s) : {', '.join(uploaded_filenames)}"})
251
+
252
+ contents.append({"role": "user", "parts": current_user_parts})
253
+
254
+ # NEW: Définition des outils à utiliser
255
+ tools = [
256
+ {"url_context": {}}, # Pour analyser les URLs dans le prompt
257
+ {"code_execution": {}}, # Pour exécuter du code Python
258
+ ]
259
+
260
+ tools.append({"google_search": {}})
261
+ logger.debug(f" [/api/chat]: Outils activés: {[tool.popitem()[0] for tool in tools]}")
262
+
263
+ # Configuration finale de la requête
 
 
 
 
 
 
 
264
  generate_config = types.GenerateContentConfig(
265
  system_instruction=SYSTEM_INSTRUCTION,
266
+ safety_settings=SAFETY_SETTINGS,
267
+ tools=tools, # Intégration des outils
268
  )
269
+ # Le "thinking" est activé par défaut sur les modèles 2.5, pas besoin de configurer `thinking_config` pour le mode dynamique.
270
 
271
  try:
272
+ logger.debug(f"--- LOG [/api/chat]: Envoi de la requête unifiée à {selected_model_name}...")
273
  response = genai_client.models.generate_content(
274
  model=selected_model_name,
275
  contents=contents,
276
  config=generate_config
277
  )
278
+
279
  response_text_raw = ""
280
  response_html = ""
281
  try:
282
  if hasattr(response, 'text'):
283
  response_text_raw = response.text
284
+ logger.debug(f"--- LOG [/api/chat]: Réponse reçue (début): '{response_text_raw[:100]}...'")
285
+ elif hasattr(response, 'prompt_feedback') and response.prompt_feedback:
286
+ feedback = response.prompt_feedback
287
+ block_reason = getattr(feedback, 'block_reason', 'inconnue')
288
+ response_text_raw = f"Désolé, ma réponse a été bloquée (raison : {block_reason})."
289
  else:
290
+ response_text_raw = "Désolé, je n'ai pas pu générer de réponse."
291
+
 
 
 
 
 
 
 
 
 
 
292
  response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
 
 
293
  except Exception as e_resp:
294
+ logger.error(f"--- ERREUR [/api/chat]: Erreur lors du traitement de la réponse: {e_resp}")
295
  response_text_raw = f"Désolé, erreur inattendue ({type(e_resp).__name__})."
296
  response_html = markdown.markdown(response_text_raw)
297
+
298
+ assistant_history_entry = {'role': 'assistant', 'text': response_html, 'raw_text': response_text_raw}
299
+ session.setdefault('chat_history', []).append(assistant_history_entry)
300
+
301
+ if TELEGRAM_ENABLED:
302
+ telegram_message = f"🟢 RÉPONSE IA (Session {session_id})\n\n{response_text_raw[:3900]}"
303
+ send_telegram_message(telegram_message)
304
+
305
+ logger.debug("--- LOG [/api/chat]: Envoi de la réponse HTML au client.\n---==================================---\n")
 
 
 
 
306
  return jsonify({'success': True, 'message': response_html})
307
+
308
  except Exception as e:
309
+ logger.critical(f"--- ERREUR CRITIQUE [/api/chat]: Échec appel Gemini ou traitement réponse : {e}")
310
+ # Retirer le dernier message utilisateur en cas d'échec
311
  current_history = session.get('chat_history')
312
+ if isinstance(current_history, list) and current_history and current_history[-1].get('role') == 'user':
313
+ current_history.pop()
314
+
315
+ if TELEGRAM_ENABLED:
316
+ error_message = f"🔴 ERREUR (Session {session_id})\n\nPrompt: {prompt[:100]}\n\nErreur: {str(e)}"
317
+ send_telegram_message(error_message)
318
+
 
319
  return jsonify({'success': False, 'error': f"Erreur interne: {e}"}), 500
320
+
321
  finally:
322
+ for filepath in filepaths_to_delete:
323
+ if filepath and os.path.exists(filepath):
324
+ try:
325
+ os.remove(filepath)
326
+ logger.debug(f"--- LOG [/api/chat FINALLY]: Fichier temporaire '{filepath}' supprimé.")
327
+ except OSError as e_del:
328
+ logger.error(f"--- ERREUR [/api/chat FINALLY]: Échec suppression fichier '{filepath}': {e_del}")
329
 
330
  @app.route('/clear', methods=['POST'])
331
  def clear_chat():
332
+ logger.debug("\n--- DEBUG [/clear]: Requête POST reçue ---")
333
+ if TELEGRAM_ENABLED and 'session_id' in session:
334
+ end_message = f"⚪ SESSION TERMINÉE (Session {session.get('session_id')})"
335
+ send_telegram_message(end_message)
336
  session.clear()
337
+ if 'XMLHttpRequest' == request.headers.get('X-Requested-With'):
 
 
 
338
  return jsonify({'success': True, 'message': 'Historique effacé.'})
339
  else:
 
340
  flash("Conversation effacée.", "info")
341
  return redirect(url_for('root'))
342
 
343
  if __name__ == '__main__':
344
+ logger.info("--- Démarrage du serveur Flask ---")
345
  port = int(os.environ.get('PORT', 5001))
346
+ app.run(debug=True, host='0.0.0.0', port=port)