UPDATE
This commit is contained in:
@@ -15,6 +15,7 @@ DB_ROOT_PASSWORD=root
|
|||||||
# SMS.ir
|
# SMS.ir
|
||||||
SMS_IR_API_KEY=
|
SMS_IR_API_KEY=
|
||||||
SMS_IR_LINE_NUMBER=300000000000
|
SMS_IR_LINE_NUMBER=300000000000
|
||||||
|
USE_EXTERNAL_API_MOCK=true
|
||||||
|
|
||||||
# External API adapter
|
# External API adapter
|
||||||
USE_EXTERNAL_API_MOCK=true
|
USE_EXTERNAL_API_MOCK=true
|
||||||
|
|||||||
@@ -0,0 +1,439 @@
|
|||||||
|
# مستند پاسخ API برای فیچرهای Farm AI
|
||||||
|
|
||||||
|
این فایل، پاسخهای API موردنیاز برای سه فیچر زیر را یکجا جمع میکند:
|
||||||
|
|
||||||
|
- `src/app/(dashboard)/(private)/farm-ai-assistant/page.tsx`
|
||||||
|
- `src/app/(dashboard)/(private)/fertilization-recommendation/page.tsx`
|
||||||
|
- `src/app/(dashboard)/(private)/irrigation-recommendation/page.tsx`
|
||||||
|
|
||||||
|
> مبنا، پیادهسازی فعلی فرانت در `src/views/dashboards/farm/...` و سرویسهای `src/libs/api/services/...` است.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## قرارداد کلی پاسخها
|
||||||
|
|
||||||
|
در هر سه سرویس، فرانت انتظار این wrapper را دارد:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `status`: معمولاً `success` یا `error`
|
||||||
|
- `data`: payload اصلی که فرانت unwrap میکند
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Farm AI Assistant
|
||||||
|
|
||||||
|
### صفحه
|
||||||
|
|
||||||
|
- `src/app/(dashboard)/(private)/farm-ai-assistant/page.tsx`
|
||||||
|
- کامپوننت اصلی: `src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx`
|
||||||
|
|
||||||
|
### APIهای موردنیاز
|
||||||
|
|
||||||
|
1. `GET /api/farm-ai-assistant/context/`
|
||||||
|
2. `POST /api/farm-ai-assistant/chat/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.1) دریافت context مزرعه
|
||||||
|
|
||||||
|
#### Endpoint
|
||||||
|
|
||||||
|
`GET /api/farm-ai-assistant/context/`
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"soilType": "Loamy",
|
||||||
|
"waterEC": "1.2 dS/m",
|
||||||
|
"selectedCrop": "Tomato",
|
||||||
|
"growthStage": "Flowering",
|
||||||
|
"lastIrrigationStatus": "2 days ago"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### فیلدهای لازم
|
||||||
|
|
||||||
|
| فیلد | نوع | مصرف در UI |
|
||||||
|
|------|-----|------------|
|
||||||
|
| `soilType` | `string` | badge نوع خاک |
|
||||||
|
| `waterEC` | `string` | badge EC آب |
|
||||||
|
| `selectedCrop` | `string` | badge محصول انتخابشده |
|
||||||
|
| `growthStage` | `string` | badge مرحله رشد |
|
||||||
|
| `lastIrrigationStatus` | `string` | badge آخرین وضعیت آبیاری |
|
||||||
|
|
||||||
|
#### نکته
|
||||||
|
|
||||||
|
اگر این API خطا بدهد، فرانت fallback داخلی دارد و toast خطا نمایش میدهد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2) ارسال پیام به دستیار
|
||||||
|
|
||||||
|
#### Endpoint
|
||||||
|
|
||||||
|
`POST /api/farm-ai-assistant/chat/`
|
||||||
|
|
||||||
|
#### Request body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "What is the best irrigation plan for tomato?",
|
||||||
|
"farm_context": {
|
||||||
|
"soilType": "Loamy",
|
||||||
|
"waterEC": "1.2 dS/m",
|
||||||
|
"selectedCrop": "Tomato",
|
||||||
|
"growthStage": "Flowering",
|
||||||
|
"lastIrrigationStatus": "2 days ago"
|
||||||
|
},
|
||||||
|
"conversation_id": "conv-123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"message_id": "msg-001",
|
||||||
|
"conversation_id": "conv-123",
|
||||||
|
"content": "Here is the recommended plan.",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"type": "recommendation",
|
||||||
|
"title": "Irrigation Plan",
|
||||||
|
"icon": "droplet",
|
||||||
|
"frequency": "3 times per week",
|
||||||
|
"amount": "15 liters per plant",
|
||||||
|
"timing": "Early morning",
|
||||||
|
"expandableExplanation": "Loamy soil holds moisture well, so moderate frequency is enough."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"title": "Important Notes",
|
||||||
|
"icon": "leaf",
|
||||||
|
"items": [
|
||||||
|
"Avoid watering at noon",
|
||||||
|
"Check leaf stress every two days"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "warning",
|
||||||
|
"title": "Heat Alert",
|
||||||
|
"icon": "warning",
|
||||||
|
"content": "Increase irrigation if temperature rises above 35°C."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### فیلدهای لازم در response
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|-------|
|
||||||
|
| `message_id` | `string` | id پیام assistant |
|
||||||
|
| `conversation_id` | `string` | برای ادامه چت در پیامهای بعدی |
|
||||||
|
| `content` | `string` | متن ساده پاسخ |
|
||||||
|
| `sections` | `ChatSection[]` | خروجی ساختیافته برای رندر کارتها |
|
||||||
|
|
||||||
|
#### ساختار `ChatSection`
|
||||||
|
|
||||||
|
| فیلد | نوع | اجباری | توضیح |
|
||||||
|
|------|-----|--------|-------|
|
||||||
|
| `type` | `'text' \| 'list' \| 'recommendation' \| 'warning'` | بله | نوع سکشن |
|
||||||
|
| `title` | `string` | خیر | عنوان سکشن |
|
||||||
|
| `content` | `string` | خیر | متن سکشن |
|
||||||
|
| `items` | `string[]` | خیر | برای لیست |
|
||||||
|
| `icon` | `'droplet' \| 'leaf' \| 'warning' \| 'fertilizer' \| 'calendar'` | خیر | آیکون نمایشی |
|
||||||
|
| `frequency` | `string` | خیر | فقط برای `recommendation` |
|
||||||
|
| `amount` | `string` | خیر | فقط برای `recommendation` |
|
||||||
|
| `timing` | `string` | خیر | فقط برای `recommendation` |
|
||||||
|
| `expandableExplanation` | `string` | خیر | توضیح بازشونده |
|
||||||
|
|
||||||
|
#### حداقل توصیه
|
||||||
|
|
||||||
|
- `sections` همیشه به صورت آرایه برگردد، حتی اگر خالی باشد.
|
||||||
|
- `conversation_id` بعد از اولین پاسخ حتماً برگردد.
|
||||||
|
- اگر پاسخ فقط structured است، `content` میتواند رشته خالی باشد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Fertilization Recommendation
|
||||||
|
|
||||||
|
### صفحه
|
||||||
|
|
||||||
|
- `src/app/(dashboard)/(private)/fertilization-recommendation/page.tsx`
|
||||||
|
- کامپوننت اصلی: `src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx`
|
||||||
|
|
||||||
|
### APIهای موردنیاز
|
||||||
|
|
||||||
|
1. `GET /api/fertilization-recommendation/config/`
|
||||||
|
2. `POST /api/fertilization-recommendation/recommend/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.1) دریافت config اولیه
|
||||||
|
|
||||||
|
#### Endpoint
|
||||||
|
|
||||||
|
`GET /api/fertilization-recommendation/config/`
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"farmData": {
|
||||||
|
"soilType": "Loamy",
|
||||||
|
"organicMatter": "Medium (2.5%)",
|
||||||
|
"waterEC": "1.2 dS/m"
|
||||||
|
},
|
||||||
|
"growthStages": [
|
||||||
|
{ "id": "prePlanting", "icon": "tabler-seedling" },
|
||||||
|
{ "id": "earlyGrowth", "icon": "tabler-leaf" },
|
||||||
|
{ "id": "flowering", "icon": "tabler-flower" },
|
||||||
|
{ "id": "fruiting", "icon": "tabler-apple" },
|
||||||
|
{ "id": "postHarvest", "icon": "tabler-basket" }
|
||||||
|
],
|
||||||
|
"cropOptions": [
|
||||||
|
{ "id": "wheat", "labelKey": "wheat", "icon": "tabler-wheat" },
|
||||||
|
{ "id": "corn", "labelKey": "corn", "icon": "tabler-plant-2" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### فیلدهای لازم
|
||||||
|
|
||||||
|
##### `farmData`
|
||||||
|
|
||||||
|
| فیلد | نوع |
|
||||||
|
|------|-----|
|
||||||
|
| `soilType` | `string` |
|
||||||
|
| `organicMatter` | `string` |
|
||||||
|
| `waterEC` | `string` |
|
||||||
|
|
||||||
|
##### `growthStages[]`
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|-------|
|
||||||
|
| `id` | `string` | id مرحله رشد |
|
||||||
|
| `icon` | `string` | نام آیکون |
|
||||||
|
|
||||||
|
##### `cropOptions[]`
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|-------|
|
||||||
|
| `id` | `string` | id محصول برای submit |
|
||||||
|
| `labelKey` | `string` | کلید ترجمه |
|
||||||
|
| `icon` | `string` | نام آیکون |
|
||||||
|
|
||||||
|
#### نکته
|
||||||
|
|
||||||
|
- اگر `growthStages` مقدار داشته باشد، اولین آیتم به عنوان پیشفرض انتخاب میشود.
|
||||||
|
- اگر `cropOptions` خالی باشد، لیست انتخاب محصول خالی نمایش داده میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2) دریافت برنامه کوددهی
|
||||||
|
|
||||||
|
#### Endpoint
|
||||||
|
|
||||||
|
`POST /api/fertilization-recommendation/recommend/`
|
||||||
|
|
||||||
|
#### Request body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"crop_id": "wheat",
|
||||||
|
"growth_stage": "flowering",
|
||||||
|
"soilType": "Loamy",
|
||||||
|
"organicMatter": "Medium (2.5%)",
|
||||||
|
"waterEC": "1.2 dS/m"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"plan": {
|
||||||
|
"npkRatio": "20-20-20",
|
||||||
|
"amountPerHectare": "150 kg/ha",
|
||||||
|
"applicationMethod": "Fertigation",
|
||||||
|
"applicationInterval": "Every 10 days",
|
||||||
|
"reasoning": "Balanced NPK is recommended during flowering to support bloom and fruit set."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### فیلدهای لازم در `plan`
|
||||||
|
|
||||||
|
| فیلد | نوع | مصرف در UI |
|
||||||
|
|------|-----|------------|
|
||||||
|
| `npkRatio` | `string` | نوع/نسبت کود |
|
||||||
|
| `amountPerHectare` | `string` | مقدار مصرف |
|
||||||
|
| `applicationMethod` | `string` | روش مصرف |
|
||||||
|
| `applicationInterval` | `string` | بازه تکرار |
|
||||||
|
| `reasoning` | `string` | توضیح بازشونده |
|
||||||
|
|
||||||
|
#### نکته
|
||||||
|
|
||||||
|
فرانت مستقیماً `data.plan` را انتظار دارد. اگر `plan` نباشد، نتیجهای نمایش داده نمیشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Irrigation Recommendation
|
||||||
|
|
||||||
|
### صفحه
|
||||||
|
|
||||||
|
- `src/app/(dashboard)/(private)/irrigation-recommendation/page.tsx`
|
||||||
|
- کامپوننت اصلی: `src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx`
|
||||||
|
|
||||||
|
### APIهای موردنیاز
|
||||||
|
|
||||||
|
1. `GET /api/irrigation-recommendation/config/`
|
||||||
|
2. `POST /api/irrigation-recommendation/recommend/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.1) دریافت config اولیه
|
||||||
|
|
||||||
|
#### Endpoint
|
||||||
|
|
||||||
|
`GET /api/irrigation-recommendation/config/`
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"farmInfo": {
|
||||||
|
"soilType": "Loamy",
|
||||||
|
"waterQuality": "Medium EC",
|
||||||
|
"climateZone": "Temperate"
|
||||||
|
},
|
||||||
|
"cropOptions": [
|
||||||
|
{ "id": "tomato", "labelKey": "tomato", "icon": "tabler-plant-2" },
|
||||||
|
{ "id": "cucumber", "labelKey": "cucumber", "icon": "tabler-leaf" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### فیلدهای لازم
|
||||||
|
|
||||||
|
##### `farmInfo`
|
||||||
|
|
||||||
|
| فیلد | نوع |
|
||||||
|
|------|-----|
|
||||||
|
| `soilType` | `string` |
|
||||||
|
| `waterQuality` | `string` |
|
||||||
|
| `climateZone` | `string` |
|
||||||
|
|
||||||
|
##### `cropOptions[]`
|
||||||
|
|
||||||
|
| فیلد | نوع | توضیح |
|
||||||
|
|------|-----|-------|
|
||||||
|
| `id` | `string` | id محصول برای submit |
|
||||||
|
| `labelKey` | `string` | کلید ترجمه |
|
||||||
|
| `icon` | `string` | نام آیکون |
|
||||||
|
|
||||||
|
#### نکته
|
||||||
|
|
||||||
|
در این صفحه `farmInfo` بدون چک null مستقیماً set میشود؛ بهتر است API همیشه این آبجکت را برگرداند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2) دریافت برنامه آبیاری
|
||||||
|
|
||||||
|
#### Endpoint
|
||||||
|
|
||||||
|
`POST /api/irrigation-recommendation/recommend/`
|
||||||
|
|
||||||
|
#### Request body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"crop_id": "tomato"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> در سرویس، فیلدهای `soilType`, `waterQuality`, `climateZone` هم پشتیبانی شدهاند، ولی در UI فعلی فقط `crop_id` ارسال میشود.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"plan": {
|
||||||
|
"frequencyPerWeek": 3,
|
||||||
|
"durationMinutes": 25,
|
||||||
|
"bestTimeOfDay": "Early morning",
|
||||||
|
"moistureLevel": 68,
|
||||||
|
"warning": "Reduce irrigation if rainfall occurs this week."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### فیلدهای لازم در `plan`
|
||||||
|
|
||||||
|
| فیلد | نوع | مصرف در UI |
|
||||||
|
|------|-----|------------|
|
||||||
|
| `frequencyPerWeek` | `number` | تعداد دفعات در هفته |
|
||||||
|
| `durationMinutes` | `number` | مدت هر نوبت |
|
||||||
|
| `bestTimeOfDay` | `string` | زمان مناسب آبیاری |
|
||||||
|
| `moistureLevel` | `number` | درصد moisture برای دایره progress |
|
||||||
|
| `warning` | `string` | هشدار اختیاری |
|
||||||
|
|
||||||
|
#### محدودیت مهم
|
||||||
|
|
||||||
|
- `moistureLevel` بهتر است بین `0` تا `100` باشد، چون مستقیم برای محاسبه progress circle استفاده میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## جمعبندی سریع برای بکاند
|
||||||
|
|
||||||
|
### Farm AI Assistant
|
||||||
|
|
||||||
|
- `GET /api/farm-ai-assistant/context/` → `data: FarmContext`
|
||||||
|
- `POST /api/farm-ai-assistant/chat/` → `data: { message_id, conversation_id, content, sections }`
|
||||||
|
|
||||||
|
### Fertilization Recommendation
|
||||||
|
|
||||||
|
- `GET /api/fertilization-recommendation/config/` → `data: { farmData, growthStages, cropOptions }`
|
||||||
|
- `POST /api/fertilization-recommendation/recommend/` → `data: { plan }`
|
||||||
|
|
||||||
|
### Irrigation Recommendation
|
||||||
|
|
||||||
|
- `GET /api/irrigation-recommendation/config/` → `data: { farmInfo, cropOptions }`
|
||||||
|
- `POST /api/irrigation-recommendation/recommend/` → `data: { plan }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## مسیرهای مرجع کد
|
||||||
|
|
||||||
|
- `src/libs/api/services/farmAiAssistantService.ts`
|
||||||
|
- `src/libs/api/services/fertilizationRecommendationService.ts`
|
||||||
|
- `src/libs/api/services/irrigationRecommendationService.ts`
|
||||||
|
- `src/views/dashboards/farm/farmAiAssistant/FarmAiAssistantChat.tsx`
|
||||||
|
- `src/views/dashboards/farm/smartFertilization/SmartFertilizationRecommendation.tsx`
|
||||||
|
- `src/views/dashboards/farm/smartIrrigation/SmartIrrigationRecommendation.tsx`
|
||||||
@@ -1,4 +1,29 @@
|
|||||||
{
|
{
|
||||||
"content_type": "text/plain; charset=utf-8",
|
"content": "Here is the recommended plan.",
|
||||||
"body": "سلام، برای بازیابی رطوبت خاک بهتر است آبیاری صبحگاهی را تنظیم کنید."
|
"sections": [
|
||||||
|
{
|
||||||
|
"type": "recommendation",
|
||||||
|
"title": "Irrigation Plan",
|
||||||
|
"icon": "droplet",
|
||||||
|
"frequency": "3 times per week",
|
||||||
|
"amount": "15 liters per plant",
|
||||||
|
"timing": "Early morning",
|
||||||
|
"expandableExplanation": "Loamy soil holds moisture well, so moderate frequency is enough."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"title": "Important Notes",
|
||||||
|
"icon": "leaf",
|
||||||
|
"items": [
|
||||||
|
"Avoid watering at noon",
|
||||||
|
"Check leaf stress every two days"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "warning",
|
||||||
|
"title": "Heat Alert",
|
||||||
|
"icon": "warning",
|
||||||
|
"content": "Increase irrigation if temperature rises above 35°C."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,83 @@
|
|||||||
"""
|
"""
|
||||||
Static mock data for Farm AI Assistant API.
|
Static mock data for Farm AI Assistant API.
|
||||||
No database, no dynamic values. All responses are fixed JSON.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CHAT_RESPONSE_DATA = {
|
CHAT_RESPONSE_DATA = {
|
||||||
"message_id": "a-1739123456789",
|
"message_id": "msg-001",
|
||||||
"conversation_id": "conv-abc123",
|
"conversation_id": "conv-123",
|
||||||
"content": "",
|
"content": "Here is the recommended plan.",
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"type": "recommendation",
|
"type": "recommendation",
|
||||||
"title": "Irrigation recommendation",
|
"title": "Irrigation Plan",
|
||||||
"icon": "droplet",
|
"icon": "droplet",
|
||||||
"frequency": "3 times per week",
|
"frequency": "3 times per week",
|
||||||
"amount": "15–20 L per plant",
|
"amount": "15 liters per plant",
|
||||||
"timing": "Early morning (05:00–07:00)",
|
"timing": "Early morning",
|
||||||
"expandableExplanation": "Your loamy soil holds moisture well...",
|
"expandableExplanation": "Loamy soil holds moisture well, so moderate frequency is enough.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "list",
|
"type": "list",
|
||||||
"title": "Key points",
|
"title": "Important Notes",
|
||||||
"icon": "leaf",
|
"icon": "leaf",
|
||||||
"items": [
|
"items": [
|
||||||
"Avoid midday watering to reduce evaporation",
|
"Avoid watering at noon",
|
||||||
"Drip irrigation preferred for root zone targeting",
|
"Check leaf stress every two days",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "warning",
|
"type": "warning",
|
||||||
"title": "Weather advisory",
|
"title": "Heat Alert",
|
||||||
"icon": "warning",
|
"icon": "warning",
|
||||||
"content": "High temps forecasted next week. Consider increasing frequency.",
|
"content": "Increase irrigation if temperature rises above 35°C.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CHAT_LIST_RESPONSE_DATA = [
|
||||||
|
{
|
||||||
|
"id": "conv-123",
|
||||||
|
"message_count": 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "conv-456",
|
||||||
|
"message_count": 2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
CHAT_MESSAGES_RESPONSE_DATA = {
|
||||||
|
"conversation_id": "conv-123",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"message_id": "msg-user-001",
|
||||||
|
"conversation_id": "conv-123",
|
||||||
|
"role": "user",
|
||||||
|
"content": "What is the best irrigation plan for tomato?",
|
||||||
|
"sections": [],
|
||||||
|
"images": [],
|
||||||
|
"created_at": "2025-01-01T08:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "msg-001",
|
||||||
|
"conversation_id": "conv-123",
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Here is the recommended plan.",
|
||||||
|
"sections": CHAT_RESPONSE_DATA["sections"],
|
||||||
|
"images": [],
|
||||||
|
"created_at": "2025-01-01T08:00:05Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
CHAT_CREATE_RESPONSE_DATA = {
|
||||||
|
"id": "conv-789",
|
||||||
|
"message_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
CHAT_DELETE_RESPONSE_DATA = {
|
||||||
|
"conversation_id": "conv-123",
|
||||||
|
}
|
||||||
|
|
||||||
CONTEXT_RESPONSE_DATA = {
|
CONTEXT_RESPONSE_DATA = {
|
||||||
"soilType": "Loamy",
|
"soilType": "Loamy",
|
||||||
"waterEC": "1.2 dS/m",
|
"waterEC": "1.2 dS/m",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"name": "Farm AI Assistant",
|
"name": "Farm AI Assistant",
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||||
"description": "Farm AI Assistant API. GET context (farm bar data). POST chat (user message + optional farm_context/images). Static JSON only."
|
"description": "Farm AI Assistant API. Context, chat send, chat list/create, message history, and chat delete."
|
||||||
},
|
},
|
||||||
"item": [
|
"item": [
|
||||||
{
|
{
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [{"key": "Content-Type", "value": "application/json"}],
|
"header": [{"key": "Content-Type", "value": "application/json"}],
|
||||||
"url": "{{baseUrl}}/api/farm-ai-assistant/context/",
|
"url": "{{baseUrl}}/api/farm-ai-assistant/context/",
|
||||||
"description": "Returns static farm context: soilType, waterEC, selectedCrop, growthStage, lastIrrigationStatus for the context bar."
|
"description": "Returns static farm context for the context bar."
|
||||||
},
|
},
|
||||||
"response": [
|
"response": [
|
||||||
{
|
{
|
||||||
@@ -23,23 +23,95 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Chat (POST)",
|
"name": "List chats (GET)",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "POST",
|
"method": "GET",
|
||||||
"header": [{"key": "Content-Type", "value": "application/json"}],
|
"header": [{"key": "Content-Type", "value": "application/json"}],
|
||||||
"body": {
|
"url": "{{baseUrl}}/api/farm-ai-assistant/chats/",
|
||||||
"mode": "raw",
|
"description": "Returns only chat id and message count for the current user."
|
||||||
"raw": "{\n \"content\": \"برنامه آبیاری برای گوجه در مرحله گلدهی چطور باشد؟\",\n \"farm_context\": {\n \"soilType\": \"Loamy\",\n \"waterEC\": \"1.2 dS/m\",\n \"selectedCrop\": \"Tomato\",\n \"growthStage\": \"Flowering\",\n \"lastIrrigationStatus\": \"2 days ago\"\n }\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/farm-ai-assistant/chat/",
|
|
||||||
"description": "Body: content (required), optional images, conversation_id, farm_context. Returns static message with sections (recommendation, list, warning). Input not processed."
|
|
||||||
},
|
},
|
||||||
"response": [
|
"response": [
|
||||||
{
|
{
|
||||||
"name": "Success",
|
"name": "Success",
|
||||||
"status": "OK",
|
"status": "OK",
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"body": "{\n \"status\": \"success\",\n \"data\": {\n \"message_id\": \"a-1739123456789\",\n \"conversation_id\": \"conv-abc123\",\n \"content\": \"\",\n \"sections\": [\n {\n \"type\": \"recommendation\",\n \"title\": \"Irrigation recommendation\",\n \"icon\": \"droplet\",\n \"frequency\": \"3 times per week\",\n \"amount\": \"15–20 L per plant\",\n \"timing\": \"Early morning (05:00–07:00)\",\n \"expandableExplanation\": \"Your loamy soil holds moisture well...\"\n },\n {\n \"type\": \"list\",\n \"title\": \"Key points\",\n \"icon\": \"leaf\",\n \"items\": [\n \"Avoid midday watering to reduce evaporation\",\n \"Drip irrigation preferred for root zone targeting\"\n ]\n },\n {\n \"type\": \"warning\",\n \"title\": \"Weather advisory\",\n \"icon\": \"warning\",\n \"content\": \"High temps forecasted next week. Consider increasing frequency.\"\n }\n ]\n }\n}"
|
"body": "{\n \"status\": \"success\",\n \"data\": [\n {\n \"id\": \"conv-123\",\n \"message_count\": 4\n },\n {\n \"id\": \"conv-456\",\n \"message_count\": 2\n }\n ]\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Create chat (POST)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [{"key": "Content-Type", "value": "application/json"}],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"title\": \"New chat\"\n}"
|
||||||
|
},
|
||||||
|
"url": "{{baseUrl}}/api/farm-ai-assistant/chats/",
|
||||||
|
"description": "Creates a new empty chat for the current user."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "Created",
|
||||||
|
"status": "Created",
|
||||||
|
"code": 201,
|
||||||
|
"body": "{\n \"status\": \"success\",\n \"data\": {\n \"id\": \"conv-789\",\n \"message_count\": 0\n }\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get chat messages (GET)",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [{"key": "Content-Type", "value": "application/json"}],
|
||||||
|
"url": "{{baseUrl}}/api/farm-ai-assistant/chats/conv-123/messages/",
|
||||||
|
"description": "Returns all user and assistant messages for one chat."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "Success",
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"body": "{\n \"status\": \"success\",\n \"data\": {\n \"conversation_id\": \"conv-123\",\n \"messages\": [\n {\n \"message_id\": \"msg-user-001\",\n \"conversation_id\": \"conv-123\",\n \"role\": \"user\",\n \"content\": \"What is the best irrigation plan for tomato?\",\n \"sections\": [],\n \"images\": [],\n \"created_at\": \"2025-01-01T08:00:00Z\"\n },\n {\n \"message_id\": \"msg-001\",\n \"conversation_id\": \"conv-123\",\n \"role\": \"assistant\",\n \"content\": \"Here is the recommended plan.\",\n \"sections\": [\n {\n \"type\": \"recommendation\",\n \"title\": \"Irrigation Plan\",\n \"icon\": \"droplet\",\n \"frequency\": \"3 times per week\",\n \"amount\": \"15 liters per plant\",\n \"timing\": \"Early morning\",\n \"expandableExplanation\": \"Loamy soil holds moisture well, so moderate frequency is enough.\"\n }\n ],\n \"images\": [],\n \"created_at\": \"2025-01-01T08:00:05Z\"\n }\n ]\n }\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send chat message (POST)",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [{"key": "Content-Type", "value": "application/json"}],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"conversation_id\": \"conv-123\",\n \"content\": \"What is the best irrigation plan for tomato?\",\n \"farm_context\": {\n \"soilType\": \"Loamy\",\n \"waterEC\": \"1.2 dS/m\",\n \"selectedCrop\": \"Tomato\",\n \"growthStage\": \"Flowering\",\n \"lastIrrigationStatus\": \"2 days ago\"\n }\n}"
|
||||||
|
},
|
||||||
|
"url": "{{baseUrl}}/api/farm-ai-assistant/chat/",
|
||||||
|
"description": "Sends a user message and returns a structured assistant reply."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "Success",
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"body": "{\n \"status\": \"success\",\n \"data\": {\n \"message_id\": \"msg-001\",\n \"conversation_id\": \"conv-123\",\n \"content\": \"Here is the recommended plan.\",\n \"sections\": [\n {\n \"type\": \"recommendation\",\n \"title\": \"Irrigation Plan\",\n \"icon\": \"droplet\",\n \"frequency\": \"3 times per week\",\n \"amount\": \"15 liters per plant\",\n \"timing\": \"Early morning\",\n \"expandableExplanation\": \"Loamy soil holds moisture well, so moderate frequency is enough.\"\n },\n {\n \"type\": \"list\",\n \"title\": \"Important Notes\",\n \"icon\": \"leaf\",\n \"items\": [\n \"Avoid watering at noon\",\n \"Check leaf stress every two days\"\n ]\n },\n {\n \"type\": \"warning\",\n \"title\": \"Heat Alert\",\n \"icon\": \"warning\",\n \"content\": \"Increase irrigation if temperature rises above 35°C.\"\n }\n ]\n }\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete chat (DELETE)",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"header": [{"key": "Content-Type", "value": "application/json"}],
|
||||||
|
"url": "{{baseUrl}}/api/farm-ai-assistant/chats/conv-123/",
|
||||||
|
"description": "Deletes one chat and all messages inside it."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "Success",
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"body": "{\n \"status\": \"success\",\n \"data\": {\n \"conversation_id\": \"conv-123\"\n }\n}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,54 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from .models import Conversation, Message
|
from .models import Message
|
||||||
|
|
||||||
|
|
||||||
class ConversationListSerializer(serializers.ModelSerializer):
|
class ChatSectionSerializer(serializers.Serializer):
|
||||||
conversation_id = serializers.UUIDField(source="uuid", read_only=True)
|
type = serializers.ChoiceField(choices=["text", "list", "recommendation", "warning"])
|
||||||
|
title = serializers.CharField(required=False, allow_blank=True)
|
||||||
class Meta:
|
content = serializers.CharField(required=False, allow_blank=True)
|
||||||
model = Conversation
|
items = serializers.ListField(child=serializers.CharField(), required=False)
|
||||||
fields = [
|
icon = serializers.CharField(required=False, allow_blank=True)
|
||||||
"conversation_id",
|
frequency = serializers.CharField(required=False, allow_blank=True)
|
||||||
"title",
|
amount = serializers.CharField(required=False, allow_blank=True)
|
||||||
"updated_at",
|
timing = serializers.CharField(required=False, allow_blank=True)
|
||||||
]
|
expandableExplanation = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
class MessageSerializer(serializers.ModelSerializer):
|
class ConversationSummarySerializer(serializers.Serializer):
|
||||||
message_id = serializers.UUIDField(source="uuid", read_only=True)
|
id = serializers.UUIDField(source="uuid", read_only=True)
|
||||||
conversation_id = serializers.UUIDField(source="conversation.uuid", read_only=True)
|
message_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Message
|
class ConversationCreateSerializer(serializers.Serializer):
|
||||||
fields = [
|
title = serializers.CharField(required=False, allow_blank=True, max_length=255)
|
||||||
"message_id",
|
farm_context = serializers.JSONField(required=False)
|
||||||
"conversation_id",
|
|
||||||
"role",
|
|
||||||
"content",
|
class ChatHistoryMessageSerializer(serializers.Serializer):
|
||||||
"images",
|
message_id = serializers.UUIDField(read_only=True)
|
||||||
"raw_response",
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
"created_at",
|
role = serializers.ChoiceField(choices=Message.ROLE_CHOICES, read_only=True)
|
||||||
]
|
content = serializers.CharField(read_only=True, allow_blank=True)
|
||||||
|
sections = ChatSectionSerializer(many=True, read_only=True)
|
||||||
|
images = serializers.ListField(child=serializers.CharField(), read_only=True)
|
||||||
|
created_at = serializers.DateTimeField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationMessagesSerializer(serializers.Serializer):
|
||||||
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
|
messages = ChatHistoryMessageSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ChatResponseDataSerializer(serializers.Serializer):
|
||||||
|
message_id = serializers.UUIDField(read_only=True)
|
||||||
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
|
content = serializers.CharField(read_only=True, allow_blank=True)
|
||||||
|
sections = ChatSectionSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationDeleteSerializer(serializers.Serializer):
|
||||||
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class ChatPostSerializer(serializers.Serializer):
|
class ChatPostSerializer(serializers.Serializer):
|
||||||
@@ -42,3 +61,10 @@ class ChatPostSerializer(serializers.Serializer):
|
|||||||
conversation_id = serializers.UUIDField(required=False)
|
conversation_id = serializers.UUIDField(required=False)
|
||||||
title = serializers.CharField(required=False, allow_blank=True, max_length=255)
|
title = serializers.CharField(required=False, allow_blank=True, max_length=255)
|
||||||
farm_context = serializers.JSONField(required=False)
|
farm_context = serializers.JSONField(required=False)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
content = attrs.get("content", "").strip()
|
||||||
|
images = attrs.get("images") or []
|
||||||
|
if not content and not images:
|
||||||
|
raise serializers.ValidationError("Either content or images is required.")
|
||||||
|
return attrs
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import ChatListView, ChatMessagesView, ChatView, ContextView
|
from .views import ChatDetailView, ChatListCreateView, ChatMessagesView, ChatView, ContextView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("context/", ContextView.as_view(), name="farm-ai-assistant-context"),
|
path("context/", ContextView.as_view(), name="farm-ai-assistant-context"),
|
||||||
path("chat/", ChatView.as_view(), name="farm-ai-assistant-chat"),
|
path("chat/", ChatView.as_view(), name="farm-ai-assistant-chat"),
|
||||||
path("chats/", ChatListView.as_view(), name="farm-ai-assistant-chat-list"),
|
path("chats/", ChatListCreateView.as_view(), name="farm-ai-assistant-chat-list-create"),
|
||||||
|
path("chats/<uuid:conversation_id>/", ChatDetailView.as_view(), name="farm-ai-assistant-chat-detail"),
|
||||||
path("chats/<uuid:conversation_id>/messages/", ChatMessagesView.as_view(), name="farm-ai-assistant-chat-messages"),
|
path("chats/<uuid:conversation_id>/messages/", ChatMessagesView.as_view(), name="farm-ai-assistant-chat-messages"),
|
||||||
]
|
]
|
||||||
|
|||||||
+212
-98
@@ -1,6 +1,9 @@
|
|||||||
"""Farm AI Assistant API views."""
|
"""Farm AI Assistant API views."""
|
||||||
|
|
||||||
from django.http import Http404, HttpResponse
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.http import Http404
|
||||||
from rest_framework import serializers, status
|
from rest_framework import serializers, status
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@@ -10,9 +13,17 @@ from drf_spectacular.utils import OpenApiParameter, extend_schema
|
|||||||
|
|
||||||
from config.swagger import status_response
|
from config.swagger import status_response
|
||||||
from external_api_adapter import request as external_api_request
|
from external_api_adapter import request as external_api_request
|
||||||
from .mock_data import CONTEXT_RESPONSE_DATA
|
from external_api_adapter.exceptions import ExternalAPIRequestError
|
||||||
|
from .mock_data import CHAT_RESPONSE_DATA, CONTEXT_RESPONSE_DATA
|
||||||
from .models import Conversation, Message
|
from .models import Conversation, Message
|
||||||
from .serializers import ChatPostSerializer, ConversationListSerializer, MessageSerializer
|
from .serializers import (
|
||||||
|
ChatPostSerializer,
|
||||||
|
ChatResponseDataSerializer,
|
||||||
|
ConversationCreateSerializer,
|
||||||
|
ConversationDeleteSerializer,
|
||||||
|
ConversationMessagesSerializer,
|
||||||
|
ConversationSummarySerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ContextView(APIView):
|
class ContextView(APIView):
|
||||||
@@ -27,63 +38,194 @@ class ContextView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChatListView(APIView):
|
class ConversationAccessMixin:
|
||||||
permission_classes = [IsAuthenticated]
|
@staticmethod
|
||||||
|
def _get_conversation(request, conversation_id):
|
||||||
@extend_schema(
|
|
||||||
tags=["Farm AI Assistant"],
|
|
||||||
responses={200: status_response("FarmAiAssistantConversationListResponse", data=ConversationListSerializer(many=True))},
|
|
||||||
)
|
|
||||||
def get(self, request):
|
|
||||||
conversations = Conversation.objects.filter(owner=request.user).order_by("-updated_at", "-created_at")
|
|
||||||
|
|
||||||
serializer = ConversationListSerializer(conversations, many=True)
|
|
||||||
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
|
|
||||||
class ChatMessagesView(APIView):
|
|
||||||
permission_classes = [IsAuthenticated]
|
|
||||||
|
|
||||||
def _get_conversation(self, request, conversation_id):
|
|
||||||
try:
|
try:
|
||||||
return Conversation.objects.get(uuid=conversation_id, owner=request.user)
|
return Conversation.objects.get(uuid=conversation_id, owner=request.user)
|
||||||
except Conversation.DoesNotExist as exc:
|
except Conversation.DoesNotExist as exc:
|
||||||
raise Http404("Conversation not found") from exc
|
raise Http404("Conversation not found") from exc
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_sections(raw_sections):
|
||||||
|
if not isinstance(raw_sections, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
allowed_keys = {
|
||||||
|
"type",
|
||||||
|
"title",
|
||||||
|
"content",
|
||||||
|
"items",
|
||||||
|
"icon",
|
||||||
|
"frequency",
|
||||||
|
"amount",
|
||||||
|
"timing",
|
||||||
|
"expandableExplanation",
|
||||||
|
}
|
||||||
|
normalized_sections = []
|
||||||
|
for section in raw_sections:
|
||||||
|
if not isinstance(section, dict) or not section.get("type"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
normalized_section = {}
|
||||||
|
for key in allowed_keys:
|
||||||
|
value = section.get(key)
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
if key == "items":
|
||||||
|
if not isinstance(value, list):
|
||||||
|
continue
|
||||||
|
normalized_section[key] = [str(item) for item in value]
|
||||||
|
continue
|
||||||
|
normalized_section[key] = str(value) if key != "type" else value
|
||||||
|
|
||||||
|
normalized_sections.append(normalized_section)
|
||||||
|
return normalized_sections
|
||||||
|
|
||||||
|
def _build_mock_assistant_payload(self, conversation_id):
|
||||||
|
payload = deepcopy(CHAT_RESPONSE_DATA)
|
||||||
|
payload["conversation_id"] = str(conversation_id)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _extract_assistant_payload(self, adapter_data, conversation_id):
|
||||||
|
payload_source = adapter_data
|
||||||
|
if isinstance(adapter_data, dict) and isinstance(adapter_data.get("data"), dict):
|
||||||
|
payload_source = adapter_data["data"]
|
||||||
|
|
||||||
|
content = ""
|
||||||
|
sections = []
|
||||||
|
|
||||||
|
if isinstance(payload_source, dict):
|
||||||
|
content = payload_source.get("content") or ""
|
||||||
|
sections = self._normalize_sections(payload_source.get("sections"))
|
||||||
|
|
||||||
|
if not sections and isinstance(adapter_data, dict):
|
||||||
|
sections = self._normalize_sections(adapter_data.get("sections"))
|
||||||
|
|
||||||
|
if not content and isinstance(adapter_data, dict):
|
||||||
|
content = adapter_data.get("body") or adapter_data.get("content") or ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message_id": "",
|
||||||
|
"conversation_id": str(conversation_id),
|
||||||
|
"content": content,
|
||||||
|
"sections": sections,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _serialize_chat_message(message):
|
||||||
|
raw_response = message.raw_response if isinstance(message.raw_response, dict) else {}
|
||||||
|
sections = raw_response.get("sections") if message.role == Message.ROLE_ASSISTANT else []
|
||||||
|
return {
|
||||||
|
"message_id": str(message.uuid),
|
||||||
|
"conversation_id": str(message.conversation.uuid),
|
||||||
|
"role": message.role,
|
||||||
|
"content": message.content,
|
||||||
|
"sections": ConversationAccessMixin._normalize_sections(sections),
|
||||||
|
"images": message.images if isinstance(message.images, list) else [],
|
||||||
|
"created_at": message.created_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ChatListCreateView(ConversationAccessMixin, APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Farm AI Assistant"],
|
||||||
|
responses={200: status_response("FarmAiAssistantConversationListResponse", data=ConversationSummarySerializer(many=True))},
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
conversations = (
|
||||||
|
Conversation.objects.filter(owner=request.user)
|
||||||
|
.annotate(message_count=Count("messages"))
|
||||||
|
.order_by("-updated_at", "-created_at")
|
||||||
|
)
|
||||||
|
serializer = ConversationSummarySerializer(conversations, many=True)
|
||||||
|
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
tags=["Farm AI Assistant"],
|
||||||
|
request=ConversationCreateSerializer,
|
||||||
|
responses={201: status_response("FarmAiAssistantConversationCreateResponse", data=ConversationSummarySerializer())},
|
||||||
|
)
|
||||||
|
def post(self, request):
|
||||||
|
serializer = ConversationCreateSerializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
validated = serializer.validated_data
|
||||||
|
conversation = Conversation.objects.create(
|
||||||
|
owner=request.user,
|
||||||
|
title=validated.get("title", "").strip() or "New chat",
|
||||||
|
farm_context=validated.get("farm_context") or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
response_serializer = ConversationSummarySerializer(
|
||||||
|
{
|
||||||
|
"uuid": conversation.uuid,
|
||||||
|
"message_count": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return Response({"status": "success", "data": response_serializer.data}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMessagesView(ConversationAccessMixin, APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Farm AI Assistant"],
|
tags=["Farm AI Assistant"],
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(name="conversation_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH),
|
OpenApiParameter(name="conversation_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH),
|
||||||
],
|
],
|
||||||
responses={200: status_response("FarmAiAssistantMessageListResponse", data=MessageSerializer(many=True))},
|
responses={200: status_response("FarmAiAssistantMessageListResponse", data=ConversationMessagesSerializer())},
|
||||||
)
|
)
|
||||||
def get(self, request, conversation_id):
|
def get(self, request, conversation_id):
|
||||||
conversation = self._get_conversation(request, conversation_id)
|
conversation = self._get_conversation(request, conversation_id)
|
||||||
messages = conversation.messages.all()
|
messages = conversation.messages.all()
|
||||||
serializer = MessageSerializer(messages, many=True)
|
serialized_messages = [self._serialize_chat_message(message) for message in messages]
|
||||||
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
|
return Response(
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"conversation_id": str(conversation.uuid),
|
||||||
|
"messages": serialized_messages,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChatView(APIView):
|
class ChatDetailView(ConversationAccessMixin, APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def _get_conversation(self, request, conversation_id):
|
|
||||||
try:
|
|
||||||
return Conversation.objects.get(uuid=conversation_id, owner=request.user)
|
|
||||||
except Conversation.DoesNotExist as exc:
|
|
||||||
raise Http404("Conversation not found") from exc
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Farm AI Assistant"],
|
tags=["Farm AI Assistant"],
|
||||||
responses={200: status_response("FarmAiAssistantConversationListAliasResponse", data=ConversationListSerializer(many=True))},
|
parameters=[
|
||||||
|
OpenApiParameter(name="conversation_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH),
|
||||||
|
],
|
||||||
|
responses={200: status_response("FarmAiAssistantConversationDeleteResponse", data=ConversationDeleteSerializer())},
|
||||||
)
|
)
|
||||||
def get(self, request):
|
def delete(self, request, conversation_id):
|
||||||
return ChatListView().get(request)
|
conversation = self._get_conversation(request, conversation_id)
|
||||||
|
deleted_conversation_id = str(conversation.uuid)
|
||||||
|
conversation.delete()
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"conversation_id": deleted_conversation_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ChatView(ConversationAccessMixin, APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Farm AI Assistant"],
|
tags=["Farm AI Assistant"],
|
||||||
request=ChatPostSerializer,
|
request=ChatPostSerializer,
|
||||||
responses={200: status_response("FarmAiAssistantChatResponse", data=serializers.JSONField())},
|
responses={200: status_response("FarmAiAssistantChatResponse", data=ChatResponseDataSerializer())},
|
||||||
)
|
)
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
serializer = ChatPostSerializer(data=request.data)
|
serializer = ChatPostSerializer(data=request.data)
|
||||||
@@ -92,7 +234,7 @@ class ChatView(APIView):
|
|||||||
validated = serializer.validated_data
|
validated = serializer.validated_data
|
||||||
conversation_id = validated.get("conversation_id")
|
conversation_id = validated.get("conversation_id")
|
||||||
farm_context = validated.get("farm_context")
|
farm_context = validated.get("farm_context")
|
||||||
title = validated.get("title", "")
|
title = validated.get("title", "").strip()
|
||||||
|
|
||||||
if conversation_id:
|
if conversation_id:
|
||||||
conversation = self._get_conversation(request, conversation_id)
|
conversation = self._get_conversation(request, conversation_id)
|
||||||
@@ -109,7 +251,7 @@ class ChatView(APIView):
|
|||||||
else:
|
else:
|
||||||
conversation = Conversation.objects.create(
|
conversation = Conversation.objects.create(
|
||||||
owner=request.user,
|
owner=request.user,
|
||||||
title=title or (validated.get("content", "")[:255]),
|
title=title or (validated.get("content", "")[:255]) or "New chat",
|
||||||
farm_context=farm_context or {},
|
farm_context=farm_context or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -118,81 +260,53 @@ class ChatView(APIView):
|
|||||||
role=Message.ROLE_USER,
|
role=Message.ROLE_USER,
|
||||||
content=validated.get("content", ""),
|
content=validated.get("content", ""),
|
||||||
images=validated.get("images", []),
|
images=validated.get("images", []),
|
||||||
|
raw_response={},
|
||||||
)
|
)
|
||||||
|
|
||||||
adapter_payload = dict(request.data)
|
adapter_payload = dict(request.data)
|
||||||
adapter_payload["conversation_id"] = str(conversation.uuid)
|
adapter_payload["conversation_id"] = str(conversation.uuid)
|
||||||
adapter_response = external_api_request(
|
|
||||||
"ai",
|
|
||||||
"/rag/chat",
|
|
||||||
method="POST",
|
|
||||||
payload=adapter_payload,
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(adapter_response.data, dict) and "body" in adapter_response.data:
|
try:
|
||||||
assistant_content = adapter_response.data.get("body") or ""
|
adapter_response = external_api_request(
|
||||||
assistant_message = Message.objects.create(
|
"ai",
|
||||||
conversation=conversation,
|
"/rag/chat",
|
||||||
role=Message.ROLE_ASSISTANT,
|
method="POST",
|
||||||
content=assistant_content,
|
payload=adapter_payload,
|
||||||
raw_response=adapter_response.data,
|
|
||||||
)
|
)
|
||||||
|
if adapter_response.status_code >= 400:
|
||||||
if not conversation.title:
|
return Response(
|
||||||
conversation.title = (validated.get("content", "") or assistant_content or "New chat")[:255]
|
{
|
||||||
conversation.save(update_fields=["title", "updated_at"])
|
"status": "error",
|
||||||
else:
|
"data": adapter_response.data,
|
||||||
conversation.save(update_fields=["updated_at"])
|
},
|
||||||
|
status=adapter_response.status_code,
|
||||||
return Response(
|
)
|
||||||
{
|
assistant_payload = self._extract_assistant_payload(adapter_response.data, conversation.uuid)
|
||||||
"conversation_id": str(conversation.uuid),
|
response_status_code = adapter_response.status_code
|
||||||
"user_message_id": str(user_message.uuid),
|
except ExternalAPIRequestError:
|
||||||
"assistant_message_id": str(assistant_message.uuid),
|
assistant_payload = self._build_mock_assistant_payload(conversation.uuid)
|
||||||
"content": assistant_content,
|
response_status_code = status.HTTP_200_OK
|
||||||
"content_type": adapter_response.data.get("content_type", "text/plain; charset=utf-8"),
|
|
||||||
},
|
|
||||||
status=adapter_response.status_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
assistant_content = ""
|
|
||||||
if isinstance(adapter_response.data, dict):
|
|
||||||
assistant_content = adapter_response.data.get("content") or ""
|
|
||||||
if not assistant_content and isinstance(adapter_response.data.get("data"), dict):
|
|
||||||
assistant_content = adapter_response.data["data"].get("content") or ""
|
|
||||||
|
|
||||||
assistant_message = Message.objects.create(
|
assistant_message = Message.objects.create(
|
||||||
conversation=conversation,
|
conversation=conversation,
|
||||||
role=Message.ROLE_ASSISTANT,
|
role=Message.ROLE_ASSISTANT,
|
||||||
content=assistant_content,
|
content=assistant_payload.get("content", ""),
|
||||||
raw_response=adapter_response.data if isinstance(adapter_response.data, dict) else {"body": str(adapter_response.data)},
|
raw_response={},
|
||||||
)
|
)
|
||||||
|
assistant_payload["message_id"] = str(assistant_message.uuid)
|
||||||
|
assistant_message.raw_response = assistant_payload
|
||||||
|
assistant_message.save(update_fields=["raw_response"])
|
||||||
|
|
||||||
if not conversation.title:
|
if not conversation.title:
|
||||||
conversation.title = (validated.get("content", "") or assistant_content or "New chat")[:255]
|
conversation.title = (validated.get("content", "") or assistant_payload.get("content", "") or "New chat")[:255]
|
||||||
conversation.save(update_fields=["title", "updated_at"])
|
conversation.save(update_fields=["title", "updated_at"])
|
||||||
else:
|
else:
|
||||||
conversation.save(update_fields=["updated_at"])
|
conversation.save(update_fields=["updated_at"])
|
||||||
|
|
||||||
conversation_uuid = str(conversation.uuid)
|
return Response(
|
||||||
response_data = adapter_response.data
|
{
|
||||||
if isinstance(response_data, dict):
|
"status": "success",
|
||||||
response_data.setdefault("conversation_id", conversation_uuid)
|
"data": assistant_payload,
|
||||||
|
},
|
||||||
data = response_data.get("data")
|
status=response_status_code,
|
||||||
if isinstance(data, dict):
|
)
|
||||||
data.setdefault("conversation_id", conversation_uuid)
|
|
||||||
data.setdefault("user_message_id", str(user_message.uuid))
|
|
||||||
data.setdefault("assistant_message_id", str(assistant_message.uuid))
|
|
||||||
else:
|
|
||||||
response_data.setdefault("user_message_id", str(user_message.uuid))
|
|
||||||
response_data.setdefault("assistant_message_id", str(assistant_message.uuid))
|
|
||||||
else:
|
|
||||||
response_data = {
|
|
||||||
"conversation_id": conversation_uuid,
|
|
||||||
"user_message_id": str(user_message.uuid),
|
|
||||||
"assistant_message_id": str(assistant_message.uuid),
|
|
||||||
"response": response_data,
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response(response_data, status=adapter_response.status_code)
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import ConfigView, RecommendTaskStatusView, RecommendView
|
from .views import ConfigView, RecommendView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("config/", ConfigView.as_view(), name="fertilization-recommendation-config"),
|
path("config/", ConfigView.as_view(), name="fertilization-recommendation-config"),
|
||||||
path("recommend/", RecommendView.as_view(), name="fertilization-recommendation-recommend"),
|
path("recommend/", RecommendView.as_view(), name="fertilization-recommendation-recommend"),
|
||||||
# path("recommend/task/", RecommendTaskCreateView.as_view(), name="fertilization-recommendation-task-create"),
|
|
||||||
path("recommend/<str:task_id>/status/", RecommendTaskStatusView.as_view(), name="fertilization-recommendation-task-status"),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import ConfigView, RecommendTaskCreateView, RecommendTaskStatusView, RecommendView
|
from .views import ConfigView, RecommendView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("config/", ConfigView.as_view(), name="irrigation-recommendation-config"),
|
path("config/", ConfigView.as_view(), name="irrigation-recommendation-config"),
|
||||||
path("recommend/", RecommendView.as_view(), name="irrigation-recommendation-recommend"),
|
path("recommend/", RecommendView.as_view(), name="irrigation-recommendation-recommend"),
|
||||||
path("recommend/task/", RecommendTaskCreateView.as_view(), name="irrigation-recommendation-task-create"),
|
|
||||||
path("recommend/<str:task_id>/status/", RecommendTaskStatusView.as_view(), name="irrigation-recommendation-task-status"),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,29 @@
|
|||||||
{
|
{
|
||||||
"content_type": "text/plain; charset=utf-8",
|
"content": "Here is the recommended plan.",
|
||||||
"body": "سلام، برای بازیابی رطوبت خاک بهتر است آبیاری صبحگاهی را تنظیم کنید."
|
"sections": [
|
||||||
|
{
|
||||||
|
"type": "recommendation",
|
||||||
|
"title": "Irrigation Plan",
|
||||||
|
"icon": "droplet",
|
||||||
|
"frequency": "3 times per week",
|
||||||
|
"amount": "15 liters per plant",
|
||||||
|
"timing": "Early morning",
|
||||||
|
"expandableExplanation": "Loamy soil holds moisture well, so moderate frequency is enough."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"title": "Important Notes",
|
||||||
|
"icon": "leaf",
|
||||||
|
"items": [
|
||||||
|
"Avoid watering at noon",
|
||||||
|
"Check leaf stress every two days"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "warning",
|
||||||
|
"title": "Heat Alert",
|
||||||
|
"icon": "warning",
|
||||||
|
"content": "Increase irrigation if temperature rises above 35°C."
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user