Files
MathApp/rag/models.py
Zareian c79be0a336 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>
2026-09-13 18:59:03 +03:30

117 lines
5.8 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.
import uuid
from django.db import models
class LessonChunk(models.Model):
"""
یک تکه‌ی درسیِ خلاصه‌شده که هم در همین جدول (منبعِ حقیقت) و هم به‌صورتِ
یک نقطه در Qdrant (فقط index، قابلِ بازسازی) ذخیره می‌شه.
برداشتِ پاک‌سازی‌شده از subjects.LessonChunk مخزنِ Rag_backend، با این
تفاوت‌ها طبق معماریِ این پروژه:
- هیچ FK بین‌اپی نداره؛ lesson_id فقط یک ارجاعِ سبک به content.Lesson است
(دقیقاً مثل Conversation.lesson_id در ai_tutor).
- subject/grade به‌صورتِ رشته/عدد نگه‌داری می‌شن چون مدلِ content ما مدلِ
Subject نداره و نامِ collectionِ تیمِ دیگه بر پایه‌ی همین رشته‌هاست.
- embedding_vector هم ذخیره می‌شه تا collectionِ Qdrant بدونِ فراخوانیِ
دوباره‌ی embedding قابلِ بازسازی باشه.
"""
# ارجاع سبک به content.Lesson. برای ingestِ کتابِ کامل خالی می‌ماند: آن
# مسیر فصل‌به‌فصل کار می‌کند و هنوز رکوردِ Lessonی برایش وجود ندارد.
lesson_id = models.IntegerField(db_index=True, null=True, blank=True)
grade_id = models.IntegerField(null=True, blank=True) # ارجاع سبک به content.Grade
subject = models.CharField(max_length=100)
lesson_number = models.IntegerField()
chapter = models.IntegerField(null=True, blank=True)
# عنوانِ کاملِ فصل، دقیقاً همون رشته‌ای که در payloadِ تیمِ دیگه هست
# («فصل ۲: کسر»). retrieval برای ریاضی روی همین مقدار فیلتر می‌کنه، پس
# بدونِ این هر چیزی که ingest کنیم در جستجو پیدا نمی‌شه.
chapter_title = models.CharField(max_length=255, blank=True)
grade_title = models.CharField(max_length=100, blank=True)
order = models.PositiveIntegerField()
title = models.CharField(max_length=255)
original_text = models.TextField()
summary = models.TextField()
embedding_vector = models.JSONField(null=True, blank=True)
# --- ساختارِ استخراج‌شده از کتاب (همون قراردادِ ریاضیِ تیمِ دیگه) -------
# vision یک‌بار و با هزینه این‌ها رو درمی‌آره؛ اگه فقط متنِ صاف‌شده رو
# نگه داریم، برای هر بازسازی باید دوباره همون هزینه رو بدیم. ضمناً
# problem/solution ورودیِ مستقیمِ ساختِ سؤال‌های مسیرِ درس‌اند.
problem_type = models.CharField(max_length=255, blank=True)
keywords = models.JSONField(default=list, blank=True)
general_solution = models.TextField(blank=True)
problem = models.TextField(blank=True)
solution = models.TextField(blank=True)
# شماره‌ی صفحه‌ی PDF که این chunk از آن درآمده. ingestِ کتاب با همین فیلد
# می‌فهمد کدام صفحه‌ها قبلاً پردازش شده‌اند و از سر می‌گیرد؛ بدونش هر
# اجرای دوباره پس از قطعی، نقاطِ تکراری در Qdrant می‌ساخت.
source_page = models.PositiveIntegerField(null=True, blank=True, db_index=True)
collection = models.CharField(max_length=200, db_index=True)
qdrant_point_id = models.UUIDField(default=uuid.uuid4, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['lesson_id', 'order']
constraints = [
models.UniqueConstraint(
fields=['collection', 'qdrant_point_id'],
name='uniq_lesson_chunk_qdrant_point',
),
]
indexes = [
models.Index(fields=['subject', 'lesson_number']),
models.Index(fields=['collection', 'chapter_title', 'source_page']),
]
def __str__(self):
return f"chunk-lesson{self.lesson_id}-{self.order} ({self.subject})"
class LessonChunkImage(models.Model):
"""تصویرِ پیوست به یک chunk (برای محتوای چندوجهیِ آینده)."""
chunk = models.ForeignKey(LessonChunk, on_delete=models.CASCADE, related_name='images')
img_url = models.TextField()
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"image-chunk{self.chunk_id}"
class IngestedPage(models.Model):
"""
نشانه‌ی «این صفحه‌ی کتاب پردازش شد».
چرا جدا از LessonChunk: صفحه‌هایی که هیچ مسئله‌ای ندارند (جلد، فهرست،
سخنی با معلم، صفحه‌ی جداکننده‌ی فصل) هیچ chunkی نمی‌سازند. اگر ازسرگیری
فقط به chunkها نگاه کند، این صفحه‌ها در هر اجرای دوباره یک فراخوانیِ
vision دیگر خرج می‌کنند و هیچ‌وقت هم «تمام» نمی‌شوند.
"""
collection = models.CharField(max_length=200, db_index=True)
chapter_title = models.CharField(max_length=255)
page = models.PositiveIntegerField()
chunk_count = models.PositiveSmallIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['collection', 'page'],
name='uniq_ingested_page_per_collection',
),
]
ordering = ['collection', 'page']
def __str__(self):
return f"{self.collection} p{self.page} ({self.chunk_count} chunk)"