Docfile commited on
Commit
dfc4deb
·
verified ·
1 Parent(s): 25ac863

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -169
app.py CHANGED
@@ -1,11 +1,13 @@
1
  import os
2
  import json
3
- from flask import Flask, render_template, request, session, redirect, url_for, flash
 
4
  from dotenv import load_dotenv
5
  import google.generativeai as genai
6
  import requests
7
  from werkzeug.utils import secure_filename
8
  import mimetypes
 
9
 
10
  load_dotenv()
11
 
@@ -23,12 +25,11 @@ try:
23
  genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
24
  safety_settings = [
25
  {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
26
- {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
27
- {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
28
  {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
29
  ]
30
  model = genai.GenerativeModel(
31
- 'gemini-2.0-flash',
32
  safety_settings=safety_settings,
33
  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"
34
  )
@@ -43,13 +44,13 @@ def allowed_file(filename):
43
  filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
44
 
45
  def perform_web_search(query):
46
- conn_key = "9b90a274d9e704ff5b21c0367f9ae1161779b573"
47
  if not conn_key:
48
  print("Clé API SERPER manquante dans .env")
49
  return None
50
  search_url = "https://google.serper.dev/search"
51
- headers = { 'X-API-KEY': conn_key, 'Content-Type': 'application/json' }
52
- payload = json.dumps({"q": query, "gl": "fr", "hl": "fr"}) # Ajout localisation FR
53
  try:
54
  response = requests.post(search_url, headers=headers, data=payload, timeout=10)
55
  response.raise_for_status()
@@ -60,230 +61,187 @@ def perform_web_search(query):
60
  print(f"Erreur lors de la recherche web : {e}")
61
  return None
62
  except json.JSONDecodeError as e:
63
- print(f"Erreur lors du décodage JSON de Serper : {e}")
64
  print(f"Réponse reçue : {response.text}")
65
  return None
66
 
 
67
  def format_search_results(data):
 
68
  if not data: return "Aucun résultat de recherche trouvé."
69
- result = "Résultats de recherche web pertinents :\n"
70
- # (Formatage légèrement simplifié pour clarté)
71
- if kg := data.get('knowledgeGraph'):
72
- result += f"\n## {kg.get('title', '')} ({kg.get('type', '')})\n{kg.get('description', '')}\n"
73
- if ab := data.get('answerBox'):
74
- result += f"\n## Réponse rapide :\n{ab.get('title','')}\n{ab.get('snippet') or ab.get('answer','')}\n"
75
- if org := data.get('organic'):
76
- result += "\n## Principaux résultats :\n"
77
- for i, item in enumerate(org[:3], 1):
78
- result += f"{i}. {item.get('title', 'N/A')}\n {item.get('snippet', 'N/A')}\n [{item.get('link', '#')}]\n"
79
- # People Also Ask peut être bruyant, on peut l'omettre pour le prompt
80
  return result
81
 
 
82
  def prepare_gemini_history(chat_history):
83
  gemini_history = []
84
  for message in chat_history:
85
  role = 'user' if message['role'] == 'user' else 'model'
86
- # Utilise la référence stockée si elle existe
87
- parts = [message.get('gemini_file')] if message.get('gemini_file') else []
88
- parts.append(message['text_for_gemini']) # Utilise le texte destiné à Gemini
89
- # Filtrer les parts None qui pourraient survenir si gemini_file était None
90
- gemini_history.append({'role': role, 'parts': [p for p in parts if p]})
 
91
  return gemini_history
92
 
93
-
94
  # --- Routes Flask ---
95
 
96
  @app.route('/', methods=['GET'])
97
  def index():
 
98
  if 'chat_history' not in session:
99
  session['chat_history'] = []
100
- if 'web_search' not in session:
101
- session['web_search'] = False
 
102
 
103
- # Récupérer l'état de traitement et l'erreur pour les afficher
104
- processing = session.get('processing', False)
105
- error = session.pop('error', None) # Utilise pop pour ne l'afficher qu'une fois
106
 
107
  return render_template(
108
  'index.html',
109
- chat_history=session.get('chat_history', []),
110
- web_search_active=session.get('web_search', False),
111
- error=error,
112
- processing_message=processing # Passer l'état de traitement
113
  )
114
 
115
- @app.route('/chat', methods=['POST'])
116
- def chat():
 
117
  if not model:
118
- session['error'] = "Le modèle Gemini n'a pas pu être chargé."
119
- return redirect(url_for('index'))
120
 
121
  prompt = request.form.get('prompt', '').strip()
122
- session['web_search'] = 'web_search' in request.form
123
  file = request.files.get('file')
124
- uploaded_gemini_file = None
125
- file_display_name = None # Pour l'affichage
126
-
127
- # Marquer le début du traitement DANS la session
128
- session['processing'] = True
129
- session['processing_web_search'] = False # Reset au début
130
- session.modified = True # Sauvegarder la session maintenant
131
 
132
  if not prompt and not file:
133
- session['error'] = "Veuillez entrer un message ou uploader un fichier."
134
- session['processing'] = False # Annuler le traitement
135
- return redirect(url_for('index'))
 
 
 
 
 
136
 
137
- # --- Gestion Upload ---
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
- print(f"Fichier sauvegardé: {filepath}")
145
- mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
146
 
147
- print("Upload vers Gemini...")
 
148
  gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type)
149
- uploaded_gemini_file = gemini_file_obj
150
- file_display_name = filename # Garder le nom pour affichage
151
- print(f"Fichier {filename} uploadé. MimeType: {mime_type}")
152
- # Optionnel: Supprimer après upload
 
153
  # os.remove(filepath)
154
 
155
  except Exception as e:
156
  print(f"Erreur upload fichier : {e}")
157
- session['error'] = f"Erreur lors du traitement du fichier : {e}"
158
- # Ne pas arrêter le traitement, continuer sans le fichier si erreur upload
159
  else:
160
- session['error'] = "Type de fichier non autorisé."
161
- session['processing'] = False # Annuler le traitement
162
- return redirect(url_for('index'))
163
-
164
- # --- Préparation Message Utilisateur ---
165
- # Texte à afficher dans le chat
166
- display_text = prompt
167
- if file_display_name:
168
- display_text = f"[Fichier joint : {file_display_name}]\n\n{prompt}" if prompt else f"[Fichier joint : {file_display_name}]"
169
-
170
- # Texte à envoyer à Gemini (peut être différent si recherche web)
171
- text_for_gemini = prompt
172
-
173
- # Ajouter à l'historique de session (pour affichage) SEULEMENT s'il y a du contenu
174
- if display_text:
175
- # Garder une trace de l'objet fichier Gemini et du texte original pour l'API
176
- session['chat_history'].append({
177
- 'role': 'user',
178
- 'text': display_text, # Texte pour affichage HTML
179
- 'text_for_gemini': text_for_gemini, # Texte initial pour l'API Gemini
180
- 'gemini_file': uploaded_gemini_file # Référence à l'objet fichier si uploadé
181
- })
182
- session.modified = True
183
-
184
- # --- Logique principale (Recherche Web + Gemini) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  try:
186
- final_prompt_parts = []
187
- if uploaded_gemini_file:
188
- final_prompt_parts.append(uploaded_gemini_file)
189
-
190
- # Recherche Web si activée ET prompt textuel existe
191
- if session['web_search'] and prompt:
192
- print("Activation recherche web...")
193
- session['processing_web_search'] = True # Indiquer que la recherche est active
194
- session.modified = True
195
- # !! Important: Forcer la sauvegarde de session AVANT l'appel bloquant
196
- # Ceci est un workaround car Flask sauvegarde normalement en fin de requête.
197
- # Pour une vraie MAJ live, il faudrait AJAX/WebSockets.
198
- from flask.sessions import SessionInterface
199
- app.session_interface.save_session(app, session, Response()) # Pseudo-réponse
200
-
201
- web_results = perform_web_search(prompt)
202
- if web_results:
203
- formatted_results = format_search_results(web_results)
204
- 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."
205
- print("Prompt enrichi avec recherche web.")
206
- else:
207
- print("Pas de résultats web ou erreur.")
208
- text_for_gemini = prompt # Garde le prompt original
209
- session['processing_web_search'] = False # Recherche terminée
210
-
211
- # Ajouter le texte (original ou enrichi) aux parts
212
- if text_for_gemini: # S'assurer qu'on ajoute pas une string vide
213
- final_prompt_parts.append(text_for_gemini)
214
-
215
- # Préparer l'historique pour Gemini en utilisant les données stockées
216
- # On prend tout sauf le dernier message utilisateur qui est en cours de traitement
217
  gemini_history = prepare_gemini_history(session['chat_history'][:-1])
 
218
 
219
- print(f"\n--- Envoi à Gemini ({len(gemini_history)} hist + {len(final_prompt_parts)} new parts) ---")
 
220
 
221
- # Appel API Gemini
222
- if not final_prompt_parts:
223
- # Cas où seul un fichier a été envoyé sans prompt textuel,
224
- # et la recherche web n'était pas activée ou n'a rien retourné.
225
- # Il faut quand même envoyer qqchose, par ex., demander de décrire le fichier.
226
- if uploaded_gemini_file:
227
- final_prompt_parts.append("Décris le contenu de ce fichier.")
228
- else:
229
- # Ne devrait pas arriver vu les checks précédents, mais par sécurité
230
- raise ValueError("Tentative d'envoyer une requête vide à Gemini.")
231
 
 
 
 
 
232
 
233
- full_conversation = gemini_history + [{'role': 'user', 'parts': final_prompt_parts}]
234
- response = model.generate_content(full_conversation)
235
 
236
- # --- Traitement Réponse ---
237
- response_text = response.text
238
- print(f"--- Réponse Gemini reçue ---")
239
-
240
- # Ajouter la réponse à l'historique (version simple pour affichage)
241
- session['chat_history'].append({
242
- 'role': 'assistant',
243
- 'text': response_text,
244
- 'text_for_gemini': response_text # Pour symétrie, même si on ne réutilise pas directement
245
- # Pas de 'gemini_file' pour les réponses du modèle
246
- })
247
  session.modified = True
248
 
 
 
249
  except Exception as e:
250
- print(f"Erreur lors de l'appel à Gemini ou traitement : {e}")
251
- session['error'] = f"Une erreur s'est produite : {e}"
252
- # En cas d'erreur, retirer le dernier message utilisateur de l'historique
253
- # pour éviter boucle d'erreur si le prompt est problématique.
254
- if session['chat_history'] and session['chat_history'][-1]['role'] == 'user':
255
- session['chat_history'].pop()
256
- session.modified = True
257
- finally:
258
- # Marquer la fin du traitement DANS la session
259
- session['processing'] = False
260
- session.pop('processing_web_search', None) # Nettoyer au cas où
261
  session.modified = True
262
- # Pas besoin de Response() ici, la sauvegarde se fera avec le redirect
263
 
264
- return redirect(url_for('index'))
265
 
266
- # Ajouter une route pour effacer la conversation
267
  @app.route('/clear', methods=['POST'])
268
  def clear_chat():
 
269
  session.pop('chat_history', None)
270
- session.pop('web_search', None)
271
- session.pop('processing', None) # Nettoyer aussi l'état de process
272
- session.pop('error', None)
273
  print("Historique de chat effacé.")
274
- flash("Conversation effacée.", "info") # Message feedback (optionnel)
275
  return redirect(url_for('index'))
276
 
277
- # Classe Response factice pour sauvegarde session précoce (workaround)
278
- # Attention: N'est PAS une vraie réponse HTTP. A utiliser avec prudence.
279
- class Response:
280
- def __init__(self):
281
- self.headers = {}
282
- def set_cookie(self, key, value, **kwargs):
283
- # Potentiellement stocker les cookies si nécessaire, mais ici on ignore
284
- pass
285
-
286
-
287
  if __name__ == '__main__':
288
- # Utiliser un port différent si 5000 est pris
289
- app.run(debug=True, host='0.0.0.0', port=5002)
 
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
 
 
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
  )
 
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()
 
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)