This commit is contained in:
2026-05-13 16:45:54 +03:30
parent 948c062b93
commit 46fe62fa04
96 changed files with 3834 additions and 155 deletions
+197 -8
View File
@@ -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"),
},
}