Files
open-school/backend/app/main.py
2026-04-05 10:13:28 +02:00

167 lines
5.7 KiB
Python

from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, File, HTTPException, UploadFile
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,
transcribe_audio,
)
@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)
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.post("/transcribe")
async def transcribe(file: UploadFile = File(...)):
if not file.filename:
raise HTTPException(status_code=400, detail="Fichier audio manquant")
audio_bytes = await file.read()
if not audio_bytes:
raise HTTPException(status_code=400, detail="Fichier audio vide")
try:
text = transcribe_audio(file.filename, audio_bytes, file.content_type)
except Exception as exc:
raise HTTPException(status_code=502, detail=f"Erreur de transcription: {exc}") from exc
return {"text": text}
@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,
)