droitpro / app.py
ramedde's picture
Update app.py
1c3ea52 verified
Raw
History Blame Contribute Delete
11.7 kB
"""
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)