lov2 / app /main.py
work-sejal
Add knowledge graph and adaptive learning path features to HF Space
c045254
Raw
History Blame Contribute Delete
7.92 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.knowledge_graph import router as knowledge_graph_router
from app.api.v2.learning_path import router as learning_path_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.adaptive_learning_path_service import AdaptiveLearningPathService
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.knowledge_graph_service import KnowledgeGraphService
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:5173",
"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)
registry.load_all()
# 3. Initialize shared components
loader = DatasetLoader(settings.dataset_dir)
explainer = ExplanationService()
pred_logger = PredictionLogger(
log_dir=Path(settings.metrics_dir),
salt=settings.log_salt,
)
# 4. Initialize service container with DI
knowledge_graph_service = KnowledgeGraphService(loader)
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),
knowledge_graph=knowledge_graph_service,
adaptive_learning_path=AdaptiveLearningPathService(loader, knowledge_graph_service),
)
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)
app.include_router(knowledge_graph_router)
app.include_router(learning_path_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,
}
},
)