نقشهی فیزیکیِ کتابها (افستِ صفحه، بازهی هر فصل، درسهای داخلِ فصل از فهرستِ خودِ کتاب) در 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>
117 lines
5.8 KiB
Python
117 lines
5.8 KiB
Python
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)"
|