UPDATE
This commit is contained in:
+150
-57
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user