This commit is contained in:
2026-04-29 02:58:45 +03:30
parent 27bdad0111
commit 64abf40be9
6 changed files with 257 additions and 64 deletions
+18 -3
View File
@@ -3,16 +3,31 @@ from rest_framework import serializers
from .models import FarmAlertNotification
class IncomingAlertSerializer(serializers.Serializer):
alert_id = serializers.CharField(required=False, allow_blank=True, help_text="شناسه هشدار")
level = serializers.CharField(required=False, allow_blank=True, help_text="سطح هشدار")
title = serializers.CharField(required=False, allow_blank=True, help_text="عنوان هشدار")
message = serializers.CharField(required=False, allow_blank=True, help_text="متن هشدار")
suggested_action = serializers.CharField(required=False, allow_blank=True, help_text="اقدام پیشنهادی")
source_metric_type = serializers.CharField(required=False, allow_blank=True, help_text="نوع شاخص")
timestamp = serializers.DateTimeField(required=False, allow_null=True, help_text="زمان هشدار")
payload = serializers.JSONField(required=False, help_text="داده تکمیلی هشدار")
class FarmAlertsRequestSerializer(serializers.Serializer):
farm_uuid = serializers.CharField(required=False, help_text="شناسه مزرعه")
sensor_uuid = serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid")
query = serializers.CharField(required=False, allow_blank=True, help_text="سوال اختیاری")
alerts = IncomingAlertSerializer(
many=True,
required=False,
help_text="لیست هشدارهای ورودی که باید در تحلیل RAG در نظر گرفته شوند",
)
def validate(self, attrs):
farm_uuid = attrs.get("farm_uuid") or attrs.get("sensor_uuid")
farm_uuid = attrs.get("farm_uuid")
if not farm_uuid:
raise serializers.ValidationError({"farm_uuid": "farm_uuid الزامی است."})
attrs["farm_uuid"] = farm_uuid
attrs["alerts"] = attrs.get("alerts") or []
return attrs
+41 -7
View File
@@ -29,7 +29,7 @@ KB_NAME = "farm_alerts"
SERVICE_ID = "farm_alerts"
TRACKER_PROMPT = (
"وضعیت هشدارهای مزرعه را فقط بر اساس داده های ساختاریافته، اطلاعات مزرعه، و متون بازیابی شده از پایگاه دانش تحلیل کن. "
"وضعیت هشدارهای مزرعه را فقط بر اساس داده های ساختاریافته، اطلاعات مزرعه، alertهاي ورودي، و متون بازیابی شده از پایگاه دانش تحلیل کن. "
"پاسخ فقط JSON معتبر باشد و این کلیدها را داشته باشد: headline, overview, status_level, notifications. "
"status_level فقط یکی از danger, warning, info باشد. "
"notifications باید آرایه ای از آبجکت ها با کلیدهای level, title, message, suggested_action, source_alert_id, source_metric_type باشد. "
@@ -38,7 +38,7 @@ TRACKER_PROMPT = (
)
TIMELINE_PROMPT = (
"بر اساس داده های هشدار مزرعه، یک timeline عملیاتی بساز. "
"بر اساس داده های هشدار مزرعه و alertهاي ورودي، یک timeline عملیاتی بساز. "
"پاسخ فقط JSON معتبر باشد و این کلیدها را داشته باشد: headline, overview, timeline, notifications. "
"timeline باید آرایه ای از آبجکت ها با کلیدهای timestamp, level, title, description, source_alert_id, source_metric_type باشد. "
"level فقط danger, warning, info باشد. "
@@ -128,7 +128,30 @@ def _farm_profile(context: dict[str, Any], farm_uuid: str) -> dict[str, Any]:
}
def _build_structured_context(farm_uuid: str) -> tuple[dict[str, Any], dict[str, Any]]:
def _normalize_incoming_alerts(alerts: list[dict[str, Any]] | None) -> list[dict[str, Any]]:
normalized: list[dict[str, Any]] = []
for item in alerts or []:
if not isinstance(item, dict):
continue
normalized.append(
{
"alert_id": item.get("alert_id") or None,
"level": item.get("level") or None,
"title": item.get("title") or None,
"message": item.get("message") or None,
"suggested_action": item.get("suggested_action") or None,
"source_metric_type": item.get("source_metric_type") or None,
"timestamp": item.get("timestamp"),
"payload": item.get("payload") if isinstance(item.get("payload"), dict) else {},
}
)
return normalized
def _build_structured_context(
farm_uuid: str,
incoming_alerts: list[dict[str, Any]] | None = None,
) -> tuple[dict[str, Any], dict[str, Any]]:
context = load_farm_context(farm_uuid)
if context is None:
raise ValueError("farm_uuid نامعتبر است یا اطلاعات هشدار مزرعه پیدا نشد.")
@@ -138,6 +161,7 @@ def _build_structured_context(farm_uuid: str) -> tuple[dict[str, Any], dict[str,
"farm_profile": _farm_profile(context, farm_uuid),
"tracker": tracker,
"forecasts": _forecast_summary(context),
"incoming_alerts": _normalize_incoming_alerts(incoming_alerts),
}
return context, structured
@@ -336,8 +360,13 @@ def _llm_response(
raise RuntimeError(f"Farm alerts generation failed for farm {farm_uuid}.") from exc
def get_farm_alerts_tracker(*, farm_uuid: str, query: str | None = None) -> dict[str, Any]:
_, structured_context = _build_structured_context(farm_uuid)
def get_farm_alerts_tracker(
*,
farm_uuid: str,
query: str | None = None,
alerts: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
_, structured_context = _build_structured_context(farm_uuid, incoming_alerts=alerts)
tracker = structured_context["tracker"]
user_query = query or "وضعیت فعلی هشدارهای مزرعه را ارزیابی کن و اگر لازم است notification بساز."
llm_result, raw_response, tone_file = _llm_response(
@@ -368,8 +397,13 @@ def get_farm_alerts_tracker(*, farm_uuid: str, query: str | None = None) -> dict
}
def get_farm_alerts_timeline(*, farm_uuid: str, query: str | None = None) -> dict[str, Any]:
_, structured_context = _build_structured_context(farm_uuid)
def get_farm_alerts_timeline(
*,
farm_uuid: str,
query: str | None = None,
alerts: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
_, structured_context = _build_structured_context(farm_uuid, incoming_alerts=alerts)
tracker = structured_context["tracker"]
user_query = query or "برای هشدارهای مزرعه یک timeline عملیاتی بساز و اگر لازم است notification ثبت کن."
llm_result, raw_response, tone_file = _llm_response(
+1 -2
View File
@@ -1,9 +1,8 @@
from django.urls import path
from .views import FarmAlertsTimelineView, FarmAlertsTrackerView
from .views import FarmAlertsTrackerView
urlpatterns = [
path("tracker/", FarmAlertsTrackerView.as_view(), name="farm-alerts-tracker"),
path("timeline/", FarmAlertsTimelineView.as_view(), name="farm-alerts-timeline"),
]
+14 -51
View File
@@ -6,7 +6,7 @@ from rest_framework.views import APIView
from config.openapi import build_envelope_serializer, build_response
from .serializers import FarmAlertsRequestSerializer
from .services import get_farm_alerts_timeline, get_farm_alerts_tracker
from .services import get_farm_alerts_tracker
FarmAlertsValidationErrorSerializer = build_envelope_serializer(
@@ -18,10 +18,6 @@ FarmAlertsTrackerResponseSerializer = build_envelope_serializer(
"FarmAlertsTrackerResponseSerializer",
data_schema=None,
)
FarmAlertsTimelineResponseSerializer = build_envelope_serializer(
"FarmAlertsTimelineResponseSerializer",
data_schema=None,
)
class FarmAlertsTrackerView(APIView):
@@ -30,7 +26,8 @@ class FarmAlertsTrackerView(APIView):
summary="ارزیابی tracker هشدارهای مزرعه",
description=(
"با دریافت farm_uuid، هشدارهای مزرعه را تحلیل می کند، "
"کانتکست مزرعه را به RAG می فرستد، و notificationهای سطح خطر/هشدار/اطلاع رسانی را در دیتابیس ذخیره می کند."
"کانتکست مزرعه و لیست alertهای ورودی را به RAG می فرستد، "
"و notificationهای سطح خطر/هشدار/اطلاع رسانی را در دیتابیس ذخیره می کند."
),
request=FarmAlertsRequestSerializer,
responses={
@@ -43,6 +40,16 @@ class FarmAlertsTrackerView(APIView):
"نمونه درخواست tracker",
value={
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"alerts": [
{
"alert_id": "soil-moisture-001",
"level": "warning",
"title": "افت رطوبت خاک",
"message": "رطوبت خاک کمتر از حد مطلوب گزارش شده است.",
"suggested_action": "آبیاری اصلاحی بررسی شود.",
"source_metric_type": "moisture",
}
],
},
request_only=True,
),
@@ -60,6 +67,7 @@ class FarmAlertsTrackerView(APIView):
result = get_farm_alerts_tracker(
farm_uuid=validated["farm_uuid"],
query=validated.get("query"),
alerts=validated.get("alerts"),
)
except Exception as exc:
return Response(
@@ -67,48 +75,3 @@ class FarmAlertsTrackerView(APIView):
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response({"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK)
class FarmAlertsTimelineView(APIView):
@extend_schema(
tags=["Farm Alerts"],
summary="دریافت timeline هشدارهای مزرعه",
description=(
"با دریافت farm_uuid، timeline هشدارهای مزرعه را با کمک RAG می سازد "
"و notificationهای استخراج شده را در دیتابیس ذخیره می کند."
),
request=FarmAlertsRequestSerializer,
responses={
200: build_response(FarmAlertsTimelineResponseSerializer, "خروجی timeline هشدارهای مزرعه."),
400: build_response(FarmAlertsValidationErrorSerializer, "پارامتر ورودی نامعتبر است."),
500: build_response(FarmAlertsValidationErrorSerializer, "خطا در تولید timeline هشدارها."),
},
examples=[
OpenApiExample(
"نمونه درخواست timeline",
value={
"farm_uuid": "11111111-1111-1111-1111-111111111111",
},
request_only=True,
),
],
)
def post(self, request):
serializer = FarmAlertsRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
validated = serializer.validated_data
try:
result = get_farm_alerts_timeline(
farm_uuid=validated["farm_uuid"],
query=validated.get("query"),
)
except Exception as exc:
return Response(
{"code": 500, "msg": f"خطا در تولید timeline هشدارها: {exc}", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response({"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK)