feat(lessons): implement comprehensive lesson configurator, session runner and AI debrief (Beta)

This commit is contained in:
Antigravity committed 2026-10-07 13:03:16 +03:00
1 parent 31e105b2c2
commit 718e9797eb
12 files changed
+4093 -1

No files matched your search

+226
View File
@@ -350,3 +350,229 @@ async def warmup_ollama_model() -> bool:
err_msg = f"{type(e).__name__}: {e}" if str(e) else type(e).__name__
logger.warning(f"Ollama warmup deferred ({err_msg})")
return False
def generate_deterministic_debrief(
session_data: Dict[str, Any],
history_data: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Fast rule-based analytical debrief engine with Murphy pedagogic insight.
Ensures zero server delay and 100% reliable fallback if Ollama is offline.
"""
history_data = history_data or {}
total_score = int(session_data.get("total_score", 0))
errors = session_data.get("errors", [])
steps = session_data.get("steps", [])
if total_score >= 90:
grade = "A+"
headline = "Превосходная сессия! Отличная концентрация и высокая точность."
elif total_score >= 75:
grade = "A"
headline = "Отличная работа! Большинство заданий выполнено уверенно."
elif total_score >= 60:
grade = "B"
headline = "Хороший результат! Есть отдельные нюансы для закрепления."
else:
grade = "C"
headline = "Урок завершён! Рекомендуем уделить внимание повторению разобранных тем."
# Identify strengths
strengths = []
for s in steps:
st_type = s.get("type", "")
st_score = s.get("score", 100)
if st_score >= 80:
if st_type == "vocab":
strengths.append(f"Уверенное воспроизведение словарного запаса ({s.get('correct', 0)} из {s.get('total', 0)} слов).")
elif st_type == "grammar_review":
strengths.append("Успешное закрепление ранее изученных грамматических правил.")
elif st_type == "grammar_new":
strengths.append("Быстрое освоение новой грамматической темы Мерфи.")
elif st_type == "listening":
strengths.append("Отличное распознавание английской речи и ключевых деталей на слух.")
elif st_type == "reading":
words_saved = s.get("words_saved", 0)
strengths.append(f"Качественная словарная работа с текстом (+{words_saved} новых слов в словарь).")
if not strengths:
strengths.append("Вы успешно завершили все запланированные этапы комплексного урока.")
# Identify error patterns
error_patterns = []
chronic_vocab = set(history_data.get("chronic_vocab_errors", []))
past_grammar = set(history_data.get("past_grammar_errors", []))
for err in errors[:5]:
err_type = err.get("type", "")
if err_type == "grammar":
title = err.get("unit_title", "Грамматика")
unit_num = err.get("unit_num", 0)
formula = err.get("formula", "")
exp = err.get("explanation", "")
is_repeating = unit_num in past_grammar
repeat_tag = " [Повторяющаяся ошибка]" if is_repeating else ""
msg = f"{title}{repeat_tag}: {exp}" if exp else f"{title}{repeat_tag}: обратите внимание на формулу {formula}."
if msg not in error_patterns:
error_patterns.append(msg)
elif err_type == "vocab":
term = err.get("term", "")
trans = err.get("translation", "")
is_chronic = term.lower() in chronic_vocab
tag = " [Трудное слово — уже встречалась ошибка]" if is_chronic else ""
error_patterns.append(f"Слово «{term}» ({trans}){tag}: требует повторного прогона в тренажёре.")
elif err_type == "listening":
q = err.get("question", "")
ideal = err.get("ideal_answer", "")
error_patterns.append(f"Аудирование («{q}»): диктор обращал внимание на ответ «{ideal}».")
if not error_patterns:
error_patterns.append("Ошибок в этой сессии не зафиксировано! Все тестовые задания решены верно.")
# Tomorrow focus
tomorrow_focus = []
grammar_errors = [e for e in errors if e.get("type") == "grammar"]
vocab_errors = [e for e in errors if e.get("type") == "vocab"]
if grammar_errors:
u_name = grammar_errors[0].get("unit_title", "пройденную тему")
tomorrow_focus.append(f"Закрепить правило: {u_name}.")
if vocab_errors:
terms_str = ", ".join([f"«{e.get('term')}»" for e in vocab_errors[:3]])
tomorrow_focus.append(f"Повторить проблемные слова: {terms_str}.")
if not tomorrow_focus:
tomorrow_focus.append("Продолжить движение по учебнику Мерфи и расширять словарный запас.")
return {
"grade": grade,
"score": total_score,
"headline": headline,
"strengths": strengths[:3],
"error_patterns": error_patterns[:4],
"tomorrow_focus": tomorrow_focus[:3],
"source": "fallback:deterministic",
}
async def generate_lesson_ai_debrief(
session_data: Dict[str, Any],
history_data: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
"""
Generates a personalized pedagogic debrief using local LLM (Qwen 2.5 3B).
Blends deterministic fact verification with natural narrative mentorship.
Gracefully falls back to deterministic analysis if LLM is offline or delayed.
"""
history_data = history_data or {}
total_score = int(session_data.get("total_score", 0))
preset_name = session_data.get("preset_name", "Комплексный урок")
duration_min = round(session_data.get("duration_seconds", 0) / 60)
errors = session_data.get("errors", [])
steps = session_data.get("steps", [])
# Format steps summary
steps_lines = []
for s in steps:
steps_lines.append(f"- {s.get('title', s.get('type', 'Блок'))}: результат {s.get('score', 100)}%")
steps_summary_text = "\n".join(steps_lines) if steps_lines else "Все блоки пройдены"
# Format errors summary
errors_lines = []
for e in errors:
e_type = e.get("type", "")
if e_type == "grammar":
errors_lines.append(
f"- [Грамматика {e.get('unit_title', '')}]: ответ студента: '{e.get('user_answer', '')}', "
f"правильно: '{e.get('correct_answer', '')}'. Формула: {e.get('formula', '')}. {e.get('explanation', '')}"
)
elif e_type == "vocab":
errors_lines.append(
f"- [Слово]: '{e.get('term', '')}' ({e.get('translation', '')}) — ошибка в карточке."
)
elif e_type == "listening":
errors_lines.append(
f"- [Аудирование]: Вопрос '{e.get('question', '')}', ответ студента: '{e.get('user_answer', '')}', "
f"правильный ответ: '{e.get('ideal_answer', '')}'"
)
errors_text = "\n".join(errors_lines) if errors_lines else "Ошибок нет, 100% правильных ответов."
# Format historical context
chronic_vocab = history_data.get("chronic_vocab_errors", [])
past_grammar = history_data.get("past_grammar_errors", [])
history_lines = []
if chronic_vocab:
history_lines.append(f"- Слова с частыми ошибками в прошлом: {', '.join(chronic_vocab[:5])}")
if past_grammar:
history_lines.append(f"- Юниты грамматики с низкой точностью в прошлом: {', '.join(map(str, past_grammar[:3]))}")
history_text = "\n".join(history_lines) if history_lines else "История чистая или ошибок ранее не зафиксировано."
prompt = (
"Ты — чуткий, высококвалифицированный преподаватель английского языка по методике Raymond Murphy.\n"
"Студент только что завершил комплексный урок. Проанализируй его результаты, свяжи ошибки с грамматическими формулами "
"и составь лаконичный, мотивирующий и практически полезный педагогический разбор на русском языке.\n\n"
f"Урок: {preset_name}\n"
f"Длительность: {duration_min} мин\n"
f"Итоговый балл: {total_score}%\n\n"
f"Пройденные блоки:\n{steps_summary_text}\n\n"
f"Ошибки в этой сессии:\n{errors_text}\n\n"
f"История прошлых трудностей:\n{history_text}\n\n"
"Требования к ответу:\n"
"1. Ответь ИСКЛЮЧИТЕЛЬНО в формате JSON.\n"
"2. Поле 'headline': краткий, воодушевляющий заголовок (1 предложение).\n"
"3. Поле 'strengths': список из 1-3 пунктов, что получилось лучше всего.\n"
"4. Поле 'error_patterns': список из 1-3 пунктов с объяснением допущенных ошибок (если были) через контраст и формулы Мерфи. Если ошибок не было — похвали за точность.\n"
"5. Поле 'tomorrow_focus': список из 2-3 конкретных рекомендаций на завтра.\n\n"
"Формат JSON:\n"
"{\n"
f' "grade": "{"A+" if total_score >= 90 else ("A" if total_score >= 75 else ("B" if total_score >= 60 else "C"))}",\n'
f' "score": {total_score},\n'
' "headline": "заголовок",\n'
' "strengths": ["пункт 1", "пункт 2"],\n'
' "error_patterns": ["разбор ошибки 1", "разбор ошибки 2"],\n'
' "tomorrow_focus": ["совет 1", "совет 2"]\n'
"}"
)
payload = {
"model": config.OLLAMA_MODEL,
"prompt": prompt,
"stream": False,
"format": "json",
"keep_alive": "24h",
"options": {
"temperature": 0.3,
"num_predict": 450,
},
}
try:
async with httpx.AsyncClient(timeout=25.0) as client:
resp = await client.post(f"{config.OLLAMA_BASE_URL}/api/generate", json=payload)
if resp.status_code == 200:
raw_response = resp.json().get("response", "{}")
# Parse JSON
clean_raw = raw_response.strip()
if clean_raw.startswith("```json"):
clean_raw = clean_raw[7:]
if clean_raw.startswith("```"):
clean_raw = clean_raw[3:]
if clean_raw.endswith("```"):
clean_raw = clean_raw[:-3]
data = json.loads(clean_raw.strip())
return {
"grade": data.get("grade", "A" if total_score >= 75 else "B"),
"score": total_score,
"headline": data.get("headline", "Отличная сессия!"),
"strengths": data.get("strengths", ["Хорошая работа на уроке"]),
"error_patterns": data.get("error_patterns", []),
"tomorrow_focus": data.get("tomorrow_focus", ["Продолжайте ежедневные занятия"]),
"source": f"ollama:{config.OLLAMA_MODEL}",
}
except Exception as e:
logger.warning(f"Ollama debrief generation deferred to deterministic engine: {e}")
# Fallback to deterministic debrief
return generate_deterministic_debrief(session_data, history_data)