o2o-negosium-original/agent/tenancy/config_loader.py

86 lines
3.5 KiB
Python

"""TenantConfig 로더 (계획서 A: YAML → env 주입 → DB 오버레이 merge, inherits_base deep-merge, TTL 캐시).
P1 범위: YAML 로드 + _base deep-merge + TTL 캐시.
DB 오버레이(action_to_card, 활성 Q-Table 버전 포인터)는 P5/P6 에서 추가한다.
env 주입(llm.api_key_ref → 실제 키)은 P7 에서 LlmCredentials 해석으로 처리한다.
"""
import os
import time
from typing import Any, Dict, Optional
import yaml
from common.logger import LOG
from config.server_configs import agent_config
from tenancy.config import TenantConfig
_BASE_TENANT_ID = "_base"
def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
"""override 를 base 위에 재귀 병합. dict 는 키 단위 병합, 그 외(리스트/스칼라)는 override 우선.
리스트는 통째로 교체한다(임계값/가중치 배열은 부분 병합이 의미 없으므로).
"""
result = dict(base)
for key, ov in override.items():
bv = result.get(key)
if isinstance(bv, dict) and isinstance(ov, dict):
result[key] = _deep_merge(bv, ov)
else:
result[key] = ov
return result
class TenantConfigLoader:
"""tenants/<id>/tenant.yaml 을 읽어 TenantConfig 로 만든다. TTL 캐시."""
def __init__(self, tenants_dir: Optional[str] = None, cache_ttl_seconds: Optional[int] = None):
self._tenants_dir = tenants_dir or agent_config.tenants_dir
self._ttl = agent_config.config_cache_ttl_seconds if cache_ttl_seconds is None else cache_ttl_seconds
self._cache: Dict[str, tuple[float, TenantConfig]] = {}
def _yaml_path(self, tenant_id: str) -> str:
return os.path.join(self._tenants_dir, tenant_id, "tenant.yaml")
def _read_yaml(self, tenant_id: str) -> Dict[str, Any]:
path = self._yaml_path(tenant_id)
if not os.path.exists(path):
raise FileNotFoundError(f"tenant.yaml 없음: {path} (tenant_id={tenant_id})")
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def is_registered(self, tenant_id: str) -> bool:
return os.path.exists(self._yaml_path(tenant_id))
def load(self, tenant_id: str, use_cache: bool = True) -> TenantConfig:
if use_cache and self._ttl > 0:
hit = self._cache.get(tenant_id)
if hit and (time.monotonic() - hit[0]) < self._ttl:
return hit[1]
raw = self._read_yaml(tenant_id)
# inherits_base 이면 _base 를 deep-merge 한다 (자기 자신이 _base 면 스킵).
inherits = raw.get("inherits_base", True)
if inherits and tenant_id != _BASE_TENANT_ID and self.is_registered(_BASE_TENANT_ID):
base_raw = self._read_yaml(_BASE_TENANT_ID)
# _base 의 식별 필드는 병합하지 않는다.
base_raw = {k: v for k, v in base_raw.items() if k not in ("tenant_id", "company_id", "name")}
raw = _deep_merge(base_raw, raw)
raw.setdefault("tenant_id", tenant_id)
config = TenantConfig.model_validate(raw)
if self._ttl > 0:
self._cache[tenant_id] = (time.monotonic(), config)
LOG.i(f"[TenantConfigLoader] loaded tenant_id={tenant_id} state_space_size={config.state.state_space_size} action_space_size={config.action_mapping.action_space_size}")
return config
def invalidate(self, tenant_id: Optional[str] = None):
if tenant_id is None:
self._cache.clear()
else:
self._cache.pop(tenant_id, None)