fix(llm): _safe_format 防 ValueError,模板里示例 JSON 也能正常 format
bug: classify_prompt 默认值里含示例 JSON {\\"categories\\": [...]},str.format
看到花括号就试图解析为 placeholder/format spec,遇到 \\" 时抛:
ValueError: Invalid format specifier ' [\\"时政\\"]' for object of type 'str'
修复:
- 引入 placeholder_re 提取所有合法 {varname} 占位符,stash 成 sentinel
- 剩余的 { / } 一律 escape 成 {{ / }},str.format 自然还原
- 用户显式写的 {{ / }}(标准转义)单独 stash,不被重复 escape
- 极端情况(KeyError/IndexError/ValueError)兜底:按原文返回,记录 warn
8 个本地单测全过(含示例 JSON 模板 / 老 prompt 缺变量 / 用户显式 {{ 场景)
This commit is contained in:
@@ -77,11 +77,49 @@ def _safe_format(template: str, vars_: Mapping[str, Any]) -> str:
|
|||||||
|
|
||||||
用途:数据库里用户已存的 prompt 模板可能是旧版的(只支持部分占位符),
|
用途:数据库里用户已存的 prompt 模板可能是旧版的(只支持部分占位符),
|
||||||
新代码传了更多变量也不应崩。
|
新代码传了更多变量也不应崩。
|
||||||
|
|
||||||
|
防御:
|
||||||
|
- 模板里出现的非占位符 `{` / `}`(比如示例 JSON `{"k": "v"}`)会被先 escape 成 `{{` / `}}`,
|
||||||
|
避免 str.format 误解析为占位符/格式说明符而抛 ValueError。
|
||||||
|
- 用户显式写的 `{{` / `}}`(标准 str.format 转义语法)会被原样保留,不被重复 escape。
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
placeholder_re = re.compile(r"\{([A-Za-z_][A-Za-z0-9_.\[\]]*)\}")
|
||||||
|
sentinels: list[str] = []
|
||||||
|
sentinel_map: dict[str, str] = {}
|
||||||
|
user_escape: list[str] = []
|
||||||
|
|
||||||
|
def _stash(m: re.Match) -> str:
|
||||||
|
name = m.group(1)
|
||||||
|
s = f"\x00PH{len(sentinels)}\x00"
|
||||||
|
sentinels.append(name)
|
||||||
|
sentinel_map[s] = name
|
||||||
|
return s
|
||||||
|
|
||||||
|
def _stash_brace(s: str) -> str:
|
||||||
|
sentinel = f"\x00UE{len(user_escape)}\x00"
|
||||||
|
user_escape.append(s)
|
||||||
|
return sentinel
|
||||||
|
|
||||||
|
# 1) 先 stash 用户显式 {{ / }}
|
||||||
|
staged = template.replace("{{", _stash_brace("{{")).replace("}}", _stash_brace("}}"))
|
||||||
|
# 2) stash 合法占位符
|
||||||
|
staged = placeholder_re.sub(_stash, staged)
|
||||||
|
# 3) escape 剩下的单个 { / }(示例 JSON 等字面量)
|
||||||
|
escaped = staged.replace("{", "{{").replace("}", "}}")
|
||||||
|
# 4) 还原占位符
|
||||||
|
final = escaped
|
||||||
|
for s, name in sentinel_map.items():
|
||||||
|
final = final.replace(s, "{" + name + "}")
|
||||||
|
# 5) 还原用户显式 {{ / }}
|
||||||
|
for i, raw in enumerate(user_escape):
|
||||||
|
final = final.replace(f"\x00UE{i}\x00", raw)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return template.format_map(_SafeDict(vars_))
|
return final.format_map(_SafeDict(vars_))
|
||||||
except (KeyError, IndexError) as e:
|
except (KeyError, IndexError, ValueError) as e:
|
||||||
# 极端情况(比如 {} 这种非法占位符)兜底
|
# 极端情况兜底:按原文返回
|
||||||
logger.warning("_safe_format 解析失败,按原文返回: %s", e)
|
logger.warning("_safe_format 解析失败,按原文返回: %s", e)
|
||||||
return template
|
return template
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user