Spaces:
Running
Running
| # 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 | |
| # Nouveaux imports pour la génération PDF côté serveur | |
| import pdfkit | |
| from jinja2 import Template | |
| import tempfile | |
| import io | |
| # --- 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) | |
| # Configuration wkhtmltopdf (ajustez le chemin selon votre système) | |
| # Sur Linux/Mac: wkhtmltopdf est généralement dans le PATH | |
| # Sur Windows: spécifiez le chemin complet vers wkhtmltopdf.exe | |
| WKHTML_PATH = os.environ.get('WKHTML_PATH', None) # None = utilise le PATH | |
| if WKHTML_PATH: | |
| config = pdfkit.configuration(wkhtmltopdf=WKHTML_PATH) | |
| else: | |
| config = pdfkit.configuration() | |
| # --- 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.") | |
| 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"}, | |
| ] | |
| # --- Template HTML pour le PDF --- | |
| PDF_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Dissertation Philosophique</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Kalam&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| margin: 0; | |
| padding: 0; | |
| font-family: 'Kalam', cursive; | |
| font-size: 20px; | |
| color: #1a2a4c; | |
| background-color: #fdfaf4; | |
| line-height: 2; | |
| } | |
| .dissertation-paper { | |
| background-image: linear-gradient(transparent 97%, #d8e2ee 98%); | |
| background-size: 100% 40px; | |
| border-left: 3px solid #ffaaab; | |
| padding-left: 4em; | |
| padding-top: 30px; | |
| padding-bottom: 40px; | |
| padding-right: 30px; | |
| min-height: 100vh; | |
| -webkit-print-color-adjust: exact; | |
| print-color-adjust: exact; | |
| } | |
| .dissertation-paper h2 { | |
| font-size: 1.5em; | |
| text-align: center; | |
| margin-bottom: 1.5em; | |
| color: #1a2a4c; | |
| } | |
| .dissertation-paper h3 { | |
| font-size: 1.2em; | |
| margin-top: 3em; | |
| margin-bottom: 1.5em; | |
| text-transform: uppercase; | |
| text-decoration: underline; | |
| color: #1a2a4c; | |
| } | |
| .dissertation-paper .development-block { | |
| margin-top: 3em; | |
| } | |
| .dissertation-paper p { | |
| text-align: justify; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| .dissertation-paper .prof { | |
| text-align: center; | |
| font-style: italic; | |
| margin-bottom: 2em; | |
| } | |
| .dissertation-paper .indented { | |
| text-indent: 3em; | |
| } | |
| .dissertation-paper .transition { | |
| margin-top: 2em; | |
| margin-bottom: 2em; | |
| font-style: italic; | |
| color: #4a6a9c; | |
| } | |
| .avoid-page-break { | |
| page-break-inside: avoid; | |
| break-inside: avoid; | |
| } | |
| @page { | |
| margin: 15mm; | |
| size: A4; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="dissertation-paper"> | |
| <h2>Sujet : {{ dissertation.sujet }}</h2> | |
| <p class="prof">Prof : {{ dissertation.prof }}</p> | |
| <h3>Introduction</h3> | |
| <p class="indented">{{ dissertation.introduction }}</p> | |
| {% for partie in dissertation.parties %} | |
| <div class="avoid-page-break"> | |
| <div class="development-block"> | |
| <p class="indented">{{ partie.chapeau }}</p> | |
| {% for arg in partie.arguments %} | |
| <p class="indented">{{ arg.paragraphe_argumentatif }}</p> | |
| {% endfor %} | |
| </div> | |
| {% if partie.transition %} | |
| <p class="indented transition">{{ partie.transition }}</p> | |
| {% endif %} | |
| </div> | |
| {% endfor %} | |
| <h3>Conclusion</h3> | |
| <p class="indented">{{ dissertation.conclusion }}</p> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| # --- 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) --- | |
| 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 = [] | |
| record = { | |
| "timestamp": datetime.now().isoformat(), | |
| "input": { | |
| "question": input_data.get('question', ''), | |
| "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 [] | |
| # --- NOUVELLE fonction pour générer le PDF côté serveur --- | |
| def generate_pdf_from_dissertation(dissertation_data): | |
| """Génère un PDF à partir des données de dissertation.""" | |
| try: | |
| # Créer le template Jinja2 | |
| template = Template(PDF_TEMPLATE) | |
| # Rendre le HTML avec les données | |
| html_content = template.render(dissertation=dissertation_data) | |
| # Options pour wkhtmltopdf | |
| options = { | |
| 'page-size': 'A4', | |
| 'margin-top': '15mm', | |
| 'margin-right': '15mm', | |
| 'margin-bottom': '15mm', | |
| 'margin-left': '15mm', | |
| 'encoding': "UTF-8", | |
| 'no-outline': None, | |
| 'enable-local-file-access': None, | |
| 'print-media-type': None | |
| } | |
| # Générer le PDF en mémoire | |
| pdf_bytes = pdfkit.from_string(html_content, False, options=options, configuration=config) | |
| return pdf_bytes | |
| except Exception as e: | |
| logging.error(f"Erreur lors de la génération PDF: {e}") | |
| raise | |
| # --- Routes (inchangées sauf nouvelle route PDF) --- | |
| def philosophie(): | |
| return render_template("philosophie.html") | |
| def gestion(): | |
| return render_template("gestion.html") | |
| def get_dissertations_data(): | |
| """Récupère toutes les données des dissertations générées.""" | |
| 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 | |
| def delete_dissertation_record(record_id): | |
| """Supprime un enregistrement spécifique.""" | |
| try: | |
| data = load_dissertations_data() | |
| record_index = None | |
| for i, record in enumerate(data): | |
| if record.get('id') == record_id: | |
| record_index = i | |
| break | |
| if record_index is None: | |
| return jsonify({ | |
| "success": False, | |
| "error": "Enregistrement non trouvé" | |
| }), 404 | |
| deleted_record = 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 | |
| def clear_all_dissertations(): | |
| """Vide toutes les données des 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 | |
| def get_philosophy_courses(): | |
| """Récupère la liste de tous les cours de philosophie pour le menu déroulant.""" | |
| 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() | |
| def generate_dissertation_api(): | |
| if not client: | |
| error_msg = "Le service IA n'est pas correctement configuré." | |
| save_dissertation_data(request.json or {}, None, False, error_msg) | |
| return jsonify({"error": error_msg}), 503 | |
| data = request.json | |
| sujet = data.get('question', '').strip() | |
| dissertation_type = data.get('type', 'type1').strip() | |
| course_id = data.get('courseId') | |
| if not sujet: | |
| error_msg = "Le champ 'question' est obligatoire." | |
| save_dissertation_data(data, None, False, error_msg) | |
| return jsonify({"error": error_msg}), 400 | |
| if dissertation_type not in ['type1', 'type2']: | |
| error_msg = "Type de méthodologie invalide." | |
| save_dissertation_data(data, None, False, error_msg) | |
| return jsonify({"error": error_msg}), 400 | |
| # Récupérer le contenu du cours si un ID est fourni | |
| context_str = "" | |
| if course_id: | |
| conn = create_connection() | |
| if not conn: | |
| error_msg = "Connexion à la base de données échouée pour récupérer le contexte." | |
| save_dissertation_data(data, None, False, error_msg) | |
| return jsonify({"error": error_msg}), 503 | |
| try: | |
| with conn.cursor(cursor_factory=RealDictCursor) as cur: | |
| cur.execute("SELECT content FROM cours_philosophie WHERE id = %s", (course_id,)) | |
| 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 {course_id}: {e}") | |
| finally: | |
| if conn: | |
| conn.close() | |
| try: | |
| prompt_filename = f"philo_dissertation_{dissertation_type}.txt" | |
| prompt_template = load_prompt(prompt_filename) | |
| if "Erreur:" in prompt_template: | |
| error_msg = "Configuration du prompt introuvable pour ce type." | |
| logging.error(f"Fichier de prompt non trouvé : {prompt_filename}") | |
| save_dissertation_data(data, None, False, error_msg) | |
| return jsonify({"error": error_msg}), 500 | |
| final_prompt = prompt_template.format(phi_prompt=sujet, context=context_str) | |
| config = types.GenerateContentConfig( | |
| safety_settings=SAFETY_SETTINGS, | |
| response_mime_type="application/json", | |
| response_schema=Dissertation, | |
| ) | |
| response = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=final_prompt, | |
| config=config | |
| ) | |
| if response.parsed: | |
| result = response.parsed.dict() | |
| save_dissertation_data(data, result, True) | |
| return jsonify(result) | |
| else: | |
| error_msg = "Le modèle n'a pas pu générer une structure valide." | |
| logging.error(f"Erreur de parsing de la réponse structurée. Réponse brute : {response.text}") | |
| save_dissertation_data(data, 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, None, False, error_msg) | |
| return jsonify({"error": error_msg}), 500 | |
| # --- NOUVELLE Route pour générer et télécharger le PDF --- | |
| def generate_pdf_api(): | |
| """Génère et retourne un PDF de dissertation côté serveur.""" | |
| try: | |
| data = request.json | |
| if not data: | |
| return jsonify({"error": "Aucune donnée de dissertation fournie."}), 400 | |
| # Vérifier que les données contiennent les champs nécessaires | |
| required_fields = ['sujet', 'prof', 'introduction', 'parties', 'conclusion'] | |
| for field in required_fields: | |
| if field not in data: | |
| return jsonify({"error": f"Champ manquant: {field}"}), 400 | |
| # Générer le PDF | |
| pdf_bytes = generate_pdf_from_dissertation(data) | |
| # Créer un nom de fichier basé sur le sujet (limité et sécurisé) | |
| safe_filename = "".join(c for c in data['sujet'][:50] if c.isalnum() or c in (' ', '-', '_')).strip() | |
| if not safe_filename: | |
| safe_filename = "dissertation" | |
| filename = f"{safe_filename}.pdf" | |
| # Retourner le PDF | |
| return send_file( | |
| io.BytesIO(pdf_bytes), | |
| mimetype='application/pdf', | |
| as_attachment=True, | |
| download_name=filename | |
| ) | |
| except Exception as e: | |
| logging.error(f"Erreur lors de la génération du PDF: {e}") | |
| return jsonify({"error": "Erreur lors de la génération du PDF."}), 500 | |
| if __name__ == '__main__': | |
| app.run(debug=True, port=5001) |