Files
MathApp/rag/services/lesson_context.py
Zareian 8cb1ecb85e Subjects per grade (math, calculus, geometry, stats, discrete), 5 new textbooks, 202 new islands, long islands split, hand-written figures and lesson texts for the new books
- 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>
2026-10-12 05:35:18 +03:30

109 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
از یک `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}