File size: 27,917 Bytes
3773549
 
c253059
da7ef42
ccfefd3
0f75c3f
10ad7a5
b212844
60b2cb1
c253059
b212844
3773549
c23cd74
ccfefd3
b212844
c23cd74
da7ef42
0ab0c95
3773549
 
 
 
ccfefd3
3773549
10ad7a5
3773549
10ad7a5
b212844
0ab0c95
3773549
10ad7a5
ccfefd3
10ad7a5
3773549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccfefd3
b212844
 
3773549
b212844
 
 
 
 
 
9c39fae
0f75c3f
10ad7a5
ccfefd3
 
b212844
ccfefd3
b212844
 
 
 
 
 
 
 
 
 
 
0ab0c95
c23cd74
0f75c3f
ccfefd3
71ffbd5
ccfefd3
c23cd74
 
b212844
 
 
0ab0c95
da7ef42
b212844
ccfefd3
 
 
10ad7a5
b212844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10ad7a5
 
b212844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3773549
b212844
 
3773549
b212844
 
 
ccfefd3
b212844
 
 
 
 
 
 
 
 
 
 
 
984f26b
b212844
 
 
 
 
0ab0c95
10ad7a5
b212844
10ad7a5
3773549
 
 
b212844
3773549
 
 
 
 
 
 
 
 
 
 
 
 
 
10ad7a5
c23cd74
 
c96425b
ccfefd3
 
0f75c3f
3773549
b212844
9c39fae
ccfefd3
 
dfacd64
3773549
dfacd64
 
 
 
3773549
 
 
 
 
 
 
 
 
 
 
 
 
 
dfacd64
c23cd74
dfc4deb
 
dfacd64
 
 
 
 
b212844
10ad7a5
dfacd64
b212844
a08ef55
dfacd64
b212844
10ad7a5
dfacd64
b212844
 
ccfefd3
dfacd64
 
 
 
 
 
3773549
dfacd64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b212844
 
 
 
 
 
 
 
 
 
 
 
 
 
3773549
 
 
 
b212844
3773549
 
b212844
 
 
 
3773549
 
 
 
 
 
 
 
 
 
b212844
3773549
b212844
 
3773549
b212844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3773549
 
 
 
 
 
 
 
 
 
 
 
984f26b
 
b212844
 
 
3773549
 
 
b212844
3773549
b212844
ccfefd3
60b2cb1
3773549
 
 
 
 
 
 
b212844
 
 
9c39fae
b212844
 
 
 
 
 
0f75c3f
 
b212844
 
 
 
 
 
 
 
3773549
 
0f75c3f
3773549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b212844
 
3773549
 
 
 
b212844
 
 
 
 
 
 
 
3773549
 
 
 
ccfefd3
3773549
c253059
b212844
 
ccfefd3
dfc4deb
984f26b
b212844
 
 
 
3773549
 
 
 
 
 
 
 
 
 
 
 
b212844
3773549
b212844
 
 
a08ef55
ccfefd3
3773549
 
 
 
 
 
 
 
 
 
 
 
ccfefd3
 
 
3773549
 
 
b212844
10ad7a5
 
c253059
ccfefd3
3773549
 
b212844
 
 
3773549
b212844
ccfefd3
 
 
b212844
ccfefd3
b212844
ccfefd3
0ab0c95
b212844
c253059
ccfefd3
b212844
 
 
 
3773549
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# --- START OF FILE app.py ---

import os
import json
import mimetypes
from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template
from dotenv import load_dotenv
import google.generativeai as genai
import requests
from werkzeug.utils import secure_filename
import markdown # Pour convertir la réponse en HTML
from flask_session import Session # <-- Importer Session

# --- Configuration Initiale ---
load_dotenv()

app = Flask(__name__)

# --- Configuration Flask Standard ---
# Clé secrète FORTEMENT recommandée (vous l'avez déjà)
# Gardez-la secrète en production !
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-super-cle-secrete-a-changer')

# Configuration pour les uploads (vous l'avez déjà)
UPLOAD_FOLDER = 'temp'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024  # Limite de taille (ex: 25MB)

# Créer le dossier temp s'il n'existe pas (vous l'avez déjà)
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")

# --- Configuration pour Flask-Session (Backend Filesystem) ---
app.config['SESSION_TYPE'] = 'filesystem'  # Indique d'utiliser le stockage par fichiers
app.config['SESSION_PERMANENT'] = False  # La session expire quand le navigateur est fermé
app.config['SESSION_USE_SIGNER'] = True  # Signe l'ID de session dans le cookie pour sécurité
app.config['SESSION_FILE_DIR'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'flask_session') # Chemin où stocker les fichiers de session
# os.path.join(...) crée un chemin vers un dossier nommé 'flask_session'
# à côté de votre fichier app.py

# Crée le dossier pour les sessions filesystem s'il n'existe pas
os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
print(f"Dossier pour les sessions serveur configuré : {app.config['SESSION_FILE_DIR']}")


# --- Initialisation de Flask-Session ---
# Doit être fait APRÈS avoir défini la configuration de session
server_session = Session(app) # <-- Lie l'extension à votre application

# --- Configuration de l'API Gemini ---
MODEL_FLASH = 'gemini-2.0-flash' # Default model
MODEL_PRO = 'gemini-2.5-pro-exp-03-25' # Advanced model
# ASSUREZ-VOUS QUE SYSTEM_INSTRUCTION EST BIEN DÉFINI ICI OU CHARGÉ
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."
SAFETY_SETTINGS = [
    {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
    {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
    {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
    {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
]
GEMINI_CONFIGURED = False
try:
    gemini_api_key = os.getenv("GOOGLE_API_KEY")
    if not gemini_api_key:
        print("ERREUR: Clé API GOOGLE_API_KEY manquante dans le fichier .env")
    else:
        genai.configure(api_key=gemini_api_key)
        # Just configure, don't create model instance yet
        # Check if we can list models as a basic configuration test
        models_list = [m.name for m in genai.list_models()]
        if f'models/{MODEL_FLASH}' in models_list and f'models/{MODEL_PRO}' in models_list:
             print(f"Configuration Gemini effectuée. Modèles requis ({MODEL_FLASH}, {MODEL_PRO}) disponibles.")
             print(f"System instruction: {SYSTEM_INSTRUCTION}")
             GEMINI_CONFIGURED = True
        else:
            print(f"ERREUR: Les modèles requis ({MODEL_FLASH}, {MODEL_PRO}) ne sont pas tous disponibles via l'API.")
            print(f"Modèles trouvés: {models_list}")

except Exception as e:
    print(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
    print("L'application fonctionnera sans les fonctionnalités IA.")

# --- Fonctions Utilitaires ---

def allowed_file(filename):
    """Vérifie si l'extension du fichier est autorisée."""
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def perform_web_search(query):
    """Effectue une recherche web via l'API Serper."""
    serper_api_key = os.getenv("SERPER_API_KEY")
    if not serper_api_key:
        print("AVERTISSEMENT: Clé API SERPER_API_KEY manquante. Recherche web désactivée.")
        return None

    search_url = "https://google.serper.dev/search"
    headers = {
        'X-API-KEY': serper_api_key,
        'Content-Type': 'application/json'
    }
    payload = json.dumps({"q": query, "gl": "fr", "hl": "fr"}) # Ajout localisation FR

    try:
        print(f"Recherche Serper pour: '{query}'")
        response = requests.post(search_url, headers=headers, data=payload, timeout=10)
        response.raise_for_status() # Lève une exception pour les erreurs HTTP (4xx, 5xx)
        data = response.json()
        print("Résultats de recherche Serper obtenus.")
        # print(json.dumps(data, indent=2)) # Décommenter pour voir les résultats bruts
        return data
    except requests.exceptions.Timeout:
        print("Erreur lors de la recherche web : Timeout")
        return None
    except requests.exceptions.RequestException as e:
        print(f"Erreur lors de la recherche web : {e}")
        # Essayer de lire le corps de la réponse d'erreur si possible
        try:
            error_details = e.response.json()
            print(f"Détails de l'erreur Serper: {error_details}")
        except:
            pass # Ignorer si le corps n'est pas JSON ou n'existe pas
        return None
    except json.JSONDecodeError as e:
        print(f"Erreur lors du décodage de la réponse JSON de Serper : {e}")
        print(f"Réponse reçue (texte brut) : {response.text}")
        return None

def format_search_results(data):
    """Met en forme les résultats de recherche (format Markdown)."""
    if not data:
        return "Aucun résultat de recherche web trouvé pertinent."

    results = []

    # Réponse directe (Answer Box)
    if data.get('answerBox'):
        ab = data['answerBox']
        title = ab.get('title', '')
        snippet = ab.get('snippet') or ab.get('answer', '')
        if snippet:
            results.append(f"**Réponse rapide : {title}**\n{snippet}\n")

    # Knowledge Graph
    if data.get('knowledgeGraph'):
        kg = data['knowledgeGraph']
        title = kg.get('title', '')
        type = kg.get('type', '')
        description = kg.get('description', '')
        if title and description:
             results.append(f"**{title} ({type})**\n{description}\n")
        if kg.get('attributes'):
             results.append("\n**Attributs :**") # Ajout titre pour clarté
             for attr, value in kg['attributes'].items():
                  results.append(f"- {attr}: {value}")
             results.append("") # Ligne vide après les attributs


    # Résultats organiques
    if data.get('organic'):
        results.append("**Pages web pertinentes :**")
        for i, item in enumerate(data['organic'][:3], 1): # Top 3
            title = item.get('title', 'Sans titre')
            link = item.get('link', '#')
            snippet = item.get('snippet', 'Pas de description.')
            results.append(f"{i}. **[{title}]({link})**\n   {snippet}\n")

    # People Also Ask
    if data.get('peopleAlsoAsk'):
        results.append("**Questions liées :**")
        for i, item in enumerate(data['peopleAlsoAsk'][:2], 1): # Top 2
             results.append(f"- {item.get('question', '')}")


    if not results:
        return "Aucun résultat structuré trouvé dans la recherche web."

    return "\n".join(results)

def prepare_gemini_history(chat_history):
    """Convertit l'historique stocké en session au format attendu par Gemini API."""
    gemini_history = []
    # Itérer sur une copie pour éviter les problèmes si on modifie pendant l'itération (peu probable ici mais bonne pratique)
    for message in list(chat_history):
        role = 'user' if message.get('role') == 'user' else 'model'
        # Utiliser le 'raw_text' stocké pour Gemini
        text_part = message.get('raw_text') # Utiliser get pour éviter KeyError si 'raw_text' manque
        if text_part is None:
             print(f"AVERTISSEMENT: 'raw_text' manquant pour un message {role}, utilisation de 'text'. Message: {message.get('text', '')[:50]}...")
             text_part = message.get('text', '') # Fallback vers 'text' si raw_text est absent

        # Vérifier si text_part n'est pas vide avant d'ajouter
        if text_part:
            parts = [text_part]
            # NOTE: La gestion des fichiers des tours PRÉCÉDENTS n'est pas gérée ici.
            # L'API generate_content se concentre généralement sur le fichier du tour ACTUEL.
            gemini_history.append({'role': role, 'parts': parts})
        else:
            print(f"AVERTISSEMENT: Message vide détecté et ignoré pour l'historique Gemini (role: {role}).")

    return gemini_history

# --- Routes Flask ---

@app.route('/')
def root():
    """Sert la page HTML principale."""
    # Pas besoin de toucher à l'historique ici, le frontend le demandera via /api/history
    return render_template('index.html')

@app.route('/api/history', methods=['GET'])
def get_history():
    """Fournit l'historique de chat stocké en session au format JSON."""
    # Flask-Session gère la session (filesystem dans ce cas)
    if 'chat_history' not in session:
        session['chat_history'] = []

    # Préparer l'historique pour l'affichage (contient déjà le HTML pour l'assistant)
    # Il est crucial que les messages stockés aient 'role' et 'text'
    display_history = []
    for msg in session.get('chat_history', []):
         # Vérification minimale pour éviter les erreurs si la structure est corrompue
         if isinstance(msg, dict) and 'role' in msg and 'text' in msg:
             display_history.append({
                 'role': msg.get('role'),
                 'text': msg.get('text')
             })
         else:
             print(f"AVERTISSEMENT: Format de message invalide dans l'historique de session ignoré: {msg}")


    print(f"API: Récupération de l'historique ({len(display_history)} messages depuis la session serveur)")
    return jsonify({'success': True, 'history': display_history})

@app.route('/api/chat', methods=['POST'])
def chat_api():
    """Gère les nouvelles requêtes de chat via AJAX."""
    if not GEMINI_CONFIGURED:
        print("API ERREUR: Tentative d'appel à /api/chat sans configuration Gemini valide.")
        return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503 # Service Unavailable

    # Récupération des données du formulaire
    prompt = request.form.get('prompt', '').strip()
    use_web_search_str = request.form.get('web_search', 'false') # 'true' ou 'false'
    use_web_search = use_web_search_str.lower() == 'true'
    file = request.files.get('file')
    use_advanced_str = request.form.get('advanced_reasoning', 'false') # Get the new flag
    use_advanced = use_advanced_str.lower() == 'true'

    # Validation simple
    if not prompt and not file:
        return jsonify({'success': False, 'error': 'Veuillez fournir un message ou un fichier.'}), 400

    print(f"\n--- Nouvelle requête /api/chat ---")
    print(f"Prompt reçu: '{prompt[:50]}...'")
    print(f"Recherche Web activée: {use_web_search}")
    print(f"Raisonnement avancé demandé: {use_advanced}")
    print(f"Fichier reçu: {file.filename if file else 'Aucun'}")

    # Initialiser l'historique de session si nécessaire (géré par Flask-Session)
    if 'chat_history' not in session:
        session['chat_history'] = []

    uploaded_gemini_file = None # L'objet fichier retourné par genai.upload_file
    uploaded_filename = None    # Juste le nom du fichier pour référence/affichage
    filepath_to_delete = None   # Chemin du fichier local à supprimer après traitement

    # --- Gestion de l'upload de fichier ---
    if file and file.filename != '':
        if allowed_file(file.filename):
            try:
                filename = secure_filename(file.filename)
                filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
                file.save(filepath)
                filepath_to_delete = filepath # Marquer pour suppression
                uploaded_filename = filename
                print(f"Fichier '{filename}' sauvegardé temporairement dans '{filepath}'")

                # Détecter le MimeType pour Gemini
                mime_type = mimetypes.guess_type(filepath)[0]
                if not mime_type:
                    mime_type = 'application/octet-stream' # Fallback
                    print(f"AVERTISSEMENT: Impossible de deviner le MimeType pour '{filename}', utilisation de '{mime_type}'.")

                # Uploader vers Google AI (peut prendre du temps)
                print(f"Upload du fichier vers Google AI (MimeType: {mime_type})...")
                # Note: L'API upload_file est générique et ne dépend pas du modèle Flash/Pro
                uploaded_gemini_file = genai.upload_file(path=filepath, mime_type=mime_type)
                print(f"Fichier '{uploaded_gemini_file.name}' uploadé avec succès vers Google AI.")

            except Exception as e:
                print(f"ERREUR Critique lors du traitement/upload du fichier '{filename}': {e}")
                # Supprimer le fichier local même en cas d'erreur d'upload Gemini
                if filepath_to_delete and os.path.exists(filepath_to_delete):
                    try:
                        os.remove(filepath_to_delete)
                        print(f"Fichier temporaire '{filepath_to_delete}' supprimé après erreur.")
                    except OSError as del_e:
                         print(f"Erreur lors de la suppression du fichier temporaire après erreur: {del_e}")
                # Renvoyer une erreur claire au client
                return jsonify({'success': False, 'error': f"Erreur lors du traitement du fichier: {e}"}), 500
        else:
            print(f"ERREUR: Type de fichier non autorisé: {file.filename}")
            return jsonify({'success': False, 'error': f"Type de fichier non autorisé. Extensions permises: {', '.join(ALLOWED_EXTENSIONS)}"}), 400

    # --- Préparation du message utilisateur pour l'historique et Gemini ---
    # Texte brut pour Gemini (et pour l'historique interne)
    raw_user_text = prompt
    # Texte pour l'affichage dans l'interface (peut inclure le nom de fichier)
    display_user_text = f"[{uploaded_filename}] {prompt}" if uploaded_filename and prompt else (prompt or f"[{uploaded_filename}]")

    # Ajout à l'historique de session
    user_history_entry = {
        'role': 'user',
        'text': display_user_text, # Pour get_history et potentiellement debug
        'raw_text': raw_user_text,   # Pour l'envoi à Gemini via prepare_gemini_history
        # On ne stocke PAS l'objet 'uploaded_gemini_file' dans la session
    }
    # Vérifier que l'historique est bien une liste avant d'ajouter
    if not isinstance(session.get('chat_history'), list):
        print("ERREUR: chat_history dans la session n'est pas une liste! Réinitialisation.")
        session['chat_history'] = []
    session['chat_history'].append(user_history_entry)
    # session.modified = True # Normalement plus nécessaire avec Flask-Session Filesystem/Redis etc.
                               # mais ne fait pas de mal de le laisser pour l'instant.

    # --- Préparation des 'parts' pour l'appel Gemini ACTUEL ---
    current_gemini_parts = []
    if uploaded_gemini_file:
        # Important: Ne pas envoyer l'objet fichier lui-même si le prompt est vide.
        # Gemini pourrait avoir du mal à l'interpréter sans contexte textuel.
        if raw_user_text:
             current_gemini_parts.append(uploaded_gemini_file)
        else:
             # Si seulement un fichier est envoyé, créer un prompt minimal
             raw_user_text = f"Décris le contenu de ce fichier : {uploaded_filename}"
             print(f"INFO: Fichier seul détecté, utilisation du prompt généré: '{raw_user_text}'")
             current_gemini_parts.append(uploaded_gemini_file)


    final_prompt_for_gemini = raw_user_text # Commencer avec le texte brut (potentiellement généré ci-dessus)

    # --- Recherche Web (si activée et si un prompt textuel existe) ---
    if use_web_search and raw_user_text: # Vérifier qu'on a bien du texte pour chercher
        print("Activation de la recherche web...")
        search_data = perform_web_search(raw_user_text)
        if search_data:
            formatted_results = format_search_results(search_data)
            # Construire un prompt enrichi pour Gemini
            final_prompt_for_gemini = f"""Voici la question originale de l'utilisateur:
"{raw_user_text}"

J'ai effectué une recherche web et voici les informations pertinentes trouvées:
--- DEBUT RESULTATS WEB ---
{formatted_results}
--- FIN RESULTATS WEB ---

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."""
            print("Prompt enrichi avec les résultats de recherche web.")
        else:
            print("Aucun résultat de recherche web pertinent trouvé ou erreur, utilisation du prompt original/généré.")
            # final_prompt_for_gemini reste raw_user_text ou le prompt généré

    # Ajouter le texte (potentiellement enrichi) aux parts pour Gemini UNIQUEMENT s'il y a du texte
    if final_prompt_for_gemini:
         current_gemini_parts.append(final_prompt_for_gemini)
    elif not current_gemini_parts: # Si on n'a ni fichier utile ni texte
         print("ERREUR: Aucune donnée (texte ou fichier valide) à envoyer à Gemini.")
         # Retirer le message utilisateur vide ajouté précédemment
         if session.get('chat_history'):
            session['chat_history'].pop()
         return jsonify({'success': False, 'error': "Impossible d'envoyer une requête vide à l'IA."}), 400


    # --- Appel à l'API Gemini ---
    try:
        # Préparer l'historique des messages PRÉCÉDENTS
        # Utiliser une copie de la session pour éviter les modifications pendant la préparation
        history_copy = list(session.get('chat_history', []))
        gemini_history = prepare_gemini_history(history_copy[:-1]) # Exclut le message actuel
        print(f"Préparation de l'appel Gemini avec {len(gemini_history)} messages d'historique.")

        # Construire le contenu complet pour l'appel
        contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]

        # DEBUG: Afficher ce qui va être envoyé (peut être très verbeux)
        # print("--- Contenu envoyé à Gemini ---")
        # import pprint
        # pprint.pprint(contents_for_gemini)
        # print("-----------------------------")


        # Choisir le nom du modèle à utiliser
        selected_model_name = MODEL_PRO if use_advanced else MODEL_FLASH
        print(f"Utilisation du modèle Gemini: {selected_model_name}")

        # Créer l'instance du modèle spécifique pour cette requête
        # Réutiliser les paramètres globaux (safety, system instruction)
        active_model = genai.GenerativeModel(
            model_name=selected_model_name,
            safety_settings=SAFETY_SETTINGS, # defined globally
            system_instruction=SYSTEM_INSTRUCTION # defined globally
        )

        # Appel API
        print(f"Envoi de la requête à {selected_model_name}...")
        # Utilisation de generate_content en mode non-streamé
        response = active_model.generate_content(contents_for_gemini)
        # print(response) # Décommenter pour voir la réponse brute de l'API

        # Extraire le texte de la réponse (gestion d'erreur potentielle ici si la réponse est bloquée etc.)
        # Gérer le cas où la réponse est bloquée par les safety settings
        response_text_raw = ""
        response_html = ""
        try:
             # Vérifier si la réponse a des 'parts' avant d'essayer d'accéder à .text
             if response.parts:
                response_text_raw = response.text # .text concatène les parts textuelles
             else:
                # Si pas de parts, vérifier le prompt_feedback pour la raison (blocage probable)
                print(f"AVERTISSEMENT: Réponse de Gemini sans contenu textuel ('parts'). Feedback: {response.prompt_feedback}")
                # Donner un message d'erreur plus informatif basé sur le feedback si possible
                if response.prompt_feedback and response.prompt_feedback.block_reason:
                    reason = response.prompt_feedback.block_reason.name # Ex: 'SAFETY', 'OTHER'
                    response_text_raw = f"Désolé, ma réponse a été bloquée ({reason})."
                    # Si SAFETY, vérifier les ratings spécifiques si disponibles
                    if reason == 'SAFETY' and response.prompt_feedback.safety_ratings:
                        blocked_categories = [r.category.name for r in response.prompt_feedback.safety_ratings if r.probability.name not in ['NEGLIGIBLE', 'LOW']]
                        if blocked_categories:
                            response_text_raw += f" Catégories potentiellement concernées: {', '.join(blocked_categories)}."

                else:
                     response_text_raw = "Désolé, je n'ai pas pu générer de réponse pour cette demande."


             # Convertir en HTML même si c'est un message d'erreur
             response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])

        except ValueError as e:
            # Gérer le cas où .text échoue même avec des parts (rare, mais possible si structure inattendue)
            print(f"ERREUR: Impossible d'extraire le texte de la réponse Gemini. Erreur: {e}")
            print(f"Réponse brute: {response}")
            print(f"Feedback: {response.prompt_feedback}")
            response_text_raw = "Désolé, une erreur interne est survenue lors de la lecture de la réponse."
            response_html = markdown.markdown(response_text_raw)
        except Exception as e_resp: # Capturer toute autre exception inattendue
            print(f"ERREUR INATTENDUE lors du traitement de la réponse Gemini : {e_resp}")
            print(f"Réponse brute: {response}")
            response_text_raw = f"Désolé, une erreur inattendue est survenue ({type(e_resp).__name__})."
            response_html = markdown.markdown(response_text_raw)


        print(f"Réponse reçue de Gemini (brute, début): '{response_text_raw[:100]}...'")
        if response_html != response_text_raw: # Log seulement si Markdown a fait quelque chose
             print("Réponse convertie en HTML.")


        # Ajouter la réponse de l'assistant à l'historique de session
        assistant_history_entry = {
            'role': 'assistant',
            'text': response_html,      # HTML pour l'affichage via get_history
            'raw_text': response_text_raw # Texte brut pour les futurs appels Gemini
        }
        # Re-vérifier que l'historique est une liste avant d'ajouter
        if not isinstance(session.get('chat_history'), list):
            print("ERREUR: chat_history dans la session n'est pas une liste avant ajout assistant! Réinitialisation.")
            session['chat_history'] = [user_history_entry] # Garder au moins le user msg
        session['chat_history'].append(assistant_history_entry)
        # session.modified = True # Probablement pas nécessaire mais sans danger

        # Renvoyer la réponse HTML au frontend
        print("Envoi de la réponse HTML au client.")
        return jsonify({'success': True, 'message': response_html})

    except Exception as e:
        print(f"ERREUR Critique lors de l'appel à Gemini ou du traitement de la réponse : {e}")
        # En cas d'erreur, retirer le dernier message utilisateur de l'historique
        # pour éviter les boucles d'erreur si le message lui-même pose problème.
        # Vérifier si l'historique n'est pas vide avant de pop
        current_history = session.get('chat_history')
        if isinstance(current_history, list) and current_history:
            try:
                # S'assurer que le dernier élément était bien le message utilisateur
                if current_history[-1].get('role') == 'user':
                    current_history.pop()
                    # session.modified = True # Si on modifie directement la liste obtenue par .get()
                    print("Le dernier message utilisateur a été retiré de l'historique suite à l'erreur.")
                else:
                    print("Le dernier message n'était pas 'user', historique non modifié après erreur.")
            except Exception as pop_e:
                 print(f"Erreur lors de la tentative de retrait du message utilisateur de l'historique: {pop_e}")
        else:
            print("L'historique était vide ou invalide lors de l'erreur.")

        # Renvoyer une erreur générique mais informative
        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

    finally:
        # --- Nettoyage du fichier uploadé vers Google AI ---
        if uploaded_gemini_file:
             try:
                 print(f"Tentative de suppression du fichier Google AI : {uploaded_gemini_file.name}")
                 genai.delete_file(uploaded_gemini_file.name)
                 print(f"Fichier Google AI '{uploaded_gemini_file.name}' supprimé.")
             except Exception as e_del_gcp:
                 # Ne pas bloquer l'utilisateur pour ça, juste logguer
                 print(f"AVERTISSEMENT: Échec de la suppression du fichier Google AI '{uploaded_gemini_file.name}': {e_del_gcp}")


        # --- Nettoyage du fichier temporaire local ---
        if filepath_to_delete and os.path.exists(filepath_to_delete):
            try:
                os.remove(filepath_to_delete)
                print(f"Fichier temporaire local '{filepath_to_delete}' supprimé avec succès.")
            except OSError as e_del_local:
                print(f"ERREUR lors de la suppression du fichier temporaire local '{filepath_to_delete}': {e_del_local}")


@app.route('/clear', methods=['POST'])
def clear_chat():
    """Efface l'historique de chat dans la session."""
    session.clear() # Efface toutes les données de la session serveur actuelle
    print("API: Historique de chat effacé via /clear (session serveur).")

    # Adapter la réponse selon si c'est une requête AJAX (fetch) ou une soumission classique
    is_ajax = 'XMLHttpRequest' == request.headers.get('X-Requested-With') or \
              'application/json' in request.headers.get('Accept', '')

    if is_ajax:
         return jsonify({'success': True, 'message': 'Historique effacé.'})
    else:
        # Comportement pour une soumission de formulaire classique (si jamais utilisé)
        flash("Conversation effacée.", "info")
        return redirect(url_for('root')) # Redirige vers la racine


# --- Démarrage de l'application ---
if __name__ == '__main__':
    print("Démarrage du serveur Flask...")
    # Utiliser host='0.0.0.0' pour rendre accessible sur le réseau local
    # debug=True est pratique pour le développement, mais à désactiver en production !
    # Utiliser un port différent si le port 5000 est déjà pris
    port = int(os.environ.get('PORT', 5001))
    # Mettre debug=False pour la production
    app.run(debug=False, host='0.0.0.0', port=port)
# --- END OF FILE app.py ---