86 lines
3.5 KiB
Python
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)
|