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:
10
Makefile
10
Makefile
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
.PHONY: help venv install run clean \
|
.PHONY: help venv install run clean \
|
||||||
docker-up docker-down docker-logs docker-rebuild docker-restart \
|
docker-up docker-down docker-logs docker-rebuild docker-restart \
|
||||||
docker-bot-restart docker-bot-rebuild docker-bot-build \
|
docker-bot-restart docker-bot-rebuild docker-bot-build docker-bot-rebuild-full \
|
||||||
migrate migrate-down migrate-current migrate-revision \
|
migrate migrate-down migrate-current migrate-revision \
|
||||||
local-migrate local-migrate-down local-migrate-current \
|
local-migrate local-migrate-down local-migrate-current \
|
||||||
docker-db docker-db-stop
|
docker-db docker-db-stop
|
||||||
@@ -90,7 +90,13 @@ docker-bot-build:
|
|||||||
|
|
||||||
docker-bot-rebuild:
|
docker-bot-rebuild:
|
||||||
docker-compose stop bot
|
docker-compose stop bot
|
||||||
docker-compose rm -f bot
|
docker-compose rm bot
|
||||||
|
docker-compose build --no-cache bot
|
||||||
|
docker-compose up -d bot
|
||||||
|
|
||||||
|
docker-bot-rebuild-full:
|
||||||
|
docker-compose stop bot
|
||||||
|
docker-compose rm -rf bot
|
||||||
docker-compose build --no-cache bot
|
docker-compose build --no-cache bot
|
||||||
docker-compose up -d bot
|
docker-compose up -d bot
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext):
|
|||||||
|
|
||||||
# Проверяем, нет ли уже такого слова
|
# Проверяем, нет ли уже такого слова
|
||||||
existing = await VocabularyService.get_word_by_original(
|
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:
|
if existing:
|
||||||
@@ -195,10 +195,7 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Добавляем слово
|
# Добавляем слово
|
||||||
learn = user.learning_language if user else 'en'
|
|
||||||
translation_lang = get_user_translation_lang(user)
|
translation_lang = get_user_translation_lang(user)
|
||||||
ctx = word_data.get('context')
|
|
||||||
examples = ([{learn: ctx, translation_lang: ''}] if ctx else [])
|
|
||||||
|
|
||||||
await VocabularyService.add_word(
|
await VocabularyService.add_word(
|
||||||
session=session,
|
session=session,
|
||||||
@@ -208,10 +205,8 @@ async def import_single_word(callback: CallbackQuery, state: FSMContext):
|
|||||||
source_lang=user.learning_language if user else None,
|
source_lang=user.learning_language if user else None,
|
||||||
translation_lang=translation_lang,
|
translation_lang=translation_lang,
|
||||||
transcription=word_data.get('transcription'),
|
transcription=word_data.get('transcription'),
|
||||||
examples=examples,
|
difficulty_level=data.get('level'),
|
||||||
source=WordSource.CONTEXT,
|
source=WordSource.CONTEXT
|
||||||
category='imported',
|
|
||||||
difficulty_level=data.get('level')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
lang = (user.language_interface if user else 'ru') or 'ru'
|
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:
|
for word_data in words:
|
||||||
# Проверяем, нет ли уже такого слова
|
# Проверяем, нет ли уже такого слова
|
||||||
existing = await VocabularyService.get_word_by_original(
|
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:
|
if existing:
|
||||||
@@ -243,10 +238,7 @@ async def import_all_words(callback: CallbackQuery, state: FSMContext):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Добавляем слово
|
# Добавляем слово
|
||||||
learn = user.learning_language if user else 'en'
|
|
||||||
translation_lang = get_user_translation_lang(user)
|
translation_lang = get_user_translation_lang(user)
|
||||||
ctx = word_data.get('context')
|
|
||||||
examples = ([{learn: ctx, translation_lang: ''}] if ctx else [])
|
|
||||||
|
|
||||||
await VocabularyService.add_word(
|
await VocabularyService.add_word(
|
||||||
session=session,
|
session=session,
|
||||||
@@ -256,10 +248,8 @@ async def import_all_words(callback: CallbackQuery, state: FSMContext):
|
|||||||
source_lang=user.learning_language if user else None,
|
source_lang=user.learning_language if user else None,
|
||||||
translation_lang=translation_lang,
|
translation_lang=translation_lang,
|
||||||
transcription=word_data.get('transcription'),
|
transcription=word_data.get('transcription'),
|
||||||
examples=examples,
|
difficulty_level=data.get('level'),
|
||||||
source=WordSource.CONTEXT,
|
source=WordSource.CONTEXT
|
||||||
category='imported',
|
|
||||||
difficulty_level=data.get('level')
|
|
||||||
)
|
)
|
||||||
added_count += 1
|
added_count += 1
|
||||||
|
|
||||||
@@ -478,7 +468,7 @@ async def import_file_all_words(callback: CallbackQuery, state: FSMContext):
|
|||||||
for word_data in words:
|
for word_data in words:
|
||||||
# Проверяем, нет ли уже такого слова
|
# Проверяем, нет ли уже такого слова
|
||||||
existing = await VocabularyService.get_word_by_original(
|
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:
|
if existing:
|
||||||
|
|||||||
@@ -180,7 +180,10 @@ async def generate_new_words_tasks(callback: CallbackQuery, state: FSMContext, u
|
|||||||
return
|
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(
|
await state.update_data(
|
||||||
tasks=tasks,
|
tasks=tasks,
|
||||||
@@ -196,26 +199,68 @@ async def generate_new_words_tasks(callback: CallbackQuery, state: FSMContext, u
|
|||||||
await show_current_task(callback.message, state)
|
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
|
import random
|
||||||
tasks = []
|
|
||||||
|
|
||||||
|
# 1. Определяем типы заданий для всех слов
|
||||||
|
word_tasks = []
|
||||||
for word in words:
|
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', '')
|
word_text = word.get('word', '')
|
||||||
translation = word.get('translation', '')
|
translation = word.get('translation', '')
|
||||||
transcription = word.get('transcription', '')
|
transcription = word.get('transcription', '')
|
||||||
example = word.get('example', '')
|
example = word.get('example', '')
|
||||||
example_translation = word.get('example_translation', '')
|
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':
|
if chosen_type == 'word_translate':
|
||||||
# Перевод слова
|
|
||||||
translate_prompt = t(lang, 'tasks.translate_to', lang_name=t(lang, f'lang.{translation_lang}'))
|
translate_prompt = t(lang, 'tasks.translate_to', lang_name=t(lang, f'lang.{translation_lang}'))
|
||||||
tasks.append({
|
tasks.append({
|
||||||
'type': 'translate',
|
'type': 'translate',
|
||||||
@@ -224,16 +269,12 @@ async def create_tasks_from_words(words: list, task_type: str, lang: str, learni
|
|||||||
'correct_answer': translation,
|
'correct_answer': translation,
|
||||||
'transcription': transcription,
|
'transcription': transcription,
|
||||||
'example': example,
|
'example': example,
|
||||||
'example_translation': example_translation
|
'example_translation': example_translation,
|
||||||
|
'difficulty_level': level
|
||||||
})
|
})
|
||||||
|
|
||||||
elif chosen_type == 'fill_blank':
|
elif chosen_type == 'fill_blank':
|
||||||
# Заполнение пропуска - генерируем предложение через AI
|
sentence_data = ai_results_map.get(i, {})
|
||||||
sentence_data = await ai_service.generate_fill_in_sentence(
|
|
||||||
word_text,
|
|
||||||
learning_lang=learning_lang,
|
|
||||||
translation_lang=translation_lang
|
|
||||||
)
|
|
||||||
if translation_lang == 'en':
|
if translation_lang == 'en':
|
||||||
fill_title = "Fill in the blank:"
|
fill_title = "Fill in the blank:"
|
||||||
elif translation_lang == 'ja':
|
elif translation_lang == 'ja':
|
||||||
@@ -243,21 +284,17 @@ async def create_tasks_from_words(words: list, task_type: str, lang: str, learni
|
|||||||
|
|
||||||
tasks.append({
|
tasks.append({
|
||||||
'type': 'fill_in',
|
'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,
|
'word': word_text,
|
||||||
'correct_answer': sentence_data['answer'],
|
'correct_answer': sentence_data.get('answer', word_text),
|
||||||
'transcription': transcription,
|
'transcription': transcription,
|
||||||
'example': example,
|
'example': example,
|
||||||
'example_translation': example_translation
|
'example_translation': example_translation,
|
||||||
|
'difficulty_level': level
|
||||||
})
|
})
|
||||||
|
|
||||||
elif chosen_type == 'sentence_translate':
|
elif chosen_type == 'sentence_translate':
|
||||||
# Перевод предложения - генерируем предложение через AI
|
sentence_data = ai_results_map.get(i, {})
|
||||||
sentence_data = await ai_service.generate_sentence_for_translation(
|
|
||||||
word_text,
|
|
||||||
learning_lang=learning_lang,
|
|
||||||
translation_lang=translation_lang
|
|
||||||
)
|
|
||||||
if translation_lang == 'en':
|
if translation_lang == 'en':
|
||||||
sentence_title = "Translate the sentence:"
|
sentence_title = "Translate the sentence:"
|
||||||
word_hint = "Word"
|
word_hint = "Word"
|
||||||
@@ -270,12 +307,13 @@ async def create_tasks_from_words(words: list, task_type: str, lang: str, learni
|
|||||||
|
|
||||||
tasks.append({
|
tasks.append({
|
||||||
'type': 'sentence_translate',
|
'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,
|
'word': word_text,
|
||||||
'correct_answer': sentence_data['translation'],
|
'correct_answer': sentence_data.get('translation', translation),
|
||||||
'transcription': transcription,
|
'transcription': transcription,
|
||||||
'example': example,
|
'example': example,
|
||||||
'example_translation': example_translation
|
'example_translation': example_translation,
|
||||||
|
'difficulty_level': level
|
||||||
})
|
})
|
||||||
|
|
||||||
return tasks
|
return tasks
|
||||||
@@ -468,6 +506,7 @@ async def add_task_word(callback: CallbackQuery, state: FSMContext):
|
|||||||
transcription = task.get('transcription', '')
|
transcription = task.get('transcription', '')
|
||||||
example = task.get('example', '') # Пример использования как контекст
|
example = task.get('example', '') # Пример использования как контекст
|
||||||
example_translation = task.get('example_translation', '') # Перевод примера
|
example_translation = task.get('example_translation', '') # Перевод примера
|
||||||
|
difficulty_level = task.get('difficulty_level') # Уровень сложности
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
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
|
return
|
||||||
|
|
||||||
lang = get_user_lang(user)
|
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:
|
if existing:
|
||||||
await callback.answer(t(lang, 'tasks.word_already_exists', word=word), show_alert=True)
|
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_original=word,
|
||||||
word_translation=translation,
|
word_translation=translation,
|
||||||
source_lang=user.learning_language,
|
source_lang=user.learning_language,
|
||||||
translation_lang=get_user_translation_lang(user),
|
translation_lang=translation_lang,
|
||||||
transcription=transcription,
|
transcription=transcription,
|
||||||
|
difficulty_level=difficulty_level,
|
||||||
source=WordSource.AI_TASK
|
source=WordSource.AI_TASK
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ async def process_word_addition(message: Message, state: FSMContext, word: str):
|
|||||||
return
|
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:
|
if existing_word:
|
||||||
lang = get_user_lang(user)
|
lang = get_user_lang(user)
|
||||||
await message.answer(t(lang, 'add.exists', word=word, translation=existing_word.word_translation))
|
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 = (
|
card_text = (
|
||||||
f"📝 <b>{word_data['word']}</b>\n"
|
f"📝 <b>{word_data['word']}</b>\n"
|
||||||
f"🔊 [{word_data.get('transcription', '')}]\n\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"{t(lang, 'add.level_label')}: {word_data.get('difficulty', 'A1')}"
|
||||||
f"{translations_text}"
|
f"{translations_text}"
|
||||||
f"{t(lang, 'add.confirm_question')}"
|
f"{t(lang, 'add.confirm_question')}"
|
||||||
@@ -153,7 +152,6 @@ async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
|
|||||||
source_lang=source_lang,
|
source_lang=source_lang,
|
||||||
translation_lang=translation_lang,
|
translation_lang=translation_lang,
|
||||||
transcription=word_data.get("transcription"),
|
transcription=word_data.get("transcription"),
|
||||||
category=word_data.get("category"),
|
|
||||||
difficulty_level=word_data.get("difficulty"),
|
difficulty_level=word_data.get("difficulty"),
|
||||||
source=WordSource.MANUAL
|
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)
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
||||||
# Проверяем, нет ли уже такого слова
|
# Проверяем, нет ли уже такого слова
|
||||||
existing = await VocabularyService.get_word_by_original(
|
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:
|
if existing:
|
||||||
async with async_session_maker() as session:
|
lang = get_user_lang(user)
|
||||||
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.answer(t(lang, 'words.already_exists', word=word_data['word']), show_alert=True)
|
await callback.answer(t(lang, 'words.already_exists', word=word_data['word']), show_alert=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Добавляем слово
|
# Добавляем слово
|
||||||
# Формируем examples с учётом языков
|
|
||||||
learn = user.learning_language if user else 'en'
|
|
||||||
translation_lang = get_user_translation_lang(user)
|
translation_lang = get_user_translation_lang(user)
|
||||||
ex = word_data.get('example')
|
|
||||||
examples = ([{learn: ex, translation_lang: ''}] if ex else [])
|
|
||||||
|
|
||||||
await VocabularyService.add_word(
|
await VocabularyService.add_word(
|
||||||
session=session,
|
session=session,
|
||||||
@@ -183,10 +177,8 @@ async def add_single_word(callback: CallbackQuery, state: FSMContext):
|
|||||||
source_lang=user.learning_language if user else None,
|
source_lang=user.learning_language if user else None,
|
||||||
translation_lang=translation_lang,
|
translation_lang=translation_lang,
|
||||||
transcription=word_data.get('transcription'),
|
transcription=word_data.get('transcription'),
|
||||||
examples=examples,
|
difficulty_level=data.get('level'),
|
||||||
source=WordSource.SUGGESTED,
|
source=WordSource.SUGGESTED
|
||||||
category=data.get('theme', 'general'),
|
|
||||||
difficulty_level=data.get('level')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
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()
|
data = await state.get_data()
|
||||||
words = data.get('words', [])
|
words = data.get('words', [])
|
||||||
user_id = data.get('user_id')
|
user_id = data.get('user_id')
|
||||||
theme = data.get('theme', 'general')
|
|
||||||
|
|
||||||
added_count = 0
|
added_count = 0
|
||||||
skipped_count = 0
|
skipped_count = 0
|
||||||
@@ -213,7 +204,7 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
|
|||||||
for word_data in words:
|
for word_data in words:
|
||||||
# Проверяем, нет ли уже такого слова
|
# Проверяем, нет ли уже такого слова
|
||||||
existing = await VocabularyService.get_word_by_original(
|
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:
|
if existing:
|
||||||
@@ -221,10 +212,7 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Добавляем слово
|
# Добавляем слово
|
||||||
learn = user.learning_language if user else 'en'
|
|
||||||
translation_lang = get_user_translation_lang(user)
|
translation_lang = get_user_translation_lang(user)
|
||||||
ex = word_data.get('example')
|
|
||||||
examples = ([{learn: ex, translation_lang: ''}] if ex else [])
|
|
||||||
|
|
||||||
await VocabularyService.add_word(
|
await VocabularyService.add_word(
|
||||||
session=session,
|
session=session,
|
||||||
@@ -234,10 +222,8 @@ async def add_all_words(callback: CallbackQuery, state: FSMContext):
|
|||||||
source_lang=user.learning_language if user else None,
|
source_lang=user.learning_language if user else None,
|
||||||
translation_lang=translation_lang,
|
translation_lang=translation_lang,
|
||||||
transcription=word_data.get('transcription'),
|
transcription=word_data.get('transcription'),
|
||||||
examples=examples,
|
difficulty_level=data.get('level'),
|
||||||
source=WordSource.SUGGESTED,
|
source=WordSource.SUGGESTED
|
||||||
category=theme,
|
|
||||||
difficulty_level=data.get('level')
|
|
||||||
)
|
)
|
||||||
added_count += 1
|
added_count += 1
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class User(Base):
|
|||||||
last_reminder_sent: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
last_reminder_sent: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||||
streak_days: Mapped[int] = mapped_column(Integer, default=0)
|
streak_days: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
tasks_count: Mapped[int] = mapped_column(Integer, default=5) # Количество заданий (5-15)
|
tasks_count: Mapped[int] = mapped_column(Integer, default=5) # Количество заданий (5-15)
|
||||||
|
ai_model_id: Mapped[Optional[int]] = mapped_column(Integer, default=None) # ID выбранной AI модели (NULL = глобальная)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
last_active: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
last_active: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||||
|
|
||||||
@@ -90,8 +91,6 @@ class Vocabulary(Base):
|
|||||||
source_lang: Mapped[Optional[str]] = mapped_column(String(5)) # ISO2 языка слова (язык изучения)
|
source_lang: Mapped[Optional[str]] = mapped_column(String(5)) # ISO2 языка слова (язык изучения)
|
||||||
translation_lang: Mapped[Optional[str]] = mapped_column(String(5)) # ISO2 языка перевода (обычно язык интерфейса)
|
translation_lang: Mapped[Optional[str]] = mapped_column(String(5)) # ISO2 языка перевода (обычно язык интерфейса)
|
||||||
transcription: Mapped[Optional[str]] = mapped_column(String(255))
|
transcription: Mapped[Optional[str]] = mapped_column(String(255))
|
||||||
examples: Mapped[Optional[dict]] = mapped_column(JSON) # JSON массив примеров
|
|
||||||
category: Mapped[Optional[str]] = mapped_column(String(100))
|
|
||||||
difficulty_level: Mapped[Optional[LanguageLevel]] = mapped_column(SQLEnum(LanguageLevel))
|
difficulty_level: Mapped[Optional[LanguageLevel]] = mapped_column(SQLEnum(LanguageLevel))
|
||||||
source: Mapped[WordSource] = mapped_column(SQLEnum(WordSource), default=WordSource.MANUAL)
|
source: Mapped[WordSource] = mapped_column(SQLEnum(WordSource), default=WordSource.MANUAL)
|
||||||
times_reviewed: Mapped[int] = mapped_column(Integer, default=0)
|
times_reviewed: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
|||||||
28
migrations/versions/20251208_rm_examples_category.py
Normal file
28
migrations/versions/20251208_rm_examples_category.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""Remove examples and category columns from vocabulary
|
||||||
|
|
||||||
|
Revision ID: 20251208_rm_examples_category
|
||||||
|
Revises: 20251208_ai_models
|
||||||
|
Create Date: 2024-12-08
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '20251208_rm_examples_category'
|
||||||
|
down_revision: Union[str, None] = '20251208_ai_models'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.drop_column('vocabulary', 'examples')
|
||||||
|
op.drop_column('vocabulary', 'category')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.add_column('vocabulary', sa.Column('category', sa.String(length=100), nullable=True))
|
||||||
|
op.add_column('vocabulary', sa.Column('examples', sa.JSON(), nullable=True))
|
||||||
26
migrations/versions/20251208_user_ai_model.py
Normal file
26
migrations/versions/20251208_user_ai_model.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""Add ai_model_id to users
|
||||||
|
|
||||||
|
Revision ID: 20251208_user_ai_model
|
||||||
|
Revises: 20251208_rm_examples_category
|
||||||
|
Create Date: 2024-12-08
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '20251208_user_ai_model'
|
||||||
|
down_revision: Union[str, None] = '20251208_rm_examples_category'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('users', sa.Column('ai_model_id', sa.Integer(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('users', 'ai_model_id')
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from sqlalchemy import select, update
|
from sqlalchemy import select, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from database.models import AIModel, AIProvider
|
from database.models import AIModel, AIProvider, User
|
||||||
from typing import Optional, List
|
from typing import Optional, List, Tuple
|
||||||
|
|
||||||
|
|
||||||
# Дефолтная модель если в БД ничего нет
|
# Дефолтная модель если в БД ничего нет
|
||||||
@@ -188,3 +188,81 @@ class AIModelService:
|
|||||||
session.add(model)
|
session.add(model)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_model(session: AsyncSession, user_id: int) -> Optional[AIModel]:
|
||||||
|
"""
|
||||||
|
Получить AI модель пользователя.
|
||||||
|
Если у пользователя не выбрана модель, возвращает глобальную активную.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя в БД
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
AIModel или None
|
||||||
|
"""
|
||||||
|
# Получаем пользователя
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.id == user_id)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if user and user.ai_model_id:
|
||||||
|
# У пользователя выбрана своя модель
|
||||||
|
model_result = await session.execute(
|
||||||
|
select(AIModel).where(AIModel.id == user.ai_model_id)
|
||||||
|
)
|
||||||
|
model = model_result.scalar_one_or_none()
|
||||||
|
if model:
|
||||||
|
return model
|
||||||
|
|
||||||
|
# Fallback на глобальную активную модель
|
||||||
|
return await AIModelService.get_active_model(session)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_user_model_info(session: AsyncSession, user_id: int) -> Tuple[str, AIProvider]:
|
||||||
|
"""
|
||||||
|
Получить название модели и провайдера для пользователя.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя в БД
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[model_name, provider]
|
||||||
|
"""
|
||||||
|
model = await AIModelService.get_user_model(session, user_id)
|
||||||
|
if model:
|
||||||
|
return model.model_name, model.provider
|
||||||
|
return DEFAULT_MODEL, DEFAULT_PROVIDER
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def set_user_model(session: AsyncSession, user_id: int, model_id: Optional[int]) -> bool:
|
||||||
|
"""
|
||||||
|
Установить AI модель для пользователя.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя в БД
|
||||||
|
model_id: ID модели или None для сброса на глобальную
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True если успешно
|
||||||
|
"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.id == user_id)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверяем существование модели если указан ID
|
||||||
|
if model_id is not None:
|
||||||
|
model_result = await session.execute(
|
||||||
|
select(AIModel).where(AIModel.id == model_id)
|
||||||
|
)
|
||||||
|
if not model_result.scalar_one_or_none():
|
||||||
|
return False
|
||||||
|
|
||||||
|
user.ai_model_id = model_id
|
||||||
|
await session.commit()
|
||||||
|
return True
|
||||||
|
|||||||
@@ -54,12 +54,26 @@ class AIService:
|
|||||||
self._cached_model: Optional[str] = None
|
self._cached_model: Optional[str] = None
|
||||||
self._cached_provider: Optional[AIProvider] = None
|
self._cached_provider: Optional[AIProvider] = None
|
||||||
|
|
||||||
async def _get_active_model(self) -> tuple[str, AIProvider]:
|
async def _get_active_model(self, user_id: Optional[int] = None) -> tuple[str, AIProvider]:
|
||||||
"""Получить активную модель и провайдера из БД"""
|
"""
|
||||||
|
Получить активную модель и провайдера из БД.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя в БД (не telegram_id). Если указан, берёт модель пользователя.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[model_name, provider]
|
||||||
|
"""
|
||||||
from services.ai_model_service import AIModelService, DEFAULT_MODEL, DEFAULT_PROVIDER
|
from services.ai_model_service import AIModelService, DEFAULT_MODEL, DEFAULT_PROVIDER
|
||||||
|
|
||||||
async with async_session_maker() as session:
|
async with async_session_maker() as session:
|
||||||
|
if user_id:
|
||||||
|
# Получаем модель пользователя (или глобальную если не выбрана)
|
||||||
|
model = await AIModelService.get_user_model(session, user_id)
|
||||||
|
else:
|
||||||
|
# Глобальная активная модель
|
||||||
model = await AIModelService.get_active_model(session)
|
model = await AIModelService.get_active_model(session)
|
||||||
|
|
||||||
if model:
|
if model:
|
||||||
self._cached_model = model.model_name
|
self._cached_model = model.model_name
|
||||||
self._cached_provider = model.provider
|
self._cached_provider = model.provider
|
||||||
@@ -67,9 +81,16 @@ class AIService:
|
|||||||
|
|
||||||
return DEFAULT_MODEL, DEFAULT_PROVIDER
|
return DEFAULT_MODEL, DEFAULT_PROVIDER
|
||||||
|
|
||||||
async def _make_request(self, messages: list, temperature: float = 0.3) -> dict:
|
async def _make_request(self, messages: list, temperature: float = 0.3, user_id: Optional[int] = None) -> dict:
|
||||||
"""Выполнить запрос к активному AI провайдеру"""
|
"""
|
||||||
model_name, provider = await self._get_active_model()
|
Выполнить запрос к активному AI провайдеру.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: Сообщения для API
|
||||||
|
temperature: Температура генерации
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
"""
|
||||||
|
model_name, provider = await self._get_active_model(user_id)
|
||||||
|
|
||||||
if provider == AIProvider.google:
|
if provider == AIProvider.google:
|
||||||
return await self._make_google_request(messages, temperature, model_name)
|
return await self._make_google_request(messages, temperature, model_name)
|
||||||
@@ -160,7 +181,7 @@ class AIService:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
async def translate_word(self, word: str, source_lang: str = "en", translation_lang: str = "ru") -> Dict:
|
async def translate_word(self, word: str, source_lang: str = "en", translation_lang: str = "ru", user_id: Optional[int] = None) -> Dict:
|
||||||
"""
|
"""
|
||||||
Перевести слово и получить дополнительную информацию
|
Перевести слово и получить дополнительную информацию
|
||||||
|
|
||||||
@@ -168,6 +189,7 @@ class AIService:
|
|||||||
word: Слово для перевода
|
word: Слово для перевода
|
||||||
source_lang: Язык исходного слова (ISO2)
|
source_lang: Язык исходного слова (ISO2)
|
||||||
translation_lang: Язык перевода (ISO2)
|
translation_lang: Язык перевода (ISO2)
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict с переводом, транскрипцией и примерами
|
Dict с переводом, транскрипцией и примерами
|
||||||
@@ -196,7 +218,7 @@ class AIService:
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.3)
|
response_data = await self._make_request(messages, temperature=0.3, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
@@ -220,7 +242,8 @@ class AIService:
|
|||||||
word: str,
|
word: str,
|
||||||
source_lang: str = "en",
|
source_lang: str = "en",
|
||||||
translation_lang: str = "ru",
|
translation_lang: str = "ru",
|
||||||
max_translations: int = 3
|
max_translations: int = 3,
|
||||||
|
user_id: Optional[int] = None
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
"""
|
"""
|
||||||
Перевести слово и получить несколько переводов с контекстами
|
Перевести слово и получить несколько переводов с контекстами
|
||||||
@@ -230,6 +253,7 @@ class AIService:
|
|||||||
source_lang: Язык исходного слова (ISO2)
|
source_lang: Язык исходного слова (ISO2)
|
||||||
translation_lang: Язык перевода (ISO2)
|
translation_lang: Язык перевода (ISO2)
|
||||||
max_translations: Максимальное количество переводов
|
max_translations: Максимальное количество переводов
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict с переводами, каждый с примером предложения
|
Dict с переводами, каждый с примером предложения
|
||||||
@@ -275,7 +299,7 @@ class AIService:
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.3)
|
response_data = await self._make_request(messages, temperature=0.3, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
content = response_data['choices'][0]['message']['content']
|
content = response_data['choices'][0]['message']['content']
|
||||||
@@ -311,7 +335,8 @@ class AIService:
|
|||||||
self,
|
self,
|
||||||
words: List[str],
|
words: List[str],
|
||||||
source_lang: str = "en",
|
source_lang: str = "en",
|
||||||
translation_lang: str = "ru"
|
translation_lang: str = "ru",
|
||||||
|
user_id: Optional[int] = None
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Перевести список слов пакетно
|
Перевести список слов пакетно
|
||||||
@@ -320,6 +345,7 @@ class AIService:
|
|||||||
words: Список слов для перевода
|
words: Список слов для перевода
|
||||||
source_lang: Язык исходных слов (ISO2)
|
source_lang: Язык исходных слов (ISO2)
|
||||||
translation_lang: Язык перевода (ISO2)
|
translation_lang: Язык перевода (ISO2)
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Dict] с переводами, транскрипциями
|
List[Dict] с переводами, транскрипциями
|
||||||
@@ -361,7 +387,7 @@ class AIService:
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.3)
|
response_data = await self._make_request(messages, temperature=0.3, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
content = response_data['choices'][0]['message']['content']
|
content = response_data['choices'][0]['message']['content']
|
||||||
@@ -393,7 +419,7 @@ class AIService:
|
|||||||
# Возвращаем слова без перевода в случае ошибки
|
# Возвращаем слова без перевода в случае ошибки
|
||||||
return [{"word": w, "translation": "", "transcription": ""} for w in words]
|
return [{"word": w, "translation": "", "transcription": ""} for w in words]
|
||||||
|
|
||||||
async def check_answer(self, question: str, correct_answer: str, user_answer: str) -> Dict:
|
async def check_answer(self, question: str, correct_answer: str, user_answer: str, user_id: Optional[int] = None) -> Dict:
|
||||||
"""
|
"""
|
||||||
Проверить ответ пользователя с помощью ИИ
|
Проверить ответ пользователя с помощью ИИ
|
||||||
|
|
||||||
@@ -401,6 +427,7 @@ class AIService:
|
|||||||
question: Вопрос задания
|
question: Вопрос задания
|
||||||
correct_answer: Правильный ответ
|
correct_answer: Правильный ответ
|
||||||
user_answer: Ответ пользователя
|
user_answer: Ответ пользователя
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict с результатом проверки и обратной связью
|
Dict с результатом проверки и обратной связью
|
||||||
@@ -428,7 +455,7 @@ class AIService:
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.3)
|
response_data = await self._make_request(messages, temperature=0.3, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
@@ -443,7 +470,7 @@ class AIService:
|
|||||||
"score": 0
|
"score": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
async def generate_fill_in_sentence(self, word: str, learning_lang: str = "en", translation_lang: str = "ru") -> Dict:
|
async def generate_fill_in_sentence(self, word: str, learning_lang: str = "en", translation_lang: str = "ru", user_id: Optional[int] = None) -> Dict:
|
||||||
"""
|
"""
|
||||||
Сгенерировать предложение с пропуском для заданного слова
|
Сгенерировать предложение с пропуском для заданного слова
|
||||||
|
|
||||||
@@ -451,6 +478,7 @@ class AIService:
|
|||||||
word: Слово (на языке обучения), для которого нужно создать предложение
|
word: Слово (на языке обучения), для которого нужно создать предложение
|
||||||
learning_lang: Язык обучения (ISO2)
|
learning_lang: Язык обучения (ISO2)
|
||||||
translation_lang: Язык перевода предложения (ISO2)
|
translation_lang: Язык перевода предложения (ISO2)
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict с предложением и правильным ответом
|
Dict с предложением и правильным ответом
|
||||||
@@ -475,7 +503,7 @@ class AIService:
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.7)
|
response_data = await self._make_request(messages, temperature=0.7, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
@@ -490,7 +518,7 @@ class AIService:
|
|||||||
"translation": f"Мне нравится {word} каждый день."
|
"translation": f"Мне нравится {word} каждый день."
|
||||||
}
|
}
|
||||||
|
|
||||||
async def generate_sentence_for_translation(self, word: str, learning_lang: str = "en", translation_lang: str = "ru") -> Dict:
|
async def generate_sentence_for_translation(self, word: str, learning_lang: str = "en", translation_lang: str = "ru", user_id: Optional[int] = None) -> Dict:
|
||||||
"""
|
"""
|
||||||
Сгенерировать предложение для перевода, содержащее заданное слово
|
Сгенерировать предложение для перевода, содержащее заданное слово
|
||||||
|
|
||||||
@@ -498,6 +526,7 @@ class AIService:
|
|||||||
word: Слово (на языке обучения), которое должно быть в предложении
|
word: Слово (на языке обучения), которое должно быть в предложении
|
||||||
learning_lang: Язык обучения (ISO2)
|
learning_lang: Язык обучения (ISO2)
|
||||||
translation_lang: Язык перевода (ISO2)
|
translation_lang: Язык перевода (ISO2)
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict с предложением и его переводом
|
Dict с предложением и его переводом
|
||||||
@@ -520,7 +549,7 @@ class AIService:
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.7)
|
response_data = await self._make_request(messages, temperature=0.7, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
@@ -535,6 +564,116 @@ class AIService:
|
|||||||
"translation": f"Я использую {word} каждый день."
|
"translation": f"Я использую {word} каждый день."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def generate_task_sentences_batch(
|
||||||
|
self,
|
||||||
|
tasks_data: List[Dict],
|
||||||
|
learning_lang: str = "en",
|
||||||
|
translation_lang: str = "ru",
|
||||||
|
user_id: Optional[int] = None
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Батч-генерация предложений для заданий за один запрос к AI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tasks_data: Список словарей с информацией о заданиях:
|
||||||
|
[{"word": "run", "task_type": "fill_blank"}, {"word": "eat", "task_type": "sentence_translate"}]
|
||||||
|
learning_lang: Язык обучения
|
||||||
|
translation_lang: Язык перевода
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список результатов в том же порядке
|
||||||
|
"""
|
||||||
|
if not tasks_data:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Формируем описание заданий для промпта
|
||||||
|
tasks_description = []
|
||||||
|
for i, task in enumerate(tasks_data):
|
||||||
|
word = task.get('word', '')
|
||||||
|
task_type = task.get('task_type', '')
|
||||||
|
|
||||||
|
if task_type == 'fill_blank':
|
||||||
|
tasks_description.append(
|
||||||
|
f'{i + 1}. Слово "{word}" - создай предложение с пропуском (замени слово на ___)'
|
||||||
|
)
|
||||||
|
elif task_type == 'sentence_translate':
|
||||||
|
tasks_description.append(
|
||||||
|
f'{i + 1}. Слово "{word}" - создай простое предложение для перевода'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not tasks_description:
|
||||||
|
return []
|
||||||
|
|
||||||
|
prompt = f"""Создай предложения на языке {learning_lang} для следующих заданий:
|
||||||
|
|
||||||
|
{chr(10).join(tasks_description)}
|
||||||
|
|
||||||
|
Верни ответ в формате JSON:
|
||||||
|
{{
|
||||||
|
"results": [
|
||||||
|
{{
|
||||||
|
"sentence": "предложение (с ___ для fill_blank)",
|
||||||
|
"answer": "слово для пропуска (только для fill_blank)",
|
||||||
|
"translation": "перевод на {translation_lang}"
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
|
||||||
|
Важно:
|
||||||
|
- Для fill_blank: замени целевое слово на ___, укажи answer
|
||||||
|
- Для sentence_translate: просто предложение со словом, answer не нужен
|
||||||
|
- Предложения должны быть простыми (5-10 слов)
|
||||||
|
- Контекст должен подсказывать правильное слово
|
||||||
|
- Верни результаты В ТОМ ЖЕ ПОРЯДКЕ что и задания"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"[AI Request] generate_task_sentences_batch: {len(tasks_data)} tasks, lang='{learning_lang}'")
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": "Ты - преподаватель иностранных языков. Создавай простые и понятные упражнения."},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
]
|
||||||
|
|
||||||
|
response_data = await self._make_request(messages, temperature=0.7, user_id=user_id)
|
||||||
|
|
||||||
|
import json
|
||||||
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
|
results = result.get('results', [])
|
||||||
|
|
||||||
|
logger.info(f"[AI Response] generate_task_sentences_batch: got {len(results)} results")
|
||||||
|
|
||||||
|
# Дополняем результаты до нужного количества если AI вернул меньше
|
||||||
|
while len(results) < len(tasks_data):
|
||||||
|
task = tasks_data[len(results)]
|
||||||
|
word = task.get('word', 'word')
|
||||||
|
results.append({
|
||||||
|
"sentence": f"I use {word} every day." if task.get('task_type') != 'fill_blank' else f"I like to ___ every day.",
|
||||||
|
"answer": word,
|
||||||
|
"translation": f"Fallback предложение"
|
||||||
|
})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[AI Error] generate_task_sentences_batch: {type(e).__name__}: {str(e)}")
|
||||||
|
# Fallback - простые предложения для всех заданий
|
||||||
|
results = []
|
||||||
|
for task in tasks_data:
|
||||||
|
word = task.get('word', 'word')
|
||||||
|
if task.get('task_type') == 'fill_blank':
|
||||||
|
results.append({
|
||||||
|
"sentence": f"I like to ___ every day.",
|
||||||
|
"answer": word,
|
||||||
|
"translation": f"Мне нравится {word} каждый день."
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
results.append({
|
||||||
|
"sentence": f"I use {word} every day.",
|
||||||
|
"translation": f"Я использую {word} каждый день."
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
async def generate_thematic_words(
|
async def generate_thematic_words(
|
||||||
self,
|
self,
|
||||||
theme: str,
|
theme: str,
|
||||||
@@ -542,7 +681,8 @@ class AIService:
|
|||||||
count: int = 10,
|
count: int = 10,
|
||||||
learning_lang: str = "en",
|
learning_lang: str = "en",
|
||||||
translation_lang: str = "ru",
|
translation_lang: str = "ru",
|
||||||
exclude_words: List[str] = None
|
exclude_words: List[str] = None,
|
||||||
|
user_id: Optional[int] = None
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Сгенерировать подборку слов по теме
|
Сгенерировать подборку слов по теме
|
||||||
@@ -554,6 +694,7 @@ class AIService:
|
|||||||
learning_lang: Язык изучения
|
learning_lang: Язык изучения
|
||||||
translation_lang: Язык перевода
|
translation_lang: Язык перевода
|
||||||
exclude_words: Список слов для исключения (уже известные)
|
exclude_words: Список слов для исключения (уже известные)
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Список словарей с информацией о словах
|
Список словарей с информацией о словах
|
||||||
@@ -601,7 +742,7 @@ class AIService:
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.7)
|
response_data = await self._make_request(messages, temperature=0.7, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
@@ -625,14 +766,17 @@ class AIService:
|
|||||||
logger.error(f"[AI Error] generate_thematic_words: {type(e).__name__}: {str(e)}")
|
logger.error(f"[AI Error] generate_thematic_words: {type(e).__name__}: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def extract_words_from_text(self, text: str, level: str = "B1", max_words: int = 15, learning_lang: str = "en", translation_lang: str = "ru") -> List[Dict]:
|
async def extract_words_from_text(self, text: str, level: str = "B1", max_words: int = 15, learning_lang: str = "en", translation_lang: str = "ru", user_id: Optional[int] = None) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Извлечь ключевые слова из текста для изучения
|
Извлечь ключевые слова из текста для изучения
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Текст на английском языке
|
text: Текст на языке изучения
|
||||||
level: Уровень пользователя (A1-C2)
|
level: Уровень пользователя (A1-C2)
|
||||||
max_words: Максимальное количество слов для извлечения
|
max_words: Максимальное количество слов для извлечения
|
||||||
|
learning_lang: Язык изучения
|
||||||
|
translation_lang: Язык перевода
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Список словарей с информацией о словах
|
Список словарей с информацией о словах
|
||||||
@@ -670,7 +814,7 @@ class AIService:
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.5)
|
response_data = await self._make_request(messages, temperature=0.5, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
@@ -682,13 +826,16 @@ class AIService:
|
|||||||
logger.error(f"[AI Error] extract_words_from_text: {type(e).__name__}: {str(e)}")
|
logger.error(f"[AI Error] extract_words_from_text: {type(e).__name__}: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def start_conversation(self, scenario: str, level: str = "B1", learning_lang: str = "en", translation_lang: str = "ru") -> Dict:
|
async def start_conversation(self, scenario: str, level: str = "B1", learning_lang: str = "en", translation_lang: str = "ru", user_id: Optional[int] = None) -> Dict:
|
||||||
"""
|
"""
|
||||||
Начать диалоговую практику с AI
|
Начать диалоговую практику с AI
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
scenario: Сценарий диалога (restaurant, shopping, travel, etc.)
|
scenario: Сценарий диалога (restaurant, shopping, travel, etc.)
|
||||||
level: Уровень пользователя (A1-C2)
|
level: Уровень пользователя (A1-C2)
|
||||||
|
learning_lang: Язык изучения
|
||||||
|
translation_lang: Язык перевода
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict с начальной репликой и контекстом
|
Dict с начальной репликой и контекстом
|
||||||
@@ -739,7 +886,7 @@ class AIService:
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.8)
|
response_data = await self._make_request(messages, temperature=0.8, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
@@ -762,7 +909,8 @@ class AIService:
|
|||||||
scenario: str,
|
scenario: str,
|
||||||
level: str = "B1",
|
level: str = "B1",
|
||||||
learning_lang: str = "en",
|
learning_lang: str = "en",
|
||||||
translation_lang: str = "ru"
|
translation_lang: str = "ru",
|
||||||
|
user_id: Optional[int] = None
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
"""
|
"""
|
||||||
Продолжить диалог и проверить ответ пользователя
|
Продолжить диалог и проверить ответ пользователя
|
||||||
@@ -772,6 +920,9 @@ class AIService:
|
|||||||
user_message: Сообщение пользователя
|
user_message: Сообщение пользователя
|
||||||
scenario: Сценарий диалога
|
scenario: Сценарий диалога
|
||||||
level: Уровень пользователя
|
level: Уровень пользователя
|
||||||
|
learning_lang: Язык изучения
|
||||||
|
translation_lang: Язык перевода
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict с ответом AI, проверкой и подсказками
|
Dict с ответом AI, проверкой и подсказками
|
||||||
@@ -833,7 +984,7 @@ User: {user_message}
|
|||||||
# Добавляем инструкцию для форматирования ответа
|
# Добавляем инструкцию для форматирования ответа
|
||||||
messages.append({"role": "user", "content": prompt})
|
messages.append({"role": "user", "content": prompt})
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.8)
|
response_data = await self._make_request(messages, temperature=0.8, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
@@ -854,12 +1005,13 @@ User: {user_message}
|
|||||||
"suggestions": ["Sure!", "Well...", "Actually..."]
|
"suggestions": ["Sure!", "Well...", "Actually..."]
|
||||||
}
|
}
|
||||||
|
|
||||||
async def generate_level_test(self, learning_language: str = "en") -> List[Dict]:
|
async def generate_level_test(self, learning_language: str = "en", user_id: Optional[int] = None) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Сгенерировать тест для определения уровня языка
|
Сгенерировать тест для определения уровня языка
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
learning_language: Язык изучения (en, es, de, fr, ja)
|
learning_language: Язык изучения (en, es, de, fr, ja)
|
||||||
|
user_id: ID пользователя в БД для получения его модели
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Список из 7 вопросов разной сложности
|
Список из 7 вопросов разной сложности
|
||||||
@@ -913,7 +1065,7 @@ User: {user_message}
|
|||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
|
|
||||||
response_data = await self._make_request(messages, temperature=0.7)
|
response_data = await self._make_request(messages, temperature=0.7, user_id=user_id)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ class TaskService:
|
|||||||
translation_lang: str = 'ru'
|
translation_lang: str = 'ru'
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Генерация заданий определённого типа
|
Генерация заданий определённого типа (оптимизировано - 1 запрос к AI)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
session: Сессия базы данных
|
session: Сессия базы данных
|
||||||
@@ -272,9 +272,10 @@ class TaskService:
|
|||||||
# Выбираем случайные слова
|
# Выбираем случайные слова
|
||||||
selected_words = random.sample(words, min(count, len(words)))
|
selected_words = random.sample(words, min(count, len(words)))
|
||||||
|
|
||||||
tasks = []
|
# 1. Подготовка: определяем типы и собираем данные для всех слов
|
||||||
|
word_data_list = []
|
||||||
for word in selected_words:
|
for word in selected_words:
|
||||||
# Получаем переводы из таблицы WordTranslation
|
# Получаем переводы
|
||||||
translations_result = await session.execute(
|
translations_result = await session.execute(
|
||||||
select(WordTranslation)
|
select(WordTranslation)
|
||||||
.where(WordTranslation.vocabulary_id == word.id)
|
.where(WordTranslation.vocabulary_id == word.id)
|
||||||
@@ -288,18 +289,57 @@ class TaskService:
|
|||||||
else:
|
else:
|
||||||
chosen_type = task_type
|
chosen_type = task_type
|
||||||
|
|
||||||
# Определяем правильный перевод
|
# Определяем перевод
|
||||||
correct_translation = word.word_translation
|
correct_translation = word.word_translation
|
||||||
if translations:
|
if translations:
|
||||||
primary = next((tr for tr in translations if tr.is_primary), translations[0] if translations else None)
|
primary = next((tr for tr in translations if tr.is_primary), translations[0] if translations else None)
|
||||||
if primary:
|
if primary:
|
||||||
correct_translation = primary.translation
|
correct_translation = primary.translation
|
||||||
|
|
||||||
|
word_data_list.append({
|
||||||
|
'word': word,
|
||||||
|
'translations': translations,
|
||||||
|
'correct_translation': correct_translation,
|
||||||
|
'chosen_type': chosen_type
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. Собираем задания, требующие AI
|
||||||
|
ai_tasks = []
|
||||||
|
ai_task_indices = []
|
||||||
|
|
||||||
|
for i, wd in enumerate(word_data_list):
|
||||||
|
if wd['chosen_type'] in ('fill_blank', 'sentence_translate'):
|
||||||
|
ai_tasks.append({
|
||||||
|
'word': wd['word'].word_original,
|
||||||
|
'task_type': wd['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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Маппинг результатов
|
||||||
|
ai_results_map = {}
|
||||||
|
for idx, result in zip(ai_task_indices, ai_results):
|
||||||
|
ai_results_map[idx] = result
|
||||||
|
|
||||||
|
# 4. Собираем финальные задания
|
||||||
|
tasks = []
|
||||||
|
for i, wd in enumerate(word_data_list):
|
||||||
|
word = wd['word']
|
||||||
|
translations = wd['translations']
|
||||||
|
correct_translation = wd['correct_translation']
|
||||||
|
chosen_type = wd['chosen_type']
|
||||||
|
|
||||||
if chosen_type == 'word_translate':
|
if chosen_type == 'word_translate':
|
||||||
# Задание на перевод слова
|
|
||||||
direction = random.choice(['learn_to_tr', 'tr_to_learn'])
|
direction = random.choice(['learn_to_tr', 'tr_to_learn'])
|
||||||
|
|
||||||
# Локализация
|
|
||||||
if translation_lang == 'en':
|
if translation_lang == 'en':
|
||||||
prompt = "Translate the word:"
|
prompt = "Translate the word:"
|
||||||
elif translation_lang == 'ja':
|
elif translation_lang == 'ja':
|
||||||
@@ -328,12 +368,7 @@ class TaskService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
elif chosen_type == 'fill_blank':
|
elif chosen_type == 'fill_blank':
|
||||||
# Задание на заполнение пропуска
|
sentence_data = ai_results_map.get(i, {})
|
||||||
sentence_data = await ai_service.generate_fill_in_sentence(
|
|
||||||
word.word_original,
|
|
||||||
learning_lang=learning_lang,
|
|
||||||
translation_lang=translation_lang
|
|
||||||
)
|
|
||||||
|
|
||||||
if translation_lang == 'en':
|
if translation_lang == 'en':
|
||||||
fill_title = "Fill in the blank:"
|
fill_title = "Fill in the blank:"
|
||||||
@@ -347,21 +382,16 @@ class TaskService:
|
|||||||
'word_id': word.id,
|
'word_id': word.id,
|
||||||
'question': (
|
'question': (
|
||||||
f"{fill_title}\n\n"
|
f"{fill_title}\n\n"
|
||||||
f"<b>{sentence_data['sentence']}</b>\n\n"
|
f"<b>{sentence_data.get('sentence', '___')}</b>\n\n"
|
||||||
f"<i>{sentence_data.get('translation', '')}</i>"
|
f"<i>{sentence_data.get('translation', '')}</i>"
|
||||||
),
|
),
|
||||||
'word': word.word_original,
|
'word': word.word_original,
|
||||||
'correct_answer': sentence_data['answer'],
|
'correct_answer': sentence_data.get('answer', word.word_original),
|
||||||
'sentence': sentence_data['sentence']
|
'sentence': sentence_data.get('sentence', '___')
|
||||||
}
|
}
|
||||||
|
|
||||||
elif chosen_type == 'sentence_translate':
|
elif chosen_type == 'sentence_translate':
|
||||||
# Задание на перевод предложения
|
sentence_data = ai_results_map.get(i, {})
|
||||||
sentence_data = await ai_service.generate_sentence_for_translation(
|
|
||||||
word.word_original,
|
|
||||||
learning_lang=learning_lang,
|
|
||||||
translation_lang=translation_lang
|
|
||||||
)
|
|
||||||
|
|
||||||
if translation_lang == 'en':
|
if translation_lang == 'en':
|
||||||
sentence_title = "Translate the sentence:"
|
sentence_title = "Translate the sentence:"
|
||||||
@@ -376,10 +406,10 @@ class TaskService:
|
|||||||
task = {
|
task = {
|
||||||
'type': 'sentence_translate',
|
'type': 'sentence_translate',
|
||||||
'word_id': word.id,
|
'word_id': word.id,
|
||||||
'question': f"{sentence_title}\n\n<b>{sentence_data['sentence']}</b>\n\n📝 {word_hint}: <code>{word.word_original}</code> — {correct_translation}",
|
'question': f"{sentence_title}\n\n<b>{sentence_data.get('sentence', word.word_original)}</b>\n\n📝 {word_hint}: <code>{word.word_original}</code> — {correct_translation}",
|
||||||
'word': word.word_original,
|
'word': word.word_original,
|
||||||
'correct_answer': sentence_data['translation'],
|
'correct_answer': sentence_data.get('translation', correct_translation),
|
||||||
'sentence': sentence_data['sentence']
|
'sentence': sentence_data.get('sentence', word.word_original)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
|
|||||||
@@ -178,3 +178,22 @@ class UserService:
|
|||||||
if user:
|
if user:
|
||||||
user.tasks_count = count
|
user.tasks_count = count
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def update_user_ai_model(session: AsyncSession, user_id: int, model_id: Optional[int]):
|
||||||
|
"""
|
||||||
|
Обновить AI модель пользователя
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Сессия базы данных
|
||||||
|
user_id: ID пользователя
|
||||||
|
model_id: ID модели или None для глобальной
|
||||||
|
"""
|
||||||
|
result = await session.execute(
|
||||||
|
select(User).where(User.id == user_id)
|
||||||
|
)
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
user.ai_model_id = model_id
|
||||||
|
await session.commit()
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ class VocabularyService:
|
|||||||
source_lang: Optional[str] = None,
|
source_lang: Optional[str] = None,
|
||||||
translation_lang: Optional[str] = None,
|
translation_lang: Optional[str] = None,
|
||||||
transcription: Optional[str] = None,
|
transcription: Optional[str] = None,
|
||||||
examples: Optional[dict] = None,
|
|
||||||
category: Optional[str] = None,
|
|
||||||
difficulty_level: Optional[str] = None,
|
difficulty_level: Optional[str] = None,
|
||||||
source: WordSource = WordSource.MANUAL,
|
source: WordSource = WordSource.MANUAL,
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
@@ -32,8 +30,6 @@ class VocabularyService:
|
|||||||
word_original: Оригинальное слово
|
word_original: Оригинальное слово
|
||||||
word_translation: Перевод
|
word_translation: Перевод
|
||||||
transcription: Транскрипция
|
transcription: Транскрипция
|
||||||
examples: Примеры использования
|
|
||||||
category: Категория слова
|
|
||||||
difficulty_level: Уровень сложности
|
difficulty_level: Уровень сложности
|
||||||
source: Источник добавления
|
source: Источник добавления
|
||||||
notes: Заметки пользователя
|
notes: Заметки пользователя
|
||||||
@@ -56,8 +52,6 @@ class VocabularyService:
|
|||||||
source_lang=source_lang,
|
source_lang=source_lang,
|
||||||
translation_lang=translation_lang,
|
translation_lang=translation_lang,
|
||||||
transcription=transcription,
|
transcription=transcription,
|
||||||
examples=examples,
|
|
||||||
category=category,
|
|
||||||
difficulty_level=difficulty_enum,
|
difficulty_level=difficulty_enum,
|
||||||
source=source,
|
source=source,
|
||||||
notes=notes
|
notes=notes
|
||||||
@@ -138,7 +132,12 @@ class VocabularyService:
|
|||||||
return len(words)
|
return len(words)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def find_word(session: AsyncSession, user_id: int, word: str) -> Optional[Vocabulary]:
|
async def find_word(
|
||||||
|
session: AsyncSession,
|
||||||
|
user_id: int,
|
||||||
|
word: str,
|
||||||
|
source_lang: Optional[str] = None
|
||||||
|
) -> Optional[Vocabulary]:
|
||||||
"""
|
"""
|
||||||
Найти слово в словаре пользователя
|
Найти слово в словаре пользователя
|
||||||
|
|
||||||
@@ -146,19 +145,28 @@ class VocabularyService:
|
|||||||
session: Сессия базы данных
|
session: Сессия базы данных
|
||||||
user_id: ID пользователя
|
user_id: ID пользователя
|
||||||
word: Слово для поиска
|
word: Слово для поиска
|
||||||
|
source_lang: Язык изучения для фильтрации (если указан)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Объект слова или None
|
Объект слова или None
|
||||||
"""
|
"""
|
||||||
result = await session.execute(
|
query = (
|
||||||
select(Vocabulary)
|
select(Vocabulary)
|
||||||
.where(Vocabulary.user_id == user_id)
|
.where(Vocabulary.user_id == user_id)
|
||||||
.where(Vocabulary.word_original.ilike(f"%{word}%"))
|
.where(Vocabulary.word_original.ilike(f"%{word}%"))
|
||||||
)
|
)
|
||||||
|
if source_lang:
|
||||||
|
query = query.where(Vocabulary.source_lang == source_lang.lower())
|
||||||
|
result = await session.execute(query)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_word_by_original(session: AsyncSession, user_id: int, word: str) -> Optional[Vocabulary]:
|
async def get_word_by_original(
|
||||||
|
session: AsyncSession,
|
||||||
|
user_id: int,
|
||||||
|
word: str,
|
||||||
|
source_lang: Optional[str] = None
|
||||||
|
) -> Optional[Vocabulary]:
|
||||||
"""
|
"""
|
||||||
Получить слово по точному совпадению
|
Получить слово по точному совпадению
|
||||||
|
|
||||||
@@ -166,15 +174,19 @@ class VocabularyService:
|
|||||||
session: Сессия базы данных
|
session: Сессия базы данных
|
||||||
user_id: ID пользователя
|
user_id: ID пользователя
|
||||||
word: Слово для поиска (точное совпадение)
|
word: Слово для поиска (точное совпадение)
|
||||||
|
source_lang: Язык изучения для фильтрации (если указан)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Объект слова или None
|
Объект слова или None
|
||||||
"""
|
"""
|
||||||
result = await session.execute(
|
query = (
|
||||||
select(Vocabulary)
|
select(Vocabulary)
|
||||||
.where(Vocabulary.user_id == user_id)
|
.where(Vocabulary.user_id == user_id)
|
||||||
.where(Vocabulary.word_original == word.lower())
|
.where(Vocabulary.word_original == word.lower())
|
||||||
)
|
)
|
||||||
|
if source_lang:
|
||||||
|
query = query.where(Vocabulary.source_lang == source_lang.lower())
|
||||||
|
result = await session.execute(query)
|
||||||
return result.scalar_one_or_none()
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
Reference in New Issue
Block a user