From c37b5c855829cadbe2709f105e0c9dbb0b6fa297 Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Sat, 21 Mar 2026 23:50:36 +0330 Subject: [PATCH] AI UPDATE --- TABLES.md | 260 ++++++++++++++++++++++++++++ config/settings.py | 1 + config/tones/fertilization_tone.txt | 22 ++- config/tones/irrigation_tone.txt | 19 +- config/urls.py | 1 + fertilization/__init__.py | 1 + fertilization/apps.py | 7 + fertilization/serializers.py | 20 +++ fertilization/urls.py | 8 + fertilization/views.py | 108 ++++++++++++ irrigation/serializers.py | 30 ++++ irrigation/urls.py | 9 +- irrigation/views.py | 105 ++++++++++- rag/services/fertilization.py | 24 ++- rag/services/irrigation.py | 22 ++- 15 files changed, 614 insertions(+), 23 deletions(-) create mode 100644 TABLES.md create mode 100644 fertilization/__init__.py create mode 100644 fertilization/apps.py create mode 100644 fertilization/serializers.py create mode 100644 fertilization/urls.py create mode 100644 fertilization/views.py diff --git a/TABLES.md b/TABLES.md new file mode 100644 index 0000000..28150e7 --- /dev/null +++ b/TABLES.md @@ -0,0 +1,260 @@ +# مستندات جداول پایگاه داده — CropLogic AI + +این سند تمام جداول (مدل‌های Django) موجود در پروژه را به همراه توضیح ستون‌ها و روابط بین آن‌ها شرح می‌دهد. + +--- + +## فهرست جداول + +| اپ | جدول | توضیح کوتاه | +|---|---|---| +| `location_data` | `SoilLocation` | موقعیت جغرافیایی (lat/lon) | +| `location_data` | `SoilDepthData` | داده‌های خاک به تفکیک عمق | +| `sensor_data` | `SensorData` | آخرین خوانش سنسور برای یک موقعیت | +| `sensor_data` | `SensorDataHistory` | تاریخچه خوانش‌های سنسور | +| `sensor_data` | `SensorParameter` | تعریف پارامترهای سنسور | +| `sensor_data` | `ParameterUpdateLog` | لاگ تغییرات پارامترهای سنسور | +| `weather` | `WeatherParameter` | تعریف پارامترهای هواشناسی | +| `weather` | `WeatherForecast` | پیش‌بینی آب‌وهوای روزانه | +| `plant` | `Plant` | اطلاعات گیاهان | +| `irrigation` | `IrrigationMethod` | روش‌های آبیاری | + +--- + +## اپ: `location_data` + +### جدول `SoilLocation` + +موقعیت‌های جغرافیایی که داده‌های خاک و سنسور به آن‌ها متصل هستند. + +| ستون | نوع | توضیح | +|---|---|---| +| `id` | PK (auto) | شناسه اتوماتیک | +| `latitude` | DecimalField (9,6) | عرض جغرافیایی | +| `longitude` | DecimalField (9,6) | طول جغرافیایی | +| `task_id` | CharField | شناسه تسک Celery در حال پردازش | +| `created_at` | DateTimeField | زمان ایجاد | +| `updated_at` | DateTimeField | آخرین زمان به‌روزرسانی | + +**محدودیت‌ها:** +- ترکیب `(latitude, longitude)` باید یکتا باشد. + +**روابط:** +- ← `SoilDepthData.soil_location` (یک به چند) +- ← `SensorData.location` (یک به چند) +- ← `WeatherForecast.location` (یک به چند) + +--- + +### جدول `SoilDepthData` + +داده‌های خاک از API SoilGrids برای سه عمق مختلف، مرتبط با هر `SoilLocation`. + +| ستون | نوع | توضیح | +|---|---|---| +| `id` | PK (auto) | شناسه اتوماتیک | +| `soil_location` | FK → SoilLocation | موقعیت مکانی مرتبط | +| `depth_label` | CharField | عمق: `0-5cm` / `5-15cm` / `15-30cm` | +| `bdod` | FloatField | چگالی ظاهری خاک (Bulk Density) | +| `cec` | FloatField | ظرفیت تبادل کاتیونی (CEC) | +| `cfvo` | FloatField | درصد حجمی سنگریزه | +| `clay` | FloatField | درصد رس | +| `nitrogen` | FloatField | نیتروژن کل | +| `ocd` | FloatField | تراکم کربن آلی | +| `ocs` | FloatField | ذخیره کربن آلی | +| `phh2o` | FloatField | pH خاک در آب | +| `sand` | FloatField | درصد شن | +| `silt` | FloatField | درصد سیلت | +| `soc` | FloatField | کربن آلی خاک (SOC) | +| `wv0010` | FloatField | رطوبت حجمی در ۱۰ kPa | +| `wv0033` | FloatField | ظرفیت زراعی — رطوبت در ۳۳ kPa | +| `wv1500` | FloatField | نقطه پژمردگی — رطوبت در ۱۵۰۰ kPa | +| `created_at` | DateTimeField | زمان ایجاد | + +**محدودیت‌ها:** +- ترکیب `(soil_location, depth_label)` باید یکتا باشد. + +--- + +## اپ: `sensor_data` + +### جدول `SensorData` + +آخرین خوانش سنسور فیزیکی برای یک موقعیت. هنگام به‌روزرسانی، نسخه قبلی به `SensorDataHistory` منتقل می‌شود. + +| ستون | نوع | توضیح | +|---|---|---| +| `uuid_sensor` | UUIDField (PK) | شناسه یکتای سنسور | +| `location` | FK → SoilLocation | موقعیت مکانی (ستون DB: `location_id`) | +| `soil_moisture` | FloatField | رطوبت خاک | +| `soil_temperature` | FloatField | دمای خاک | +| `soil_ph` | FloatField | pH خاک | +| `electrical_conductivity` | FloatField | هدایت الکتریکی (EC) | +| `nitrogen` | FloatField | ازت (N) | +| `phosphorus` | FloatField | فسفر (P) | +| `potassium` | FloatField | پتاسیم (K) | +| `plants` | M2M → Plant | گیاهان مرتبط با این سنسور | +| `created_at` | DateTimeField | زمان ایجاد | +| `updated_at` | DateTimeField | آخرین زمان به‌روزرسانی | + +--- + +### جدول `SensorDataHistory` + +تاریخچه کامل خوانش‌های سنسور. هر بار که `SensorData` به‌روز می‌شود، نسخه قبلی اینجا ذخیره می‌شود. + +| ستون | نوع | توضیح | +|---|---|---| +| `id` | PK (auto) | شناسه اتوماتیک | +| `uuid_sensor` | UUIDField | شناسه سنسور اصلی | +| `location_id` | IntegerField | شناسه موقعیت مکانی | +| `soil_moisture` | FloatField | رطوبت خاک | +| `soil_temperature` | FloatField | دمای خاک | +| `soil_ph` | FloatField | pH خاک | +| `electrical_conductivity` | FloatField | هدایت الکتریکی | +| `nitrogen` | FloatField | ازت | +| `phosphorus` | FloatField | فسفر | +| `potassium` | FloatField | پتاسیم | +| `recorded_at` | DateTimeField | زمان ثبت در تاریخچه | + +> **نکته:** این جدول FK مستقیم به SoilLocation ندارد تا در صورت حذف موقعیت، تاریخچه حفظ شود. + +--- + +### جدول `SensorParameter` + +کاتالوگ پارامترهای قابل اندازه‌گیری توسط سنسورها. + +| ستون | نوع | توضیح | +|---|---|---| +| `id` | PK (auto) | شناسه اتوماتیک | +| `code` | CharField (unique) | کد یکتا (مثال: `soil_moisture`) | +| `name_fa` | CharField | نام فارسی پارامتر | +| `unit` | CharField | واحد اندازه‌گیری | +| `created_at` | DateTimeField | زمان ایجاد | + +--- + +### جدول `ParameterUpdateLog` + +لاگ تغییرات (افزودن یا ویرایش) پارامترهای سنسور. + +| ستون | نوع | توضیح | +|---|---|---| +| `id` | PK (auto) | شناسه اتوماتیک | +| `parameter` | FK → SensorParameter | پارامتر مرتبط | +| `action` | CharField | نوع عملیات: `added` یا `modified` | +| `updated_at` | DateTimeField | زمان ثبت لاگ | + +--- + +## اپ: `weather` + +### جدول `WeatherParameter` + +کاتالوگ پارامترهای هواشناسی تعریف‌شده در سیستم. + +| ستون | نوع | توضیح | +|---|---|---| +| `id` | PK (auto) | شناسه اتوماتیک | +| `code` | CharField (unique) | کد یکتا (مثال: `temperature_max`) | +| `name_fa` | CharField | نام فارسی پارامتر | +| `unit` | CharField | واحد اندازه‌گیری | +| `created_at` | DateTimeField | زمان ایجاد | + +--- + +### جدول `WeatherForecast` + +پیش‌بینی روزانه آب‌وهوا (تا ۷ روز آینده) برای هر موقعیت مکانی. + +| ستون | نوع | توضیح | +|---|---|---| +| `id` | PK (auto) | شناسه اتوماتیک | +| `location` | FK → SoilLocation | موقعیت مکانی مرتبط | +| `forecast_date` | DateField | تاریخ پیش‌بینی | +| `temperature_min` | FloatField | حداقل دمای هوا (°C) | +| `temperature_max` | FloatField | حداکثر دمای هوا (°C) | +| `temperature_mean` | FloatField | میانگین دمای هوا (°C) | +| `precipitation` | FloatField | مجموع بارش (mm) | +| `precipitation_probability` | FloatField | احتمال بارش (%) | +| `humidity_mean` | FloatField | میانگین رطوبت نسبی (%) | +| `wind_speed_max` | FloatField | حداکثر سرعت باد (km/h) | +| `et0` | FloatField | تبخیر-تعرق مرجع ET₀ (mm/day) | +| `weather_code` | IntegerField | کد وضعیت آب‌وهوا (WMO) | +| `fetched_at` | DateTimeField | آخرین زمان واکشی از API | +| `created_at` | DateTimeField | زمان ایجاد | + +**محدودیت‌ها:** +- ترکیب `(location, forecast_date)` باید یکتا باشد. + +**Property:** +- `will_rain` → `True` اگر `precipitation > 0` + +--- + +## اپ: `plant` + +### جدول `Plant` + +اطلاعات گیاهان شامل شرایط کاشت، نگهداری و برداشت. + +| ستون | نوع | توضیح | +|---|---|---| +| `id` | PK (auto) | شناسه اتوماتیک | +| `name` | CharField (unique) | نام گیاه | +| `light` | CharField | نور مورد نیاز | +| `watering` | CharField | نیاز آبیاری | +| `soil` | CharField | نوع خاک مناسب | +| `temperature` | CharField | دمای مناسب رشد | +| `planting_season` | CharField | فصل کاشت | +| `harvest_time` | CharField | زمان برداشت | +| `spacing` | CharField | فاصله کاشت | +| `fertilizer` | CharField | کود مناسب | +| `created_at` | DateTimeField | زمان ایجاد | +| `updated_at` | DateTimeField | آخرین زمان به‌روزرسانی | + +**روابط:** +- ← `SensorData.plants` (M2M از طریق جدول واسط) + +--- + +## اپ: `irrigation` + +### جدول `IrrigationMethod` + +مشخصات فنی روش‌های مختلف آبیاری. + +| ستون | نوع | توضیح | +|---|---|---| +| `id` | PK (auto) | شناسه اتوماتیک | +| `name` | CharField (unique) | نام روش آبیاری | +| `category` | CharField | دسته‌بندی (موضعی / تحت فشار / سطحی) | +| `description` | TextField | توضیحات کامل | +| `water_efficiency_percent` | FloatField | راندمان مصرف آب (%) | +| `water_pressure_required` | CharField | فشار مورد نیاز | +| `flow_rate` | CharField | دبی / میزان جریان آب | +| `coverage_area` | CharField | مساحت قابل پوشش | +| `soil_type` | CharField | نوع خاک مناسب | +| `climate_suitability` | CharField | اقلیم مناسب | +| `created_at` | DateTimeField | زمان ایجاد | +| `updated_at` | DateTimeField | آخرین زمان به‌روزرسانی | + +--- + +## نمودار روابط (خلاصه) + +``` +SoilLocation + ├── SoilDepthData (1:N — depth_label: 0-5cm, 5-15cm, 15-30cm) + ├── SensorData (1:N — uuid_sensor PK) + │ └── Plant (M:N — جدول واسط) + └── WeatherForecast (1:N — یکتا per location+date) + +SensorParameter + └── ParameterUpdateLog (1:N — action: added/modified) + +Plant (مستقل — از طریق M2M به SensorData متصل) +IrrigationMethod (مستقل — بدون FK) +WeatherParameter (مستقل — کاتالوگ) +``` diff --git a/config/settings.py b/config/settings.py index 5036317..c2a3467 100644 --- a/config/settings.py +++ b/config/settings.py @@ -31,6 +31,7 @@ INSTALLED_APPS = [ "weather", "plant", "irrigation", + "fertilization", ] MIDDLEWARE = [ diff --git a/config/tones/fertilization_tone.txt b/config/tones/fertilization_tone.txt index 66b63a1..3eabee0 100644 --- a/config/tones/fertilization_tone.txt +++ b/config/tones/fertilization_tone.txt @@ -1,8 +1,24 @@ # لحن توصیه کودهی سبک پاسخ: -- تخصصی و دقیق: نوع کود، مقدار و زمان مصرف را مشخص کن +- تخصصی و دقیق: نسبت NPK، مقدار در هکتار، روش مصرف و فاصله زمانی را مشخص کن - بر اساس داده‌های NPK خاک، pH، و نوع محصول -- فرمت خروجی: JSON با فیلدهای fertilizer_needed (bool), fertilizer_type (str), amount_kg_per_hectare (float), reason (str), npk_status (dict) -- اگر سطح NPK خاک مناسب باشد، کودهی لازم نیست +- فرمت خروجی حتماً JSON باشد و دقیقاً به شکل زیر: +{ + "plan": { + "npkRatio": "", + "amountPerHectare": "", + "applicationMethod": "", + "applicationInterval": "", + "reasoning": "" + } +} +- فقط JSON خروجی بده، بدون توضیح اضافی +- اگر سطح NPK خاک مناسب باشد، در reasoning ذکر کن و مقدار کمتر پیشنهاد بده - هشدارهای مهم درباره مصرف بیش از حد کود را ذکر کن +- npkRatio بر اساس مرحله رشد گیاه و وضعیت خاک تعیین شود +- amountPerHectare بر اساس نوع خاک و نیاز گیاه +- applicationMethod بر اساس نوع کود و شرایط مزرعه +- applicationInterval بر اساس مرحله رشد و سرعت جذب +- reasoning باید شامل تحلیل EC خاک، pH، و مواد آلی باشد +- مقادیر را به انگلیسی و با واحد بنویس (مثل kg/ha) diff --git a/config/tones/irrigation_tone.txt b/config/tones/irrigation_tone.txt index 16db07f..cdac272 100644 --- a/config/tones/irrigation_tone.txt +++ b/config/tones/irrigation_tone.txt @@ -1,8 +1,23 @@ # لحن توصیه آبیاری سبک پاسخ: -- مستقیم و عملیاتی: زمان، مقدار و روش آبیاری را مشخص کن +- مستقیم و عملیاتی: زمان، مدت، تعداد دفعات و روش آبیاری را مشخص کن - بر اساس داده‌های هواشناسی (بارش، ET0، دما) و رطوبت خاک -- فرمت خروجی: JSON با فیلدهای irrigation_needed (bool), amount_mm (float), reason (str), next_check_date (str) +- فرمت خروجی حتماً JSON باشد و دقیقاً به شکل زیر: +{ + "plan": { + "frequencyPerWeek": , + "durationMinutes": , + "bestTimeOfDay": "", + "moistureLevel": , + "warning": "" + } +} +- فقط JSON خروجی بده، بدون توضیح اضافی - اگر بارش پیش‌بینی شده باشد، آبیاری را به تعویق بینداز - اگر رطوبت خاک کافی است، آبیاری لازم نیست +- هشدارها را در فیلد warning قرار بده +- مقادیر عددی را بر اساس نوع گیاه، روش آبیاری و مرحله رشد محاسبه کن +- bestTimeOfDay باید بر اساس شرایط آب و هوایی و فصل تعیین شود +- frequencyPerWeek بر اساس نیاز آبی گیاه و شرایط خاک +- durationMinutes بر اساس روش آبیاری و ظرفیت خاک diff --git a/config/urls.py b/config/urls.py index da3134e..37d9074 100644 --- a/config/urls.py +++ b/config/urls.py @@ -19,4 +19,5 @@ urlpatterns = [ path("api/sensor-data/", include("sensor_data.urls")), path("api/plants/", include("plant.urls")), path("api/irrigation/", include("irrigation.urls")), + path("api/fertilization/", include("fertilization.urls")), ] diff --git a/fertilization/__init__.py b/fertilization/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/fertilization/__init__.py @@ -0,0 +1 @@ + diff --git a/fertilization/apps.py b/fertilization/apps.py new file mode 100644 index 0000000..5e02cf9 --- /dev/null +++ b/fertilization/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class FertilizationConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "fertilization" + verbose_name = "Fertilization" diff --git a/fertilization/serializers.py b/fertilization/serializers.py new file mode 100644 index 0000000..554145a --- /dev/null +++ b/fertilization/serializers.py @@ -0,0 +1,20 @@ +from rest_framework import serializers + + +class FertilizationRecommendRequestSerializer(serializers.Serializer): + """سریالایزر ورودی برای درخواست توصیه کودهی.""" + + sensor_uuid = serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)") + 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="سوال اختیاری") + + +class FertilizationPlanSerializer(serializers.Serializer): + """سریالایزر خروجی برای پلن توصیه کودهی.""" + + npkRatio = serializers.CharField(help_text="نسبت NPK مثل 20-20-20 (NPK)") + amountPerHectare = serializers.CharField(help_text="مقدار مصرف در هکتار مثل 150 kg/ha") + applicationMethod = serializers.CharField(help_text="روش مصرف کود") + applicationInterval = serializers.CharField(help_text="فاصله زمانی مصرف") + reasoning = serializers.CharField(help_text="توضیح دقیق دلیل انتخاب برنامه کودهی") diff --git a/fertilization/urls.py b/fertilization/urls.py new file mode 100644 index 0000000..b882f16 --- /dev/null +++ b/fertilization/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from .views import FertilizationRecommendView, FertilizationRecommendStatusView + +urlpatterns = [ + path("recommend/", FertilizationRecommendView.as_view(), name="fertilization-recommend"), + path("recommend//status/", FertilizationRecommendStatusView.as_view(), name="fertilization-recommend-status"), +] diff --git a/fertilization/views.py b/fertilization/views.py new file mode 100644 index 0000000..044d487 --- /dev/null +++ b/fertilization/views.py @@ -0,0 +1,108 @@ +from drf_spectacular.utils import ( + OpenApiExample, + OpenApiResponse, + extend_schema, +) +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView + +from .serializers import FertilizationRecommendRequestSerializer + + +class FertilizationRecommendView(APIView): + """ + توصیه کودهی با Celery. + POST با sensor_uuid، plant_name، growth_stage. + اطلاعات گیاه از plant app دریافت می‌شود. + نیازی به دریافت نوع آبیاری نیست. + """ + + @extend_schema( + tags=["Fertilization Recommendation"], + summary="درخواست توصیه کودهی", + description=( + "داده‌های سنسور و گیاه را دریافت کرده و یک تسک Celery " + "برای تولید توصیه کودهی در صف قرار می‌دهد. " + "اطلاعات گیاه از جدول Plant بارگذاری می‌شود." + ), + request=FertilizationRecommendRequestSerializer, + responses={ + 202: OpenApiResponse(description="تسک در صف قرار گرفت"), + 400: OpenApiResponse(description="پارامتر ورودی نامعتبر"), + }, + examples=[ + OpenApiExample( + "نمونه درخواست", + value={ + "sensor_uuid": "550e8400-e29b-41d4-a716-446655440000", + "plant_name": "گوجه‌فرنگی", + "growth_stage": "گلدهی", + }, + request_only=True, + ), + ], + ) + def post(self, request): + from rag.tasks import fertilization_recommendation_task + + serializer = FertilizationRecommendRequestSerializer(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 + sensor_uuid = validated["sensor_uuid"] + plant_name = validated.get("plant_name") + growth_stage = validated.get("growth_stage") + query = validated.get("query") + + task = fertilization_recommendation_task.delay( + sensor_uuid=sensor_uuid, + plant_name=plant_name, + growth_stage=growth_stage, + query=query, + ) + return Response( + { + "code": 202, + "msg": "تسک توصیه کودهی در صف قرار گرفت.", + "data": { + "task_id": task.id, + "status_url": f"/api/fertilization/recommend/{task.id}/status/", + }, + }, + status=status.HTTP_202_ACCEPTED, + ) + + +class FertilizationRecommendStatusView(APIView): + """وضعیت تسک توصیه کودهی.""" + + @extend_schema( + tags=["Fertilization Recommendation"], + summary="وضعیت تسک توصیه کودهی", + description="وضعیت تسک Celery توصیه کودهی را برمی‌گرداند.", + responses={ + 200: OpenApiResponse(description="وضعیت تسک"), + }, + ) + def get(self, request, task_id): + from celery.result import AsyncResult + + result = AsyncResult(task_id) + data = {"task_id": task_id, "status": result.state} + if result.state == "PENDING": + data["message"] = "تسک در صف یا یافت نشد." + elif result.state == "PROGRESS": + data["progress"] = result.info + elif result.state == "SUCCESS": + data["result"] = result.result + elif result.state == "FAILURE": + data["error"] = str(result.result) + return Response( + {"code": 200, "msg": "success", "data": data}, + status=status.HTTP_200_OK, + ) diff --git a/irrigation/serializers.py b/irrigation/serializers.py index f11f4b5..60fef6d 100644 --- a/irrigation/serializers.py +++ b/irrigation/serializers.py @@ -23,3 +23,33 @@ class IrrigationMethodSerializer(serializers.ModelSerializer): "updated_at", ] read_only_fields = ["id", "created_at", "updated_at"] + + +class IrrigationRecommendRequestSerializer(serializers.Serializer): + """سریالایزر ورودی برای درخواست توصیه آبیاری.""" + + sensor_uuid = serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)") + plant_name = serializers.CharField(required=False, allow_blank=True, help_text="نام گیاه") + growth_stage = serializers.CharField(required=False, allow_blank=True, help_text="مرحله رشد گیاه") + irrigation_method_name = serializers.CharField( + required=False, allow_blank=True, help_text="نام روش آبیاری" + ) + query = serializers.CharField(required=False, allow_blank=True, help_text="سوال اختیاری") + + +class IrrigationPlanSerializer(serializers.Serializer): + """سریالایزر خروجی برای پلن توصیه آبیاری.""" + + frequencyPerWeek = serializers.IntegerField(help_text="تعداد دفعات آبیاری در هفته") + durationMinutes = serializers.IntegerField(help_text="مدت هر بار آبیاری به دقیقه") + bestTimeOfDay = serializers.CharField(help_text="بهترین زمان آبیاری") + moistureLevel = serializers.IntegerField(help_text="سطح رطوبت مطلوب خاک به درصد") + warning = serializers.CharField(help_text="هشدار یا توصیه مهم", allow_blank=True) + + +class IrrigationRecommendResponseSerializer(serializers.Serializer): + """سریالایزر خروجی برای ریسپانس توصیه آبیاری.""" + + code = serializers.IntegerField() + msg = serializers.CharField() + data = serializers.DictField(child=serializers.JSONField()) diff --git a/irrigation/urls.py b/irrigation/urls.py index 97308ce..a013015 100644 --- a/irrigation/urls.py +++ b/irrigation/urls.py @@ -1,8 +1,15 @@ from django.urls import path -from .views import IrrigationMethodDetailView, IrrigationMethodListCreateView +from .views import ( + IrrigationMethodDetailView, + IrrigationMethodListCreateView, + IrrigationRecommendView, + IrrigationRecommendStatusView, +) urlpatterns = [ path("", IrrigationMethodListCreateView.as_view(), name="irrigation-list-create"), path("/", IrrigationMethodDetailView.as_view(), name="irrigation-detail"), + path("recommend/", IrrigationRecommendView.as_view(), name="irrigation-recommend"), + path("recommend//status/", IrrigationRecommendStatusView.as_view(), name="irrigation-recommend-status"), ] diff --git a/irrigation/views.py b/irrigation/views.py index 72682f6..2c7488f 100644 --- a/irrigation/views.py +++ b/irrigation/views.py @@ -8,7 +8,10 @@ from rest_framework.response import Response from rest_framework.views import APIView from .models import IrrigationMethod -from .serializers import IrrigationMethodSerializer +from .serializers import ( + IrrigationMethodSerializer, + IrrigationRecommendRequestSerializer, +) class IrrigationMethodListCreateView(APIView): @@ -28,6 +31,106 @@ class IrrigationMethodListCreateView(APIView): status=status.HTTP_200_OK, ) + +class IrrigationRecommendView(APIView): + """ + توصیه آبیاری با Celery. + POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name. + اطلاعات گیاه از plant app و روش آبیاری از irrigation app دریافت می‌شود. + """ + + @extend_schema( + tags=["Irrigation Recommendation"], + summary="درخواست توصیه آبیاری", + description=( + "داده‌های سنسور، گیاه و روش آبیاری را دریافت کرده و یک تسک Celery " + "برای تولید توصیه آبیاری در صف قرار می‌دهد. " + "اطلاعات گیاه از جدول Plant و روش آبیاری از جدول IrrigationMethod بارگذاری می‌شود." + ), + request=IrrigationRecommendRequestSerializer, + responses={ + 202: OpenApiResponse(description="تسک در صف قرار گرفت"), + 400: OpenApiResponse(description="پارامتر ورودی نامعتبر"), + }, + examples=[ + OpenApiExample( + "نمونه درخواست", + value={ + "sensor_uuid": "550e8400-e29b-41d4-a716-446655440000", + "plant_name": "گوجه‌فرنگی", + "growth_stage": "گلدهی", + "irrigation_method_name": "آبیاری قطره‌ای", + }, + request_only=True, + ), + ], + ) + def post(self, request): + from rag.tasks import irrigation_recommendation_task + + serializer = IrrigationRecommendRequestSerializer(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 + sensor_uuid = validated["sensor_uuid"] + plant_name = validated.get("plant_name") + growth_stage = validated.get("growth_stage") + irrigation_method_name = validated.get("irrigation_method_name") + query = validated.get("query") + + task = irrigation_recommendation_task.delay( + sensor_uuid=sensor_uuid, + plant_name=plant_name, + growth_stage=growth_stage, + irrigation_method_name=irrigation_method_name, + query=query, + ) + return Response( + { + "code": 202, + "msg": "تسک توصیه آبیاری در صف قرار گرفت.", + "data": { + "task_id": task.id, + "status_url": f"/api/irrigation/recommend/{task.id}/status/", + }, + }, + status=status.HTTP_202_ACCEPTED, + ) + + +class IrrigationRecommendStatusView(APIView): + """وضعیت تسک توصیه آبیاری.""" + + @extend_schema( + tags=["Irrigation Recommendation"], + summary="وضعیت تسک توصیه آبیاری", + description="وضعیت تسک Celery توصیه آبیاری را برمی‌گرداند.", + responses={ + 200: OpenApiResponse(description="وضعیت تسک"), + }, + ) + def get(self, request, task_id): + from celery.result import AsyncResult + + result = AsyncResult(task_id) + data = {"task_id": task_id, "status": result.state} + if result.state == "PENDING": + data["message"] = "تسک در صف یا یافت نشد." + elif result.state == "PROGRESS": + data["progress"] = result.info + elif result.state == "SUCCESS": + data["result"] = result.result + elif result.state == "FAILURE": + data["error"] = str(result.result) + return Response( + {"code": 200, "msg": "success", "data": data}, + status=status.HTTP_200_OK, + ) + @extend_schema( tags=["Irrigation"], summary="ایجاد روش آبیاری جدید", diff --git a/rag/services/fertilization.py b/rag/services/fertilization.py index d5a878f..dc7155e 100644 --- a/rag/services/fertilization.py +++ b/rag/services/fertilization.py @@ -17,10 +17,18 @@ KB_NAME = "fertilization" DEFAULT_FERTILIZATION_PROMPT = ( "بر اساس داده‌های خاک (NPK، pH)، مشخصات گیاه، مرحله رشد و پایگاه دانش کودهی، " "یک توصیه کودهی دقیق بده. " - "پاسخ حتماً به فرمت JSON با فیلدهای زیر باشد:\n" - "fertilizer_needed (bool), fertilizer_type (str), amount_kg_per_hectare (float), " - "reason (str), npk_status (dict با کلیدهای nitrogen, phosphorus, potassium و مقادیر low/normal/high)\n" - "فقط JSON خروجی بده، بدون توضیح اضافی." + "پاسخ حتماً به فرمت JSON با ساختار زیر باشد:\n" + '{\n' + ' "plan": {\n' + ' "npkRatio": "",\n' + ' "amountPerHectare": "",\n' + ' "applicationMethod": "",\n' + ' "applicationInterval": "",\n' + ' "reasoning": ""\n' + ' }\n' + '}\n' + "فقط JSON خروجی بده، بدون توضیح اضافی. " + "مقادیر را بر اساس شرایط واقعی خاک و گیاه محاسبه کن." ) @@ -101,11 +109,9 @@ def get_fertilization_recommendation( result = json.loads(cleaned) except (json.JSONDecodeError, ValueError): result = { - "fertilizer_needed": None, - "fertilizer_type": None, - "amount_kg_per_hectare": None, - "reason": raw, - "npk_status": None, + "plan": { + "reasoning": raw, + }, } result["raw_response"] = raw diff --git a/rag/services/irrigation.py b/rag/services/irrigation.py index dfd1566..08c4686 100644 --- a/rag/services/irrigation.py +++ b/rag/services/irrigation.py @@ -17,9 +17,18 @@ KB_NAME = "irrigation" DEFAULT_IRRIGATION_PROMPT = ( "بر اساس داده‌های خاک، هواشناسی، مشخصات گیاه، روش آبیاری و پایگاه دانش آبیاری، " "یک توصیه آبیاری دقیق بده. " - "پاسخ حتماً به فرمت JSON با فیلدهای زیر باشد:\n" - "irrigation_needed (bool), amount_mm (float), reason (str), next_check_date (str)\n" - "فقط JSON خروجی بده، بدون توضیح اضافی." + "پاسخ حتماً به فرمت JSON با ساختار زیر باشد:\n" + '{\n' + ' "plan": {\n' + ' "frequencyPerWeek": ,\n' + ' "durationMinutes": ,\n' + ' "bestTimeOfDay": "",\n' + ' "moistureLevel": ,\n' + ' "warning": ""\n' + ' }\n' + '}\n' + "فقط JSON خروجی بده، بدون توضیح اضافی. " + "مقادیر عددی را بر اساس شرایط واقعی محاسبه کن." ) @@ -105,10 +114,9 @@ def get_irrigation_recommendation( result = json.loads(cleaned) except (json.JSONDecodeError, ValueError): result = { - "irrigation_needed": None, - "amount_mm": None, - "reason": raw, - "next_check_date": None, + "plan": { + "warning": raw, + }, } result["raw_response"] = raw