diagram kit: triangles and quadrilaterals from problem data, shape patterns, translate points, arithmetic/quote/duplicate-key repairs, clock guard (tutor-v10-shape-anchor)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 06:11:43 +03:30
parent a8cd681d65
commit 50df7bd371
4 changed files with 335 additions and 13 deletions

View File

@@ -130,7 +130,7 @@ class DiagramThroughTheApiTests(TestCase):
message = Message.objects.get(role='assistant', conversation__user_id=self.user.id)
self.assertEqual(message.diagram, done['diagram'])
self.assertEqual(message.prompt_version, 'tutor-v8-asked-figure')
self.assertEqual(message.prompt_version, 'tutor-v10-shape-anchor')
history = self.client.get(f'/api/ai/conversations/{done["conversationId"]}/messages').json()
self.assertEqual(history['messages'][-1]['diagram']['kind'], 'plane')
self.assertIsNone(history['messages'][0]['diagram'])
@@ -356,7 +356,7 @@ class FollowUpHintTests(SimpleTestCase):
from .services.diagram import ASKED_HINT, DIAGRAM_SPEC, asked_hint_for
from .services.generation import build_messages
last = build_messages('یه باغچه ۵ در ۸ متره، شکلش رو برام بکش', None, 'ششم')[-1]['content']
last = build_messages('یه جعبه‌ی کادو دارم، شکلش رو برام بکش', None, 'ششم')[-1]['content']
self.assertIn(ASKED_HINT, last)
self.assertIn(DIAGRAM_SPEC, last)
for q in ('مشکل دارم با این سوال', 'می‌خوام یه سوال بپرسم', '۳۴۵ × ۱۲'):
@@ -390,3 +390,73 @@ class ThirdRoundTests(SimpleTestCase):
self.assertIn(_PARTS, hint_for('یک‌پنجم و نُه‌دهم رو با هم مقایسه کن، کدوم بزرگ‌تره؟'))
self.assertNotIn(_PARTS, hint_for('رتبه‌ی پنجمین نفر در مسابقه چند است؟'))
class FourthRoundTests(SimpleTestCase):
"""سنجشِ ۲۰۲۶-۰۹-۲۹ (۶۴ مسئله‌ی تازه): مثلث و چهارضلعی از روی داده، الگوی شکلی، و لغزش‌های JSON."""
def test_triangle_and_quad_from_problem_data(self):
from .services.diagram import parse
tri = parse('{"kind":"plane","items":[{"type":"triangle","names":["A","B","C"],"sides":{"AB":6,"AC":8},'
'"angles":{"A":90},"labels":{"BC":"x","Z":"?"},"altitude":"C"}]}')['items'][0]
self.assertEqual(tri['sides'], {'AB': 6.0, 'AC': 8.0})
self.assertEqual(tri['labels'], {'BC': 'x'})
self.assertEqual(tri['altitude'], 'C')
# زاویه‌هایی که جمعشان ۱۸۰ یا بیشتر است مثلث نمی‌سازند
self.assertIsNone(parse('{"kind":"plane","items":[{"type":"triangle","angles":{"A":100,"B":90}}]}'))
quad = parse('{"kind":"plane","items":[{"type":"quad","shape":"rhombus","d1":8,"d2":6}]}')['items'][0]
self.assertEqual((quad['names'], quad['d1'], quad['d2']), (['A', 'B', 'C', 'D'], 8.0, 6.0))
self.assertIsNone(parse('{"kind":"plane","items":[{"type":"quad","shape":"kite","w":3}]}'))
def test_pattern_kind(self):
from .services.diagram import _PATTERN, hint_for, parse
spec = parse('{"kind":"pattern","layout":"triangle","unit":"dot","stages":[1,2,3,40],"next":true}')
self.assertEqual(spec['stages'], [1, 2, 3, 12])
self.assertIn(_PATTERN, hint_for('در الگوی زیر شکلِ ۵ چند چوب کبریت دارد؟'))
def test_json_slips_seen_in_the_benchmark(self):
from .services.diagram import parse
# رشته‌ی بسته‌نشده پیش از }
self.assertEqual(parse('{"kind":"plane","items":[{"type":"segment","from":"A","to":"B","color":"orange}]}')
['items'][0]['color'], 'orange')
# کلیدِ تکراریِ points: هر دو نگه داشته می‌شوند
spec = parse('{"kind":"plane","points":{"A":[0,0]},"items":[{"type":"point","point":"A"}],"points":{"B":[1,1]}}')
self.assertEqual(set(spec['points']), {'A', 'B'})
# عددِ کسری بی‌گیومه؛ برچسبِ «۱۲/۱۸» دست نمی‌خورد
spec = parse('{"kind":"plane","items":[{"type":"function","expr":"x^2","from":-5/3,"to":5/3,"label":"12/18"}]}')
self.assertAlmostEqual(spec['items'][0]['from'], -5 / 3, places=4)
self.assertEqual(spec['items'][0]['label'], '12/18')
self.assertEqual(parse('{"kind":"compare","compare":[{"parts":5,"shaded":1},{"parts":10,"shaded":9}]}')['kind'],
'fraction')
def test_real_world_geometry_words_get_the_plane_section(self):
from .services.diagram import _PLANE, hint_for
for q in ('نردبانی به طولِ ۵ متر به دیوار تکیه داده شده', 'زمینی به طولِ ۲۰ و عرضِ ۱۲ متر'):
self.assertIn(_PLANE, hint_for(q), q)
class FifthRoundTests(SimpleTestCase):
"""نگهبانِ ساعت، نقطه‌ی انتقال، جای شکل."""
def test_clock_only_for_telling_time(self):
from .services.diagram import _CLOCK, fits, hint_for
clock = {'kind': 'clock', 'title': '', 'hour': 0, 'minute': 12}
self.assertIsNone(fits('۲۰ درصد یک ساعت چند دقیقه است؟', clock))
self.assertEqual(fits('ساعت ۳ و ۲۵ دقیقه را روی ساعت نشان بده', clock), clock)
self.assertNotIn(_CLOCK, hint_for('۲۰ درصد یک ساعت چند دقیقه است؟'))
self.assertEqual(fits('حالا ۱۰ دقیقه بعد؟\n\nساعت ۳ و ۲۵ دقیقه', clock), clock)
self.assertIsNone(fits('x', None))
def test_translate_point_and_shape_anchor(self):
from .services.diagram import parse
spec = parse('{"kind":"plane","points":{"A":[0,0],"B":{"translate":[2,3],"from":"A"}},'
'"items":[{"type":"quad","shape":"square","side":2,"at":[5,0]}]}')
self.assertEqual(spec['points']['B'], {'translate': [2.0, 3.0], 'from': 'A'})
self.assertEqual(spec['items'][0]['at'], [5.0, 0.0])
def test_shape_name_as_kind_with_aliases(self):
from .services.diagram import parse
item = parse('{"kind":"trapezoid","title":"ذوزنقه","bases":[10,6],"height":4,"color":"blue"}')['items'][0]
self.assertEqual((item['type'], item['shape'], item['w'], item['top'], item['h']), ('quad', 'trapezoid', 10.0, 6.0, 4.0))
item = parse('{"kind":"rectangle","length":8,"width":5}')['items'][0]
self.assertEqual((item['w'], item['h']), (8.0, 5.0))