Update app.py
Browse files
app.py
CHANGED
|
@@ -1,346 +1,313 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
-
import
|
| 4 |
-
from flask import Flask, request, session, jsonify, redirect, url_for, flash, render_template
|
| 5 |
-
from dotenv import load_dotenv
|
| 6 |
from google import genai
|
| 7 |
from google.genai import types
|
| 8 |
-
import requests
|
| 9 |
from werkzeug.utils import secure_filename
|
| 10 |
-
import
|
| 11 |
-
from
|
| 12 |
-
import
|
| 13 |
-
import
|
| 14 |
-
import html # Pour échapper le HTML dans les messages Telegram
|
| 15 |
-
|
| 16 |
-
# --- Configuration Initiale ---
|
| 17 |
-
load_dotenv()
|
| 18 |
|
| 19 |
app = Flask(__name__)
|
|
|
|
| 20 |
|
| 21 |
-
#
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
# --- Configuration Telegram ---
|
| 27 |
-
TELEGRAM_BOT_TOKEN = "8004545342:AAGcZaoDjYg8dmbbXRsR1N3TfSSbEiAGz88"
|
| 28 |
-
TELEGRAM_CHAT_ID = "-1002497861230"
|
| 29 |
-
TELEGRAM_ENABLED = TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID
|
| 30 |
-
|
| 31 |
-
if TELEGRAM_ENABLED:
|
| 32 |
-
logger.info(f"Notifications Telegram activées pour le chat ID: {TELEGRAM_CHAT_ID}")
|
| 33 |
-
else:
|
| 34 |
-
logger.warning("Notifications Telegram désactivées. Ajoutez TELEGRAM_BOT_TOKEN et TELEGRAM_CHAT_ID dans .env")
|
| 35 |
|
| 36 |
-
# --- Configuration Flask Standard ---
|
| 37 |
-
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-super-cle-secrete-a-changer')
|
| 38 |
-
|
| 39 |
-
# Configuration pour les uploads
|
| 40 |
-
UPLOAD_FOLDER = 'temp'
|
| 41 |
-
# CHANGED: Ajout des extensions audio
|
| 42 |
-
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'mp3', 'wav', 'aac', 'ogg', 'flac'}
|
| 43 |
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
| 44 |
-
app.config['MAX_CONTENT_LENGTH'] =
|
| 45 |
|
|
|
|
| 46 |
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 47 |
-
logger.info(f"Dossier d'upload configuré : {os.path.abspath(UPLOAD_FOLDER)}")
|
| 48 |
-
|
| 49 |
-
# --- Configuration pour Flask-Session ---
|
| 50 |
-
app.config['SESSION_TYPE'] = 'filesystem'
|
| 51 |
-
app.config['SESSION_PERMANENT'] = False
|
| 52 |
-
app.config['SESSION_USE_SIGNER'] = True
|
| 53 |
-
app.config['SESSION_FILE_DIR'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'flask_session')
|
| 54 |
-
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
| 55 |
-
app.config['SESSION_COOKIE_SECURE'] = True
|
| 56 |
-
os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
|
| 57 |
-
logger.info(f"Dossier pour les sessions serveur configuré : {app.config['SESSION_FILE_DIR']}")
|
| 58 |
-
|
| 59 |
-
server_session = Session(app)
|
| 60 |
|
| 61 |
-
#
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
MODEL_PRO = 'gemini-2.5-pro'
|
| 65 |
-
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. Tu peux analyser des textes, des images, des fichiers audio, exécuter du code, faire des recherches sur le web et analyser le contenu d'URLs."
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
GEMINI_CONFIGURED = False
|
| 75 |
-
genai_client = None
|
| 76 |
-
try:
|
| 77 |
-
gemini_api_key = os.getenv("GOOGLE_API_KEY")
|
| 78 |
-
if not gemini_api_key:
|
| 79 |
-
logger.error("ERREUR: Clé API GOOGLE_API_KEY manquante dans le fichier .env")
|
| 80 |
-
else:
|
| 81 |
-
genai_client = genai.Client(api_key=gemini_api_key)
|
| 82 |
-
try:
|
| 83 |
-
models = genai_client.list_models()
|
| 84 |
-
models_list = [model.name for model in models]
|
| 85 |
-
if any(MODEL_FLASH in model for model in models_list) and any(MODEL_PRO in model for model in models_list):
|
| 86 |
-
logger.info(f"Configuration Gemini effectuée. Modèles requis ({MODEL_FLASH}, {MODEL_PRO}) disponibles.")
|
| 87 |
-
logger.info(f"System instruction: {SYSTEM_INSTRUCTION}")
|
| 88 |
-
GEMINI_CONFIGURED = True
|
| 89 |
-
else:
|
| 90 |
-
logger.error(f"ERREUR: Les modèles requis ({MODEL_FLASH}, {MODEL_PRO}) ne sont pas tous disponibles via l'API.")
|
| 91 |
-
logger.error(f"Modèles trouvés: {models_list}")
|
| 92 |
-
except Exception as e_models:
|
| 93 |
-
logger.error(f"ERREUR lors de la vérification des modèles: {e_models}")
|
| 94 |
-
logger.warning("Tentative de continuer sans vérification des modèles disponibles.")
|
| 95 |
-
GEMINI_CONFIGURED = True
|
| 96 |
-
except Exception as e:
|
| 97 |
-
logger.critical(f"ERREUR Critique lors de la configuration initiale de Gemini : {e}")
|
| 98 |
-
logger.warning("L'application fonctionnera sans les fonctionnalités IA.")
|
| 99 |
-
|
| 100 |
-
# --- Fonctions Utilitaires ---
|
| 101 |
|
| 102 |
def allowed_file(filename):
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
return None, None, None
|
| 127 |
-
if allowed_file(file.filename):
|
| 128 |
-
try:
|
| 129 |
-
filename = secure_filename(file.filename)
|
| 130 |
-
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 131 |
-
file.save(filepath)
|
| 132 |
-
logger.debug(f" [process_uploaded_file]: Fichier '{filename}' sauvegardé dans '{filepath}'")
|
| 133 |
-
mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
|
| 134 |
-
with open(filepath, "rb") as f:
|
| 135 |
-
file_data = f.read()
|
| 136 |
-
file_part = {"inline_data": {"mime_type": mime_type, "data": file_data}}
|
| 137 |
-
return file_part, filename, filepath
|
| 138 |
-
except Exception as e:
|
| 139 |
-
logger.error(f"--- ERREUR [process_uploaded_file]: Échec traitement fichier '{file.filename}': {e}")
|
| 140 |
-
return None, None, None
|
| 141 |
-
else:
|
| 142 |
-
logger.error(f"--- ERREUR [process_uploaded_file]: Type de fichier non autorisé: {file.filename}")
|
| 143 |
-
return None, None, None
|
| 144 |
-
|
| 145 |
-
def send_telegram_message(message):
|
| 146 |
-
"""Envoie un message à Telegram via l'API Bot."""
|
| 147 |
-
if not TELEGRAM_ENABLED:
|
| 148 |
-
return False
|
| 149 |
-
try:
|
| 150 |
-
sanitized_message = html.escape(message)
|
| 151 |
-
if len(sanitized_message) > 4000:
|
| 152 |
-
sanitized_message = sanitized_message[:3997] + "..."
|
| 153 |
-
response = requests.post(
|
| 154 |
-
f'https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage',
|
| 155 |
-
data={'chat_id': TELEGRAM_CHAT_ID, 'text': sanitized_message, 'parse_mode': 'HTML'}
|
| 156 |
-
)
|
| 157 |
-
if response.status_code == 200:
|
| 158 |
-
logger.debug("Message Telegram envoyé avec succès")
|
| 159 |
-
return True
|
| 160 |
-
else:
|
| 161 |
-
logger.error(f"Échec d'envoi du message Telegram: {response.status_code} - {response.text}")
|
| 162 |
-
return False
|
| 163 |
-
except Exception as e:
|
| 164 |
-
logger.error(f"Erreur lors de l'envoi du message Telegram: {e}")
|
| 165 |
-
return False
|
| 166 |
-
|
| 167 |
-
def generate_session_id():
|
| 168 |
-
"""Génère un ID de session unique."""
|
| 169 |
-
import uuid
|
| 170 |
-
return str(uuid.uuid4())[:8]
|
| 171 |
-
|
| 172 |
-
# --- Routes Flask ---
|
| 173 |
-
|
| 174 |
-
@app.route('/')
|
| 175 |
-
def root():
|
| 176 |
-
if 'session_id' not in session:
|
| 177 |
-
session['session_id'] = generate_session_id()
|
| 178 |
-
return render_template('index.html')
|
| 179 |
-
|
| 180 |
-
@app.route('/api/history', methods=['GET'])
|
| 181 |
-
def get_history():
|
| 182 |
-
if 'chat_history' not in session:
|
| 183 |
-
session['chat_history'] = []
|
| 184 |
-
return jsonify({'success': True, 'history': session.get('chat_history', [])})
|
| 185 |
-
|
| 186 |
-
@app.route('/api/chat', methods=['POST'])
|
| 187 |
-
def chat_api():
|
| 188 |
-
logger.debug("\n---===================================---")
|
| 189 |
-
logger.debug("--- DEBUG [/api/chat]: Nouvelle requête POST ---")
|
| 190 |
-
|
| 191 |
-
if not GEMINI_CONFIGURED or not genai_client:
|
| 192 |
-
return jsonify({'success': False, 'error': "Le service IA n'est pas configuré correctement."}), 503
|
| 193 |
-
|
| 194 |
-
prompt = request.form.get('prompt', '').strip()
|
| 195 |
-
use_web_search = request.form.get('web_search', 'false').lower() == 'true'
|
| 196 |
-
use_advanced = request.form.get('advanced_reasoning', 'false').lower() == 'true'
|
| 197 |
-
files = request.files.getlist('file')
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
| 202 |
|
| 203 |
-
|
| 204 |
-
return jsonify({'success': False, 'error': 'Veuillez fournir un message ou au moins un fichier.'}), 400
|
| 205 |
-
|
| 206 |
-
if 'session_id' not in session:
|
| 207 |
-
session['session_id'] = generate_session_id()
|
| 208 |
-
session_id = session['session_id']
|
| 209 |
-
|
| 210 |
-
if 'chat_history' not in session:
|
| 211 |
-
session['chat_history'] = []
|
| 212 |
-
|
| 213 |
-
uploaded_file_parts = []
|
| 214 |
-
uploaded_filenames = []
|
| 215 |
-
filepaths_to_delete = []
|
| 216 |
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
uploaded_file_parts.append(file_part)
|
| 222 |
-
uploaded_filenames.append(filename)
|
| 223 |
-
filepaths_to_delete.append(filepath)
|
| 224 |
-
|
| 225 |
-
raw_user_text = prompt
|
| 226 |
-
files_text = ", ".join([f"[{filename}]" for filename in uploaded_filenames]) if uploaded_filenames else ""
|
| 227 |
-
display_user_text = f"{files_text} {prompt}" if files_text and prompt else (prompt or files_text)
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
if TELEGRAM_ENABLED:
|
| 233 |
-
files_info = f"[{len(uploaded_filenames)} fichiers] " if uploaded_filenames else ""
|
| 234 |
-
telegram_message = f"🔵 NOUVEAU MESSAGE (Session {session_id})\n\n{files_info}{prompt}"
|
| 235 |
-
send_telegram_message(telegram_message)
|
| 236 |
-
|
| 237 |
-
selected_model_name = MODEL_PRO if use_advanced else MODEL_FLASH
|
| 238 |
|
| 239 |
-
#
|
| 240 |
-
|
| 241 |
-
history_for_gemini = list(session.get('chat_history', []))[:-1]
|
| 242 |
-
gemini_history_to_send = prepare_gemini_history(history_for_gemini)
|
| 243 |
-
contents = gemini_history_to_send.copy()
|
| 244 |
-
|
| 245 |
-
current_user_parts = uploaded_file_parts.copy()
|
| 246 |
-
if raw_user_text:
|
| 247 |
-
current_user_parts.append({"text": raw_user_text})
|
| 248 |
-
elif uploaded_filenames and not raw_user_text:
|
| 249 |
-
# Si seulement un fichier est envoyé, générer un prompt
|
| 250 |
-
current_user_parts.append({"text": f"Décris le contenu de ce(s) fichier(s) : {', '.join(uploaded_filenames)}"})
|
| 251 |
-
|
| 252 |
-
contents.append({"role": "user", "parts": current_user_parts})
|
| 253 |
-
|
| 254 |
-
# NEW: Définition des outils à utiliser
|
| 255 |
-
tools = [
|
| 256 |
-
{"url_context": {}}, # Pour analyser les URLs dans le prompt
|
| 257 |
-
{"code_execution": {}}, # Pour exécuter du code Python
|
| 258 |
-
]
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
-
|
| 264 |
-
generate_config = types.GenerateContentConfig(
|
| 265 |
-
system_instruction=SYSTEM_INSTRUCTION,
|
| 266 |
-
safety_settings=SAFETY_SETTINGS,
|
| 267 |
-
tools=tools, # Intégration des outils
|
| 268 |
-
)
|
| 269 |
-
# Le "thinking" est activé par défaut sur les modèles 2.5, pas besoin de configurer `thinking_config` pour le mode dynamique.
|
| 270 |
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
response_html = ""
|
| 281 |
try:
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
response_text_raw = f"Désolé, ma réponse a été bloquée (raison : {block_reason})."
|
| 289 |
-
else:
|
| 290 |
-
response_text_raw = "Désolé, je n'ai pas pu générer de réponse."
|
| 291 |
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
|
|
|
|
|
|
| 308 |
except Exception as e:
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
|
| 319 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
-
|
| 322 |
-
for filepath in filepaths_to_delete:
|
| 323 |
-
if filepath and os.path.exists(filepath):
|
| 324 |
-
try:
|
| 325 |
-
os.remove(filepath)
|
| 326 |
-
logger.debug(f"--- LOG [/api/chat FINALLY]: Fichier temporaire '{filepath}' supprimé.")
|
| 327 |
-
except OSError as e_del:
|
| 328 |
-
logger.error(f"--- ERREUR [/api/chat FINALLY]: Échec suppression fichier '{filepath}': {e_del}")
|
| 329 |
|
| 330 |
-
@app.route('/
|
| 331 |
-
def
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
if __name__ == '__main__':
|
| 344 |
-
|
| 345 |
-
port = int(os.environ.get('PORT', 5001))
|
| 346 |
-
app.run(debug=True, host='0.0.0.0', port=port)
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify, Response, session
|
| 2 |
import os
|
| 3 |
import json
|
| 4 |
+
import base64
|
|
|
|
|
|
|
| 5 |
from google import genai
|
| 6 |
from google.genai import types
|
|
|
|
| 7 |
from werkzeug.utils import secure_filename
|
| 8 |
+
import uuid
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
import io
|
| 11 |
+
from typing import Optional, List, Dict, Union
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
app = Flask(__name__)
|
| 14 |
+
app.secret_key = 'your-secret-key-here' # Changez cette clé !
|
| 15 |
|
| 16 |
+
# Configuration de l'upload
|
| 17 |
+
UPLOAD_FOLDER = 'uploads'
|
| 18 |
+
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'pdf', 'mp4', 'mov', 'txt', 'csv', 'json'}
|
| 19 |
+
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
| 22 |
+
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
|
| 23 |
|
| 24 |
+
# Créer le dossier uploads s'il n'existe pas
|
| 25 |
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
# Initialisation du client Gemini
|
| 28 |
+
API_KEY = "AIzaSyAMYpF67aqFnWDJESWOx1dC-w3sEU29VcM"
|
| 29 |
+
client = genai.Client(api_key=API_KEY)
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
# Configuration des modèles disponibles
|
| 32 |
+
MODELS = {
|
| 33 |
+
"Gemini 2.5 Flash": "gemini-2.5-flash",
|
| 34 |
+
"Gemini 2.5 Pro": "gemini-2.5-pro",
|
| 35 |
+
"Gemini 2.5 Flash Lite": "gemini-2.5-flash-lite",
|
| 36 |
+
"Gemini 2.0 Flash": "gemini-2.0-flash"
|
| 37 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
def allowed_file(filename):
|
| 40 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 41 |
+
|
| 42 |
+
def configure_tools() -> List[types.Tool]:
|
| 43 |
+
"""Configure tous les outils par défaut"""
|
| 44 |
+
tools = []
|
| 45 |
+
tools.append(types.Tool(code_execution=types.ToolCodeExecution()))
|
| 46 |
+
tools.append(types.Tool(google_search=types.GoogleSearch()))
|
| 47 |
+
tools.append(types.Tool(url_context=types.UrlContext()))
|
| 48 |
+
return tools
|
| 49 |
+
|
| 50 |
+
def get_generation_config(
|
| 51 |
+
model: str,
|
| 52 |
+
thinking_enabled: bool = True,
|
| 53 |
+
include_thoughts: bool = False
|
| 54 |
+
) -> types.GenerateContentConfig:
|
| 55 |
+
"""Configuration optimisée par défaut"""
|
| 56 |
+
config_dict = {
|
| 57 |
+
"temperature": 0.7,
|
| 58 |
+
"max_output_tokens": 8192,
|
| 59 |
+
"top_p": 0.9,
|
| 60 |
+
"top_k": 40,
|
| 61 |
+
"tools": configure_tools()
|
| 62 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
if thinking_enabled:
|
| 65 |
+
config_dict["thinking_config"] = types.ThinkingConfig(
|
| 66 |
+
thinking_budget=-1, # Dynamique par défaut
|
| 67 |
+
include_thoughts=include_thoughts
|
| 68 |
+
)
|
| 69 |
|
| 70 |
+
return types.GenerateContentConfig(**config_dict)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
def process_uploaded_file(file) -> Optional[types.Part]:
|
| 73 |
+
"""Traite les fichiers uploadés"""
|
| 74 |
+
if not file:
|
| 75 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
file_bytes = file.read()
|
| 78 |
+
mime_type = file.content_type
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
# Réinitialiser le pointeur du fichier
|
| 81 |
+
file.seek(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
+
# Gestion des différents types de fichiers
|
| 84 |
+
if mime_type.startswith("image/"):
|
| 85 |
+
return types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
|
| 86 |
+
elif mime_type == "application/pdf":
|
| 87 |
+
return types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
|
| 88 |
+
elif mime_type.startswith("video/"):
|
| 89 |
+
return types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
|
| 90 |
+
elif mime_type in ["text/plain", "text/csv", "application/json"]:
|
| 91 |
+
return types.Part.from_bytes(data=file_bytes, mime_type=mime_type)
|
| 92 |
|
| 93 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
+
@app.route('/')
|
| 96 |
+
def index():
|
| 97 |
+
"""Page principale"""
|
| 98 |
+
return render_template('index.html', models=MODELS)
|
| 99 |
+
|
| 100 |
+
@app.route('/send_message', methods=['POST'])
|
| 101 |
+
def send_message():
|
| 102 |
+
"""Endpoint pour envoyer un message avec streaming"""
|
| 103 |
+
def generate():
|
|
|
|
| 104 |
try:
|
| 105 |
+
# Récupération des données
|
| 106 |
+
data = request.get_json()
|
| 107 |
+
message = data.get('message', '')
|
| 108 |
+
model = data.get('model', 'gemini-2.5-flash')
|
| 109 |
+
thinking_enabled = data.get('thinking_enabled', True)
|
| 110 |
+
include_thoughts = data.get('include_thoughts', False)
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
if not message.strip():
|
| 113 |
+
yield f"data: {json.dumps({'error': 'Message vide'})}\n\n"
|
| 114 |
+
return
|
| 115 |
+
|
| 116 |
+
# Configuration de génération
|
| 117 |
+
generation_config = get_generation_config(
|
| 118 |
+
model=model,
|
| 119 |
+
thinking_enabled=thinking_enabled,
|
| 120 |
+
include_thoughts=include_thoughts
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
# Initialisation ou récupération du chat
|
| 124 |
+
chat_id = session.get('chat_id')
|
| 125 |
+
if not chat_id:
|
| 126 |
+
chat_id = str(uuid.uuid4())
|
| 127 |
+
session['chat_id'] = chat_id
|
| 128 |
+
session['chat'] = client.chats.create(
|
| 129 |
+
model=model,
|
| 130 |
+
config=generation_config
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
chat = session.get('chat')
|
| 134 |
+
if not chat:
|
| 135 |
+
session['chat'] = client.chats.create(
|
| 136 |
+
model=model,
|
| 137 |
+
config=generation_config
|
| 138 |
+
)
|
| 139 |
+
chat = session['chat']
|
| 140 |
+
|
| 141 |
+
# Préparation du contenu
|
| 142 |
+
contents = [message]
|
| 143 |
+
|
| 144 |
+
# Envoi du message avec streaming
|
| 145 |
+
response_stream = chat.send_message_stream(
|
| 146 |
+
contents,
|
| 147 |
+
config=generation_config
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
# Streaming de la réponse
|
| 151 |
+
full_response = ""
|
| 152 |
+
thoughts_content = ""
|
| 153 |
+
|
| 154 |
+
for chunk in response_stream:
|
| 155 |
+
for part in chunk.candidates[0].content.parts:
|
| 156 |
+
if part.text:
|
| 157 |
+
if part.thought and include_thoughts:
|
| 158 |
+
thoughts_content += part.text
|
| 159 |
+
yield f"data: {json.dumps({'type': 'thought', 'content': part.text})}\n\n"
|
| 160 |
+
else:
|
| 161 |
+
full_response += part.text
|
| 162 |
+
yield f"data: {json.dumps({'type': 'text', 'content': part.text})}\n\n"
|
| 163 |
+
|
| 164 |
+
# Message de fin
|
| 165 |
+
yield f"data: {json.dumps({'type': 'end', 'full_response': full_response})}\n\n"
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
| 169 |
+
|
| 170 |
+
return Response(generate(), mimetype='text/event-stream')
|
| 171 |
+
|
| 172 |
+
@app.route('/upload_file', methods=['POST'])
|
| 173 |
+
def upload_file():
|
| 174 |
+
"""Endpoint pour uploader des fichiers"""
|
| 175 |
+
try:
|
| 176 |
+
if 'file' not in request.files:
|
| 177 |
+
return jsonify({'error': 'Aucun fichier fourni'}), 400
|
| 178 |
|
| 179 |
+
file = request.files['file']
|
| 180 |
+
if file.filename == '':
|
| 181 |
+
return jsonify({'error': 'Aucun fichier sélectionné'}), 400
|
| 182 |
|
| 183 |
+
if file and allowed_file(file.filename):
|
| 184 |
+
filename = secure_filename(file.filename)
|
| 185 |
+
file_id = str(uuid.uuid4())
|
| 186 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{file_id}_{filename}")
|
| 187 |
+
|
| 188 |
+
# Sauvegarder le fichier
|
| 189 |
+
file.save(file_path)
|
| 190 |
+
|
| 191 |
+
# Traiter le fichier pour Gemini
|
| 192 |
+
with open(file_path, 'rb') as f:
|
| 193 |
+
file_part = process_uploaded_file(f)
|
| 194 |
+
|
| 195 |
+
if file_part:
|
| 196 |
+
# Stocker les informations du fichier en session
|
| 197 |
+
if 'uploaded_files' not in session:
|
| 198 |
+
session['uploaded_files'] = {}
|
| 199 |
+
|
| 200 |
+
session['uploaded_files'][file_id] = {
|
| 201 |
+
'filename': filename,
|
| 202 |
+
'path': file_path,
|
| 203 |
+
'mime_type': file.content_type
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
return jsonify({
|
| 207 |
+
'success': True,
|
| 208 |
+
'file_id': file_id,
|
| 209 |
+
'filename': filename,
|
| 210 |
+
'mime_type': file.content_type
|
| 211 |
+
})
|
| 212 |
+
else:
|
| 213 |
+
# Supprimer le fichier si le traitement a échoué
|
| 214 |
+
os.remove(file_path)
|
| 215 |
+
return jsonify({'error': 'Type de fichier non supporté'}), 400
|
| 216 |
|
| 217 |
+
return jsonify({'error': 'Type de fichier non autorisé'}), 400
|
| 218 |
+
|
| 219 |
except Exception as e:
|
| 220 |
+
return jsonify({'error': str(e)}), 500
|
| 221 |
+
|
| 222 |
+
@app.route('/send_message_with_file', methods=['POST'])
|
| 223 |
+
def send_message_with_file():
|
| 224 |
+
"""Endpoint pour envoyer un message avec fichier attaché"""
|
| 225 |
+
def generate():
|
| 226 |
+
try:
|
| 227 |
+
data = request.get_json()
|
| 228 |
+
message = data.get('message', '')
|
| 229 |
+
model = data.get('model', 'gemini-2.5-flash')
|
| 230 |
+
thinking_enabled = data.get('thinking_enabled', True)
|
| 231 |
+
include_thoughts = data.get('include_thoughts', False)
|
| 232 |
+
file_id = data.get('file_id')
|
| 233 |
+
|
| 234 |
+
if not message.strip():
|
| 235 |
+
yield f"data: {json.dumps({'error': 'Message vide'})}\n\n"
|
| 236 |
+
return
|
| 237 |
+
|
| 238 |
+
# Configuration de génération
|
| 239 |
+
generation_config = get_generation_config(
|
| 240 |
+
model=model,
|
| 241 |
+
thinking_enabled=thinking_enabled,
|
| 242 |
+
include_thoughts=include_thoughts
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# Récupération du chat
|
| 246 |
+
chat = session.get('chat')
|
| 247 |
+
if not chat:
|
| 248 |
+
session['chat'] = client.chats.create(
|
| 249 |
+
model=model,
|
| 250 |
+
config=generation_config
|
| 251 |
+
)
|
| 252 |
+
chat = session['chat']
|
| 253 |
+
|
| 254 |
+
# Préparation du contenu
|
| 255 |
+
contents = [message]
|
| 256 |
+
|
| 257 |
+
# Ajout du fichier si présent
|
| 258 |
+
if file_id and 'uploaded_files' in session and file_id in session['uploaded_files']:
|
| 259 |
+
file_info = session['uploaded_files'][file_id]
|
| 260 |
+
with open(file_info['path'], 'rb') as f:
|
| 261 |
+
file_part = process_uploaded_file(f)
|
| 262 |
+
if file_part:
|
| 263 |
+
contents.append(file_part)
|
| 264 |
+
|
| 265 |
+
# Envoi du message avec streaming
|
| 266 |
+
response_stream = chat.send_message_stream(
|
| 267 |
+
contents,
|
| 268 |
+
config=generation_config
|
| 269 |
+
)
|
| 270 |
|
| 271 |
+
# Streaming de la réponse
|
| 272 |
+
full_response = ""
|
| 273 |
+
|
| 274 |
+
for chunk in response_stream:
|
| 275 |
+
for part in chunk.candidates[0].content.parts:
|
| 276 |
+
if part.text:
|
| 277 |
+
if part.thought and include_thoughts:
|
| 278 |
+
yield f"data: {json.dumps({'type': 'thought', 'content': part.text})}\n\n"
|
| 279 |
+
else:
|
| 280 |
+
full_response += part.text
|
| 281 |
+
yield f"data: {json.dumps({'type': 'text', 'content': part.text})}\n\n"
|
| 282 |
+
|
| 283 |
+
# Message de fin
|
| 284 |
+
yield f"data: {json.dumps({'type': 'end', 'full_response': full_response})}\n\n"
|
| 285 |
+
|
| 286 |
+
except Exception as e:
|
| 287 |
+
yield f"data: {json.dumps({'error': str(e)})}\n\n"
|
| 288 |
|
| 289 |
+
return Response(generate(), mimetype='text/event-stream')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
|
| 291 |
+
@app.route('/reset_chat', methods=['POST'])
|
| 292 |
+
def reset_chat():
|
| 293 |
+
"""Reset la conversation"""
|
| 294 |
+
try:
|
| 295 |
+
session.pop('chat_id', None)
|
| 296 |
+
session.pop('chat', None)
|
| 297 |
+
|
| 298 |
+
# Nettoyer les fichiers uploadés
|
| 299 |
+
if 'uploaded_files' in session:
|
| 300 |
+
for file_info in session['uploaded_files'].values():
|
| 301 |
+
try:
|
| 302 |
+
if os.path.exists(file_info['path']):
|
| 303 |
+
os.remove(file_info['path'])
|
| 304 |
+
except:
|
| 305 |
+
pass
|
| 306 |
+
session.pop('uploaded_files', None)
|
| 307 |
+
|
| 308 |
+
return jsonify({'success': True})
|
| 309 |
+
except Exception as e:
|
| 310 |
+
return jsonify({'error': str(e)}), 500
|
| 311 |
|
| 312 |
if __name__ == '__main__':
|
| 313 |
+
app.run(debug=True, host='0.0.0.0', port=5000)
|
|
|
|
|
|