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) |