o2o-negosium-original/negodata/backend/router/v1/card/protocol.py
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

77 lines
3.2 KiB
Python

import uuid
from datetime import datetime
from typing import Any, Optional
from pydantic import ConfigDict
from common.enums import CardStatus, CardUsageType
from common.models.gmodel import Res_PageProtocol, Res_WebPacketProtocol, WebPacketProtocol
class CardProtocol(WebPacketProtocol):
pass
class Req_CreateCard(CardProtocol):
is_wildcard: bool = False
is_shared: bool = False # True(전체 공용 등록)는 거부된다 — 공용 카드는 DB 시드로만 관리. 항상 False 로 보낼 것
name: Optional[str] = None
number: Optional[str] = None
script: Optional[str] = None
edit_script: Optional[Any] = None
usage_type: int = CardUsageType.COMMON.value # 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용
status: int = CardStatus.ACTIVE.value # 와일드카드 적용 여부(available 매핑). 일반카드는 무시.
condition: Optional[str] = None # 와일드카드 전용
memo: Optional[str] = None # 와일드카드 전용
tactic: Optional[Any] = None # 전술 운영 규칙 {"min_round": N, "closing": bool}. 제안가는 script 변수 파싱(agent)
class Req_UpdateCard(CardProtocol):
name: Optional[str] = None
number: Optional[str] = None
script: Optional[str] = None
edit_script: Optional[Any] = None
usage_type: Optional[int] = None # 카드 적용 견적 구분(CardUsageType): 1=공통 2=신규견적전용 3=재견적전용
status: Optional[int] = None
condition: Optional[str] = None
memo: Optional[str] = None
tactic: Optional[Any] = None # 전술 운영 규칙 {"min_round": N, "closing": bool}
# 통합 카드 표현(nego_cards + wild_cards 공통).
class CardData(WebPacketProtocol):
model_config = ConfigDict(from_attributes=True)
nego_card_id: uuid.UUID # 통합 식별자(일반=nego_card_id / 와일드=wild_card_id)
user_id: Optional[uuid.UUID] = None
is_wildcard: bool = False
is_shared: bool = False # 전체(공용) 카드 여부 = user_id NULL. 목록 배지/폼 스코프 표시용
creator_name: Optional[str] = None # 작성자(등록자) 이름. user_id→company.users.name 조인. 공용 카드는 None
name: Optional[str] = None
number: Optional[str] = None
script: Optional[str] = None
edit_script: Optional[Any] = None
usage_type: CardUsageType = CardUsageType.COMMON # 카드 적용 견적 구분: 1=공통 2=신규견적전용 3=재견적전용
status: CardStatus = CardStatus.ACTIVE
condition: Optional[str] = None
memo: Optional[str] = None
tactic: Optional[Any] = None # 전술 운영 규칙 {"min_round": N, "closing": bool}
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
success_rate: float = 0.0 # 카드 성공률(사용 세션 중 타결 비율). #12 순위용
used_count: int = 0 # 카드 사용 세션 수(표본)
class Res_Card(Res_WebPacketProtocol):
card: Optional[CardData] = None
class Res_CardList(Res_PageProtocol):
cards: list[CardData] = []
total_nego: int = 0 # 협상카드 탭 카운트(검색 필터 반영)
total_wild: int = 0 # 와일드카드 탭 카운트(검색 필터 반영)
class Res_DeleteCard(Res_WebPacketProtocol):
pass