""" Обёртка над Яндекс Cloud API (через openai-совместимый клиент). """ import json import os from openai import AsyncOpenAI, APIConnectionError, APIStatusError, APITimeoutError from services.meta_prompt import ( STANDARD_SYSTEM_PROMPT, META_SYSTEM_PROMPT, build_meta_user_message, ) YANDEX_BASE_URL = "https://ai.api.cloud.yandex.net/v1" YANDEX_MODEL_NAME = "gpt-oss-120b/latest" def _get_client() -> AsyncOpenAI: api_key = os.getenv("YANDEX_API_KEY") folder_id = os.getenv("YANDEX_FOLDER_ID") if not api_key or not folder_id: raise RuntimeError( "Переменные окружения YANDEX_API_KEY и YANDEX_FOLDER_ID не заданы. " "Проверь файл .env." ) return AsyncOpenAI( api_key=api_key, base_url=YANDEX_BASE_URL, default_headers={"x-folder-id": folder_id}, ) def _model_uri() -> str: folder_id = os.getenv("YANDEX_FOLDER_ID", "") return f"gpt://{folder_id}/{YANDEX_MODEL_NAME}" async def _call_llm( system_prompt: str, user_message: str, temperature: float = 0.4, max_tokens: int = 2000, ) -> str: """Базовый вызов LLM. Возвращает текст ответа модели.""" client = _get_client() try: response = await client.chat.completions.create( model=_model_uri(), temperature=temperature, max_tokens=max_tokens, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], ) except APIConnectionError as e: raise RuntimeError( "Не удалось подключиться к Яндекс Cloud API. " "Проверьте сетевое соединение." ) from e except APITimeoutError as e: raise RuntimeError( "Яндекс Cloud API не ответил в отведённое время. Попробуйте позже." ) from e except APIStatusError as e: # Передаём статус и тело ошибки от Яндекса в понятном виде raise RuntimeError( f"Яндекс Cloud API вернул ошибку {e.status_code}: {e.message}" ) from e text = response.choices[0].message.content or "" if not text.strip(): raise RuntimeError("Модель вернула пустой ответ.") return text async def get_standard_answer(history: list[dict], meta_feedbacks: list[str]) -> str: """ Получить ответ модели с учётом истории диалога и рекомендаций мета-мониторинга. history — список {"role": "user"/"assistant", "content": "..."} за текущую сессию. meta_feedbacks — список рекомендаций от предыдущих мета-мониторингов в этой сессии. """ client = _get_client() # Формируем системный промпт: базовый + рекомендации мета-мониторинга если есть system_content = STANDARD_SYSTEM_PROMPT if meta_feedbacks: joined = "\n".join(f"- {fb}" for fb in meta_feedbacks) system_content += ( "\n\nВ этой сессии агент мета-мониторинга уже оценивал твои предыдущие ответы " "и выдал следующие рекомендации по улучшению. Учитывай их в своих ответах:\n" + joined ) messages = [{"role": "system", "content": system_content}] + history try: response = await client.chat.completions.create( model=_model_uri(), temperature=0.4, max_tokens=2000, messages=messages, ) except APIConnectionError as e: raise RuntimeError( "Не удалось подключиться к Яндекс Cloud API. Проверьте сетевое соединение." ) from e except APITimeoutError as e: raise RuntimeError( "Яндекс Cloud API не ответил в отведённое время. Попробуйте позже." ) from e except APIStatusError as e: raise RuntimeError( f"Яндекс Cloud API вернул ошибку {e.status_code}: {e.message}" ) from e text = response.choices[0].message.content or "" if not text.strip(): raise RuntimeError("Модель вернула пустой ответ.") return text async def get_meta_monitoring_result(user_request: str, model_answer: str) -> dict: """ Запустить мета-мониторинг ответа модели. Возвращает распарсенный словарь с результатами оценки. """ user_message = build_meta_user_message(user_request, model_answer) raw = await _call_llm( system_prompt=META_SYSTEM_PROMPT, user_message=user_message, temperature=0.2, # меньше случайности — нужен стабильный JSON max_tokens=1500, ) # Очищаем на случай если модель всё же добавила markdown-обёртку cleaned = raw.strip() if cleaned.startswith("```"): lines = cleaned.splitlines() # Убираем первую и последнюю строки с ``` cleaned = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:]) try: result = json.loads(cleaned) except json.JSONDecodeError as e: raise RuntimeError( f"Агент мета-мониторинга вернул невалидный JSON. " f"Ошибка парсинга: {e}. Сырой ответ: {raw[:300]}" ) from e # Гарантируем обязательные поля required = {"factual_correctness", "completeness", "logical_consistency", "overall_score", "comment", "issues", "recommendations"} missing = required - result.keys() if missing: raise RuntimeError( f"В ответе мета-мониторинга отсутствуют поля: {', '.join(missing)}" ) # Убеждаемся что issues — это список if not isinstance(result.get("issues"), list): result["issues"] = [str(result.get("issues", "Недостатки не указаны"))] return result