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

@@ -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'])