feat(lessons): implement comprehensive lesson configurator, session runner and AI debrief (Beta)
This commit is contained in:
1 parent
31e105b2c2
commit
718e9797eb
12 files changed
+4093
-1
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user