o2o-negosium-original/agent/negotiation/qtable/domain/model/snapshot.py

50 lines
2.0 KiB
Python

"""NegotiationSnapshot — 한 협상 의사결정 시점의 관측치 (우리 자체 스키마).
이산 상태(state_index)와 연속 feature 가 한 곳에 공존한다. experience_logs.snapshot(JSON)에
저장되어 3개 알고리즘(Q-Table/LinUCB/Offline RL)이 같은 데이터를 공유한다(계획서 핵심통찰).
클린룸: 필드 구성은 우리 설계다. 상태 산출에 필요한 관측치 + reward 계산 입력을 담는다.
"""
from dataclasses import dataclass, asdict
from enum import Enum
from typing import Any, Dict, Optional
class NegotiationOutcome(str, Enum):
"""협상 라운드 결과 (reward 계산 입력)."""
ONGOING = "ongoing"
SUCCESS = "success"
FAILURE = "failure"
@dataclass
class NegotiationSnapshot:
# --- 이산 상태 산출 입력 ---
revenue_amount: float # 매출액(원)
distribution_code: str # 유통 구조 외부 코드 (테넌트 code_map 으로 해석)
partner_count: int # 파트너사 수
acceptance_ratio: float # 가격 수용률 (0~1)
input_price: float # 현재 제시/입력 가격
anchor_price: float # 앵커(시작) 가격
target_price: float # 목표 가격
# --- 시퀀스/보상 컨텍스트 ---
round_number: int = 0 # 협상 라운드(turn)
outcome: NegotiationOutcome = NegotiationOutcome.ONGOING
def to_dict(self) -> Dict[str, Any]:
d = asdict(self)
d["outcome"] = self.outcome.value
return d
@classmethod
def from_dict(cls, d: Dict[str, Any]) -> "NegotiationSnapshot":
d = dict(d)
outcome = d.get("outcome", NegotiationOutcome.ONGOING.value)
d["outcome"] = NegotiationOutcome(outcome) if not isinstance(outcome, NegotiationOutcome) else outcome
# 알 수 없는 키는 무시(스키마 진화 내성).
allowed = cls.__dataclass_fields__.keys()
return cls(**{k: v for k, v in d.items() if k in allowed})