From 0f36f985138b7c30a89550b0895ff2bb5417454e Mon Sep 17 00:00:00 2001 From: Mohammad Sajad Pourajam Date: Sun, 26 Apr 2026 01:15:38 +0330 Subject: [PATCH] UPDATE --- API_REFERENCE_FA.md | 1565 +++++++++++++++++ API_RELIABILITY_AUDIT_FA.md | 20 - APPS_URLS_AUDIT.md | 3 - config/settings.py | 1 - config/tones/chat_tone.txt | 61 +- docker-compose-prod.yaml | 28 +- docker-compose.yaml | 36 +- fertilization/views.py | 4 +- .../test_reporting_and_ai_api_flow.py | 67 +- irrigation/views.py | 4 +- rag/tests/test_recommendation_api.py | 77 - rag/urls.py | 4 - rag/views.py | 171 -- 13 files changed, 1684 insertions(+), 357 deletions(-) create mode 100644 API_REFERENCE_FA.md delete mode 100644 rag/tests/test_recommendation_api.py diff --git a/API_REFERENCE_FA.md b/API_REFERENCE_FA.md new file mode 100644 index 0000000..14ca25e --- /dev/null +++ b/API_REFERENCE_FA.md @@ -0,0 +1,1565 @@ +# مستند کامل APIهای CropLogic AI + +این فایل مرجع اجرایی APIهای پروژه است و بر اساس کد فعلی `config/urls.py`، `views.py` و `serializers.py` تهیه شده است. + +## نکات کلی + +- پیشوند تمام APIها: `/api/` +- اکثر endpointها خروجی را در envelope زیر برمی‌گردانند: + +```json +{ + "code": 200, + "msg": "success", + "data": {} +} +``` + +- بعضی endpointهای AI علاوه بر داده نهایی، این فیلدها را هم برمی‌گردانند: + - `raw_response`: متن خام خروجی مدل + - `knowledge_base`: نام knowledge base مرتبط +- در چند endpoint قدیمی، به‌جای `farm_uuid` می‌توان `sensor_uuid` فرستاد؛ در صورت وجود، backend آن را به `farm_uuid` تبدیل می‌کند. +- فیلدهایی که در این داکیومنت با برچسب `الزامی` آمده‌اند، باید حتما ارسال شوند. + +--- + +## 1) RAG + +### 1.1) چت RAG + +- مسیر: `POST /api/rag/chat/` +- نوع خروجی: `application/json` + +#### ورودی + +- `farm_uuid` — `string` — الزامی +- `query` — `string` — اختیاری، ولی اگر تصویر نفرستید عملا الزامی است +- `message` — `string` — نام قدیمی `query` +- `history` — `array | string(JSON)` — اختیاری +- `image_urls` — `array` — اختیاری +- `image` — `file` — اختیاری +- `images` — `file[]` — اختیاری + +#### قواعد الزامی + +- اگر `query/message` خالی باشد و هیچ تصویری ارسال نشده باشد، درخواست `400` می‌گیرد +- `farm_uuid` نباید خالی باشد +- `history` باید آرایه باشد یا رشته JSON معتبرِ آرایه + +#### خروجی موفق + +- خروجی این endpoint به‌صورت JSON ساختاریافته برمی‌گردد +- پاسخ موفق داخل `data` شامل آرایه `sections` است + +#### شکل خروجی + +```json +{ + "code": 200, + "msg": "success", + "data": { + "sections": [ + { + "type": "recommendation", + "title": "جمع بندي اصلي", + "icon": "message-circle", + "content": "خلاصه يک جمله اي از بهترين پاسخ يا اقدام اصلي", + "primaryAction": "اقدام اصلي پيشنهادي", + "timing": "بهترين زمان اجرا يا بررسي", + "validityPeriod": "مدت اعتبار اين پاسخ يا توصيه", + "expandableExplanation": "توضيح روشن درباره دليل پاسخ با ارجاع به داده مزرعه، آب و هوا، گياه و شبيه سازي" + }, + { + "type": "list", + "title": "نکات اجرايي", + "icon": "list", + "items": [ + "نکته عملي 1", + "نکته عملي 2" + ] + }, + { + "type": "warning", + "title": "هشدار يا محدوديت", + "icon": "alert-triangle", + "content": "هشدار کوتاه و کاربردي" + } + ] + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر، `farm_uuid` خالی، `history` نامعتبر، `query` خالی +- `404`: `farm` پیدا نشد + +--- + +## 2) Farm Alerts + +### 2.1) Tracker هشدارهای مزرعه + +- مسیر: `POST /api/farm-alerts/tracker/` + +#### ورودی + +- `farm_uuid` — `string` — الزامی +- `sensor_uuid` — `string` — اختیاری، alias +- `query` — `string` — اختیاری + +#### خروجی موفق + +خروجی این endpoint برای نمایش وضعیت هشدارهای مزرعه به این شکل است: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "headline": "string", + "overview": "string", + "status_level": "info | warning | danger", + "notifications": [] + } +} +``` + +`notifications` معمولا رکوردهای ذخیره‌شده هشدار یا هشدارهای نرمال‌شده را برمی‌گرداند. + +#### خطاها + +- `400`: نبودن `farm_uuid` +- `500`: خطا در تولید tracker + +--- + +### 2.2) Timeline هشدارهای مزرعه + +- مسیر: `POST /api/farm-alerts/timeline/` + +#### ورودی + +- `farm_uuid` — `string` — الزامی +- `sensor_uuid` — `string` — اختیاری، alias +- `query` — `string` — اختیاری + +#### خروجی موفق + +خروجی این endpoint برای نمایش timeline هشدارها به این شکل است: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "headline": "string", + "overview": "string", + "timeline": [], + "notifications": [] + } +} +``` + +#### خطاها + +- `400`: نبودن `farm_uuid` +- `500`: خطا در تولید timeline + +--- + +## 3) Soil Data + +### 3.1) دریافت داده خاک با GET + +- مسیر: `GET /api/soil-data/?lat=...&lon=...` + +#### ورودی + +- `lat` — `decimal(9,6)` — الزامی +- `lon` — `decimal(9,6)` — الزامی + +#### خروجی موفق از دیتابیس + +```json +{ + "code": 200, + "msg": "success", + "data": { + "source": "database", + "id": 1, + "lon": 51.400000, + "lat": 35.700000, + "depths": [ + { + "depth_label": "0-5cm", + "bdod": null, + "cec": null, + "cfvo": null, + "clay": 22.0, + "nitrogen": 14.0, + "ocd": null, + "ocs": null, + "phh2o": 6.6, + "sand": 40.0, + "silt": 25.0, + "soc": null, + "wv0010": 0.41, + "wv0033": 0.28, + "wv1500": 0.12 + } + ] + } +} +``` + +#### خروجی وقتی داده هنوز آماده نیست + +```json +{ + "code": 202, + "msg": "تسک در صف. وضعیت را با task_id بررسی کنید.", + "data": { + "source": "task", + "task_id": "string", + "lon": 51.4, + "lat": 35.7, + "status_url": "/api/soil-data/tasks//status/" + } +} +``` + +#### خطاها + +- `400`: نبودن `lat/lon` یا نامعتبر بودن آن‌ها + +--- + +### 3.2) دریافت داده خاک با POST + +- مسیر: `POST /api/soil-data/` + +#### ورودی + +- `lat` — `decimal(9,6)` — الزامی +- `lon` — `decimal(9,6)` — الزامی + +#### خروجی + +- دقیقا مشابه endpoint قبلی + +--- + +### 3.3) وضعیت تسک داده خاک + +- مسیر: `GET /api/soil-data/tasks//status/` + +#### ورودی + +- `task_id` از path — الزامی + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "string", + "status": "PENDING | PROGRESS | SUCCESS | FAILURE", + "message": "...", + "progress": {}, + "result": {}, + "error": "..." + } +} +``` + +--- + +### 3.4) NDVI سلامت مزرعه + +- مسیر: `POST /api/soil-data/ndvi-health/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "ndviIndex": 0.73, + "mean_ndvi": 0.73, + "ndvi_map": {}, + "vegetation_health_class": "Healthy", + "observation_date": "2026-04-10", + "satellite_source": "sentinel-2", + "healthData": [ + { + "title": "string", + "value": {}, + "color": "string", + "icon": "string" + } + ] + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر +- `404`: مزرعه پیدا نشد + +--- + +## 4) Soile + +### 4.1) Heatmap رطوبت خاک + +- مسیر: `POST /api/soile/moisture-heatmap/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "string", + "location": {}, + "current_sensor": {}, + "soil_profile": [], + "timestamp": "string | null", + "grid_resolution": {}, + "grid_cells": [], + "sensor_points": [], + "quality_legend": {} + } +} +``` + +خروجی واقعی معمولا علاوه بر موارد بالا شامل `depth_layers`, `model_metadata`, `summary` هم هست. + +#### خطاها + +- `400`: ورودی نامعتبر +- `404`: مزرعه پیدا نشد + +--- + +### 4.2) خلاصه سلامت خاک + +- مسیر: `POST /api/soile/health-summary/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "string", + "healthScore": 82, + "profileSource": "Tomato", + "healthScoreDetails": {}, + "healthLanguage": {}, + "avgSoilMoisture": 46, + "avgSoilMoistureRaw": 46.0, + "avgSoilMoistureStatus": "بهینه" + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر +- `404`: مزرعه پیدا نشد + +--- + +### 4.3) تحلیل ناهنجاری خاک + +- مسیر: `POST /api/soile/anomaly-detection/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی + +#### خروجی موفق + +خروجی این endpoint شامل تحلیل ناهنجاری خاک به همراه metadata تکمیلی است: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "string", + "summary": "جمع بندی کوتاه ناهنجاری", + "explanation": "توضیح کوتاه", + "likely_cause": "علت محتمل", + "recommended_action": "اقدام عملی", + "monitoring_priority": "low | medium | high | urgent", + "confidence": 0.0, + "generated_at": "string", + "anomalies": [], + "interpretation": {}, + "knowledge_base": "string | null", + "raw_response": "string | null" + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر +- `404`: مزرعه پیدا نشد +- `500`: خطا در تحلیل + +--- + +## 5) Farm Data + +### 5.1) ایجاد/آپدیت farm data + +- مسیر: `POST /api/farm-data/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی +- `farm_boundary` — `object` — الزامی +- `sensor_key` — `string` — اختیاری، پیش‌فرض: `sensor-7-1` +- `sensor_payload` — `object` — اختیاری +- `plant_ids` — `integer[]` — اختیاری +- `irrigation_method_id` — `integer | null` — اختیاری + +#### نکته مهم + +- حداقل یکی از این سه فیلد باید ارسال شود: + - `sensor_payload` + - `plant_ids` + - `irrigation_method_id` + +#### ساختار `farm_boundary` + +دو فرم اصلی در کد پشتیبانی می‌شود: + +1. GeoJSON Polygon + +```json +{ + "type": "Polygon", + "coordinates": [ + [ + [51.39, 35.70], + [51.41, 35.70], + [51.41, 35.72], + [51.39, 35.72], + [51.39, 35.70] + ] + ] +} +``` + +2. corners + +```json +{ + "corners": [ + {"lat": 35.70, "lon": 51.39}, + {"lat": 35.70, "lon": 51.41}, + {"lat": 35.72, "lon": 51.41}, + {"lat": 35.72, "lon": 51.39} + ] +} +``` + +#### ساختار `sensor_payload` + +```json +{ + "sensor-7-1": { + "soil_moisture": 45.2, + "soil_temperature": 22.5, + "soil_ph": 6.8, + "electrical_conductivity": 1.2, + "nitrogen": 30.0, + "phosphorus": 15.0, + "potassium": 20.0 + }, + "leaf-sensor": { + "leaf_wetness": 11.0 + } +} +``` + +#### خروجی موفق + +بخش `insight` این endpoint خلاصه تحلیلی و عملیاتی نیاز آبی را برمی‌گرداند: + +```json +{ + "code": 201, + "msg": "success", + "data": { + "farm_uuid": "uuid", + "center_location_id": 1, + "weather_forecast_id": 10, + "sensor_payload": {}, + "plant_ids": [1, 2], + "irrigation_method_id": 3, + "created_at": "datetime", + "updated_at": "datetime" + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر، boundary نامعتبر، `sensor_payload` نامعتبر +- `502`: خطا در sync داده‌های بیرونی خاک/آب‌وهوا + +--- + +### 5.2) جزئیات کامل farm + +- مسیر: `GET /api/farm-data//detail/` + +#### ورودی + +- `farm_uuid` در path — الزامی + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "center_location": { + "id": 1, + "lat": 35.700000, + "lon": 51.400000, + "farm_boundary": {} + }, + "weather": { + "id": 10, + "forecast_date": "2026-04-10", + "temperature_min": 12.0, + "temperature_max": 23.0, + "temperature_mean": 18.0, + "precipitation": 1.2, + "precipitation_probability": 35.0, + "humidity_mean": 52.0, + "wind_speed_max": 11.0, + "et0": 3.4, + "weather_code": 0 + }, + "sensor_payload": {}, + "soil": { + "resolved_metrics": {}, + "metric_sources": {}, + "depths": [] + }, + "plant_ids": [1, 2], + "plants": [], + "irrigation_method_id": 3, + "irrigation_method": {}, + "created_at": "datetime", + "updated_at": "datetime" + } +} +``` + +#### خطاها + +- `404`: farm یافت نشد + +--- + +### 5.3) تعریف/ویرایش پارامتر سنسور + +- مسیر: `POST /api/farm-data/parameters/` + +#### ورودی + +- `sensor_key` — `string` — اختیاری، پیش‌فرض `sensor-7-1` +- `code` — `string` — الزامی +- `name_fa` — `string` — الزامی +- `unit` — `string` — اختیاری +- `data_type` — `string` — اختیاری، پیش‌فرض `float` +- `metadata` — `object` — اختیاری + +#### خروجی موفق + +```json +{ + "code": 201, + "msg": "success", + "data": { + "id": 1, + "sensor_key": "sensor-7-1", + "code": "soil_moisture", + "name_fa": "رطوبت خاک", + "unit": "%", + "data_type": "float", + "metadata": {}, + "created_at": "datetime", + "action": "added | modified" + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر + +--- + +## 6) Weather + +### 6.1) کارت آب‌وهوای مزرعه + +- مسیر: `POST /api/weather/farm-card/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "condition": "صاف", + "temperature": 28.0, + "unit": "°C", + "humidity": 42.0, + "windSpeed": 15.0, + "windUnit": "km/h", + "chartData": { + "labels": ["2026-04-01", "2026-04-02"], + "series": [[28.0, 29.0]] + } + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر +- `404`: مزرعه یافت نشد + +--- + +### 6.2) پیش‌بینی نیاز آبی + +- مسیر: `POST /api/weather/water-need-prediction/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "string", + "totalNext7Days": 24.6, + "unit": "mm", + "categories": ["روز 1", "روز 2"], + "series": [], + "dailyBreakdown": [], + "insight": { + "summary": "جمع بندی نیاز آبی بازه کوتاه مدت", + "irrigation_outlook": "برداشت عملیاتی از روند آبیاری روزهای آینده", + "recommended_action": "اقدام عملی پیشنهادی برای آبیاری", + "risk_note": "ریسک یا عدم قطعیت مهم", + "confidence": 0.0 + }, + "knowledge_base": "water_need_prediction", + "raw_response": "..." + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر +- `404`: مزرعه یافت نشد +- `500`: خطا در تحلیل + +--- + +## 7) Economy + +### 7.1) نمای اقتصادی مزرعه + +- مسیر: `POST /api/economy/overview/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "string", + "source": "mock", + "economicData": [ + { + "title": "string", + "value": "string", + "subtitle": "string", + "avatarIcon": "string", + "avatarColor": "string" + } + ], + "chartSeries": [ + { + "name": "string", + "data": [1.0, 2.0] + } + ], + "chartCategories": ["فروردین", "اردیبهشت"] + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر + +--- + +## 8) Plants + +### 8.1) لیست گیاهان + +- مسیر: `GET /api/plants/` + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": [ + { + "id": 1, + "name": "گوجه‌فرنگی", + "light": "آفتاب کامل", + "watering": "منظم", + "soil": "لومی", + "temperature": "20-30", + "growth_stage": "رویشی", + "planting_season": "بهار", + "harvest_time": "70-90 روز", + "spacing": "45-60 سانتی‌متر", + "fertilizer": "NPK", + "created_at": "datetime", + "updated_at": "datetime" + } + ] +} +``` + +--- + +### 8.2) ایجاد گیاه + +- مسیر: `POST /api/plants/` + +#### ورودی + +- `name` — `string` — الزامی +- `light` — `string` — اختیاری +- `watering` — `string` — اختیاری +- `soil` — `string` — اختیاری +- `temperature` — `string` — اختیاری +- `growth_stage` — `string` — اختیاری +- `planting_season` — `string` — اختیاری +- `harvest_time` — `string` — اختیاری +- `spacing` — `string` — اختیاری +- `fertilizer` — `string` — اختیاری + +#### خروجی موفق + +- همان ساختار `PlantSerializer` +- status: `201` + +#### خطاها + +- `400`: ورودی نامعتبر + +--- + +### 8.3) جزئیات گیاه + +- مسیر: `GET /api/plants//` + +#### خروجی موفق + +- همان `PlantSerializer` + +#### خطاها + +- `404`: گیاه یافت نشد + +--- + +### 8.4) ویرایش کامل گیاه + +- مسیر: `PUT /api/plants//` + +#### ورودی + +- تمام فیلدهای `PlantSerializer` +- عملا `name` باید وجود داشته باشد + +#### خروجی موفق + +- همان `PlantSerializer` + +#### خطاها + +- `400`: ورودی نامعتبر +- `404`: گیاه یافت نشد + +--- + +### 8.5) ویرایش جزئی گیاه + +- مسیر: `PATCH /api/plants//` + +#### ورودی + +- هر زیرمجموعه‌ای از فیلدهای `PlantSerializer` + +#### خروجی موفق + +- همان `PlantSerializer` + +#### خطاها + +- `400`, `404` + +--- + +### 8.6) حذف گیاه + +- مسیر: `DELETE /api/plants//` + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "گیاه با موفقیت حذف شد.", + "data": null +} +``` + +#### خطاها + +- `404`: گیاه یافت نشد + +--- + +### 8.7) دریافت اطلاعات گیاه از API خارجی + +- مسیر: `POST /api/plants/fetch-info/` + +#### ورودی + +- `name` — `string` — الزامی + +#### خروجی موفق + +- object شامل اطلاعات گیاه +- ساختار مورد انتظار مشابه `PlantSerializer` است + +#### خطاها + +- `400`: نام گیاه ارسال نشده +- `503`: سرویس خارجی در دسترس نیست یا پیاده‌سازی نشده + +--- + +## 9) Pest & Disease + +### 9.1) تشخیص آفت/بیماری از روی تصویر + +- مسیر: `POST /api/pest-disease/detect/` + +#### ورودی + +- `farm_uuid` — `string` — الزامی +- `sensor_uuid` — `string` — اختیاری +- `plant_name` — `string` — اختیاری +- `query` — `string` — اختیاری +- `image_urls` — `array` — اختیاری +- `image` — `file` — اختیاری +- `images` — `file[]` — اختیاری + +#### قاعده مهم + +- حداقل یک تصویر باید به یکی از این روش‌ها ارسال شود: `image_urls` یا `image` یا `images` + +#### خروجی موفق + +خروجی این endpoint برای تشخیص آفت/بیماری از روی تصویر به این شکل است: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "has_issue": true, + "category": "pest | disease | nutrient_stress | abiotic_stress | no_issue | unknown", + "confidence": 0.0, + "severity": "low | medium | high", + "summary": "جمع بندی کوتاه تشخیص", + "detected_signs": [], + "possible_causes": [], + "immediate_actions": [], + "reasoning": [] + } +} +``` + +#### خطاها + +- `400`: نبودن `farm_uuid` یا نبودن تصویر +- `500`: خطا در تحلیل تصویر + +--- + +### 9.2) پیش‌بینی ریسک آفات و بیماری + +- مسیر: `POST /api/pest-disease/risk/` + +#### ورودی + +- `farm_uuid` — `string` — الزامی +- `sensor_uuid` — `string` — اختیاری +- `plant_name` — `string` — اختیاری +- `growth_stage` — `string` — اختیاری +- `query` — `string` — اختیاری + +#### خروجی موفق + +خروجی این endpoint برای پیش بینی ریسک آفات و بیماری به این شکل است: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "summary": "جمع بندی کوتاه ریسک", + "forecast_window": "بازه زمانی", + "overall_risk": "low | medium | high", + "disease_risk": { + "score": 0.0, + "level": "low | medium | high", + "likely_conditions": [], + "reasoning": [] + }, + "pest_risk": { + "score": 0.0, + "level": "low | medium | high", + "likely_conditions": [], + "reasoning": [] + }, + "key_drivers": [], + "recommended_actions": [] + } +} +``` + +#### خطاها + +- `400`: نبودن `farm_uuid` +- `500`: خطا در پیش‌بینی + +--- + +### 9.3) خلاصه ریسک بیماری و آفات + +- مسیر: `POST /api/pest-disease/risk-summary/` + +#### ورودی + +- `farm_uuid` — `string` — الزامی +- `sensor_uuid` — `string` — اختیاری + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "string", + "diseaseRisk": {}, + "pestRisk": {}, + "drivers": {} + } +} +``` + +#### خطاها + +- `400`: نبودن `farm_uuid` +- `404`: مزرعه پیدا نشد + +--- + +## 10) Irrigation + +### 10.1) لیست روش‌های آبیاری + +- مسیر: `GET /api/irrigation/` + +#### خروجی موفق + +- آرایه‌ای از `IrrigationMethodSerializer` + +#### فیلدهای هر آیتم + +- `id` +- `name` +- `category` +- `description` +- `water_efficiency_percent` +- `water_pressure_required` +- `flow_rate` +- `coverage_area` +- `soil_type` +- `climate_suitability` +- `created_at` +- `updated_at` + +--- + +### 10.2) ایجاد روش آبیاری + +- مسیر: `POST /api/irrigation/` + +#### ورودی + +- `name` — `string` — الزامی +- `category` — `string` — اختیاری +- `description` — `string` — اختیاری +- `water_efficiency_percent` — `float | null` — اختیاری +- `water_pressure_required` — `string` — اختیاری +- `flow_rate` — `string` — اختیاری +- `coverage_area` — `string` — اختیاری +- `soil_type` — `string` — اختیاری +- `climate_suitability` — `string` — اختیاری + +#### خروجی موفق + +- همان `IrrigationMethodSerializer` + +#### خطاها + +- `400`: ورودی نامعتبر + +--- + +### 10.3) جزئیات روش آبیاری + +- مسیر: `GET /api/irrigation//` + +#### خروجی موفق + +- همان `IrrigationMethodSerializer` + +#### خطاها + +- `404`: یافت نشد + +--- + +### 10.4) ویرایش کامل روش آبیاری + +- مسیر: `PUT /api/irrigation//` + +#### ورودی + +- تمام فیلدهای `IrrigationMethodSerializer` + +#### خروجی موفق + +- همان `IrrigationMethodSerializer` + +#### خطاها + +- `400`, `404` + +--- + +### 10.5) ویرایش جزئی روش آبیاری + +- مسیر: `PATCH /api/irrigation//` + +#### ورودی + +- هر زیرمجموعه‌ای از فیلدهای `IrrigationMethodSerializer` + +#### خروجی موفق + +- همان `IrrigationMethodSerializer` + +#### خطاها + +- `400`, `404` + +--- + +### 10.6) حذف روش آبیاری + +- مسیر: `DELETE /api/irrigation//` + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "روش آبیاری با موفقیت حذف شد.", + "data": null +} +``` + +#### خطاها + +- `404`: یافت نشد + +--- + +### 10.7) توصیه آبیاری + +- مسیر: `POST /api/irrigation/recommend/` + +#### ورودی + +- `farm_uuid` — `string` — الزامی +- `sensor_uuid` — `string` — اختیاری +- `plant_name` — `string` — اختیاری +- `growth_stage` — `string` — اختیاری +- `irrigation_method_name` — `string` — اختیاری +- `query` — `string` — اختیاری + +#### خروجی موفق + +این endpoint فقط آبجکت نهایی و قابل استفاده برای کشاورز را برمی‌گرداند و فیلدهای داخلی مثل `simulation_optimizer`، `water_balance`، `mergeMetadata`، `selected_irrigation_method` و `raw_response` در پاسخ public نمایش داده نمی‌شوند. خروجی نهایی به این شکل است: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "sections": [ + { + "type": "recommendation", + "title": "برنامه آبیاری بهینه", + "icon": "droplet", + "content": "خلاصه یک جمله ای از بهترین سناریوی شبیه سازی", + "frequency": "تعداد نوبت آبیاری در بازه اعتبار", + "amount": "مقدار آب در هر نوبت و جمع کل", + "timing": "بهترین زمان اجرا", + "validityPeriod": "مدت اعتبار دقیق توصیه", + "expandableExplanation": "توضیح دلیل انتخاب این سناریو با ارجاع به تنش آبی، دما، بارش و شبیه سازی" + }, + { + "type": "list", + "title": "اقدامات اجرایی", + "icon": "list", + "items": ["نکته عملی 1", "نکته عملی 2"] + }, + { + "type": "warning", + "title": "هشدار آبیاری", + "icon": "alert-triangle", + "content": "هشدار کوتاه و کاربردی" + } + ] + } +} +``` + +#### خطاها + +- `400`: نبودن `farm_uuid` +- `500`: خطا در تولید توصیه + +--- + +### 10.8) شاخص تنش آبی + +- مسیر: `POST /api/irrigation/water-stress/` + +#### ورودی + +- `farm_uuid` — `string` — الزامی +- `sensor_uuid` — `string` — اختیاری + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "string", + "waterStressIndex": 12, + "level": "پایین", + "sourceMetric": {} + } +} +``` + +#### خطاها + +- `400`: نبودن `farm_uuid` +- `404`: مزرعه پیدا نشد + +--- + +## 11) Fertilization + +### 11.1) توصیه کودهی + +- مسیر: `POST /api/fertilization/recommend/` + +#### ورودی + +- `farm_uuid` — `string` — الزامی +- `sensor_uuid` — `string` — اختیاری +- `plant_name` — `string` — اختیاری +- `growth_stage` — `string` — اختیاری +- `query` — `string` — اختیاری + +#### خروجی موفق + +این endpoint فقط خروجی نهایی بهینه شده و آماده استفاده را برمی‌گرداند و جزئیات داخلی مثل `simulation_optimizer`، `mergeMetadata` و `raw_response` را در پاسخ public برنمی‌گرداند. خروجی نهایی به این شکل است: + +```json +{ + "code": 200, + "msg": "success", + "data": { + "sections": [ + { + "type": "recommendation", + "title": "برنامه کودهی بهینه", + "icon": "leaf", + "content": "خلاصه یک جمله ای از سناریوی منتخب", + "fertilizerType": "نوع کود پیشنهادی", + "amount": "مقدار مصرف دقیق", + "applicationMethod": "روش مصرف", + "timing": "بهترین زمان اجرا", + "validityPeriod": "مدت اعتبار این توصیه", + "expandableExplanation": "دلیل انتخاب این سناریو بر اساس کمبود عناصر، pH، مرحله رشد و شبیه سازی" + }, + { + "type": "list", + "title": "نکات اجرایی و اختلاط", + "icon": "list", + "items": ["نکته عملی 1", "نکته عملی 2"] + }, + { + "type": "warning", + "title": "هشدار کودهی", + "icon": "alert-triangle", + "content": "هشدار کوتاه و کاربردی" + } + ] + } +} +``` + +#### خطاها + +- `400`: نبودن `farm_uuid` +- `500`: خطا در تولید توصیه + +--- + +## 12) Crop Simulation + +### 12.1) شروع شبیه‌سازی رشد + +- مسیر: `POST /api/crop-simulation/growth/` + +#### ورودی + +- `plant_name` — `string` — الزامی +- `dynamic_parameters` — `string[]` — الزامی و نباید خالی باشد +- `farm_uuid` — `uuid | null` — اختیاری +- `weather` — `object | array` — اختیاری +- `soil_parameters` — `object` — اختیاری +- `site_parameters` — `object` — اختیاری +- `crop_parameters` — `object` — اختیاری +- `agromanagement` — `object` — اختیاری +- `page_size` — `integer` — اختیاری، بین 1 تا 50 + +#### قاعده مهم + +- حداقل یکی از `farm_uuid` یا `weather` باید ارسال شود + +#### خروجی موفق + +```json +{ + "code": 202, + "msg": "تسک شبیه سازی رشد در صف قرار گرفت.", + "data": { + "task_id": "string", + "status_url": "/api/crop-simulation/growth//status/", + "plant_name": "گوجه‌فرنگی" + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر + +--- + +### 12.2) وضعیت شبیه‌سازی رشد + +- مسیر: `GET /api/crop-simulation/growth//status/` + +#### query params + +- `page` — `integer` — اختیاری +- `page_size` — `integer` — اختیاری + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "task_id": "string", + "status": "PENDING | PROGRESS | SUCCESS | FAILURE", + "message": "string", + "progress": {}, + "result": { + "plant_name": "string", + "dynamic_parameters": ["DVS", "LAI"], + "engine": "string | null", + "model_name": "string | null", + "scenario_id": 1, + "simulation_warning": "", + "summary_metrics": {}, + "stage_timeline": [], + "stages_page": [], + "pagination": { + "page": 1, + "page_size": 10, + "total_items": 2, + "total_pages": 1, + "has_next": false, + "has_previous": false + }, + "daily_records_count": 51, + "default_page_size": 10 + }, + "error": "string" + } +} +``` + +--- + +### 12.3) chart وضعیت فعلی مزرعه + +- مسیر: `POST /api/crop-simulation/current-farm-chart/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی +- `plant_name` — `string` — اختیاری + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "string | null", + "plant_name": "string", + "engine": "string | null", + "model_name": "string | null", + "scenario_id": 1, + "simulation_warning": "", + "categories": [], + "series": {}, + "summary": {}, + "current_state": {}, + "metrics": {}, + "daily_output": {} + } +} +``` + +#### خطاها + +- `400`: ورودی نامعتبر +- `500`: خطا در اجرای chart + +--- + +### 12.4) پیش‌بینی برداشت + +- مسیر: `POST /api/crop-simulation/harvest-prediction/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی +- `plant_name` — `string` — اختیاری + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "date": "2026-07-15", + "dateFormatted": "15 Jul 2026", + "daysUntil": 96, + "description": "string", + "optimalWindowStart": "2026-07-10", + "optimalWindowEnd": "2026-07-20", + "gddDetails": {} + } +} +``` + +#### خطاها + +- `400`, `500` + +--- + +### 12.5) پیش‌بینی عملکرد + +- مسیر: `POST /api/crop-simulation/yield-prediction/` + +#### ورودی + +- `farm_uuid` — `uuid` — الزامی +- `plant_name` — `string` — اختیاری + +#### خروجی موفق + +```json +{ + "code": 200, + "msg": "success", + "data": { + "farm_uuid": "string", + "plant_name": "string | null", + "predictedYieldTons": 8.4, + "predictedYieldRaw": 8400.0, + "unit": "t/ha", + "sourceUnit": "kg/ha", + "simulationEngine": "string | null", + "simulationModel": "string | null", + "scenarioId": 1, + "simulationWarning": "", + "supportingMetrics": {} + } +} +``` + +#### خطاها + +- `400`, `500` + +--- + +## 13) وضعیت کدهای HTTP + +- `200` — موفق +- `201` — ایجاد/آپدیت موفق +- `202` — تسک async در صف قرار گرفت +- `400` — ورودی نامعتبر یا فیلد الزامی ارسال نشده +- `404` — رکورد/مزرعه/گیاه پیدا نشد +- `500` — خطای داخلی در پردازش تحلیلی +- `502` — خطا در sync داده بیرونی +- `503` — سرویس خارجی در دسترس نیست + +--- + +## 14) endpointهای نهایی به‌صورت خلاصه + +- `POST /api/rag/chat/` +- `POST /api/farm-alerts/tracker/` +- `POST /api/farm-alerts/timeline/` +- `GET /api/soil-data/` +- `POST /api/soil-data/` +- `GET /api/soil-data/tasks//status/` +- `POST /api/soil-data/ndvi-health/` +- `POST /api/soile/moisture-heatmap/` +- `POST /api/soile/health-summary/` +- `POST /api/soile/anomaly-detection/` +- `POST /api/farm-data/` +- `GET /api/farm-data//detail/` +- `POST /api/farm-data/parameters/` +- `POST /api/weather/farm-card/` +- `POST /api/weather/water-need-prediction/` +- `POST /api/economy/overview/` +- `GET /api/plants/` +- `POST /api/plants/` +- `GET /api/plants//` +- `PUT /api/plants//` +- `PATCH /api/plants//` +- `DELETE /api/plants//` +- `POST /api/plants/fetch-info/` +- `POST /api/pest-disease/detect/` +- `POST /api/pest-disease/risk/` +- `POST /api/pest-disease/risk-summary/` +- `GET /api/irrigation/` +- `POST /api/irrigation/` +- `GET /api/irrigation//` +- `PUT /api/irrigation//` +- `PATCH /api/irrigation//` +- `DELETE /api/irrigation//` +- `POST /api/irrigation/recommend/` +- `POST /api/irrigation/water-stress/` +- `POST /api/fertilization/recommend/` +- `POST /api/crop-simulation/growth/` +- `GET /api/crop-simulation/growth//status/` +- `POST /api/crop-simulation/current-farm-chart/` +- `POST /api/crop-simulation/harvest-prediction/` +- `POST /api/crop-simulation/yield-prediction/` diff --git a/API_RELIABILITY_AUDIT_FA.md b/API_RELIABILITY_AUDIT_FA.md index 3b2e58e..1eff5c8 100644 --- a/API_RELIABILITY_AUDIT_FA.md +++ b/API_RELIABILITY_AUDIT_FA.md @@ -489,25 +489,6 @@ - سطح اعتماد: - `کم تا متوسط` -### `POST /api/rag/recommend/irrigation/` - -- ماهیت: - - عملا همان recommendation RAG-based آبیاری را مستقیما expose می‌کند. -- ضعف‌ها: - - همان ضعف‌های `POST /api/irrigation/recommend/` را دارد. - - ظاهر کامل پاسخ می‌تواند fallback بودن بخش‌هایی از محتوا را پنهان کند، هرچند provenance اضافه شده است. -- سطح اعتماد: - - `متوسط` با ورودی خوب - - `کم تا متوسط` با fallback زیاد - -### `POST /api/rag/recommend/fertilization/` - -- ماهیت: - - مستقیم‌ترین مسیر recommendation کودهی مبتنی بر RAG است. -- ضعف‌ها: - - همان ضعف‌های `POST /api/fertilization/recommend/` را دارد. -- سطح اعتماد: - - `متوسط رو به کم` --- @@ -553,4 +534,3 @@ 4. افزودن uncertainty و freshness صریح به heatmap و water-need outputs 5. crop-specific و region-specific کردن alert/risk thresholds 6. اضافه کردن confidence واقعی و source provenance استاندارد به تمام endpointهای RAG-based - diff --git a/APPS_URLS_AUDIT.md b/APPS_URLS_AUDIT.md index 68b836d..02a6d3d 100644 --- a/APPS_URLS_AUDIT.md +++ b/APPS_URLS_AUDIT.md @@ -30,8 +30,6 @@ Base: `/api/rag/` | Method | URL | توضیح | |---|---|---| | POST | `/api/rag/chat/` | چت RAG به صورت stream | -| POST | `/api/rag/recommend/irrigation/` | توصیه آبیاری | -| POST | `/api/rag/recommend/fertilization/` | توصیه کودهی | ### App: Farm Alerts @@ -354,4 +352,3 @@ Base: `/api/crop-simulation/` - `rag/services/irrigation.py:147` - `rag/services/fertilization.py:130` - `crop_simulation/growth_simulation.py:404` - diff --git a/config/settings.py b/config/settings.py index f62dc63..fb0b5cc 100644 --- a/config/settings.py +++ b/config/settings.py @@ -152,7 +152,6 @@ SPECTACULAR_SETTINGS = { {"name": "Dashboard Data", "description": "تجمیع داده‌های داشبورد مزرعه"}, {"name": "Farm Alerts", "description": "tracker و timeline مستقل هشدارهای مزرعه"}, {"name": "RAG Chat", "description": "چت هوشمند RAG"}, - {"name": "RAG Recommendations", "description": "توصیه‌های آبیاری و کودهی مبتنی بر RAG"}, {"name": "Soil Data", "description": "داده‌های خاک (SoilGrids)"}, {"name": "Soile", "description": "heatmap مستقل رطوبت خاک و داده های مزرعه"}, {"name": "Farm Data", "description": "داده‌های مزرعه و سنسورها"}, diff --git a/config/tones/chat_tone.txt b/config/tones/chat_tone.txt index f5856cb..9052d13 100644 --- a/config/tones/chat_tone.txt +++ b/config/tones/chat_tone.txt @@ -1,14 +1,51 @@ -شما دستيار عمومي CropLogic براي چت با کاربر هستيد. +You are a general farm assistant for CropLogic. -قواعد مهم: -- اين سرويس خروجی را به صورت متن استريمي `text/plain` برمي گرداند، نه JSON. -- بنابراين فقط متن ساده و خوانا توليد کن و هرگز JSON، markdown fence يا ساختار کدي برنگردان. -- پاسخ را به فارسي، دوستانه، شفاف و کاربردي بنويس. -- اگر لازم بود، پاسخ را در 2 تا 4 پاراگراف کوتاه يا چند خط فهرست گونه اما بدون JSON ارائه کن. -- اگر داده کافي نيست، همان را صريح بگو و از حدس زدن پرهيز کن. +### GOAL +Convert farm context, soil data, plant stage, weather risk, and any optimization block such as `[خروجي بهينه ساز شبيه سازي]` into a precise and practical Persian response for the farmer. -شکل خروجي مورد انتظار: -- يک پاسخ متني يکپارچه -- بدون کليد JSON -- بدون `sections` -- بدون کاراکترهاي ابتدايي/انتهايي اضافه +### HARD RULES +1. If an optimizer block exists, it is the source of truth for numeric recommendations, timing, validity period, and scientific reasoning. +2. Do not invent numbers or recommendations that conflict with the optimizer block. +3. Always return only valid JSON with a top-level `sections` array. +4. The `sections` array must include at least: + - one `recommendation` section for the core answer or action + - one `list` section for operational notes, follow-up checks, or execution details + - one `warning` section when there is uncertainty, data limitation, weather risk, sensor conflict, or execution risk +5. Write in clear Persian and keep the answer practical, short, and field-usable. + +### OUTPUT CONTRACT +{ + "sections": [ + { + "type": "recommendation", + "title": "جمع بندي اصلي", + "icon": "message-circle", + "content": "خلاصه يک جمله اي از بهترين پاسخ يا اقدام اصلي", + "primaryAction": "اقدام اصلي پيشنهادي", + "timing": "بهترين زمان اجرا يا بررسي", + "validityPeriod": "مدت اعتبار اين پاسخ يا توصيه", + "expandableExplanation": "توضيح روشن درباره دليل پاسخ با ارجاع به داده مزرعه، آب و هوا، گياه و شبيه سازي" + }, + { + "type": "list", + "title": "نکات اجرايي", + "icon": "list", + "items": [ + "نکته عملي 1", + "نکته عملي 2" + ] + }, + { + "type": "warning", + "title": "هشدار يا محدوديت", + "icon": "alert-triangle", + "content": "هشدار کوتاه و کاربردي" + } + ] +} + +### WRITING RULES +- If the user asks a general question, still shape the answer inside the same `sections` contract. +- If the optimizer highlights an important tradeoff or dominant issue, explain it briefly in `expandableExplanation`. +- If data is incomplete or conflicting, state that clearly in the `warning` section. +- Never output markdown, code fences, greetings, or extra commentary outside the JSON object. diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml index 4a49b14..87e7940 100644 --- a/docker-compose-prod.yaml +++ b/docker-compose-prod.yaml @@ -16,14 +16,14 @@ services: timeout: 5s retries: 5 networks: - - ai-network + - crop_network redis: image: docker.iranserver.com/redis container_name: ai-redis restart: always networks: - - ai-network + - crop_network qdrant: image: docker.iranserver.com/qdrant/qdrant:latest @@ -32,7 +32,7 @@ services: volumes: - qdrant_data:/qdrant/storage networks: - - ai-network + - crop_network web: build: @@ -50,10 +50,10 @@ services: env_file: - .env environment: - DB_HOST: db - CELERY_BROKER_URL: redis://redis:6379/0 - CELERY_RESULT_BACKEND: redis://redis:6379/0 - QDRANT_HOST: qdrant + DB_HOST: ai-db + CELERY_BROKER_URL: redis://ai-redis:6379/0 + CELERY_RESULT_BACKEND: redis://ai-redis:6379/0 + QDRANT_HOST: ai-qdrant QDRANT_PORT: 6333 DEBUG: "False" depends_on: @@ -63,7 +63,7 @@ services: condition: service_started networks: - - ai-network + - crop_network celery: build: @@ -83,9 +83,9 @@ services: env_file: - .env environment: - DB_HOST: db - CELERY_BROKER_URL: redis://redis:6379/0 - CELERY_RESULT_BACKEND: redis://redis:6379/0 + DB_HOST: ai-db + CELERY_BROKER_URL: redis://ai-redis:6379/0 + CELERY_RESULT_BACKEND: redis://ai-redis:6379/0 SKIP_MIGRATE: "1" DEBUG: "False" depends_on: @@ -94,7 +94,7 @@ services: redis: condition: service_started networks: - - ai-network + - crop_network volumes: @@ -102,5 +102,5 @@ volumes: qdrant_data: networks: - ai-network: - driver: bridge + crop_network: + external: true diff --git a/docker-compose.yaml b/docker-compose.yaml index 4d53679..bc81dc7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,7 +2,7 @@ services: db: image: docker.iranserver.com/mysql:8 - container_name: ai-db + container_name: ai-mysql environment: MYSQL_DATABASE: ${DB_NAME:-ai} MYSQL_USER: ${DB_USER:-ai} @@ -10,17 +10,21 @@ services: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-changeme} volumes: - ai_mysql_data:/var/lib/mysql + ports: + - "3307:3306" healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD:-changeme}"] interval: 5s timeout: 5s retries: 5 + networks: + - crop_network phpmyadmin: image: docker-mirror.liara.ir/phpmyadmin:latest container_name: ai-phpmyadmin environment: - PMA_HOST: db + PMA_HOST: ai-mysql PMA_PORT: 3306 UPLOAD_LIMIT: 64M ports: @@ -28,10 +32,14 @@ services: depends_on: db: condition: service_healthy + networks: + - crop_network redis: image: redis:7-alpine container_name: ai-redis + networks: + - crop_network qdrant: image: qdrant/qdrant:latest @@ -42,6 +50,8 @@ services: volumes: - qdrant_data:/qdrant/storage restart: unless-stopped + networks: + - crop_network web: build: @@ -61,10 +71,10 @@ services: env_file: - .env environment: - DB_HOST: db - CELERY_BROKER_URL: redis://redis:6379/0 - CELERY_RESULT_BACKEND: redis://redis:6379/0 - QDRANT_HOST: qdrant + DB_HOST: ai-mysql + CELERY_BROKER_URL: redis://ai-redis:6379/0 + CELERY_RESULT_BACKEND: redis://ai-redis:6379/0 + QDRANT_HOST: ai-qdrant QDRANT_PORT: 6333 depends_on: db: @@ -73,6 +83,8 @@ services: condition: service_started qdrant: condition: service_started + networks: + - crop_network celery: build: @@ -90,16 +102,22 @@ services: env_file: - .env environment: - DB_HOST: db - CELERY_BROKER_URL: redis://redis:6379/0 - CELERY_RESULT_BACKEND: redis://redis:6379/0 + DB_HOST: ai-mysql + CELERY_BROKER_URL: redis://ai-redis:6379/0 + CELERY_RESULT_BACKEND: redis://ai-redis:6379/0 SKIP_MIGRATE: "1" depends_on: db: condition: service_healthy redis: condition: service_started + networks: + - crop_network volumes: ai_mysql_data: qdrant_data: + +networks: + crop_network: + external: true diff --git a/fertilization/views.py b/fertilization/views.py index 7c151da..2ab3f31 100644 --- a/fertilization/views.py +++ b/fertilization/views.py @@ -99,7 +99,9 @@ class FertilizationRecommendView(APIView): status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) + # Public API exposes only the final farmer-facing recommendation object. + final_result = {"sections": result.get("sections", [])} return Response( - {"code": 200, "msg": "success", "data": result}, + {"code": 200, "msg": "success", "data": final_result}, status=status.HTTP_200_OK, ) diff --git a/integration_tests/test_reporting_and_ai_api_flow.py b/integration_tests/test_reporting_and_ai_api_flow.py index aaae0ba..77b4c10 100644 --- a/integration_tests/test_reporting_and_ai_api_flow.py +++ b/integration_tests/test_reporting_and_ai_api_flow.py @@ -239,14 +239,13 @@ class ReportingAndAiJourneyTests(IntegrationAPITestCase): patch( "rag.services.irrigation.get_irrigation_recommendation", return_value={ - "plan": { - "frequencyPerWeek": 3, - "durationMinutes": 28, - "bestTimeOfDay": "05:30", - "moistureLevel": 68, - "warning": "", - }, - "raw_response": "{\"plan\": {\"frequencyPerWeek\": 3}}", + "sections": [ + { + "type": "recommendation", + "title": "برنامه آبیاری بهینه", + "content": "هفته ای 3 نوبت آبیاری انجام شود.", + } + ], }, ) ) @@ -254,11 +253,14 @@ class ReportingAndAiJourneyTests(IntegrationAPITestCase): patch( "rag.services.fertilization.get_fertilization_recommendation", return_value={ - "plan": { - "npkRatio": "15-5-30", - "amountPerHectare": "60 kg", - }, - "raw_response": "{\"plan\": {\"npkRatio\": \"15-5-30\"}}", + "sections": [ + { + "type": "recommendation", + "title": "برنامه کودهی بهینه", + "fertilizerType": "15-5-30", + "amount": "60 kg", + } + ], }, ) ) @@ -304,37 +306,6 @@ class ReportingAndAiJourneyTests(IntegrationAPITestCase): streamed_text = b"".join(chat_response.streaming_content).decode("utf-8") self.assertIn("Moisture is acceptable", streamed_text) - rag_irrigation_response = self.client.post( - "/api/rag/recommend/irrigation/", - data={ - "farm_uuid": str(self.farm_uuid), - "plant_name": "Tomato", - "growth_stage": "flowering", - "irrigation_method_name": "Analytics Drip", - }, - format="json", - ) - self.assertEqual(rag_irrigation_response.status_code, 200) - self.assertEqual( - rag_irrigation_response.json()["data"]["plan"]["frequencyPerWeek"], - 3, - ) - - rag_fertilization_response = self.client.post( - "/api/rag/recommend/fertilization/", - data={ - "farm_uuid": str(self.farm_uuid), - "plant_name": "Tomato", - "growth_stage": "flowering", - }, - format="json", - ) - self.assertEqual(rag_fertilization_response.status_code, 200) - self.assertEqual( - rag_fertilization_response.json()["data"]["plan"]["npkRatio"], - "15-5-30", - ) - irrigation_recommend_response = self.client.post( "/api/irrigation/recommend/", data={ @@ -346,6 +317,10 @@ class ReportingAndAiJourneyTests(IntegrationAPITestCase): format="json", ) self.assertEqual(irrigation_recommend_response.status_code, 200) + self.assertEqual( + irrigation_recommend_response.json()["data"]["sections"][0]["type"], + "recommendation", + ) fertilization_recommend_response = self.client.post( "/api/fertilization/recommend/", @@ -357,6 +332,10 @@ class ReportingAndAiJourneyTests(IntegrationAPITestCase): format="json", ) self.assertEqual(fertilization_recommend_response.status_code, 200) + self.assertEqual( + fertilization_recommend_response.json()["data"]["sections"][0]["fertilizerType"], + "15-5-30", + ) pest_detect_response = self.client.post( "/api/pest-disease/detect/", diff --git a/irrigation/views.py b/irrigation/views.py index 8d720ec..7bac824 100644 --- a/irrigation/views.py +++ b/irrigation/views.py @@ -187,8 +187,10 @@ class IrrigationRecommendView(APIView): status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) + # Public API exposes only the final farmer-facing recommendation object. + final_result = {"sections": result.get("sections", [])} return Response( - {"code": 200, "msg": "success", "data": result}, + {"code": 200, "msg": "success", "data": final_result}, status=status.HTTP_200_OK, ) diff --git a/rag/tests/test_recommendation_api.py b/rag/tests/test_recommendation_api.py deleted file mode 100644 index 9c7d7d6..0000000 --- a/rag/tests/test_recommendation_api.py +++ /dev/null @@ -1,77 +0,0 @@ -from unittest.mock import patch - -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() - - @patch("rag.services.irrigation.get_irrigation_recommendation") - def test_irrigation_recommendation_returns_direct_result(self, mock_get_irrigation_recommendation): - mock_get_irrigation_recommendation.return_value = { - "plan": { - "frequencyPerWeek": 3, - "durationMinutes": 25, - }, - "raw_response": "{\"plan\": {\"frequencyPerWeek\": 3, \"durationMinutes\": 25}}", - } - - response = self.client.post( - "/api/rag/recommend/irrigation/", - data={ - "farm_uuid": "sensor-123", - "plant_name": "گوجه‌فرنگی", - "growth_stage": "میوه‌دهی", - "irrigation_method_name": "قطره‌ای", - }, - format="json", - ) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()["data"]["plan"]["frequencyPerWeek"], 3) - mock_get_irrigation_recommendation.assert_called_once_with( - farm_uuid="sensor-123", - plant_name="گوجه‌فرنگی", - growth_stage="میوه‌دهی", - irrigation_method_name="قطره‌ای", - query=None, - ) - - @patch("rag.services.fertilization.get_fertilization_recommendation") - def test_fertilization_recommendation_returns_direct_result(self, mock_get_fertilization_recommendation): - mock_get_fertilization_recommendation.return_value = { - "plan": { - "npkRatio": "20-20-20", - "amountPerHectare": "80 kg", - }, - "raw_response": "{\"plan\": {\"npkRatio\": \"20-20-20\", \"amountPerHectare\": \"80 kg\"}}", - } - - response = self.client.post( - "/api/rag/recommend/fertilization/", - data={ - "farm_uuid": "sensor-456", - "plant_name": "گندم", - "growth_stage": "رویشی", - }, - format="json", - ) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()["data"]["plan"]["npkRatio"], "20-20-20") - mock_get_fertilization_recommendation.assert_called_once_with( - farm_uuid="sensor-456", - plant_name="گندم", - growth_stage="رویشی", - query=None, - ) - - def test_removed_status_endpoints_return_404(self): - irrigation_response = self.client.get("/api/rag/recommend/irrigation/sample-task/status/") - fertilization_response = self.client.get("/api/rag/recommend/fertilization/sample-task/status/") - - self.assertEqual(irrigation_response.status_code, 404) - self.assertEqual(fertilization_response.status_code, 404) diff --git a/rag/urls.py b/rag/urls.py index 3b64bcb..14b8450 100644 --- a/rag/urls.py +++ b/rag/urls.py @@ -2,12 +2,8 @@ from django.urls import path from .views import ( ChatView, - IrrigationRecommendationView, - FertilizationRecommendationView, ) urlpatterns = [ path("chat/", ChatView.as_view()), - path("recommend/irrigation/", IrrigationRecommendationView.as_view(), name="recommend-irrigation"), - path("recommend/fertilization/", FertilizationRecommendationView.as_view(), name="recommend-fertilization"), ] diff --git a/rag/views.py b/rag/views.py index 8f94dd3..79fb9c4 100644 --- a/rag/views.py +++ b/rag/views.py @@ -38,14 +38,6 @@ RagValidationErrorResponseSerializer = build_envelope_serializer( data_required=False, allow_null=True, ) -RagIrrigationResponseSerializer = build_envelope_serializer( - "RagIrrigationResponseSerializer", - drf_serializers.JSONField(), -) -RagFertilizationResponseSerializer = build_envelope_serializer( - "RagFertilizationResponseSerializer", - drf_serializers.JSONField(), -) class ChatView(APIView): @@ -211,166 +203,3 @@ class ChatView(APIView): generate(), content_type="text/plain; charset=utf-8", ) - - -class IrrigationRecommendationView(APIView): - """ - توصیه آبیاری به صورت مستقیم. - POST با farm_uuid، plant_name، growth_stage، irrigation_method_name. - نتیجه همان لحظه برگشت داده می‌شود. - """ - - @extend_schema( - tags=["RAG Recommendations"], - summary="درخواست توصیه آبیاری", - description=( - "داده‌های سنسور، گیاه و روش آبیاری را دریافت کرده و " - "توصیه آبیاری را به صورت مستقیم برمی‌گرداند." - ), - request=inline_serializer( - name="IrrigationRecommendationRequest", - fields={ - "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="نام روش آبیاری"), - "query": drf_serializers.CharField(required=False, help_text="سوال اختیاری"), - }, - ), - responses={ - 200: build_response( - RagIrrigationResponseSerializer, - "توصیه آبیاری با موفقیت تولید شد.", - ), - 400: build_response( - RagValidationErrorResponseSerializer, - "پارامتر ورودی نامعتبر است.", - ), - 500: build_response( - RagValidationErrorResponseSerializer, - "خطا در تولید توصیه آبیاری.", - ), - }, - examples=[ - OpenApiExample( - "نمونه درخواست", - value={ - "farm_uuid": "11111111-1111-1111-1111-111111111111", - "plant_name": "گوجه‌فرنگی", - "growth_stage": "میوه‌دهی", - "irrigation_method_name": "آبیاری قطره‌ای", - }, - request_only=True, - ), - ], - ) - def post(self, request: Request): - from rag.services.irrigation import get_irrigation_recommendation - - farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid") - if not farm_uuid: - return Response( - {"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None}, - status=status.HTTP_400_BAD_REQUEST, - ) - - try: - result = get_irrigation_recommendation( - 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 farm %s", farm_uuid) - return Response( - {"code": 500, "msg": "خطا در تولید توصیه آبیاری.", "data": None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - - return Response( - {"code": 200, "msg": "success", "data": result}, - status=status.HTTP_200_OK, - ) - - -class FertilizationRecommendationView(APIView): - """ - توصیه کودهی به صورت مستقیم. - POST با farm_uuid، plant_name، growth_stage. - نتیجه همان لحظه برگشت داده می‌شود. - """ - - @extend_schema( - tags=["RAG Recommendations"], - summary="درخواست توصیه کودهی", - description=( - "داده‌های سنسور و گیاه را دریافت کرده و " - "توصیه کودهی را به صورت مستقیم برمی‌گرداند." - ), - request=inline_serializer( - name="FertilizationRecommendationRequest", - fields={ - "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="سوال اختیاری"), - }, - ), - responses={ - 200: build_response( - RagFertilizationResponseSerializer, - "توصیه کودهی با موفقیت تولید شد.", - ), - 400: build_response( - RagValidationErrorResponseSerializer, - "پارامتر ورودی نامعتبر است.", - ), - 500: build_response( - RagValidationErrorResponseSerializer, - "خطا در تولید توصیه کودهی.", - ), - }, - examples=[ - OpenApiExample( - "نمونه درخواست", - value={ - "farm_uuid": "11111111-1111-1111-1111-111111111111", - "plant_name": "گوجه‌فرنگی", - "growth_stage": "رویشی", - }, - request_only=True, - ), - ], - ) - def post(self, request: Request): - from rag.services.fertilization import get_fertilization_recommendation - - farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid") - if not farm_uuid: - return Response( - {"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None}, - status=status.HTTP_400_BAD_REQUEST, - ) - - try: - result = get_fertilization_recommendation( - 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 farm %s", farm_uuid) - return Response( - {"code": 500, "msg": "خطا در تولید توصیه کودهی.", "data": None}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR, - ) - - return Response( - {"code": 200, "msg": "success", "data": result}, - status=status.HTTP_200_OK, - )