| 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" |
|
|
| |
|
|
| |
| 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) |