2026-02-27 19:44:49 +03:30
|
|
|
"""
|
|
|
|
|
ویوهای RAG — چت با استریم
|
|
|
|
|
"""
|
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-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,
|
|
|
|
|
build_task_queue_data_serializer,
|
|
|
|
|
build_task_status_data_serializer,
|
|
|
|
|
)
|
2026-02-27 19:44:49 +03:30
|
|
|
from .chat import chat_rag_stream
|
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
)
|
|
|
|
|
RagIrrigationQueueResponseSerializer = build_envelope_serializer(
|
|
|
|
|
"RagIrrigationQueueResponseSerializer",
|
|
|
|
|
build_task_queue_data_serializer("RagIrrigationQueueDataSerializer"),
|
|
|
|
|
)
|
|
|
|
|
RagIrrigationStatusResponseSerializer = build_envelope_serializer(
|
|
|
|
|
"RagIrrigationStatusResponseSerializer",
|
|
|
|
|
build_task_status_data_serializer("RagIrrigationStatusDataSerializer"),
|
|
|
|
|
)
|
|
|
|
|
RagFertilizationQueueResponseSerializer = build_envelope_serializer(
|
|
|
|
|
"RagFertilizationQueueResponseSerializer",
|
|
|
|
|
build_task_queue_data_serializer("RagFertilizationQueueDataSerializer"),
|
|
|
|
|
)
|
|
|
|
|
RagFertilizationStatusResponseSerializer = build_envelope_serializer(
|
|
|
|
|
"RagFertilizationStatusResponseSerializer",
|
|
|
|
|
build_task_status_data_serializer("RagFertilizationStatusDataSerializer"),
|
|
|
|
|
)
|
|
|
|
|
RagValidationErrorResponseSerializer = build_envelope_serializer(
|
|
|
|
|
"RagValidationErrorResponseSerializer",
|
|
|
|
|
data_required=False,
|
|
|
|
|
allow_null=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-02-27 19:44:49 +03:30
|
|
|
class ChatView(APIView):
|
|
|
|
|
"""
|
|
|
|
|
چت RAG با استریم.
|
2026-04-24 01:23:56 +03:30
|
|
|
POST با {"query": "متن سوال", "farm_uuid": "شناسه مزرعه"}.
|
|
|
|
|
همیشه از سرویس ثابت `chat` استفاده میکند و اطلاعات مزرعه را مستقیم به مدل میفرستد.
|
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-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-24 01:23:56 +03:30
|
|
|
"farm_uuid": "550e8400-e29b-41d4-a716-446655440000",
|
|
|
|
|
"query": "وضعیت مزرعه من چطور است؟",
|
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-02-27 19:44:49 +03:30
|
|
|
if not message or not isinstance(message, str):
|
|
|
|
|
return Response(
|
2026-03-22 03:08:27 +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,
|
|
|
|
|
)
|
|
|
|
|
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-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",
|
|
|
|
|
)
|
2026-03-19 22:54:29 +03:30
|
|
|
|
|
|
|
|
|
|
|
|
|
class IrrigationRecommendationView(APIView):
|
|
|
|
|
"""
|
|
|
|
|
توصیه آبیاری با Celery.
|
|
|
|
|
POST با sensor_uuid، plant_name، growth_stage، irrigation_method_name.
|
|
|
|
|
تسک در صف قرار میگیرد و task_id برگشت داده میشود.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@extend_schema(
|
|
|
|
|
tags=["RAG Recommendations"],
|
|
|
|
|
summary="درخواست توصیه آبیاری",
|
|
|
|
|
description=(
|
|
|
|
|
"دادههای سنسور، گیاه و روش آبیاری را دریافت کرده و یک تسک Celery "
|
|
|
|
|
"برای تولید توصیه آبیاری در صف قرار میدهد."
|
|
|
|
|
),
|
|
|
|
|
request=inline_serializer(
|
|
|
|
|
name="IrrigationRecommendationRequest",
|
|
|
|
|
fields={
|
|
|
|
|
"sensor_uuid": drf_serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)"),
|
|
|
|
|
"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-03-25 01:56:41 +03:30
|
|
|
202: build_response(
|
|
|
|
|
RagIrrigationQueueResponseSerializer,
|
|
|
|
|
"تسک توصیه آبیاری در صف قرار گرفت.",
|
|
|
|
|
),
|
|
|
|
|
400: build_response(
|
|
|
|
|
RagValidationErrorResponseSerializer,
|
|
|
|
|
"پارامتر ورودی نامعتبر است.",
|
2026-03-19 22:54:29 +03:30
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
examples=[
|
|
|
|
|
OpenApiExample(
|
|
|
|
|
"نمونه درخواست",
|
|
|
|
|
value={
|
|
|
|
|
"sensor_uuid": "550e8400-e29b-41d4-a716-446655440000",
|
|
|
|
|
"plant_name": "گوجهفرنگی",
|
|
|
|
|
"growth_stage": "میوهدهی",
|
|
|
|
|
"irrigation_method_name": "آبیاری قطرهای",
|
|
|
|
|
},
|
|
|
|
|
request_only=True,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def post(self, request: Request):
|
|
|
|
|
from rag.tasks import irrigation_recommendation_task
|
|
|
|
|
|
|
|
|
|
sensor_uuid = request.data.get("sensor_uuid")
|
|
|
|
|
if not sensor_uuid:
|
|
|
|
|
return Response(
|
|
|
|
|
{"code": 400, "msg": "پارامتر sensor_uuid الزامی است.", "data": None},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
task = irrigation_recommendation_task.delay(
|
|
|
|
|
sensor_uuid=str(sensor_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"),
|
|
|
|
|
)
|
|
|
|
|
return Response(
|
|
|
|
|
{
|
|
|
|
|
"code": 202,
|
|
|
|
|
"msg": "تسک توصیه آبیاری در صف قرار گرفت.",
|
|
|
|
|
"data": {
|
|
|
|
|
"task_id": task.id,
|
|
|
|
|
"status_url": f"/api/rag/recommend/irrigation/{task.id}/status/",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
status=status.HTTP_202_ACCEPTED,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class IrrigationRecommendationStatusView(APIView):
|
|
|
|
|
"""وضعیت تسک توصیه آبیاری."""
|
|
|
|
|
|
|
|
|
|
@extend_schema(
|
|
|
|
|
tags=["RAG Recommendations"],
|
|
|
|
|
summary="وضعیت تسک توصیه آبیاری",
|
|
|
|
|
description="وضعیت تسک Celery توصیه آبیاری را برمیگرداند.",
|
|
|
|
|
responses={
|
2026-03-25 01:56:41 +03:30
|
|
|
200: build_response(
|
|
|
|
|
RagIrrigationStatusResponseSerializer,
|
|
|
|
|
"وضعیت فعلی تسک توصیه آبیاری.",
|
2026-03-19 22:54:29 +03:30
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
def get(self, request, task_id):
|
|
|
|
|
from celery.result import AsyncResult
|
|
|
|
|
|
|
|
|
|
result = AsyncResult(task_id)
|
|
|
|
|
data = {"task_id": task_id, "status": result.state}
|
|
|
|
|
if result.state == "PENDING":
|
|
|
|
|
data["message"] = "تسک در صف یا یافت نشد."
|
|
|
|
|
elif result.state == "PROGRESS":
|
|
|
|
|
data["progress"] = result.info
|
|
|
|
|
elif result.state == "SUCCESS":
|
|
|
|
|
data["result"] = result.result
|
|
|
|
|
elif result.state == "FAILURE":
|
|
|
|
|
data["error"] = str(result.result)
|
|
|
|
|
return Response(
|
|
|
|
|
{"code": 200, "msg": "success", "data": data},
|
|
|
|
|
status=status.HTTP_200_OK,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FertilizationRecommendationView(APIView):
|
|
|
|
|
"""
|
|
|
|
|
توصیه کودهی با Celery.
|
|
|
|
|
POST با sensor_uuid، plant_name، growth_stage.
|
|
|
|
|
تسک در صف قرار میگیرد و task_id برگشت داده میشود.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@extend_schema(
|
|
|
|
|
tags=["RAG Recommendations"],
|
|
|
|
|
summary="درخواست توصیه کودهی",
|
|
|
|
|
description=(
|
|
|
|
|
"دادههای سنسور و گیاه را دریافت کرده و یک تسک Celery "
|
|
|
|
|
"برای تولید توصیه کودهی در صف قرار میدهد."
|
|
|
|
|
),
|
|
|
|
|
request=inline_serializer(
|
|
|
|
|
name="FertilizationRecommendationRequest",
|
|
|
|
|
fields={
|
|
|
|
|
"sensor_uuid": drf_serializers.CharField(help_text="شناسه یکتای سنسور (اجباری)"),
|
|
|
|
|
"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-03-25 01:56:41 +03:30
|
|
|
202: build_response(
|
|
|
|
|
RagFertilizationQueueResponseSerializer,
|
|
|
|
|
"تسک توصیه کودهی در صف قرار گرفت.",
|
|
|
|
|
),
|
|
|
|
|
400: build_response(
|
|
|
|
|
RagValidationErrorResponseSerializer,
|
|
|
|
|
"پارامتر ورودی نامعتبر است.",
|
2026-03-19 22:54:29 +03:30
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
examples=[
|
|
|
|
|
OpenApiExample(
|
|
|
|
|
"نمونه درخواست",
|
|
|
|
|
value={
|
|
|
|
|
"sensor_uuid": "550e8400-e29b-41d4-a716-446655440000",
|
|
|
|
|
"plant_name": "گوجهفرنگی",
|
|
|
|
|
"growth_stage": "رویشی",
|
|
|
|
|
},
|
|
|
|
|
request_only=True,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def post(self, request: Request):
|
|
|
|
|
from rag.tasks import fertilization_recommendation_task
|
|
|
|
|
|
|
|
|
|
sensor_uuid = request.data.get("sensor_uuid")
|
|
|
|
|
if not sensor_uuid:
|
|
|
|
|
return Response(
|
|
|
|
|
{"code": 400, "msg": "پارامتر sensor_uuid الزامی است.", "data": None},
|
|
|
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
task = fertilization_recommendation_task.delay(
|
|
|
|
|
sensor_uuid=str(sensor_uuid),
|
|
|
|
|
plant_name=request.data.get("plant_name"),
|
|
|
|
|
growth_stage=request.data.get("growth_stage"),
|
|
|
|
|
query=request.data.get("query"),
|
|
|
|
|
)
|
|
|
|
|
return Response(
|
|
|
|
|
{
|
|
|
|
|
"code": 202,
|
|
|
|
|
"msg": "تسک توصیه کودهی در صف قرار گرفت.",
|
|
|
|
|
"data": {
|
|
|
|
|
"task_id": task.id,
|
|
|
|
|
"status_url": f"/api/rag/recommend/fertilization/{task.id}/status/",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
status=status.HTTP_202_ACCEPTED,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FertilizationRecommendationStatusView(APIView):
|
|
|
|
|
"""وضعیت تسک توصیه کودهی."""
|
|
|
|
|
|
|
|
|
|
@extend_schema(
|
|
|
|
|
tags=["RAG Recommendations"],
|
|
|
|
|
summary="وضعیت تسک توصیه کودهی",
|
|
|
|
|
description="وضعیت تسک Celery توصیه کودهی را برمیگرداند.",
|
|
|
|
|
responses={
|
2026-03-25 01:56:41 +03:30
|
|
|
200: build_response(
|
|
|
|
|
RagFertilizationStatusResponseSerializer,
|
|
|
|
|
"وضعیت فعلی تسک توصیه کودهی.",
|
2026-03-19 22:54:29 +03:30
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
def get(self, request, task_id):
|
|
|
|
|
from celery.result import AsyncResult
|
|
|
|
|
|
|
|
|
|
result = AsyncResult(task_id)
|
|
|
|
|
data = {"task_id": task_id, "status": result.state}
|
|
|
|
|
if result.state == "PENDING":
|
|
|
|
|
data["message"] = "تسک در صف یا یافت نشد."
|
|
|
|
|
elif result.state == "PROGRESS":
|
|
|
|
|
data["progress"] = result.info
|
|
|
|
|
elif result.state == "SUCCESS":
|
|
|
|
|
data["result"] = result.result
|
|
|
|
|
elif result.state == "FAILURE":
|
|
|
|
|
data["error"] = str(result.result)
|
|
|
|
|
return Response(
|
|
|
|
|
{"code": 200, "msg": "success", "data": data},
|
|
|
|
|
status=status.HTTP_200_OK,
|
|
|
|
|
)
|