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()

View File

@@ -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()

View File

@@ -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,

View File

@@ -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()