draw a diagram in tutor answers: validated JSON from the model, rendered by the app; add the fourth companion tedi
The model ends a shape question with <diagram>{json}</diagram>; rag/services/diagram.py
splits it off before mathtext, validates it against a whitelist and stores it on
Message.diagram (ai_tutor.0006). It reaches the app in the SSE done event and in
conversation history. Prompt tutor-v6-diagrams; a hint next to the question (shape
words only) raised gpt-4.1-mini from 3/12 to 12/12 diagrams, 0/8 on arithmetic.
progress.0005 adds tedi (600 xp) to the companion catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ from collections.abc import Iterator
|
||||
from django.conf import settings
|
||||
|
||||
from . import clients
|
||||
from . import diagram as diagram_mod
|
||||
from .mathtext import StreamSanitizer, to_plain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -37,7 +38,7 @@ _RAW_MARKUP = re.compile(r'\\[A-Za-z(\[]|\$|\*\*|^\s*#{1,6}\s|^\s*\|\s*:?-{3,}',
|
||||
# بشود گفت «کدام نسخهی پرامپت جوابِ بهتری میداد» — بدونِ این، همهی فیدبکها
|
||||
# در یک کاسه میریزند و مقایسهی نسخهها ممکن نیست. کشِ پاسخ هم با همین نسخه
|
||||
# کلید میخورد، پس جوابِ نسخهی قبلی بعد از دیپلوی دوباره سرو نمیشود.
|
||||
PROMPT_VERSION = 'tutor-v5-slash-in-question'
|
||||
PROMPT_VERSION = 'tutor-v6-diagrams'
|
||||
|
||||
_PRIMARY = {'اول', 'دوم', 'سوم', 'چهارم', 'پنجم', 'ششم'}
|
||||
_MIDDLE = {'هفتم', 'هشتم', 'نهم'}
|
||||
@@ -158,6 +159,7 @@ def system_prompt(grade_title: str = '') -> str:
|
||||
f'تو «پی» هستی، دستیارِ ریاضیِ اپِ آموزشیِ تدریسینو، و مثلِ یک {teacher} '
|
||||
f'با یک {student} حرف میزنی. همیشه فارسی جواب بده.\n\n'
|
||||
f'{_TONE[grade_tier(grade_title)]}\n\n{_METHOD}\n\n{_FORMAT.format(numbers=numbers)}'
|
||||
f'\n\n{diagram_mod.DIAGRAM_RULE}'
|
||||
)
|
||||
|
||||
|
||||
@@ -176,13 +178,16 @@ _SLASH_READING = (
|
||||
def _user_prompt(question: str, contexts: list[str], focus: str = '',
|
||||
primary: bool = False) -> str:
|
||||
asked = f'سوالِ دانشآموز: {question}'
|
||||
shape_hint = diagram_mod.hint_for(f'{question}\n{focus}')
|
||||
if shape_hint:
|
||||
asked = f'{asked}\n\n{shape_hint}'
|
||||
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 or slash_rule) else question
|
||||
return asked if (focus or slash_rule or shape_hint) else question
|
||||
# این جمله برای کتابهای دبیرستان نوشته شده بود (۳/۱۰۴ = 3.104) ولی به همه
|
||||
# میرفت، و gpt-4.1-mini با آن هر کسرِ دبستانی را ممیز میخواند.
|
||||
slash_note = (
|
||||
@@ -273,6 +278,8 @@ def generate_answer(question: str, context_chunks: list[dict] | None,
|
||||
latency_ms = int((time.monotonic() - started) * 1000)
|
||||
|
||||
raw = (completion.choices[0].message.content or '').strip()
|
||||
# بلوکِ شکل پیش از to_plain جدا میشود؛ to_plain کسرها و «*» را در JSON عوض میکرد.
|
||||
raw, diagram = diagram_mod.extract(raw)
|
||||
answer = to_plain(raw, fraction_word=grade_tier(grade_title) == 'primary')
|
||||
if not answer:
|
||||
answer = 'متأسفم، الان نتونستم پاسخِ مناسبی بسازم. لطفاً دوباره بپرس.'
|
||||
@@ -296,14 +303,18 @@ def generate_answer(question: str, context_chunks: list[dict] | None,
|
||||
# GapGPT هزینهی دقیقِ هر فراخوانی را (به دلار) در usage میفرستد؛
|
||||
# providerی که نفرستد None میدهد.
|
||||
'costUsd': getattr(usage, 'cost', None),
|
||||
'diagram': diagram,
|
||||
}
|
||||
return answer, list(_DEFAULT_FOLLOW_UPS), meta
|
||||
|
||||
|
||||
def stream_answer(question: str, context_chunks: list[dict] | None,
|
||||
grade_title: str = '', history: list[dict] | None = None,
|
||||
focus: str = '') -> Iterator[str]:
|
||||
focus: str = '', result: dict | None = None) -> Iterator[str]:
|
||||
"""
|
||||
`result` (اختیاری) بعد از پایانِ استریم کلیدِ 'diagram' را میگیرد: شکلِ
|
||||
اعتبارسنجیشده یا None. بلوکِ شکل هیچوقت بهصورتِ متن yield نمیشود.
|
||||
|
||||
همون generate_answer ولی تکهبهتکه (برای SSE). فقط متنِ پاسخ را yield
|
||||
میکند؛ follow_upها ثابتاند و caller خودش اضافهشان میکند.
|
||||
|
||||
@@ -319,16 +330,19 @@ def stream_answer(question: str, context_chunks: list[dict] | None,
|
||||
**completion_kwargs(model),
|
||||
)
|
||||
sanitizer = StreamSanitizer(fraction_word=grade_tier(grade_title) == 'primary')
|
||||
splitter = diagram_mod.StreamSplitter()
|
||||
for event in stream:
|
||||
if not event.choices:
|
||||
continue
|
||||
delta = event.choices[0].delta.content or ''
|
||||
if delta:
|
||||
clean = sanitizer.feed(delta)
|
||||
clean = sanitizer.feed(splitter.feed(delta))
|
||||
if clean:
|
||||
yield clean
|
||||
tail = sanitizer.flush()
|
||||
if tail:
|
||||
tail = sanitizer.feed(splitter.flush()) + sanitizer.flush()
|
||||
if result is not None:
|
||||
result['diagram'] = splitter.diagram
|
||||
if tail.strip():
|
||||
yield tail
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user