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:
@@ -187,7 +187,7 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext):
|
||||
|
||||
# Проверяем, нет ли уже такого слова
|
||||
existing = await VocabularyService.get_word_by_original(
|
||||
session, user_id, word_data['word']
|
||||
session, user_id, word_data['word'], source_lang=user.learning_language
|
||||
)
|
||||
|
||||
if existing:
|
||||
@@ -195,10 +195,7 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext):
|
||||
return
|
||||
|
||||
# Добавляем слово
|
||||
learn = user.learning_language if user else 'en'
|
||||
translation_lang = get_user_translation_lang(user)
|
||||
ctx = word_data.get('context')
|
||||
examples = ([{learn: ctx, translation_lang: ''}] if ctx else [])
|
||||
|
||||
await VocabularyService.add_word(
|
||||
session=session,
|
||||
@@ -208,10 +205,8 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext):
|
||||
source_lang=user.learning_language if user else None,
|
||||
translation_lang=translation_lang,
|
||||
transcription=word_data.get('transcription'),
|
||||
examples=examples,
|
||||
source=WordSource.CONTEXT,
|
||||
category='imported',
|
||||
difficulty_level=data.get('level')
|
||||
difficulty_level=data.get('level'),
|
||||
source=WordSource.CONTEXT
|
||||
)
|
||||
|
||||
lang = (user.language_interface if user else 'ru') or 'ru'
|
||||
@@ -235,7 +230,7 @@ async def import_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
for word_data in words:
|
||||
# Проверяем, нет ли уже такого слова
|
||||
existing = await VocabularyService.get_word_by_original(
|
||||
session, user_id, word_data['word']
|
||||
session, user_id, word_data['word'], source_lang=user.learning_language
|
||||
)
|
||||
|
||||
if existing:
|
||||
@@ -243,10 +238,7 @@ async def import_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
continue
|
||||
|
||||
# Добавляем слово
|
||||
learn = user.learning_language if user else 'en'
|
||||
translation_lang = get_user_translation_lang(user)
|
||||
ctx = word_data.get('context')
|
||||
examples = ([{learn: ctx, translation_lang: ''}] if ctx else [])
|
||||
|
||||
await VocabularyService.add_word(
|
||||
session=session,
|
||||
@@ -256,10 +248,8 @@ async def import_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
source_lang=user.learning_language if user else None,
|
||||
translation_lang=translation_lang,
|
||||
transcription=word_data.get('transcription'),
|
||||
examples=examples,
|
||||
source=WordSource.CONTEXT,
|
||||
category='imported',
|
||||
difficulty_level=data.get('level')
|
||||
difficulty_level=data.get('level'),
|
||||
source=WordSource.CONTEXT
|
||||
)
|
||||
added_count += 1
|
||||
|
||||
@@ -478,7 +468,7 @@ async def import_file_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
for word_data in words:
|
||||
# Проверяем, нет ли уже такого слова
|
||||
existing = await VocabularyService.get_word_by_original(
|
||||
session, user_id, word_data['word']
|
||||
session, user_id, word_data['word'], source_lang=user.learning_language
|
||||
)
|
||||
|
||||
if existing:
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ async def process_word_addition(message: Message, state: FSMContext, word: str):
|
||||
return
|
||||
|
||||
# Проверяем, есть ли уже такое слово
|
||||
existing_word = await VocabularyService.find_word(session, user.id, word)
|
||||
existing_word = await VocabularyService.find_word(session, user.id, word, source_lang=user.learning_language)
|
||||
if existing_word:
|
||||
lang = get_user_lang(user)
|
||||
await message.answer(t(lang, 'add.exists', word=word, translation=existing_word.word_translation))
|
||||
@@ -107,7 +107,6 @@ async def process_word_addition(message: Message, state: FSMContext, word: str):
|
||||
card_text = (
|
||||
f"📝 <b>{word_data['word']}</b>\n"
|
||||
f"🔊 [{word_data.get('transcription', '')}]\n\n"
|
||||
f"{t(lang, 'add.category_label')}: {word_data.get('category', '')}\n"
|
||||
f"{t(lang, 'add.level_label')}: {word_data.get('difficulty', 'A1')}"
|
||||
f"{translations_text}"
|
||||
f"{t(lang, 'add.confirm_question')}"
|
||||
@@ -153,7 +152,6 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
|
||||
source_lang=source_lang,
|
||||
translation_lang=translation_lang,
|
||||
transcription=word_data.get("transcription"),
|
||||
category=word_data.get("category"),
|
||||
difficulty_level=word_data.get("difficulty"),
|
||||
source=WordSource.MANUAL
|
||||
)
|
||||
|
||||
@@ -158,22 +158,16 @@ async def add_single_word(callback: CallbackQuery, state: FSMContext):
|
||||
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||
# Проверяем, нет ли уже такого слова
|
||||
existing = await VocabularyService.get_word_by_original(
|
||||
session, user_id, word_data['word']
|
||||
session, user_id, word_data['word'], source_lang=user.learning_language
|
||||
)
|
||||
|
||||
if existing:
|
||||
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'
|
||||
lang = get_user_lang(user)
|
||||
await callback.answer(t(lang, 'words.already_exists', word=word_data['word']), show_alert=True)
|
||||
return
|
||||
|
||||
# Добавляем слово
|
||||
# Формируем examples с учётом языков
|
||||
learn = user.learning_language if user else 'en'
|
||||
translation_lang = get_user_translation_lang(user)
|
||||
ex = word_data.get('example')
|
||||
examples = ([{learn: ex, translation_lang: ''}] if ex else [])
|
||||
|
||||
await VocabularyService.add_word(
|
||||
session=session,
|
||||
@@ -183,10 +177,8 @@ async def add_single_word(callback: CallbackQuery, state: FSMContext):
|
||||
source_lang=user.learning_language if user else None,
|
||||
translation_lang=translation_lang,
|
||||
transcription=word_data.get('transcription'),
|
||||
examples=examples,
|
||||
source=WordSource.SUGGESTED,
|
||||
category=data.get('theme', 'general'),
|
||||
difficulty_level=data.get('level')
|
||||
difficulty_level=data.get('level'),
|
||||
source=WordSource.SUGGESTED
|
||||
)
|
||||
|
||||
async with async_session_maker() as session:
|
||||
@@ -203,7 +195,6 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
data = await state.get_data()
|
||||
words = data.get('words', [])
|
||||
user_id = data.get('user_id')
|
||||
theme = data.get('theme', 'general')
|
||||
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
@@ -213,7 +204,7 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
for word_data in words:
|
||||
# Проверяем, нет ли уже такого слова
|
||||
existing = await VocabularyService.get_word_by_original(
|
||||
session, user_id, word_data['word']
|
||||
session, user_id, word_data['word'], source_lang=user.learning_language
|
||||
)
|
||||
|
||||
if existing:
|
||||
@@ -221,10 +212,7 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
continue
|
||||
|
||||
# Добавляем слово
|
||||
learn = user.learning_language if user else 'en'
|
||||
translation_lang = get_user_translation_lang(user)
|
||||
ex = word_data.get('example')
|
||||
examples = ([{learn: ex, translation_lang: ''}] if ex else [])
|
||||
|
||||
await VocabularyService.add_word(
|
||||
session=session,
|
||||
@@ -234,10 +222,8 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
|
||||
source_lang=user.learning_language if user else None,
|
||||
translation_lang=translation_lang,
|
||||
transcription=word_data.get('transcription'),
|
||||
examples=examples,
|
||||
source=WordSource.SUGGESTED,
|
||||
category=theme,
|
||||
difficulty_level=data.get('level')
|
||||
difficulty_level=data.get('level'),
|
||||
source=WordSource.SUGGESTED
|
||||
)
|
||||
added_count += 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user