Files
zhenxun_bot/zhenxun/services/ai/llm/engine/schema_transformer.py
T
80fc5b86a7 ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架 (#2146)
* ✨ feat!(llm): 重构并升级大语言模型服务为全新 AI 智能体框架

- 【重构】将原 services/llm 重构并迁移至全新的 services/ai 架构,提供向下兼容垫片
- 【新增】引入 Agent、Team、Workflow 三大智能体与工作流编排范式
- 【新增】引入基于 RAG 的长期向量记忆与中期槽位记忆系统
- 【新增】引入基于 Docker 的安全代码执行沙箱环境
- 【新增】支持 MCP 协议,允许动态管理和调用 MCP 服务
- 【新增】引入输入输出安全合规护栏与自愈反思机制
- 【优化】重构并优化多厂商 API 适配器 (Gemini, OpenAI, DeepSeek, GLM 等)
- 【优化】优化日志脱敏与 Token 预估机制
- 【移除】移除旧版 llm default 和 llm reset-key 命令,新增 llm mcp 管理命令

* 🔧 chore(deps): 更新项目依赖与配置

- 添加 mcp、jieba 和 aiodocker 依赖到配置文件及 requirements.txt
- 在 pyright 配置中设置 reportMissingImports 为 none
- 调整 .gitignore 中 resources 目录的忽略规则

* ♻️ refactor(tools): 重构工具终止机制并清理知识库日志输出

- 统一使用 `context.state["__end_run__"]` 替代 `EndRunResult` 控制任务结束
- 移除文件系统和向量知识库检索工具中 `ToolResult` 的 `.with_log` 调用
- 调整指令处理器(Directive)的返回值为 `tool_res.output`
- 修复部分类型检查警告并优化联合类型判断语法

* ♻️ refactor(tools): 重构工具副作用指令与控制流熔断机制

- 引入 `DirectivePayload` 及 `ToolResult` 的子类以结构化表达工具副作用
- 移除通过 `context.state` 传递魔术变量的隐式控制流设计
- 重构 `DirectiveManager` 处理器接口,直接在处理器中修改 `AgentState` 并构建 `AgentRunResult`
- 在 `StandardAgentExecutor` 中统一通过 `directive_manager` 调度工具返回的副作用指令
- 补全 `MessageBuilder` 中部分核心方法的文档注释

* 🐛 fix(sandbox): 修复 Docker 沙箱容器状态检测与会话清理逻辑

-【修复】修正 `is_alive` 中直接读取私有属性的问题,改用 `show()` 返回值
-【修复】解决 `execute_code` 中缓存的执行器与当前会话不一致的问题
-【优化】在清理工作区前增加容器存活检测,避免向已死容器发送请求
-【优化】创建容器时增加运行状态校验,若已停止则自动从缓存中移除并重建
-【优化】优化容器销毁和清理逻辑,静默处理容器不存在 (404) 的异常

* 📝 docs(core): 补充核心模块初始化方法的文档注释

* 🚨 auto fix by pre-commit hooks

---------

Co-authored-by: webjoin111 <455457521@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-03 08:53:56 +08:00

357 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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