"""시뮬레이션 구매자 (계획서 G, H5). PoC 본체의 핵심: **카드(action) 선택에 따라 협상 결과가 달라지는** 환경. 그래야 에이전트가 "어떤 카드가 좋은가"를 보상으로 학습할 수 있고, 학습 정책이 random 보다 성과가 오르는지 검증 가능. - HeuristicBuyer: LLM 없이 결정론적(seed)으로 동작. 각 카드에 숨은 효과(effectiveness)를 부여해 수락확률·타결가에 반영. 에이전트는 이를 모르고 보상으로만 추정한다. - LLMBuyer(옵션): Azure OpenAI 가상 구매자(페르소나/예산). 자격증명(llm.enabled) 있을 때만. """ from dataclasses import dataclass from typing import Dict, List, Optional import numpy as np @dataclass class Scenario: """KT 구매자 관점. anchor=협력사 기준가(높음) ≥ target=KT 목표 매입가(낮음).""" anchor_price: float # 협력사 기준가(앵커). 제시가 ≤ anchor → 우선협상 target_price: float # KT 목표 매입가(낮음). 낮게 타결할수록 보상↑ revenue_amount: float = 20_000_000 distribution_code: str = "A" partner_count: int = 1 acceptance_ratio: float = 0.05 @dataclass class BuyerResponse: accept: bool new_price: float # 협력사가 양보한 새 제시가(이번 턴 후) walked: bool = False # 협상 이탈(결렬) class HeuristicBuyer: """카드 효과 기반 결정론적 '협력사(판매자)' 모델 (KT 구매자가 상대). card_effectiveness[action] ∈ [0,1] 가 클수록: 협력사가 더 크게 양보(가격 인하)하고 수락확률↑ → KT 에게 유리(낮은 타결가). 효과는 비공개이며 에이전트는 보상으로만 추정. """ def __init__(self, card_effectiveness: Dict[int, float], seed: int = 0, accept_base: float = 0.10, accept_gain: float = 0.75, max_turns: int = 5): self.eff = card_effectiveness self.rng = np.random.default_rng(seed) self.accept_base = accept_base self.accept_gain = accept_gain self.max_turns = max_turns def reseed(self, seed: int): self.rng = np.random.default_rng(seed) def respond(self, action_id: int, scenario: Scenario, turn: int, current_price: float) -> BuyerResponse: eff = float(self.eff.get(action_id, 0.1)) # 협력사 양보: 효과 클수록 앵커가 쪽으로 더 많이 내려온다(KT 이득). 앵커가 이하까지 도달 가능. floor = scenario.anchor_price * 0.95 concession = (current_price - floor) * (0.15 + 0.55 * eff) new_price = max(floor, current_price - concession) # 수락(현 가격에 합의)확률: 효과 + 후반 라운드 압박. p_accept = min(0.97, self.accept_base + self.accept_gain * eff + 0.06 * (turn - 1)) accept = bool(self.rng.random() < p_accept) walked = (not accept) and (turn >= self.max_turns) return BuyerResponse(accept=accept, new_price=new_price, walked=walked) def make_card_effectiveness(action_space_size: int, seed: int = 0, n_good: int = 3) -> Dict[int, float]: """카드 효과 벡터 생성: n_good 개의 '좋은 카드'(0.75~0.95) + 나머지 약효(0.05~0.35).""" rng = np.random.default_rng(seed) eff = {a: float(rng.uniform(0.05, 0.35)) for a in range(action_space_size)} good = rng.choice(action_space_size, size=min(n_good, action_space_size), replace=False) for a in good: eff[int(a)] = float(rng.uniform(0.75, 0.95)) return eff def best_actions(card_effectiveness: Dict[int, float], k: int = 3) -> List[int]: return [a for a, _ in sorted(card_effectiveness.items(), key=lambda kv: -kv[1])[:k]]