This commit is contained in:
2026-03-25 15:43:00 +03:30
parent eb30251362
commit 2846db1647
9 changed files with 325 additions and 60 deletions
+2
View File
@@ -38,8 +38,10 @@ RUN pip config --user set global.index-url https://package-mirror.liara.ir/repos
RUN pip install -r requirements.txt
COPY entrypoint.sh /app/entrypoint.sh
COPY . .
EXPOSE 8000
ENTRYPOINT ["sh", "/app/entrypoint.sh"]
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
+2 -2
View File
@@ -4,6 +4,6 @@ from .views import AccountView, ProfileView
urlpatterns = [
path("profile/", ProfileView.as_view(), name="profile-update"),
path("<uuid:uuid>/", AccountView.as_view(), name="account-detail"),
path("", AccountView.as_view(), name="account-list"),
# path("<uuid:uuid>/", AccountView.as_view(), name="account-detail"),
# path("", AccountView.as_view(), name="account-list"),
]
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
set -e
if [ "${SKIP_MIGRATE}" != "1" ]; then
echo "Running migrations..."
python manage.py migrate --noinput --fake-initial
echo "Migrations done."
fi
echo "Starting command: $*"
exec "$@"
@@ -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"],
},
),
]
+52
View File
@@ -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}"
+58
View File
@@ -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)
+3 -1
View File
@@ -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
View File
@@ -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)