# app.py import os import logging import json from datetime import datetime from flask import Flask, jsonify, render_template, request, send_file from pydantic import BaseModel, Field from typing import List, Optional import psycopg2 from psycopg2.extras import RealDictCursor from google import genai from google.genai import types from utils import load_prompt from weasyprint import HTML import io import mimetypes # --- Configuration de l'application --- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') app = Flask(__name__) app.secret_key = os.environ.get("FLASK_SECRET_KEY", "un-secret-par-defaut") # --- Configuration de la base de données et de l'API --- DATABASE_URL = os.environ.get("DATABASE") GOOGLE_API_KEY = os.environ.get("TOKEN") # Dossier pour stocker les données de gestion DATA_DIR = "data" DISSERTATIONS_FILE = os.path.join(DATA_DIR, "dissertations_log.json") # Créer le dossier data s'il n'existe pas os.makedirs(DATA_DIR, exist_ok=True) # --- Modèles de Données Pydantic (inchangés) --- class Argument(BaseModel): paragraphe_argumentatif: str = Field(description="Un unique paragraphe formant un argument complet. Il doit commencer par un connecteur logique (ex: 'Premièrement,'), suivi de son développement.") class Partie(BaseModel): chapeau: str = Field(description="La phrase d'introduction de la partie.") arguments: list[Argument] = Field(description="La liste des paragraphes argumentatifs qui suivent le chapeau.") transition: Optional[str] = Field(description="Phrase ou court paragraphe de transition.", default=None) class Dissertation(BaseModel): sujet: str = Field(description="Le sujet exact de la dissertation, tel que posé par l'utilisateur (ou le type de texte analysé).") prof: str = Field(description="Le nom du professeur, qui est toujours 'Mariam AI'.", default="Mariam AI") introduction: str = Field(description="L'introduction complète de la dissertation.") parties: List[Partie] conclusion: str = Field(description="La conclusion complète de la dissertation.") # --- Configuration Gemini --- try: if not GOOGLE_API_KEY: logging.warning("La variable d'environnement TOKEN (GOOGLE_API_KEY) n'est pas définie.") client = None else: client = genai.Client(api_key=GOOGLE_API_KEY) except Exception as e: logging.error(f"Erreur lors de l'initialisation du client GenAI: {e}") client = None 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"}, ] # --- Helpers de base de données (inchangés) --- def create_connection(): """Crée et retourne une connexion à la base de données PostgreSQL.""" if not DATABASE_URL: logging.error("La variable d'environnement DATABASE n'est pas configurée.") return None try: return psycopg2.connect(DATABASE_URL) except psycopg2.OperationalError as e: logging.error(f"Impossible de se connecter à la base de données : {e}") return None # --- Helpers pour la gestion des données (inchangés dans leur fonction, ajustés pour le logging) --- def save_dissertation_data(input_data, output_data, success=True, error_message=None): """Sauvegarde les données d'entrée et de sortie dans un fichier JSON.""" try: if os.path.exists(DISSERTATIONS_FILE): with open(DISSERTATIONS_FILE, 'r', encoding='utf-8') as f: data = json.load(f) else: data = [] # S'assurer que 'question' est bien dans input_data question_logged = input_data.get('question', 'N/A (Image upload)') record = { "timestamp": datetime.now().isoformat(), "input": { "question": question_logged, "type": input_data.get('type', ''), "courseId": input_data.get('courseId') }, "output": output_data if success else None, "success": success, "error": error_message, "id": len(data) + 1 } data.append(record) with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) except Exception as e: logging.error(f"Erreur lors de la sauvegarde des données: {e}") def load_dissertations_data(): """Charge toutes les données des dissertations depuis le fichier JSON.""" try: if os.path.exists(DISSERTATIONS_FILE): with open(DISSERTATIONS_FILE, 'r', encoding='utf-8') as f: return json.load(f) return [] except Exception as e: logging.error(f"Erreur lors du chargement des données: {e}") return [] # --- Routes (inchangées) --- @app.route('/') def philosophie(): return render_template("philosophie.html") @app.route('/gestion') def gestion(): return render_template("gestion.html") # --- API de gestion (inchangées) --- @app.route('/api/gestion/dissertations', methods=['GET']) def get_dissertations_data(): try: data = load_dissertations_data() return jsonify({"success": True, "data": data, "total": len(data)}) except Exception as e: logging.error(f"Erreur lors de la récupération des données de gestion: {e}") return jsonify({"success": False, "error": "Erreur lors de la récupération des données"}), 500 @app.route('/api/gestion/dissertations/', methods=['DELETE']) def delete_dissertation_record(record_id): try: data = load_dissertations_data() record_index = next((i for i, record in enumerate(data) if record.get('id') == record_id), None) if record_index is None: return jsonify({"success": False, "error": "Enregistrement non trouvé"}), 404 data.pop(record_index) with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) return jsonify({"success": True, "message": "Enregistrement supprimé avec succès"}) except Exception as e: logging.error(f"Erreur lors de la suppression: {e}") return jsonify({"success": False, "error": "Erreur lors de la suppression"}), 500 @app.route('/api/gestion/dissertations/clear', methods=['DELETE']) def clear_all_dissertations(): try: with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f: json.dump([], f) return jsonify({"success": True, "message": "Toutes les données ont été supprimées"}) except Exception as e: logging.error(f"Erreur lors de la suppression générale: {e}") return jsonify({"success": False, "error": "Erreur lors de la suppression"}), 500 # --- API pour lister les cours (inchangée) --- @app.route('/api/philosophy/courses', methods=['GET']) def get_philosophy_courses(): conn = create_connection() if not conn: return jsonify({"error": "Connexion à la base de données échouée."}), 503 try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute("SELECT id, title FROM cours_philosophie ORDER BY title") courses = cur.fetchall() return jsonify(courses) except Exception as e: logging.error(f"Erreur lors de la récupération des cours : {e}") return jsonify({"error": "Erreur interne du serveur lors de la récupération des cours."}), 500 finally: if conn: conn.close() # --- API pour la génération de dissertation (MISE À JOUR pour le Type 3) --- @app.route('/api/generate_dissertation', methods=['POST']) def generate_dissertation_api(): if not client: error_msg = "Le service IA n'est pas correctement configuré." # Le logging d'erreur ici est plus complexe car on ne sait pas encore si on a du JSON ou des fichiers return jsonify({"error": error_msg}), 503 # Détecter le type de requête et extraire les données is_file_upload = 'image' in request.files data_for_log = {} # Structure pour le log if is_file_upload: dissertation_type = request.form.get('type', 'type3').strip() data_for_log = {'type': dissertation_type, 'courseId': None} # Pas de courseId pour type3 if dissertation_type != 'type3': error_msg = "Le type 3 nécessite un fichier image." save_dissertation_data(data_for_log, None, False, error_msg) return jsonify({"error": error_msg}), 400 else: # JSON (Type 1 ou Type 2) data = request.json if not data: error_msg = "Requête JSON vide ou format incorrect." save_dissertation_data({'type': 'unknown'}, None, False, error_msg) return jsonify({"error": error_msg}), 400 dissertation_type = data.get('type', 'type1').strip() sujet = data.get('question', '').strip() course_id = data.get('courseId') data_for_log = {'type': dissertation_type, 'question': sujet, 'courseId': course_id} if dissertation_type not in ['type1', 'type2']: error_msg = "Type de méthodologie invalide." save_dissertation_data(data_for_log, None, False, error_msg) return jsonify({"error": error_msg}), 400 if not sujet: error_msg = "Le champ 'question' est obligatoire." save_dissertation_data(data_for_log, None, False, error_msg) return jsonify({"error": error_msg}), 400 try: prompt_filename = f"philo_dissertation_{dissertation_type}.txt" prompt_template = load_prompt(prompt_filename) if "Erreur:" in prompt_template: error_msg = f"Configuration du prompt introuvable pour le type '{dissertation_type}'." logging.error(f"Fichier de prompt non trouvé : {prompt_filename}") save_dissertation_data(data_for_log, None, False, error_msg) return jsonify({"error": error_msg}), 500 # --- Préparation du contenu pour Gemini --- contents = [] if dissertation_type == 'type3': image_file = request.files['image'] image_bytes = image_file.read() mime_type = image_file.mimetype or mimetypes.guess_type(image_file.filename)[0] # Mise à jour du log pour le commentaire de texte data_for_log['question'] = f"Image: {image_file.filename}" # Contenu multimodal: le prompt + l'image contents = [ prompt_template, types.Part.from_bytes(data=image_bytes, mime_type=mime_type) ] # Utiliser le modèle multimodal 1.5 model_name = "models/gemini-2.5-flash" else: # Type 1 et 2 (texte uniquement) context_str = "" # Récupération du contexte du cours (uniquement pour type 1 et 2) if data_for_log.get('courseId'): conn = create_connection() if conn: try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute("SELECT content FROM cours_philosophie WHERE id = %s", (data_for_log['courseId'],)) result = cur.fetchone() if result and result.get('content'): context_str = f"\n\n--- EXTRAIT DE COURS À UTILISER COMME CONTEXTE PRINCIPAL ---\n{result['content']}" except Exception as e: logging.error(f"Erreur lors de la récupération du contexte du cours {data_for_log['courseId']}: {e}") finally: conn.close() final_prompt = prompt_template.format(phi_prompt=data_for_log['question'], context=context_str) contents = [final_prompt] model_name = "models/gemini-2.5-flash" # Modèle texte optimisé # --- Appel à l'IA --- config = types.GenerateContentConfig( safety_settings=SAFETY_SETTINGS, response_mime_type="application/json", response_schema=Dissertation, ) response = client.models.generate_content( model=model_name, contents=contents, config=config ) # --- Traitement de la réponse --- if response.text: try: # response.text contient la chaîne JSON result = json.loads(response.text) # Validation Pydantic optionnelle ici, mais on se fie au modèle pour être rigide # S'assurer que le sujet est bien rempli pour le log if dissertation_type == 'type3' and 'sujet' in result: data_for_log['question'] = result['sujet'] save_dissertation_data(data_for_log, result, True) return jsonify(result) except json.JSONDecodeError: error_msg = "Le modèle n'a pas pu générer une structure JSON valide." logging.error(f"Erreur JSON Decode: Réponse brute : {response.text}") save_dissertation_data(data_for_log, None, False, error_msg) return jsonify({"error": error_msg}), 500 else: error_msg = "Le modèle n'a retourné aucune réponse textuelle." save_dissertation_data(data_for_log, None, False, error_msg) return jsonify({"error": error_msg}), 500 except Exception as e: error_msg = f"Une erreur est survenue avec le service IA : {e}" logging.error(f"Erreur de génération Gemini : {e}") save_dissertation_data(data_for_log, None, False, error_msg) return jsonify({"error": error_msg}), 500 # --- ROUTE API POUR LA GÉNÉRATION DE PDF (inchangée) --- @app.route('/api/generate_pdf', methods=['POST']) def generate_pdf_api(): """Génère un PDF à partir des données JSON de la dissertation.""" dissertation_data = request.json if not dissertation_data: return jsonify({"error": "Aucune donnée de dissertation fournie."}), 400 try: # 1. Rendre le template HTML avec les données de la dissertation html_string = render_template('dissertation_pdf.html', dissertation=dissertation_data) # 2. Utiliser WeasyPrint pour convertir le HTML en PDF html = HTML(string=html_string) pdf_bytes = html.write_pdf() # 3. Envoyer le PDF en tant que fichier à télécharger return send_file( io.BytesIO(pdf_bytes), mimetype='application/pdf', as_attachment=True, download_name='dissertation-philosophie.pdf' ) except Exception as e: logging.error(f"Erreur lors de la génération du PDF : {e}") return jsonify({"error": "Une erreur est survenue lors de la création du PDF."}), 500 if __name__ == '__main__': app.run(debug=True, port=5001)