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

180 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""TenantConfig — 테넌트별 협상 설정 단일 소스 (계획서 설계 A).
Chat_server 에서 23곳에 하드코딩돼 있던 KT 가정(state 임계값·가중치·유통코드, reward,
policy, 카드 매핑, LLM 배포)을 한 pydantic 모델로 외부화한다. 도메인 코드는 이 config 를
주입받아 tenant-agnostic 하게 동작한다(P2).
저장 위치 (YAML + DB 하이브리드, 계획서 A):
- YAML(tenants/<id>/tenant.yaml, git 형상관리): state·reward·policy·language·resource 경로.
- env/시크릿(llm.api_key_ref): LLM api_key (YAML 평문 금지).
- DB: action_to_card 매핑(P6 tenant_action_cards), 활성 Q-Table 버전 포인터(P5).
테넌트 식별자는 company.companies.company_id(uuid)에 매핑된다. 공유 베이스는 예약어 "_base".
"""
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
# ---- state: 이산 상태 차원 정의 -------------------------------------------
# 모든 기본값은 우리 플랫폼의 중립 데모 기본값이다(CLEANROOM.md). 특정 고객의 튜닝값을
# 복제하지 않으며, 실제 운영값은 테넌트 YAML/DB 에서 주입한다. 차원 구성(개수)은 기능적
# 설계이고, 값/라벨은 우리 자체 선택이다.
class RevenueConfig(BaseModel):
"""매출 가격구간. thresholds[i] 이하이면 i번째 구간. 마지막 구간은 초과분."""
thresholds: List[float] = [10_000_000, 50_000_000] # 플랫폼 중립 기본값
weights: List[float] = [0.3, 0.6, 1.0]
descriptions: List[str] = ["low", "mid", "high"]
class DistributionConfig(BaseModel):
"""유통 구조. code_map: 테넌트 외부 코드 → 구간 인덱스(테넌트가 자사 코드로 정의)."""
code_map: Dict[str, int] = {"A": 0, "B": 1, "C": 2} # 중립 예시 코드(테넌트가 오버라이드)
weights: List[float] = [0.3, 0.6, 1.0]
descriptions: List[str] = ["channel_a", "channel_b", "channel_c"]
class PartnerConfig(BaseModel):
"""파트너사 수 → 구간. count==0:none, ==1:single, >=2:multiple.
인덱스 규약: SINGLE=0, MULTIPLE=1, NONE=2.
"""
weights: List[float] = [0.5, 1.0, 0.3] # single, multiple, none
descriptions: List[str] = ["single", "multiple", "none"]
class AcceptanceConfig(BaseModel):
"""가격 수용률 구간(0~1). < thresholds[0]: low, <= thresholds[1]: mid, else high."""
thresholds: List[float] = [0.03, 0.09]
weights: List[float] = [0.3, 0.6, 1.0]
descriptions: List[str] = ["low", "mid", "high"]
class PriceZoneConfig(BaseModel):
"""입력가격 구간 (KT 구매자: anchor=협력사 기준가 ≥ target=목표 매입가).
zone0: 제시가 ≤ anchor(우선협상 가능), zone1: > anchor(협상 지속)."""
weights: List[float] = [1.0, 0.5] # at_or_below_anchor, above_anchor
descriptions: List[str] = ["at_or_below_anchor", "above_anchor"]
class StateConfig(BaseModel):
revenue: RevenueConfig = Field(default_factory=RevenueConfig)
distribution: DistributionConfig = Field(default_factory=DistributionConfig)
partner: PartnerConfig = Field(default_factory=PartnerConfig)
acceptance: AcceptanceConfig = Field(default_factory=AcceptanceConfig)
price_zone: PriceZoneConfig = Field(default_factory=PriceZoneConfig)
@property
def state_space_size(self) -> int:
"""차원 곱으로 자동 산출 (KT: 3×3×3×3×2 = 162). 회사별 차원이 다르면 값이 달라진다.
→ warm-start 차원 호환 체크의 기준이 된다(P5).
"""
revenue_dim = len(self.revenue.weights)
dist_dim = len(self.distribution.weights)
partner_dim = len(self.partner.weights)
accept_dim = len(self.acceptance.weights)
price_dim = len(self.price_zone.weights)
return revenue_dim * dist_dim * partner_dim * accept_dim * price_dim
# ---- reward: 보상 공식 파라미터 (공식 형태는 기능적, 값은 우리 자체 중립 기본값) ----------
class RewardConfig(BaseModel):
"""보상 계산 파라미터. 필드 구성은 P2 RewardCalculator 주입용이며,
기본값은 플랫폼 중립값(균등 가중치)이다 — 특정 고객 튜닝값 복제 아님(CLEANROOM.md).
"""
beta: float = 0.2
success_reward: float = 1.0
ongoing_reward: float = 0.0
failure_penalty: float = -0.5
penalty_lambda: float = 0.02
# 동적 가중치 기본값: 균등 분배(0.2×5). 테넌트가 자사 특성에 맞게 오버라이드.
w1: float = 0.2
w2: float = 0.2
w3: float = 0.2
w4: float = 0.2
w5: float = 0.2
min_weight: float = 0.2
max_weight: float = 0.8
# ---- policy / cards / llm / resources / action_mapping ----------------------
class ActionMappingConfig(BaseModel):
"""action_id ↔ card_id 매핑. PoC 는 카드 매핑 고정(차원 정합성 리스크 회피)."""
type: str = "file" # "file" | "db"(P6 tenant_action_cards)
action_to_card: Dict[str, str] = {}
@property
def action_space_size(self) -> int:
return len(self.action_to_card)
class PolicyConfig(BaseModel):
"""정책 종류 + 하이퍼파라미터. params 는 알고리즘별 불투명 dict(LinUCB/CQL 등)."""
type: str = "ucb" # ucb | linucb | cql (eval_harness registry 키)
learning_rate: float = 0.1
gamma: float = 0.95 # discount_factor
params: Dict[str, Any] = {"exploration_constant": 1.4142135623730951, "epsilon": 1e-6}
class CardsConfig(BaseModel):
source_type: str = "file" # file | backoffice_db (card.* 스키마)
sync_interval_seconds: int = 300
connection: Dict[str, Any] = {}
class LlmConfig(BaseModel):
enabled: bool = False
endpoint: Optional[str] = None
deployment: Optional[str] = None
api_version: Optional[str] = None
api_key_ref: Optional[str] = None # env 변수명 (평문 금지). P7 에서 LlmCredentials 로 해석.
class NegotiationConfig(BaseModel):
"""협상 가격 정책. 앵커링값은 목표가에서 자동 산출한다(KT 구매자: anchor < target).
anchor = round(target * (1 - anchor_rate)). 예) target=10000, rate=0.01 → anchor=9900.
협력사 제시가 ≤ anchor → 우선협상.
"""
anchor_rate: float = 0.01 # 목표가 대비 앵커링 인하율 (기본 1%)
max_rounds: int = 5 # 라운드 상한(보조). 실제 종료는 '카드 소진' 기준.
def anchor_for(self, target_price: float) -> float:
return round(target_price * (1.0 - self.anchor_rate))
class ResourcesConfig(BaseModel):
language: str = "ko"
scripts_dir: str = "resources" # tenants/<id>/resources/ (없으면 _base/resources/ 폴백)
variable_mapping: Optional[str] = None
# 스크립트 템플릿의 브랜드 치환값 (클린룸: 특정사 브랜드 대신 테넌트별 주입).
company_name: str = "당사"
service_name: str = "Negosium"
class TenantConfig(BaseModel):
"""한 테넌트의 협상 설정 전체."""
tenant_id: str
company_id: Optional[str] = None # company.companies.company_id (uuid). _base 는 None.
name: str = ""
inherits_base: bool = True
state: StateConfig = Field(default_factory=StateConfig)
reward: RewardConfig = Field(default_factory=RewardConfig)
negotiation: NegotiationConfig = Field(default_factory=NegotiationConfig)
action_mapping: ActionMappingConfig = Field(default_factory=ActionMappingConfig)
policy: PolicyConfig = Field(default_factory=PolicyConfig)
cards: CardsConfig = Field(default_factory=CardsConfig)
llm: LlmConfig = Field(default_factory=LlmConfig)
resources: ResourcesConfig = Field(default_factory=ResourcesConfig)