"""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})