Subjects per grade (math, calculus, geometry, stats, discrete), 5 new textbooks, 202 new islands, long islands split, hand-written figures and lesson texts for the new books
- content.Subject + Unit.subject, /api/home?subject=, tutor grounded in the chosen book - 5 new books ingested; ~1150 hand-written questions, islands named by hand - node_split: islands over 7 lessons split into parts of <=6 (lesson ids and progress kept) - Lesson.figures hand-authored and visually checked for 653 lessons of the new books; gen_lesson_figures got --subject and survives network errors - seed files re-exported Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -124,7 +124,7 @@ class Command(BaseCommand):
|
||||
continue
|
||||
finder._book = book
|
||||
collection = build_collection_name(book.subject, book.grade)
|
||||
lessons = (Lesson.objects.filter(node__unit__grade=grade)
|
||||
lessons = (Lesson.objects.filter(node__unit__grade=grade, node__unit__subject__title=book.subject)
|
||||
.select_related('node__unit').order_by('node__unit__order', 'node__order', 'id'))
|
||||
for lesson in lessons:
|
||||
if lesson.example.strip() or lesson.steps:
|
||||
|
||||
@@ -24,6 +24,7 @@ import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
|
||||
import openai
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from content.models import Lesson
|
||||
@@ -109,6 +110,7 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, p):
|
||||
p.add_argument('--grade', type=int, help='بدونِ آن همهی پایهها')
|
||||
p.add_argument('--subject', help='عنوانِ کتاب (مثلاً هندسه)؛ بدونِ آن همهی کتابها')
|
||||
p.add_argument('--limit', type=int, default=4)
|
||||
p.add_argument('--lesson', type=int, help='فقط همین شناسه')
|
||||
p.add_argument('--redo', action='store_true', help='درسهایی که شکل دارند هم')
|
||||
@@ -128,6 +130,8 @@ class Command(BaseCommand):
|
||||
'node__unit__grade__number', 'node__unit__order', 'node__order', 'id')
|
||||
if o['grade']:
|
||||
qs = qs.filter(node__unit__grade__number=o['grade'])
|
||||
if o['subject']:
|
||||
qs = qs.filter(node__unit__subject__title=o['subject'])
|
||||
if o['lesson']:
|
||||
qs = qs.filter(pk=o['lesson'])
|
||||
if not o['redo']:
|
||||
@@ -179,7 +183,8 @@ class Command(BaseCommand):
|
||||
return
|
||||
try:
|
||||
items = _json_list(ask(PROMPT + spec, text))
|
||||
except (ValueError, KeyError) as e:
|
||||
# خطای شبکه/درگاه یک درس را رد میکند، نه کلِ اجرا را؛ در ledger ثبت نمیشود تا دفعهی بعد دوباره برود.
|
||||
except (ValueError, KeyError, openai.OpenAIError) as e:
|
||||
with lock:
|
||||
stats['invalid'] += 1
|
||||
self.stderr.write(f'درس {lesson.id}: خروجیِ نامعتبر ({e}); ذخیره نشد.')
|
||||
@@ -218,7 +223,7 @@ class Command(BaseCommand):
|
||||
ok = {v.get('i'): v for v in verdicts if isinstance(v, dict)}
|
||||
kept = [f for i, f in enumerate(figures) if ok.get(i, {}).get('ok') is True]
|
||||
notes = [f"{i}: {v.get('why', '')}" for i, v in ok.items() if v.get('ok') is not True]
|
||||
except (ValueError, KeyError, AttributeError) as e:
|
||||
except (ValueError, KeyError, AttributeError, openai.OpenAIError) as e:
|
||||
kept, notes = [], [f'وارسی ناموفق: {e}']
|
||||
with lock:
|
||||
stats['rejected'] += len(figures) - len(kept)
|
||||
|
||||
@@ -298,7 +298,7 @@ class Command(BaseCommand):
|
||||
"""[(lesson, chunk), ...] — فقط درسهایی که هنوز سوال کم دارند."""
|
||||
lessons = (
|
||||
Lesson.objects
|
||||
.filter(node__unit__grade=grade)
|
||||
.filter(node__unit__grade=grade, node__unit__subject__title=self._book.subject)
|
||||
.select_related('node__unit')
|
||||
.order_by('node__unit__order', 'node__order', 'id')
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
from content.models import Grade, Lesson, Node, Unit
|
||||
from content.models import Grade, Lesson, Node, Subject, Unit
|
||||
from rag.models import LessonChunk
|
||||
from rag.services.config import build_collection_name
|
||||
from rag.services.textbooks import TEXTBOOKS, get_textbook
|
||||
@@ -179,11 +179,17 @@ class Command(BaseCommand):
|
||||
title=grade_title, defaults={'icon': icon, 'color': color},
|
||||
)
|
||||
|
||||
# کتاب باید در جدولِ Subject باشد (migrationِ 0015 پنجتایی را میسازد؛ کتابِ تازه از ادمین).
|
||||
subject = Subject.objects.filter(title=book.subject).first()
|
||||
if subject is None:
|
||||
raise CommandError(
|
||||
f'کتابِ «{book.subject}» در جدولِ Subject نیست؛ از ادمین (محتوا ← کتابها) بسازید.'
|
||||
)
|
||||
counts = {'units': 0, 'nodes': 0, 'lessons': 0}
|
||||
for chapter, groups in plan:
|
||||
unit, _ = Unit.objects.update_or_create(
|
||||
grade=grade, title=chapter.title,
|
||||
defaults={'order': chapter.number},
|
||||
defaults={'order': chapter.number, 'subject': subject},
|
||||
)
|
||||
counts['units'] += 1
|
||||
|
||||
|
||||
36
content/management/commands/split_long_nodes.py
Normal file
36
content/management/commands/split_long_nodes.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
جزیرههای بلند را به جزیرههای حداکثر ۶ درسی میشکند (منطق در `content/node_split.py`).
|
||||
|
||||
python manage.py split_long_nodes --grade 8 --dry-run
|
||||
python manage.py split_long_nodes --grade 4 --grade 5
|
||||
|
||||
پایههای ۳ تا ۱۲ را migrationِ 0018 یک بار شکسته است؛ این دستور برای پایهی تازه یا آستانهی دیگر است و اجرای
|
||||
دوبارهاش بیاثر. بعد از اجرا فایلهای seed را دوباره بسازید (`seed_content --export`)، وگرنه
|
||||
`seed_content` جزیرههای بلندِ قدیمی را کنارِ بخشهای تازه دوباره میسازد.
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from content.models import Lesson, Node, NodeProgress, Unit, UserNodeCompletion
|
||||
from content.node_split import split_long_nodes
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'شکستنِ جزیرههای بلند به جزیرههای کوتاهتر، با حفظِ پیشرفتِ کاربران.'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--grade', type=int, action='append', required=True,
|
||||
help='شمارهی پایه؛ چند بار قابلِ تکرار.')
|
||||
parser.add_argument('--max-lessons', type=int, default=7)
|
||||
parser.add_argument('--part-cap', type=int, default=6)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
|
||||
def handle(self, *args, **opts):
|
||||
units = Unit.objects.filter(grade__number__in=opts['grade']).order_by('id')
|
||||
with transaction.atomic():
|
||||
counts = split_long_nodes(
|
||||
Node=Node, Lesson=Lesson, UserNodeCompletion=UserNodeCompletion,
|
||||
NodeProgress=NodeProgress, units=units,
|
||||
max_lessons=opts['max_lessons'], part_cap=opts['part_cap'], dry_run=opts['dry_run'],
|
||||
)
|
||||
self.stdout.write(f"{'(dry-run) ' if opts['dry_run'] else ''}{counts}")
|
||||
Reference in New Issue
Block a user