This commit is contained in:
2026-03-27 18:32:49 +03:30
parent bf244042a9
commit cef1b5335a
6 changed files with 319 additions and 26 deletions
+267 -24
View File
@@ -19,6 +19,8 @@ from .models import Conversation, Message
from .serializers import (
ChatPostSerializer,
ChatResponseDataSerializer,
ChatTaskStatusDataSerializer,
ChatTaskSubmitDataSerializer,
ConversationCreateSerializer,
ConversationDeleteSerializer,
ConversationMessagesSerializer,
@@ -87,6 +89,46 @@ class ConversationAccessMixin:
payload["conversation_id"] = str(conversation_id)
return payload
def _get_or_create_conversation(self, request, validated):
conversation_id = validated.get("conversation_id")
farm_context = validated.get("farm_context")
title = validated.get("title", "").strip()
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)
return conversation
return Conversation.objects.create(
owner=request.user,
title=title or (validated.get("content", "")[:255]) or "New chat",
farm_context=farm_context or {},
)
@staticmethod
def _build_adapter_payload(request, validated, conversation):
payload = {
"content": validated.get("content", ""),
"query": validated.get("content", ""),
"images": validated.get("images", []),
"conversation_id": str(conversation.uuid),
"user_id": request.user.id,
}
if "farm_context" in validated:
payload["farm_context"] = validated.get("farm_context") or {}
if "title" in validated:
payload["title"] = validated.get("title", "")
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):
@@ -112,6 +154,53 @@ class ConversationAccessMixin:
"sections": sections,
}
@staticmethod
def _extract_task_submit_payload(adapter_data, conversation_id, message_id):
payload_source = adapter_data
if isinstance(adapter_data, dict) and isinstance(adapter_data.get("data"), dict):
payload_source = adapter_data["data"]
if not isinstance(payload_source, dict):
payload_source = {}
return {
"task_id": str(payload_source.get("task_id") or ""),
"status": str(payload_source.get("status") or ""),
"status_url": str(payload_source.get("status_url") or ""),
"conversation_id": str(conversation_id),
"message_id": str(message_id),
}
def _extract_task_status_payload(self, adapter_data, task_id, conversation_id=None):
payload_source = adapter_data
if isinstance(adapter_data, dict) and isinstance(adapter_data.get("data"), dict):
payload_source = adapter_data["data"]
if not isinstance(payload_source, dict):
payload_source = {}
task_status_payload = {
"task_id": str(payload_source.get("task_id") or task_id),
"status": str(payload_source.get("status") or ""),
}
if conversation_id:
task_status_payload["conversation_id"] = str(conversation_id)
progress = payload_source.get("progress")
if progress is not None:
task_status_payload["progress"] = progress
elif payload_source.get("message") and task_status_payload["status"] != "SUCCESS":
task_status_payload["progress"] = {"message": payload_source.get("message")}
if payload_source.get("error"):
task_status_payload["error"] = str(payload_source["error"])
result = payload_source.get("result")
if result is not None:
task_status_payload["result"] = result
return task_status_payload
@staticmethod
def _serialize_chat_message(message):
raw_response = message.raw_response if isinstance(message.raw_response, dict) else {}
@@ -126,6 +215,55 @@ class ConversationAccessMixin:
"created_at": message.created_at,
}
@staticmethod
def _find_user_message_for_task(request, task_id):
return (
Message.objects.select_related("conversation")
.filter(
conversation__owner=request.user,
role=Message.ROLE_USER,
raw_response__task_id=task_id,
)
.order_by("-created_at")
.first()
)
def _persist_task_result(self, user_message, task_id, result):
assistant_payload = self._extract_assistant_payload(result, user_message.conversation.uuid)
assistant_message = (
user_message.conversation.messages.filter(
role=Message.ROLE_ASSISTANT,
raw_response__task_id=task_id,
)
.order_by("-created_at")
.first()
)
if assistant_message is None:
assistant_message = Message.objects.create(
conversation=user_message.conversation,
role=Message.ROLE_ASSISTANT,
content=assistant_payload.get("content", ""),
raw_response={},
)
assistant_payload["message_id"] = str(assistant_message.uuid)
assistant_payload["task_id"] = task_id
assistant_message.content = assistant_payload.get("content", "")
assistant_message.raw_response = assistant_payload
assistant_message.save(update_fields=["content", "raw_response"])
conversation = user_message.conversation
if not conversation.title:
conversation.title = (
user_message.content or assistant_payload.get("content", "") or "New chat"
)[:255]
conversation.save(update_fields=["title", "updated_at"])
else:
conversation.save(update_fields=["updated_at"])
return assistant_payload
class ChatListCreateView(ConversationAccessMixin, APIView):
permission_classes = [IsAuthenticated]
@@ -232,28 +370,7 @@ class ChatView(ConversationAccessMixin, APIView):
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", "").strip()
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]) or "New chat",
farm_context=farm_context or {},
)
conversation = self._get_or_create_conversation(request, validated)
user_message = Message.objects.create(
conversation=conversation,
@@ -263,8 +380,7 @@ class ChatView(ConversationAccessMixin, APIView):
raw_response={},
)
adapter_payload = dict(request.data)
adapter_payload["conversation_id"] = str(conversation.uuid)
adapter_payload = self._build_adapter_payload(request, validated, conversation)
try:
adapter_response = external_api_request(
@@ -310,3 +426,130 @@ class ChatView(ConversationAccessMixin, APIView):
},
status=response_status_code,
)
class ChatTaskCreateView(ConversationAccessMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["Farm AI Assistant"],
request=ChatPostSerializer,
responses={202: status_response("FarmAiAssistantChatTaskCreateResponse", data=ChatTaskSubmitDataSerializer())},
)
def post(self, request):
serializer = ChatPostSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated = serializer.validated_data
conversation = self._get_or_create_conversation(request, validated)
user_message = Message.objects.create(
conversation=conversation,
role=Message.ROLE_USER,
content=validated.get("content", ""),
images=validated.get("images", []),
raw_response={},
)
adapter_payload = self._build_adapter_payload(request, validated, conversation)
try:
adapter_response = external_api_request(
"ai",
"/rag/chat/generate",
method="POST",
payload=adapter_payload,
)
except ExternalAPIRequestError:
return Response(
{
"status": "error",
"data": {
"message": "External AI service is unavailable.",
},
},
status=status.HTTP_503_SERVICE_UNAVAILABLE,
)
if adapter_response.status_code >= 400:
return Response(
{
"status": "error",
"data": adapter_response.data,
},
status=adapter_response.status_code,
)
task_payload = self._extract_task_submit_payload(
adapter_response.data,
conversation.uuid,
user_message.uuid,
)
user_message.raw_response = task_payload
user_message.save(update_fields=["raw_response"])
conversation.save(update_fields=["updated_at"])
return Response(
{
"status": "success",
"data": task_payload,
},
status=adapter_response.status_code,
)
class ChatTaskStatusView(ConversationAccessMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["Farm AI Assistant"],
parameters=[
OpenApiParameter(name="task_id", type=OpenApiTypes.STR, location=OpenApiParameter.PATH),
],
responses={200: status_response("FarmAiAssistantChatTaskStatusResponse", data=ChatTaskStatusDataSerializer())},
)
def get(self, request, task_id):
try:
adapter_response = external_api_request(
"ai",
f"/tasks/{task_id}/status",
method="GET",
)
except ExternalAPIRequestError:
return Response(
{
"status": "error",
"data": {
"message": "External AI service is unavailable.",
},
},
status=status.HTTP_503_SERVICE_UNAVAILABLE,
)
if adapter_response.status_code >= 400:
return Response(
{
"status": "error",
"data": adapter_response.data,
},
status=adapter_response.status_code,
)
user_message = self._find_user_message_for_task(request, task_id)
conversation_id = user_message.conversation.uuid if user_message else None
task_status_payload = self._extract_task_status_payload(
adapter_response.data,
task_id,
conversation_id=conversation_id,
)
result = task_status_payload.get("result")
if user_message and task_status_payload.get("status") == "SUCCESS" and isinstance(result, dict):
assistant_payload = self._persist_task_result(user_message, task_id, result)
task_status_payload["result"] = assistant_payload
return Response(
{
"status": "success",
"data": task_status_payload,
},
status=adapter_response.status_code,
)