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:
2026-09-28 17:44:44 +03:30
parent 74c3ee5e86
commit 33d8f43d74
10 changed files with 581 additions and 9 deletions

View File

@@ -54,6 +54,8 @@ def _annotation(retrieval_meta: dict, generation_meta: dict, *, from_cache: bool
'completion_tokens': int(generation_meta.get('completionTokens') or 0),
'latency_ms': int(generation_meta.get('latencyMs') or 0),
'from_cache': bool(from_cache),
# شکلِ اعتبارسنجی‌شده در rag.services.diagram؛ کشِ قدیمی آن را ندارد.
'diagram': generation_meta.get('diagram') if isinstance(generation_meta.get('diagram'), dict) else None,
}
@@ -461,6 +463,7 @@ class AskAIView(APIView):
'answer': answer_text,
'suggestedFollowUps': follow_ups,
'remainingQuota': remaining_quota,
'diagram': assistant_message.diagram,
'usedChunks': [
{'title': c.get('title', ''), 'summary': c.get('summary', '')}
for c in used_chunks
@@ -486,7 +489,7 @@ class AskAIView(APIView):
نسخه‌ی SSE از post(): همون منطقِ retrieval + کوتا + idempotency، ولی
پاسخ توکن‌به‌توکن استریم می‌شه. رویدادها:
event: chunk data: {"delta": "..."}
event: done data: {conversationId, messageId, remainingQuota, usedChunks[, retrievalDebug]}
event: done data: {conversationId, messageId, remainingQuota, diagram, usedChunks[, retrievalDebug]}
event: error data: {"detail": "..."}
فاز ۱ (رزرو کوتا + conversation + user_message) قبلاً در post() انجام شده.
"""
@@ -529,11 +532,12 @@ class AskAIView(APIView):
for c in used_chunks
]
parts: list[str] = []
stream_result: dict = {}
started = time.monotonic()
effective_grade = retrieval_meta.get('gradeTitle') or grade_title or ''
for delta in stream_answer(
v['userQuestion'], used_chunks or None, effective_grade,
history=history, focus=focus,
history=history, focus=focus, result=stream_result,
):
parts.append(delta)
yield _sse('chunk', {'delta': delta})
@@ -548,6 +552,7 @@ class AskAIView(APIView):
'promptVersion': PROMPT_VERSION,
'latencyMs': int((time.monotonic() - started) * 1000),
'hadContext': bool(used_chunks),
'diagram': stream_result.get('diagram'),
}
if cacheable:
answer_cache.set(cache_key, {
@@ -591,6 +596,7 @@ class AskAIView(APIView):
'messageId': str(existing.assistant_message_id),
'suggestedFollowUps': existing.suggested_follow_ups,
'remainingQuota': max(DAILY_AI_QUOTA - used_today, 0),
'diagram': existing.assistant_message.diagram,
'usedChunks': [],
})
return
@@ -601,6 +607,7 @@ class AskAIView(APIView):
'messageId': str(assistant_message.id),
'suggestedFollowUps': follow_ups,
'remainingQuota': max(DAILY_AI_QUOTA - used_today, 0),
'diagram': assistant_message.diagram,
'usedChunks': [
{'title': c.get('title', ''), 'summary': c.get('summary', '')}
for c in used_chunks
@@ -663,6 +670,7 @@ class AskAIView(APIView):
'answer': existing.assistant_message.text,
'suggestedFollowUps': existing.suggested_follow_ups,
'remainingQuota': remaining_quota,
'diagram': existing.assistant_message.diagram,
}
return AskResponseSerializer(data)
@@ -683,7 +691,8 @@ class ConversationMessagesView(APIView):
'conversationId': str(conversation.id),
'lessonId': str(conversation.lesson_id) if conversation.lesson_id else None,
'messages': [
{'messageId': str(m.id), 'role': m.role, 'text': m.text} for m in messages
{'messageId': str(m.id), 'role': m.role, 'text': m.text, 'diagram': m.diagram}
for m in messages
],
}
return Response(ConversationMessagesResponseSerializer(data).data)