UPDATE
This commit is contained in:
@@ -38,8 +38,10 @@ RUN pip config --user set global.index-url https://package-mirror.liara.ir/repos
|
|||||||
|
|
||||||
RUN pip install -r requirements.txt
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
COPY entrypoint.sh /app/entrypoint.sh
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
ENTRYPOINT ["sh", "/app/entrypoint.sh"]
|
||||||
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
|
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||||
|
|||||||
+2
-2
@@ -4,6 +4,6 @@ from .views import AccountView, ProfileView
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("profile/", ProfileView.as_view(), name="profile-update"),
|
path("profile/", ProfileView.as_view(), name="profile-update"),
|
||||||
path("<uuid:uuid>/", AccountView.as_view(), name="account-detail"),
|
# path("<uuid:uuid>/", AccountView.as_view(), name="account-detail"),
|
||||||
path("", AccountView.as_view(), name="account-list"),
|
# path("", AccountView.as_view(), name="account-list"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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 django.urls import path
|
||||||
|
|
||||||
from .views import ChatView, ContextView
|
from .views import ChatListView, ChatMessagesView, ChatView, ContextView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("context/", ContextView.as_view(), name="farm-ai-assistant-context"),
|
path("context/", ContextView.as_view(), name="farm-ai-assistant-context"),
|
||||||
path("chat/", ChatView.as_view(), name="farm-ai-assistant-chat"),
|
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."""
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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 import serializers, status
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from drf_spectacular.types import OpenApiTypes
|
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 config.swagger import status_response
|
||||||
from external_api_adapter import request as external_api_request
|
from external_api_adapter import request as external_api_request
|
||||||
from .mock_data import CONTEXT_RESPONSE_DATA
|
from .mock_data import CONTEXT_RESPONSE_DATA
|
||||||
|
from .models import Conversation, Message
|
||||||
|
from .serializers import ChatPostSerializer, ConversationListSerializer, MessageSerializer
|
||||||
|
|
||||||
|
|
||||||
class ContextView(APIView):
|
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(
|
@extend_schema(
|
||||||
tags=["Farm AI Assistant"],
|
tags=["Farm AI Assistant"],
|
||||||
responses={200: status_response("FarmAiAssistantContextResponse", data=serializers.JSONField())},
|
responses={200: status_response("FarmAiAssistantContextResponse", data=serializers.JSONField())},
|
||||||
@@ -48,50 +28,163 @@ class ContextView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChatView(APIView):
|
class ChatListView(APIView):
|
||||||
"""
|
permission_classes = [IsAuthenticated]
|
||||||
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
tags=["Farm AI Assistant"],
|
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())},
|
responses={200: status_response("FarmAiAssistantChatResponse", data=serializers.JSONField())},
|
||||||
)
|
)
|
||||||
def post(self, request):
|
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(
|
adapter_response = external_api_request(
|
||||||
"ai",
|
"ai",
|
||||||
"/rag/chat",
|
"/rag/chat",
|
||||||
method="POST",
|
method="POST",
|
||||||
payload=request.data,
|
payload=adapter_payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(adapter_response.data, dict) and "body" in adapter_response.data:
|
if isinstance(adapter_response.data, dict) and "body" in adapter_response.data:
|
||||||
|
conversation.save(update_fields=["updated_at"])
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
adapter_response.data["body"],
|
adapter_response.data["body"],
|
||||||
status=adapter_response.status_code,
|
status=adapter_response.status_code,
|
||||||
content_type=adapter_response.data.get("content_type", "text/plain; charset=utf-8"),
|
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