Files
MathApp/rag/services/known_collections.py
Zareian ac93e17305 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>
2026-08-29 02:29:54 +03:30

103 lines
5.0 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.
"""
کاتالوگِ دقیقِ 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