2026-02-27 19:44:49 +03:30
|
|
|
"""
|
|
|
|
|
ویوهای RAG — چت با استریم
|
|
|
|
|
"""
|
2026-04-25 17:22:41 +03:30
|
|
|
import json
|
2026-03-25 01:56:41 +03:30
|
|
|
import logging
|
|
|
|
|
|
2026-02-27 19:44:49 +03:30
|
|
|
from django.http import StreamingHttpResponse
|
2026-03-25 01:56:41 +03:30
|
|
|
from drf_spectacular.types import OpenApiTypes
|
2026-03-19 22:54:29 +03:30
|
|
|
from drf_spectacular.utils import (
|
|
|
|
|
OpenApiExample,
|
|
|
|
|
OpenApiResponse,
|
|
|
|
|
extend_schema,
|
|
|
|
|
inline_serializer,
|
|
|
|
|
)
|
2026-02-27 19:44:49 +03:30
|
|
|
from rest_framework import status
|
2026-03-19 22:54:29 +03:30
|
|
|
from rest_framework import serializers as drf_serializers
|
2026-04-25 17:22:41 +03:30
|
|
|
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
2026-02-27 19:44:49 +03:30
|
|
|
from rest_framework.request import Request
|
|
|
|
|
from rest_framework.response import Response
|
|
|
|
|
from rest_framework.views import APIView
|
|
|
|
|
|
2026-03-25 01:56:41 +03:30
|
|
|
from config.openapi import (
|
|
|
|
|
build_envelope_serializer,
|
|
|
|
|
build_message_response_serializer,
|
|
|
|
|
build_response,
|
|
|
|
|
)
|
2026-04-25 17:22:41 +03:30
|
|
|
from .chat import chat_rag_stream, encode_uploaded_image
|
2026-02-27 19:44:49 +03:30
|
|
|
|
|
|
|
|
|
2026-03-19 22:54:29 +03:30
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2026-03-25 01:56:41 +03:30
|
|
|
RagChatErrorResponseSerializer = build_message_response_serializer(
|
|
|
|
|
"RagChatErrorResponseSerializer"
|
|
|
|
|
)
|
|
|
|
|
RagValidationErrorResponseSerializer = build_envelope_serializer(
|
|
|
|
|
"RagValidationErrorResponseSerializer",
|
|
|
|
|
data_required=False,
|
|
|
|
|
allow_null=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-27 19:44:49 +03:30
|
|
|
class ChatView(APIView):
|
2026-04-25 17:22:41 +03:30
|
|
|
parser_classes = [JSONParser, MultiPartParser, FormParser]
|
|
|
|
|
|
|
|
|
|
def _parse_history(self, raw_history):
|
|
|
|
|
if raw_history in (None, "", []):
|
|
|
|
|
return []
|
|
|
|
|
if isinstance(raw_history, list):
|
|
|
|
|
return raw_history
|
|
|
|
|
if isinstance(raw_history, str):
|
|
|
|
|
try:
|
|
|
|
|
parsed = json.loads(raw_history)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
raise ValueError("history باید JSON معتبر باشد.")
|
|
|
|
|
if not isinstance(parsed, list):
|
|
|
|
|
raise ValueError("history باید آرایه باشد.")
|
|
|
|
|
return parsed
|
|
|
|
|
raise ValueError("history فرمت پشتیبانی شده ندارد.")
|
|
|
|
|
|
|
|
|
|
def _collect_uploaded_images(self, request: Request):
|
|
|
|
|
images = []
|
|
|
|
|
for uploaded in request.FILES.getlist("images"):
|
|
|
|
|
images.append(encode_uploaded_image(uploaded))
|
|
|
|
|
single_image = request.FILES.get("image")
|
|
|
|
|
if single_image is not None:
|
|
|
|
|
images.append(encode_uploaded_image(single_image))
|
|
|
|
|
image_urls = request.data.get("image_urls")
|
|
|
|
|
if isinstance(image_urls, str) and image_urls.strip():
|
|
|
|
|
try:
|
|
|
|
|
parsed_urls = json.loads(image_urls)
|
|
|
|
|
except (json.JSONDecodeError, ValueError):
|
|
|
|
|
parsed_urls = [image_urls]
|
|
|
|
|
image_urls = parsed_urls
|
|
|
|
|
if isinstance(image_urls, list):
|
|
|
|
|
for item in image_urls:
|
|
|
|
|
if isinstance(item, str) and item.strip():
|
|
|
|
|
images.append({"url": item.strip(), "detail": "auto"})
|
|
|
|
|
elif isinstance(item, dict) and isinstance(item.get("url"), str):
|
|
|
|
|
image_payload = {"url": item["url"].strip(), "detail": item.get("detail", "auto")}
|
|
|
|
|
images.append(image_payload)
|
|
|
|
|
return images
|
2026-02-27 19:44:49 +03:30
|
|
|
|
2026-03-19 22:54:29 +03:30
|
|
|
@extend_schema(
|
|
|
|
|
tags=["RAG Chat"],
|
|
|
|
|
summary="چت RAG با استریم",
|
|
|
|
|
description="پیام کاربر را دریافت و پاسخ را به صورت استریم برمیگرداند.",
|
|
|
|
|
request=inline_serializer(
|
|
|
|
|
name="ChatRequest",
|
|
|
|
|
fields={
|
2026-03-22 03:08:27 +03:30
|
|
|
"query": drf_serializers.CharField(required=False, help_text="متن سوال کاربر"),
|
|
|
|
|
"message": drf_serializers.CharField(required=False, help_text="نام قبلی فیلد query"),
|
2026-04-24 01:23:56 +03:30
|
|
|
"farm_uuid": drf_serializers.CharField(help_text="شناسه مزرعه"),
|
2026-04-25 17:22:41 +03:30
|
|
|
"history": drf_serializers.JSONField(required=False, help_text="آرایه پیام های قبلی با role=user/assistant"),
|
|
|
|
|
"image_urls": drf_serializers.JSONField(required=False, help_text="آرایه URL تصاویر برای پیام فعلی"),
|
|
|
|
|
"image": drf_serializers.FileField(required=False, help_text="یک تصویر برای پیام فعلی"),
|
|
|
|
|
"images": drf_serializers.ListField(
|
|
|
|
|
child=drf_serializers.FileField(),
|
|
|
|
|
required=False,
|
|
|
|
|
help_text="چند تصویر برای پیام فعلی",
|
|
|
|
|
),
|
2026-03-19 22:54:29 +03:30
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
responses={
|
|
|
|
|
200: OpenApiResponse(
|
2026-03-25 01:56:41 +03:30
|
|
|
response=OpenApiTypes.STR,
|
2026-03-19 22:54:29 +03:30
|
|
|
description="پاسخ استریم متنی (text/plain)",
|
|
|
|
|
),
|
2026-03-25 01:56:41 +03:30
|
|
|
400: build_response(
|
|
|
|
|
RagChatErrorResponseSerializer,
|
|
|
|
|
"پارامترهای ورودی نامعتبر هستند.",
|
2026-03-19 22:54:29 +03:30
|
|
|
),
|
2026-04-24 01:23:56 +03:30
|
|
|
404: build_response(
|
|
|
|
|
RagChatErrorResponseSerializer,
|
|
|
|
|
"مزرعه پیدا نشد.",
|
|
|
|
|
),
|
2026-03-19 22:54:29 +03:30
|
|
|
},
|
|
|
|
|
examples=[
|
|
|
|
|
OpenApiExample(
|
|
|
|
|
"نمونه درخواست",
|
2026-03-22 03:08:27 +03:30
|
|
|
value={
|
2026-04-25 17:22:41 +03:30
|
|
|
"farm_uuid": "11111111-1111-1111-1111-111111111111",
|
2026-04-24 01:23:56 +03:30
|
|
|
"query": "وضعیت مزرعه من چطور است؟",
|
2026-04-25 17:22:41 +03:30
|
|
|
"history": [
|
|
|
|
|
{"role": "user", "content": "رطوبت خاک من پایین بود؟"},
|
|
|
|
|
{"role": "assistant", "content": "بله، رطوبت خاک کمتر از محدوده مطلوب بود."},
|
|
|
|
|
],
|
|
|
|
|
"image_urls": ["https://example.com/farm-photo.jpg"],
|
2026-03-22 03:08:27 +03:30
|
|
|
},
|
2026-03-19 22:54:29 +03:30
|
|
|
request_only=True,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
)
|
2026-02-27 19:44:49 +03:30
|
|
|
def post(self, request: Request):
|
2026-04-24 01:23:56 +03:30
|
|
|
from farm_data.services import get_farm_details
|
|
|
|
|
from .config import load_rag_config
|
2026-03-22 03:08:27 +03:30
|
|
|
|
2026-02-27 20:06:46 +03:30
|
|
|
data = request.data if request.method == "POST" else request.query_params
|
2026-03-22 03:08:27 +03:30
|
|
|
message = data.get("query", data.get("message"))
|
2026-04-24 01:23:56 +03:30
|
|
|
farm_uuid = data.get("farm_uuid")
|
2026-04-25 17:22:41 +03:30
|
|
|
raw_history = data.get("history")
|
|
|
|
|
try:
|
|
|
|
|
images = self._collect_uploaded_images(request)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
return Response(
|
|
|
|
|
{"code": 400, "msg": str(exc)},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
|
|
|
|
if message is None and images:
|
|
|
|
|
message = "لطفا تصویر ارسالی را در کنار اطلاعات مزرعه بررسی کن."
|
2026-02-27 19:44:49 +03:30
|
|
|
if not message or not isinstance(message, str):
|
|
|
|
|
return Response(
|
2026-04-25 17:22:41 +03:30
|
|
|
{"code": 400, "msg": "پارامتر query الزامی است، مگر اینکه تصویر ارسال شده باشد."},
|
2026-02-27 19:44:49 +03:30
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
|
|
|
|
message = str(message).strip()
|
|
|
|
|
if not message:
|
|
|
|
|
return Response(
|
|
|
|
|
{"code": 400, "msg": "پیام نباید خالی باشد."},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
2026-04-24 01:23:56 +03:30
|
|
|
if not farm_uuid or not isinstance(farm_uuid, str):
|
2026-03-22 03:08:27 +03:30
|
|
|
return Response(
|
2026-04-24 01:23:56 +03:30
|
|
|
{"code": 400, "msg": "پارامتر farm_uuid الزامی است."},
|
2026-03-22 03:08:27 +03:30
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
2026-04-24 01:23:56 +03:30
|
|
|
farm_uuid = str(farm_uuid).strip()
|
|
|
|
|
if not farm_uuid:
|
2026-03-22 03:08:27 +03:30
|
|
|
return Response(
|
2026-04-24 01:23:56 +03:30
|
|
|
{"code": 400, "msg": "farm_uuid نباید خالی باشد."},
|
2026-03-22 03:08:27 +03:30
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
2026-04-25 17:22:41 +03:30
|
|
|
try:
|
|
|
|
|
history = self._parse_history(raw_history)
|
|
|
|
|
except ValueError as exc:
|
|
|
|
|
return Response(
|
|
|
|
|
{"code": 400, "msg": str(exc)},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
2026-03-22 03:08:27 +03:30
|
|
|
cfg = load_rag_config()
|
2026-04-24 01:23:56 +03:30
|
|
|
farm_details = get_farm_details(farm_uuid)
|
|
|
|
|
if farm_details is None:
|
2026-02-27 20:06:46 +03:30
|
|
|
return Response(
|
2026-04-24 01:23:56 +03:30
|
|
|
{"code": 404, "msg": "farm پیدا نشد."},
|
|
|
|
|
status=status.HTTP_404_NOT_FOUND,
|
2026-02-27 20:06:46 +03:30
|
|
|
)
|
2026-02-27 19:44:49 +03:30
|
|
|
|
|
|
|
|
def generate():
|
|
|
|
|
try:
|
2026-03-22 03:08:27 +03:30
|
|
|
for chunk in chat_rag_stream(
|
|
|
|
|
message,
|
2026-04-24 01:23:56 +03:30
|
|
|
farm_uuid=farm_uuid,
|
2026-03-22 03:08:27 +03:30
|
|
|
config=cfg,
|
2026-04-24 01:23:56 +03:30
|
|
|
farm_details=farm_details,
|
2026-04-25 17:22:41 +03:30
|
|
|
history=history,
|
|
|
|
|
images=images,
|
2026-03-22 03:08:27 +03:30
|
|
|
):
|
2026-02-27 19:44:49 +03:30
|
|
|
yield chunk
|
|
|
|
|
except Exception as e:
|
|
|
|
|
yield f"\n[خطا: {e}]"
|
|
|
|
|
|
|
|
|
|
return StreamingHttpResponse(
|
|
|
|
|
generate(),
|
|
|
|
|
content_type="text/plain; charset=utf-8",
|
|
|
|
|
)
|