tutor runs on gpt-4.1-mini, and primary-school "/" is read as decimal or fraction by rule
Two benchmark rounds (14 models on 30 problems, then gpt-4o-mini vs gpt-4.1-mini on 102 problems under three prompt versions, hand-graded): gpt-4.1-mini scored 95-96/102 against 85-90 for gpt-4o-mini, at 0.12 vs 0.04 cents per answer. The default now lives in settings so the deploy needs no .env edit. The context note "in the book / is a decimal point, not a fraction" was written for the upper-grade books but went to every grade, so gpt-4.1-mini computed 1/7 + 1/8 as 3.5. For primary grades the note now says / can be either, and a reading rule sits next to the question itself (the same rule at the end of the system prompt had no effect). Slash problems went from 11 to 14 of 17; three are still wrong. generate_answer now reports the provider's exact per-call cost, reasoning tokens and whether the raw output broke the plain-text rule. The benchmark fixes primary grades being sent with no book context, caches retrieval so runs compare on identical context, and retries Qdrant timeouts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
3. **لحن سه سطح دارد** (دبستان / متوسطهی اول / متوسطهی دوم)، نه فقط نامِ پایه.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Iterator
|
||||
|
||||
@@ -29,13 +30,14 @@ from .mathtext import StreamSanitizer, to_plain
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_FOLLOW_UPS = ['یک مثال دیگه بزن', 'بیشتر توضیح بده']
|
||||
_RAW_MARKUP = re.compile(r'\\[A-Za-z(\[]|\$|\*\*|^\s*#{1,6}\s|^\s*\|\s*:?-{3,}', re.M)
|
||||
|
||||
# ⚠️ با هر تغییرِ معنادار در متنِ پرامپتهای پایین، این را یکی جلو ببرید.
|
||||
# روی هر پیامِ دستیار ذخیره میشود تا وقتی لایک/دیسلایکِ کاربرها جمع شد،
|
||||
# بشود گفت «کدام نسخهی پرامپت جوابِ بهتری میداد» — بدونِ این، همهی فیدبکها
|
||||
# در یک کاسه میریزند و مقایسهی نسخهها ممکن نیست. کشِ پاسخ هم با همین نسخه
|
||||
# کلید میخورد، پس جوابِ نسخهی قبلی بعد از دیپلوی دوباره سرو نمیشود.
|
||||
PROMPT_VERSION = 'tutor-v3-stepwise-plain'
|
||||
PROMPT_VERSION = 'tutor-v5-slash-in-question'
|
||||
|
||||
_PRIMARY = {'اول', 'دوم', 'سوم', 'چهارم', 'پنجم', 'ششم'}
|
||||
_MIDDLE = {'هفتم', 'هشتم', 'نهم'}
|
||||
@@ -130,7 +132,8 @@ _METHOD = (
|
||||
'- اگر سوال مفهومی است، مفهوم را با یک مثالِ عددیِ کوتاه و حلشده توضیح بده.\n'
|
||||
'- دقت از همهچیز مهمتر است. پیش از نوشتنِ جوابِ نهایی آن را وارسی کن '
|
||||
'(جواب را در معادله جایگذاری کن، مشتق یا سادهسازی را دوباره انجام بده، '
|
||||
'بزرگیِ عدد را تخمین بزن). اگر در وارسی اشتباهی دیدی، راهحل را اصلاح کن؛ '
|
||||
'بزرگیِ عدد را تخمین بزن؛ توانِ هر متغیر و علامتِ هر جمله را جدا با صورتِ '
|
||||
'سوال مقایسه کن). اگر در وارسی اشتباهی دیدی، راهحل را اصلاح کن؛ '
|
||||
'هرگز جوابی را که وارسی نکردهای ننویس.\n'
|
||||
'- اگر مسئله جوابِ مشخصی دارد، خطِ آخرِ حل این باشد: «✅ جواب: …».\n'
|
||||
'- اگر اطلاعاتِ مسئله ناقص است (مثلاً به شکلی اشاره میکند که نمیبینی)، '
|
||||
@@ -158,12 +161,35 @@ def system_prompt(grade_title: str = '') -> str:
|
||||
)
|
||||
|
||||
|
||||
def _user_prompt(question: str, contexts: list[str], focus: str = '') -> str:
|
||||
# «/» در سوالِ دبستانی هم ممیز است هم کسر: کتاب کسرِ دوطبقه را چاپ میکند و
|
||||
# استخراج/تایپ آن را صاف میکند. سنجشِ ۲۰۲۶-۰۹-۲۶: gpt-4.1-mini همه را ممیز
|
||||
# میخواند (۱/۷ + ۱/۸ = ۳/۵) و gpt-4o-mini بیشتر کسر. همین قاعده در انتهای
|
||||
# پرامپتِ سیستم اثری نداشت؛ اینجا کنارِ خودِ سوال است.
|
||||
_SLASH_READING = (
|
||||
'پیش از حل، معنای هر «/» در این سوال را تعیین کن و در خطِ اولِ پاسخ بنویس '
|
||||
'(مثلاً «اینجا ۲/۹ کسرِ دونهم است»): اگر سوال عددی با صفر پیش از «/» دارد '
|
||||
'(مثلِ ۰/۴) یا عدد همراهِ واحد آمده (۳/۶ متر)، «/» ممیزِ اعشار است. در غیرِ '
|
||||
'این صورت، و همیشه وقتی سوال کلمهی «کسر» دارد، «/» خطِ کسر است.'
|
||||
)
|
||||
|
||||
|
||||
def _user_prompt(question: str, contexts: list[str], focus: str = '',
|
||||
primary: bool = False) -> str:
|
||||
asked = f'سوالِ دانشآموز: {question}'
|
||||
slash_rule = primary and '/' in question
|
||||
if slash_rule:
|
||||
asked = f'{_SLASH_READING}\n\n{asked}'
|
||||
if focus:
|
||||
asked = f'{focus}\n\n{asked}'
|
||||
if not contexts:
|
||||
return asked if focus else question
|
||||
return asked if (focus or slash_rule) else question
|
||||
# این جمله برای کتابهای دبیرستان نوشته شده بود (۳/۱۰۴ = 3.104) ولی به همه
|
||||
# میرفت، و gpt-4.1-mini با آن هر کسرِ دبستانی را ممیز میخواند.
|
||||
slash_note = (
|
||||
'در متنِ کتاب «/» بینِ دو رقم گاهی ممیزِ اعشار است و گاهی خطِ کسر.'
|
||||
if primary else
|
||||
'در متنِ کتاب «/» بینِ دو رقم ممیزِ اعشار است (۳/۱۰۴ یعنی 3.104)، نه کسر.'
|
||||
)
|
||||
excerpts = '\n\n'.join(f'[{i}] {text}' for i, text in enumerate(contexts, 1))
|
||||
return (
|
||||
'چند بخش از کتابِ درسیِ همین دانشآموز که با جستجو پیدا شدهاند '
|
||||
@@ -173,8 +199,7 @@ def _user_prompt(question: str, contexts: list[str], focus: str = '') -> str:
|
||||
'اگر به سوال مربوطاند، از روش و نمادگذاریِ همانها پیروی کن؛ اگر مربوط '
|
||||
'نیستند نادیدهشان بگیر و از دانشِ خودت در سطحِ همین پایه جواب بده. '
|
||||
'متنِ کتاب از روی تصویر خوانده شده و ممکن است غلطِ تایپی داشته باشد؛ '
|
||||
'حسابِ خودت را ملاک بگذار. در متنِ کتاب «/» بینِ دو رقم ممیزِ اعشار است '
|
||||
'(۳/۱۰۴ یعنی 3.104)، نه کسر.\n\n'
|
||||
f'حسابِ خودت را ملاک بگذار. {slash_note}\n\n'
|
||||
f'{asked}'
|
||||
)
|
||||
|
||||
@@ -220,7 +245,8 @@ def build_messages(question: str, context_chunks: list[dict] | None,
|
||||
text = (turn.get('text') or '').strip()
|
||||
if text:
|
||||
messages.append({'role': role, 'content': text[:_HISTORY_CHARS]})
|
||||
messages.append({'role': 'user', 'content': _user_prompt(question, contexts, focus)})
|
||||
messages.append({'role': 'user', 'content': _user_prompt(
|
||||
question, contexts, focus, primary=grade_tier(grade_title) == 'primary')})
|
||||
return messages
|
||||
|
||||
|
||||
@@ -262,6 +288,14 @@ def generate_answer(question: str, context_chunks: list[dict] | None,
|
||||
# آیا تورِ ایمنی چیزی را عوض کرد؟ نشانِ این است که مدل قاعدهی «متنِ
|
||||
# ساده» را رعایت نکرده — برای مقایسهی مدلها در rag_model_benchmark.
|
||||
'sanitized': answer != raw,
|
||||
# LaTeX/markdownِ واقعی در خروجیِ خام. `sanitized` برای دبستان همیشه
|
||||
# روشن است (کسر به «۳ بر ۴» تبدیل میشود)؛ این یکی فقط نقضِ قاعده را میشمارد.
|
||||
'rawMarkup': bool(_RAW_MARKUP.search(raw)),
|
||||
'reasoningTokens': getattr(getattr(usage, 'completion_tokens_details', None),
|
||||
'reasoning_tokens', None) or 0,
|
||||
# GapGPT هزینهی دقیقِ هر فراخوانی را (به دلار) در usage میفرستد؛
|
||||
# providerی که نفرستد None میدهد.
|
||||
'costUsd': getattr(usage, 'cost', None),
|
||||
}
|
||||
return answer, list(_DEFAULT_FOLLOW_UPS), meta
|
||||
|
||||
|
||||
Reference in New Issue
Block a user