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:
190
services/ai_model_service.py
Normal file
190
services/ai_model_service.py
Normal 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()
|
||||
@@ -2,56 +2,160 @@ import logging
|
||||
import httpx
|
||||
from openai import AsyncOpenAI
|
||||
from config.settings import settings
|
||||
from typing import Dict, List
|
||||
from database.db import async_session_maker
|
||||
from database.models import AIProvider
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AIService:
|
||||
"""Сервис для работы с OpenAI API через Cloudflare Gateway"""
|
||||
"""Сервис для работы с AI API (OpenAI и Google)"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = settings.openai_api_key
|
||||
self.openai_api_key = settings.openai_api_key
|
||||
self.google_api_key = settings.google_api_key
|
||||
|
||||
# Проверяем, настроен ли Cloudflare AI Gateway
|
||||
if settings.cloudflare_account_id:
|
||||
# Используем Cloudflare AI Gateway с прямыми HTTP запросами
|
||||
self.base_url = (
|
||||
self.openai_base_url = (
|
||||
f"https://gateway.ai.cloudflare.com/v1/"
|
||||
f"{settings.cloudflare_account_id}/"
|
||||
f"{settings.cloudflare_gateway_id}/"
|
||||
f"openai"
|
||||
)
|
||||
self.use_cloudflare = True
|
||||
logger.info(f"AI Service initialized with Cloudflare Gateway: {self.base_url}")
|
||||
logger.info(f"AI Service initialized with Cloudflare Gateway: {self.openai_base_url}")
|
||||
else:
|
||||
# Прямое подключение к OpenAI
|
||||
self.base_url = "https://api.openai.com/v1"
|
||||
self.openai_base_url = "https://api.openai.com/v1"
|
||||
self.use_cloudflare = False
|
||||
logger.info("AI Service initialized with direct OpenAI connection")
|
||||
|
||||
# Google Gemini API URL (через Cloudflare Gateway или напрямую)
|
||||
if settings.cloudflare_account_id:
|
||||
self.google_base_url = (
|
||||
f"https://gateway.ai.cloudflare.com/v1/"
|
||||
f"{settings.cloudflare_account_id}/"
|
||||
f"{settings.cloudflare_gateway_id}/"
|
||||
f"google-ai-studio/v1"
|
||||
)
|
||||
else:
|
||||
self.google_base_url = "https://generativelanguage.googleapis.com/v1beta"
|
||||
|
||||
# HTTP клиент для всех запросов
|
||||
self.http_client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(60.0, connect=10.0),
|
||||
limits=httpx.Limits(max_keepalive_connections=5, max_connections=10)
|
||||
)
|
||||
|
||||
async def _make_openai_request(self, messages: list, temperature: float = 0.3, model: str = "gpt-4o-mini") -> dict:
|
||||
"""Выполнить запрос к OpenAI API (через Cloudflare или напрямую)"""
|
||||
url = f"{self.base_url}/chat/completions"
|
||||
# Кеш активной модели (обновляется при запросах)
|
||||
self._cached_model: Optional[str] = None
|
||||
self._cached_provider: Optional[AIProvider] = None
|
||||
|
||||
async def _get_active_model(self) -> tuple[str, AIProvider]:
|
||||
"""Получить активную модель и провайдера из БД"""
|
||||
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 model:
|
||||
self._cached_model = model.model_name
|
||||
self._cached_provider = model.provider
|
||||
return model.model_name, model.provider
|
||||
|
||||
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()
|
||||
|
||||
if provider == AIProvider.google:
|
||||
return await self._make_google_request(messages, temperature, model_name)
|
||||
else:
|
||||
return await self._make_openai_request(messages, temperature, model_name)
|
||||
|
||||
async def _make_google_request(self, messages: list, temperature: float = 0.3, model: str = "gemini-2.5-flash-lite") -> dict:
|
||||
"""Выполнить запрос к Google Gemini API (через Cloudflare Gateway или напрямую)"""
|
||||
url = f"{self.google_base_url}/models/{model}:generateContent"
|
||||
|
||||
# Конвертируем формат сообщений OpenAI в формат Google
|
||||
# System message добавляем как первое user сообщение
|
||||
contents = []
|
||||
|
||||
for msg in messages:
|
||||
role = msg["role"]
|
||||
content = msg["content"]
|
||||
|
||||
if role == "system":
|
||||
# Добавляем system как user сообщение в начало
|
||||
contents.insert(0, {"role": "user", "parts": [{"text": f"[System instruction]: {content}"}]})
|
||||
elif role == "user":
|
||||
contents.append({"role": "user", "parts": [{"text": content}]})
|
||||
elif role == "assistant":
|
||||
contents.append({"role": "model", "parts": [{"text": content}]})
|
||||
|
||||
payload = {
|
||||
"contents": contents,
|
||||
"generationConfig": {
|
||||
"temperature": temperature
|
||||
}
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"x-goog-api-key": self.google_api_key
|
||||
}
|
||||
|
||||
response = await self.http_client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
# Конвертируем ответ Google в формат OpenAI для совместимости
|
||||
text = data["candidates"][0]["content"]["parts"][0]["text"]
|
||||
|
||||
# Убираем markdown обёртку если есть (```json ... ```)
|
||||
if text.startswith('```'):
|
||||
lines = text.split('\n')
|
||||
# Убираем первую строку (```json) и последнюю (```)
|
||||
if lines[-1].strip() == '```':
|
||||
lines = lines[1:-1]
|
||||
else:
|
||||
lines = lines[1:]
|
||||
text = '\n'.join(lines)
|
||||
|
||||
return {
|
||||
"choices": [{
|
||||
"message": {
|
||||
"content": text
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
async def _make_openai_request(self, messages: list, temperature: float = 0.3, model: str = "gpt-4o-mini") -> dict:
|
||||
"""Выполнить запрос к OpenAI API (через Cloudflare или напрямую)"""
|
||||
url = f"{self.openai_base_url}/chat/completions"
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.openai_api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"response_format": {"type": "json_object"}
|
||||
"messages": messages
|
||||
}
|
||||
|
||||
# Модели с ограничениями (не поддерживают temperature и json mode)
|
||||
limited_models = {"gpt-5-nano", "o1", "o1-mini", "o1-preview", "o3-mini"}
|
||||
|
||||
if model not in limited_models:
|
||||
payload["temperature"] = temperature
|
||||
|
||||
# JSON mode
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
|
||||
response = await self.http_client.post(url, headers=headers, json=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
@@ -85,22 +189,22 @@ class AIService:
|
||||
Важно: верни только JSON, без дополнительного текста."""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] translate_word: word='{word}', source='{source_lang}', to='{translation_lang}'")
|
||||
logger.info(f"[AI Request] translate_word: word='{word}', source='{source_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - помощник для изучения языков. Отвечай только в формате JSON."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.3)
|
||||
response_data = await self._make_request(messages, temperature=0.3)
|
||||
|
||||
import json
|
||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||
logger.info(f"[GPT Response] translate_word: success, translation='{result.get('translation', 'N/A')}'")
|
||||
logger.info(f"[AI Response] translate_word: success, translation='{result.get('translation', 'N/A')}'")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] translate_word: {type(e).__name__}: {str(e)}")
|
||||
logger.error(f"[AI Error] translate_word: {type(e).__name__}: {str(e)}")
|
||||
# Fallback в случае ошибки
|
||||
return {
|
||||
"word": word,
|
||||
@@ -164,14 +268,14 @@ class AIService:
|
||||
- Верни только JSON, без дополнительного текста"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] translate_word_with_contexts: word='{word}', source='{source_lang}', to='{translation_lang}'")
|
||||
logger.info(f"[AI Request] translate_word_with_contexts: word='{word}', source='{source_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - помощник для изучения языков. Отвечай только в формате JSON."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.3)
|
||||
response_data = await self._make_request(messages, temperature=0.3)
|
||||
|
||||
import json
|
||||
content = response_data['choices'][0]['message']['content']
|
||||
@@ -184,11 +288,11 @@ class AIService:
|
||||
|
||||
result = json.loads(content)
|
||||
translations_count = len(result.get('translations', []))
|
||||
logger.info(f"[GPT Response] translate_word_with_contexts: success, {translations_count} translations")
|
||||
logger.info(f"[AI Response] translate_word_with_contexts: success, {translations_count} translations")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] translate_word_with_contexts: {type(e).__name__}: {str(e)}")
|
||||
logger.error(f"[AI Error] translate_word_with_contexts: {type(e).__name__}: {str(e)}")
|
||||
# Fallback в случае ошибки
|
||||
return {
|
||||
"word": word,
|
||||
@@ -250,14 +354,14 @@ class AIService:
|
||||
- Для каждого слова укажи точный перевод и транскрипцию"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] translate_words_batch: {len(words)} words, {source_lang} -> {translation_lang}")
|
||||
logger.info(f"[AI Request] translate_words_batch: {len(words)} words, {source_lang} -> {translation_lang}")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - помощник для изучения языков. Отвечай только в формате JSON."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.3)
|
||||
response_data = await self._make_request(messages, temperature=0.3)
|
||||
|
||||
import json
|
||||
content = response_data['choices'][0]['message']['content']
|
||||
@@ -278,14 +382,14 @@ class AIService:
|
||||
break
|
||||
|
||||
if not isinstance(result, list):
|
||||
logger.warning(f"[GPT Warning] translate_words_batch: unexpected format, got {type(result)}")
|
||||
logger.warning(f"[AI Warning] translate_words_batch: unexpected format, got {type(result)}")
|
||||
return [{"word": w, "translation": "", "transcription": ""} for w in words]
|
||||
|
||||
logger.info(f"[GPT Response] translate_words_batch: success, got {len(result)} translations")
|
||||
logger.info(f"[AI Response] translate_words_batch: success, got {len(result)} translations")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] translate_words_batch: {type(e).__name__}: {str(e)}")
|
||||
logger.error(f"[AI Error] translate_words_batch: {type(e).__name__}: {str(e)}")
|
||||
# Возвращаем слова без перевода в случае ошибки
|
||||
return [{"word": w, "translation": "", "transcription": ""} for w in words]
|
||||
|
||||
@@ -317,22 +421,22 @@ class AIService:
|
||||
Учитывай возможные вариации ответа. Если смысл передан правильно, даже с небольшими грамматическими неточностями, засчитывай ответ."""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] check_answer: user_answer='{user_answer[:30]}...'")
|
||||
logger.info(f"[AI Request] check_answer: user_answer='{user_answer[:30]}...'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - преподаватель английского языка. Проверяй ответы справедливо, учитывая контекст."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.3)
|
||||
response_data = await self._make_request(messages, temperature=0.3)
|
||||
|
||||
import json
|
||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||
logger.info(f"[GPT Response] check_answer: is_correct={result.get('is_correct', False)}, score={result.get('score', 0)}")
|
||||
logger.info(f"[AI Response] check_answer: is_correct={result.get('is_correct', False)}, score={result.get('score', 0)}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] check_answer: {type(e).__name__}: {str(e)}")
|
||||
logger.error(f"[AI Error] check_answer: {type(e).__name__}: {str(e)}")
|
||||
return {
|
||||
"is_correct": False,
|
||||
"feedback": "Ошибка проверки ответа",
|
||||
@@ -364,28 +468,73 @@ class AIService:
|
||||
Предложение должно быть простым и естественным. Контекст должен четко подсказывать правильное слово."""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] generate_fill_in_sentence: word='{word}', lang='{learning_lang}', to='{translation_lang}'")
|
||||
logger.info(f"[AI Request] generate_fill_in_sentence: word='{word}', lang='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - преподаватель иностранных языков. Создавай простые и понятные упражнения."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.7)
|
||||
response_data = await self._make_request(messages, temperature=0.7)
|
||||
|
||||
import json
|
||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||
logger.info(f"[GPT Response] generate_fill_in_sentence: success")
|
||||
logger.info(f"[AI Response] generate_fill_in_sentence: success")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] generate_fill_in_sentence: {type(e).__name__}: {str(e)}")
|
||||
logger.error(f"[AI Error] generate_fill_in_sentence: {type(e).__name__}: {str(e)}")
|
||||
return {
|
||||
"sentence": f"I like to ___ every day.",
|
||||
"answer": word,
|
||||
"translation": f"Мне нравится {word} каждый день."
|
||||
}
|
||||
|
||||
async def generate_sentence_for_translation(self, word: str, learning_lang: str = "en", translation_lang: str = "ru") -> Dict:
|
||||
"""
|
||||
Сгенерировать предложение для перевода, содержащее заданное слово
|
||||
|
||||
Args:
|
||||
word: Слово (на языке обучения), которое должно быть в предложении
|
||||
learning_lang: Язык обучения (ISO2)
|
||||
translation_lang: Язык перевода (ISO2)
|
||||
|
||||
Returns:
|
||||
Dict с предложением и его переводом
|
||||
"""
|
||||
prompt = f"""Создай простое предложение на языке {learning_lang}, используя слово "{word}".
|
||||
|
||||
Верни ответ в формате JSON:
|
||||
{{
|
||||
"sentence": "предложение на {learning_lang} со словом {word}",
|
||||
"translation": "перевод предложения на {translation_lang}"
|
||||
}}
|
||||
|
||||
Предложение должно быть простым (5-10 слов), естественным и подходящим для изучения языка."""
|
||||
|
||||
try:
|
||||
logger.info(f"[AI Request] generate_sentence_for_translation: word='{word}', lang='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - преподаватель иностранных языков. Создавай простые и понятные примеры для практики перевода."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_request(messages, temperature=0.7)
|
||||
|
||||
import json
|
||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||
logger.info(f"[AI Response] generate_sentence_for_translation: success")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[AI Error] generate_sentence_for_translation: {type(e).__name__}: {str(e)}")
|
||||
# Fallback - простое предложение
|
||||
return {
|
||||
"sentence": f"I use {word} every day.",
|
||||
"translation": f"Я использую {word} каждый день."
|
||||
}
|
||||
|
||||
async def generate_thematic_words(
|
||||
self,
|
||||
theme: str,
|
||||
@@ -445,14 +594,14 @@ class AIService:
|
||||
- Разнообразными (существительные, глаголы, прилагательные)"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] generate_thematic_words: theme='{theme}', level='{level}', count={count}, learn='{learning_lang}', to='{translation_lang}'")
|
||||
logger.info(f"[AI Request] generate_thematic_words: theme='{theme}', level='{level}', count={count}, learn='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - преподаватель иностранных языков. Подбирай полезные и актуальные слова."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.7)
|
||||
response_data = await self._make_request(messages, temperature=0.7)
|
||||
|
||||
import json
|
||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||
@@ -466,14 +615,14 @@ class AIService:
|
||||
]
|
||||
filtered_count = len(words) - len(filtered_words)
|
||||
if filtered_count > 0:
|
||||
logger.info(f"[GPT Response] generate_thematic_words: filtered out {filtered_count} excluded words")
|
||||
logger.info(f"[AI Response] generate_thematic_words: filtered out {filtered_count} excluded words")
|
||||
words = filtered_words
|
||||
|
||||
logger.info(f"[GPT Response] generate_thematic_words: success, generated {len(words)} words")
|
||||
logger.info(f"[AI Response] generate_thematic_words: success, generated {len(words)} words")
|
||||
return words
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] generate_thematic_words: {type(e).__name__}: {str(e)}")
|
||||
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]:
|
||||
@@ -514,23 +663,23 @@ class AIService:
|
||||
|
||||
try:
|
||||
text_preview = text[:100] + "..." if len(text) > 100 else text
|
||||
logger.info(f"[GPT Request] extract_words_from_text: text_length={len(text)}, level='{level}', max_words={max_words}, learn='{learning_lang}', to='{translation_lang}'")
|
||||
logger.info(f"[AI Request] extract_words_from_text: text_length={len(text)}, level='{level}', max_words={max_words}, learn='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - преподаватель иностранных языков. Помогаешь извлекать полезные слова для изучения из текстов."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.5)
|
||||
response_data = await self._make_request(messages, temperature=0.5)
|
||||
|
||||
import json
|
||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||
words_count = len(result.get('words', []))
|
||||
logger.info(f"[GPT Response] extract_words_from_text: success, extracted {words_count} words")
|
||||
logger.info(f"[AI Response] extract_words_from_text: success, extracted {words_count} words")
|
||||
return result.get('words', [])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT 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 []
|
||||
|
||||
async def start_conversation(self, scenario: str, level: str = "B1", learning_lang: str = "en", translation_lang: str = "ru") -> Dict:
|
||||
@@ -583,22 +732,22 @@ class AIService:
|
||||
- Подсказки должны помочь пользователю ответить"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] start_conversation: scenario='{scenario}', level='{level}', learn='{learning_lang}', to='{translation_lang}'")
|
||||
logger.info(f"[AI Request] start_conversation: scenario='{scenario}', level='{level}', learn='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "Ты - дружелюбный собеседник для практики иностранных языков. Веди естественный диалог."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.8)
|
||||
response_data = await self._make_request(messages, temperature=0.8)
|
||||
|
||||
import json
|
||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||
logger.info(f"[GPT Response] start_conversation: success, scenario='{scenario}'")
|
||||
logger.info(f"[AI Response] start_conversation: success, scenario='{scenario}'")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] start_conversation: {type(e).__name__}: {str(e)}")
|
||||
logger.error(f"[AI Error] start_conversation: {type(e).__name__}: {str(e)}")
|
||||
return {
|
||||
"message": "Hello! How are you today?",
|
||||
"translation": "Привет! Как дела сегодня?",
|
||||
@@ -667,7 +816,7 @@ User: {user_message}
|
||||
- Используй лексику уровня {level}"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] continue_conversation: scenario='{scenario}', level='{level}', history_length={len(conversation_history)}, learn='{learning_lang}', to='{translation_lang}'")
|
||||
logger.info(f"[AI Request] continue_conversation: scenario='{scenario}', level='{level}', history_length={len(conversation_history)}, learn='{learning_lang}', to='{translation_lang}'")
|
||||
|
||||
# Формируем сообщения для API
|
||||
messages = [
|
||||
@@ -684,16 +833,16 @@ User: {user_message}
|
||||
# Добавляем инструкцию для форматирования ответа
|
||||
messages.append({"role": "user", "content": prompt})
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.8)
|
||||
response_data = await self._make_request(messages, temperature=0.8)
|
||||
|
||||
import json
|
||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||
has_errors = result.get('feedback', {}).get('has_errors', False)
|
||||
logger.info(f"[GPT Response] continue_conversation: success, has_errors={has_errors}")
|
||||
logger.info(f"[AI Response] continue_conversation: success, has_errors={has_errors}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] continue_conversation: {type(e).__name__}: {str(e)}")
|
||||
logger.error(f"[AI Error] continue_conversation: {type(e).__name__}: {str(e)}")
|
||||
return {
|
||||
"response": "I see. Tell me more about that.",
|
||||
"translation": "Понятно. Расскажи мне больше об этом.",
|
||||
@@ -756,7 +905,7 @@ User: {user_message}
|
||||
- Вопросы на грамматику, лексику и понимание"""
|
||||
|
||||
try:
|
||||
logger.info(f"[GPT Request] generate_level_test: generating 7 questions for {learning_language}")
|
||||
logger.info(f"[AI Request] generate_level_test: generating 7 questions for {learning_language}")
|
||||
|
||||
system_msg = f"Ты - эксперт по тестированию уровня {language_name} языка. Создавай объективные тесты."
|
||||
messages = [
|
||||
@@ -764,16 +913,16 @@ User: {user_message}
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
response_data = await self._make_openai_request(messages, temperature=0.7)
|
||||
response_data = await self._make_request(messages, temperature=0.7)
|
||||
|
||||
import json
|
||||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||||
questions_count = len(result.get('questions', []))
|
||||
logger.info(f"[GPT Response] generate_level_test: success, generated {questions_count} questions")
|
||||
logger.info(f"[AI Response] generate_level_test: success, generated {questions_count} questions")
|
||||
return result.get('questions', [])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[GPT Error] generate_level_test: {type(e).__name__}: {str(e)}, using fallback questions")
|
||||
logger.error(f"[AI Error] generate_level_test: {type(e).__name__}: {str(e)}, using fallback questions")
|
||||
# Fallback с базовыми вопросами
|
||||
if learning_language == "ja":
|
||||
return self._get_jlpt_fallback_questions()
|
||||
|
||||
@@ -15,7 +15,8 @@ class TaskService:
|
||||
async def generate_translation_tasks(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
count: int = 5
|
||||
count: int = 5,
|
||||
learning_lang: str = 'en'
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Генерация заданий на перевод слов
|
||||
@@ -28,10 +29,11 @@ class TaskService:
|
||||
Returns:
|
||||
Список заданий
|
||||
"""
|
||||
# Получаем слова пользователя
|
||||
# Получаем слова пользователя на изучаемом языке
|
||||
result = await session.execute(
|
||||
select(Vocabulary)
|
||||
.where(Vocabulary.user_id == user_id)
|
||||
.where(Vocabulary.source_lang == learning_lang)
|
||||
.order_by(Vocabulary.last_reviewed.asc().nullsfirst())
|
||||
.limit(count * 2) # Берем больше, чтобы было из чего выбрать
|
||||
)
|
||||
@@ -90,10 +92,11 @@ class TaskService:
|
||||
Returns:
|
||||
Список заданий разных типов
|
||||
"""
|
||||
# Получаем слова пользователя
|
||||
# Получаем слова пользователя на изучаемом языке
|
||||
result = await session.execute(
|
||||
select(Vocabulary)
|
||||
.where(Vocabulary.user_id == user_id)
|
||||
.where(Vocabulary.source_lang == learning_lang)
|
||||
.order_by(Vocabulary.last_reviewed.asc().nullsfirst())
|
||||
.limit(count * 2)
|
||||
)
|
||||
@@ -230,6 +233,159 @@ class TaskService:
|
||||
|
||||
return tasks
|
||||
|
||||
@staticmethod
|
||||
async def generate_tasks_by_type(
|
||||
session: AsyncSession,
|
||||
user_id: int,
|
||||
count: int = 5,
|
||||
task_type: str = 'mix',
|
||||
learning_lang: str = 'en',
|
||||
translation_lang: str = 'ru'
|
||||
) -> List[Dict]:
|
||||
"""
|
||||
Генерация заданий определённого типа
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
count: Количество заданий
|
||||
task_type: Тип заданий (mix, word_translate, fill_blank, sentence_translate)
|
||||
learning_lang: Язык обучения
|
||||
translation_lang: Язык перевода
|
||||
|
||||
Returns:
|
||||
Список заданий
|
||||
"""
|
||||
# Получаем слова пользователя на изучаемом языке
|
||||
result = await session.execute(
|
||||
select(Vocabulary)
|
||||
.where(Vocabulary.user_id == user_id)
|
||||
.where(Vocabulary.source_lang == learning_lang)
|
||||
.order_by(Vocabulary.last_reviewed.asc().nullsfirst())
|
||||
.limit(count * 2)
|
||||
)
|
||||
words = list(result.scalars().all())
|
||||
|
||||
if not words:
|
||||
return []
|
||||
|
||||
# Выбираем случайные слова
|
||||
selected_words = random.sample(words, min(count, len(words)))
|
||||
|
||||
tasks = []
|
||||
for word in selected_words:
|
||||
# Получаем переводы из таблицы WordTranslation
|
||||
translations_result = await session.execute(
|
||||
select(WordTranslation)
|
||||
.where(WordTranslation.vocabulary_id == word.id)
|
||||
.order_by(WordTranslation.is_primary.desc())
|
||||
)
|
||||
translations = list(translations_result.scalars().all())
|
||||
|
||||
# Определяем тип задания
|
||||
if task_type == 'mix':
|
||||
chosen_type = random.choice(['word_translate', 'fill_blank', 'sentence_translate'])
|
||||
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
|
||||
|
||||
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':
|
||||
prompt = "単語を訳してください:"
|
||||
else:
|
||||
prompt = "Переведи слово:"
|
||||
|
||||
if direction == 'learn_to_tr':
|
||||
task = {
|
||||
'type': f'translate_to_{translation_lang}',
|
||||
'word_id': word.id,
|
||||
'question': f"{prompt} <b>{word.word_original}</b>",
|
||||
'word': word.word_original,
|
||||
'correct_answer': correct_translation,
|
||||
'transcription': word.transcription,
|
||||
'all_translations': [tr.translation for tr in translations] if translations else [correct_translation]
|
||||
}
|
||||
else:
|
||||
task = {
|
||||
'type': f'translate_to_{learning_lang}',
|
||||
'word_id': word.id,
|
||||
'question': f"{prompt} <b>{correct_translation}</b>",
|
||||
'word': correct_translation,
|
||||
'correct_answer': word.word_original,
|
||||
'transcription': word.transcription
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
if translation_lang == 'en':
|
||||
fill_title = "Fill in the blank:"
|
||||
elif translation_lang == 'ja':
|
||||
fill_title = "空欄を埋めてください:"
|
||||
else:
|
||||
fill_title = "Заполни пропуск:"
|
||||
|
||||
task = {
|
||||
'type': 'fill_in',
|
||||
'word_id': word.id,
|
||||
'question': (
|
||||
f"{fill_title}\n\n"
|
||||
f"<b>{sentence_data['sentence']}</b>\n\n"
|
||||
f"<i>{sentence_data.get('translation', '')}</i>"
|
||||
),
|
||||
'word': word.word_original,
|
||||
'correct_answer': sentence_data['answer'],
|
||||
'sentence': sentence_data['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
|
||||
)
|
||||
|
||||
if translation_lang == 'en':
|
||||
sentence_title = "Translate the sentence:"
|
||||
word_hint = "Word"
|
||||
elif translation_lang == 'ja':
|
||||
sentence_title = "文を翻訳してください:"
|
||||
word_hint = "単語"
|
||||
else:
|
||||
sentence_title = "Переведи предложение:"
|
||||
word_hint = "Слово"
|
||||
|
||||
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}",
|
||||
'word': word.word_original,
|
||||
'correct_answer': sentence_data['translation'],
|
||||
'sentence': sentence_data['sentence']
|
||||
}
|
||||
|
||||
tasks.append(task)
|
||||
|
||||
return tasks
|
||||
|
||||
@staticmethod
|
||||
async def save_task_result(
|
||||
session: AsyncSession,
|
||||
|
||||
@@ -156,3 +156,25 @@ class UserService:
|
||||
if user:
|
||||
user.translation_language = language
|
||||
await session.commit()
|
||||
|
||||
@staticmethod
|
||||
async def update_user_tasks_count(session: AsyncSession, user_id: int, count: int):
|
||||
"""
|
||||
Обновить количество заданий пользователя
|
||||
|
||||
Args:
|
||||
session: Сессия базы данных
|
||||
user_id: ID пользователя
|
||||
count: Количество заданий (5-15)
|
||||
"""
|
||||
# Валидация диапазона
|
||||
count = max(5, min(15, count))
|
||||
|
||||
result = await session.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user:
|
||||
user.tasks_count = count
|
||||
await session.commit()
|
||||
|
||||
Reference in New Issue
Block a user