UPDATE
This commit is contained in:
@@ -181,7 +181,9 @@
|
||||
|
||||
| ردیف | API | متد | Endpoint | وضعیت |
|
||||
|------|-----|------|----------|--------|
|
||||
| ۱ | Farm AI Chat | POST | `/api/farm-ai-assistant/chat/` (پیشنهادی) | **پیادهسازی نشده** |
|
||||
| ۱ | Farm AI Chat | POST | `/api/farm-ai-assistant/chat/` | موجود |
|
||||
| ۱.۱ | Farm AI Chat Task Create | POST | `/api/farm-ai-assistant/chat/task/` | موجود |
|
||||
| ۱.۲ | Farm AI Chat Task Status | GET | `/api/farm-ai-assistant/chat/task/{task_id}/status/` | موجود |
|
||||
| ۲ | Farm Context | GET | `/api/farm-ai-assistant/context/` (پیشنهادی) | **پیادهسازی نشده**؛ استفاده از configهای آبیاری/کوددهی |
|
||||
| ۳ | توصیه آبیاری | GET | `/api/irrigation-recommendation/config/` | موجود (mock) |
|
||||
| ۳ | توصیه آبیاری | POST | `/api/irrigation-recommendation/recommend/` | موجود (mock) |
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"code": 202,
|
||||
"msg": "تسک چت دستیار مزرعه در صف قرار گرفت.",
|
||||
"data": {
|
||||
"task_id": "farm-ai-chat-task-123",
|
||||
"status": "PENDING",
|
||||
"status_url": "/api/tasks/farm-ai-chat-task-123/status/"
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,18 @@ class MockLoader:
|
||||
if flat_files:
|
||||
return flat_files
|
||||
|
||||
normalized_parts = [part for part in str(path).strip().strip("/").split("/") if part]
|
||||
for index in range(len(normalized_parts) - 1, -1, -1):
|
||||
candidate_parts = normalized_parts[:index] + normalized_parts[index + 1 :]
|
||||
if not candidate_parts:
|
||||
continue
|
||||
|
||||
candidate_directory = service_root / Path(*candidate_parts)
|
||||
if candidate_directory.exists() and candidate_directory.is_dir():
|
||||
candidate_files = list(candidate_directory.glob(pattern))
|
||||
if candidate_files:
|
||||
return candidate_files
|
||||
|
||||
raise MockDirectoryNotFound(
|
||||
f"Mock directory not found for service='{service_name}' path='{path}': {directory_path}"
|
||||
)
|
||||
|
||||
@@ -51,6 +51,23 @@ class ConversationDeleteSerializer(serializers.Serializer):
|
||||
conversation_id = serializers.UUIDField(read_only=True)
|
||||
|
||||
|
||||
class ChatTaskSubmitDataSerializer(serializers.Serializer):
|
||||
task_id = serializers.CharField(required=False, allow_blank=True)
|
||||
status = serializers.CharField(required=False, allow_blank=True)
|
||||
status_url = serializers.CharField(required=False, allow_blank=True)
|
||||
conversation_id = serializers.UUIDField(read_only=True)
|
||||
message_id = serializers.UUIDField(read_only=True)
|
||||
|
||||
|
||||
class ChatTaskStatusDataSerializer(serializers.Serializer):
|
||||
task_id = serializers.CharField(required=False, allow_blank=True)
|
||||
status = serializers.CharField(required=False, allow_blank=True)
|
||||
conversation_id = serializers.UUIDField(read_only=True)
|
||||
progress = serializers.JSONField(required=False)
|
||||
result = serializers.JSONField(required=False)
|
||||
error = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
|
||||
class ChatPostSerializer(serializers.Serializer):
|
||||
content = serializers.CharField(required=False, allow_blank=True, default="")
|
||||
images = serializers.ListField(
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import ChatDetailView, ChatListCreateView, ChatMessagesView, ChatView, ContextView
|
||||
from .views import (
|
||||
ChatDetailView,
|
||||
ChatListCreateView,
|
||||
ChatMessagesView,
|
||||
ChatTaskCreateView,
|
||||
ChatTaskStatusView,
|
||||
ChatView,
|
||||
ContextView,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path("context/", ContextView.as_view(), name="farm-ai-assistant-context"),
|
||||
path("chat/", ChatView.as_view(), name="farm-ai-assistant-chat"),
|
||||
path("chat/task/", ChatTaskCreateView.as_view(), name="farm-ai-assistant-chat-task-create"),
|
||||
path("chat/task/<str:task_id>/status/", ChatTaskStatusView.as_view(), name="farm-ai-assistant-chat-task-status"),
|
||||
path("chats/", ChatListCreateView.as_view(), name="farm-ai-assistant-chat-list-create"),
|
||||
path("chats/<uuid:conversation_id>/", ChatDetailView.as_view(), name="farm-ai-assistant-chat-detail"),
|
||||
path("chats/<uuid:conversation_id>/messages/", ChatMessagesView.as_view(), name="farm-ai-assistant-chat-messages"),
|
||||
|
||||
+267
-24
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user