Docfile commited on
Commit
5524fc2
·
verified ·
1 Parent(s): fae6b38

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +122 -86
app.py CHANGED
@@ -2,33 +2,43 @@
2
 
3
  import os
4
  import logging
5
- from flask import Flask, make_response, render_template, request, redirect, url_for, session, jsonify, flash, Response, stream_with_context
 
 
 
 
6
  from datetime import datetime, timedelta
7
  import psycopg2
8
  from psycopg2.extras import RealDictCursor
9
  from google import genai
10
  from google.genai import types
11
- from utils import load_prompt # Assurez-vous que utils.py et la fonction load_prompt existent
12
 
13
- # --- Configuration ---
14
- logging.basicConfig(level=logging.INFO)
 
 
 
15
 
16
  # Initialisation de Flask
17
  app = Flask(__name__)
18
- app.secret_key = os.environ.get("FLASK_SECRET_KEY", "uyyhhy77uu")
19
- app.permanent_session_lifetime = timedelta(days=200)
20
 
21
- # URLs et Clés API
22
  DATABASE_URL = os.environ.get("DATABASE")
23
  GOOGLE_API_KEY = os.environ.get("TOKEN")
24
 
25
- # Configuration du client Gemini
26
  try:
27
- client = genai.Client(api_key=GOOGLE_API_KEY)
 
 
 
 
28
  except Exception as e:
29
- logging.error(f"Erreur lors de l'initialisation du client GenAI: {e}")
30
  client = None
31
 
 
32
  SAFETY_SETTINGS = [
33
  {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
34
  {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
@@ -36,18 +46,25 @@ SAFETY_SETTINGS = [
36
  {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
37
  ]
38
 
39
- # --- Helpers ---
40
  def create_connection():
41
- return psycopg2.connect(DATABASE_URL)
 
 
 
 
 
42
 
43
- # --- Routes Principales ---
44
  @app.route('/')
45
  def philosophie():
 
46
  return render_template("philosophie.html")
47
 
48
- # --- Routes API (Non-Streaming) ---
49
  @app.route('/api/philosophy/courses', methods=['GET'])
50
  def get_philosophy_courses():
 
51
  try:
52
  with create_connection() as conn:
53
  with conn.cursor(cursor_factory=RealDictCursor) as cur:
@@ -56,68 +73,81 @@ def get_philosophy_courses():
56
  return jsonify(courses)
57
  except Exception as e:
58
  logging.error(f"Erreur lors de la récupération des cours : {e}")
59
- return jsonify({"error": "Erreur interne du serveur."}), 500
60
 
61
  @app.route('/api/philosophy/courses/<int:course_id>', methods=['GET'])
62
  def get_philosophy_course(course_id):
 
63
  try:
64
  with create_connection() as conn:
65
  with conn.cursor(cursor_factory=RealDictCursor) as cur:
66
- cur.execute("SELECT id, title, content, author, created_at, updated_at FROM cours_philosophie WHERE id = %s", (course_id,))
67
  course = cur.fetchone()
68
  if course:
69
  return jsonify(course)
70
  return jsonify({"error": "Cours non trouvé"}), 404
71
  except Exception as e:
72
- logging.error(f"Erreur lors de la récupération du cours : {e}")
73
- return jsonify({"error": "Erreur interne du serveur."}), 500
74
-
75
- # --- Routes de Génération (Streaming) ---
76
-
77
- def process_and_stream(model_id, prompt_content):
78
- """Génère et stream le contenu depuis le modèle Gemini."""
 
 
79
  if not client:
80
- yield "Erreur: Le client API n'est pas configuré."
 
81
  return
82
 
83
  try:
 
 
 
 
 
84
  stream = client.models.generate_content_stream(
85
- model=model_id,
86
- contents=prompt_content,
87
- config=types.GenerateContentConfig(safety_settings=SAFETY_SETTINGS)
88
  )
 
89
  for chunk in stream:
90
- if chunk.text:
91
- yield chunk.text
 
 
 
 
 
 
92
  except Exception as e:
93
  logging.error(f"Erreur de streaming Gemini ({model_id}): {e}")
94
- yield f"\n\n**Erreur de génération :** {str(e)}"
 
95
 
 
96
  @app.route('/stream_philo', methods=['POST'])
97
  @app.route('/stream_philo_deepthink', methods=['POST'])
98
  def stream_philo_text():
 
99
  data = request.json
100
  phi_prompt = data.get('question', '').strip()
101
  phi_type = data.get('type', '1')
102
  course_id = data.get('courseId')
103
 
104
  if not phi_prompt:
105
- return Response("Erreur: Veuillez saisir un sujet.", status=400, mimetype='text/plain')
106
 
107
- # Déterminer le modèle et le fichier de prompt
108
  is_deepthink = 'deepthink' in request.path
109
  model_id = "gemini-2.5-pro" if is_deepthink else "gemini-2.5-flash"
110
- prompt_files = {'1': 'philo_type1.txt', '2': 'philo_type2.txt'}
111
- prompt_file = prompt_files.get(phi_type)
112
-
113
  if not prompt_file:
114
- return Response(f"Erreur: Type de sujet '{phi_type}' non valide.", status=400, mimetype='text/plain')
115
 
116
- # Charger et formater le prompt
117
  prompt_template = load_prompt(prompt_file)
118
  final_prompt = prompt_template.format(phi_prompt=phi_prompt)
119
 
120
- # Ajouter le contenu du cours si disponible
121
  if course_id:
122
  try:
123
  with create_connection() as conn:
@@ -128,90 +158,96 @@ def stream_philo_text():
128
  final_prompt += f"\n\n--- EXTRAIT DE COURS POUR CONTEXTE ---\n{result['content']}"
129
  except Exception as e:
130
  logging.error(f"Erreur DB pour le cours {course_id}: {e}")
131
- # Continuer sans le contenu du cours en cas d'erreur
132
-
133
- def generate():
134
- yield from process_and_stream(model_id, final_prompt)
135
 
136
- return Response(stream_with_context(generate()), mimetype='text/plain')
137
 
138
  @app.route('/stream_philo_image', methods=['POST'])
139
  def stream_philo_image():
 
140
  if 'image' not in request.files:
141
- return Response("Erreur: Fichier image manquant.", status=400, mimetype='text/plain')
142
 
143
  image_file = request.files['image']
144
- if not image_file.filename:
145
- return Response("Erreur: Aucun fichier sélectionné.", status=400, mimetype='text/plain')
146
 
147
  try:
148
  img_bytes = image_file.read()
149
  image_part = types.Part.from_bytes(data=img_bytes, mime_type=image_file.mimetype)
150
 
151
  prompt_text = load_prompt('philo_image_analysis.txt')
152
-
153
  contents = [prompt_text, image_part]
154
 
155
- # Le modèle vision pro est souvent le meilleur pour les images
156
  model_id = "gemini-2.5-pro"
157
 
158
- def generate():
159
- yield from process_and_stream(model_id, contents)
160
-
161
- return Response(stream_with_context(generate()), mimetype='text/plain')
162
 
163
  except Exception as e:
164
  logging.error(f"Erreur lors du traitement de l'image : {e}")
165
- return Response("Erreur interne lors du traitement de l'image.", status=500, mimetype='text/plain')
166
-
167
 
168
  # --- Routes d'Administration (inchangées) ---
169
  @app.route('/admin/philosophy/courses', methods=['GET', 'POST', 'DELETE'])
170
  def manage_philosophy_courses():
171
- # ... (Le code de cette route reste le même qu'avant)
172
  if request.method == 'GET':
173
  try:
174
- conn = psycopg2.connect(DATABASE_URL)
175
- cur = conn.cursor(cursor_factory=RealDictCursor)
176
- cur.execute("SELECT * FROM cours_philosophie")
177
- courses = cur.fetchall()
178
- cur.close()
179
- conn.close()
180
  return render_template('philosophy_courses.html', courses=courses)
181
  except Exception as e:
182
  flash(f'Erreur lors de la récupération des cours : {e}', 'danger')
183
- return redirect(url_for('manage_philosophy_courses'))
184
 
185
  elif request.method == 'POST':
186
- if 'title' in request.form:
 
 
 
 
 
 
 
 
 
 
 
 
187
  try:
188
  title = request.form.get('title')
189
  content = request.form.get('content')
190
  author = request.form.get('author')
191
- conn = psycopg2.connect(DATABASE_URL)
192
- cur = conn.cursor()
193
- cur.execute("INSERT INTO cours_philosophie (title, content, author) VALUES (%s, %s, %s)", (title, content, author))
194
- conn.commit()
195
- cur.close()
196
- conn.close()
 
197
  flash('Cours ajouté avec succès !', 'success')
198
- return redirect(url_for('manage_philosophy_courses'))
199
- except Exception as e:
200
- flash(f'Erreur lors de l\'ajout du cours : {e}', 'danger')
201
- return redirect(url_for('manage_philosophy_courses'))
202
- else:
203
- try:
204
- course_id = request.form.get('id')
205
- conn = psycopg2.connect(DATABASE_URL)
206
- cur = conn.cursor()
207
- cur.execute("DELETE FROM cours_philosophie WHERE id = %s", (course_id,))
208
- conn.commit()
209
- cur.close()
210
- conn.close()
211
- flash('Cours supprimé avec succès !', 'success')
212
- return redirect(url_for('manage_philosophy_courses'))
213
  except Exception as e:
214
- flash(f'Erreur lors de la suppression du cours : {e}', 'danger')
215
- return redirect(url_for('manage_philosophy_courses'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
  # --- END 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.")
34
+ client = None
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
  {"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:
 
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:
 
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 ---