"""Загрузка llm.config.yaml и переопределений из переменных окружения.""" from __future__ import annotations import os from functools import lru_cache from pathlib import Path from typing import Any import yaml from dotenv import load_dotenv PROJECT_ROOT = Path(__file__).resolve().parents[2] CONFIG_PATH = PROJECT_ROOT / "llm.config.yaml" # .env в корне репозитория (секреты не коммитить) load_dotenv(PROJECT_ROOT / ".env") def _deep_get(data: dict[str, Any], *keys: str, default: Any = None) -> Any: node: Any = data for key in keys: if not isinstance(node, dict): return default node = node.get(key) if node is None: return default return node @lru_cache(maxsize=1) def load_config() -> dict[str, Any]: if not CONFIG_PATH.exists(): return {} with CONFIG_PATH.open(encoding="utf-8") as fh: return yaml.safe_load(fh) or {} class Settings: """Настройки интеграции LLM и временного локального RAG.""" @property def project_root(self) -> Path: return PROJECT_ROOT @property def llm_enabled(self) -> bool: env = os.getenv("VKUS_LLM_ENABLED") if env is not None: return env.lower() in ("1", "true", "yes") return bool(_deep_get(load_config(), "provider", "enabled", default=False)) @property def llm_base_url(self) -> str: # Полный URL из .env (OpenRouter и др.) — нормализуем до base api_url = os.getenv("VKUS_LLM_API_URL") if api_url: url = api_url.rstrip("/") if url.endswith("/v1/chat/completions"): return url[: -len("/v1/chat/completions")] return url return os.getenv( "VKUS_LLM_BASE_URL", _deep_get(load_config(), "api", "base_url", default="https://apiai.sibur.local"), ) @property def llm_chat_url(self) -> str: """Полный URL для chat/completions.""" api_url = os.getenv("VKUS_LLM_API_URL") if api_url: return api_url.rstrip("/") return f"{self.llm_base_url.rstrip('/')}/v1/chat/completions" @property def llm_model(self) -> str: return os.getenv( "VKUS_LLM_MODEL", _deep_get(load_config(), "llm", "model", default="gpt-oss-120b"), ) @property def llm_api_key(self) -> str | None: key_name = _deep_get(load_config(), "auth", "api_key_env", default="VKUS_LLM_API_KEY") return os.getenv(key_name or "VKUS_LLM_API_KEY") or None @property def llm_jwt_token(self) -> str | None: key_name = _deep_get(load_config(), "auth", "jwt_token_env", default="VKUS_LLM_JWT_TOKEN") return os.getenv(key_name or "VKUS_LLM_JWT_TOKEN") or None @property def llm_timeout(self) -> float: env = os.getenv("VKUS_LLM_TIMEOUT_SEC") if env: return float(env) return float(_deep_get(load_config(), "api", "timeout_seconds", default=60)) @property def llm_verify_ssl(self) -> bool: return bool(_deep_get(load_config(), "api", "verify_ssl", default=True)) @property def llm_temperature(self) -> float: env = os.getenv("VKUS_LLM_TEMPERATURE") if env: return float(env) return float(_deep_get(load_config(), "llm", "parameters", "temperature", default=0.3)) @property def llm_max_tokens(self) -> int: env = os.getenv("VKUS_LLM_MAX_TOKENS") if env: return int(env) return int(_deep_get(load_config(), "llm", "parameters", "max_tokens", default=2048)) @property def system_prompt_path(self) -> Path: rel = _deep_get( load_config(), "llm", "system_prompt", "file", default="Start data/Системный промпт - правила общения с пользователем.md", ) return PROJECT_ROOT / str(rel) @property def system_prompt_fallback(self) -> str: return str( _deep_get( load_config(), "llm", "system_prompt", "fallback", default="Ты — корпоративный ИТ-ассистент портала ВКУС.", ) ) @property def confidence_threshold(self) -> int: return int(_deep_get(load_config(), "assistant", "confidence_threshold", default=70)) # --- Временный локальный RAG (внутри backend) --- @property def rag_mode(self) -> str: """local — временный RAG в backend; external — целевая платформа ИИ (будущее).""" return str(_deep_get(load_config(), "rag", "mode", default="local")) @property def rag_local_enabled(self) -> bool: env = os.getenv("VKUS_RAG_LOCAL_ENABLED") if env is not None: return env.lower() in ("1", "true", "yes") return bool(_deep_get(load_config(), "rag", "local", "enabled", default=True)) @property def rag_documents_path(self) -> Path: rel = _deep_get(load_config(), "rag", "local", "documents_path", default="Manuals") return PROJECT_ROOT / str(rel) @property def rag_top_k(self) -> int: return int(_deep_get(load_config(), "rag", "local", "top_k", default=5)) @property def rag_min_score(self) -> float: return float(_deep_get(load_config(), "rag", "local", "min_score", default=0.12)) @property def rag_chunk_size(self) -> int: return int(_deep_get(load_config(), "rag", "local", "chunk_size", default=450)) @property def use_pattern_kb(self) -> bool: return bool(_deep_get(load_config(), "fallback", "use_local_knowledge_base", default=True)) @property def use_template_on_llm_failure(self) -> bool: return bool(_deep_get(load_config(), "fallback", "use_template_on_llm_failure", default=True)) settings = Settings()