File size: 12,878 Bytes
c253059
da7ef42
10ad7a5
 
 
60b2cb1
c253059
60b2cb1
c23cd74
10ad7a5
c23cd74
da7ef42
10ad7a5
 
60b2cb1
10ad7a5
60b2cb1
10ad7a5
 
 
60b2cb1
10ad7a5
 
 
 
 
 
 
 
c23cd74
60b2cb1
c23cd74
10ad7a5
c23cd74
10ad7a5
c23cd74
10ad7a5
60b2cb1
c23cd74
60b2cb1
c23cd74
 
 
 
da7ef42
10ad7a5
 
 
 
 
60b2cb1
 
da7ef42
10ad7a5
60b2cb1
10ad7a5
 
 
 
 
 
 
60b2cb1
10ad7a5
 
 
 
60b2cb1
 
 
 
 
 
 
 
 
 
 
 
10ad7a5
 
 
 
 
 
60b2cb1
 
 
 
 
10ad7a5
c23cd74
60b2cb1
c23cd74
c96425b
10ad7a5
 
 
60b2cb1
10ad7a5
60b2cb1
 
 
 
 
10ad7a5
 
 
60b2cb1
 
 
 
10ad7a5
c23cd74
10ad7a5
 
 
60b2cb1
10ad7a5
 
 
 
 
 
60b2cb1
 
 
 
 
 
10ad7a5
 
 
60b2cb1
10ad7a5
 
60b2cb1
10ad7a5
 
 
 
 
 
60b2cb1
 
10ad7a5
60b2cb1
10ad7a5
60b2cb1
 
 
 
 
10ad7a5
 
60b2cb1
10ad7a5
60b2cb1
10ad7a5
 
60b2cb1
10ad7a5
c23cd74
60b2cb1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10ad7a5
 
 
 
60b2cb1
10ad7a5
60b2cb1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10ad7a5
60b2cb1
 
10ad7a5
 
 
 
 
60b2cb1
10ad7a5
60b2cb1
 
 
 
 
 
 
 
 
10ad7a5
c253059
 
60b2cb1
 
 
 
10ad7a5
60b2cb1
 
10ad7a5
60b2cb1
10ad7a5
60b2cb1
10ad7a5
60b2cb1
10ad7a5
60b2cb1
10ad7a5
60b2cb1
10ad7a5
c253059
60b2cb1
 
 
 
10ad7a5
60b2cb1
 
 
 
 
 
 
 
 
 
 
 
10ad7a5
c253059
60b2cb1
 
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
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
from werkzeug.utils import secure_filename
import mimetypes

load_dotenv()

app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-clé-secrète-par-défaut-pour-dev')
UPLOAD_FOLDER = 'temp'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024

os.makedirs(UPLOAD_FOLDER, exist_ok=True)

# --- Configuration Gemini (inchangée) ---
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-2.0-flash',
        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"
    )
    print("Modèle Gemini chargé.")
except Exception as e:
    print(f"Erreur lors de la configuration de Gemini : {e}")
    model = None

# --- Fonctions Utilitaires (inchangées) ---
def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

def perform_web_search(query):
    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, "gl": "fr", "hl": "fr"}) # Ajout localisation FR
    try:
        response = requests.post(search_url, headers=headers, data=payload, timeout=10)
        response.raise_for_status()
        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 JSON de Serper : {e}")
        print(f"Réponse reçue : {response.text}")
        return None

def format_search_results(data):
    if not data: return "Aucun résultat de recherche trouvé."
    result = "Résultats de recherche web pertinents :\n"
    # (Formatage légèrement simplifié pour clarté)
    if kg := data.get('knowledgeGraph'):
        result += f"\n## {kg.get('title', '')} ({kg.get('type', '')})\n{kg.get('description', '')}\n"
    if ab := data.get('answerBox'):
        result += f"\n## Réponse rapide :\n{ab.get('title','')}\n{ab.get('snippet') or ab.get('answer','')}\n"
    if org := data.get('organic'):
        result += "\n## Principaux résultats :\n"
        for i, item in enumerate(org[:3], 1):
            result += f"{i}. {item.get('title', 'N/A')}\n   {item.get('snippet', 'N/A')}\n   [{item.get('link', '#')}]\n"
    # People Also Ask peut être bruyant, on peut l'omettre pour le prompt
    return result

def prepare_gemini_history(chat_history):
    gemini_history = []
    for message in chat_history:
        role = 'user' if message['role'] == 'user' else 'model'
        # Utilise la référence stockée si elle existe
        parts = [message.get('gemini_file')] if message.get('gemini_file') else []
        parts.append(message['text_for_gemini']) # Utilise le texte destiné à Gemini
        # Filtrer les parts None qui pourraient survenir si gemini_file était None
        gemini_history.append({'role': role, 'parts': [p for p in parts if p]})
    return gemini_history


# --- Routes Flask ---

@app.route('/', methods=['GET'])
def index():
    if 'chat_history' not in session:
        session['chat_history'] = []
    if 'web_search' not in session:
        session['web_search'] = False

    # Récupérer l'état de traitement et l'erreur pour les afficher
    processing = session.get('processing', False)
    error = session.pop('error', None) # Utilise pop pour ne l'afficher qu'une fois

    return render_template(
        'index.html',
        chat_history=session.get('chat_history', []),
        web_search_active=session.get('web_search', False),
        error=error,
        processing_message=processing # Passer l'état de traitement
    )

@app.route('/chat', methods=['POST'])
def chat():
    if not model:
        session['error'] = "Le modèle Gemini n'a pas pu être chargé."
        return redirect(url_for('index'))

    prompt = request.form.get('prompt', '').strip()
    session['web_search'] = 'web_search' in request.form
    file = request.files.get('file')
    uploaded_gemini_file = None
    file_display_name = None # Pour l'affichage

    # Marquer le début du traitement DANS la session
    session['processing'] = True
    session['processing_web_search'] = False # Reset au début
    session.modified = True # Sauvegarder la session maintenant

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

    # --- Gestion Upload ---
    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)
                print(f"Fichier sauvegardé: {filepath}")
                mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'

                print("Upload vers Gemini...")
                gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type)
                uploaded_gemini_file = gemini_file_obj
                file_display_name = filename # Garder le nom pour affichage
                print(f"Fichier {filename} uploadé. MimeType: {mime_type}")
                # Optionnel: Supprimer après upload
                # os.remove(filepath)

            except Exception as e:
                print(f"Erreur upload fichier : {e}")
                session['error'] = f"Erreur lors du traitement du fichier : {e}"
                # Ne pas arrêter le traitement, continuer sans le fichier si erreur upload
        else:
            session['error'] = "Type de fichier non autorisé."
            session['processing'] = False # Annuler le traitement
            return redirect(url_for('index'))

    # --- Préparation Message Utilisateur ---
    # Texte à afficher dans le chat
    display_text = prompt
    if file_display_name:
        display_text = f"[Fichier joint : {file_display_name}]\n\n{prompt}" if prompt else f"[Fichier joint : {file_display_name}]"

    # Texte à envoyer à Gemini (peut être différent si recherche web)
    text_for_gemini = prompt

    # Ajouter à l'historique de session (pour affichage) SEULEMENT s'il y a du contenu
    if display_text:
         # Garder une trace de l'objet fichier Gemini et du texte original pour l'API
         session['chat_history'].append({
             'role': 'user',
             'text': display_text, # Texte pour affichage HTML
             'text_for_gemini': text_for_gemini, # Texte initial pour l'API Gemini
             'gemini_file': uploaded_gemini_file # Référence à l'objet fichier si uploadé
         })
         session.modified = True

    # --- Logique principale (Recherche Web + Gemini) ---
    try:
        final_prompt_parts = []
        if uploaded_gemini_file:
            final_prompt_parts.append(uploaded_gemini_file)

        # Recherche Web si activée ET prompt textuel existe
        if session['web_search'] and prompt:
            print("Activation recherche web...")
            session['processing_web_search'] = True # Indiquer que la recherche est active
            session.modified = True
            # !! Important: Forcer la sauvegarde de session AVANT l'appel bloquant
            # Ceci est un workaround car Flask sauvegarde normalement en fin de requête.
            # Pour une vraie MAJ live, il faudrait AJAX/WebSockets.
            from flask.sessions import SessionInterface
            app.session_interface.save_session(app, session, Response()) # Pseudo-réponse

            web_results = perform_web_search(prompt)
            if web_results:
                formatted_results = format_search_results(web_results)
                text_for_gemini = f"Question originale: {prompt}\n\n{formatted_results}\n\nRéponds à la question originale en te basant sur ces informations et ta connaissance."
                print("Prompt enrichi avec recherche web.")
            else:
                print("Pas de résultats web ou erreur.")
                text_for_gemini = prompt # Garde le prompt original
            session['processing_web_search'] = False # Recherche terminée

        # Ajouter le texte (original ou enrichi) aux parts
        if text_for_gemini: # S'assurer qu'on ajoute pas une string vide
            final_prompt_parts.append(text_for_gemini)

        # Préparer l'historique pour Gemini en utilisant les données stockées
        # On prend tout sauf le dernier message utilisateur qui est en cours de traitement
        gemini_history = prepare_gemini_history(session['chat_history'][:-1])

        print(f"\n--- Envoi à Gemini ({len(gemini_history)} hist + {len(final_prompt_parts)} new parts) ---")

        # Appel API Gemini
        if not final_prompt_parts:
             # Cas où seul un fichier a été envoyé sans prompt textuel,
             # et la recherche web n'était pas activée ou n'a rien retourné.
             # Il faut quand même envoyer qqchose, par ex., demander de décrire le fichier.
             if uploaded_gemini_file:
                  final_prompt_parts.append("Décris le contenu de ce fichier.")
             else:
                  # Ne devrait pas arriver vu les checks précédents, mais par sécurité
                  raise ValueError("Tentative d'envoyer une requête vide à Gemini.")


        full_conversation = gemini_history + [{'role': 'user', 'parts': final_prompt_parts}]
        response = model.generate_content(full_conversation)

        # --- Traitement Réponse ---
        response_text = response.text
        print(f"--- Réponse Gemini reçue ---")

        # Ajouter la réponse à l'historique (version simple pour affichage)
        session['chat_history'].append({
            'role': 'assistant',
            'text': response_text,
            'text_for_gemini': response_text # Pour symétrie, même si on ne réutilise pas directement
            # Pas de 'gemini_file' pour les réponses du modèle
        })
        session.modified = True

    except Exception as e:
        print(f"Erreur lors de l'appel à Gemini ou traitement : {e}")
        session['error'] = f"Une erreur s'est produite : {e}"
        # En cas d'erreur, retirer le dernier message utilisateur de l'historique
        # pour éviter boucle d'erreur si le prompt est problématique.
        if session['chat_history'] and session['chat_history'][-1]['role'] == 'user':
            session['chat_history'].pop()
            session.modified = True
    finally:
        # Marquer la fin du traitement DANS la session
        session['processing'] = False
        session.pop('processing_web_search', None) # Nettoyer au cas où
        session.modified = True
        # Pas besoin de Response() ici, la sauvegarde se fera avec le redirect

    return redirect(url_for('index'))

# Ajouter une route pour effacer la conversation
@app.route('/clear', methods=['POST'])
def clear_chat():
    session.pop('chat_history', None)
    session.pop('web_search', None)
    session.pop('processing', None) # Nettoyer aussi l'état de process
    session.pop('error', None)
    print("Historique de chat effacé.")
    flash("Conversation effacée.", "info") # Message feedback (optionnel)
    return redirect(url_for('index'))

# Classe Response factice pour sauvegarde session précoce (workaround)
# Attention: N'est PAS une vraie réponse HTTP. A utiliser avec prudence.
class Response:
    def __init__(self):
        self.headers = {}
    def set_cookie(self, key, value, **kwargs):
        # Potentiellement stocker les cookies si nécessaire, mais ici on ignore
        pass


if __name__ == '__main__':
    # Utiliser un port différent si 5000 est pris
    app.run(debug=True, host='0.0.0.0', port=5002)