import os import json from flask import Flask, render_template, request, session, redirect, url_for, flash from dotenv import load_dotenv import google.generativeai as genai import requests # Pour Serper API from werkzeug.utils import secure_filename import mimetypes # Pour vérifier le type de fichier load_dotenv() app = Flask(__name__) # Très important pour utiliser les sessions Flask ! app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-clé-secrète-par-défaut-pour-dev') # Configuration pour les uploads UPLOAD_FOLDER = 'temp' ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'} # Extensions autorisées app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # Limite de taille (ex: 16MB) # Créer le dossier temp s'il n'existe pas os.makedirs(UPLOAD_FOLDER, exist_ok=True) # Configuration de l'API Gemini try: genai.configure(api_key=os.getenv("GOOGLE_API_KEY")) safety_settings = [ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"}, ] model = genai.GenerativeModel( 'gemini-1.5-flash', # Utiliser un modèle stable recommandé si 'gemini-2.0-flash-exp' cause problème # 'gemini-1.5-pro-latest', # Alternative plus puissante safety_settings=safety_settings, system_instruction="Tu es un assistant intelligent. ton but est d'assister au mieux que tu peux. tu as été créé par Aenir et tu t'appelles Mariam" # Note: 'tools' n'est pas directement utilisé ici comme dans Streamlit, # la logique de recherche web est gérée manuellement. ) print("Modèle Gemini chargé.") except Exception as e: print(f"Erreur lors de la configuration de Gemini : {e}") model = None # Empêche l'app de crasher si l'API key est mauvaise # --- Fonctions Utilitaires --- def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def perform_web_search(query): """Effectue une recherche web via l'API Serper.""" conn_key = "9b90a274d9e704ff5b21c0367f9ae1161779b573" if not conn_key: print("Clé API SERPER manquante dans .env") return None search_url = "https://google.serper.dev/search" headers = { 'X-API-KEY': conn_key, 'Content-Type': 'application/json' } payload = json.dumps({"q": query}) try: response = requests.post(search_url, headers=headers, data=payload, timeout=10) response.raise_for_status() # Lève une exception pour les codes d'erreur HTTP data = response.json() print("Résultats de recherche obtenus.") return data except requests.exceptions.RequestException as e: print(f"Erreur lors de la recherche web : {e}") return None except json.JSONDecodeError as e: print(f"Erreur lors du décodage de la réponse JSON de Serper : {e}") print(f"Réponse reçue : {response.text}") return None def format_search_results(data): """Met en forme les résultats de recherche pour le prompt Gemini.""" if not data: return "Aucun résultat de recherche trouvé." result = "Résultats de recherche web :\n" if 'knowledgeGraph' in data: kg = data['knowledgeGraph'] result += f"\n## Graphe de connaissances :\n" result += f"### {kg.get('title', '')} ({kg.get('type', '')})\n" result += f"{kg.get('description', '')}\n" if 'attributes' in kg: for attr, value in kg['attributes'].items(): result += f"- {attr}: {value}\n" if 'answerBox' in data: ab = data['answerBox'] result += f"\n## Réponse directe :\n" result += f"{ab.get('title','')}\n{ab.get('snippet') or ab.get('answer','')}\n" if 'organic' in data and data['organic']: result += "\n## Résultats principaux :\n" for i, item in enumerate(data['organic'][:3], 1): # Limite aux 3 premiers result += f"{i}. **{item.get('title', 'N/A')}**\n" result += f" {item.get('snippet', 'N/A')}\n" result += f" Lien : {item.get('link', '#')}\n\n" if 'peopleAlsoAsk' in data and data['peopleAlsoAsk']: result += "## Questions fréquentes :\n" for i, item in enumerate(data['peopleAlsoAsk'][:2], 1): # Limite aux 2 premières result += f"{i}. **{item.get('question', 'N/A')}**\n" # result += f" {item.get('snippet', 'N/A')}\n\n" # Le snippet est souvent redondant return result def prepare_gemini_history(chat_history): """Convertit l'historique stocké en session au format attendu par Gemini API.""" gemini_history = [] for message in chat_history: role = 'user' if message['role'] == 'user' else 'model' parts = [message['text']] if message.get('gemini_file'): # Ajoute la référence au fichier si présente parts.insert(0, message['gemini_file']) # Met le fichier avant le texte gemini_history.append({'role': role, 'parts': parts}) return gemini_history # --- Routes Flask --- @app.route('/', methods=['GET']) def index(): """Affiche la page principale du chat.""" if 'chat_history' not in session: session['chat_history'] = [] # Initialise l'historique if 'web_search' not in session: session['web_search'] = False # Initialise l'état de la recherche web return render_template( 'index.html', chat_history=session['chat_history'], web_search_active=session['web_search'], error=session.pop('error', None), # Récupère et supprime l'erreur s'il y en a une processing_message=session.pop('processing', False) # Indicateur de traitement ) @app.route('/chat', methods=['POST']) def chat(): """Gère la soumission du formulaire de chat.""" if not model: session['error'] = "Le modèle Gemini n'a pas pu être chargé. Vérifiez la clé API et la configuration." return redirect(url_for('index')) prompt = request.form.get('prompt', '').strip() # Met à jour l'état de la recherche web depuis la checkbox session['web_search'] = 'web_search' in request.form file = request.files.get('file') uploaded_gemini_file = None user_message_content = {'role': 'user', 'text': prompt} if not prompt and not file: session['error'] = "Veuillez entrer un message ou uploader un fichier." return redirect(url_for('index')) # --- Gestion de l'upload de fichier --- if file and file.filename != '': if allowed_file(file.filename): try: # Sécurise le nom du fichier et détermine le chemin complet filename = secure_filename(file.filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) print(f"Fichier sauvegardé temporairement : {filepath}") # Essayer d'uploader vers Gemini print("Upload vers Gemini en cours...") # Détecter le mime type pour l'upload Gemini mime_type = mimetypes.guess_type(filepath)[0] if not mime_type: # Fallback si la détection échoue mime_type = 'application/octet-stream' print(f"Impossible de deviner le mime_type pour {filename}, utilisation de {mime_type}") gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type) uploaded_gemini_file = gemini_file_obj # Garder l'objet fichier pour l'API user_message_content['gemini_file'] = uploaded_gemini_file # Stocke la référence pour l'historique Gemini user_message_content['text'] = f"[Fichier: {filename}]\n\n{prompt}" # Modifie le texte affiché print(f"Fichier {filename} uploadé avec succès vers Gemini. MimeType: {mime_type}") # Optionnel: Supprimer le fichier local après upload vers Gemini # try: # os.remove(filepath) # print(f"Fichier temporaire {filepath} supprimé.") # except OSError as e: # print(f"Erreur lors de la suppression du fichier temporaire {filepath}: {e}") except Exception as e: print(f"Erreur lors du traitement ou de l'upload du fichier : {e}") session['error'] = f"Erreur lors du traitement du fichier : {e}" # Ne pas bloquer si l'upload échoue, on peut continuer sans le fichier # return redirect(url_for('index')) # Décommentez si l'upload doit être bloquant else: session['error'] = "Type de fichier non autorisé." return redirect(url_for('index')) elif file and file.filename == '': # Si un champ fichier existe mais est vide, on l'ignore simplement pass # --- Ajout du message utilisateur à l'historique de session --- if prompt or uploaded_gemini_file: # N'ajoute que s'il y a du contenu # Version simplifiée pour l'affichage HTML (sans l'objet gemini_file) display_history_message = {'role': 'user', 'text': user_message_content['text']} session['chat_history'].append(display_history_message) session.modified = True # Important car on modifie une liste dans la session # --- Préparation et Envoi à Gemini --- try: # Indiquer qu'un traitement est en cours session['processing'] = True session.modified = True # Construire le prompt final (avec recherche web si activée) final_prompt_parts = [] if uploaded_gemini_file: final_prompt_parts.append(uploaded_gemini_file) current_prompt_text = prompt # Effectuer la recherche web si activée ET si un prompt textuel existe if session['web_search'] and prompt: print("Recherche web activée pour le prompt:", prompt) # Afficher l'indicateur de recherche # Note: dans Flask simple, l'indicateur ne sera visible qu'APRES le rechargement. # Pour un affichage dynamique, il faudrait JS/AJAX. # On peut passer une variable au template pour l'afficher avant la réponse. session['processing_web_search'] = True # Indicateur spécifique session.modified = True web_results = perform_web_search(prompt) if web_results: formatted_results = format_search_results(web_results) current_prompt_text = f"Question originale: {prompt}\n\n{formatted_results}\n\nBasé sur ces informations et ta connaissance générale, réponds à la question originale." print("Prompt modifié avec les résultats de recherche.") else: print("Aucun résultat de recherche web obtenu ou erreur.") # On continue avec le prompt original final_prompt_parts.append(current_prompt_text) # Préparer l'historique complet pour Gemini gemini_history = prepare_gemini_history(session['chat_history'][:-1]) # Exclut le dernier message utilisateur (qui est dans final_prompt_parts) print(f"\n--- Envoi à Gemini ---") print(f"Historique envoyé: {len(gemini_history)} messages") print(f"Parties du prompt actuel: {len(final_prompt_parts)}") # print(f"Prompt textuel final: {current_prompt_text[:200]}...") # Afficher début du prompt # print("----------------------\n") # Appel à Gemini API # Utiliser generate_content qui prend l'historique + le nouveau message full_conversation = gemini_history + [{'role': 'user', 'parts': final_prompt_parts}] response = model.generate_content(full_conversation) # --- Traitement de la réponse --- response_text = response.text print(f"\n--- Réponse de Gemini ---") print(response_text[:500] + ('...' if len(response_text) > 500 else '')) # print("-------------------------\n") # Ajout de la réponse à l'historique de session session['chat_history'].append({'role': 'assistant', 'text': response_text}) session.modified = True except Exception as e: print(f"Erreur lors de l'appel à Gemini : {e}") session['error'] = f"Une erreur s'est produite lors de la communication avec l'IA : {e}" # Si une erreur survient, on retire le message utilisateur qui a causé l'erreur # pour éviter de le renvoyer indéfiniment. if session['chat_history'] and session['chat_history'][-1]['role'] == 'user': session['chat_history'].pop() session.modified = True finally: # Indiquer que le traitement est terminé session['processing'] = False session.pop('processing_web_search', None) # Nettoyer l'indicateur de recherche session.modified = True return redirect(url_for('index')) # Redirige vers la page principale pour afficher le nouvel état @app.route('/clear', methods=['POST']) def clear_chat(): """Efface l'historique de la conversation.""" session.pop('chat_history', None) # Supprime l'historique de la session session.pop('web_search', None) # Réinitialise aussi le toggle web print("Historique de chat effacé.") return redirect(url_for('index')) # Redirige vers la page d'accueil # --- Démarrage de l'application --- if __name__ == '__main__': # Utiliser host='0.0.0.0' pour rendre l'app accessible sur le réseau local # Debug=True est utile pour le développement, mais à désactiver en production app.run(debug=True, host='0.0.0.0', port=5001)