""" ویوهای RAG — چت با استریم """ import json import logging from django.http import StreamingHttpResponse from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( OpenApiExample, OpenApiResponse, extend_schema, inline_serializer, ) from rest_framework import status from rest_framework import serializers as drf_serializers 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 from config.openapi import ( build_envelope_serializer, build_message_response_serializer, build_response, ) from .chat import chat_rag_stream, encode_uploaded_image logger = logging.getLogger(__name__) RagChatErrorResponseSerializer = build_message_response_serializer( "RagChatErrorResponseSerializer" ) RagValidationErrorResponseSerializer = build_envelope_serializer( "RagValidationErrorResponseSerializer", data_required=False, allow_null=True, ) RagIrrigationResponseSerializer = build_envelope_serializer( "RagIrrigationResponseSerializer", drf_serializers.JSONField(), ) RagFertilizationResponseSerializer = build_envelope_serializer( "RagFertilizationResponseSerializer", drf_serializers.JSONField(), ) class ChatView(APIView): 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 @extend_schema( tags=["RAG Chat"], summary="چت RAG با استریم", description="پیام کاربر را دریافت و پاسخ را به صورت استریم برمی‌گرداند.", request=inline_serializer( name="ChatRequest", fields={ "query": drf_serializers.CharField(required=False, help_text="متن سوال کاربر"), "message": drf_serializers.CharField(required=False, help_text="نام قبلی فیلد query"), "farm_uuid": drf_serializers.CharField(help_text="شناسه مزرعه"), "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="چند تصویر برای پیام فعلی", ), }, ), responses={ 200: OpenApiResponse( response=OpenApiTypes.STR, description="پاسخ استریم متنی (text/plain)", ), 400: build_response( RagChatErrorResponseSerializer, "پارامترهای ورودی نامعتبر هستند.", ), 404: build_response( RagChatErrorResponseSerializer, "مزرعه پیدا نشد.", ), }, examples=[ OpenApiExample( "نمونه درخواست", value={ "farm_uuid": "11111111-1111-1111-1111-111111111111", "query": "وضعیت مزرعه من چطور است؟", "history": [ {"role": "user", "content": "رطوبت خاک من پایین بود؟"}, {"role": "assistant", "content": "بله، رطوبت خاک کمتر از محدوده مطلوب بود."}, ], "image_urls": ["https://example.com/farm-photo.jpg"], }, request_only=True, ), ], ) def post(self, request: Request): from farm_data.services import get_farm_details from .config import load_rag_config data = request.data if request.method == "POST" else request.query_params message = data.get("query", data.get("message")) farm_uuid = data.get("farm_uuid") 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( {"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, ) if not farm_uuid or not isinstance(farm_uuid, str): return Response( {"code": 400, "msg": "پارامتر farm_uuid الزامی است."}, status=status.HTTP_400_BAD_REQUEST, ) farm_uuid = str(farm_uuid).strip() if not farm_uuid: return Response( {"code": 400, "msg": "farm_uuid نباید خالی باشد."}, status=status.HTTP_400_BAD_REQUEST, ) try: history = self._parse_history(raw_history) except ValueError as exc: return Response( {"code": 400, "msg": str(exc)}, status=status.HTTP_400_BAD_REQUEST, ) cfg = load_rag_config() farm_details = get_farm_details(farm_uuid) if farm_details is None: return Response( {"code": 404, "msg": "farm پیدا نشد."}, status=status.HTTP_404_NOT_FOUND, ) def generate(): try: for chunk in chat_rag_stream( message, farm_uuid=farm_uuid, config=cfg, farm_details=farm_details, history=history, images=images, ): yield chunk except Exception as e: yield f"\n[خطا: {e}]" return StreamingHttpResponse( generate(), content_type="text/plain; charset=utf-8", ) class IrrigationRecommendationView(APIView): """ توصیه آبیاری به صورت مستقیم. POST با farm_uuid، plant_name، growth_stage، irrigation_method_name. نتیجه همان لحظه برگشت داده می‌شود. """ @extend_schema( tags=["RAG Recommendations"], summary="درخواست توصیه آبیاری", description=( "داده‌های سنسور، گیاه و روش آبیاری را دریافت کرده و " "توصیه آبیاری را به صورت مستقیم برمی‌گرداند." ), request=inline_serializer( name="IrrigationRecommendationRequest", fields={ "farm_uuid": drf_serializers.CharField(help_text="شناسه یکتای مزرعه (اجباری)"), "sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid"), "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={ 200: build_response( RagIrrigationResponseSerializer, "توصیه آبیاری با موفقیت تولید شد.", ), 400: build_response( RagValidationErrorResponseSerializer, "پارامتر ورودی نامعتبر است.", ), 500: build_response( RagValidationErrorResponseSerializer, "خطا در تولید توصیه آبیاری.", ), }, examples=[ OpenApiExample( "نمونه درخواست", value={ "farm_uuid": "11111111-1111-1111-1111-111111111111", "plant_name": "گوجه‌فرنگی", "growth_stage": "میوه‌دهی", "irrigation_method_name": "آبیاری قطره‌ای", }, request_only=True, ), ], ) def post(self, request: Request): from rag.services.irrigation import get_irrigation_recommendation farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid") if not farm_uuid: return Response( {"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None}, status=status.HTTP_400_BAD_REQUEST, ) try: result = get_irrigation_recommendation( farm_uuid=str(farm_uuid), 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: logger.exception("Direct irrigation recommendation failed for farm %s", farm_uuid) return Response( {"code": 500, "msg": "خطا در تولید توصیه آبیاری.", "data": None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) return Response( {"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK, ) class FertilizationRecommendationView(APIView): """ توصیه کودهی به صورت مستقیم. POST با farm_uuid، plant_name، growth_stage. نتیجه همان لحظه برگشت داده می‌شود. """ @extend_schema( tags=["RAG Recommendations"], summary="درخواست توصیه کودهی", description=( "داده‌های سنسور و گیاه را دریافت کرده و " "توصیه کودهی را به صورت مستقیم برمی‌گرداند." ), request=inline_serializer( name="FertilizationRecommendationRequest", fields={ "farm_uuid": drf_serializers.CharField(help_text="شناسه یکتای مزرعه (اجباری)"), "sensor_uuid": drf_serializers.CharField(required=False, help_text="نام قدیمی برای farm_uuid"), "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={ 200: build_response( RagFertilizationResponseSerializer, "توصیه کودهی با موفقیت تولید شد.", ), 400: build_response( RagValidationErrorResponseSerializer, "پارامتر ورودی نامعتبر است.", ), 500: build_response( RagValidationErrorResponseSerializer, "خطا در تولید توصیه کودهی.", ), }, examples=[ OpenApiExample( "نمونه درخواست", value={ "farm_uuid": "11111111-1111-1111-1111-111111111111", "plant_name": "گوجه‌فرنگی", "growth_stage": "رویشی", }, request_only=True, ), ], ) def post(self, request: Request): from rag.services.fertilization import get_fertilization_recommendation farm_uuid = request.data.get("farm_uuid") or request.data.get("sensor_uuid") if not farm_uuid: return Response( {"code": 400, "msg": "پارامتر farm_uuid الزامی است.", "data": None}, status=status.HTTP_400_BAD_REQUEST, ) try: result = get_fertilization_recommendation( farm_uuid=str(farm_uuid), plant_name=request.data.get("plant_name"), growth_stage=request.data.get("growth_stage"), query=request.data.get("query"), ) except Exception: logger.exception("Direct fertilization recommendation failed for farm %s", farm_uuid) return Response( {"code": 500, "msg": "خطا در تولید توصیه کودهی.", "data": None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) return Response( {"code": 200, "msg": "success", "data": result}, status=status.HTTP_200_OK, )