"""완전 자율 협상 행동 공간 (numpy 전용 — 학습(tools)과 서빙(policy)이 공유). 카드 카탈로그 대신 행동의 '의미'만 남긴다: ACCEPT 현재 제시가로 타결 WALK 협상 결렬 선언 COUNTER(q, s) "C원이면 수락" 역제안. C = anchor + q×(target−anchor), s = 화법 전략 PRESS(s) 설득 압박 (카드의 일반화 — 전략 1경쟁/2수용/3고수/4협력) 특징 벡터(ACTION_DIM=8) = 유형 one-hot(3) + 가격 위치(1) + 전략 one-hot(4). ScoreNet(상태 + 행동특징) → 스칼라 점수로 후보 30개를 채점해 argmax 한다. """ from dataclasses import dataclass import numpy as np COUNTER_GRID = [-0.05, 0.0, 0.25, 0.5, 0.75, 1.0] # C = anchor + q×(target−anchor) ACTION_DIM = 3 + 1 + 4 + 1 # 유형(3) + 위치(1) + 전략(4) + 컷폭(1: 현 제시가 대비 인하 요구율) # 자율 전용 추가 상태 (v3): # [0] 직전 역제안 존재(0/1) [1] 직전 역제안 위치 q ← 에피소드 기억(같은 숫자 반복 방지) # [2] 마감 잔여율(남은시간/전체, 미상 0.5) ← 견적 마감(quotations.end_time) # [3] 과거 협상 횟수 min(n,5)/5 [4] 과거 성사율(미상 0.5) # [5] 과거 평균 타결수준 norm((타결가/목표가−0.8)/0.4, 미상 0.5) ← 이 협력사와의 이력(experience_logs) # [6] 인터넷최저가 갭 clip((최저가−앵커)/앵커/0.1, ±1, 미상 0) ← 숨은 하한가의 관측 가능한 힌트 # 특징은 학습 시뮬에도 동일하게 존재해야 한다(train_full_autonomy 가 대응물을 생성). EXTRA_STATE_DIM = 7 def extra_state(last_kind: str = "", last_q: float = 0.0, deadline: float = 0.5, hist_n: float = 0.0, hist_success: float = 0.5, hist_settle: float = 0.5, internet_gap: float = 0.0) -> np.ndarray: has_counter = 1.0 if last_kind == "counter" else 0.0 return np.array([ has_counter, float(np.clip(last_q, -1.0, 1.0)) * has_counter, float(np.clip(deadline, 0.0, 1.0)), float(np.clip(hist_n, 0.0, 1.0)), float(np.clip(hist_success, 0.0, 1.0)), float(np.clip(hist_settle, 0.0, 1.0)), float(np.clip(internet_gap, -1.0, 1.0)), ], dtype=np.float32) def settle_norm(avg_settle_ratio: float) -> float: """평균 (타결가/목표가) → 0~1 정규화 (0.8→0, 1.0→0.5, 1.2→1).""" return float(np.clip((avg_settle_ratio - 0.8) / 0.4, 0.0, 1.0)) def internet_gap_feat(internet_lowest: float, anchor: float) -> float: """인터넷최저가의 앵커 대비 갭 (±10% 스케일). 최저가 없으면 0을 쓴다.""" if not internet_lowest or anchor <= 0: return 0.0 return float(np.clip((internet_lowest - anchor) / anchor / 0.1, -1.0, 1.0)) @dataclass(frozen=True) class Action: kind: str # accept | walk | counter | press counter_q: float = 0.0 # counter 위치 (anchor~target 스팬 비율) strategy: int = 0 # press/counter 의 화법 전략 (1~4, 0=없음) def feat(self, price_pos: float, cut: float = 0.0) -> np.ndarray: """cut: 이 행동이 요구하는 인하폭 (현 제시가 대비, counter 만 >0) — 대형컷의 무례함을 정책이 지각하게 한다. 갭이 크면 역제안 대신 압박이 낫다는 걸 배우는 근거 특징.""" t = {"accept": [1, 0, 0], "walk": [0, 1, 0]}.get(self.kind, [0, 0, 1]) pos = price_pos if self.kind == "accept" else self.counter_q s = np.zeros(4, dtype=np.float32) if self.strategy: s[self.strategy - 1] = 1.0 return np.concatenate([np.array(t, dtype=np.float32), np.array([float(np.clip(pos, -1.0, 2.0)), ], dtype=np.float32), s, np.array([float(np.clip(cut, 0.0, 1.0))], dtype=np.float32)]) def candidate_actions(): """전 행동 후보: 수락 1 + 결렬 1 + 역제안 6×전략4 + 압박 4 = 30.""" out = [Action("accept"), Action("walk")] out += [Action("counter", q, s) for q in COUNTER_GRID for s in (1, 2, 3, 4)] out += [Action("press", 0.0, s) for s in (1, 2, 3, 4)] return out ACTIONS = candidate_actions()