- content.Subject + Unit.subject, /api/home?subject=, tutor grounded in the chosen book - 5 new books ingested; ~1150 hand-written questions, islands named by hand - node_split: islands over 7 lessons split into parts of <=6 (lesson ids and progress kept) - Lesson.figures hand-authored and visually checked for 653 lessons of the new books; gen_lesson_figures got --subject and survives network errors - seed files re-exported Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
109 lines
5.4 KiB
Python
109 lines
5.4 KiB
Python
"""
|
||
از یک `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', 'node__unit__subject')
|
||
.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`)، نه از حدسِ روی عنوان: یک پایه چند کتاب دارد و
|
||
# عنوانِ فصل بینِ کتابها میتواند تکرار شود. عنوانِ Unit همان رشتهی دقیقِ فصل است؛ در جدولِ
|
||
# فصلهای همان کتاب دنبالش میگردیم تا شمارهی فصل دربیاید. برابریِ رشتهای عمدیست: فیلترِ
|
||
# Qdrant هم روی همین رشتهی دقیق است، پس اگر اینجا نخورد آنجا هم نمیخورد.
|
||
unit_title = (unit.title or '').strip()
|
||
subject = unit.subject.title
|
||
for number, title in MATH_CHAPTERS.get((subject, grade_title), {}).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
|
||
کلاً رد میشد و همهی جوابهای آن صفحه عمومی بودند.
|
||
|
||
⚠️ از ۲۰۲۶-۱۰-۱۱ یک پایه چند کتاب دارد (دوازدهم: حسابان، هندسه، گسسته). این تابع فقط
|
||
«اولین کتابِ پایه» را برمیگرداند (ترتیبِ ثبتِ کتابها در `TEXTBOOKS`: ریاضی/حسابان پیش از
|
||
هندسه…)؛ دانشآموزی که در اپ «هندسه» را انتخاب کرده باید `subject` را صریح بفرستد تا از
|
||
کتابِ خودش جواب بگیرد. فصل معلوم نیست و جستجو روی کلِ همان کتاب انجام میشود.
|
||
"""
|
||
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}
|