mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-28 16:20:56 +08:00
✨ 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:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
bdc1374848
commit
80fc5b86a7
@@ -1,18 +1,15 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.services.llm import (
|
||||
LLMException,
|
||||
get_global_default_model_name,
|
||||
from zhenxun.configs.path_config import DATA_PATH
|
||||
from zhenxun.services.ai.core.exceptions import LLMException
|
||||
from zhenxun.services.ai.llm.api import chat
|
||||
from zhenxun.services.ai.llm.manager import (
|
||||
get_model_instance,
|
||||
list_available_models,
|
||||
set_global_default_model_name,
|
||||
)
|
||||
from zhenxun.services.llm.core import KeyStatus
|
||||
from zhenxun.services.llm.manager import (
|
||||
reset_key_status,
|
||||
)
|
||||
from zhenxun.services.llm.types import LLMMessage
|
||||
from zhenxun.services.ai.tools.providers.mcp.provider import mcp_provider
|
||||
|
||||
|
||||
class DataSource:
|
||||
@@ -39,27 +36,12 @@ class DataSource:
|
||||
except LLMException:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def get_default_model() -> str | None:
|
||||
"""获取全局默认模型"""
|
||||
return get_global_default_model_name()
|
||||
|
||||
@staticmethod
|
||||
async def set_default_model(model_name_str: str) -> tuple[bool, str]:
|
||||
"""设置全局默认模型"""
|
||||
success = set_global_default_model_name(model_name_str)
|
||||
if success:
|
||||
return True, f"✅ 成功将默认模型设置为: {model_name_str}"
|
||||
else:
|
||||
return False, f"❌ 设置失败,模型 '{model_name_str}' 不存在或无效。"
|
||||
|
||||
@staticmethod
|
||||
async def test_model_connectivity(model_name_str: str) -> tuple[bool, str]:
|
||||
"""测试模型连通性"""
|
||||
start_time = time.monotonic()
|
||||
try:
|
||||
async with await get_model_instance(model_name_str) as model:
|
||||
await model.generate_response([LLMMessage.user("你好")])
|
||||
await chat("你好", model=model_name_str)
|
||||
end_time = time.monotonic()
|
||||
latency = (end_time - start_time) * 1000
|
||||
return (
|
||||
@@ -70,7 +52,7 @@ class DataSource:
|
||||
return (
|
||||
False,
|
||||
f"❌ 模型 '{model_name_str}' 连接测试失败:\n"
|
||||
f"{e.user_friendly_message}\n错误码: {e.code.name}",
|
||||
f"{e.user_friendly_message}\n错误类型: {e.__class__.__name__}",
|
||||
)
|
||||
except Exception as e:
|
||||
return False, f"❌ 测试时发生未知错误: {e!s}"
|
||||
@@ -78,7 +60,7 @@ class DataSource:
|
||||
@staticmethod
|
||||
async def get_key_status(provider_name: str) -> list[dict[str, Any]] | None:
|
||||
"""获取并排序指定提供商的API Key状态"""
|
||||
from zhenxun.services.llm.manager import get_key_usage_stats
|
||||
from zhenxun.services.ai.llm.manager import get_key_usage_stats
|
||||
|
||||
all_stats = await get_key_usage_stats()
|
||||
provider_stats = all_stats.get(provider_name)
|
||||
@@ -93,11 +75,30 @@ class DataSource:
|
||||
]
|
||||
|
||||
def sort_key(item: dict[str, Any]):
|
||||
status_priority = item.get("status_enum", KeyStatus.UNUSED).value
|
||||
status_map = {
|
||||
"DISABLED": 0,
|
||||
"ERROR": 1,
|
||||
"COOLDOWN": 2,
|
||||
"WARNING": 3,
|
||||
"HEALTHY": 4,
|
||||
"UNUSED": 5,
|
||||
}
|
||||
status_str = item.get("status", "HEALTHY")
|
||||
if (
|
||||
item.get("successes", 0) == 0
|
||||
and item.get("failures", 0) == 0
|
||||
and status_str == "HEALTHY"
|
||||
):
|
||||
status_str = "UNUSED"
|
||||
status_priority = status_map.get(status_str, 5)
|
||||
total = item.get("successes", 0) + item.get("failures", 0)
|
||||
success_rate = (
|
||||
(item.get("successes", 0) / total * 100) if total > 0 else 100.0
|
||||
)
|
||||
return (
|
||||
status_priority,
|
||||
100 - item.get("success_rate", 100.0),
|
||||
-item.get("total_calls", 0),
|
||||
100 - success_rate,
|
||||
-total,
|
||||
)
|
||||
|
||||
sorted_stats_list = sorted(stats_list, key=sort_key)
|
||||
@@ -105,17 +106,159 @@ class DataSource:
|
||||
return sorted_stats_list
|
||||
|
||||
@staticmethod
|
||||
async def reset_key(provider_name: str, api_key: str | None) -> tuple[bool, str]:
|
||||
"""重置API Key状态"""
|
||||
success = await reset_key_status(provider_name, api_key)
|
||||
if success:
|
||||
if api_key:
|
||||
if len(api_key) > 8:
|
||||
target = f"API Key '{api_key[:4]}...{api_key[-4:]}'"
|
||||
else:
|
||||
target = f"API Key '{api_key}'"
|
||||
async def get_mcp_list() -> list[dict[str, Any]]:
|
||||
"""获取排序后的 MCP 列表"""
|
||||
await mcp_provider.initialize()
|
||||
if not mcp_provider._config:
|
||||
return []
|
||||
|
||||
mcp_servers = mcp_provider._config.mcpServers
|
||||
sorted_names = sorted(mcp_servers.keys())
|
||||
|
||||
result = []
|
||||
for idx, name in enumerate(sorted_names):
|
||||
conf = mcp_servers[name]
|
||||
target = ""
|
||||
if conf.transport in ("stdio", "sandbox_proxy") and conf.command:
|
||||
target = f"{conf.command} {' '.join(conf.args)}"
|
||||
elif conf.transport in ("sse", "streamable-http") and conf.url:
|
||||
target = conf.url
|
||||
|
||||
result.append(
|
||||
{
|
||||
"id": idx + 1,
|
||||
"name": name,
|
||||
"enabled": conf.enabled,
|
||||
"transport": conf.transport,
|
||||
"target": target,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def resolve_mcp_targets(
|
||||
targets: tuple[Any, ...],
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""将输入的 ID 或名称解析为实际的 MCP 服务名称"""
|
||||
await mcp_provider.initialize()
|
||||
if not mcp_provider._config:
|
||||
return [], list(map(str, targets))
|
||||
|
||||
mcp_servers = mcp_provider._config.mcpServers
|
||||
sorted_names = sorted(mcp_servers.keys())
|
||||
|
||||
valid_names = []
|
||||
invalid_targets = []
|
||||
|
||||
for tgt in targets:
|
||||
tgt_str = str(tgt)
|
||||
target_name = None
|
||||
|
||||
if tgt_str.isdigit():
|
||||
idx = int(tgt_str) - 1
|
||||
if 0 <= idx < len(sorted_names):
|
||||
target_name = sorted_names[idx]
|
||||
else:
|
||||
target = "所有API Keys"
|
||||
return True, f"✅ 成功重置提供商 '{provider_name}' 的 {target} 的状态。"
|
||||
else:
|
||||
return False, "❌ 重置失败,请检查提供商名称或API Key是否正确。"
|
||||
if tgt_str in mcp_servers:
|
||||
target_name = tgt_str
|
||||
|
||||
if target_name:
|
||||
valid_names.append(target_name)
|
||||
else:
|
||||
invalid_targets.append(tgt_str)
|
||||
|
||||
return list(dict.fromkeys(valid_names)), list(dict.fromkeys(invalid_targets))
|
||||
|
||||
@staticmethod
|
||||
async def toggle_mcp_servers(
|
||||
targets: tuple[Any, ...], is_enable: bool
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""批量切换 MCP 状态"""
|
||||
valid_names, invalid_targets = await DataSource.resolve_mcp_targets(targets)
|
||||
if not mcp_provider._config:
|
||||
return [], invalid_targets
|
||||
|
||||
mcp_servers = mcp_provider._config.mcpServers
|
||||
success_names = []
|
||||
|
||||
for target_name in valid_names:
|
||||
conf = mcp_servers[target_name]
|
||||
if conf.enabled != is_enable:
|
||||
conf.enabled = is_enable
|
||||
if not is_enable:
|
||||
if tk := mcp_provider._toolkits.pop(target_name, None):
|
||||
await tk.close()
|
||||
else:
|
||||
if target_name not in mcp_provider._toolkits:
|
||||
mcp_provider._setup_toolkit(target_name, conf)
|
||||
success_names.append(target_name)
|
||||
|
||||
if success_names:
|
||||
mcp_provider._discovered_tools = None
|
||||
mcp_provider._save_config()
|
||||
|
||||
return success_names, invalid_targets
|
||||
|
||||
@staticmethod
|
||||
async def reload_mcp_config() -> None:
|
||||
"""完全重新加载 MCP 配置"""
|
||||
await mcp_provider.shutdown()
|
||||
mcp_provider._config = None
|
||||
mcp_provider._discovered_tools = None
|
||||
await mcp_provider.initialize()
|
||||
|
||||
@staticmethod
|
||||
async def delete_mcp_servers(names: list[str]) -> None:
|
||||
"""删除指定的 MCP 服务"""
|
||||
for name in names:
|
||||
await mcp_provider.unregister_server(name)
|
||||
|
||||
@staticmethod
|
||||
async def add_mcp_servers_from_json(json_str: str) -> tuple[bool, str]:
|
||||
"""将 JSON 字符串解析并合并到 mcp.json"""
|
||||
mcp_path = DATA_PATH / "ai" / "mcp.json"
|
||||
|
||||
try:
|
||||
json_str = json_str.strip()
|
||||
if json_str.startswith("```"):
|
||||
lines = json_str.split("\n")
|
||||
if lines[0].startswith("```"):
|
||||
lines = lines[1:]
|
||||
if lines and lines[-1].startswith("```"):
|
||||
lines = lines[:-1]
|
||||
json_str = "\n".join(lines).strip()
|
||||
|
||||
new_config = json.loads(json_str)
|
||||
if not isinstance(new_config, dict) or "mcpServers" not in new_config:
|
||||
return False, "❌ JSON 格式不正确,必须包含顶层键 'mcpServers'。"
|
||||
|
||||
new_servers = new_config["mcpServers"]
|
||||
if not isinstance(new_servers, dict) or not new_servers:
|
||||
return False, "❌ 'mcpServers' 不能为空且必须为 JSON 对象(dict)。"
|
||||
|
||||
if mcp_path.exists():
|
||||
with mcp_path.open("r", encoding="utf-8") as f:
|
||||
current_config = json.load(f)
|
||||
else:
|
||||
current_config = {"mcpServers": {}}
|
||||
|
||||
if "mcpServers" not in current_config:
|
||||
current_config["mcpServers"] = {}
|
||||
|
||||
added_names = []
|
||||
for name, conf in new_servers.items():
|
||||
current_config["mcpServers"][name] = conf
|
||||
added_names.append(name)
|
||||
|
||||
mcp_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with mcp_path.open("w", encoding="utf-8") as f:
|
||||
json.dump(current_config, f, ensure_ascii=False, indent=2)
|
||||
|
||||
await DataSource.reload_mcp_config()
|
||||
|
||||
return True, f"✅ 成功添加/更新 MCP 服务: {', '.join(added_names)}"
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
return False, f"❌ JSON 解析失败: {e}"
|
||||
except Exception as e:
|
||||
return False, f"❌ 添加 MCP 服务时发生未知错误: {e}"
|
||||
|
||||
Reference in New Issue
Block a user