|
|
| """ |
| 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 |
|
|
| |
| |
| |
| 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." |
| ) |
|
|
| |
| |
| |
| with open("lessons.json", "r", encoding="utf-8") as f: |
| LESSONS = json.load(f) |
|
|
| LESSON_MAP = {lesson["id"]: lesson for lesson in LESSONS} |
|
|
|
|
| |
| |
| |
| 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", |
| "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 |
|
|
|
|
| |
| threading.Thread(target=warmup_server, daemon=True).start() |
|
|
|
|
| |
| |
| |
| 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) |
|
|
|
|
| |
| |
| |
| 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, |
| "", |
| "", |
| ) |
|
|
| 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(): |
| |
| 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"], |
| ) |
|
|
| |
| 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 »." |
| ), |
| ) |
|
|
| |
| 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], |
| ) |
|
|
| |
| |
| |
| if __name__ == "__main__": |
| demo.launch(server_name="0.0.0.0", server_port=7860) |