2026-03-22 01:09:09 +03:30
|
|
|
from django.utils import timezone
|
|
|
|
|
|
|
|
|
|
from .ai_bundle import request_dashboard_ai_bundle
|
|
|
|
|
from .card_utils import is_fresh, ttl_for_card
|
|
|
|
|
from .cards.anomaly_detection_card import build_anomaly_detection_card
|
|
|
|
|
from .cards.economic_overview import build_economic_overview
|
|
|
|
|
from .cards.farm_alerts_timeline import build_farm_alerts_timeline
|
|
|
|
|
from .cards.farm_alerts_tracker import build_farm_alerts_tracker
|
|
|
|
|
from .cards.farm_overview_kpis import build_farm_overview_kpis
|
|
|
|
|
from .cards.farm_weather_card import build_farm_weather_card
|
|
|
|
|
from .cards.harvest_prediction_card import build_harvest_prediction_card
|
|
|
|
|
from .cards.ndvi_health_card import build_ndvi_health_card
|
|
|
|
|
from .cards.recommendations_list import build_recommendations_list
|
|
|
|
|
from .cards.sensor_comparison_chart import build_sensor_comparison_chart
|
|
|
|
|
from .cards.sensor_radar_chart import build_sensor_radar_chart
|
|
|
|
|
from .cards.sensor_values_list import build_sensor_values_list
|
|
|
|
|
from .cards.soil_moisture_heatmap import build_soil_moisture_heatmap
|
|
|
|
|
from .cards.water_need_prediction import build_water_need_prediction
|
|
|
|
|
from .cards.yield_prediction_chart import build_yield_prediction_chart
|
|
|
|
|
from .context import load_dashboard_context
|
|
|
|
|
from .models import DashboardCardSnapshot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CARD_BUILDERS = {
|
|
|
|
|
"farmOverviewKpis": build_farm_overview_kpis,
|
|
|
|
|
"farmWeatherCard": build_farm_weather_card,
|
|
|
|
|
"farmAlertsTracker": build_farm_alerts_tracker,
|
|
|
|
|
"sensorValuesList": build_sensor_values_list,
|
|
|
|
|
"sensorRadarChart": build_sensor_radar_chart,
|
|
|
|
|
"sensorComparisonChart": build_sensor_comparison_chart,
|
|
|
|
|
"anomalyDetectionCard": build_anomaly_detection_card,
|
|
|
|
|
"farmAlertsTimeline": build_farm_alerts_timeline,
|
|
|
|
|
"waterNeedPrediction": build_water_need_prediction,
|
|
|
|
|
"harvestPredictionCard": build_harvest_prediction_card,
|
|
|
|
|
"yieldPredictionChart": build_yield_prediction_chart,
|
|
|
|
|
"soilMoistureHeatmap": build_soil_moisture_heatmap,
|
|
|
|
|
"ndviHealthCard": build_ndvi_health_card,
|
|
|
|
|
"recommendationsList": build_recommendations_list,
|
|
|
|
|
"economicOverview": build_economic_overview,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AI_DRIVEN_CARDS = {
|
|
|
|
|
"farmAlertsTimeline",
|
|
|
|
|
"recommendationsList",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-03-22 03:08:27 +03:30
|
|
|
def _farm_profile_from_context(context: dict) -> dict:
|
|
|
|
|
sensor = context.get("sensor")
|
|
|
|
|
location = context.get("location")
|
|
|
|
|
plants = context.get("plants", [])
|
|
|
|
|
irrigation_methods = context.get("irrigation_methods", [])
|
|
|
|
|
|
|
|
|
|
return {
|
2026-04-06 23:50:24 +03:30
|
|
|
"sensor_id": str(getattr(sensor, "farm_uuid", "")) if sensor else "",
|
2026-03-22 03:08:27 +03:30
|
|
|
"crop_type": getattr(plants[0], "name", None) if plants else None,
|
|
|
|
|
"region": {
|
|
|
|
|
"latitude": float(location.latitude) if location else None,
|
|
|
|
|
"longitude": float(location.longitude) if location else None,
|
|
|
|
|
},
|
|
|
|
|
"season": timezone.now().date().isoformat(),
|
|
|
|
|
"farming_method": getattr(irrigation_methods[0], "name", None) if irrigation_methods else None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sensor_trends_payload(sensor_id: str, context: dict) -> dict:
|
|
|
|
|
chart = build_sensor_comparison_chart(sensor_id=sensor_id, context=context, ai_bundle=None)
|
|
|
|
|
return {
|
|
|
|
|
"current_value": chart.get("currentValue"),
|
|
|
|
|
"current_value_quality": chart.get("currentValueQuality"),
|
|
|
|
|
"vs_last_week": chart.get("vsLastWeekValue"),
|
|
|
|
|
"series": chart.get("series", []),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _alerts_payload(sensor_id: str, context: dict) -> dict:
|
|
|
|
|
tracker = build_farm_alerts_tracker(sensor_id=sensor_id, context=context, ai_bundle=None)
|
|
|
|
|
return {
|
|
|
|
|
"total_alerts": tracker.get("totalAlerts", 0),
|
|
|
|
|
"alerts": tracker.get("alerts", []),
|
|
|
|
|
"most_critical_issue": tracker.get("mostCriticalIssue"),
|
|
|
|
|
"clusters": tracker.get("alertClusters", []),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _anomalies_payload(sensor_id: str, context: dict) -> dict:
|
|
|
|
|
anomaly_card = build_anomaly_detection_card(sensor_id=sensor_id, context=context, ai_bundle=None)
|
|
|
|
|
return {
|
|
|
|
|
"anomalies": anomaly_card.get("anomalies", []),
|
|
|
|
|
"interpretation_seed": anomaly_card.get("interpretation"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_ai_payload_request(sensor_id: str, context: dict) -> dict:
|
|
|
|
|
structured_context = {
|
|
|
|
|
"farm_profile": _farm_profile_from_context(context),
|
|
|
|
|
"detected_alerts": _alerts_payload(sensor_id, context),
|
|
|
|
|
"anomaly_events": _anomalies_payload(sensor_id, context),
|
|
|
|
|
"sensor_trends": _sensor_trends_payload(sensor_id, context),
|
|
|
|
|
"timestamps": {
|
|
|
|
|
"generated_at": timezone.now().isoformat(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
system_prompt = (
|
|
|
|
|
"You are an agricultural decision-support assistant. "
|
|
|
|
|
"Use only the structured data provided. "
|
|
|
|
|
"Do not hallucinate sensor values, timestamps, severities, or agronomic events. "
|
|
|
|
|
"Generate concise, actionable outputs.\n\n"
|
|
|
|
|
"For the timeline, explain what happened, when it happened, and why it matters.\n"
|
|
|
|
|
"For recommendations, prioritize by alert severity, time proximity, and potential crop impact.\n"
|
|
|
|
|
"Return recommendation objects with: recommendation_title, explanation, suggested_action, urgency_level, related_alert_id (optional)."
|
|
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
"sensor_id": sensor_id,
|
|
|
|
|
"cards": sorted(AI_DRIVEN_CARDS),
|
|
|
|
|
"system_prompt": system_prompt,
|
|
|
|
|
"structured_context": structured_context,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-03-22 01:09:09 +03:30
|
|
|
def build_dashboard_payload(sensor_id: str) -> dict:
|
|
|
|
|
context = load_dashboard_context(sensor_id)
|
|
|
|
|
if context is None:
|
|
|
|
|
return {}
|
|
|
|
|
|
2026-03-22 03:08:27 +03:30
|
|
|
ai_payload_request = _build_ai_payload_request(sensor_id, context)
|
2026-03-22 01:09:09 +03:30
|
|
|
ai_bundle = request_dashboard_ai_bundle(sensor_id=sensor_id, payload=ai_payload_request)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
card_name: builder(sensor_id=sensor_id, context=context, ai_bundle=ai_bundle)
|
|
|
|
|
for card_name, builder in CARD_BUILDERS.items()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_or_build_card(sensor_id: str, card_name: str, context: dict | None = None, ai_bundle: dict | None = None) -> dict:
|
|
|
|
|
latest_snapshot = (
|
|
|
|
|
DashboardCardSnapshot.objects.filter(sensor_id=sensor_id, card_name=card_name)
|
|
|
|
|
.order_by("-generated_at")
|
|
|
|
|
.first()
|
|
|
|
|
)
|
|
|
|
|
if is_fresh(latest_snapshot):
|
|
|
|
|
return latest_snapshot.payload
|
|
|
|
|
|
|
|
|
|
payload = CARD_BUILDERS[card_name](sensor_id=sensor_id, context=context, ai_bundle=ai_bundle)
|
|
|
|
|
DashboardCardSnapshot.objects.create(
|
|
|
|
|
sensor_id=sensor_id,
|
|
|
|
|
card_name=card_name,
|
|
|
|
|
payload=payload,
|
|
|
|
|
expires_at=timezone.now() + ttl_for_card(card_name),
|
|
|
|
|
source="ai" if card_name in AI_DRIVEN_CARDS else "computed",
|
|
|
|
|
)
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_dashboard_payload_with_cache(sensor_id: str) -> dict:
|
|
|
|
|
context = load_dashboard_context(sensor_id)
|
|
|
|
|
if context is None:
|
|
|
|
|
return {}
|
|
|
|
|
|
2026-03-22 03:08:27 +03:30
|
|
|
ai_payload_request = _build_ai_payload_request(sensor_id, context)
|
2026-03-22 01:09:09 +03:30
|
|
|
ai_bundle = request_dashboard_ai_bundle(sensor_id=sensor_id, payload=ai_payload_request)
|
|
|
|
|
|
|
|
|
|
payload = {}
|
|
|
|
|
for card_name in CARD_BUILDERS:
|
|
|
|
|
payload[card_name] = get_or_build_card(
|
|
|
|
|
sensor_id=sensor_id,
|
|
|
|
|
card_name=card_name,
|
|
|
|
|
context=context,
|
|
|
|
|
ai_bundle=ai_bundle,
|
|
|
|
|
)
|
|
|
|
|
return payload
|