Files
MathApp/content/management/commands/gen_lesson_figures.py

94 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
شکلِ ثابتِ درس‌ها را یک بار می‌سازد و در `Lesson.figures` می‌نویسد.
همان مسیرِ ارزانِ دستیار: همان مدلِ چت، همان کاتالوگِ <diagram>، همان
اعتبارسنجِ `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}')