This commit is contained in:
2026-04-25 17:22:41 +03:30
parent 569d520a5c
commit aa24fc22b0
124 changed files with 8491 additions and 2582 deletions
View File
+17
View File
@@ -0,0 +1,17 @@
from django.apps import AppConfig
from functools import cached_property
class PestDiseaseConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "pest_disease"
verbose_name = "Pest & Disease"
@cached_property
def risk_summary_service(self):
from .services import build_pest_disease_risk_summary
return build_pest_disease_risk_summary
def get_risk_summary_service(self):
return self.risk_summary_service
+50
View File
@@ -0,0 +1,50 @@
from rest_framework import serializers
class PestDiseaseDetectionRequestSerializer(serializers.Serializer):
farm_uuid = serializers.CharField(required=False, help_text="شناسه یکتای مزرعه")
sensor_uuid = serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid")
plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه")
query = serializers.CharField(required=False, allow_blank=True, help_text="توضیح اختیاری")
image_urls = serializers.JSONField(required=False, help_text="آرایه URL تصاویر")
def validate(self, attrs):
farm_uuid = attrs.get("farm_uuid") or attrs.get("sensor_uuid")
if not farm_uuid:
raise serializers.ValidationError({"farm_uuid": "farm_uuid الزامی است."})
attrs["farm_uuid"] = farm_uuid
return attrs
class PestDiseaseRiskRequestSerializer(serializers.Serializer):
farm_uuid = serializers.CharField(required=False, help_text="شناسه یکتای مزرعه")
sensor_uuid = serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid")
plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه")
growth_stage = serializers.CharField(required=False, allow_blank=True, help_text="مرحله رشد")
query = serializers.CharField(required=False, allow_blank=True, help_text="سوال اختیاری")
def validate(self, attrs):
farm_uuid = attrs.get("farm_uuid") or attrs.get("sensor_uuid")
if not farm_uuid:
raise serializers.ValidationError({"farm_uuid": "farm_uuid الزامی است."})
attrs["farm_uuid"] = farm_uuid
return attrs
class PestDiseaseRiskSummaryRequestSerializer(serializers.Serializer):
farm_uuid = serializers.CharField(required=False, help_text="شناسه یکتای مزرعه")
sensor_uuid = serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid")
def validate(self, attrs):
farm_uuid = attrs.get("farm_uuid") or attrs.get("sensor_uuid")
if not farm_uuid:
raise serializers.ValidationError({"farm_uuid": "farm_uuid الزامی است."})
attrs["farm_uuid"] = farm_uuid
return attrs
class PestDiseaseRiskSummaryResponseSerializer(serializers.Serializer):
farm_uuid = serializers.CharField()
diseaseRisk = serializers.JSONField()
pestRisk = serializers.JSONField()
drivers = serializers.JSONField()
+37
View File
@@ -0,0 +1,37 @@
from __future__ import annotations
from typing import Any
from rag.services import get_pest_disease_risk
def _stats_label(level: str | None) -> str:
if level == "high":
return "بالا"
if level == "medium":
return "متوسط"
return "پایین"
def _normalize_risk_block(block: dict[str, Any] | None) -> dict[str, Any]:
payload = dict(block or {})
payload.setdefault("score", 0.0)
payload.setdefault("level", "low")
payload["statsLabel"] = _stats_label(payload.get("level"))
return payload
def build_pest_disease_risk_summary(*, farm_uuid: str) -> dict[str, Any]:
rag_result = get_pest_disease_risk(farm_uuid=farm_uuid)
return {
"farm_uuid": farm_uuid,
"diseaseRisk": _normalize_risk_block(rag_result.get("disease_risk")),
"pestRisk": _normalize_risk_block(rag_result.get("pest_risk")),
"drivers": {
"keyDrivers": rag_result.get("key_drivers") or [],
"summary": rag_result.get("summary"),
"forecastWindow": rag_result.get("forecast_window"),
"source": "rag",
},
}
+10
View File
@@ -0,0 +1,10 @@
from django.urls import path
from .views import PestDiseaseDetectionView, PestDiseaseRiskSummaryView, PestDiseaseRiskView
urlpatterns = [
path("detect/", PestDiseaseDetectionView.as_view(), name="pest-disease-detect"),
path("risk/", PestDiseaseRiskView.as_view(), name="pest-disease-risk"),
path("risk-summary/", PestDiseaseRiskSummaryView.as_view(), name="pest-disease-risk-summary"),
]
+203
View File
@@ -0,0 +1,203 @@
import json
from drf_spectacular.utils import OpenApiExample, extend_schema
from rest_framework import status
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView
from config.openapi import build_envelope_serializer, build_response
from rag.chat import encode_uploaded_image
from rag.services import get_pest_disease_detection, get_pest_disease_risk
from .serializers import (
PestDiseaseDetectionRequestSerializer,
PestDiseaseRiskRequestSerializer,
PestDiseaseRiskSummaryRequestSerializer,
PestDiseaseRiskSummaryResponseSerializer,
)
PestDiseaseValidationErrorSerializer = build_envelope_serializer(
"PestDiseaseValidationErrorSerializer",
data_required=False,
allow_null=True,
)
PestDiseaseDetectionResponseSerializer = build_envelope_serializer(
"PestDiseaseDetectionResponseSerializer",
data_schema=None,
)
PestDiseaseRiskResponseSerializer = build_envelope_serializer(
"PestDiseaseRiskResponseSerializer",
data_schema=None,
)
PestDiseaseRiskSummaryEnvelopeSerializer = build_envelope_serializer(
"PestDiseaseRiskSummaryEnvelopeSerializer",
PestDiseaseRiskSummaryResponseSerializer,
)
class _ImageMixin:
parser_classes = [JSONParser, MultiPartParser, FormParser]
def _collect_uploaded_images(self, request):
images = []
for uploaded in request.FILES.getlist("images"):
images.append(encode_uploaded_image(uploaded))
single_image = request.FILES.get("image")
if single_image is not None:
images.append(encode_uploaded_image(single_image))
image_urls = request.data.get("image_urls")
if isinstance(image_urls, str) and image_urls.strip():
try:
parsed = json.loads(image_urls)
except (json.JSONDecodeError, ValueError):
parsed = [image_urls]
image_urls = parsed
if isinstance(image_urls, list):
for item in image_urls:
if isinstance(item, str) and item.strip():
images.append({"url": item.strip(), "detail": "auto"})
elif isinstance(item, dict) and isinstance(item.get("url"), str):
images.append({"url": item["url"].strip(), "detail": item.get("detail", "auto")})
return images
class PestDiseaseDetectionView(_ImageMixin, APIView):
@extend_schema(
tags=["Pest & Disease"],
summary="تشخیص آفت یا بیماری از روی تصویر",
description="با دریافت farm_uuid و حداقل یک تصویر، تصویر گیاه را با کمک RAG بررسی می کند و نتیجه تشخیص را برمی گرداند.",
request=PestDiseaseDetectionRequestSerializer,
responses={
200: build_response(PestDiseaseDetectionResponseSerializer, "نتیجه تشخیص آفت/بیماری."),
400: build_response(PestDiseaseValidationErrorSerializer, "پارامتر ورودی نامعتبر است."),
500: build_response(PestDiseaseValidationErrorSerializer, "خطا در تحلیل تصویر گیاه."),
},
examples=[
OpenApiExample(
"نمونه درخواست تشخیص",
value={
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"plant_name": "گوجه فرنگی",
"query": "این برگ ها مشکوک به آفت هستند؟",
"image_urls": ["https://example.com/leaf.jpg"],
},
request_only=True,
),
],
)
def post(self, request):
serializer = PestDiseaseDetectionRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
images = self._collect_uploaded_images(request)
if not images:
return Response(
{"code": 400, "msg": "حداقل یک تصویر باید ارسال شود.", "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
validated = serializer.validated_data
try:
result = get_pest_disease_detection(
farm_uuid=validated["farm_uuid"],
plant_name=validated.get("plant_name"),
query=validated.get("query"),
images=images,
)
except Exception as exc:
return Response(
{"code": 500, "msg": f"خطا در تحلیل تصویر گیاه: {exc}", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response({"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK)
class PestDiseaseRiskView(APIView):
@extend_schema(
tags=["Pest & Disease"],
summary="پیش بینی ریسک آفات و بیماری",
description="با دریافت farm_uuid، داده های مزرعه و پایگاه دانش تخصصی را به RAG می دهد و ریسک آفات و بیماری را برمی گرداند.",
request=PestDiseaseRiskRequestSerializer,
responses={
200: build_response(PestDiseaseRiskResponseSerializer, "خروجی پیش بینی ریسک آفات و بیماری."),
400: build_response(PestDiseaseValidationErrorSerializer, "پارامتر ورودی نامعتبر است."),
500: build_response(PestDiseaseValidationErrorSerializer, "خطا در پیش بینی ریسک آفات و بیماری."),
},
examples=[
OpenApiExample(
"نمونه درخواست ریسک",
value={
"farm_uuid": "11111111-1111-1111-1111-111111111111",
"plant_name": "گوجه فرنگی",
"growth_stage": "گلدهی",
},
request_only=True,
),
],
)
def post(self, request):
serializer = PestDiseaseRiskRequestSerializer(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_pest_disease_risk(
farm_uuid=validated["farm_uuid"],
plant_name=validated.get("plant_name"),
growth_stage=validated.get("growth_stage"),
query=validated.get("query"),
)
except Exception as exc:
return Response(
{"code": 500, "msg": f"خطا در پیش بینی ریسک آفات و بیماری: {exc}", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response({"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK)
class PestDiseaseRiskSummaryView(APIView):
@extend_schema(
tags=["Pest & Disease"],
summary="خلاصه ریسک بیماری و آفات",
description=(
"با دریافت farm_uuid، خلاصه ریسک بیماری و آفات را از سرویس RAG "
"گرفته و در قالب سبک تر برای KPI بازمی گرداند."
),
request=PestDiseaseRiskSummaryRequestSerializer,
responses={
200: build_response(PestDiseaseRiskSummaryEnvelopeSerializer, "خلاصه ریسک بیماری و آفات."),
400: build_response(PestDiseaseValidationErrorSerializer, "پارامتر ورودی نامعتبر است."),
404: build_response(PestDiseaseValidationErrorSerializer, "مزرعه یافت نشد."),
},
examples=[
OpenApiExample(
"نمونه درخواست risk summary",
value={"farm_uuid": "11111111-1111-1111-1111-111111111111"},
request_only=True,
)
],
)
def post(self, request):
from .services import build_pest_disease_risk_summary
serializer = PestDiseaseRiskSummaryRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(
{"code": 400, "msg": "داده نامعتبر.", "data": serializer.errors},
status=status.HTTP_400_BAD_REQUEST,
)
try:
result = build_pest_disease_risk_summary(farm_uuid=serializer.validated_data["farm_uuid"])
except ValueError as exc:
return Response(
{"code": 404, "msg": str(exc), "data": None},
status=status.HTTP_404_NOT_FOUND,
)
return Response({"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK)