diagrams for every grade, sectioned prompt, repair of real model slips; fill lesson texts and thin islands
Diagrams: 14 kinds drawn by the app (plane with constructed points, graphs with computed roots/extrema/tangents, number lines with intervals, solids, charts, tables, Venn, mapping, trees, fractions, grids, groups, clock). The catalog is split by topic and only the matching sections go next to a question (200-1073 tokens instead of 1854). parse() repairs slips seen on 96 real book problems: Persian digits in JSON numbers, 2*pi, // comments, bare items, Venn with Persian set names. Measured: 45/48 valid on the second round, 0/8 on plain arithmetic; visual review of 83: 64 right, 3 weak, 16 wrong. Content: 82 new questions so no island has fewer than four; fill_lesson_texts writes Unit.blurb (58) and Lesson.example/steps (590) from verified book problems, never the lesson's own quiz problem. seed_content only fills empty fields, so admin edits survive a deploy. Tedi unlocks at 500 xp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,8 +48,9 @@ class ValidateTests(SimpleTestCase):
|
||||
self.assertEqual(list(spec['points']), ['B'])
|
||||
|
||||
def test_labels_are_truncated_and_fraction_shaded_is_capped(self):
|
||||
# بیش از تعدادِ تکهها محدود میشود، نه اینکه کلِ شکل حذف شود
|
||||
spec = validate({'kind': 'fraction', 'parts': 4, 'shaded': 9, 'title': 'x' * 500})
|
||||
self.assertIsNone(spec)
|
||||
self.assertEqual(spec['shaded'], 4)
|
||||
spec = validate({'kind': 'fraction', 'parts': 4, 'shaded': 3, 'title': 'x' * 500})
|
||||
self.assertEqual((spec['parts'], spec['shaded'], len(spec['title'])), (4, 3, 40))
|
||||
|
||||
@@ -148,7 +149,7 @@ class ShapeHintTests(SimpleTestCase):
|
||||
from .services.generation import _user_prompt
|
||||
|
||||
for q in ('وترِ مثلث با ضلعهای ۳ و ۴؟', 'سهچهارم پیتزا', 'نمودار y=x²', 'sin 30°'):
|
||||
self.assertEqual(hint_for(q), DIAGRAM_HINT, q)
|
||||
self.assertTrue(hint_for(q).startswith(DIAGRAM_HINT), q)
|
||||
self.assertIn(DIAGRAM_HINT, _user_prompt(q, []))
|
||||
for q in ('۲۳ × ۴۵ چند میشود؟', 'عبارت (x+2)(x-3) را ساده کن.'):
|
||||
self.assertEqual(hint_for(q), '', q)
|
||||
@@ -169,3 +170,153 @@ class AxesDefaultTests(SimpleTestCase):
|
||||
graph = validate({'kind': 'plane', 'items': [{'type': 'function', 'expr': 'x^2', 'from': -2, 'to': 2}]})
|
||||
self.assertTrue(graph['axes'])
|
||||
self.assertTrue(validate({**CIRCLE})['axes']) # صریحاً true خواسته شده
|
||||
|
||||
|
||||
class SectionedSpecTests(SimpleTestCase):
|
||||
"""کنارِ هر سوال فقط بخشِ مربوط به همان سوال از کاتالوگ میرود (هزینهی توکن)."""
|
||||
|
||||
def test_only_matching_sections_are_sent(self):
|
||||
from .services.diagram import _GRAPH, _PLANE, _SOLID, _TREE, hint_for
|
||||
|
||||
solid = hint_for('حجمِ استوانهای با شعاعِ ۳ و ارتفاعِ ۵ چقدر است؟')
|
||||
self.assertIn(_SOLID, solid)
|
||||
self.assertNotIn(_GRAPH, solid)
|
||||
self.assertNotIn(_TREE, solid)
|
||||
graph = hint_for('نمودارِ تابعِ y = x² − 4 را رسم کن')
|
||||
self.assertIn(_GRAPH, graph)
|
||||
self.assertIn(_PLANE, graph) # نمودار داخلِ plane است
|
||||
self.assertNotIn(_SOLID, graph)
|
||||
self.assertLess(len(solid), len(graph))
|
||||
|
||||
def test_bar_chart_does_not_pull_function_graphs(self):
|
||||
from .services.diagram import _CHARTS, _GRAPH, hint_for
|
||||
|
||||
text = hint_for('نمودارِ ستونیِ آبمیوهها را رسم کن')
|
||||
self.assertIn(_CHARTS, text)
|
||||
self.assertNotIn(_GRAPH.split(':')[0], text.replace('رسم', ''))
|
||||
|
||||
|
||||
class ExpandedKindsTests(SimpleTestCase):
|
||||
"""نوعهای تازه: هر کدام با یک نمونهی واقعی و یک نمونهی خراب."""
|
||||
|
||||
def test_constructed_points_and_new_items(self):
|
||||
spec = validate({
|
||||
'kind': 'plane', 'points': {
|
||||
'A': [0, 0], 'B': [4, 0], 'C': [1, 3], 'M': {'mid': ['A', 'B']}, 'H': {'foot': ['C', 'A', 'B']},
|
||||
'I': {'intersect': [['A', 'C'], ['B', [0, 3]]]}, 'T': {'along': ['A', 'B', 0.25]}, 'F': {'fx': ['x^2', 3]},
|
||||
'X': {'mid': ['A']}, 'Y': {'fx': ['alert(1)', 2]},
|
||||
},
|
||||
'items': [
|
||||
{'type': 'segment', 'from': 'A', 'to': 'M', 'ticks': 2, 'color': 'green'},
|
||||
{'type': 'angle', 'vertex': 'A', 'from': 'B', 'to': 'C', 'arcs': 9},
|
||||
{'type': 'image', 'of': ['A', 'B', 'C'], 'transform': {'rotate': {'center': 'C', 'deg': -90}}},
|
||||
{'type': 'piecewise', 'pieces': [{'expr': 'x+1', 'from': -2, 'to': 1, 'closedTo': False}, {'expr': 'bad;', 'from': 1, 'to': 2}]},
|
||||
{'type': 'segment', 'from': 'A', 'to': 'B', 'color': '#ff0000'},
|
||||
],
|
||||
})
|
||||
self.assertEqual(set(spec['points']), {'A', 'B', 'C', 'M', 'H', 'I', 'T', 'F'})
|
||||
self.assertEqual(spec['items'][1]['arcs'], 3)
|
||||
self.assertEqual(spec['items'][2]['transform'], {'rotate': {'center': 'C', 'deg': -90.0}})
|
||||
self.assertEqual(len(spec['items'][3]['pieces']), 1)
|
||||
self.assertNotIn('color', spec['items'][4]) # فقط نامِ رنگِ شناختهشده
|
||||
|
||||
def test_solid_chart_venn_tree_mapping_grid_clock_table(self):
|
||||
self.assertEqual(validate({'kind': 'solid', 'shape': 'cone', 'dims': {'r': 4, 'h': 12}, 'labels': {'r': '4 cm'}})['dims'], {'r': 4.0, 'h': 12.0})
|
||||
self.assertIsNone(validate({'kind': 'solid', 'shape': 'teapot'}))
|
||||
pie = validate({'kind': 'pie', 'labels': ['a', 'b', 'c'], 'values': [1, 2]})
|
||||
self.assertEqual(pie['labels'], ['a', 'b'])
|
||||
self.assertIsNone(validate({'kind': 'pie', 'labels': ['a'], 'values': [0]}))
|
||||
venn = validate({'kind': 'venn', 'sets': ['A', 'B'], 'regions': {'AB': ['3'], 'AC': ['x'], 'out': ['6']}, 'shade': ['AB', 'Z']})
|
||||
self.assertEqual(set(venn['regions']), {'AB', 'out'})
|
||||
self.assertEqual(venn['shade'], ['AB'])
|
||||
tree = validate({'kind': 'tree', 'root': {'label': 'شروع', 'children': [{'label': 'رو', 'p': '1/2', 'children': [{'label': 'a', 'children': [{'label': 'b', 'children': [{'label': 'too deep'}]}]}]}]}})
|
||||
leaf = tree['root']['children'][0]['children'][0]['children'][0]
|
||||
self.assertNotIn('children', leaf) # حداکثر سه سطح زیرِ ریشه
|
||||
m = validate({'kind': 'mapping', 'left': ['1', '2'], 'right': ['3'], 'arrows': [[0, 0], [1, 5]]})
|
||||
self.assertEqual(m['arrows'], [[0, 0]])
|
||||
self.assertEqual(validate({'kind': 'grid', 'rows': 5, 'cols': 5, 'shaded': 99})['shaded'], 25)
|
||||
self.assertEqual(validate({'kind': 'clock', 'hour': 3, 'minute': 25})['minute'], 25)
|
||||
self.assertEqual(validate({'kind': 'table', 'headers': ['x', '1'], 'rows': [['y', '+'], 'bad']})['rows'], [['y', '+']])
|
||||
|
||||
def test_numberline_intervals_and_fraction_step(self):
|
||||
nl = validate({'kind': 'numberline', 'min': 0, 'max': 2, 'step': 0.25, 'fractions': True,
|
||||
'intervals': [{'from': -2, 'to': 3, 'closedFrom': True}, {'from': 1, 'to': None}, {'from': 3, 'to': 1}]})
|
||||
self.assertEqual(nl['step'], 0.25)
|
||||
self.assertEqual(nl['intervals'], [{'from': -2.0, 'to': 3.0, 'closedFrom': True}, {'from': 1.0, 'to': None}])
|
||||
self.assertNotIn('step', validate({'kind': 'numberline', 'min': 0, 'max': 100, 'step': 0.01}))
|
||||
|
||||
|
||||
class ModelSlipRepairTests(SimpleTestCase):
|
||||
"""خطاهای قالبیِ واقعیِ gpt-4.1-mini در سنجشِ ۲۰۲۶-۰۹-۲۸؛ عیناً همان خروجیها."""
|
||||
|
||||
def test_persian_digits_in_json_numbers(self):
|
||||
from .services.diagram import parse
|
||||
|
||||
spec = parse('{"kind":"grid","title":"نمایش ۱۸ درصد","rows":۱۰,"cols":۱۰,"shaded":۱۸}')
|
||||
self.assertEqual((spec['rows'], spec['cols'], spec['shaded']), (10, 10, 18))
|
||||
|
||||
def test_pi_expressions_as_numbers(self):
|
||||
from .services.diagram import parse
|
||||
|
||||
spec = parse('{"kind":"plane","title":"t","axes":true,"items":[{"type":"function","expr":"sin(x)^2","from":-2*pi,"to":2*pi},'
|
||||
'{"type":"vline","x":pi/2},{"type":"vline","x":3pi/4}]}')
|
||||
self.assertAlmostEqual(spec['items'][0]['from'], -6.283185, places=5)
|
||||
self.assertAlmostEqual(spec['items'][1]['x'], 1.570796, places=5)
|
||||
self.assertAlmostEqual(spec['items'][2]['x'], 2.356194, places=5)
|
||||
|
||||
def test_bare_item_and_function_kind_become_a_plane(self):
|
||||
from .services.diagram import parse
|
||||
|
||||
bare = parse('{"type":"function","expr":"sqrt(3*x - 5)","from":1.6,"to":5,"label":"f(x) = √(3x - 5)","marks":["yIntercept"]}')
|
||||
self.assertEqual(bare['kind'], 'plane')
|
||||
self.assertTrue(bare['axes'])
|
||||
self.assertEqual(bare['items'][0]['expr'], 'sqrt(3*x - 5)')
|
||||
kinded = parse('{"kind":"function","title":"نمودار تابع p(x)","axes":true,"xRange":[-3,3],"yRange":[-10,10],'
|
||||
'"type":"function","expr":"x^3 - x^2 - 4*x + 4","from":-3,"to":3,"marks":["roots"]}')
|
||||
self.assertEqual(kinded['kind'], 'plane')
|
||||
self.assertEqual(kinded['xRange'], [-3.0, 3.0])
|
||||
self.assertEqual(kinded['items'][0]['marks'], ['roots'])
|
||||
|
||||
def test_venn_with_persian_set_names(self):
|
||||
from .services.diagram import parse
|
||||
|
||||
spec = parse('{"kind":"venn","title":"ون","sets":["والیبال","فوتبال"],"regions":{"والیبال":["7-x"],'
|
||||
'"والیبال_فوتبال":["x"],"فوتبال":["4-x"],"out":[]},"shade":["والیبال_فوتبال"]}')
|
||||
self.assertEqual(spec['sets'], ['A', 'B'])
|
||||
self.assertEqual(spec['names'], ['والیبال', 'فوتبال'])
|
||||
self.assertEqual(spec['regions'], {'A': ['7-x'], 'AB': ['x'], 'B': ['4-x'], 'out': []})
|
||||
self.assertEqual(spec['shade'], ['AB'])
|
||||
|
||||
def test_unknown_kind_is_still_dropped(self):
|
||||
from .services.diagram import parse
|
||||
|
||||
self.assertIsNone(parse('{"kind":"line_segment","points":[{"label":"مشهد","position":0}]}'))
|
||||
|
||||
def test_comments_after_values_and_wide_numberline(self):
|
||||
from .services.diagram import parse
|
||||
|
||||
spec = parse('{"kind":"venn","sets":["A","B","C"],"regions":{"AC":["5"]},"shade":[\n "AC", // الف\n "B" // ب\n]}')
|
||||
self.assertEqual(spec['shade'], ['AC', 'B'])
|
||||
wide = parse('{"kind":"numberline","min":-۱۴۰,"max":۸۷,"step":۱۰,"points":[{"at":-۵۵,"label":"-۵۵"}]}')
|
||||
self.assertEqual((wide['min'], wide['max'], wide['step']), (-140.0, 87.0, 10.0))
|
||||
|
||||
|
||||
class SecondRoundKindsTests(SimpleTestCase):
|
||||
"""افزودههای دورِ دومِ سنجش: خط از معادله، مقایسهی کسرها، عددِ مخلوط، محرکِ ساعت."""
|
||||
|
||||
def test_line_from_equation(self):
|
||||
spec = validate({'kind': 'plane', 'items': [{'type': 'line', 'eq': [3, 5, 4]}, {'type': 'line', 'eq': [0, 0, 1]}]})
|
||||
self.assertEqual(spec['items'], [{'type': 'line', 'eq': [3.0, 5.0, 4.0]}])
|
||||
|
||||
def test_fraction_compare_and_wholes(self):
|
||||
cmp = validate({'kind': 'fraction', 'compare': [{'parts': 5, 'shaded': 1, 'label': 'یکپنجم'}, {'parts': 10, 'shaded': 12}]})
|
||||
self.assertEqual(cmp['compare'][1], {'parts': 10, 'shaded': 10})
|
||||
mixed = validate({'kind': 'fraction', 'parts': 10, 'shaded': 7, 'wholes': 2})
|
||||
self.assertEqual((mixed['wholes'], mixed['shaded']), (2, 7))
|
||||
|
||||
def test_clock_only_for_telling_time(self):
|
||||
from .services.diagram import _CLOCK, hint_for
|
||||
|
||||
self.assertNotIn(_CLOCK, hint_for('فاطمه ۶ ساعت کتاب خواند. چند صفحه ماند؟'))
|
||||
self.assertIn(_CLOCK, hint_for('عقربهی ساعتشمار روی ۳ است. ساعت چند است؟'))
|
||||
self.assertIn(_CLOCK, hint_for('ساعت ۳ و ۲۵ دقیقه را روی ساعت نشان بده'))
|
||||
|
||||
Reference in New Issue
Block a user