145 lines
5.2 KiB
Python
145 lines
5.2 KiB
Python
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,
|
|
)
|