o2o-negosium-original/agent/eval_harness/buyer.py

80 lines
3.7 KiB
Python

"""시뮬레이션 구매자 (계획서 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]]