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)