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>
This commit is contained in:
2025-12-08 15:16:24 +03:00
parent 3e5c1be464
commit eb666ec9bc
17 changed files with 1095 additions and 129 deletions

View File

@@ -19,10 +19,25 @@ 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 — показываем меню выбора режима"""
@@ -52,8 +67,8 @@ async def cmd_task(message: Message, state: FSMContext):
@router.callback_query(F.data == "task_mode_vocabulary", TaskStates.choosing_mode)
async def start_vocabulary_tasks(callback: CallbackQuery, state: FSMContext):
"""Начать задания по словам из словаря"""
async def choose_vocabulary_task_type(callback: CallbackQuery, state: FSMContext):
"""Показать выбор типа заданий для режима vocabulary"""
await callback.answer()
async with async_session_maker() as session:
@@ -65,37 +80,25 @@ async def start_vocabulary_tasks(callback: CallbackQuery, state: FSMContext):
lang = get_user_lang(user)
# Генерируем задания по словам из словаря
tasks = await TaskService.generate_mixed_tasks(
session, user.id, count=5,
learning_lang=user.learning_language,
translation_lang=get_user_translation_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)
)
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'
)
await state.set_state(TaskStates.doing_tasks)
await callback.message.delete()
await show_current_task(callback.message, state)
@router.callback_query(F.data == "task_mode_new", TaskStates.choosing_mode)
async def start_new_words_tasks(callback: CallbackQuery, state: FSMContext):
"""Начать задания с новыми словами"""
@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)
@@ -104,64 +107,202 @@ async def start_new_words_tasks(callback: CallbackQuery, state: FSMContext):
return
lang = get_user_lang(user)
level = get_user_level_for_language(user)
# Показываем индикатор загрузки
await callback.message.edit_text(t(lang, 'tasks.generating_new'))
# Получаем количество заданий из настроек пользователя
tasks_count = getattr(user, 'tasks_count', 5) or 5
# Получаем слова для исключения:
# 1. Все слова из словаря пользователя
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
)
# 2. Слова из предыдущих заданий new_words, на которые ответили правильно
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=5,
learning_lang=user.learning_language,
translation_lang=translation_lang,
exclude_words=exclude_words if exclude_words else None,
)
# Генерируем новые слова через 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
if not words:
await callback.message.edit_text(t(lang, 'tasks.generate_failed'))
await state.clear()
return
# Преобразуем слова в задания
tasks = []
translate_prompt = t(lang, 'tasks.translate_to', lang_name=t(lang, f'lang.{translation_lang}'))
for word in words:
# Преобразуем слова в задания нужного типа
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}: {word.get('word', '')}",
'word': word.get('word', ''),
'correct_answer': word.get('translation', ''),
'transcription': word.get('transcription', ''),
'example': word.get('example', ''), # Пример на изучаемом языке
'example_translation': word.get('example_translation', '') # Перевод примера
'question': f"{translate_prompt}: <b>{word_text}</b>",
'word': word_text,
'correct_answer': translation,
'transcription': transcription,
'example': example,
'example_translation': example_translation
})
await state.update_data(
tasks=tasks,
current_task_index=0,
correct_count=0,
user_id=user.id,
mode='new_words'
)
await state.set_state(TaskStates.doing_tasks)
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 = "Заполни пропуск:"
await callback.message.delete()
await show_current_task(callback.message, state)
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):