Flasksite / app.py
Docfile's picture
Update app.py
ab7c7ab verified
raw
history blame
18.2 kB
# 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) ---
@app.route('/')
def philosophie():
return render_template("philosophie.html")
@app.route('/gestion')
def gestion():
return render_template("gestion.html")
@app.route('/api/gestion/dissertations', methods=['GET'])
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
@app.route('/api/gestion/dissertations/<int:record_id>', methods=['DELETE'])
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
@app.route('/api/gestion/dissertations/clear', methods=['DELETE'])
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
@app.route('/api/philosophy/courses', methods=['GET'])
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()
@app.route('/api/generate_dissertation', methods=['POST'])
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 ---
@app.route('/api/generate_pdf', methods=['POST'])
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)