✨ 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>
This commit is contained in:
Rumio
2026-07-03 08:53:56 +08:00
committed by GitHub
co-authored by webjoin111 pre-commit-ci[bot]
parent bdc1374848
commit 80fc5b86a7
223 changed files with 41885 additions and 10006 deletions
@@ -0,0 +1,366 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
import json
import re
from typing import Any, Literal, cast
from pydantic import BaseModel, Field
from zhenxun.configs.path_config import DATA_PATH
from zhenxun.services.ai.core.protocols.tool import (
ToolExecutable,
ToolProvider,
ToolResolvable,
)
from zhenxun.services.ai.sandbox.models import SandboxBlueprint
from zhenxun.services.ai.tools.models import ResolvedToolPayload
from zhenxun.services.ai.tools.providers.mcp.toolkit import MCPToolkit
from zhenxun.services.log import logger
from zhenxun.utils.pydantic_compat import model_dump, model_validate, model_validator
MCP_PATH = DATA_PATH / "ai" / "mcp.json"
class MCPServerConfig(BaseModel):
transport: Literal["stdio", "sse", "streamable-http", "sandbox_proxy"] = Field(
default="stdio"
)
"""传输协议类型:stdio / sse / streamable-http / sandbox_proxy"""
url: str | None = Field(default=None)
"""远端地址(用于 sse 或 streamable-http)"""
timeout: int = Field(default=30)
"""请求超时时间(秒)"""
command: str | None = None
"""启动命令(用于 stdio 或 sandbox_proxy)"""
args: list[str] = Field(default_factory=list)
"""命令参数列表"""
env: dict[str, str] | None = None
"""启动子进程的环境变量(可选)"""
cwd: str | None = Field(default=None)
"""进程的工作目录"""
install_command: str | None = Field(default=None)
"""首次运行前的安装/预热命令"""
description: str | None = None
"""服务描述信息(用于配置可读性与展示)"""
enabled: bool = Field(default=True)
"""是否全局启用"""
admin_level: int = Field(default=0)
"""执行该服务器工具所需的群管等级"""
sandbox_blueprint: SandboxBlueprint | None = Field(default=None)
"""沙箱环境装配配置(用于 sandbox_proxy 自动处理依赖)"""
class MCPToolsConfig(BaseModel):
mcpServers: dict[str, MCPServerConfig] = Field(default_factory=dict)
class GlobalMCPProvider(ToolProvider):
def __init__(self):
self._config: MCPToolsConfig | None = None
self._toolkits: dict[str, MCPToolkit] = {}
self._discovery_lock = asyncio.Lock()
self._discovered_tools: dict[str, ToolExecutable] | None = None
self.env_provider: Callable[[Any], dict[str, str]] | None = None
self.header_provider: Callable[[Any], dict[str, str]] | None = None
self._code_registered_servers: dict[str, tuple[MCPServerConfig, bool]] = {}
def register_server(
self, name: str, config: MCPServerConfig, persist: bool = False
) -> None:
"""代码优先:动态注册 MCP 服务 (支持无痕内存级注册与热重载)"""
self._code_registered_servers[name] = (config, persist)
if self._config is not None:
if persist:
if name not in self._config.mcpServers:
self._config.mcpServers[name] = config
self._save_config()
else:
json_conf = self._config.mcpServers[name]
code_dict = model_dump(config, exclude_unset=True)
json_dict = model_dump(json_conf, exclude_unset=True)
merged_dict = {**code_dict, **json_dict}
merged_conf = model_validate(MCPServerConfig, merged_dict)
if model_dump(merged_conf) != model_dump(json_conf):
self._config.mcpServers[name] = merged_conf
self._save_config()
if name not in self._toolkits:
self._setup_toolkit(name, config)
logger.info(
f"🔄 MCP 代理 '{name}' 已完成动态热重载注册 (持久化: {persist})"
)
async def unregister_server(self, name: str) -> None:
"""动态注销 MCP 服务,关闭底层进程并从配置文件中移除"""
if name in self._code_registered_servers:
self._code_registered_servers.pop(name)
if self._config and name in self._config.mcpServers:
self._config.mcpServers.pop(name)
self._save_config()
if tk := self._toolkits.pop(name, None):
await tk.close()
logger.debug(f"🛑 MCP 代理 '{name}' 已注销并销毁所有连接")
def _save_config(self) -> None:
"""将当前配置持久化到 JSON 文件"""
if not self._config:
return
try:
with MCP_PATH.open("w", encoding="utf-8") as f:
data = model_dump(self._config, exclude_unset=True)
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"保存 MCP 配置失败: {e}", e=e)
async def initialize(self) -> None:
if self._config is not None:
return
if not MCP_PATH.exists():
MCP_PATH.parent.mkdir(parents=True, exist_ok=True)
self._config = MCPToolsConfig(
mcpServers={
"amap-maps": MCPServerConfig(
command="npx",
args=["-y", "@amap/amap-maps-mcp-server"],
env={"AMAP_MAPS_API_KEY": "Your_API_Key"},
enabled=False,
),
"bilibili-search": MCPServerConfig(
command="npx",
args=["bilibili-mcp"],
),
"fetch": MCPServerConfig(
command="uvx",
args=["mcp-server-fetch"],
),
"bingcn": MCPServerConfig(
command="npx",
args=["bing-cn-mcp"],
),
}
)
with MCP_PATH.open("w", encoding="utf-8") as f:
data = model_dump(self._config, exclude_unset=True)
json.dump(data, f, ensure_ascii=False, indent=2)
else:
try:
with MCP_PATH.open("r", encoding="utf-8") as f:
self._config = model_validate(MCPToolsConfig, json.load(f))
except Exception as e:
logger.error(f"加载 MCP 工具配置文件失败: {e}", e=e)
self._config = MCPToolsConfig()
config_changed = False
for name, (code_conf, persist) in self._code_registered_servers.items():
if persist:
if name not in self._config.mcpServers:
self._config.mcpServers[name] = code_conf
config_changed = True
else:
json_conf = self._config.mcpServers[name]
code_dict = model_dump(code_conf, exclude_unset=True)
json_dict = model_dump(json_conf, exclude_unset=True)
merged_dict = {**code_dict, **json_dict}
merged_conf = model_validate(MCPServerConfig, merged_dict)
if model_dump(merged_conf) != model_dump(json_conf):
self._config.mcpServers[name] = merged_conf
config_changed = True
if config_changed or not MCP_PATH.exists():
self._save_config()
for name, conf in self._config.mcpServers.items():
self._setup_toolkit(name, conf)
for name, (code_conf, persist) in self._code_registered_servers.items():
if not persist and name not in self._toolkits:
self._setup_toolkit(name, code_conf)
def _setup_toolkit(self, name: str, conf: MCPServerConfig) -> None:
"""辅助方法:装载单个 MCP Toolkit"""
clean_name = re.sub(r"[^a-zA-Z0-9]", "_", name)
prefix = f"mcp_{clean_name}_"
self._toolkits[name] = MCPToolkit(
server_name=name,
prefix=prefix,
transport=conf.transport,
command=conf.command,
args=conf.args,
url=conf.url,
env=conf.env,
cwd=conf.cwd,
install_command=conf.install_command,
timeout=conf.timeout,
admin_level=conf.admin_level,
header_provider=self.header_provider,
env_provider=self.env_provider,
sandbox_blueprint=conf.sandbox_blueprint,
)
async def shutdown(self):
for tk in self._toolkits.values():
await tk.close()
self._toolkits.clear()
async def get_tools_for_server(self, server_name: str) -> dict[str, ToolExecutable]:
return await self.discover_tools(allowed_servers=[server_name])
async def discover_tools(
self,
allowed_servers: list[str] | None = None,
excluded_servers: list[str] | None = None,
) -> dict[str, ToolExecutable]:
async with self._discovery_lock:
if not self._config:
await self.initialize()
if not self._config:
return {}
if (
self._discovered_tools is not None
and allowed_servers is None
and excluded_servers is None
):
return self._discovered_tools
servers_to_query: list[str] = []
for name, conf in self._config.mcpServers.items():
if allowed_servers and name not in allowed_servers:
continue
if excluded_servers and name in excluded_servers:
continue
if not conf.enabled and not allowed_servers:
continue
servers_to_query.append(name)
for name, (conf, persist) in self._code_registered_servers.items():
if persist:
continue
if allowed_servers and name not in allowed_servers:
continue
if excluded_servers and name in excluded_servers:
continue
if not conf.enabled and not allowed_servers:
continue
if name not in servers_to_query:
servers_to_query.append(name)
all_tools: dict[str, ToolExecutable] = {}
tasks = []
for s_name in servers_to_query:
if tk := self._toolkits.get(s_name):
tasks.append(tk.get_tools())
if tasks:
results = await asyncio.gather(*tasks, return_exceptions=True)
for res in results:
if isinstance(res, dict):
for name, t in res.items():
all_tools[name] = t
elif isinstance(res, list):
for t in res:
all_tools[t.name] = t
elif isinstance(res, Exception):
logger.error(f"MCP 并发发现工具失败: {res}")
if allowed_servers is None and excluded_servers is None:
self._discovered_tools = all_tools
return all_tools
async def get_tool_executable(
self, name: str, config: dict[str, Any]
) -> ToolExecutable | None:
_ = name, config
return None
class MCPSource(BaseModel):
"""显式定义的 MCP 工具源 (动态解析器)"""
server_name: str | None = None
"""目标 MCP 服务名(走全局注册表模式时使用)"""
namespace: str | None = None
"""可选命名空间标识(用于上层路由或分组)"""
config: MCPServerConfig | None = None
"""内联临时配置 (MCPServerConfig 实例)"""
fetch_all_enabled: bool = Field(default=False)
"""内部标识:是否拉取所有已启用的 MCP 服务"""
exclude_servers: list[str] | None = Field(default=None)
"""排除的服务名列表(仅当 fetch_all_enabled 为 True 时生效)"""
def __hash__(self):
return hash((self.server_name, self.namespace))
@classmethod
def all_enabled(cls, exclude: list[str] | None = None) -> "MCPSource":
"""
声明式获取所有已启用的 MCP 服务器下的工具。
参数:
exclude: 需要显式排除的 MCP 服务名称列表。
"""
return cls(fetch_all_enabled=True, exclude_servers=exclude)
@model_validator(mode="after")
def validate_source(self) -> "MCPSource":
if not self.server_name and not self.config and not self.fetch_all_enabled:
raise ValueError(
"MCPSource 必须提供 server_name 或 config,或者开启 fetch_all_enabled"
)
return self
async def resolve(self, context: Any | None = None) -> Any:
tools = []
if self.fetch_all_enabled:
server_tools_dict = await mcp_provider.discover_tools(
excluded_servers=self.exclude_servers
)
for t in server_tools_dict.values():
p = await cast(ToolResolvable, t).resolve(context)
if p:
tools.extend(p.tools)
return ResolvedToolPayload(tools=tools)
elif self.config:
import uuid
temp_server_name = self.server_name or f"inline_mcp_{uuid.uuid4().hex[:8]}"
inline_toolkit = MCPToolkit(
server_name=temp_server_name,
transport=self.config.transport,
command=self.config.command,
args=self.config.args,
url=self.config.url,
env=self.config.env,
cwd=self.config.cwd,
install_command=self.config.install_command,
timeout=self.config.timeout,
admin_level=self.config.admin_level,
sandbox_blueprint=self.config.sandbox_blueprint,
)
payload = await inline_toolkit.resolve(context)
payload.toolkits.insert(0, inline_toolkit)
return payload
else:
assert self.server_name is not None
server_tools = await mcp_provider.get_tools_for_server(self.server_name)
for t in server_tools.values():
p = await cast(ToolResolvable, t).resolve(context)
if p:
tools.extend(p.tools)
return ResolvedToolPayload(tools=tools)
mcp_provider = GlobalMCPProvider()
__all__ = ["GlobalMCPProvider", "MCPSource", "mcp_provider"]