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>
78 lines
3.4 KiB
Python
78 lines
3.4 KiB
Python
"""
|
||
فهرستِ زندهی collectionهای Qdrant + تعدادِ نقطه و مقادیرِ فیلترِ درس/فصل.
|
||
|
||
فقط خواندنی؛ هیچچیزی روی Qdrant نمینویسد. برای اینکه همیشه بدونیم چه
|
||
کتاب/درسی روی سرورِ تیمِ دیگه در دسترس است و rag/services/known_collections.py
|
||
با واقعیت همخوان است.
|
||
|
||
python manage.py rag_catalog
|
||
python manage.py rag_catalog --values # مقادیرِ lesson_number/chapter را هم چاپ کن
|
||
"""
|
||
import json
|
||
|
||
from django.core.management.base import BaseCommand
|
||
|
||
|
||
class Command(BaseCommand):
|
||
help = 'فهرستِ زندهی collectionهای Qdrant (فقط خواندنی).'
|
||
|
||
def add_arguments(self, parser):
|
||
parser.add_argument('--values', action='store_true',
|
||
help='مقادیرِ متمایزِ lesson_number/chapter هر collection را هم چاپ کن.')
|
||
parser.add_argument('--json', action='store_true', help='خروجی JSON.')
|
||
|
||
def handle(self, *args, **opts):
|
||
from rag.services.clients import RagNotConfigured, get_qdrant_client
|
||
|
||
try:
|
||
client = get_qdrant_client()
|
||
names = sorted(c.name for c in client.get_collections().collections)
|
||
except RagNotConfigured as exc:
|
||
self.stderr.write(self.style.ERROR(f'RAG پیکربندی نشده: {exc}'))
|
||
return
|
||
except Exception as exc: # noqa: BLE001
|
||
self.stderr.write(self.style.ERROR(f'اتصال به Qdrant شکست خورد: {exc}'))
|
||
return
|
||
|
||
catalog = []
|
||
for name in names:
|
||
info = client.get_collection(name)
|
||
row = {
|
||
'collection': name,
|
||
'points': info.points_count,
|
||
'vectorSize': info.config.params.vectors.size,
|
||
}
|
||
if opts['values']:
|
||
lessons, chapters = set(), set()
|
||
offset = None
|
||
while True:
|
||
pts, offset = client.scroll(collection_name=name, limit=256,
|
||
with_payload=True, with_vectors=False, offset=offset)
|
||
for p in pts:
|
||
pl = p.payload or {}
|
||
if 'lesson_number' in pl:
|
||
lessons.add(pl['lesson_number'])
|
||
if 'chapter' in pl:
|
||
chapters.add(pl['chapter'])
|
||
if offset is None:
|
||
break
|
||
if lessons:
|
||
row['lessonNumbers'] = sorted(lessons, key=lambda x: (isinstance(x, str), x))
|
||
if chapters:
|
||
row['chapters'] = sorted(chapters)
|
||
catalog.append(row)
|
||
|
||
if opts['json']:
|
||
self.stdout.write(json.dumps(catalog, ensure_ascii=False, indent=2))
|
||
return
|
||
|
||
total_points = sum(r['points'] for r in catalog)
|
||
self.stdout.write(self.style.SUCCESS(f'{len(catalog)} collection، مجموعاً {total_points} نقطه:\n'))
|
||
for r in catalog:
|
||
self.stdout.write(f" • {r['collection']} — {r['points']} نقطه (بُعد {r['vectorSize']})")
|
||
if 'lessonNumbers' in r:
|
||
self.stdout.write(f" lesson_number: {r['lessonNumbers']}")
|
||
if 'chapters' in r:
|
||
for ch in r['chapters']:
|
||
self.stdout.write(f" chapter: {ch!r}")
|