Spaces:
Sleeping
Sleeping
| """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, | |
| } | |
| 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 --- | |
| 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, | |
| } | |
| }, | |
| ) | |
| 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, | |
| } | |
| }, | |
| ) | |
| 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, | |
| } | |
| }, | |
| ) | |