Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
| 1 |
-
# --- START OF FILE app.py ---
|
| 2 |
-
|
| 3 |
import os
|
| 4 |
import json
|
| 5 |
import mimetypes
|
|
@@ -11,62 +9,66 @@ import requests
|
|
| 11 |
from werkzeug.utils import secure_filename
|
| 12 |
import markdown # Pour convertir la réponse en HTML
|
| 13 |
from flask_session import Session
|
| 14 |
-
import pprint # Pour un affichage plus lisible (optionnel)
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# --- Configuration Initiale ---
|
| 17 |
load_dotenv()
|
| 18 |
|
| 19 |
app = Flask(__name__)
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
# --- Configuration Flask Standard ---
|
| 22 |
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-super-cle-secrete-a-changer')
|
| 23 |
|
| 24 |
# Configuration pour les uploads
|
| 25 |
UPLOAD_FOLDER = 'temp'
|
| 26 |
-
|
|
|
|
| 27 |
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
| 28 |
-
app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024
|
| 29 |
|
| 30 |
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 31 |
-
|
| 32 |
|
| 33 |
-
# --- Configuration pour Flask-Session
|
| 34 |
app.config['SESSION_TYPE'] = 'filesystem'
|
| 35 |
app.config['SESSION_PERMANENT'] = False
|
| 36 |
app.config['SESSION_USE_SIGNER'] = True
|
| 37 |
app.config['SESSION_FILE_DIR'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'flask_session')
|
| 38 |
app.config['SESSION_COOKIE_SAMESITE'] = 'None'
|
| 39 |
app.config['SESSION_COOKIE_SECURE'] = True
|
| 40 |
-
|
| 41 |
os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
|
| 42 |
-
|
| 43 |
|
| 44 |
-
# --- Initialisation de Flask-Session ---
|
| 45 |
server_session = Session(app)
|
| 46 |
|
| 47 |
# --- Configuration de l'API Gemini ---
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
|
| 53 |
SAFETY_SETTINGS = [
|
| 54 |
-
types.SafetySetting(
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
)
|
| 58 |
-
types.SafetySetting(
|
| 59 |
-
category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
|
| 60 |
-
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
| 61 |
-
),
|
| 62 |
-
types.SafetySetting(
|
| 63 |
-
category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
| 64 |
-
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
| 65 |
-
),
|
| 66 |
-
types.SafetySetting(
|
| 67 |
-
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
| 68 |
-
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
| 69 |
-
)
|
| 70 |
]
|
| 71 |
|
| 72 |
GEMINI_CONFIGURED = False
|
|
@@ -74,341 +76,271 @@ genai_client = None
|
|
| 74 |
try:
|
| 75 |
gemini_api_key = os.getenv("GOOGLE_API_KEY")
|
| 76 |
if not gemini_api_key:
|
| 77 |
-
|
| 78 |
else:
|
| 79 |
-
# Initialisation du client avec le SDK
|
| 80 |
genai_client = genai.Client(api_key=gemini_api_key)
|
| 81 |
-
|
| 82 |
-
# Vérification de la disponibilité des modèles
|
| 83 |
try:
|
| 84 |
models = genai_client.list_models()
|
| 85 |
models_list = [model.name for model in models]
|
| 86 |
if any(MODEL_FLASH in model for model in models_list) and any(MODEL_PRO in model for model in models_list):
|
| 87 |
-
|
| 88 |
-
|
| 89 |
GEMINI_CONFIGURED = True
|
| 90 |
else:
|
| 91 |
-
|
| 92 |
-
|
| 93 |
except Exception as e_models:
|
| 94 |
-
|
| 95 |
-
|
| 96 |
GEMINI_CONFIGURED = True
|
| 97 |
-
|
| 98 |
except Exception as e:
|
| 99 |
-
|
| 100 |
-
|
| 101 |
|
| 102 |
# --- Fonctions Utilitaires ---
|
| 103 |
|
| 104 |
def allowed_file(filename):
|
| 105 |
"""Vérifie si l'extension du fichier est autorisée."""
|
| 106 |
-
return '.' in filename and
|
| 107 |
-
|
| 108 |
-
def perform_web_search(query, client, model_id):
|
| 109 |
-
"""
|
| 110 |
-
Réalise un appel minimal pour la recherche web en utilisant le format documenté.
|
| 111 |
-
"""
|
| 112 |
-
try:
|
| 113 |
-
print(f"--- LOG WEBSEARCH: Recherche Google pour: '{query}'")
|
| 114 |
-
response = client.models.generate_content(
|
| 115 |
-
model=model_id,
|
| 116 |
-
contents=query,
|
| 117 |
-
config={"tools": [{"google_search": {}}]},
|
| 118 |
-
)
|
| 119 |
-
print("--- LOG WEBSEARCH: Résultats de recherche obtenus.")
|
| 120 |
-
return response
|
| 121 |
-
except Exception as e:
|
| 122 |
-
print(f"--- LOG WEBSEARCH: Erreur lors de la recherche web : {e}")
|
| 123 |
-
return None
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
if not response:
|
| 128 |
-
return ""
|
| 129 |
-
try:
|
| 130 |
-
return response.text
|
| 131 |
-
except Exception as e:
|
| 132 |
-
print(f"--- LOG WEBSEARCH: Erreur lors de l'extraction du texte: {e}")
|
| 133 |
-
return ""
|
| 134 |
|
| 135 |
def prepare_gemini_history(chat_history):
|
| 136 |
-
"""Convertit l'historique stocké en session au format attendu par
|
| 137 |
-
|
| 138 |
gemini_history = []
|
| 139 |
-
for
|
| 140 |
role = message.get('role')
|
| 141 |
text_part = message.get('raw_text')
|
| 142 |
-
print(f" [prepare_gemini_history] Message {i} (rôle: {role}): raw_text présent? {'Oui' if text_part else 'NON'}")
|
| 143 |
if text_part:
|
| 144 |
-
if role == 'user'
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
"parts": [{"text": text_part}]
|
| 148 |
-
})
|
| 149 |
-
else: # assistant ou autre
|
| 150 |
-
gemini_history.append({
|
| 151 |
-
"role": "model",
|
| 152 |
-
"parts": [{"text": text_part}]
|
| 153 |
-
})
|
| 154 |
-
else:
|
| 155 |
-
print(f" AVERTISSEMENT [prepare_gemini_history]: Message {i} sans texte, ignoré.")
|
| 156 |
-
print(f"--- DEBUG [prepare_gemini_history]: Sortie avec {len(gemini_history)} messages")
|
| 157 |
return gemini_history
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
# --- Routes Flask ---
|
| 160 |
|
| 161 |
@app.route('/')
|
| 162 |
def root():
|
| 163 |
-
|
|
|
|
| 164 |
return render_template('index.html')
|
| 165 |
|
| 166 |
@app.route('/api/history', methods=['GET'])
|
| 167 |
def get_history():
|
| 168 |
-
print("\n--- DEBUG [/api/history]: Début requête GET ---")
|
| 169 |
if 'chat_history' not in session:
|
| 170 |
session['chat_history'] = []
|
| 171 |
-
|
| 172 |
-
display_history = []
|
| 173 |
-
current_history = session.get('chat_history', [])
|
| 174 |
-
print(f" [/api/history]: Historique récupéré: {len(current_history)} messages.")
|
| 175 |
-
for i, msg in enumerate(current_history):
|
| 176 |
-
if isinstance(msg, dict) and 'role' in msg and 'text' in msg:
|
| 177 |
-
display_history.append({
|
| 178 |
-
'role': msg.get('role'),
|
| 179 |
-
'text': msg.get('text')
|
| 180 |
-
})
|
| 181 |
-
else:
|
| 182 |
-
print(f" AVERTISSEMENT [/api/history]: Format invalide pour le message {i}: {msg}")
|
| 183 |
-
print(f" [/api/history]: Historique préparé pour le frontend: {len(display_history)} messages.")
|
| 184 |
-
return jsonify({'success': True, 'history': display_history})
|
| 185 |
|
| 186 |
@app.route('/api/chat', methods=['POST'])
|
| 187 |
def chat_api():
|
| 188 |
-
|
| 189 |
-
|
|
|
|
| 190 |
if not GEMINI_CONFIGURED or not genai_client:
|
| 191 |
-
print("--- ERREUR [/api/chat]: Service IA non configuré.")
|
| 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 |
-
file = request.files.get('file')
|
| 197 |
use_advanced = request.form.get('advanced_reasoning', 'false').lower() == 'true'
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
return jsonify({'success': False, 'error': 'Veuillez fournir un message ou un fichier.'}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
if 'chat_history' not in session:
|
| 208 |
session['chat_history'] = []
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
| 222 |
-
file.save(filepath)
|
| 223 |
-
filepath_to_delete = filepath
|
| 224 |
-
uploaded_filename = filename
|
| 225 |
-
print(f" [/api/chat]: Fichier '{filename}' sauvegardé dans '{filepath}'")
|
| 226 |
-
mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
|
| 227 |
-
with open(filepath, "rb") as f:
|
| 228 |
-
file_data = f.read()
|
| 229 |
-
uploaded_file_part = {
|
| 230 |
-
"inline_data": {
|
| 231 |
-
"mime_type": mime_type,
|
| 232 |
-
"data": file_data
|
| 233 |
-
}
|
| 234 |
-
}
|
| 235 |
-
print(f" [/api/chat]: Fichier préparé pour Gemini.")
|
| 236 |
-
except Exception as e:
|
| 237 |
-
print(f"--- ERREUR [/api/chat]: Échec traitement fichier '{filename}': {e}")
|
| 238 |
-
if filepath_to_delete and os.path.exists(filepath_to_delete):
|
| 239 |
-
try:
|
| 240 |
-
os.remove(filepath_to_delete)
|
| 241 |
-
except OSError as del_e:
|
| 242 |
-
print(f" Erreur suppression fichier temp: {del_e}")
|
| 243 |
-
return jsonify({'success': False, 'error': f"Erreur traitement fichier: {e}"}), 500
|
| 244 |
-
else:
|
| 245 |
-
print(f"--- ERREUR [/api/chat]: Type de fichier non autorisé: {file.filename}")
|
| 246 |
-
return jsonify({'success': False, 'error': "Type de fichier non autorisé."}), 400
|
| 247 |
|
| 248 |
raw_user_text = prompt
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
if
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
history_after_user_add = list(session.get('chat_history', []))
|
| 260 |
-
print(f"--- DEBUG [/api/chat]: Historique après ajout: {len(history_after_user_add)} messages")
|
| 261 |
|
| 262 |
selected_model_name = MODEL_PRO if use_advanced else MODEL_FLASH
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
print(f" [/api/chat]: Fichier seul détecté, prompt généré: '{final_prompt_for_gemini}'")
|
| 269 |
-
|
| 270 |
-
# --- Appel séparé pour la recherche web ---
|
| 271 |
-
search_results_text = ""
|
| 272 |
-
if use_web_search and final_prompt_for_gemini:
|
| 273 |
-
print(f"--- LOG [/api/chat]: Exécution de la recherche web pour: '{final_prompt_for_gemini[:60]}...'")
|
| 274 |
-
search_response = perform_web_search(final_prompt_for_gemini, genai_client, selected_model_name)
|
| 275 |
-
search_results_text = format_search_response(search_response)
|
| 276 |
-
print(f" [/api/chat]: Résultat de recherche obtenu (longueur {len(search_results_text)} caractères).")
|
| 277 |
-
# Ajout dans l'historique pour conserver le contexte de la recherche
|
| 278 |
-
search_context_entry = {
|
| 279 |
-
'role': 'assistant',
|
| 280 |
-
'text': f"<i>Résultats de recherche récents :</i><br>{search_results_text}",
|
| 281 |
-
'raw_text': f"Résultats de recherche récents :\n{search_results_text}"
|
| 282 |
-
}
|
| 283 |
-
session['chat_history'].append(search_context_entry)
|
| 284 |
-
print(" [/api/chat]: Résultats de recherche ajoutés dans l'historique.")
|
| 285 |
-
|
| 286 |
-
# --- Préparation de l'historique pour l'appel final ---
|
| 287 |
-
# On prend l'historique complet incluant le résultat de recherche
|
| 288 |
-
gemini_history_to_send = prepare_gemini_history(session.get('chat_history', []))
|
| 289 |
contents = gemini_history_to_send.copy()
|
| 290 |
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
elif isinstance(part, dict) and "inline_data" in part:
|
| 311 |
-
parts_info.append(f"File(mime={part['inline_data']['mime_type']})")
|
| 312 |
-
else:
|
| 313 |
-
parts_info.append(f"Part({type(part)})")
|
| 314 |
-
print(f" Message {i} (role: {role}): {', '.join(parts_info)}")
|
| 315 |
-
|
| 316 |
-
# Configuration finale pour l'appel (sans outil google_search)
|
| 317 |
generate_config = types.GenerateContentConfig(
|
| 318 |
system_instruction=SYSTEM_INSTRUCTION,
|
| 319 |
-
safety_settings=SAFETY_SETTINGS
|
|
|
|
| 320 |
)
|
|
|
|
| 321 |
|
| 322 |
try:
|
| 323 |
-
|
| 324 |
response = genai_client.models.generate_content(
|
| 325 |
model=selected_model_name,
|
| 326 |
contents=contents,
|
| 327 |
config=generate_config
|
| 328 |
)
|
| 329 |
-
|
| 330 |
response_text_raw = ""
|
| 331 |
response_html = ""
|
| 332 |
try:
|
| 333 |
if hasattr(response, 'text'):
|
| 334 |
response_text_raw = response.text
|
| 335 |
-
|
| 336 |
-
elif hasattr(response, '
|
| 337 |
-
|
| 338 |
-
|
|
|
|
| 339 |
else:
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
if feedback:
|
| 343 |
-
block_reason = getattr(feedback, 'block_reason', None)
|
| 344 |
-
if block_reason:
|
| 345 |
-
response_text_raw = f"Désolé, ma réponse a été bloquée ({block_reason})."
|
| 346 |
-
else:
|
| 347 |
-
response_text_raw = "Désolé, je n'ai pas pu générer de réponse (restrictions de sécurité)."
|
| 348 |
-
else:
|
| 349 |
-
response_text_raw = "Désolé, je n'ai pas pu générer de réponse."
|
| 350 |
-
print(f" [/api/chat]: Message d'erreur: '{response_text_raw}'")
|
| 351 |
-
|
| 352 |
response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
|
| 353 |
-
if response_html != response_text_raw:
|
| 354 |
-
print(" [/api/chat]: Réponse convertie en HTML.")
|
| 355 |
except Exception as e_resp:
|
| 356 |
-
|
| 357 |
response_text_raw = f"Désolé, erreur inattendue ({type(e_resp).__name__})."
|
| 358 |
response_html = markdown.markdown(response_text_raw)
|
| 359 |
-
|
| 360 |
-
assistant_history_entry = {
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
session['chat_history'].append(assistant_history_entry)
|
| 369 |
-
history_final_turn = list(session.get('chat_history', []))
|
| 370 |
-
print(f"--- DEBUG [/api/chat]: Historique FINAL: {len(history_final_turn)} messages")
|
| 371 |
-
print("--- LOG [/api/chat]: Envoi de la réponse HTML au client.\n---==================================---\n")
|
| 372 |
return jsonify({'success': True, 'message': response_html})
|
| 373 |
-
|
| 374 |
except Exception as e:
|
| 375 |
-
|
|
|
|
| 376 |
current_history = session.get('chat_history')
|
| 377 |
-
if isinstance(current_history, list) and current_history:
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
print("---==================================---\n")
|
| 385 |
return jsonify({'success': False, 'error': f"Erreur interne: {e}"}), 500
|
| 386 |
-
|
| 387 |
finally:
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
| 394 |
|
| 395 |
@app.route('/clear', methods=['POST'])
|
| 396 |
def clear_chat():
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
| 398 |
session.clear()
|
| 399 |
-
|
| 400 |
-
is_ajax = 'XMLHttpRequest' == request.headers.get('X-Requested-With') or 'application/json' in request.headers.get('Accept', '')
|
| 401 |
-
if is_ajax:
|
| 402 |
-
print(" [/clear]: Réponse JSON (AJAX).")
|
| 403 |
return jsonify({'success': True, 'message': 'Historique effacé.'})
|
| 404 |
else:
|
| 405 |
-
print(" [/clear]: Réponse Flash + Redirect (non-AJAX).")
|
| 406 |
flash("Conversation effacée.", "info")
|
| 407 |
return redirect(url_for('root'))
|
| 408 |
|
| 409 |
if __name__ == '__main__':
|
| 410 |
-
|
| 411 |
port = int(os.environ.get('PORT', 5001))
|
| 412 |
-
app.run(debug=True, host='0.0.0.0', port=port)
|
| 413 |
-
|
| 414 |
-
# --- END OF FILE app.py ---
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
import mimetypes
|
|
|
|
| 9 |
from werkzeug.utils import secure_filename
|
| 10 |
import markdown # Pour convertir la réponse en HTML
|
| 11 |
from flask_session import Session
|
| 12 |
+
import pprint # Pour un affichage plus lisible des structures complexes (optionnel)
|
| 13 |
+
import logging # Import logging
|
| 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 |
+
# --- Configuration Logging ---
|
| 22 |
+
logging.basicConfig(level=logging.DEBUG,
|
| 23 |
+
format='%(asctime)s - %(levelname)s - %(message)s')
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 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'] = 25 * 1024 * 1024
|
| 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 |
# --- Configuration de l'API Gemini ---
|
| 62 |
+
# CHANGED: Mise à jour des noms de modèles pour utiliser les versions avec "thinking" intégré
|
| 63 |
+
MODEL_FLASH = 'gemini-2.5-flash'
|
| 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 |
SAFETY_SETTINGS = [
|
| 68 |
+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold=types.HarmBlockThreshold.BLOCK_NONE),
|
| 69 |
+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
|
| 70 |
+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
|
| 71 |
+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_NONE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
]
|
| 73 |
|
| 74 |
GEMINI_CONFIGURED = False
|
|
|
|
| 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 |
"""Vérifie si l'extension du fichier est autorisée."""
|
| 104 |
+
return '.' in filename and \
|
| 105 |
+
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
+
# REMOVED: La fonction perform_web_search est maintenant obsolète grâce à l'intégration des outils.
|
| 108 |
+
# REMOVED: La fonction format_search_response est également obsolète.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
def prepare_gemini_history(chat_history):
|
| 111 |
+
"""Convertit l'historique stocké en session au format attendu par Gemini API."""
|
| 112 |
+
logger.debug(f"--- DEBUG [prepare_gemini_history]: Entrée avec {len(chat_history)} messages")
|
| 113 |
gemini_history = []
|
| 114 |
+
for message in list(chat_history):
|
| 115 |
role = message.get('role')
|
| 116 |
text_part = message.get('raw_text')
|
|
|
|
| 117 |
if text_part:
|
| 118 |
+
role_gemini = "user" if role == 'user' else "model"
|
| 119 |
+
gemini_history.append({"role": role_gemini, "parts": [{"text": text_part}]})
|
| 120 |
+
logger.debug(f"--- DEBUG [prepare_gemini_history]: Sortie avec {len(gemini_history)} messages")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
return gemini_history
|
| 122 |
|
| 123 |
+
def process_uploaded_file(file):
|
| 124 |
+
"""Traite un fichier uploadé et retourne les infos nécessaires pour Gemini."""
|
| 125 |
+
if not file or file.filename == '':
|
| 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 |
+
logger.debug(f" [/api/chat]: Prompt reçu: '{prompt[:50]}...'")
|
| 200 |
+
logger.debug(f" [/api/chat]: Recherche Web: {use_web_search}, Raisonnement Avancé: {use_advanced}")
|
| 201 |
+
logger.debug(f" [/api/chat]: Nombre de fichiers: {len(files) if files else 0}")
|
| 202 |
+
|
| 203 |
+
if not prompt and not files:
|
| 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 |
+
for file in files:
|
| 218 |
+
if file and file.filename != '':
|
| 219 |
+
file_part, filename, filepath = process_uploaded_file(file)
|
| 220 |
+
if file_part:
|
| 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 |
+
user_history_entry = {'role': 'user', 'text': display_user_text, 'raw_text': raw_user_text}
|
| 230 |
+
session.setdefault('chat_history', []).append(user_history_entry)
|
| 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 |
+
# --- NOUVELLE LOGIQUE UNIFIÉE ---
|
| 240 |
+
# Préparation de l'historique et du message courant
|
| 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 |
+
tools.append({"google_search": {}})
|
| 261 |
+
logger.debug(f" [/api/chat]: Outils activés: {[tool.popitem()[0] for tool in tools]}")
|
| 262 |
+
|
| 263 |
+
# Configuration finale de la requête
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
try:
|
| 272 |
+
logger.debug(f"--- LOG [/api/chat]: Envoi de la requête unifiée à {selected_model_name}...")
|
| 273 |
response = genai_client.models.generate_content(
|
| 274 |
model=selected_model_name,
|
| 275 |
contents=contents,
|
| 276 |
config=generate_config
|
| 277 |
)
|
| 278 |
+
|
| 279 |
response_text_raw = ""
|
| 280 |
response_html = ""
|
| 281 |
try:
|
| 282 |
if hasattr(response, 'text'):
|
| 283 |
response_text_raw = response.text
|
| 284 |
+
logger.debug(f"--- LOG [/api/chat]: Réponse reçue (début): '{response_text_raw[:100]}...'")
|
| 285 |
+
elif hasattr(response, 'prompt_feedback') and response.prompt_feedback:
|
| 286 |
+
feedback = response.prompt_feedback
|
| 287 |
+
block_reason = getattr(feedback, 'block_reason', 'inconnue')
|
| 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 |
response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables', 'nl2br'])
|
|
|
|
|
|
|
| 293 |
except Exception as e_resp:
|
| 294 |
+
logger.error(f"--- ERREUR [/api/chat]: Erreur lors du traitement de la réponse: {e_resp}")
|
| 295 |
response_text_raw = f"Désolé, erreur inattendue ({type(e_resp).__name__})."
|
| 296 |
response_html = markdown.markdown(response_text_raw)
|
| 297 |
+
|
| 298 |
+
assistant_history_entry = {'role': 'assistant', 'text': response_html, 'raw_text': response_text_raw}
|
| 299 |
+
session.setdefault('chat_history', []).append(assistant_history_entry)
|
| 300 |
+
|
| 301 |
+
if TELEGRAM_ENABLED:
|
| 302 |
+
telegram_message = f"🟢 RÉPONSE IA (Session {session_id})\n\n{response_text_raw[:3900]}"
|
| 303 |
+
send_telegram_message(telegram_message)
|
| 304 |
+
|
| 305 |
+
logger.debug("--- LOG [/api/chat]: Envoi de la réponse HTML au client.\n---==================================---\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
return jsonify({'success': True, 'message': response_html})
|
| 307 |
+
|
| 308 |
except Exception as e:
|
| 309 |
+
logger.critical(f"--- ERREUR CRITIQUE [/api/chat]: Échec appel Gemini ou traitement réponse : {e}")
|
| 310 |
+
# Retirer le dernier message utilisateur en cas d'échec
|
| 311 |
current_history = session.get('chat_history')
|
| 312 |
+
if isinstance(current_history, list) and current_history and current_history[-1].get('role') == 'user':
|
| 313 |
+
current_history.pop()
|
| 314 |
+
|
| 315 |
+
if TELEGRAM_ENABLED:
|
| 316 |
+
error_message = f"🔴 ERREUR (Session {session_id})\n\nPrompt: {prompt[:100]}\n\nErreur: {str(e)}"
|
| 317 |
+
send_telegram_message(error_message)
|
| 318 |
+
|
|
|
|
| 319 |
return jsonify({'success': False, 'error': f"Erreur interne: {e}"}), 500
|
| 320 |
+
|
| 321 |
finally:
|
| 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('/clear', methods=['POST'])
|
| 331 |
def clear_chat():
|
| 332 |
+
logger.debug("\n--- DEBUG [/clear]: Requête POST reçue ---")
|
| 333 |
+
if TELEGRAM_ENABLED and 'session_id' in session:
|
| 334 |
+
end_message = f"⚪ SESSION TERMINÉE (Session {session.get('session_id')})"
|
| 335 |
+
send_telegram_message(end_message)
|
| 336 |
session.clear()
|
| 337 |
+
if 'XMLHttpRequest' == request.headers.get('X-Requested-With'):
|
|
|
|
|
|
|
|
|
|
| 338 |
return jsonify({'success': True, 'message': 'Historique effacé.'})
|
| 339 |
else:
|
|
|
|
| 340 |
flash("Conversation effacée.", "info")
|
| 341 |
return redirect(url_for('root'))
|
| 342 |
|
| 343 |
if __name__ == '__main__':
|
| 344 |
+
logger.info("--- Démarrage du serveur Flask ---")
|
| 345 |
port = int(os.environ.get('PORT', 5001))
|
| 346 |
+
app.run(debug=True, host='0.0.0.0', port=port)
|
|
|
|
|
|