File size: 11,092 Bytes
5fb7c14
 
fae6b38
6ca0885
5524fc2
 
 
 
 
fae6b38
6ca0885
fae6b38
5fb7c14
 
6ca0885
5524fc2
 
 
 
 
6ca0885
fae6b38
 
5524fc2
6ca0885
5524fc2
fae6b38
6ca0885
 
5524fc2
fae6b38
5524fc2
 
 
 
 
fae6b38
5524fc2
fae6b38
 
5524fc2
fae6b38
 
 
 
 
6ca0885
 
5524fc2
6ca0885
5524fc2
 
 
 
 
 
6ca0885
5524fc2
6ca0885
 
5524fc2
6ca0885
 
5524fc2
6ca0885
 
5524fc2
6ca0885
fae6b38
 
 
 
6ca0885
 
fae6b38
5524fc2
6ca0885
 
 
5524fc2
6ca0885
fae6b38
 
5524fc2
fae6b38
6ca0885
 
 
 
5524fc2
 
 
 
 
 
 
 
 
fae6b38
5524fc2
 
fae6b38
6ca0885
 
5524fc2
 
 
 
 
fae6b38
5524fc2
fae6b38
5524fc2
fae6b38
5524fc2
 
 
 
 
 
 
 
5fb7c14
fae6b38
5524fc2
 
5fb7c14
5524fc2
fae6b38
 
 
5524fc2
5fb7c14
 
fae6b38
5fb7c14
 
 
5524fc2
fae6b38
 
 
5524fc2
 
fae6b38
5524fc2
fae6b38
 
 
 
 
 
 
 
 
 
 
 
 
 
533067f
5524fc2
6ca0885
fae6b38
 
5524fc2
fae6b38
5524fc2
6ca0885
fae6b38
5524fc2
 
6ca0885
fae6b38
 
 
 
 
 
 
5524fc2
fae6b38
5fb7c14
5524fc2
6ca0885
 
fae6b38
5524fc2
6ca0885
fae6b38
6ca0885
 
5524fc2
6ca0885
 
5524fc2
 
 
 
6ca0885
 
 
5524fc2
6ca0885
 
5524fc2
 
 
 
 
 
 
 
 
 
 
 
 
6ca0885
 
 
 
5524fc2
 
 
 
 
 
 
6ca0885
 
5524fc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5fb7c14
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# --- 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 ---