o2o-negosium-original/postgres-init/alters/2026-08-04-card-tactic.sql
Mina Choi 6f5d69b5cb [feat] agent·negodata: 협상카드 전술 데이터화(tactic JSONB·스크립트 파싱) + 발동규칙 정비
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
(불변식: 카드 중복 금지·종결 카드 자리·타결가≤목표가·표시가=타결가·토큰 잔존 금지·종료 보장)
2026-08-05 11:48:14 +09:00

71 lines
6.2 KiB
SQL

-- 2026-08-04 · 카드 전술 데이터화(tactic JSONB) + 카드번호 정본화 (기존 DB 보정)
-- 배경: IMK 요청 2건(동일 카드 연속 사용 방지 · 중간값 절충 계산 확인) 대응 —
-- 카드 전술을 코드 하드코딩(agent tactics.py _TACTICS)에서 DB 로 옮긴다.
-- 제안가는 스크립트 변수 파싱으로 결정하고, 문장으로 알 수 없는 운영 규칙만 tactic 에 둔다:
-- {"min_round": N, "closing": bool} -- NULL/빈 객체 = 기본값(1라운드부터·비종결)
-- 멱등: ADD COLUMN IF NOT EXISTS + 조건 가드 UPDATE — 여러 번 실행해도 안전.
-- 적용: psql -h <host> -p <port> -U <user> -d <db> -f postgres-init/alters/2026-08-04-card-tactic.sql
-- (구번호 '1'~'11'/'1'~'5' DB 는 이 파일이 정본 번호로 먼저 정규화한 뒤,
-- 2026-07-24-nego-card-scripts.sql 을 이어서 적용해야 스크립트 개정까지 맞는다.)
\connect negosium_db
-- 1) 전술 컬럼
ALTER TABLE card.nego_cards ADD COLUMN IF NOT EXISTS tactic JSONB NULL;
ALTER TABLE card.wild_cards ADD COLUMN IF NOT EXISTS tactic JSONB NULL;
COMMENT ON COLUMN card.nego_cards.tactic IS '카드 전술 운영 규칙 {"min_round": N, "closing": bool}. 제안가는 script 변수 파싱으로 결정 — agent tactics.build_card_spec';
COMMENT ON COLUMN card.wild_cards.tactic IS '카드 전술 운영 규칙 {"min_round": N, "closing": bool}. 제안가는 script 변수 파싱으로 결정 — agent tactics.build_card_spec';
-- 2) 카드번호 정본화 — 초기 시드(구번호 '1'~'11'/'1'~'5')를 정본 표기(NGC-0xx/WC-0x)로.
-- agent 전술 스펙·backend 카드 UUID 역조회·negodata 표기가 전부 번호 기준이라 표기를 통일한다.
-- (정본 번호가 이미 들어간 DB 는 정규식 가드에 안 걸려 no-op.)
UPDATE card.nego_cards
SET number = 'NGC-' || lpad(number, 3, '0'), updated_at = now()
WHERE number ~ '^[0-9]{1,2}$' AND deleted = FALSE;
UPDATE card.wild_cards
SET number = 'WC-' || lpad(number, 2, '0'), updated_at = now()
WHERE number ~ '^[0-9]{1,2}$' AND deleted = FALSE;
-- 3) 구시드 WC-01 스크립트의 미정의 변수 {customer_reference} 제거 — 치환표에 없어 토큰이
-- 협력사 화면에 그대로 노출된다. 정본 문구(시장 상황과 거래 조건)로 교체.
UPDATE card.wild_cards
SET script = replace(script, '{customer_reference}을(를)', '시장 상황과 거래 조건을'),
edit_script = replace(edit_script::text, '{customer_reference}을(를)', '시장 상황과 거래 조건을')::jsonb,
updated_at = now()
WHERE number = 'WC-01' AND deleted = FALSE AND script LIKE '%{customer_reference}%';
-- 4) 전술 값 — 문장으로 알 수 없는 운영 규칙만.
-- WC-03 최종 통보 · WC-05 중간값 절충 = 종결 전용(라운드 상한·카드 소진 때의 마지막 한 방).
-- WC-04 단계적 인하 제안 = 2라운드부터("당초 적정가 → 한발 물러서" 서사가 첫 제안엔 성립 안 함).
UPDATE card.wild_cards SET tactic = '{"closing": true}'::jsonb, updated_at = now()
WHERE number IN ('WC-03', 'WC-05') AND deleted = FALSE
AND (tactic IS NULL OR tactic = '{}'::jsonb);
UPDATE card.wild_cards SET tactic = '{"min_round": 2}'::jsonb, updated_at = now()
WHERE number = 'WC-04' AND deleted = FALSE
AND (tactic IS NULL OR tactic = '{}'::jsonb);
-- 5) 구시드 WC-03(최종 통보) 멘트 정본화 — 구멘트엔 가격 변수가 없어 "최종 제안 금액이 안 보이는 채
-- 수락 버튼"이 뜨고, 전술 파싱도 설득 카드로 오판한다(종결에서 스킵 → 항상 최후통첩 폴백).
-- 정본(init-data.sql)의 {target_price} 포함 멘트로 교체. 멱등: 가격 변수 없는 행만.
UPDATE card.wild_cards
SET script = '합리적인 기준에 근거하여 당사의 최종 목표 가격 {target_price}원(VAT별도)을 제안 드립니다. 귀사의 기존 제안 가격으로는 긍정적인 합의가 어려울 것으로 예상됩니다.
이번 협상이 결렬되는 경우 우선 협상권을 보장하기 어려우며, 다른 공급 업체를 선정하기 위한 검토가 진행될 수 있습니다.
본 금액을 수락하시면 해당 가격으로 최종 확정되며, 재검토가 필요하시면 다른 제안 가격을 입력해 주시기 바랍니다.',
edit_script = '[{"type": "paragraph", "children": [{"text": "합리적인 기준에 근거하여 당사의 최종 목표 가격 "}, {"type": "variable", "name": "target_price", "label": "목표가격(고객사 지향가)", "children": [{"text": ""}], "suffix": "원(VAT별도)", "style": {"bold": true, "color": "red"}}, {"text": "을 제안 드립니다. 귀사의 기존 제안 가격으로는 긍정적인 합의가 어려울 것으로 예상됩니다."}]}, {"type": "paragraph", "children": [{"text": "이번 협상이 결렬되는 경우 "}, {"text": "우선 협상권을 보장하기 어려우며, 다른 공급 업체를 선정하기 위한 검토가 진행될 수 있습니다.", "bold": true}]}, {"type": "paragraph", "children": [{"text": "본 금액을 수락하시면 해당 가격으로 최종 확정되며, 재검토가 필요하시면 다른 제안 가격을 입력해 주시기 바랍니다."}]}]'::jsonb,
updated_at = now()
WHERE number = 'WC-03' AND deleted = FALSE
AND script NOT LIKE '%{target_price}%';
-- 6) 구시드 NGC-010(향후 거래 연계) 멘트 정본화 — 구멘트의 {customer_condition} 토큰은 조건 미작성 시
-- 협상 채팅에 원형 노출된다(정본은 "2년 장기계약" 평문). edit_script 는 비워 평문 폴백(재편집 시 복원).
UPDATE card.nego_cards
SET script = '이번 거래의 가격을 {target_price}원으로 조정하는 대신, 2년 장기계약을 함께 검토해 주실 것을 제안 드립니다.
당장의 단가 한 건만 보기보다 향후 이어질 거래까지 함께 고려한다면, 양사 모두에게 더 큰 가치를 만들 수 있습니다. 이번 합의를 장기적 관계의 출발점으로 삼아, 서로에게 이익이 되는 구조를 함께 설계하기를 바랍니다.',
edit_script = NULL,
updated_at = now()
WHERE number = 'NGC-010' AND deleted = FALSE
AND script LIKE '%{customer_condition}%';