diff --git a/config/tones/farm_alerts_tone.txt b/config/tones/farm_alerts_tone.txt index 6f937ae..9566166 100644 --- a/config/tones/farm_alerts_tone.txt +++ b/config/tones/farm_alerts_tone.txt @@ -53,7 +53,9 @@ } قواعد تکميلي: -- اگر هشدار مهمي وجود ندارد، آرايه هاي `notifications` يا `timeline` را خالي برگردان. +- اگر در کانتکست، notificationهاي قبلي براي بازه اي بين 1 روز تا 7 روز گذشته ارسال شده بود، آن ها را بررسي کن و notification تکراري يا هم معنا دوباره نساز. +- فقط وقتي notification جديد بساز که وضعيت واقعا جديد باشد، شدت هشدار تغيير معنادار کرده باشد، يا اقدام پيشنهادي جديد و مهم لازم باشد. +- اگر هشدار مهمي وجود ندارد، آرايه هاي `notifications` يا `timeline` را خالي برگردان و در `headline` و `overview` شفاف بگو که فعلا مورد مهمي براي اعلام وجود ندارد. - `headline` و `overview` هميشه الزامي هستند. - عنوان ها کوتاه و عملياتي باشند. - `suggested_action` بايد يک اقدام مشخص مزرعه اي باشد، نه توصيه کلي. diff --git a/docs/farm_alerts_tracker_api.md b/docs/farm_alerts_tracker_api.md new file mode 100644 index 0000000..9d0a68e --- /dev/null +++ b/docs/farm_alerts_tracker_api.md @@ -0,0 +1,180 @@ +# راهنمای استفاده از API هشدارهای مزرعه + +این سند نحوه کار با API فعال هشدارهای مزرعه را توضیح می‌دهد. + +## Endpoint فعال + +- `POST /api/farm-alerts/tracker/` + +نکته: +- endpoint `POST /api/farm-alerts/timeline/` حذف شده و دیگر قابل استفاده نیست. + +## کاربرد API + +این API با دریافت `farm_uuid` و یک لیست از `alerts`: + +- وضعیت فعلی هشدارهای مزرعه را تحلیل می‌کند +- context مزرعه را همراه با alertهای ارسالی به RAG می‌فرستد +- فقط notificationهای مهم را تولید می‌کند +- notificationهای تولیدشده را در دیتابیس ذخیره می‌کند + +## ساختار درخواست + +فیلدهای ورودی: + +- `farm_uuid`: شناسه مزرعه +- `alerts`: لیست alertهای ورودی برای تحلیل + +فیلد `farm_uuid` الزامی است. + +## ساختار هر alert + +هر آیتم داخل `alerts` می‌تواند این فیلدها را داشته باشد: + +- `alert_id`: شناسه هشدار +- `level`: سطح هشدار مثل `info` یا `warning` یا `danger` +- `title`: عنوان هشدار +- `message`: توضیح هشدار +- `suggested_action`: اقدام پیشنهادی +- `source_metric_type`: نوع شاخص مثل `moisture` +- `timestamp`: زمان هشدار با فرمت datetime +- `payload`: داده تکمیلی به صورت JSON object + +همه فیلدهای داخل هر alert اختیاری هستند، ولی بهتر است برای تحلیل دقیق‌تر حداقل `title` یا `message` و در صورت امکان `level` و `source_metric_type` ارسال شوند. + +## نمونه درخواست + +```json +{ + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "alerts": [ + { + "alert_id": "soil-moisture-001", + "level": "warning", + "title": "افت رطوبت خاک", + "message": "رطوبت خاک کمتر از حد مطلوب گزارش شده است.", + "suggested_action": "آبیاری اصلاحی بررسی شود.", + "source_metric_type": "moisture", + "timestamp": "2025-02-14T09:30:00Z", + "payload": { + "window": "3d", + "current_value": 38.5, + "threshold": 45 + } + }, + { + "alert_id": "fungal-risk-002", + "level": "danger", + "title": "ریسک قارچی بالا", + "message": "شرایط محیطی برای بیماری قارچی شدید شده است.", + "suggested_action": "بازدید و اقدام پیشگیرانه فوری انجام شود.", + "source_metric_type": "fungal_risk", + "timestamp": "2025-02-14T10:00:00Z", + "payload": { + "humidity": 89, + "duration_hours": 18 + } + } + ] +} +``` + +## نمونه درخواست با curl + +```bash +curl -X POST http://localhost:8000/api/farm-alerts/tracker/ \ + -H "Content-Type: application/json" \ + -d '{ + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "alerts": [ + { + "alert_id": "soil-moisture-001", + "level": "warning", + "title": "افت رطوبت خاک", + "message": "رطوبت خاک کمتر از حد مطلوب گزارش شده است.", + "suggested_action": "آبیاری اصلاحی بررسی شود.", + "source_metric_type": "moisture" + } + ] + }' +``` + +## ساختار پاسخ موفق + +پاسخ HTTP با envelope زیر برمی‌گردد: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "service_id": "farm_alerts", + "tracker": {}, + "headline": "جمع بندی کوتاه وضعیت هشدارها", + "overview": "توضیح کوتاه و اجرایی از مهم ترین وضعیت مزرعه", + "status_level": "warning", + "notifications": [ + { + "id": 12, + "farm_uuid": "11111111-1111-1111-1111-111111111111", + "endpoint": "tracker", + "level": "warning", + "title": "افت رطوبت خاک", + "message": "تنش رطوبتی در مزرعه ادامه دارد.", + "suggested_action": "آبیاری جبرانی کوتاه مدت اجرا شود.", + "source_alert_id": "soil-moisture-001", + "source_metric_type": "moisture", + "payload": {}, + "created_at": "2025-02-14T10:15:00+00:00", + "updated_at": "2025-02-14T10:15:00+00:00" + } + ], + "raw_llm_response": "{\"headline\":\"...\"}", + "structured_context": {} + } +} +``` + +## وضعیت‌های خطا + +### خطای ورودی نامعتبر + +اگر `farm_uuid` ارسال نشود: + +```json +{ + "code": 400, + "msg": "داده نامعتبر.", + "data": { + "farm_uuid": [ + "farm_uuid الزامی است." + ] + } +} +``` + +### خطای داخلی + +اگر در مرحله تحلیل RAG یا تولید پاسخ خطایی رخ دهد: + +```json +{ + "code": 500, + "msg": "خطا در تولید tracker هشدارها: ...", + "data": null +} +``` + +## نکات رفتاری API + +- اگر `alerts` ارسال نشود، API آن را به صورت آرایه خالی در نظر می‌گیرد. +- notificationهای ساخته‌شده برای endpoint `tracker` در دیتابیس ذخیره می‌شوند. +- مدل باید notification تکراری نسازد و اگر مورد مهمی وجود نداشته باشد، خروجی notification می‌تواند خالی باشد. +- تحلیل فقط روی endpoint `tracker` فعال است. + +## پیشنهاد برای مصرف‌کننده API + +- برای هر alert یک `alert_id` پایدار بفرستید تا ردیابی و جلوگیری از تکرار بهتر انجام شود. +- برای alertهای حساس، `timestamp` و `source_metric_type` را حتما ارسال کنید. +- اگر داده تکمیلی دارید، آن را داخل `payload` بفرستید تا RAG context کامل‌تر شود. diff --git a/farm_alerts/serializers.py b/farm_alerts/serializers.py index 4f10216..27c70a7 100644 --- a/farm_alerts/serializers.py +++ b/farm_alerts/serializers.py @@ -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 diff --git a/farm_alerts/services.py b/farm_alerts/services.py index f7e18bb..c63c01d 100644 --- a/farm_alerts/services.py +++ b/farm_alerts/services.py @@ -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( diff --git a/farm_alerts/urls.py b/farm_alerts/urls.py index 4befa81..f93c9b8 100644 --- a/farm_alerts/urls.py +++ b/farm_alerts/urls.py @@ -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"), ] diff --git a/farm_alerts/views.py b/farm_alerts/views.py index cc83925..0240e01 100644 --- a/farm_alerts/views.py +++ b/farm_alerts/views.py @@ -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)