This commit is contained in:
2026-04-30 04:00:07 +03:30
parent 2ab9866323
commit 8159190a84
12 changed files with 886 additions and 4 deletions
+1 -1
View File
@@ -4,4 +4,4 @@ from django.apps import AppConfig
class IrrigationRecommendationConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "irrigation_recommendation"
verbose_name = "Irrigation Recommendation"
verbose_name = "Irrigation Recommendation & Plan Parser"
+39
View File
@@ -66,6 +66,45 @@ class IrrigationRecommendationListItemSerializer(serializers.Serializer):
requested_at = serializers.DateTimeField(source="created_at", read_only=True)
class FreeTextPlanParserRequestSerializer(serializers.Serializer):
message = serializers.CharField(required=False, allow_blank=True, help_text="متن آزاد کاربر.")
answers = serializers.DictField(required=False, help_text="پاسخ های تکمیلی کاربر.")
partial_plan = serializers.DictField(required=False, help_text="داده استخراج شده از مرحله قبل.")
farm_uuid = serializers.UUIDField(
required=False,
allow_null=True,
initial="11111111-1111-1111-1111-111111111111",
help_text="UUID مزرعه برای context اختیاری.",
)
def validate(self, attrs):
has_message = bool((attrs.get("message") or "").strip())
has_answers = isinstance(attrs.get("answers"), dict) and bool(attrs.get("answers"))
has_partial_plan = isinstance(attrs.get("partial_plan"), dict) and bool(attrs.get("partial_plan"))
if not (has_message or has_answers or has_partial_plan):
raise serializers.ValidationError(
{"non_field_errors": ["حداقل یکی از message، answers یا partial_plan باید ارسال شود."]}
)
return attrs
class PlanParserQuestionSerializer(serializers.Serializer):
id = serializers.CharField(required=False, allow_blank=True)
field = serializers.CharField(required=False, allow_blank=True)
question = serializers.CharField(required=False, allow_blank=True)
rationale = serializers.CharField(required=False, allow_blank=True)
class FreeTextPlanParserResponseDataSerializer(serializers.Serializer):
status = serializers.CharField(required=False, allow_blank=True)
status_fa = serializers.CharField(required=False, allow_blank=True)
summary = serializers.CharField(required=False, allow_blank=True)
missing_fields = serializers.ListField(child=serializers.CharField(), required=False)
questions = PlanParserQuestionSerializer(many=True, required=False)
collected_data = serializers.DictField(required=False)
final_plan = serializers.DictField(required=False, allow_null=True)
class IrrigationRecommendResponseDataSerializer(serializers.Serializer):
recommendation_uuid = serializers.UUIDField(read_only=True, required=False)
crop_id = serializers.CharField(read_only=True, required=False, allow_blank=True)
+60
View File
@@ -10,6 +10,7 @@ from farm_hub.models import FarmHub, FarmType
from .models import IrrigationRecommendationRequest
from .views import (
IrrigationMethodListView,
PlanFromTextView,
RecommendView,
RecommendationDetailView,
RecommendationListView,
@@ -89,6 +90,65 @@ class WaterStressViewTests(TestCase):
self.assertEqual(response.data["data"]["farm_uuid"][0], "Farm not found.")
class IrrigationPlanFromTextViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.user = get_user_model().objects.create_user(
username="plan-parser-user",
password="secret123",
email="plan-parser@example.com",
phone_number="09120000005",
)
self.farm_type = FarmType.objects.create(name="گلخانه ای")
self.farm = FarmHub.objects.create(owner=self.user, farm_type=self.farm_type, name="Plan Parser Farm")
@patch("irrigation_recommendation.views.external_api_request")
def test_plan_from_text_proxies_to_ai_service(self, mock_external_api_request):
mock_external_api_request.return_value = AdapterResponse(
status_code=200,
data={
"code": 200,
"msg": "موفق",
"data": {
"status": "completed",
"status_fa": "تکمیل شد",
"summary": "done",
"missing_fields": [],
"questions": [],
"collected_data": {"crop_name": "گوجه فرنگی"},
"final_plan": {"crop_name": "گوجه فرنگی"},
},
},
)
request = self.factory.post(
"/api/irrigation/plan-from-text/",
{"message": "متن برنامه", "farm_uuid": str(self.farm.farm_uuid)},
format="json",
)
force_authenticate(request, user=self.user)
response = PlanFromTextView.as_view()(request)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["data"]["status"], "completed")
mock_external_api_request.assert_called_once_with(
"ai",
"/api/irrigation/plan-from-text/",
method="POST",
payload={"message": "متن برنامه", "farm_uuid": str(self.farm.farm_uuid)},
)
def test_plan_from_text_requires_message_or_answers_or_partial_plan(self):
request = self.factory.post("/api/irrigation/plan-from-text/", {}, format="json")
force_authenticate(request, user=self.user)
response = PlanFromTextView.as_view()(request)
self.assertEqual(response.status_code, 400)
self.assertIn("non_field_errors", response.data)
class IrrigationMethodListViewTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
+2
View File
@@ -3,6 +3,7 @@ from django.urls import path
from .views import (
ConfigView,
IrrigationMethodListView,
PlanFromTextView,
RecommendationDetailView,
RecommendationListView,
RecommendView,
@@ -15,5 +16,6 @@ urlpatterns = [
path("recommendations/<uuid:recommendation_uuid>/", RecommendationDetailView.as_view(), name="irrigation-recommendation-detail"),
path("recommendations/", RecommendationListView.as_view(), name="irrigation-recommendation-list"),
path("recommend/", RecommendView.as_view(), name="irrigation-recommendation-recommend"),
path("plan-from-text/", PlanFromTextView.as_view(), name="irrigation-plan-from-text"),
path("water-stress/", WaterStressView.as_view(), name="irrigation-water-stress"),
]
+38
View File
@@ -20,6 +20,8 @@ from water.views import WaterStressIndexView
from .mock_data import CONFIG_RESPONSE_DATA
from .models import IrrigationRecommendationRequest
from .serializers import (
FreeTextPlanParserRequestSerializer,
FreeTextPlanParserResponseDataSerializer,
IrrigationMethodSerializer,
IrrigationRecommendationListItemSerializer,
IrrigationRecommendationListQuerySerializer,
@@ -353,3 +355,39 @@ class WaterStressView(APIView):
{"code": 200, "msg": "success", "data": stress_payload},
status=status.HTTP_200_OK,
)
class PlanFromTextView(FarmAccessMixin, APIView):
@extend_schema(
tags=["Irrigation Recommendation"],
request=FreeTextPlanParserRequestSerializer,
responses={200: code_response("IrrigationPlanFromTextResponse", data=FreeTextPlanParserResponseDataSerializer())},
)
def post(self, request):
serializer = FreeTextPlanParserRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
payload = serializer.validated_data.copy()
farm_uuid = payload.get("farm_uuid")
if farm_uuid:
farm = self._get_farm(request, farm_uuid)
payload["farm_uuid"] = str(farm.farm_uuid)
adapter_response = external_api_request(
"ai",
"/api/irrigation/plan-from-text/",
method="POST",
payload=payload,
)
response_data = adapter_response.data if isinstance(adapter_response.data, dict) else {"message": str(adapter_response.data)}
if adapter_response.status_code >= 400:
return Response(
{"code": adapter_response.status_code, "msg": "error", "data": response_data},
status=adapter_response.status_code,
)
return Response(
{"code": 200, "msg": response_data.get("msg", "موفق"), "data": response_data.get("data", response_data)},
status=status.HTTP_200_OK,
)