UPDATE
This commit is contained in:
@@ -521,13 +521,95 @@ def _build_alert_stats(alerts: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return stats
|
||||
|
||||
|
||||
def _snapshot_metric(ai_snapshot: dict[str, Any] | None, metric_name: str) -> float | None:
|
||||
if not isinstance(ai_snapshot, dict):
|
||||
return None
|
||||
farm_metrics = ai_snapshot.get("farm_metrics") or {}
|
||||
resolved_metrics = farm_metrics.get("resolved_metrics") if isinstance(farm_metrics, dict) else {}
|
||||
if not isinstance(resolved_metrics, dict):
|
||||
return None
|
||||
return safe_number(resolved_metrics.get(metric_name), None)
|
||||
|
||||
|
||||
|
||||
def _snapshot_weather(ai_snapshot: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if not isinstance(ai_snapshot, dict):
|
||||
return {}
|
||||
weather = ai_snapshot.get("weather") or {}
|
||||
forecast = weather.get("forecast") if isinstance(weather, dict) else None
|
||||
return forecast if isinstance(forecast, dict) else {}
|
||||
|
||||
|
||||
|
||||
def _block_metric_alerts(ai_snapshot: dict[str, Any] | None, sensor_id: str) -> list[dict[str, Any]]:
|
||||
alerts: list[dict[str, Any]] = []
|
||||
if not isinstance(ai_snapshot, dict):
|
||||
return alerts
|
||||
for block in ai_snapshot.get("block_metrics") or []:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
block_code = str(block.get("block_code") or "default-block")
|
||||
metrics = block.get("resolved_metrics") or {}
|
||||
moisture = safe_number(metrics.get("soil_moisture"), None)
|
||||
if moisture is not None and moisture < 25:
|
||||
alerts.append(
|
||||
_make_alert(
|
||||
metric_type="moisture",
|
||||
current_value=moisture,
|
||||
threshold_value=25.0,
|
||||
severity="warning" if moisture >= 18 else "danger",
|
||||
duration_hours=1.0,
|
||||
timestamp=_now(),
|
||||
sensor_id=sensor_id,
|
||||
zone_id=block_code,
|
||||
direction="below",
|
||||
metadata={
|
||||
"evaluation_level": "block",
|
||||
"affected_blocks": [block_code],
|
||||
"source": "block_metrics",
|
||||
},
|
||||
)
|
||||
)
|
||||
return alerts
|
||||
|
||||
|
||||
|
||||
def _sub_block_support(ai_snapshot: dict[str, Any] | None, metric_type: str) -> list[dict[str, Any]]:
|
||||
evidence: list[dict[str, Any]] = []
|
||||
if not isinstance(ai_snapshot, dict):
|
||||
return evidence
|
||||
metric_key = {
|
||||
"moisture": "soil_moisture",
|
||||
"ph": "soil_ph",
|
||||
"ec": "electrical_conductivity",
|
||||
}.get(metric_type)
|
||||
if not metric_key:
|
||||
return evidence
|
||||
for sub_block in ai_snapshot.get("sub_block_metrics") or []:
|
||||
if not isinstance(sub_block, dict):
|
||||
continue
|
||||
metrics = sub_block.get("resolved_metrics") or {}
|
||||
value = metrics.get(metric_key)
|
||||
if value is None:
|
||||
continue
|
||||
evidence.append(
|
||||
{
|
||||
"block_code": sub_block.get("block_code") or "default-block",
|
||||
"sub_block_code": sub_block.get("sub_block_code") or "default-sub-block",
|
||||
"metric": metric_key,
|
||||
"value": round(float(value), 2),
|
||||
}
|
||||
)
|
||||
return evidence
|
||||
|
||||
|
||||
|
||||
def build_farm_alerts_tracker(sensor_id: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
|
||||
context = context or {}
|
||||
sensor = context.get("sensor")
|
||||
ai_snapshot = (ai_bundle or {}).get("ai_snapshot") if isinstance(ai_bundle, dict) else None
|
||||
forecasts = context.get("forecasts", [])
|
||||
history = context.get("history", [])
|
||||
|
||||
if sensor is None:
|
||||
if not isinstance(ai_snapshot, dict):
|
||||
return {
|
||||
"totalAlerts": 0,
|
||||
"alerts": [],
|
||||
@@ -537,14 +619,116 @@ def build_farm_alerts_tracker(sensor_id: str, context: dict | None = None, ai_bu
|
||||
"prioritizedAlertSummaries": [],
|
||||
"recommendedOperationalActions": [],
|
||||
"humanReadableExplanations": [],
|
||||
"source_metadata": {"status": "missing", "fallback": "no_ai_snapshot"},
|
||||
}
|
||||
|
||||
alerts = []
|
||||
alerts.extend(_detect_moisture_alert(sensor, history, sensor_id))
|
||||
alerts.extend(_detect_ph_alert(sensor, history, sensor_id))
|
||||
alerts.extend(_detect_ec_alert(sensor, history, sensor_id))
|
||||
alerts.extend(_detect_frost_alert(forecasts, sensor_id))
|
||||
alerts.extend(_detect_fungal_risk(sensor, forecasts, history, sensor_id))
|
||||
|
||||
moisture = _snapshot_metric(ai_snapshot, "soil_moisture")
|
||||
if moisture is not None and moisture < 25:
|
||||
alerts.append(
|
||||
_make_alert(
|
||||
metric_type="moisture",
|
||||
current_value=moisture,
|
||||
threshold_value=25.0,
|
||||
severity="warning" if moisture >= 18 else "danger",
|
||||
duration_hours=1.0,
|
||||
timestamp=_now(),
|
||||
sensor_id=sensor_id,
|
||||
direction="below",
|
||||
metadata={
|
||||
"evaluation_level": "farm",
|
||||
"affected_blocks": [item.get("block_code") for item in (ai_snapshot.get("block_metrics") or [])],
|
||||
"supporting_sub_blocks": _sub_block_support(ai_snapshot, "moisture"),
|
||||
"source": "farm_metrics",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
soil_ph = _snapshot_metric(ai_snapshot, "soil_ph")
|
||||
if soil_ph is not None and not (6.0 <= soil_ph <= 7.8):
|
||||
alerts.append(
|
||||
_make_alert(
|
||||
metric_type="ph",
|
||||
current_value=soil_ph,
|
||||
threshold_value="6.0-7.8",
|
||||
severity="warning" if 5.5 <= soil_ph <= 8.2 else "danger",
|
||||
duration_hours=1.0,
|
||||
timestamp=_now(),
|
||||
sensor_id=sensor_id,
|
||||
direction="below" if soil_ph < 6.0 else "above",
|
||||
metadata={
|
||||
"evaluation_level": "farm",
|
||||
"supporting_sub_blocks": _sub_block_support(ai_snapshot, "ph"),
|
||||
"source": "farm_metrics",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
ec = _snapshot_metric(ai_snapshot, "electrical_conductivity")
|
||||
if ec is not None and ec > 2.5:
|
||||
alerts.append(
|
||||
_make_alert(
|
||||
metric_type="ec",
|
||||
current_value=ec,
|
||||
threshold_value=2.5,
|
||||
severity="warning" if ec <= 3.2 else "danger",
|
||||
duration_hours=1.0,
|
||||
timestamp=_now(),
|
||||
sensor_id=sensor_id,
|
||||
direction="above",
|
||||
metadata={
|
||||
"evaluation_level": "farm",
|
||||
"supporting_sub_blocks": _sub_block_support(ai_snapshot, "ec"),
|
||||
"source": "farm_metrics",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
weather = _snapshot_weather(ai_snapshot)
|
||||
if weather:
|
||||
temp_min = safe_number(weather.get("temperature_min"), None)
|
||||
if temp_min is not None and temp_min < 0:
|
||||
alerts.append(
|
||||
_make_alert(
|
||||
metric_type="temperature",
|
||||
current_value=temp_min,
|
||||
threshold_value=0.0,
|
||||
severity="warning" if temp_min >= -2 else "danger",
|
||||
duration_hours=24.0,
|
||||
timestamp=_now(),
|
||||
sensor_id=sensor_id,
|
||||
direction="below",
|
||||
metadata={
|
||||
"evaluation_level": "farm",
|
||||
"source": "weather_forecast",
|
||||
"weather_policy": "center_location_latest_forecast",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
humidity = safe_number(weather.get("humidity_mean"), None)
|
||||
if humidity is not None and moisture is not None and humidity > 75 and moisture > 60:
|
||||
alerts.append(
|
||||
_make_alert(
|
||||
metric_type="fungal_risk",
|
||||
current_value=humidity,
|
||||
threshold_value=75.0,
|
||||
severity="warning" if humidity <= 85 else "danger",
|
||||
duration_hours=24.0,
|
||||
timestamp=_now(),
|
||||
sensor_id=sensor_id,
|
||||
direction="above",
|
||||
metadata={
|
||||
"evaluation_level": "farm",
|
||||
"supporting_sub_blocks": _sub_block_support(ai_snapshot, "moisture"),
|
||||
"source": "farm_metrics+weather_forecast",
|
||||
"soil_moisture": round(moisture, 2),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
alerts.extend(_block_metric_alerts(ai_snapshot, sensor_id))
|
||||
|
||||
ordered_alerts = _sort_alerts(alerts)
|
||||
clusters = _build_clusters(ordered_alerts)
|
||||
@@ -559,4 +743,9 @@ def build_farm_alerts_tracker(sensor_id: str, context: dict | None = None, ai_bu
|
||||
"prioritizedAlertSummaries": [alert["summary"] for alert in ordered_alerts],
|
||||
"recommendedOperationalActions": [alert["recommended_action"] for alert in ordered_alerts],
|
||||
"humanReadableExplanations": [alert["explanation"] for alert in ordered_alerts],
|
||||
"source_metadata": {
|
||||
"farm_metrics": (ai_snapshot.get("source_metadata") or {}).get("farm_metrics", {}),
|
||||
"weather": ((ai_snapshot.get("weather") or {}).get("source_metadata") or {}),
|
||||
"default_block_policy": (ai_snapshot.get("aggregation_policy") or {}).get("default_block_policy"),
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user