Compare commits
2 Commits
3026df0397
...
6a6d0200d2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a6d0200d2 | |||
| 82edd0ecb0 |
80
CLAUDE.md
80
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.
|
||||
@@ -386,3 +388,75 @@ Still open, and deliberately not acted on:
|
||||
The frontend's `Question` type carries a `type` field that nothing reads, and
|
||||
`count_objects` in `Question.TYPE_CHOICES` is an empty shell — no rows, no serializer
|
||||
branch, no rendering anywhere. Do not build content for it without asking first.
|
||||
|
||||
## وضعیت ۱۴۰۵/۰۶/۲۹ (۲۰۲۶-۰۹-۲۰) — این را اول بخوان
|
||||
|
||||
سرور روی `3026df0` است، کد و محتوا هر دو منتشر شدند، سرویس واقعاً ریاستارت شد، و
|
||||
`/api/health` عدد `questions: 1783` میدهد. `/api/nodes/1/quiz` چهار سوالِ واقعی برمیگرداند.
|
||||
|
||||
**۵ کتاب روی Qdrant، همه با دیتابیس همگام:** ریاضی هشتم (۵۱۸)، نهم (۴۵۰)، دهم (۴۵۸)،
|
||||
حسابان یازدهم (۳۷۶)، حسابان دوازدهم (۳۲۶). صفحهی گمشده: صفر. سه کتابِ تیمِ دیگر
|
||||
(سوم/ششم/هفتم، مجموعاً ۱٬۲۵۲) عمداً دستنخورده ماندند.
|
||||
|
||||
خرجِ کلِ آن روز ۳٫۴۴ دلار بود. روشِ ارزان و دقیقِ استخراج در `docs/extraction-playbook.md`
|
||||
نوشته شده — **قبل از هر ingest تازه آن را بخوان**، مخصوصاً بخشِ انتخابِ مدل.
|
||||
|
||||
### حسابان یازدهم و دوازدهم: وارد مسیرِ بازی شد (۲۰۲۶-۰۹-۲۱)
|
||||
|
||||
کاربر تصمیمِ محصولی را گرفت و هر دو پایه ساخته شدند. `seed_curriculum_from_textbook`
|
||||
اسکلت را رایگان ساخت — یازدهم ۵ فصل/۲۱ نود/۳۶۹ درس، دوازدهم ۵ فصل/۱۲ نود/۳۲۰ درس — و
|
||||
سوالها با subagentهای Sonnet نوشته شدند: **۵۵۹ سوال** (یازدهم ۳۲۲، دوازدهم ۲۳۷).
|
||||
هر ۳۳ نودِ هر دو پایه آزمونِ کاملِ چهارسوالی دارند.
|
||||
|
||||
`GRADE_STYLE` حالا برای یازدهم/دوازدهم رنگ دارد (فیروزهای و نیلی، عمداً جدا از شش
|
||||
پایهی عمومی چون حسابان فقط کتابِ رشتهی ریاضیفیزیک است).
|
||||
|
||||
### روالِ ساختِ سوال با subagent، حالا ابزار دارد
|
||||
|
||||
`generate_questions_from_chunks --export DIR` جفتهای (درس، مسئلهی تأییدشده) را در
|
||||
فایلهای JSON میریزد و **هیچ فراخوانیِ پولی نمیکند**؛ کنارشان یک `*-RULES.txt` با
|
||||
عینِ همان PROMPTی که به مدلِ پولی میرفت مینویسد، تا کیفیتِ دو شاخه از هم جدا نیفتد.
|
||||
خروجیِ subagent با `content.question_io` وارد میشود (فایلِ بد هیچچیز نمینویسد).
|
||||
|
||||
بازده روی ۷۷۸ ورودی: **۶۸۰ سوال، ۸۷٪** — بقیه عمداً رد شدند، بیشترشان مسئلههایی که
|
||||
بدونِ شکلِ کتاب خودکفا نمیشوند. چند subagent خطایِ حسابِ خودِ کتاب را هم گرفتند
|
||||
(راهحلی که √ را ۶ نوشته بود ولی ۶٫۵ بود؛ راهحلی که میگفت حد وجود ندارد درحالیکه
|
||||
داشت) — یعنی `solution_verdict='agrees'` تضمینِ کامل نیست، فقط فیلترِ اول است.
|
||||
|
||||
**باگی که همانجا پیدا و اصلاح شد:** اجرای دوم با `--per-lesson` بالاتر همان chunkِ
|
||||
قبلی را دوباره تحویل میداد و سوالِ تقریباً تکراری میساخت. حالا `_targets` به اندازهی
|
||||
سوالهایی که درس از قبل دارد از ترتیبِ قطعیِ chunkها میپرد. این خطا نه لاگ میداد، نه
|
||||
در import گیر میافتاد (چون صورتِ سوال بازنویسی میشود و رشتهاش یکی نیست).
|
||||
|
||||
⚠️ **سقفِ هفتگیِ Sonnet واقعی است و ناگهانی میرسد.** ۲۰ subagentِ همزمان (هرکدام
|
||||
~۹۵ هزار توکن برای ۳۰ سوال) آن را در یک نشست پر کرد. اگر کارِ انبوه داری، دستهها را
|
||||
کوچکتر بگیر و بینشان نتیجه را وارد کن — خوشبختانه بیشترِ agentها فایلشان را نوشته
|
||||
بودند و فقط در تماسِ آخر خوردند، پس چیزی از دست نرفت.
|
||||
|
||||
|
||||
### چیزهایی که آن روز کشف شد و تکرارشان گران است
|
||||
|
||||
- **مدل را اندازه بگیر، از جدولِ قیمت نخوان.** قیمتِ GapGPT با قیمتِ رسمی یکی نیست:
|
||||
`gpt-4.1-mini` هم از `gpt-4o` ارزانتر بود هم دقیقتر، و `gemini-3.6-flash` برخلافِ
|
||||
انتظار سه برابرِ `gpt-4o` گران درآمد. `OPENAI_VISION_MODEL=gpt-4.1-mini` الان در `.env`
|
||||
است و هر پنج کتاب با همان خوانده شد.
|
||||
- **ساختِ سوال را با subagent انجام بده، نه با API.** `generate_questions_from_chunks`
|
||||
پیشفرضش `o4-mini` است و ~۰٫۷۷ سنت برای هر سوال میگیرد (چهار برابرِ تخمینِ خودش، چون
|
||||
مدلِ ریزنینگ است). همان کار با subagentهای Sonnet رایگان است و بازدهاش ۹۲٪ بود.
|
||||
روالش: جفتهای (درس، مسئلهی تأییدشده) را به JSON بده، subagent خروجی بنویسد، و با
|
||||
`content.question_io` واردش کن.
|
||||
- **`--recheck` در `rag_verify_solutions` همهچیز را دوباره میفرستد**، نه فقط موارد تازه.
|
||||
برای بازبینیِ یک زیرمجموعه، رأیشان را خالی کن و اجرای عادی بزن.
|
||||
- **شمارندهی صفحات حینِ `--refresh` معنی ندارد** — هر صفحه اول پاک و بعد خوانده میشود،
|
||||
پس عدد دورِ کل نوسان میکند. پیشرفت را از `IngestedPage.created_at` بسنج.
|
||||
- **در `until`ِ انتظار، `ps | grep` خودش را میشمارد** و حلقه هرگز تمام نمیشود. از
|
||||
`pgrep -f` استفاده کن.
|
||||
- **کد و محتوا دو مسیرِ جدا دارند.** `db.sqlite3` در gitignore است؛ سوالها فقط از راهِ
|
||||
`seed_content --export --only questions` و کامیتِ `content/seed/questions.json` به سرور
|
||||
میرسند. پوش کردنِ کد بهتنهایی محتوا را نمیبرد.
|
||||
|
||||
### CORS
|
||||
|
||||
پورتهای توسعه دیگر شمرده نمیشوند؛ `CORS_ALLOWED_ORIGIN_REGEXES` هر پورتی روی
|
||||
`localhost`/`127.0.0.1` را میپذیرد. اگر باز هم گزارشِ CORS رسید، اول بپرس **Hard Refresh**
|
||||
زدهاند یا نه — مرورگر نتیجهی preflight را کش میکند.
|
||||
|
||||
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'),
|
||||
|
||||
@@ -13,8 +13,16 @@
|
||||
python manage.py generate_questions_from_chunks --grade نهم --limit 5 --dry-run
|
||||
python manage.py generate_questions_from_chunks --grade نهم --limit 20
|
||||
python manage.py generate_questions_from_chunks --grade نهم
|
||||
|
||||
مسیرِ رایگان (و پیشفرضِ عملی): `--export DIR` هیچ فراخوانیِ پولی نمیکند و
|
||||
فقط جفتهای (درس، مسئلهی تأییدشده) را در چند فایلِ JSON میریزد تا subagentها
|
||||
سوالها را بنویسند و خروجی با `content.question_io` وارد شود. اندازهگیریِ
|
||||
۱۴۰۵/۰۶/۲۹: o4-mini روی این پروکسی ۰٫۷۷ سنت برای هر سوال، subagentِ Sonnet
|
||||
صفر — با بازدهی ۹۲٪. متنِ پرامپت برای هر دو مسیر یکی است (همین PROMPT پایین)،
|
||||
پس کیفیت دو شاخه از هم جدا نمیافتد.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
@@ -92,6 +100,11 @@ class Command(BaseCommand):
|
||||
help='چند سوال برای هر درس.')
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
help='فقط نشان بده چند سوال ساخته میشود؛ هیچ فراخوانیِ پولی نکن.')
|
||||
parser.add_argument('--export', metavar='DIR',
|
||||
help='بهجای فراخوانیِ مدل، جفتهای (درس، مسئله) را در این '
|
||||
'پوشه به JSON بنویس تا subagent سوالها را بسازد. رایگان.')
|
||||
parser.add_argument('--batch-size', type=int, default=30,
|
||||
help='چند جفت در هر فایلِ --export. پیشفرض ۳۰.')
|
||||
parser.add_argument('--model', default='o4-mini',
|
||||
help='مدلِ سازنده. پیشفرض o4-mini؛ gpt-4o-mini کیفیتِ '
|
||||
'قابلِ قبولی نداد (۴ سوال از ۶ خراب).')
|
||||
@@ -135,6 +148,10 @@ class Command(BaseCommand):
|
||||
if len(targets) > 10:
|
||||
self.stdout.write(f' … و {len(targets) - 10} مورد دیگر')
|
||||
|
||||
if opts['export']:
|
||||
self._export(targets, opts)
|
||||
return
|
||||
|
||||
# ~۹۰۰ توکن ورودی و ~۱۵۰ خروجی برای هر سوال با gpt-4o-mini.
|
||||
self.model = opts['model']
|
||||
cost = len(targets) * 0.0018 # اندازهگیریِ واقعیِ o4-mini
|
||||
@@ -196,6 +213,58 @@ class Command(BaseCommand):
|
||||
f'\n{created} سوال ساخته شد، {skipped} رد شد، {failed} خطا.'
|
||||
))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _export(self, targets, opts):
|
||||
"""
|
||||
جفتها را در فایلهای کوچکِ JSON بنویس؛ هیچ مدلی صدا زده نمیشود.
|
||||
|
||||
چرا تکهتکه و نه یک فایلِ بزرگ: هر فایل سهمِ یک subagent است و باید در
|
||||
یک نشست جا شود. و چرا `prompt` را هم کنارش مینویسیم: تا کسی که
|
||||
سوالها را میسازد قواعد را از حافظه بازنویسی نکند — همان متنی که به
|
||||
مدلِ پولی میرفت عیناً در دسترسِ subagent است.
|
||||
"""
|
||||
directory = opts['export']
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
book = self._book
|
||||
size = max(1, opts['batch_size'])
|
||||
slug = f'{book.subject}-{book.grade}'
|
||||
|
||||
written = []
|
||||
for index in range(0, len(targets), size):
|
||||
rows = [
|
||||
{
|
||||
'lessonId': lesson.pk,
|
||||
'unit': lesson.node.unit.title,
|
||||
'node': lesson.node.title,
|
||||
'lessonTitle': lesson.title,
|
||||
'problemType': chunk.problem_type,
|
||||
'problem': chunk.problem,
|
||||
'solution': chunk.solution,
|
||||
}
|
||||
for lesson, chunk in targets[index:index + size]
|
||||
]
|
||||
path = os.path.join(directory, f'{slug}-{index // size + 1:02d}.json')
|
||||
with open(path, 'w', encoding='utf-8') as handle:
|
||||
json.dump(rows, handle, ensure_ascii=False, indent=1)
|
||||
written.append((path, len(rows)))
|
||||
|
||||
rules = os.path.join(directory, f'{slug}-RULES.txt')
|
||||
with open(rules, 'w', encoding='utf-8') as handle:
|
||||
handle.write(PROMPT.format(
|
||||
grade=book.grade, problem_type='<problemType>',
|
||||
problem='<problem>', solution='<solution>',
|
||||
))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f'\n{len(targets)} جفت در {len(written)} فایل نوشته شد: {directory}'
|
||||
))
|
||||
self.stdout.write(f' قواعدِ ساختِ سوال: {rules}')
|
||||
self.stdout.write(
|
||||
' خروجیِ هر subagent باید قالبِ content.question_io باشد '
|
||||
'({"lessonId", "prompt", "choices", "correct"}) و با '
|
||||
'«ورودِ سوال» در پنلِ ادمین یا import_questions وارد شود.'
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def _page_span(self, lesson):
|
||||
"""بازهی صفحهی PDF برای نودِ این درس، از فهرستِ خودِ کتاب."""
|
||||
@@ -214,7 +283,7 @@ class Command(BaseCommand):
|
||||
return book.pdf_pages(chapter)
|
||||
return None
|
||||
|
||||
def _chunks_by_page(self, lesson, collection, need, opts, used):
|
||||
def _chunks_by_page(self, lesson, collection, need, opts, used, skip=0):
|
||||
span = self._page_span(lesson)
|
||||
if span is None:
|
||||
return []
|
||||
@@ -224,7 +293,7 @@ class Command(BaseCommand):
|
||||
.exclude(problem='').exclude(pk__in=used))
|
||||
if opts['verified_only']:
|
||||
qs = qs.filter(solution_verdict=LessonChunk.VERDICT_AGREES)
|
||||
return list(qs.order_by('source_page', 'order')[:need])
|
||||
return list(qs.order_by('source_page', 'order')[skip:skip + need])
|
||||
|
||||
def _targets(self, grade, collection, opts):
|
||||
"""[(lesson, chunk), ...] — فقط درسهایی که هنوز سوال کم دارند."""
|
||||
@@ -252,7 +321,12 @@ class Command(BaseCommand):
|
||||
if opts['verified_only']:
|
||||
candidates = candidates.filter(
|
||||
solution_verdict=LessonChunk.VERDICT_AGREES)
|
||||
chunks = list(candidates.order_by('source_page', 'order')[:need])
|
||||
# از `have` تا به بعد، نه از اول: ترتیب قطعی است، پس اجرای قبلی
|
||||
# دقیقاً `have` تکهی نخست را مصرف کرده. بدونِ این پرش، هر اجرای
|
||||
# دوم با --per-lesson بالاتر همان مسئله را دوباره تحویل میداد و
|
||||
# سوالِ تقریباً تکراری میساخت — که نه خطا میدهد و نه در import
|
||||
# گیر میافتد، چون صورتِ سوال بازنویسی شده و رشتهاش یکی نیست.
|
||||
chunks = list(candidates.order_by('source_page', 'order')[have:have + need])
|
||||
|
||||
# تطبیقِ عنوان شکننده است: عنوانِ درس از یک استخراجِ قدیمی آمده و
|
||||
# هر بار که کتاب دوباره خوانده شود مدل عنوانِ تازه میسازد. بعد از
|
||||
@@ -264,7 +338,8 @@ class Command(BaseCommand):
|
||||
# chunkهای همان بازه به درسهای همان نود میخورند — بدونِ هیچ
|
||||
# فراخوانیِ مدلی.
|
||||
if not chunks and opts['by_page']:
|
||||
chunks = self._chunks_by_page(lesson, collection, need, opts, used)
|
||||
chunks = self._chunks_by_page(lesson, collection, need, opts, used,
|
||||
skip=have)
|
||||
|
||||
for chunk in chunks:
|
||||
used.add(chunk.pk)
|
||||
|
||||
@@ -32,6 +32,10 @@ GRADE_STYLE = {
|
||||
'هشتم': ('grade-8', '#9C27B0'),
|
||||
'نهم': ('grade-9', '#FF9800'),
|
||||
'دهم': ('grade-10', '#E91E63'),
|
||||
# حسابان: رشتهی ریاضیفیزیک، پس عمداً رنگِ سرد و جدا از شش پایهی عمومی
|
||||
# تا در نقشهی بازی بهچشم یک مسیرِ دیگر بیاید، نه ادامهی همانها.
|
||||
'یازدهم': ('grade-11', '#00BCD4'),
|
||||
'دوازدهم': ('grade-12', '#3F51B5'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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