File size: 10,858 Bytes
c253059
da7ef42
dfc4deb
 
10ad7a5
 
60b2cb1
c253059
60b2cb1
dfc4deb
c23cd74
10ad7a5
c23cd74
da7ef42
10ad7a5
 
60b2cb1
10ad7a5
60b2cb1
10ad7a5
 
 
60b2cb1
10ad7a5
 
 
 
dfc4deb
10ad7a5
 
c23cd74
dfc4deb
c23cd74
10ad7a5
c23cd74
10ad7a5
c23cd74
10ad7a5
60b2cb1
c23cd74
60b2cb1
c23cd74
 
 
 
da7ef42
dfc4deb
10ad7a5
 
 
 
dfc4deb
 
da7ef42
10ad7a5
60b2cb1
10ad7a5
 
 
 
 
 
 
dfc4deb
10ad7a5
 
 
dfc4deb
10ad7a5
dfc4deb
60b2cb1
dfc4deb
 
 
 
 
 
 
 
 
10ad7a5
 
dfc4deb
10ad7a5
 
 
 
dfc4deb
 
 
 
 
 
10ad7a5
c23cd74
 
c96425b
10ad7a5
 
dfc4deb
10ad7a5
60b2cb1
dfc4deb
 
 
60b2cb1
dfc4deb
 
10ad7a5
 
 
dfc4deb
 
10ad7a5
c23cd74
dfc4deb
 
 
10ad7a5
dfc4deb
10ad7a5
 
dfc4deb
10ad7a5
dfc4deb
 
10ad7a5
 
dfc4deb
 
 
 
 
 
 
 
10ad7a5
 
 
 
 
 
 
dfc4deb
 
10ad7a5
dfc4deb
 
10ad7a5
dfc4deb
 
 
 
 
60b2cb1
10ad7a5
 
60b2cb1
dfc4deb
 
10ad7a5
dfc4deb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60b2cb1
dfc4deb
60b2cb1
dfc4deb
60b2cb1
dfc4deb
 
60b2cb1
dfc4deb
 
10ad7a5
dfc4deb
 
 
 
10ad7a5
dfc4deb
10ad7a5
dfc4deb
 
10ad7a5
c253059
dfc4deb
 
c253059
dfc4deb
 
 
10ad7a5
dfc4deb
10ad7a5
 
 
c253059
dfc4deb
60b2cb1
dfc4deb
10ad7a5
dfc4deb
60b2cb1
 
dfc4deb
c253059
dfc4deb
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
import os
import json
# Importer jsonify pour les réponses API
from flask import Flask, render_template, request, session, redirect, url_for, flash, jsonify
from dotenv import load_dotenv
import google.generativeai as genai
import requests
from werkzeug.utils import secure_filename
import mimetypes
import markdown # <-- Importer la bibliothèque Markdown

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"},
        # ... (autres catégories)
        {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
    ]
    model = genai.GenerativeModel(
        'gemini-1.5-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 = os.getenv("SERPER_API_KEY")
    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()
        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):
    # (Fonction inchangée - assurez-vous qu'elle renvoie du texte formaté)
    if not data: return "Aucun résultat de recherche trouvé."
    result = "Résultats de recherche web :\n"
    # ... (reste de la fonction inchangé) ...
    if 'organic' in data and data['organic']:
        result += "\n## Résultats principaux :\n"
        for i, item in enumerate(data['organic'][:3], 1):
            result += f"{i}. **{item.get('title', 'N/A')}**\n" # Format Markdown
            result += f"   {item.get('snippet', 'N/A')}\n"
            result += f"   [Lien]({item.get('link', '#')})\n\n" # Lien Markdown
    # ... (reste de la fonction inchangé) ...
    return result


def prepare_gemini_history(chat_history):
    gemini_history = []
    for message in chat_history:
        role = 'user' if message['role'] == 'user' else 'model'
        # Utiliser le 'raw_text' pour Gemini, pas le HTML rendu
        text_part = message.get('raw_text', message.get('text', ''))
        parts = [text_part]
        if message.get('gemini_file_ref'): # Utiliser une clé différente pour la référence interne
             parts.insert(0, message['gemini_file_ref'])
        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'] = []
    # L'état 'web_search' est maintenant géré côté client initialement,
    # mais on peut le pré-cocher depuis la session si on veut le persister.
    web_search_initial_state = session.get('web_search', False)

    # On ne passe que l'historique nécessaire à l'affichage initial
    display_history = session['chat_history']

    return render_template(
        'index.html',
        chat_history=display_history,
        web_search_active=web_search_initial_state
    )

@app.route('/api/chat', methods=['POST'])
def chat_api():
    """Gère les requêtes de chat AJAX et retourne du JSON."""
    if not model:
        return jsonify({'success': False, 'error': "Le modèle Gemini n'est pas configuré."}), 500

    prompt = request.form.get('prompt', '').strip()
    use_web_search = request.form.get('web_search') == 'true'
    file = request.files.get('file')
    uploaded_gemini_file = None # Référence à l'objet fichier uploadé à Gemini
    uploaded_filename = None # Juste le nom pour référence

    if not prompt and not file:
        return jsonify({'success': False, 'error': 'Message ou fichier requis.'}), 400

    # Mettre à jour l'état de la recherche web dans la session si on veut le persister
    session['web_search'] = use_web_search

    # --- Gestion de l'upload ---
    user_message_parts_for_gemini = []
    raw_user_text = prompt # Texte brut pour l'historique Gemini

    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)
                uploaded_filename = filename # Garder le nom
                print(f"Fichier sauvegardé : {filepath}")

                mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
                print(f"Upload vers Gemini (mime: {mime_type})...")
                gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type)
                uploaded_gemini_file = gemini_file_obj # Garder l'objet pour l'API
                user_message_parts_for_gemini.append(uploaded_gemini_file) # Ajouter l'objet fichier pour Gemini
                print(f"Fichier {filename} uploadé vers Gemini.")

                # Optionnel: Supprimer le fichier local
                # os.remove(filepath)

            except Exception as e:
                print(f"Erreur upload fichier : {e}")
                # Ne pas bloquer, mais renvoyer une erreur partielle si nécessaire
                return jsonify({'success': False, 'error': f"Erreur traitement fichier: {e}"}), 500
        else:
            return jsonify({'success': False, 'error': 'Type de fichier non autorisé.'}), 400

    # --- Préparation du message utilisateur et de l'historique ---
    # Ajouter le texte après le fichier pour Gemini
    user_message_parts_for_gemini.append(prompt)

    # Stocker le message utilisateur dans l'historique de session
    # Stocker le texte brut et la référence au fichier séparément
    user_history_entry = {
        'role': 'user',
        'text': f"[Fichier: {uploaded_filename}]\n\n{prompt}" if uploaded_filename else prompt, # Pour affichage
        'raw_text': raw_user_text, # Texte brut pour Gemini
    }
    if uploaded_gemini_file:
         # Ne stockez pas l'objet complet dans la session, juste une réf si nécessaire,
         # ou reconstruisez l'historique sans la référence de fichier si non critique.
         # Pour simplifier, on ne stocke pas la référence objet dans la session ici.
         # On pourrait stocker gemini_file_obj.name si on a besoin de le réutiliser plus tard.
         # user_history_entry['gemini_file_ref_name'] = uploaded_gemini_file.name
         pass # L'objet est dans user_message_parts_for_gemini pour l'appel API actuel

    if 'chat_history' not in session: session['chat_history'] = []
    session['chat_history'].append(user_history_entry)
    session.modified = True


    # --- Web Search ---
    final_prompt_text = prompt
    if use_web_search and prompt: # Recherche uniquement si texte ET activé
        print("Recherche web en cours pour:", prompt)
        web_results = perform_web_search(prompt)
        if web_results:
            formatted_results = format_search_results(web_results)
             # Mettre à jour le texte à envoyer à Gemini (le fichier est déjà dans les parts)
            final_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 résultats web.")
            # Remplacer le texte dans la liste des parts
            user_message_parts_for_gemini[-1] = final_prompt_text
        else:
            print("Pas de résultats web ou erreur.")

    # --- Appel à Gemini ---
    try:
        # Préparer l'historique SANS le dernier message utilisateur (il est dans `contents`)
        gemini_history = prepare_gemini_history(session['chat_history'][:-1])
        print(f"Historique Gemini: {len(gemini_history)} messages.")

        # Construire le contenu pour generate_content
        contents = gemini_history + [{'role': 'user', 'parts': user_message_parts_for_gemini}]

        print("Appel à model.generate_content...")
        response = model.generate_content(contents)

        # --- Traitement Réponse ---
        response_text_raw = response.text
        # Convertir Markdown en HTML pour un meilleur rendu
        response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables'])

        print(f"Réponse Gemini reçue (premiers 500 chars): {response_text_raw[:500]}")

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

        return jsonify({'success': True, 'message': response_html}) # Envoyer le HTML au client

    except Exception as e:
        print(f"Erreur lors de l'appel à Gemini : {e}")
        # Retirer le dernier message utilisateur de l'historique en cas d'échec
        session['chat_history'].pop()
        session.modified = True
        return jsonify({'success': False, 'error': f"Erreur communication IA: {e}"}), 500


@app.route('/clear', methods=['POST'])
def clear_chat():
    """Efface l'historique de la conversation."""
    session.pop('chat_history', None)
    session.pop('web_search', None) # Réinitialiser aussi le toggle web
    print("Historique de chat effacé.")
    flash("Conversation effacée.", "info") # Optionnel: message flash
    return redirect(url_for('index'))

# --- Démarrage ---
if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5001)