Files
zhenxun_bot/zhenxun/services/ai/llm/engine/schema_transformer.py
T

357 lines
13 KiB
Python
Raw Normal View History

from abc import ABC, abstractmethod
import copy
from typing import Any
class BaseSchemaTransformer(ABC):
"""JSON Schema 节点处理器基类"""
@abstractmethod
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
"""处理单个 Schema 节点并返回修改后的节点"""
pass
class SchemaPipeline:
"""JSON Schema 转换管道执行器"""
def __init__(self, transformers: list[BaseSchemaTransformer]):
self.transformers = transformers
def run(self, schema: dict[str, Any]) -> dict[str, Any]:
"""执行管道,递归遍历并处理整个 Schema AST"""
if not isinstance(schema, dict):
return schema
schema_copy = copy.deepcopy(schema)
return self._walk(schema_copy, is_root=True)
def _walk(self, node: Any, is_root: bool = False) -> Any:
"""核心递归遍历算法,定向深入标准的 Schema 容器键"""
if isinstance(node, list):
return [self._walk(item, is_root=False) for item in node]
if not isinstance(node, dict):
return node
current_node = node
for transformer in self.transformers:
current_node = transformer.process_node(current_node, is_root=is_root)
if not isinstance(current_node, dict):
return current_node
for dict_key in ["properties", "patternProperties", "$defs", "definitions"]:
if dict_key in current_node and isinstance(current_node[dict_key], dict):
current_node[dict_key] = {
k: self._walk(v, is_root=False)
for k, v in current_node[dict_key].items()
}
for list_key in ["anyOf", "allOf", "oneOf", "prefixItems"]:
if list_key in current_node and isinstance(current_node[list_key], list):
current_node[list_key] = [
self._walk(v, is_root=False) for v in current_node[list_key]
]
for single_key in ["items", "additionalProperties", "contains"]:
if single_key in current_node and isinstance(
current_node[single_key], dict
):
current_node[single_key] = self._walk(
current_node[single_key], is_root=False
)
return current_node
class RootRefInlineTransformer(BaseSchemaTransformer):
"""将根节点的 $ref 展开,保留 $defs 供内部递归使用"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if is_root and "$ref" in node:
ref_path = node.pop("$ref")
ref_name = ref_path.split("/")[-1]
defs = node.get("$defs") or node.get("definitions") or {}
if ref_name in defs:
def_content = defs[ref_name].copy()
for k, v in def_content.items():
if k not in node:
node[k] = v
return node
class RemoveUnsupportedKeysTransformer(BaseSchemaTransformer):
"""移除不支持的键"""
def __init__(self, keys_to_remove: list[str]):
self.keys_to_remove = keys_to_remove
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
for key in self.keys_to_remove:
node.pop(key, None)
return node
class GeminiEnumTransformer(BaseSchemaTransformer):
"""将 const 转换为 enum (Gemini 专用)"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if "const" in node:
node["enum"] = [node.pop("const")]
return node
class GeminiNullableUnionTransformer(BaseSchemaTransformer):
"""处理 anyOf 和 type 列表中的 null,转换为 nullable: True (Gemini 专用)"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if "type" in node and isinstance(node["type"], list):
types_list = node["type"]
if "null" in types_list:
node["nullable"] = True
types_list = [t for t in types_list if t != "null"]
node["type"] = types_list[0] if len(types_list) == 1 else types_list
if "anyOf" in node and isinstance(node["anyOf"], list):
any_of = node["anyOf"]
has_null = any(
isinstance(x, dict) and x.get("type") == "null" for x in any_of
)
if has_null:
node["nullable"] = True
new_any_of = [
x
for x in any_of
if not (isinstance(x, dict) and x.get("type") == "null")
]
if len(new_any_of) == 1:
node.update(new_any_of[0])
node.pop("anyOf", None)
else:
node["anyOf"] = new_any_of
return node
class GeminiFormatTransformer(BaseSchemaTransformer):
"""清理不支持的 format 格式 (Gemini 专用)"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if node.get("format") and node["format"] not in ["enum", "date-time"]:
node.pop("format", None)
return node
class StrictObjectTransformer(BaseSchemaTransformer):
"""
强制对象必须关闭 additionalProperties,
并将所有 properties 设为 required (OpenAI/DeepSeek 专用)
"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if node.get("type") == "object" or "properties" in node:
node["type"] = "object"
node["additionalProperties"] = False
if "properties" not in node:
node["properties"] = {}
node["required"] = list(node["properties"].keys())
return node
class OpenAIUnionFlattenTransformer(BaseSchemaTransformer):
"""
将 anyOf/allOf/oneOf 拍平,选取第一个非 null 的类型作为降级方案
(OpenAI 严格模式不支持复杂 Union)
"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
for union_key in ["anyOf", "allOf", "oneOf"]:
if union_key in node:
union_list = node.pop(union_key)
if isinstance(union_list, list) and len(union_list) > 0:
fallback = {}
for item in union_list:
if isinstance(item, dict) and item.get("type") != "null":
fallback = item
break
if not fallback and isinstance(union_list[0], dict):
fallback = union_list[0]
for k, v in fallback.items():
if k not in node:
node[k] = v
return node
class TypeEnforcerTransformer(BaseSchemaTransformer):
"""强制赋予空节点类型 (默认 string)"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if "type" not in node and "properties" not in node and "$ref" not in node:
node["type"] = "string"
return node
class DeepSeekFallbackTransformer(BaseSchemaTransformer):
"""非根空对象转为字符串 (DeepSeek 专用避坑)"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if node.get("type") == "object" and not is_root and not node.get("properties"):
node["type"] = "string"
for k in ["properties", "required", "additionalProperties"]:
node.pop(k, None)
node["description"] = (
f"{node.get('description', '')} (Please provide a JSON string)".strip()
)
return node
class GeminiCyclicRefTransformer(BaseSchemaTransformer):
"""
识别并处理循环引用:
Gemini 要求循环引用的 $ref 决不能出现在父级的 required 列表中
"""
def __init__(self, full_schema: dict):
self.cyclic_refs = self._detect_cycles(full_schema)
def _detect_cycles(self, schema: dict) -> set[str]:
defs = schema.get("$defs") or schema.get("definitions") or {}
cyclic = set()
def check_cycle(def_name: str, visited: set, path: list):
if def_name in path:
for node in path[path.index(def_name) :]:
cyclic.add(f"#/$defs/{node}")
cyclic.add(f"#/definitions/{node}")
return
if def_name in visited:
return
visited.add(def_name)
node = defs.get(def_name, {})
def find_refs(n: Any) -> list[str]:
refs = []
if isinstance(n, dict):
if "$ref" in n:
refs.append(n["$ref"])
for v in n.values():
refs.extend(find_refs(v))
elif isinstance(n, list):
for v in n:
refs.extend(find_refs(v))
return refs
for ref in find_refs(node):
if ref.startswith("#/$defs/") or ref.startswith("#/definitions/"):
next_def = ref.split("/")[-1]
check_cycle(next_def, visited, [*path, def_name])
visited_set = set()
for name in defs:
check_cycle(name, visited_set, [])
return cyclic
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if "properties" in node and isinstance(node.get("required"), list):
new_required = []
for req_key in node["required"]:
prop = node.get("properties", {}).get(req_key, {})
is_cyclic = False
if prop.get("$ref") in self.cyclic_refs:
is_cyclic = True
elif prop.get("type") == "array" and isinstance(
prop.get("items"), dict
):
if prop["items"].get("$ref") in self.cyclic_refs:
is_cyclic = True
if not is_cyclic:
new_required.append(req_key)
if not new_required:
node.pop("required")
else:
node["required"] = new_required
return node
class RefComplianceTransformer(BaseSchemaTransformer):
"""如果包含 $ref,同级不允许出现任何非 $ 开头的键 (如 description, title)"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if "$ref" in node:
keys_to_remove = [k for k in node.keys() if not k.startswith("$")]
for k in keys_to_remove:
node.pop(k)
return node
class GeminiDeepRefInlineTransformer(BaseSchemaTransformer):
"""
深度展开所有 $ref,并清理 $defs (Gemini Function Calling 专用)。
大模型 API 网关层面严格拒绝 $defs 与 $ref,所以进行全量物理替换。
如果遇到循环引用,将安全退化为 string 类型防止栈溢出死循环。
"""
def process_node(
self, node: dict[str, Any], is_root: bool = False
) -> dict[str, Any]:
if not is_root:
return node
defs = {}
if "$defs" in node:
defs.update(node["$defs"])
if "definitions" in node:
defs.update(node["definitions"])
def _resolve_refs(current: Any, visited: set) -> Any:
if isinstance(current, list):
return [_resolve_refs(item, visited) for item in current]
elif isinstance(current, dict):
if "$ref" in current:
ref_path = current["$ref"]
ref_name = ref_path.split("/")[-1]
if ref_name in defs:
if ref_name in visited:
return {
"type": "string",
"description": "Cyclic reference omitted",
}
new_visited = visited | {ref_name}
resolved = _resolve_refs(defs[ref_name], new_visited)
result = current.copy()
result.pop("$ref")
for k, v in resolved.items():
if k not in result:
result[k] = v
return result
return {k: _resolve_refs(v, visited) for k, v in current.items()}
return current
node = _resolve_refs(node, set())
node.pop("$defs", None)
node.pop("definitions", None)
return node