UPDATE
This commit is contained in:
@@ -0,0 +1,651 @@
|
|||||||
|
# راهنمای اتصال Landing Page به Farm AI Assistant و Auth API
|
||||||
|
|
||||||
|
این فایل برای تیم فرانت/لندینگ نوشته شده تا بتوانند از داخل Docker Network به بکاند وصل شوند و APIهای احراز هویت و Farm AI Assistant را مصرف کنند.
|
||||||
|
|
||||||
|
## 1) اتصال از طریق Docker Network
|
||||||
|
|
||||||
|
بکاند در `docker-compose.yaml` روی network خارجی `crop_network` اجرا میشود و سرویس وب آن:
|
||||||
|
|
||||||
|
- service name: `web`
|
||||||
|
- container name: `backend-web`
|
||||||
|
- internal port: `8000`
|
||||||
|
|
||||||
|
### نکته مهم
|
||||||
|
|
||||||
|
اگر فرانت هم داخل Docker اجرا میشود، از داخل کانتینر نباید از `localhost:8000` استفاده کنید، چون `localhost` به همان کانتینر فرانت اشاره میکند، نه به بکاند.
|
||||||
|
|
||||||
|
### آدرس پیشنهادی برای اتصال داخل Docker Network
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://backend-web:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
در بسیاری از موارد `http://web:8000` هم کار میکند، اما چون `container_name` بهصورت صریح `backend-web` تعریف شده، برای اتصال بین دو سرویس روی `crop_network` بهتر است از همین آدرس استفاده شود.
|
||||||
|
|
||||||
|
### اگر network هنوز ساخته نشده است
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create crop_network
|
||||||
|
```
|
||||||
|
|
||||||
|
### نمونه اتصال سرویس فرانت به همان network
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
landing:
|
||||||
|
build: .
|
||||||
|
container_name: landing-web
|
||||||
|
environment:
|
||||||
|
BACKEND_BASE_URL: http://backend-web:8000
|
||||||
|
networks:
|
||||||
|
- crop_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
crop_network:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### پیشنهاد برای env فرانت
|
||||||
|
|
||||||
|
```env
|
||||||
|
BACKEND_BASE_URL=http://backend-web:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### اگر درخواست از خود مرورگر کاربر ارسال میشود
|
||||||
|
|
||||||
|
اگر فرانت در مرورگر API را مستقیم صدا میزند، معمولاً باید از آدرس host-mapped استفاده کنید:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
اما اگر درخواست از سمت سرور فرانت/SSR/Nuxt/Next داخل کانتینر ارسال میشود، از این آدرس استفاده کنید:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://backend-web:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Base URL
|
||||||
|
|
||||||
|
### برای ارتباط container-to-container
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://backend-web:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prefixهای موردنیاز
|
||||||
|
|
||||||
|
- Auth: `/api/auth/`
|
||||||
|
- Farm AI Assistant: `/api/farm-ai-assistant/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) جریان کلی برای Landing Page
|
||||||
|
|
||||||
|
برای چت landing page، ارسال `farm_uuid` الزامی نیست.
|
||||||
|
|
||||||
|
یعنی اگر کاربر داخل landing page پیام بدهد:
|
||||||
|
|
||||||
|
- فقط کافی است کاربر لاگین باشد
|
||||||
|
- `farm_uuid` را ارسال نکنید
|
||||||
|
- سیستم conversation را بهصورت landing conversation با `farm = null` ذخیره میکند
|
||||||
|
- در responseها معمولاً `farm_uuid` برابر `null` خواهد بود
|
||||||
|
|
||||||
|
### فلو پیشنهادی
|
||||||
|
|
||||||
|
1. کاربر با `register` یا `login` توکن بگیرد.
|
||||||
|
2. توکن را در هدر `Authorization: Bearer <token>` بفرستید.
|
||||||
|
3. برای شروع چت landing از `POST /api/farm-ai-assistant/chat/task/` بدون `farm_uuid` استفاده کنید.
|
||||||
|
4. `task_id` و `conversation_id` را از پاسخ نگه دارید.
|
||||||
|
5. با `GET /api/farm-ai-assistant/chat/task/{task_id}/status/` وضعیت را poll کنید.
|
||||||
|
6. برای history از `GET /api/farm-ai-assistant/chats/{conversation_id}/messages/` استفاده کنید.
|
||||||
|
7. برای لیست چتهای landing از `GET /api/farm-ai-assistant/chats/` بدون `farm_uuid` استفاده کنید.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Authentication APIs
|
||||||
|
|
||||||
|
## 4.1) Register
|
||||||
|
|
||||||
|
- Method: `POST`
|
||||||
|
- URL: `/api/auth/register/`
|
||||||
|
- Auth: نیاز ندارد
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "landing_user",
|
||||||
|
"email": "landing@example.com",
|
||||||
|
"phone_number": "09120000000",
|
||||||
|
"password": "secret123",
|
||||||
|
"first_name": "Landing",
|
||||||
|
"last_name": "User"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response 201
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 201,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "landing_user",
|
||||||
|
"email": "landing@example.com",
|
||||||
|
"first_name": "Landing",
|
||||||
|
"last_name": "User",
|
||||||
|
"phone_number": "09120000000"
|
||||||
|
},
|
||||||
|
"token": "<access_token>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### خطاهای رایج
|
||||||
|
|
||||||
|
- `400`: username تکراری
|
||||||
|
- `400`: email تکراری
|
||||||
|
- `400`: phone_number تکراری
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4.2) Login
|
||||||
|
|
||||||
|
- Method: `POST`
|
||||||
|
- URL: `/api/auth/login/`
|
||||||
|
- Auth: نیاز ندارد
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
`identifier` میتواند `username` یا `email` یا `phone_number` باشد.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"identifier": "landing_user",
|
||||||
|
"password": "secret123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response 200
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"username": "landing_user",
|
||||||
|
"email": "landing@example.com",
|
||||||
|
"first_name": "Landing",
|
||||||
|
"last_name": "User",
|
||||||
|
"phone_number": "09120000000"
|
||||||
|
},
|
||||||
|
"token": "<access_token>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### خطای رایج
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 401,
|
||||||
|
"msg": "Invalid credentials."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) هدر احراز هویت برای Farm AI Assistant
|
||||||
|
|
||||||
|
تمام endpointهای Farm AI Assistant نیاز به لاگین دارند:
|
||||||
|
|
||||||
|
```http
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) Farm AI Assistant APIs
|
||||||
|
|
||||||
|
## 6.1) ارسال پیام و ساخت task
|
||||||
|
|
||||||
|
- Method: `POST`
|
||||||
|
- URL: `/api/farm-ai-assistant/chat/task/`
|
||||||
|
- Auth: لازم است
|
||||||
|
- برای landing page: `farm_uuid` را ارسال نکنید
|
||||||
|
|
||||||
|
### Request نمونه برای landing
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "برای شروع کشاورزی چه محصولی مناسبتر است؟",
|
||||||
|
"images": [],
|
||||||
|
"title": "Landing chat",
|
||||||
|
"farm_context": {
|
||||||
|
"source": "landing",
|
||||||
|
"page": "home"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request با ادامه conversation قبلی
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"content": "برای منطقه کمآب چه پیشنهادی داری؟",
|
||||||
|
"images": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### فیلدها
|
||||||
|
|
||||||
|
| فیلد | نوع | اجباری | توضیح |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `farm_uuid` | `uuid` | خیر | برای landing ارسال نشود |
|
||||||
|
| `content` | `string` | اختیاری* | متن پیام |
|
||||||
|
| `images` | `string[]` | اختیاری | لیست URL/base64/path |
|
||||||
|
| `conversation_id` | `uuid` | اختیاری | ادامه چت قبلی |
|
||||||
|
| `title` | `string` | اختیاری | عنوان چت جدید |
|
||||||
|
| `farm_context` | `object` | اختیاری | context اضافی UI |
|
||||||
|
|
||||||
|
`*` حداقل یکی از `content` یا `images` باید ارسال شود.
|
||||||
|
|
||||||
|
### Response 202
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"task_id": "farm-ai-chat-task-123",
|
||||||
|
"status": "PENDING",
|
||||||
|
"status_url": "/api/tasks/farm-ai-chat-task-123/status/",
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"message_id": "5d3f7a8c-9f2e-4d0a-b56f-1f2c2f9c1a22",
|
||||||
|
"farm_uuid": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### خطاهای رایج
|
||||||
|
|
||||||
|
- `400`: اگر نه `content` باشد نه `images`
|
||||||
|
- `404`: اگر `conversation_id` متعلق به کاربر نباشد
|
||||||
|
- `503`: اگر سرویس خارجی AI در دسترس نباشد
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.2) بررسی وضعیت task
|
||||||
|
|
||||||
|
- Method: `GET`
|
||||||
|
- URL: `/api/farm-ai-assistant/chat/task/{task_id}/status/`
|
||||||
|
- Auth: لازم است
|
||||||
|
- برای landing page: بدون `farm_uuid`
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/farm-ai-assistant/chat/task/farm-ai-chat-task-123/status/
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response در حالت pending
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"task_id": "farm-ai-chat-task-123",
|
||||||
|
"status": "PENDING",
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null,
|
||||||
|
"progress": {
|
||||||
|
"message": "Processing request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response در حالت success
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"task_id": "farm-ai-chat-task-123",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null,
|
||||||
|
"result": {
|
||||||
|
"message_id": "9f3f8f61-cc71-4f70-a650-2f4dc6f4e5c2",
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null,
|
||||||
|
"task_id": "farm-ai-chat-task-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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response در حالت failure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"task_id": "farm-ai-chat-task-123",
|
||||||
|
"status": "FAILURE",
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null,
|
||||||
|
"error": "something went wrong"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### پیشنهاد برای polling
|
||||||
|
|
||||||
|
- هر `2` تا `3` ثانیه یک بار status را چک کنید
|
||||||
|
- وقتی `status` برابر `SUCCESS` یا `FAILURE` شد polling را متوقف کنید
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.3) دریافت لیست chatها
|
||||||
|
|
||||||
|
- Method: `GET`
|
||||||
|
- URL: `/api/farm-ai-assistant/chats/`
|
||||||
|
- Auth: لازم است
|
||||||
|
- برای landing page: بدون `farm_uuid`
|
||||||
|
|
||||||
|
### رفتار
|
||||||
|
|
||||||
|
اگر `farm_uuid` ارسال نشود:
|
||||||
|
|
||||||
|
- فقط conversationهای landing همان کاربر برمیگردند
|
||||||
|
- یعنی فقط رکوردهایی که مزرعه ندارند
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/farm-ai-assistant/chats/
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response 200
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null,
|
||||||
|
"message_count": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.4) ساخت conversation خالی
|
||||||
|
|
||||||
|
- Method: `POST`
|
||||||
|
- URL: `/api/farm-ai-assistant/chats/`
|
||||||
|
- Auth: لازم است
|
||||||
|
- برای landing page: بدون `farm_uuid`
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"title": "مشاوره اولیه",
|
||||||
|
"farm_context": {
|
||||||
|
"source": "landing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response 201
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null,
|
||||||
|
"message_count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.5) حذف conversation
|
||||||
|
|
||||||
|
- Method: `DELETE`
|
||||||
|
- URL: `/api/farm-ai-assistant/chats/{conversation_id}/`
|
||||||
|
- Auth: لازم است
|
||||||
|
- برای landing page: بدون `farm_uuid`
|
||||||
|
|
||||||
|
### رفتار
|
||||||
|
|
||||||
|
اگر `farm_uuid` نفرستید، فقط conversationهای landing همان کاربر قابل حذف هستند.
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```http
|
||||||
|
DELETE /api/farm-ai-assistant/chats/4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1/
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response 200
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6.6) دریافت پیامهای یک conversation
|
||||||
|
|
||||||
|
- Method: `GET`
|
||||||
|
- URL: `/api/farm-ai-assistant/chats/{conversation_id}/messages/`
|
||||||
|
- Auth: لازم است
|
||||||
|
- برای landing page: بدون `farm_uuid`
|
||||||
|
|
||||||
|
### رفتار
|
||||||
|
|
||||||
|
اگر `farm_uuid` ارسال نشود، فقط conversationهای landing همان کاربر لود میشوند.
|
||||||
|
|
||||||
|
### Request
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/farm-ai-assistant/chats/4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1/messages/
|
||||||
|
Authorization: Bearer <access_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response 200
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"data": {
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null,
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"message_id": "11111111-1111-1111-1111-111111111111",
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null,
|
||||||
|
"role": "user",
|
||||||
|
"content": "برای شروع کشاورزی چه چیزی پیشنهاد میکنی؟",
|
||||||
|
"sections": [],
|
||||||
|
"images": [],
|
||||||
|
"created_at": "2025-03-27T12:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"message_id": "22222222-2222-2222-2222-222222222222",
|
||||||
|
"conversation_id": "4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1",
|
||||||
|
"farm_uuid": null,
|
||||||
|
"role": "assistant",
|
||||||
|
"content": "Here is the recommended plan.",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"type": "list",
|
||||||
|
"title": "Important Notes",
|
||||||
|
"items": [
|
||||||
|
"Avoid watering at noon",
|
||||||
|
"Check leaf stress every two days"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"images": [],
|
||||||
|
"created_at": "2025-03-27T12:00:05Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) مثال کامل فرانت برای Landing Chat
|
||||||
|
|
||||||
|
## 7.1) Register
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://backend-web:8000/api/auth/register/' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"username": "landing_user",
|
||||||
|
"email": "landing@example.com",
|
||||||
|
"phone_number": "09120000000",
|
||||||
|
"password": "secret123",
|
||||||
|
"first_name": "Landing",
|
||||||
|
"last_name": "User"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.2) Login
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://backend-web:8000/api/auth/login/' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"identifier": "landing_user",
|
||||||
|
"password": "secret123"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.3) Create task
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://backend-web:8000/api/farm-ai-assistant/chat/task/' \
|
||||||
|
-H 'Authorization: Bearer <token>' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"content": "برای شروع کشاورزی چه محصولی مناسب است؟",
|
||||||
|
"farm_context": {
|
||||||
|
"source": "landing",
|
||||||
|
"page": "home"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.4) Poll task status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET 'http://backend-web:8000/api/farm-ai-assistant/chat/task/farm-ai-chat-task-123/status/' \
|
||||||
|
-H 'Authorization: Bearer <token>'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.5) Get chat list
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET 'http://backend-web:8000/api/farm-ai-assistant/chats/' \
|
||||||
|
-H 'Authorization: Bearer <token>'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.6) Get messages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET 'http://backend-web:8000/api/farm-ai-assistant/chats/4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1/messages/' \
|
||||||
|
-H 'Authorization: Bearer <token>'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.7) Delete conversation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE 'http://backend-web:8000/api/farm-ai-assistant/chats/4b9f4d2f-2e5f-4f7a-ae24-6e7fd9c3e0f1/' \
|
||||||
|
-H 'Authorization: Bearer <token>'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) رفتار `farm_uuid` در Landing Page
|
||||||
|
|
||||||
|
برای endpointهای زیر در landing page لازم نیست `farm_uuid` ارسال شود:
|
||||||
|
|
||||||
|
- `POST /api/farm-ai-assistant/chat/task/`
|
||||||
|
- `GET /api/farm-ai-assistant/chat/task/{task_id}/status/`
|
||||||
|
- `GET /api/farm-ai-assistant/chats/`
|
||||||
|
- `POST /api/farm-ai-assistant/chats/`
|
||||||
|
- `DELETE /api/farm-ai-assistant/chats/{conversation_id}/`
|
||||||
|
- `GET /api/farm-ai-assistant/chats/{conversation_id}/messages/`
|
||||||
|
|
||||||
|
### نتیجه این رفتار
|
||||||
|
|
||||||
|
- چت به کاربر وابسته است
|
||||||
|
- چت به مزرعه خاصی وابسته نیست
|
||||||
|
- در response مقدار `farm_uuid` معمولاً `null` است
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9) چیزی که فرانت باید نگه دارد
|
||||||
|
|
||||||
|
بعد از login این موارد را در state یا storage نگه دارید:
|
||||||
|
|
||||||
|
- `token`
|
||||||
|
- `user`
|
||||||
|
|
||||||
|
بعد از `POST /chat/task/` این موارد را نگه دارید:
|
||||||
|
|
||||||
|
- `conversation_id`
|
||||||
|
- `task_id`
|
||||||
|
- `message_id`
|
||||||
|
|
||||||
|
برای ادامه chat:
|
||||||
|
|
||||||
|
- `conversation_id` را در درخواست بعدی دوباره بفرستید
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10) نکات نهایی
|
||||||
|
|
||||||
|
- تمام APIهای Farm AI Assistant به توکن نیاز دارند.
|
||||||
|
- برای landing page، `farm_uuid` را نفرستید.
|
||||||
|
- اگر فرانت داخل Docker است، آدرس بکاند را `http://backend-web:8000` بگذارید.
|
||||||
|
- اگر فرانت از داخل browser مستقیم درخواست میزند، معمولاً `http://localhost:8000` لازم است.
|
||||||
|
- `localhost` داخل کانتینر فرانت، آدرس بکاند نیست.
|
||||||
|
|
||||||
@@ -17,12 +17,12 @@ class ChatSectionSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
class ConversationSummarySerializer(serializers.Serializer):
|
class ConversationSummarySerializer(serializers.Serializer):
|
||||||
id = serializers.UUIDField(source="uuid", read_only=True)
|
id = serializers.UUIDField(source="uuid", read_only=True)
|
||||||
farm_uuid = serializers.UUIDField(source="farm.farm_uuid", read_only=True)
|
farm_uuid = serializers.UUIDField(source="farm.farm_uuid", read_only=True, allow_null=True)
|
||||||
message_count = serializers.IntegerField(read_only=True)
|
message_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class ConversationCreateSerializer(serializers.Serializer):
|
class ConversationCreateSerializer(serializers.Serializer):
|
||||||
farm_uuid = serializers.UUIDField(required=True)
|
farm_uuid = serializers.UUIDField(required=False, allow_null=True)
|
||||||
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)
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class ConversationCreateSerializer(serializers.Serializer):
|
|||||||
class ChatHistoryMessageSerializer(serializers.Serializer):
|
class ChatHistoryMessageSerializer(serializers.Serializer):
|
||||||
message_id = serializers.UUIDField(read_only=True)
|
message_id = serializers.UUIDField(read_only=True)
|
||||||
conversation_id = serializers.UUIDField(read_only=True)
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
farm_uuid = serializers.UUIDField(read_only=True)
|
farm_uuid = serializers.UUIDField(read_only=True, allow_null=True)
|
||||||
role = serializers.ChoiceField(choices=Message.ROLE_CHOICES, read_only=True)
|
role = serializers.ChoiceField(choices=Message.ROLE_CHOICES, read_only=True)
|
||||||
content = serializers.CharField(read_only=True, allow_blank=True)
|
content = serializers.CharField(read_only=True, allow_blank=True)
|
||||||
sections = ChatSectionSerializer(many=True, read_only=True)
|
sections = ChatSectionSerializer(many=True, read_only=True)
|
||||||
@@ -40,21 +40,21 @@ class ChatHistoryMessageSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
class ConversationMessagesSerializer(serializers.Serializer):
|
class ConversationMessagesSerializer(serializers.Serializer):
|
||||||
conversation_id = serializers.UUIDField(read_only=True)
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
farm_uuid = serializers.UUIDField(read_only=True)
|
farm_uuid = serializers.UUIDField(read_only=True, allow_null=True)
|
||||||
messages = ChatHistoryMessageSerializer(many=True, read_only=True)
|
messages = ChatHistoryMessageSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class ChatResponseDataSerializer(serializers.Serializer):
|
class ChatResponseDataSerializer(serializers.Serializer):
|
||||||
message_id = serializers.UUIDField(read_only=True)
|
message_id = serializers.UUIDField(read_only=True)
|
||||||
conversation_id = serializers.UUIDField(read_only=True)
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
farm_uuid = serializers.UUIDField(read_only=True)
|
farm_uuid = serializers.UUIDField(read_only=True, allow_null=True)
|
||||||
content = serializers.CharField(read_only=True, allow_blank=True)
|
content = serializers.CharField(read_only=True, allow_blank=True)
|
||||||
sections = ChatSectionSerializer(many=True, read_only=True)
|
sections = ChatSectionSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class ConversationDeleteSerializer(serializers.Serializer):
|
class ConversationDeleteSerializer(serializers.Serializer):
|
||||||
conversation_id = serializers.UUIDField(read_only=True)
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
farm_uuid = serializers.UUIDField(read_only=True)
|
farm_uuid = serializers.UUIDField(read_only=True, allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
class ChatTaskSubmitDataSerializer(serializers.Serializer):
|
class ChatTaskSubmitDataSerializer(serializers.Serializer):
|
||||||
@@ -63,21 +63,21 @@ class ChatTaskSubmitDataSerializer(serializers.Serializer):
|
|||||||
status_url = serializers.CharField(required=False, allow_blank=True)
|
status_url = serializers.CharField(required=False, allow_blank=True)
|
||||||
conversation_id = serializers.UUIDField(read_only=True)
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
message_id = serializers.UUIDField(read_only=True)
|
message_id = serializers.UUIDField(read_only=True)
|
||||||
farm_uuid = serializers.UUIDField(read_only=True)
|
farm_uuid = serializers.UUIDField(read_only=True, allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
class ChatTaskStatusDataSerializer(serializers.Serializer):
|
class ChatTaskStatusDataSerializer(serializers.Serializer):
|
||||||
task_id = serializers.CharField(required=False, allow_blank=True)
|
task_id = serializers.CharField(required=False, allow_blank=True)
|
||||||
status = serializers.CharField(required=False, allow_blank=True)
|
status = serializers.CharField(required=False, allow_blank=True)
|
||||||
conversation_id = serializers.UUIDField(read_only=True)
|
conversation_id = serializers.UUIDField(read_only=True)
|
||||||
farm_uuid = serializers.UUIDField(read_only=True)
|
farm_uuid = serializers.UUIDField(read_only=True, allow_null=True)
|
||||||
progress = serializers.JSONField(required=False)
|
progress = serializers.JSONField(required=False)
|
||||||
result = serializers.JSONField(required=False)
|
result = serializers.JSONField(required=False)
|
||||||
error = serializers.CharField(required=False, allow_blank=True)
|
error = serializers.CharField(required=False, allow_blank=True)
|
||||||
|
|
||||||
|
|
||||||
class ChatPostSerializer(serializers.Serializer):
|
class ChatPostSerializer(serializers.Serializer):
|
||||||
farm_uuid = serializers.UUIDField(required=True)
|
farm_uuid = serializers.UUIDField(required=False, allow_null=True)
|
||||||
content = serializers.CharField(required=False, allow_blank=True, default="")
|
content = serializers.CharField(required=False, allow_blank=True, default="")
|
||||||
images = serializers.ListField(
|
images = serializers.ListField(
|
||||||
child=serializers.CharField(),
|
child=serializers.CharField(),
|
||||||
|
|||||||
+171
-25
@@ -5,11 +5,18 @@ from rest_framework.test import APIRequestFactory, force_authenticate
|
|||||||
from farm_hub.models import FarmHub, FarmType
|
from farm_hub.models import FarmHub, FarmType
|
||||||
|
|
||||||
from .models import Conversation, Message
|
from .models import Conversation, Message
|
||||||
from .views import ChatTaskStatusView
|
from .views import (
|
||||||
|
ChatDetailView,
|
||||||
|
ChatListCreateView,
|
||||||
|
ChatMessagesView,
|
||||||
|
ChatTaskCreateView,
|
||||||
|
ChatTaskStatusView,
|
||||||
|
ContextView,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@override_settings(USE_EXTERNAL_API_MOCK=True)
|
@override_settings(USE_EXTERNAL_API_MOCK=True)
|
||||||
class ChatTaskStatusViewTests(TestCase):
|
class FarmAiAssistantOptionalFarmUuidTests(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.factory = APIRequestFactory()
|
self.factory = APIRequestFactory()
|
||||||
self.user = get_user_model().objects.create_user(
|
self.user = get_user_model().objects.create_user(
|
||||||
@@ -24,14 +31,94 @@ class ChatTaskStatusViewTests(TestCase):
|
|||||||
farm_type=self.farm_type,
|
farm_type=self.farm_type,
|
||||||
name="Farm 1",
|
name="Farm 1",
|
||||||
)
|
)
|
||||||
self.conversation = Conversation.objects.create(
|
|
||||||
|
def test_context_allows_missing_farm_uuid(self):
|
||||||
|
request = self.factory.get("/api/farm-ai-assistant/context/")
|
||||||
|
force_authenticate(request, user=self.user)
|
||||||
|
|
||||||
|
response = ContextView.as_view()(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["status"], "success")
|
||||||
|
self.assertIsNone(response.data["data"]["farm_uuid"])
|
||||||
|
|
||||||
|
def test_chat_task_create_allows_missing_farm_uuid_for_landing_chat(self):
|
||||||
|
request = self.factory.post(
|
||||||
|
"/api/farm-ai-assistant/chat/task/",
|
||||||
|
{"content": "Give me a landing page recommendation"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
force_authenticate(request, user=self.user)
|
||||||
|
|
||||||
|
response = ChatTaskCreateView.as_view()(request)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 202)
|
||||||
|
self.assertEqual(response.data["status"], "success")
|
||||||
|
self.assertEqual(response.data["data"]["task_id"], "farm-ai-chat-task-123")
|
||||||
|
self.assertIsNone(response.data["data"]["farm_uuid"])
|
||||||
|
|
||||||
|
conversation = Conversation.objects.get(uuid=response.data["data"]["conversation_id"])
|
||||||
|
self.assertIsNone(conversation.farm)
|
||||||
|
self.assertEqual(conversation.owner_id, self.user.id)
|
||||||
|
|
||||||
|
user_message = conversation.messages.get(role=Message.ROLE_USER)
|
||||||
|
self.assertIsNone(user_message.farm)
|
||||||
|
self.assertIsNone(user_message.raw_response["farm_uuid"])
|
||||||
|
|
||||||
|
def test_status_success_without_farm_uuid_persists_assistant_message(self):
|
||||||
|
conversation = Conversation.objects.create(
|
||||||
owner=self.user,
|
owner=self.user,
|
||||||
farm=self.farm,
|
farm=None,
|
||||||
title="Irrigation chat",
|
title="Landing chat",
|
||||||
farm_context={},
|
farm_context={},
|
||||||
)
|
)
|
||||||
self.user_message = Message.objects.create(
|
Message.objects.create(
|
||||||
conversation=self.conversation,
|
conversation=conversation,
|
||||||
|
farm=None,
|
||||||
|
role=Message.ROLE_USER,
|
||||||
|
content="What should I plant?",
|
||||||
|
raw_response={
|
||||||
|
"task_id": "farm-ai-chat-task-123",
|
||||||
|
"status": "PENDING",
|
||||||
|
"status_url": "/api/tasks/farm-ai-chat-task-123/status/",
|
||||||
|
"farm_uuid": None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self.factory.get("/api/farm-ai-assistant/chat/task/farm-ai-chat-task-123/status/")
|
||||||
|
force_authenticate(request, user=self.user)
|
||||||
|
|
||||||
|
response = ChatTaskStatusView.as_view()(request, task_id="farm-ai-chat-task-123")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.data["status"], "success")
|
||||||
|
self.assertEqual(response.data["data"]["task_id"], "farm-ai-chat-task-123")
|
||||||
|
self.assertEqual(response.data["data"]["status"], "SUCCESS")
|
||||||
|
self.assertEqual(response.data["data"]["conversation_id"], str(conversation.uuid))
|
||||||
|
self.assertIsNone(response.data["data"]["farm_uuid"])
|
||||||
|
self.assertEqual(response.data["data"]["result"]["content"], "Here is the recommended plan.")
|
||||||
|
self.assertEqual(response.data["data"]["result"]["task_id"], "farm-ai-chat-task-123")
|
||||||
|
|
||||||
|
assistant_message = (
|
||||||
|
conversation.messages.filter(role=Message.ROLE_ASSISTANT)
|
||||||
|
.order_by("-created_at")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(assistant_message)
|
||||||
|
self.assertIsNone(assistant_message.farm)
|
||||||
|
self.assertEqual(assistant_message.content, "Here is the recommended plan.")
|
||||||
|
self.assertIsNone(assistant_message.raw_response["farm_uuid"])
|
||||||
|
self.assertEqual(assistant_message.raw_response["task_id"], "farm-ai-chat-task-123")
|
||||||
|
|
||||||
|
def test_status_success_with_farm_uuid_still_works_for_farm_chat(self):
|
||||||
|
conversation = Conversation.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
farm=self.farm,
|
||||||
|
title="Farm chat",
|
||||||
|
farm_context={},
|
||||||
|
)
|
||||||
|
Message.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
farm=self.farm,
|
farm=self.farm,
|
||||||
role=Message.ROLE_USER,
|
role=Message.ROLE_USER,
|
||||||
content="What is the best irrigation plan?",
|
content="What is the best irrigation plan?",
|
||||||
@@ -43,7 +130,6 @@ class ChatTaskStatusViewTests(TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_status_success_uses_chat_mock_result_and_persists_assistant_message(self):
|
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
f"/api/farm-ai-assistant/chat/task/farm-ai-chat-task-123/status/?farm_uuid={self.farm.farm_uuid}"
|
f"/api/farm-ai-assistant/chat/task/farm-ai-chat-task-123/status/?farm_uuid={self.farm.farm_uuid}"
|
||||||
)
|
)
|
||||||
@@ -52,23 +138,83 @@ class ChatTaskStatusViewTests(TestCase):
|
|||||||
response = ChatTaskStatusView.as_view()(request, task_id="farm-ai-chat-task-123")
|
response = ChatTaskStatusView.as_view()(request, task_id="farm-ai-chat-task-123")
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(response.data["status"], "success")
|
self.assertEqual(response.data["data"]["conversation_id"], str(conversation.uuid))
|
||||||
self.assertEqual(response.data["data"]["task_id"], "farm-ai-chat-task-123")
|
|
||||||
self.assertEqual(response.data["data"]["status"], "SUCCESS")
|
|
||||||
self.assertEqual(response.data["data"]["conversation_id"], str(self.conversation.uuid))
|
|
||||||
self.assertEqual(response.data["data"]["farm_uuid"], str(self.farm.farm_uuid))
|
self.assertEqual(response.data["data"]["farm_uuid"], str(self.farm.farm_uuid))
|
||||||
self.assertEqual(response.data["data"]["result"]["content"], "Here is the recommended plan.")
|
|
||||||
self.assertEqual(len(response.data["data"]["result"]["sections"]), 3)
|
|
||||||
self.assertEqual(response.data["data"]["result"]["task_id"], "farm-ai-chat-task-123")
|
|
||||||
|
|
||||||
assistant_message = (
|
def test_chat_list_create_messages_and_delete_work_without_farm_uuid(self):
|
||||||
self.conversation.messages.filter(role=Message.ROLE_ASSISTANT)
|
landing_conversation = Conversation.objects.create(
|
||||||
.order_by("-created_at")
|
owner=self.user,
|
||||||
.first()
|
farm=None,
|
||||||
|
title="Landing chat",
|
||||||
|
farm_context={"source": "landing"},
|
||||||
)
|
)
|
||||||
self.assertIsNotNone(assistant_message)
|
Message.objects.create(
|
||||||
self.assertEqual(assistant_message.farm_id, self.farm.id)
|
conversation=landing_conversation,
|
||||||
self.assertEqual(assistant_message.content, "Here is the recommended plan.")
|
farm=None,
|
||||||
self.assertEqual(assistant_message.raw_response["task_id"], "farm-ai-chat-task-123")
|
role=Message.ROLE_USER,
|
||||||
self.assertEqual(assistant_message.raw_response["farm_uuid"], str(self.farm.farm_uuid))
|
content="Hello from landing",
|
||||||
self.assertEqual(len(assistant_message.raw_response["sections"]), 3)
|
raw_response={"farm_uuid": None},
|
||||||
|
)
|
||||||
|
farm_conversation = Conversation.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
farm=self.farm,
|
||||||
|
title="Farm chat",
|
||||||
|
farm_context={},
|
||||||
|
)
|
||||||
|
Message.objects.create(
|
||||||
|
conversation=farm_conversation,
|
||||||
|
farm=self.farm,
|
||||||
|
role=Message.ROLE_USER,
|
||||||
|
content="Hello from farm",
|
||||||
|
raw_response={"farm_uuid": str(self.farm.farm_uuid)},
|
||||||
|
)
|
||||||
|
|
||||||
|
list_request = self.factory.get("/api/farm-ai-assistant/chats/")
|
||||||
|
force_authenticate(list_request, user=self.user)
|
||||||
|
list_response = ChatListCreateView.as_view()(list_request)
|
||||||
|
|
||||||
|
self.assertEqual(list_response.status_code, 200)
|
||||||
|
self.assertEqual(len(list_response.data["data"]), 1)
|
||||||
|
self.assertEqual(list_response.data["data"][0]["id"], str(landing_conversation.uuid))
|
||||||
|
self.assertIsNone(list_response.data["data"][0]["farm_uuid"])
|
||||||
|
|
||||||
|
create_request = self.factory.post(
|
||||||
|
"/api/farm-ai-assistant/chats/",
|
||||||
|
{"title": "New landing conversation"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
force_authenticate(create_request, user=self.user)
|
||||||
|
create_response = ChatListCreateView.as_view()(create_request)
|
||||||
|
|
||||||
|
self.assertEqual(create_response.status_code, 201)
|
||||||
|
self.assertIsNone(create_response.data["data"]["farm_uuid"])
|
||||||
|
|
||||||
|
created_conversation = Conversation.objects.get(uuid=create_response.data["data"]["id"])
|
||||||
|
self.assertIsNone(created_conversation.farm)
|
||||||
|
|
||||||
|
messages_request = self.factory.get(
|
||||||
|
f"/api/farm-ai-assistant/chats/{landing_conversation.uuid}/messages/"
|
||||||
|
)
|
||||||
|
force_authenticate(messages_request, user=self.user)
|
||||||
|
messages_response = ChatMessagesView.as_view()(
|
||||||
|
messages_request,
|
||||||
|
conversation_id=landing_conversation.uuid,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(messages_response.status_code, 200)
|
||||||
|
self.assertEqual(messages_response.data["data"]["conversation_id"], str(landing_conversation.uuid))
|
||||||
|
self.assertIsNone(messages_response.data["data"]["farm_uuid"])
|
||||||
|
self.assertEqual(len(messages_response.data["data"]["messages"]), 1)
|
||||||
|
self.assertIsNone(messages_response.data["data"]["messages"][0]["farm_uuid"])
|
||||||
|
|
||||||
|
delete_request = self.factory.delete(f"/api/farm-ai-assistant/chats/{landing_conversation.uuid}/")
|
||||||
|
force_authenticate(delete_request, user=self.user)
|
||||||
|
delete_response = ChatDetailView.as_view()(
|
||||||
|
delete_request,
|
||||||
|
conversation_id=landing_conversation.uuid,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(delete_response.status_code, 200)
|
||||||
|
self.assertEqual(delete_response.data["data"]["conversation_id"], str(landing_conversation.uuid))
|
||||||
|
self.assertIsNone(delete_response.data["data"]["farm_uuid"])
|
||||||
|
self.assertFalse(Conversation.objects.filter(uuid=landing_conversation.uuid).exists())
|
||||||
|
|||||||
+61
-36
@@ -34,11 +34,21 @@ class FarmAccessMixin:
|
|||||||
def _get_farm(request, farm_uuid):
|
def _get_farm(request, farm_uuid):
|
||||||
if not farm_uuid:
|
if not farm_uuid:
|
||||||
raise serializers.ValidationError({"farm_uuid": ["This field is required."]})
|
raise serializers.ValidationError({"farm_uuid": ["This field is required."]})
|
||||||
|
return FarmAccessMixin._get_optional_farm(request, farm_uuid)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_optional_farm(request, farm_uuid):
|
||||||
|
if not farm_uuid:
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
return FarmHub.objects.get(farm_uuid=farm_uuid, owner=request.user)
|
return FarmHub.objects.get(farm_uuid=farm_uuid, owner=request.user)
|
||||||
except FarmHub.DoesNotExist as exc:
|
except FarmHub.DoesNotExist as exc:
|
||||||
raise Http404("Farm not found") from exc
|
raise Http404("Farm not found") from exc
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _farm_uuid_or_none(farm):
|
||||||
|
return str(farm.farm_uuid) if farm else None
|
||||||
|
|
||||||
|
|
||||||
class ContextView(FarmAccessMixin, APIView):
|
class ContextView(FarmAccessMixin, APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
@@ -46,14 +56,14 @@ class ContextView(FarmAccessMixin, APIView):
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Farm AI Assistant"],
|
tags=["Farm AI Assistant"],
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"),
|
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False, default="11111111-1111-1111-1111-111111111111"),
|
||||||
],
|
],
|
||||||
responses={200: status_response("FarmAiAssistantContextResponse", data=serializers.JSONField())},
|
responses={200: status_response("FarmAiAssistantContextResponse", data=serializers.JSONField())},
|
||||||
)
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
farm = self._get_farm(request, request.query_params.get("farm_uuid"))
|
farm = self._get_optional_farm(request, request.query_params.get("farm_uuid"))
|
||||||
data = deepcopy(CONTEXT_RESPONSE_DATA)
|
data = deepcopy(CONTEXT_RESPONSE_DATA)
|
||||||
data["farm_uuid"] = str(farm.farm_uuid)
|
data["farm_uuid"] = self._farm_uuid_or_none(farm)
|
||||||
return Response(
|
return Response(
|
||||||
{"status": "success", "data": data},
|
{"status": "success", "data": data},
|
||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
@@ -66,6 +76,8 @@ class ConversationAccessMixin(FarmAccessMixin):
|
|||||||
filters = {"uuid": conversation_id, "owner": request.user}
|
filters = {"uuid": conversation_id, "owner": request.user}
|
||||||
if farm_uuid:
|
if farm_uuid:
|
||||||
filters["farm__farm_uuid"] = farm_uuid
|
filters["farm__farm_uuid"] = farm_uuid
|
||||||
|
else:
|
||||||
|
filters["farm__isnull"] = True
|
||||||
try:
|
try:
|
||||||
return Conversation.objects.select_related("farm").get(**filters)
|
return Conversation.objects.select_related("farm").get(**filters)
|
||||||
except Conversation.DoesNotExist as exc:
|
except Conversation.DoesNotExist as exc:
|
||||||
@@ -110,17 +122,21 @@ class ConversationAccessMixin(FarmAccessMixin):
|
|||||||
def _build_mock_assistant_payload(self, conversation):
|
def _build_mock_assistant_payload(self, conversation):
|
||||||
payload = deepcopy(CHAT_RESPONSE_DATA)
|
payload = deepcopy(CHAT_RESPONSE_DATA)
|
||||||
payload["conversation_id"] = str(conversation.uuid)
|
payload["conversation_id"] = str(conversation.uuid)
|
||||||
payload["farm_uuid"] = str(conversation.farm.farm_uuid)
|
payload["farm_uuid"] = self._farm_uuid_or_none(conversation.farm)
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
def _get_or_create_conversation(self, request, validated):
|
def _get_or_create_conversation(self, request, validated):
|
||||||
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", "").strip()
|
title = validated.get("title", "").strip()
|
||||||
farm = self._get_farm(request, validated.get("farm_uuid"))
|
farm = self._get_optional_farm(request, validated.get("farm_uuid"))
|
||||||
|
|
||||||
if conversation_id:
|
if conversation_id:
|
||||||
conversation = self._get_conversation(request, conversation_id, farm.farm_uuid)
|
conversation = self._get_conversation(
|
||||||
|
request,
|
||||||
|
conversation_id,
|
||||||
|
farm.farm_uuid if farm else None,
|
||||||
|
)
|
||||||
updated_fields = []
|
updated_fields = []
|
||||||
if farm_context is not None:
|
if farm_context is not None:
|
||||||
conversation.farm_context = farm_context
|
conversation.farm_context = farm_context
|
||||||
@@ -143,13 +159,14 @@ class ConversationAccessMixin(FarmAccessMixin):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_adapter_payload(request, validated, conversation):
|
def _build_adapter_payload(request, validated, conversation):
|
||||||
payload = {
|
payload = {
|
||||||
"farm_uuid": str(conversation.farm.farm_uuid),
|
|
||||||
"content": validated.get("content", ""),
|
"content": validated.get("content", ""),
|
||||||
"query": validated.get("content", ""),
|
"query": validated.get("content", ""),
|
||||||
"images": validated.get("images", []),
|
"images": validated.get("images", []),
|
||||||
"conversation_id": str(conversation.uuid),
|
"conversation_id": str(conversation.uuid),
|
||||||
"user_id": request.user.id,
|
"user_id": request.user.id,
|
||||||
}
|
}
|
||||||
|
if conversation.farm:
|
||||||
|
payload["farm_uuid"] = str(conversation.farm.farm_uuid)
|
||||||
if "farm_context" in validated:
|
if "farm_context" in validated:
|
||||||
payload["farm_context"] = validated.get("farm_context") or {}
|
payload["farm_context"] = validated.get("farm_context") or {}
|
||||||
if "title" in validated:
|
if "title" in validated:
|
||||||
@@ -177,7 +194,7 @@ class ConversationAccessMixin(FarmAccessMixin):
|
|||||||
return {
|
return {
|
||||||
"message_id": "",
|
"message_id": "",
|
||||||
"conversation_id": str(conversation.uuid),
|
"conversation_id": str(conversation.uuid),
|
||||||
"farm_uuid": str(conversation.farm.farm_uuid),
|
"farm_uuid": self._farm_uuid_or_none(conversation.farm),
|
||||||
"content": content,
|
"content": content,
|
||||||
"sections": sections,
|
"sections": sections,
|
||||||
}
|
}
|
||||||
@@ -197,7 +214,7 @@ class ConversationAccessMixin(FarmAccessMixin):
|
|||||||
"status_url": str(payload_source.get("status_url") or ""),
|
"status_url": str(payload_source.get("status_url") or ""),
|
||||||
"conversation_id": str(conversation.uuid),
|
"conversation_id": str(conversation.uuid),
|
||||||
"message_id": str(message_id),
|
"message_id": str(message_id),
|
||||||
"farm_uuid": str(conversation.farm.farm_uuid),
|
"farm_uuid": ConversationAccessMixin._farm_uuid_or_none(conversation.farm),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _extract_task_status_payload(self, adapter_data, task_id, conversation=None, farm_uuid=None):
|
def _extract_task_status_payload(self, adapter_data, task_id, conversation=None, farm_uuid=None):
|
||||||
@@ -214,8 +231,8 @@ class ConversationAccessMixin(FarmAccessMixin):
|
|||||||
}
|
}
|
||||||
if conversation:
|
if conversation:
|
||||||
task_status_payload["conversation_id"] = str(conversation.uuid)
|
task_status_payload["conversation_id"] = str(conversation.uuid)
|
||||||
task_status_payload["farm_uuid"] = str(conversation.farm.farm_uuid)
|
task_status_payload["farm_uuid"] = self._farm_uuid_or_none(conversation.farm)
|
||||||
elif farm_uuid:
|
elif farm_uuid is not None:
|
||||||
task_status_payload["farm_uuid"] = str(farm_uuid)
|
task_status_payload["farm_uuid"] = str(farm_uuid)
|
||||||
|
|
||||||
progress = payload_source.get("progress")
|
progress = payload_source.get("progress")
|
||||||
@@ -263,7 +280,7 @@ class ConversationAccessMixin(FarmAccessMixin):
|
|||||||
return {
|
return {
|
||||||
"message_id": str(message.uuid),
|
"message_id": str(message.uuid),
|
||||||
"conversation_id": str(message.conversation.uuid),
|
"conversation_id": str(message.conversation.uuid),
|
||||||
"farm_uuid": str(message.farm.farm_uuid),
|
"farm_uuid": ConversationAccessMixin._farm_uuid_or_none(message.farm),
|
||||||
"role": message.role,
|
"role": message.role,
|
||||||
"content": message.content,
|
"content": message.content,
|
||||||
"sections": ConversationAccessMixin._normalize_sections(sections),
|
"sections": ConversationAccessMixin._normalize_sections(sections),
|
||||||
@@ -273,14 +290,18 @@ class ConversationAccessMixin(FarmAccessMixin):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_user_message_for_task(request, task_id, farm_uuid):
|
def _find_user_message_for_task(request, task_id, farm_uuid):
|
||||||
|
filters = {
|
||||||
|
"conversation__owner": request.user,
|
||||||
|
"role": Message.ROLE_USER,
|
||||||
|
"raw_response__task_id": task_id,
|
||||||
|
}
|
||||||
|
if farm_uuid:
|
||||||
|
filters["farm__farm_uuid"] = farm_uuid
|
||||||
|
else:
|
||||||
|
filters["farm__isnull"] = True
|
||||||
return (
|
return (
|
||||||
Message.objects.select_related("conversation", "farm")
|
Message.objects.select_related("conversation", "farm")
|
||||||
.filter(
|
.filter(**filters)
|
||||||
conversation__owner=request.user,
|
|
||||||
farm__farm_uuid=farm_uuid,
|
|
||||||
role=Message.ROLE_USER,
|
|
||||||
raw_response__task_id=task_id,
|
|
||||||
)
|
|
||||||
.order_by("-created_at")
|
.order_by("-created_at")
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
@@ -329,12 +350,12 @@ class ChatListCreateView(ConversationAccessMixin, APIView):
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Farm AI Assistant"],
|
tags=["Farm AI Assistant"],
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"),
|
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False, default="11111111-1111-1111-1111-111111111111"),
|
||||||
],
|
],
|
||||||
responses={200: status_response("FarmAiAssistantConversationListResponse", data=ConversationSummarySerializer(many=True))},
|
responses={200: status_response("FarmAiAssistantConversationListResponse", data=ConversationSummarySerializer(many=True))},
|
||||||
)
|
)
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
farm = self._get_farm(request, request.query_params.get("farm_uuid"))
|
farm = self._get_optional_farm(request, request.query_params.get("farm_uuid"))
|
||||||
conversations = (
|
conversations = (
|
||||||
Conversation.objects.filter(owner=request.user, farm=farm)
|
Conversation.objects.filter(owner=request.user, farm=farm)
|
||||||
.annotate(message_count=Count("messages"))
|
.annotate(message_count=Count("messages"))
|
||||||
@@ -353,7 +374,7 @@ class ChatListCreateView(ConversationAccessMixin, APIView):
|
|||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
validated = serializer.validated_data
|
validated = serializer.validated_data
|
||||||
farm = self._get_farm(request, validated.get("farm_uuid"))
|
farm = self._get_optional_farm(request, validated.get("farm_uuid"))
|
||||||
conversation = Conversation.objects.create(
|
conversation = Conversation.objects.create(
|
||||||
owner=request.user,
|
owner=request.user,
|
||||||
farm=farm,
|
farm=farm,
|
||||||
@@ -378,13 +399,13 @@ class ChatMessagesView(ConversationAccessMixin, APIView):
|
|||||||
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),
|
||||||
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"),
|
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False, default="11111111-1111-1111-1111-111111111111"),
|
||||||
],
|
],
|
||||||
responses={200: status_response("FarmAiAssistantMessageListResponse", data=ConversationMessagesSerializer())},
|
responses={200: status_response("FarmAiAssistantMessageListResponse", data=ConversationMessagesSerializer())},
|
||||||
)
|
)
|
||||||
def get(self, request, conversation_id):
|
def get(self, request, conversation_id):
|
||||||
farm = self._get_farm(request, request.query_params.get("farm_uuid"))
|
farm = self._get_optional_farm(request, request.query_params.get("farm_uuid"))
|
||||||
conversation = self._get_conversation(request, conversation_id, farm.farm_uuid)
|
conversation = self._get_conversation(request, conversation_id, farm.farm_uuid if farm else None)
|
||||||
messages = conversation.messages.select_related("farm").all()
|
messages = conversation.messages.select_related("farm").all()
|
||||||
serialized_messages = [self._serialize_chat_message(message) for message in messages]
|
serialized_messages = [self._serialize_chat_message(message) for message in messages]
|
||||||
return Response(
|
return Response(
|
||||||
@@ -392,7 +413,7 @@ class ChatMessagesView(ConversationAccessMixin, APIView):
|
|||||||
"status": "success",
|
"status": "success",
|
||||||
"data": {
|
"data": {
|
||||||
"conversation_id": str(conversation.uuid),
|
"conversation_id": str(conversation.uuid),
|
||||||
"farm_uuid": str(farm.farm_uuid),
|
"farm_uuid": self._farm_uuid_or_none(farm),
|
||||||
"messages": serialized_messages,
|
"messages": serialized_messages,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -407,15 +428,15 @@ class ChatDetailView(ConversationAccessMixin, APIView):
|
|||||||
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),
|
||||||
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"),
|
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False, default="11111111-1111-1111-1111-111111111111"),
|
||||||
],
|
],
|
||||||
responses={200: status_response("FarmAiAssistantConversationDeleteResponse", data=ConversationDeleteSerializer())},
|
responses={200: status_response("FarmAiAssistantConversationDeleteResponse", data=ConversationDeleteSerializer())},
|
||||||
)
|
)
|
||||||
def delete(self, request, conversation_id):
|
def delete(self, request, conversation_id):
|
||||||
farm = self._get_farm(request, request.query_params.get("farm_uuid"))
|
farm = self._get_optional_farm(request, request.query_params.get("farm_uuid"))
|
||||||
conversation = self._get_conversation(request, conversation_id, farm.farm_uuid)
|
conversation = self._get_conversation(request, conversation_id, farm.farm_uuid if farm else None)
|
||||||
deleted_conversation_id = str(conversation.uuid)
|
deleted_conversation_id = str(conversation.uuid)
|
||||||
deleted_farm_uuid = str(conversation.farm.farm_uuid)
|
deleted_farm_uuid = self._farm_uuid_or_none(conversation.farm)
|
||||||
conversation.delete()
|
conversation.delete()
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -450,7 +471,7 @@ class ChatView(ConversationAccessMixin, 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={"farm_uuid": str(conversation.farm.farm_uuid)},
|
raw_response={"farm_uuid": self._farm_uuid_or_none(conversation.farm)},
|
||||||
)
|
)
|
||||||
|
|
||||||
adapter_payload = self._build_adapter_payload(request, validated, conversation)
|
adapter_payload = self._build_adapter_payload(request, validated, conversation)
|
||||||
@@ -522,7 +543,7 @@ class ChatTaskCreateView(ConversationAccessMixin, 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={"farm_uuid": str(conversation.farm.farm_uuid)},
|
raw_response={"farm_uuid": self._farm_uuid_or_none(conversation.farm)},
|
||||||
)
|
)
|
||||||
|
|
||||||
adapter_payload = self._build_adapter_payload(request, validated, conversation)
|
adapter_payload = self._build_adapter_payload(request, validated, conversation)
|
||||||
@@ -578,18 +599,21 @@ class ChatTaskStatusView(ConversationAccessMixin, APIView):
|
|||||||
tags=["Farm AI Assistant"],
|
tags=["Farm AI Assistant"],
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(name="task_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH),
|
OpenApiParameter(name="task_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH),
|
||||||
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=True, default="11111111-1111-1111-1111-111111111111"),
|
OpenApiParameter(name="farm_uuid", type=OpenApiTypes.UUID, location=OpenApiParameter.QUERY, required=False, default="11111111-1111-1111-1111-111111111111"),
|
||||||
],
|
],
|
||||||
responses={200: status_response("FarmAiAssistantChatTaskStatusResponse", data=ChatTaskStatusDataSerializer())},
|
responses={200: status_response("FarmAiAssistantChatTaskStatusResponse", data=ChatTaskStatusDataSerializer())},
|
||||||
)
|
)
|
||||||
def get(self, request, task_id):
|
def get(self, request, task_id):
|
||||||
farm = self._get_farm(request, request.query_params.get("farm_uuid"))
|
farm = self._get_optional_farm(request, request.query_params.get("farm_uuid"))
|
||||||
try:
|
try:
|
||||||
|
query = {}
|
||||||
|
if farm:
|
||||||
|
query["farm_uuid"] = str(farm.farm_uuid)
|
||||||
adapter_response = external_api_request(
|
adapter_response = external_api_request(
|
||||||
"ai",
|
"ai",
|
||||||
f"/tasks/{task_id}/status",
|
f"/tasks/{task_id}/status",
|
||||||
method="GET",
|
method="GET",
|
||||||
query={"farm_uuid": str(farm.farm_uuid)},
|
query=query,
|
||||||
)
|
)
|
||||||
except ExternalAPIRequestError:
|
except ExternalAPIRequestError:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -611,13 +635,14 @@ class ChatTaskStatusView(ConversationAccessMixin, APIView):
|
|||||||
status=adapter_response.status_code,
|
status=adapter_response.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
user_message = self._find_user_message_for_task(request, task_id, farm.farm_uuid)
|
farm_uuid = farm.farm_uuid if farm else None
|
||||||
|
user_message = self._find_user_message_for_task(request, task_id, farm_uuid)
|
||||||
conversation = user_message.conversation if user_message else None
|
conversation = user_message.conversation if user_message else None
|
||||||
task_status_payload = self._extract_task_status_payload(
|
task_status_payload = self._extract_task_status_payload(
|
||||||
adapter_response.data,
|
adapter_response.data,
|
||||||
task_id,
|
task_id,
|
||||||
conversation=conversation,
|
conversation=conversation,
|
||||||
farm_uuid=farm.farm_uuid,
|
farm_uuid=farm_uuid,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self._extract_structured_task_result(adapter_response.data)
|
result = self._extract_structured_task_result(adapter_response.data)
|
||||||
|
|||||||
Reference in New Issue
Block a user