import os import json # Importer jsonify pour les réponses API from flask import Flask, render_template, request, session, redirect, url_for, flash, jsonify from dotenv import load_dotenv import google.generativeai as genai import requests from werkzeug.utils import secure_filename import mimetypes import markdown # <-- Importer la bibliothèque Markdown 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"}, # ... (autres catégories) {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"}, ] model = genai.GenerativeModel( 'gemini-1.5-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 = os.getenv("SERPER_API_KEY") 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() 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): # (Fonction inchangée - assurez-vous qu'elle renvoie du texte formaté) if not data: return "Aucun résultat de recherche trouvé." result = "Résultats de recherche web :\n" # ... (reste de la fonction inchangé) ... if 'organic' in data and data['organic']: result += "\n## Résultats principaux :\n" for i, item in enumerate(data['organic'][:3], 1): result += f"{i}. **{item.get('title', 'N/A')}**\n" # Format Markdown result += f" {item.get('snippet', 'N/A')}\n" result += f" [Lien]({item.get('link', '#')})\n\n" # Lien Markdown # ... (reste de la fonction inchangé) ... return result def prepare_gemini_history(chat_history): gemini_history = [] for message in chat_history: role = 'user' if message['role'] == 'user' else 'model' # Utiliser le 'raw_text' pour Gemini, pas le HTML rendu text_part = message.get('raw_text', message.get('text', '')) parts = [text_part] if message.get('gemini_file_ref'): # Utiliser une clé différente pour la référence interne parts.insert(0, message['gemini_file_ref']) 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'] = [] # L'état 'web_search' est maintenant géré côté client initialement, # mais on peut le pré-cocher depuis la session si on veut le persister. web_search_initial_state = session.get('web_search', False) # On ne passe que l'historique nécessaire à l'affichage initial display_history = session['chat_history'] return render_template( 'index.html', chat_history=display_history, web_search_active=web_search_initial_state ) @app.route('/api/chat', methods=['POST']) def chat_api(): """Gère les requêtes de chat AJAX et retourne du JSON.""" if not model: return jsonify({'success': False, 'error': "Le modèle Gemini n'est pas configuré."}), 500 prompt = request.form.get('prompt', '').strip() use_web_search = request.form.get('web_search') == 'true' file = request.files.get('file') uploaded_gemini_file = None # Référence à l'objet fichier uploadé à Gemini uploaded_filename = None # Juste le nom pour référence if not prompt and not file: return jsonify({'success': False, 'error': 'Message ou fichier requis.'}), 400 # Mettre à jour l'état de la recherche web dans la session si on veut le persister session['web_search'] = use_web_search # --- Gestion de l'upload --- user_message_parts_for_gemini = [] raw_user_text = prompt # Texte brut pour l'historique Gemini 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) uploaded_filename = filename # Garder le nom print(f"Fichier sauvegardé : {filepath}") mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream' print(f"Upload vers Gemini (mime: {mime_type})...") gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type) uploaded_gemini_file = gemini_file_obj # Garder l'objet pour l'API user_message_parts_for_gemini.append(uploaded_gemini_file) # Ajouter l'objet fichier pour Gemini print(f"Fichier {filename} uploadé vers Gemini.") # Optionnel: Supprimer le fichier local # os.remove(filepath) except Exception as e: print(f"Erreur upload fichier : {e}") # Ne pas bloquer, mais renvoyer une erreur partielle si nécessaire return jsonify({'success': False, 'error': f"Erreur traitement fichier: {e}"}), 500 else: return jsonify({'success': False, 'error': 'Type de fichier non autorisé.'}), 400 # --- Préparation du message utilisateur et de l'historique --- # Ajouter le texte après le fichier pour Gemini user_message_parts_for_gemini.append(prompt) # Stocker le message utilisateur dans l'historique de session # Stocker le texte brut et la référence au fichier séparément user_history_entry = { 'role': 'user', 'text': f"[Fichier: {uploaded_filename}]\n\n{prompt}" if uploaded_filename else prompt, # Pour affichage 'raw_text': raw_user_text, # Texte brut pour Gemini } if uploaded_gemini_file: # Ne stockez pas l'objet complet dans la session, juste une réf si nécessaire, # ou reconstruisez l'historique sans la référence de fichier si non critique. # Pour simplifier, on ne stocke pas la référence objet dans la session ici. # On pourrait stocker gemini_file_obj.name si on a besoin de le réutiliser plus tard. # user_history_entry['gemini_file_ref_name'] = uploaded_gemini_file.name pass # L'objet est dans user_message_parts_for_gemini pour l'appel API actuel if 'chat_history' not in session: session['chat_history'] = [] session['chat_history'].append(user_history_entry) session.modified = True # --- Web Search --- final_prompt_text = prompt if use_web_search and prompt: # Recherche uniquement si texte ET activé print("Recherche web en cours pour:", prompt) web_results = perform_web_search(prompt) if web_results: formatted_results = format_search_results(web_results) # Mettre à jour le texte à envoyer à Gemini (le fichier est déjà dans les parts) final_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 résultats web.") # Remplacer le texte dans la liste des parts user_message_parts_for_gemini[-1] = final_prompt_text else: print("Pas de résultats web ou erreur.") # --- Appel à Gemini --- try: # Préparer l'historique SANS le dernier message utilisateur (il est dans `contents`) gemini_history = prepare_gemini_history(session['chat_history'][:-1]) print(f"Historique Gemini: {len(gemini_history)} messages.") # Construire le contenu pour generate_content contents = gemini_history + [{'role': 'user', 'parts': user_message_parts_for_gemini}] print("Appel à model.generate_content...") response = model.generate_content(contents) # --- Traitement Réponse --- response_text_raw = response.text # Convertir Markdown en HTML pour un meilleur rendu response_html = markdown.markdown(response_text_raw, extensions=['fenced_code', 'tables']) print(f"Réponse Gemini reçue (premiers 500 chars): {response_text_raw[:500]}") # Ajouter la réponse à l'historique de session session['chat_history'].append({'role': 'assistant', 'text': response_html, 'raw_text': response_text_raw}) session.modified = True return jsonify({'success': True, 'message': response_html}) # Envoyer le HTML au client except Exception as e: print(f"Erreur lors de l'appel à Gemini : {e}") # Retirer le dernier message utilisateur de l'historique en cas d'échec session['chat_history'].pop() session.modified = True return jsonify({'success': False, 'error': f"Erreur communication IA: {e}"}), 500 @app.route('/clear', methods=['POST']) def clear_chat(): """Efface l'historique de la conversation.""" session.pop('chat_history', None) session.pop('web_search', None) # Réinitialiser aussi le toggle web print("Historique de chat effacé.") flash("Conversation effacée.", "info") # Optionnel: message flash return redirect(url_for('index')) # --- Démarrage --- if __name__ == '__main__': app.run(debug=True, host='0.0.0.0', port=5001)