411sst/SensorLens-backend
0
1import logging2from datetime import datetime3 4from fastapi import FastAPI, HTTPException5from fastapi.middleware.cors import CORSMiddleware6 7from data_loader import load_dataset, FEATURE_COLUMNS8from detector import IsolationForestDetector9from explainer import GroqExplainer10from models import (11 AnalyzeRequest,12 AnalyzeResponse,13 ExplainRequest,14 ExplainResponse,15 QueryRequest,16 QueryResponse,17)18 19logger = logging.getLogger(__name__)20 21app = FastAPI(title="SensorLens API")22 23app.add_middleware(24 CORSMiddleware,25 allow_origins=["*"],26 allow_credentials=True,27 allow_methods=["*"],28 allow_headers=["*"],29)30 31# Module-level state32df = None33detector = None34cache = {"params": None, "result": None}35 36 37@app.on_event("startup")38def startup() -> None:39 """Load the dataset and initialize the Isolation Forest detector on startup."""40 global df, detector41 df = load_dataset()42 detector = IsolationForestDetector(df)43 44 45@app.get("/health")46def health() -> dict:47 """Return a health check with current timestamp.48 49 Returns:50 Dict with status "ok" and ISO-format timestamp.51 """52 return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}53 54 55@app.get("/dataset")56def dataset() -> list[dict]:57 """Return the first 100 rows of the dataset as a list of dicts.58 59 Returns:60 List of row dicts for the frontend table preview.61 """62 return df.head(100).to_dict(orient="records")63 64 65@app.get("/dataset/full")66def dataset_full() -> list[dict]:67 """Return all 10,000 rows of the dataset for visualizations.68 69 Returns:70 List of all row dicts.71 """72 return df.to_dict(orient="records")73 74 75@app.get("/dataset/stats")76def dataset_stats() -> dict:77 """Return full-dataset statistics for all 10,000 rows.78 79 Returns:80 Dict with total_rows, feature_means, and failure_rate.81 """82 feature_means = {col: float(df[col].mean()) for col in FEATURE_COLUMNS}83 failure_rate = float(df["machine_failure"].sum() / len(df) * 100)84 return {85 "total_rows": len(df),86 "feature_means": feature_means,87 "failure_rate": failure_rate,88 }89 90 91@app.post("/analyze", response_model=AnalyzeResponse)92def analyze(req: AnalyzeRequest) -> dict:93 """Run Isolation Forest anomaly detection with caching.94 95 Args:96 req: Analysis parameters including features, contamination, and model config.97 98 Returns:99 AnalyzeResponse with anomaly rows, scores, and cache status.100 """101 cache_key = (102 tuple(sorted(req.features)),103 req.contamination,104 req.n_estimators,105 str(req.max_samples),106 )107 108 if cache["params"] == cache_key:109 result = cache["result"].copy()110 result["cached"] = True111 return result112 113 flags, scores = detector.detect(114 req.features, req.contamination, req.n_estimators, req.max_samples115 )116 anomalies = detector.build_anomaly_rows(flags, scores, df)117 118 result = {119 "total_rows": len(df),120 "anomaly_count": len(anomalies),121 "contamination_used": req.contamination,122 "features": req.features,123 "anomalies": anomalies,124 "all_scores": scores.tolist(),125 "cached": False,126 }127 128 cache["params"] = cache_key129 cache["result"] = result130 131 return result132 133 134@app.post("/explain", response_model=ExplainResponse)135def explain(req: ExplainRequest) -> dict:136 """Generate LLM explanations for anomaly rows via Groq.137 138 Args:139 req: Request containing list of anomaly row dicts.140 141 Returns:142 ExplainResponse with a list of {row_id, explanation} dicts.143 """144 try:145 explainer = GroqExplainer()146 except RuntimeError as e:147 raise HTTPException(status_code=422, detail=str(e))148 explanations = explainer.explain_anomalies(req.anomalies)149 return {"explanations": explanations}150 151 152@app.post("/query", response_model=QueryResponse)153def query(req: QueryRequest) -> dict:154 """Answer a natural language question using cached anomaly data.155 156 Args:157 req: Request with the question string and optional context_rows.158 159 Returns:160 QueryResponse with the LLM's answer.161 """162 if cache["result"] is None:163 raise HTTPException(status_code=400, detail="Run analysis first")164 try:165 explainer = GroqExplainer()166 except RuntimeError as e:167 raise HTTPException(status_code=422, detail=str(e))168 try:169 answer = explainer.answer_query(req.question, cache["result"], req.context_rows)170 return {"answer": answer}171 except ValueError as e:172 raise HTTPException(status_code=400, detail=str(e))173 except RuntimeError as e:174 logger.error("Groq query endpoint failed: %s", e)175 detail = str(e)176 if "rate limit" in detail.lower():177 raise HTTPException(status_code=429, detail=detail)178 raise HTTPException(status_code=502, detail=detail)179 180 