import json 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** -> bold text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) # *italic* -> italic (но не внутри уже конвертированных тегов) text = re.sub(r'(?)\*([^*]+?)\*(?![^<]*\1', 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 [] # Инструкция для фуриганы в японском furigana_instruction = "" if learning_lang == "ja": furigana_instruction = """ - ОБЯЗАТЕЛЬНО: добавляй фуригану к кандзи в формате: 漢字(かんじ) - Пример: 私(わたし)は毎日(まいにち)___を読(よ)みます。""" 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 слов) - Контекст должен подсказывать правильное слово{furigana_instruction} - Верни результаты В ТОМ ЖЕ ПОРЯДКЕ что и задания""" 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)} Эти слова пользователь уже знает. ОБЯЗАТЕЛЬНО выбери ДРУГИЕ слова!""" # Инструкция для фуриганы в японском furigana_instruction = "" if learning_lang == "ja": furigana_instruction = """ - ОБЯЗАТЕЛЬНО: в примерах добавляй фуригану к кандзи в формате: 漢字(かんじ) - Пример: 私(わたし)は毎日(まいにち)本(ほん)を読(よ)みます。""" 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}" - Разнообразными (существительные, глаголы, прилагательные){furigana_instruction}""" 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: Список словарей с информацией о словах """ # Инструкция для фуриганы в японском furigana_instruction = "" if learning_lang == "ja": furigana_instruction = """ - ОБЯЗАТЕЛЬНО: в context добавляй фуригану к кандзи в формате: 漢字(かんじ) - Для transcription используй хирагану""" 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, и т.д.) - Слова должны быть в базовой форме (инфинитив для глаголов, ед.число для существительных) - Разнообразие: существительные, глаголы, прилагательные, устойчивые выражения{furigana_instruction}""" 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 вопросов разной сложности """ # Определяем систему уровней и язык для промпта furigana_instruction = "" 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" furigana_instruction = """ - ОБЯЗАТЕЛЬНО: добавляй фуригану к кандзи в формате: 漢字(かんじ) - Пример: 私(わたし)は毎日(まいにち)学校(がっこう)に行(い)きます。""" 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) - Вопросы на грамматику, лексику и понимание{furigana_instruction}""" 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): - жирный текст для важного (НЕ **жирный**) - курсив для примеров (НЕ *курсив*) - НЕ используй звёздочки *, НЕ используй 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"📖 {topic_name}\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: Список упражнений """ furigana_instruction = "" if learning_lang == "ja": language_name = "японском" furigana_instruction = """ - ОБЯЗАТЕЛЬНО: добавляй фуригану к кандзи в формате: 漢字(かんじ) - Пример: 私(わたし)は毎日(まいにち)___に行(い)きます。""" 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}{furigana_instruction}""" 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_words_of_day_batch( self, language: str, levels: List[str], translation_lang: str = "ru", excluded_words: Dict[str, List[str]] = None ) -> Optional[Dict[str, Dict]]: """ Генерация слов дня для всех уровней одного языка за один запрос. Args: language: Язык изучения (en/ja) levels: Список уровней (A1-C2 или N5-N1) translation_lang: Язык перевода excluded_words: Dict {level: [excluded_words]} для исключения Returns: Dict {level: word_data} или None при ошибке """ language_names = {"en": "английский", "ja": "японский"} language_name = language_names.get(language, "английский") translation_names = {"ru": "русский", "en": "английский", "ja": "японский"} translation_name = translation_names.get(translation_lang, "русский") # Формируем список исключений по уровням excluded_info = "" if excluded_words: excluded_parts = [] for level, words in excluded_words.items(): if words: excluded_parts.append(f"- {level}: {', '.join(words[:15])}") if excluded_parts: excluded_info = "\n\nНЕ используй эти слова (уже были недавно):\n" + "\n".join(excluded_parts) levels_str = ", ".join(levels) # Инструкция для фуриганы в японском furigana_instruction = "" if language == "ja": furigana_instruction = """ - ОБЯЗАТЕЛЬНО: в примерах (sentence) добавляй фуригану к кандзи в формате: 漢字(かんじ) - Пример: 私(わたし)は毎日(まいにち)本(ほん)を読(よ)みます。""" prompt = f"""Сгенерируй "слово дня" для изучающих {language_name} язык на каждом из уровней: {levels_str}. Требования для каждого слова: - Слово должно быть полезным и интересным - Строго соответствовать указанному уровню сложности - Желательно с интересной этимологией или фактом - Все слова должны быть РАЗНЫМИ{furigana_instruction}{excluded_info} Верни JSON объект, где ключи - уровни ({levels_str}): {{ "{levels[0]}": {{ "word": "слово на {language_name}", "transcription": "транскрипция (IPA для английского, хирагана для японского)", "translation": "перевод на {translation_name}", "examples": [ {{"sentence": "пример предложения", "translation": "перевод примера"}}, {{"sentence": "второй пример", "translation": "перевод"}} ], "synonyms": "синоним1, синоним2", "etymology": "краткий интересный факт (1-2 предложения)" }}, ... (для каждого уровня) }}""" try: logger.info(f"[AI Request] generate_words_of_day_batch: lang='{language}', levels={levels}") messages = [ {"role": "system", "content": "Ты - опытный лингвист. Отвечай только валидным JSON без markdown."}, {"role": "user", "content": prompt} ] model_name, provider = await self._get_active_model(None) 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_words_of_day_batch: generated {len(result)} words") return result except Exception as e: logger.error(f"[AI Error] generate_words_of_day_batch: {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) # Специальные инструкции для японского языка japanese_instructions = "" if learning_lang == "ja": japanese_instructions = """ - ОБЯЗАТЕЛЬНО: Для КАЖДОГО кандзи добавляй фуригану в формате: 漢字(かんじ) - Пример: 私(わたし)は毎日(まいにち)学校(がっこう)に行(い)きます。 - Это критически важно для изучающих японский!""" prompt = f"""Создай {genre_desc} на {language_name} языке для уровня {level}. Требования: - Длина: {word_range} слов - Используй лексику и грамматику подходящую для уровня {level} - История должна быть интересной и законченной - Выдели 5-8 ключевых слов которые могут быть новыми для изучающего - Добавь полный перевод текста на {translation_name} язык{japanese_instructions} Верни 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()