This commit is contained in:
2026-04-24 22:20:15 +03:30
parent f7dc05dc9e
commit 569d520a5c
24 changed files with 687 additions and 152 deletions
+13 -8
View File
@@ -173,19 +173,20 @@ def _merge_fertilization_response(
def get_fertilization_recommendation(
sensor_uuid: str,
farm_uuid: str | None = None,
plant_name: str | None = None,
growth_stage: str | None = None,
query: str | None = None,
config: RAGConfig | None = None,
limit: int = 8,
sensor_uuid: str | None = None,
) -> dict:
"""
توصیه کودهی برای یک سنسور (کاربر).
توصیه کودهی برای یک مزرعه.
از RAG با پایگاه دانش fertilization استفاده می‌کند.
Args:
sensor_uuid: شناسه سنسور کاربر
farm_uuid: شناسه مزرعه
plant_name: نام گیاه (برای بارگذاری مشخصات از جدول Plant)
growth_stage: مرحله رشد گیاه
query: سوال اختیاری
@@ -193,7 +194,7 @@ def get_fertilization_recommendation(
limit: تعداد چانک‌های بازیابی‌شده
Returns:
dict با کلیدهای fertilizer_needed, fertilizer_type, amount_kg_per_hectare, reason, npk_status, raw_response
dict ساختاریافته برای توصیه کودهی
"""
cfg = config or load_rag_config()
service = get_service_config(SERVICE_ID, cfg)
@@ -209,12 +210,16 @@ def get_fertilization_recommendation(
client = get_chat_client(service_cfg)
model = service.llm.model
resolved_farm_uuid = str(farm_uuid or sensor_uuid or "").strip()
if not resolved_farm_uuid:
raise ValueError("farm_uuid is required.")
user_query = query or "توصیه کودهی برای مزرعه من چیست؟"
sensor = (
SensorData.objects.select_related("center_location")
.prefetch_related("plants")
.filter(farm_uuid=sensor_uuid)
.filter(farm_uuid=resolved_farm_uuid)
.first()
)
resolved_plant_name = plant_name
@@ -246,7 +251,7 @@ def get_fertilization_recommendation(
)
context = build_rag_context(
user_query, sensor_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID,
user_query, resolved_farm_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID,
)
extra_parts: list[str] = []
@@ -276,7 +281,7 @@ def get_fertilization_recommendation(
{"role": "user", "content": user_query},
]
audit_log = _create_audit_log(
farm_uuid=sensor_uuid,
farm_uuid=resolved_farm_uuid,
service_id=SERVICE_ID,
model=model,
query=user_query,
@@ -291,7 +296,7 @@ def get_fertilization_recommendation(
)
raw = response.choices[0].message.content.strip()
except Exception as exc:
logger.error("Fertilization recommendation error for %s: %s", sensor_uuid, exc)
logger.error("Fertilization recommendation error for %s: %s", resolved_farm_uuid, exc)
result = _build_fertilization_fallback(optimized_result=optimized_result)
result["error"] = f"خطا در دریافت توصیه: {exc}"
result["raw_response"] = None
+13 -8
View File
@@ -219,20 +219,21 @@ def _persist_irrigation_method_on_farm(
def get_irrigation_recommendation(
sensor_uuid: str,
farm_uuid: str | None = None,
plant_name: str | None = None,
growth_stage: str | None = None,
irrigation_method_name: str | None = None,
query: str | None = None,
config: RAGConfig | None = None,
limit: int = 8,
sensor_uuid: str | None = None,
) -> dict:
"""
توصیه آبیاری برای یک سنسور (کاربر).
توصیه آبیاری برای یک مزرعه.
از RAG با پایگاه دانش irrigation استفاده می‌کند.
Args:
sensor_uuid: شناسه سنسور کاربر
farm_uuid: شناسه مزرعه
plant_name: نام گیاه (برای بارگذاری مشخصات از جدول Plant)
growth_stage: مرحله رشد گیاه
irrigation_method_name: نام روش آبیاری (برای بارگذاری از جدول IrrigationMethod)
@@ -241,7 +242,7 @@ def get_irrigation_recommendation(
limit: تعداد چانک‌های بازیابی‌شده
Returns:
dict با کلیدهای irrigation_needed, amount_mm, reason, next_check_date, raw_response
dict ساختاریافته برای توصیه آبیاری
"""
cfg = config or load_rag_config()
service = get_service_config(SERVICE_ID, cfg)
@@ -257,12 +258,16 @@ def get_irrigation_recommendation(
client = get_chat_client(service_cfg)
model = service.llm.model
resolved_farm_uuid = str(farm_uuid or sensor_uuid or "").strip()
if not resolved_farm_uuid:
raise ValueError("farm_uuid is required.")
user_query = query or "توصیه آبیاری برای مزرعه من چیست؟"
sensor = (
SensorData.objects.select_related("center_location", "irrigation_method")
.prefetch_related("plants")
.filter(farm_uuid=sensor_uuid)
.filter(farm_uuid=resolved_farm_uuid)
.first()
)
irrigation_method = _resolve_irrigation_method(sensor, irrigation_method_name)
@@ -309,7 +314,7 @@ def get_irrigation_recommendation(
)
context = build_rag_context(
user_query, sensor_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID,
user_query, resolved_farm_uuid, config=cfg, limit=limit, kb_name=KB_NAME, service_id=SERVICE_ID,
)
extra_parts: list[str] = []
@@ -360,7 +365,7 @@ def get_irrigation_recommendation(
{"role": "user", "content": user_query},
]
audit_log = _create_audit_log(
farm_uuid=sensor_uuid,
farm_uuid=resolved_farm_uuid,
service_id=SERVICE_ID,
model=model,
query=user_query,
@@ -375,7 +380,7 @@ def get_irrigation_recommendation(
)
raw = response.choices[0].message.content.strip()
except Exception as exc:
logger.error("Irrigation recommendation error for %s: %s", sensor_uuid, exc)
logger.error("Irrigation recommendation error for %s: %s", resolved_farm_uuid, exc)
result = _build_irrigation_fallback(
optimized_result=optimized_result,
daily_water_needs=daily_water_needs,
+4 -4
View File
@@ -20,7 +20,7 @@ def rag_ingest_task(recreate: bool = True):
@app.task(bind=True)
def irrigation_recommendation_task(
self,
sensor_uuid: str,
farm_uuid: str,
plant_name: str | None = None,
growth_stage: str | None = None,
irrigation_method_name: str | None = None,
@@ -38,7 +38,7 @@ def irrigation_recommendation_task(
meta={"message": "در حال پردازش توصیه آبیاری..."},
)
result = get_irrigation_recommendation(
sensor_uuid=sensor_uuid,
farm_uuid=farm_uuid,
plant_name=plant_name,
growth_stage=growth_stage,
irrigation_method_name=irrigation_method_name,
@@ -51,7 +51,7 @@ def irrigation_recommendation_task(
@app.task(bind=True)
def fertilization_recommendation_task(
self,
sensor_uuid: str,
farm_uuid: str,
plant_name: str | None = None,
growth_stage: str | None = None,
query: str | None = None,
@@ -68,7 +68,7 @@ def fertilization_recommendation_task(
meta={"message": "در حال پردازش توصیه کودهی..."},
)
result = get_fertilization_recommendation(
sensor_uuid=sensor_uuid,
farm_uuid=farm_uuid,
plant_name=plant_name,
growth_stage=growth_stage,
query=query,
+6 -5
View File
@@ -1,9 +1,10 @@
from unittest.mock import patch
from django.test import TestCase
from django.test import TestCase, override_settings
from rest_framework.test import APIClient
@override_settings(ROOT_URLCONF="config.test_urls")
class RagRecommendationApiTests(TestCase):
def setUp(self):
self.client = APIClient()
@@ -21,7 +22,7 @@ class RagRecommendationApiTests(TestCase):
response = self.client.post(
"/api/rag/recommend/irrigation/",
data={
"sensor_uuid": "sensor-123",
"farm_uuid": "sensor-123",
"plant_name": "گوجه‌فرنگی",
"growth_stage": "میوه‌دهی",
"irrigation_method_name": "قطره‌ای",
@@ -32,7 +33,7 @@ class RagRecommendationApiTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["data"]["plan"]["frequencyPerWeek"], 3)
mock_get_irrigation_recommendation.assert_called_once_with(
sensor_uuid="sensor-123",
farm_uuid="sensor-123",
plant_name="گوجه‌فرنگی",
growth_stage="میوه‌دهی",
irrigation_method_name="قطره‌ای",
@@ -52,7 +53,7 @@ class RagRecommendationApiTests(TestCase):
response = self.client.post(
"/api/rag/recommend/fertilization/",
data={
"sensor_uuid": "sensor-456",
"farm_uuid": "sensor-456",
"plant_name": "گندم",
"growth_stage": "رویشی",
},
@@ -62,7 +63,7 @@ class RagRecommendationApiTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["data"]["plan"]["npkRatio"], "20-20-20")
mock_get_fertilization_recommendation.assert_called_once_with(
sensor_uuid="sensor-456",
farm_uuid="sensor-456",
plant_name="گندم",
growth_stage="رویشی",
query=None,
+4 -4
View File
@@ -130,7 +130,7 @@ class RecommendationServiceDefaultsTests(TestCase):
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
result = get_irrigation_recommendation(
sensor_uuid=str(self.farm_uuid),
farm_uuid=str(self.farm_uuid),
growth_stage="میوه‌دهی",
)
@@ -176,7 +176,7 @@ class RecommendationServiceDefaultsTests(TestCase):
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
result = get_irrigation_recommendation(
sensor_uuid=str(self.farm_uuid),
farm_uuid=str(self.farm_uuid),
growth_stage="میوه‌دهی",
irrigation_method_name="بارانی",
)
@@ -205,7 +205,7 @@ class RecommendationServiceDefaultsTests(TestCase):
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
result = get_fertilization_recommendation(
sensor_uuid=str(self.farm_uuid),
farm_uuid=str(self.farm_uuid),
growth_stage="رویشی",
)
@@ -232,7 +232,7 @@ class RecommendationServiceDefaultsTests(TestCase):
mock_get_chat_client.return_value.chat.completions.create.return_value = mock_response
result = get_fertilization_recommendation(
sensor_uuid=str(self.farm_uuid),
farm_uuid=str(self.farm_uuid),
growth_stage="رویشی",
)
+2 -2
View File
@@ -8,6 +8,6 @@ from .views import (
urlpatterns = [
path("chat/", ChatView.as_view()),
# path("recommend/irrigation/", IrrigationRecommendationView.as_view(), name="recommend-irrigation"),
# path("recommend/fertilization/", FertilizationRecommendationView.as_view(), name="recommend-fertilization"),
path("recommend/irrigation/", IrrigationRecommendationView.as_view(), name="recommend-irrigation"),
path("recommend/fertilization/", FertilizationRecommendationView.as_view(), name="recommend-fertilization"),
]
+18 -16
View File
@@ -148,7 +148,7 @@ class ChatView(APIView):
class IrrigationRecommendationView(APIView):
"""
توصیه آبیاری به صورت مستقیم.
POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name.
POST با farm_uuid، plant_name، growth_stage، irrigation_method_name.
نتیجه همان لحظه برگشت داده می‌شود.
"""
@@ -162,7 +162,8 @@ class IrrigationRecommendationView(APIView):
request=inline_serializer(
name="IrrigationRecommendationRequest",
fields={
"sensor_uuid": drf_serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)"),
"farm_uuid": drf_serializers.CharField(help_text="شناسه یکتای مزرعه (اجباری)"),
"sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid"),
"plant_name": drf_serializers.CharField(required=False, help_text="نام گیاه"),
"growth_stage": drf_serializers.CharField(required=False, help_text="مرحله رشد گیاه"),
"irrigation_method_name": drf_serializers.CharField(required=False, help_text="نام روش آبیاری"),
@@ -187,7 +188,7 @@ class IrrigationRecommendationView(APIView):
OpenApiExample(
"نمونه درخواست",
value={
"sensor_uuid": "550e8400-e29b-41d4-a716-446655440000",
"farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
"plant_name": "گوجه‌فرنگی",
"growth_stage": "میوه‌دهی",
"irrigation_method_name": "آبیاری قطره‌ای",
@@ -199,23 +200,23 @@ class IrrigationRecommendationView(APIView):
def post(self, request: Request):
from rag.services.irrigation import get_irrigation_recommendation
sensor_uuid = request.data.get("sensor_uuid")
if not sensor_uuid:
farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid")
if not farm_uuid:
return Response(
{"code": 400, "msg": "پارامتر sensor_uuid الزامی است.", "data": None},
{"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
try:
result = get_irrigation_recommendation(
sensor_uuid=str(sensor_uuid),
farm_uuid=str(farm_uuid),
plant_name=request.data.get("plant_name"),
growth_stage=request.data.get("growth_stage"),
irrigation_method_name=request.data.get("irrigation_method_name"),
query=request.data.get("query"),
)
except Exception:
logger.exception("Direct irrigation recommendation failed for sensor %s", sensor_uuid)
logger.exception("Direct irrigation recommendation failed for farm %s", farm_uuid)
return Response(
{"code": 500, "msg": "خطا در تولید توصیه آبیاری.", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
@@ -230,7 +231,7 @@ class IrrigationRecommendationView(APIView):
class FertilizationRecommendationView(APIView):
"""
توصیه کودهی به صورت مستقیم.
POST با sensor_uuid، plant_name، growth_stage.
POST با farm_uuid، plant_name، growth_stage.
نتیجه همان لحظه برگشت داده می‌شود.
"""
@@ -244,7 +245,8 @@ class FertilizationRecommendationView(APIView):
request=inline_serializer(
name="FertilizationRecommendationRequest",
fields={
"sensor_uuid": drf_serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)"),
"farm_uuid": drf_serializers.CharField(help_text="شناسه یکتای مزرعه (اجباری)"),
"sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid"),
"plant_name": drf_serializers.CharField(required=False, help_text="نام گیاه"),
"growth_stage": drf_serializers.CharField(required=False, help_text="مرحله رشد گیاه"),
"query": drf_serializers.CharField(required=False, help_text="سوال اختیاری"),
@@ -268,7 +270,7 @@ class FertilizationRecommendationView(APIView):
OpenApiExample(
"نمونه درخواست",
value={
"sensor_uuid": "550e8400-e29b-41d4-a716-446655440000",
"farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
"plant_name": "گوجه‌فرنگی",
"growth_stage": "رویشی",
},
@@ -279,22 +281,22 @@ class FertilizationRecommendationView(APIView):
def post(self, request: Request):
from rag.services.fertilization import get_fertilization_recommendation
sensor_uuid = request.data.get("sensor_uuid")
if not sensor_uuid:
farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid")
if not farm_uuid:
return Response(
{"code": 400, "msg": "پارامتر sensor_uuid الزامی است.", "data": None},
{"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None},
status=status.HTTP_400_BAD_REQUEST,
)
try:
result = get_fertilization_recommendation(
sensor_uuid=str(sensor_uuid),
farm_uuid=str(farm_uuid),
plant_name=request.data.get("plant_name"),
growth_stage=request.data.get("growth_stage"),
query=request.data.get("query"),
)
except Exception:
logger.exception("Direct fertilization recommendation failed for sensor %s", sensor_uuid)
logger.exception("Direct fertilization recommendation failed for farm %s", farm_uuid)
return Response(
{"code": 500, "msg": "خطا در تولید توصیه کودهی.", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,