This commit is contained in:
2026-04-24 02:12:06 +03:30
parent 31f4bf5d38
commit 302124aa87
18 changed files with 392 additions and 406 deletions
@@ -1,24 +1,80 @@
# دانش پایه کودهی بخش دوم: راهنمای کوددهی گوجه‌فرنگی
گوجه‌فرنگی گیاهی پرمصرف است و به عناصر درشت‌مغذی (نیتروژن، فسفر، پتاسیم - NPK) و ریزمغذی‌ها (به ویژه کلسیم و منیزیم) نیاز دارد.
## نیتروژن (N) ۱. مراحل مختلف کوددهی:
نیتروژن برای رشد سبزینه و برگ‌ها ضروری است. کمبود آن باعث زردی برگ‌ها و کاهش رشد می‌شود.
منابع نیتروژن: اوره (46% N)، نیترات آمونیوم (34% N)، سولفات آمونیوم (21% N).
مصرف بیش از حد نیتروژن باعث رشد رویشی بیش از حد و کاهش مقاومت به بیماری می‌شود.
## فسفر (P) قبل از کاشت (آماده‌سازی خاک):
فسفر برای ریشه‌زایی، گلدهی و میوه‌دهی مهم است. کمبود آن رشد ریشه را محدود می‌کند. افزودن کود دامی پوسیده یا ورمی‌کمپوست جهت بهبود بافت خاک.
منابع فسفر: سوپرفسفات تریپل (46% P2O5)، DAP (18-46-0). استفاده از کودهای پایه فسفر بالا (برای ریشه‌زایی) و پتاسیم.
فسفر در خاک‌های قلیایی (pH > 7.5) به‌سختی جذب می‌شود. مرحله رشد رویشی (قبل از گل‌دهی):
نیاز به نیتروژن (
𝑁
N
) برای رشد برگ‌ها و ساقه‌ها بیشتر است.
احتیاط: نیتروژن بیش از حد باعث رشد علفی گیاه شده و گل‌دهی را به تاخیر می‌اندازد. استفاده از کود متعادل مانند
20
20
20
202020
با غلظت مناسب توصیه می‌شود.
مرحله گل‌دهی و تشکیل میوه:
در این مرحله نیاز به نیتروژن کاهش و نیاز به فسفر (
𝑃
P
) و پتاسیم (
𝐾
K
) به شدت افزایش می‌یابد. پتاسیم برای کیفیت، اندازه و رنگ میوه ضروری است.
کودهای پتاس‌بالا (مانند
12
12
36
121236
) مناسب هستند.
مرحله رشد و رسیدن میوه:
ادامه تغذیه با پتاسیم بالا.
محلول‌پاشی کلسیم در این مرحله بسیار حیاتی است.
۲. عناصر کلیدی و ریزمغذی‌های ضروری:
## پتاسیم (K) کلسیم (
پتاسیم مقاومت به خشکی، سرما و بیماری را افزایش می‌دهد. در کیفیت میوه نقش دارد. 𝐶
منابع پتاسیم: سولفات پتاسیم (50% K2O)، کلرید پتاسیم (60% K2O). 𝑎
Ca
## pH و جذب عناصر ): کمبود کلسیم (یا عدم جذب آن به دلیل نوسانات آبیاری) باعث عارضه پوسیدگی گلگاه (سیاه شدن ته گوجه‌فرنگی) می‌شود. استفاده از کود نیترات کلسیم به صورت کودآبیاری یا محلول‌پاشی ضروری است.
pH خاک بر جذب عناصر غذایی تأثیر مستقیم دارد. pH مناسب برای اغلب محصولات ۶ تا ۷ است. منیزیم (
در pH پایین (اسیدی): آهن و منگنز زیاد جذب می‌شوند ولی فسفر و کلسیم کم. 𝑀
در pH بالا (قلیایی): آهن، روی و منگنز کم جذب می‌شوند. 𝑔
Mg
## EC و کودهی ): کمبود آن باعث زرد شدن برگ‌های پیر (در حالی که رگبرگ‌ها سبز می‌مانند) می‌شود. سولفات منیزیم برای رفع این مشکل مفید است.
EC بالا نشان‌دهنده شوری خاک است. قبل از کودهی باید EC بررسی شود. آهن (
اگر EC بالای ۴ dS/m باشد، کودهی باید با احتیاط انجام شود. 𝐹
𝑒
Fe
) و روی (
𝑍
𝑛
Zn
): برای شادابی و فتوسنتز گیاه لازم هستند و معمولاً به صورت محلول‌پاشی یا کودهای کلاته استفاده می‌شوند.
خلاصه نکات طلایی پایگاه دانش:
پوسیدگی گلگاه: ترکیبی از کمبود کلسیم و آبیاری نامنظم است. همیشه رطوبت خاک را یکنواخت نگه دارید و از کلسیم استفاده کنید.
ترک‌خوردگی میوه: ناشی از تغییر ناگهانی رطوبت خاک (مثلاً آبیاری سنگین بعد از یک دوره خشکی) است.
تنظیم
𝑝
𝐻
pH
خاک: گوجه‌فرنگی در خاکی با
𝑝
𝐻
pH
بین
6.0
6.0
تا
6.8
6.8
بهترین جذب مواد مغذی را دارد.
فاصله کوددهی کلسیم و فسفر: کودهای حاوی کلسیم را هرگز با کودهای حاوی فسفر یا سولفات همزمان مخلوط نکنید (رسوب می‌کنند).
@@ -1,18 +1,15 @@
# دانش پایه آبیاری بخش اول: راهنمای آبیاری گوجه‌فرنگی (آب‌دهی)
نیاز آبی گوجه‌فرنگی به مرحله رشد، نوع خاک و شرایط آب و هوایی بستگی دارد. مهم‌ترین اصل در آبیاری گوجه‌فرنگی نظم و یکنواختی است.
## تبخیر-تعرق مرجع (ET0) ۱. مراحل مختلف رشد و نیاز آبی:
ET0 نشان‌دهنده میزان آب مورد نیاز گیاه مرجع (چمن) در یک روز است. واحد آن mm/day است.
ET0 بالا یعنی هوا گرم و خشک است و گیاه آب بیشتری نیاز دارد.
## رابطه بارش و آبیاری مرحله نشاء و رشد اولیه: خاک باید مرطوب (نه غرقاب) نگه داشته شود تا ریشه‌ها به خوبی مستقر شوند. آبیاری سطحی و مکرر توصیه می‌شود.
اگر بارش پیش‌بینی شده از ET0 بیشتر باشد، معمولاً آبیاری لازم نیست. مرحله گل‌دهی: تنش آبی در این مرحله باعث ریزش گل‌ها می‌شود. آبیاری باید منظم باشد.
بارش مؤثر حدود ۷۰-۸۰ درصد بارش واقعی است (بخشی تبخیر و رواناب می‌شود). مرحله تشکیل و بزرگ شدن میوه: بیشترین نیاز آبی در این مرحله است. آبیاری باید عمیق و منظم باشد تا از مشکلاتی مانند ترک‌خوردگی میوه و پوسیدگی گلگاه جلوگیری شود.
مرحله رسیدن میوه: با شروع رنگ گرفتن گوجه‌ها، آبیاری را کمی کاهش دهید. این کار باعث افزایش قند، بهبود طعم و جلوگیری از ترک خوردن میوه می‌شود.
۲. نکات کلیدی در آبیاری:
## رطوبت خاک روش آبیاری: بهترین روش، آبیاری قطره‌ای است. آبیاری بارانی باعث خیس شدن برگ‌ها و افزایش خطر بیماری‌های قارچی می‌شود.
رطوبت مناسب خاک بسته به نوع خاک و محصول متفاوت است. زمان آبیاری: بهترین زمان، صبح زود است تا گیاه در طول روز رطوبت کافی داشته باشد و برگ‌ها تا شب خشک شوند.
خاک رسی رطوبت بیشتری نگه می‌دارد. خاک شنی سریع‌تر خشک می‌شود. عمق آبیاری: آبیاری باید عمیق باشد تا ریشه‌ها به عمق خاک نفوذ کنند (حداقل ۱۵ تا ۲۰ سانتی‌متر).
آبیاری باید وقتی انجام شود که رطوبت خاک به حد بحرانی (MAD) رسیده باشد. مالچ‌پاشی: استفاده از مالچ (پلاستیک کشاورزی یا کاه و کلش) روی خاک، رطوبت را حفظ کرده و از تبخیر سریع آب جلوگیری می‌کند.
## دمای هوا و آبیاری
در دماهای بالای ۳۵ درجه، تبخیر سطحی زیاد است و آبیاری صبح زود یا عصر توصیه می‌شود.
در دماهای زیر ۵ درجه، آبیاری ممکن است به ریشه آسیب بزند.
-2
View File
@@ -26,7 +26,6 @@ INSTALLED_APPS = [
"drf_spectacular_sidecar", "drf_spectacular_sidecar",
"dashboard_data", "dashboard_data",
"rag", "rag",
"tasks",
"location_data", "location_data",
"farm_data.apps.FarmDataConfig", "farm_data.apps.FarmDataConfig",
"weather", "weather",
@@ -123,7 +122,6 @@ SPECTACULAR_SETTINGS = {
{"name": "Dashboard Data", "description": "تجمیع داده‌های داشبورد مزرعه"}, {"name": "Dashboard Data", "description": "تجمیع داده‌های داشبورد مزرعه"},
{"name": "RAG Chat", "description": "چت هوشمند RAG"}, {"name": "RAG Chat", "description": "چت هوشمند RAG"},
{"name": "RAG Recommendations", "description": "توصیه‌های آبیاری و کودهی مبتنی بر RAG"}, {"name": "RAG Recommendations", "description": "توصیه‌های آبیاری و کودهی مبتنی بر RAG"},
{"name": "Tasks", "description": "مدیریت تسک‌های Celery"},
{"name": "Soil Data", "description": "داده‌های خاک (SoilGrids)"}, {"name": "Soil Data", "description": "داده‌های خاک (SoilGrids)"},
{"name": "Farm Data", "description": "داده‌های مزرعه و سنسورها"}, {"name": "Farm Data", "description": "داده‌های مزرعه و سنسورها"},
{"name": "Farm Parameters", "description": "پارامترهای سنسورهای مزرعه"}, {"name": "Farm Parameters", "description": "پارامترهای سنسورهای مزرعه"},
+44 -20
View File
@@ -1,24 +1,48 @@
# لحن توصیه کودهی You are an expert agricultural consultant AI specializing in plant nutrition and soil fertility. Your task is to analyze the provided Knowledge Base (Context) — which includes soil test results, crop growth stage, and current farm conditions — to provide actionable fertilization advice to farmers.
سبک پاسخ: ### TONE & STYLE
- تخصصی و دقیق: نسبت NPK، مقدار در هکتار، روش مصرف و فاصله زمانی را مشخص کن - Be friendly, respectful, and easy to understand for a farmer. Avoid overly complex academic jargon unless explained.
- بر اساس داده‌های NPK خاک، pH، و نوع محصول - Speak directly to the farmer in Persian (Farsi).
- فرمت خروجی حتماً JSON باشد و دقیقاً به شکل زیر: - If mathematical expressions or chemical ratios are used (like $N-P-K$ formulas or percentages like $20\%$), ensure they are clear.
### CORE RULES
1. MANDATORY FERTILIZER RECOMMENDATION: Your response MUST always include a clear fertilization recommendation. You must tell the farmer exactly what nutrient or fertilizer is needed based on the context.
2. METHOD AND TIMING: Every fertilization recommendation MUST specify the application method (e.g., foliar spray, fertigation, soil application) and the precise timing (e.g., early morning, avoiding high wind/temperature).
3. VALIDITY PERIOD: Specify the time window during which this fertilizer should be applied for maximum efficacy based on the crop's growth stage.
4. NO EXTRA TEXT: Your entire response MUST be ONLY a valid JSON object. Do not include any text or markdown formatting outside of the JSON structure itself.
5. JSON STRUCTURE: You must strictly adhere to the JSON structure provided below.
### JSON OUTPUT STRUCTURE
{ {
"plan": { "sections": [
"npkRatio": "<str - نسبت NPK مثل 20-20-20 (NPK)>", {
"amountPerHectare": "<str - مقدار مصرف در هکتار مثل 150 kg/ha>", "type": "recommendation",
"applicationMethod": "<str - روش مصرف مثل Foliar spray + soil broadcast>", "title": "عنوان توصیه (مانند: برنامه محلول‌پاشی تقویتی)",
"applicationInterval": "<str - فاصله زمانی مصرف مثل Every 14 days>", "icon": "leaf",
"reasoning": "<str - توضیح دقیق دلیل انتخاب این برنامه کودهی بر اساس شرایط خاک، آب و گیاه>" "content": "توضیح کوتاه توصیه",
"fertilizerType": "نوع کود پیشنهادی (مثلاً: کود $N-P-K$ با فرمول $20-20-20$ یا اوره)",
"amount": "میزان مصرف دقیق (مثلاً: ۳ در هزار یا ۵۰ کیلوگرم در هکتار)",
"applicationMethod": "روش مصرف (مثلاً: محلول‌پاشی روی برگ، همراه با آبیاری، چالکود)",
"timing": "بهترین زمان انجام کوددهی (مثلاً: ساعات خنک صبح، قبل از آبیاری)",
"validityPeriod": "محدوده زمانی مجاز برای انجام این کوددهی (مثلاً: تا پایان مرحله پنجه‌زنی)",
"expandableExplanation": "توضیحات تکمیلی و دلایل علمی برای رفع کمبود عناصر (اختیاری)"
},
{
"type": "list",
"title": "نکات مهم ایمنی و اختلاط",
"icon": "list",
"items": [
"نکته اول (مثلاً: از اختلاط با ترکیبات مسی خودداری شود)",
"نکته دوم"
]
},
{
"type": "warning",
"title": "هشدار کمبود عناصر یا سوختگی",
"icon": "alert-triangle",
"content": "متن هشدار (در صورت وجود علائم کمبود شدید یا خطر سوختگی گیاه)"
} }
]
} }
- فقط JSON خروجی بده، بدون توضیح اضافی
- اگر سطح NPK خاک مناسب باشد، در reasoning ذکر کن و مقدار کمتر پیشنهاد بده Note: The "sections" array MUST contain at least one object with "type": "recommendation" dedicated to fertilization. Valid icons for this topic include "leaf", "flask", "list", and "alert-triangle". Ensure the JSON is properly escaped and strictly valid.
- هشدارهای مهم درباره مصرف بیش از حد کود را ذکر کن
- npkRatio بر اساس مرحله رشد گیاه و وضعیت خاک تعیین شود
- amountPerHectare بر اساس نوع خاک و نیاز گیاه
- applicationMethod بر اساس نوع کود و شرایط مزرعه
- applicationInterval بر اساس مرحله رشد و سرعت جذب
- reasoning باید شامل تحلیل EC خاک، pH، و مواد آلی باشد
- مقادیر را به انگلیسی و با واحد بنویس (مثل kg/ha)
+43 -20
View File
@@ -1,23 +1,46 @@
# لحن توصیه آبیاری You are an expert agricultural consultant AI. Your task is to analyze the provided Knowledge Base (Context), which includes scientific agricultural data and specific farm conditions, to provide actionable irrigation advice to farmers.
سبک پاسخ: ### TONE & STYLE
- مستقیم و عملیاتی: زمان، مدت، تعداد دفعات و روش آبیاری را مشخص کن - Be friendly, respectful, and easy to understand for a farmer. Avoid overly complex academic jargon unless explained.
- بر اساس داده‌های هواشناسی (بارش، ET0، دما) و رطوبت خاک - Speak directly to the farmer in Persian (Farsi).
- فرمت خروجی حتماً JSON باشد و دقیقاً به شکل زیر:
### CORE RULES
1. MANDATORY IRRIGATION RECOMMENDATION: Your response MUST always include a clear irrigation recommendation. You cannot simply provide general information; you must tell the farmer what to do regarding irrigation.
2. VALIDITY PERIOD: Every irrigation recommendation MUST include its validity period (e.g., "This recommendation is valid for the next 3 days" or "Valid until the next rainfall"). You must specify this clearly so the farmer knows when to seek new advice.
3. NO EXTRA TEXT: Your entire response MUST be ONLY a valid JSON object. Do not include any greeting text or markdown formatting (like
```json) outside of the JSON structure itself.
4. JSON STRUCTURE: You must strictly adhere to the JSON structure provided below.
### JSON OUTPUT STRUCTURE
{ {
"plan": { "sections": [
"frequencyPerWeek": <int - تعداد دفعات آبیاری در هفته>, {
"durationMinutes": <int - مدت هر بار آبیاری به دقیقه>, "type": "recommendation",
"bestTimeOfDay": "<str - بهترین زمان آبیاری مثل 05:00 - 07:00>", "title": "عنوان توصیه (مانند: برنامه آبیاری فوری)",
"moistureLevel": <int - سطح رطوبت مطلوب خاک به درصد>, "icon": "droplet",
"warning": "<str - هشدار یا توصیه مهم>" "content": "توضیح کوتاه توصیه",
} "frequency": "دوره تناوب آبیاری (اختیاری)",
"amount": "میزان آب مورد نیاز (مثلاً بر اساس میلیمتر یا ساعت آبیاری)",
"timing": "بهترین زمان انجام آبیاری",
"validityPeriod": "مدت زمان اعتبار این توصیه (مثلاً: معتبر برای ۳ روز آینده با توجه به پیش‌بینی هوا)",
"expandableExplanation": "توضیحات تکمیلی و دلایل علمی برای کشاورز (اختیاری)"
},
{
"type": "list",
"title": "نکات مهم",
"icon": "list",
"items": [
"نکته اول",
"نکته دوم"
]
},
{
"type": "warning",
"title": "هشدار تنش آبی یا شرایط خاص",
"icon": "alert-triangle",
"content": "متن هشدار"
} }
- فقط JSON خروجی بده، بدون توضیح اضافی ]
- اگر بارش پیش‌بینی شده باشد، آبیاری را به تعویق بینداز }
- اگر رطوبت خاک کافی است، آبیاری لازم نیست
- هشدارها را در فیلد warning قرار بده Note: The "sections" array MUST contain at least one object with "type": "recommendation" dedicated to irrigation. You can use "list" or "warning" types as needed based on the context. Ensure the JSON is properly escaped and strictly valid.
- مقادیر عددی را بر اساس نوع گیاه، روش آبیاری و مرحله رشد محاسبه کن
- bestTimeOfDay باید بر اساس شرایط آب و هوایی و فصل تعیین شود
- frequencyPerWeek بر اساس نیاز آبی گیاه و شرایط خاک
- durationMinutes بر اساس روش آبیاری و ظرفیت خاک
-1
View File
@@ -15,7 +15,6 @@ urlpatterns = [
# --- App APIs --- # --- App APIs ---
path("api/rag/", include("rag.urls")), path("api/rag/", include("rag.urls")),
path("api/dashboard-data/", include("dashboard_data.urls")), path("api/dashboard-data/", include("dashboard_data.urls")),
path("api/tasks/", include("tasks.urls")),
path("api/soil-data/", include("location_data.urls")), path("api/soil-data/", include("location_data.urls")),
path("api/farm-data/", include("farm_data.urls")), path("api/farm-data/", include("farm_data.urls")),
path("api/plants/", include("plant.urls")), path("api/plants/", include("plant.urls")),
+25 -9
View File
@@ -111,6 +111,29 @@ def _create_audit_log(
return log return log
def _complete_audit_log(audit_log: "ChatAuditLog", response_text: str) -> None:
from .models import ChatAuditLog
audit_log.response_text = response_text
audit_log.status = ChatAuditLog.STATUS_COMPLETED
audit_log.save(update_fields=["response_text", "status", "updated_at"])
def _fail_audit_log(
audit_log: "ChatAuditLog",
error_message: str,
response_text: str = "",
) -> None:
from .models import ChatAuditLog
audit_log.response_text = response_text
audit_log.error_message = error_message
audit_log.status = ChatAuditLog.STATUS_FAILED
audit_log.save(
update_fields=["response_text", "error_message", "status", "updated_at"]
)
def build_rag_context( def build_rag_context(
query: str, query: str,
sensor_uuid: str | None = None, sensor_uuid: str | None = None,
@@ -267,9 +290,7 @@ def chat_rag_stream(
yield content yield content
full_response = "".join(response_chunks) full_response = "".join(response_chunks)
audit_log.response_text = full_response _complete_audit_log(audit_log, full_response)
audit_log.status = ChatAuditLog.STATUS_COMPLETED
audit_log.save(update_fields=["response_text", "status", "updated_at"])
logger.info( logger.info(
"Completed chat response id=%s farm_uuid=%s response_len=%s response=\n%s", "Completed chat response id=%s farm_uuid=%s response_len=%s response=\n%s",
audit_log.id, audit_log.id,
@@ -279,12 +300,7 @@ def chat_rag_stream(
) )
except Exception as exc: except Exception as exc:
partial_response = "".join(response_chunks) partial_response = "".join(response_chunks)
audit_log.response_text = partial_response _fail_audit_log(audit_log, str(exc), partial_response)
audit_log.error_message = str(exc)
audit_log.status = ChatAuditLog.STATUS_FAILED
audit_log.save(
update_fields=["response_text", "error_message", "status", "updated_at"]
)
logger.exception( logger.exception(
"Chat request failed id=%s service_id=%s farm_uuid=%s partial_response_len=%s", "Chat request failed id=%s service_id=%s farm_uuid=%s partial_response_len=%s",
audit_log.id, audit_log.id,
+26 -2
View File
@@ -6,7 +6,13 @@ import json
import logging import logging
from rag.api_provider import get_chat_client from rag.api_provider import get_chat_client
from rag.chat import build_rag_context, _load_service_tone from rag.chat import (
_complete_audit_log,
_create_audit_log,
_fail_audit_log,
_load_service_tone,
build_rag_context,
)
from rag.config import load_rag_config, RAGConfig, get_service_config from rag.config import load_rag_config, RAGConfig, get_service_config
from rag.user_data import build_plant_text from rag.user_data import build_plant_text
@@ -97,6 +103,14 @@ def get_fertilization_recommendation(
{"role": "system", "content": system_content}, {"role": "system", "content": system_content},
{"role": "user", "content": user_query}, {"role": "user", "content": user_query},
] ]
audit_log = _create_audit_log(
farm_uuid=sensor_uuid,
service_id=SERVICE_ID,
model=model,
query=user_query,
system_prompt=system_content,
messages=messages,
)
try: try:
response = client.chat.completions.create( response = client.chat.completions.create(
@@ -106,7 +120,7 @@ def get_fertilization_recommendation(
raw = response.choices[0].message.content.strip() raw = response.choices[0].message.content.strip()
except Exception as exc: except Exception as exc:
logger.error("Fertilization recommendation error for %s: %s", sensor_uuid, exc) logger.error("Fertilization recommendation error for %s: %s", sensor_uuid, exc)
return { result = {
"fertilizer_needed": None, "fertilizer_needed": None,
"fertilizer_type": None, "fertilizer_type": None,
"amount_kg_per_hectare": None, "amount_kg_per_hectare": None,
@@ -114,6 +128,12 @@ def get_fertilization_recommendation(
"npk_status": None, "npk_status": None,
"raw_response": None, "raw_response": None,
} }
_fail_audit_log(
audit_log,
str(exc),
response_text=json.dumps(result, ensure_ascii=False, default=str),
)
return result
try: try:
cleaned = raw cleaned = raw
@@ -128,4 +148,8 @@ def get_fertilization_recommendation(
} }
result["raw_response"] = raw result["raw_response"] = raw
_complete_audit_log(
audit_log,
json.dumps(result, ensure_ascii=False, default=str),
)
return result return result
+26 -2
View File
@@ -8,7 +8,13 @@ import logging
from irrigation.evapotranspiration import calculate_forecast_water_needs, resolve_crop_profile, resolve_kc from irrigation.evapotranspiration import calculate_forecast_water_needs, resolve_crop_profile, resolve_kc
from farm_data.models import SensorData from farm_data.models import SensorData
from rag.api_provider import get_chat_client from rag.api_provider import get_chat_client
from rag.chat import build_rag_context, _load_service_tone from rag.chat import (
_complete_audit_log,
_create_audit_log,
_fail_audit_log,
_load_service_tone,
build_rag_context,
)
from rag.config import load_rag_config, RAGConfig, get_service_config from rag.config import load_rag_config, RAGConfig, get_service_config
from rag.user_data import build_plant_text, build_irrigation_method_text from rag.user_data import build_plant_text, build_irrigation_method_text
from weather.models import WeatherForecast from weather.models import WeatherForecast
@@ -149,6 +155,14 @@ def get_irrigation_recommendation(
{"role": "system", "content": system_content}, {"role": "system", "content": system_content},
{"role": "user", "content": user_query}, {"role": "user", "content": user_query},
] ]
audit_log = _create_audit_log(
farm_uuid=sensor_uuid,
service_id=SERVICE_ID,
model=model,
query=user_query,
system_prompt=system_content,
messages=messages,
)
try: try:
response = client.chat.completions.create( response = client.chat.completions.create(
@@ -158,13 +172,19 @@ def get_irrigation_recommendation(
raw = response.choices[0].message.content.strip() raw = response.choices[0].message.content.strip()
except Exception as exc: except Exception as exc:
logger.error("Irrigation recommendation error for %s: %s", sensor_uuid, exc) logger.error("Irrigation recommendation error for %s: %s", sensor_uuid, exc)
return { result = {
"irrigation_needed": None, "irrigation_needed": None,
"amount_mm": None, "amount_mm": None,
"reason": f"خطا در دریافت توصیه: {exc}", "reason": f"خطا در دریافت توصیه: {exc}",
"next_check_date": None, "next_check_date": None,
"raw_response": None, "raw_response": None,
} }
_fail_audit_log(
audit_log,
str(exc),
response_text=json.dumps(result, ensure_ascii=False, default=str),
)
return result
try: try:
cleaned = raw cleaned = raw
@@ -184,4 +204,8 @@ def get_irrigation_recommendation(
"crop_profile": crop_profile, "crop_profile": crop_profile,
"active_kc": active_kc, "active_kc": active_kc,
} }
_complete_audit_log(
audit_log,
json.dumps(result, ensure_ascii=False, default=str),
)
return result return result
+76
View File
@@ -0,0 +1,76 @@
from unittest.mock import patch
from django.test import TestCase
from rest_framework.test import APIClient
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={
"sensor_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(
sensor_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={
"sensor_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(
sensor_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)
-4
View File
@@ -3,15 +3,11 @@ from django.urls import path
from .views import ( from .views import (
ChatView, ChatView,
IrrigationRecommendationView, IrrigationRecommendationView,
IrrigationRecommendationStatusView,
FertilizationRecommendationView, FertilizationRecommendationView,
FertilizationRecommendationStatusView,
) )
urlpatterns = [ urlpatterns = [
path("chat/", ChatView.as_view()), path("chat/", ChatView.as_view()),
path("recommend/irrigation/", IrrigationRecommendationView.as_view(), name="recommend-irrigation"), path("recommend/irrigation/", IrrigationRecommendationView.as_view(), name="recommend-irrigation"),
path("recommend/irrigation/<str:task_id>/status/", IrrigationRecommendationStatusView.as_view(), name="recommend-irrigation-status"),
path("recommend/fertilization/", FertilizationRecommendationView.as_view(), name="recommend-fertilization"), path("recommend/fertilization/", FertilizationRecommendationView.as_view(), name="recommend-fertilization"),
path("recommend/fertilization/<str:task_id>/status/", FertilizationRecommendationStatusView.as_view(), name="recommend-fertilization-status"),
] ]
+63 -129
View File
@@ -21,8 +21,6 @@ from config.openapi import (
build_envelope_serializer, build_envelope_serializer,
build_message_response_serializer, build_message_response_serializer,
build_response, build_response,
build_task_queue_data_serializer,
build_task_status_data_serializer,
) )
from .chat import chat_rag_stream from .chat import chat_rag_stream
@@ -33,27 +31,19 @@ logger = logging.getLogger(__name__)
RagChatErrorResponseSerializer = build_message_response_serializer( RagChatErrorResponseSerializer = build_message_response_serializer(
"RagChatErrorResponseSerializer" "RagChatErrorResponseSerializer"
) )
RagIrrigationQueueResponseSerializer = build_envelope_serializer(
"RagIrrigationQueueResponseSerializer",
build_task_queue_data_serializer("RagIrrigationQueueDataSerializer"),
)
RagIrrigationStatusResponseSerializer = build_envelope_serializer(
"RagIrrigationStatusResponseSerializer",
build_task_status_data_serializer("RagIrrigationStatusDataSerializer"),
)
RagFertilizationQueueResponseSerializer = build_envelope_serializer(
"RagFertilizationQueueResponseSerializer",
build_task_queue_data_serializer("RagFertilizationQueueDataSerializer"),
)
RagFertilizationStatusResponseSerializer = build_envelope_serializer(
"RagFertilizationStatusResponseSerializer",
build_task_status_data_serializer("RagFertilizationStatusDataSerializer"),
)
RagValidationErrorResponseSerializer = build_envelope_serializer( RagValidationErrorResponseSerializer = build_envelope_serializer(
"RagValidationErrorResponseSerializer", "RagValidationErrorResponseSerializer",
data_required=False, data_required=False,
allow_null=True, allow_null=True,
) )
RagIrrigationResponseSerializer = build_envelope_serializer(
"RagIrrigationResponseSerializer",
drf_serializers.JSONField(),
)
RagFertilizationResponseSerializer = build_envelope_serializer(
"RagFertilizationResponseSerializer",
drf_serializers.JSONField(),
)
class ChatView(APIView): class ChatView(APIView):
@@ -157,17 +147,17 @@ class ChatView(APIView):
class IrrigationRecommendationView(APIView): class IrrigationRecommendationView(APIView):
""" """
توصیه آبیاری با Celery. توصیه آبیاری به صورت مستقیم.
POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name. POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name.
تسک در صف قرار میگیرد و task_id برگشت داده میشود. نتیجه همان لحظه برگشت داده میشود.
""" """
@extend_schema( @extend_schema(
tags=["RAG Recommendations"], tags=["RAG Recommendations"],
summary="درخواست توصیه آبیاری", summary="درخواست توصیه آبیاری",
description=( description=(
"داده‌های سنسور، گیاه و روش آبیاری را دریافت کرده و یک تسک Celery " "داده‌های سنسور، گیاه و روش آبیاری را دریافت کرده و "
"برای تولید توصیه آبیاری در صف قرار می‌دهد." "توصیه آبیاری را به صورت مستقیم برمی‌گرداند."
), ),
request=inline_serializer( request=inline_serializer(
name="IrrigationRecommendationRequest", name="IrrigationRecommendationRequest",
@@ -180,14 +170,18 @@ class IrrigationRecommendationView(APIView):
}, },
), ),
responses={ responses={
202: build_response( 200: build_response(
RagIrrigationQueueResponseSerializer, RagIrrigationResponseSerializer,
"تسک توصیه آبیاری در صف قرار گرفت.", "توصیه آبیاری با موفقیت تولید شد.",
), ),
400: build_response( 400: build_response(
RagValidationErrorResponseSerializer, RagValidationErrorResponseSerializer,
"پارامتر ورودی نامعتبر است.", "پارامتر ورودی نامعتبر است.",
), ),
500: build_response(
RagValidationErrorResponseSerializer,
"خطا در تولید توصیه آبیاری.",
),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -203,7 +197,7 @@ class IrrigationRecommendationView(APIView):
], ],
) )
def post(self, request: Request): def post(self, request: Request):
from rag.tasks import irrigation_recommendation_task from rag.services.irrigation import get_irrigation_recommendation
sensor_uuid = request.data.get("sensor_uuid") sensor_uuid = request.data.get("sensor_uuid")
if not sensor_uuid: if not sensor_uuid:
@@ -212,72 +206,40 @@ class IrrigationRecommendationView(APIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
task = irrigation_recommendation_task.delay( try:
sensor_uuid=str(sensor_uuid), result = get_irrigation_recommendation(
plant_name=request.data.get("plant_name"), sensor_uuid=str(sensor_uuid),
growth_stage=request.data.get("growth_stage"), plant_name=request.data.get("plant_name"),
irrigation_method_name=request.data.get("irrigation_method_name"), growth_stage=request.data.get("growth_stage"),
query=request.data.get("query"), 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)
return Response(
{"code": 500, "msg": "خطا در تولید توصیه آبیاری.", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response( return Response(
{ {"code": 200, "msg": "success", "data": result},
"code": 202,
"msg": "تسک توصیه آبیاری در صف قرار گرفت.",
"data": {
"task_id": task.id,
"status_url": f"/api/rag/recommend/irrigation/{task.id}/status/",
},
},
status=status.HTTP_202_ACCEPTED,
)
class IrrigationRecommendationStatusView(APIView):
"""وضعیت تسک توصیه آبیاری."""
@extend_schema(
tags=["RAG Recommendations"],
summary="وضعیت تسک توصیه آبیاری",
description="وضعیت تسک Celery توصیه آبیاری را برمی‌گرداند.",
responses={
200: build_response(
RagIrrigationStatusResponseSerializer,
"وضعیت فعلی تسک توصیه آبیاری.",
),
},
)
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, status=status.HTTP_200_OK,
) )
class FertilizationRecommendationView(APIView): class FertilizationRecommendationView(APIView):
""" """
توصیه کودهی با Celery. توصیه کودهی به صورت مستقیم.
POST با sensor_uuid، plant_name، growth_stage. POST با sensor_uuid، plant_name، growth_stage.
تسک در صف قرار میگیرد و task_id برگشت داده میشود. نتیجه همان لحظه برگشت داده میشود.
""" """
@extend_schema( @extend_schema(
tags=["RAG Recommendations"], tags=["RAG Recommendations"],
summary="درخواست توصیه کودهی", summary="درخواست توصیه کودهی",
description=( description=(
"داده‌های سنسور و گیاه را دریافت کرده و یک تسک Celery " "داده‌های سنسور و گیاه را دریافت کرده و "
"برای تولید توصیه کودهی در صف قرار می‌دهد." "توصیه کودهی را به صورت مستقیم برمی‌گرداند."
), ),
request=inline_serializer( request=inline_serializer(
name="FertilizationRecommendationRequest", name="FertilizationRecommendationRequest",
@@ -289,14 +251,18 @@ class FertilizationRecommendationView(APIView):
}, },
), ),
responses={ responses={
202: build_response( 200: build_response(
RagFertilizationQueueResponseSerializer, RagFertilizationResponseSerializer,
"تسک توصیه کودهی در صف قرار گرفت.", "توصیه کودهی با موفقیت تولید شد.",
), ),
400: build_response( 400: build_response(
RagValidationErrorResponseSerializer, RagValidationErrorResponseSerializer,
"پارامتر ورودی نامعتبر است.", "پارامتر ورودی نامعتبر است.",
), ),
500: build_response(
RagValidationErrorResponseSerializer,
"خطا در تولید توصیه کودهی.",
),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
@@ -311,7 +277,7 @@ class FertilizationRecommendationView(APIView):
], ],
) )
def post(self, request: Request): def post(self, request: Request):
from rag.tasks import fertilization_recommendation_task from rag.services.fertilization import get_fertilization_recommendation
sensor_uuid = request.data.get("sensor_uuid") sensor_uuid = request.data.get("sensor_uuid")
if not sensor_uuid: if not sensor_uuid:
@@ -320,53 +286,21 @@ class FertilizationRecommendationView(APIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
task = fertilization_recommendation_task.delay( try:
sensor_uuid=str(sensor_uuid), result = get_fertilization_recommendation(
plant_name=request.data.get("plant_name"), sensor_uuid=str(sensor_uuid),
growth_stage=request.data.get("growth_stage"), plant_name=request.data.get("plant_name"),
query=request.data.get("query"), 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)
return Response(
{"code": 500, "msg": "خطا در تولید توصیه کودهی.", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
return Response( return Response(
{ {"code": 200, "msg": "success", "data": result},
"code": 202,
"msg": "تسک توصیه کودهی در صف قرار گرفت.",
"data": {
"task_id": task.id,
"status_url": f"/api/rag/recommend/fertilization/{task.id}/status/",
},
},
status=status.HTTP_202_ACCEPTED,
)
class FertilizationRecommendationStatusView(APIView):
"""وضعیت تسک توصیه کودهی."""
@extend_schema(
tags=["RAG Recommendations"],
summary="وضعیت تسک توصیه کودهی",
description="وضعیت تسک Celery توصیه کودهی را برمی‌گرداند.",
responses={
200: build_response(
RagFertilizationStatusResponseSerializer,
"وضعیت فعلی تسک توصیه کودهی.",
),
},
)
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, status=status.HTTP_200_OK,
) )
-23
View File
@@ -803,29 +803,6 @@ def main():
error_response(400, "داده نامعتبر.", {"code": ["This field is required."]}), error_response(400, "داده نامعتبر.", {"code": ["This field is required."]}),
) )
register(
"tasks/post_200.json",
"POST",
"/api/tasks/",
200,
"Task trigger success",
ok_response({"task_id": "sample-task-123"}),
)
for name, payload in {
"pending": task_pending("sample-task-123"),
"progress": task_progress("sample-task-123", TASK_PROGRESS),
"success": task_success("sample-task-123", "done"),
"failure": task_failure("sample-task-123", "Sample task failed."),
}.items():
register(
f"tasks/status/get_200_{name}.json",
"GET",
"/api/tasks/{task_id}/status/",
200,
f"Task status {name}",
payload,
)
(MOCK_DIR / "index.json").write_text( (MOCK_DIR / "index.json").write_text(
json.dumps(index, ensure_ascii=False, indent=2) + "\n", json.dumps(index, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8", encoding="utf-8",
View File
-7
View File
@@ -1,7 +0,0 @@
from django.apps import AppConfig
class TasksConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "tasks"
verbose_name = "Celery Tasks"
-15
View File
@@ -1,15 +0,0 @@
import time
from config.celery import app
@app.task(bind=True)
def sample_task(self, duration: int = 1):
"""تسک نمونه برای تست. duration تعداد ثانیه‌ای که تسک طول می‌کشه."""
for i in range(duration):
self.update_state(
state="PROGRESS",
meta={"current": i + 1, "total": duration, "message": "در حال پردازش..."},
)
time.sleep(1)
return {"status": "completed", "duration": duration}
-8
View File
@@ -1,8 +0,0 @@
from django.urls import path
from .views import TaskStatusView, TaskTriggerView
urlpatterns = [
path("", TaskTriggerView.as_view(), name="task-trigger"),
path("<str:task_id>/status/", TaskStatusView.as_view(), name="task-status"),
]
-128
View File
@@ -1,128 +0,0 @@
from celery.result import AsyncResult
from drf_spectacular.utils import (
OpenApiExample,
OpenApiResponse,
extend_schema,
inline_serializer,
)
from rest_framework import status
from rest_framework import serializers as drf_serializers
from rest_framework.response import Response
from rest_framework.views import APIView
from config.openapi import (
build_envelope_serializer,
build_response,
build_task_status_data_serializer,
)
from .celery_tasks import sample_task
TaskTriggerResponseSerializer = build_envelope_serializer(
"TaskTriggerResponseSerializer",
inline_serializer(
name="TaskTriggerPayloadSerializer",
fields={
"task_id": drf_serializers.CharField(),
},
),
)
TaskStatusResponseSerializer = build_envelope_serializer(
"TaskStatusEnvelopeSerializer",
build_task_status_data_serializer("TaskStatusPayloadSerializer"),
)
class TaskTriggerView(APIView):
"""
ثبت و اجرای تسک.
POST با بدنه اختیاری: {"duration": 3} - مدت زمان تسک به ثانیه.
"""
@extend_schema(
tags=["Tasks"],
summary="ثبت و اجرای تسک",
description="یک تسک نمونه را در صف Celery قرار می‌دهد.",
request=inline_serializer(
name="TaskTriggerRequest",
fields={
"duration": drf_serializers.IntegerField(
required=False, default=1, help_text="مدت زمان تسک به ثانیه (۱–۶۰)"
),
},
),
responses={
200: build_response(
TaskTriggerResponseSerializer,
"تسک نمونه با موفقیت در صف قرار گرفت.",
),
},
examples=[
OpenApiExample(
"نمونه درخواست",
value={"duration": 3},
request_only=True,
),
OpenApiExample(
"نمونه پاسخ",
value={"code": 200, "msg": "success", "data": {"task_id": "abc123-def456"}},
response_only=True,
),
],
)
def post(self, request):
duration = request.data.get("duration", 1)
try:
duration = int(duration)
duration = max(1, min(duration, 60))
except (TypeError, ValueError):
duration = 1
result = sample_task.delay(duration)
return Response(
{"code": 200, "msg": "success", "data": {"task_id": result.id}},
status=status.HTTP_200_OK,
)
class TaskStatusView(APIView):
"""
وضعیت تسک بر اساس task_id.
GET /api/tasks/<task_id>/status/
"""
@extend_schema(
tags=["Tasks"],
summary="وضعیت تسک",
description="وضعیت یک تسک Celery را بر اساس task_id برمی‌گرداند.",
responses={
200: build_response(
TaskStatusResponseSerializer,
"وضعیت فعلی تسک Celery.",
),
},
examples=[
OpenApiExample(
"تسک موفق",
value={"code": 200, "msg": "success", "data": {"task_id": "abc123", "status": "SUCCESS", "result": "done"}},
response_only=True,
),
],
)
def get(self, request, task_id):
result = AsyncResult(task_id)
state = result.state
data = {"task_id": task_id, "status": state}
if state == "PENDING":
data["message"] = "تسک در صف یا یافت نشد."
elif state == "PROGRESS":
data["progress"] = result.info
elif state == "SUCCESS":
data["result"] = result.result
elif state == "FAILURE":
data["error"] = str(result.result)
return Response(
{"code": 200, "msg": "success", "data": data},
status=status.HTTP_200_OK,
)