Добавлены основные функции 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>
This commit is contained in:
2025-12-04 15:46:02 +03:00
parent 2c51fa19b6
commit 72a63eeda5
13 changed files with 1781 additions and 23 deletions

228
bot/handlers/practice.py Normal file
View File

@@ -0,0 +1,228 @@
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 services.user_service import UserService
from services.ai_service import ai_service
router = Router()
class PracticeStates(StatesGroup):
"""Состояния для диалоговой практики"""
choosing_scenario = State()
in_conversation = State()
# Доступные сценарии
SCENARIOS = {
"restaurant": "🍽️ Ресторан",
"shopping": "🛍️ Магазин",
"travel": "✈️ Путешествие",
"work": "💼 Работа",
"doctor": "🏥 Врач",
"casual": "💬 Общение"
}
@router.message(Command("practice"))
async def cmd_practice(message: Message, state: FSMContext):
"""Обработчик команды /practice"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
await message.answer("Сначала запусти бота командой /start")
return
# Показываем выбор сценария
keyboard = []
for scenario_id, scenario_name in SCENARIOS.items():
keyboard.append([
InlineKeyboardButton(
text=scenario_name,
callback_data=f"scenario_{scenario_id}"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
await state.update_data(user_id=user.id, level=user.level.value)
await state.set_state(PracticeStates.choosing_scenario)
await message.answer(
"💬 <b>Диалоговая практика с AI</b>\n\n"
"Выбери сценарий для разговора:\n\n"
"• AI будет играть роль собеседника\n"
"• Ты можешь общаться на английском\n"
"• AI будет исправлять твои ошибки\n"
"• Используй /stop для завершения диалога\n\n"
"Выбери сценарий:",
reply_markup=reply_markup
)
@router.callback_query(F.data.startswith("scenario_"), PracticeStates.choosing_scenario)
async def start_scenario(callback: CallbackQuery, state: FSMContext):
"""Начать диалог с выбранным сценарием"""
scenario = callback.data.split("_")[1]
data = await state.get_data()
level = data.get('level', 'B1')
# Удаляем клавиатуру
await callback.message.edit_reply_markup(reply_markup=None)
# Показываем индикатор
thinking_msg = await callback.message.answer("🤔 AI готовится к диалогу...")
# Начинаем диалог
conversation_start = await ai_service.start_conversation(scenario, level)
await thinking_msg.delete()
# Сохраняем данные в состоянии
await state.update_data(
scenario=scenario,
scenario_name=SCENARIOS[scenario],
conversation_history=[],
message_count=0
)
await state.set_state(PracticeStates.in_conversation)
# Формируем сообщение
text = (
f"<b>{SCENARIOS[scenario]}</b>\n\n"
f"📝 <i>{conversation_start.get('context', '')}</i>\n\n"
f"<b>AI:</b> {conversation_start.get('message', '')}\n"
f"<i>({conversation_start.get('translation', '')})</i>\n\n"
"💡 <b>Подсказки:</b>\n"
)
for suggestion in conversation_start.get('suggestions', []):
text += f"{suggestion}\n"
text += "\n📝 Напиши свой ответ на английском или используй /stop для завершения"
# Кнопки управления
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💡 Показать подсказки", callback_data="show_hints")],
[InlineKeyboardButton(text="🔚 Завершить диалог", callback_data="stop_practice")]
])
await callback.message.answer(text, reply_markup=keyboard)
await callback.answer()
@router.message(Command("stop"), PracticeStates.in_conversation)
async def stop_practice(message: Message, state: FSMContext):
"""Завершить диалоговую практику"""
data = await state.get_data()
message_count = data.get('message_count', 0)
await state.clear()
await message.answer(
f"✅ <b>Диалог завершён!</b>\n\n"
f"Сообщений обменено: <b>{message_count}</b>\n\n"
"Отличная работа! Продолжай практиковаться.\n"
"Используй /practice для нового диалога."
)
@router.callback_query(F.data == "stop_practice", PracticeStates.in_conversation)
async def stop_practice_callback(callback: CallbackQuery, state: FSMContext):
"""Завершить диалог через кнопку"""
data = await state.get_data()
message_count = data.get('message_count', 0)
await callback.message.delete()
await state.clear()
await callback.message.answer(
f"✅ <b>Диалог завершён!</b>\n\n"
f"Сообщений обменено: <b>{message_count}</b>\n\n"
"Отличная работа! Продолжай практиковаться.\n"
"Используй /practice для нового диалога."
)
await callback.answer()
@router.message(PracticeStates.in_conversation)
async def handle_conversation(message: Message, state: FSMContext):
"""Обработка сообщений в диалоге"""
user_message = message.text.strip()
if not user_message:
await message.answer("Напиши что-нибудь на английском или используй /stop для завершения")
return
data = await state.get_data()
conversation_history = data.get('conversation_history', [])
scenario = data.get('scenario', 'casual')
level = data.get('level', 'B1')
message_count = data.get('message_count', 0)
# Показываем индикатор
thinking_msg = await message.answer("🤔 AI думает...")
# Добавляем сообщение пользователя в историю
conversation_history.append({
"role": "user",
"content": user_message
})
# Получаем ответ от AI
ai_response = await ai_service.continue_conversation(
conversation_history=conversation_history,
user_message=user_message,
scenario=scenario,
level=level
)
await thinking_msg.delete()
# Добавляем ответ AI в историю
conversation_history.append({
"role": "assistant",
"content": ai_response.get('response', '')
})
# Обновляем состояние
message_count += 1
await state.update_data(
conversation_history=conversation_history,
message_count=message_count
)
# Формируем ответ
text = ""
# Показываем feedback, если есть ошибки
feedback = ai_response.get('feedback', {})
if feedback.get('has_errors') and feedback.get('corrections'):
text += f"⚠️ <b>Исправления:</b>\n{feedback['corrections']}\n\n"
if feedback.get('comment'):
text += f"💬 {feedback['comment']}\n\n"
# Ответ AI
text += (
f"<b>AI:</b> {ai_response.get('response', '')}\n"
f"<i>({ai_response.get('translation', '')})</i>\n\n"
)
# Подсказки
suggestions = ai_response.get('suggestions', [])
if suggestions:
text += "💡 <b>Подсказки:</b>\n"
for suggestion in suggestions[:3]:
text += f"{suggestion}\n"
# Кнопки
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔚 Завершить диалог", callback_data="stop_practice")]
])
await message.answer(text, reply_markup=keyboard)