Files
tg_bot_language/bot/handlers/tasks.py
mamonov.ep eb666ec9bc feat: мульти-провайдер AI, выбор типов заданий, настройка количества
- Добавлена поддержка нескольких AI провайдеров (OpenAI, Google Gemini)
- Добавлена админ-панель (/admin) для переключения AI моделей
- Добавлен AIModelService для управления моделями в БД
- Добавлен выбор типа заданий (микс, перевод слов, подстановка, перевод предложений)
- Добавлена настройка количества заданий (5-15)
- ai_service динамически выбирает провайдера на основе активной модели
- Обработка ограничений моделей (temperature, response_format)
- Очистка markdown обёртки из ответов Gemini

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 15:16:24 +03:00

622 lines
24 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 WordSource
from services.user_service import UserService
from services.task_service import TaskService
from services.vocabulary_service import VocabularyService
from services.ai_service import ai_service
from utils.i18n import t, get_user_lang, get_user_translation_lang
from utils.levels import get_user_level_for_language
router = Router()
class TaskStates(StatesGroup):
"""Состояния для прохождения заданий"""
choosing_mode = State()
choosing_type = State() # Выбор типа заданий
doing_tasks = State()
waiting_for_answer = State()
# Типы заданий
TASK_TYPES = ['mix', 'word_translate', 'fill_blank', 'sentence_translate']
def get_task_type_keyboard(lang: str) -> InlineKeyboardMarkup:
"""Клавиатура выбора типа заданий"""
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'tasks.type_mix'), callback_data="task_type_mix")],
[InlineKeyboardButton(text=t(lang, 'tasks.type_word_translate'), callback_data="task_type_word_translate")],
[InlineKeyboardButton(text=t(lang, 'tasks.type_fill_blank'), callback_data="task_type_fill_blank")],
[InlineKeyboardButton(text=t(lang, 'tasks.type_sentence_translate'), callback_data="task_type_sentence_translate")],
])
@router.message(Command("task"))
async def cmd_task(message: Message, state: FSMContext):
"""Обработчик команды /task — показываем меню выбора режима"""
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(t('ru', 'common.start_first'))
return
lang = get_user_lang(user)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=t(lang, 'tasks.mode_vocabulary'),
callback_data="task_mode_vocabulary"
)],
[InlineKeyboardButton(
text=t(lang, 'tasks.mode_new_words'),
callback_data="task_mode_new"
)]
])
await state.update_data(user_id=user.id)
await state.set_state(TaskStates.choosing_mode)
await message.answer(t(lang, 'tasks.choose_mode'), reply_markup=keyboard)
@router.callback_query(F.data == "task_mode_vocabulary", TaskStates.choosing_mode)
async def choose_vocabulary_task_type(callback: CallbackQuery, state: FSMContext):
"""Показать выбор типа заданий для режима vocabulary"""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.message.edit_text(t('ru', 'common.start_first'))
return
lang = get_user_lang(user)
# Сохраняем режим и переходим к выбору типа
await state.update_data(user_id=user.id, mode='vocabulary')
await state.set_state(TaskStates.choosing_type)
await callback.message.edit_text(
t(lang, 'tasks.choose_type'),
reply_markup=get_task_type_keyboard(lang)
)
@router.callback_query(F.data.startswith("task_type_"), TaskStates.choosing_type)
async def start_tasks_with_type(callback: CallbackQuery, state: FSMContext):
"""Начать задания выбранного типа"""
await callback.answer()
task_type = callback.data.replace("task_type_", "") # mix, word_translate, fill_blank, sentence_translate
data = await state.get_data()
mode = data.get('mode', 'vocabulary')
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.message.edit_text(t('ru', 'common.start_first'))
return
lang = get_user_lang(user)
# Получаем количество заданий из настроек пользователя
tasks_count = getattr(user, 'tasks_count', 5) or 5
if mode == 'vocabulary':
# Генерируем задания по словам из словаря
tasks = await TaskService.generate_tasks_by_type(
session, user.id, count=tasks_count,
task_type=task_type,
learning_lang=user.learning_language,
translation_lang=get_user_translation_lang(user),
)
if not tasks:
await callback.message.edit_text(t(lang, 'tasks.no_words'))
await state.clear()
return
# Сохраняем задания в состоянии
await state.update_data(
tasks=tasks,
current_task_index=0,
correct_count=0,
user_id=user.id,
mode='vocabulary',
task_type=task_type
)
await state.set_state(TaskStates.doing_tasks)
await callback.message.delete()
await show_current_task(callback.message, state)
else:
# Режим new_words - генерируем новые слова
await generate_new_words_tasks(callback, state, user, task_type)
async def generate_new_words_tasks(callback: CallbackQuery, state: FSMContext, user, task_type: str):
"""Генерация заданий с новыми словами"""
lang = get_user_lang(user)
level = get_user_level_for_language(user)
tasks_count = getattr(user, 'tasks_count', 5) or 5
# Показываем индикатор загрузки
await callback.message.edit_text(t(lang, 'tasks.generating_new'))
async with async_session_maker() as session:
# Получаем слова для исключения
vocab_words = await VocabularyService.get_all_user_word_strings(
session, user.id, learning_lang=user.learning_language
)
correct_task_words = await TaskService.get_correctly_answered_words(
session, user.id
)
exclude_words = list(set(vocab_words + correct_task_words))
# Генерируем новые слова через AI
translation_lang = get_user_translation_lang(user)
words = await ai_service.generate_thematic_words(
theme="random everyday vocabulary",
level=level,
count=tasks_count,
learning_lang=user.learning_language,
translation_lang=translation_lang,
exclude_words=exclude_words if exclude_words else None,
)
if not words:
await callback.message.edit_text(t(lang, 'tasks.generate_failed'))
await state.clear()
return
# Преобразуем слова в задания нужного типа
tasks = await create_tasks_from_words(words, task_type, lang, user.learning_language, translation_lang)
await state.update_data(
tasks=tasks,
current_task_index=0,
correct_count=0,
user_id=user.id,
mode='new_words',
task_type=task_type
)
await state.set_state(TaskStates.doing_tasks)
await callback.message.delete()
await show_current_task(callback.message, state)
async def create_tasks_from_words(words: list, task_type: str, lang: str, learning_lang: str, translation_lang: str) -> list:
"""Создать задания из списка слов в зависимости от типа"""
import random
tasks = []
for word in words:
word_text = word.get('word', '')
translation = word.get('translation', '')
transcription = word.get('transcription', '')
example = word.get('example', '')
example_translation = word.get('example_translation', '')
if task_type == 'mix':
# Случайный тип
chosen_type = random.choice(['word_translate', 'fill_blank', 'sentence_translate'])
else:
chosen_type = task_type
if chosen_type == 'word_translate':
# Перевод слова
translate_prompt = t(lang, 'tasks.translate_to', lang_name=t(lang, f'lang.{translation_lang}'))
tasks.append({
'type': 'translate',
'question': f"{translate_prompt}: <b>{word_text}</b>",
'word': word_text,
'correct_answer': translation,
'transcription': transcription,
'example': example,
'example_translation': example_translation
})
elif chosen_type == 'fill_blank':
# Заполнение пропуска - генерируем предложение через AI
sentence_data = await ai_service.generate_fill_in_sentence(
word_text,
learning_lang=learning_lang,
translation_lang=translation_lang
)
if translation_lang == 'en':
fill_title = "Fill in the blank:"
elif translation_lang == 'ja':
fill_title = "空欄を埋めてください:"
else:
fill_title = "Заполни пропуск:"
tasks.append({
'type': 'fill_in',
'question': f"{fill_title}\n\n<b>{sentence_data['sentence']}</b>\n\n<i>{sentence_data.get('translation', '')}</i>",
'word': word_text,
'correct_answer': sentence_data['answer'],
'transcription': transcription,
'example': example,
'example_translation': example_translation
})
elif chosen_type == 'sentence_translate':
# Перевод предложения - генерируем предложение через AI
sentence_data = await ai_service.generate_sentence_for_translation(
word_text,
learning_lang=learning_lang,
translation_lang=translation_lang
)
if translation_lang == 'en':
sentence_title = "Translate the sentence:"
word_hint = "Word"
elif translation_lang == 'ja':
sentence_title = "文を翻訳してください:"
word_hint = "単語"
else:
sentence_title = "Переведи предложение:"
word_hint = "Слово"
tasks.append({
'type': 'sentence_translate',
'question': f"{sentence_title}\n\n<b>{sentence_data['sentence']}</b>\n\n📝 {word_hint}: <code>{word_text}</code> — {translation}",
'word': word_text,
'correct_answer': sentence_data['translation'],
'transcription': transcription,
'example': example,
'example_translation': example_translation
})
return tasks
@router.callback_query(F.data == "task_mode_new", TaskStates.choosing_mode)
async def choose_new_words_task_type(callback: CallbackQuery, state: FSMContext):
"""Показать выбор типа заданий для режима new_words"""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.message.edit_text(t('ru', 'common.start_first'))
return
lang = get_user_lang(user)
# Сохраняем режим и переходим к выбору типа
await state.update_data(user_id=user.id, mode='new_words')
await state.set_state(TaskStates.choosing_type)
await callback.message.edit_text(
t(lang, 'tasks.choose_type'),
reply_markup=get_task_type_keyboard(lang)
)
async def show_current_task(message: Message, state: FSMContext):
"""Показать текущее задание"""
data = await state.get_data()
tasks = data.get('tasks', [])
current_index = data.get('current_task_index', 0)
user_id = data.get('user_id')
if current_index >= len(tasks):
# Все задания выполнены
await finish_tasks(message, state)
return
task = tasks[current_index]
# Определяем язык пользователя (берём user_id из state, т.к. message может быть от бота)
async with async_session_maker() as session:
user = await UserService.get_user_by_id(session, user_id)
lang = (user.language_interface if user else 'ru') or 'ru'
task_text = (
t(lang, 'tasks.header', i=current_index + 1, n=len(tasks)) + "\n\n" +
f"{task['question']}\n"
)
if task.get('transcription'):
task_text += f"🔊 [{task['transcription']}]\n"
task_text += t(lang, 'tasks.write_answer')
await state.set_state(TaskStates.waiting_for_answer)
await message.answer(task_text)
@router.message(TaskStates.waiting_for_answer)
async def process_answer(message: Message, state: FSMContext):
"""Обработка ответа пользователя"""
user_answer = message.text.strip()
data = await state.get_data()
tasks = data.get('tasks', [])
current_index = data.get('current_task_index', 0)
correct_count = data.get('correct_count', 0)
user_id = data.get('user_id')
task = tasks[current_index]
# Показываем индикатор проверки
# Язык пользователя
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
checking_msg = await message.answer(t(lang, 'tasks.checking'))
# Проверяем ответ через AI
check_result = await ai_service.check_answer(
question=task['question'],
correct_answer=task['correct_answer'],
user_answer=user_answer
)
await checking_msg.delete()
is_correct = check_result.get('is_correct', False)
feedback = check_result.get('feedback', '')
# Формируем ответ
if is_correct:
result_text = t(lang, 'tasks.correct') + "\n\n"
correct_count += 1
else:
result_text = t(lang, 'tasks.incorrect') + "\n\n"
result_text += f"{t(lang, 'tasks.your_answer')}: <i>{user_answer}</i>\n"
result_text += f"{t(lang, 'tasks.right_answer')}: <b>{task['correct_answer']}</b>\n\n"
if feedback:
result_text += f"💬 {feedback}\n\n"
# Показываем пример использования если есть
example = task.get('example', '')
example_translation = task.get('example_translation', '')
if example:
result_text += f"📖 {t(lang, 'tasks.example_label')}:\n"
result_text += f"<i>{example}</i>\n"
if example_translation:
result_text += f"<i>({example_translation})</i>\n"
result_text += "\n"
# Сохраняем результат в БД
async with async_session_maker() as session:
await TaskService.save_task_result(
session=session,
user_id=user_id,
task_type=task['type'],
content={
'question': task['question'],
'word': task['word']
},
user_answer=user_answer,
correct_answer=task['correct_answer'],
is_correct=is_correct,
ai_feedback=feedback
)
# Обновляем статистику слова
if 'word_id' in task:
await TaskService.update_word_statistics(
session=session,
word_id=task['word_id'],
is_correct=is_correct
)
# Обновляем счетчик
await state.update_data(
current_task_index=current_index + 1,
correct_count=correct_count
)
# Показываем результат и кнопку "Далее"
mode = data.get('mode')
buttons = [[InlineKeyboardButton(text=t(lang, 'tasks.next_btn'), callback_data="next_task")]]
# Для режима new_words добавляем кнопку "Добавить слово"
if mode == 'new_words':
buttons.append([InlineKeyboardButton(
text=t(lang, 'tasks.add_word_btn'),
callback_data=f"add_task_word_{current_index}"
)])
buttons.append([InlineKeyboardButton(text=t(lang, 'tasks.stop_btn'), callback_data="stop_tasks")])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await message.answer(result_text, reply_markup=keyboard)
# После показа результата ждём нажатия кнопки переключаемся в состояние doing_tasks
await state.set_state(TaskStates.doing_tasks)
@router.callback_query(F.data == "next_task", TaskStates.doing_tasks)
async def next_task(callback: CallbackQuery, state: FSMContext):
"""Переход к следующему заданию"""
await callback.message.delete()
await show_current_task(callback.message, state)
await callback.answer()
@router.callback_query(F.data.startswith("add_task_word_"), TaskStates.doing_tasks)
async def add_task_word(callback: CallbackQuery, state: FSMContext):
"""Добавить слово из задания в словарь"""
task_index = int(callback.data.split("_")[-1])
data = await state.get_data()
tasks = data.get('tasks', [])
if task_index >= len(tasks):
await callback.answer()
return
task = tasks[task_index]
word = task.get('word', '')
translation = task.get('correct_answer', '')
transcription = task.get('transcription', '')
example = task.get('example', '') # Пример использования как контекст
example_translation = task.get('example_translation', '') # Перевод примера
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if not user:
await callback.answer()
return
lang = get_user_lang(user)
# Проверяем, есть ли слово уже в словаре
existing = await VocabularyService.get_word_by_original(session, user.id, word)
if existing:
await callback.answer(t(lang, 'tasks.word_already_exists', word=word), show_alert=True)
return
# Добавляем слово в словарь
new_word = await VocabularyService.add_word(
session=session,
user_id=user.id,
word_original=word,
word_translation=translation,
source_lang=user.learning_language,
translation_lang=get_user_translation_lang(user),
transcription=transcription,
source=WordSource.AI_TASK
)
# Сохраняем перевод в таблицу word_translations
await VocabularyService.add_translations_bulk(
session=session,
vocabulary_id=new_word.id,
translations=[{
'translation': translation,
'context': example if example else None,
'context_translation': example_translation if example_translation else None,
'is_primary': True
}]
)
await callback.answer(t(lang, 'tasks.word_added', word=word), show_alert=True)
@router.callback_query(F.data == "stop_tasks", TaskStates.doing_tasks)
async def stop_tasks_callback(callback: CallbackQuery, state: FSMContext):
"""Остановить выполнение заданий через кнопку"""
await state.clear()
await callback.message.edit_reply_markup(reply_markup=None)
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await callback.message.answer(t(lang, 'tasks.finished'))
await callback.answer()
@router.message(Command("stop"), TaskStates.doing_tasks)
@router.message(Command("stop"), TaskStates.waiting_for_answer)
async def stop_tasks(message: Message, state: FSMContext):
"""Остановить выполнение заданий командой /stop"""
await state.clear()
# Определяем язык пользователя
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
await message.answer(t((user.language_interface if user else 'ru') or 'ru', 'tasks.stopped'))
@router.message(Command("cancel"), TaskStates.doing_tasks)
@router.message(Command("cancel"), TaskStates.waiting_for_answer)
async def cancel_tasks(message: Message, state: FSMContext):
"""Отмена выполнения заданий командой /cancel"""
await state.clear()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await message.answer(t(lang, 'tasks.cancelled'))
async def finish_tasks(message: Message, state: FSMContext):
"""Завершение всех заданий"""
data = await state.get_data()
tasks = data.get('tasks', [])
correct_count = data.get('correct_count', 0)
total_count = len(tasks)
user_id = data.get('user_id')
accuracy = int((correct_count / total_count) * 100) if total_count > 0 else 0
# Определяем эмодзи на основе результата
if accuracy >= 90:
emoji = "🏆"
comment_key = 'excellent'
elif accuracy >= 70:
emoji = "👍"
comment_key = 'good'
elif accuracy >= 50:
emoji = "📚"
comment_key = 'average'
else:
emoji = "💪"
comment_key = 'poor'
# Язык пользователя (берём user_id из state, т.к. message может быть от бота)
async with async_session_maker() as session:
user = await UserService.get_user_by_id(session, user_id)
lang = (user.language_interface if user else 'ru') or 'ru'
result_text = (
t(lang, 'tasks.finish_title', emoji=emoji) + "\n\n" +
t(lang, 'tasks.correct_of', correct=correct_count, total=total_count) + "\n" +
t(lang, 'tasks.accuracy', accuracy=accuracy) + "\n\n" +
t(lang, f"tasks.comment.{comment_key}") + "\n\n" +
t(lang, 'tasks.use_task') + "\n" +
t(lang, 'tasks.use_stats')
)
await state.clear()
await message.answer(result_text)
@router.message(Command("stats"))
async def cmd_stats(message: Message):
"""Обработчик команды /stats"""
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(t('ru', 'common.start_first'))
return
# Получаем статистику
stats = await TaskService.get_user_stats(session, user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
stats_text = (
t(lang, 'stats.header') + "\n\n" +
t(lang, 'stats.total_words', n=stats['total_words']) + "\n" +
t(lang, 'stats.studied_words', n=stats['reviewed_words']) + "\n" +
t(lang, 'stats.total_tasks', n=stats['total_tasks']) + "\n" +
t(lang, 'stats.correct_tasks', n=stats['correct_tasks']) + "\n" +
t(lang, 'stats.accuracy', n=stats['accuracy']) + "\n\n"
)
if stats['total_words'] == 0:
stats_text += t(lang, 'stats.hint_add_words')
elif stats['total_tasks'] == 0:
stats_text += t(lang, 'stats.hint_first_task')
else:
stats_text += t(lang, 'stats.hint_keep_practice')
await message.answer(stats_text)