hvac-agents / backend /rules /context.py
subhan07's picture
Phase 2: simulation API, writes off, Dataset feeder on (part 2)
0e9df3d verified
Raw History Blame Contribute Delete
4.49 kB
"""Build Rule Engine evaluation context from telemetry + limits."""
from __future__ import annotations
import os
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional
from backend.agents.runtime.safety import load_limits
from backend.config.engineering_limits import get_limits_config
from backend.services.canonical_telemetry_service import latest_points
from backend.services.hvac_safety_contract import is_safe_mode
def _now() -> datetime:
return datetime.now(timezone.utc).replace(tzinfo=None)
def _latest_normalized(zone_id: str, building_id: Optional[str]) -> Optional[Dict[str, Any]]:
try:
from backend.services.ai_normalized_telemetry import build_ai_records
end = _now()
start = end - timedelta(minutes=30)
payload = build_ai_records(
zone_id=zone_id or "ZONE-01",
t0=start.isoformat(),
t1=end.isoformat(),
step_seconds=60,
building_id=building_id,
)
for row in reversed(payload.get("records") or []):
q = str(row.get("quality") or "").upper()
if q in ("GOOD", "STALE"):
return row
except Exception:
return None
return None
def _point_lookup(building_id: Optional[str]) -> Dict[str, Dict[str, Any]]:
out: Dict[str, Dict[str, Any]] = {}
try:
for p in latest_points(building_id, limit=500):
pid = str(p.get("point_id") or "")
if pid:
out[pid] = p
except Exception:
pass
return out
def build_rule_context(raw: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""Normalize caller payload into a full Rule Engine context."""
ctx = dict(raw or {})
building_id = ctx.get("building_id") or os.getenv("HVAC_DEFAULT_BUILDING_ID") or "bldg-corp-hq-01"
zone_id = ctx.get("zone_id") or "ZONE-01"
point_id = ctx.get("point_id")
action = str(ctx.get("action") or "EVALUATE").upper()
points = _point_lookup(building_id)
point_row = points.get(str(point_id or ""), {}) if point_id else {}
old_value = ctx.get("old_value")
if old_value is None and point_row.get("value") is not None:
old_value = point_row.get("value")
new_value = ctx.get("new_value")
if new_value is None:
new_value = ctx.get("target_value")
normalized = ctx.get("normalized")
if normalized is None:
normalized = _latest_normalized(zone_id, building_id)
engineering = get_limits_config()
eng_limits = load_limits(building_id)
telemetry = ctx.get("telemetry") or {}
if not telemetry and point_row:
telemetry = {
"source": point_row.get("source"),
"quality": point_row.get("quality"),
"age_seconds": point_row.get("age_seconds"),
"value": point_row.get("value"),
}
if not telemetry and normalized:
telemetry = {
"source": normalized.get("Source") or normalized.get("source"),
"quality": normalized.get("quality"),
"age_seconds": 0,
}
return {
**ctx,
"action": action,
"building_id": building_id,
"zone_id": zone_id,
"point_id": point_id,
"old_value": old_value,
"new_value": new_value,
"target_value": new_value,
"current_value": old_value if ctx.get("current_value") is None else ctx.get("current_value"),
"opportunity_id": ctx.get("opportunity_id") or ctx.get("opportunity") or ctx.get("id"),
"normalized": normalized,
"points": points,
"point_row": point_row,
"engineering_limits_db": eng_limits,
"engineering_config": engineering,
"telemetry": telemetry,
"safe_mode": is_safe_mode(),
"decision": ctx.get("decision") or "OPTIMIZE",
"supervisory": ctx.get("supervisory") or {"decision": ctx.get("decision") or "OPTIMIZE"},
"safety": ctx.get("safety") or {"status": "PASS", "passed": True},
"confidence": ctx.get("confidence") if ctx.get("confidence") is not None else 0.9,
"approval_status": ctx.get("approval_status"),
"user": ctx.get("user") or {},
"strict": os.getenv("HVAC_RULE_ENGINE_STRICT", "1").strip() not in ("0", "false", "FALSE"),
"skip_audit": bool(ctx.get("skip_audit")),
}