new app — cleaned-up RAG pipeline (Qdrant retrieval + GPT), ported from Rag_backend
Self-contained app; wired into settings/urls in the next commit. Data
stays in the parallel team's live Qdrant — nothing is copied into our DB.
apps.py
- RagConfig.
models.py
- LessonChunk: DB-of-record for one summarized lesson chunk
(original_text + summary + embedding_vector) mirrored as a single
Qdrant point; loose lesson_id/grade_id ints, no cross-app FK.
LessonChunkImage for future multimodal lessons.
migrations/0001_initial.py
- initial tables for the two models.
services/clients.py
- lazy, lru_cache'd OpenAI + Qdrant clients (Rag_backend builds them at
import time and crashes without keys). RagNotConfigured raised when a
key/lib is missing; Qdrant client uses check_compatibility=False for
the 1.19-client / 1.17-server minor version gap.
services/config.py
- rag_is_configured(), collection-name template, grade-title resolution
(RAG_GRADE_TITLE_OVERRIDES -> content.Grade.title), retrieval top-k.
services/known_collections.py
- exact live Qdrant catalog: 6 collection names, math chapter-number ->
exact "فصل N: ..." string tables per (subject, grade), and the
علوم/اجتماعی lesson-number -> title maps. filter_key_for_subject()
picks `chapter` for math, `lesson_number` otherwise;
resolve_math_chapter() maps a plain chapter number (Persian or Latin
digits) to the stored string.
services/embedding.py
- embed_text() pinned to OPENAI_EMBEDDING_MODEL (text-embedding-3-small,
1536d) so vectors match the other team's existing collections.
services/chunking.py
- split_text_into_chunks(), summarize_chunk() (gpt-4o-mini, JSON out);
plain exceptions instead of ninja HttpError.
services/qdrant_store.py
- ensure_collection(), upsert_points(), search(): the lesson filter
matches the value both as str and native, since Qdrant stores
lesson_number as int and chapter as string.
services/retrieval.py
- resolve_target() -> (collection, filter_key, filter_value), reused by
the cache key and debug block.
- retrieve_context() -> (chunks, meta): embed -> Qdrant search scoped to
one lesson/chapter -> top-k. Math payloads (no `summary`) fall back to
generalSolution/problem/solution; a 404 collection returns empty.
services/generation.py
- generate_answer() and stream_answer() (token iterator for SSE);
context-grounded Persian prompt that explains from retrieved text
rather than refusing.
services/pdf.py
- textbook PDF -> page images -> gpt-4o vision problem-type extraction
(adapted from Rag_backend mathapp); returns dicts, no on-disk JSON.
pymupdf/pdf2image optional — missing libs raise RagNotConfigured.
services/ingest.py
- ingest_text() / ingest_pdf(): chunk -> summarize -> embed -> persist
LessonChunk + upsert Qdrant, in one transaction.
services/answer_cache.py
- short-TTL cache of the final answer keyed by
sha1(collection | filter_value | normalized-question).
services/tutor.py
- thin answer_question() (retrieve -> generate) for direct callers.
serializers.py, urls.py, views.py
- POST /api/rag/ingest/text, POST /api/rag/ingest/pdf (HasInternalServiceKey,
throttle scope rag_ingest), GET /api/rag/chunks review list.
RagProviderUnavailable -> clean 503.
admin.py
- LessonChunk admin with an inline for its images.
management/commands/rag_catalog.py
- read-only live Qdrant listing (--values, --json); never writes.
tests.py
- mocked-client tests: ask-with-retrieval, ingest, SSE stream, answer
cache, and rag_is_configured() gating.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
102
rag/services/known_collections.py
Normal file
102
rag/services/known_collections.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
کاتالوگِ دقیقِ collectionهای موجود روی Qdrant تیمِ دیگر
|
||||
(qdrant.tadrisino2.ir) — استخراجشده و بررسیشده در ۱۴۰۵/۰۶/۰۶ (2026-08-28).
|
||||
|
||||
هدف: کاربر لازم نباشه رشتههای دقیقِ گرید/فصل رو حفظ کنه. با داشتنِ subject و
|
||||
یک شمارهی ساده (شمارهی درس یا شمارهی فصل)، retrieval خودش:
|
||||
- کلیدِ فیلترِ درست رو انتخاب میکنه (ریاضی → chapter ، بقیه → lesson_number)
|
||||
- برای ریاضی، شمارهی فصل رو به رشتهی دقیقِ فصل تبدیل میکنه.
|
||||
|
||||
اگه تیمِ دیگه collection جدید اضافه/تغییر داد، فقط همین فایل بهروز بشه.
|
||||
"""
|
||||
|
||||
# --- درسهای متنی: payload کلیدِ `lesson_number` (رشته) + `summary` دارد ---
|
||||
# نامِ collection = f"{subject} {grade}".lower()
|
||||
TEXT_SUBJECT_LESSON_FILTER_KEY = 'lesson_number'
|
||||
|
||||
# --- ریاضی: payload کلیدِ `chapter` (رشته) + `generalSolution` دارد، `summary` ندارد ---
|
||||
MATH_LESSON_FILTER_KEY = 'chapter'
|
||||
|
||||
# subjectهایی که ساختارِ «ریاضی» دارند (فیلتر با chapter، متن از generalSolution)
|
||||
MATH_SUBJECTS = {'ریاضی'}
|
||||
|
||||
# فصول ریاضی به تفکیکِ (subject, grade) — کلیدِ داخلی هم شمارهی فصل و هم
|
||||
# رشتهی کامل رو میپذیره.
|
||||
MATH_CHAPTERS = {
|
||||
('ریاضی', 'سوم'): {
|
||||
1: 'فصل ۱: الگوها',
|
||||
2: 'فصل ۲: عددهای چهار رقمی',
|
||||
3: 'فصل ۳: عددهای کسری',
|
||||
4: 'فصل ۴: ضرب و تقسیم',
|
||||
5: 'فصل ۵: محیط و مساحت',
|
||||
6: 'فصل ۶: جمع و تفریق',
|
||||
7: 'فصل ۷: آمار و احتمال',
|
||||
8: 'فصل ۸: ضرب عددها',
|
||||
},
|
||||
('ریاضی', 'ششم'): {
|
||||
1: 'فصل ۱: عدد و الگوهای عددی',
|
||||
2: 'فصل ۲: کسر',
|
||||
3: 'فصل ۳: اعداد اعشاری',
|
||||
4: 'فصل ۴: تقارن و مختصات',
|
||||
5: 'فصل ۵: اندازه گیری',
|
||||
6: 'فصل ۶: تناسب و درصد',
|
||||
7: 'فصل ۷: تقریب',
|
||||
},
|
||||
('ریاضی', 'هفتم'): {
|
||||
1: 'فصل ۱: راهبردهای حل مسئله',
|
||||
2: 'فصل ۲: عددهای صحیح',
|
||||
3: 'فصل ۳: جبر و معادله',
|
||||
4: 'فصل ۴: هندسه و استدلال',
|
||||
5: 'فصل ۵: شمارندهها و اعداد اول',
|
||||
6: 'فصل ۶: سطح و حجم',
|
||||
7: 'فصل ۷: توان و جذر',
|
||||
8: 'فصل ۸: بردار و مختصات',
|
||||
9: 'فصل ۹: آمار و احتمال',
|
||||
},
|
||||
}
|
||||
|
||||
# درسهای علوم/اجتماعی — فقط برای مرجع/اعتبارسنجی (lesson_number → عنوان).
|
||||
TEXT_LESSONS = {
|
||||
('علوم تجربی', 'سوم'): {
|
||||
1: 'زنگ علوم', 2: 'خوراکیها', 3: 'اندازهگیری مواد', 4: 'مواد اطراف ما',
|
||||
5: 'آب مادهی باارزش', 6: 'زندگی ما و آب', 7: 'نور و مشاهدهی اجسام',
|
||||
8: 'جستجو کنیم و بسازیم', 9: 'نیرو همهجا ۱', 10: 'نیرو همهجا ۲',
|
||||
11: 'بکارید و ببینید', 12: 'هرکدام جای خود ۱', 13: 'هرکدام جای خود ۲',
|
||||
14: 'از گذشته تا آینده(نگهداری مواد غذایی)',
|
||||
},
|
||||
('مطالعات اجتماعی', 'ششم'): {1: 'lesson_1', 2: 'lesson_2', 3: 'lesson_3', 9: 'lesson_9'},
|
||||
('مطالعات اجتماعی', 'چهارم'): {1: 'lesson_1', 2: 'lesson_2', 3: 'lesson_3'},
|
||||
}
|
||||
|
||||
# فهرستِ کاملِ collectionهای موجود (نامِ نهایی، lowercase).
|
||||
KNOWN_COLLECTIONS = {
|
||||
'ریاضی سوم', 'ریاضی ششم', 'ریاضی هفتم',
|
||||
'علوم تجربی سوم', 'مطالعات اجتماعی ششم', 'مطالعات اجتماعی چهارم',
|
||||
}
|
||||
|
||||
|
||||
def is_math_subject(subject: str) -> bool:
|
||||
return (subject or '').strip() in MATH_SUBJECTS
|
||||
|
||||
|
||||
def filter_key_for_subject(subject: str) -> str:
|
||||
return MATH_LESSON_FILTER_KEY if is_math_subject(subject) else TEXT_SUBJECT_LESSON_FILTER_KEY
|
||||
|
||||
|
||||
def resolve_math_chapter(subject: str, grade: str, lesson_or_chapter) -> str | None:
|
||||
"""
|
||||
برای ریاضی: ورودی میتونه شمارهی فصل (2 / '2' / '۲') یا خودِ رشتهی
|
||||
کامل باشه. خروجی رشتهی دقیقِ `chapter` در Qdrant یا None.
|
||||
"""
|
||||
if lesson_or_chapter is None:
|
||||
return None
|
||||
table = MATH_CHAPTERS.get((( subject or '').strip(), (grade or '').strip()))
|
||||
text = str(lesson_or_chapter).strip()
|
||||
if text.startswith('فصل'):
|
||||
return text
|
||||
# نرمالسازی ارقامِ فارسی → لاتین
|
||||
trans = str.maketrans('۰۱۲۳۴۵۶۷۸۹', '0123456789')
|
||||
digits = text.translate(trans)
|
||||
if table and digits.isdigit() and int(digits) in table:
|
||||
return table[int(digits)]
|
||||
return text or None
|
||||
Reference in New Issue
Block a user