fix(agent): 하드코딩 '재원부족' 와일드카드 제거

_pick_wildcard 가 실제 선택 와일드카드와 매핑되지 않은 채 '와일드카드를
하나라도 골랐으면' 조건만으로 wild_card_budget(재원부족)을 반환해, 선택하지
않은 재원부족 멘트가 노출되는 오작동이 있었다.

- 재원부족 분기 삭제 → 해당 구간은 일반 가격협상으로 폴백. 와일드카드는
  앵커<제시가≤앵커*1.02 구간의 1% 인하(wild_card_1pct)만 발동한다.
- wildcard_used 마킹을 1% 분기 안으로 이동 — 가격협상으로 돌아갈 때도
  마킹하면 이후 라운드의 정당한 1% 카드까지 억제되는 부작용 방지.
- test_p7_chat: 와일드카드 발동 단언을 wild_card_1pct 로 교체.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
민헌 2026-07-07 18:10:45 +09:00
parent 98df5e2608
commit c2760c804f
2 changed files with 17 additions and 12 deletions

View File

@ -173,20 +173,24 @@ class ChatEngine:
return "가격협상" return "가격협상"
def _pick_wildcard(self, session: ChatSession) -> str: def _pick_wildcard(self, session: ChatSession) -> str:
"""앵커가 살짝 초과 구간에서 인하 압박 카드 선택. """앵커가에 아주 근접(≤ anchor*1.02)한 구간에서만 1% 인하 요청(wild_card_1pct)으로
- 앵커가에 아주 근접(≤ anchor*1.02): 1% 인하 요청(wild_card_1pct) → 앵커가 이하로 유도. 앵커가 이하로 유도한다. 그 외 구간은 일반 가격협상(카드 플레이)으로 돌린다.
- 그 외: 목표 매입가 맞춰달라(wild_card_budget).
과거 여기서 반환하던 '재원부족'(wild_card_budget) 하드코딩 카드는 제거했다 —
견적에서 실제 선택한 와일드카드(중간값 절충·목표가 선제안 등)와 매핑되지 않은 채
'와일드카드를 하나라도 골랐으면' 조건만으로 발동해, 선택하지도 않은 재원부족 멘트가
노출되는 오작동이 있었다.
""" """
ctx = session.context ctx = session.context
price = ctx.get("input_price", 0) price = ctx.get("input_price", 0)
anchor = ctx.get("anchor_price", 0) anchor = ctx.get("anchor_price", 0)
ctx["wildcard_used"] = True
if anchor > 0 and price <= anchor * 1.02: if anchor > 0 and price <= anchor * 1.02:
# 와일드카드는 실제로 노출할 때만 '사용됨'으로 마킹한다 — 가격협상으로 돌아가는
# 경우에도 마킹하면 이후 라운드에서 정당한 1% 카드까지 억제된다.
ctx["wildcard_used"] = True
ctx["offer_1pct"] = int(round(price * 0.99)) # 1% 인하가 ctx["offer_1pct"] = int(round(price * 0.99)) # 1% 인하가
return "wild_card_1pct" return "wild_card_1pct"
if not ctx.get("allow_selected_wildcards", True): return "가격협상"
return "가격협상"
return "wild_card_budget"
# ---- render -------------------------------------------------------- # ---- render --------------------------------------------------------
def vars_for(self, session: ChatSession) -> Dict[str, Any]: def vars_for(self, session: ChatSession) -> Dict[str, Any]:

View File

@ -2,7 +2,7 @@
1. 전체 대화: 서비스안내→담당자확인→협상품목안내→가격협상→와일드카드→협상완료→협상종료(chat_end). 1. 전체 대화: 서비스안내→담당자확인→협상품목안내→가격협상→와일드카드→협상완료→협상종료(chat_end).
2. 가격협상 턴에서 카드 선택 + 학습(card_id, updated_q). 2. 가격협상 턴에서 카드 선택 + 학습(card_id, updated_q).
3. 와일드카드 발동(wild_card_budget) + 종료 보상(success). 3. 와일드카드 발동(wild_card_1pct) + 종료 보상(success).
4. 브랜드 치환 테넌트별(데모상사 A/B), 클린룸(스크립트에 KT 흔적 없음). 4. 브랜드 치환 테넌트별(데모상사 A/B), 클린룸(스크립트에 KT 흔적 없음).
5. 경험로그 적재 + 종료 후 진행 시 에러. 5. 경험로그 적재 + 종료 후 진행 시 에러.
6. (HTTP) 헤더로 새 세션 시작 + 한 턴 진행. 6. (HTTP) 헤더로 새 세션 시작 + 한 턴 진행.
@ -46,8 +46,9 @@ async def test_full_conversation_reaches_completion(db_engine):
reset_sessions() reset_sessions()
eng = await _reg().get_engine("ktcommerce") eng = await _reg().get_engine("ktcommerce")
svc = ChatService() svc = ChatService()
# anchor=9900, target=10000(기본). 11000(>10395)→가격협상(카드), 10200(9900~10395)→와일드카드, 9800(≤anchor)→우선협상 # anchor=9900, target=10000(기본). 11000(>anchor*1.02)→가격협상(카드),
turns = [None, "확인", "예", "확인", "11000", "예", "10200", "예", "9800", "예", # 10000(anchor<p≤anchor*1.02)→1% 와일드카드→수락하면 협상완료.
turns = [None, "확인", "예", "확인", "11000", "예", "10000", "예", "예",
"협상 내용을 확인했으며, 이의가 없음에 동의합니다."] "협상 내용을 확인했으며, 이의가 없음에 동의합니다."]
out = await _run(svc, eng, turns) out = await _run(svc, eng, turns)
steps = [r.step for r in out] steps = [r.step for r in out]
@ -61,8 +62,8 @@ async def test_full_conversation_reaches_completion(db_engine):
assert nego and nego[0].card_id and nego[0].card_id.startswith("NGC-A") assert nego and nego[0].card_id and nego[0].card_id.startswith("NGC-A")
assert nego[0].updated_q is not None assert nego[0].updated_q is not None
# 와일드카드 발동 # 와일드카드 발동 (1% 인하)
assert any(r.step == "wild_card_budget" for r in out) assert any(r.step == "wild_card_1pct" for r in out)
# 종료 보상(success) # 종료 보상(success)
done = [r for r in out if r.outcome == "success"] done = [r for r in out if r.outcome == "success"]