مهمترین خواستهی سینیور این بود که بسنجیم پاسخِ AI واقعاً یاد میدهد یا نه. برای سنجیدن، اول باید پاسخها قابلِ ردیابی میشدند. پرامپتها پایهآگاه شدند: تا پیش از این «معلمِ دبستانی» برای پایه نهم هاردکد بود، هم در تولیدِ پاسخ و هم در vision — یعنی دانشآموزِ نهم لحنِ ابتدایی میگرفت. PROMPT_VERSION حالا روی هر پیام ذخیره میشود. هر پاسخ حاشیهنویسی میشود: مدل، نسخهی پرامپت، پایه، collection، مقدارِ فیلتر، تعداد chunk، توکن، تأخیر و from_cache. فیدبک reason اختیاری گرفت تا لایک/دیسلایک بعداً قابلِ تحلیل باشد («بشه چک کرد کدوم بهتر بوده»). ai_feedback_report --by prompt_version نسبتِ رضایت را کنارِ مصرف/هزینه میگذارد. rag_eval سوالهای واقعی را از مسیرِ واقعی میپرسد، با --retrieval-only برای اجرای ارزان. rag/services/lesson_context.py از lessonId تنها پایه و فصل را درمیآورد؛ normalize_grade_title هم اختلافِ «پایه نهم» در دیتابیس با «ریاضی نهم» در نامِ collection را میبندد — بدونِ آن مسیرِ gradeId بیسروصدا خالی برمیگشت. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
52 lines
2.0 KiB
Python
52 lines
2.0 KiB
Python
"""
|
|
کشِ پاسخِ RAG برای سوالِ عیناً تکراری.
|
|
|
|
کلید = هشِ (collection + مقدارِ فیلترِ درس/فصل + پایه + متنِ نرمالشدهی سوال).
|
|
فقط مرحلهی گران (embedding + جستجوی Qdrant + تولیدِ GPT) رو رد میکنه؛
|
|
کوتای روزانه، idempotency و ثبتِ Message همچنان در view اجرا میشن.
|
|
"""
|
|
import hashlib
|
|
|
|
from django.conf import settings
|
|
from django.core.cache import cache
|
|
|
|
# v2: پایه هم واردِ کلید شد (پایین را ببینید). نسخهی prefix عمداً جلو رفت تا
|
|
# کلیدهای v1 که پایه در آنها نبود، خودبهخود بیاثر شوند.
|
|
_PREFIX = 'rag:ans:v2:'
|
|
|
|
|
|
def ttl() -> int:
|
|
return int(getattr(settings, 'RAG_ANSWER_CACHE_TTL', 0))
|
|
|
|
|
|
def enabled() -> bool:
|
|
return ttl() > 0
|
|
|
|
|
|
def _norm(text: str) -> str:
|
|
return ' '.join((text or '').split()).strip().lower()
|
|
|
|
|
|
def make_key(collection, filter_value, question: str, grade_title: str = '') -> str:
|
|
"""
|
|
⚠️ `grade_title` جزءِ کلید است چون تولیدِ پاسخ از این به بعد پایهآگاه است
|
|
(لحن و سطحِ توضیح از پایه میآید). بدونِ آن، وقتی subject/فصل مشخص نیست
|
|
collection و filter هر دو None میشوند و کلید فقط به متنِ سوال میماند —
|
|
یعنی پاسخی که برای دانشآموزِ نهم ساخته شده عیناً به دانشآموزِ سوم
|
|
برمیگشت.
|
|
"""
|
|
raw = f'{collection}|{filter_value}|{(grade_title or "").strip()}|{_norm(question)}'
|
|
return _PREFIX + hashlib.sha1(raw.encode('utf-8')).hexdigest()
|
|
|
|
|
|
def get(key: str) -> dict | None:
|
|
if not enabled():
|
|
return None
|
|
return cache.get(key)
|
|
|
|
|
|
def set(key: str, payload: dict) -> None: # noqa: A003
|
|
if not enabled():
|
|
return
|
|
cache.set(key, payload, ttl())
|