o2o-negosium-original/agent/tenancy/config.py
hbyang be1b4b968f [fix] agent: 보상함수를 q-table 상세 설명 v4 명세에 정합화
- 동적 가중치 W: 라운드 감쇠 → 상태 5차원 가중합 clip(Σwᵢ·Sᵢ, 0.2, 0.8) (식 12~13, 기존 w1~w5 연결)
- 종료보상에 (1−W) 적용: R = W×R_price + (1−W)×R_end − λ×round (식 8)
- R_price 3단계: P<anchor 시 1+β·(anchor−P)/anchor 초과달성 보너스 추가 (식 9~11, beta 의미 재정의)
- price zone 경계는 명세(T)와 달리 anchor 유지(우선협상 규칙이 실제 의사결정 경계) — 사유 docstring 명시
- state_calculator/config 의 낡은 반대 컨벤션(anchor≥target) 주석 정정
- RewardCalculator(RewardConfig, StateConfig) 시그니처 변경 + 호출부 5곳 갱신, 테스트 기대값 정정 (76/76 PASS)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:10:44 +09:00

180 lines
7.6 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 # 앵커 초과달성 보너스 계수 (명세 v4 식(9): P<anchor 시 1+β·(anchor−P)/anchor)
success_reward: float = 1.0
ongoing_reward: float = 0.0
failure_penalty: float = -0.5
penalty_lambda: float = 0.02
# 동적 가중치 W 의 메타 가중치 (명세 v4 식(12): W_raw = Σ wᵢ·Sᵢ). 균등 기본값, 테넌트 오버라이드.
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)