"""FastAPI application entry point. Configures the app instance, middleware, exception handlers, and startup hooks. Launch with: uvicorn app.main:app """ import logging import uuid from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.api.v2.answer_evaluation import router as answer_evaluation_router from app.api.v2.bloom import router as bloom_router from app.api.v2.class_insights import router as class_insights_router from app.api.v2.dependencies import ServiceContainer from app.api.v2.health import router as health_router from app.api.v2.lo_tagging import router as lo_tagging_router from app.api.v2.mastery import router as mastery_router from app.api.v2.monitoring import router as monitoring_router from app.api.v2.recommendations import router as recommendations_router from app.api.v2.risk import router as risk_router from app.api.v2.student_profile import router as student_profile_router from app.api.v2.teacher_feedback import router as teacher_feedback_router from app.core.config import settings from app.core.exceptions import DatasetError, EntityNotFoundError, ModelNotLoadedError from app.data.loader import DatasetLoader from app.models.registry import ModelRegistry from app.monitoring.prediction_logger import PredictionLogger from app.services.answer_evaluation_service import AnswerEvaluationService from app.services.bloom_service import BloomService from app.services.class_insights_service import ClassInsightsService from app.services.explanation_service import ExplanationService from app.services.lo_tagging_service import LOTaggingService from app.services.mastery_service import MasteryService from app.services.monitoring_service import MonitoringService from app.services.recommendation_service import RecommendationService from app.services.risk_service import RiskService from app.services.student_profile_service import StudentProfileService from app.services.teacher_feedback_service import TeacherFeedbackService logger = logging.getLogger(__name__) # Internal development origins — no wildcard "*" in production. ALLOWED_ORIGINS = [ "http://localhost:3000", "http://localhost:8000", ] def _cache_dataset_metadata(app: FastAPI) -> None: """Load and cache dataset metadata for the data summary endpoint.""" try: loader = DatasetLoader(settings.dataset_dir) metadata = loader.load_metadata() table_counts: dict[str, int] = metadata.get("table_counts", {}) app.state.dataset_metadata = { "version": metadata.get("version", "unknown"), "table_counts": table_counts, "validation_status": "not_run", "total_issues": 0, } logger.info("Dataset metadata cached successfully — %d tables", len(table_counts)) except DatasetError as exc: logger.warning("Failed to cache dataset metadata: %s", exc) app.state.dataset_metadata = { "version": "unknown", "table_counts": {}, "validation_status": "not_run", "total_issues": 0, } @asynccontextmanager async def lifespan(app: FastAPI): """Application lifespan: startup and shutdown hooks.""" # 1. Cache dataset metadata (existing) _cache_dataset_metadata(app) # 2. Initialize model registry and load all models registry = ModelRegistry(settings.model_artifact_dir) try: registry.load_all() logger.info("All models loaded successfully") except Exception as e: logger.warning("Failed to load models: %s. Continuing with fallback mode.", e) # 3. Initialize shared components try: loader = DatasetLoader(settings.dataset_dir) except Exception as e: logger.warning("Failed to load dataset: %s. Continuing with fallback mode.", e) loader = None explainer = ExplanationService() pred_logger = PredictionLogger( log_dir=Path(settings.metrics_dir), salt=settings.log_salt, ) # 4. Initialize service container with DI app.state.services = ServiceContainer( registry=registry, loader=loader, explainer=explainer, pred_logger=pred_logger, lo_tagging=LOTaggingService(registry, loader, explainer, pred_logger), bloom=BloomService(registry, loader, explainer, pred_logger), mastery=MasteryService(registry, loader, explainer, pred_logger), risk=RiskService(registry, loader, explainer, pred_logger), recommendation=RecommendationService(registry, loader, explainer, pred_logger), answer_evaluation=AnswerEvaluationService(registry, loader, explainer, pred_logger), student_profile=StudentProfileService(loader), class_insights=ClassInsightsService(loader), teacher_feedback=TeacherFeedbackService(), monitoring=MonitoringService(registry, pred_logger), ) logger.info("Service container initialized — model registry and shared components ready") yield app = FastAPI( title=settings.ai_service_name, version=settings.ai_service_version, lifespan=lifespan, ) # CORS middleware — internal origins only, no wildcard "*". app.add_middleware( CORSMiddleware, allow_origins=ALLOWED_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include routers app.include_router(health_router) app.include_router(lo_tagging_router) app.include_router(bloom_router) app.include_router(mastery_router) app.include_router(risk_router) app.include_router(recommendations_router) app.include_router(answer_evaluation_router) app.include_router(student_profile_router) app.include_router(class_insights_router) app.include_router(teacher_feedback_router) app.include_router(monitoring_router) # --- Exception Handlers --- @app.exception_handler(DatasetError) async def dataset_error_handler(request: Request, exc: DatasetError) -> JSONResponse: """Handle DatasetError — returns 500 with INTERNAL error code.""" request_id = str(uuid.uuid4()) logger.error("DatasetError [request_id=%s]: %s", request_id, exc) return JSONResponse( status_code=500, content={ "error": { "code": "INTERNAL", "message": str(exc), "request_id": request_id, } }, ) @app.exception_handler(ModelNotLoadedError) async def model_not_loaded_handler(request: Request, exc: ModelNotLoadedError) -> JSONResponse: """Handle ModelNotLoadedError — returns 503 with MODEL_NOT_LOADED error code.""" request_id = str(uuid.uuid4()) logger.error("ModelNotLoadedError [request_id=%s]: %s", request_id, exc) return JSONResponse( status_code=503, content={ "error": { "code": "MODEL_NOT_LOADED", "message": str(exc), "request_id": request_id, } }, ) @app.exception_handler(EntityNotFoundError) async def entity_not_found_handler(request: Request, exc: EntityNotFoundError) -> JSONResponse: """Handle EntityNotFoundError — returns 404 with NOT_FOUND error code.""" request_id = str(uuid.uuid4()) logger.warning("EntityNotFoundError [request_id=%s]: %s", request_id, exc) return JSONResponse( status_code=404, content={ "error": { "code": "NOT_FOUND", "message": str(exc), "request_id": request_id, } }, )