File size: 5,474 Bytes
95c00ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1bd74e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95c00ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
"""Shared HVAC ML HTTP API. Training datasets are never LIVE_BMS."""
from __future__ import annotations

from typing import Any, Dict, Optional

from fastapi import APIRouter
from pydantic import BaseModel, Field

from backend.ml.prediction.service import list_models, model_status, predict
from backend.ml.registry.service import list_datasets, list_maps, opportunity_health, register_datasets
from backend.ml.training.pipeline import train_all

router = APIRouter(prefix="/api/ml", tags=["ml"])


class PredictBody(BaseModel):
    opportunity_id: str
    agent_id: Optional[str] = None
    building_id: Optional[str] = None
    features: Dict[str, Any] = Field(default_factory=dict)


@router.get("/datasets")
def get_datasets():
    return {"datasets": list_datasets(), "source": "TRAINING_DATASET"}


@router.post("/datasets/register")
def post_register():
    return {"datasets": register_datasets(), "source": "TRAINING_DATASET"}


@router.get("/datasets/{dataset_id}")
def get_dataset(dataset_id: str):
    for row in list_datasets():
        if row["id"] == dataset_id:
            return row
    return {"status": "NOT_FOUND", "id": dataset_id}


@router.post("/datasets/{dataset_id}/validate")
def post_validate(dataset_id: str):
    rows = {r["id"]: r for r in list_datasets()}
    row = rows.get(dataset_id)
    if not row:
        return {"status": "NOT_FOUND", "id": dataset_id}
    return {"status": row["status"], "source": "TRAINING_DATASET", "dataset": row}


@router.get("/maps")
def get_maps(opportunity_id: Optional[str] = None):
    return {"maps": list_maps(opportunity_id)}


@router.get("/health")
def get_health():
    try:
        return opportunity_health()
    except Exception as exc:
        from backend.ml.features.maps import AGENT_FOR, OPPORTUNITY_MAPS

        maps = {m["opportunity_id"]: m for m in OPPORTUNITY_MAPS}
        rows = []
        for i in range(1, 21):
            oid = f"O{i}"
            primary = maps.get(oid)
            trainable = bool(primary and primary.get("training_allowed") and primary.get("target_column"))
            rows.append(
                {
                    "opportunity_id": oid,
                    "agent_id": (primary or {}).get("agent_id") or AGENT_FOR.get(oid),
                    "dataset_id": (primary or {}).get("dataset_id"),
                    "dataset_name": None,
                    "dataset_status": None,
                    "dataset_quality": None,
                    "feature_map": (primary or {}).get("feature_map") or {},
                    "target": (primary or {}).get("target_column"),
                    "task_type": (primary or {}).get("task_type"),
                    "training_allowed": trainable,
                    "notes": (primary or {}).get("notes") or "No legitimate mapping.",
                    "missing_dataset": (primary or {}).get("missing_dataset"),
                    "model_id": None,
                    "model_version": None,
                    "status": "MODEL_NOT_TRAINABLE" if oid == "O10" or not trainable else "MODEL_NOT_AVAILABLE",
                    "validation_status": "N/A",
                    "metrics": None,
                    "last_trained": None,
                    "prediction_availability": "UNAVAILABLE",
                    "provenance": "TRAINING DATA",
                    "last_prediction": None,
                    "training_run": None,
                }
            )
        return {"opportunities": rows, "source": "TRAINING_DATASET", "datasets": [], "degraded": type(exc).__name__}


@router.get("/models")
def get_models():
    return {"models": list_models(), "source": "TRAINING_DATASET"}


@router.get("/models/{opportunity_id}")
def get_model(opportunity_id: str):
    return model_status(opportunity_id)


@router.post("/train")
def post_train():
    from backend.ml.features.maps import trainable_maps

    return {"runs": train_all(), "trainable_maps": len(trainable_maps())}


@router.post("/predict")
def post_predict(body: PredictBody):
    return predict(body.opportunity_id, features=body.features, agent_id=body.agent_id, building_id=body.building_id)


@router.post("/batch-predict")
def post_batch(body: Dict[str, Any]):
    items = body.get("items") or []
    return {"results": [predict(it.get("opportunity_id"), features=it.get("features") or {}, persist=False) for it in items]}


@router.get("/predictions/{opportunity_id}")
def get_predictions(opportunity_id: str):
    from database.models_ml import MLPredictionDB
    from database.session import SessionLocal

    db = SessionLocal()
    try:
        rows = (
            db.query(MLPredictionDB)
            .filter_by(opportunity_id=opportunity_id.upper())
            .order_by(MLPredictionDB.created_at.desc())
            .limit(50)
            .all()
        )
        return {
            "predictions": [
                {
                    "id": r.id,
                    "status": r.status,
                    "provenance": r.provenance,
                    "source": r.source,
                    "prediction": r.prediction_json,
                    "confidence": r.confidence,
                    "created_at": r.created_at.isoformat() if r.created_at else None,
                }
                for r in rows
            ]
        }
    finally:
        db.close()