zcbot/core/salvage.py

44 lines
2.1 KiB
Python

"""从损坏的 tool_call arguments 里抢救完好 JSON 的纯逻辑。
单独成模块(不 import litellm)是刻意的:core/loop.py 顶层 import litellm 在部分
开发机导入期会拉 GitHub cost map 卡死(见 memory),使 import core.loop 无法跑;而
本机制的正确性盲区(线上真垃圾前缀本机复现不出)恰恰只能靠单测兜底。把纯函数放这里,
tests 可无 litellm 依赖直接 import 校验守卫逻辑。
定层结论(见 project_deepseek_malformed_toolcall_salvage / DESIGN):畸形 arguments 是
provider 在 wire 上把别处正文字节错标成本 tool_call 的 arguments delta,粘在**开头**
(char-0 垃圾前缀),尾部真 JSON 完好并跑到串尾。
"""
from __future__ import annotations
import json
from typing import Optional
def salvage_tool_arguments(raw: str, allowed_keys: set) -> Optional[dict]:
"""从损坏的 arguments 里抢救出完好的 JSON 对象;抢不出返回 None。
左→右扫每个 `{`,试 `json.loads(raw[i:])`——第一个满足三条的即采纳:
(a) parse-to-end 成功(json.loads 要求整个后缀是合法 JSON,尾部有残渣就失败);
(b) 结果是非空 dict;
(c) 顶层 key ⊆ 该工具 schema 的参数名。
双护栏缺一不可:(a) 挡尾部被截断的半个 JSON(截断则拒绝、回落既有重试,不冒险执行
残缺写入);(c) 挡垃圾前缀里恰好自洽的旁支 JSON(如模型正文里引用的 `{...}` 片段)。
都不满足返回 None。纯函数、无副作用,便于单测(线上真前缀样本当夹具)。
"""
# 只可能出现在 '{' 处;非 '{' 直接跳过,省下大部分 json.loads 尝试。
start = 0
while True:
i = raw.find("{", start)
if i < 0:
return None
try:
obj = json.loads(raw[i:])
except (json.JSONDecodeError, ValueError):
start = i + 1
continue
if isinstance(obj, dict) and obj and set(obj.keys()) <= allowed_keys:
return obj
# 解析成功但不是想要的 dict(如前缀里一段自洽 JSON):继续往后找,真 JSON 可能在更后面。
start = i + 1