""" از یک `lessonId` به مقصدِ retrieval در Qdrant. چرا لازم است: تا پیش از این، برای اینکه پاسخِ دستیار به کتاب استناد کند، اپ باید `subject` + `grade` («نهم») + `lessonNumber` (شماره‌ی فصل) را خودش می‌فرستاد — یعنی فرانت باید از جزئیاتِ داخلیِ نام‌گذاریِ collectionهای Qdrant خبر می‌داشت. اگر نمی‌فرستاد (که حالتِ طبیعی است: اپ فقط می‌داند دانش‌آموز روی کدام درس است)، retrieval کلاً رد می‌شد و مدل بدونِ پشتوانه‌ی کتاب جواب می‌داد — بی‌هیچ خطایی، فقط جوابِ ضعیف‌تر. حالا که Grade/Unit از روی ساختارِ خودِ کتاب ساخته می‌شوند (`seed_curriculum_from_textbook`)، همین زنجیره کافی‌ست: Lesson → Node → Unit(عنوان = «فصل ۴: توان و ریشه») → Grade(عنوان = «پایه نهم») پس اپ فقط `lessonId` می‌فرستد و بقیه اینجا درمی‌آید. مقادیرِ صریحِ درخواست همیشه اولویت دارند؛ این فقط جای خالی را پر می‌کند. """ import logging import re from .known_collections import MATH_CHAPTERS logger = logging.getLogger(__name__) # «پایه نهم» / «پایهٔ نهم» / «نهم دبستان» → «نهم» _GRADE_PREFIX = re.compile(r'^\s*پایه[‌ٔ\s]*') def normalize_grade_title(title: str) -> str: """عنوانِ Grade در دیتابیس → همان رشته‌ای که در نامِ collection می‌آید.""" return _GRADE_PREFIX.sub('', (title or '').strip()).strip() def resolve_from_lesson(lesson_id) -> dict: """ {'subject', 'gradeTitle', 'chapter'} — هر کدام که پیدا شود. هیچ‌وقت استثنا نمی‌دهد: نبودنِ درس یا نخوردنِ عنوانِ فصل با کاتالوگ، یعنی «نتوانستیم حدس بزنیم»، نه خطا — همان مسیرِ قبلی (پاسخِ عمومی) ادامه می‌یابد. """ if lesson_id in (None, ''): return {} try: lesson_id = int(lesson_id) except (TypeError, ValueError): return {} try: from content.models import Lesson lesson = ( Lesson.objects .select_related('node__unit__grade') .filter(pk=lesson_id) .first() ) except Exception: # noqa: BLE001 — اپ content ممکن است در دسترس نباشد logger.exception('rag.lesson_context: خواندنِ درس %s شکست خورد', lesson_id) return {} if lesson is None: return {} unit = lesson.node.unit grade_title = normalize_grade_title(unit.grade.title) if not grade_title: return {} # عنوانِ Unit همان رشته‌ی دقیقِ فصل است؛ در کاتالوگ دنبالش می‌گردیم تا هم # subject و هم شماره‌ی فصل دربیاید. برابریِ رشته‌ای عمدی‌ست: فیلترِ Qdrant # هم روی همین رشته‌ی دقیق است، پس اگر اینجا نخورد آنجا هم نمی‌خورد. unit_title = (unit.title or '').strip() for (subject, grade), chapters in MATH_CHAPTERS.items(): if grade != grade_title: continue for number, title in chapters.items(): if title == unit_title: return {'subject': subject, 'gradeTitle': grade_title, 'chapter': number} logger.info( 'rag.lesson_context: عنوانِ فصل «%s» (پایه %s) در کاتالوگ نبود', unit_title, grade_title, ) return {'gradeTitle': grade_title} def resolve_from_grade(grade_id) -> dict: """ {'subject', 'gradeTitle'} برای وقتی که هیچ درسی در کار نیست. صفحه‌ی «از پی بپرس» از نوارِ پایینِ اپ باز می‌شود، نه از داخلِ یک درس؛ پس نه lessonId دارد نه subject. بدونِ این تابع retrieval کلاً رد می‌شد و همه‌ی جواب‌های آن صفحه عمومی بودند. هر پایه دقیقاً یک کتابِ ریاضی دارد، پس پایه‌ی خودِ کاربر برای انتخابِ کتاب کافی‌ست؛ فصل معلوم نیست و جستجو روی کلِ همان کتاب انجام می‌شود. """ from .config import resolve_grade_title grade_title = resolve_grade_title(grade_id) if not grade_title: return {} for subject, grade in MATH_CHAPTERS: if grade == grade_title: return {'subject': subject, 'gradeTitle': grade_title} return {'gradeTitle': grade_title}