|
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 |
|
import traceback |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
GEMINI_API_KEY = os.getenv("GOOGLE_API_KEY") |
|
SERPER_API_KEY = "9b90a274d9e704ff5b21c0367f9ae1161779b573" |
|
FLASK_SECRET_KEY = "jdjdjdjdj" |
|
|
|
|
|
if not GEMINI_API_KEY: |
|
raise ValueError("ERREUR CRITIQUE: La variable d'environnement GEMINI_API_KEY n'est pas définie. Vérifiez votre fichier .env") |
|
if not FLASK_SECRET_KEY: |
|
raise ValueError("ERREUR CRITIQUE: La variable d'environnement FLASK_SECRET_KEY n'est pas définie. Vérifiez votre fichier .env") |
|
if not SERPER_API_KEY: |
|
print("AVERTISSEMENT: SERPER_API_KEY n'est pas définie dans .env. La fonction de recherche web sera désactivée.") |
|
|
|
|
|
app = Flask(__name__) |
|
app.secret_key = FLASK_SECRET_KEY |
|
app.config['UPLOAD_FOLDER'] = 'temp_uploads' |
|
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 |
|
|
|
|
|
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) |
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
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"}, |
|
] |
|
|
|
|
|
generation_config = { |
|
"temperature": 0.7, |
|
"top_p": 0.95, |
|
"top_k": 64, |
|
"max_output_tokens": 8192, |
|
"response_mime_type": "text/plain", |
|
} |
|
|
|
|
|
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. |
|
""" |
|
|
|
|
|
try: |
|
model = genai.GenerativeModel( |
|
model_name="gemini-1.5-flash-latest", |
|
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}") |
|
|
|
|
|
|
|
chat_sessions = {} |
|
|
|
|
|
ALLOWED_EXTENSIONS = { |
|
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'webp', |
|
'heic', 'heif', |
|
'mp3', 'wav', 'ogg', 'flac', |
|
'mp4', 'mov', 'avi', 'mkv', 'webm' |
|
} |
|
|
|
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 |
|
|
|
|
|
|
|
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}") |
|
|
|
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 |
|
|
|
conn = http.client.HTTPSConnection("google.serper.dev") |
|
|
|
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: |
|
|
|
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}':") |
|
|
|
|
|
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}") |
|
|
|
|
|
if 'organic' in data and data['organic']: |
|
results.append("- Résultats principaux :") |
|
for i, item in enumerate(data['organic'][:3], 1): |
|
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: |
|
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) |
|
|
|
|
|
|
|
|
|
@app.route('/') |
|
def home(): |
|
"""Affiche l'interface de chat principale.""" |
|
|
|
if 'session_id' not in session: |
|
session_id = str(uuid.uuid4()) |
|
session['session_id'] = session_id |
|
session['messages'] = [] |
|
session['uploaded_files_gemini'] = [] |
|
session.modified = True |
|
print(f"Nouvelle session Flask créée: {session_id}") |
|
else: |
|
|
|
if 'messages' not in session: session['messages'] = [] |
|
if 'uploaded_files_gemini' not in session: session['uploaded_files_gemini'] = [] |
|
|
|
|
|
|
|
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.""" |
|
|
|
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) |
|
|
|
|
|
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 |
|
|
|
|
|
chat_gemini = get_chat_session(session_id) |
|
|
|
|
|
prompt_parts = [] |
|
|
|
|
|
|
|
if uploaded_files: |
|
prompt_parts.extend(uploaded_files) |
|
file_names = [f.display_name for f in uploaded_files] |
|
print(f"Ajout au prompt des fichiers Gemini: {', '.join(file_names)}") |
|
|
|
|
|
session['uploaded_files_gemini'] = [] |
|
session.modified = True |
|
print("Liste des fichiers uploadés vidée pour le prochain message.") |
|
|
|
|
|
|
|
search_prompt_text = "" |
|
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.") |
|
|
|
|
|
|
|
final_user_text = user_message_text + search_prompt_text |
|
prompt_parts.append(final_user_text) |
|
print(f"Texte final envoyé à Gemini:\n{final_user_text[:500]}...") |
|
|
|
|
|
print(f"Envoi de {len(prompt_parts)} partie(s) à l'API Gemini...") |
|
ai_response_text = "" |
|
try: |
|
|
|
response = chat_gemini.send_message(prompt_parts) |
|
ai_response_text = response.text |
|
print("Réponse reçue de Gemini.") |
|
|
|
|
|
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() |
|
|
|
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}" |
|
|
|
|
|
|
|
|
|
current_messages = session.get('messages', []) |
|
|
|
current_messages.append({'role': 'user', 'content': user_message_text}) |
|
current_messages.append({'role': 'assistant', 'content': ai_response_text}) |
|
session['messages'] = current_messages |
|
session.modified = True |
|
print("Historique de session Flask mis à jour.") |
|
|
|
|
|
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) |
|
|
|
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.") |
|
|
|
|
|
print(f"Upload de '{filename}' vers Google AI Studio...") |
|
|
|
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}") |
|
|
|
|
|
|
|
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)}).") |
|
|
|
|
|
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}) |
|
|
|
except Exception as e: |
|
print(f"ERREUR lors de l'upload ou du traitement Gemini: {e}") |
|
traceback.print_exc() |
|
|
|
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}) |
|
|
|
session_id = session['session_id'] |
|
print(f"\n--- Requête /clear_chat reçue (Session: {session_id}) ---") |
|
|
|
|
|
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.") |
|
|
|
|
|
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.") |
|
|
|
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
print(f"--- Fin Requête /clear_chat (Session: {session_id}) ---") |
|
return jsonify({'success': True}) |
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
app.run(debug=True, host='0.0.0.0', port=5000) |