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