Initial commit: VKUS vitrina MVP (frontend + backend).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-29 23:13:06 +07:00
commit 862e6b7b25
113 changed files with 50949 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
"""Клиент внешней 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