from __future__ import annotations import os from pathlib import Path from typing import Any import joblib from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel, Field from aots_pipeline import DEFAULT_CONFIDENCE_THRESHOLD, predict_records MODEL_PATH = Path(os.getenv("MODEL_PATH", "artifacts/aots_technology_model.joblib")) SPACE_API_KEY = os.getenv("SPACE_API_KEY") class AlertRequest(BaseModel): aots_number: str | None = Field(default=None, description="Ticket or AOTS number.") asset_id: str = Field(default="", description="Asset ID from the alert email.") problem_abstract: str = Field( default="", description="Problem abstract from the alert email.", ) confidence_threshold: float | None = Field( default=None, ge=0.0, le=1.0, description="Optional override for manual review routing.", ) top_k: int = Field( default=3, ge=1, le=5, description="How many ranked predictions to return.", ) class BatchRequest(BaseModel): alerts: list[AlertRequest] confidence_threshold: float | None = Field(default=None, ge=0.0, le=1.0) top_k: int = Field(default=3, ge=1, le=5) app = FastAPI( title="AOTS Technology Predictor", version="1.0.0", description="Predicts the storage/network technology for an AOTS alert.", ) _model_bundle: dict[str, Any] | None = None def get_model_bundle() -> dict[str, Any]: global _model_bundle if _model_bundle is None: if not MODEL_PATH.exists(): raise HTTPException( status_code=503, detail=( f"Model bundle not found at '{MODEL_PATH}'. " "Train the model with train_aots_model.py before calling /predict." ), ) _model_bundle = joblib.load(MODEL_PATH) return _model_bundle def validate_api_key(x_api_key: str | None) -> None: if not SPACE_API_KEY: return if x_api_key != SPACE_API_KEY: raise HTTPException(status_code=401, detail="Invalid or missing API key.") @app.get("/health") def health(x_api_key: str | None = Header(default=None)) -> dict[str, Any]: validate_api_key(x_api_key) model_ready = MODEL_PATH.exists() threshold = DEFAULT_CONFIDENCE_THRESHOLD if model_ready: bundle = get_model_bundle() threshold = bundle.get("confidence_threshold", DEFAULT_CONFIDENCE_THRESHOLD) return { "status": "ok" if model_ready else "degraded", "model_path": str(MODEL_PATH), "model_ready": model_ready, "confidence_threshold": threshold, } @app.post("/predict") def predict( request: AlertRequest, x_api_key: str | None = Header(default=None), ) -> dict[str, Any]: validate_api_key(x_api_key) bundle = get_model_bundle() result = predict_records( bundle=bundle, records=[request.model_dump(exclude_none=True)], confidence_threshold=request.confidence_threshold, top_k=request.top_k, )[0] return result @app.post("/predict-batch") def predict_batch( request: BatchRequest, x_api_key: str | None = Header(default=None), ) -> dict[str, Any]: validate_api_key(x_api_key) bundle = get_model_bundle() results = predict_records( bundle=bundle, records=[alert.model_dump(exclude_none=True) for alert in request.alerts], confidence_threshold=request.confidence_threshold, top_k=request.top_k, ) return {"count": len(results), "results": results} if __name__ == "__main__": import uvicorn uvicorn.run( "app:app", host="0.0.0.0", port=int(os.getenv("PORT", "7860")), reload=False, )