Files
tg_bot_language/bot/handlers/practice.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

229 lines
8.3 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 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)