UPDATE
This commit is contained in:
@@ -15,6 +15,7 @@ DB_ROOT_PASSWORD=root
|
||||
# SMS.ir
|
||||
SMS_IR_API_KEY=
|
||||
SMS_IR_LINE_NUMBER=300000000000
|
||||
USE_EXTERNAL_API_MOCK=true
|
||||
|
||||
# External API adapter
|
||||
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",
|
||||
"body": "سلام، برای بازیابی رطوبت خاک بهتر است آبیاری صبحگاهی را تنظیم کنید."
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,40 +1,83 @@
|
||||
"""
|
||||
Static mock data for Farm AI Assistant API.
|
||||
No database, no dynamic values. All responses are fixed JSON.
|
||||
"""
|
||||
|
||||
CHAT_RESPONSE_DATA = {
|
||||
"message_id": "a-1739123456789",
|
||||
"conversation_id": "conv-abc123",
|
||||
"content": "",
|
||||
"message_id": "msg-001",
|
||||
"conversation_id": "conv-123",
|
||||
"content": "Here is the recommended plan.",
|
||||
"sections": [
|
||||
{
|
||||
"type": "recommendation",
|
||||
"title": "Irrigation recommendation",
|
||||
"title": "Irrigation Plan",
|
||||
"icon": "droplet",
|
||||
"frequency": "3 times per week",
|
||||
"amount": "15–20 L per plant",
|
||||
"timing": "Early morning (05:00–07:00)",
|
||||
"expandableExplanation": "Your loamy soil holds moisture well...",
|
||||
"amount": "15 liters per plant",
|
||||
"timing": "Early morning",
|
||||
"expandableExplanation": "Loamy soil holds moisture well, so moderate frequency is enough.",
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"title": "Key points",
|
||||
"title": "Important Notes",
|
||||
"icon": "leaf",
|
||||
"items": [
|
||||
"Avoid midday watering to reduce evaporation",
|
||||
"Drip irrigation preferred for root zone targeting",
|
||||
"Avoid watering at noon",
|
||||
"Check leaf stress every two days",
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "warning",
|
||||
"title": "Weather advisory",
|
||||
"title": "Heat Alert",
|
||||
"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 = {
|
||||
"soilType": "Loamy",
|
||||
"waterEC": "1.2 dS/m",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"info": {
|
||||
"name": "Farm AI Assistant",
|
||||
"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": [
|
||||
{
|
||||
@@ -11,7 +11,7 @@
|
||||
"method": "GET",
|
||||
"header": [{"key": "Content-Type", "value": "application/json"}],
|
||||
"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": [
|
||||
{
|
||||
@@ -23,23 +23,95 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Chat (POST)",
|
||||
"name": "List chats (GET)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"method": "GET",
|
||||
"header": [{"key": "Content-Type", "value": "application/json"}],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"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."
|
||||
"url": "{{baseUrl}}/api/farm-ai-assistant/chats/",
|
||||
"description": "Returns only chat id and message count for the current user."
|
||||
},
|
||||
"response": [
|
||||
{
|
||||
"name": "Success",
|
||||
"status": "OK",
|
||||
"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 .models import Conversation, Message
|
||||
from .models import Message
|
||||
|
||||
|
||||
class ConversationListSerializer(serializers.ModelSerializer):
|
||||
conversation_id = serializers.UUIDField(source="uuid", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Conversation
|
||||
fields = [
|
||||
"conversation_id",
|
||||
"title",
|
||||
"updated_at",
|
||||
]
|
||||
class ChatSectionSerializer(serializers.Serializer):
|
||||
type = serializers.ChoiceField(choices=["text", "list", "recommendation", "warning"])
|
||||
title = serializers.CharField(required=False, allow_blank=True)
|
||||
content = serializers.CharField(required=False, allow_blank=True)
|
||||
items = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
icon = serializers.CharField(required=False, allow_blank=True)
|
||||
frequency = serializers.CharField(required=False, allow_blank=True)
|
||||
amount = serializers.CharField(required=False, allow_blank=True)
|
||||
timing = serializers.CharField(required=False, allow_blank=True)
|
||||
expandableExplanation = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
|
||||
class MessageSerializer(serializers.ModelSerializer):
|
||||
message_id = serializers.UUIDField(source="uuid", read_only=True)
|
||||
conversation_id = serializers.UUIDField(source="conversation.uuid", read_only=True)
|
||||
class ConversationSummarySerializer(serializers.Serializer):
|
||||
id = serializers.UUIDField(source="uuid", read_only=True)
|
||||
message_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = [
|
||||
"message_id",
|
||||
"conversation_id",
|
||||
"role",
|
||||
"content",
|
||||
"images",
|
||||
"raw_response",
|
||||
"created_at",
|
||||
]
|
||||
|
||||
class ConversationCreateSerializer(serializers.Serializer):
|
||||
title = serializers.CharField(required=False, allow_blank=True, max_length=255)
|
||||
farm_context = serializers.JSONField(required=False)
|
||||
|
||||
|
||||
class ChatHistoryMessageSerializer(serializers.Serializer):
|
||||
message_id = serializers.UUIDField(read_only=True)
|
||||
conversation_id = serializers.UUIDField(read_only=True)
|
||||
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):
|
||||
@@ -42,3 +61,10 @@ class ChatPostSerializer(serializers.Serializer):
|
||||
conversation_id = serializers.UUIDField(required=False)
|
||||
title = serializers.CharField(required=False, allow_blank=True, max_length=255)
|
||||
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 .views import ChatListView, ChatMessagesView, ChatView, ContextView
|
||||
from .views import ChatDetailView, ChatListCreateView, ChatMessagesView, ChatView, ContextView
|
||||
|
||||
urlpatterns = [
|
||||
path("context/", ContextView.as_view(), name="farm-ai-assistant-context"),
|
||||
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"),
|
||||
]
|
||||
|
||||
+204
-90
@@ -1,6 +1,9 @@
|
||||
"""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.permissions import IsAuthenticated
|
||||
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 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 .serializers import ChatPostSerializer, ConversationListSerializer, MessageSerializer
|
||||
from .serializers import (
|
||||
ChatPostSerializer,
|
||||
ChatResponseDataSerializer,
|
||||
ConversationCreateSerializer,
|
||||
ConversationDeleteSerializer,
|
||||
ConversationMessagesSerializer,
|
||||
ConversationSummarySerializer,
|
||||
)
|
||||
|
||||
|
||||
class ContextView(APIView):
|
||||
@@ -27,63 +38,194 @@ class ContextView(APIView):
|
||||
)
|
||||
|
||||
|
||||
class ChatListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@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):
|
||||
class ConversationAccessMixin:
|
||||
@staticmethod
|
||||
def _get_conversation(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
|
||||
|
||||
@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(
|
||||
tags=["Farm AI Assistant"],
|
||||
parameters=[
|
||||
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):
|
||||
conversation = self._get_conversation(request, conversation_id)
|
||||
messages = conversation.messages.all()
|
||||
serializer = MessageSerializer(messages, many=True)
|
||||
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
|
||||
serialized_messages = [self._serialize_chat_message(message) for message in messages]
|
||||
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]
|
||||
|
||||
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(
|
||||
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):
|
||||
return ChatListView().get(request)
|
||||
def delete(self, request, conversation_id):
|
||||
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(
|
||||
tags=["Farm AI Assistant"],
|
||||
request=ChatPostSerializer,
|
||||
responses={200: status_response("FarmAiAssistantChatResponse", data=serializers.JSONField())},
|
||||
responses={200: status_response("FarmAiAssistantChatResponse", data=ChatResponseDataSerializer())},
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = ChatPostSerializer(data=request.data)
|
||||
@@ -92,7 +234,7 @@ class ChatView(APIView):
|
||||
validated = serializer.validated_data
|
||||
conversation_id = validated.get("conversation_id")
|
||||
farm_context = validated.get("farm_context")
|
||||
title = validated.get("title", "")
|
||||
title = validated.get("title", "").strip()
|
||||
|
||||
if conversation_id:
|
||||
conversation = self._get_conversation(request, conversation_id)
|
||||
@@ -109,7 +251,7 @@ class ChatView(APIView):
|
||||
else:
|
||||
conversation = Conversation.objects.create(
|
||||
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 {},
|
||||
)
|
||||
|
||||
@@ -118,81 +260,53 @@ class ChatView(APIView):
|
||||
role=Message.ROLE_USER,
|
||||
content=validated.get("content", ""),
|
||||
images=validated.get("images", []),
|
||||
raw_response={},
|
||||
)
|
||||
|
||||
adapter_payload = dict(request.data)
|
||||
adapter_payload["conversation_id"] = str(conversation.uuid)
|
||||
|
||||
try:
|
||||
adapter_response = external_api_request(
|
||||
"ai",
|
||||
"/rag/chat",
|
||||
method="POST",
|
||||
payload=adapter_payload,
|
||||
)
|
||||
if adapter_response.status_code >= 400:
|
||||
return Response(
|
||||
{
|
||||
"status": "error",
|
||||
"data": adapter_response.data,
|
||||
},
|
||||
status=adapter_response.status_code,
|
||||
)
|
||||
assistant_payload = self._extract_assistant_payload(adapter_response.data, conversation.uuid)
|
||||
response_status_code = adapter_response.status_code
|
||||
except ExternalAPIRequestError:
|
||||
assistant_payload = self._build_mock_assistant_payload(conversation.uuid)
|
||||
response_status_code = status.HTTP_200_OK
|
||||
|
||||
if isinstance(adapter_response.data, dict) and "body" in adapter_response.data:
|
||||
assistant_content = adapter_response.data.get("body") or ""
|
||||
assistant_message = Message.objects.create(
|
||||
conversation=conversation,
|
||||
role=Message.ROLE_ASSISTANT,
|
||||
content=assistant_content,
|
||||
raw_response=adapter_response.data,
|
||||
content=assistant_payload.get("content", ""),
|
||||
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:
|
||||
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"])
|
||||
else:
|
||||
conversation.save(update_fields=["updated_at"])
|
||||
|
||||
return Response(
|
||||
{
|
||||
"conversation_id": str(conversation.uuid),
|
||||
"user_message_id": str(user_message.uuid),
|
||||
"assistant_message_id": str(assistant_message.uuid),
|
||||
"content": assistant_content,
|
||||
"content_type": adapter_response.data.get("content_type", "text/plain; charset=utf-8"),
|
||||
"status": "success",
|
||||
"data": assistant_payload,
|
||||
},
|
||||
status=adapter_response.status_code,
|
||||
status=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(
|
||||
conversation=conversation,
|
||||
role=Message.ROLE_ASSISTANT,
|
||||
content=assistant_content,
|
||||
raw_response=adapter_response.data if isinstance(adapter_response.data, dict) else {"body": str(adapter_response.data)},
|
||||
)
|
||||
|
||||
if not conversation.title:
|
||||
conversation.title = (validated.get("content", "") or assistant_content or "New chat")[:255]
|
||||
conversation.save(update_fields=["title", "updated_at"])
|
||||
else:
|
||||
conversation.save(update_fields=["updated_at"])
|
||||
|
||||
conversation_uuid = str(conversation.uuid)
|
||||
response_data = adapter_response.data
|
||||
if isinstance(response_data, dict):
|
||||
response_data.setdefault("conversation_id", conversation_uuid)
|
||||
|
||||
data = response_data.get("data")
|
||||
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 .views import ConfigView, RecommendTaskStatusView, RecommendView
|
||||
from .views import ConfigView, RecommendView
|
||||
|
||||
urlpatterns = [
|
||||
path("config/", ConfigView.as_view(), name="fertilization-recommendation-config"),
|
||||
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 .views import ConfigView, RecommendTaskCreateView, RecommendTaskStatusView, RecommendView
|
||||
from .views import ConfigView, RecommendView
|
||||
|
||||
urlpatterns = [
|
||||
path("config/", ConfigView.as_view(), name="irrigation-recommendation-config"),
|
||||
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",
|
||||
"body": "سلام، برای بازیابی رطوبت خاک بهتر است آبیاری صبحگاهی را تنظیم کنید."
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user