from contextlib import asynccontextmanager from fastapi import Depends, FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session from .database import Base, engine, get_db from . import models, schemas from .curriculum import QUESTIONS from .services import build_llm_reply, ensure_student_mastery, evaluate_answer, pick_next_skill, seed_skills @asynccontextmanager async def lifespan(app: FastAPI): Base.metadata.create_all(bind=engine) db = next(get_db()) try: seed_skills(db) finally: db.close() yield app = FastAPI(title="Professeur Virtuel API", version="0.1.0", lifespan=lifespan) from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=[ "https://prof.open-squared.tech", "http://localhost:3000", "http://localhost:3001", ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.get("/health") def health(): return {"status": "ok"} @app.get("/students", response_model=list[schemas.StudentRead]) def list_students(db: Session = Depends(get_db)): return db.query(models.Student).order_by(models.Student.id.asc()).all() @app.post("/students", response_model=schemas.StudentRead) def create_student(payload: schemas.StudentCreate, db: Session = Depends(get_db)): student = models.Student(**payload.model_dump()) db.add(student) db.commit() db.refresh(student) ensure_student_mastery(db, student) return student @app.post("/session/start", response_model=schemas.ChatResponse) def start_session(student_id: int, db: Session = Depends(get_db)): student = db.query(models.Student).filter_by(id=student_id).first() if not student: raise HTTPException(status_code=404, detail="Élève introuvable") ensure_student_mastery(db, student) message = ( f"Bonjour {student.first_name} ! Je suis ton professeur virtuel. " "Aujourd'hui, on va apprendre pas à pas et faire un petit test pour voir ce que tu maîtrises déjà." ) db.add(models.Message(student_id=student.id, role="assistant", content=message)) db.commit() return schemas.ChatResponse(reply=message) @app.post("/chat", response_model=schemas.ChatResponse) def chat(payload: schemas.ChatRequest, db: Session = Depends(get_db)): student = db.query(models.Student).filter_by(id=payload.student_id).first() if not student: raise HTTPException(status_code=404, detail="Élève introuvable") db.add(models.Message(student_id=student.id, role="user", content=payload.message)) db.commit() reply = build_llm_reply(db, payload.student_id, payload.message) db.add(models.Message(student_id=student.id, role="assistant", content=reply)) db.commit() return schemas.ChatResponse(reply=reply) @app.get("/progress/{student_id}", response_model=schemas.ProgressResponse) def get_progress(student_id: int, db: Session = Depends(get_db)): student = db.query(models.Student).filter_by(id=student_id).first() if not student: raise HTTPException(status_code=404, detail="Élève introuvable") rows = ( db.query(models.StudentSkillMastery, models.Skill) .join(models.Skill, models.Skill.id == models.StudentSkillMastery.skill_id) .filter(models.StudentSkillMastery.student_id == student_id) .order_by(models.Skill.subject.asc(), models.Skill.label.asc()) .all() ) progress = [ schemas.SkillProgress( code=skill.code, subject=skill.subject, label=skill.label, mastery_score=mastery.mastery_score, confidence=mastery.confidence, evidence_count=mastery.evidence_count, ) for mastery, skill in rows ] return schemas.ProgressResponse(student=student, progress=progress) @app.get("/assessment/next/{student_id}", response_model=schemas.AssessmentQuestionResponse) def next_assessment(student_id: int, db: Session = Depends(get_db)): student = db.query(models.Student).filter_by(id=student_id).first() if not student: raise HTTPException(status_code=404, detail="Élève introuvable") skill = pick_next_skill(db, student_id) question = QUESTIONS[skill.code]["question"] return schemas.AssessmentQuestionResponse(skill_code=skill.code, skill_label=skill.label, question=question) @app.post("/assessment/answer", response_model=schemas.AssessmentAnswerResponse) def answer_assessment(payload: schemas.AssessmentAnswerRequest, db: Session = Depends(get_db)): student = db.query(models.Student).filter_by(id=payload.student_id).first() if not student: raise HTTPException(status_code=404, detail="Élève introuvable") if payload.skill_code not in QUESTIONS: raise HTTPException(status_code=400, detail="Compétence inconnue") correct, feedback, mastery_score = evaluate_answer( db, payload.student_id, payload.skill_code, payload.answer ) db.add(models.Message(student_id=student.id, role="assistant", content=feedback)) db.commit() return schemas.AssessmentAnswerResponse( correct=correct, feedback=feedback, mastery_score=mastery_score, )