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