/
YoungFreddy
/
NetologyPipeLineService
Обзор
Документация
Войти
/
YoungFreddy
/
NetologyPipeLineService
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
config/loader.py
164 строки
6 KB
Igor
Initial commit: LLM сервис с FastAPI, pipeline, ретраями, fallback, кэшем
18 июл 2026, 17:01
18 июл 2026, 17:01
5b26a48
Код
Авторство
О чём код?
"""Загрузчик конфигурации: читает config.json/.yaml и .env, валидирует значения.""" import json import os import re from pathlib import Path from typing import Any, Dict, Optional CONFIG_DIR = Path(__file__).parent.resolve() DEFAULT_CONFIG_PATH = CONFIG_DIR / "config.json" YAML_CONFIG_PATH = CONFIG_DIR / "config.yaml" YML_CONFIG_PATH = CONFIG_DIR / "config.yml" def _resolve_env_vars(value: Any) -> Any: """Заменяет ${VAR_NAME} на значение переменной окружения.""" if isinstance(value, str): def replace_var(match): var_name = match.group(1) return os.environ.get(var_name, match.group(0)) return re.sub(r"\$\{(\w+)\}", replace_var, value) if isinstance(value, dict): return {k: _resolve_env_vars(v) for k, v in value.items()} if isinstance(value, list): return [_resolve_env_vars(v) for v in value] return value def load_json_config(path: str) -> Optional[Dict[str, Any]]: """Загружает конфигурацию из JSON файла.""" try: with open(path, "r", encoding="utf-8") as f: return json.load(f) except Exception: return None def load_yaml_config(path: str) -> Optional[Dict[str, Any]]: """Загружает конфигурацию из YAML файла.""" try: import yaml with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) except Exception: return None def _flatten_config(cfg: dict, prefix: str = "") -> dict: """Разворачивает вложенные ключи: {api:{key:x}} -> {api_key:x}.""" result = {} for key, value in cfg.items(): flat_key = f"{prefix}{key}" if prefix else key if isinstance(value, dict): result.update(_flatten_config(value, f"{flat_key}_")) else: result[flat_key] = value return result def _validate_positive_int(value: Any, name: str) -> int: """Проверяет, что значение — положительное целое.""" try: v = int(value) except (TypeError, ValueError): raise ValueError(f"{name} must be a positive integer, got {value!r}") if v <= 0: raise ValueError(f"{name} must be positive, got {v}") return v def _validate_float_range(value: Any, name: str, lo: float, hi: float) -> float: """Проверяет, что значение — число в диапазоне [lo, hi].""" try: v = float(value) except (TypeError, ValueError): raise ValueError(f"{name} must be a number, got {value!r}") if not (lo <= v <= hi): raise ValueError(f"{name} must be between {lo} and {hi}, got {v}") return v def load_config() -> Dict[str, Any]: """Загружает конфигурацию из config.json (или .yaml) с переопределением из .env. Приоритет (выше — переопределяет): 1. config/config.json (базовая) 2. config/config.yaml (если есть) 3. Переменные окружения LLM_* (наивысший приоритет) Поддерживает ${VAR_NAME} подстановку в JSON/YAML. Валидирует api_timeout, max_retries, retry_delay_base, temperature. """ config: Dict[str, Any] = {} config_paths = [DEFAULT_CONFIG_PATH, YAML_CONFIG_PATH, YML_CONFIG_PATH] for path in config_paths: if path.suffix in (".json",): loaded = load_json_config(str(path)) else: loaded = load_yaml_config(str(path)) if loaded: flat = _flatten_config(loaded) config.update(flat) break config = _resolve_env_vars(config) env_prefix = "LLM_" env_overrides = { "api_key": os.environ.get(f"{env_prefix}API_KEY"), "model": os.environ.get(f"{env_prefix}MODEL"), "temperature": os.environ.get(f"{env_prefix}TEMPERATURE"), "max_tokens": os.environ.get(f"{env_prefix}MAX_TOKENS"), "cache_enabled": os.environ.get(f"{env_prefix}CACHE_ENABLED"), "api_timeout": os.environ.get(f"{env_prefix}API_TIMEOUT"), "max_retries": os.environ.get(f"{env_prefix}MAX_RETRIES"), "retry_delay_base": os.environ.get(f"{env_prefix}RETRY_DELAY_BASE"), "cache_ttl": os.environ.get(f"{env_prefix}CACHE_TTL"), "system_prompt": os.environ.get(f"{env_prefix}SYSTEM_PROMPT"), "fallback_response": os.environ.get(f"{env_prefix}FALLBACK_RESPONSE"), "api_base": os.environ.get(f"{env_prefix}API_BASE"), } for key, val in env_overrides.items(): if val is not None: if key in ("temperature", "api_timeout", "retry_delay_base"): config[key] = float(val) elif key in ("max_tokens", "max_retries", "cache_ttl"): config[key] = int(val) elif key == "cache_enabled": config[key] = val.lower() == "true" else: config[key] = val config.setdefault("model", "gpt-3.5-turbo") config.setdefault("temperature", 0.7) config.setdefault("max_tokens", 1000) config.setdefault("api_timeout", 30.0) config.setdefault("max_retries", 3) config.setdefault("retry_delay_base", 1.0) config.setdefault("cache_ttl", 600) config.setdefault("cache_enabled", True) config.setdefault("system_prompt", "Ты — полезный ассистент. Ответь кратко.") config.setdefault( "fallback_response", "Сервис временно недоступен, попробуйте позже" ) config.setdefault("api_base", "https://api.openai.com/v1") config["max_retries"] = _validate_positive_int(config["max_retries"], "max_retries") config["max_tokens"] = _validate_positive_int(config["max_tokens"], "max_tokens") config["cache_ttl"] = _validate_positive_int(config["cache_ttl"], "cache_ttl") config["api_timeout"] = _validate_float_range( config["api_timeout"], "api_timeout", 0.1, 300.0 ) config["retry_delay_base"] = _validate_float_range( config["retry_delay_base"], "retry_delay_base", 0.01, 60.0 ) config["temperature"] = _validate_float_range( config["temperature"], "temperature", 0.0, 2.0 ) return config