""" ابزارِ سوالِ شکل‌دار: `export` درس‌های یک فصل را برای نویسنده بیرون می‌دهد، `check` خروجیِ او را سخت‌گیرانه می‌سنجد (validate بی‌صدا آیتمِ خراب را حذف می‌کند؛ اینجا حذف یعنی رد). python tools/figures/batch.py spec # کاتالوگِ کاملِ نوع‌های شکل python tools/figures/batch.py export "پایه دهم" "فصل ۱" هندسه [--out f.json] python tools/figures/batch.py check batch.json # بدونِ نوشتن در seed python tools/figures/batch.py merge batch.json # افزودن به content/seed/questions.json آزموده نشده: درستیِ جوابِ سوال‌ها را این ابزار نمی‌سنجد؛ آن را re-solveِ مستقل می‌سنجد. """ import json import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') import django # noqa: E402 django.setup() from rag.services import diagram # noqa: E402 SEED = os.path.join(os.path.dirname(__file__), '..', '..', 'content', 'seed') def _curriculum(): return json.load(open(os.path.join(SEED, 'curriculum.json')))['grades'] def _questions(): return json.load(open(os.path.join(SEED, 'questions.json')))['questions'] def export(grade, unit_prefix, subject=None): qs = _questions() rows = [] for g in _curriculum(): if g['title'] != grade: continue for u in g['units']: if not u['title'].startswith(unit_prefix + ':') or (subject and u['subject'] != subject): continue for n in u['nodes']: for l in n['lessons']: path = [g['title'], u['title'], n['title'], l['title']] have = [q for q in qs if q['path'] == path] rows.append({ 'path': path, 'subject': u['subject'], 'intro': l.get('introText', '')[:400], 'figuredAlready': sum(1 for q in have if q.get('figure')), 'existingPrompts': [q['prompt'][:110] for q in have[:3]], }) return rows def _strict(row, i): errs = [] path = row.get('path') if not (isinstance(path, list) and len(path) == 4): errs.append('path باید چهار عنوان باشد') ch = row.get('choices') if not (isinstance(ch, list) and len(ch) == 4 and len(set(map(str, ch))) == 4): errs.append('choices باید چهار گزینه‌ی متفاوت باشد') if row.get('correct') not in (0, 1, 2, 3): errs.append('correct باید ۰ تا ۳ باشد') if row.get('type') != 'multiple_choice': errs.append('type باید multiple_choice باشد') fig = row.get('figure') if not fig: errs.append('figure خالی است') else: clean = diagram.validate(fig) if clean is None: errs.append('figure نامعتبر') elif json.dumps(clean, sort_keys=True) != json.dumps(diagram.validate(clean), sort_keys=True): errs.append('figure پایدار نیست') else: n_in = len(fig.get('items') or []) + len(fig.get('points') or {}) n_out = len(clean.get('items') or []) + len(clean.get('points') or {}) if n_out < n_in: errs.append(f'validate {n_in - n_out} آیتم/نقطه را حذف کرد') p = row.get('prompt') or '' if any(t in p for t in ('$', '\\frac', '\\(', '**')): errs.append('LaTeX/markdown در prompt') return [f'ردیف {i}: {e}' for e in errs] def check(path): rows = json.load(open(path)) rows = rows['questions'] if isinstance(rows, dict) else rows curr = {tuple([g['title'], u['title'], n['title'], l['title']]) for g in _curriculum() for u in g['units'] for n in u['nodes'] for l in n['lessons']} errs = [] for i, r in enumerate(rows): errs += _strict(r, i) if tuple(r.get('path') or ()) not in curr: errs.append(f'ردیف {i}: درس در برنامه‌ی درسی نیست') print(f'{len(rows)} سوال، {len(errs)} خطا') print('\n'.join(errs[:40])) return not errs def merge(path): if not check(path): sys.exit('خطا دارد؛ ادغام نشد') rows = json.load(open(path)) rows = rows['questions'] if isinstance(rows, dict) else rows p = os.path.join(SEED, 'questions.json') data = json.load(open(p)) have = {(tuple(q['path']), q['prompt'].strip()) for q in data['questions']} new = [r for r in rows if (tuple(r['path']), r['prompt'].strip()) not in have] data['questions'] += new json.dump(data, open(p, 'w'), ensure_ascii=False, indent=1) print(f'{len(new)} سوال افزوده شد') if __name__ == '__main__': cmd = sys.argv[1] if cmd == 'spec': print(diagram.DIAGRAM_SPEC) elif cmd == 'export': out = json.dumps(export(sys.argv[2], sys.argv[3], sys.argv[4] if len(sys.argv) > 4 and not sys.argv[4].startswith('--') else None), ensure_ascii=False, indent=1) if '--out' in sys.argv: open(sys.argv[sys.argv.index('--out') + 1], 'w').write(out) else: print(out) elif cmd == 'check': sys.exit(0 if check(sys.argv[2]) else 1) elif cmd == 'merge': merge(sys.argv[2])