aaa / app /main.py
work-sejal
Handle missing models/dataset gracefully
84a0358
Raw
History Blame Contribute Delete
7.64 kB
"""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,
}
},
)