mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-05 20:00:04 +08:00
* ✨ 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>
357 lines
13 KiB
Python
357 lines
13 KiB
Python
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
|