와일들카드 선택 수정 .
This commit is contained in:
parent
8c700b1e08
commit
15f3d10eb6
BIN
agent/docs/q-table 상세 설명.pdf
Normal file
BIN
agent/docs/q-table 상세 설명.pdf
Normal file
Binary file not shown.
BIN
agent/docs/동적_협상_알고리즘_설계서.pdf
Normal file
BIN
agent/docs/동적_협상_알고리즘_설계서.pdf
Normal file
Binary file not shown.
30
agent/docs/조견표(Test-Set).csv
Normal file
30
agent/docs/조견표(Test-Set).csv
Normal file
@ -0,0 +1,30 @@
|
||||
ID,??MD,?????,?????,??? ???,????,???,???,????,????,???,???,1???,1? ?? ???,1? ????,????,2???,2? ?? ???,2? ????,????,3???,3? ?? ???,3? ????,????,4???,,,,,
|
||||
1,,???,,,AT2025042400050,?????,"163,600",??,None (??),"8,180","8,099",,,,,,,,,,,,,,,,,,
|
||||
2,,???,,,AT2025042200057,????,"350,000",??,Single (??),"35,000","33,600",35000,0,?? ?? ?? ??,o,34900,0.00286533,??? ?? ?? ??,o,33700,0.038575668,??? ?? ?? ??,x,33600,,,,,
|
||||
3,,???,,,AT2025042300003,TV???,"6,790,000",??,Multiple (??),"48,500","46,560",50000,0,?? ?? ?? ??,x,46560,,?? ??,,,,,,,,,,,
|
||||
4,,???,,,AT2025042200041,?????,"18,700,000",??,Multiple (??),"1,804,550","1,678,232",2200000,0,?? ?? ?? ??,x,2000000,0.1,?? ?? ?? ??,,1700000,0.294117647,??? ?? ?? ??,,1700000,,,,,
|
||||
5,,???,,,AT2025041600034,???,"760,320",??,Multiple (??),"73,304","70,372",70000,,?? ??,,,,,,,,,,,,,,,
|
||||
6,,???,,,AT2025042400010,????? ??,"780,000",??,Multiple (??),"75,642"," 72,617 ",,,,,,,,,,,,,,,,,,
|
||||
7,,???,,,AT2025042400030,???,"313,000",??,Multiple (??),"31,300","29,735",50000,0,?? ?? ?? ??,,40000,0.25,?? ?? ?? ??,,30500,0.639344262,??? ?? ?? ??,,30500,,,,,
|
||||
8,,???,,,AT2025042300096,?????,"706,800",??,None (??),"68,162","65,436",115800,0,?? ?? ?? ??,,70000,0.654285714,?? ?? ?? ??,,69000,0.67826087,??? ?? ?? ??,,65400,,,,,
|
||||
9,,???,,,AT2025042300082,??????,"12,000",??,Multiple (??),"1,200","1,188",,,,,,,,,,,,,,,,,,
|
||||
2.1,,???,ID 2 ???? ??(??>??),,AT2025042200057,????,"350,000",??,Single (??),"35,000"," 33,250 ",,,,,,,,,,,,,,,,,,
|
||||
6.1,,???,ID 6 ???? ??(??>??),,AT2025042400010,????? ??,780000,??,Multiple (??),"75,642"," 71,860 ",,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,1,AT2025042400050,SOOT777,?????,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,2,AT2025042200057,JAJE89,????,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,3,AT2025042300003,VOV2,TV???,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,4,AT2025042200041,HEMEELY,?????,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,5,AT2025041600034,KMS5939,???,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,6,AT2025042400010,YTS111,????? ??,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,7,AT2025042400030,SJUN98,???,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,8,AT2025042300096,DREAMBIZ,?????,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,9,AT2025042300082,BWTCO40,??????,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,2.1,AT2025042200057,JAJE89,????,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,6.1,AT2025042400010,YTS111,????? ??,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
|
||||
|
@ -28,7 +28,31 @@ _SESSIONS = table(
|
||||
)
|
||||
_ITEMS = table("items", column("item_id"), column("price"), column("deleted"), schema="partner")
|
||||
_SUPPLIERS = table("suppliers", column("supplier_id"), column("total_revenue"), column("deleted"), schema="partner")
|
||||
_QUOTATIONS = table("quotations", column("qt_id"), column("supplier_type"), column("deleted"), schema="quotation")
|
||||
_QUOTATIONS = table(
|
||||
"quotations",
|
||||
column("qt_id"), column("version_id"), column("supplier_type"), column("deleted"),
|
||||
schema="quotation",
|
||||
)
|
||||
_VERSION_NEGO_CARDS = table(
|
||||
"version_nego_cards",
|
||||
column("version_id"), column("nego_card_id"), column("created_at"), column("deleted"),
|
||||
schema="card",
|
||||
)
|
||||
_VERSION_WILD_CARDS = table(
|
||||
"version_wild_cards",
|
||||
column("version_id"), column("wild_card_id"), column("created_at"), column("deleted"),
|
||||
schema="card",
|
||||
)
|
||||
_NEGO_CARDS = table(
|
||||
"nego_cards",
|
||||
column("nego_card_id"), column("number"), column("deleted"),
|
||||
schema="card",
|
||||
)
|
||||
_WILD_CARDS = table(
|
||||
"wild_cards",
|
||||
column("wild_card_id"), column("number"), column("deleted"),
|
||||
schema="card",
|
||||
)
|
||||
# 상품↔협력사 매핑 (2026-07-07 신설): supply_type = 이 협력사가 이 상품을 공급하는 방식(SupplierType).
|
||||
_SUPPLIER_ITEMS = table(
|
||||
"supplier_items",
|
||||
@ -74,6 +98,11 @@ class INegoContextCRUD(ABC):
|
||||
"""상품에 연결된 협력사 수 — 협상 세션 이력 기준(supplier_items 매핑 부재 시 폴백)."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_quotation_card_numbers(self, cdb: AsyncSession, quotation_id) -> Tuple[ErrorType, tuple[list[str], list[str]]]:
|
||||
"""견적 version_id 에 연결된 (일반카드 번호 목록, 와일드카드 번호 목록). 없으면 빈 목록."""
|
||||
pass
|
||||
|
||||
|
||||
class NegoContextCRUD(INegoContextCRUD):
|
||||
async def get_session_row(self, cdb: AsyncSession, session_id) -> Tuple[ErrorType, Optional[tuple]]:
|
||||
@ -181,3 +210,63 @@ class NegoContextCRUD(INegoContextCRUD):
|
||||
except Exception as ex:
|
||||
LOG.e_no_callstack(ex)
|
||||
return ErrorType.DB_RUN_FAILED, 0
|
||||
|
||||
async def get_quotation_card_numbers(self, cdb: AsyncSession, quotation_id) -> Tuple[ErrorType, tuple[list[str], list[str]]]:
|
||||
try:
|
||||
version_q = (
|
||||
select(_QUOTATIONS.c.version_id)
|
||||
.where(_QUOTATIONS.c.qt_id == quotation_id, _QUOTATIONS.c.deleted == False) # noqa: E712
|
||||
.limit(1)
|
||||
)
|
||||
err_type, rows = await DB_SESSION_MNG.execute(cdb, version_q, "get_quotation_version failed.", raise_error=False)
|
||||
if err_type != ErrorType.SUCCESS:
|
||||
return err_type, ([], [])
|
||||
if not rows or rows[0] is None:
|
||||
return ErrorType.SUCCESS, ([], [])
|
||||
version_id = rows[0]
|
||||
|
||||
nego_q = (
|
||||
select(_NEGO_CARDS.c.number)
|
||||
.select_from(
|
||||
_VERSION_NEGO_CARDS.join(
|
||||
_NEGO_CARDS,
|
||||
_VERSION_NEGO_CARDS.c.nego_card_id == _NEGO_CARDS.c.nego_card_id,
|
||||
)
|
||||
)
|
||||
.where(
|
||||
_VERSION_NEGO_CARDS.c.version_id == version_id,
|
||||
_VERSION_NEGO_CARDS.c.deleted == False, # noqa: E712
|
||||
_NEGO_CARDS.c.deleted == False, # noqa: E712
|
||||
)
|
||||
.order_by(_VERSION_NEGO_CARDS.c.created_at)
|
||||
)
|
||||
n_err, n_rows = await DB_SESSION_MNG.execute(cdb, nego_q, "get_quotation_nego_cards failed.", raise_error=False)
|
||||
if n_err != ErrorType.SUCCESS:
|
||||
return n_err, ([], [])
|
||||
|
||||
wild_q = (
|
||||
select(_WILD_CARDS.c.number)
|
||||
.select_from(
|
||||
_VERSION_WILD_CARDS.join(
|
||||
_WILD_CARDS,
|
||||
_VERSION_WILD_CARDS.c.wild_card_id == _WILD_CARDS.c.wild_card_id,
|
||||
)
|
||||
)
|
||||
.where(
|
||||
_VERSION_WILD_CARDS.c.version_id == version_id,
|
||||
_VERSION_WILD_CARDS.c.deleted == False, # noqa: E712
|
||||
_WILD_CARDS.c.deleted == False, # noqa: E712
|
||||
)
|
||||
.order_by(_VERSION_WILD_CARDS.c.created_at)
|
||||
)
|
||||
w_err, w_rows = await DB_SESSION_MNG.execute(cdb, wild_q, "get_quotation_wild_cards failed.", raise_error=False)
|
||||
if w_err != ErrorType.SUCCESS:
|
||||
return w_err, ([], [])
|
||||
|
||||
return ErrorType.SUCCESS, (
|
||||
[str(r) for r in n_rows if r is not None],
|
||||
[str(r) for r in w_rows if r is not None],
|
||||
)
|
||||
except Exception as ex:
|
||||
LOG.e_no_callstack(ex)
|
||||
return ErrorType.DB_RUN_FAILED, ([], [])
|
||||
|
||||
@ -146,7 +146,15 @@ class ChatEngine:
|
||||
cond = c.get("condition")
|
||||
ok = False
|
||||
if cond == "check_wildcard_entry":
|
||||
ok = (not ctx.get("wildcard_used")) and anchor < price <= anchor * 1.05
|
||||
ok = (
|
||||
(not ctx.get("wildcard_used"))
|
||||
and anchor > 0
|
||||
and anchor < price
|
||||
and (
|
||||
price <= anchor * 1.02
|
||||
or (bool(ctx.get("allow_selected_wildcards", True)) and price <= anchor * 1.05)
|
||||
)
|
||||
)
|
||||
elif cond == "check_is_supplier_type_c":
|
||||
ok = False # 공급사 유형 미보유 (PoC 단순화)
|
||||
elif cond == "check_price_match": # = 우선협상: 제시가가 앵커가 이하
|
||||
@ -176,6 +184,8 @@ class ChatEngine:
|
||||
if anchor > 0 and price <= anchor * 1.02:
|
||||
ctx["offer_1pct"] = int(round(price * 0.99)) # 1% 인하가
|
||||
return "wild_card_1pct"
|
||||
if not ctx.get("allow_selected_wildcards", True):
|
||||
return "가격협상"
|
||||
return "wild_card_budget"
|
||||
|
||||
# ---- render --------------------------------------------------------
|
||||
|
||||
@ -43,6 +43,8 @@ class NegotiationDbContext:
|
||||
partner_type: PartnerType # 상품에 연결된 협력사 수(supplier_items 매핑, 없으면 세션 이력) → NONE/SINGLE/MULTIPLE
|
||||
revenue_amount: float # 매출액(원) — suppliers.total_revenue(KTC 미러). 없으면 0
|
||||
distribution_code: Optional[str] # 유통 코드(A/B/C) — supplier_items.supply_type → quotations.supplier_type. 미지정 시 None
|
||||
selected_nego_card_numbers: list[str] # 견적 생성 시 선택된 일반 협상카드 번호(card.nego_cards.number)
|
||||
selected_wild_card_numbers: list[str] # 견적 생성 시 선택된 와일드카드 번호(card.wild_cards.number)
|
||||
|
||||
|
||||
class NegotiationContextLoader:
|
||||
@ -93,6 +95,11 @@ class NegotiationContextLoader:
|
||||
if err != ErrorType.SUCCESS:
|
||||
supplier_count = 1
|
||||
|
||||
# 견적 생성 모달에서 고른 카드셋. 값이 없으면 운영 DB 기준으로 "선택 카드 없음"이다.
|
||||
# 데모/직접호출 경로(DB context 없음)만 ChatService 에서 기존 기본 카드셋으로 폴백한다.
|
||||
_, selected_cards = await self.crud.get_quotation_card_numbers(s, quotation_id)
|
||||
selected_nego_cards, selected_wild_cards = selected_cards
|
||||
|
||||
return NegotiationDbContext(
|
||||
rq_type="재협상" if int(qt_type) in _ONE_TO_ONE_QT_TYPES else "재견적",
|
||||
target_price=target,
|
||||
@ -101,6 +108,8 @@ class NegotiationContextLoader:
|
||||
partner_type=PartnerType.from_count(supplier_count),
|
||||
revenue_amount=revenue_amount,
|
||||
distribution_code=_SUPPLIER_TYPE_TO_CODE.get(supplier_type) if supplier_type else None,
|
||||
selected_nego_card_numbers=selected_nego_cards,
|
||||
selected_wild_card_numbers=selected_wild_cards,
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@ -90,14 +90,15 @@ class ScriptRepository:
|
||||
return self.format_script(text, variables)
|
||||
|
||||
async def resolve_card_script(self, action_id: int, card_id: Optional[str],
|
||||
variables: Optional[Dict[str, Any]] = None) -> Optional[str]:
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
prefer_db: bool = False) -> Optional[str]:
|
||||
"""카드 멘트 해석. cards.source_type == 'backoffice_db' 면 card.nego_cards.script(DB)를
|
||||
우선 조회하고, 없거나 file 모드면 scripts_cards.json(파일) 폴백. 변수 치환 후 반환.
|
||||
|
||||
DB 멘트는 백오피스(negodata)가 편집한 정본이라 파일보다 우선한다. 마커(**굵게** 등)가
|
||||
섞여 있어도 agent 는 불투명 텍스트로 취급 — 표현 렌더는 프론트 소유.
|
||||
"""
|
||||
if self._config.cards.source_type == _CARD_SOURCE_DB and card_id:
|
||||
if (prefer_db or self._config.cards.source_type == _CARD_SOURCE_DB) and card_id:
|
||||
db_text = await self._fetch_card_script_db(card_id)
|
||||
if db_text:
|
||||
return self.format_script(db_text, variables)
|
||||
|
||||
@ -63,11 +63,20 @@ class ChatService:
|
||||
)
|
||||
|
||||
if session is None:
|
||||
selected_nego_cards = db_ctx.selected_nego_card_numbers if db_ctx else []
|
||||
selected_wild_cards = db_ctx.selected_wild_card_numbers if db_ctx else []
|
||||
# 운영 DB 세션은 견적 version_id 에 묶인 카드만 사용한다. 직접 호출/데모(DB context 없음)는
|
||||
# 기존 테넌트 기본 action mapping 으로 폴백해 로컬 테스트와 콘솔 데모를 유지한다.
|
||||
action_space_size = (
|
||||
min(len(selected_nego_cards), engine.action_space_size)
|
||||
if db_ctx is not None
|
||||
else engine.action_space_size
|
||||
)
|
||||
# session_id honoring: backend 가 보낸 session_id(= negotiation.sessions.session_id)를
|
||||
# 새 uuid 발급 없이 그대로 세션 키로 쓴다. 없으면(직접 호출/데모) 생성.
|
||||
session = ChatSession(
|
||||
session_id=req.session_id or str(uuid.uuid4()), tenant_id=engine.tenant_id, company_id=engine.company_id,
|
||||
rq_type=rq_type, action_space_size=engine.action_space_size,
|
||||
rq_type=rq_type, action_space_size=action_space_size,
|
||||
context={
|
||||
# 매출액 = suppliers.total_revenue, 유통코드 = quotations.supplier_type 매핑 (loader).
|
||||
# 미기재/미지정이면 기본값 폴백.
|
||||
@ -83,6 +92,13 @@ class ChatService:
|
||||
"round": 0,
|
||||
# 기존 공급가(품목 기준가) — 가격협상_확인 인하율 산출용.
|
||||
"item_price": db_ctx.item_price if db_ctx else 0,
|
||||
# 견적 생성 시 선택한 카드. 일반카드는 action_id 0..N-1 에 그대로 매핑한다.
|
||||
# 1% 인하는 기본 와일드카드로 항상 열고, 재원부족 등 선택형 와일드카드는
|
||||
# 선택된 와일드카드가 있을 때만 허용한다.
|
||||
"db_context_loaded": db_ctx is not None,
|
||||
"selected_nego_card_numbers": selected_nego_cards,
|
||||
"selected_wild_card_numbers": selected_wild_cards,
|
||||
"allow_selected_wildcards": True if db_ctx is None else bool(selected_wild_cards),
|
||||
},
|
||||
)
|
||||
view = chat_engine.start(session)
|
||||
@ -105,7 +121,7 @@ class ChatService:
|
||||
res.settled_price = int(session.context["input_price"])
|
||||
|
||||
# 3) 학습 결합 (가격협상 카드선택 → 카드 스크립트·협상지표 / 종료 보상)
|
||||
if view.error is None and engine.action_space_size > 0:
|
||||
if view.error is None and session.action_space_size > 0:
|
||||
if view.needs_card_selection:
|
||||
await self._select_and_learn(engine, chat_engine, repo, session, res)
|
||||
elif view.outcome is not None:
|
||||
@ -172,11 +188,11 @@ class ChatService:
|
||||
LOG.e_no_callstack(f"[ChatService] state error: {ex}")
|
||||
return
|
||||
policy, version_id, repo = await QTablePolicyStore.load(engine)
|
||||
ctx = PolicyContext(state_index=idx, snapshot=snap, action_space_size=engine.action_space_size,
|
||||
ctx = PolicyContext(state_index=idx, snapshot=snap, action_space_size=session.action_space_size,
|
||||
episode=EpisodeState(used_action_ids=set(session.used_action_ids)))
|
||||
decision = policy.select(ctx)
|
||||
session.used_action_ids.add(decision.action_id)
|
||||
card_id = engine.mapper.get_card_id(decision.action_id)
|
||||
card_id = self._card_id_for_action(engine, session, decision.action_id)
|
||||
reward = RewardCalculator(engine.config.reward, engine.config.state).calculate(snap)
|
||||
policy.update(Transition(state_index=idx, action_id=decision.action_id, reward=reward.total, done=False))
|
||||
await QTablePolicyStore.persist_cell(repo, version_id, policy, idx, decision.action_id)
|
||||
@ -195,7 +211,12 @@ class ChatService:
|
||||
# ① 선택된 카드의 스크립트를 봇 메시지(script)로 출력 ② 협상지표 게이지(indicator_value) 동봉.
|
||||
# backend/front 가 indicator/bot_chat_type 패스스루·게이지 렌더 준비 완료 → 값만 채우면 표시된다.
|
||||
# 카드 멘트: backoffice_db 모드면 card.nego_cards.script(negodata 편집 정본), 아니면 파일 폴백.
|
||||
card_script = await scripts.resolve_card_script(decision.action_id, card_id, chat_engine.vars_for(session))
|
||||
card_script = await scripts.resolve_card_script(
|
||||
decision.action_id,
|
||||
card_id,
|
||||
chat_engine.vars_for(session),
|
||||
prefer_db=bool(session.context.get("selected_nego_card_numbers")),
|
||||
)
|
||||
if card_script:
|
||||
res.script = card_script
|
||||
c = session.context
|
||||
@ -218,9 +239,16 @@ class ChatService:
|
||||
policy.update(Transition(state_index=last_state, action_id=last_action, reward=reward.total, done=True))
|
||||
await QTablePolicyStore.persist_cell(repo, version_id, policy, last_state, last_action)
|
||||
await self._log(repo, session, last_state, last_action,
|
||||
engine.mapper.get_card_id(last_action), snap, reward, None, done=True)
|
||||
self._card_id_for_action(engine, session, last_action), snap, reward, None, done=True)
|
||||
res.updated_q = float(policy.qtable.q[last_state, last_action])
|
||||
|
||||
@staticmethod
|
||||
def _card_id_for_action(engine: TenantEngine, session: ChatSession, action_id: int) -> Optional[str]:
|
||||
selected = session.context.get("selected_nego_card_numbers") or []
|
||||
if 0 <= action_id < len(selected):
|
||||
return str(selected[action_id])
|
||||
return engine.mapper.get_card_id(action_id)
|
||||
|
||||
async def _log(self, repo: LearningRepository, session, state_index, action_id, card_id, snap, reward, propensity, done):
|
||||
data = {
|
||||
"session_id": session.session_id, "state_index": state_index, "action_id": action_id,
|
||||
|
||||
@ -15,6 +15,8 @@ import pytest
|
||||
|
||||
from router.v1.chat.protocol import Req_Chat
|
||||
from services.chat_service import ChatService, reset_sessions
|
||||
from negotiation.chat.service.chat_engine import ChatEngine, ChatSession
|
||||
from negotiation.chat.service.script_repository import ScriptRepository
|
||||
from negotiation.qtable.infra.repository.learning_repository import LearningRepository
|
||||
from tenancy.config_loader import TenantConfigLoader
|
||||
from tenancy.registry import TenantEngineRegistry
|
||||
@ -122,6 +124,75 @@ def test_acceptance_ratio_dynamic_calc():
|
||||
assert calc({"input_price": 9800}) == 0.0 # 기준가 전무(과거 세션 호환)
|
||||
|
||||
|
||||
def test_selected_card_mapping_overrides_tenant_default():
|
||||
"""운영 세션은 견적 version_id 에 묶인 카드 번호를 action_id 순서로 사용한다."""
|
||||
session = ChatSession(
|
||||
session_id="00000000-0000-0000-0000-000000000001",
|
||||
tenant_id="ktcommerce",
|
||||
company_id="ktcommerce",
|
||||
context={"selected_nego_card_numbers": ["2", "7"]},
|
||||
action_space_size=2,
|
||||
)
|
||||
class _Mapper:
|
||||
def get_card_id(self, action_id):
|
||||
return f"DEFAULT-{action_id}"
|
||||
class _Engine:
|
||||
mapper = _Mapper()
|
||||
|
||||
assert ChatService._card_id_for_action(_Engine(), session, 0) == "2"
|
||||
assert ChatService._card_id_for_action(_Engine(), session, 1) == "7"
|
||||
assert ChatService._card_id_for_action(_Engine(), session, 8) == "DEFAULT-8"
|
||||
|
||||
|
||||
def test_default_1pct_wildcard_still_runs_without_selected_wildcard():
|
||||
"""1% 인하는 기본 제공 카드라 DB 견적에서 와일드카드를 선택하지 않아도 발동한다."""
|
||||
cfg = TenantConfigLoader(tenants_dir=_TENANTS_DIR, cache_ttl_seconds=0).load("ktcommerce")
|
||||
repo = ScriptRepository(cfg, _TENANTS_DIR)
|
||||
engine = ChatEngine(repo, rq_type="재협상")
|
||||
session = ChatSession(
|
||||
session_id="00000000-0000-0000-0000-000000000002",
|
||||
tenant_id="ktcommerce",
|
||||
company_id="ktcommerce",
|
||||
step="가격협상_확인",
|
||||
action_space_size=0,
|
||||
context={
|
||||
"input_price": 10000,
|
||||
"anchor_price": 9900,
|
||||
"target_price": 10000,
|
||||
"round": 1,
|
||||
"allow_selected_wildcards": False,
|
||||
},
|
||||
)
|
||||
|
||||
view = engine.advance(session, "예")
|
||||
assert view.step == "wild_card_1pct"
|
||||
|
||||
|
||||
def test_budget_wildcard_requires_selected_wildcard_for_db_context():
|
||||
"""재원부족 구간은 DB 견적에서 와일드카드를 선택했을 때만 발동한다."""
|
||||
cfg = TenantConfigLoader(tenants_dir=_TENANTS_DIR, cache_ttl_seconds=0).load("ktcommerce")
|
||||
repo = ScriptRepository(cfg, _TENANTS_DIR)
|
||||
engine = ChatEngine(repo, rq_type="재협상")
|
||||
session = ChatSession(
|
||||
session_id="00000000-0000-0000-0000-000000000003",
|
||||
tenant_id="ktcommerce",
|
||||
company_id="ktcommerce",
|
||||
step="가격협상_확인",
|
||||
action_space_size=0,
|
||||
context={
|
||||
"input_price": 10200,
|
||||
"anchor_price": 9900,
|
||||
"target_price": 10000,
|
||||
"round": 1,
|
||||
"allow_selected_wildcards": False,
|
||||
},
|
||||
)
|
||||
|
||||
view = engine.advance(session, "예")
|
||||
assert view.step == "가격협상"
|
||||
assert not view.step.startswith("wild_card_")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_priority_completes_without_wildcard(db_engine):
|
||||
reset_sessions()
|
||||
|
||||
@ -151,3 +151,20 @@ async def test_resolve_card_script_db_mode_falls_back_to_file(monkeypatch):
|
||||
|
||||
out = await repo.resolve_card_script(0, "NGC-A001", {"input_price": 9800})
|
||||
assert out and "9800" in out # 파일 폴백 멘트
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve_card_script_prefer_db_for_selected_cards(monkeypatch):
|
||||
"""견적에서 선택된 백오피스 카드 번호는 file 모드여도 DB 멘트를 우선한다."""
|
||||
cfg = TenantConfigLoader(tenants_dir=_TENANTS_DIR, cache_ttl_seconds=0).load("ktcommerce")
|
||||
assert cfg.cards.source_type == "file"
|
||||
fake = _FakeCardRepo({"2": "선택 카드 DB 멘트 **{input_price}원**"})
|
||||
repo = ScriptRepository(cfg, _TENANTS_DIR, card_repo=fake)
|
||||
|
||||
async def _fake_lambda(_db, _wr, func):
|
||||
return await func(None)
|
||||
from negotiation.chat.service import script_repository as _sr
|
||||
monkeypatch.setattr(_sr.DB_SESSION_MNG, "execute_lambda", _fake_lambda)
|
||||
|
||||
out = await repo.resolve_card_script(1, "2", {"input_price": 10200}, prefer_db=True)
|
||||
assert out == "선택 카드 DB 멘트 **10200원**"
|
||||
|
||||
1
negodata/front/tsconfig.tsbuildinfo
Normal file
1
negodata/front/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user