feat: мульти-провайдер AI, выбор типов заданий, настройка количества

- Добавлена поддержка нескольких AI провайдеров (OpenAI, Google Gemini)
- Добавлена админ-панель (/admin) для переключения AI моделей
- Добавлен AIModelService для управления моделями в БД
- Добавлен выбор типа заданий (микс, перевод слов, подстановка, перевод предложений)
- Добавлена настройка количества заданий (5-15)
- ai_service динамически выбирает провайдера на основе активной модели
- Обработка ограничений моделей (temperature, response_format)
- Очистка markdown обёртки из ответов Gemini

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-08 15:16:24 +03:00
parent 3e5c1be464
commit eb666ec9bc
17 changed files with 1095 additions and 129 deletions

View File

@@ -0,0 +1,190 @@
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from database.models import AIModel, AIProvider
from typing import Optional, List
# Дефолтная модель если в БД ничего нет
DEFAULT_MODEL = "gpt-4o-mini"
DEFAULT_PROVIDER = AIProvider.openai
class AIModelService:
"""Сервис для работы с AI моделями"""
@staticmethod
async def get_active_model(session: AsyncSession) -> Optional[AIModel]:
"""
Получить активную AI модель
Returns:
AIModel или None если нет активной модели
"""
result = await session.execute(
select(AIModel).where(AIModel.is_active == True)
)
return result.scalar_one_or_none()
@staticmethod
async def get_active_model_name(session: AsyncSession) -> str:
"""
Получить название активной модели
Returns:
Название модели (например "gpt-4o-mini") или дефолтное
"""
model = await AIModelService.get_active_model(session)
if model:
return model.model_name
return DEFAULT_MODEL
@staticmethod
async def get_active_provider(session: AsyncSession) -> AIProvider:
"""
Получить провайдера активной модели
Returns:
AIProvider (OPENAI или GOOGLE)
"""
model = await AIModelService.get_active_model(session)
if model:
return model.provider
return DEFAULT_PROVIDER
@staticmethod
async def get_all_models(session: AsyncSession) -> List[AIModel]:
"""
Получить все доступные модели
Returns:
Список всех моделей
"""
result = await session.execute(
select(AIModel).order_by(AIModel.provider, AIModel.model_name)
)
return list(result.scalars().all())
@staticmethod
async def set_active_model(session: AsyncSession, model_id: int) -> bool:
"""
Установить активную модель по ID
Args:
model_id: ID модели для активации
Returns:
True если успешно, False если модель не найдена
"""
# Проверяем существование модели
result = await session.execute(
select(AIModel).where(AIModel.id == model_id)
)
model = result.scalar_one_or_none()
if not model:
return False
# Деактивируем все модели
await session.execute(
update(AIModel).values(is_active=False)
)
# Активируем выбранную
model.is_active = True
await session.commit()
return True
@staticmethod
async def set_active_model_by_name(session: AsyncSession, model_name: str) -> bool:
"""
Установить активную модель по названию
Args:
model_name: Название модели (например "gpt-4o-mini")
Returns:
True если успешно, False если модель не найдена
"""
result = await session.execute(
select(AIModel).where(AIModel.model_name == model_name)
)
model = result.scalar_one_or_none()
if not model:
return False
# Деактивируем все модели
await session.execute(
update(AIModel).values(is_active=False)
)
# Активируем выбранную
model.is_active = True
await session.commit()
return True
@staticmethod
async def create_model(
session: AsyncSession,
provider: AIProvider,
model_name: str,
display_name: str,
is_active: bool = False
) -> AIModel:
"""
Создать новую модель
Args:
provider: Провайдер (OPENAI, GOOGLE)
model_name: Техническое название модели
display_name: Отображаемое название
is_active: Активна ли модель
Returns:
Созданная модель
"""
# Если активируем новую модель, деактивируем остальные
if is_active:
await session.execute(
update(AIModel).values(is_active=False)
)
model = AIModel(
provider=provider,
model_name=model_name,
display_name=display_name,
is_active=is_active
)
session.add(model)
await session.commit()
await session.refresh(model)
return model
@staticmethod
async def ensure_default_models(session: AsyncSession):
"""
Создать дефолтные модели если их нет в БД
"""
result = await session.execute(select(AIModel))
existing = list(result.scalars().all())
if existing:
return # Модели уже есть
# Создаём дефолтные модели
default_models = [
(AIProvider.openai, "gpt-4o-mini", "GPT-4o Mini", True),
(AIProvider.openai, "gpt-5-nano", "GPT-5 Nano", False),
(AIProvider.google, "gemini-2.5-flash-lite", "Gemini 2.5 Flash Lite", False),
]
for provider, name, display, active in default_models:
model = AIModel(
provider=provider,
model_name=name,
display_name=display,
is_active=active
)
session.add(model)
await session.commit()