UPDATE
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
|||||||
|
راهنمای استخراج برنامه کودهی از متن آزاد
|
||||||
|
|
||||||
|
هدف:
|
||||||
|
تبدیل توضیح متنی کشاورز درباره برنامه کودهی به JSON ساختاریافته.
|
||||||
|
|
||||||
|
اطلاعات کلیدی که معمولا باید استخراج شوند:
|
||||||
|
- نام محصول
|
||||||
|
- مرحله رشد
|
||||||
|
- هدف مصرف
|
||||||
|
- نام یا فرمول کود
|
||||||
|
- مقدار مصرف
|
||||||
|
- روش مصرف
|
||||||
|
- زمان مصرف
|
||||||
|
- فاصله بین نوبت ها
|
||||||
|
- توضیح تکمیلی یا هشدار
|
||||||
|
|
||||||
|
نمونه عبارت های رایج:
|
||||||
|
- هر 10 روز یک بار
|
||||||
|
- بعد از آبیاری
|
||||||
|
- به صورت کودآبیاری
|
||||||
|
- سرک
|
||||||
|
- محلول پاشی
|
||||||
|
- 35 کیلوگرم در هکتار
|
||||||
|
- 20-20-20
|
||||||
|
- برای تقویت رشد رویشی
|
||||||
|
- برای شروع گلدهی
|
||||||
|
|
||||||
|
اگر متن ناقص بود، باید فقط سوال های لازم برای تکمیل برنامه نهایی پرسیده شود و از حدس زدن خودداری شود.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
راهنمای استخراج برنامه آبیاری از متن آزاد
|
||||||
|
|
||||||
|
هدف:
|
||||||
|
تبدیل توضیح متنی کشاورز درباره برنامه آبیاری به JSON ساختاریافته.
|
||||||
|
|
||||||
|
اطلاعات کلیدی که معمولا باید استخراج شوند:
|
||||||
|
- نام محصول
|
||||||
|
- مرحله رشد
|
||||||
|
- روش آبیاری
|
||||||
|
- مقدار آب در هر نوبت
|
||||||
|
- مدت زمان هر نوبت
|
||||||
|
- فاصله یا تعداد دفعات آبیاری
|
||||||
|
- زمان مناسب اجرا در روز
|
||||||
|
- تاریخ شروع یا شرایط شروع
|
||||||
|
- ناحیه یا سطح هدف
|
||||||
|
- نکات تکمیلی
|
||||||
|
|
||||||
|
نمونه عبارت های رایج:
|
||||||
|
- هر سه روز یک بار
|
||||||
|
- هفته ای دو نوبت
|
||||||
|
- صبح زود
|
||||||
|
- بعد از غروب
|
||||||
|
- 20 لیتر برای هر بوته
|
||||||
|
- 25 دقیقه
|
||||||
|
- فقط در ردیف های جنوبی
|
||||||
|
- اگر هوا خیلی گرم شد یک نوبت اضافه شود
|
||||||
|
|
||||||
|
اگر متن ناقص بود، باید فقط درباره اطلاعاتی سوال شود که برای ساخت برنامه قابل استفاده لازم هستند.
|
||||||
@@ -51,6 +51,16 @@ knowledge_bases:
|
|||||||
tone_file: "config/tones/fertilization_tone.txt"
|
tone_file: "config/tones/fertilization_tone.txt"
|
||||||
description: "پایگاه دانش توصیه کودهی"
|
description: "پایگاه دانش توصیه کودهی"
|
||||||
|
|
||||||
|
irrigation_plan_parser:
|
||||||
|
path: "config/knowledge_base/irrigation_plan_parser"
|
||||||
|
tone_file: "config/tones/irrigation_plan_parser_tone.txt"
|
||||||
|
description: "پایگاه دانش استخراج برنامه آبیاری از متن آزاد کاربر"
|
||||||
|
|
||||||
|
fertilization_plan_parser:
|
||||||
|
path: "config/knowledge_base/fertilization_plan_parser"
|
||||||
|
tone_file: "config/tones/fertilization_plan_parser_tone.txt"
|
||||||
|
description: "پایگاه دانش استخراج برنامه کودهی از متن آزاد کاربر"
|
||||||
|
|
||||||
farm_alerts:
|
farm_alerts:
|
||||||
path: "config/knowledge_base/farm_alerts"
|
path: "config/knowledge_base/farm_alerts"
|
||||||
tone_file: "config/tones/farm_alerts_tone.txt"
|
tone_file: "config/tones/farm_alerts_tone.txt"
|
||||||
@@ -130,6 +140,34 @@ services:
|
|||||||
avalai_base_url: "https://api.avalai.ir/v1"
|
avalai_base_url: "https://api.avalai.ir/v1"
|
||||||
avalai_api_key_env: "AVALAI_API_KEY"
|
avalai_api_key_env: "AVALAI_API_KEY"
|
||||||
|
|
||||||
|
irrigation_plan_parser:
|
||||||
|
knowledge_base: "irrigation_plan_parser"
|
||||||
|
tone_file: "config/tones/irrigation_plan_parser_tone.txt"
|
||||||
|
use_user_embeddings: false
|
||||||
|
description: "سرویس استخراج برنامه آبیاری از متن کاربر"
|
||||||
|
system_prompt: "Only return valid JSON for irrigation plan extraction and clarification."
|
||||||
|
llm:
|
||||||
|
provider: "gapgpt"
|
||||||
|
model: "gpt-4o"
|
||||||
|
base_url: "https://api.gapgpt.app/v1"
|
||||||
|
api_key_env: "GAPGPT_API_KEY"
|
||||||
|
avalai_base_url: "https://api.avalai.ir/v1"
|
||||||
|
avalai_api_key_env: "AVALAI_API_KEY"
|
||||||
|
|
||||||
|
fertilization_plan_parser:
|
||||||
|
knowledge_base: "fertilization_plan_parser"
|
||||||
|
tone_file: "config/tones/fertilization_plan_parser_tone.txt"
|
||||||
|
use_user_embeddings: false
|
||||||
|
description: "سرویس استخراج برنامه کودهی از متن کاربر"
|
||||||
|
system_prompt: "Only return valid JSON for fertilization plan extraction and clarification."
|
||||||
|
llm:
|
||||||
|
provider: "gapgpt"
|
||||||
|
model: "gpt-4o"
|
||||||
|
base_url: "https://api.gapgpt.app/v1"
|
||||||
|
api_key_env: "GAPGPT_API_KEY"
|
||||||
|
avalai_base_url: "https://api.avalai.ir/v1"
|
||||||
|
avalai_api_key_env: "AVALAI_API_KEY"
|
||||||
|
|
||||||
farm_alerts:
|
farm_alerts:
|
||||||
knowledge_base: "farm_alerts"
|
knowledge_base: "farm_alerts"
|
||||||
tone_file: "config/tones/farm_alerts_tone.txt"
|
tone_file: "config/tones/farm_alerts_tone.txt"
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
شما یک دستیار دقیق برای استخراج برنامه کودهی از متن آزاد کشاورز هستید.
|
||||||
|
|
||||||
|
هدف:
|
||||||
|
- متن آزاد کاربر را به JSON ساختاریافته برنامه کودهی تبدیل کن.
|
||||||
|
- اگر هر بخش مهمی ناقص بود، به جای حدس زدن سوال بپرس.
|
||||||
|
|
||||||
|
قواعد قطعی:
|
||||||
|
- فقط و فقط JSON معتبر برگردان.
|
||||||
|
- هیچ متن اضافه، markdown، توضیح بیرون از JSON، کدبلاک یا کلید اضافه تولید نکن.
|
||||||
|
- اگر حتی یکی از فیلدهای اصلی خالی، null، نامشخص یا مبهم بود باید `status` برابر `needs_clarification` باشد.
|
||||||
|
- در حالت `completed` هیچ فیلد null یا رشته خالی در `collected_data` و `final_plan` نباید وجود داشته باشد.
|
||||||
|
- اگر متن ناقص بود، سوال ها باید کوتاه، روشن، غیرتکراری و کاملا کاربردی باشند.
|
||||||
|
- از حدس زدن نام کود، فرمول، مقدار، روش مصرف، زمان مصرف، فاصله بین نوبت ها یا مرحله رشد خودداری کن.
|
||||||
|
- اگر کاربر `answers` و `partial_plan` داده باشد، اول آن ها را با متن جدید ادغام کن و فقط سوال های باقی مانده را بپرس.
|
||||||
|
- اگر چند کود در متن آمده بود، همه را در `applications` لیست کن.
|
||||||
|
- زبان همه `summary`، `question` و `rationale` ها فارسی باشد.
|
||||||
|
|
||||||
|
فیلدهای اصلی که برای تکمیل برنامه لازم هستند:
|
||||||
|
- `crop_name`
|
||||||
|
- `growth_stage`
|
||||||
|
- `fertilizer_name`
|
||||||
|
- `formula`
|
||||||
|
- `amount`
|
||||||
|
- `application_method`
|
||||||
|
- `timing`
|
||||||
|
- `interval_days`
|
||||||
|
|
||||||
|
ساختار دقیق JSON خروجی:
|
||||||
|
{
|
||||||
|
"status": "completed" | "needs_clarification",
|
||||||
|
"summary": "string",
|
||||||
|
"missing_fields": ["string"],
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"field": "string",
|
||||||
|
"question": "string",
|
||||||
|
"rationale": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collected_data": {
|
||||||
|
"crop_name": "string|null",
|
||||||
|
"growth_stage": "string|null",
|
||||||
|
"objective": "string|null",
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"fertilizer_name": "string|null",
|
||||||
|
"formula": "string|null",
|
||||||
|
"amount": "string|null",
|
||||||
|
"application_method": "string|null",
|
||||||
|
"timing": "string|null",
|
||||||
|
"interval_days": "integer|null",
|
||||||
|
"purpose": "string|null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notes": ["string"]
|
||||||
|
},
|
||||||
|
"final_plan": {
|
||||||
|
"crop_name": "string",
|
||||||
|
"growth_stage": "string",
|
||||||
|
"objective": "string|null",
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"fertilizer_name": "string",
|
||||||
|
"formula": "string",
|
||||||
|
"amount": "string",
|
||||||
|
"application_method": "string",
|
||||||
|
"timing": "string",
|
||||||
|
"interval_days": "integer",
|
||||||
|
"purpose": "string|null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notes": ["string"]
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
|
||||||
|
منطق وضعیت:
|
||||||
|
- اگر همه فیلدهای اصلی کامل بودند:
|
||||||
|
- `status = "completed"`
|
||||||
|
- `missing_fields = []`
|
||||||
|
- `questions = []`
|
||||||
|
- `final_plan` باید کامل و بدون null در فیلدهای اصلی باشد
|
||||||
|
- اگر حتی یکی از فیلدهای اصلی ناقص بود:
|
||||||
|
- `status = "needs_clarification"`
|
||||||
|
- `missing_fields` فقط فیلدهای ناقص را شامل شود
|
||||||
|
- `questions` برای همان فیلدهای ناقص ساخته شود
|
||||||
|
- `final_plan = null`
|
||||||
|
|
||||||
|
نمونه سوال خوب:
|
||||||
|
- "محصول الان در چه مرحله رشدی قرار دارد؟"
|
||||||
|
- "فرمول یا آنالیز کود چیست؟ مثلا 20-20-20."
|
||||||
|
- "مقدار مصرف هر نوبت کود چقدر است؟"
|
||||||
|
- "فاصله بین نوبت های مصرف کود چند روز است؟"
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
شما یک دستیار دقیق برای استخراج برنامه آبیاری از متن آزاد کشاورز هستید.
|
||||||
|
|
||||||
|
هدف:
|
||||||
|
- متن آزاد کاربر را به JSON ساختاریافته برنامه آبیاری تبدیل کن.
|
||||||
|
- اگر هر بخش مهمی ناقص بود، به جای حدس زدن سوال بپرس.
|
||||||
|
|
||||||
|
قواعد قطعی:
|
||||||
|
- فقط و فقط JSON معتبر برگردان.
|
||||||
|
- هیچ متن اضافه، markdown، توضیح بیرون از JSON، کدبلاک یا کلید اضافه تولید نکن.
|
||||||
|
- اگر حتی یکی از فیلدهای اصلی خالی، null، نامشخص یا مبهم بود باید `status` برابر `needs_clarification` باشد.
|
||||||
|
- در حالت `completed` هیچ فیلد null یا رشته خالی در `collected_data` و `final_plan` نباید وجود داشته باشد.
|
||||||
|
- اگر متن ناقص بود، سوال ها باید کوتاه، روشن، غیرتکراری و کاملا کاربردی باشند.
|
||||||
|
- از حدس زدن مقدار آب، مدت زمان، فاصله آبیاری، زمان اجرا، مرحله رشد، تاریخ شروع یا محدوده هدف خودداری کن.
|
||||||
|
- اگر کاربر `answers` و `partial_plan` داده باشد، اول آن ها را با متن جدید ادغام کن و فقط سوال های باقی مانده را بپرس.
|
||||||
|
- زبان همه `summary`، `question` و `rationale` ها فارسی باشد.
|
||||||
|
|
||||||
|
فیلدهای اصلی که برای تکمیل برنامه لازم هستند:
|
||||||
|
- `crop_name`
|
||||||
|
- `growth_stage`
|
||||||
|
- `irrigation_method`
|
||||||
|
- `water_amount_per_event`
|
||||||
|
- `duration_minutes`
|
||||||
|
- `frequency_text`
|
||||||
|
- `interval_days`
|
||||||
|
- `preferred_time_of_day`
|
||||||
|
- `start_date`
|
||||||
|
- `target_area`
|
||||||
|
|
||||||
|
ساختار دقیق JSON خروجی:
|
||||||
|
{
|
||||||
|
"status": "completed" | "needs_clarification",
|
||||||
|
"summary": "string",
|
||||||
|
"missing_fields": ["string"],
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": "string",
|
||||||
|
"field": "string",
|
||||||
|
"question": "string",
|
||||||
|
"rationale": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collected_data": {
|
||||||
|
"crop_name": "string|null",
|
||||||
|
"growth_stage": "string|null",
|
||||||
|
"irrigation_method": "string|null",
|
||||||
|
"water_amount_per_event": "string|null",
|
||||||
|
"duration_minutes": "integer|null",
|
||||||
|
"frequency_text": "string|null",
|
||||||
|
"interval_days": "integer|null",
|
||||||
|
"preferred_time_of_day": "string|null",
|
||||||
|
"start_date": "string|null",
|
||||||
|
"target_area": "string|null",
|
||||||
|
"trigger_conditions": ["string"],
|
||||||
|
"notes": ["string"]
|
||||||
|
},
|
||||||
|
"final_plan": {
|
||||||
|
"crop_name": "string",
|
||||||
|
"growth_stage": "string",
|
||||||
|
"irrigation_method": "string",
|
||||||
|
"water_amount_per_event": "string",
|
||||||
|
"duration_minutes": "integer",
|
||||||
|
"frequency_text": "string",
|
||||||
|
"interval_days": "integer",
|
||||||
|
"preferred_time_of_day": "string",
|
||||||
|
"start_date": "string",
|
||||||
|
"target_area": "string",
|
||||||
|
"trigger_conditions": ["string"],
|
||||||
|
"notes": ["string"]
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
|
||||||
|
منطق وضعیت:
|
||||||
|
- اگر همه فیلدهای اصلی کامل بودند:
|
||||||
|
- `status = "completed"`
|
||||||
|
- `missing_fields = []`
|
||||||
|
- `questions = []`
|
||||||
|
- `final_plan` باید کامل و بدون null باشد
|
||||||
|
- اگر حتی یکی از فیلدهای اصلی ناقص بود:
|
||||||
|
- `status = "needs_clarification"`
|
||||||
|
- `missing_fields` فقط فیلدهای ناقص را شامل شود
|
||||||
|
- `questions` برای همان فیلدهای ناقص ساخته شود
|
||||||
|
- `final_plan = null`
|
||||||
|
|
||||||
|
نمونه سوال خوب:
|
||||||
|
- "محصول الان در چه مرحله رشدی قرار دارد؟"
|
||||||
|
- "این برنامه از چه تاریخی باید شروع شود؟"
|
||||||
|
- "این برنامه برای کل مزرعه است یا فقط یک بخش خاص؟"
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
# Free-Text Plan Parser APIs
|
||||||
|
|
||||||
|
این فایل برای تیم فرانتاند آماده شده و دو API جدید زیر را توضیح میدهد:
|
||||||
|
|
||||||
|
- `POST /api/irrigation/plan-from-text/`
|
||||||
|
- `POST /api/fertilization/plan-from-text/`
|
||||||
|
|
||||||
|
هدف هر دو API:
|
||||||
|
|
||||||
|
- کاربر یک متن آزاد مینویسد
|
||||||
|
- بکاند تلاش میکند برنامه آبیاری یا کودهی را به JSON ساختاریافته تبدیل کند
|
||||||
|
- اگر اطلاعات کامل باشد، JSON نهایی برمیگردد
|
||||||
|
- اگر اطلاعات ناقص باشد، API سوالهای تکمیلی برمیگرداند
|
||||||
|
- فرانتاند سوالها را از کاربر میپرسد و پاسخها را دوباره برای API میفرستد
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## رفتار کلی هر دو API
|
||||||
|
|
||||||
|
هر دو endpoint یک flow یکسان دارند:
|
||||||
|
|
||||||
|
1. کاربر متن آزاد اولیه را میفرستد
|
||||||
|
2. اگر متن کامل باشد:
|
||||||
|
- `status = "completed"`
|
||||||
|
- `final_plan` برمیگردد
|
||||||
|
3. اگر متن ناقص باشد:
|
||||||
|
- `status = "needs_clarification"`
|
||||||
|
- `missing_fields` برمیگردد
|
||||||
|
- `questions` برمیگردد
|
||||||
|
4. فرانتاند پاسخ کاربر به سوالها را جمع میکند
|
||||||
|
5. دوباره همان endpoint را با `answers` و `partial_plan` صدا میزند
|
||||||
|
6. این روند تا ساخته شدن `final_plan` ادامه پیدا میکند
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## الگوی کلی response
|
||||||
|
|
||||||
|
هر دو API از envelope استاندارد استفاده میکنند:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "موفق",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### معنی فیلدهای envelope
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|---|---|---|
|
||||||
|
| `code` | number | کد منطقی پاسخ |
|
||||||
|
| `msg` | string | پیام کوتاه پاسخ |
|
||||||
|
| `data` | object | داده اصلی API |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) API استخراج برنامه آبیاری
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/irrigation/plan-from-text/
|
||||||
|
```
|
||||||
|
|
||||||
|
### کاربرد
|
||||||
|
|
||||||
|
این API متن آزاد کاربر درباره برنامه آبیاری را به JSON ساختاریافته تبدیل میکند.
|
||||||
|
|
||||||
|
### Request Body
|
||||||
|
|
||||||
|
هر سه فیلد زیر اختیاری هستند، اما حداقل یکی از اینها باید ارسال شود:
|
||||||
|
|
||||||
|
- `message`
|
||||||
|
- `answers`
|
||||||
|
- `partial_plan`
|
||||||
|
|
||||||
|
#### ساختار request
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "برای گوجه فرنگی با آبیاری قطره ای هر سه روز یک بار صبح زود 25 دقیقه آبیاری می کنم و حدود 18 لیتر برای هر بوته می دهم.",
|
||||||
|
"answers": {
|
||||||
|
"growth_stage": "گلدهی"
|
||||||
|
},
|
||||||
|
"partial_plan": {
|
||||||
|
"crop_name": "گوجه فرنگی",
|
||||||
|
"irrigation_method": "قطره ای"
|
||||||
|
},
|
||||||
|
"farm_uuid": "11111111-1111-1111-1111-111111111111"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### فیلدهای request
|
||||||
|
|
||||||
|
| فیلد | نوع | اجباری | توضیح |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `message` | string | خیر | متن آزاد کاربر |
|
||||||
|
| `answers` | object | خیر | پاسخهای تکمیلی کاربر به سوالهایی که قبلا API داده |
|
||||||
|
| `partial_plan` | object | خیر | خروجی مرحله قبل برای ادامه تکمیل |
|
||||||
|
| `farm_uuid` | string | خیر | برای غنیسازی context مزرعه در RAG |
|
||||||
|
|
||||||
|
### قانون validation
|
||||||
|
|
||||||
|
اگر هیچکدام از `message`، `answers` یا `partial_plan` ارسال نشوند:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"msg": "داده نامعتبر.",
|
||||||
|
"data": {
|
||||||
|
"non_field_errors": [
|
||||||
|
"حداقل یکی از message، answers یا partial_plan باید ارسال شود."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## پاسخ موفق - حالت تکمیل شده
|
||||||
|
|
||||||
|
وقتی همه اطلاعات لازم موجود باشد:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "موفق",
|
||||||
|
"data": {
|
||||||
|
"status": "completed",
|
||||||
|
"status_fa": "تکمیل شد",
|
||||||
|
"summary": "برنامه آبیاری برای گوجهفرنگی به روش قطرهای هر سه روز یکبار صبح زود به مدت 25 دقیقه اجرا میشود.",
|
||||||
|
"missing_fields": [],
|
||||||
|
"questions": [],
|
||||||
|
"collected_data": {
|
||||||
|
"crop_name": "گوجهفرنگی",
|
||||||
|
"growth_stage": "گلدهی",
|
||||||
|
"irrigation_method": "قطرهای",
|
||||||
|
"water_amount_per_event": "18 لیتر برای هر بوته",
|
||||||
|
"duration_minutes": 25,
|
||||||
|
"frequency_text": "هر سه روز یکبار",
|
||||||
|
"interval_days": 3,
|
||||||
|
"preferred_time_of_day": "صبح زود",
|
||||||
|
"start_date": "از امروز",
|
||||||
|
"target_area": "کل مزرعه",
|
||||||
|
"trigger_conditions": [],
|
||||||
|
"notes": []
|
||||||
|
},
|
||||||
|
"final_plan": {
|
||||||
|
"crop_name": "گوجهفرنگی",
|
||||||
|
"growth_stage": "گلدهی",
|
||||||
|
"irrigation_method": "قطرهای",
|
||||||
|
"water_amount_per_event": "18 لیتر برای هر بوته",
|
||||||
|
"duration_minutes": 25,
|
||||||
|
"frequency_text": "هر سه روز یکبار",
|
||||||
|
"interval_days": 3,
|
||||||
|
"preferred_time_of_day": "صبح زود",
|
||||||
|
"start_date": "از امروز",
|
||||||
|
"target_area": "کل مزرعه",
|
||||||
|
"trigger_conditions": [],
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### فیلدهای `data`
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|---|---|---|
|
||||||
|
| `status` | string | یکی از `completed` یا `needs_clarification` |
|
||||||
|
| `status_fa` | string | نسخه فارسی وضعیت |
|
||||||
|
| `summary` | string | خلاصه قابل نمایش برای کاربر |
|
||||||
|
| `missing_fields` | array[string] | فیلدهای ناقص |
|
||||||
|
| `questions` | array[object] | سوالهای تکمیلی |
|
||||||
|
| `collected_data` | object | دادهای که تا الان از متن و جوابها استخراج شده |
|
||||||
|
| `final_plan` | object/null | برنامه نهایی؛ فقط در حالت `completed` |
|
||||||
|
|
||||||
|
### فیلدهای `collected_data` و `final_plan`
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|---|---|---|
|
||||||
|
| `crop_name` | string | نام محصول |
|
||||||
|
| `growth_stage` | string | مرحله رشد محصول |
|
||||||
|
| `irrigation_method` | string | روش آبیاری |
|
||||||
|
| `water_amount_per_event` | string | مقدار آب هر نوبت |
|
||||||
|
| `duration_minutes` | number | مدت هر نوبت آبیاری به دقیقه |
|
||||||
|
| `frequency_text` | string | توصیف متنی فاصله آبیاری |
|
||||||
|
| `interval_days` | number | فاصله آبیاری بر حسب روز |
|
||||||
|
| `preferred_time_of_day` | string | زمان مناسب اجرای آبیاری |
|
||||||
|
| `start_date` | string | زمان یا تاریخ شروع برنامه |
|
||||||
|
| `target_area` | string | محدوده هدف برنامه |
|
||||||
|
| `trigger_conditions` | array[string] | شرایط تریگر اختیاری |
|
||||||
|
| `notes` | array[string] | نکات تکمیلی |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## پاسخ موفق - حالت نیاز به سوال تکمیلی
|
||||||
|
|
||||||
|
اگر اطلاعات کامل نباشد:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "موفق",
|
||||||
|
"data": {
|
||||||
|
"status": "needs_clarification",
|
||||||
|
"status_fa": "نیازمند پرسش تکمیلی",
|
||||||
|
"summary": "اطلاعات برنامه آبیاری برای ساخت JSON نهایی کافی نیست و به چند پاسخ تکمیلی نیاز است.",
|
||||||
|
"missing_fields": [
|
||||||
|
"growth_stage",
|
||||||
|
"start_date",
|
||||||
|
"target_area"
|
||||||
|
],
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": "growth_stage",
|
||||||
|
"field": "growth_stage",
|
||||||
|
"question": "محصول الان در چه مرحله رشدی قرار دارد؟",
|
||||||
|
"rationale": "مرحله رشد برای کامل شدن برنامه لازم است."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "start_date",
|
||||||
|
"field": "start_date",
|
||||||
|
"question": "این برنامه از چه تاریخی یا از چه زمانی باید شروع شود؟",
|
||||||
|
"rationale": "زمان شروع برنامه هنوز مشخص نشده است."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "target_area",
|
||||||
|
"field": "target_area",
|
||||||
|
"question": "این برنامه برای کل مزرعه است یا بخش/ناحیه خاصی از مزرعه؟",
|
||||||
|
"rationale": "محدوده اجرای برنامه باید مشخص باشد."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collected_data": {
|
||||||
|
"crop_name": "گوجهفرنگی",
|
||||||
|
"growth_stage": null,
|
||||||
|
"irrigation_method": "قطرهای",
|
||||||
|
"water_amount_per_event": "18 لیتر برای هر بوته",
|
||||||
|
"duration_minutes": 25,
|
||||||
|
"frequency_text": "هر سه روز یکبار",
|
||||||
|
"interval_days": 3,
|
||||||
|
"preferred_time_of_day": "صبح زود",
|
||||||
|
"start_date": null,
|
||||||
|
"target_area": null,
|
||||||
|
"trigger_conditions": [],
|
||||||
|
"notes": []
|
||||||
|
},
|
||||||
|
"final_plan": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ساختار `questions`
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | string | شناسه سوال |
|
||||||
|
| `field` | string | فیلدی که این سوال برای آن پرسیده شده |
|
||||||
|
| `question` | string | متن سوال برای نمایش به کاربر |
|
||||||
|
| `rationale` | string | توضیح کوتاه برای اینکه چرا این سوال لازم است |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## flow پیشنهادی فرانتاند برای آبیاری
|
||||||
|
|
||||||
|
### مرحله 1
|
||||||
|
|
||||||
|
کاربر متن آزاد میفرستد:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "برای گوجه فرنگی هر سه روز یک بار آبیاری می کنم."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### مرحله 2
|
||||||
|
|
||||||
|
اگر `status = needs_clarification` بود:
|
||||||
|
|
||||||
|
- سوالها را از `data.questions` به کاربر نمایش بده
|
||||||
|
- پاسخها را جمع کن
|
||||||
|
|
||||||
|
### مرحله 3
|
||||||
|
|
||||||
|
درخواست تکمیلی بزن:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"partial_plan": {
|
||||||
|
"crop_name": "گوجه فرنگی",
|
||||||
|
"growth_stage": null,
|
||||||
|
"irrigation_method": null,
|
||||||
|
"water_amount_per_event": null,
|
||||||
|
"duration_minutes": null,
|
||||||
|
"frequency_text": "هر سه روز یک بار",
|
||||||
|
"interval_days": 3,
|
||||||
|
"preferred_time_of_day": null,
|
||||||
|
"start_date": null,
|
||||||
|
"target_area": null,
|
||||||
|
"trigger_conditions": [],
|
||||||
|
"notes": []
|
||||||
|
},
|
||||||
|
"answers": {
|
||||||
|
"growth_stage": "گلدهی",
|
||||||
|
"irrigation_method": "قطره ای",
|
||||||
|
"water_amount_per_event": "18 لیتر برای هر بوته",
|
||||||
|
"duration_minutes": 25,
|
||||||
|
"preferred_time_of_day": "صبح زود",
|
||||||
|
"start_date": "از امروز",
|
||||||
|
"target_area": "کل مزرعه"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### مرحله 4
|
||||||
|
|
||||||
|
اگر `status = completed` شد:
|
||||||
|
|
||||||
|
- از `data.final_plan` به عنوان JSON نهایی استفاده کن
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) API استخراج برنامه کودهی
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/fertilization/plan-from-text/
|
||||||
|
```
|
||||||
|
|
||||||
|
### کاربرد
|
||||||
|
|
||||||
|
این API متن آزاد کاربر درباره برنامه کودهی را به JSON ساختاریافته تبدیل میکند.
|
||||||
|
|
||||||
|
### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "برای گندم در مرحله پنجه زنی هر 12 روز یک بار 20-20-20 به مقدار 35 کیلوگرم در هکتار از طریق کودآبیاری می دهم.",
|
||||||
|
"answers": {
|
||||||
|
"timing": "هر 12 روز یک بار"
|
||||||
|
},
|
||||||
|
"partial_plan": {
|
||||||
|
"crop_name": "گندم"
|
||||||
|
},
|
||||||
|
"farm_uuid": "11111111-1111-1111-1111-111111111111"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### فیلدهای request
|
||||||
|
|
||||||
|
| فیلد | نوع | اجباری | توضیح |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| `message` | string | خیر | متن آزاد کاربر |
|
||||||
|
| `answers` | object | خیر | پاسخهای تکمیلی کاربر |
|
||||||
|
| `partial_plan` | object | خیر | داده استخراج شده مرحله قبل |
|
||||||
|
| `farm_uuid` | string | خیر | برای context مزرعه |
|
||||||
|
|
||||||
|
### validation error
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 400,
|
||||||
|
"msg": "داده نامعتبر.",
|
||||||
|
"data": {
|
||||||
|
"non_field_errors": [
|
||||||
|
"حداقل یکی از message، answers یا partial_plan باید ارسال شود."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## پاسخ موفق - حالت تکمیل شده
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "موفق",
|
||||||
|
"data": {
|
||||||
|
"status": "completed",
|
||||||
|
"status_fa": "تکمیل شد",
|
||||||
|
"summary": "برنامه کودهی برای گندم در مرحله پنجه زنی با کود 20-20-20 به صورت کودآبیاری هر 12 روز یک بار اجرا می شود.",
|
||||||
|
"missing_fields": [],
|
||||||
|
"questions": [],
|
||||||
|
"collected_data": {
|
||||||
|
"crop_name": "گندم",
|
||||||
|
"growth_stage": "پنجه زنی",
|
||||||
|
"objective": "تقویت رشد رویشی",
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"fertilizer_name": "کود کامل 20-20-20",
|
||||||
|
"formula": "20-20-20",
|
||||||
|
"amount": "35 کیلوگرم در هکتار",
|
||||||
|
"application_method": "کودآبیاری",
|
||||||
|
"timing": "هر 12 روز یک بار",
|
||||||
|
"interval_days": 12,
|
||||||
|
"purpose": "تقویت رشد رویشی"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notes": []
|
||||||
|
},
|
||||||
|
"final_plan": {
|
||||||
|
"crop_name": "گندم",
|
||||||
|
"growth_stage": "پنجه زنی",
|
||||||
|
"objective": "تقویت رشد رویشی",
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"fertilizer_name": "کود کامل 20-20-20",
|
||||||
|
"formula": "20-20-20",
|
||||||
|
"amount": "35 کیلوگرم در هکتار",
|
||||||
|
"application_method": "کودآبیاری",
|
||||||
|
"timing": "هر 12 روز یک بار",
|
||||||
|
"interval_days": 12,
|
||||||
|
"purpose": "تقویت رشد رویشی"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notes": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### فیلدهای `collected_data` و `final_plan`
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|---|---|---|
|
||||||
|
| `crop_name` | string | نام محصول |
|
||||||
|
| `growth_stage` | string | مرحله رشد |
|
||||||
|
| `objective` | string/null | هدف برنامه |
|
||||||
|
| `applications` | array[object] | لیست نوبتها یا اقلام کودی |
|
||||||
|
| `notes` | array[string] | نکات تکمیلی |
|
||||||
|
|
||||||
|
### ساختار هر application
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|---|---|---|
|
||||||
|
| `fertilizer_name` | string | نام کود |
|
||||||
|
| `formula` | string | فرمول یا آنالیز کود |
|
||||||
|
| `amount` | string | مقدار مصرف |
|
||||||
|
| `application_method` | string | روش مصرف |
|
||||||
|
| `timing` | string | زمانبندی مصرف |
|
||||||
|
| `interval_days` | number | فاصله بین نوبتها |
|
||||||
|
| `purpose` | string/null | هدف آن نوبت |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## پاسخ موفق - حالت نیاز به سوال تکمیلی
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "موفق",
|
||||||
|
"data": {
|
||||||
|
"status": "needs_clarification",
|
||||||
|
"status_fa": "نیازمند پرسش تکمیلی",
|
||||||
|
"summary": "اطلاعات برنامه کودهی برای ساخت JSON نهایی کافی نیست و به چند پاسخ تکمیلی نیاز است.",
|
||||||
|
"missing_fields": [
|
||||||
|
"growth_stage",
|
||||||
|
"formula",
|
||||||
|
"interval_days"
|
||||||
|
],
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"id": "growth_stage",
|
||||||
|
"field": "growth_stage",
|
||||||
|
"question": "محصول الان در چه مرحله رشدی قرار دارد؟",
|
||||||
|
"rationale": "مرحله رشد برای تکمیل برنامه لازم است."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "formula",
|
||||||
|
"field": "formula",
|
||||||
|
"question": "فرمول یا آنالیز کود چیست؟ مثلا 20-20-20.",
|
||||||
|
"rationale": "ترکیب دقیق کود هنوز مشخص نشده است."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "interval_days",
|
||||||
|
"field": "interval_days",
|
||||||
|
"question": "فاصله بین نوبت های مصرف کود چند روز است؟",
|
||||||
|
"rationale": "عدد فاصله بین نوبت ها برای JSON نهایی لازم است."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collected_data": {
|
||||||
|
"crop_name": "گندم",
|
||||||
|
"growth_stage": null,
|
||||||
|
"objective": null,
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"fertilizer_name": "کود کامل",
|
||||||
|
"formula": null,
|
||||||
|
"amount": "35 کیلوگرم در هکتار",
|
||||||
|
"application_method": "کودآبیاری",
|
||||||
|
"timing": "هر چند وقت یک بار",
|
||||||
|
"interval_days": null,
|
||||||
|
"purpose": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notes": []
|
||||||
|
},
|
||||||
|
"final_plan": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## flow پیشنهادی فرانتاند برای کودهی
|
||||||
|
|
||||||
|
### درخواست اولیه
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "برای گندم از کود کامل استفاده می کنم."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### اگر incomplete بود
|
||||||
|
|
||||||
|
- از `questions` سوالها را بگیر
|
||||||
|
- در UI نمایش بده
|
||||||
|
- پاسخها را جمع کن
|
||||||
|
|
||||||
|
### درخواست تکمیلی
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"partial_plan": {
|
||||||
|
"crop_name": "گندم",
|
||||||
|
"growth_stage": null,
|
||||||
|
"objective": null,
|
||||||
|
"applications": [
|
||||||
|
{
|
||||||
|
"fertilizer_name": "کود کامل",
|
||||||
|
"formula": null,
|
||||||
|
"amount": null,
|
||||||
|
"application_method": null,
|
||||||
|
"timing": null,
|
||||||
|
"interval_days": null,
|
||||||
|
"purpose": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notes": []
|
||||||
|
},
|
||||||
|
"answers": {
|
||||||
|
"growth_stage": "پنجه زنی",
|
||||||
|
"formula": "20-20-20",
|
||||||
|
"amount": "35 کیلوگرم در هکتار",
|
||||||
|
"application_method": "کودآبیاری",
|
||||||
|
"timing": "هر 12 روز یک بار",
|
||||||
|
"interval_days": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### اگر complete شد
|
||||||
|
|
||||||
|
- از `final_plan` استفاده کن
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## نکات مهم برای فرانتاند
|
||||||
|
|
||||||
|
### 1. به `status` تکیه کنید
|
||||||
|
|
||||||
|
مهمترین فیلد برای کنترل flow:
|
||||||
|
|
||||||
|
- `completed`
|
||||||
|
- `needs_clarification`
|
||||||
|
|
||||||
|
### 2. اگر `needs_clarification` بود
|
||||||
|
|
||||||
|
باید:
|
||||||
|
|
||||||
|
- `questions` را به کاربر نمایش دهید
|
||||||
|
- `partial_plan` را نگه دارید
|
||||||
|
- پاسخهای کاربر را در `answers` ارسال کنید
|
||||||
|
|
||||||
|
### 3. اگر `completed` بود
|
||||||
|
|
||||||
|
باید:
|
||||||
|
|
||||||
|
- `final_plan` را به عنوان نسخه نهایی برنامه ذخیره یا نمایش دهید
|
||||||
|
|
||||||
|
### 4. `collected_data` همیشه مهم است
|
||||||
|
|
||||||
|
حتی اگر برنامه ناقص باشد، `collected_data` نشان میدهد سیستم تا این لحظه چه چیزهایی را فهمیده است.
|
||||||
|
|
||||||
|
### 5. null در حالت ناقص طبیعی است
|
||||||
|
|
||||||
|
در حالت `needs_clarification` ممکن است بعضی فیلدهای `collected_data` `null` باشند.
|
||||||
|
اما در حالت `completed` نباید فیلدهای اصلی ناقص باشند.
|
||||||
|
|
||||||
|
### 6. فرانتاند بهتر است سوالها را به صورت step-by-step بپرسد
|
||||||
|
|
||||||
|
پیشنهاد:
|
||||||
|
|
||||||
|
- سوال اول را نشان بده
|
||||||
|
- جواب را بگیر
|
||||||
|
- همه جوابها را در `answers` جمع کن
|
||||||
|
- دوباره API را صدا بزن
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## جمعبندی تفاوت دو API
|
||||||
|
|
||||||
|
| API | موضوع | خروجی نهایی |
|
||||||
|
|---|---|---|
|
||||||
|
| `/api/irrigation/plan-from-text/` | استخراج برنامه آبیاری | `final_plan` با ساختار آبیاری |
|
||||||
|
| `/api/fertilization/plan-from-text/` | استخراج برنامه کودهی | `final_plan` با ساختار کودهی |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## مسیر فایل
|
||||||
|
|
||||||
|
این داکیومنت در این مسیر ذخیره شده:
|
||||||
|
|
||||||
|
`docs/irrigation_fertilization_plan_parser_apis.md`
|
||||||
@@ -84,3 +84,12 @@ class FertilizationConfig(AppConfig):
|
|||||||
|
|
||||||
def get_optimizer_defaults(self):
|
def get_optimizer_defaults(self):
|
||||||
return self.optimizer_defaults
|
return self.optimizer_defaults
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def free_text_plan_parser_service(self):
|
||||||
|
from rag.services.fertilization_plan_parser import FertilizationPlanParserService
|
||||||
|
|
||||||
|
return FertilizationPlanParserService()
|
||||||
|
|
||||||
|
def get_free_text_plan_parser_service(self):
|
||||||
|
return self.free_text_plan_parser_service
|
||||||
|
|||||||
@@ -115,3 +115,37 @@ class FertilizationRecommendationResponseDataSerializer(serializers.Serializer):
|
|||||||
application_guide = FertilizationApplicationGuideSerializer()
|
application_guide = FertilizationApplicationGuideSerializer()
|
||||||
alternative_recommendations = AlternativeFertilizationRecommendationSerializer(many=True)
|
alternative_recommendations = AlternativeFertilizationRecommendationSerializer(many=True)
|
||||||
sections = FertilizationSectionSerializer(many=True, required=False)
|
sections = FertilizationSectionSerializer(many=True, required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationPlanParserRequestSerializer(serializers.Serializer):
|
||||||
|
message = serializers.CharField(required=False, allow_blank=True, help_text="توضیح آزاد کاربر درباره برنامه کودهی")
|
||||||
|
answers = serializers.JSONField(required=False, help_text="پاسخ های تکمیلی کاربر به سوالات مرحله قبل")
|
||||||
|
partial_plan = serializers.JSONField(required=False, help_text="داده استخراج شده مرحله قبل برای ادامه تکمیل")
|
||||||
|
farm_uuid = serializers.CharField(required=False, allow_blank=True, help_text="شناسه مزرعه برای غنی سازی context")
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
message = (attrs.get("message") or "").strip()
|
||||||
|
answers = attrs.get("answers")
|
||||||
|
partial_plan = attrs.get("partial_plan")
|
||||||
|
if not message and not isinstance(answers, dict) and not isinstance(partial_plan, dict):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"حداقل یکی از message، answers یا partial_plan باید ارسال شود."
|
||||||
|
)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class PlanClarificationQuestionSerializer(serializers.Serializer):
|
||||||
|
id = serializers.CharField()
|
||||||
|
field = serializers.CharField()
|
||||||
|
question = serializers.CharField()
|
||||||
|
rationale = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationPlanParserResponseSerializer(serializers.Serializer):
|
||||||
|
status = serializers.CharField()
|
||||||
|
status_fa = serializers.CharField()
|
||||||
|
summary = serializers.CharField()
|
||||||
|
missing_fields = serializers.ListField(child=serializers.CharField())
|
||||||
|
questions = PlanClarificationQuestionSerializer(many=True)
|
||||||
|
collected_data = serializers.JSONField()
|
||||||
|
final_plan = serializers.JSONField(required=False, allow_null=True)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import FertilizationRecommendView
|
from .views import FertilizationPlanParserView, FertilizationRecommendView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("recommend/", FertilizationRecommendView.as_view(), name="fertilization-recommend"),
|
path("recommend/", FertilizationRecommendView.as_view(), name="fertilization-recommend"),
|
||||||
|
path("plan-from-text/", FertilizationPlanParserView.as_view(), name="fertilization-plan-from-text"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from django.apps import apps
|
||||||
|
|
||||||
from drf_spectacular.utils import OpenApiExample, extend_schema
|
from drf_spectacular.utils import OpenApiExample, extend_schema
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@@ -6,6 +8,8 @@ from rest_framework.views import APIView
|
|||||||
from config.openapi import build_envelope_serializer, build_response
|
from config.openapi import build_envelope_serializer, build_response
|
||||||
|
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
|
FertilizationPlanParserRequestSerializer,
|
||||||
|
FertilizationPlanParserResponseSerializer,
|
||||||
FertilizationRecommendationResponseDataSerializer,
|
FertilizationRecommendationResponseDataSerializer,
|
||||||
FertilizationRecommendRequestSerializer,
|
FertilizationRecommendRequestSerializer,
|
||||||
)
|
)
|
||||||
@@ -20,6 +24,10 @@ FertilizationResponseSerializer = build_envelope_serializer(
|
|||||||
"FertilizationResponseSerializer",
|
"FertilizationResponseSerializer",
|
||||||
data_schema=FertilizationRecommendationResponseDataSerializer,
|
data_schema=FertilizationRecommendationResponseDataSerializer,
|
||||||
)
|
)
|
||||||
|
FertilizationPlanParserEnvelopeSerializer = build_envelope_serializer(
|
||||||
|
"FertilizationPlanParserEnvelopeSerializer",
|
||||||
|
data_schema=FertilizationPlanParserResponseSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FertilizationRecommendView(APIView):
|
class FertilizationRecommendView(APIView):
|
||||||
@@ -147,3 +155,80 @@ class FertilizationRecommendView(APIView):
|
|||||||
{"code": 200, "msg": "success", "data": final_result},
|
{"code": 200, "msg": "success", "data": final_result},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationPlanParserView(APIView):
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Fertilization Recommendation"],
|
||||||
|
summary="استخراج برنامه کودهی از متن آزاد",
|
||||||
|
description=(
|
||||||
|
"توضیح متنی کاربر درباره برنامه کودهی را می گیرد و آن را به JSON ساختاریافته تبدیل می کند. "
|
||||||
|
"اگر اطلاعات کافی نباشد، سوالات تکمیلی لازم را برمی گرداند تا در درخواست بعدی پاسخ داده شوند."
|
||||||
|
),
|
||||||
|
request=FertilizationPlanParserRequestSerializer,
|
||||||
|
responses={
|
||||||
|
200: build_response(
|
||||||
|
FertilizationPlanParserEnvelopeSerializer,
|
||||||
|
"نتیجه استخراج یا سوالات تکمیلی برنامه کودهی.",
|
||||||
|
),
|
||||||
|
400: build_response(
|
||||||
|
FertilizationValidationErrorSerializer,
|
||||||
|
"داده ورودی نامعتبر است.",
|
||||||
|
),
|
||||||
|
500: build_response(
|
||||||
|
FertilizationValidationErrorSerializer,
|
||||||
|
"خطا در پردازش برنامه کودهی.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
"نمونه درخواست کامل",
|
||||||
|
value={
|
||||||
|
"message": "برای گندم در مرحله پنجه زنی هر 12 روز یک بار 20-20-20 به مقدار 35 کیلوگرم در هکتار از طریق کودآبیاری می دهم.",
|
||||||
|
"farm_uuid": "11111111-1111-1111-1111-111111111111",
|
||||||
|
},
|
||||||
|
request_only=True,
|
||||||
|
),
|
||||||
|
OpenApiExample(
|
||||||
|
"نمونه درخواست تکمیلی",
|
||||||
|
value={
|
||||||
|
"partial_plan": {
|
||||||
|
"crop_name": "گندم",
|
||||||
|
"applications": [{"fertilizer_name": "20-20-20"}],
|
||||||
|
},
|
||||||
|
"answers": {
|
||||||
|
"amount": "35 کیلوگرم در هکتار",
|
||||||
|
"timing": "هر 12 روز یک بار",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request_only=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
serializer = FertilizationPlanParserRequestSerializer(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
|
||||||
|
service = apps.get_app_config("fertilization").get_free_text_plan_parser_service()
|
||||||
|
try:
|
||||||
|
result = service.parse_plan(
|
||||||
|
message=validated.get("message", ""),
|
||||||
|
answers=validated.get("answers"),
|
||||||
|
partial_plan=validated.get("partial_plan"),
|
||||||
|
farm_uuid=validated.get("farm_uuid"),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return Response(
|
||||||
|
{"code": 500, "msg": f"خطا در پردازش برنامه کودهی: {exc}", "data": None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"code": 200, "msg": "موفق", "data": result},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|||||||
@@ -58,3 +58,12 @@ class IrrigationConfig(AppConfig):
|
|||||||
|
|
||||||
def get_water_stress_service(self):
|
def get_water_stress_service(self):
|
||||||
return self.water_stress_service
|
return self.water_stress_service
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def free_text_plan_parser_service(self):
|
||||||
|
from rag.services.irrigation_plan_parser import IrrigationPlanParserService
|
||||||
|
|
||||||
|
return IrrigationPlanParserService()
|
||||||
|
|
||||||
|
def get_free_text_plan_parser_service(self):
|
||||||
|
return self.free_text_plan_parser_service
|
||||||
|
|||||||
@@ -80,3 +80,37 @@ class WaterStressResponseSerializer(serializers.Serializer):
|
|||||||
waterStressIndex = serializers.IntegerField()
|
waterStressIndex = serializers.IntegerField()
|
||||||
level = serializers.CharField()
|
level = serializers.CharField()
|
||||||
sourceMetric = serializers.JSONField()
|
sourceMetric = serializers.JSONField()
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationPlanParserRequestSerializer(serializers.Serializer):
|
||||||
|
message = serializers.CharField(required=False, allow_blank=True, help_text="توضیح آزاد کاربر درباره برنامه آبیاری")
|
||||||
|
answers = serializers.JSONField(required=False, help_text="پاسخ های تکمیلی کاربر به سوالات مرحله قبل")
|
||||||
|
partial_plan = serializers.JSONField(required=False, help_text="داده استخراج شده مرحله قبل برای ادامه تکمیل")
|
||||||
|
farm_uuid = serializers.CharField(required=False, allow_blank=True, help_text="شناسه مزرعه برای غنی سازی context")
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
message = (attrs.get("message") or "").strip()
|
||||||
|
answers = attrs.get("answers")
|
||||||
|
partial_plan = attrs.get("partial_plan")
|
||||||
|
if not message and not isinstance(answers, dict) and not isinstance(partial_plan, dict):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"حداقل یکی از message، answers یا partial_plan باید ارسال شود."
|
||||||
|
)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class PlanClarificationQuestionSerializer(serializers.Serializer):
|
||||||
|
id = serializers.CharField()
|
||||||
|
field = serializers.CharField()
|
||||||
|
question = serializers.CharField()
|
||||||
|
rationale = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationPlanParserResponseSerializer(serializers.Serializer):
|
||||||
|
status = serializers.CharField()
|
||||||
|
status_fa = serializers.CharField()
|
||||||
|
summary = serializers.CharField()
|
||||||
|
missing_fields = serializers.ListField(child=serializers.CharField())
|
||||||
|
questions = PlanClarificationQuestionSerializer(many=True)
|
||||||
|
collected_data = serializers.JSONField()
|
||||||
|
final_plan = serializers.JSONField(required=False, allow_null=True)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django.urls import path
|
|||||||
from .views import (
|
from .views import (
|
||||||
IrrigationMethodDetailView,
|
IrrigationMethodDetailView,
|
||||||
IrrigationMethodListCreateView,
|
IrrigationMethodListCreateView,
|
||||||
|
IrrigationPlanParserView,
|
||||||
IrrigationRecommendView,
|
IrrigationRecommendView,
|
||||||
WaterStressView,
|
WaterStressView,
|
||||||
)
|
)
|
||||||
@@ -11,5 +12,6 @@ urlpatterns = [
|
|||||||
path("", IrrigationMethodListCreateView.as_view(), name="irrigation-list-create"),
|
path("", IrrigationMethodListCreateView.as_view(), name="irrigation-list-create"),
|
||||||
path("<int:pk>/", IrrigationMethodDetailView.as_view(), name="irrigation-detail"),
|
path("<int:pk>/", IrrigationMethodDetailView.as_view(), name="irrigation-detail"),
|
||||||
path("recommend/", IrrigationRecommendView.as_view(), name="irrigation-recommend"),
|
path("recommend/", IrrigationRecommendView.as_view(), name="irrigation-recommend"),
|
||||||
|
path("plan-from-text/", IrrigationPlanParserView.as_view(), name="irrigation-plan-from-text"),
|
||||||
path("water-stress/", WaterStressView.as_view(), name="water-stress"),
|
path("water-stress/", WaterStressView.as_view(), name="water-stress"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ from config.openapi import (
|
|||||||
from .models import IrrigationMethod
|
from .models import IrrigationMethod
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
IrrigationMethodSerializer,
|
IrrigationMethodSerializer,
|
||||||
|
IrrigationPlanParserRequestSerializer,
|
||||||
|
IrrigationPlanParserResponseSerializer,
|
||||||
IrrigationRecommendRequestSerializer,
|
IrrigationRecommendRequestSerializer,
|
||||||
WaterStressRequestSerializer,
|
WaterStressRequestSerializer,
|
||||||
WaterStressResponseSerializer,
|
WaterStressResponseSerializer,
|
||||||
@@ -37,6 +39,10 @@ IrrigationRecommendResponseSerializer = build_envelope_serializer(
|
|||||||
"IrrigationRecommendResponseSerializer",
|
"IrrigationRecommendResponseSerializer",
|
||||||
data_schema=None,
|
data_schema=None,
|
||||||
)
|
)
|
||||||
|
IrrigationPlanParserEnvelopeSerializer = build_envelope_serializer(
|
||||||
|
"IrrigationPlanParserEnvelopeSerializer",
|
||||||
|
IrrigationPlanParserResponseSerializer,
|
||||||
|
)
|
||||||
WaterStressEnvelopeSerializer = build_envelope_serializer(
|
WaterStressEnvelopeSerializer = build_envelope_serializer(
|
||||||
"WaterStressEnvelopeSerializer",
|
"WaterStressEnvelopeSerializer",
|
||||||
WaterStressResponseSerializer,
|
WaterStressResponseSerializer,
|
||||||
@@ -219,6 +225,83 @@ class IrrigationRecommendView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationPlanParserView(APIView):
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Irrigation Recommendation"],
|
||||||
|
summary="استخراج برنامه آبیاری از متن آزاد",
|
||||||
|
description=(
|
||||||
|
"توضیح متنی کاربر درباره برنامه آبیاری را می گیرد و آن را به JSON ساختاریافته تبدیل می کند. "
|
||||||
|
"اگر اطلاعات کافی نباشد، سوالات تکمیلی لازم را برمی گرداند تا در درخواست بعدی پاسخ داده شوند."
|
||||||
|
),
|
||||||
|
request=IrrigationPlanParserRequestSerializer,
|
||||||
|
responses={
|
||||||
|
200: build_response(
|
||||||
|
IrrigationPlanParserEnvelopeSerializer,
|
||||||
|
"نتیجه استخراج یا سوالات تکمیلی برنامه آبیاری.",
|
||||||
|
),
|
||||||
|
400: build_response(
|
||||||
|
IrrigationValidationErrorSerializer,
|
||||||
|
"داده ورودی نامعتبر است.",
|
||||||
|
),
|
||||||
|
500: build_response(
|
||||||
|
IrrigationValidationErrorSerializer,
|
||||||
|
"خطا در پردازش برنامه آبیاری.",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
"نمونه درخواست کامل",
|
||||||
|
value={
|
||||||
|
"message": "برای گوجه فرنگی با آبیاری قطره ای هر سه روز یک بار صبح زود 25 دقیقه آبیاری می کنم و حدود 18 لیتر برای هر بوته می دهم.",
|
||||||
|
"farm_uuid": "11111111-1111-1111-1111-111111111111",
|
||||||
|
},
|
||||||
|
request_only=True,
|
||||||
|
),
|
||||||
|
OpenApiExample(
|
||||||
|
"نمونه درخواست تکمیلی",
|
||||||
|
value={
|
||||||
|
"partial_plan": {
|
||||||
|
"crop_name": "گوجه فرنگی",
|
||||||
|
"irrigation_method": "قطره ای",
|
||||||
|
},
|
||||||
|
"answers": {
|
||||||
|
"water_amount_per_event": "18 لیتر برای هر بوته",
|
||||||
|
"preferred_time_of_day": "صبح زود",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request_only=True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
serializer = IrrigationPlanParserRequestSerializer(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
|
||||||
|
service = apps.get_app_config("irrigation").get_free_text_plan_parser_service()
|
||||||
|
try:
|
||||||
|
result = service.parse_plan(
|
||||||
|
message=validated.get("message", ""),
|
||||||
|
answers=validated.get("answers"),
|
||||||
|
partial_plan=validated.get("partial_plan"),
|
||||||
|
farm_uuid=validated.get("farm_uuid"),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return Response(
|
||||||
|
{"code": 500, "msg": f"خطا در پردازش برنامه آبیاری: {exc}", "data": None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"code": 200, "msg": "موفق", "data": result},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IrrigationMethodDetailView(APIView):
|
class IrrigationMethodDetailView(APIView):
|
||||||
"""دریافت، ویرایش و حذف یک روش آبیاری."""
|
"""دریافت، ویرایش و حذف یک روش آبیاری."""
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
بدون API — قابل استفاده از سایر سرویسها
|
بدون API — قابل استفاده از سایر سرویسها
|
||||||
"""
|
"""
|
||||||
from .irrigation import get_irrigation_recommendation
|
from .irrigation import get_irrigation_recommendation
|
||||||
|
from .irrigation_plan_parser import IrrigationPlanParserService
|
||||||
from .fertilization import get_fertilization_recommendation
|
from .fertilization import get_fertilization_recommendation
|
||||||
|
from .fertilization_plan_parser import FertilizationPlanParserService
|
||||||
from .pest_disease import get_pest_disease_detection, get_pest_disease_risk
|
from .pest_disease import get_pest_disease_detection, get_pest_disease_risk
|
||||||
from .soil_anomaly import get_soil_anomaly_insight
|
from .soil_anomaly import get_soil_anomaly_insight
|
||||||
from .water_need_prediction import get_water_need_prediction_insight
|
from .water_need_prediction import get_water_need_prediction_insight
|
||||||
@@ -11,7 +13,9 @@ from .yield_harvest import YieldHarvestRAGService
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"get_irrigation_recommendation",
|
"get_irrigation_recommendation",
|
||||||
|
"IrrigationPlanParserService",
|
||||||
"get_fertilization_recommendation",
|
"get_fertilization_recommendation",
|
||||||
|
"FertilizationPlanParserService",
|
||||||
"get_pest_disease_detection",
|
"get_pest_disease_detection",
|
||||||
"get_pest_disease_risk",
|
"get_pest_disease_risk",
|
||||||
"get_soil_anomaly_insight",
|
"get_soil_anomaly_insight",
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
|
||||||
|
from rag.api_provider import get_chat_client
|
||||||
|
from rag.chat import (
|
||||||
|
_complete_audit_log,
|
||||||
|
_create_audit_log,
|
||||||
|
_fail_audit_log,
|
||||||
|
_load_service_tone,
|
||||||
|
build_rag_context,
|
||||||
|
)
|
||||||
|
from rag.config import RAGConfig, get_service_config, load_rag_config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SERVICE_ID = "fertilization_plan_parser"
|
||||||
|
KB_NAME = "fertilization_plan_parser"
|
||||||
|
CORE_FIELDS = [
|
||||||
|
"crop_name",
|
||||||
|
"growth_stage",
|
||||||
|
"fertilizer_name",
|
||||||
|
"formula",
|
||||||
|
"amount",
|
||||||
|
"application_method",
|
||||||
|
"timing",
|
||||||
|
"interval_days",
|
||||||
|
]
|
||||||
|
|
||||||
|
FERTILIZATION_PLAN_PROMPT = (
|
||||||
|
"شما یک تحلیل گر برنامه کودهی هستی. "
|
||||||
|
"کاربر ممکن است برنامه کودهی را کامل یا ناقص توضیح دهد. "
|
||||||
|
"فقط JSON معتبر برگردان و هرگز متن خارج از JSON، markdown یا کلید اضافه تولید نکن. "
|
||||||
|
"اگر اطلاعات کافی بود status را completed بگذار و final_plan را تکمیل کن. "
|
||||||
|
"اگر اطلاعات ناقص بود status را needs_clarification بگذار، missing_fields را پر کن و در questions سوال های کوتاه و دقیق برگردان. "
|
||||||
|
"اگر چند کود در متن بود، همه را در applications لیست کن. "
|
||||||
|
"اگر هرکدام از فیلدهای اصلی خالی، null یا نامشخص بود، حق نداری status را completed بگذاری. "
|
||||||
|
"در حالت completed هیچ فیلد null در collected_data و final_plan نباید وجود داشته باشد. "
|
||||||
|
"از حدس زدن مقدار، زمان یا روش مصرف خودداری کن. "
|
||||||
|
"Schema: "
|
||||||
|
"{"
|
||||||
|
'"status": "completed" | "needs_clarification", '
|
||||||
|
'"summary": string, '
|
||||||
|
'"missing_fields": [string], '
|
||||||
|
'"questions": [{"id": string, "field": string, "question": string, "rationale": string}], '
|
||||||
|
'"collected_data": {'
|
||||||
|
'"crop_name": string|null, '
|
||||||
|
'"growth_stage": string|null, '
|
||||||
|
'"objective": string|null, '
|
||||||
|
'"applications": ['
|
||||||
|
"{"
|
||||||
|
'"fertilizer_name": string|null, '
|
||||||
|
'"formula": string|null, '
|
||||||
|
'"amount": string|null, '
|
||||||
|
'"application_method": string|null, '
|
||||||
|
'"timing": string|null, '
|
||||||
|
'"interval_days": integer|null, '
|
||||||
|
'"purpose": string|null'
|
||||||
|
"}"
|
||||||
|
"], "
|
||||||
|
'"notes": [string]'
|
||||||
|
"}, "
|
||||||
|
'"final_plan": {same shape as collected_data} | null'
|
||||||
|
"}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClarificationQuestionSchema(BaseModel):
|
||||||
|
id: str
|
||||||
|
field: str
|
||||||
|
question: str
|
||||||
|
rationale: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizerApplicationSchema(BaseModel):
|
||||||
|
fertilizer_name: str | None = None
|
||||||
|
formula: str | None = None
|
||||||
|
amount: str | None = None
|
||||||
|
application_method: str | None = None
|
||||||
|
timing: str | None = None
|
||||||
|
interval_days: int | None = None
|
||||||
|
purpose: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationPlanSchema(BaseModel):
|
||||||
|
crop_name: str | None = None
|
||||||
|
growth_stage: str | None = None
|
||||||
|
objective: str | None = None
|
||||||
|
applications: list[FertilizerApplicationSchema] = Field(default_factory=list)
|
||||||
|
notes: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationPlanParseResultSchema(BaseModel):
|
||||||
|
status: Literal["completed", "needs_clarification"]
|
||||||
|
summary: str
|
||||||
|
missing_fields: list[str] = Field(default_factory=list)
|
||||||
|
questions: list[ClarificationQuestionSchema] = Field(default_factory=list)
|
||||||
|
collected_data: FertilizationPlanSchema = Field(default_factory=FertilizationPlanSchema)
|
||||||
|
final_plan: FertilizationPlanSchema | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class FertilizationPlanParserService:
|
||||||
|
def parse_plan(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
message: str = "",
|
||||||
|
answers: dict[str, Any] | None = None,
|
||||||
|
partial_plan: dict[str, Any] | None = None,
|
||||||
|
farm_uuid: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
cfg = load_rag_config()
|
||||||
|
service, client, model = self._build_service_client(cfg)
|
||||||
|
|
||||||
|
normalized_message = (message or "").strip()
|
||||||
|
normalized_answers = answers if isinstance(answers, dict) else {}
|
||||||
|
normalized_partial = partial_plan if isinstance(partial_plan, dict) else {}
|
||||||
|
structured_context = {
|
||||||
|
"message": normalized_message,
|
||||||
|
"answers": normalized_answers,
|
||||||
|
"partial_plan": normalized_partial,
|
||||||
|
"required_core_fields": CORE_FIELDS,
|
||||||
|
"service": "fertilization_plan_parser",
|
||||||
|
}
|
||||||
|
|
||||||
|
rag_query = self._build_retrieval_query(
|
||||||
|
message=normalized_message,
|
||||||
|
answers=normalized_answers,
|
||||||
|
)
|
||||||
|
rag_context = build_rag_context(
|
||||||
|
query=rag_query,
|
||||||
|
sensor_uuid=farm_uuid,
|
||||||
|
config=cfg,
|
||||||
|
kb_name=KB_NAME,
|
||||||
|
service_id=SERVICE_ID,
|
||||||
|
)
|
||||||
|
system_prompt, messages = self._build_messages(
|
||||||
|
service=service,
|
||||||
|
cfg=cfg,
|
||||||
|
structured_context=structured_context,
|
||||||
|
rag_context=rag_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
audit_log = None
|
||||||
|
if farm_uuid:
|
||||||
|
try:
|
||||||
|
audit_log = _create_audit_log(
|
||||||
|
farm_uuid=farm_uuid,
|
||||||
|
service_id=SERVICE_ID,
|
||||||
|
model=model,
|
||||||
|
query=rag_query,
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
messages=messages,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Fertilization plan parser audit log creation failed for %s: %s", farm_uuid, exc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
response_format={"type": "json_object"},
|
||||||
|
)
|
||||||
|
raw = (response.choices[0].message.content or "").strip()
|
||||||
|
parsed = self._clean_json(raw)
|
||||||
|
validated = FertilizationPlanParseResultSchema.model_validate(parsed)
|
||||||
|
normalized = self._normalize_result(validated)
|
||||||
|
if audit_log is not None:
|
||||||
|
_complete_audit_log(audit_log, raw)
|
||||||
|
return normalized
|
||||||
|
except (ValidationError, ValueError, KeyError, IndexError) as exc:
|
||||||
|
logger.warning("Fertilization plan parser parsing failed: %s", exc)
|
||||||
|
if audit_log is not None:
|
||||||
|
_fail_audit_log(audit_log, str(exc))
|
||||||
|
return self._fallback_result(
|
||||||
|
message=normalized_message,
|
||||||
|
answers=normalized_answers,
|
||||||
|
partial_plan=normalized_partial,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Fertilization plan parser failed: %s", exc)
|
||||||
|
if audit_log is not None:
|
||||||
|
_fail_audit_log(audit_log, str(exc))
|
||||||
|
return self._fallback_result(
|
||||||
|
message=normalized_message,
|
||||||
|
answers=normalized_answers,
|
||||||
|
partial_plan=normalized_partial,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_service_client(self, cfg: RAGConfig):
|
||||||
|
service = get_service_config(SERVICE_ID, cfg)
|
||||||
|
service_cfg = RAGConfig(
|
||||||
|
embedding=cfg.embedding,
|
||||||
|
qdrant=cfg.qdrant,
|
||||||
|
chunking=cfg.chunking,
|
||||||
|
llm=service.llm,
|
||||||
|
knowledge_bases=cfg.knowledge_bases,
|
||||||
|
services=cfg.services,
|
||||||
|
chromadb=cfg.chromadb,
|
||||||
|
)
|
||||||
|
client = get_chat_client(service_cfg)
|
||||||
|
return service, client, service.llm.model
|
||||||
|
|
||||||
|
def _build_messages(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
service: Any,
|
||||||
|
cfg: RAGConfig,
|
||||||
|
structured_context: dict[str, Any],
|
||||||
|
rag_context: str,
|
||||||
|
) -> tuple[str, list[dict[str, str]]]:
|
||||||
|
tone = _load_service_tone(service, cfg)
|
||||||
|
system_parts = [tone] if tone else []
|
||||||
|
if service.system_prompt:
|
||||||
|
system_parts.append(service.system_prompt)
|
||||||
|
system_parts.append(FERTILIZATION_PLAN_PROMPT)
|
||||||
|
system_parts.append(
|
||||||
|
"[structured_context]\n"
|
||||||
|
+ json.dumps(structured_context, ensure_ascii=False, indent=2, default=str)
|
||||||
|
)
|
||||||
|
if rag_context:
|
||||||
|
system_parts.append(rag_context)
|
||||||
|
system_prompt = "\n\n".join(part for part in system_parts if part)
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": "برنامه کودهی را استخراج یا برای تکمیل آن سوال بپرس."},
|
||||||
|
]
|
||||||
|
return system_prompt, messages
|
||||||
|
|
||||||
|
def _build_retrieval_query(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
message: str,
|
||||||
|
answers: dict[str, Any],
|
||||||
|
) -> str:
|
||||||
|
answer_lines = [f"{key}: {value}" for key, value in answers.items()]
|
||||||
|
parts = [part for part in [message, "\n".join(answer_lines)] if part]
|
||||||
|
return "\n".join(parts) or "استخراج برنامه کودهی از متن کاربر"
|
||||||
|
|
||||||
|
def _normalize_result(self, validated: FertilizationPlanParseResultSchema) -> dict[str, Any]:
|
||||||
|
collected = validated.collected_data.model_dump()
|
||||||
|
final_plan = validated.final_plan.model_dump() if validated.final_plan is not None else None
|
||||||
|
missing_fields = list(dict.fromkeys(validated.missing_fields))
|
||||||
|
computed_missing = self._find_missing_fields(final_plan or collected)
|
||||||
|
for field in computed_missing:
|
||||||
|
if field not in missing_fields:
|
||||||
|
missing_fields.append(field)
|
||||||
|
|
||||||
|
can_complete = validated.status == "completed" and not missing_fields
|
||||||
|
|
||||||
|
if can_complete:
|
||||||
|
final_plan = final_plan or collected
|
||||||
|
questions: list[dict[str, Any]] = []
|
||||||
|
status_fa = "تکمیل شد"
|
||||||
|
else:
|
||||||
|
questions = [item.model_dump() for item in validated.questions]
|
||||||
|
if not questions and missing_fields:
|
||||||
|
questions = self._build_generic_questions(missing_fields)
|
||||||
|
final_plan = None
|
||||||
|
validated.status = "needs_clarification"
|
||||||
|
status_fa = "نیازمند پرسش تکمیلی"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "completed" if can_complete else "needs_clarification",
|
||||||
|
"status_fa": status_fa,
|
||||||
|
"summary": validated.summary,
|
||||||
|
"missing_fields": missing_fields,
|
||||||
|
"questions": questions,
|
||||||
|
"collected_data": collected,
|
||||||
|
"final_plan": final_plan,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _fallback_result(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
message: str,
|
||||||
|
answers: dict[str, Any],
|
||||||
|
partial_plan: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
applications = partial_plan.get("applications")
|
||||||
|
if not isinstance(applications, list):
|
||||||
|
applications = []
|
||||||
|
notes = list(partial_plan.get("notes") or [])
|
||||||
|
if message:
|
||||||
|
notes.append(f"متن اولیه کاربر: {message}")
|
||||||
|
if answers:
|
||||||
|
notes.append("پاسخ های تکمیلی کاربر دریافت شده است.")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "needs_clarification",
|
||||||
|
"status_fa": "نیازمند پرسش تکمیلی",
|
||||||
|
"summary": "اطلاعات برنامه کودهی برای ساخت JSON نهایی کافی نیست و به چند پاسخ تکمیلی نیاز است.",
|
||||||
|
"missing_fields": CORE_FIELDS,
|
||||||
|
"questions": self._build_generic_questions(CORE_FIELDS),
|
||||||
|
"collected_data": {
|
||||||
|
"crop_name": partial_plan.get("crop_name"),
|
||||||
|
"growth_stage": partial_plan.get("growth_stage"),
|
||||||
|
"objective": partial_plan.get("objective"),
|
||||||
|
"applications": applications,
|
||||||
|
"notes": notes,
|
||||||
|
},
|
||||||
|
"final_plan": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _build_generic_questions(self, missing_fields: list[str]) -> list[dict[str, str]]:
|
||||||
|
catalog = {
|
||||||
|
"crop_name": {
|
||||||
|
"id": "crop_name",
|
||||||
|
"field": "crop_name",
|
||||||
|
"question": "این برنامه کودهی برای کدام محصول است؟",
|
||||||
|
"rationale": "نام محصول برای ثبت برنامه لازم است.",
|
||||||
|
},
|
||||||
|
"growth_stage": {
|
||||||
|
"id": "growth_stage",
|
||||||
|
"field": "growth_stage",
|
||||||
|
"question": "محصول الان در چه مرحله رشدی قرار دارد؟",
|
||||||
|
"rationale": "مرحله رشد برای تکمیل برنامه لازم است.",
|
||||||
|
},
|
||||||
|
"fertilizer_name": {
|
||||||
|
"id": "fertilizer_name",
|
||||||
|
"field": "fertilizer_name",
|
||||||
|
"question": "نام کود یا ترکیب کودی چیست؟",
|
||||||
|
"rationale": "بدون نام کود نمی توان برنامه را نهایی کرد.",
|
||||||
|
},
|
||||||
|
"formula": {
|
||||||
|
"id": "formula",
|
||||||
|
"field": "formula",
|
||||||
|
"question": "فرمول یا آنالیز کود چیست؟ مثلا 20-20-20.",
|
||||||
|
"rationale": "ترکیب دقیق کود هنوز مشخص نشده است.",
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"id": "amount",
|
||||||
|
"field": "amount",
|
||||||
|
"question": "مقدار مصرف هر نوبت کود چقدر است؟",
|
||||||
|
"rationale": "دوز مصرف در متن مشخص نشده است.",
|
||||||
|
},
|
||||||
|
"application_method": {
|
||||||
|
"id": "application_method",
|
||||||
|
"field": "application_method",
|
||||||
|
"question": "روش مصرف کود چیست؟ مثلا کودآبیاری، سرک یا محلول پاشی.",
|
||||||
|
"rationale": "روش اجرا هنوز معلوم نیست.",
|
||||||
|
},
|
||||||
|
"timing": {
|
||||||
|
"id": "timing",
|
||||||
|
"field": "timing",
|
||||||
|
"question": "زمان مصرف کود چه موقع است؟ مثلا هر 10 روز یا بعد از آبیاری.",
|
||||||
|
"rationale": "زمان بندی برنامه نیاز به شفاف سازی دارد.",
|
||||||
|
},
|
||||||
|
"interval_days": {
|
||||||
|
"id": "interval_days",
|
||||||
|
"field": "interval_days",
|
||||||
|
"question": "فاصله بین نوبت های مصرف کود چند روز است؟",
|
||||||
|
"rationale": "عدد فاصله بین نوبت ها برای JSON نهایی لازم است.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return [catalog[field] for field in missing_fields if field in catalog][:5]
|
||||||
|
|
||||||
|
def _find_missing_fields(self, plan: dict[str, Any]) -> list[str]:
|
||||||
|
missing: list[str] = []
|
||||||
|
if not isinstance(plan, dict):
|
||||||
|
return CORE_FIELDS[:]
|
||||||
|
|
||||||
|
if plan.get("crop_name") in (None, ""):
|
||||||
|
missing.append("crop_name")
|
||||||
|
if plan.get("growth_stage") in (None, ""):
|
||||||
|
missing.append("growth_stage")
|
||||||
|
|
||||||
|
applications = plan.get("applications")
|
||||||
|
if not isinstance(applications, list) or not applications:
|
||||||
|
return missing + [
|
||||||
|
field
|
||||||
|
for field in ["fertilizer_name", "formula", "amount", "application_method", "timing", "interval_days"]
|
||||||
|
if field not in missing
|
||||||
|
]
|
||||||
|
|
||||||
|
first_application = applications[0] if isinstance(applications[0], dict) else {}
|
||||||
|
for field in ["fertilizer_name", "formula", "amount", "application_method", "timing", "interval_days"]:
|
||||||
|
value = first_application.get(field)
|
||||||
|
if value is None or (isinstance(value, str) and not value.strip()):
|
||||||
|
missing.append(field)
|
||||||
|
return missing
|
||||||
|
|
||||||
|
def _clean_json(self, raw: str) -> dict[str, Any]:
|
||||||
|
cleaned = (raw or "").strip()
|
||||||
|
if cleaned.startswith("```"):
|
||||||
|
cleaned = cleaned.strip("`")
|
||||||
|
if cleaned.startswith("json"):
|
||||||
|
cleaned = cleaned[4:]
|
||||||
|
cleaned = cleaned.strip()
|
||||||
|
if not cleaned:
|
||||||
|
raise ValueError("Fertilization plan parser response was empty.")
|
||||||
|
parsed = json.loads(cleaned)
|
||||||
|
if not isinstance(parsed, dict):
|
||||||
|
raise ValueError("Fertilization plan parser response root must be an object.")
|
||||||
|
return parsed
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
|
||||||
|
from rag.api_provider import get_chat_client
|
||||||
|
from rag.chat import (
|
||||||
|
_complete_audit_log,
|
||||||
|
_create_audit_log,
|
||||||
|
_fail_audit_log,
|
||||||
|
_load_service_tone,
|
||||||
|
build_rag_context,
|
||||||
|
)
|
||||||
|
from rag.config import RAGConfig, get_service_config, load_rag_config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SERVICE_ID = "irrigation_plan_parser"
|
||||||
|
KB_NAME = "irrigation_plan_parser"
|
||||||
|
CORE_FIELDS = [
|
||||||
|
"crop_name",
|
||||||
|
"growth_stage",
|
||||||
|
"irrigation_method",
|
||||||
|
"water_amount_per_event",
|
||||||
|
"duration_minutes",
|
||||||
|
"frequency_text",
|
||||||
|
"interval_days",
|
||||||
|
"preferred_time_of_day",
|
||||||
|
"start_date",
|
||||||
|
"target_area",
|
||||||
|
]
|
||||||
|
|
||||||
|
IRRIGATION_PLAN_PROMPT = (
|
||||||
|
"شما یک تحلیل گر برنامه آبیاری هستی. "
|
||||||
|
"کاربر ممکن است برنامه آبیاری را کامل یا ناقص توضیح دهد. "
|
||||||
|
"وظیفه شما این است که فقط JSON معتبر برگردانی و متن اضافه، markdown، توضیح بیرون از JSON یا کلید اضافه تولید نکنی. "
|
||||||
|
"اگر اطلاعات کافی بود status را completed بگذار و final_plan را کامل کن. "
|
||||||
|
"اگر اطلاعات کافی نبود status را needs_clarification بگذار، missing_fields را پر کن و 1 تا 5 سوال کوتاه و دقیق در questions برگردان. "
|
||||||
|
"اگر هرکدام از فیلدهای اصلی خالی، null یا نامشخص بود، حق نداری status را completed بگذاری. "
|
||||||
|
"در حالت completed هیچ فیلد null در collected_data و final_plan نباید وجود داشته باشد. "
|
||||||
|
"از حدس زدن جزئیات برنامه خودداری کن. "
|
||||||
|
"اگر کاربر فقط بخشی از سوالات قبلی را جواب داد، داده های جدید را با partial_plan ادغام کن و فقط سوالات باقی مانده را بپرس. "
|
||||||
|
"Schema: "
|
||||||
|
"{"
|
||||||
|
'"status": "completed" | "needs_clarification", '
|
||||||
|
'"summary": string, '
|
||||||
|
'"missing_fields": [string], '
|
||||||
|
'"questions": [{"id": string, "field": string, "question": string, "rationale": string}], '
|
||||||
|
'"collected_data": {'
|
||||||
|
'"crop_name": string|null, '
|
||||||
|
'"growth_stage": string|null, '
|
||||||
|
'"irrigation_method": string|null, '
|
||||||
|
'"water_amount_per_event": string|null, '
|
||||||
|
'"duration_minutes": integer|null, '
|
||||||
|
'"frequency_text": string|null, '
|
||||||
|
'"interval_days": integer|null, '
|
||||||
|
'"preferred_time_of_day": string|null, '
|
||||||
|
'"start_date": string|null, '
|
||||||
|
'"target_area": string|null, '
|
||||||
|
'"trigger_conditions": [string], '
|
||||||
|
'"notes": [string]'
|
||||||
|
"}, "
|
||||||
|
'"final_plan": {same shape as collected_data} | null'
|
||||||
|
"}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClarificationQuestionSchema(BaseModel):
|
||||||
|
id: str
|
||||||
|
field: str
|
||||||
|
question: str
|
||||||
|
rationale: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationPlanSchema(BaseModel):
|
||||||
|
crop_name: str | None = None
|
||||||
|
growth_stage: str | None = None
|
||||||
|
irrigation_method: str | None = None
|
||||||
|
water_amount_per_event: str | None = None
|
||||||
|
duration_minutes: int | None = None
|
||||||
|
frequency_text: str | None = None
|
||||||
|
interval_days: int | None = None
|
||||||
|
preferred_time_of_day: str | None = None
|
||||||
|
start_date: str | None = None
|
||||||
|
target_area: str | None = None
|
||||||
|
trigger_conditions: list[str] = Field(default_factory=list)
|
||||||
|
notes: list[str] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationPlanParseResultSchema(BaseModel):
|
||||||
|
status: Literal["completed", "needs_clarification"]
|
||||||
|
summary: str
|
||||||
|
missing_fields: list[str] = Field(default_factory=list)
|
||||||
|
questions: list[ClarificationQuestionSchema] = Field(default_factory=list)
|
||||||
|
collected_data: IrrigationPlanSchema = Field(default_factory=IrrigationPlanSchema)
|
||||||
|
final_plan: IrrigationPlanSchema | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class IrrigationPlanParserService:
|
||||||
|
def parse_plan(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
message: str = "",
|
||||||
|
answers: dict[str, Any] | None = None,
|
||||||
|
partial_plan: dict[str, Any] | None = None,
|
||||||
|
farm_uuid: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
cfg = load_rag_config()
|
||||||
|
service, client, model = self._build_service_client(cfg)
|
||||||
|
|
||||||
|
normalized_message = (message or "").strip()
|
||||||
|
normalized_answers = answers if isinstance(answers, dict) else {}
|
||||||
|
normalized_partial = partial_plan if isinstance(partial_plan, dict) else {}
|
||||||
|
structured_context = {
|
||||||
|
"message": normalized_message,
|
||||||
|
"answers": normalized_answers,
|
||||||
|
"partial_plan": normalized_partial,
|
||||||
|
"required_core_fields": CORE_FIELDS,
|
||||||
|
"service": "irrigation_plan_parser",
|
||||||
|
}
|
||||||
|
|
||||||
|
rag_query = self._build_retrieval_query(
|
||||||
|
message=normalized_message,
|
||||||
|
answers=normalized_answers,
|
||||||
|
)
|
||||||
|
rag_context = build_rag_context(
|
||||||
|
query=rag_query,
|
||||||
|
sensor_uuid=farm_uuid,
|
||||||
|
config=cfg,
|
||||||
|
kb_name=KB_NAME,
|
||||||
|
service_id=SERVICE_ID,
|
||||||
|
)
|
||||||
|
system_prompt, messages = self._build_messages(
|
||||||
|
service=service,
|
||||||
|
cfg=cfg,
|
||||||
|
structured_context=structured_context,
|
||||||
|
rag_context=rag_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
audit_log = None
|
||||||
|
if farm_uuid:
|
||||||
|
try:
|
||||||
|
audit_log = _create_audit_log(
|
||||||
|
farm_uuid=farm_uuid,
|
||||||
|
service_id=SERVICE_ID,
|
||||||
|
model=model,
|
||||||
|
query=rag_query,
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
messages=messages,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("Irrigation plan parser audit log creation failed for %s: %s", farm_uuid, exc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
response_format={"type": "json_object"},
|
||||||
|
)
|
||||||
|
raw = (response.choices[0].message.content or "").strip()
|
||||||
|
parsed = self._clean_json(raw)
|
||||||
|
validated = IrrigationPlanParseResultSchema.model_validate(parsed)
|
||||||
|
normalized = self._normalize_result(validated)
|
||||||
|
if audit_log is not None:
|
||||||
|
_complete_audit_log(audit_log, raw)
|
||||||
|
return normalized
|
||||||
|
except (ValidationError, ValueError, KeyError, IndexError) as exc:
|
||||||
|
logger.warning("Irrigation plan parser parsing failed: %s", exc)
|
||||||
|
if audit_log is not None:
|
||||||
|
_fail_audit_log(audit_log, str(exc))
|
||||||
|
return self._fallback_result(
|
||||||
|
message=normalized_message,
|
||||||
|
answers=normalized_answers,
|
||||||
|
partial_plan=normalized_partial,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.error("Irrigation plan parser failed: %s", exc)
|
||||||
|
if audit_log is not None:
|
||||||
|
_fail_audit_log(audit_log, str(exc))
|
||||||
|
return self._fallback_result(
|
||||||
|
message=normalized_message,
|
||||||
|
answers=normalized_answers,
|
||||||
|
partial_plan=normalized_partial,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_service_client(self, cfg: RAGConfig):
|
||||||
|
service = get_service_config(SERVICE_ID, cfg)
|
||||||
|
service_cfg = RAGConfig(
|
||||||
|
embedding=cfg.embedding,
|
||||||
|
qdrant=cfg.qdrant,
|
||||||
|
chunking=cfg.chunking,
|
||||||
|
llm=service.llm,
|
||||||
|
knowledge_bases=cfg.knowledge_bases,
|
||||||
|
services=cfg.services,
|
||||||
|
chromadb=cfg.chromadb,
|
||||||
|
)
|
||||||
|
client = get_chat_client(service_cfg)
|
||||||
|
return service, client, service.llm.model
|
||||||
|
|
||||||
|
def _build_messages(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
service: Any,
|
||||||
|
cfg: RAGConfig,
|
||||||
|
structured_context: dict[str, Any],
|
||||||
|
rag_context: str,
|
||||||
|
) -> tuple[str, list[dict[str, str]]]:
|
||||||
|
tone = _load_service_tone(service, cfg)
|
||||||
|
system_parts = [tone] if tone else []
|
||||||
|
if service.system_prompt:
|
||||||
|
system_parts.append(service.system_prompt)
|
||||||
|
system_parts.append(IRRIGATION_PLAN_PROMPT)
|
||||||
|
system_parts.append(
|
||||||
|
"[structured_context]\n"
|
||||||
|
+ json.dumps(structured_context, ensure_ascii=False, indent=2, default=str)
|
||||||
|
)
|
||||||
|
if rag_context:
|
||||||
|
system_parts.append(rag_context)
|
||||||
|
system_prompt = "\n\n".join(part for part in system_parts if part)
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": "برنامه آبیاری را استخراج یا برای تکمیل آن سوال بپرس."},
|
||||||
|
]
|
||||||
|
return system_prompt, messages
|
||||||
|
|
||||||
|
def _build_retrieval_query(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
message: str,
|
||||||
|
answers: dict[str, Any],
|
||||||
|
) -> str:
|
||||||
|
answer_lines = [f"{key}: {value}" for key, value in answers.items()]
|
||||||
|
parts = [part for part in [message, "\n".join(answer_lines)] if part]
|
||||||
|
return "\n".join(parts) or "استخراج برنامه آبیاری از متن کاربر"
|
||||||
|
|
||||||
|
def _normalize_result(self, validated: IrrigationPlanParseResultSchema) -> dict[str, Any]:
|
||||||
|
collected = validated.collected_data.model_dump()
|
||||||
|
final_plan = validated.final_plan.model_dump() if validated.final_plan is not None else None
|
||||||
|
missing_fields = list(dict.fromkeys(validated.missing_fields))
|
||||||
|
computed_missing = self._find_missing_fields(final_plan or collected)
|
||||||
|
for field in computed_missing:
|
||||||
|
if field not in missing_fields:
|
||||||
|
missing_fields.append(field)
|
||||||
|
|
||||||
|
can_complete = validated.status == "completed" and not missing_fields
|
||||||
|
|
||||||
|
if can_complete:
|
||||||
|
final_plan = final_plan or collected
|
||||||
|
questions: list[dict[str, Any]] = []
|
||||||
|
status_fa = "تکمیل شد"
|
||||||
|
else:
|
||||||
|
questions = [item.model_dump() for item in validated.questions]
|
||||||
|
if not questions and missing_fields:
|
||||||
|
questions = self._build_generic_questions(missing_fields)
|
||||||
|
final_plan = None
|
||||||
|
validated.status = "needs_clarification"
|
||||||
|
status_fa = "نیازمند پرسش تکمیلی"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "completed" if can_complete else "needs_clarification",
|
||||||
|
"status_fa": status_fa,
|
||||||
|
"summary": validated.summary,
|
||||||
|
"missing_fields": missing_fields,
|
||||||
|
"questions": questions,
|
||||||
|
"collected_data": collected,
|
||||||
|
"final_plan": final_plan,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _fallback_result(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
message: str,
|
||||||
|
answers: dict[str, Any],
|
||||||
|
partial_plan: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
merged = dict(partial_plan)
|
||||||
|
notes = list(merged.get("notes") or [])
|
||||||
|
if message:
|
||||||
|
notes.append(f"متن اولیه کاربر: {message}")
|
||||||
|
for key, value in answers.items():
|
||||||
|
merged.setdefault(key, value)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "needs_clarification",
|
||||||
|
"status_fa": "نیازمند پرسش تکمیلی",
|
||||||
|
"summary": "اطلاعات برنامه آبیاری برای ساخت JSON نهایی کافی نیست و به چند پاسخ تکمیلی نیاز است.",
|
||||||
|
"missing_fields": CORE_FIELDS,
|
||||||
|
"questions": self._build_generic_questions(CORE_FIELDS),
|
||||||
|
"collected_data": {
|
||||||
|
"crop_name": merged.get("crop_name"),
|
||||||
|
"growth_stage": merged.get("growth_stage"),
|
||||||
|
"irrigation_method": merged.get("irrigation_method"),
|
||||||
|
"water_amount_per_event": merged.get("water_amount_per_event"),
|
||||||
|
"duration_minutes": merged.get("duration_minutes"),
|
||||||
|
"frequency_text": merged.get("frequency_text"),
|
||||||
|
"interval_days": merged.get("interval_days"),
|
||||||
|
"preferred_time_of_day": merged.get("preferred_time_of_day"),
|
||||||
|
"start_date": merged.get("start_date"),
|
||||||
|
"target_area": merged.get("target_area"),
|
||||||
|
"trigger_conditions": merged.get("trigger_conditions") or [],
|
||||||
|
"notes": notes,
|
||||||
|
},
|
||||||
|
"final_plan": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _build_generic_questions(self, missing_fields: list[str]) -> list[dict[str, str]]:
|
||||||
|
catalog = {
|
||||||
|
"crop_name": {
|
||||||
|
"id": "crop_name",
|
||||||
|
"field": "crop_name",
|
||||||
|
"question": "این برنامه آبیاری برای کدام محصول است؟",
|
||||||
|
"rationale": "نام محصول برای ثبت برنامه لازم است.",
|
||||||
|
},
|
||||||
|
"growth_stage": {
|
||||||
|
"id": "growth_stage",
|
||||||
|
"field": "growth_stage",
|
||||||
|
"question": "محصول الان در چه مرحله رشدی قرار دارد؟",
|
||||||
|
"rationale": "مرحله رشد برای کامل شدن برنامه لازم است.",
|
||||||
|
},
|
||||||
|
"irrigation_method": {
|
||||||
|
"id": "irrigation_method",
|
||||||
|
"field": "irrigation_method",
|
||||||
|
"question": "روش آبیاری چیست؟ مثلا قطره ای، بارانی یا غرقابی.",
|
||||||
|
"rationale": "روش اجرا روی شکل برنامه تاثیر دارد.",
|
||||||
|
},
|
||||||
|
"water_amount_per_event": {
|
||||||
|
"id": "water_amount_per_event",
|
||||||
|
"field": "water_amount_per_event",
|
||||||
|
"question": "در هر نوبت آبیاری چه مقدار آب داده می شود؟",
|
||||||
|
"rationale": "حجم یا عمق آب هر نوبت مشخص نشده است.",
|
||||||
|
},
|
||||||
|
"duration_minutes": {
|
||||||
|
"id": "duration_minutes",
|
||||||
|
"field": "duration_minutes",
|
||||||
|
"question": "مدت زمان هر نوبت آبیاری چند دقیقه است؟",
|
||||||
|
"rationale": "مدت اجرای هر نوبت هنوز مشخص نیست.",
|
||||||
|
},
|
||||||
|
"frequency_text": {
|
||||||
|
"id": "frequency_text",
|
||||||
|
"field": "frequency_text",
|
||||||
|
"question": "فاصله یا تعداد نوبت های آبیاری چگونه است؟ مثلا هر 3 روز یک بار.",
|
||||||
|
"rationale": "الگوی تکرار آبیاری باید مشخص باشد.",
|
||||||
|
},
|
||||||
|
"interval_days": {
|
||||||
|
"id": "interval_days",
|
||||||
|
"field": "interval_days",
|
||||||
|
"question": "فاصله بین دو آبیاری چند روز است؟",
|
||||||
|
"rationale": "عدد فاصله آبیاری برای JSON نهایی لازم است.",
|
||||||
|
},
|
||||||
|
"preferred_time_of_day": {
|
||||||
|
"id": "preferred_time_of_day",
|
||||||
|
"field": "preferred_time_of_day",
|
||||||
|
"question": "بهترین زمان اجرای آبیاری چه موقع از روز است؟",
|
||||||
|
"rationale": "زمان اجرای برنامه هنوز معلوم نیست.",
|
||||||
|
},
|
||||||
|
"start_date": {
|
||||||
|
"id": "start_date",
|
||||||
|
"field": "start_date",
|
||||||
|
"question": "این برنامه از چه تاریخی یا از چه زمانی باید شروع شود؟",
|
||||||
|
"rationale": "زمان شروع برنامه هنوز مشخص نشده است.",
|
||||||
|
},
|
||||||
|
"target_area": {
|
||||||
|
"id": "target_area",
|
||||||
|
"field": "target_area",
|
||||||
|
"question": "این برنامه برای کل مزرعه است یا بخش/ناحیه خاصی از مزرعه؟",
|
||||||
|
"rationale": "محدوده اجرای برنامه باید مشخص باشد.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return [catalog[field] for field in missing_fields if field in catalog][:5]
|
||||||
|
|
||||||
|
def _find_missing_fields(self, plan: dict[str, Any]) -> list[str]:
|
||||||
|
missing: list[str] = []
|
||||||
|
for field in CORE_FIELDS:
|
||||||
|
value = plan.get(field)
|
||||||
|
if value is None:
|
||||||
|
missing.append(field)
|
||||||
|
continue
|
||||||
|
if isinstance(value, str) and not value.strip():
|
||||||
|
missing.append(field)
|
||||||
|
return missing
|
||||||
|
|
||||||
|
def _clean_json(self, raw: str) -> dict[str, Any]:
|
||||||
|
cleaned = (raw or "").strip()
|
||||||
|
if cleaned.startswith("```"):
|
||||||
|
cleaned = cleaned.strip("`")
|
||||||
|
if cleaned.startswith("json"):
|
||||||
|
cleaned = cleaned[4:]
|
||||||
|
cleaned = cleaned.strip()
|
||||||
|
if not cleaned:
|
||||||
|
raise ValueError("Irrigation plan parser response was empty.")
|
||||||
|
parsed = json.loads(cleaned)
|
||||||
|
if not isinstance(parsed, dict):
|
||||||
|
raise ValueError("Irrigation plan parser response root must be an object.")
|
||||||
|
return parsed
|
||||||
Reference in New Issue
Block a user