tutor-v7-diagram-kit: anchor short follow-ups to the previous question, new diagram pieces in the prompt
A real chat answered "what if 6 were 4?" with a semicircle problem from the retrieved book excerpt instead of the parabola asked a moment before. A short follow-up now carries the previous user question next to it. The catalog teaches line equations as written, regularPolygon, exteriorAngle, graphs only through function items and tangents through tangent; fraction words (یکپنجم، نُهدهم) pull the fraction section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,18 +58,24 @@ _PLANE = (
|
||||
'items (همه color، dashed و label اختیاری دارند): {"type":"point","point":"A"} · '
|
||||
'{"type":"segment","from":"A","to":"B","label":"۵","ticks":1} (ticks علامتِ ضلعِ مساوی ۱ تا ۳) · '
|
||||
'{"type":"vector","from":"A","to":"B"} · {"type":"line","through":["A","B"]} (خطِ بیپایان؛ ضلع و قطر segmentاند) · '
|
||||
'{"type":"line","eq":[3,5,4]} یعنی خطِ 3x + 5y = 4 (ضریبها را عیناً از صورتِ مسئله بردار؛ خودت شیب حساب نکن) · '
|
||||
'{"type":"line","equation":"3x + 5y = 4"} خط از روی معادله، عیناً همانطور که در مسئله یا جوابت نوشتهای '
|
||||
'(«y = 12x − 16»، «x = 2.5» هم درست است؛ خودت شیب و نقطه حساب نکن) · '
|
||||
'{"type":"ray","from":"A","through":"B"} · {"type":"circle","center":"O","r":2} یا "through":"A" · '
|
||||
'{"type":"arc","center":"O","r":2,"start":0,"end":60} · {"type":"sector",…} · '
|
||||
'{"type":"angle","vertex":"B","from":"A","to":"C","label":"۴۰°","arcs":1} (arcs ۱ تا ۳ برای زاویههای مساوی؛ '
|
||||
'زاویهی خارجیِ رأسِ B: نقطهی "E":{"along":["A","B",1.5]} روی امتدادِ AB، بعد angle از E تا C) · '
|
||||
'{"type":"angle","vertex":"B","from":"A","to":"C","label":"۴۰°","arcs":1} (زاویهی داخلی؛ arcs ۱ تا ۳ برای زاویههای مساوی) · '
|
||||
'{"type":"rightAngle","vertex":"H","from":"A","to":"B"} · {"type":"polygon","points":["A","B","C"]} · '
|
||||
'{"type":"regularPolygon","n":5,"r":2,"names":["A","B","C","D","E"]} (چندضلعیِ منتظم؛ رأسها را اپ میسازد و '
|
||||
'با همین نامها قابلِ ارجاعاند؛ مختصاتِ رأسِ چندضلعیِ منتظم را هرگز خودت ننویس) · '
|
||||
'{"type":"exteriorAngle","vertex":"B","from":"A","to":"C","label":"72°"} (زاویهی خارجیِ رأسِ B: اپ ضلعِ AB را از B '
|
||||
'امتداد میدهد و زاویهی بینِ امتداد و BC را میکشد) · '
|
||||
'{"type":"image","of":["A","B","C"],"transform":{"rotate":{"center":"C","deg":-90}}} (نامِ تصویرها A′؛ '
|
||||
'rotate مثبت=پادساعتگرد و «در جهتِ عقربهها» منفی؛ یا {"reflect":"x"|"y"|"y=x"|["A","B"]}، {"translate":[2,1]}، '
|
||||
'{"scale":{"center":"O","k":2}}) · {"type":"text","at":[1,2],"text":"…"}'
|
||||
)
|
||||
_GRAPH = (
|
||||
'نمودارِ تابع (در plane با "axes":true؛ "trig":true یعنی محورِ x با مضربِ π؛ "xRange"/"yRange" اختیاری): '
|
||||
'نمودارِ تابع (در plane با "axes":true؛ "trig":true یعنی محورِ x با مضربِ π؛ "xRange"/"yRange" اختیاری). '
|
||||
'هر نمودار با item نوعِ function کشیده میشود؛ ریشه، رأس و اکسترمم را با marks بخواه، نه با نقطهی دستی. '
|
||||
'خطِ مماس همیشه با tangent: '
|
||||
'{"type":"function","expr":"x^2-2*x-3","from":-2,"to":4,"label":"y = x² − 2x − 3",'
|
||||
'"marks":["roots","vertex","axis","yIntercept"]} (marks: roots ریشه، vertex رأس، extrema ماکزیمم/مینیمم، axis '
|
||||
'محورِ تقارن؛ اپ دقیق حساب میکند) · {"type":"piecewise","pieces":[{"expr":"x+1","from":-2,"to":1,'
|
||||
@@ -132,7 +138,9 @@ SECTIONS = [
|
||||
(_W(_GEOMETRY_WORDS + _GRAPH_WORDS), _PLANE),
|
||||
(_W(_GRAPH_WORDS), _GRAPH),
|
||||
(_W((r'محور', r'بازه', r'نامعادله', r'واحد به')), _NUMBERLINE),
|
||||
(_W((r'کسر', r'تکه', r'پیتزا', r'کیک', r'درصد', r'خانه', r'گروه', r'نصف', r'چهارم')), _PARTS),
|
||||
(_W((r'کسر', r'تکه', r'پیتزا', r'کیک', r'درصد', r'خانه', r'گروه', r'نصف',
|
||||
# «یکپنجم»، «نُهدهم»، «سهچهارم»: کسر با کلمه، که کتابِ دبستان همینطور مینویسد
|
||||
r'(?:یک|دو|سه|چهار|پنج|شش|هفت|هشت|نه|نُه|ده)[\u200c\s]?(?:دوم|سوم|چهارم|پنجم|ششم|هفتم|هشتم|نهم|دهم)(?!ین)')), _PARTS),
|
||||
# «۶ ساعت مطالعه» مدت است، نه ساعتِ دیواری؛ فقط عقربه یا «ساعت … و … دقیقه»
|
||||
(_W((r'عقربه', r'ساعت\s*[0-9۰-۹]+\s*(?::|و)\s*[0-9۰-۹]+', r'ساعت\s*چند')), _CLOCK),
|
||||
(_W((r'ستونی', r'میلهای', r'دایرهای', r'خط\s*شکسته', r'میانگین', r'آمار', r'فراوانی')), _CHARTS),
|
||||
@@ -243,6 +251,19 @@ def _expr(value) -> str:
|
||||
return value.strip()
|
||||
|
||||
|
||||
_LINEAR_OK = re.compile(r'^[0-9xy+\-=.\s/]*$')
|
||||
|
||||
|
||||
def _linear_equation(value) -> str:
|
||||
"""«3x + 5y = 4»، «y = 12x − 16»: فقط x، y، عدد، + − = و ممیز؛ خواندنِ دقیقش با فرانت است."""
|
||||
if not isinstance(value, str):
|
||||
raise ValueError('equation')
|
||||
text = value.translate(_DIGITS).replace('−', '-').replace('–', '-').strip()
|
||||
if not text or len(text) > 60 or text.count('=') != 1 or not _LINEAR_OK.match(text) or not re.search('[xy]', text):
|
||||
raise ValueError('equation chars')
|
||||
return text
|
||||
|
||||
|
||||
def _pair(value, conv):
|
||||
if not (isinstance(value, (list, tuple)) and len(value) == 2):
|
||||
raise ValueError('pair')
|
||||
@@ -327,7 +348,11 @@ def _plane_item(item: dict) -> dict:
|
||||
if kind == 'segment' and item.get('ticks') is not None:
|
||||
out['ticks'] = _clamp(item['ticks'], 1, 3)
|
||||
elif kind == 'line':
|
||||
if item.get('eq') is not None:
|
||||
if item.get('equation') is not None:
|
||||
out['equation'] = _linear_equation(item['equation'])
|
||||
elif item.get('eq') is not None:
|
||||
# ax + by + c = 0 — همان صورتی که gpt-4.1-mini هر سه بار نوشت (سنجشِ ۲۰۲۶-۰۹-۲۹)؛
|
||||
# قراردادِ قبلیِ ax + by = c علامتِ c را برعکس میکرد و خط جابهجا میافتاد.
|
||||
eq = item['eq']
|
||||
if not (isinstance(eq, (list, tuple)) and len(eq) == 3):
|
||||
raise ValueError('eq')
|
||||
@@ -360,6 +385,20 @@ def _plane_item(item: dict) -> dict:
|
||||
out.update({'vertex': _ref(item['vertex']), 'from': _ref(item['from']), 'to': _ref(item['to'])})
|
||||
if kind == 'angle' and item.get('arcs') is not None:
|
||||
out['arcs'] = _clamp(item['arcs'], 1, 3)
|
||||
elif kind == 'regularPolygon':
|
||||
out['n'] = _clamp(item.get('n', 5), 3, 12)
|
||||
out['r'] = _num(item.get('r', 2), 0.01, MAX_COORD)
|
||||
if item.get('center') is not None:
|
||||
out['center'] = _ref(item['center'])
|
||||
if item.get('start') is not None:
|
||||
out['start'] = _num(item['start'], -360, 360)
|
||||
names = item.get('names')
|
||||
if isinstance(names, list):
|
||||
clean = [n for n in names[:out['n']] if isinstance(n, str) and _NAME_OK.match(n)]
|
||||
if clean:
|
||||
out['names'] = clean
|
||||
elif kind == 'exteriorAngle':
|
||||
out.update({'vertex': _ref(item['vertex']), 'from': _ref(item['from']), 'to': _ref(item['to'])})
|
||||
elif kind == 'polygon':
|
||||
points = item['points']
|
||||
if not isinstance(points, list) or not 3 <= len(points) <= 12:
|
||||
|
||||
@@ -38,7 +38,7 @@ _RAW_MARKUP = re.compile(r'\\[A-Za-z(\[]|\$|\*\*|^\s*#{1,6}\s|^\s*\|\s*:?-{3,}',
|
||||
# بشود گفت «کدام نسخهی پرامپت جوابِ بهتری میداد» — بدونِ این، همهی فیدبکها
|
||||
# در یک کاسه میریزند و مقایسهی نسخهها ممکن نیست. کشِ پاسخ هم با همین نسخه
|
||||
# کلید میخورد، پس جوابِ نسخهی قبلی بعد از دیپلوی دوباره سرو نمیشود.
|
||||
PROMPT_VERSION = 'tutor-v6-diagrams'
|
||||
PROMPT_VERSION = 'tutor-v7-diagram-kit'
|
||||
|
||||
_PRIMARY = {'اول', 'دوم', 'سوم', 'چهارم', 'پنجم', 'ششم'}
|
||||
_MIDDLE = {'هفتم', 'هشتم', 'نهم'}
|
||||
@@ -178,6 +178,11 @@ _SLASH_READING = (
|
||||
def _user_prompt(question: str, contexts: list[str], focus: str = '',
|
||||
primary: bool = False, previous: str = '') -> str:
|
||||
asked = f'سوالِ دانشآموز: {question}'
|
||||
# سوالِ کوتاهِ ادامهای («اگه بهجای ۶ بذاریم ۴؟») بدونِ لنگر، مدل را سراغِ تکهی کتابِ
|
||||
# نامربوط میبرد (چتِ واقعی، ۲۰۲۶-۰۹-۲۹: جوابِ یک «نیمدایره» بهجای همان سهمی).
|
||||
if previous and len(question) <= 80:
|
||||
asked = (f'{asked}\n(این ادامهی گفتگوست؛ سوالِ قبلیِ دانشآموز: «{previous[:300]}». '
|
||||
'اگر به آن مربوط است، همان مسئله را با تغییرِ خواستهشده حل کن.)')
|
||||
shape_hint = (diagram_mod.hint_for(f'{question}\n{focus}')
|
||||
or diagram_mod.follow_up_hint_for(question, previous))
|
||||
if shape_hint:
|
||||
@@ -188,7 +193,7 @@ def _user_prompt(question: str, contexts: list[str], focus: str = '',
|
||||
if focus:
|
||||
asked = f'{focus}\n\n{asked}'
|
||||
if not contexts:
|
||||
return asked if (focus or slash_rule or shape_hint) else question
|
||||
return asked if (focus or slash_rule or shape_hint or previous) else question
|
||||
# این جمله برای کتابهای دبیرستان نوشته شده بود (۳/۱۰۴ = 3.104) ولی به همه
|
||||
# میرفت، و gpt-4.1-mini با آن هر کسرِ دبستانی را ممیز میخواند.
|
||||
slash_note = (
|
||||
|
||||
@@ -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-v6-diagrams')
|
||||
self.assertEqual(message.prompt_version, 'tutor-v7-diagram-kit')
|
||||
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'])
|
||||
@@ -351,3 +351,32 @@ class FollowUpHintTests(SimpleTestCase):
|
||||
last = build_messages('حالا دایرهای به شعاعِ ۲', None, 'نهم', history=history)[-1]['content']
|
||||
self.assertIn(DIAGRAM_HINT, last)
|
||||
self.assertNotIn(FOLLOW_UP_HINT, last)
|
||||
|
||||
|
||||
class ThirdRoundTests(SimpleTestCase):
|
||||
"""از چتِ واقعی با gpt-4.1-mini (۲۰۲۶-۰۹-۲۹): خط، چندضلعیِ منتظم، زاویهی خارجی، کسرِ کلمهای."""
|
||||
|
||||
def test_line_as_written_equation(self):
|
||||
spec = validate({'kind': 'plane', 'items': [
|
||||
{'type': 'line', 'equation': '3x + 5y = 4'}, {'type': 'line', 'equation': 'y = 12x − 16'},
|
||||
{'type': 'line', 'equation': 'y = sin(x)'}, {'type': 'line', 'equation': 'x + y'}]})
|
||||
self.assertEqual([i.get('equation') for i in spec['items']], ['3x + 5y = 4', 'y = 12x - 16'])
|
||||
|
||||
def test_eq_array_is_general_form(self):
|
||||
# عینِ خروجیِ مدل برای مماسِ y = 12x − 16
|
||||
spec = validate({'kind': 'plane', 'items': [{'type': 'line', 'eq': [-12, 1, 16]}]})
|
||||
self.assertEqual(spec['items'][0]['eq'], [-12.0, 1.0, 16.0])
|
||||
|
||||
def test_regular_polygon_and_exterior_angle(self):
|
||||
spec = validate({'kind': 'plane', 'items': [
|
||||
{'type': 'regularPolygon', 'n': 50, 'r': 2, 'names': ['A', 'B', 'C', 'D', 'E', 'bad name!']},
|
||||
{'type': 'exteriorAngle', 'vertex': 'B', 'from': 'A', 'to': 'C', 'label': '72°'}]})
|
||||
self.assertEqual(spec['items'][0]['n'], 12)
|
||||
self.assertEqual(spec['items'][0]['names'], ['A', 'B', 'C', 'D', 'E'])
|
||||
self.assertEqual(spec['items'][1]['type'], 'exteriorAngle')
|
||||
|
||||
def test_fraction_words_pull_the_fraction_section(self):
|
||||
from .services.diagram import _PARTS, hint_for
|
||||
|
||||
self.assertIn(_PARTS, hint_for('یکپنجم و نُهدهم رو با هم مقایسه کن، کدوم بزرگتره؟'))
|
||||
self.assertNotIn(_PARTS, hint_for('رتبهی پنجمین نفر در مسابقه چند است؟'))
|
||||
|
||||
@@ -106,7 +106,9 @@ class TutorPromptTests(SimpleTestCase):
|
||||
self.assertEqual(messages[0]['role'], 'system')
|
||||
self.assertEqual(len(messages), 1 + HISTORY_TURNS + 1)
|
||||
self.assertEqual(messages[-2], {'role': 'assistant', 'content': 'جواب 9'})
|
||||
self.assertTrue(messages[-1]['content'].endswith('سوالِ دانشآموز: نفهمیدم'))
|
||||
self.assertIn('سوالِ دانشآموز: نفهمیدم', messages[-1]['content'])
|
||||
# سوالِ کوتاهِ ادامهای به سوالِ قبلیِ همان گفتگو لنگر میشود
|
||||
self.assertIn('سوالِ قبلیِ دانشآموز: «سوال 8»', messages[-1]['content'])
|
||||
self.assertIn('متنِ کتاب', messages[-1]['content'])
|
||||
|
||||
def test_reasoning_models_get_no_temperature(self):
|
||||
|
||||
Reference in New Issue
Block a user