""" شکلِ ثابتِ درس‌ها را یک بار می‌سازد و در `Lesson.figures` می‌نویسد. همان مسیرِ ارزانِ دستیار: همان مدلِ چت، همان کاتالوگِ ، همان اعتبارسنجِ `rag.services.diagram.validate`. فرقش این است که نتیجه ذخیره می‌شود و بعد از تأییدِ چشمی با `seed_content --export` به curriculum.json می‌رود. فقط درس‌هایی که شکل لازم دارند شکل می‌گیرند؛ مدل می‌تواند `[]` بدهد. آزموده نشده: درستیِ هندسی/ریاضیِ شکل؛ باید با چشم دید. python manage.py gen_lesson_figures --grade 12 --limit 4 [--redo] [--dry-run] """ import json import re from django.core.management.base import BaseCommand, CommandError from content.models import Lesson from rag.services import clients, diagram as diagram_mod from rag.services.generation import chat_model_for, completion_kwargs PROMPT = ( 'تو برای یک درسِ ریاضی (اپِ آموزشی) شکل‌های آموزشی می‌سازی. متنِ درس: «توضیح»، «مثال» و ' '«قدم‌ها» (شماره از صفر). فقط جاهایی شکل بگذار که دیدنش مفهوم را روشن می‌کند (نمودار، ' 'انتقال، تقارن، محور، جدول، هندسه)؛ هر شکل باید دقیقاً همان چیزی را نشان دهد که آن بخش ' 'می‌گوید، با همان اعداد. بخشِ بی‌نیاز شکل نمی‌گیرد؛ اگر هیچ‌جا لازم نبود []. حداکثر ۵ شکل.\n' 'خروجی فقط یک آرایه‌ی JSON: [{"where":"intro"|"example"|"step","index":0,"diagram":{...}}] ' '(index فقط برای step). بدونِ توضیح و بدونِ ```.\n' 'مثال‌های کاتالوگ فقط قالب‌اند: نقطه، تابع یا آیتمی را که این درس لازم ندارد کپی نکن؛ فقط ' 'چیزهایی بگذار که همین شکل نشان می‌دهد (برای نمودارِ تابع معمولاً فقط items با type=function). نمودارِ خط‌شکسته یا تکه‌ای از نقطه‌ها را ' 'با type=segment بین نقطه‌های متوالی بکش، نه type=line (آن خطِ بی‌پایان است). نقطه‌هایی که ' 'در items نمی‌آیند تعریف نکن.\n\n' ) class Command(BaseCommand): help = 'ساختِ شکلِ ثابتِ درس‌ها (یک بار، ذخیره در Lesson.figures).' def add_arguments(self, p): p.add_argument('--grade', type=int, required=True) p.add_argument('--limit', type=int, default=4) p.add_argument('--lesson', type=int, help='فقط همین شناسه') p.add_argument('--redo', action='store_true', help='درس‌هایی که شکل دارند را هم بساز') p.add_argument('--dry-run', action='store_true') def handle(self, *a, **o): qs = Lesson.objects.filter(node__unit__grade__number=o['grade']).order_by( 'node__unit__order', 'node__order', 'id') if o['lesson']: qs = qs.filter(pk=o['lesson']) if not o['redo']: qs = qs.filter(figures=[]) lessons = list(qs[:o['limit']]) if not lessons: raise CommandError('درسی پیدا نشد.') model = chat_model_for('') client = clients.get_openai_client() spent = 0 for lesson in lessons: body = (f'عنوان: {lesson.title}\nتوضیح: {lesson.intro_text}\nمثال: {lesson.example}\n' + '\n'.join(f'قدم {i}: {s}' for i, s in enumerate(lesson.steps or []))) r = client.chat.completions.create( model=model, **completion_kwargs(model), messages=[{'role': 'system', 'content': PROMPT + diagram_mod.DIAGRAM_SPEC}, {'role': 'user', 'content': body}]) u = getattr(r, 'usage', None) spent += (getattr(u, 'total_tokens', 0) or 0) raw = re.sub(r'^```(?:json)?|```$', '', (r.choices[0].message.content or '').strip()).strip() try: start = raw.index('[') items, _ = json.JSONDecoder().raw_decode(raw[start:]) if not isinstance(items, list): raise ValueError('not a list') except ValueError as e: self.stderr.write(f'درس {lesson.id}: خروجیِ نامعتبر ({e}); ذخیره نشد.') continue figures, dropped = [], 0 for it in items: where = it.get('where') if isinstance(it, dict) else None spec = diagram_mod.validate(it.get('diagram')) if where else None idx = it.get('index') if (spec is None or where not in ('intro', 'example', 'step') or (where == 'step' and not (isinstance(idx, int) and 0 <= idx < len(lesson.steps or []))) or (where == 'example' and not lesson.example)): dropped += 1 continue figures.append({'where': where, **({'index': idx} if where == 'step' else {}), 'diagram': spec}) self.stdout.write(f'درس {lesson.id} «{lesson.title}»: {len(figures)} شکل، {dropped} ردشده') for f in figures: self.stdout.write(' ' + json.dumps(f, ensure_ascii=False)) if figures and not o['dry_run']: lesson.figures = figures lesson.save(update_fields=['figures']) self.stdout.write(f'توکنِ مصرفی: {spent}')