UPDATE
This commit is contained in:
+212
-98
@@ -1,6 +1,9 @@
|
||||
"""Farm AI Assistant API views."""
|
||||
|
||||
from django.http import Http404, HttpResponse
|
||||
from copy import deepcopy
|
||||
|
||||
from django.db.models import Count
|
||||
from django.http import Http404
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
@@ -10,9 +13,17 @@ 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 external_api_adapter.exceptions import ExternalAPIRequestError
|
||||
from .mock_data import CHAT_RESPONSE_DATA, CONTEXT_RESPONSE_DATA
|
||||
from .models import Conversation, Message
|
||||
from .serializers import ChatPostSerializer, ConversationListSerializer, MessageSerializer
|
||||
from .serializers import (
|
||||
ChatPostSerializer,
|
||||
ChatResponseDataSerializer,
|
||||
ConversationCreateSerializer,
|
||||
ConversationDeleteSerializer,
|
||||
ConversationMessagesSerializer,
|
||||
ConversationSummarySerializer,
|
||||
)
|
||||
|
||||
|
||||
class ContextView(APIView):
|
||||
@@ -27,63 +38,194 @@ class ContextView(APIView):
|
||||
)
|
||||
|
||||
|
||||
class ChatListView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
tags=["Farm AI Assistant"],
|
||||
responses={200: status_response("FarmAiAssistantConversationListResponse", data=ConversationListSerializer(many=True))},
|
||||
)
|
||||
def get(self, request):
|
||||
conversations = Conversation.objects.filter(owner=request.user).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):
|
||||
class ConversationAccessMixin:
|
||||
@staticmethod
|
||||
def _get_conversation(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
|
||||
|
||||
@staticmethod
|
||||
def _normalize_sections(raw_sections):
|
||||
if not isinstance(raw_sections, list):
|
||||
return []
|
||||
|
||||
allowed_keys = {
|
||||
"type",
|
||||
"title",
|
||||
"content",
|
||||
"items",
|
||||
"icon",
|
||||
"frequency",
|
||||
"amount",
|
||||
"timing",
|
||||
"expandableExplanation",
|
||||
}
|
||||
normalized_sections = []
|
||||
for section in raw_sections:
|
||||
if not isinstance(section, dict) or not section.get("type"):
|
||||
continue
|
||||
|
||||
normalized_section = {}
|
||||
for key in allowed_keys:
|
||||
value = section.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
if key == "items":
|
||||
if not isinstance(value, list):
|
||||
continue
|
||||
normalized_section[key] = [str(item) for item in value]
|
||||
continue
|
||||
normalized_section[key] = str(value) if key != "type" else value
|
||||
|
||||
normalized_sections.append(normalized_section)
|
||||
return normalized_sections
|
||||
|
||||
def _build_mock_assistant_payload(self, conversation_id):
|
||||
payload = deepcopy(CHAT_RESPONSE_DATA)
|
||||
payload["conversation_id"] = str(conversation_id)
|
||||
return payload
|
||||
|
||||
def _extract_assistant_payload(self, adapter_data, conversation_id):
|
||||
payload_source = adapter_data
|
||||
if isinstance(adapter_data, dict) and isinstance(adapter_data.get("data"), dict):
|
||||
payload_source = adapter_data["data"]
|
||||
|
||||
content = ""
|
||||
sections = []
|
||||
|
||||
if isinstance(payload_source, dict):
|
||||
content = payload_source.get("content") or ""
|
||||
sections = self._normalize_sections(payload_source.get("sections"))
|
||||
|
||||
if not sections and isinstance(adapter_data, dict):
|
||||
sections = self._normalize_sections(adapter_data.get("sections"))
|
||||
|
||||
if not content and isinstance(adapter_data, dict):
|
||||
content = adapter_data.get("body") or adapter_data.get("content") or ""
|
||||
|
||||
return {
|
||||
"message_id": "",
|
||||
"conversation_id": str(conversation_id),
|
||||
"content": content,
|
||||
"sections": sections,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_chat_message(message):
|
||||
raw_response = message.raw_response if isinstance(message.raw_response, dict) else {}
|
||||
sections = raw_response.get("sections") if message.role == Message.ROLE_ASSISTANT else []
|
||||
return {
|
||||
"message_id": str(message.uuid),
|
||||
"conversation_id": str(message.conversation.uuid),
|
||||
"role": message.role,
|
||||
"content": message.content,
|
||||
"sections": ConversationAccessMixin._normalize_sections(sections),
|
||||
"images": message.images if isinstance(message.images, list) else [],
|
||||
"created_at": message.created_at,
|
||||
}
|
||||
|
||||
|
||||
class ChatListCreateView(ConversationAccessMixin, APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
tags=["Farm AI Assistant"],
|
||||
responses={200: status_response("FarmAiAssistantConversationListResponse", data=ConversationSummarySerializer(many=True))},
|
||||
)
|
||||
def get(self, request):
|
||||
conversations = (
|
||||
Conversation.objects.filter(owner=request.user)
|
||||
.annotate(message_count=Count("messages"))
|
||||
.order_by("-updated_at", "-created_at")
|
||||
)
|
||||
serializer = ConversationSummarySerializer(conversations, many=True)
|
||||
return Response({"status": "success", "data": serializer.data}, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
tags=["Farm AI Assistant"],
|
||||
request=ConversationCreateSerializer,
|
||||
responses={201: status_response("FarmAiAssistantConversationCreateResponse", data=ConversationSummarySerializer())},
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = ConversationCreateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
validated = serializer.validated_data
|
||||
conversation = Conversation.objects.create(
|
||||
owner=request.user,
|
||||
title=validated.get("title", "").strip() or "New chat",
|
||||
farm_context=validated.get("farm_context") or {},
|
||||
)
|
||||
|
||||
response_serializer = ConversationSummarySerializer(
|
||||
{
|
||||
"uuid": conversation.uuid,
|
||||
"message_count": 0,
|
||||
}
|
||||
)
|
||||
return Response({"status": "success", "data": response_serializer.data}, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class ChatMessagesView(ConversationAccessMixin, APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@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))},
|
||||
responses={200: status_response("FarmAiAssistantMessageListResponse", data=ConversationMessagesSerializer())},
|
||||
)
|
||||
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)
|
||||
serialized_messages = [self._serialize_chat_message(message) for message in messages]
|
||||
return Response(
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"conversation_id": str(conversation.uuid),
|
||||
"messages": serialized_messages,
|
||||
},
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class ChatView(APIView):
|
||||
class ChatDetailView(ConversationAccessMixin, 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))},
|
||||
parameters=[
|
||||
OpenApiParameter(name="conversation_id", type=OpenApiTypes.UUID, location=OpenApiParameter.PATH),
|
||||
],
|
||||
responses={200: status_response("FarmAiAssistantConversationDeleteResponse", data=ConversationDeleteSerializer())},
|
||||
)
|
||||
def get(self, request):
|
||||
return ChatListView().get(request)
|
||||
def delete(self, request, conversation_id):
|
||||
conversation = self._get_conversation(request, conversation_id)
|
||||
deleted_conversation_id = str(conversation.uuid)
|
||||
conversation.delete()
|
||||
return Response(
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"conversation_id": deleted_conversation_id,
|
||||
},
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class ChatView(ConversationAccessMixin, APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(
|
||||
tags=["Farm AI Assistant"],
|
||||
request=ChatPostSerializer,
|
||||
responses={200: status_response("FarmAiAssistantChatResponse", data=serializers.JSONField())},
|
||||
responses={200: status_response("FarmAiAssistantChatResponse", data=ChatResponseDataSerializer())},
|
||||
)
|
||||
def post(self, request):
|
||||
serializer = ChatPostSerializer(data=request.data)
|
||||
@@ -92,7 +234,7 @@ class ChatView(APIView):
|
||||
validated = serializer.validated_data
|
||||
conversation_id = validated.get("conversation_id")
|
||||
farm_context = validated.get("farm_context")
|
||||
title = validated.get("title", "")
|
||||
title = validated.get("title", "").strip()
|
||||
|
||||
if conversation_id:
|
||||
conversation = self._get_conversation(request, conversation_id)
|
||||
@@ -109,7 +251,7 @@ class ChatView(APIView):
|
||||
else:
|
||||
conversation = Conversation.objects.create(
|
||||
owner=request.user,
|
||||
title=title or (validated.get("content", "")[:255]),
|
||||
title=title or (validated.get("content", "")[:255]) or "New chat",
|
||||
farm_context=farm_context or {},
|
||||
)
|
||||
|
||||
@@ -118,81 +260,53 @@ class ChatView(APIView):
|
||||
role=Message.ROLE_USER,
|
||||
content=validated.get("content", ""),
|
||||
images=validated.get("images", []),
|
||||
raw_response={},
|
||||
)
|
||||
|
||||
adapter_payload = dict(request.data)
|
||||
adapter_payload["conversation_id"] = str(conversation.uuid)
|
||||
adapter_response = external_api_request(
|
||||
"ai",
|
||||
"/rag/chat",
|
||||
method="POST",
|
||||
payload=adapter_payload,
|
||||
)
|
||||
|
||||
if isinstance(adapter_response.data, dict) and "body" in adapter_response.data:
|
||||
assistant_content = adapter_response.data.get("body") or ""
|
||||
assistant_message = Message.objects.create(
|
||||
conversation=conversation,
|
||||
role=Message.ROLE_ASSISTANT,
|
||||
content=assistant_content,
|
||||
raw_response=adapter_response.data,
|
||||
try:
|
||||
adapter_response = external_api_request(
|
||||
"ai",
|
||||
"/rag/chat",
|
||||
method="POST",
|
||||
payload=adapter_payload,
|
||||
)
|
||||
|
||||
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"])
|
||||
|
||||
return Response(
|
||||
{
|
||||
"conversation_id": str(conversation.uuid),
|
||||
"user_message_id": str(user_message.uuid),
|
||||
"assistant_message_id": str(assistant_message.uuid),
|
||||
"content": assistant_content,
|
||||
"content_type": adapter_response.data.get("content_type", "text/plain; charset=utf-8"),
|
||||
},
|
||||
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 ""
|
||||
if adapter_response.status_code >= 400:
|
||||
return Response(
|
||||
{
|
||||
"status": "error",
|
||||
"data": adapter_response.data,
|
||||
},
|
||||
status=adapter_response.status_code,
|
||||
)
|
||||
assistant_payload = self._extract_assistant_payload(adapter_response.data, conversation.uuid)
|
||||
response_status_code = adapter_response.status_code
|
||||
except ExternalAPIRequestError:
|
||||
assistant_payload = self._build_mock_assistant_payload(conversation.uuid)
|
||||
response_status_code = status.HTTP_200_OK
|
||||
|
||||
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)},
|
||||
content=assistant_payload.get("content", ""),
|
||||
raw_response={},
|
||||
)
|
||||
assistant_payload["message_id"] = str(assistant_message.uuid)
|
||||
assistant_message.raw_response = assistant_payload
|
||||
assistant_message.save(update_fields=["raw_response"])
|
||||
|
||||
if not conversation.title:
|
||||
conversation.title = (validated.get("content", "") or assistant_content or "New chat")[:255]
|
||||
conversation.title = (validated.get("content", "") or assistant_payload.get("content", "") or "New chat")[:255]
|
||||
conversation.save(update_fields=["title", "updated_at"])
|
||||
else:
|
||||
conversation.save(update_fields=["updated_at"])
|
||||
|
||||
conversation_uuid = str(conversation.uuid)
|
||||
response_data = adapter_response.data
|
||||
if isinstance(response_data, dict):
|
||||
response_data.setdefault("conversation_id", conversation_uuid)
|
||||
|
||||
data = response_data.get("data")
|
||||
if isinstance(data, dict):
|
||||
data.setdefault("conversation_id", conversation_uuid)
|
||||
data.setdefault("user_message_id", str(user_message.uuid))
|
||||
data.setdefault("assistant_message_id", str(assistant_message.uuid))
|
||||
else:
|
||||
response_data.setdefault("user_message_id", str(user_message.uuid))
|
||||
response_data.setdefault("assistant_message_id", str(assistant_message.uuid))
|
||||
else:
|
||||
response_data = {
|
||||
"conversation_id": 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)
|
||||
return Response(
|
||||
{
|
||||
"status": "success",
|
||||
"data": assistant_payload,
|
||||
},
|
||||
status=response_status_code,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user