pp003/services/yandex.py
2026-05-27 15:55:36 +03:00

168 lines
6.6 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.

"""
Обёртка над Яндекс 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