File size: 11,700 Bytes
71c1d44
329ea19
1c3ea52
 
329ea19
1c3ea52
 
 
 
 
329ea19
1c3ea52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71c1d44
1c3ea52
 
 
71c1d44
329ea19
1c3ea52
 
 
 
 
71c1d44
1c3ea52
 
 
 
 
b8d2228
1c3ea52
 
 
b8d2228
1c3ea52
b8d2228
1c3ea52
 
b8d2228
1c3ea52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b8d2228
1c3ea52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06c8bed
1c3ea52
 
 
 
 
 
 
71c1d44
1c3ea52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06c8bed
1c3ea52
 
06c8bed
bf8de3d
1c3ea52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305

"""
DroitPro – Application d'apprentissage du droit procédural pénal
Évaluation des réponses par Gemma-4 via le serveur formegemma (llama-server OpenAI-compatible)
"""

import json
import threading
import time
import requests
import gradio as gr

# ─────────────────────────────────────────────
# Configuration du serveur IA distant
# ─────────────────────────────────────────────
LLAMA_SERVER_URL = "https://ramedde-formegemma.hf.space/v1/chat/completions"

SYSTEM_PROMPT = (
    "Tu es un professeur expert en droit procédural pénal algérien et français. "
    "Tu évalues les réponses des étudiants de manière pédagogique, bienveillante et précise. "
    "Tu réponds UNIQUEMENT en français. "
    "Tu bases tes évaluations sur les textes du Code de procédure pénale algérien et les principes généraux du droit pénal."
)

# ─────────────────────────────────────────────
# Chargement des leçons
# ─────────────────────────────────────────────
with open("lessons.json", "r", encoding="utf-8") as f:
    LESSONS = json.load(f)

LESSON_MAP = {lesson["id"]: lesson for lesson in LESSONS}


# ─────────────────────────────────────────────
# Client IA – appel HTTP au serveur formegemma
# ─────────────────────────────────────────────
def call_model(user_prompt: str, timeout: int = 60) -> str:
    """
    Envoie un prompt au serveur llama-server (formegemma) via l'API OpenAI-compatible.
    Retourne le texte généré ou un message d'erreur lisible.
    """
    payload = {
        "model": "gemma",           # llama-server ignore ce champ, on le met par convention
        "messages": [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user",   "content": user_prompt},
        ],
        "max_tokens": 600,
        "temperature": 0.3,
        "stream": False,
    }
    try:
        resp = requests.post(
            LLAMA_SERVER_URL,
            json=payload,
            timeout=timeout,
            headers={"Content-Type": "application/json"},
        )
        resp.raise_for_status()
        data = resp.json()
        return data["choices"][0]["message"]["content"].strip()

    except requests.exceptions.Timeout:
        return (
            "⏳ Le serveur IA (formegemma) n'a pas répondu dans le délai imparti.\n"
            "Il est peut-être en cours de démarrage (cold-start). "
            "Veuillez réessayer dans 30 secondes."
        )
    except requests.exceptions.ConnectionError:
        return (
            "🔌 Impossible de joindre le serveur IA (formegemma).\n"
            "Vérifiez que l'espace ramedde/formegemma est bien actif sur Hugging Face."
        )
    except requests.exceptions.HTTPError as e:
        code = e.response.status_code if e.response else "?"
        if code == 503:
            return (
                "💤 Le serveur IA est en veille (503). "
                "Ouvrez l'espace ramedde/formegemma pour le réveiller, puis réessayez."
            )
        return f"❌ Erreur HTTP {code} du serveur IA : {e}"
    except Exception as e:
        return f"❌ Erreur inattendue lors de l'appel au modèle : {e}"


def warmup_server():
    """Ping silencieux au démarrage pour réveiller formegemma en avance."""
    time.sleep(3)
    try:
        requests.post(
            LLAMA_SERVER_URL,
            json={
                "model": "gemma",
                "messages": [{"role": "user", "content": "Bonjour."}],
                "max_tokens": 5,
            },
            timeout=90,
        )
    except Exception:
        pass   # échec silencieux, c'est juste un warmup


# Lancer le warmup en arrière-plan sans bloquer le démarrage de Gradio
threading.Thread(target=warmup_server, daemon=True).start()


# ─────────────────────────────────────────────
# Logique métier – évaluation
# ─────────────────────────────────────────────
def build_eval_prompt(question: str, reponse_modele: str, reponse_etudiant: str) -> str:
    return f"""Voici une question de droit procédural pénal :

QUESTION : {question}

RÉPONSE MODÈLE (correcte et complète) :
{reponse_modele}

RÉPONSE DE L'ÉTUDIANT :
{reponse_etudiant}

Évalue la réponse de l'étudiant selon ces critères :
1. Exactitude juridique (les éléments cités sont-ils corrects ?)
2. Complétude (les points essentiels sont-ils abordés ?)
3. Clarté et formulation

Donne :
- Une note sur 10
- Les points forts de la réponse
- Les lacunes ou erreurs à corriger
- Un conseil d'amélioration

Sois pédagogue et encourageant."""


def evaluer_reponse(lesson_id: str, question_id: str, reponse_etudiant: str) -> str:
    if not reponse_etudiant or not reponse_etudiant.strip():
        return "⚠️ Veuillez saisir une réponse avant de demander l'évaluation."

    lesson = LESSON_MAP.get(lesson_id)
    if not lesson:
        return "❌ Leçon introuvable."

    question_obj = next(
        (q for q in lesson["questions"] if q["id"] == question_id), None
    )
    if not question_obj:
        return "❌ Question introuvable."

    prompt = build_eval_prompt(
        question=question_obj["question"],
        reponse_modele=question_obj["reponse_modele"],
        reponse_etudiant=reponse_etudiant.strip(),
    )
    return call_model(prompt)


# ─────────────────────────────────────────────
# Interface Gradio
# ─────────────────────────────────────────────
CSS = """
#header { text-align: center; margin-bottom: 1rem; }
#lecon_box { background: #f0f4ff; border-radius: 8px; padding: 1rem; }
#evaluation_box { border-left: 4px solid #4f6ef7; padding-left: 1rem; }
.gr-button-primary { background: #4f6ef7 !important; }
"""

def get_lesson_choices():
    return [(f"{l['id']}{l['titre']}", l["id"]) for l in LESSONS]

def get_question_choices(lesson_id: str):
    lesson = LESSON_MAP.get(lesson_id)
    if not lesson:
        return []
    return [(f"{q['id']}{q['question'][:60]}…", q["id"]) for q in lesson["questions"]]

def load_lesson_content(lesson_id: str) -> str:
    lesson = LESSON_MAP.get(lesson_id)
    if not lesson:
        return ""
    return lesson["contenu"]

def load_question_text(lesson_id: str, question_id: str) -> str:
    lesson = LESSON_MAP.get(lesson_id)
    if not lesson:
        return ""
    q = next((q for q in lesson["questions"] if q["id"] == question_id), None)
    return q["question"] if q else ""

def on_lesson_change(lesson_id: str):
    choices = get_question_choices(lesson_id)
    first_qid = choices[0][1] if choices else None
    content = load_lesson_content(lesson_id)
    q_text = load_question_text(lesson_id, first_qid) if first_qid else ""
    return (
        gr.update(choices=choices, value=first_qid),
        content,
        q_text,
        "",   # reset réponse étudiant
        "",   # reset évaluation
    )

def on_question_change(lesson_id: str, question_id: str):
    q_text = load_question_text(lesson_id, question_id)
    return q_text, "", ""

def on_evaluer(lesson_id, question_id, reponse):
    result = evaluer_reponse(lesson_id, question_id, reponse)
    return result

def on_reset(lesson_id, question_id):
    return "", ""


with gr.Blocks(css=CSS, title="DroitPro – Droit Procédural Pénal") as demo:

    gr.Markdown(
        "# ⚖️ DroitPro\n### Entraînement au droit procédural pénal – Évaluation par IA (Gemma 4)",
        elem_id="header",
    )

    with gr.Row():
        # ── Colonne gauche : navigation ──────────────────────────
        with gr.Column(scale=1):
            gr.Markdown("### 📚 Leçons")
            lesson_dd = gr.Dropdown(
                label="Choisir une leçon",
                choices=get_lesson_choices(),
                value=LESSONS[0]["id"],
            )
            gr.Markdown("### ❓ Questions")
            question_dd = gr.Dropdown(
                label="Choisir une question",
                choices=get_question_choices(LESSONS[0]["id"]),
                value=LESSONS[0]["questions"][0]["id"],
            )

        # ── Colonne droite : contenu + exercice ─────────────────
        with gr.Column(scale=3):
            with gr.Tab("📖 Leçon"):
                lecon_text = gr.Textbox(
                    label="Contenu de la leçon",
                    value=LESSONS[0]["contenu"],
                    lines=8,
                    interactive=False,
                    elem_id="lecon_box",
                )

            with gr.Tab("✏️ Exercice"):
                question_text = gr.Textbox(
                    label="Question",
                    value=LESSONS[0]["questions"][0]["question"],
                    lines=3,
                    interactive=False,
                )
                reponse_input = gr.Textbox(
                    label="Votre réponse",
                    placeholder="Rédigez votre réponse ici...",
                    lines=6,
                )
                with gr.Row():
                    btn_evaluer = gr.Button("🔍 Évaluer ma réponse", variant="primary")
                    btn_reset   = gr.Button("🔄 Effacer", variant="secondary")

            with gr.Tab("📊 Évaluation IA"):
                eval_output = gr.Textbox(
                    label="Résultat de l'évaluation",
                    lines=15,
                    interactive=False,
                    elem_id="evaluation_box",
                    placeholder=(
                        "L'évaluation de votre réponse apparaîtra ici après avoir cliqué "
                        "sur « Évaluer ma réponse »."
                    ),
                )

    # ── Événements ───────────────────────────────────────────────
    lesson_dd.change(
        fn=on_lesson_change,
        inputs=[lesson_dd],
        outputs=[question_dd, lecon_text, question_text, reponse_input, eval_output],
    )

    question_dd.change(
        fn=on_question_change,
        inputs=[lesson_dd, question_dd],
        outputs=[question_text, reponse_input, eval_output],
    )

    btn_evaluer.click(
        fn=on_evaluer,
        inputs=[lesson_dd, question_dd, reponse_input],
        outputs=[eval_output],
    )

    btn_reset.click(
        fn=on_reset,
        inputs=[lesson_dd, question_dd],
        outputs=[reponse_input, eval_output],
    )

# ─────────────────────────────────────────────
# Lancement
# ─────────────────────────────────────────────
if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860)