"""Клиент внешней LLM (OpenAI-совместимый API платформы ИИ).""" from __future__ import annotations import logging from dataclasses import dataclass import httpx from app.config import settings logger = logging.getLogger(__name__) @dataclass class LlmTestResult: ok: bool configured: bool model: str endpoint: str message: str sample: str | None = None status_code: int | None = None error: str | None = None class LlmClient: def __init__(self) -> None: self._system_prompt = self._load_system_prompt() def is_configured(self) -> bool: return settings.llm_enabled and bool(settings.llm_api_key or settings.llm_jwt_token) def _load_system_prompt(self) -> str: path = settings.system_prompt_path if path.exists(): try: content = path.read_text(encoding="utf-8") return content[:6000] except OSError: pass return settings.system_prompt_fallback def _headers(self) -> dict[str, str]: headers = {"Content-Type": "application/json"} if settings.llm_api_key: headers["Authorization"] = f"Bearer {settings.llm_api_key}" if settings.llm_jwt_token: headers["X-JWT-Token"] = settings.llm_jwt_token # OpenRouter рекомендует указать referer if "openrouter.ai" in settings.llm_chat_url: headers["HTTP-Referer"] = "http://localhost:5173" headers["X-Title"] = "VKUS IT Assistant" return headers def test_connection(self) -> LlmTestResult: endpoint = settings.llm_chat_url model = settings.llm_model if not settings.llm_enabled: return LlmTestResult( ok=False, configured=False, model=model, endpoint=endpoint, message="LLM отключена (VKUS_LLM_ENABLED=false)", ) if not settings.llm_api_key and not settings.llm_jwt_token: return LlmTestResult( ok=False, configured=False, model=model, endpoint=endpoint, message="API-ключ не задан (VKUS_LLM_API_KEY)", ) payload = { "model": model, "messages": [ {"role": "user", "content": "Ответь одним словом: работает"}, ], "max_tokens": 16, "temperature": 0, } try: with httpx.Client(timeout=settings.llm_timeout, verify=settings.llm_verify_ssl) as client: response = client.post(endpoint, json=payload, headers=self._headers()) if response.status_code >= 400: return LlmTestResult( ok=False, configured=True, model=model, endpoint=endpoint, message="Ошибка HTTP при обращении к LLM", status_code=response.status_code, error=response.text[:500], ) data = response.json() sample = data["choices"][0]["message"]["content"].strip() return LlmTestResult( ok=True, configured=True, model=model, endpoint=endpoint, message="Соединение с LLM установлено", sample=sample, status_code=response.status_code, ) except httpx.TimeoutException: return LlmTestResult( ok=False, configured=True, model=model, endpoint=endpoint, message=f"Таймаут ({settings.llm_timeout} сек)", error="timeout", ) except Exception as exc: logger.exception("LLM connection test failed") return LlmTestResult( ok=False, configured=True, model=model, endpoint=endpoint, message="Не удалось подключиться к LLM", error=str(exc)[:500], ) def generate_with_context(self, query: str, context: str) -> str | None: if not self.is_configured(): return None user_content = ( "Используй только предоставленный контекст из корпоративных инструкций.\n" "Если контекста недостаточно — честно скажи об этом.\n" "Ответ должен быть кратким, структурированным, на русском языке.\n\n" f"### Контекст\n{context}\n\n" f"### Вопрос сотрудника\n{query}" ) payload = { "model": settings.llm_model, "messages": [ {"role": "system", "content": self._system_prompt}, {"role": "user", "content": user_content}, ], "temperature": settings.llm_temperature, "max_tokens": settings.llm_max_tokens, } try: with httpx.Client(timeout=settings.llm_timeout, verify=settings.llm_verify_ssl) as client: response = client.post(settings.llm_chat_url, json=payload, headers=self._headers()) response.raise_for_status() data = response.json() return data["choices"][0]["message"]["content"].strip() except Exception as exc: if settings.use_template_on_llm_failure: logger.warning("LLM request failed, fallback to template: %s", exc) else: logger.error("LLM request failed: %s", exc) return None _llm_client: LlmClient | None = None def get_llm_client() -> LlmClient: global _llm_client if _llm_client is None: _llm_client = LlmClient() return _llm_client