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) |