public user IDs, plans priced by tutor cost, age-based tutor tone, self-service account deletion
- profile publicId ("User482917", accounts/public_id.py); admin shows and searches it
- plans API reports aiDailyQuota per plan and freeAiDailyQuota; migration 0007 sets the
temporary prices (monthly 99,000 / yearly 990,000 toman, 5 tutor questions a day) unless
an admin already edited the yearly plan
- tutor tone follows the learner's age (18+ gets an adult tone at any grade); notation
still follows the grade's book; answer cache keyed by tone (prompt tutor-v12-age-tone)
- POST /api/user/delete: phone users confirm with a fresh SMS code (same checks as login,
now shared in verify_phone_otp), guests type "حذف"; payment records are kept, demo
accounts refused
- delete_users --skip-paid
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,22 @@ from .serializers import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _learner_age(user) -> int | None:
|
||||
"""سنِ کاربر از پروفایلش (accounts)؛ لحنِ دستیار برای ۱۸+ بزرگسالانه است."""
|
||||
from accounts.jalali import age_of
|
||||
from accounts.models import Profile
|
||||
|
||||
profile = Profile.objects.filter(user_id=user.id).only('birth_date', 'birth_year').first()
|
||||
return age_of(profile.birth_date, profile.birth_year) if profile else None
|
||||
|
||||
|
||||
def _cache_grade(grade_title: str, age: int | None) -> str:
|
||||
"""پایه در کلیدِ کشِ جواب، با لحن: جوابِ بزرگسالانه به کودک سرو نشود و برعکس."""
|
||||
from rag.services.generation import tone_for
|
||||
|
||||
return f'{grade_title}|adult' if tone_for(grade_title, age) == 'adult' else grade_title
|
||||
|
||||
|
||||
def daily_quota(user_id: int) -> int:
|
||||
"""سقفِ سوالِ روزانهی این کاربر؛ رایگان یا از پلنِ اشتراکش (subscription.services)."""
|
||||
from subscription.services import ai_daily_quota
|
||||
@@ -368,6 +384,7 @@ class AskAIView(APIView):
|
||||
from rag.services import guardrails
|
||||
|
||||
screened = guardrails.screen(v['userQuestion'])
|
||||
age = _learner_age(user)
|
||||
|
||||
# --- مسیرِ استریم (SSE) --- فقط وقتی stream=true و RAG پیکربندیشده.
|
||||
# قراردادِ JSONِ غیراستریم دستنخورده میماند؛ Swagger که stream
|
||||
@@ -381,7 +398,7 @@ class AskAIView(APIView):
|
||||
user_message=user_message, conversation_is_new=conversation_is_new,
|
||||
idempotency_key=idempotency_key, used_today=used_today,
|
||||
subject=subject, grade_id=grade_id, grade_title=grade_title,
|
||||
lesson_number=lesson_number, screened=screened, quota=quota,
|
||||
lesson_number=lesson_number, screened=screened, quota=quota, age=age,
|
||||
)
|
||||
|
||||
used_chunks: list[dict] = []
|
||||
@@ -404,7 +421,7 @@ class AskAIView(APIView):
|
||||
focus = _question_focus(v, user)
|
||||
cacheable = answer_cache.cacheable(history) and not focus
|
||||
_col, _fk, _fv = resolve_target(subject, grade_title, grade_id, lesson_number)
|
||||
cache_key = answer_cache.make_key(_col, _fv, v['userQuestion'], grade_title)
|
||||
cache_key = answer_cache.make_key(_col, _fv, v['userQuestion'], _cache_grade(grade_title, age))
|
||||
cached = answer_cache.get(cache_key) if cacheable else None
|
||||
if cached is not None:
|
||||
answer_text = cached['answer']
|
||||
@@ -426,7 +443,7 @@ class AskAIView(APIView):
|
||||
answer_text, follow_ups, generation_meta = generate_answer(
|
||||
v['userQuestion'], used_chunks or None,
|
||||
retrieval_meta.get('gradeTitle') or grade_title or '',
|
||||
history=history, focus=focus,
|
||||
history=history, focus=focus, age=age,
|
||||
)
|
||||
provider_status.record_success()
|
||||
# ردِ «غیرِ ریاضی» نه منبعِ کتاب نشان میدهد نه کش میشود.
|
||||
@@ -508,7 +525,8 @@ class AskAIView(APIView):
|
||||
|
||||
def _streaming_response(self, *, request, v, user, conversation, user_message,
|
||||
conversation_is_new, idempotency_key, used_today,
|
||||
subject, grade_id, grade_title, lesson_number, screened=None, quota=None):
|
||||
subject, grade_id, grade_title, lesson_number, screened=None, quota=None,
|
||||
age=None):
|
||||
"""
|
||||
نسخهی SSE از post(): همون منطقِ retrieval + کوتا + idempotency، ولی
|
||||
پاسخ توکنبهتوکن استریم میشه. رویدادها:
|
||||
@@ -531,7 +549,7 @@ class AskAIView(APIView):
|
||||
generation_meta: dict = {}
|
||||
answer_from_cache = False
|
||||
_col, _fk, _fv = resolve_target(subject, grade_title, grade_id, lesson_number)
|
||||
cache_key = answer_cache.make_key(_col, _fv, v['userQuestion'], grade_title)
|
||||
cache_key = answer_cache.make_key(_col, _fv, v['userQuestion'], _cache_grade(grade_title, age))
|
||||
try:
|
||||
history = _conversation_history(conversation, user_message)
|
||||
focus = _question_focus(v, user)
|
||||
@@ -566,7 +584,7 @@ class AskAIView(APIView):
|
||||
effective_grade = retrieval_meta.get('gradeTitle') or grade_title or ''
|
||||
for delta in stream_answer(
|
||||
v['userQuestion'], used_chunks or None, effective_grade,
|
||||
history=history, focus=focus, result=stream_result,
|
||||
history=history, focus=focus, result=stream_result, age=age,
|
||||
):
|
||||
parts.append(delta)
|
||||
yield _sse('chunk', {'delta': delta})
|
||||
|
||||
Reference in New Issue
Block a user