Files
Ai/rag/views.py
T

377 lines
15 KiB
Python
Raw Normal View History

"""
ویوهای RAG — چت با استریم
"""
2026-04-25 17:22:41 +03:30
import json
2026-03-25 01:56:41 +03:30
import logging
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,
)
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
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-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-04-24 02:12:06 +03:30
RagIrrigationResponseSerializer = build_envelope_serializer(
"RagIrrigationResponseSerializer",
drf_serializers.JSONField(),
)
RagFertilizationResponseSerializer = build_envelope_serializer(
"RagFertilizationResponseSerializer",
drf_serializers.JSONField(),
)
2026-03-25 01:56:41 +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-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,
),
],
)
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
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 = "لطفا تصویر ارسالی را در کنار اطلاعات مزرعه بررسی کن."
if not message or not isinstance(message, str):
return Response(
2026-04-25 17:22:41 +03:30
{"code": 400, "msg": "پارامتر query الزامی است، مگر اینکه تصویر ارسال شده باشد."},
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:
return Response(
2026-04-24 01:23:56 +03:30
{"code": 404, "msg": "farm پیدا نشد."},
status=status.HTTP_404_NOT_FOUND,
)
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
):
yield chunk
except Exception as e:
yield f"\n[خطا: {e}]"
return StreamingHttpResponse(
generate(),
content_type="text/plain; charset=utf-8",
)
2026-03-19 22:54:29 +03:30
class IrrigationRecommendationView(APIView):
"""
2026-04-24 02:12:06 +03:30
توصیه آبیاری به صورت مستقیم.
2026-04-24 22:20:15 +03:30
POST با farm_uuid، plant_name، growth_stage، irrigation_method_name.
2026-04-24 02:12:06 +03:30
نتیجه همان لحظه برگشت داده می‌شود.
2026-03-19 22:54:29 +03:30
"""
@extend_schema(
tags=["RAG Recommendations"],
summary="درخواست توصیه آبیاری",
description=(
2026-04-24 02:12:06 +03:30
"داده‌های سنسور، گیاه و روش آبیاری را دریافت کرده و "
"توصیه آبیاری را به صورت مستقیم برمی‌گرداند."
2026-03-19 22:54:29 +03:30
),
request=inline_serializer(
name="IrrigationRecommendationRequest",
fields={
2026-04-24 22:20:15 +03:30
"farm_uuid": drf_serializers.CharField(help_text="شناسه یکتای مزرعه (اجباری)"),
"sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid"),
2026-03-19 22:54:29 +03:30
"plant_name": drf_serializers.CharField(required=False, help_text="نام گیاه"),
"growth_stage": drf_serializers.CharField(required=False, help_text="مرحله رشد گیاه"),
"irrigation_method_name": drf_serializers.CharField(required=False, help_text="نام روش آبیاری"),
"query": drf_serializers.CharField(required=False, help_text="سوال اختیاری"),
},
),
responses={
2026-04-24 02:12:06 +03:30
200: build_response(
RagIrrigationResponseSerializer,
"توصیه آبیاری با موفقیت تولید شد.",
2026-03-25 01:56:41 +03:30
),
400: build_response(
RagValidationErrorResponseSerializer,
"پارامتر ورودی نامعتبر است.",
2026-03-19 22:54:29 +03:30
),
2026-04-24 02:12:06 +03:30
500: build_response(
RagValidationErrorResponseSerializer,
"خطا در تولید توصیه آبیاری.",
),
2026-03-19 22:54:29 +03:30
},
examples=[
OpenApiExample(
"نمونه درخواست",
value={
2026-04-25 17:22:41 +03:30
"farm_uuid": "11111111-1111-1111-1111-111111111111",
2026-03-19 22:54:29 +03:30
"plant_name": "گوجه‌فرنگی",
"growth_stage": "میوه‌دهی",
"irrigation_method_name": "آبیاری قطره‌ای",
},
request_only=True,
),
],
)
def post(self, request: Request):
2026-04-24 02:12:06 +03:30
from rag.services.irrigation import get_irrigation_recommendation
2026-03-19 22:54:29 +03:30
2026-04-24 22:20:15 +03:30
farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid")
if not farm_uuid:
2026-03-19 22:54:29 +03:30
return Response(
2026-04-24 22:20:15 +03:30
{"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None},
2026-03-19 22:54:29 +03:30
status=status.HTTP_400_BAD_REQUEST,
)
2026-04-24 02:12:06 +03:30
try:
result = get_irrigation_recommendation(
2026-04-24 22:20:15 +03:30
farm_uuid=str(farm_uuid),
2026-04-24 02:12:06 +03:30
plant_name=request.data.get("plant_name"),
growth_stage=request.data.get("growth_stage"),
irrigation_method_name=request.data.get("irrigation_method_name"),
query=request.data.get("query"),
)
except Exception:
2026-04-24 22:20:15 +03:30
logger.exception("Direct irrigation recommendation failed for farm %s", farm_uuid)
2026-04-24 02:12:06 +03:30
return Response(
{"code": 500, "msg": "خطا در تولید توصیه آبیاری.", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
2026-03-19 22:54:29 +03:30
return Response(
2026-04-24 02:12:06 +03:30
{"code": 200, "msg": "success", "data": result},
2026-03-19 22:54:29 +03:30
status=status.HTTP_200_OK,
)
class FertilizationRecommendationView(APIView):
"""
2026-04-24 02:12:06 +03:30
توصیه کودهی به صورت مستقیم.
2026-04-24 22:20:15 +03:30
POST با farm_uuid، plant_name، growth_stage.
2026-04-24 02:12:06 +03:30
نتیجه همان لحظه برگشت داده می‌شود.
2026-03-19 22:54:29 +03:30
"""
@extend_schema(
tags=["RAG Recommendations"],
summary="درخواست توصیه کودهی",
description=(
2026-04-24 02:12:06 +03:30
"داده‌های سنسور و گیاه را دریافت کرده و "
"توصیه کودهی را به صورت مستقیم برمی‌گرداند."
2026-03-19 22:54:29 +03:30
),
request=inline_serializer(
name="FertilizationRecommendationRequest",
fields={
2026-04-24 22:20:15 +03:30
"farm_uuid": drf_serializers.CharField(help_text="شناسه یکتای مزرعه (اجباری)"),
"sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid"),
2026-03-19 22:54:29 +03:30
"plant_name": drf_serializers.CharField(required=False, help_text="نام گیاه"),
"growth_stage": drf_serializers.CharField(required=False, help_text="مرحله رشد گیاه"),
"query": drf_serializers.CharField(required=False, help_text="سوال اختیاری"),
},
),
responses={
2026-04-24 02:12:06 +03:30
200: build_response(
RagFertilizationResponseSerializer,
"توصیه کودهی با موفقیت تولید شد.",
2026-03-25 01:56:41 +03:30
),
400: build_response(
RagValidationErrorResponseSerializer,
"پارامتر ورودی نامعتبر است.",
2026-03-19 22:54:29 +03:30
),
2026-04-24 02:12:06 +03:30
500: build_response(
RagValidationErrorResponseSerializer,
"خطا در تولید توصیه کودهی.",
),
2026-03-19 22:54:29 +03:30
},
examples=[
OpenApiExample(
"نمونه درخواست",
value={
2026-04-25 17:22:41 +03:30
"farm_uuid": "11111111-1111-1111-1111-111111111111",
2026-03-19 22:54:29 +03:30
"plant_name": "گوجه‌فرنگی",
"growth_stage": "رویشی",
},
request_only=True,
),
],
)
def post(self, request: Request):
2026-04-24 02:12:06 +03:30
from rag.services.fertilization import get_fertilization_recommendation
2026-03-19 22:54:29 +03:30
2026-04-24 22:20:15 +03:30
farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid")
if not farm_uuid:
2026-03-19 22:54:29 +03:30
return Response(
2026-04-24 22:20:15 +03:30
{"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None},
2026-03-19 22:54:29 +03:30
status=status.HTTP_400_BAD_REQUEST,
)
2026-04-24 02:12:06 +03:30
try:
result = get_fertilization_recommendation(
2026-04-24 22:20:15 +03:30
farm_uuid=str(farm_uuid),
2026-04-24 02:12:06 +03:30
plant_name=request.data.get("plant_name"),
growth_stage=request.data.get("growth_stage"),
query=request.data.get("query"),
)
except Exception:
2026-04-24 22:20:15 +03:30
logger.exception("Direct fertilization recommendation failed for farm %s", farm_uuid)
2026-04-24 02:12:06 +03:30
return Response(
{"code": 500, "msg": "خطا در تولید توصیه کودهی.", "data": None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
2026-03-19 22:54:29 +03:30
return Response(
2026-04-24 02:12:06 +03:30
{"code": 200, "msg": "success", "data": result},
2026-03-19 22:54:29 +03:30
status=status.HTTP_200_OK,
)