| from flask import Flask, request, render_template, jsonify, Response |
| import json |
| import os |
| from google import genai |
| from google.genai import types |
| import base64 |
| from werkzeug.utils import secure_filename |
| import mimetypes |
| from dotenv import load_dotenv |
| from datetime import datetime |
| import logging |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', |
| handlers=[ |
| logging.StreamHandler(), |
| logging.FileHandler('app.log', mode='a', encoding='utf-8') |
| ] |
| ) |
| logger = logging.getLogger(__name__) |
|
|
| app = Flask(__name__) |
| app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 |
| load_dotenv() |
|
|
| @app.before_request |
| def log_request_info(): |
| logger.info(f'Request: {request.method} {request.url} from {request.remote_addr}') |
|
|
| def load_system_instruction(): |
| """Charge les instructions système depuis le fichier Markdown""" |
| try: |
| with open('instructions/system_instruction.md', 'r', encoding='utf-8') as f: |
| return f.read().strip() |
| except FileNotFoundError: |
| logger.error("Fichier d'instructions système non trouvé.") |
| return "Tu es un assistant intelligent et amical nommé Mariam. Tu assistes les utilisateurs au mieux de tes capacités. Tu as été créé par Aenir." |
| except Exception as e: |
| logger.exception("Erreur lors du chargement des instructions système") |
| return "Tu es un assistant intelligent et amical nommé Mariam. Tu assistes les utilisateurs au mieux de tes capacités. Tu as été créé par Aenir." |
|
|
| |
| API_KEY = os.getenv("GOOGLE_API_KEY") |
| SYSTEM_INSTRUCTION = load_system_instruction() |
|
|
| if not API_KEY: |
| logger.warning("GOOGLE_API_KEY non définie dans les variables d'environnement") |
| logger.warning("L'application démarrera mais les fonctionnalités de chat seront limitées") |
| client = None |
| else: |
| try: |
| client = genai.Client(api_key=API_KEY) |
| logger.info("Client Gemini initialisé avec succès") |
| except Exception as e: |
| logger.exception("Erreur lors de l'initialisation du client Gemini") |
| client = None |
|
|
| |
| MODEL = "gemini-2.5-flash" |
| DEFAULT_CONFIG = { |
| "temperature": 0.7, |
| "max_output_tokens": 8192, |
| "top_p": 0.9, |
| "top_k": 40 |
| } |
|
|
| |
| DEFAULT_TOOLS = [ |
| types.Tool(code_execution=types.ToolCodeExecution()), |
| types.Tool(google_search=types.GoogleSearch()) |
| ] |
|
|
| |
| conversations = {} |
| conversation_metadata = {} |
|
|
| def add_message_to_history(conversation_id, role, content, has_file=False, file_data=None): |
| """Ajoute un message à l'historique de la conversation""" |
| if conversation_id not in conversation_metadata: |
| conversation_metadata[conversation_id] = { |
| 'id': conversation_id, |
| 'created_at': datetime.now().isoformat(), |
| 'last_activity': datetime.now().isoformat(), |
| 'messages': [], |
| 'status': 'active' |
| } |
| |
| message_data = { |
| 'role': role, |
| 'content': content, |
| 'timestamp': datetime.now().isoformat(), |
| 'hasFile': has_file |
| } |
| if file_data: |
| message_data['fileData'] = file_data |
| conversation_metadata[conversation_id]['messages'].append(message_data) |
| conversation_metadata[conversation_id]['last_activity'] = datetime.now().isoformat() |
|
|
| @app.route('/') |
| def index(): |
| return render_template('index.html') |
|
|
| @app.route('/admin1') |
| def admin(): |
| """Page d'administration""" |
| return render_template('admin.html') |
|
|
| @app.route('/admin/conversations') |
| def get_conversations(): |
| """API pour récupérer les conversations pour l'admin""" |
| try: |
| |
| total_conversations = len(conversation_metadata) |
| total_messages = sum(len(conv['messages']) for conv in conversation_metadata.values()) |
| active_conversations = sum(1 for conv in conversation_metadata.values() if conv.get('status') == 'active') |
| conversations_with_files = sum(1 for conv in conversation_metadata.values() |
| if any(msg.get('hasFile') for msg in conv['messages'])) |
| |
| |
| conversations_data = [] |
| for conv_id, conv_data in conversation_metadata.items(): |
| conversations_data.append({ |
| 'id': conv_id, |
| 'createdAt': conv_data.get('created_at'), |
| 'lastActivity': conv_data.get('last_activity'), |
| 'status': conv_data.get('status', 'active'), |
| 'messages': conv_data.get('messages', []) |
| }) |
| |
| |
| conversations_data.sort(key=lambda x: x.get('lastActivity', ''), reverse=True) |
| |
| return jsonify({ |
| 'conversations': conversations_data, |
| 'stats': { |
| 'total': total_conversations, |
| 'totalMessages': total_messages, |
| 'active': active_conversations, |
| 'withFiles': conversations_with_files |
| } |
| }) |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/chat', methods=['POST']) |
| def chat(): |
| try: |
| if not client: |
| return jsonify({'error': 'Client Gemini non initialisé. Vérifiez GOOGLE_API_KEY.'}), 500 |
|
|
| data = request.get_json() |
| message = data.get('message', '') |
| thinking_enabled = data.get('thinking_enabled', True) |
| conversation_id = data.get('conversation_id', 'default') |
|
|
| logger.info(f"Requête chat reçue: message='{message[:50]}...', conversation_id={conversation_id}") |
|
|
| |
| add_message_to_history(conversation_id, 'user', message) |
|
|
| |
| config_dict = DEFAULT_CONFIG.copy() |
| config_dict["system_instruction"] = SYSTEM_INSTRUCTION |
| config_dict["tools"] = DEFAULT_TOOLS |
|
|
| |
| if thinking_enabled: |
| config_dict["thinking_config"] = types.ThinkingConfig( |
| thinking_budget=-1, |
| include_thoughts=True |
| ) |
| generation_config = types.GenerateContentConfig(**config_dict) |
| |
| |
| if conversation_id not in conversations: |
| conversations[conversation_id] = client.chats.create( |
| model=MODEL, |
| config=generation_config |
| ) |
| |
| chat = conversations[conversation_id] |
| |
| |
| def generate(): |
| try: |
| if not client: |
| yield f"data: {json.dumps({'type': 'error', 'content': 'API Gemini non configurée. Définissez GOOGLE_API_KEY.'})}\n\n" |
| return |
|
|
| logger.info(f"Démarrage du streaming pour conversation {conversation_id}") |
| response_stream = chat.send_message_stream( |
| message, |
| config=generation_config |
| ) |
|
|
| full_response = "" |
| thoughts = "" |
| chunk_count = 0 |
|
|
| for chunk in response_stream: |
| chunk_count += 1 |
| logger.debug(f"Chunk {chunk_count} reçu") |
| if chunk.candidates and chunk.candidates[0].content: |
| for part in chunk.candidates[0].content.parts: |
| if part.text: |
| if part.thought and thinking_enabled: |
| thoughts += part.text |
| yield f"data: {json.dumps({'type': 'thought', 'content': part.text})}\n\n" |
| else: |
| full_response += part.text |
| yield f"data: {json.dumps({'type': 'text', 'content': part.text})}\n\n" |
|
|
| logger.info(f"Streaming terminé, réponse complète: {len(full_response)} caractères") |
|
|
| |
| if full_response: |
| add_message_to_history(conversation_id, 'assistant', full_response) |
|
|
| |
| yield f"data: {json.dumps({'type': 'end'})}\n\n" |
|
|
| except Exception as e: |
| logger.exception("Erreur lors du streaming") |
| yield f"data: {json.dumps({'type': 'error', 'content': f'Erreur API: {str(e)}'})}\n\n" |
|
|
| return Response(generate(), mimetype='text/event-stream', headers={ |
| 'Cache-Control': 'no-cache', |
| 'Connection': 'keep-alive', |
| 'Access-Control-Allow-Origin': '*', |
| 'Access-Control-Allow-Headers': 'Content-Type', |
| }) |
| |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/upload', methods=['POST']) |
| def upload_file(): |
| try: |
| if 'file' not in request.files: |
| return jsonify({'error': 'No file uploaded'}), 400 |
|
|
| file = request.files['file'] |
| if file.filename == '': |
| return jsonify({'error': 'No file selected'}), 400 |
|
|
| |
| file_bytes = file.read() |
| mime_type = file.content_type or mimetypes.guess_type(file.filename)[0] |
| logger.info(f"Fichier uploadé: {file.filename}, taille: {len(file_bytes)} bytes, type: {mime_type}") |
|
|
| |
| file_b64 = base64.b64encode(file_bytes).decode() |
|
|
| return jsonify({ |
| 'success': True, |
| 'mime_type': mime_type, |
| 'data': file_b64 |
| }) |
|
|
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/chat_with_file', methods=['POST']) |
| def chat_with_file(): |
| try: |
| if not client: |
| return jsonify({'error': 'Client Gemini non initialisé. Vérifiez GOOGLE_API_KEY.'}), 500 |
|
|
| data = request.get_json() |
| message = data.get('message', '') |
| file_data_list = data.get('file_data', []) |
| thinking_enabled = data.get('thinking_enabled', True) |
| conversation_id = data.get('conversation_id', 'default') |
|
|
| logger.info(f"Requête chat_with_file reçue: message='{message[:50]}...', fichiers={len(file_data_list)}, conversation_id={conversation_id}") |
|
|
| |
| if not isinstance(file_data_list, list): |
| file_data_list = [file_data_list] |
|
|
| |
| display_message = message if message else 'Analyse ces fichiers' |
| if file_data_list: |
| file_count = len(file_data_list) |
| display_message += f" [{file_count} fichier{'s' if file_count > 1 else ''}]" |
| add_message_to_history(conversation_id, 'user', display_message, has_file=len(file_data_list) > 0, file_data=file_data_list) |
|
|
| |
| config_dict = DEFAULT_CONFIG.copy() |
| config_dict["tools"] = DEFAULT_TOOLS |
| config_dict["system_instruction"] = SYSTEM_INSTRUCTION |
|
|
| |
| if thinking_enabled: |
| config_dict["thinking_config"] = types.ThinkingConfig( |
| thinking_budget=-1, |
| include_thoughts=True |
| ) |
| generation_config = types.GenerateContentConfig(**config_dict) |
| |
| |
| if conversation_id not in conversations: |
| conversations[conversation_id] = client.chats.create( |
| model=MODEL, |
| config=generation_config |
| ) |
| |
| chat = conversations[conversation_id] |
| |
| |
| contents = [message] |
|
|
| for file_data in file_data_list: |
| file_bytes = base64.b64decode(file_data['data']) |
| file_part = types.Part.from_bytes( |
| data=file_bytes, |
| mime_type=file_data['mime_type'] |
| ) |
| contents.append(file_part) |
| |
| |
| def generate(): |
| try: |
| if not client: |
| yield f"data: {json.dumps({'type': 'error', 'content': 'API Gemini non configurée. Définissez GOOGLE_API_KEY.'})}\n\n" |
| return |
|
|
| logger.info(f"Démarrage du streaming avec fichiers pour conversation {conversation_id}") |
| response_stream = chat.send_message_stream( |
| contents, |
| config=generation_config |
| ) |
|
|
| full_response = "" |
| thoughts = "" |
| chunk_count = 0 |
|
|
| for chunk in response_stream: |
| chunk_count += 1 |
| logger.debug(f"Chunk {chunk_count} reçu (avec fichiers)") |
| for part in chunk.candidates[0].content.parts: |
| if part.text: |
| if part.thought and thinking_enabled: |
| thoughts += part.text |
| yield f"data: {json.dumps({'type': 'thought', 'content': part.text})}\n\n" |
| else: |
| full_response += part.text |
| yield f"data: {json.dumps({'type': 'text', 'content': part.text})}\n\n" |
|
|
| logger.info(f"Streaming avec fichiers terminé, réponse complète: {len(full_response)} caractères") |
|
|
| |
| if full_response: |
| add_message_to_history(conversation_id, 'assistant', full_response) |
|
|
| |
| yield f"data: {json.dumps({'type': 'end'})}\n\n" |
|
|
| except Exception as e: |
| logger.exception("Erreur lors du streaming avec fichiers") |
| yield f"data: {json.dumps({'type': 'error', 'content': f'Erreur API avec fichiers: {str(e)}'})}\n\n" |
|
|
| return Response(generate(), mimetype='text/event-stream', headers={ |
| 'Cache-Control': 'no-cache', |
| 'Connection': 'keep-alive', |
| 'Access-Control-Allow-Origin': '*', |
| 'Access-Control-Allow-Headers': 'Content-Type', |
| }) |
| |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/reset_conversation', methods=['POST']) |
| def reset_conversation(): |
| try: |
| data = request.get_json() |
| conversation_id = data.get('conversation_id', 'default') |
| |
| if conversation_id in conversations: |
| del conversations[conversation_id] |
|
|
| |
| if conversation_id in conversation_metadata: |
| conversation_metadata[conversation_id]['status'] = 'reset' |
| conversation_metadata[conversation_id]['last_activity'] = datetime.now().isoformat() |
|
|
| logger.info(f"Conversation {conversation_id} réinitialisée") |
| return jsonify({'success': True}) |
| |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/admin/conversations/<conversation_id>', methods=['DELETE']) |
| def delete_conversation(conversation_id): |
| """Supprimer une conversation (pour l'admin)""" |
| try: |
| if conversation_id in conversations: |
| del conversations[conversation_id] |
|
|
| if conversation_id in conversation_metadata: |
| del conversation_metadata[conversation_id] |
|
|
| logger.info(f"Conversation {conversation_id} supprimée") |
| return jsonify({'success': True}) |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/admin/conversations/<conversation_id>/export') |
| def export_conversation(conversation_id): |
| """Exporter une conversation en JSON""" |
| try: |
| if conversation_id not in conversation_metadata: |
| return jsonify({'error': 'Conversation non trouvée'}), 404 |
| |
| conversation_data = conversation_metadata[conversation_id] |
|
|
| logger.info(f"Conversation {conversation_id} exportée") |
| return jsonify({ |
| 'conversation_id': conversation_id, |
| 'export_date': datetime.now().isoformat(), |
| 'data': conversation_data |
| }) |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/admin/stats') |
| def get_admin_stats(): |
| """Statistiques détaillées pour l'admin""" |
| try: |
| |
| total_conversations = len(conversation_metadata) |
| total_messages = sum(len(conv['messages']) for conv in conversation_metadata.values()) |
|
|
| |
| status_stats = {} |
| for conv in conversation_metadata.values(): |
| status = conv.get('status', 'active') |
| status_stats[status] = status_stats.get(status, 0) + 1 |
|
|
| |
| conversations_with_files = sum(1 for conv in conversation_metadata.values() |
| if any(msg.get('hasFile') for msg in conv['messages'])) |
|
|
| |
| from collections import defaultdict |
| daily_activity = defaultdict(int) |
|
|
| for conv in conversation_metadata.values(): |
| for message in conv['messages']: |
| if message.get('timestamp'): |
| try: |
| date = datetime.fromisoformat(message['timestamp']).date() |
| daily_activity[date.isoformat()] += 1 |
| except: |
| continue |
|
|
| return jsonify({ |
| 'total_conversations': total_conversations, |
| 'total_messages': total_messages, |
| 'status_distribution': status_stats, |
| 'conversations_with_files': conversations_with_files, |
| 'daily_activity': dict(daily_activity) |
| }) |
| except Exception as e: |
| return jsonify({'error': str(e)}), 500 |
|
|
| @app.route('/debug/api_test') |
| def debug_api_test(): |
| """Endpoint de debug pour tester la connectivité API""" |
| try: |
| if not client: |
| return jsonify({ |
| 'status': 'error', |
| 'message': 'Client Gemini non initialisé', |
| 'api_key_set': bool(API_KEY) |
| }) |
|
|
| |
| response = client.models.generate_content( |
| model=MODEL, |
| contents="Hello", |
| config=types.GenerateContentConfig( |
| max_output_tokens=10, |
| system_instruction="Réponds brièvement." |
| ) |
| ) |
|
|
| return jsonify({ |
| 'status': 'success', |
| 'message': 'API Gemini fonctionnelle', |
| 'model': MODEL, |
| 'response_length': len(response.text) if response.text else 0, |
| 'sample_response': response.text[:100] if response.text else None |
| }) |
|
|
| except Exception as e: |
| return jsonify({ |
| 'status': 'error', |
| 'message': f'Erreur API: {str(e)}', |
| 'api_key_set': bool(API_KEY) |
| }) |
|
|
| if __name__ == '__main__': |
| app.run(debug=True, host='0.0.0.0', port=7860) |