| """ |
| ARCHAI Adaptive Assessment API |
| ============================== |
| FastAPI backend that plugs into the archai frontend. |
| |
| Endpoints: |
| POST /api/v1/session/start → Initialize assessment |
| POST /api/v1/session/answer → Submit answer, get next question |
| GET /api/v1/session/{id} → Get current state/results |
| POST /api/v1/path/generate → Generate learning path with day/week/month actionables |
| GET /api/v1/questions → Get full question bank (for offline study) |
| GET /api/v1/health → Health check |
| """ |
|
|
| from fastapi import FastAPI, HTTPException |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel, Field |
| from typing import Optional, List, Dict, Any |
| from adaptive_engine import AdaptiveAssessmentEngine, engine, Dimension, DIMENSION_LABELS, DIMENSION_COLORS |
|
|
| app = FastAPI( |
| title="ARCHAI Adaptive Assessment Engine", |
| description="IRT-based adaptive AI readiness assessment with LLM-powered learning paths", |
| version="2.0.0", |
| docs_url="/docs", |
| redoc_url="/redoc", |
| ) |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| |
| |
|
|
| class StartSessionResponse(BaseModel): |
| session_id: str |
| question: Optional[Dict[str, Any]] |
| progress: Dict[str, Any] |
| status: str |
|
|
| class SubmitAnswerRequest(BaseModel): |
| session_id: str |
| question_id: str |
| option_index: int = Field(ge=0, le=3) |
|
|
| class SubmitAnswerResponse(BaseModel): |
| session_id: str |
| question: Optional[Dict[str, Any]] |
| progress: Dict[str, Any] |
| interim_scores: Optional[Dict[str, int]] |
| status: str |
|
|
| class GeneratePathRequest(BaseModel): |
| session_id: str |
| persona_id: str |
| hours_per_week: int = Field(ge=1, le=40) |
| budget_usd: int = Field(ge=0) |
| hardware_id: Optional[str] = None |
| preference: Optional[str] = None |
|
|
| class GeneratePathResponse(BaseModel): |
| session_id: str |
| overall_score: int |
| stage: Dict[str, Any] |
| archetype: Dict[str, Any] |
| dimension_scores: Dict[str, int] |
| gaps: List[Dict[str, Any]] |
| strengths: List[Dict[str, Any]] |
| learning_path: Dict[str, List[Any]] |
| projections: Dict[str, Any] |
| meta: Dict[str, Any] |
|
|
| |
| |
| |
|
|
| @app.get("/api/v1/health") |
| def health(): |
| return { |
| "status": "healthy", |
| "engine": "IRT-2PL adaptive", |
| "version": "2.0.0", |
| "features": ["adaptive_selection", "bayesian_knowledge_tracing", "structured_learning_paths"], |
| } |
|
|
| @app.post("/api/v1/session/start", response_model=StartSessionResponse) |
| def start_session(): |
| """Start a new adaptive assessment session.""" |
| result = engine.start_session() |
| return result |
|
|
| @app.post("/api/v1/session/answer", response_model=SubmitAnswerResponse) |
| def submit_answer(req: SubmitAnswerRequest): |
| """Submit an answer and get the next adaptive question.""" |
| result = engine.submit_answer(req.session_id, req.question_id, req.option_index) |
| if "error" in result: |
| raise HTTPException(status_code=404, detail=result["error"]) |
| return result |
|
|
| @app.get("/api/v1/session/{session_id}") |
| def get_session(session_id: str): |
| """Get current session state or final results.""" |
| state = engine.sessions.get(session_id) |
| if not state: |
| raise HTTPException(status_code=404, detail="Session not found") |
| |
| |
| if len(state.asked_questions) > 0 and engine.selector.should_stop(state): |
| return engine._finalize(state) |
| |
| |
| dim_coverage = set() |
| for qid in state.asked_questions: |
| q = next((qq for qq in engine.question_bank if qq.id == qid), None) |
| if q: |
| dim_coverage.add(q.dimension.value) |
| |
| return { |
| "session_id": session_id, |
| "status": "in_progress", |
| "progress": { |
| "asked": len(state.asked_questions), |
| "total": 12, |
| "dimensions_covered": list(dim_coverage), |
| }, |
| "interim_scores": engine.tracer.get_dimension_scores(state), |
| "latent_abilities": {d.value: round(t, 2) for d, t in state.theta.items()}, |
| } |
|
|
| @app.post("/api/v1/path/generate", response_model=GeneratePathResponse) |
| def generate_path(req: GeneratePathRequest): |
| """Generate a structured learning path with day/week/month actionables.""" |
| result = engine.generate_path( |
| req.session_id, |
| req.persona_id, |
| req.hours_per_week, |
| req.budget_usd, |
| req.hardware_id, |
| req.preference, |
| ) |
| if "error" in result: |
| raise HTTPException(status_code=404, detail=result["error"]) |
| return result |
|
|
| @app.get("/api/v1/questions") |
| def get_questions(): |
| """Get the full calibrated question bank for offline study.""" |
| return { |
| "questions": [ |
| { |
| "id": q.id, |
| "dimension": q.dimension.value, |
| "dimension_label": DIMENSION_LABELS.get(q.dimension, q.dimension.value), |
| "text": q.text, |
| "options": q.options, |
| "difficulty": round(q.difficulty, 2), |
| "discrimination": round(q.discrimination, 2), |
| "concept_tags": q.concept_tags, |
| } |
| for q in engine.question_bank |
| ], |
| "dimensions": [ |
| {"id": d.value, "label": DIMENSION_LABELS.get(d, d.value), "color": DIMENSION_COLORS.get(d.value, "#14B8A6")} |
| for d in Dimension |
| ], |
| } |
|
|
| @app.get("/api/v1/dimensions") |
| def get_dimensions(): |
| """Get dimension metadata for UI rendering.""" |
| return { |
| "dimensions": [ |
| {"id": d.value, "label": DIMENSION_LABELS.get(d, d.value), "color": DIMENSION_COLORS.get(d.value, "#14B8A6")} |
| for d in Dimension |
| ] |
| } |
|
|
| |
| |
| |
|
|
| @app.post("/api/v1/assessment/start") |
| def assessment_start_compat(): |
| """Backward-compatible endpoint name.""" |
| return start_session() |
|
|
| @app.post("/api/v1/assessment/answer") |
| def assessment_answer_compat(req: SubmitAnswerRequest): |
| """Backward-compatible endpoint name.""" |
| return submit_answer(req) |
|
|
| @app.get("/api/v1/assessment/results/{session_id}") |
| def assessment_results_compat(session_id: str): |
| """Backward-compatible endpoint for getting results.""" |
| return get_session(session_id) |
|
|
| |
| |
| |
|
|
| @app.get("/") |
| def root(): |
| return { |
| "service": "ARCHAI Adaptive Assessment Engine", |
| "version": "2.0.0", |
| "docs": "/docs", |
| "endpoints": { |
| "start": "POST /api/v1/session/start", |
| "answer": "POST /api/v1/session/answer", |
| "results": "GET /api/v1/session/{session_id}", |
| "path": "POST /api/v1/path/generate", |
| "questions": "GET /api/v1/questions", |
| "health": "GET /api/v1/health", |
| }, |
| "adaptive_features": { |
| "irt_model": "2PL (Two-Parameter Logistic)", |
| "selection": "Fisher Information Maximization", |
| "tracing": "Bayesian Knowledge Updating", |
| "stopping": "Precision-based early stopping", |
| "question_count": "Adaptive 6-12 questions", |
| } |
| } |
|
|
|
|
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run(app, host="0.0.0.0", port=7860) |
|
|