File size: 14,032 Bytes
c253059
da7ef42
10ad7a5
 
 
 
c253059
10ad7a5
c23cd74
10ad7a5
c23cd74
da7ef42
10ad7a5
 
 
 
 
 
 
 
 
 
 
 
 
 
c23cd74
10ad7a5
 
 
 
 
 
da7ef42
c23cd74
10ad7a5
 
c23cd74
10ad7a5
 
 
c23cd74
10ad7a5
c23cd74
10ad7a5
 
c23cd74
10ad7a5
c23cd74
 
 
 
 
da7ef42
10ad7a5
 
 
 
 
 
da7ef42
10ad7a5
da7ef42
 
10ad7a5
da7ef42
10ad7a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da7ef42
10ad7a5
 
 
 
 
 
c23cd74
10ad7a5
 
 
 
c23cd74
c253059
10ad7a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c23cd74
 
c96425b
10ad7a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c23cd74
10ad7a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c253059
10ad7a5
 
c23cd74
 
10ad7a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c253059
 
10ad7a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c253059
10ad7a5
 
 
 
 
 
 
c253059
10ad7a5
 
 
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
import os
import json
from flask import Flask, render_template, request, session, redirect, url_for, flash
from dotenv import load_dotenv
import google.generativeai as genai
import requests # Pour Serper API
from werkzeug.utils import secure_filename
import mimetypes # Pour vérifier le type de fichier

load_dotenv()

app = Flask(__name__)
# Très important pour utiliser les sessions Flask !
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-clé-secrète-par-défaut-pour-dev')
# 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'] = 16 * 1024 * 1024  # Limite de taille (ex: 16MB)

# Créer le dossier temp s'il n'existe pas
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

# Configuration de l'API Gemini
try:
    genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))

    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"},
    ]

    model = genai.GenerativeModel(
        'gemini-1.5-flash', # Utiliser un modèle stable recommandé si 'gemini-2.0-flash-exp' cause problème
        # 'gemini-1.5-pro-latest', # Alternative plus puissante
        safety_settings=safety_settings,
        system_instruction="Tu es un assistant intelligent. ton but est d'assister au mieux que tu peux. tu as été créé par Aenir et tu t'appelles Mariam"
        # Note: 'tools' n'est pas directement utilisé ici comme dans Streamlit,
        # la logique de recherche web est gérée manuellement.
    )
    print("Modèle Gemini chargé.")
except Exception as e:
    print(f"Erreur lors de la configuration de Gemini : {e}")
    model = None # Empêche l'app de crasher si l'API key est mauvaise

# --- Fonctions Utilitaires ---

def allowed_file(filename):
    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."""
    conn_key = "9b90a274d9e704ff5b21c0367f9ae1161779b573"
    if not conn_key:
        print("Clé API SERPER manquante dans .env")
        return None
    search_url = "https://google.serper.dev/search"
    headers = {
        'X-API-KEY': conn_key,
        'Content-Type': 'application/json'
    }
    payload = json.dumps({"q": query})
    try:
        response = requests.post(search_url, headers=headers, data=payload, timeout=10)
        response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP
        data = response.json()
        print("Résultats de recherche obtenus.")
        return data
    except requests.exceptions.RequestException as e:
        print(f"Erreur lors de la recherche web : {e}")
        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 : {response.text}")
        return None

def format_search_results(data):
    """Met en forme les résultats de recherche pour le prompt Gemini."""
    if not data:
        return "Aucun résultat de recherche trouvé."

    result = "Résultats de recherche web :\n"

    if 'knowledgeGraph' in data:
        kg = data['knowledgeGraph']
        result += f"\n## Graphe de connaissances :\n"
        result += f"### {kg.get('title', '')} ({kg.get('type', '')})\n"
        result += f"{kg.get('description', '')}\n"
        if 'attributes' in kg:
            for attr, value in kg['attributes'].items():
                 result += f"- {attr}: {value}\n"

    if 'answerBox' in data:
         ab = data['answerBox']
         result += f"\n## Réponse directe :\n"
         result += f"{ab.get('title','')}\n{ab.get('snippet') or ab.get('answer','')}\n"


    if 'organic' in data and data['organic']:
        result += "\n## Résultats principaux :\n"
        for i, item in enumerate(data['organic'][:3], 1): # Limite aux 3 premiers
            result += f"{i}. **{item.get('title', 'N/A')}**\n"
            result += f"   {item.get('snippet', 'N/A')}\n"
            result += f"   Lien : {item.get('link', '#')}\n\n"

    if 'peopleAlsoAsk' in data and data['peopleAlsoAsk']:
        result += "## Questions fréquentes :\n"
        for i, item in enumerate(data['peopleAlsoAsk'][:2], 1): # Limite aux 2 premières
            result += f"{i}. **{item.get('question', 'N/A')}**\n"
            # result += f"   {item.get('snippet', 'N/A')}\n\n" # Le snippet est souvent redondant

    return result

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'
        parts = [message['text']]
        if message.get('gemini_file'): # Ajoute la référence au fichier si présente
             parts.insert(0, message['gemini_file']) # Met le fichier avant le texte
        gemini_history.append({'role': role, 'parts': parts})
    return gemini_history

# --- Routes Flask ---

@app.route('/', methods=['GET'])
def index():
    """Affiche la page principale du chat."""
    if 'chat_history' not in session:
        session['chat_history'] = [] # Initialise l'historique
    if 'web_search' not in session:
        session['web_search'] = False # Initialise l'état de la recherche web

    return render_template(
        'index.html',
        chat_history=session['chat_history'],
        web_search_active=session['web_search'],
        error=session.pop('error', None), # Récupère et supprime l'erreur s'il y en a une
        processing_message=session.pop('processing', False) # Indicateur de traitement
    )

@app.route('/chat', methods=['POST'])
def chat():
    """Gère la soumission du formulaire de chat."""
    if not model:
        session['error'] = "Le modèle Gemini n'a pas pu être chargé. Vérifiez la clé API et la configuration."
        return redirect(url_for('index'))

    prompt = request.form.get('prompt', '').strip()
    # Met à jour l'état de la recherche web depuis la checkbox
    session['web_search'] = 'web_search' in request.form
    file = request.files.get('file')
    uploaded_gemini_file = None
    user_message_content = {'role': 'user', 'text': prompt}

    if not prompt and not file:
        session['error'] = "Veuillez entrer un message ou uploader un fichier."
        return redirect(url_for('index'))

    # --- Gestion de l'upload de fichier ---
    if file and file.filename != '':
        if allowed_file(file.filename):
            try:
                # Sécurise le nom du fichier et détermine le chemin complet
                filename = secure_filename(file.filename)
                filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
                file.save(filepath)
                print(f"Fichier sauvegardé temporairement : {filepath}")

                # Essayer d'uploader vers Gemini
                print("Upload vers Gemini en cours...")
                # Détecter le mime type pour l'upload Gemini
                mime_type = mimetypes.guess_type(filepath)[0]
                if not mime_type:
                    # Fallback si la détection échoue
                     mime_type = 'application/octet-stream'
                     print(f"Impossible de deviner le mime_type pour {filename}, utilisation de {mime_type}")

                gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type)
                uploaded_gemini_file = gemini_file_obj # Garder l'objet fichier pour l'API
                user_message_content['gemini_file'] = uploaded_gemini_file # Stocke la référence pour l'historique Gemini
                user_message_content['text'] = f"[Fichier: {filename}]\n\n{prompt}" # Modifie le texte affiché
                print(f"Fichier {filename} uploadé avec succès vers Gemini. MimeType: {mime_type}")

                # Optionnel: Supprimer le fichier local après upload vers Gemini
                # try:
                #    os.remove(filepath)
                #    print(f"Fichier temporaire {filepath} supprimé.")
                # except OSError as e:
                #    print(f"Erreur lors de la suppression du fichier temporaire {filepath}: {e}")


            except Exception as e:
                print(f"Erreur lors du traitement ou de l'upload du fichier : {e}")
                session['error'] = f"Erreur lors du traitement du fichier : {e}"
                # Ne pas bloquer si l'upload échoue, on peut continuer sans le fichier
                # return redirect(url_for('index')) # Décommentez si l'upload doit être bloquant
        else:
            session['error'] = "Type de fichier non autorisé."
            return redirect(url_for('index'))
    elif file and file.filename == '':
         # Si un champ fichier existe mais est vide, on l'ignore simplement
         pass

    # --- Ajout du message utilisateur à l'historique de session ---
    if prompt or uploaded_gemini_file: # N'ajoute que s'il y a du contenu
        # Version simplifiée pour l'affichage HTML (sans l'objet gemini_file)
        display_history_message = {'role': 'user', 'text': user_message_content['text']}
        session['chat_history'].append(display_history_message)
        session.modified = True # Important car on modifie une liste dans la session

    # --- Préparation et Envoi à Gemini ---
    try:
        # Indiquer qu'un traitement est en cours
        session['processing'] = True
        session.modified = True

        # Construire le prompt final (avec recherche web si activée)
        final_prompt_parts = []
        if uploaded_gemini_file:
            final_prompt_parts.append(uploaded_gemini_file)

        current_prompt_text = prompt

        # Effectuer la recherche web si activée ET si un prompt textuel existe
        if session['web_search'] and prompt:
             print("Recherche web activée pour le prompt:", prompt)
             # Afficher l'indicateur de recherche
             # Note: dans Flask simple, l'indicateur ne sera visible qu'APRES le rechargement.
             # Pour un affichage dynamique, il faudrait JS/AJAX.
             # On peut passer une variable au template pour l'afficher avant la réponse.
             session['processing_web_search'] = True # Indicateur spécifique
             session.modified = True

             web_results = perform_web_search(prompt)
             if web_results:
                 formatted_results = format_search_results(web_results)
                 current_prompt_text = f"Question originale: {prompt}\n\n{formatted_results}\n\nBasé sur ces informations et ta connaissance générale, réponds à la question originale."
                 print("Prompt modifié avec les résultats de recherche.")
             else:
                 print("Aucun résultat de recherche web obtenu ou erreur.")
                 # On continue avec le prompt original

        final_prompt_parts.append(current_prompt_text)

        # Préparer l'historique complet pour Gemini
        gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le dernier message utilisateur (qui est dans final_prompt_parts)

        print(f"\n--- Envoi à Gemini ---")
        print(f"Historique envoyé: {len(gemini_history)} messages")
        print(f"Parties du prompt actuel: {len(final_prompt_parts)}")
        # print(f"Prompt textuel final: {current_prompt_text[:200]}...") # Afficher début du prompt
        # print("----------------------\n")

        # Appel à Gemini API
        # Utiliser generate_content qui prend l'historique + le nouveau message
        full_conversation = gemini_history + [{'role': 'user', 'parts': final_prompt_parts}]
        response = model.generate_content(full_conversation)

        # --- Traitement de la réponse ---
        response_text = response.text
        print(f"\n--- Réponse de Gemini ---")
        print(response_text[:500] + ('...' if len(response_text) > 500 else ''))
        # print("-------------------------\n")

        # Ajout de la réponse à l'historique de session
        session['chat_history'].append({'role': 'assistant', 'text': response_text})
        session.modified = True

    except Exception as e:
        print(f"Erreur lors de l'appel à Gemini : {e}")
        session['error'] = f"Une erreur s'est produite lors de la communication avec l'IA : {e}"
        # Si une erreur survient, on retire le message utilisateur qui a causé l'erreur
        # pour éviter de le renvoyer indéfiniment.
        if session['chat_history'] and session['chat_history'][-1]['role'] == 'user':
             session['chat_history'].pop()
             session.modified = True

    finally:
        # Indiquer que le traitement est terminé
        session['processing'] = False
        session.pop('processing_web_search', None) # Nettoyer l'indicateur de recherche
        session.modified = True


    return redirect(url_for('index')) # Redirige vers la page principale pour afficher le nouvel état

@app.route('/clear', methods=['POST'])
def clear_chat():
    """Efface l'historique de la conversation."""
    session.pop('chat_history', None) # Supprime l'historique de la session
    session.pop('web_search', None)   # Réinitialise aussi le toggle web
    print("Historique de chat effacé.")
    return redirect(url_for('index')) # Redirige vers la page d'accueil

# --- Démarrage de l'application ---
if __name__ == '__main__':
    # Utiliser host='0.0.0.0' pour rendre l'app accessible sur le réseau local
    # Debug=True est utile pour le développement, mais à désactiver en production
    app.run(debug=True, host='0.0.0.0', port=5001)