Files
tg_bot_language/bot/handlers/level_test.py
mamonov.ep 72a63eeda5 Добавлены основные функции MVP: тематические подборки, импорт слов, диалоговая практика, напоминания и тест уровня
Новые команды:
- /words [тема] - AI-генерация тематических подборок слов (10 слов по теме с учётом уровня)
- /import - извлечение до 15 ключевых слов из текста (книги, статьи, песни)
- /practice - диалоговая практика с AI в 6 сценариях (ресторан, магазин, путешествие, работа, врач, общение)
- /reminder - настройка ежедневных напоминаний по расписанию
- /level_test - тест из 7 вопросов для определения уровня английского (A1-C2)

Основные изменения:
- AI сервис: добавлены методы generate_thematic_words, extract_words_from_text, start_conversation, continue_conversation, generate_level_test
- Диалоговая практика: исправление ошибок в реальном времени, подсказки, перевод реплик
- Напоминания: APScheduler для ежедневной отправки напоминаний в выбранное время
- Тест уровня: автоматическое определение уровня при регистрации, можно пропустить
- База данных: добавлены поля reminders_enabled, last_reminder_sent
- Vocabulary service: метод get_word_by_original для проверки дубликатов
- Зависимости: apscheduler==3.10.4

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 15:46:02 +03:00

265 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from database.db import async_session_maker
from database.models import LanguageLevel
from services.user_service import UserService
from services.ai_service import ai_service
router = Router()
class LevelTestStates(StatesGroup):
"""Состояния для прохождения теста уровня"""
taking_test = State()
@router.message(Command("level_test"))
async def cmd_level_test(message: Message, state: FSMContext):
"""Обработчик команды /level_test"""
await start_level_test(message, state)
async def start_level_test(message: Message, state: FSMContext):
"""Начать тест определения уровня"""
# Показываем описание теста
await message.answer(
"📊 <b>Тест определения уровня</b>\n\n"
"Этот короткий тест поможет определить твой уровень английского.\n\n"
"📋 Тест включает 7 вопросов:\n"
"• Грамматика\n"
"• Лексика\n"
"• Понимание\n\n"
"⏱ Займёт около 2-3 минут\n\n"
"Готов начать?"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="✅ Начать тест", callback_data="start_test")],
[InlineKeyboardButton(text="❌ Отмена", callback_data="cancel_test")]
])
await message.answer("Нажми кнопку когда будешь готов:", reply_markup=keyboard)
@router.callback_query(F.data == "cancel_test")
async def cancel_test(callback: CallbackQuery, state: FSMContext):
"""Отменить тест"""
await state.clear()
await callback.message.delete()
await callback.message.answer("❌ Тест отменён")
await callback.answer()
@router.callback_query(F.data == "start_test")
async def begin_test(callback: CallbackQuery, state: FSMContext):
"""Начать прохождение теста"""
await callback.message.delete()
# Показываем индикатор загрузки
loading_msg = await callback.message.answer("🔄 Генерирую вопросы...")
# Генерируем тест через AI
questions = await ai_service.generate_level_test()
await loading_msg.delete()
if not questions:
await callback.message.answer(
"Не удалось сгенерировать тест. Попробуй позже или используй /settings для ручной установки уровня."
)
await state.clear()
await callback.answer()
return
# Сохраняем данные в состоянии
await state.update_data(
questions=questions,
current_question=0,
correct_answers=0,
answers=[] # Для отслеживания ответов по уровням
)
await state.set_state(LevelTestStates.taking_test)
# Показываем первый вопрос
await show_question(callback.message, state)
await callback.answer()
async def show_question(message: Message, state: FSMContext):
"""Показать текущий вопрос"""
data = await state.get_data()
questions = data.get('questions', [])
current_idx = data.get('current_question', 0)
if current_idx >= len(questions):
# Тест завершён
await finish_test(message, state)
return
question = questions[current_idx]
# Формируем текст вопроса
text = (
f"❓ <b>Вопрос {current_idx + 1} из {len(questions)}</b>\n\n"
f"<b>{question['question']}</b>\n"
f"<i>{question.get('question_ru', '')}</i>\n\n"
)
# Создаем кнопки с вариантами ответа
keyboard = []
letters = ['A', 'B', 'C', 'D']
for idx, option in enumerate(question['options']):
keyboard.append([
InlineKeyboardButton(
text=f"{letters[idx]}) {option}",
callback_data=f"answer_{idx}"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
await message.answer(text, reply_markup=reply_markup)
@router.callback_query(F.data.startswith("answer_"), LevelTestStates.taking_test)
async def process_answer(callback: CallbackQuery, state: FSMContext):
"""Обработать ответ на вопрос"""
answer_idx = int(callback.data.split("_")[1])
data = await state.get_data()
questions = data.get('questions', [])
current_idx = data.get('current_question', 0)
correct_answers = data.get('correct_answers', 0)
answers = data.get('answers', [])
question = questions[current_idx]
is_correct = (answer_idx == question['correct'])
# Сохраняем результат
if is_correct:
correct_answers += 1
# Сохраняем ответ с уровнем вопроса
answers.append({
'level': question['level'],
'correct': is_correct
})
# Показываем результат
if is_correct:
result_text = "✅ Правильно!"
else:
correct_option = question['options'][question['correct']]
result_text = f"❌ Неправильно\nПравильный ответ: <b>{correct_option}</b>"
await callback.message.edit_text(
f"❓ <b>Вопрос {current_idx + 1} из {len(questions)}</b>\n\n"
f"{result_text}"
)
# Переходим к следующему вопросу
await state.update_data(
current_question=current_idx + 1,
correct_answers=correct_answers,
answers=answers
)
# Небольшая пауза перед следующим вопросом
import asyncio
await asyncio.sleep(1.5)
await show_question(callback.message, state)
await callback.answer()
async def finish_test(message: Message, state: FSMContext):
"""Завершить тест и определить уровень"""
data = await state.get_data()
questions = data.get('questions', [])
correct_answers = data.get('correct_answers', 0)
answers = data.get('answers', [])
total = len(questions)
accuracy = int((correct_answers / total) * 100) if total > 0 else 0
# Определяем уровень на основе правильных ответов по уровням
level = determine_level(answers)
# Сохраняем уровень в базе данных
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.chat.id)
if user:
user.level = level
await session.commit()
# Описания уровней
level_descriptions = {
"A1": "Начальный - понимаешь основные фразы и можешь представиться",
"A2": "Элементарный - можешь общаться на простые темы",
"B1": "Средний - можешь поддержать беседу на знакомые темы",
"B2": "Выше среднего - свободно общаешься в большинстве ситуаций",
"C1": "Продвинутый - используешь язык гибко и эффективно",
"C2": "Профессиональный - владеешь языком на уровне носителя"
}
await state.clear()
result_text = (
f"🎉 <b>Тест завершён!</b>\n\n"
f"📊 Результаты:\n"
f"Правильных ответов: <b>{correct_answers}</b> из {total}\n"
f"Точность: <b>{accuracy}%</b>\n\n"
f"🎯 Твой уровень: <b>{level.value}</b>\n"
f"<i>{level_descriptions.get(level.value, '')}</i>\n\n"
f"Теперь задания и материалы будут подбираться под твой уровень!\n"
f"Ты можешь изменить уровень в любое время через /settings"
)
await message.answer(result_text)
def determine_level(answers: list) -> LanguageLevel:
"""
Определить уровень на основе ответов
Args:
answers: Список ответов с уровнями
Returns:
Определённый уровень
"""
# Подсчитываем правильные ответы по уровням
level_stats = {
'A1': {'correct': 0, 'total': 0},
'A2': {'correct': 0, 'total': 0},
'B1': {'correct': 0, 'total': 0},
'B2': {'correct': 0, 'total': 0},
'C1': {'correct': 0, 'total': 0},
'C2': {'correct': 0, 'total': 0}
}
for answer in answers:
level = answer['level']
if level in level_stats:
level_stats[level]['total'] += 1
if answer['correct']:
level_stats[level]['correct'] += 1
# Определяем уровень: ищем последний уровень, где правильно >= 50%
levels_order = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
determined_level = 'A1'
for level in levels_order:
if level_stats[level]['total'] > 0:
accuracy = level_stats[level]['correct'] / level_stats[level]['total']
if accuracy >= 0.5: # 50% и выше
determined_level = level
else:
# Если не прошёл этот уровень, останавливаемся
break
return LanguageLevel[determined_level]