Flasksite / app.py
Docfile's picture
Update app.py
5524fc2 verified
raw
history blame
11.1 kB
# --- 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 ---
@app.route('/')
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) ---
@app.route('/api/philosophy/courses', methods=['GET'])
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
@app.route('/api/philosophy/courses/<int:course_id>', methods=['GET'])
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 ---
@app.route('/stream_philo', methods=['POST'])
@app.route('/stream_philo_deepthink', methods=['POST'])
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')
@app.route('/stream_philo_image', methods=['POST'])
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) ---
@app.route('/admin/philosophy/courses', methods=['GET', 'POST', 'DELETE'])
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 ---