complete the quiz path for grades 8 through 12, and close three gaps in the API contract
Content ------- Every node in grades 8, 9, 10, 11 and 12 now holds a full four-question quiz — 121 more questions, filling the two حسابان دوازدهم nodes in فصل ۵: کاربردهای مشتق that were empty and the two ریاضی هشتم nodes that were short. The repo's own seed guard was already failing on those two empty nodes; it passes now. Grades 3, 6 and 7 are the other team's books and were left alone. 39 of their 145 nodes still hold fewer than four questions, but none is empty, so every node on the map still plays. An audit over all 1,593 questions in grades 8–12 found no structural fault: no question with the wrong number of options, no question with zero or two correct answers, no empty or duplicated option, no leftover LaTeX, and no two identical questions inside one lesson. Contract -------- Three endpoints were missing or malformed in the OpenAPI schema, which matters more than it looks: that schema is the contract the frontend generates its client from, so a view missing there is a view the other repo cannot call. - NotificationMarkReadView was dropped from the schema entirely, because spectacular could not guess a request body for its PATCH and gives up on the whole view rather than the one field. It declares request=None. - PaymentRedirectView was dropped for the same reason; it answers with a 302 and no JSON body, which it now says. - /api/health and /api/health/ collided on operationId and were emitted as health_retrieve and health_retrieve_2. The slash variant exists only for a monitor that calls it that way, so it is excluded from the schema. `spectacular` now runs clean — no errors, no warnings. Tidying ------- Condensed the two longest comment blocks (18 and 14 lines) in rag_verify_solutions and settings, keeping the measurement and the rule and dropping the retelling. README gains the bulk question-authoring workflow; the playbook's subagent section now points at the real tool instead of describing the steps by hand. 458 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
16
CLAUDE.md
16
CLAUDE.md
@@ -170,8 +170,8 @@ actually connected without asking a human.
|
||||
## Content: where it lives and how it gets to a server
|
||||
|
||||
`content/seed/` holds the whole curriculum and question bank as committed JSON —
|
||||
`curriculum.json` (6 grades, 48 units, 230 nodes, 1,928 lessons) and `questions.json`
|
||||
(1,441 multiple-choice questions). `deploy.sh` runs `seed_content` on every release, so a
|
||||
`curriculum.json` (8 grades, 58 units, 263 nodes, 2,617 lessons) and `questions.json`
|
||||
(2,463 multiple-choice questions). `deploy.sh` runs `seed_content` on every release, so a
|
||||
fresh server fills itself with one command: **no Qdrant, no AI credit, no SSH session.**
|
||||
|
||||
`seed_content` is idempotent and never deletes. Questions bind to lessons **by title path**
|
||||
@@ -243,7 +243,9 @@ reliably pull the final answer out of a solution and produced ~40% broken output
|
||||
- Throttle rates need periodic review against real traffic.
|
||||
- `DEV_MASTER_OTP` (`0000`) must stay gated behind `DEBUG` — never enabled in prod.
|
||||
- No video content exists yet, though `Lesson.kind`/`video_url` can express it.
|
||||
- 58 of 230 nodes still hold fewer than four questions; their verified chunks ran out.
|
||||
- Grades 8–12 are fully covered: every node holds a four-question quiz. 39 of the 145
|
||||
nodes in grades 3/6/7 (the other team's books) still hold fewer than four; none is
|
||||
empty, so every node still plays.
|
||||
- Kavenegar is unconfigured, so `POST /api/auth/otp/request` returns 503. With `DEBUG=0`
|
||||
there is no way to log into the API without creating an `OTPCode` row in the admin
|
||||
(`/admin/accounts/otpcode/add/`) — this is how to hand a tester a working login.
|
||||
@@ -404,11 +406,7 @@ branch, no rendering anywhere. Do not build content for it without asking first.
|
||||
کاربر تصمیمِ محصولی را گرفت و هر دو پایه ساخته شدند. `seed_curriculum_from_textbook`
|
||||
اسکلت را رایگان ساخت — یازدهم ۵ فصل/۲۱ نود/۳۶۹ درس، دوازدهم ۵ فصل/۱۲ نود/۳۲۰ درس — و
|
||||
سوالها با subagentهای Sonnet نوشته شدند: **۵۵۹ سوال** (یازدهم ۳۲۲، دوازدهم ۲۳۷).
|
||||
هر ۲۱ نودِ یازدهم آزمونِ کاملِ چهارسوالی دارند؛ از دوازدهم ۱۰ نود از ۱۲.
|
||||
|
||||
⚠️ **دو نودِ باقیمانده، هر دو در «فصل ۵: کاربردهای مشتق»** («جهت تقعر نمودار…» و
|
||||
«رسم نمودار تابع»): سوال ندارند چون سقفِ هفتگیِ Sonnet وسطِ کار پر شد، نه چون داده کم
|
||||
است. جفتهایشان آمادهاند — دو دستهی آخرِ همان اجرای `--export`.
|
||||
هر ۳۳ نودِ هر دو پایه آزمونِ کاملِ چهارسوالی دارند.
|
||||
|
||||
`GRADE_STYLE` حالا برای یازدهم/دوازدهم رنگ دارد (فیروزهای و نیلی، عمداً جدا از شش
|
||||
پایهی عمومی چون حسابان فقط کتابِ رشتهی ریاضیفیزیک است).
|
||||
@@ -420,7 +418,7 @@ branch, no rendering anywhere. Do not build content for it without asking first.
|
||||
عینِ همان PROMPTی که به مدلِ پولی میرفت مینویسد، تا کیفیتِ دو شاخه از هم جدا نیفتد.
|
||||
خروجیِ subagent با `content.question_io` وارد میشود (فایلِ بد هیچچیز نمینویسد).
|
||||
|
||||
بازده روی ۶۳۰ ورودی: **۵۵۹ سوال، ۸۹٪** — بقیه عمداً رد شدند، بیشترشان مسئلههایی که
|
||||
بازده روی ۷۷۸ ورودی: **۶۸۰ سوال، ۸۷٪** — بقیه عمداً رد شدند، بیشترشان مسئلههایی که
|
||||
بدونِ شکلِ کتاب خودکفا نمیشوند. چند subagent خطایِ حسابِ خودِ کتاب را هم گرفتند
|
||||
(راهحلی که √ را ۶ نوشته بود ولی ۶٫۵ بود؛ راهحلی که میگفت حد وجود ندارد درحالیکه
|
||||
داشت) — یعنی `solution_verdict='agrees'` تضمینِ کامل نیست، فقط فیلترِ اول است.
|
||||
|
||||
22
README.md
22
README.md
@@ -170,6 +170,28 @@ curl https://<host>/api/health
|
||||
- **برنامهی درسی:** چیزی پاک نمیشود. هر چه در فایل هست ساخته/بهروز میشود و
|
||||
هر چه نیست دستنخورده میماند، پس یک فایلِ ناقص محتوای سرور را از بین نمیبرد.
|
||||
|
||||
### ساختِ انبوهِ سوال از کتاب
|
||||
|
||||
وقتی کتابی تازه روی Qdrant آمده و باید برایش سوال ساخته شود، مسیرِ رایگان این است:
|
||||
|
||||
```bash
|
||||
# اسکلتِ برنامهی درسی از فهرستِ خودِ کتاب (بدونِ هزینه)
|
||||
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم --dry-run
|
||||
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم
|
||||
|
||||
# جفتهای (درس، مسئلهی تأییدشده) را برای سوالنویسی بیرون بده (بدونِ هزینه)
|
||||
python manage.py generate_questions_from_chunks --subject ریاضی --grade نهم --export /tmp/batches
|
||||
```
|
||||
|
||||
خروجی چند فایلِ JSON است بهعلاوهی یک `*-RULES.txt` که قواعدِ ساختِ سوال را
|
||||
دارد. سوالها را با آن قواعد بنویسید و از «وارد کردنِ فلهای سوال» در جدولِ
|
||||
بالا واردشان کنید. همان فرمان بدونِ `--export` کار را به مدلِ پولی میسپارد،
|
||||
ولی گرانتر است و کیفیتِ بهتری نمیدهد. جزئیات و هزینههای اندازهگیریشده در
|
||||
`docs/extraction-playbook.md`.
|
||||
|
||||
`--dry-run` روی هر دو فرمان کار میکند و پیش از هر نوشتنی نشان میدهد چه ساخته
|
||||
خواهد شد.
|
||||
|
||||
### دادهی تستی روی سرور
|
||||
|
||||
`seed_test_data` فقط برای محیطِ لوکال است و **نباید** روی سروری که کاربرِ واقعی
|
||||
|
||||
@@ -537,17 +537,13 @@ BAZAAR_API_BASE = os.environ.get('BAZAAR_API_BASE', 'https://pardakht.cafebazaar
|
||||
# -----------------------------------------------------------------------
|
||||
# Zarinpal (درگاهِ اصلیِ پرداختِ ما — Payment API v4)
|
||||
# -----------------------------------------------------------------------
|
||||
# آدرسِ عمومیِ خودِ بکاند؛ هم برای ساختنِ paymentUrlی که به کلاینت برمیگردد
|
||||
# (PaymentRedirectView) و هم برای callback_url که به زرینپال میفرستیم لازم
|
||||
# است.
|
||||
# آدرسِ عمومیِ بکاند: هم paymentUrlی که به کلاینت برمیگردد از آن ساخته
|
||||
# میشود، هم callback_urlی که به زرینپال میرود.
|
||||
#
|
||||
# چرا پیشفرض دارد: خالی بودنش سرویس را نمیخواباند، فقط مسیرِ پرداخت را
|
||||
# بیسروصدا میشکند — paymentUrl نسبی میشود و زرینپال callback خالی
|
||||
# میگیرد. این دقیقاً همان دسته خطاست که فقط موقعِ اولین پرداختِ واقعی معلوم
|
||||
# میشود. دامنهی خودمان از قبل معلوم است، پس حدس زدنی نیست.
|
||||
# .strip() اول لازم است: کلید در .env سرور وجود دارد ولی خالی است، و
|
||||
# os.environ.get فقط وقتی کلید *نباشد* پیشفرض میدهد — مقدارِ خالی آن را
|
||||
# خنثی میکرد و مسیرِ پرداخت باز هم بیصدا میشکست.
|
||||
# پیشفرض دارد چون خالی بودنش سرویس را نمیخواباند، فقط مسیرِ پرداخت را
|
||||
# بیصدا میشکند و تا اولین پرداختِ واقعی معلوم نمیشود. `.strip() or` هم
|
||||
# لازم است: کلید در .env سرور هست ولی خالی، و os.environ.get فقط وقتی کلید
|
||||
# *نباشد* پیشفرض میدهد.
|
||||
PUBLIC_BASE_URL = (
|
||||
os.environ.get('PUBLIC_BASE_URL', '').strip() or 'https://api.math.tadrisino2.ir'
|
||||
).rstrip('/')
|
||||
|
||||
@@ -10,6 +10,8 @@ from django.contrib import admin
|
||||
from django.http import JsonResponse
|
||||
from django.urls import include, path
|
||||
from django.views import defaults as django_default_views
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
from config.health import HealthView
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
@@ -50,7 +52,10 @@ urlpatterns = [
|
||||
# هر `git pull` بعدی روی همین فایل گیر میکرد. یک مسیرِ اضافه ارزانش
|
||||
# است در برابرِ یک فایلِ دستکاریشده روی پروداکشن.
|
||||
path('api/health', HealthView.as_view(), name='health'),
|
||||
path('api/health/', HealthView.as_view(), name='health-slash'),
|
||||
# همان ویو، فقط برای پایشی که با اسلش صدا میزند. از اسکیما بیرون است تا دو
|
||||
# مسیرِ یکسان operationIdهای health_retrieve و health_retrieve_2 نسازند.
|
||||
path('api/health/', extend_schema(exclude=True)(HealthView).as_view(),
|
||||
name='health-slash'),
|
||||
|
||||
# OpenAPI schema + Swagger UI + Redoc
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -90,9 +90,31 @@ regexِ اولیه شاملِ `شکل|جدول|روبهرو|مقابل|زیر
|
||||
روی ۳۴۵ ورودی **۹۲٪** بود. Haiku برای این کار مناسب نیست — جوابِ نهایی را
|
||||
از راهحل بیرون نمیکشد.
|
||||
|
||||
مسیر: جفتهای (درس، مسئلهی تأییدشده) را به JSON بیرون بدهید، subagent فایلِ
|
||||
خروجی بنویسد، و از `content.question_io` واردش کنید — که اعتبارسنجی دارد و
|
||||
فلسفهاش این است که **فایلِ بد هیچچیز نمینویسد**.
|
||||
مسیر حالا ابزار دارد و دستی نیست:
|
||||
|
||||
```bash
|
||||
# ۱. جفتهای (درس، مسئلهی تأییدشده) را بیرون بده — رایگان، هیچ فراخوانیِ پولی
|
||||
python manage.py generate_questions_from_chunks \
|
||||
--subject حسابان --grade یازدهم --export /tmp/batches
|
||||
# ۲. برای هر فایلِ دسته یک subagentِ Sonnet بگذار که خروجی را در /tmp/out بنویسد
|
||||
# ۳. وارد کن (اعتبارسنجی دارد؛ فایلِ بد هیچچیز نمینویسد)
|
||||
# از پنلِ ادمین: /admin/content/question/ → «وارد کردنِ سوالها از فایل»
|
||||
# ۴. به فایلِ ریپو منتقل کن و کامیت کن
|
||||
python manage.py seed_content --export --only questions
|
||||
```
|
||||
|
||||
`--export` کنارِ دستهها یک `*-RULES.txt` هم مینویسد که **عینِ همان PROMPTی است
|
||||
که به مدلِ پولی میرفت**؛ پس کیفیتِ دو شاخه از هم جدا نمیافتد. اندازهی دسته با
|
||||
`--batch-size` تنظیم میشود (پیشفرض ۳۰).
|
||||
|
||||
⚠️ **سقفِ هفتگیِ Sonnet واقعی است و ناگهانی میرسد.** هر ۳۰ سوال حدودِ ۹۵ هزار
|
||||
توکن میبرد، پس ۲۰ subagentِ همزمان ≈ ۲ میلیون توکن در یک نشست. قبل از شروع
|
||||
حساب کنید؛ دستهها را کوچک نگه دارید و بینشان نتیجه را وارد کنید.
|
||||
|
||||
**یک سوال بسازید، نه یک ترجمه.** subagentها چند بار خطای حسابِ خودِ کتاب را
|
||||
گرفتند (راهحلی که ریشه را ۶ نوشته بود ولی ۶٫۵ بود؛ راهحلی که میگفت حد وجود
|
||||
ندارد درحالیکه داشت). یعنی `solution_verdict='agrees'` فیلترِ اول است، نه تضمین —
|
||||
در پرامپت صریح بخواهید که جواب را مستقل حل کند و اگر نخواند، ردیف را رد کند.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -48,7 +48,11 @@ class NotificationMarkReadView(APIView):
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@extend_schema(responses=MarkReadResponseSerializer, tags=['Notifications (notifications)'])
|
||||
# request=None لازم است: بدونِ آن spectacular نمیتواند بدنهی PATCH را حدس
|
||||
# بزند و **کلِ ویو را از اسکیما کنار میگذارد** — یعنی این مسیر در قراردادی
|
||||
# که فرانت از آن کلاینت میسازد اصلاً دیده نمیشود.
|
||||
@extend_schema(request=None, responses=MarkReadResponseSerializer,
|
||||
tags=['Notifications (notifications)'])
|
||||
def patch(self, request, id):
|
||||
notification = get_object_or_404(Notification, pk=id, user_id=request.user.id)
|
||||
if not notification.is_read:
|
||||
|
||||
@@ -191,6 +191,11 @@ class PaymentRedirectView(APIView):
|
||||
throttle_scope = 'payment_pay'
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
|
||||
@extend_schema(
|
||||
responses={302: OpenApiTypes.NONE},
|
||||
description='به درگاهِ زرینپال ریدایرکت میکند؛ بدنهی JSON ندارد.',
|
||||
tags=['Payment (payment)'],
|
||||
)
|
||||
def get(self, request, order_id):
|
||||
order = get_object_or_404(Order, pk=order_id, provider='gateway')
|
||||
|
||||
|
||||
@@ -82,24 +82,14 @@ def is_account_limit(exc) -> bool:
|
||||
return '403' in text and 'insufficient' in text
|
||||
|
||||
|
||||
# مسئلهای که به شکل/جدولِ کتاب ارجاع میدهد و **محتوای آن را ننوشته** بدونِ
|
||||
# تصویر قابلِ حل نیست؛ فرستادنش هم پول میسوزاند هم آمار را خراب میکند.
|
||||
# مسئلهای که به شکلِ کتاب ارجاع میدهد و محتوایش را ننوشته، بدونِ تصویر
|
||||
# قابلِ حل نیست؛ فرستادنش هم پول میسوزاند هم آمار را خراب میکند.
|
||||
#
|
||||
# ⚠️ نسخهی اولِ این محافظ یک regexِ تخت بود:
|
||||
# شکل|جدول|نمودار|روبهرو|روبرو|مقابل|...|زیر را|بالا را
|
||||
# و فاجعهبار گشاد بود. سه نمونه از چیزهایی که بیدلیل رد میکرد:
|
||||
# «زاویههای روبهرو در لوزی برابرند؟» ← «روبهرو» یعنی مقابل، نه شکل
|
||||
# «کسرهای زیر را گویا کنید: ۲/√۳» ← داده همانجاست
|
||||
# «به شکل مخروطی با شعاع ۴ و ارتفاع ۱۲» ← «به شکلِ ...» یعنی شبیهِ
|
||||
# اندازهگیری (۱۴۰۵/۰۶/۲۹، نمونهی ۵۰تایی از کتابهای بازخوانیشده): از
|
||||
# مسئلههایی که این محافظ رد کرده بود، **۸۴٪ واقعاً قابلِ قضاوت بودند** —
|
||||
# یعنی ~۷۸۴ مسئلهی سالم بیصدا از چرخهی ساختِ سوال بیرون مانده بودند.
|
||||
#
|
||||
# قاعدهی تازه دو شرط دارد، و هر دو باید برقرار باشند:
|
||||
# ۱. ارجاعِ صریح به چیزی بیرونِ متن باشد («شکل زیر»، «جدول روبهرو»)
|
||||
# ۲. بعد از آن ارجاع، توصیفی نیامده باشد (نه «که ...»، نه عدد، نه نماد)
|
||||
# شرطِ دوم همان چیزی است که قاعدهی self-contained تولید میکند: مدل حالا
|
||||
# مینویسد «با توجه به شکل مقابل **که شامل {a,1} است**».
|
||||
# ⚠️ این محافظ باید دو شرطی بماند — ارجاعِ صریح به بیرونِ متن، *و* نیامدنِ
|
||||
# توصیف بعد از آن. نسخهی اولش یک regexِ تخت روی «شکل|جدول|روبهرو|…» بود و
|
||||
# «زاویههای روبهرو در لوزی» یا «کسرهای زیر را گویا کنید: ۲/√۳» را هم میگرفت:
|
||||
# در نمونهی ۵۰تایی ۸۴٪ از ردشدهها واقعاً قابلِ قضاوت بودند، یعنی ~۷۸۴ مسئلهی
|
||||
# سالم بیصدا از چرخهی ساختِ سوال بیرون مانده بود.
|
||||
_FIGURE_REFERENCE = re.compile(
|
||||
r'(شکل|جدول|نمودار|تصویر|نقشه)\s*(ِ|های|های)?\s*'
|
||||
r'(زیر|بالا|روبه\u200cرو|روبرو|مقابل|فوق|داده\s*شده)'
|
||||
|
||||
Reference in New Issue
Block a user