File size: 18,014 Bytes
c253059
da7ef42
ccfefd3
0f75c3f
10ad7a5
b212844
60b2cb1
c253059
b212844
c23cd74
ccfefd3
b212844
c23cd74
da7ef42
0ab0c95
b212844
 
ccfefd3
 
10ad7a5
b212844
10ad7a5
b212844
0ab0c95
b212844
10ad7a5
ccfefd3
10ad7a5
ccfefd3
b212844
 
 
 
 
 
 
 
9c39fae
0f75c3f
10ad7a5
ccfefd3
 
b212844
ccfefd3
b212844
 
 
 
 
 
 
 
 
 
 
0ab0c95
c23cd74
0f75c3f
ccfefd3
71ffbd5
ccfefd3
c23cd74
 
b212844
 
 
0ab0c95
da7ef42
b212844
ccfefd3
 
 
10ad7a5
b212844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10ad7a5
 
b212844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccfefd3
b212844
 
 
 
 
 
 
 
 
 
 
 
984f26b
b212844
 
 
 
 
0ab0c95
10ad7a5
b212844
10ad7a5
 
 
b212844
 
 
 
 
 
 
 
10ad7a5
c23cd74
 
c96425b
ccfefd3
 
0f75c3f
b212844
9c39fae
be38ed4
ccfefd3
 
be38ed4
 
 
c23cd74
dfc4deb
 
b212844
10ad7a5
be38ed4
b212844
a08ef55
be38ed4
b212844
10ad7a5
b212844
 
ccfefd3
be38ed4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b212844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984f26b
b212844
 
984f26b
b212844
 
 
 
 
 
ccfefd3
60b2cb1
b212844
 
 
9c39fae
b212844
 
 
 
 
 
0f75c3f
 
b212844
 
 
 
 
 
 
 
0f75c3f
 
b212844
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ccfefd3
10ad7a5
c253059
b212844
 
ccfefd3
dfc4deb
984f26b
b212844
 
 
 
 
a08ef55
 
b212844
 
 
 
 
 
a08ef55
ccfefd3
b212844
ccfefd3
 
 
b212844
ccfefd3
b212844
 
10ad7a5
 
c253059
ccfefd3
a08ef55
b212844
ccfefd3
b212844
 
 
 
 
 
ccfefd3
 
 
b212844
ccfefd3
b212844
ccfefd3
0ab0c95
b212844
c253059
ccfefd3
b212844
 
 
 
 
0f75c3f
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
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

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

app = Flask(__name__)

# Clé secrète FORTEMENT recommandée pour les sessions
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key-replace-in-prod')

# Configuration pour les uploads
UPLOAD_FOLDER = 'temp'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'} # Extensions autorisées
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
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")

# --- Configuration de l'API Gemini ---
MODEL_FLASH = 'gemini-2.0-flash' # Default model
MODEL_PRO = 'gemini-2.5-pro-exp-03-25' # Advanced model
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'):
             for attr, value in kg['attributes'].items():
                  results.append(f"- {attr}: {value}")


    # 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 = []
    for message in chat_history:
        role = 'user' if message['role'] == 'user' else 'model'
        # Utiliser le 'raw_text' stocké pour Gemini
        text_part = message.get('raw_text', '') # Fallback au cas où
        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.
        # Si une référence de fichier passée était nécessaire, il faudrait la stocker
        # et la ré-attacher ici (potentiellement plus complexe).
        gemini_history.append({'role': role, 'parts': parts})
    return gemini_history

# --- Routes Flask ---

@app.route('/')
def root():
    """Sert la page HTML principale."""
    return render_template('index.html')


@app.route('/api/history', methods=['GET'])
def get_history():
    # Retourne toujours un historique vide
    return jsonify({'success': True, 'history': []})


@app.route('/api/chat', methods=['POST'])
def chat_api():
    # Récupération des données du formulaire
    prompt = request.form.get('prompt', '').strip()
    use_web_search_str = request.form.get('web_search', 'false')
    use_web_search = use_web_search_str.lower() == 'true'
    file = request.files.get('file')
    use_advanced_str = request.form.get('advanced_reasoning', 'false')
    use_advanced = use_advanced_str.lower() == 'true'

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

    # Gestion éventuelle de l'upload de fichier et recherche web...
    # Préparation du prompt pour Gemini, etc.
    raw_user_text = prompt
    final_prompt_for_gemini = raw_user_text
    if use_web_search and raw_user_text:
        search_data = perform_web_search(raw_user_text)
        if search_data:
            formatted_results = format_search_results(search_data)
            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."""
    
    # Préparation des parts pour l'appel Gemini
    current_gemini_parts = [final_prompt_for_gemini]
    # (Gestion de fichier si applicable...)

    try:
        # Appel à Gemini avec current_gemini_parts et sans historique précédent
        active_model = genai.GenerativeModel(
            model_name=MODEL_PRO if use_advanced else MODEL_FLASH,
            safety_settings=SAFETY_SETTINGS,
            system_instruction=SYSTEM_INSTRUCTION
        )
        response = active_model.generate_content(current_gemini_parts)
        response_text_raw = response.text
        response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
        return jsonify({'success': True, 'message': response_html})
    except Exception as e:
        return jsonify({'success': False, 'error': f"Erreur interne: {e}"}), 500


    # --- 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
    }
    session['chat_history'].append(user_history_entry)
    session.modified = True # Indiquer que la session a été modifiée

    # --- Préparation des 'parts' pour l'appel Gemini ACTUEL ---
    current_gemini_parts = []
    if uploaded_gemini_file:
        current_gemini_parts.append(uploaded_gemini_file) # L'objet fichier uploadé

    final_prompt_for_gemini = raw_user_text # Commencer avec le texte brut

    # --- Recherche Web (si activée et si un prompt textuel existe) ---
    if use_web_search and raw_user_text:
        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.")
            # final_prompt_for_gemini reste raw_user_text

    # Ajouter le texte (potentiellement enrichi) aux parts pour Gemini
    current_gemini_parts.append(final_prompt_for_gemini)

    # --- Appel à l'API Gemini ---
    try:
        # Préparer l'historique des messages PRÉCÉDENTS
        gemini_history = prepare_gemini_history(session['chat_history'][:-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}]

        # 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
        try:
             response_text_raw = response.text
        except ValueError:
            # Si response.text échoue, la réponse a probablement été bloquée.
            print("ERREUR: La réponse de Gemini a été bloquée (probablement par les safety settings).")
            print(f"Détails du blocage : {response.prompt_feedback}")
            # Vous pouvez décider quoi renvoyer au client ici.
            # Soit une erreur spécifique, soit un message générique.
            response_text_raw = "Désolé, ma réponse a été bloquée car elle pourrait enfreindre les règles de sécurité."
            # Convertir ce message d'erreur en HTML aussi pour la cohérence
            response_html = markdown.markdown(response_text_raw)

        else:
            # Si response.text réussit, continuer normalement
            print(f"Réponse reçue de Gemini (brute, début): '{response_text_raw[:100]}...'")
            # Convertir la réponse Markdown en HTML pour l'affichage
            response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
            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
        }
        session['chat_history'].append(assistant_history_entry)
        session.modified = True

        # 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
        if session.get('chat_history'):
            session['chat_history'].pop()
            session.modified = True
            print("Le dernier message utilisateur a été retiré de l'historique suite à l'erreur.")
        else:
            print("L'historique était déjà vide 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 temporaire ---
        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é avec succès.")
            except OSError as e:
                print(f"ERREUR lors de la suppression du fichier temporaire '{filepath_to_delete}': {e}")


@app.route('/clear', methods=['POST'])
def clear_chat():
    """Efface l'historique de chat dans la session."""
    session.pop('chat_history', None)
    # session.pop('web_search', None) # On ne stocke pas ça en session
    print("API: Historique de chat effacé via /clear.")

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

    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 !
    # Changer le port si nécessaire (ex: 5000, 5001, 8080)
    # Utiliser un port différent si le port 5000 est déjà pris
    port = int(os.environ.get('PORT', 5001))
    app.run(debug=True, host='0.0.0.0', port=port)