This commit is contained in:
2026-03-25 15:43:00 +03:30
parent eb30251362
commit 2846db1647
9 changed files with 325 additions and 60 deletions
+150 -57
View File
@@ -1,42 +1,22 @@
"""
Farm AI Assistant API views.
No database. All responses are static mock data.
Response format: {"status": "success", "data": <payload>}. HTTP 200 only.
No processing, validation, or use of input parameters in responses.
"""
"""Farm AI Assistant API views."""
from django.http import HttpResponse
from django.db.models import Count, OuterRef, Subquery
from django.http import Http404, HttpResponse
from rest_framework import serializers, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import OpenApiParameter, extend_schema
from config.swagger import status_response
from external_api_adapter import request as external_api_request
from .mock_data import CONTEXT_RESPONSE_DATA
from .models import Conversation, Message
from .serializers import ChatPostSerializer, ConversationListSerializer, MessageSerializer
class ContextView(APIView):
"""
GET endpoint for farm context (Farm AI Assistant bar).
Purpose:
Returns static farm context for the Farm AI Assistant UI bar:
soilType, waterEC, selectedCrop, growthStage, lastIrrigationStatus.
Used when loading the farm-ai-assistant page to populate the context strip.
Input parameters:
None. Query parameters, if sent, are not read or used.
Response structure:
- status: string, always "success".
- data: object with keys soilType, waterEC, selectedCrop,
growthStage, lastIrrigationStatus (all strings).
No processing or validation is performed on inputs.
"""
@extend_schema(
tags=["Farm AI Assistant"],
responses={200: status_response("FarmAiAssistantContextResponse", data=serializers.JSONField())},
@@ -48,50 +28,163 @@ class ContextView(APIView):
)
class ChatView(APIView):
"""
POST endpoint for Farm AI Assistant chat (send message, get structured reply).
Purpose:
Accepts user message (and optional images, conversation_id, farm_context)
and returns a static structured reply with sections (recommendation,
list, warning) for rendering as cards in the chat UI. No AI or
computation; response is fixed.
Input parameters (body, JSON; all optional except conceptually content):
- content: string. Location: body. User message text. Not read or used.
- images: array of strings (URLs or base64). Location: body. Not read.
- conversation_id: string. Location: body. Conversation id. Not used.
- farm_context: object (soilType, waterEC, selectedCrop, growthStage,
lastIrrigationStatus). Location: body. Not read or used.
Response structure:
- status: string, always "success".
- data: object with message_id, conversation_id, content (string),
sections (array of section objects). Each section has type, title,
icon, and type-specific fields (content, items, frequency, amount,
timing, expandableExplanation).
No processing or validation is performed on inputs. Input values are
not used in the response.
"""
class ChatListView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["Farm AI Assistant"],
request=OpenApiTypes.OBJECT,
responses={200: status_response("FarmAiAssistantConversationListResponse", data=ConversationListSerializer(many=True))},
)
def get(self, request):
last_message_subquery = Message.objects.filter(conversation=OuterRef("pk")).order_by("-created_at", "-id")
conversations = (
Conversation.objects.filter(owner=request.user)
.annotate(
message_count=Count("messages"),
last_message=Subquery(last_message_subquery.values("content")[:1]),
)
.order_by("-updated_at", "-created_at")
)
serializer = ConversationListSerializer(conversations, many=True)
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
class ChatMessagesView(APIView):
permission_classes = [IsAuthenticated]
def _get_conversation(self, request, conversation_id):
try:
return Conversation.objects.get(uuid=conversation_id, owner=request.user)
except Conversation.DoesNotExist as exc:
raise Http404("Conversation not found") from exc
@extend_schema(
tags=["Farm AI Assistant"],
parameters=[
OpenApiParameter(name="conversation_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH),
],
responses={200: status_response("FarmAiAssistantMessageListResponse", data=MessageSerializer(many=True))},
)
def get(self, request, conversation_id):
conversation = self._get_conversation(request, conversation_id)
messages = conversation.messages.all()
serializer = MessageSerializer(messages, many=True)
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
class ChatView(APIView):
permission_classes = [IsAuthenticated]
def _get_conversation(self, request, conversation_id):
try:
return Conversation.objects.get(uuid=conversation_id, owner=request.user)
except Conversation.DoesNotExist as exc:
raise Http404("Conversation not found") from exc
@extend_schema(
tags=["Farm AI Assistant"],
responses={200: status_response("FarmAiAssistantConversationListAliasResponse", data=ConversationListSerializer(many=True))},
)
def get(self, request):
return ChatListView().get(request)
@extend_schema(
tags=["Farm AI Assistant"],
request=ChatPostSerializer,
responses={200: status_response("FarmAiAssistantChatResponse", data=serializers.JSONField())},
)
def post(self, request):
serializer = ChatPostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated = serializer.validated_data
conversation_id = validated.get("conversation_id")
farm_context = validated.get("farm_context")
title = validated.get("title", "")
if conversation_id:
conversation = self._get_conversation(request, conversation_id)
updated_fields = []
if farm_context is not None:
conversation.farm_context = farm_context
updated_fields.append("farm_context")
if title:
conversation.title = title
updated_fields.append("title")
if updated_fields:
updated_fields.append("updated_at")
conversation.save(update_fields=updated_fields)
else:
conversation = Conversation.objects.create(
owner=request.user,
title=title or (validated.get("content", "")[:255]),
farm_context=farm_context or {},
)
user_message = Message.objects.create(
conversation=conversation,
role=Message.ROLE_USER,
content=validated.get("content", ""),
images=validated.get("images", []),
)
adapter_payload = dict(request.data)
adapter_payload["conversation_id"] = str(conversation.uuid)
adapter_response = external_api_request(
"ai",
"/rag/chat",
method="POST",
payload=request.data,
payload=adapter_payload,
)
if isinstance(adapter_response.data, dict) and "body" in adapter_response.data:
conversation.save(update_fields=["updated_at"])
return HttpResponse(
adapter_response.data["body"],
status=adapter_response.status_code,
content_type=adapter_response.data.get("content_type", "text/plain; charset=utf-8"),
)
return Response(adapter_response.data, status=adapter_response.status_code)
assistant_content = ""
if isinstance(adapter_response.data, dict):
assistant_content = adapter_response.data.get("content") or ""
if not assistant_content and isinstance(adapter_response.data.get("data"), dict):
assistant_content = adapter_response.data["data"].get("content") or ""
assistant_message = Message.objects.create(
conversation=conversation,
role=Message.ROLE_ASSISTANT,
content=assistant_content,
raw_response=adapter_response.data if isinstance(adapter_response.data, dict) else {"body": str(adapter_response.data)},
)
if not conversation.title:
conversation.title = (validated.get("content", "") or assistant_content or "New chat")[:255]
conversation.save(update_fields=["title", "updated_at"])
else:
conversation.save(update_fields=["updated_at"])
response_data = adapter_response.data
if isinstance(response_data, dict):
data = response_data.get("data")
if isinstance(data, dict):
data.setdefault("conversation_id", str(conversation.uuid))
data.setdefault("user_message_id", str(user_message.uuid))
data.setdefault("assistant_message_id", str(assistant_message.uuid))
else:
response_data = {
"conversation_id": str(conversation.uuid),
"user_message_id": str(user_message.uuid),
"assistant_message_id": str(assistant_message.uuid),
"response": response_data,
}
else:
response_data = {
"conversation_id": str(conversation.uuid),
"user_message_id": str(user_message.uuid),
"assistant_message_id": str(assistant_message.uuid),
"response": response_data,
}
return Response(response_data, status=adapter_response.status_code)