- Добавлена поддержка персональных 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>
1194 lines
57 KiB
Python
1194 lines
57 KiB
Python
import logging
|
||
import httpx
|
||
from openai import AsyncOpenAI
|
||
from config.settings import settings
|
||
from database.db import async_session_maker
|
||
from database.models import AIProvider
|
||
from typing import Dict, List, Optional
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class AIService:
|
||
"""Сервис для работы с AI API (OpenAI и Google)"""
|
||
|
||
def __init__(self):
|
||
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.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.openai_base_url}")
|
||
else:
|
||
# Прямое подключение к OpenAI
|
||
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)
|
||
)
|
||
|
||
# Кеш активной модели (обновляется при запросах)
|
||
self._cached_model: Optional[str] = None
|
||
self._cached_provider: Optional[AIProvider] = None
|
||
|
||
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:
|
||
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
|
||
return model.model_name, model.provider
|
||
|
||
return DEFAULT_MODEL, DEFAULT_PROVIDER
|
||
|
||
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)
|
||
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 = {
|
||
"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 и 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()
|
||
|
||
async def translate_word(self, word: str, source_lang: str = "en", translation_lang: str = "ru", user_id: Optional[int] = None) -> Dict:
|
||
"""
|
||
Перевести слово и получить дополнительную информацию
|
||
|
||
Args:
|
||
word: Слово для перевода
|
||
source_lang: Язык исходного слова (ISO2)
|
||
translation_lang: Язык перевода (ISO2)
|
||
user_id: ID пользователя в БД для получения его модели
|
||
|
||
Returns:
|
||
Dict с переводом, транскрипцией и примерами
|
||
"""
|
||
prompt = f"""Переведи слово/фразу "{word}" с языка {source_lang} на {translation_lang}.
|
||
|
||
Верни ответ строго в формате JSON:
|
||
{{
|
||
"word": "исходное слово на {source_lang}",
|
||
"translation": "перевод на {translation_lang}",
|
||
"transcription": "транскрипция в IPA (если применимо)",
|
||
"examples": [
|
||
{{"{source_lang}": "пример на языке обучения", "{translation_lang}": "перевод примера"}}
|
||
],
|
||
"category": "категория слова (работа, еда, путешествия и т.д.)",
|
||
"difficulty": "уровень сложности (A1/A2/B1/B2/C1/C2)"
|
||
}}
|
||
|
||
Важно: верни только JSON, без дополнительного текста."""
|
||
|
||
try:
|
||
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_request(messages, temperature=0.3, user_id=user_id)
|
||
|
||
import json
|
||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||
logger.info(f"[AI Response] translate_word: success, translation='{result.get('translation', 'N/A')}'")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"[AI Error] translate_word: {type(e).__name__}: {str(e)}")
|
||
# Fallback в случае ошибки
|
||
return {
|
||
"word": word,
|
||
"translation": "Ошибка перевода",
|
||
"transcription": "",
|
||
"examples": [],
|
||
"category": "unknown",
|
||
"difficulty": "A1"
|
||
}
|
||
|
||
async def translate_word_with_contexts(
|
||
self,
|
||
word: str,
|
||
source_lang: str = "en",
|
||
translation_lang: str = "ru",
|
||
max_translations: int = 3,
|
||
user_id: Optional[int] = None
|
||
) -> Dict:
|
||
"""
|
||
Перевести слово и получить несколько переводов с контекстами
|
||
|
||
Args:
|
||
word: Слово для перевода
|
||
source_lang: Язык исходного слова (ISO2)
|
||
translation_lang: Язык перевода (ISO2)
|
||
max_translations: Максимальное количество переводов
|
||
user_id: ID пользователя в БД для получения его модели
|
||
|
||
Returns:
|
||
Dict с переводами, каждый с примером предложения
|
||
"""
|
||
prompt = f"""Переведи слово/фразу "{word}" с языка {source_lang} на {translation_lang}.
|
||
|
||
Если у слова есть несколько значений в разных контекстах, дай до {max_translations} разных переводов.
|
||
Для каждого перевода дай пример предложения, показывающий это значение.
|
||
|
||
Верни ответ строго в формате JSON:
|
||
{{
|
||
"word": "исходное слово на {source_lang}",
|
||
"transcription": "транскрипция в IPA (если применимо)",
|
||
"category": "основная категория слова",
|
||
"difficulty": "уровень сложности (A1/A2/B1/B2/C1/C2)",
|
||
"translations": [
|
||
{{
|
||
"translation": "перевод 1 на {translation_lang}",
|
||
"context": "пример предложения на {source_lang}, показывающий это значение",
|
||
"context_translation": "перевод примера на {translation_lang}",
|
||
"is_primary": true
|
||
}},
|
||
{{
|
||
"translation": "перевод 2 на {translation_lang} (если есть другое значение)",
|
||
"context": "пример предложения на {source_lang}",
|
||
"context_translation": "перевод примера на {translation_lang}",
|
||
"is_primary": false
|
||
}}
|
||
]
|
||
}}
|
||
|
||
Важно:
|
||
- Первый перевод должен быть самым распространённым (is_primary: true)
|
||
- Давай разные переводы только если слово реально имеет разные значения
|
||
- Примеры должны чётко показывать конкретное значение слова
|
||
- Верни только JSON, без дополнительного текста"""
|
||
|
||
try:
|
||
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_request(messages, temperature=0.3, user_id=user_id)
|
||
|
||
import json
|
||
content = response_data['choices'][0]['message']['content']
|
||
# Убираем markdown обёртку если есть
|
||
if content.startswith('```'):
|
||
content = content.split('\n', 1)[1] if '\n' in content else content[3:]
|
||
if content.endswith('```'):
|
||
content = content[:-3]
|
||
content = content.strip()
|
||
|
||
result = json.loads(content)
|
||
translations_count = len(result.get('translations', []))
|
||
logger.info(f"[AI Response] translate_word_with_contexts: success, {translations_count} translations")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"[AI Error] translate_word_with_contexts: {type(e).__name__}: {str(e)}")
|
||
# Fallback в случае ошибки
|
||
return {
|
||
"word": word,
|
||
"transcription": "",
|
||
"category": "unknown",
|
||
"difficulty": "A1",
|
||
"translations": [{
|
||
"translation": "Ошибка перевода",
|
||
"context": "",
|
||
"context_translation": "",
|
||
"is_primary": True
|
||
}]
|
||
}
|
||
|
||
async def translate_words_batch(
|
||
self,
|
||
words: List[str],
|
||
source_lang: str = "en",
|
||
translation_lang: str = "ru",
|
||
user_id: Optional[int] = None
|
||
) -> List[Dict]:
|
||
"""
|
||
Перевести список слов пакетно
|
||
|
||
Args:
|
||
words: Список слов для перевода
|
||
source_lang: Язык исходных слов (ISO2)
|
||
translation_lang: Язык перевода (ISO2)
|
||
user_id: ID пользователя в БД для получения его модели
|
||
|
||
Returns:
|
||
List[Dict] с переводами, транскрипциями
|
||
"""
|
||
if not words:
|
||
return []
|
||
|
||
words_list = "\n".join(f"- {w}" for w in words[:50]) # Максимум 50 слов за раз
|
||
|
||
# Добавляем инструкцию для фуриганы если японский
|
||
furigana_instruction = ""
|
||
if source_lang == "ja":
|
||
furigana_instruction = '\n "reading": "чтение хираганой (только для кандзи)",'
|
||
|
||
prompt = f"""Переведи следующие слова/фразы с языка {source_lang} на {translation_lang}:
|
||
|
||
{words_list}
|
||
|
||
Верни ответ строго в формате JSON массива:
|
||
[
|
||
{{
|
||
"word": "исходное слово",
|
||
"translation": "перевод",
|
||
"transcription": "транскрипция (IPA или ромадзи для японского)",{furigana_instruction}
|
||
}},
|
||
...
|
||
]
|
||
|
||
Важно:
|
||
- Верни только JSON массив, без дополнительного текста
|
||
- Сохрани порядок слов как в исходном списке
|
||
- Для каждого слова укажи точный перевод и транскрипцию"""
|
||
|
||
try:
|
||
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_request(messages, temperature=0.3, user_id=user_id)
|
||
|
||
import json
|
||
content = response_data['choices'][0]['message']['content']
|
||
# Убираем markdown обёртку если есть
|
||
if content.startswith('```'):
|
||
content = content.split('\n', 1)[1] if '\n' in content else content[3:]
|
||
if content.endswith('```'):
|
||
content = content[:-3]
|
||
content = content.strip()
|
||
|
||
result = json.loads(content)
|
||
|
||
# Если вернулся dict с ключом типа "words" или "translations" — извлекаем список
|
||
if isinstance(result, dict):
|
||
for key in ['words', 'translations', 'result', 'data']:
|
||
if key in result and isinstance(result[key], list):
|
||
result = result[key]
|
||
break
|
||
|
||
if not isinstance(result, list):
|
||
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"[AI Response] translate_words_batch: success, got {len(result)} translations")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"[AI Error] translate_words_batch: {type(e).__name__}: {str(e)}")
|
||
# Возвращаем слова без перевода в случае ошибки
|
||
return [{"word": w, "translation": "", "transcription": ""} for w in words]
|
||
|
||
async def check_answer(self, question: str, correct_answer: str, user_answer: str, user_id: Optional[int] = None) -> Dict:
|
||
"""
|
||
Проверить ответ пользователя с помощью ИИ
|
||
|
||
Args:
|
||
question: Вопрос задания
|
||
correct_answer: Правильный ответ
|
||
user_answer: Ответ пользователя
|
||
user_id: ID пользователя в БД для получения его модели
|
||
|
||
Returns:
|
||
Dict с результатом проверки и обратной связью
|
||
"""
|
||
prompt = f"""Проверь ответ пользователя на задание по английскому языку.
|
||
|
||
Задание: {question}
|
||
Правильный ответ: {correct_answer}
|
||
Ответ пользователя: {user_answer}
|
||
|
||
Верни ответ в формате JSON:
|
||
{{
|
||
"is_correct": true/false,
|
||
"feedback": "краткое объяснение (если ответ неверный, объясни ошибку и дай правильный вариант)",
|
||
"score": 0-100
|
||
}}
|
||
|
||
Учитывай возможные вариации ответа. Если смысл передан правильно, даже с небольшими грамматическими неточностями, засчитывай ответ."""
|
||
|
||
try:
|
||
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_request(messages, temperature=0.3, user_id=user_id)
|
||
|
||
import json
|
||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||
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"[AI Error] check_answer: {type(e).__name__}: {str(e)}")
|
||
return {
|
||
"is_correct": False,
|
||
"feedback": "Ошибка проверки ответа",
|
||
"score": 0
|
||
}
|
||
|
||
async def generate_fill_in_sentence(self, word: str, learning_lang: str = "en", translation_lang: str = "ru", user_id: Optional[int] = None) -> Dict:
|
||
"""
|
||
Сгенерировать предложение с пропуском для заданного слова
|
||
|
||
Args:
|
||
word: Слово (на языке обучения), для которого нужно создать предложение
|
||
learning_lang: Язык обучения (ISO2)
|
||
translation_lang: Язык перевода предложения (ISO2)
|
||
user_id: ID пользователя в БД для получения его модели
|
||
|
||
Returns:
|
||
Dict с предложением и правильным ответом
|
||
"""
|
||
prompt = f"""Создай предложение на языке {learning_lang}, используя слово "{word}".
|
||
Замени это слово на пропуск "___".
|
||
|
||
Верни ответ в формате JSON:
|
||
{{
|
||
"sentence": "предложение с пропуском ___",
|
||
"answer": "{word}",
|
||
"translation": "перевод предложения на {translation_lang}"
|
||
}}
|
||
|
||
Предложение должно быть простым и естественным. Контекст должен четко подсказывать правильное слово."""
|
||
|
||
try:
|
||
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_request(messages, temperature=0.7, user_id=user_id)
|
||
|
||
import json
|
||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||
logger.info(f"[AI Response] generate_fill_in_sentence: success")
|
||
return result
|
||
|
||
except Exception as 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", user_id: Optional[int] = None) -> Dict:
|
||
"""
|
||
Сгенерировать предложение для перевода, содержащее заданное слово
|
||
|
||
Args:
|
||
word: Слово (на языке обучения), которое должно быть в предложении
|
||
learning_lang: Язык обучения (ISO2)
|
||
translation_lang: Язык перевода (ISO2)
|
||
user_id: ID пользователя в БД для получения его модели
|
||
|
||
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, user_id=user_id)
|
||
|
||
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_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,
|
||
level: str = "B1",
|
||
count: int = 10,
|
||
learning_lang: str = "en",
|
||
translation_lang: str = "ru",
|
||
exclude_words: List[str] = None,
|
||
user_id: Optional[int] = None
|
||
) -> List[Dict]:
|
||
"""
|
||
Сгенерировать подборку слов по теме
|
||
|
||
Args:
|
||
theme: Тема для подборки слов
|
||
level: Уровень сложности (A1-C2)
|
||
count: Количество слов
|
||
learning_lang: Язык изучения
|
||
translation_lang: Язык перевода
|
||
exclude_words: Список слов для исключения (уже известные)
|
||
user_id: ID пользователя в БД для получения его модели
|
||
|
||
Returns:
|
||
Список словарей с информацией о словах
|
||
"""
|
||
exclude_instruction = ""
|
||
exclude_words_set = set()
|
||
if exclude_words:
|
||
# Ограничиваем список до 100 слов чтобы не раздувать промпт
|
||
words_sample = exclude_words[:100]
|
||
exclude_words_set = set(w.lower() for w in exclude_words)
|
||
exclude_instruction = f"""
|
||
|
||
⚠️ ЗАПРЕЩЁННЫЕ СЛОВА (НЕ ИСПОЛЬЗОВАТЬ!):
|
||
{', '.join(words_sample)}
|
||
|
||
Эти слова пользователь уже знает. ОБЯЗАТЕЛЬНО выбери ДРУГИЕ слова!"""
|
||
|
||
prompt = f"""Создай подборку из {count} слов на языке {learning_lang} по теме "{theme}" для уровня {level}. Переводы дай на {translation_lang}.
|
||
{exclude_instruction}
|
||
Верни ответ в формате JSON:
|
||
{{
|
||
"theme": "{theme}",
|
||
"words": [
|
||
{{
|
||
"word": "слово на {learning_lang}",
|
||
"translation": "перевод на {translation_lang}",
|
||
"transcription": "транскрипция в IPA (если применимо)",
|
||
"example": "пример использования на {learning_lang}",
|
||
"example_translation": "перевод примера на {translation_lang}"
|
||
}}
|
||
]
|
||
}}
|
||
|
||
Слова должны быть:
|
||
- Полезными и часто используемыми
|
||
- Соответствовать уровню {level}
|
||
- Связаны с темой "{theme}"
|
||
- Разнообразными (существительные, глаголы, прилагательные)"""
|
||
|
||
try:
|
||
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_request(messages, temperature=0.7, user_id=user_id)
|
||
|
||
import json
|
||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||
words = result.get('words', [])
|
||
|
||
# Фильтруем слова которые AI мог вернуть несмотря на инструкцию
|
||
if exclude_words_set:
|
||
filtered_words = [
|
||
w for w in words
|
||
if w.get('word', '').lower() not in exclude_words_set
|
||
]
|
||
filtered_count = len(words) - len(filtered_words)
|
||
if filtered_count > 0:
|
||
logger.info(f"[AI Response] generate_thematic_words: filtered out {filtered_count} excluded words")
|
||
words = filtered_words
|
||
|
||
logger.info(f"[AI Response] generate_thematic_words: success, generated {len(words)} words")
|
||
return words
|
||
|
||
except Exception as 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", user_id: Optional[int] = None) -> List[Dict]:
|
||
"""
|
||
Извлечь ключевые слова из текста для изучения
|
||
|
||
Args:
|
||
text: Текст на языке изучения
|
||
level: Уровень пользователя (A1-C2)
|
||
max_words: Максимальное количество слов для извлечения
|
||
learning_lang: Язык изучения
|
||
translation_lang: Язык перевода
|
||
user_id: ID пользователя в БД для получения его модели
|
||
|
||
Returns:
|
||
Список словарей с информацией о словах
|
||
"""
|
||
prompt = f"""Проанализируй следующий текст на языке {learning_lang} и извлеки из него до {max_words} самых полезных слов для изучения на уровне {level}. Переводы дай на {translation_lang}.
|
||
|
||
Текст:
|
||
{text}
|
||
|
||
Верни ответ в формате JSON:
|
||
{{
|
||
"words": [
|
||
{{
|
||
"word": "слово на {learning_lang} (в базовой форме)",
|
||
"translation": "перевод на {translation_lang}",
|
||
"transcription": "транскрипция в IPA (если применимо)",
|
||
"context": "предложение из текста на {learning_lang}, где используется это слово"
|
||
}}
|
||
]
|
||
}}
|
||
|
||
Критерии отбора слов:
|
||
- Выбирай самые важные и полезные слова из текста
|
||
- Слова должны быть интересны для уровня {level}
|
||
- Не включай простейшие слова (a, the, is, и т.д.)
|
||
- Слова должны быть в базовой форме (инфинитив для глаголов, ед.число для существительных)
|
||
- Разнообразие: существительные, глаголы, прилагательные, устойчивые выражения"""
|
||
|
||
try:
|
||
text_preview = text[:100] + "..." if len(text) > 100 else text
|
||
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_request(messages, temperature=0.5, user_id=user_id)
|
||
|
||
import json
|
||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||
words_count = len(result.get('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"[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", 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 с начальной репликой и контекстом
|
||
"""
|
||
scenarios = {
|
||
"restaurant": "ресторан - заказ еды",
|
||
"shopping": "магазин - покупка одежды",
|
||
"travel": "аэропорт/отель - путешествие",
|
||
"work": "офис - рабочая встреча",
|
||
"doctor": "клиника - визит к врачу",
|
||
"casual": "повседневный разговор"
|
||
}
|
||
|
||
scenario_desc = scenarios.get(scenario, "повседневный разговор")
|
||
|
||
extra_fields = ''
|
||
if learning_lang.lower() == 'ja':
|
||
# Для японского просим версию с фуриганой в скобках ТОЛЬКО для кандзи
|
||
# Не добавляй фуригану к кана или латинским буквам
|
||
extra_fields = ",\n \"message_annotated\": \"фраза на {learning_lang} с фуриганой в скобках ТОЛЬКО к кандзи (Так правильно: いらっしゃいませ!今日は何を注文(ちゅうもん)しますか?, Так неправильно: こんにちは(こんにちは)!今日ははどうですか?); к こんにちは не добовляй фурагану; не добавляй фуригану к катакане, фуригане, хирагане, частице и латинице\""
|
||
|
||
prompt = f"""Ты - собеседник для практики языка {learning_lang} уровня {level}.
|
||
Начни диалог в сценарии: {scenario_desc} на {learning_lang}.
|
||
|
||
Верни ответ в формате JSON:
|
||
{{
|
||
"message": "твоя первая реплика на {learning_lang}",
|
||
"translation": "перевод на {translation_lang}",
|
||
"context": "краткое описание ситуации на {translation_lang}",
|
||
"suggestions": [
|
||
{{"learn": "подсказка на {learning_lang}", "learn_annotated": "подсказка с фуриганой в скобках ТОЛЬКО к кандзи (Так правильно: いらっしゃいませ!今日は何を注文(ちゅうもん)しますか?, Так неправильно: こんにちは(こんにちは)!今日ははどうですか?); к こんにちは не добовляй фурагану; не добавляй фуригану к катакане, фуригане, хирагане, частице и латинице; {learning_lang})", "trans": "перевод подсказки на {translation_lang}"}},
|
||
{{"learn": "...", "learn_annotated": "...", "trans": "..."}},
|
||
{{"learn": "...", "learn_annotated": "...", "trans": "..."}}
|
||
]{extra_fields}
|
||
}}
|
||
|
||
Требования:
|
||
- Говори естественно, используй уровень {level}
|
||
- Создай интересную ситуацию
|
||
- Задай вопрос или начни разговор
|
||
- Подсказки должны помочь пользователю ответить"""
|
||
|
||
try:
|
||
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_request(messages, temperature=0.8, user_id=user_id)
|
||
|
||
import json
|
||
result = json.loads(response_data['choices'][0]['message']['content'])
|
||
logger.info(f"[AI Response] start_conversation: success, scenario='{scenario}'")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"[AI Error] start_conversation: {type(e).__name__}: {str(e)}")
|
||
return {
|
||
"message": "Hello! How are you today?",
|
||
"translation": "Привет! Как дела сегодня?",
|
||
"context": "Повседневный разговор",
|
||
"suggestions": ["I'm fine, thank you!", "Good, and you?", "Not bad!"]
|
||
}
|
||
|
||
async def continue_conversation(
|
||
self,
|
||
conversation_history: List[Dict],
|
||
user_message: str,
|
||
scenario: str,
|
||
level: str = "B1",
|
||
learning_lang: str = "en",
|
||
translation_lang: str = "ru",
|
||
user_id: Optional[int] = None
|
||
) -> Dict:
|
||
"""
|
||
Продолжить диалог и проверить ответ пользователя
|
||
|
||
Args:
|
||
conversation_history: История диалога
|
||
user_message: Сообщение пользователя
|
||
scenario: Сценарий диалога
|
||
level: Уровень пользователя
|
||
learning_lang: Язык изучения
|
||
translation_lang: Язык перевода
|
||
user_id: ID пользователя в БД для получения его модели
|
||
|
||
Returns:
|
||
Dict с ответом AI, проверкой и подсказками
|
||
"""
|
||
# Формируем историю для контекста
|
||
history_text = "\n".join([
|
||
f"{'AI' if msg['role'] == 'assistant' else 'User'}: {msg['content']}"
|
||
for msg in conversation_history[-6:] # Последние 6 сообщений
|
||
])
|
||
|
||
extra_fields_resp = ''
|
||
if learning_lang.lower() == 'ja':
|
||
# Для японского просим версию ответа с фуриганой ТОЛЬКО для кандзи
|
||
# Не добавляй фуригану к кана или латинским буквам
|
||
extra_fields_resp = ",\n \"response_annotated\": \"ответ на {learning_lang} с фуриганой ТОЛЬКО для кандзи (напр.: 今日(きょう)); не добавляй фуригану к кана или латинице\""
|
||
|
||
prompt = f"""Ты ведешь диалог на языке {learning_lang} уровня {level} в сценарии "{scenario}".
|
||
|
||
История диалога:
|
||
{history_text}
|
||
User: {user_message}
|
||
|
||
Верни ответ в формате JSON:
|
||
{{
|
||
"response": "твой ответ на {learning_lang}",
|
||
"translation": "перевод твоего ответа на {translation_lang}",
|
||
"feedback": {{
|
||
"has_errors": true/false,
|
||
"corrections": "исправления ошибок пользователя (если есть)",
|
||
"comment": "краткий комментарий об ответе пользователя"
|
||
}},
|
||
"suggestions": [
|
||
{{"learn": "подсказка на {learning_lang}", "learn_annotated": "подсказка с фуриганой (ТОЛЬКО для кандзи; {learning_lang})", "trans": "перевод подсказки на {translation_lang}"}},
|
||
{{"learn": "...", "learn_annotated": "...", "trans": "..."}}
|
||
]{extra_fields_resp}
|
||
}}
|
||
|
||
Требования:
|
||
- Продолжай естественный диалог
|
||
- Если у пользователя есть грамматические или лексические ошибки, укажи их в corrections
|
||
- Будь дружелюбным и поддерживающим
|
||
- Используй лексику уровня {level}"""
|
||
|
||
try:
|
||
logger.info(f"[AI Request] continue_conversation: scenario='{scenario}', level='{level}', history_length={len(conversation_history)}, learn='{learning_lang}', to='{translation_lang}'")
|
||
|
||
# Формируем сообщения для API
|
||
messages = [
|
||
{"role": "system", "content": f"Ты - дружелюбный собеседник для практики языка {learning_lang} уровня {level}. Веди естественный диалог и помогай исправлять ошибки."}
|
||
]
|
||
|
||
# Добавляем историю
|
||
for msg in conversation_history[-6:]:
|
||
messages.append(msg)
|
||
|
||
# Добавляем текущее сообщение пользователя
|
||
messages.append({"role": "user", "content": user_message})
|
||
|
||
# Добавляем инструкцию для форматирования ответа
|
||
messages.append({"role": "user", "content": prompt})
|
||
|
||
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'])
|
||
has_errors = result.get('feedback', {}).get('has_errors', False)
|
||
logger.info(f"[AI Response] continue_conversation: success, has_errors={has_errors}")
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"[AI Error] continue_conversation: {type(e).__name__}: {str(e)}")
|
||
return {
|
||
"response": "I see. Tell me more about that.",
|
||
"translation": "Понятно. Расскажи мне больше об этом.",
|
||
"feedback": {
|
||
"has_errors": False,
|
||
"corrections": "",
|
||
"comment": "Good!"
|
||
},
|
||
"suggestions": ["Sure!", "Well...", "Actually..."]
|
||
}
|
||
|
||
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 вопросов разной сложности
|
||
"""
|
||
# Определяем систему уровней и язык для промпта
|
||
if learning_language == "ja":
|
||
level_system = "JLPT (N5-N1)"
|
||
language_name = "японского"
|
||
levels_req = """- Вопросы 1-2: уровень N5 (базовый)
|
||
- Вопросы 3-4: уровень N4-N3 (элементарный-средний)
|
||
- Вопросы 5-6: уровень N2 (продвинутый)
|
||
- Вопрос 7: уровень N1 (профессиональный)"""
|
||
level_example = "N5"
|
||
else:
|
||
level_system = "CEFR (A1-C2)"
|
||
lang_names = {"en": "английского", "es": "испанского", "de": "немецкого", "fr": "французского"}
|
||
language_name = lang_names.get(learning_language, "английского")
|
||
levels_req = """- Вопросы 1-2: уровень A1 (базовый)
|
||
- Вопросы 3-4: уровень A2-B1 (элементарный-средний)
|
||
- Вопросы 5-6: уровень B2-C1 (продвинутый)
|
||
- Вопрос 7: уровень C2 (профессиональный)"""
|
||
level_example = "A1"
|
||
|
||
prompt = f"""Создай тест из 7 вопросов для определения уровня {language_name} языка ({level_system}).
|
||
|
||
Верни ответ в формате JSON:
|
||
{{
|
||
"questions": [
|
||
{{
|
||
"question": "текст вопроса на изучаемом языке",
|
||
"question_ru": "перевод вопроса на русский",
|
||
"options": ["вариант A", "вариант B", "вариант C", "вариант D"],
|
||
"correct": 0,
|
||
"level": "{level_example}"
|
||
}}
|
||
]
|
||
}}
|
||
|
||
Требования:
|
||
{levels_req}
|
||
- Каждый вопрос с 4 вариантами ответа
|
||
- correct - индекс правильного ответа (0-3)
|
||
- Вопросы на грамматику, лексику и понимание"""
|
||
|
||
try:
|
||
logger.info(f"[AI Request] generate_level_test: generating 7 questions for {learning_language}")
|
||
|
||
system_msg = f"Ты - эксперт по тестированию уровня {language_name} языка. Создавай объективные тесты."
|
||
messages = [
|
||
{"role": "system", "content": system_msg},
|
||
{"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'])
|
||
questions_count = len(result.get('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"[AI Error] generate_level_test: {type(e).__name__}: {str(e)}, using fallback questions")
|
||
# Fallback с базовыми вопросами
|
||
if learning_language == "ja":
|
||
return self._get_jlpt_fallback_questions()
|
||
return self._get_cefr_fallback_questions()
|
||
|
||
def _get_cefr_fallback_questions(self) -> List[Dict]:
|
||
"""Fallback вопросы для CEFR (английский и европейские языки)"""
|
||
return [
|
||
{
|
||
"question": "What is your name?",
|
||
"question_ru": "Как тебя зовут?",
|
||
"options": ["My name is", "I am name", "Name my is", "Is name my"],
|
||
"correct": 0,
|
||
"level": "A1"
|
||
},
|
||
{
|
||
"question": "I ___ to school every day.",
|
||
"question_ru": "Я ___ в школу каждый день.",
|
||
"options": ["go", "goes", "going", "went"],
|
||
"correct": 0,
|
||
"level": "A1"
|
||
},
|
||
{
|
||
"question": "She ___ been to Paris twice.",
|
||
"question_ru": "Она ___ в Париже дважды.",
|
||
"options": ["have", "has", "had", "having"],
|
||
"correct": 1,
|
||
"level": "A2"
|
||
},
|
||
{
|
||
"question": "If I ___ rich, I would travel the world.",
|
||
"question_ru": "Если бы я был богат, я бы путешествовал по миру.",
|
||
"options": ["am", "was", "were", "be"],
|
||
"correct": 2,
|
||
"level": "B1"
|
||
},
|
||
{
|
||
"question": "The project ___ by next Monday.",
|
||
"question_ru": "Проект ___ к следующему понедельнику.",
|
||
"options": ["will complete", "will be completed", "completes", "is completing"],
|
||
"correct": 1,
|
||
"level": "B2"
|
||
},
|
||
{
|
||
"question": "Had I known about the meeting, I ___ attended.",
|
||
"question_ru": "Если бы я знал о встрече, я бы посетил.",
|
||
"options": ["would have", "will have", "would", "will"],
|
||
"correct": 0,
|
||
"level": "C1"
|
||
},
|
||
{
|
||
"question": "The nuances of his argument were so ___ that few could grasp them.",
|
||
"question_ru": "Нюансы его аргумента были настолько ___, что немногие могли их понять.",
|
||
"options": ["subtle", "obvious", "simple", "clear"],
|
||
"correct": 0,
|
||
"level": "C2"
|
||
}
|
||
]
|
||
|
||
def _get_jlpt_fallback_questions(self) -> List[Dict]:
|
||
"""Fallback вопросы для JLPT (японский)"""
|
||
return [
|
||
{
|
||
"question": "これは ___です。",
|
||
"question_ru": "Это ___.",
|
||
"options": ["ほん", "本ん", "ぼん", "もと"],
|
||
"correct": 0,
|
||
"level": "N5"
|
||
},
|
||
{
|
||
"question": "私は毎日学校に___。",
|
||
"question_ru": "Я каждый день хожу в школу.",
|
||
"options": ["いきます", "いくます", "いきす", "いきました"],
|
||
"correct": 0,
|
||
"level": "N5"
|
||
},
|
||
{
|
||
"question": "昨日、映画を___から、今日は勉強します。",
|
||
"question_ru": "Вчера я посмотрел фильм, поэтому сегодня буду учиться.",
|
||
"options": ["見た", "見て", "見る", "見ない"],
|
||
"correct": 0,
|
||
"level": "N4"
|
||
},
|
||
{
|
||
"question": "この本は読み___です。",
|
||
"question_ru": "Эту книгу легко/трудно читать.",
|
||
"options": ["やすい", "にくい", "たい", "そう"],
|
||
"correct": 0,
|
||
"level": "N3"
|
||
},
|
||
{
|
||
"question": "彼の話を聞く___、涙が出てきた。",
|
||
"question_ru": "Слушая его рассказ, у меня потекли слёзы.",
|
||
"options": ["につれて", "にしたがって", "とともに", "うちに"],
|
||
"correct": 0,
|
||
"level": "N2"
|
||
},
|
||
{
|
||
"question": "その計画は実現不可能と___。",
|
||
"question_ru": "Этот план считается невыполнимым.",
|
||
"options": ["言わざるを得ない", "言うまでもない", "言いかねない", "言うに及ばない"],
|
||
"correct": 0,
|
||
"level": "N2"
|
||
},
|
||
{
|
||
"question": "彼の行動は___に堪えない。",
|
||
"question_ru": "Его поведение невозможно понять/вынести.",
|
||
"options": ["理解", "批判", "説明", "弁解"],
|
||
"correct": 0,
|
||
"level": "N1"
|
||
}
|
||
]
|
||
|
||
|
||
# Глобальный экземпляр сервиса
|
||
ai_service = AIService()
|