Files
tg_bot_language/services/ai_service.py
mamonov.ep f38ff2f18e feat: мини-истории, слово дня, меню практики
- Добавлены мини-истории для чтения с выбором жанра и вопросами
- Кнопка показа/скрытия перевода истории
- Количество вопросов берётся из настроек пользователя
- Слово дня генерируется глобально в 00:00 UTC
- Кнопка "Практика" открывает меню выбора режима
- Убран автоматический create_all при запуске (только миграции)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 15:05:38 +03:00

1638 lines
77 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
def _markdown_to_html(self, text: str) -> str:
"""Конвертировать markdown форматирование в HTML для Telegram."""
import re
# **bold** -> <b>bold</b>
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
# *italic* -> <i>italic</i> (но не внутри уже конвертированных тегов)
text = re.sub(r'(?<!</[bi]>)\*([^*]+?)\*(?![^<]*</)', r'<i>\1</i>', text)
# Убираем оставшиеся одиночные * в начале строк (списки)
text = re.sub(r'^\*\s+', '', text, flags=re.MULTILINE)
return text
def _strip_markdown_code_block(self, text: str) -> str:
"""Удалить markdown обёртку ```json ... ``` из текста."""
import re
text = text.strip()
# Паттерн для ```json ... ``` или просто ``` ... ```
pattern = r'^```(?:json)?\s*\n?(.*?)\n?```$'
match = re.match(pattern, text, re.DOTALL)
if match:
return match.group(1).strip()
# Альтернативный способ - если начинается с ``` но паттерн не сработал
if text.startswith('```'):
lines = text.split('\n')
# Убираем первую строку (```json или ```)
lines = lines[1:]
# Убираем последнюю строку если это ```
if lines and lines[-1].strip() == '```':
lines = lines[:-1]
return '\n'.join(lines).strip()
return text
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 ... ``` или ```...```)
text = self._strip_markdown_code_block(text)
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()
async def generate_grammar_rule(
self,
topic_name: str,
topic_description: str,
level: str,
learning_lang: str = "en",
ui_lang: str = "ru",
user_id: Optional[int] = None
) -> str:
"""
Генерация объяснения грамматического правила.
Args:
topic_name: Название темы (например, "Present Simple")
topic_description: Описание темы (например, "I work, he works")
level: Уровень пользователя (A1-C2 или N5-N1)
learning_lang: Язык изучения
ui_lang: Язык интерфейса для объяснения
user_id: ID пользователя в БД
Returns:
Текст с объяснением правила
"""
if learning_lang == "ja":
language_name = "японского"
else:
language_name = "английского"
prompt = f"""Объясни грамматическое правило "{topic_name}" ({topic_description}) для изучающих {language_name} язык.
Уровень ученика: {level}
Язык объяснения: {ui_lang}
Требования:
- Объяснение должно быть кратким и понятным (3-5 предложений)
- Приведи формулу/структуру правила
- Дай 2-3 примера с переводом
- Упомяни типичные ошибки (если есть)
- Адаптируй сложность под уровень {level}
ВАЖНО - форматирование для Telegram (используй ТОЛЬКО HTML теги, НЕ markdown):
- <b>жирный текст</b> для важного (НЕ **жирный**)
- <i>курсив</i> для примеров (НЕ *курсив*)
- НЕ используй звёздочки *, НЕ используй markdown
- Можно использовать эмодзи"""
try:
logger.info(f"[AI Request] generate_grammar_rule: topic='{topic_name}', level='{level}'")
messages = [
{"role": "system", "content": f"Ты - опытный преподаватель {language_name} языка. Объясняй правила просто и понятно."},
{"role": "user", "content": prompt}
]
# Для этого запроса не используем JSON mode
model_name, provider = await self._get_active_model(user_id)
if provider == AIProvider.google:
response_data = await self._make_google_request_text(messages, temperature=0.5, model=model_name)
else:
response_data = await self._make_openai_request_text(messages, temperature=0.5, model=model_name)
rule_text = response_data['choices'][0]['message']['content']
# Конвертируем markdown в HTML на случай если AI использовал звёздочки
rule_text = self._markdown_to_html(rule_text)
logger.info(f"[AI Response] generate_grammar_rule: success, {len(rule_text)} chars")
return rule_text
except Exception as e:
logger.error(f"[AI Error] generate_grammar_rule: {type(e).__name__}: {str(e)}")
return f"📖 <b>{topic_name}</b>\n\n{topic_description}\n\nИзучите это правило и приступайте к упражнениям."
async def _make_google_request_text(self, messages: list, temperature: float = 0.3, model: str = "gemini-2.0-flash-lite") -> dict:
"""Запрос к Google без JSON mode (для текстовых ответов)"""
url = f"{self.google_base_url}/models/{model}:generateContent"
contents = []
for msg in messages:
role = msg["role"]
content = msg["content"]
if role == "system":
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()
text = data["candidates"][0]["content"]["parts"][0]["text"]
return {"choices": [{"message": {"content": text}}]}
async def _make_openai_request_text(self, messages: list, temperature: float = 0.3, model: str = "gpt-4o-mini") -> dict:
"""Запрос к OpenAI без JSON mode (для текстовых ответов)"""
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 = await self.http_client.post(url, headers=headers, json=payload)
response.raise_for_status()
return response.json()
async def generate_grammar_exercise(
self,
topic_id: str,
topic_name: str,
topic_description: str,
level: str,
learning_lang: str = "en",
translation_lang: str = "ru",
count: int = 3,
user_id: Optional[int] = None
) -> List[Dict]:
"""
Генерация грамматических упражнений по теме.
Args:
topic_id: ID темы (например, "present_simple")
topic_name: Название темы (например, "Present Simple")
topic_description: Описание темы (например, "I work, he works")
level: Уровень пользователя (A1-C2 или N5-N1)
learning_lang: Язык изучения
translation_lang: Язык перевода
count: Количество упражнений
user_id: ID пользователя в БД для получения его модели
Returns:
Список упражнений
"""
if learning_lang == "ja":
language_name = "японском"
else:
language_name = "английском"
prompt = f"""Создай {count} грамматических упражнения на тему "{topic_name}" ({topic_description}).
Уровень: {level}
Язык: {language_name}
Язык перевода: {translation_lang}
Верни ответ в формате JSON:
{{
"exercises": [
{{
"sentence": "предложение с пропуском ___ на {learning_lang}",
"translation": "ПОЛНЫЙ перевод предложения на {translation_lang} (без пропусков, с правильным ответом)",
"correct_answer": "правильный ответ для пропуска",
"hint": "краткая подсказка на {translation_lang} (1-2 слова)",
"explanation": "объяснение правила на {translation_lang} (1-2 предложения)"
}}
]
}}
Требования:
- Предложения должны быть естественными и полезными
- Пропуск обозначай как ___
- ВАЖНО: translation должен быть ПОЛНЫМ переводом готового предложения (без пропусков), чтобы ученик понимал смысл
- Подсказка должна направлять к ответу, но не содержать его
- Объяснение должно быть понятным для уровня {level}
- Сложность должна соответствовать уровню {level}"""
try:
logger.info(f"[AI Request] generate_grammar_exercise: topic='{topic_name}', level='{level}'")
messages = [
{"role": "system", "content": f"Ты - преподаватель {language_name} языка. Создавай качественные упражнения. Отвечай только JSON."},
{"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'])
exercises = result.get('exercises', [])
logger.info(f"[AI Response] generate_grammar_exercise: success, {len(exercises)} exercises generated")
return exercises
except Exception as e:
logger.error(f"[AI Error] generate_grammar_exercise: {type(e).__name__}: {str(e)}")
# Fallback с простым упражнением
return [{
"sentence": f"Example sentence with ___ ({topic_name})",
"translation": "Пример предложения",
"correct_answer": "answer",
"hint": "hint",
"explanation": f"This exercise is about {topic_name}."
}]
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"
}
]
async def generate_word_of_day(
self,
level: str,
learning_lang: str = "en",
translation_lang: str = "ru",
excluded_words: List[str] = None,
user_id: Optional[int] = None
) -> Optional[Dict]:
"""
Генерация слова дня.
Args:
level: Уровень пользователя (A1-C2 или N5-N1)
learning_lang: Язык изучения
translation_lang: Язык перевода
excluded_words: Список слов для исключения (уже были)
user_id: ID пользователя для выбора модели
Returns:
Dict с полями: word, transcription, translation, examples, synonyms, etymology
"""
language_names = {
"en": "английский",
"ja": "японский"
}
language_name = language_names.get(learning_lang, "английский")
translation_names = {
"ru": "русский",
"en": "английский",
"ja": "японский"
}
translation_name = translation_names.get(translation_lang, "русский")
excluded_str = ""
if excluded_words:
excluded_str = f"\n\nНЕ используй эти слова (уже были): {', '.join(excluded_words[:20])}"
prompt = f"""Сгенерируй интересное "слово дня" для изучающего {language_name} язык на уровне {level}.
Требования:
- Слово должно быть полезным и интересным
- Подходящее для уровня {level}
- НЕ слишком простое и НЕ слишком сложное
- Желательно с интересной этимологией или фактом{excluded_str}
Верни JSON:
{{
"word": "слово на {language_name}",
"transcription": "транскрипция (IPA для английского, хирагана для японского)",
"translation": "перевод на {translation_name}",
"examples": [
{{"sentence": "пример предложения", "translation": "перевод примера"}},
{{"sentence": "второй пример", "translation": "перевод"}}
],
"synonyms": "синоним1, синоним2, синоним3",
"etymology": "краткий интересный факт о слове или его происхождении (1-2 предложения)"
}}"""
try:
logger.info(f"[AI Request] generate_word_of_day: level='{level}', lang='{learning_lang}'")
messages = [
{"role": "system", "content": "Ты - опытный лингвист, который подбирает интересные слова для изучения."},
{"role": "user", "content": prompt}
]
model_name, provider = await self._get_active_model(user_id)
if provider == AIProvider.google:
response_data = await self._make_google_request(messages, temperature=0.8, model=model_name)
else:
response_data = await self._make_openai_request(messages, temperature=0.8, model=model_name)
content = response_data['choices'][0]['message']['content']
content = self._strip_markdown_code_block(content)
result = json.loads(content)
logger.info(f"[AI Response] generate_word_of_day: word='{result.get('word', 'N/A')}'")
return result
except Exception as e:
logger.error(f"[AI Error] generate_word_of_day: {type(e).__name__}: {str(e)}")
return None
async def generate_mini_story(
self,
genre: str,
level: str,
learning_lang: str = "en",
translation_lang: str = "ru",
user_id: Optional[int] = None,
num_questions: int = 5
) -> Optional[Dict]:
"""
Генерация мини-истории для чтения.
Args:
genre: Жанр (dialogue, news, story, letter, recipe)
level: Уровень (A1-C2 или N5-N1)
learning_lang: Язык истории
translation_lang: Язык переводов
user_id: ID пользователя для выбора модели
num_questions: Количество вопросов (из настроек пользователя)
Returns:
Dict с полями: title, content, vocabulary, questions, word_count
"""
import json
language_names = {
"en": "английский",
"ja": "японский"
}
language_name = language_names.get(learning_lang, "английский")
translation_names = {
"ru": "русский",
"en": "английский",
"ja": "японский"
}
translation_name = translation_names.get(translation_lang, "русский")
genre_descriptions = {
"dialogue": "разговорный диалог между людьми",
"news": "короткая новостная статья",
"story": "художественный рассказ с сюжетом",
"letter": "email или письмо",
"recipe": "рецепт блюда с инструкциями"
}
genre_desc = genre_descriptions.get(genre, "короткий рассказ")
# Определяем длину текста по уровню
word_counts = {
"A1": "50-80", "N5": "30-50",
"A2": "80-120", "N4": "50-80",
"B1": "120-180", "N3": "80-120",
"B2": "180-250", "N2": "120-180",
"C1": "250-350", "N1": "180-250",
"C2": "300-400"
}
word_range = word_counts.get(level, "100-150")
# Генерируем примеры вопросов для промпта
questions_examples = []
for i in range(num_questions):
questions_examples.append(f''' {{
"question": "Вопрос {i + 1} на понимание на {translation_name}",
"options": ["вариант 1", "вариант 2", "вариант 3"],
"correct": {i % 3}
}}''')
questions_json = ",\n".join(questions_examples)
prompt = f"""Создай {genre_desc} на {language_name} языке для уровня {level}.
Требования:
- Длина: {word_range} слов
- Используй лексику и грамматику подходящую для уровня {level}
- История должна быть интересной и законченной
- Выдели 5-8 ключевых слов которые могут быть новыми для изучающего
- Добавь полный перевод текста на {translation_name} язык
Верни JSON:
{{
"title": "Название истории на {language_name}",
"content": "Полный текст истории",
"translation": "Полный перевод истории на {translation_name}",
"vocabulary": [
{{"word": "слово", "translation": "перевод на {translation_name}", "transcription": "транскрипция"}},
...
],
"questions": [
{questions_json}
],
"word_count": число_слов_в_тексте
}}
Важно:
- Создай ровно {num_questions} вопросов на понимание текста
- У каждого вопроса ровно 3 варианта ответа
- correct — индекс правильного ответа (0, 1 или 2)"""
try:
logger.info(f"[AI Request] generate_mini_story: genre='{genre}', level='{level}', lang='{learning_lang}'")
messages = [
{"role": "system", "content": "Ты - автор адаптированных текстов для изучающих иностранные языки."},
{"role": "user", "content": prompt}
]
model_name, provider = await self._get_active_model(user_id)
if provider == AIProvider.google:
response_data = await self._make_google_request(messages, temperature=0.8, model=model_name)
else:
response_data = await self._make_openai_request(messages, temperature=0.8, model=model_name)
content = response_data['choices'][0]['message']['content']
content = self._strip_markdown_code_block(content)
result = json.loads(content)
logger.info(f"[AI Response] generate_mini_story: title='{result.get('title', 'N/A')}', words={result.get('word_count', 0)}")
return result
except Exception as e:
logger.error(f"[AI Error] generate_mini_story: {type(e).__name__}: {str(e)}")
return None
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()