aaa / app /models /registry.py
work-sejal
Deploy AI service with FastAPI
70ea7be
Raw
History Blame Contribute Delete
5.78 kB
"""Model Registry for loading, caching, and serving trained model artifacts.
Supports eager loading at startup and lazy loading on first access.
Reports per-model health status.
"""
import json
import logging
from pathlib import Path
from typing import Any
import joblib
from app.core.exceptions import ModelNotLoadedError
logger = logging.getLogger(__name__)
# All registered model names and their artifact subdirectory names
REGISTERED_MODELS: list[str] = [
"lo_tagger",
"bloom_classifier",
"mastery_model",
"risk_model",
"answer_scorer",
"recommender",
]
class ModelRegistry:
"""Loads and manages trained model artifacts.
Supports eager loading at startup and lazy loading on first access.
Reports per-model health status.
"""
def __init__(self, artifact_dir: str | Path) -> None:
self._artifact_dir = Path(artifact_dir)
self._models: dict[str, dict[str, Any]] = {}
self._status: dict[str, str] = {} # "loaded" | "not_loaded" | "error"
self._metadata: dict[str, dict] = {} # metrics.json content per model
# Initialize all registered models as not_loaded
for model_name in REGISTERED_MODELS:
self._status[model_name] = "not_loaded"
def load_all(self) -> None:
"""Eagerly load all model artifacts from artifact_dir subdirectories."""
for model_name in REGISTERED_MODELS:
self._load_model(model_name)
def _load_model(self, model_name: str) -> None:
"""Load a single model's artifacts from its subdirectory.
On failure, sets status to 'error' and logs the exception without crashing.
"""
model_dir = self._artifact_dir / model_name
if not model_dir.exists():
logger.warning(
"Model directory not found for '%s': %s", model_name, model_dir
)
self._status[model_name] = "not_loaded"
return
try:
model_data: dict[str, Any] = {}
# Required: model.joblib
model_path = model_dir / "model.joblib"
if not model_path.exists():
logger.warning(
"model.joblib not found for '%s' at %s", model_name, model_path
)
self._status[model_name] = "not_loaded"
return
model_data["model"] = joblib.load(model_path)
# Optional: vectorizer.joblib
vectorizer_path = model_dir / "vectorizer.joblib"
if vectorizer_path.exists():
model_data["vectorizer"] = joblib.load(vectorizer_path)
# Optional: label_encoder.joblib
label_encoder_path = model_dir / "label_encoder.joblib"
if label_encoder_path.exists():
model_data["label_encoder"] = joblib.load(label_encoder_path)
# Optional: feature_columns.json
feature_columns_path = model_dir / "feature_columns.json"
if feature_columns_path.exists():
with open(feature_columns_path, "r", encoding="utf-8") as f:
model_data["feature_columns"] = json.load(f)
# Optional: metrics.json (stored as metadata)
metrics_path = model_dir / "metrics.json"
if metrics_path.exists():
with open(metrics_path, "r", encoding="utf-8") as f:
self._metadata[model_name] = json.load(f)
self._models[model_name] = model_data
self._status[model_name] = "loaded"
logger.info("Model '%s' loaded successfully from %s", model_name, model_dir)
except Exception:
logger.exception("Failed to load model '%s' from %s", model_name, model_dir)
self._status[model_name] = "error"
def get_model(self, model_name: str) -> dict[str, Any]:
"""Retrieve a loaded model by name. Triggers lazy load if not yet loaded.
Returns a dict containing 'model' and optional 'vectorizer',
'label_encoder', 'feature_columns' keys.
Raises ModelNotLoadedError if artifact is missing/corrupted after load attempt.
"""
if model_name not in self._status:
raise ModelNotLoadedError(
f"Model '{model_name}' is not registered in the registry."
)
# Lazy loading: attempt to load if not yet loaded
if self._status[model_name] == "not_loaded":
self._load_model(model_name)
if self._status[model_name] != "loaded":
raise ModelNotLoadedError(
f"Model '{model_name}' is not available (status: {self._status[model_name]})."
)
return self._models[model_name]
def get_status(self, model_name: str) -> str:
"""Return 'loaded', 'not_loaded', or 'error' for a given model."""
return self._status.get(model_name, "not_loaded")
def get_metadata(self, model_name: str) -> dict | None:
"""Return metrics.json content for a model, or None if not available."""
return self._metadata.get(model_name)
def get_all_status(self) -> dict[str, dict]:
"""Return status + metadata for all registered models."""
result: dict[str, dict] = {}
for model_name in REGISTERED_MODELS:
entry: dict[str, Any] = {
"status": self._status.get(model_name, "not_loaded"),
}
metadata = self._metadata.get(model_name)
if metadata:
entry["metadata"] = metadata
result[model_name] = entry
return result
def is_loaded(self, model_name: str) -> bool:
"""Check if a model is loaded and ready for inference."""
return self._status.get(model_name) == "loaded"