UPDATE
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Conversation",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("uuid", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
|
||||
("title", models.CharField(blank=True, default="", max_length=255)),
|
||||
("farm_context", models.JSONField(blank=True, default=dict)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="farm_ai_conversations", to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
"db_table": "farm_ai_conversations",
|
||||
"ordering": ["-updated_at", "-created_at"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Message",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("uuid", models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, unique=True)),
|
||||
("role", models.CharField(choices=[("user", "User"), ("assistant", "Assistant")], max_length=32)),
|
||||
("content", models.TextField(blank=True, default="")),
|
||||
("images", models.JSONField(blank=True, default=list)),
|
||||
("raw_response", models.JSONField(blank=True, default=dict)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("conversation", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="messages", to="farm_ai_assistant.conversation")),
|
||||
],
|
||||
options={
|
||||
"db_table": "farm_ai_messages",
|
||||
"ordering": ["created_at", "id"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,52 @@
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Conversation(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
owner = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="farm_ai_conversations",
|
||||
)
|
||||
title = models.CharField(max_length=255, blank=True, default="")
|
||||
farm_context = models.JSONField(default=dict, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "farm_ai_conversations"
|
||||
ordering = ["-updated_at", "-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return self.title or f"Conversation {self.uuid}"
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
ROLE_USER = "user"
|
||||
ROLE_ASSISTANT = "assistant"
|
||||
ROLE_CHOICES = (
|
||||
(ROLE_USER, "User"),
|
||||
(ROLE_ASSISTANT, "Assistant"),
|
||||
)
|
||||
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True)
|
||||
conversation = models.ForeignKey(
|
||||
Conversation,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="messages",
|
||||
)
|
||||
role = models.CharField(max_length=32, choices=ROLE_CHOICES)
|
||||
content = models.TextField(blank=True, default="")
|
||||
images = models.JSONField(default=list, blank=True)
|
||||
raw_response = models.JSONField(default=dict, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "farm_ai_messages"
|
||||
ordering = ["created_at", "id"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.role}: {self.uuid}"
|
||||
@@ -0,0 +1,58 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Conversation, Message
|
||||
|
||||
|
||||
class ConversationListSerializer(serializers.ModelSerializer):
|
||||
conversation_id = serializers.UUIDField(source="uuid", read_only=True)
|
||||
last_message_preview = serializers.SerializerMethodField()
|
||||
message_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Conversation
|
||||
fields = [
|
||||
"conversation_id",
|
||||
"title",
|
||||
"farm_context",
|
||||
"message_count",
|
||||
"last_message_preview",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def get_last_message_preview(self, obj):
|
||||
last_message = getattr(obj, "last_message", None)
|
||||
if last_message is None:
|
||||
last_message = obj.messages.order_by("-created_at", "-id").first()
|
||||
if last_message is None:
|
||||
return ""
|
||||
return (last_message.content or "")[:120]
|
||||
|
||||
|
||||
class MessageSerializer(serializers.ModelSerializer):
|
||||
message_id = serializers.UUIDField(source="uuid", read_only=True)
|
||||
conversation_id = serializers.UUIDField(source="conversation.uuid", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = [
|
||||
"message_id",
|
||||
"conversation_id",
|
||||
"role",
|
||||
"content",
|
||||
"images",
|
||||
"raw_response",
|
||||
"created_at",
|
||||
]
|
||||
|
||||
|
||||
class ChatPostSerializer(serializers.Serializer):
|
||||
content = serializers.CharField(required=False, allow_blank=True, default="")
|
||||
images = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
required=False,
|
||||
default=list,
|
||||
)
|
||||
conversation_id = serializers.UUIDField(required=False)
|
||||
title = serializers.CharField(required=False, allow_blank=True, max_length=255)
|
||||
farm_context = serializers.JSONField(required=False)
|
||||
@@ -1,8 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import ChatView, ContextView
|
||||
from .views import ChatListView, ChatMessagesView, ChatView, ContextView
|
||||
|
||||
urlpatterns = [
|
||||
path("context/", ContextView.as_view(), name="farm-ai-assistant-context"),
|
||||
path("chat/", ChatView.as_view(), name="farm-ai-assistant-chat"),
|
||||
path("chats/", ChatListView.as_view(), name="farm-ai-assistant-chat-list"),
|
||||
path("chats/<uuid:conversation_id>/messages/", ChatMessagesView.as_view(), name="farm-ai-assistant-chat-messages"),
|
||||
]
|
||||
|
||||
+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