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

@@ -1,7 +1,7 @@
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from database.models import AIModel, AIProvider
from typing import Optional, List
from database.models import AIModel, AIProvider, User
from typing import Optional, List, Tuple
# Дефолтная модель если в БД ничего нет
@@ -188,3 +188,81 @@ class AIModelService:
session.add(model)
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

View File

@@ -54,12 +54,26 @@ class AIService:
self._cached_model: Optional[str] = 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
async with async_session_maker() as session:
model = await AIModelService.get_active_model(session)
if user_id:
# Получаем модель пользователя (или глобальную если не выбрана)
model = await AIModelService.get_user_model(session, user_id)
else:
# Глобальная активная модель
model = await AIModelService.get_active_model(session)
if model:
self._cached_model = model.model_name
self._cached_provider = model.provider
@@ -67,9 +81,16 @@ class AIService:
return DEFAULT_MODEL, DEFAULT_PROVIDER
async def _make_request(self, messages: list, temperature: float = 0.3) -> dict:
"""Выполнить запрос к активному AI провайдеру"""
model_name, provider = await self._get_active_model()
async def _make_request(self, messages: list, temperature: float = 0.3, user_id: Optional[int] = None) -> dict:
"""
Выполнить запрос к активному AI провайдеру.
Args:
messages: Сообщения для API
temperature: Температура генерации
user_id: ID пользователя в БД для получения его модели
"""
model_name, provider = await self._get_active_model(user_id)
if provider == AIProvider.google:
return await self._make_google_request(messages, temperature, model_name)
@@ -160,7 +181,7 @@ class AIService:
response.raise_for_status()
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: Слово для перевода
source_lang: Язык исходного слова (ISO2)
translation_lang: Язык перевода (ISO2)
user_id: ID пользователя в БД для получения его модели
Returns:
Dict с переводом, транскрипцией и примерами
@@ -196,7 +218,7 @@ class AIService:
{"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
result = json.loads(response_data['choices'][0]['message']['content'])
@@ -220,7 +242,8 @@ class AIService:
word: str,
source_lang: str = "en",
translation_lang: str = "ru",
max_translations: int = 3
max_translations: int = 3,
user_id: Optional[int] = None
) -> Dict:
"""
Перевести слово и получить несколько переводов с контекстами
@@ -230,6 +253,7 @@ class AIService:
source_lang: Язык исходного слова (ISO2)
translation_lang: Язык перевода (ISO2)
max_translations: Максимальное количество переводов
user_id: ID пользователя в БД для получения его модели
Returns:
Dict с переводами, каждый с примером предложения
@@ -275,7 +299,7 @@ class AIService:
{"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
content = response_data['choices'][0]['message']['content']
@@ -311,7 +335,8 @@ class AIService:
self,
words: List[str],
source_lang: str = "en",
translation_lang: str = "ru"
translation_lang: str = "ru",
user_id: Optional[int] = None
) -> List[Dict]:
"""
Перевести список слов пакетно
@@ -320,6 +345,7 @@ class AIService:
words: Список слов для перевода
source_lang: Язык исходных слов (ISO2)
translation_lang: Язык перевода (ISO2)
user_id: ID пользователя в БД для получения его модели
Returns:
List[Dict] с переводами, транскрипциями
@@ -361,7 +387,7 @@ class AIService:
{"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
content = response_data['choices'][0]['message']['content']
@@ -393,7 +419,7 @@ class AIService:
# Возвращаем слова без перевода в случае ошибки
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: Вопрос задания
correct_answer: Правильный ответ
user_answer: Ответ пользователя
user_id: ID пользователя в БД для получения его модели
Returns:
Dict с результатом проверки и обратной связью
@@ -428,7 +455,7 @@ class AIService:
{"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
result = json.loads(response_data['choices'][0]['message']['content'])
@@ -443,7 +470,7 @@ class AIService:
"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: Слово (на языке обучения), для которого нужно создать предложение
learning_lang: Язык обучения (ISO2)
translation_lang: Язык перевода предложения (ISO2)
user_id: ID пользователя в БД для получения его модели
Returns:
Dict с предложением и правильным ответом
@@ -475,7 +503,7 @@ class AIService:
{"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
result = json.loads(response_data['choices'][0]['message']['content'])
@@ -490,7 +518,7 @@ class AIService:
"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: Слово (на языке обучения), которое должно быть в предложении
learning_lang: Язык обучения (ISO2)
translation_lang: Язык перевода (ISO2)
user_id: ID пользователя в БД для получения его модели
Returns:
Dict с предложением и его переводом
@@ -520,7 +549,7 @@ class AIService:
{"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
result = json.loads(response_data['choices'][0]['message']['content'])
@@ -535,6 +564,116 @@ class AIService:
"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(
self,
theme: str,
@@ -542,7 +681,8 @@ class AIService:
count: int = 10,
learning_lang: str = "en",
translation_lang: str = "ru",
exclude_words: List[str] = None
exclude_words: List[str] = None,
user_id: Optional[int] = None
) -> List[Dict]:
"""
Сгенерировать подборку слов по теме
@@ -554,6 +694,7 @@ class AIService:
learning_lang: Язык изучения
translation_lang: Язык перевода
exclude_words: Список слов для исключения (уже известные)
user_id: ID пользователя в БД для получения его модели
Returns:
Список словарей с информацией о словах
@@ -601,7 +742,7 @@ class AIService:
{"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
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)}")
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:
text: Текст на английском языке
text: Текст на языке изучения
level: Уровень пользователя (A1-C2)
max_words: Максимальное количество слов для извлечения
learning_lang: Язык изучения
translation_lang: Язык перевода
user_id: ID пользователя в БД для получения его модели
Returns:
Список словарей с информацией о словах
@@ -670,7 +814,7 @@ class AIService:
{"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
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)}")
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
Args:
scenario: Сценарий диалога (restaurant, shopping, travel, etc.)
level: Уровень пользователя (A1-C2)
learning_lang: Язык изучения
translation_lang: Язык перевода
user_id: ID пользователя в БД для получения его модели
Returns:
Dict с начальной репликой и контекстом
@@ -739,7 +886,7 @@ class AIService:
{"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
result = json.loads(response_data['choices'][0]['message']['content'])
@@ -762,7 +909,8 @@ class AIService:
scenario: str,
level: str = "B1",
learning_lang: str = "en",
translation_lang: str = "ru"
translation_lang: str = "ru",
user_id: Optional[int] = None
) -> Dict:
"""
Продолжить диалог и проверить ответ пользователя
@@ -772,6 +920,9 @@ class AIService:
user_message: Сообщение пользователя
scenario: Сценарий диалога
level: Уровень пользователя
learning_lang: Язык изучения
translation_lang: Язык перевода
user_id: ID пользователя в БД для получения его модели
Returns:
Dict с ответом AI, проверкой и подсказками
@@ -833,7 +984,7 @@ User: {user_message}
# Добавляем инструкцию для форматирования ответа
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
result = json.loads(response_data['choices'][0]['message']['content'])
@@ -854,12 +1005,13 @@ User: {user_message}
"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:
learning_language: Язык изучения (en, es, de, fr, ja)
user_id: ID пользователя в БД для получения его модели
Returns:
Список из 7 вопросов разной сложности
@@ -913,7 +1065,7 @@ User: {user_message}
{"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
result = json.loads(response_data['choices'][0]['message']['content'])

View File

@@ -243,7 +243,7 @@ class TaskService:
translation_lang: str = 'ru'
) -> List[Dict]:
"""
Генерация заданий определённого типа
Генерация заданий определённого типа (оптимизировано - 1 запрос к AI)
Args:
session: Сессия базы данных
@@ -272,9 +272,10 @@ class TaskService:
# Выбираем случайные слова
selected_words = random.sample(words, min(count, len(words)))
tasks = []
# 1. Подготовка: определяем типы и собираем данные для всех слов
word_data_list = []
for word in selected_words:
# Получаем переводы из таблицы WordTranslation
# Получаем переводы
translations_result = await session.execute(
select(WordTranslation)
.where(WordTranslation.vocabulary_id == word.id)
@@ -288,18 +289,57 @@ class TaskService:
else:
chosen_type = task_type
# Определяем правильный перевод
# Определяем перевод
correct_translation = word.word_translation
if translations:
primary = next((tr for tr in translations if tr.is_primary), translations[0] if translations else None)
if primary:
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':
# Задание на перевод слова
direction = random.choice(['learn_to_tr', 'tr_to_learn'])
# Локализация
if translation_lang == 'en':
prompt = "Translate the word:"
elif translation_lang == 'ja':
@@ -328,12 +368,7 @@ class TaskService:
}
elif chosen_type == 'fill_blank':
# Задание на заполнение пропуска
sentence_data = await ai_service.generate_fill_in_sentence(
word.word_original,
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:"
@@ -347,21 +382,16 @@ class TaskService:
'word_id': word.id,
'question': (
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>"
),
'word': word.word_original,
'correct_answer': sentence_data['answer'],
'sentence': sentence_data['sentence']
'correct_answer': sentence_data.get('answer', word.word_original),
'sentence': sentence_data.get('sentence', '___')
}
elif chosen_type == 'sentence_translate':
# Задание на перевод предложения
sentence_data = await ai_service.generate_sentence_for_translation(
word.word_original,
learning_lang=learning_lang,
translation_lang=translation_lang
)
sentence_data = ai_results_map.get(i, {})
if translation_lang == 'en':
sentence_title = "Translate the sentence:"
@@ -376,10 +406,10 @@ class TaskService:
task = {
'type': 'sentence_translate',
'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,
'correct_answer': sentence_data['translation'],
'sentence': sentence_data['sentence']
'correct_answer': sentence_data.get('translation', correct_translation),
'sentence': sentence_data.get('sentence', word.word_original)
}
tasks.append(task)

View File

@@ -178,3 +178,22 @@ class UserService:
if user:
user.tasks_count = count
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()

View File

@@ -17,8 +17,6 @@ class VocabularyService:
source_lang: Optional[str] = None,
translation_lang: Optional[str] = None,
transcription: Optional[str] = None,
examples: Optional[dict] = None,
category: Optional[str] = None,
difficulty_level: Optional[str] = None,
source: WordSource = WordSource.MANUAL,
notes: Optional[str] = None
@@ -32,8 +30,6 @@ class VocabularyService:
word_original: Оригинальное слово
word_translation: Перевод
transcription: Транскрипция
examples: Примеры использования
category: Категория слова
difficulty_level: Уровень сложности
source: Источник добавления
notes: Заметки пользователя
@@ -56,8 +52,6 @@ class VocabularyService:
source_lang=source_lang,
translation_lang=translation_lang,
transcription=transcription,
examples=examples,
category=category,
difficulty_level=difficulty_enum,
source=source,
notes=notes
@@ -138,7 +132,12 @@ class VocabularyService:
return len(words)
@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: Сессия базы данных
user_id: ID пользователя
word: Слово для поиска
source_lang: Язык изучения для фильтрации (если указан)
Returns:
Объект слова или None
"""
result = await session.execute(
query = (
select(Vocabulary)
.where(Vocabulary.user_id == user_id)
.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()
@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: Сессия базы данных
user_id: ID пользователя
word: Слово для поиска (точное совпадение)
source_lang: Язык изучения для фильтрации (если указан)
Returns:
Объект слова или None
"""
result = await session.execute(
query = (
select(Vocabulary)
.where(Vocabulary.user_id == user_id)
.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()
@staticmethod