Docfile commited on
Commit
ab7c7ab
·
verified ·
1 Parent(s): ab2c959

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +432 -182
app.py CHANGED
@@ -1,33 +1,66 @@
1
- # --- START OF FILE app.py ---
2
-
3
  import os
4
  import logging
5
  import json
6
- from flask import (
7
- Flask, make_response, render_template, request, redirect, url_for,
8
- session, jsonify, flash, Response, stream_with_context
9
- )
10
- from datetime import datetime, timedelta
11
  import psycopg2
12
  from psycopg2.extras import RealDictCursor
13
  from google import genai
14
  from google.genai import types
15
-
16
- # Import de la fonction de chargement des prompts depuis notre module utilitaire
17
  from utils import load_prompt
18
 
 
 
 
 
 
 
19
  # --- Configuration de l'application ---
20
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21
-
22
- # Initialisation de Flask
23
  app = Flask(__name__)
24
- app.secret_key = os.environ.get("FLASK_SECRET_KEY", "uyyhhy77uu-default-secret-key")
25
 
26
- # Configuration des variables d'environnement
27
  DATABASE_URL = os.environ.get("DATABASE")
28
  GOOGLE_API_KEY = os.environ.get("TOKEN")
29
 
30
- # Configuration du client Google GenAI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  try:
32
  if not GOOGLE_API_KEY:
33
  logging.warning("La variable d'environnement TOKEN (GOOGLE_API_KEY) n'est pas définie.")
@@ -35,10 +68,9 @@ try:
35
  else:
36
  client = genai.Client(api_key=GOOGLE_API_KEY)
37
  except Exception as e:
38
- logging.error(f"Erreur critique lors de l'initialisation du client GenAI: {e}")
39
  client = None
40
 
41
- # Paramètres de sécurité pour l'API Gemini
42
  SAFETY_SETTINGS = [
43
  {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
44
  {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
@@ -46,208 +78,426 @@ SAFETY_SETTINGS = [
46
  {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
47
  ]
48
 
49
- # --- Helpers de base de données ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  def create_connection():
51
  """Crée et retourne une connexion à la base de données PostgreSQL."""
 
 
 
52
  try:
53
  return psycopg2.connect(DATABASE_URL)
54
  except psycopg2.OperationalError as e:
55
  logging.error(f"Impossible de se connecter à la base de données : {e}")
56
  return None
57
 
58
- # --- Route principale pour l'affichage de la page ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  @app.route('/')
60
  def philosophie():
61
- """Affiche la page principale de l'assistant philosophique."""
62
  return render_template("philosophie.html")
63
 
64
- # --- Routes API pour les données des cours (Non-Streaming) ---
65
- @app.route('/api/philosophy/courses', methods=['GET'])
66
- def get_philosophy_courses():
67
- """Récupère la liste de tous les cours de philosophie."""
 
 
 
68
  try:
69
- with create_connection() as conn:
70
- with conn.cursor(cursor_factory=RealDictCursor) as cur:
71
- cur.execute("SELECT id, title, author, updated_at FROM cours_philosophie ORDER BY title")
72
- courses = cur.fetchall()
73
- return jsonify(courses)
 
74
  except Exception as e:
75
- logging.error(f"Erreur lors de la récupération des cours : {e}")
76
- return jsonify({"error": "Erreur interne du serveur lors de la récupération des cours."}), 500
 
 
 
77
 
78
- @app.route('/api/philosophy/courses/<int:course_id>', methods=['GET'])
79
- def get_philosophy_course(course_id):
80
- """Récupère les détails d'un cours spécifique par son ID."""
81
  try:
82
- with create_connection() as conn:
83
- with conn.cursor(cursor_factory=RealDictCursor) as cur:
84
- cur.execute("SELECT content, author, updated_at FROM cours_philosophie WHERE id = %s", (course_id,))
85
- course = cur.fetchone()
86
- if course:
87
- return jsonify(course)
88
- return jsonify({"error": "Cours non trouvé"}), 404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  except Exception as e:
90
- logging.error(f"Erreur lors de la récupération du cours ID {course_id} : {e}")
91
- return jsonify({"error": "Erreur interne du serveur lors de la récupération du cours."}), 500
92
-
93
- # --- Logique de Génération en Streaming ---
94
- def process_and_stream_with_thinking(model_id, prompt_content):
95
- """
96
- Génère et streame le contenu avec la pensée activée, en envoyant des objets JSON.
97
- Chaque objet JSON est délimité par un retour à la ligne.
98
- """
99
- if not client:
100
- error_data = json.dumps({"type": "error", "content": "Le service IA n'est pas correctement configuré. Veuillez contacter l'administrateur."}) + "\n"
101
- yield error_data
102
- return
103
 
 
 
 
104
  try:
105
- config = types.GenerateContentConfig(
106
- safety_settings=SAFETY_SETTINGS,
107
- thinking_config=types.ThinkingConfig(include_thoughts=True)
108
- )
109
-
110
- stream = client.models.generate_content_stream(
111
- model=model_id, contents=prompt_content, config=config
112
- )
113
 
114
- for chunk in stream:
115
- for part in chunk.candidates[0].content.parts:
116
- if not part.text:
117
- continue
118
-
119
- data_type = "thought" if part.thought else "answer"
120
- data = {"type": data_type, "content": part.text}
121
- yield json.dumps(data, ensure_ascii=False) + "\n"
122
 
 
 
 
 
 
 
 
 
 
 
 
123
  except Exception as e:
124
- logging.error(f"Erreur de streaming Gemini ({model_id}): {e}")
125
- error_data = json.dumps({"type": "error", "content": f"Une erreur est survenue avec le service IA : {e}"}) + "\n"
126
- yield error_data
127
-
128
- # --- Routes de Génération en Streaming ---
129
- @app.route('/stream_philo', methods=['POST'])
130
- @app.route('/stream_philo_deepthink', methods=['POST'])
131
- def stream_philo_text():
132
- """Gère les requêtes de génération de texte en streaming."""
 
 
 
 
133
  data = request.json
134
- phi_prompt = data.get('question', '').strip()
135
- phi_type = data.get('type', '1')
136
  course_id = data.get('courseId')
137
 
138
- if not phi_prompt:
139
- return Response("Erreur: Le champ 'sujet' est obligatoire.", status=400)
140
-
141
- is_deepthink = 'deepthink' in request.path
142
- model_id = "gemini-2.5-pro" if is_deepthink else "gemini-2.5-flash"
143
- prompt_file = {'1': 'philo_type1.txt', '2': 'philo_type2.txt'}.get(phi_type)
144
-
145
- if not prompt_file:
146
- return Response(f"Erreur: Type de sujet '{phi_type}' invalide.", status=400)
147
-
148
- prompt_template = load_prompt(prompt_file)
149
- final_prompt = prompt_template.format(phi_prompt=phi_prompt)
150
 
 
 
151
  if course_id:
 
 
 
 
 
152
  try:
153
- with create_connection() as conn:
154
- with conn.cursor(cursor_factory=RealDictCursor) as cur:
155
- cur.execute("SELECT content FROM cours_philosophie WHERE id = %s", (course_id,))
156
- result = cur.fetchone()
157
- if result and result['content']:
158
- final_prompt += f"\n\n--- EXTRAIT DE COURS POUR CONTEXTE ---\n{result['content']}"
159
  except Exception as e:
160
- logging.error(f"Erreur DB pour le cours {course_id}: {e}")
 
 
 
161
 
162
- return Response(stream_with_context(process_and_stream_with_thinking(model_id, final_prompt)), mimetype='application/x-json-stream; charset=utf-8')
 
 
 
 
 
 
 
 
163
 
164
- @app.route('/stream_philo_image', methods=['POST'])
165
- def stream_philo_image():
166
- """Gère les requêtes d'analyse d'image en streaming."""
167
- if 'image' not in request.files:
168
- return Response("Erreur: Fichier image manquant.", status=400)
169
 
170
- image_file = request.files['image']
171
- if not image_file or not image_file.filename:
172
- return Response("Erreur: Aucun fichier sélectionné.", status=400)
 
 
173
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  try:
175
- img_bytes = image_file.read()
176
- image_part = types.Part.from_bytes(data=img_bytes, mime_type=image_file.mimetype)
177
 
178
- prompt_text = load_prompt('philo_image_analysis.txt')
179
- contents = [prompt_text, image_part]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- # Le modèle "pro" est plus performant pour l'analyse d'image complexe
182
- model_id = "gemini-2.5-pro"
183
-
184
- return Response(stream_with_context(process_and_stream_with_thinking(model_id, contents)), mimetype='application/x-json-stream; charset=utf-8')
185
-
186
  except Exception as e:
187
- logging.error(f"Erreur lors du traitement de l'image : {e}")
188
- return Response("Erreur interne lors de la préparation de l'image.", status=500)
189
-
190
- # --- Routes d'Administration (inchangées) ---
191
- @app.route('/admin/philosophy/courses', methods=['GET', 'POST', 'DELETE'])
192
- def manage_philosophy_courses():
193
- """Gère le CRUD pour les cours de philosophie."""
194
- if request.method == 'GET':
195
- try:
196
- with create_connection() as conn:
197
- with conn.cursor(cursor_factory=RealDictCursor) as cur:
198
- cur.execute("SELECT * FROM cours_philosophie ORDER BY updated_at DESC")
199
- courses = cur.fetchall()
200
- return render_template('philosophy_courses.html', courses=courses)
201
- except Exception as e:
202
- flash(f'Erreur lors de la récupération des cours : {e}', 'danger')
203
- return redirect(url_for('some_admin_dashboard_route')) # Remplacez par une route de fallback
204
-
205
- elif request.method == 'POST':
206
- # La logique de suppression est maintenant dans une route DELETE dédiée pour être plus RESTful
207
- # Mais on garde la logique formulaire pour la simplicité
208
- if 'delete_course_id' in request.form:
209
- try:
210
- course_id = request.form.get('delete_course_id')
211
- with create_connection() as conn:
212
- with conn.cursor() as cur:
213
- cur.execute("DELETE FROM cours_philosophie WHERE id = %s", (course_id,))
214
- conn.commit()
215
- flash('Cours supprimé avec succès !', 'success')
216
- except Exception as e:
217
- flash(f'Erreur lors de la suppression du cours : {e}', 'danger')
218
- else: # Logique d'ajout/modification
219
- try:
220
- title = request.form.get('title')
221
- content = request.form.get('content')
222
- author = request.form.get('author')
223
- with create_connection() as conn:
224
- with conn.cursor() as cur:
225
- cur.execute(
226
- "INSERT INTO cours_philosophie (title, content, author) VALUES (%s, %s, %s)",
227
- (title, content, author)
228
- )
229
- conn.commit()
230
- flash('Cours ajouté avec succès !', 'success')
231
- except Exception as e:
232
- flash(f"Erreur lors de l'ajout du cours : {e}", 'danger')
233
-
234
- return redirect(url_for('manage_philosophy_courses'))
235
-
236
- # Pour la suppression via DELETE http method (par ex: avec du JS)
237
- elif request.method == 'DELETE':
238
- course_id = request.form.get('id')
239
- try:
240
- with create_connection() as conn:
241
- with conn.cursor() as cur:
242
- cur.execute("DELETE FROM cours_philosophie WHERE id = %s", (course_id,))
243
- conn.commit()
244
- return jsonify({'success': True, 'message': 'Cours supprimé'}), 200
245
- except Exception as e:
246
- return jsonify({'success': False, 'message': str(e)}), 500
247
 
248
  if __name__ == '__main__':
249
- # Utiliser debug=True uniquement pour le développement local
250
- # Pour la production, utilisez un serveur WSGI comme Gunicorn ou uWSGI
251
- app.run(debug=True, port=5000)
252
-
253
- # --- END OF FILE app.py ---
 
1
+ # app.py
 
2
  import os
3
  import logging
4
  import json
5
+ from datetime import datetime
6
+ from flask import Flask, jsonify, render_template, request, send_file
7
+ from pydantic import BaseModel, Field
8
+ from typing import List, Optional
 
9
  import psycopg2
10
  from psycopg2.extras import RealDictCursor
11
  from google import genai
12
  from google.genai import types
 
 
13
  from utils import load_prompt
14
 
15
+ # Nouveaux imports pour la génération PDF côté serveur
16
+ import pdfkit
17
+ from jinja2 import Template
18
+ import tempfile
19
+ import io
20
+
21
  # --- Configuration de l'application ---
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
23
  app = Flask(__name__)
24
+ app.secret_key = os.environ.get("FLASK_SECRET_KEY", "un-secret-par-defaut")
25
 
26
+ # --- Configuration de la base de données et de l'API ---
27
  DATABASE_URL = os.environ.get("DATABASE")
28
  GOOGLE_API_KEY = os.environ.get("TOKEN")
29
 
30
+ # Dossier pour stocker les données de gestion
31
+ DATA_DIR = "data"
32
+ DISSERTATIONS_FILE = os.path.join(DATA_DIR, "dissertations_log.json")
33
+
34
+ # Créer le dossier data s'il n'existe pas
35
+ os.makedirs(DATA_DIR, exist_ok=True)
36
+
37
+ # Configuration wkhtmltopdf (ajustez le chemin selon votre système)
38
+ # Sur Linux/Mac: wkhtmltopdf est généralement dans le PATH
39
+ # Sur Windows: spécifiez le chemin complet vers wkhtmltopdf.exe
40
+ WKHTML_PATH = os.environ.get('WKHTML_PATH', None) # None = utilise le PATH
41
+
42
+ if WKHTML_PATH:
43
+ config = pdfkit.configuration(wkhtmltopdf=WKHTML_PATH)
44
+ else:
45
+ config = pdfkit.configuration()
46
+
47
+ # --- Modèles de Données Pydantic (inchangés) ---
48
+ class Argument(BaseModel):
49
+ 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.")
50
+
51
+ class Partie(BaseModel):
52
+ chapeau: str = Field(description="La phrase d'introduction de la partie.")
53
+ arguments: list[Argument] = Field(description="La liste des paragraphes argumentatifs qui suivent le chapeau.")
54
+ transition: Optional[str] = Field(description="Phrase ou court paragraphe de transition.", default=None)
55
+
56
+ class Dissertation(BaseModel):
57
+ sujet: str = Field(description="Le sujet exact de la dissertation, tel que posé par l'utilisateur.")
58
+ prof: str = Field(description="Le nom du professeur, qui est toujours 'Mariam AI'.", default="Mariam AI")
59
+ introduction: str = Field(description="L'introduction complète de la dissertation.")
60
+ parties: List[Partie]
61
+ conclusion: str = Field(description="La conclusion complète de la dissertation.")
62
+
63
+ # --- Configuration Gemini ---
64
  try:
65
  if not GOOGLE_API_KEY:
66
  logging.warning("La variable d'environnement TOKEN (GOOGLE_API_KEY) n'est pas définie.")
 
68
  else:
69
  client = genai.Client(api_key=GOOGLE_API_KEY)
70
  except Exception as e:
71
+ logging.error(f"Erreur lors de l'initialisation du client GenAI: {e}")
72
  client = None
73
 
 
74
  SAFETY_SETTINGS = [
75
  {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
76
  {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
 
78
  {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
79
  ]
80
 
81
+ # --- Template HTML pour le PDF ---
82
+ PDF_TEMPLATE = """
83
+ <!DOCTYPE html>
84
+ <html lang="fr">
85
+ <head>
86
+ <meta charset="UTF-8">
87
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
88
+ <title>Dissertation Philosophique</title>
89
+ <link rel="preconnect" href="https://fonts.googleapis.com">
90
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
91
+ <link href="https://fonts.googleapis.com/css2?family=Kalam&display=swap" rel="stylesheet">
92
+ <style>
93
+ body {
94
+ margin: 0;
95
+ padding: 0;
96
+ font-family: 'Kalam', cursive;
97
+ font-size: 20px;
98
+ color: #1a2a4c;
99
+ background-color: #fdfaf4;
100
+ line-height: 2;
101
+ }
102
+
103
+ .dissertation-paper {
104
+ background-image: linear-gradient(transparent 97%, #d8e2ee 98%);
105
+ background-size: 100% 40px;
106
+ border-left: 3px solid #ffaaab;
107
+ padding-left: 4em;
108
+ padding-top: 30px;
109
+ padding-bottom: 40px;
110
+ padding-right: 30px;
111
+ min-height: 100vh;
112
+ -webkit-print-color-adjust: exact;
113
+ print-color-adjust: exact;
114
+ }
115
+
116
+ .dissertation-paper h2 {
117
+ font-size: 1.5em;
118
+ text-align: center;
119
+ margin-bottom: 1.5em;
120
+ color: #1a2a4c;
121
+ }
122
+
123
+ .dissertation-paper h3 {
124
+ font-size: 1.2em;
125
+ margin-top: 3em;
126
+ margin-bottom: 1.5em;
127
+ text-transform: uppercase;
128
+ text-decoration: underline;
129
+ color: #1a2a4c;
130
+ }
131
+
132
+ .dissertation-paper .development-block {
133
+ margin-top: 3em;
134
+ }
135
+
136
+ .dissertation-paper p {
137
+ text-align: justify;
138
+ margin: 0;
139
+ padding: 0;
140
+ }
141
+
142
+ .dissertation-paper .prof {
143
+ text-align: center;
144
+ font-style: italic;
145
+ margin-bottom: 2em;
146
+ }
147
+
148
+ .dissertation-paper .indented {
149
+ text-indent: 3em;
150
+ }
151
+
152
+ .dissertation-paper .transition {
153
+ margin-top: 2em;
154
+ margin-bottom: 2em;
155
+ font-style: italic;
156
+ color: #4a6a9c;
157
+ }
158
+
159
+ .avoid-page-break {
160
+ page-break-inside: avoid;
161
+ break-inside: avoid;
162
+ }
163
+
164
+ @page {
165
+ margin: 15mm;
166
+ size: A4;
167
+ }
168
+ </style>
169
+ </head>
170
+ <body>
171
+ <div class="dissertation-paper">
172
+ <h2>Sujet : {{ dissertation.sujet }}</h2>
173
+ <p class="prof">Prof : {{ dissertation.prof }}</p>
174
+
175
+ <h3>Introduction</h3>
176
+ <p class="indented">{{ dissertation.introduction }}</p>
177
+
178
+ {% for partie in dissertation.parties %}
179
+ <div class="avoid-page-break">
180
+ <div class="development-block">
181
+ <p class="indented">{{ partie.chapeau }}</p>
182
+ {% for arg in partie.arguments %}
183
+ <p class="indented">{{ arg.paragraphe_argumentatif }}</p>
184
+ {% endfor %}
185
+ </div>
186
+ {% if partie.transition %}
187
+ <p class="indented transition">{{ partie.transition }}</p>
188
+ {% endif %}
189
+ </div>
190
+ {% endfor %}
191
+
192
+ <h3>Conclusion</h3>
193
+ <p class="indented">{{ dissertation.conclusion }}</p>
194
+ </div>
195
+ </body>
196
+ </html>
197
+ """
198
+
199
+ # --- Helpers de base de données (inchangés) ---
200
  def create_connection():
201
  """Crée et retourne une connexion à la base de données PostgreSQL."""
202
+ if not DATABASE_URL:
203
+ logging.error("La variable d'environnement DATABASE n'est pas configurée.")
204
+ return None
205
  try:
206
  return psycopg2.connect(DATABASE_URL)
207
  except psycopg2.OperationalError as e:
208
  logging.error(f"Impossible de se connecter à la base de données : {e}")
209
  return None
210
 
211
+ # --- Helpers pour la gestion des données (inchangés) ---
212
+ def save_dissertation_data(input_data, output_data, success=True, error_message=None):
213
+ """Sauvegarde les données d'entrée et de sortie dans un fichier JSON."""
214
+ try:
215
+ if os.path.exists(DISSERTATIONS_FILE):
216
+ with open(DISSERTATIONS_FILE, 'r', encoding='utf-8') as f:
217
+ data = json.load(f)
218
+ else:
219
+ data = []
220
+
221
+ record = {
222
+ "timestamp": datetime.now().isoformat(),
223
+ "input": {
224
+ "question": input_data.get('question', ''),
225
+ "type": input_data.get('type', ''),
226
+ "courseId": input_data.get('courseId')
227
+ },
228
+ "output": output_data if success else None,
229
+ "success": success,
230
+ "error": error_message,
231
+ "id": len(data) + 1
232
+ }
233
+
234
+ data.append(record)
235
+
236
+ with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f:
237
+ json.dump(data, f, ensure_ascii=False, indent=2)
238
+
239
+ except Exception as e:
240
+ logging.error(f"Erreur lors de la sauvegarde des données: {e}")
241
+
242
+ def load_dissertations_data():
243
+ """Charge toutes les données des dissertations depuis le fichier JSON."""
244
+ try:
245
+ if os.path.exists(DISSERTATIONS_FILE):
246
+ with open(DISSERTATIONS_FILE, 'r', encoding='utf-8') as f:
247
+ return json.load(f)
248
+ return []
249
+ except Exception as e:
250
+ logging.error(f"Erreur lors du chargement des données: {e}")
251
+ return []
252
+
253
+ # --- NOUVELLE fonction pour générer le PDF côté serveur ---
254
+ def generate_pdf_from_dissertation(dissertation_data):
255
+ """Génère un PDF à partir des données de dissertation."""
256
+ try:
257
+ # Créer le template Jinja2
258
+ template = Template(PDF_TEMPLATE)
259
+
260
+ # Rendre le HTML avec les données
261
+ html_content = template.render(dissertation=dissertation_data)
262
+
263
+ # Options pour wkhtmltopdf
264
+ options = {
265
+ 'page-size': 'A4',
266
+ 'margin-top': '15mm',
267
+ 'margin-right': '15mm',
268
+ 'margin-bottom': '15mm',
269
+ 'margin-left': '15mm',
270
+ 'encoding': "UTF-8",
271
+ 'no-outline': None,
272
+ 'enable-local-file-access': None,
273
+ 'print-media-type': None
274
+ }
275
+
276
+ # Générer le PDF en mémoire
277
+ pdf_bytes = pdfkit.from_string(html_content, False, options=options, configuration=config)
278
+
279
+ return pdf_bytes
280
+
281
+ except Exception as e:
282
+ logging.error(f"Erreur lors de la génération PDF: {e}")
283
+ raise
284
+
285
+ # --- Routes (inchangées sauf nouvelle route PDF) ---
286
  @app.route('/')
287
  def philosophie():
 
288
  return render_template("philosophie.html")
289
 
290
+ @app.route('/gestion')
291
+ def gestion():
292
+ return render_template("gestion.html")
293
+
294
+ @app.route('/api/gestion/dissertations', methods=['GET'])
295
+ def get_dissertations_data():
296
+ """Récupère toutes les données des dissertations générées."""
297
  try:
298
+ data = load_dissertations_data()
299
+ return jsonify({
300
+ "success": True,
301
+ "data": data,
302
+ "total": len(data)
303
+ })
304
  except Exception as e:
305
+ logging.error(f"Erreur lors de la récupération des données de gestion: {e}")
306
+ return jsonify({
307
+ "success": False,
308
+ "error": "Erreur lors de la récupération des données"
309
+ }), 500
310
 
311
+ @app.route('/api/gestion/dissertations/<int:record_id>', methods=['DELETE'])
312
+ def delete_dissertation_record(record_id):
313
+ """Supprime un enregistrement spécifique."""
314
  try:
315
+ data = load_dissertations_data()
316
+
317
+ record_index = None
318
+ for i, record in enumerate(data):
319
+ if record.get('id') == record_id:
320
+ record_index = i
321
+ break
322
+
323
+ if record_index is None:
324
+ return jsonify({
325
+ "success": False,
326
+ "error": "Enregistrement non trouvé"
327
+ }), 404
328
+
329
+ deleted_record = data.pop(record_index)
330
+
331
+ with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f:
332
+ json.dump(data, f, ensure_ascii=False, indent=2)
333
+
334
+ return jsonify({
335
+ "success": True,
336
+ "message": "Enregistrement supprimé avec succès"
337
+ })
338
+
339
  except Exception as e:
340
+ logging.error(f"Erreur lors de la suppression: {e}")
341
+ return jsonify({
342
+ "success": False,
343
+ "error": "Erreur lors de la suppression"
344
+ }), 500
 
 
 
 
 
 
 
 
345
 
346
+ @app.route('/api/gestion/dissertations/clear', methods=['DELETE'])
347
+ def clear_all_dissertations():
348
+ """Vide toutes les données des dissertations."""
349
  try:
350
+ with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f:
351
+ json.dump([], f)
352
+
353
+ return jsonify({
354
+ "success": True,
355
+ "message": "Toutes les données ont été supprimées"
356
+ })
 
357
 
358
+ except Exception as e:
359
+ logging.error(f"Erreur lors de la suppression générale: {e}")
360
+ return jsonify({
361
+ "success": False,
362
+ "error": "Erreur lors de la suppression"
363
+ }), 500
 
 
364
 
365
+ @app.route('/api/philosophy/courses', methods=['GET'])
366
+ def get_philosophy_courses():
367
+ """Récupère la liste de tous les cours de philosophie pour le menu déroulant."""
368
+ conn = create_connection()
369
+ if not conn:
370
+ return jsonify({"error": "Connexion à la base de données échouée."}), 503
371
+ try:
372
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
373
+ cur.execute("SELECT id, title FROM cours_philosophie ORDER BY title")
374
+ courses = cur.fetchall()
375
+ return jsonify(courses)
376
  except Exception as e:
377
+ logging.error(f"Erreur lors de la récupération des cours : {e}")
378
+ return jsonify({"error": "Erreur interne du serveur lors de la récupération des cours."}), 500
379
+ finally:
380
+ if conn:
381
+ conn.close()
382
+
383
+ @app.route('/api/generate_dissertation', methods=['POST'])
384
+ def generate_dissertation_api():
385
+ if not client:
386
+ error_msg = "Le service IA n'est pas correctement configuré."
387
+ save_dissertation_data(request.json or {}, None, False, error_msg)
388
+ return jsonify({"error": error_msg}), 503
389
+
390
  data = request.json
391
+ sujet = data.get('question', '').strip()
392
+ dissertation_type = data.get('type', 'type1').strip()
393
  course_id = data.get('courseId')
394
 
395
+ if not sujet:
396
+ error_msg = "Le champ 'question' est obligatoire."
397
+ save_dissertation_data(data, None, False, error_msg)
398
+ return jsonify({"error": error_msg}), 400
399
+
400
+ if dissertation_type not in ['type1', 'type2']:
401
+ error_msg = "Type de méthodologie invalide."
402
+ save_dissertation_data(data, None, False, error_msg)
403
+ return jsonify({"error": error_msg}), 400
 
 
 
404
 
405
+ # Récupérer le contenu du cours si un ID est fourni
406
+ context_str = ""
407
  if course_id:
408
+ conn = create_connection()
409
+ if not conn:
410
+ error_msg = "Connexion à la base de données échouée pour récupérer le contexte."
411
+ save_dissertation_data(data, None, False, error_msg)
412
+ return jsonify({"error": error_msg}), 503
413
  try:
414
+ with conn.cursor(cursor_factory=RealDictCursor) as cur:
415
+ cur.execute("SELECT content FROM cours_philosophie WHERE id = %s", (course_id,))
416
+ result = cur.fetchone()
417
+ if result and result.get('content'):
418
+ context_str = f"\n\n--- EXTRAIT DE COURS À UTILISER COMME CONTEXTE PRINCIPAL ---\n{result['content']}"
 
419
  except Exception as e:
420
+ logging.error(f"Erreur lors de la récupération du contexte du cours {course_id}: {e}")
421
+ finally:
422
+ if conn:
423
+ conn.close()
424
 
425
+ try:
426
+ prompt_filename = f"philo_dissertation_{dissertation_type}.txt"
427
+ prompt_template = load_prompt(prompt_filename)
428
+
429
+ if "Erreur:" in prompt_template:
430
+ error_msg = "Configuration du prompt introuvable pour ce type."
431
+ logging.error(f"Fichier de prompt non trouvé : {prompt_filename}")
432
+ save_dissertation_data(data, None, False, error_msg)
433
+ return jsonify({"error": error_msg}), 500
434
 
435
+ final_prompt = prompt_template.format(phi_prompt=sujet, context=context_str)
 
 
 
 
436
 
437
+ config = types.GenerateContentConfig(
438
+ safety_settings=SAFETY_SETTINGS,
439
+ response_mime_type="application/json",
440
+ response_schema=Dissertation,
441
+ )
442
 
443
+ response = client.models.generate_content(
444
+ model="gemini-2.5-flash",
445
+ contents=final_prompt,
446
+ config=config
447
+ )
448
+
449
+ if response.parsed:
450
+ result = response.parsed.dict()
451
+ save_dissertation_data(data, result, True)
452
+ return jsonify(result)
453
+ else:
454
+ error_msg = "Le modèle n'a pas pu générer une structure valide."
455
+ logging.error(f"Erreur de parsing de la réponse structurée. Réponse brute : {response.text}")
456
+ save_dissertation_data(data, None, False, error_msg)
457
+ return jsonify({"error": error_msg}), 500
458
+
459
+ except Exception as e:
460
+ error_msg = f"Une erreur est survenue avec le service IA : {e}"
461
+ logging.error(f"Erreur de génération Gemini : {e}")
462
+ save_dissertation_data(data, None, False, error_msg)
463
+ return jsonify({"error": error_msg}), 500
464
+
465
+ # --- NOUVELLE Route pour générer et télécharger le PDF ---
466
+ @app.route('/api/generate_pdf', methods=['POST'])
467
+ def generate_pdf_api():
468
+ """Génère et retourne un PDF de dissertation côté serveur."""
469
  try:
470
+ data = request.json
 
471
 
472
+ if not data:
473
+ return jsonify({"error": "Aucune donnée de dissertation fournie."}), 400
474
+
475
+ # Vérifier que les données contiennent les champs nécessaires
476
+ required_fields = ['sujet', 'prof', 'introduction', 'parties', 'conclusion']
477
+ for field in required_fields:
478
+ if field not in data:
479
+ return jsonify({"error": f"Champ manquant: {field}"}), 400
480
+
481
+ # Générer le PDF
482
+ pdf_bytes = generate_pdf_from_dissertation(data)
483
+
484
+ # Créer un nom de fichier basé sur le sujet (limité et sécurisé)
485
+ safe_filename = "".join(c for c in data['sujet'][:50] if c.isalnum() or c in (' ', '-', '_')).strip()
486
+ if not safe_filename:
487
+ safe_filename = "dissertation"
488
+ filename = f"{safe_filename}.pdf"
489
+
490
+ # Retourner le PDF
491
+ return send_file(
492
+ io.BytesIO(pdf_bytes),
493
+ mimetype='application/pdf',
494
+ as_attachment=True,
495
+ download_name=filename
496
+ )
497
 
 
 
 
 
 
498
  except Exception as e:
499
+ logging.error(f"Erreur lors de la génération du PDF: {e}")
500
+ return jsonify({"error": "Erreur lors de la génération du PDF."}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
 
502
  if __name__ == '__main__':
503
+ app.run(debug=True, port=5001)