[fix] agent: format_script 색 마커 {{강조|..}} 보존 — format_map → 정규식 치환

str.format_map 이 {{·}} 를 이스케이프로 해석해 색 마커 {{강조|텍스트}} 를 {강조|텍스트} 로
붕괴시키던 버그 수정. 정규식 \{(\w+)\} 으로 {변수} 단일 토큰만 치환 — 색 마커는 내부 |(비-\w)로
매칭되지 않아 그대로 보존된다. {변수} 치환·미등록 토큰 원형 유지는 동일.

엔드투엔드 검증(backoffice_db + 실 DB 카드)에서 발견. 회귀 테스트 추가, agent 87/87.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-07-07 15:53:48 +09:00
parent 1917d76304
commit 8c700b1e08
2 changed files with 21 additions and 8 deletions

View File

@ -13,6 +13,7 @@ Chat_server 구조 참고, 동적 import 해킹/특정사 표현은 제거(CLEAN
import json
import os
import re
from typing import Any, Dict, Optional
from common.database.db_session_manager import DB_SESSION_MNG
@ -24,6 +25,8 @@ from tenancy.config import TenantConfig
_RQ_FILES = {"재협상": "scripts_renegotiation.json", "재견적": "scripts_requote.json"}
_CARD_SOURCE_DB = "backoffice_db"
# `{name}` 단일 토큰만 매칭(색 마커 `{{강조|...}}` 는 내부 `|` 로 인해 비매칭 → 보존).
_VAR_TOKEN_RE = re.compile(r"\{(\w+)\}")
class ScriptRepository:
@ -125,21 +128,23 @@ class ScriptRepository:
}
def format_script(self, text: str, variables: Optional[Dict[str, Any]] = None) -> str:
"""{company_name}/{service_name} + 협상 변수 치환. 누락 변수는 원형 유지(KeyError 방지)."""
"""{company_name}/{service_name} + 협상 변수 치환. 누락 변수는 원형 유지.
정규식으로 `{name}` 단일 토큰만 치환한다 str.format_map `{{`·`}}` 이스케이프로
해석해 마커 `{{강조|...}}` `{강조|...}` 붕괴시키므로 쓰지 않는다.
마커는 내부에 `|`(-\\w) 있어 `\\{(\\w+)\\}` 매칭되지 않아 그대로 보존된다.
"""
if not text:
return text
ctx = self._brand_vars()
if variables:
ctx.update({k: v for k, v in variables.items() if v is not None})
class _Safe(dict):
def __missing__(self, key):
return "{" + key + "}"
def _repl(m):
key = m.group(1)
return str(ctx[key]) if key in ctx else m.group(0) # 미등록 토큰은 원형
try:
return text.format_map(_Safe(ctx))
except (ValueError, IndexError):
return text # 형식 토큰 충돌 시 원형
return _VAR_TOKEN_RE.sub(_repl, text)
def get_step(self, step: str, rq_type: str = "재협상", variables: Optional[Dict[str, Any]] = None) -> Optional[dict]:
"""step 정의를 반환하되 script 를 치환해서 돌려준다."""

View File

@ -89,6 +89,14 @@ def test_client_step_and_variable_mapping_load():
assert vm["인터넷 최저가"] == "internet_min_price"
def test_format_script_preserves_color_markers():
"""변수 치환이 색 마커 {{강조|...}} 를 보존해야 한다(회귀: format_map 이 {{}} 를 {} 로 붕괴시킴)."""
repo = _repo()
out = repo.format_script("**{input_price}원**·{{안내|{target}원}}·{unknown}",
{"input_price": 9800, "target": 10000})
assert out == "**9800원**·{{안내|10000원}}·{unknown}" # 마커 보존 + 변수 치환 + 미등록 원형
@pytest.mark.asyncio
async def test_resolve_card_script_file_mode_default():
"""기본(source_type='file'): resolve_card_script 가 파일 카드 멘트를 반환(하위호환)."""