ingest whole textbooks chapter by chapter, resumably
نقشهی فیزیکیِ کتابها (افستِ صفحه، بازهی هر فصل، درسهای داخلِ فصل از فهرستِ خودِ کتاب) در rag/services/textbooks.py ثبت شد تا عنوانِ فصل از کتاب بیاید نه از حدسِ مدل — رشتهی فصل باید دقیقاً بینِ ingest و retrieval یکی باشد وگرنه نتیجه بیسروصدا خالی برمیگردد. rag_ingest_book کتاب را فصلبهفصل ingest میکند و قابلِ ازسرگیری است: rag.IngestedPage نشانهی «این صفحه پردازش شد» را نگه میدارد حتی وقتی صفحه chunkی نساخته، وگرنه هر بار اجرای دوباره بابتِ صفحههای بیمسئله دوباره پول میگرفت. --plan هزینه را رایگان برآورد میکند. rag_import_collection نقاطِ موجودِ Qdrant را در LessonChunk میریزد — فقط میخواند، رایگان و idempotent، برای پایههایی که تیمِ دیگر ingest کرده بود. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
302
rag/services/textbooks.py
Normal file
302
rag/services/textbooks.py
Normal file
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
نقشهی فیزیکیِ فایلهای PDF کتابهای درسی در data/textbooks/.
|
||||
|
||||
چرا این فایل لازم است: `ingest_pdf` یک PDF را یکتکه و با یک نامِ فصل ingest
|
||||
میکند. کتابِ کاملِ درسی چند فصل دارد، پس بدونِ نگاشتِ «فصل → بازهی صفحه»
|
||||
همهی نقاطِ کتاب زیرِ یک فصلِ اشتباه ذخیره میشدند و فیلترِ `chapter` در
|
||||
retrieval هیچوقت درست جواب نمیداد.
|
||||
|
||||
دو نکته که موقعِ افزودنِ کتابِ جدید باید حواستان باشد:
|
||||
|
||||
- `page_offset`: شمارهی صفحهای که روی کاغذ چاپ شده با شمارهی صفحه در PDF
|
||||
یکی نیست (جلد، شناسنامه، فهرست و سخنی با معلم جلوترند). فهرستِ کتاب شمارهی
|
||||
کاغذی میدهد، pdf2image شمارهی PDF میخواهد: pdf_page = book_page + offset.
|
||||
- بازهها بر اساسِ فهرستِ خودِ کتاباند و شاملِ صفحهی جداکنندهی فصل هم
|
||||
میشوند؛ صفحهی آخرِ هر فصل = یکی قبل از شروعِ فصلِ بعد.
|
||||
|
||||
مقادیرِ زیر با خواندنِ فهرست و بازبینیِ چشمیِ صفحههای مرزی از همان فایلها
|
||||
درآمدهاند (۱۴۰۵/۰۶/۲۱).
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Section:
|
||||
"""یک «درس» داخلِ فصل، همانطور که در فهرستِ کتاب آمده."""
|
||||
|
||||
number: int
|
||||
title: str
|
||||
first_page: int # شمارهی چاپشده روی کاغذ
|
||||
last_page: int = 0 # از روی درسِ بعدی پر میشود؛ صفر یعنی «تا آخرِ فصل»
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Chapter:
|
||||
number: int
|
||||
title: str # عنوانِ کاملِ فصل، دقیقاً همان رشتهای که در payload مینشیند
|
||||
first_page: int # شمارهی چاپشده روی کاغذ
|
||||
last_page: int
|
||||
sections: tuple[Section, ...] = ()
|
||||
|
||||
def resolved_sections(self) -> tuple[Section, ...]:
|
||||
"""درسها با last_page پرشده (هر درس تا یکی قبل از درسِ بعدی)."""
|
||||
out = []
|
||||
for index, section in enumerate(self.sections):
|
||||
end = (
|
||||
self.sections[index + 1].first_page - 1
|
||||
if index + 1 < len(self.sections) else self.last_page
|
||||
)
|
||||
out.append(Section(section.number, section.title, section.first_page, end))
|
||||
return tuple(out)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Textbook:
|
||||
subject: str
|
||||
grade: str # همان رشتهای که در نامِ collection میآید («نهم»)
|
||||
grade_label: str # همان رشتهای که در payloadِ `grade` مینشیند
|
||||
filename: str
|
||||
page_offset: int
|
||||
chapters: tuple[Chapter, ...]
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
return f'{self.subject} {self.grade}'.strip()
|
||||
|
||||
def pdf_pages(self, chapter: Chapter) -> tuple[int, int]:
|
||||
return chapter.first_page + self.page_offset, chapter.last_page + self.page_offset
|
||||
|
||||
def chapter(self, number: int) -> Chapter | None:
|
||||
return next((c for c in self.chapters if c.number == number), None)
|
||||
|
||||
|
||||
def _ch(*rows) -> tuple[Chapter, ...]:
|
||||
"""rows: (number, title, first_page, last_page, [(sec_no, sec_title, sec_page), ...])"""
|
||||
chapters = []
|
||||
for row in rows:
|
||||
number, title, first, last = row[:4]
|
||||
sections = tuple(Section(*s) for s in (row[4] if len(row) > 4 else ()))
|
||||
chapters.append(Chapter(number, title, first, last, sections))
|
||||
return tuple(chapters)
|
||||
|
||||
|
||||
MATH_3 = Textbook(
|
||||
subject='ریاضی', grade='سوم', grade_label='سوم دبستان',
|
||||
filename='', page_offset=0,
|
||||
chapters=_ch(
|
||||
(1, 'فصل ۱: الگوها', 0, 0),
|
||||
(2, 'فصل ۲: عددهای چهار رقمی', 0, 0),
|
||||
(3, 'فصل ۳: عددهای کسری', 0, 0),
|
||||
(4, 'فصل ۴: ضرب و تقسیم', 0, 0),
|
||||
(5, 'فصل ۵: محیط و مساحت', 0, 0),
|
||||
(6, 'فصل ۶: جمع و تفریق', 0, 0),
|
||||
(7, 'فصل ۷: آمار و احتمال', 0, 0),
|
||||
(8, 'فصل ۸: ضرب عددها', 0, 0),
|
||||
),
|
||||
)
|
||||
|
||||
# هشتم تنها کتابیست که هم PDF دارد و هم روی Qdrant نیست — یعنی فصلبندی و
|
||||
# درسهایش کاملاً از فهرستِ خودِ کتاب میآید و ingest دقیقاً همین رشتهها را
|
||||
# مینویسد. «مرور فصل» جزء آخرین درسِ هر فصل حساب شده است.
|
||||
MATH_8 = Textbook(
|
||||
subject='ریاضی', grade='هشتم', grade_label='هشتم',
|
||||
filename='ریاضی هشتم.pdf', page_offset=8,
|
||||
chapters=_ch(
|
||||
(1, 'فصل ۱: عددهای صحیح و گویا', 1, 18, (
|
||||
(1, 'یادآوری عددهای صحیح', 2),
|
||||
(2, 'معرفی عددهای گویا', 6),
|
||||
(3, 'جمع و تفریق عددهای گویا', 10),
|
||||
(4, 'ضرب و تقسیم عددهای گویا', 14),
|
||||
)),
|
||||
(2, 'فصل ۲: عددهای اول', 19, 28, (
|
||||
(1, 'یادآوری عددهای اول', 20),
|
||||
(2, 'تعیین عددهای اول', 24),
|
||||
)),
|
||||
(3, 'فصل ۳: چندضلعیها', 29, 50, (
|
||||
(1, 'چندضلعیها و تقارن', 30),
|
||||
(2, 'توازی و تعامد', 34),
|
||||
(3, 'چهارضلعیها', 38),
|
||||
(4, 'زاویههای داخلی', 42),
|
||||
(5, 'زاویههای خارجی', 46),
|
||||
)),
|
||||
(4, 'فصل ۴: جبر و معادله', 51, 68, (
|
||||
(1, 'ساده کردن عبارتهای جبری', 52),
|
||||
(2, 'پیدا کردن مقدار یک عبارت جبری', 56),
|
||||
(3, 'تجزیه عبارتهای جبری', 60),
|
||||
(4, 'معادله', 64),
|
||||
)),
|
||||
(5, 'فصل ۵: بردار و مختصات', 69, 82, (
|
||||
(1, 'جمع بردارها', 70),
|
||||
(2, 'ضرب عدد در بردار', 74),
|
||||
(3, 'بردارهای واحد مختصات', 78),
|
||||
)),
|
||||
(6, 'فصل ۶: مثلث', 83, 100, (
|
||||
(1, 'رابطهٔ فیثاغورس', 84),
|
||||
(2, 'شکلهای همنهشت', 88),
|
||||
(3, 'مثلثهای همنهشت', 92),
|
||||
(4, 'همنهشتی مثلثهای قائمالزاویه', 96),
|
||||
)),
|
||||
(7, 'فصل ۷: توان و جذر', 101, 118, (
|
||||
(1, 'توان', 102),
|
||||
(2, 'تقسیم اعداد تواندار', 106),
|
||||
(3, 'جذر تقریبی', 110),
|
||||
(4, 'نمایش اعداد رادیکالی روی محور اعداد', 114),
|
||||
(5, 'خواص ضرب و تقسیم رادیکالها', 115),
|
||||
)),
|
||||
(8, 'فصل ۸: آمار و احتمال', 119, 136, (
|
||||
(1, 'دستهبندی دادهها', 120),
|
||||
(2, 'میانگین دادهها', 124),
|
||||
(3, 'احتمال یا اندازهگیری شانس', 128),
|
||||
(4, 'بررسی حالتهای ممکن', 132),
|
||||
)),
|
||||
(9, 'فصل ۹: دایره', 137, 150, (
|
||||
(1, 'خط و دایره', 138),
|
||||
(2, 'زاویههای مرکزی', 142),
|
||||
(3, 'زاویههای محاطی', 146),
|
||||
)),
|
||||
),
|
||||
)
|
||||
|
||||
MATH_9 = Textbook(
|
||||
subject='ریاضی', grade='نهم', grade_label='نهم',
|
||||
filename='ریاضی نهم.pdf', page_offset=8,
|
||||
chapters=_ch(
|
||||
(1, 'فصل ۱: مجموعهها', 1, 17, (
|
||||
(1, 'معرفی مجموعه', 2),
|
||||
(2, 'مجموعههای برابر و نمایش مجموعهها', 6),
|
||||
(3, 'اجتماع، اشتراک و تفاضلِ مجموعهها', 11),
|
||||
(4, 'مجموعهها و احتمال', 15),
|
||||
)),
|
||||
(2, 'فصل ۲: عددهای حقیقی', 18, 31, (
|
||||
(1, 'عددهای گویا', 19),
|
||||
(2, 'عددهای حقیقی', 23),
|
||||
(3, 'قدر مطلق و محاسبهٔ تقریبی', 28),
|
||||
)),
|
||||
(3, 'فصل ۳: استدلال و اثبات در هندسه', 32, 58, (
|
||||
(1, 'استدلال', 33),
|
||||
(2, 'آشنایی با اثبات در هندسه', 37),
|
||||
(3, 'همنهشتی مثلثها', 44),
|
||||
(4, 'حل مسئله در هندسه', 49),
|
||||
(5, 'شکلهای متشابه', 53),
|
||||
)),
|
||||
(4, 'فصل ۴: توان و ریشه', 59, 77, (
|
||||
(1, 'توان صحیح', 60),
|
||||
(2, 'نماد علمی', 65),
|
||||
(3, 'ریشهگیری', 68),
|
||||
(4, 'جمع و تفریق رادیکالها', 73),
|
||||
)),
|
||||
(5, 'فصل ۵: عبارتهای جبری', 78, 94, (
|
||||
(1, 'عبارتهای جبری و مفهوم اتحاد', 79),
|
||||
(2, 'چند اتحاد دیگر، تجزیه و کاربردها', 86),
|
||||
(3, 'نابرابریها و نامعادلهها', 90),
|
||||
)),
|
||||
(6, 'فصل ۶: خط و معادلههای خطی', 95, 112, (
|
||||
(1, 'معادلهٔ خط', 96),
|
||||
(2, 'شیب خط و عرض از مبدأ', 102),
|
||||
(3, 'دستگاه معادلههای خطی', 108),
|
||||
)),
|
||||
(7, 'فصل ۷: عبارتهای گویا', 113, 129, (
|
||||
(1, 'معرفی و ساده کردن عبارتهای گویا', 114),
|
||||
(2, 'محاسبات عبارتهای گویا', 119),
|
||||
(3, 'تقسیم چندجملهایها', 126),
|
||||
)),
|
||||
(8, 'فصل ۸: حجم و مساحت', 130, 143, (
|
||||
(1, 'حجم و مساحت کره', 131),
|
||||
(2, 'حجم هرم و مخروط', 135),
|
||||
(3, 'سطح و حجم', 140),
|
||||
)),
|
||||
),
|
||||
)
|
||||
|
||||
MATH_10 = Textbook(
|
||||
subject='ریاضی', grade='دهم', grade_label='دهم',
|
||||
filename='ریاضی دهم.pdf', page_offset=6,
|
||||
chapters=_ch(
|
||||
(1, 'فصل ۱: مجموعه، الگو و دنباله', 1, 27, (
|
||||
(1, 'مجموعههای متناهی و نامتناهی', 2),
|
||||
(2, 'متمم یک مجموعه', 8),
|
||||
(3, 'الگو و دنباله', 14),
|
||||
(4, 'دنبالههای حسابی و هندسی', 21),
|
||||
)),
|
||||
(2, 'فصل ۲: مثلثات', 28, 46, (
|
||||
(1, 'نسبتهای مثلثاتی', 29),
|
||||
(2, 'دایرهٔ مثلثاتی', 36),
|
||||
(3, 'روابط بین نسبتهای مثلثاتی', 42),
|
||||
)),
|
||||
(3, 'فصل ۳: توانهای گویا و عبارتهای جبری', 47, 68, (
|
||||
(1, 'ریشه و توان', 48),
|
||||
(2, 'ریشهٔ nام', 54),
|
||||
(3, 'توانهای گویا', 59),
|
||||
(4, 'عبارتهای جبری', 62),
|
||||
)),
|
||||
(4, 'فصل ۴: معادلهها و نامعادلهها', 69, 93, (
|
||||
(1, 'معادلهٔ درجه دوم و روشهای مختلف حل آن', 70),
|
||||
(2, 'سهمی', 78),
|
||||
(3, 'تعیین علامت', 83),
|
||||
)),
|
||||
(5, 'فصل ۵: تابع', 94, 117, (
|
||||
(1, 'مفهوم تابع و بازنماییهای آن', 95),
|
||||
(2, 'دامنه و برد توابع', 101),
|
||||
(3, 'انواع تابع', 109),
|
||||
)),
|
||||
(6, 'فصل ۶: شمارش، بدون شمردن', 118, 140, (
|
||||
(1, 'شمارش', 119),
|
||||
(2, 'جایگشت', 127),
|
||||
(3, 'ترکیب', 133),
|
||||
)),
|
||||
(7, 'فصل ۷: آمار و احتمال', 141, 170, (
|
||||
(1, 'احتمال یا اندازهگیری شانس', 142),
|
||||
(2, 'مقدمهای بر علم آمار، جامعه و نمونه', 152),
|
||||
(3, 'متغیر و انواع آن', 159),
|
||||
)),
|
||||
),
|
||||
)
|
||||
|
||||
# پایههای ششم و هفتم را تیمِ دیگر ingest کرده و روی Qdrant هستند؛ فایلِ PDF
|
||||
# برایشان لازم نیست و بازهی صفحه هم نداریم (نقاطشان شمارهی صفحه ندارند). فقط
|
||||
# فصلبندی ثبت میشود تا `seed_curriculum_from_textbook` بتواند برایشان برنامهی
|
||||
# درسی بسازد؛ `filename` خالی یعنی «این کتاب از مسیرِ PDF ingest نمیشود».
|
||||
MATH_6 = Textbook(
|
||||
subject='ریاضی', grade='ششم', grade_label='ششم دبستان',
|
||||
filename='', page_offset=0,
|
||||
chapters=_ch(
|
||||
(1, 'فصل ۱: عدد و الگوهای عددی', 0, 0),
|
||||
(2, 'فصل ۲: کسر', 0, 0),
|
||||
(3, 'فصل ۳: اعداد اعشاری', 0, 0),
|
||||
(4, 'فصل ۴: تقارن و مختصات', 0, 0),
|
||||
(5, 'فصل ۵: اندازه گیری', 0, 0),
|
||||
(6, 'فصل ۶: تناسب و درصد', 0, 0),
|
||||
(7, 'فصل ۷: تقریب', 0, 0),
|
||||
),
|
||||
)
|
||||
|
||||
MATH_7 = Textbook(
|
||||
subject='ریاضی', grade='هفتم', grade_label='هفتم',
|
||||
filename='', page_offset=0,
|
||||
chapters=_ch(
|
||||
(1, 'فصل ۱: راهبردهای حل مسئله', 0, 0),
|
||||
(2, 'فصل ۲: عددهای صحیح', 0, 0),
|
||||
(3, 'فصل ۳: جبر و معادله', 0, 0),
|
||||
(4, 'فصل ۴: هندسه و استدلال', 0, 0),
|
||||
(5, 'فصل ۵: شمارندهها و اعداد اول', 0, 0),
|
||||
(6, 'فصل ۶: سطح و حجم', 0, 0),
|
||||
(7, 'فصل ۷: توان و جذر', 0, 0),
|
||||
(8, 'فصل ۸: بردار و مختصات', 0, 0),
|
||||
(9, 'فصل ۹: آمار و احتمال', 0, 0),
|
||||
),
|
||||
)
|
||||
|
||||
TEXTBOOKS: dict[str, Textbook] = {
|
||||
b.key: b for b in (MATH_3, MATH_6, MATH_7, MATH_8, MATH_9, MATH_10)
|
||||
}
|
||||
|
||||
# کتابهایی که واقعاً فایلِ PDF دارند و `rag_ingest_book` رویشان کار میکند.
|
||||
INGESTABLE = {b.key: b for b in TEXTBOOKS.values() if b.filename}
|
||||
|
||||
|
||||
def get_textbook(subject: str, grade: str) -> Textbook | None:
|
||||
return TEXTBOOKS.get(f'{(subject or "").strip()} {(grade or "").strip()}')
|
||||
|
||||
|
||||
def chapter_titles(book: Textbook) -> dict[int, str]:
|
||||
return {c.number: c.title for c in book.chapters}
|
||||
Reference in New Issue
Block a user