ClareVoiceV1 / api /learning_tracker_backend.py
ghazariann's picture
feat: add Learning Tracker Insights endpoint (section 3.4)
430c7f3
# api/learning_tracker_backend.py
"""
Learning Tracker AI Backend — v2.2
Generates weekly highlights and improvement suggestions for a student's learning summary.
Called by the Hanbridge backend service (no sessions, no RAG).
Ref: docs/AI_Interface_Design_v2.2.md §3.4
"""
import json
from api.config import async_client, DEFAULT_MODEL
from api.quiz_backend import _strip_markdown
async def generate_learning_tracker_insights(
context: dict,
language: str = "CN",
model_name: str | None = None,
) -> tuple[dict, int]:
"""
Analyze a student's weekly learning summary and return AI insights.
Returns: ({"weekHighlights": "...", "improvementSuggestions": "..."}, tokens_used)
Raises ValueError on bad AI output.
"""
model = model_name or DEFAULT_MODEL
lang_instruction = (
"Respond in Chinese (中文)."
if language.upper() in ("CN", "ZH", "中文")
else "Respond in English."
)
system = (
"You are an academic advisor AI. You will receive a JSON object containing a student's "
"weekly learning data: overall grade, course progress, attendance, skill mastery scores, "
"learning hours, and grade trend. Analyze the data and output ONLY a valid JSON object "
"with exactly two keys:\n"
' "weekHighlights": a 1-3 sentence summary of what the student did well this week,\n'
' "improvementSuggestions": a 1-3 sentence actionable recommendation for next week.\n'
"Do not include any other text, markdown, or explanation outside the JSON object.\n"
f"{lang_instruction}"
)
user = (
"Here is the student's weekly learning summary. Analyze it and return your insights:\n\n"
f"{json.dumps(context, ensure_ascii=False, default=str)}"
)
resp = await async_client.chat.completions.create(
model=model,
messages=[{"role": "system", "content": system}, {"role": "user", "content": user}],
temperature=0.4,
max_tokens=800,
)
content = (resp.choices[0].message.content or "").strip()
tokens_used = getattr(resp.usage, "total_tokens", None) or 0
content = _strip_markdown(content)
try:
data = json.loads(content)
except json.JSONDecodeError as exc:
raise ValueError("json_parse_error") from exc
if not isinstance(data, dict):
raise ValueError("json_parse_error")
week_highlights = (data.get("weekHighlights") or "").strip()
improvement_suggestions = (data.get("improvementSuggestions") or "").strip()
if not week_highlights or not improvement_suggestions:
raise ValueError("missing_fields")
return {
"weekHighlights": week_highlights,
"improvementSuggestions": improvement_suggestions,
}, tokens_used