Files
MathApp/ai_tutor/views.py

148 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils import timezone
from drf_spectacular.utils import extend_schema
from rest_framework.exceptions import Throttled
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from accounts.models import User as AccountUser
from .models import Conversation, Message, MessageFeedback
from .serializers import (
AskRequestSerializer,
AskResponseSerializer,
ConversationMessagesResponseSerializer,
FeedbackRequestSerializer,
FeedbackResponseSerializer,
)
DAILY_AI_QUOTA = 15
def generate_tutor_answer(user_question: str, grade_id, lesson_id) -> tuple[str, list[str]]:
"""
استاب پاسخ AI Tutor. اینجا محل اتصال به مدل واقعی (مثلاً Claude API از طریق
Anthropic SDK) هست؛ فعلاً برای اینکه بدون نیاز به کلید API هم بشه کل جریان
رو با Swagger تست کرد، یک پاسخ ساده و قطعی برمی‌گردونه.
"""
answer = (
f'این یک پاسخ نمونه (stub) برای سوال «{user_question}» است. '
'برای اتصال به مدل واقعی هوش مصنوعی، این تابع را در ai_tutor/views.py '
'جایگزین کنید (مثلاً با فراخوانی Anthropic API).'
)
follow_ups = ['یک مثال دیگر بزن', 'بیشتر توضیح بده']
return answer, follow_ups
class AskAIView(APIView):
"""POST /api/ai/ask — سوال از AI (استریم اختیاری؛ در این فاز استریم پیاده نشده)"""
permission_classes = [IsAuthenticated]
@extend_schema(request=AskRequestSerializer, responses=AskResponseSerializer, tags=['AI Tutor (ai_tutor)'])
def post(self, request):
serializer = AskRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
v = serializer.validated_data
user = request.user
# ⚠️ اصلاح: چک و مصرفِ کوتای روزانه قبلاً race condition داشت —
# دو درخواست هم‌زمان از همون کاربر می‌تونستن هر دو شمارشِ «امروز»
# رو قبل از ساختن پیامِ خودشون بخونن و هر دو رد بشن، در نتیجه از
# سقف ۱۵ سوال روزانه عبور کنن. حالا با قفل‌کردن ردیف کاربر
# (select_for_update) در یک تراکنش اتمیک، چک و مصرفِ کوتا برای هر
# کاربر serialize می‌شه؛ درخواست‌های هم‌زمانِ همون کاربر منتظر آزاد
# شدن قفل می‌مونن و دیگه نمی‌تونن هم‌زمان از کنار چک رد بشن.
with transaction.atomic():
AccountUser.objects.select_for_update().get(pk=user.id)
today = timezone.localdate()
used_today = Message.objects.filter(
conversation__user_id=user.id, role='user', created_at__date=today
).count()
if used_today >= DAILY_AI_QUOTA:
raise Throttled(detail='سقف روزانه‌ی سوال از هوش مصنوعی به پایان رسیده است.')
conversation_id = v.get('conversationId')
if conversation_id:
conversation = get_object_or_404(Conversation, pk=conversation_id, user_id=user.id)
else:
conversation = Conversation.objects.create(
user_id=user.id,
lesson_id=v.get('lessonId') or None,
chapter_id=v.get('chapterId') or None,
grade_id=v.get('gradeId'),
)
Message.objects.create(
conversation=conversation,
role='user',
text=v['userQuestion'],
question_id=v.get('questionId') or None,
)
answer_text, follow_ups = generate_tutor_answer(
v['userQuestion'], v.get('gradeId'), v.get('lessonId')
)
assistant_message = Message.objects.create(
conversation=conversation,
role='assistant',
text=answer_text,
question_id=v.get('questionId') or None,
)
used_today += 1 # همین سوالی که الان ثبت شد هم جزو مصرف امروزه
remaining_quota = max(DAILY_AI_QUOTA - used_today, 0)
data = {
'conversationId': str(conversation.id),
'messageId': str(assistant_message.id),
'answer': answer_text,
'suggestedFollowUps': follow_ups,
'remainingQuota': remaining_quota,
}
return Response(AskResponseSerializer(data).data)
class ConversationMessagesView(APIView):
"""GET /api/ai/conversations/{id}/messages — تاریخچه‌ی مکالمه"""
permission_classes = [IsAuthenticated]
@extend_schema(responses=ConversationMessagesResponseSerializer, tags=['AI Tutor (ai_tutor)'])
def get(self, request, conversation_id):
conversation = get_object_or_404(Conversation, pk=conversation_id, user_id=request.user.id)
messages = conversation.messages.all()
data = {
'conversationId': str(conversation.id),
'lessonId': str(conversation.lesson_id) if conversation.lesson_id else None,
'messages': [
{'messageId': str(m.id), 'role': m.role, 'text': m.text} for m in messages
],
}
return Response(ConversationMessagesResponseSerializer(data).data)
class MessageFeedbackView(APIView):
"""POST /api/ai/messages/{id}/feedback — فیدبک 👍/👎"""
permission_classes = [IsAuthenticated]
@extend_schema(
request=FeedbackRequestSerializer,
responses=FeedbackResponseSerializer,
tags=['AI Tutor (ai_tutor)'],
)
def post(self, request, message_id):
message = get_object_or_404(Message, pk=message_id, conversation__user_id=request.user.id)
serializer = FeedbackRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
MessageFeedback.objects.update_or_create(
message=message,
defaults={'is_helpful': serializer.validated_data['isHelpful']},
)
return Response(FeedbackResponseSerializer({'success': True}).data)