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", } 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 { "sensor_id": str(getattr(sensor, "uuid_sensor", "")) if sensor else "", "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, } def build_dashboard_payload(sensor_id: str) -> dict: context = load_dashboard_context(sensor_id) if context is None: return {} ai_payload_request = _build_ai_payload_request(sensor_id, context) 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 {} ai_payload_request = _build_ai_payload_request(sensor_id, context) 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