"""NegotiationSnapshot — 한 협상 의사결정 시점의 관측치 (우리 자체 스키마). 이산 상태(state_index)와 연속 feature 가 한 곳에 공존한다. experience_logs.snapshot(JSON)에 저장되어 3개 알고리즘(Q-Table/LinUCB/Offline RL)이 같은 데이터를 공유한다(계획서 핵심통찰). 클린룸: 필드 구성은 우리 설계다. 상태 산출에 필요한 관측치 + reward 계산 입력을 담는다. """ from dataclasses import dataclass, asdict from enum import Enum, IntEnum from typing import Any, Dict, Optional class NegotiationOutcome(str, Enum): """협상 라운드 결과 (reward 계산 입력).""" ONGOING = "ongoing" SUCCESS = "success" FAILURE = "failure" class PartnerType(IntEnum): """파트너사 유형 — 상품 하나를 취급하는 협력사의 경쟁 구조. 상품별 협력사 수 DB 조회(NegotiationContextLoader)로 세션 시작 시 확정한다: 없음=NONE(0), 하나=SINGLE(1), 여러 곳=MULTIPLE(2). 값이 협력사 수와 호환되도록 설계됨(0/1/≥2) — snapshot.partner_count 로 그대로 흘러 state 버킷(_partner_bucket)과 W 가중치 계산에 쓰인다. """ NONE = 0 SINGLE = 1 MULTIPLE = 2 @classmethod def from_count(cls, count: int) -> "PartnerType": """협력사 수 → 유형. 0=NONE, 1=SINGLE, 2 이상=MULTIPLE.""" if count <= 0: return cls.NONE if count == 1: return cls.SINGLE return cls.MULTIPLE @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})