o2o-negosium-original/agent/router/v1/chat/protocol.py
hbyang 1682481b01 [feat] agent: 협상 고도화 — LLM 표현/이해층 + 카드 전술 실행계층 + ktcommerce 정리
카드 재설계("멘트 카드 → 전술 카드"):
- tactics.py 신설: 카드번호→전술(카운터 산식) 레지스트리, min(counter,target) 클램프
- 카운터 수락=즉시 타결(pending_counter_price 일반화, 구 offer_1pct 흡수)
- 목표가 초과 타결 금지(성공스텝 진입 가드) + "카드 소진=실패" 폐지→종결 국면
- 선택형 와일드카드(WC-*) 발동 + card.wild_cards 멘트 DB 어댑터

LLM 계층:
- Phase 2 표현층 ScriptNaturalizer(카드 멘트 자연화, 마커·치환자·숫자 보존 검증)
- Phase 3 이해층 InputInterpreter(자유발화 NLU→기대입력, 한국어 가격 파서)
- OPENAI_API_KEY env override(server_configs) + 전역 자격증명 게이트

결정 스택(Phase 1):
- 협상 규칙 데이터화(negotiation.wildcard_*_ratio/max_counter_rounds)
- 선택카드 우선순위 prior(UCB 방문수 감쇠, Q-table 오염 없음)

버그픽스:
- 인하율 음수 표기 제거 + 인상/동일/인하 구분(discount_phrase)
- 자연화 강조마커 보존(볼드/색 소실 시 원본 폴백)
- 카드 시드 가격변수(prev_partner_price·target_mid_price·middle_price 등) 치환

정리:
- ktcommerce 테넌트 삭제 + 테스트 21파일 imarketkorea/_base 로 마이그레이션
- 실 LLM 호출 차단 conftest 가드

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:37:36 +09:00

70 lines
4.1 KiB
Python

"""chat 라우터 프로토콜 (대화형 /chat, P7 슬라이스)."""
from typing import List, Optional
from pydantic import Field
from common.models.gmodel import Req_WebPacketProtocol, Res_WebPacketProtocol
class Req_Chat(Req_WebPacketProtocol):
"""대화 한 턴. session_id 없으면 새 협상 시작. tenant 는 헤더로만.
협상 컨텍스트(rq_type/목표가/앵커링가/품목가/매출액/유통코드/파트너 유형)는 요청에 싣지
않는다 — 세션 시작 시 agent 가 DB 에서 1회 조회해 확정한다(NegotiationContextLoader):
negotiation.sessions(qt_type·target_price·anchoring_price), partner.items(price),
partner.suppliers(total_revenue), partner.supplier_items(supply_type), 상품별 협력사 수.
행이 없으면(데모/테스트 직접 호출) 기본값 폴백.
가격 수용률은 세션 내 라운드별 제시가로 매 턴 동적 계산: max(0, (기존 공급가−현재가)/기존 공급가)
— 첫 제시가부터 기존 공급가 대비 인하가 반영되므로 첫 라운드도 실값. 기존 공급가 없으면 첫 제시가 기준.
"""
session_id: Optional[str] = Field(None, description="없으면 새 세션 생성. 운영 경로는 negotiation.sessions.session_id 를 그대로 사용")
user_input: Optional[str] = Field(None, description="가격 정보, 예/아니오, 배송, 담당자 변경, 종료, 기타, 특수 케이스 등")
# ① desync 감지: backend 가 보는 직전 봇 step(내부 step 또는 표시 step). 없으면 검사 생략.
# agent 는 자기 세션 step 을 정답으로 보고 진행하되, 불일치 시 경고 로깅하고 응답에 desynced 를 실어
# backend/front 가 agent 응답의 step/client_step 으로 리싱크하게 한다.
client_step: Optional[str] = Field(None, description="backend 가 본 직전 봇 step (desync 감지용)")
class Res_Chat(Res_WebPacketProtocol):
session_id: Optional[str] = None
step: Optional[str] = None
client_step: Optional[str] = None
script: Optional[str] = None
input_mode: Optional[str] = None
input_options: Optional[List[str]] = None
chat_end: bool = False
outcome: Optional[str] = None
# ① step desync 신호: 요청 client_step 이 agent 세션 step 과 달랐음을 알린다(agent step 이 정답).
desynced: bool = False
# 가격협상 턴에서 선택된 협상 카드 + 학습 메타
card_id: Optional[str] = None
# ⑦ 표현 계약: 가격협상 턴은 카드 스크립트를 script 로, 협상지표 게이지를 indicator_value 로 내려보낸다.
# backend/front 가 이미 indicator/bot_chat_type 패스스루·게이지 렌더 준비 완료.
bot_chat_type: Optional[str] = None # 가격협상="indicator", 종료폼=summaryRSP/CM 등. 일반 텍스트는 None.
indicator_value: Optional[float] = None # 협상 성공률 1~99 (가격협상 턴)
indicator_range: Optional[str] = None # PZ1|PZ2|PZ3 (가격 구간)
policy: Optional[str] = None
q_value: Optional[float] = None
updated_q: Optional[float] = None
visit_count: Optional[int] = None
reward_total: Optional[float] = None
# 성공 확정 이후 턴(협상완료 요약·협상종료)에 내려주는 합의가. 와일드카드 1% 인하 수락 등
# 유저가 직접 입력하지 않은 가격으로 타결될 수 있어, backend 요약/입찰가는 이 값을 최우선 사용한다.
settled_price: Optional[int] = None
# Phase 3 이해층: 자유 발화를 NLU 로 해석해 진행한 경우, 엔진에 실제 전달된 입력.
# (예: "만원까지는 어렵고 10,500원이면 가능합니다" → "10500") 미해석/정형 입력이면 None.
interpreted_input: Optional[str] = None
class Res_ChatSession(Res_WebPacketProtocol):
"""① 세션 상태 조회 응답. backend 가 타임아웃/재진입 시 정합을 맞출 때 사용."""
session_id: Optional[str] = None
step: Optional[str] = None
client_step: Optional[str] = None
rq_type: Optional[str] = None
ended: bool = False
found: bool = False