register rag app, RAG provider settings, autoload .env

settings.py
  - load .env via python-dotenv (override=False) so local runs and the RAG
    keys work without `export`; production/docker env still wins.
  - add 'rag' to INSTALLED_APPS.
  - new RAG / AI provider block: OPENAI_API_KEY / OPENAI_BASE_URL (GapGPT)
    / OPENAI_CHAT_MODEL / OPENAI_VISION_MODEL / OPENAI_EMBEDDING_MODEL /
    OPENAI_TIMEOUT_SECONDS, QDRANT_URL / QDRANT_API_KEY,
    RAG_COLLECTION_NAME_TEMPLATE / RAG_GRADE_TITLE_OVERRIDES /
    RAG_RETRIEVAL_TOP_K / RAG_FORCE_MOCK / RAG_ANSWER_CACHE_TTL.
  - add 'rag_ingest' to DEFAULT_THROTTLE_RATES.
  - under `manage.py test`: force RAG_FORCE_MOCK on and RAG_ANSWER_CACHE_TTL
    to 0 so no test hits OpenAI/Qdrant or a warm cache.
urls.py
  - mount rag.urls under /api/.
requirements.txt
  - python-dotenv; openai, qdrant-client; pymupdf / pdf2image / pillow
    (optional, only for PDF ingestion).
.env.example
  - document the OPENAI_* / QDRANT_* / RAG_* keys, the discovered Qdrant
    collection names, and the parked OPENAI_API_KEY_OPENAI_DIRECT slot
    (no provider-switch logic by design).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 02:30:12 +03:30
parent ac93e17305
commit ff47c8d4ba
4 changed files with 111 additions and 0 deletions

View File

@@ -18,3 +18,38 @@ KAVENEGAR_API_KEY=
# رمزِ ادمینِ Grafana (کاربرِ admin) — فقط برای دسترسیِ محلی # رمزِ ادمینِ Grafana (کاربرِ admin) — فقط برای دسترسیِ محلی
GRAFANA_ADMIN_PASSWORD=admin GRAFANA_ADMIN_PASSWORD=admin
# -----------------------------------------------------------------------
# RAG / AI tutor (اپ rag) — موقعِ تست پر کنید
# -----------------------------------------------------------------------
# بدونِ OPENAI_API_KEY و QDRANT_API_KEY، مسیرِ RAG غیرفعاله و
# POST /api/ai/ask همون پاسخِ stubِ قبلی رو می‌ده (سازگاریِ عقب‌رو).
# فعلاً از GapGPT (پروکسیِ OpenAI برای ایران) استفاده می‌کنیم.
OPENAI_API_KEY=
OPENAI_BASE_URL=https://api.gapgpt.app/v1
# اگه بعداً خواستی مستقیم OpenAI واقعی رو تست کنی: کلیدِ واقعی رو این پایین
# پارک کن، بعد فقط دو خطِ بالا رو باهاش عوض کن. هیچ منطقِ سوییچی در کد نیست
# تا ساده و بی‌اشتباه بمونه.
# OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_API_KEY_OPENAI_DIRECT=
OPENAI_CHAT_MODEL=gpt-4o-mini
OPENAI_VISION_MODEL=gpt-4o
# این رو تغییر نده؛ باید با مدلی که تیمِ دیگه باهاش collectionها رو ساخته یکی باشه.
OPENAI_EMBEDDING_MODEL=text-embedding-3-small
QDRANT_URL=https://qdrant.tadrisino2.ir:443
QDRANT_API_KEY=
# نامِ collection = f"{subject} {grade}".lower() . collectionهای موجود روی
# سرورِ تیمِ دیگه (بررسی‌شده): «علوم تجربی سوم» ، «مطالعات اجتماعی چهارم» ،
# «مطالعات اجتماعی ششم» ، «ریاضی سوم» ، «ریاضی ششم» ، «ریاضی هفتم».
# یعنی جزءِ گرید فقط «سوم/چهارم/ششم/هفتم» است (بدونِ «دبستان»).
#
# کلیدِ فیلترِ درس دیگر دستی تنظیم نمی‌شه — rag/services/known_collections.py
# خودش تشخیص می‌ده: ریاضی → `chapter` (و شماره‌ی فصل رو به رشته‌ی دقیق تبدیل
# می‌کنه)، بقیه → `lesson_number`. RAG_QDRANT_LESSON_FILTER_KEY فقط برای
# سازگاریِ عقب‌رو مونده و استفاده نمی‌شه.
RAG_COLLECTION_NAME_TEMPLATE={subject} {grade}
RAG_RETRIEVAL_TOP_K=4
# RAG_FORCE_MOCK=1 یعنی حتی با وجودِ کلیدها هم مسیرِ RAG خاموش بمونه.
RAG_FORCE_MOCK=

View File

@@ -18,6 +18,17 @@ from django.core.exceptions import ImproperlyConfigured
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# مقادیرِ .env رو داخلِ os.environ می‌ذاره تا os.environ.get های پایین کار کنن.
# override=False: env واقعیِ سیستم و مقادیرِ docker-compose همچنان اولویت دارن،
# پس رفتارِ پروداکشن/داکر عوض نمی‌شه؛ این فقط برای اجرای محلی (بدون export) و
# کلیدهای RAG (accounts/... , OPENAI_API_KEY, QDRANT_API_KEY, ...) هست.
try:
from dotenv import load_dotenv
load_dotenv(BASE_DIR / '.env', override=False)
except ModuleNotFoundError:
pass
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
@@ -83,6 +94,10 @@ INSTALLED_APPS = [
# Tadrisino apps (مدلِ مینیمالِ ذخیره‌سازیِ کمپین‌های پیامی، بدونِ منطقِ ارسال) # Tadrisino apps (مدلِ مینیمالِ ذخیره‌سازیِ کمپین‌های پیامی، بدونِ منطقِ ارسال)
'campaigns', 'campaigns',
# Tadrisino apps (لایه‌ی RAG: ذخیره‌ی chunkهای درسی + بازیابی از Qdrant،
# مصرف‌کننده‌ش ai_tutor.generate_tutor_answer است)
'rag',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -239,6 +254,7 @@ REST_FRAMEWORK = {
'guest_session': '20/hour', 'guest_session': '20/hour',
'bazaar_verify': '10/hour', 'bazaar_verify': '10/hour',
'ai_ask': '20/hour', 'ai_ask': '20/hour',
'rag_ingest': '30/hour',
'purchase': '10/hour', 'purchase': '10/hour',
'token_refresh': '30/hour', 'token_refresh': '30/hour',
'question_answer': '60/min', 'question_answer': '60/min',
@@ -355,6 +371,52 @@ BAZAAR_CLIENT_SECRET = os.environ.get('BAZAAR_CLIENT_SECRET', '')
BAZAAR_REFRESH_TOKEN = os.environ.get('BAZAAR_REFRESH_TOKEN', '') BAZAAR_REFRESH_TOKEN = os.environ.get('BAZAAR_REFRESH_TOKEN', '')
BAZAAR_API_BASE = os.environ.get('BAZAAR_API_BASE', 'https://pardakht.cafebazaar.ir/devapi/v2') BAZAAR_API_BASE = os.environ.get('BAZAAR_API_BASE', 'https://pardakht.cafebazaar.ir/devapi/v2')
# -----------------------------------------------------------------------
# RAG / AI provider (اپ rag + ai_tutor.generate_tutor_answer)
# -----------------------------------------------------------------------
# provider همون OpenAI است ولی از طریقِ پروکسیِ ایرانیِ GapGPT (base_url).
# بدونِ OPENAI_API_KEY و QDRANT_API_KEY، rag_is_configured() برابرِ False
# می‌شه و generate_tutor_answer به همون پاسخِ stubِ قبلی برمی‌گرده (پس
# تست‌های موجود و اجرای بدونِ کلید دست‌نخورده کار می‌کنن).
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '')
OPENAI_BASE_URL = os.environ.get('OPENAI_BASE_URL', 'https://api.gapgpt.app/v1')
OPENAI_CHAT_MODEL = os.environ.get('OPENAI_CHAT_MODEL', 'gpt-4o-mini')
OPENAI_VISION_MODEL = os.environ.get('OPENAI_VISION_MODEL', 'gpt-4o')
# نباید تغییر کنه مگر اینکه collectionهای Qdrant هم با همون مدلِ جدید بازساخته بشن.
OPENAI_EMBEDDING_MODEL = os.environ.get('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small')
OPENAI_EMBEDDING_DIM = int(os.environ.get('OPENAI_EMBEDDING_DIM', '1536'))
OPENAI_TIMEOUT_SECONDS = int(os.environ.get('OPENAI_TIMEOUT_SECONDS', '60'))
QDRANT_URL = os.environ.get('QDRANT_URL', 'https://qdrant.tadrisino2.ir:443')
QDRANT_API_KEY = os.environ.get('QDRANT_API_KEY', '')
# نامِ collection در دادهٔ تیمِ دیگه = f"{subject} {grade}".lower() . کلیدِ
# فیلترِ درس خودکار انتخاب می‌شه (rag/services/known_collections.py): ریاضی →
# `chapter` ، بقیه → `lesson_number`.
RAG_COLLECTION_NAME_TEMPLATE = os.environ.get('RAG_COLLECTION_NAME_TEMPLATE', '{subject} {grade}')
# نگاشتِ اختیاریِ gradeId → عنوانِ دقیقِ گرید همان‌طور که در نامِ collection
# تیمِ دیگه اومده (مثلاً {6: 'ششم دبستان'})؛ اگه خالی باشه از content.Grade.title
# استفاده می‌شه. کاربر قبلاً با نامِ نادرستِ گرید به مشکل خورده بود.
RAG_GRADE_TITLE_OVERRIDES = {}
RAG_RETRIEVAL_TOP_K = int(os.environ.get('RAG_RETRIEVAL_TOP_K', '4'))
RAG_FORCE_MOCK = os.environ.get('RAG_FORCE_MOCK', '') == '1'
# کشِ پاسخِ نهایی برای سوالِ عیناً تکراری روی همون (collection + فصل + متنِ
# سوال). فقط embedding/جستجو/تولید رو رد می‌کنه؛ کوتا/idempotency/ثبتِ پیام
# مثلِ همیشه اجرا می‌شن. ۰ یعنی خاموش. TTL کوتاه چون دادهٔ Qdrant ممکنه
# به‌روز بشه.
RAG_ANSWER_CACHE_TTL = int(os.environ.get('RAG_ANSWER_CACHE_TTL', '1800'))
# در اجرای تست‌ها همیشه RAG خاموش است تا هیچ تستی به OpenAI/Qdrant واقعی
# نزند (سرعت + پایداری). تست‌هایی که مسیرِ RAG را می‌آزمایند خودشان با
# override_settings + mockِ کلاینت‌ها این را برمی‌گردانند (rag/tests.py).
import sys as _sys
if 'test' in _sys.argv:
RAG_FORCE_MOCK = True
RAG_ANSWER_CACHE_TTL = 0
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

View File

@@ -51,6 +51,7 @@ urlpatterns = [
path('api/', include('content.urls')), path('api/', include('content.urls')),
path('api/', include('progress.urls')), path('api/', include('progress.urls')),
path('api/', include('ai_tutor.urls')), path('api/', include('ai_tutor.urls')),
path('api/', include('rag.urls')),
# اپ‌های فاز ۲ / بخش ۱ (subscription, payment, discount) # اپ‌های فاز ۲ / بخش ۱ (subscription, payment, discount)
path('api/', include('subscription.urls')), path('api/', include('subscription.urls')),

View File

@@ -3,3 +3,16 @@ djangorestframework==3.18.0
drf-spectacular==0.30.0 drf-spectacular==0.30.0
requests==2.33.1 requests==2.33.1
redis==5.2.1 redis==5.2.1
python-dotenv==1.0.1
# RAG / AI tutor (اپ rag). کلاینت‌ها lazy import می‌شن؛ نبودِ این‌ها فقط
# مسیرِ RAG رو از کار می‌ندازه، نه بقیه‌ی پروژه رو.
openai>=1.40,<3
qdrant-client>=1.12
# فقط برای ingestِ PDF (اختیاری). pdf2image به poppler سیستمی نیاز داره
# (macOS: brew install poppler). نبودشون فقط endpointِ /api/rag/ingest/pdf
# رو با خطای صریح از کار می‌ندازه.
pymupdf>=1.24
pdf2image>=1.17
pillow>=10