feat: персональные AI модели, оптимизация задач, фильтрация словаря

- Добавлена поддержка персональных AI моделей для каждого пользователя
- Оптимизация создания заданий: батч-запрос к AI вместо N запросов
- Фильтрация слов по языку изучения (source_lang) в словаре
- Удалены неиспользуемые колонки examples и category из vocabulary
- Миграции для ai_model_id и удаления колонок

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-08 16:43:08 +03:00
parent 6138af4e63
commit 16a7df0343
13 changed files with 507 additions and 142 deletions

View File

@@ -180,7 +180,10 @@ async def generate_new_words_tasks(callback: CallbackQuery, state: FSMContext, u
return
# Преобразуем слова в задания нужного типа
tasks = await create_tasks_from_words(words, task_type, lang, user.learning_language, translation_lang)
tasks = await create_tasks_from_words(
words, task_type, lang, user.learning_language, translation_lang,
level=level
)
await state.update_data(
tasks=tasks,
@@ -196,26 +199,68 @@ async def generate_new_words_tasks(callback: CallbackQuery, state: FSMContext, u
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:
"""Создать задания из списка слов в зависимости от типа"""
async def create_tasks_from_words(
words: list,
task_type: str,
lang: str,
learning_lang: str,
translation_lang: str,
level: str = None
) -> list:
"""Создать задания из списка слов в зависимости от типа (оптимизировано - 1 запрос к AI)"""
import random
tasks = []
# 1. Определяем типы заданий для всех слов
word_tasks = []
for word in words:
if task_type == 'mix':
chosen_type = random.choice(['word_translate', 'fill_blank', 'sentence_translate'])
else:
chosen_type = task_type
word_tasks.append({
'word_data': word,
'chosen_type': chosen_type
})
# 2. Собираем задания, требующие генерации предложений
ai_tasks = []
ai_task_indices = [] # Индексы в word_tasks для сопоставления результатов
for i, wt in enumerate(word_tasks):
if wt['chosen_type'] in ('fill_blank', 'sentence_translate'):
ai_tasks.append({
'word': wt['word_data'].get('word', ''),
'task_type': wt['chosen_type']
})
ai_task_indices.append(i)
# 3. Один запрос к AI для всех предложений (если нужно)
ai_results = []
if ai_tasks:
ai_results = await ai_service.generate_task_sentences_batch(
ai_tasks,
learning_lang=learning_lang,
translation_lang=translation_lang
)
# Создаём маппинг: индекс в word_tasks -> результат AI
ai_results_map = {}
for idx, result in zip(ai_task_indices, ai_results):
ai_results_map[idx] = result
# 4. Собираем финальные задания
tasks = []
for i, wt in enumerate(word_tasks):
word = wt['word_data']
chosen_type = wt['chosen_type']
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',
@@ -224,16 +269,12 @@ async def create_tasks_from_words(words: list, task_type: str, lang: str, learni
'correct_answer': translation,
'transcription': transcription,
'example': example,
'example_translation': example_translation
'example_translation': example_translation,
'difficulty_level': level
})
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
)
sentence_data = ai_results_map.get(i, {})
if translation_lang == 'en':
fill_title = "Fill in the blank:"
elif translation_lang == 'ja':
@@ -243,21 +284,17 @@ async def create_tasks_from_words(words: list, task_type: str, lang: str, learni
tasks.append({
'type': 'fill_in',
'question': f"{fill_title}\n\n<b>{sentence_data['sentence']}</b>\n\n<i>{sentence_data.get('translation', '')}</i>",
'question': f"{fill_title}\n\n<b>{sentence_data.get('sentence', '___')}</b>\n\n<i>{sentence_data.get('translation', '')}</i>",
'word': word_text,
'correct_answer': sentence_data['answer'],
'correct_answer': sentence_data.get('answer', word_text),
'transcription': transcription,
'example': example,
'example_translation': example_translation
'example_translation': example_translation,
'difficulty_level': level
})
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
)
sentence_data = ai_results_map.get(i, {})
if translation_lang == 'en':
sentence_title = "Translate the sentence:"
word_hint = "Word"
@@ -270,12 +307,13 @@ async def create_tasks_from_words(words: list, task_type: str, lang: str, learni
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}",
'question': f"{sentence_title}\n\n<b>{sentence_data.get('sentence', word_text)}</b>\n\n📝 {word_hint}: <code>{word_text}</code> — {translation}",
'word': word_text,
'correct_answer': sentence_data['translation'],
'correct_answer': sentence_data.get('translation', translation),
'transcription': transcription,
'example': example,
'example_translation': example_translation
'example_translation': example_translation,
'difficulty_level': level
})
return tasks
@@ -468,6 +506,7 @@ async def add_task_word(callback: CallbackQuery, state: FSMContext):
transcription = task.get('transcription', '')
example = task.get('example', '') # Пример использования как контекст
example_translation = task.get('example_translation', '') # Перевод примера
difficulty_level = task.get('difficulty_level') # Уровень сложности
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
@@ -477,9 +516,10 @@ async def add_task_word(callback: CallbackQuery, state: FSMContext):
return
lang = get_user_lang(user)
translation_lang = get_user_translation_lang(user)
# Проверяем, есть ли слово уже в словаре
existing = await VocabularyService.get_word_by_original(session, user.id, word)
existing = await VocabularyService.get_word_by_original(session, user.id, word, source_lang=user.learning_language)
if existing:
await callback.answer(t(lang, 'tasks.word_already_exists', word=word), show_alert=True)
@@ -492,8 +532,9 @@ async def add_task_word(callback: CallbackQuery, state: FSMContext):
word_original=word,
word_translation=translation,
source_lang=user.learning_language,
translation_lang=get_user_translation_lang(user),
translation_lang=translation_lang,
transcription=transcription,
difficulty_level=difficulty_level,
source=WordSource.AI_TASK
)