Docfile commited on
Commit
ccfefd3
·
verified ·
1 Parent(s): f87680f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +285 -146
app.py CHANGED
@@ -1,247 +1,386 @@
1
  import os
2
  import json
3
- # Importer jsonify pour les réponses API
4
- from flask import Flask, render_template, request, session, redirect, url_for, flash, jsonify
5
  from dotenv import load_dotenv
6
  import google.generativeai as genai
7
  import requests
8
  from werkzeug.utils import secure_filename
9
- import mimetypes
10
- import markdown # <-- Importer la bibliothèque Markdown
11
 
 
12
  load_dotenv()
13
 
14
  app = Flask(__name__)
15
- app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-clé-secrète-par-défaut-pour-dev')
 
 
 
 
16
  UPLOAD_FOLDER = 'temp'
17
- ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'}
18
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
19
- app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
20
 
 
21
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
 
22
 
23
- # --- Configuration Gemini (inchangée) ---
 
24
  try:
25
- genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
26
- safety_settings = [
27
- {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
28
- # ... (autres catégories)
29
- {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
30
- ]
31
- model = genai.GenerativeModel(
32
- 'gemini-1.5-flash',
33
- safety_settings=safety_settings,
34
- 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"
35
- )
36
- print("Modèle Gemini chargé.")
 
 
 
 
 
 
 
 
 
37
  except Exception as e:
38
- print(f"Erreur lors de la configuration de Gemini : {e}")
39
- model = None
 
 
40
 
41
- # --- Fonctions Utilitaires (inchangées) ---
42
  def allowed_file(filename):
 
43
  return '.' in filename and \
44
  filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
45
 
46
  def perform_web_search(query):
47
- conn_key = os.getenv("SERPER_API_KEY")
48
- if not conn_key:
49
- print("Clé API SERPER manquante dans .env")
 
50
  return None
 
51
  search_url = "https://google.serper.dev/search"
52
- headers = {'X-API-KEY': conn_key, 'Content-Type': 'application/json'}
53
- payload = json.dumps({"q": query})
 
 
 
 
54
  try:
 
55
  response = requests.post(search_url, headers=headers, data=payload, timeout=10)
56
- response.raise_for_status()
57
  data = response.json()
58
- print("Résultats de recherche obtenus.")
 
59
  return data
 
 
 
60
  except requests.exceptions.RequestException as e:
61
  print(f"Erreur lors de la recherche web : {e}")
 
 
 
 
 
 
62
  return None
63
  except json.JSONDecodeError as e:
64
  print(f"Erreur lors du décodage de la réponse JSON de Serper : {e}")
65
- print(f"Réponse reçue : {response.text}")
66
  return None
67
 
68
-
69
  def format_search_results(data):
70
- # (Fonction inchangée - assurez-vous qu'elle renvoie du texte formaté)
71
- if not data: return "Aucun résultat de recherche trouvé."
72
- result = "Résultats de recherche web :\n"
73
- # ... (reste de la fonction inchangé) ...
74
- if 'organic' in data and data['organic']:
75
- result += "\n## Résultats principaux :\n"
76
- for i, item in enumerate(data['organic'][:3], 1):
77
- result += f"{i}. **{item.get('title', 'N/A')}**\n" # Format Markdown
78
- result += f" {item.get('snippet', 'N/A')}\n"
79
- result += f" [Lien]({item.get('link', '#')})\n\n" # Lien Markdown
80
- # ... (reste de la fonction inchangé) ...
81
- return result
82
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  def prepare_gemini_history(chat_history):
 
85
  gemini_history = []
86
  for message in chat_history:
87
  role = 'user' if message['role'] == 'user' else 'model'
88
- # Utiliser le 'raw_text' pour Gemini, pas le HTML rendu
89
- text_part = message.get('raw_text', message.get('text', ''))
90
  parts = [text_part]
91
- if message.get('gemini_file_ref'): # Utiliser une clé différente pour la référence interne
92
- parts.insert(0, message['gemini_file_ref'])
 
 
93
  gemini_history.append({'role': role, 'parts': parts})
94
  return gemini_history
95
 
96
  # --- Routes Flask ---
97
 
98
- @app.route('/', methods=['GET'])
99
- def index():
100
- """Affiche la page principale du chat."""
 
 
 
 
 
 
 
101
  if 'chat_history' not in session:
102
  session['chat_history'] = []
103
- # L'état 'web_search' est maintenant géré côté client initialement,
104
- # mais on peut le pré-cocher depuis la session si on veut le persister.
105
- web_search_initial_state = session.get('web_search', False)
106
-
107
- # On ne passe que l'historique nécessaire à l'affichage initial
108
- display_history = session['chat_history']
109
 
110
- return render_template(
111
- 'index.html',
112
- chat_history=display_history,
113
- web_search_active=web_search_initial_state
114
- )
 
 
115
 
116
  @app.route('/api/chat', methods=['POST'])
117
  def chat_api():
118
- """Gère les requêtes de chat AJAX et retourne du JSON."""
119
  if not model:
120
- return jsonify({'success': False, 'error': "Le modèle Gemini n'est pas configuré."}), 500
 
121
 
 
122
  prompt = request.form.get('prompt', '').strip()
123
- use_web_search = request.form.get('web_search') == 'true'
 
124
  file = request.files.get('file')
125
- uploaded_gemini_file = None # Référence à l'objet fichier uploadé à Gemini
126
- uploaded_filename = None # Juste le nom pour référence
127
 
 
128
  if not prompt and not file:
129
- return jsonify({'success': False, 'error': 'Message ou fichier requis.'}), 400
 
 
 
 
 
130
 
131
- # Mettre à jour l'état de la recherche web dans la session si on veut le persister
132
- session['web_search'] = use_web_search
 
133
 
134
- # --- Gestion de l'upload ---
135
- user_message_parts_for_gemini = []
136
- raw_user_text = prompt # Texte brut pour l'historique Gemini
137
 
 
138
  if file and file.filename != '':
139
  if allowed_file(file.filename):
140
  try:
141
  filename = secure_filename(file.filename)
142
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
143
  file.save(filepath)
144
- uploaded_filename = filename # Garder le nom
145
- print(f"Fichier sauvegardé : {filepath}")
 
146
 
147
- mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
148
- print(f"Upload vers Gemini (mime: {mime_type})...")
149
- gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type)
150
- uploaded_gemini_file = gemini_file_obj # Garder l'objet pour l'API
151
- user_message_parts_for_gemini.append(uploaded_gemini_file) # Ajouter l'objet fichier pour Gemini
152
- print(f"Fichier {filename} uploadé vers Gemini.")
153
 
154
- # Optionnel: Supprimer le fichier local
155
- # os.remove(filepath)
 
 
156
 
157
  except Exception as e:
158
- print(f"Erreur upload fichier : {e}")
159
- # Ne pas bloquer, mais renvoyer une erreur partielle si nécessaire
160
- return jsonify({'success': False, 'error': f"Erreur traitement fichier: {e}"}), 500
 
 
 
 
 
 
 
161
  else:
162
- return jsonify({'success': False, 'error': 'Type de fichier non autorisé.'}), 400
 
163
 
164
- # --- Préparation du message utilisateur et de l'historique ---
165
- # Ajouter le texte après le fichier pour Gemini
166
- user_message_parts_for_gemini.append(prompt)
 
 
167
 
168
- # Stocker le message utilisateur dans l'historique de session
169
- # Stocker le texte brut et la référence au fichier séparément
170
  user_history_entry = {
171
  'role': 'user',
172
- 'text': f"[Fichier: {uploaded_filename}]\n\n{prompt}" if uploaded_filename else prompt, # Pour affichage
173
- 'raw_text': raw_user_text, # Texte brut pour Gemini
 
174
  }
175
- if uploaded_gemini_file:
176
- # Ne stockez pas l'objet complet dans la session, juste une réf si nécessaire,
177
- # ou reconstruisez l'historique sans la référence de fichier si non critique.
178
- # Pour simplifier, on ne stocke pas la référence objet dans la session ici.
179
- # On pourrait stocker gemini_file_obj.name si on a besoin de le réutiliser plus tard.
180
- # user_history_entry['gemini_file_ref_name'] = uploaded_gemini_file.name
181
- pass # L'objet est dans user_message_parts_for_gemini pour l'appel API actuel
182
-
183
- if 'chat_history' not in session: session['chat_history'] = []
184
  session['chat_history'].append(user_history_entry)
185
- session.modified = True
186
-
187
-
188
- # --- Web Search ---
189
- final_prompt_text = prompt
190
- if use_web_search and prompt: # Recherche uniquement si texte ET activé
191
- print("Recherche web en cours pour:", prompt)
192
- web_results = perform_web_search(prompt)
193
- if web_results:
194
- formatted_results = format_search_results(web_results)
195
- # Mettre à jour le texte à envoyer à Gemini (le fichier est déjà dans les parts)
196
- 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."
197
- print("Prompt modifié avec résultats web.")
198
- # Remplacer le texte dans la liste des parts
199
- user_message_parts_for_gemini[-1] = final_prompt_text
 
 
 
 
 
 
 
 
 
 
 
200
  else:
201
- print("Pas de résultats web ou erreur.")
 
 
 
 
202
 
203
- # --- Appel à Gemini ---
204
  try:
205
- # Préparer l'historique SANS le dernier message utilisateur (il est dans `contents`)
206
- gemini_history = prepare_gemini_history(session['chat_history'][:-1])
207
- print(f"Historique Gemini: {len(gemini_history)} messages.")
208
 
209
- # Construire le contenu pour generate_content
210
- contents = gemini_history + [{'role': 'user', 'parts': user_message_parts_for_gemini}]
211
 
212
- print("Appel à model.generate_content...")
213
- response = model.generate_content(contents)
 
 
 
214
 
215
- # --- Traitement Réponse ---
216
  response_text_raw = response.text
217
- # Convertir Markdown en HTML pour un meilleur rendu
218
- response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables'])
219
-
220
- print(f"Réponse Gemini reçue (premiers 500 chars): {response_text_raw[:500]}")
221
-
222
- # Ajouter la réponse à l'historique de session
223
- session['chat_history'].append({'role': 'assistant', 'text': response_html, 'raw_text': response_text_raw})
 
 
 
 
 
 
224
  session.modified = True
225
 
226
- return jsonify({'success': True, 'message': response_html}) # Envoyer le HTML au client
 
 
227
 
228
  except Exception as e:
229
- print(f"Erreur lors de l'appel à Gemini : {e}")
230
- # Retirer le dernier message utilisateur de l'historique en cas d'échec
 
231
  session['chat_history'].pop()
232
  session.modified = True
233
- return jsonify({'success': False, 'error': f"Erreur communication IA: {e}"}), 500
 
 
 
 
 
 
 
 
 
 
 
234
 
235
 
236
  @app.route('/clear', methods=['POST'])
237
  def clear_chat():
238
- """Efface l'historique de la conversation."""
239
  session.pop('chat_history', None)
240
- session.pop('web_search', None) # Réinitialiser aussi le toggle web
241
- print("Historique de chat effacé.")
242
- flash("Conversation effacée.", "info") # Optionnel: message flash
243
- return redirect(url_for('index'))
 
 
 
 
 
 
 
 
 
 
 
244
 
245
- # --- Démarrage ---
246
  if __name__ == '__main__':
 
 
 
 
247
  app.run(debug=True, host='0.0.0.0', port=5001)
 
1
  import os
2
  import json
3
+ import mimetypes
4
+ from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template # Ajout de render_template au cas où
5
  from dotenv import load_dotenv
6
  import google.generativeai as genai
7
  import requests
8
  from werkzeug.utils import secure_filename
9
+ import markdown # Pour convertir la réponse en HTML
 
10
 
11
+ # --- Configuration Initiale ---
12
  load_dotenv()
13
 
14
  app = Flask(__name__)
15
+
16
+ # Clé secrète FORTEMENT recommandée pour les sessions
17
+ app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'dev-secret-key-replace-in-prod')
18
+
19
+ # Configuration pour les uploads
20
  UPLOAD_FOLDER = 'temp'
21
+ ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'} # Extensions autorisées
22
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
23
+ app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024 # Limite de taille (ex: 25MB)
24
 
25
+ # Créer le dossier temp s'il n'existe pas
26
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
27
+ print(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
28
 
29
+ # --- Configuration de l'API Gemini ---
30
+ model = None # Initialiser à None
31
  try:
32
+ gemini_api_key = os.getenv("GOOGLE_API_KEY")
33
+ if not gemini_api_key:
34
+ print("ERREUR: Clé API GOOGLE_API_KEY manquante dans le fichier .env")
35
+ else:
36
+ genai.configure(api_key=gemini_api_key)
37
+
38
+ safety_settings = [
39
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
40
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
41
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
42
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
43
+ ]
44
+
45
+ # Utiliser un modèle stable comme gemini-1.5-flash
46
+ model = genai.GenerativeModel(
47
+ 'gemini-1.5-flash-latest',
48
+ safety_settings=safety_settings,
49
+ 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."
50
+ )
51
+ print("Modèle Gemini (gemini-1.5-flash-latest) chargé avec succès.")
52
+
53
  except Exception as e:
54
+ print(f"ERREUR Critique lors de la configuration de Gemini : {e}")
55
+ print("L'application fonctionnera sans les fonctionnalités IA.")
56
+
57
+ # --- Fonctions Utilitaires ---
58
 
 
59
  def allowed_file(filename):
60
+ """Vérifie si l'extension du fichier est autorisée."""
61
  return '.' in filename and \
62
  filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
63
 
64
  def perform_web_search(query):
65
+ """Effectue une recherche web via l'API Serper."""
66
+ serper_api_key = os.getenv("SERPER_API_KEY")
67
+ if not serper_api_key:
68
+ print("AVERTISSEMENT: Clé API SERPER_API_KEY manquante. Recherche web désactivée.")
69
  return None
70
+
71
  search_url = "https://google.serper.dev/search"
72
+ headers = {
73
+ 'X-API-KEY': serper_api_key,
74
+ 'Content-Type': 'application/json'
75
+ }
76
+ payload = json.dumps({"q": query, "gl": "fr", "hl": "fr"}) # Ajout localisation FR
77
+
78
  try:
79
+ print(f"Recherche Serper pour: '{query}'")
80
  response = requests.post(search_url, headers=headers, data=payload, timeout=10)
81
+ response.raise_for_status() # Lève une exception pour les erreurs HTTP (4xx, 5xx)
82
  data = response.json()
83
+ print("Résultats de recherche Serper obtenus.")
84
+ # print(json.dumps(data, indent=2)) # Décommenter pour voir les résultats bruts
85
  return data
86
+ except requests.exceptions.Timeout:
87
+ print("Erreur lors de la recherche web : Timeout")
88
+ return None
89
  except requests.exceptions.RequestException as e:
90
  print(f"Erreur lors de la recherche web : {e}")
91
+ # Essayer de lire le corps de la réponse d'erreur si possible
92
+ try:
93
+ error_details = e.response.json()
94
+ print(f"Détails de l'erreur Serper: {error_details}")
95
+ except:
96
+ pass # Ignorer si le corps n'est pas JSON ou n'existe pas
97
  return None
98
  except json.JSONDecodeError as e:
99
  print(f"Erreur lors du décodage de la réponse JSON de Serper : {e}")
100
+ print(f"Réponse reçue (texte brut) : {response.text}")
101
  return None
102
 
 
103
  def format_search_results(data):
104
+ """Met en forme les résultats de recherche (format Markdown)."""
105
+ if not data:
106
+ return "Aucun résultat de recherche web trouvé pertinent."
107
+
108
+ results = []
109
+
110
+ # Réponse directe (Answer Box)
111
+ if data.get('answerBox'):
112
+ ab = data['answerBox']
113
+ title = ab.get('title', '')
114
+ snippet = ab.get('snippet') or ab.get('answer', '')
115
+ if snippet:
116
+ results.append(f"**Réponse rapide : {title}**\n{snippet}\n")
117
+
118
+ # Knowledge Graph
119
+ if data.get('knowledgeGraph'):
120
+ kg = data['knowledgeGraph']
121
+ title = kg.get('title', '')
122
+ type = kg.get('type', '')
123
+ description = kg.get('description', '')
124
+ if title and description:
125
+ results.append(f"**{title} ({type})**\n{description}\n")
126
+ if kg.get('attributes'):
127
+ for attr, value in kg['attributes'].items():
128
+ results.append(f"- {attr}: {value}")
129
+
130
+
131
+ # Résultats organiques
132
+ if data.get('organic'):
133
+ results.append("**Pages web pertinentes :**")
134
+ for i, item in enumerate(data['organic'][:3], 1): # Top 3
135
+ title = item.get('title', 'Sans titre')
136
+ link = item.get('link', '#')
137
+ snippet = item.get('snippet', 'Pas de description.')
138
+ results.append(f"{i}. **[{title}]({link})**\n {snippet}\n")
139
+
140
+ # People Also Ask
141
+ if data.get('peopleAlsoAsk'):
142
+ results.append("**Questions liées :**")
143
+ for i, item in enumerate(data['peopleAlsoAsk'][:2], 1): # Top 2
144
+ results.append(f"- {item.get('question', '')}")
145
+
146
+
147
+ if not results:
148
+ return "Aucun résultat structuré trouvé dans la recherche web."
149
+
150
+ return "\n".join(results)
151
 
152
  def prepare_gemini_history(chat_history):
153
+ """Convertit l'historique stocké en session au format attendu par Gemini API."""
154
  gemini_history = []
155
  for message in chat_history:
156
  role = 'user' if message['role'] == 'user' else 'model'
157
+ # Utiliser le 'raw_text' stocké pour Gemini
158
+ text_part = message.get('raw_text', '') # Fallback au cas où
159
  parts = [text_part]
160
+ # NOTE: La gestion des fichiers des tours PRÉCÉDENTS n'est pas gérée ici.
161
+ # L'API generate_content se concentre généralement sur le fichier du tour ACTUEL.
162
+ # Si une référence de fichier passée était nécessaire, il faudrait la stocker
163
+ # et la ré-attacher ici (potentiellement plus complexe).
164
  gemini_history.append({'role': role, 'parts': parts})
165
  return gemini_history
166
 
167
  # --- Routes Flask ---
168
 
169
+ @app.route('/')
170
+ def root():
171
+ """Route racine simple, confirme que le serveur est actif."""
172
+ # On pourrait aussi servir index.html ici si on le met dans 'templates'
173
+ # return render_template('index.html')
174
+ return "Serveur Mariam AI (Flask) est en cours d'exécution. Ouvrez index.html."
175
+
176
+ @app.route('/api/history', methods=['GET'])
177
+ def get_history():
178
+ """Fournit l'historique de chat stocké en session au format JSON."""
179
  if 'chat_history' not in session:
180
  session['chat_history'] = []
 
 
 
 
 
 
181
 
182
+ # Préparer l'historique pour l'affichage (contient déjà le HTML pour l'assistant)
183
+ display_history = [
184
+ {'role': msg.get('role', 'unknown'), 'text': msg.get('text', '')}
185
+ for msg in session.get('chat_history', [])
186
+ ]
187
+ print(f"API: Récupération de l'historique ({len(display_history)} messages)")
188
+ return jsonify({'success': True, 'history': display_history})
189
 
190
  @app.route('/api/chat', methods=['POST'])
191
  def chat_api():
192
+ """Gère les nouvelles requêtes de chat via AJAX."""
193
  if not model:
194
+ print("API ERREUR: Tentative d'appel à /api/chat sans modèle Gemini chargé.")
195
+ return jsonify({'success': False, 'error': "Le service IA n'est pas disponible."}), 503 # Service Unavailable
196
 
197
+ # Récupération des données du formulaire
198
  prompt = request.form.get('prompt', '').strip()
199
+ use_web_search_str = request.form.get('web_search', 'false') # 'true' ou 'false'
200
+ use_web_search = use_web_search_str.lower() == 'true'
201
  file = request.files.get('file')
 
 
202
 
203
+ # Validation simple
204
  if not prompt and not file:
205
+ return jsonify({'success': False, 'error': 'Veuillez fournir un message ou un fichier.'}), 400
206
+
207
+ print(f"\n--- Nouvelle requête /api/chat ---")
208
+ print(f"Prompt reçu: '{prompt[:50]}...'")
209
+ print(f"Recherche Web activée: {use_web_search}")
210
+ print(f"Fichier reçu: {file.filename if file else 'Aucun'}")
211
 
212
+ # Initialiser l'historique de session si nécessaire
213
+ if 'chat_history' not in session:
214
+ session['chat_history'] = []
215
 
216
+ uploaded_gemini_file = None # L'objet fichier retourné par genai.upload_file
217
+ uploaded_filename = None # Juste le nom du fichier pour référence/affichage
218
+ filepath_to_delete = None # Chemin du fichier local à supprimer après traitement
219
 
220
+ # --- Gestion de l'upload de fichier ---
221
  if file and file.filename != '':
222
  if allowed_file(file.filename):
223
  try:
224
  filename = secure_filename(file.filename)
225
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
226
  file.save(filepath)
227
+ filepath_to_delete = filepath # Marquer pour suppression
228
+ uploaded_filename = filename
229
+ print(f"Fichier '{filename}' sauvegardé temporairement dans '{filepath}'")
230
 
231
+ # Détecter le MimeType pour Gemini
232
+ mime_type = mimetypes.guess_type(filepath)[0]
233
+ if not mime_type:
234
+ mime_type = 'application/octet-stream' # Fallback
235
+ print(f"AVERTISSEMENT: Impossible de deviner le MimeType pour '{filename}', utilisation de '{mime_type}'.")
 
236
 
237
+ # Uploader vers Google AI (peut prendre du temps)
238
+ print(f"Upload du fichier vers Google AI (MimeType: {mime_type})...")
239
+ uploaded_gemini_file = genai.upload_file(path=filepath, mime_type=mime_type)
240
+ print(f"Fichier '{uploaded_gemini_file.name}' uploadé avec succès vers Google AI.")
241
 
242
  except Exception as e:
243
+ print(f"ERREUR Critique lors du traitement/upload du fichier '{filename}': {e}")
244
+ # Supprimer le fichier local même en cas d'erreur d'upload Gemini
245
+ if filepath_to_delete and os.path.exists(filepath_to_delete):
246
+ try:
247
+ os.remove(filepath_to_delete)
248
+ print(f"Fichier temporaire '{filepath_to_delete}' supprimé après erreur.")
249
+ except OSError as del_e:
250
+ print(f"Erreur lors de la suppression du fichier temporaire après erreur: {del_e}")
251
+ # Renvoyer une erreur claire au client
252
+ return jsonify({'success': False, 'error': f"Erreur lors du traitement du fichier: {e}"}), 500
253
  else:
254
+ print(f"ERREUR: Type de fichier non autorisé: {file.filename}")
255
+ return jsonify({'success': False, 'error': f"Type de fichier non autorisé. Extensions permises: {', '.join(ALLOWED_EXTENSIONS)}"}), 400
256
 
257
+ # --- Préparation du message utilisateur pour l'historique et Gemini ---
258
+ # Texte brut pour Gemini (et pour l'historique interne)
259
+ raw_user_text = prompt
260
+ # Texte pour l'affichage dans l'interface (peut inclure le nom de fichier)
261
+ display_user_text = f"[{uploaded_filename}] {prompt}" if uploaded_filename and prompt else (prompt or f"[{uploaded_filename}]")
262
 
263
+ # Ajout à l'historique de session
 
264
  user_history_entry = {
265
  'role': 'user',
266
+ 'text': display_user_text, # Pour get_history et potentiellement debug
267
+ 'raw_text': raw_user_text, # Pour l'envoi à Gemini via prepare_gemini_history
268
+ # On ne stocke PAS l'objet 'uploaded_gemini_file' dans la session
269
  }
 
 
 
 
 
 
 
 
 
270
  session['chat_history'].append(user_history_entry)
271
+ session.modified = True # Indiquer que la session a été modifiée
272
+
273
+ # --- Préparation des 'parts' pour l'appel Gemini ACTUEL ---
274
+ current_gemini_parts = []
275
+ if uploaded_gemini_file:
276
+ current_gemini_parts.append(uploaded_gemini_file) # L'objet fichier uploadé
277
+
278
+ final_prompt_for_gemini = raw_user_text # Commencer avec le texte brut
279
+
280
+ # --- Recherche Web (si activée et si un prompt textuel existe) ---
281
+ if use_web_search and raw_user_text:
282
+ print("Activation de la recherche web...")
283
+ search_data = perform_web_search(raw_user_text)
284
+ if search_data:
285
+ formatted_results = format_search_results(search_data)
286
+ # Construire un prompt enrichi pour Gemini
287
+ final_prompt_for_gemini = f"""Voici la question originale de l'utilisateur:
288
+ "{raw_user_text}"
289
+
290
+ J'ai effectué une recherche web et voici les informations pertinentes trouvées:
291
+ --- DEBUT RESULTATS WEB ---
292
+ {formatted_results}
293
+ --- FIN RESULTATS WEB ---
294
+
295
+ 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."""
296
+ print("Prompt enrichi avec les résultats de recherche web.")
297
  else:
298
+ print("Aucun résultat de recherche web pertinent trouvé ou erreur, utilisation du prompt original.")
299
+ # final_prompt_for_gemini reste raw_user_text
300
+
301
+ # Ajouter le texte (potentiellement enrichi) aux parts pour Gemini
302
+ current_gemini_parts.append(final_prompt_for_gemini)
303
 
304
+ # --- Appel à l'API Gemini ---
305
  try:
306
+ # Préparer l'historique des messages PRÉCÉDENTS
307
+ gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le message actuel
308
+ print(f"Préparation de l'appel Gemini avec {len(gemini_history)} messages d'historique.")
309
 
310
+ # Construire le contenu complet pour l'appel
311
+ contents_for_gemini = gemini_history + [{'role': 'user', 'parts': current_gemini_parts}]
312
 
313
+ # Appel API
314
+ print("Envoi de la requête à Gemini...")
315
+ # Utilisation de generate_content en mode non-streamé
316
+ response = model.generate_content(contents_for_gemini)
317
+ # print(response) # Décommenter pour voir la réponse brute de l'API
318
 
319
+ # Extraire le texte de la réponse
320
  response_text_raw = response.text
321
+ print(f"Réponse reçue de Gemini (brute, début): '{response_text_raw[:100]}...'")
322
+
323
+ # Convertir la réponse Markdown en HTML pour l'affichage
324
+ response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
325
+ print("Réponse convertie en HTML.")
326
+
327
+ # Ajouter la réponse de l'assistant à l'historique de session
328
+ assistant_history_entry = {
329
+ 'role': 'assistant',
330
+ 'text': response_html, # HTML pour l'affichage via get_history
331
+ 'raw_text': response_text_raw # Texte brut pour les futurs appels Gemini
332
+ }
333
+ session['chat_history'].append(assistant_history_entry)
334
  session.modified = True
335
 
336
+ # Renvoyer la réponse HTML au frontend
337
+ print("Envoi de la réponse HTML au client.")
338
+ return jsonify({'success': True, 'message': response_html})
339
 
340
  except Exception as e:
341
+ print(f"ERREUR Critique lors de l'appel à Gemini ou du traitement de la réponse : {e}")
342
+ # En cas d'erreur, retirer le dernier message utilisateur de l'historique
343
+ # pour éviter les boucles d'erreur si le message lui-même pose problème.
344
  session['chat_history'].pop()
345
  session.modified = True
346
+ print("Le dernier message utilisateur a été retiré de l'historique suite à l'erreur.")
347
+ # Renvoyer une erreur générique mais informative
348
+ 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
349
+
350
+ finally:
351
+ # --- Nettoyage du fichier temporaire ---
352
+ if filepath_to_delete and os.path.exists(filepath_to_delete):
353
+ try:
354
+ os.remove(filepath_to_delete)
355
+ print(f"Fichier temporaire '{filepath_to_delete}' supprimé avec succès.")
356
+ except OSError as e:
357
+ print(f"ERREUR lors de la suppression du fichier temporaire '{filepath_to_delete}': {e}")
358
 
359
 
360
  @app.route('/clear', methods=['POST'])
361
  def clear_chat():
362
+ """Efface l'historique de chat dans la session."""
363
  session.pop('chat_history', None)
364
+ session.pop('web_search', None) # Réinitialiser aussi le toggle web si besoin
365
+ print("API: Historique de chat effacé via /clear.")
366
+
367
+ # Adapter la réponse selon si c'est une requête AJAX (fetch) ou une soumission classique
368
+ # Vérification si la requête vient probablement de fetch (simple)
369
+ is_ajax = 'XMLHttpRequest' == request.headers.get('X-Requested-With') or \
370
+ request.headers.get('Accept') == 'application/json'
371
+
372
+ if is_ajax:
373
+ return jsonify({'success': True, 'message': 'Historique effacé.'})
374
+ else:
375
+ # Comportement pour une soumission de formulaire classique (si jamais utilisé)
376
+ flash("Conversation effacée.", "info")
377
+ return redirect(url_for('root')) # Redirige vers la racine
378
+
379
 
380
+ # --- Démarrage de l'application ---
381
  if __name__ == '__main__':
382
+ print("Démarrage du serveur Flask...")
383
+ # Utiliser host='0.0.0.0' pour rendre accessible sur le réseau local
384
+ # debug=True est pratique pour le développement, mais à désactiver en production !
385
+ # Changer le port si nécessaire (ex: 5000, 5001, 8080)
386
  app.run(debug=True, host='0.0.0.0', port=5001)