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 from werkzeug.utils import secure_filename import mimetypes load_dotenv() app = Flask(__name__) app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-clé-secrète-par-défaut-pour-dev') UPLOAD_FOLDER = 'temp' ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'} app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 os.makedirs(UPLOAD_FOLDER, exist_ok=True) # --- Configuration Gemini (inchangée) --- 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-2.0-flash', 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" ) print("Modèle Gemini chargé.") except Exception as e: print(f"Erreur lors de la configuration de Gemini : {e}") model = None # --- Fonctions Utilitaires (inchangées) --- def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def perform_web_search(query): 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, "gl": "fr", "hl": "fr"}) # Ajout localisation FR try: response = requests.post(search_url, headers=headers, data=payload, timeout=10) response.raise_for_status() 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 JSON de Serper : {e}") print(f"Réponse reçue : {response.text}") return None def format_search_results(data): if not data: return "Aucun résultat de recherche trouvé." result = "Résultats de recherche web pertinents :\n" # (Formatage légèrement simplifié pour clarté) if kg := data.get('knowledgeGraph'): result += f"\n## {kg.get('title', '')} ({kg.get('type', '')})\n{kg.get('description', '')}\n" if ab := data.get('answerBox'): result += f"\n## Réponse rapide :\n{ab.get('title','')}\n{ab.get('snippet') or ab.get('answer','')}\n" if org := data.get('organic'): result += "\n## Principaux résultats :\n" for i, item in enumerate(org[:3], 1): result += f"{i}. {item.get('title', 'N/A')}\n {item.get('snippet', 'N/A')}\n [{item.get('link', '#')}]\n" # People Also Ask peut être bruyant, on peut l'omettre pour le prompt return result def prepare_gemini_history(chat_history): gemini_history = [] for message in chat_history: role = 'user' if message['role'] == 'user' else 'model' # Utilise la référence stockée si elle existe parts = [message.get('gemini_file')] if message.get('gemini_file') else [] parts.append(message['text_for_gemini']) # Utilise le texte destiné à Gemini # Filtrer les parts None qui pourraient survenir si gemini_file était None gemini_history.append({'role': role, 'parts': [p for p in parts if p]}) return gemini_history # --- Routes Flask --- @app.route('/', methods=['GET']) def index(): if 'chat_history' not in session: session['chat_history'] = [] if 'web_search' not in session: session['web_search'] = False # Récupérer l'état de traitement et l'erreur pour les afficher processing = session.get('processing', False) error = session.pop('error', None) # Utilise pop pour ne l'afficher qu'une fois return render_template( 'index.html', chat_history=session.get('chat_history', []), web_search_active=session.get('web_search', False), error=error, processing_message=processing # Passer l'état de traitement ) @app.route('/chat', methods=['POST']) def chat(): if not model: session['error'] = "Le modèle Gemini n'a pas pu être chargé." return redirect(url_for('index')) prompt = request.form.get('prompt', '').strip() session['web_search'] = 'web_search' in request.form file = request.files.get('file') uploaded_gemini_file = None file_display_name = None # Pour l'affichage # Marquer le début du traitement DANS la session session['processing'] = True session['processing_web_search'] = False # Reset au début session.modified = True # Sauvegarder la session maintenant if not prompt and not file: session['error'] = "Veuillez entrer un message ou uploader un fichier." session['processing'] = False # Annuler le traitement return redirect(url_for('index')) # --- Gestion Upload --- if file and file.filename != '': if allowed_file(file.filename): try: filename = secure_filename(file.filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(filepath) print(f"Fichier sauvegardé: {filepath}") mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream' print("Upload vers Gemini...") gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type) uploaded_gemini_file = gemini_file_obj file_display_name = filename # Garder le nom pour affichage print(f"Fichier {filename} uploadé. MimeType: {mime_type}") # Optionnel: Supprimer après upload # os.remove(filepath) except Exception as e: print(f"Erreur upload fichier : {e}") session['error'] = f"Erreur lors du traitement du fichier : {e}" # Ne pas arrêter le traitement, continuer sans le fichier si erreur upload else: session['error'] = "Type de fichier non autorisé." session['processing'] = False # Annuler le traitement return redirect(url_for('index')) # --- Préparation Message Utilisateur --- # Texte à afficher dans le chat display_text = prompt if file_display_name: display_text = f"[Fichier joint : {file_display_name}]\n\n{prompt}" if prompt else f"[Fichier joint : {file_display_name}]" # Texte à envoyer à Gemini (peut être différent si recherche web) text_for_gemini = prompt # Ajouter à l'historique de session (pour affichage) SEULEMENT s'il y a du contenu if display_text: # Garder une trace de l'objet fichier Gemini et du texte original pour l'API session['chat_history'].append({ 'role': 'user', 'text': display_text, # Texte pour affichage HTML 'text_for_gemini': text_for_gemini, # Texte initial pour l'API Gemini 'gemini_file': uploaded_gemini_file # Référence à l'objet fichier si uploadé }) session.modified = True # --- Logique principale (Recherche Web + Gemini) --- try: final_prompt_parts = [] if uploaded_gemini_file: final_prompt_parts.append(uploaded_gemini_file) # Recherche Web si activée ET prompt textuel existe if session['web_search'] and prompt: print("Activation recherche web...") session['processing_web_search'] = True # Indiquer que la recherche est active session.modified = True # !! Important: Forcer la sauvegarde de session AVANT l'appel bloquant # Ceci est un workaround car Flask sauvegarde normalement en fin de requête. # Pour une vraie MAJ live, il faudrait AJAX/WebSockets. from flask.sessions import SessionInterface app.session_interface.save_session(app, session, Response()) # Pseudo-réponse web_results = perform_web_search(prompt) if web_results: formatted_results = format_search_results(web_results) text_for_gemini = f"Question originale: {prompt}\n\n{formatted_results}\n\nRéponds à la question originale en te basant sur ces informations et ta connaissance." print("Prompt enrichi avec recherche web.") else: print("Pas de résultats web ou erreur.") text_for_gemini = prompt # Garde le prompt original session['processing_web_search'] = False # Recherche terminée # Ajouter le texte (original ou enrichi) aux parts if text_for_gemini: # S'assurer qu'on ajoute pas une string vide final_prompt_parts.append(text_for_gemini) # Préparer l'historique pour Gemini en utilisant les données stockées # On prend tout sauf le dernier message utilisateur qui est en cours de traitement gemini_history = prepare_gemini_history(session['chat_history'][:-1]) print(f"\n--- Envoi à Gemini ({len(gemini_history)} hist + {len(final_prompt_parts)} new parts) ---") # Appel API Gemini if not final_prompt_parts: # Cas où seul un fichier a été envoyé sans prompt textuel, # et la recherche web n'était pas activée ou n'a rien retourné. # Il faut quand même envoyer qqchose, par ex., demander de décrire le fichier. if uploaded_gemini_file: final_prompt_parts.append("Décris le contenu de ce fichier.") else: # Ne devrait pas arriver vu les checks précédents, mais par sécurité raise ValueError("Tentative d'envoyer une requête vide à Gemini.") full_conversation = gemini_history + [{'role': 'user', 'parts': final_prompt_parts}] response = model.generate_content(full_conversation) # --- Traitement Réponse --- response_text = response.text print(f"--- Réponse Gemini reçue ---") # Ajouter la réponse à l'historique (version simple pour affichage) session['chat_history'].append({ 'role': 'assistant', 'text': response_text, 'text_for_gemini': response_text # Pour symétrie, même si on ne réutilise pas directement # Pas de 'gemini_file' pour les réponses du modèle }) session.modified = True except Exception as e: print(f"Erreur lors de l'appel à Gemini ou traitement : {e}") session['error'] = f"Une erreur s'est produite : {e}" # En cas d'erreur, retirer le dernier message utilisateur de l'historique # pour éviter boucle d'erreur si le prompt est problématique. if session['chat_history'] and session['chat_history'][-1]['role'] == 'user': session['chat_history'].pop() session.modified = True finally: # Marquer la fin du traitement DANS la session session['processing'] = False session.pop('processing_web_search', None) # Nettoyer au cas où session.modified = True # Pas besoin de Response() ici, la sauvegarde se fera avec le redirect return redirect(url_for('index')) # Ajouter une route pour effacer la conversation @app.route('/clear', methods=['POST']) def clear_chat(): session.pop('chat_history', None) session.pop('web_search', None) session.pop('processing', None) # Nettoyer aussi l'état de process session.pop('error', None) print("Historique de chat effacé.") flash("Conversation effacée.", "info") # Message feedback (optionnel) return redirect(url_for('index')) # Classe Response factice pour sauvegarde session précoce (workaround) # Attention: N'est PAS une vraie réponse HTTP. A utiliser avec prudence. class Response: def __init__(self): self.headers = {} def set_cookie(self, key, value, **kwargs): # Potentiellement stocker les cookies si nécessaire, mais ici on ignore pass if __name__ == '__main__': # Utiliser un port différent si 5000 est pris app.run(debug=True, host='0.0.0.0', port=5002)