Chatm / app.py
Docfile's picture
Update app.py
bf21ae1 verified
raw
history blame
21.8 kB
from flask import Flask, render_template, request, jsonify, session
import google.generativeai as genai
import os
from dotenv import load_dotenv
import http.client
import json
from werkzeug.utils import secure_filename
import uuid
from PIL import Image # Required for checking image files if needed, or directly by genai
import traceback # For detailed error logging
# --- Configuration ---
load_dotenv() # Charge les variables depuis le fichier .env
# Check for essential environment variables
GEMINI_API_KEY = os.getenv("GOOGLE_API_KEY")
SERPER_API_KEY = "9b90a274d9e704ff5b21c0367f9ae1161779b573" # Clé pour la recherche web (optionnelle)
FLASK_SECRET_KEY = "jdjdjdjdj" # Clé secrète pour les sessions Flask
# Validation critique des clés au démarrage
# Configure Flask App
app = Flask(__name__)
app.secret_key = FLASK_SECRET_KEY
app.config['UPLOAD_FOLDER'] = 'temp_uploads' # Dossier pour les uploads temporaires
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # Limite la taille des uploads à 100 Mo
# Assurer l'existence du dossier d'upload
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
# Configure Google AI
try:
genai.configure(api_key=GEMINI_API_KEY)
except Exception as e:
raise RuntimeError(f"Échec de la configuration de l'API Google AI: {e}. Vérifiez votre GEMINI_API_KEY.")
# Paramètres de Sécurité Gemini (Ajustez si nécessaire)
safety_settings = [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
]
# Configuration de la Génération Gemini
generation_config = {
"temperature": 0.7,
"top_p": 0.95,
"top_k": 64,
"max_output_tokens": 8192,
"response_mime_type": "text/plain", # Demande une réponse texte simple
}
# Prompt Système (Instructions pour l'IA)
SYSTEM_PROMPT = """
Vous êtes Mariam, une assistante IA conversationnelle polyvalente conçue par Youssouf.
Votre objectif est d'aider les utilisateurs de manière informative, créative et engageante.
Répondez en français. Soyez concise mais complète.
Si l'utilisateur télécharge des fichiers, prenez-les en compte dans votre réponse si pertinent pour la question posée.
Si la recherche web est activée et que des résultats sont fournis via le prompt, utilisez-les pour enrichir votre réponse en citant brièvement les points clés trouvés.
Formattez vos réponses en Markdown lorsque cela améliore la lisibilité (listes, blocs de code, gras, italique, etc.).
Ne mentionnez pas explicitement les "Résultats de recherche web" dans votre réponse finale à moins que ce ne soit naturel dans le contexte. Intégrez l'information trouvée.
"""
# Initialisation du Modèle Gemini
try:
model = genai.GenerativeModel(
model_name="gemini-1.5-flash-latest", # Utilisation de la dernière version flash
safety_settings=safety_settings,
generation_config=generation_config,
system_instruction=SYSTEM_PROMPT
)
print("Modèle Gemini initialisé avec succès.")
except Exception as e:
raise RuntimeError(f"Échec de l'initialisation du modèle Gemini: {e}")
# Stockage en mémoire des sessions de chat (Pour la production, envisagez une solution persistante comme Redis ou une base de données)
chat_sessions = {}
# Extensions de fichiers autorisées pour l'upload
ALLOWED_EXTENSIONS = {
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp', # Texte et Images
'heic', 'heif', # Formats d'image Apple
'mp3', 'wav', 'ogg', 'flac', # Audio
'mp4', 'mov', 'avi', 'mkv', 'webm' # Vidéo
}
def allowed_file(filename):
"""Vérifie si l'extension du fichier est autorisée."""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
# --- Fonctions Utilitaires ---
def get_chat_session(session_id):
"""Récupère ou crée une session de chat Gemini pour l'ID de session Flask donné."""
if session_id not in chat_sessions:
print(f"Création d'une nouvelle session de chat Gemini pour Flask session ID: {session_id}")
# L'historique est géré dynamiquement lors de l'envoi des messages
chat_sessions[session_id] = model.start_chat(history=[])
return chat_sessions[session_id]
def perform_web_search(query):
"""Effectue une recherche web via l'API Serper.dev."""
if not SERPER_API_KEY:
print("Recherche web ignorée : SERPER_API_KEY non configurée.")
return None # Retourne None si la clé n'est pas dispo
conn = http.client.HTTPSConnection("google.serper.dev")
# Payload pour la recherche en français
payload = json.dumps({"q": query, "gl": "fr", "hl": "fr"})
headers = {
'X-API-KEY': SERPER_API_KEY,
'Content-Type': 'application/json'
}
try:
print(f"Serper: Envoi de la requête pour '{query}'...")
conn.request("POST", "/search", payload, headers)
res = conn.getresponse()
data_bytes = res.read()
data_str = data_bytes.decode("utf-8")
print(f"Serper: Réponse reçue - Statut: {res.status}")
if res.status == 200:
print("Serper: Recherche réussie.")
return json.loads(data_str)
else:
print(f"Serper: Erreur API - Statut {res.status}, Réponse: {data_str}")
return {"error": f"L'API de recherche a échoué avec le statut {res.status}"}
except http.client.HTTPException as http_err:
print(f"Erreur HTTP lors de la recherche web: {http_err}")
return {"error": f"Erreur de connexion HTTP: {str(http_err)}"}
except json.JSONDecodeError as json_err:
print(f"Erreur de décodage JSON de la réponse Serper: {json_err}")
print(f"Réponse brute reçue: {data_str}")
return {"error": "Impossible de lire la réponse de l'API de recherche."}
except Exception as e:
print(f"Erreur inattendue lors de la recherche web: {e}")
traceback.print_exc()
return {"error": f"Exception lors de la recherche web: {str(e)}"}
finally:
conn.close()
def format_search_results_for_prompt(data):
"""Formate les résultats de recherche de manière concise pour les injecter dans le prompt de l'IA."""
if not data or data.get("searchParameters", {}).get("q") is None:
# Si data est vide ou ne semble pas être une réponse valide
print("Formatage recherche: Données invalides ou vides reçues.")
return "La recherche web n'a pas retourné de résultats exploitables."
if "error" in data:
print(f"Formatage recherche: Erreur détectée dans les données - {data['error']}")
return f"Note : La recherche web a rencontré une erreur ({data['error']})."
results = []
query = data.get("searchParameters", {}).get("q", "Terme inconnu")
results.append(f"Résultats de recherche web pour '{query}':")
# Boîte de réponse / Knowledge Graph (si présents)
if 'answerBox' in data:
ab = data['answerBox']
answer = ab.get('answer') or ab.get('snippet') or ab.get('title')
if answer:
results.append(f"- Réponse directe trouvée : {answer}")
elif 'knowledgeGraph' in data:
kg = data['knowledgeGraph']
description = kg.get('description')
if description:
results.append(f"- Info (Knowledge Graph) : {kg.get('title', '')} - {description}")
# Résultats Organiques (naturels)
if 'organic' in data and data['organic']:
results.append("- Résultats principaux :")
for i, item in enumerate(data['organic'][:3], 1): # Limite aux 3 premiers
title = item.get('title', 'Sans titre')
snippet = item.get('snippet', 'Pas de description')
link = item.get('link', '#')
results.append(f" {i}. {title}: {snippet} (Source: {link})")
elif not results: # Si ni answerbox/KG ni organic n'ont donné qqch
return f"Aucun résultat de recherche web pertinent trouvé pour '{query}'."
print(f"Formatage recherche: {len(results)-1} éléments formatés pour le prompt.")
return "\n".join(results)
# --- Routes Flask ---
@app.route('/')
def home():
"""Affiche l'interface de chat principale."""
# Initialise la session Flask si elle n'existe pas
if 'session_id' not in session:
session_id = str(uuid.uuid4())
session['session_id'] = session_id
session['messages'] = [] # Historique pour l'affichage client
session['uploaded_files_gemini'] = [] # Stocke les objets File retournés par genai.upload_file
session.modified = True # Marque la session comme modifiée
print(f"Nouvelle session Flask créée: {session_id}")
else:
# Assure que les clés nécessaires existent même si la session existe déjà
if 'messages' not in session: session['messages'] = []
if 'uploaded_files_gemini' not in session: session['uploaded_files_gemini'] = []
# Récupère les messages de la session pour les passer au template
messages_for_template = session.get('messages', [])
return render_template('index.html', messages=messages_for_template)
@app.route('/send_message', methods=['POST'])
def send_message():
"""Gère l'envoi d'un message à l'IA et retourne sa réponse."""
# Vérifie si la session utilisateur est valide
if 'session_id' not in session:
print("Erreur send_message: ID de session manquant.")
return jsonify({'error': 'Session expirée ou invalide. Veuillez rafraîchir la page.'}), 400
session_id = session['session_id']
print(f"\n--- Requête /send_message reçue (Session: {session_id}) ---")
try:
data = request.json
user_message_text = data.get('message', '').strip()
web_search_enabled = data.get('web_search', False)
# Vérifie si le message est vide (sauf si des fichiers sont attachés)
uploaded_files = session.get('uploaded_files_gemini', [])
if not user_message_text and not uploaded_files:
print("Avertissement send_message: Message vide et aucun fichier attaché.")
return jsonify({'error': 'Veuillez entrer un message ou joindre un fichier.'}), 400
# Récupère ou crée la session de chat Gemini associée
chat_gemini = get_chat_session(session_id)
# --- Préparation du contenu pour Gemini ---
prompt_parts = [] # Liste pour contenir texte, fichiers, résultats web
# 1. Ajouter les fichiers uploadés (depuis la session)
# On utilise directement les objets File retournés par genai.upload_file
if uploaded_files:
prompt_parts.extend(uploaded_files) # Ajoute les objets File à la liste
file_names = [f.display_name for f in uploaded_files] # Noms pour log
print(f"Ajout au prompt des fichiers Gemini: {', '.join(file_names)}")
# Vider la liste des fichiers après les avoir ajoutés au prompt de CE message
# Permet d'associer les fichiers au message en cours uniquement
session['uploaded_files_gemini'] = []
session.modified = True
print("Liste des fichiers uploadés vidée pour le prochain message.")
# 2. Ajouter les résultats de recherche web (si activé et clé API dispo)
search_prompt_text = "" # Texte à ajouter au prompt textuel
if web_search_enabled and SERPER_API_KEY:
print(f"Recherche web activée pour: '{user_message_text}'")
web_results_data = perform_web_search(user_message_text)
if web_results_data:
formatted_results = format_search_results_for_prompt(web_results_data)
search_prompt_text = f"\n\n--- Informations issues de la recherche web ---\n{formatted_results}\n--- Fin des informations de recherche ---\n"
print("Résultats de recherche formatés ajoutés au contexte.")
else:
search_prompt_text = "\n\n(Note: La recherche web a été tentée mais n'a pas retourné de résultats ou a échoué.)\n"
print("Aucun résultat de recherche web ou échec.")
# 3. Ajouter le message texte de l'utilisateur (incluant le contexte de recherche si existant)
final_user_text = user_message_text + search_prompt_text
prompt_parts.append(final_user_text) # Ajoute le texte (potentiellement enrichi)
print(f"Texte final envoyé à Gemini:\n{final_user_text[:500]}...") # Log tronqué
# --- Envoi à l'API Gemini ---
print(f"Envoi de {len(prompt_parts)} partie(s) à l'API Gemini...")
ai_response_text = ""
try:
# Envoie la liste complète des parties (fichiers + texte)
response = chat_gemini.send_message(prompt_parts)
ai_response_text = response.text # Récupère le texte de la réponse
print("Réponse reçue de Gemini.")
# Vérifier si la réponse a été bloquée par les filtres de sécurité
if response.prompt_feedback.block_reason:
block_reason = response.prompt_feedback.block_reason
print(f"AVERTISSEMENT: Réponse bloquée par Gemini. Raison: {block_reason}")
ai_response_text = f"⚠️ Ma réponse a été bloquée en raison des filtres de sécurité (Raison: {block_reason}). Veuillez reformuler votre demande."
except Exception as e:
print(f"ERREUR lors de l'appel à Gemini API: {e}")
traceback.print_exc() # Log détaillé de l'erreur serveur
# Essaye de donner une erreur plus spécifique si possible
error_details = str(e)
ai_response_text = f"❌ Désolé, une erreur est survenue lors de la communication avec l'IA. Détails techniques: {error_details}"
# --- Mise à jour de l'historique de session Flask (pour l'affichage) ---
# Stocke le message *original* de l'utilisateur et la réponse de l'IA
current_messages = session.get('messages', [])
# !! Ne pas stocker le prompt enrichi dans l'historique affiché !!
current_messages.append({'role': 'user', 'content': user_message_text}) # Message original
current_messages.append({'role': 'assistant', 'content': ai_response_text}) # Réponse IA
session['messages'] = current_messages
session.modified = True
print("Historique de session Flask mis à jour.")
# --- Retourner la réponse de l'IA au client ---
print(f"--- Fin Requête /send_message (Session: {session_id}) ---")
return jsonify({'response': ai_response_text})
except Exception as e:
print(f"ERREUR Inattendue dans /send_message: {e}")
traceback.print_exc()
return jsonify({'error': f'Erreur interne du serveur: {str(e)}'}), 500
@app.route('/upload', methods=['POST'])
def upload_file():
"""Gère l'upload de fichiers, les enregistre temporairement et les prépare pour Gemini."""
if 'session_id' not in session:
print("Erreur upload: ID de session manquant.")
return jsonify({'error': 'Session expirée ou invalide. Veuillez rafraîchir la page.'}), 400
session_id = session['session_id']
print(f"\n--- Requête /upload reçue (Session: {session_id}) ---")
if 'file' not in request.files:
print("Erreur upload: 'file' manquant dans request.files.")
return jsonify({'error': 'Aucun fichier trouvé dans la requête.'}), 400
file = request.files['file']
if file.filename == '':
print("Erreur upload: Nom de fichier vide.")
return jsonify({'error': 'Aucun fichier sélectionné.'}), 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
# Crée un chemin de sauvegarde temporaire unique (évite écrasement si même nom)
temp_filename = f"{session_id}_{uuid.uuid4().hex}_{filename}"
save_path = os.path.join(app.config['UPLOAD_FOLDER'], temp_filename)
try:
print(f"Sauvegarde du fichier '{filename}' vers '{save_path}'...")
file.save(save_path)
print(f"Fichier sauvegardé localement.")
# Uploader le fichier vers Google AI Studio (API Gemini)
print(f"Upload de '{filename}' vers Google AI Studio...")
# display_name est optionnel mais utile pour le débogage ou l'affichage
gemini_file_object = genai.upload_file(path=save_path, display_name=filename)
print(f"Fichier uploadé vers Gemini. Référence obtenue: {gemini_file_object.name}") # L'objet a un attribut 'name'
# Stocker l'objet File retourné par Gemini dans la session Flask
# C'est cet objet qui sera utilisé directement dans send_message
current_uploads = session.get('uploaded_files_gemini', [])
current_uploads.append(gemini_file_object)
session['uploaded_files_gemini'] = current_uploads
session.modified = True
print(f"Objet Fichier Gemini ajouté à la session Flask (Total: {len(current_uploads)}).")
# Supprimer le fichier local temporaire après l'upload réussi vers Gemini
try:
os.remove(save_path)
print(f"Fichier local temporaire '{save_path}' supprimé.")
except OSError as e:
print(f"AVERTISSEMENT: Échec de la suppression du fichier local '{save_path}': {e}")
print(f"--- Fin Requête /upload (Session: {session_id}) - Succès ---")
return jsonify({'success': True, 'filename': filename}) # Renvoie le nom original pour l'UI
except Exception as e:
print(f"ERREUR lors de l'upload ou du traitement Gemini: {e}")
traceback.print_exc()
# Nettoyer le fichier local s'il existe encore en cas d'erreur
if os.path.exists(save_path):
try:
os.remove(save_path)
print(f"Nettoyage: Fichier local '{save_path}' supprimé après erreur.")
except OSError as rm_err:
print(f"Erreur lors du nettoyage du fichier '{save_path}': {rm_err}")
print(f"--- Fin Requête /upload (Session: {session_id}) - Échec ---")
return jsonify({'error': f'Échec de l\'upload ou du traitement du fichier: {str(e)}'}), 500
else:
print(f"Erreur upload: Type de fichier non autorisé - '{file.filename}'")
return jsonify({'error': 'Type de fichier non autorisé.'}), 400
@app.route('/clear_chat', methods=['POST'])
def clear_chat():
"""Efface l'historique de chat et les fichiers uploadés pour la session en cours."""
if 'session_id' not in session:
print("Avertissement clear_chat: Pas de session à effacer.")
return jsonify({'success': True}) # Pas d'erreur si pas de session
session_id = session['session_id']
print(f"\n--- Requête /clear_chat reçue (Session: {session_id}) ---")
# 1. Effacer la session de chat Gemini en mémoire (si elle existe)
if session_id in chat_sessions:
del chat_sessions[session_id]
print(f"Session de chat Gemini pour {session_id} effacée de la mémoire.")
# 2. Effacer les données pertinentes de la session Flask
session['messages'] = []
files_to_delete = session.get('uploaded_files_gemini', [])
session['uploaded_files_gemini'] = []
session.modified = True
print("Historique des messages et références de fichiers effacés de la session Flask.")
# 3. (Optionnel mais recommandé) Supprimer les fichiers uploadés correspondants depuis Google AI Studio
# Cela nécessite de stocker les 'name' (IDs) des fichiers Gemini et d'appeler genai.delete_file()
# Pour l'instant, on efface juste la référence dans la session Flask.
# L'API delete_file est importante pour gérer l'espace de stockage Gemini.
if files_to_delete:
print(f"Tentative de suppression de {len(files_to_delete)} fichier(s) sur Google AI Studio...")
for file_obj in files_to_delete:
try:
print(f" Suppression de {file_obj.display_name} (ID: {file_obj.name})...")
genai.delete_file(file_obj.name)
print(f" Fichier {file_obj.name} supprimé de Google AI.")
except Exception as e:
print(f" ERREUR lors de la suppression du fichier {file_obj.name} de Google AI: {e}")
# Continuer même si un fichier ne peut être supprimé
print(f"--- Fin Requête /clear_chat (Session: {session_id}) ---")
return jsonify({'success': True})
# --- Démarrage de l'Application ---
if __name__ == '__main__':
# Rappel des dépendances:
# pip install Flask python-dotenv google-generativeai Pillow werkzeug requests gunicorn
print("\n" + "="*40)
print(" Démarrage du serveur Flask Assistant IA ")
print("="*40)
print("Vérifiez que votre fichier .env contient :")
print(" - GEMINI_API_KEY=VOTRE_CLE_GEMINI")
print(" - FLASK_SECRET_KEY=VOTRE_CLE_SECRET_FLASK")
print(" - SERPER_API_KEY=VOTRE_CLE_SERPER (Optionnel, pour recherche web)")
print("-"*40)
# Utiliser host='0.0.0.0' pour rendre accessible sur le réseau local
# debug=True active le rechargement automatique et le débogueur (NE PAS UTILISER EN PRODUCTION)
# Utiliser Gunicorn ou un autre serveur WSGI pour la production
app.run(debug=True, host='0.0.0.0', port=5000)