Spaces:
Running
Running
# --- START OF FILE app.py --- | |
import os | |
import logging | |
import json | |
from flask import ( | |
Flask, make_response, render_template, request, redirect, url_for, | |
session, jsonify, flash, Response, stream_with_context | |
) | |
from datetime import datetime, timedelta | |
import psycopg2 | |
from psycopg2.extras import RealDictCursor | |
from google import genai | |
from google.genai import types | |
# Import de la fonction de chargement des prompts depuis notre module utilitaire | |
from utils import load_prompt | |
# --- Configuration de l'application --- | |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
# Initialisation de Flask | |
app = Flask(__name__) | |
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "uyyhhy77uu-default-secret-key") | |
# Configuration des variables d'environnement | |
DATABASE_URL = os.environ.get("DATABASE") | |
GOOGLE_API_KEY = os.environ.get("TOKEN") | |
# Configuration du client Google GenAI | |
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 critique lors de l'initialisation du client GenAI: {e}") | |
client = None | |
# Paramètres de sécurité pour l'API Gemini | |
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 --- | |
def create_connection(): | |
"""Crée et retourne une connexion à la base de données PostgreSQL.""" | |
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 | |
# --- Route principale pour l'affichage de la page --- | |
def philosophie(): | |
"""Affiche la page principale de l'assistant philosophique.""" | |
return render_template("philosophie.html") | |
# --- Routes API pour les données des cours (Non-Streaming) --- | |
def get_philosophy_courses(): | |
"""Récupère la liste de tous les cours de philosophie.""" | |
try: | |
with create_connection() as conn: | |
with conn.cursor(cursor_factory=RealDictCursor) as cur: | |
cur.execute("SELECT id, title, author, updated_at 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 | |
def get_philosophy_course(course_id): | |
"""Récupère les détails d'un cours spécifique par son ID.""" | |
try: | |
with create_connection() as conn: | |
with conn.cursor(cursor_factory=RealDictCursor) as cur: | |
cur.execute("SELECT content, author, updated_at FROM cours_philosophie WHERE id = %s", (course_id,)) | |
course = cur.fetchone() | |
if course: | |
return jsonify(course) | |
return jsonify({"error": "Cours non trouvé"}), 404 | |
except Exception as e: | |
logging.error(f"Erreur lors de la récupération du cours ID {course_id} : {e}") | |
return jsonify({"error": "Erreur interne du serveur lors de la récupération du cours."}), 500 | |
# --- Logique de Génération en Streaming --- | |
def process_and_stream_with_thinking(model_id, prompt_content): | |
""" | |
Génère et streame le contenu avec la pensée activée, en envoyant des objets JSON. | |
Chaque objet JSON est délimité par un retour à la ligne. | |
""" | |
if not client: | |
error_data = json.dumps({"type": "error", "content": "Le service IA n'est pas correctement configuré. Veuillez contacter l'administrateur."}) + "\n" | |
yield error_data | |
return | |
try: | |
config = types.GenerateContentConfig( | |
safety_settings=SAFETY_SETTINGS, | |
thinking_config=types.ThinkingConfig(include_thoughts=True) | |
) | |
stream = client.models.generate_content_stream( | |
model=model_id, contents=prompt_content, config=config | |
) | |
for chunk in stream: | |
for part in chunk.candidates[0].content.parts: | |
if not part.text: | |
continue | |
data_type = "thought" if part.thought else "answer" | |
data = {"type": data_type, "content": part.text} | |
yield json.dumps(data, ensure_ascii=False) + "\n" | |
except Exception as e: | |
logging.error(f"Erreur de streaming Gemini ({model_id}): {e}") | |
error_data = json.dumps({"type": "error", "content": f"Une erreur est survenue avec le service IA : {e}"}) + "\n" | |
yield error_data | |
# --- Routes de Génération en Streaming --- | |
def stream_philo_text(): | |
"""Gère les requêtes de génération de texte en streaming.""" | |
data = request.json | |
phi_prompt = data.get('question', '').strip() | |
phi_type = data.get('type', '1') | |
course_id = data.get('courseId') | |
if not phi_prompt: | |
return Response("Erreur: Le champ 'sujet' est obligatoire.", status=400) | |
is_deepthink = 'deepthink' in request.path | |
model_id = "gemini-2.5-pro" if is_deepthink else "gemini-2.5-flash" | |
prompt_file = {'1': 'philo_type1.txt', '2': 'philo_type2.txt'}.get(phi_type) | |
if not prompt_file: | |
return Response(f"Erreur: Type de sujet '{phi_type}' invalide.", status=400) | |
prompt_template = load_prompt(prompt_file) | |
final_prompt = prompt_template.format(phi_prompt=phi_prompt) | |
if course_id: | |
try: | |
with create_connection() as conn: | |
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['content']: | |
final_prompt += f"\n\n--- EXTRAIT DE COURS POUR CONTEXTE ---\n{result['content']}" | |
except Exception as e: | |
logging.error(f"Erreur DB pour le cours {course_id}: {e}") | |
return Response(stream_with_context(process_and_stream_with_thinking(model_id, final_prompt)), mimetype='application/x-json-stream; charset=utf-8') | |
def stream_philo_image(): | |
"""Gère les requêtes d'analyse d'image en streaming.""" | |
if 'image' not in request.files: | |
return Response("Erreur: Fichier image manquant.", status=400) | |
image_file = request.files['image'] | |
if not image_file or not image_file.filename: | |
return Response("Erreur: Aucun fichier sélectionné.", status=400) | |
try: | |
img_bytes = image_file.read() | |
image_part = types.Part.from_bytes(data=img_bytes, mime_type=image_file.mimetype) | |
prompt_text = load_prompt('philo_image_analysis.txt') | |
contents = [prompt_text, image_part] | |
# Le modèle "pro" est plus performant pour l'analyse d'image complexe | |
model_id = "gemini-2.5-pro" | |
return Response(stream_with_context(process_and_stream_with_thinking(model_id, contents)), mimetype='application/x-json-stream; charset=utf-8') | |
except Exception as e: | |
logging.error(f"Erreur lors du traitement de l'image : {e}") | |
return Response("Erreur interne lors de la préparation de l'image.", status=500) | |
# --- Routes d'Administration (inchangées) --- | |
def manage_philosophy_courses(): | |
"""Gère le CRUD pour les cours de philosophie.""" | |
if request.method == 'GET': | |
try: | |
with create_connection() as conn: | |
with conn.cursor(cursor_factory=RealDictCursor) as cur: | |
cur.execute("SELECT * FROM cours_philosophie ORDER BY updated_at DESC") | |
courses = cur.fetchall() | |
return render_template('philosophy_courses.html', courses=courses) | |
except Exception as e: | |
flash(f'Erreur lors de la récupération des cours : {e}', 'danger') | |
return redirect(url_for('some_admin_dashboard_route')) # Remplacez par une route de fallback | |
elif request.method == 'POST': | |
# La logique de suppression est maintenant dans une route DELETE dédiée pour être plus RESTful | |
# Mais on garde la logique formulaire pour la simplicité | |
if 'delete_course_id' in request.form: | |
try: | |
course_id = request.form.get('delete_course_id') | |
with create_connection() as conn: | |
with conn.cursor() as cur: | |
cur.execute("DELETE FROM cours_philosophie WHERE id = %s", (course_id,)) | |
conn.commit() | |
flash('Cours supprimé avec succès !', 'success') | |
except Exception as e: | |
flash(f'Erreur lors de la suppression du cours : {e}', 'danger') | |
else: # Logique d'ajout/modification | |
try: | |
title = request.form.get('title') | |
content = request.form.get('content') | |
author = request.form.get('author') | |
with create_connection() as conn: | |
with conn.cursor() as cur: | |
cur.execute( | |
"INSERT INTO cours_philosophie (title, content, author) VALUES (%s, %s, %s)", | |
(title, content, author) | |
) | |
conn.commit() | |
flash('Cours ajouté avec succès !', 'success') | |
except Exception as e: | |
flash(f"Erreur lors de l'ajout du cours : {e}", 'danger') | |
return redirect(url_for('manage_philosophy_courses')) | |
# Pour la suppression via DELETE http method (par ex: avec du JS) | |
elif request.method == 'DELETE': | |
course_id = request.form.get('id') | |
try: | |
with create_connection() as conn: | |
with conn.cursor() as cur: | |
cur.execute("DELETE FROM cours_philosophie WHERE id = %s", (course_id,)) | |
conn.commit() | |
return jsonify({'success': True, 'message': 'Cours supprimé'}), 200 | |
except Exception as e: | |
return jsonify({'success': False, 'message': str(e)}), 500 | |
if __name__ == '__main__': | |
# Utiliser debug=True uniquement pour le développement local | |
# Pour la production, utilisez un serveur WSGI comme Gunicorn ou uWSGI | |
app.run(debug=True, port=5000) | |
# --- END OF FILE app.py --- |