import os from typing import List from openai import OpenAI from sqlalchemy.orm import Session from . import models from .curriculum import QUESTIONS, SKILLS client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) SYSTEM_PROMPT = """ Tu es ProfAmi, un professeur virtuel français pour enfants de 8 à 12 ans. Règles : - Tu parles toujours en français simple et chaleureux. - Tu donnes des explications très courtes, puis un mini exemple. - Tu tiens compte du niveau de l'élève et de ses faiblesses indiquées dans le contexte. - Tu n'inventes pas la progression : elle est fournie dans le contexte. - Tu encourages sans infantiliser. - Quand l'élève se trompe, tu expliques calmement puis proposes une question très simple. - Tu enseignes principalement le programme national français niveau primaire/cycle 3. """.strip() def seed_skills(db: Session) -> None: for skill in SKILLS: existing = db.query(models.Skill).filter(models.Skill.code == skill["code"]).first() if not existing: db.add(models.Skill(**skill)) db.commit() def ensure_student_mastery(db: Session, student: models.Student) -> None: all_skills = db.query(models.Skill).all() for skill in all_skills: found = ( db.query(models.StudentSkillMastery) .filter_by(student_id=student.id, skill_id=skill.id) .first() ) if not found: db.add(models.StudentSkillMastery(student_id=student.id, skill_id=skill.id)) db.commit() def get_student_context(db: Session, student_id: int) -> str: student = db.query(models.Student).filter_by(id=student_id).first() mastery = ( db.query(models.StudentSkillMastery, models.Skill) .join(models.Skill, models.Skill.id == models.StudentSkillMastery.skill_id) .filter(models.StudentSkillMastery.student_id == student_id) .all() ) recent_messages = ( db.query(models.Message) .filter_by(student_id=student_id) .order_by(models.Message.created_at.desc()) .limit(6) .all() ) lines: List[str] = [ f"Élève: {student.first_name}, {student.age} ans, classe {student.grade}.", "Progression par compétence:", ] for mastery_row, skill in mastery: lines.append( f"- {skill.label}: score={mastery_row.mastery_score:.1f}, confiance={mastery_row.confidence:.2f}, preuves={mastery_row.evidence_count}" ) lines.append("Historique récent:") for message in reversed(recent_messages): lines.append(f"- {message.role}: {message.content}") return "\n".join(lines) def build_llm_reply(db: Session, student_id: int, user_message: str) -> str: context = get_student_context(db, student_id) response = client.responses.create( model="gpt-4.1-mini", input=[ {"role": "system", "content": SYSTEM_PROMPT}, { "role": "user", "content": f"Contexte pédagogique:\n{context}\n\nMessage de l'élève:\n{user_message}", }, ], temperature=0.7, ) return response.output_text.strip() def pick_next_skill(db: Session, student_id: int) -> models.Skill: weakest = ( db.query(models.StudentSkillMastery) .filter_by(student_id=student_id) .order_by(models.StudentSkillMastery.mastery_score.asc()) .first() ) return db.query(models.Skill).filter_by(id=weakest.skill_id).first() def evaluate_answer(db: Session, student_id: int, skill_code: str, answer: str): q = QUESTIONS[skill_code] normalized_student = answer.strip().lower() normalized_expected = q["expected_answer"].strip().lower() correct = normalized_student == normalized_expected skill = db.query(models.Skill).filter_by(code=skill_code).first() mastery = ( db.query(models.StudentSkillMastery) .filter_by(student_id=student_id, skill_id=skill.id) .first() ) if correct: mastery.mastery_score = min(100.0, mastery.mastery_score + 8) mastery.confidence = min(1.0, mastery.confidence + 0.15) feedback = q["feedback_ok"] else: mastery.mastery_score = max(0.0, mastery.mastery_score - 6) mastery.confidence = min(1.0, mastery.confidence + 0.1) feedback = q["feedback_ko"] mastery.evidence_count += 1 db.add( models.AssessmentAttempt( student_id=student_id, skill_code=skill_code, question=q["question"], expected_answer=q["expected_answer"], student_answer=answer, is_correct=1 if correct else 0, feedback=feedback, ) ) db.commit() db.refresh(mastery) return correct, feedback, mastery.mastery_score