File size: 14,032 Bytes
c253059 da7ef42 10ad7a5 c253059 10ad7a5 c23cd74 10ad7a5 c23cd74 da7ef42 10ad7a5 c23cd74 10ad7a5 da7ef42 c23cd74 10ad7a5 c23cd74 10ad7a5 c23cd74 10ad7a5 c23cd74 10ad7a5 c23cd74 10ad7a5 c23cd74 da7ef42 10ad7a5 da7ef42 10ad7a5 da7ef42 10ad7a5 da7ef42 10ad7a5 da7ef42 10ad7a5 c23cd74 10ad7a5 c23cd74 c253059 10ad7a5 c23cd74 c96425b 10ad7a5 c23cd74 10ad7a5 c253059 10ad7a5 c23cd74 10ad7a5 c253059 10ad7a5 c253059 10ad7a5 c253059 10ad7a5 |
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 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
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 # Pour Serper API
from werkzeug.utils import secure_filename
import mimetypes # Pour vérifier le type de fichier
load_dotenv()
app = Flask(__name__)
# Très important pour utiliser les sessions Flask !
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-clé-secrète-par-défaut-pour-dev')
# Configuration pour les uploads
UPLOAD_FOLDER = 'temp'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'} # Extensions autorisées
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # Limite de taille (ex: 16MB)
# Créer le dossier temp s'il n'existe pas
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# Configuration de l'API Gemini
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-1.5-flash', # Utiliser un modèle stable recommandé si 'gemini-2.0-flash-exp' cause problème
# 'gemini-1.5-pro-latest', # Alternative plus puissante
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"
# Note: 'tools' n'est pas directement utilisé ici comme dans Streamlit,
# la logique de recherche web est gérée manuellement.
)
print("Modèle Gemini chargé.")
except Exception as e:
print(f"Erreur lors de la configuration de Gemini : {e}")
model = None # Empêche l'app de crasher si l'API key est mauvaise
# --- Fonctions Utilitaires ---
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def perform_web_search(query):
"""Effectue une recherche web via l'API Serper."""
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})
try:
response = requests.post(search_url, headers=headers, data=payload, timeout=10)
response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP
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):
"""Met en forme les résultats de recherche pour le prompt Gemini."""
if not data:
return "Aucun résultat de recherche trouvé."
result = "Résultats de recherche web :\n"
if 'knowledgeGraph' in data:
kg = data['knowledgeGraph']
result += f"\n## Graphe de connaissances :\n"
result += f"### {kg.get('title', '')} ({kg.get('type', '')})\n"
result += f"{kg.get('description', '')}\n"
if 'attributes' in kg:
for attr, value in kg['attributes'].items():
result += f"- {attr}: {value}\n"
if 'answerBox' in data:
ab = data['answerBox']
result += f"\n## Réponse directe :\n"
result += f"{ab.get('title','')}\n{ab.get('snippet') or ab.get('answer','')}\n"
if 'organic' in data and data['organic']:
result += "\n## Résultats principaux :\n"
for i, item in enumerate(data['organic'][:3], 1): # Limite aux 3 premiers
result += f"{i}. **{item.get('title', 'N/A')}**\n"
result += f" {item.get('snippet', 'N/A')}\n"
result += f" Lien : {item.get('link', '#')}\n\n"
if 'peopleAlsoAsk' in data and data['peopleAlsoAsk']:
result += "## Questions fréquentes :\n"
for i, item in enumerate(data['peopleAlsoAsk'][:2], 1): # Limite aux 2 premières
result += f"{i}. **{item.get('question', 'N/A')}**\n"
# result += f" {item.get('snippet', 'N/A')}\n\n" # Le snippet est souvent redondant
return result
def prepare_gemini_history(chat_history):
"""Convertit l'historique stocké en session au format attendu par Gemini API."""
gemini_history = []
for message in chat_history:
role = 'user' if message['role'] == 'user' else 'model'
parts = [message['text']]
if message.get('gemini_file'): # Ajoute la référence au fichier si présente
parts.insert(0, message['gemini_file']) # Met le fichier avant le texte
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'] = [] # Initialise l'historique
if 'web_search' not in session:
session['web_search'] = False # Initialise l'état de la recherche web
return render_template(
'index.html',
chat_history=session['chat_history'],
web_search_active=session['web_search'],
error=session.pop('error', None), # Récupère et supprime l'erreur s'il y en a une
processing_message=session.pop('processing', False) # Indicateur de traitement
)
@app.route('/chat', methods=['POST'])
def chat():
"""Gère la soumission du formulaire de chat."""
if not model:
session['error'] = "Le modèle Gemini n'a pas pu être chargé. Vérifiez la clé API et la configuration."
return redirect(url_for('index'))
prompt = request.form.get('prompt', '').strip()
# Met à jour l'état de la recherche web depuis la checkbox
session['web_search'] = 'web_search' in request.form
file = request.files.get('file')
uploaded_gemini_file = None
user_message_content = {'role': 'user', 'text': prompt}
if not prompt and not file:
session['error'] = "Veuillez entrer un message ou uploader un fichier."
return redirect(url_for('index'))
# --- Gestion de l'upload de fichier ---
if file and file.filename != '':
if allowed_file(file.filename):
try:
# Sécurise le nom du fichier et détermine le chemin complet
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
print(f"Fichier sauvegardé temporairement : {filepath}")
# Essayer d'uploader vers Gemini
print("Upload vers Gemini en cours...")
# Détecter le mime type pour l'upload Gemini
mime_type = mimetypes.guess_type(filepath)[0]
if not mime_type:
# Fallback si la détection échoue
mime_type = 'application/octet-stream'
print(f"Impossible de deviner le mime_type pour {filename}, utilisation de {mime_type}")
gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type)
uploaded_gemini_file = gemini_file_obj # Garder l'objet fichier pour l'API
user_message_content['gemini_file'] = uploaded_gemini_file # Stocke la référence pour l'historique Gemini
user_message_content['text'] = f"[Fichier: {filename}]\n\n{prompt}" # Modifie le texte affiché
print(f"Fichier {filename} uploadé avec succès vers Gemini. MimeType: {mime_type}")
# Optionnel: Supprimer le fichier local après upload vers Gemini
# try:
# os.remove(filepath)
# print(f"Fichier temporaire {filepath} supprimé.")
# except OSError as e:
# print(f"Erreur lors de la suppression du fichier temporaire {filepath}: {e}")
except Exception as e:
print(f"Erreur lors du traitement ou de l'upload du fichier : {e}")
session['error'] = f"Erreur lors du traitement du fichier : {e}"
# Ne pas bloquer si l'upload échoue, on peut continuer sans le fichier
# return redirect(url_for('index')) # Décommentez si l'upload doit être bloquant
else:
session['error'] = "Type de fichier non autorisé."
return redirect(url_for('index'))
elif file and file.filename == '':
# Si un champ fichier existe mais est vide, on l'ignore simplement
pass
# --- Ajout du message utilisateur à l'historique de session ---
if prompt or uploaded_gemini_file: # N'ajoute que s'il y a du contenu
# Version simplifiée pour l'affichage HTML (sans l'objet gemini_file)
display_history_message = {'role': 'user', 'text': user_message_content['text']}
session['chat_history'].append(display_history_message)
session.modified = True # Important car on modifie une liste dans la session
# --- Préparation et Envoi à Gemini ---
try:
# Indiquer qu'un traitement est en cours
session['processing'] = True
session.modified = True
# Construire le prompt final (avec recherche web si activée)
final_prompt_parts = []
if uploaded_gemini_file:
final_prompt_parts.append(uploaded_gemini_file)
current_prompt_text = prompt
# Effectuer la recherche web si activée ET si un prompt textuel existe
if session['web_search'] and prompt:
print("Recherche web activée pour le prompt:", prompt)
# Afficher l'indicateur de recherche
# Note: dans Flask simple, l'indicateur ne sera visible qu'APRES le rechargement.
# Pour un affichage dynamique, il faudrait JS/AJAX.
# On peut passer une variable au template pour l'afficher avant la réponse.
session['processing_web_search'] = True # Indicateur spécifique
session.modified = True
web_results = perform_web_search(prompt)
if web_results:
formatted_results = format_search_results(web_results)
current_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 les résultats de recherche.")
else:
print("Aucun résultat de recherche web obtenu ou erreur.")
# On continue avec le prompt original
final_prompt_parts.append(current_prompt_text)
# Préparer l'historique complet pour Gemini
gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le dernier message utilisateur (qui est dans final_prompt_parts)
print(f"\n--- Envoi à Gemini ---")
print(f"Historique envoyé: {len(gemini_history)} messages")
print(f"Parties du prompt actuel: {len(final_prompt_parts)}")
# print(f"Prompt textuel final: {current_prompt_text[:200]}...") # Afficher début du prompt
# print("----------------------\n")
# Appel à Gemini API
# Utiliser generate_content qui prend l'historique + le nouveau message
full_conversation = gemini_history + [{'role': 'user', 'parts': final_prompt_parts}]
response = model.generate_content(full_conversation)
# --- Traitement de la réponse ---
response_text = response.text
print(f"\n--- Réponse de Gemini ---")
print(response_text[:500] + ('...' if len(response_text) > 500 else ''))
# print("-------------------------\n")
# Ajout de la réponse à l'historique de session
session['chat_history'].append({'role': 'assistant', 'text': response_text})
session.modified = True
except Exception as e:
print(f"Erreur lors de l'appel à Gemini : {e}")
session['error'] = f"Une erreur s'est produite lors de la communication avec l'IA : {e}"
# Si une erreur survient, on retire le message utilisateur qui a causé l'erreur
# pour éviter de le renvoyer indéfiniment.
if session['chat_history'] and session['chat_history'][-1]['role'] == 'user':
session['chat_history'].pop()
session.modified = True
finally:
# Indiquer que le traitement est terminé
session['processing'] = False
session.pop('processing_web_search', None) # Nettoyer l'indicateur de recherche
session.modified = True
return redirect(url_for('index')) # Redirige vers la page principale pour afficher le nouvel état
@app.route('/clear', methods=['POST'])
def clear_chat():
"""Efface l'historique de la conversation."""
session.pop('chat_history', None) # Supprime l'historique de la session
session.pop('web_search', None) # Réinitialise aussi le toggle web
print("Historique de chat effacé.")
return redirect(url_for('index')) # Redirige vers la page d'accueil
# --- Démarrage de l'application ---
if __name__ == '__main__':
# Utiliser host='0.0.0.0' pour rendre l'app accessible sur le réseau local
# Debug=True est utile pour le développement, mais à désactiver en production
app.run(debug=True, host='0.0.0.0', port=5001) |