IMK QA 2건(BB9A 카드 중복·8AB0 중간값 오계산)의 근본 원인이 전술 하드코딩(_TACTICS 번호 매칭)이라 전술을 데이터로 옮기고, 발동을 유효성 검사로 바꿨다. 전술 정본 = 카드 스크립트의 마지막 가격 변수(파싱), 문장으로 알 수 없는 운영 규칙(closing·min_round)만 card.*.tactic JSONB. 세션 시작 시 card_specs 스냅샷 박제. 발동 유효성(하나라도 걸리면 그 라운드 미발동 — 클램프 폐지): 목표가 초과 / 협력사 제시가 이상 / 당사 직전 제안 미만(역행 금지, IMK 논의) / 재료 결측 / 필수 변수 결측(시장가 카드 requires — 토큰 노출 방지) / 이미 쓴 카드(played_card_numbers 공용 이력) - agent: 와일드=비종결·종결=전용 풀 분리(같은 카드 2회 구조적 차단), 발동 시 자기 제안가 기록(절충가 수렴), 진입 존 프로브(빈 덱 재사용 교착 방지), 낼 카드 전무 시 소진→종결, 무효 금액 카드는 설득 폴백도 금지(playable), 에디터 anchor_price 별칭 등록, 에러 재렌더 변수 치환 - backend: 카드 사용 기록을 step 휴리스틱→번호 prefix 판정(종결 발동 card:null 누락 해소) - negodata: 카드 상세 "협상 전술" 섹션(제시 가격 파싱 표시·종결 전용·최소 라운드) + tactic API 배선 - postgres-init: tactic 컬럼·시드(WC-03/05 closing, WC-04 min_round 2), 멱등 alter 로 dev 정본화 (번호 WC-0x 정규화, WC-01·03·NGC-010 구멘트 교체, WC-05 변수 middle_price 교정) 검증: agent 178 통과 · 시나리오 하네스 14케이스(BB9A·8AB0·역행 실수치 재현) · 랜덤 퍼즈 50협상 불변식 위반 0 (불변식: 카드 중복 금지·종결 카드 자리·타결가≤목표가·표시가=타결가·토큰 잔존 금지·종료 보장)
71 lines
3.1 KiB
Python
71 lines
3.1 KiB
Python
"""AGENT_INTEGRATION.md 규약 검증 (backend ↔ agent).
|
|
|
|
4-1 session_id honoring: backend 가 보낸 session_id(=negotiation.sessions.session_id)를
|
|
새 uuid 발급 없이 그대로 세션 키로 사용.
|
|
4-4 tenant 키 = company_id(uuid): 미등록 company_id 는 _base 자동 온보딩(cold-start) 으로 수용.
|
|
"""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from router.v1.chat.protocol import Req_Chat
|
|
from services.chat_service import ChatService, reset_sessions
|
|
from negotiation.chat.service.chat_session_repository import ChatSessionRepository
|
|
from tenancy.config_loader import TenantConfigLoader
|
|
from tenancy.registry import TenantEngineRegistry
|
|
|
|
_TENANTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "tenants")
|
|
|
|
# backend 가 보내는 값 형식: session_id = negotiation.sessions.session_id(uuid), X-Tenant-ID = company_id(uuid)
|
|
BACKEND_SESSION_ID = "11111111-1111-1111-1111-111111111111"
|
|
COMPANY_ID = "00000000-0000-0000-0000-000000000099"
|
|
|
|
|
|
def _reg():
|
|
return TenantEngineRegistry(loader=TenantConfigLoader(tenants_dir=_TENANTS_DIR, cache_ttl_seconds=0))
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_4_1_honors_backend_session_id(db_engine):
|
|
reset_sessions()
|
|
eng = await _reg().get_engine("imarketkorea")
|
|
svc = ChatService()
|
|
|
|
# 첫 턴: backend 의 session_id 를 그대로 키로 써야 함 (새 uuid 발급 X)
|
|
# 컨텍스트는 DB 조회(행 없음 → 기본값 폴백: target=10000, anchor=9900)
|
|
r = await svc.chat(eng, Req_Chat(session_id=BACKEND_SESSION_ID))
|
|
assert r.session_id == BACKEND_SESSION_ID
|
|
assert r.step == "서비스안내"
|
|
|
|
# 같은 session_id 로 이어가면 그 세션이 DB 에서 복원됨
|
|
r2 = await svc.chat(eng, Req_Chat(session_id=BACKEND_SESSION_ID, user_input="확인"))
|
|
assert r2.session_id == BACKEND_SESSION_ID and r2.step == "담당자확인"
|
|
|
|
# learning.chat_sessions 에 backend 의 session_id 그대로 저장됨
|
|
saved = await ChatSessionRepository(eng.company_id).get(BACKEND_SESSION_ID)
|
|
assert saved is not None and saved.session_id == BACKEND_SESSION_ID
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_4_4_company_id_auto_onboard():
|
|
# 미등록 company_id → _base 자동 온보딩 (전용 tenant.yaml 없이도 엔진 생성)
|
|
eng = await _reg().get_engine(COMPANY_ID)
|
|
assert eng.tenant_id == COMPANY_ID
|
|
assert eng.company_id == COMPANY_ID # 학습/세션이 이 company_id 로 격리
|
|
assert eng.action_space_size == 9 # DB 카탈로그 9장(NGC-006·009 소프트삭제)
|
|
assert eng.state_space_size == 162
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_4_4_company_id_chat_end_to_end(db_engine):
|
|
reset_sessions()
|
|
eng = await _reg().get_engine(COMPANY_ID) # 자동 온보딩 테넌트
|
|
svc = ChatService()
|
|
r = await svc.chat(eng, Req_Chat(session_id=BACKEND_SESSION_ID))
|
|
assert r.session_id == BACKEND_SESSION_ID and r.step == "서비스안내"
|
|
# 카드선택 턴까지 진행 → company_id 스코프로 학습 기록
|
|
for ui in ["확인", "예", "확인", "11000", "예"]:
|
|
r = await svc.chat(eng, Req_Chat(session_id=BACKEND_SESSION_ID, user_input=ui))
|
|
assert r.step == "가격협상" and r.card_id is not None
|