mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-11 15:00:00 +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>
261 lines
9.8 KiB
Python
261 lines
9.8 KiB
Python
from pathlib import Path
|
|
from typing import Annotated, Any, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from zhenxun.services.ai.sandbox.models import SandboxBlueprint
|
|
from zhenxun.utils.pydantic_compat import model_validator
|
|
|
|
DisclosureLevel = Annotated[
|
|
Literal[1, 2, 3], "Progressive disclosure levels for skill loading."
|
|
]
|
|
METADATA: DisclosureLevel = 1
|
|
INSTRUCTIONS: DisclosureLevel = 2
|
|
RESOURCES: DisclosureLevel = 3
|
|
|
|
|
|
class SkillEnvConfig(BaseModel):
|
|
"""全量技能环境变量配置根节点"""
|
|
|
|
envs: dict[str, dict[str, dict[str, str]]] = Field(default_factory=dict)
|
|
|
|
|
|
class SkillFrontmatter(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True) # type: ignore
|
|
|
|
name: str = Field(...)
|
|
"""技能名称"""
|
|
description: str = Field(...)
|
|
"""技能描述"""
|
|
compatibility: str | None = Field(default=None)
|
|
"""环境要求"""
|
|
metadata: dict[str, Any] | None = Field(default=None)
|
|
"""自定义元数据"""
|
|
allowed_tools: list[str] | None = Field(default=None, alias="allowed-tools")
|
|
"""允许的工具"""
|
|
permissions: dict[str, Any] | None = Field(default=None)
|
|
"""沙箱权限声明 (如 network: true/false)"""
|
|
blueprint: SandboxBlueprint = Field(default_factory=SandboxBlueprint)
|
|
"""统一环境装配蓝图声明 (根据 metadata 等自动推导)"""
|
|
required_envs: list[str] = Field(default_factory=list)
|
|
"""声明该技能必需的全局环境变量 Key"""
|
|
|
|
@model_validator(mode="before")
|
|
@classmethod
|
|
def parse_allowed_tools(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
"""将字符串格式的 allowed-tools 按空格切分为列表"""
|
|
key = "allowed-tools"
|
|
alt_key = "allowed_tools"
|
|
raw = values.get(key) or values.get(alt_key)
|
|
if isinstance(raw, str):
|
|
values[key] = raw.split()
|
|
return values
|
|
|
|
@property
|
|
def enable_network(self) -> bool:
|
|
"""网络权限:默认开启(高信任静态资产),
|
|
允许在 YAML 中通过 permissions.network 显式关闭"""
|
|
if self.permissions and isinstance(self.permissions, dict):
|
|
return self.permissions.get("network", True)
|
|
return True
|
|
|
|
|
|
class Skill(BaseModel):
|
|
frontmatter: SkillFrontmatter
|
|
"""解析后的YAML元数据"""
|
|
instructions: str | None = Field(default=None)
|
|
"""SKILL.md正文指令,在INSTRUCTIONS级别填充"""
|
|
path: Path
|
|
"""技能所在目录"""
|
|
disclosure_level: DisclosureLevel = Field(default=METADATA)
|
|
"""当前披露等级"""
|
|
scripts: list[str] = Field(default_factory=list)
|
|
"""脚本文件列表"""
|
|
references: list[str] = Field(default_factory=list)
|
|
"""参考文档列表"""
|
|
source: str = Field(default="local")
|
|
"""技能来源提供者标识"""
|
|
namespace: str = Field(default="global")
|
|
"""隔离所属的插件命名空间"""
|
|
|
|
def with_disclosure_level(
|
|
self,
|
|
level: DisclosureLevel,
|
|
instructions: str | None = None,
|
|
scripts: list[str] | None = None,
|
|
references: list[str] | None = None,
|
|
) -> "Skill":
|
|
"""创建一个提升了披露等级的全新 Skill 实例"""
|
|
return Skill(
|
|
frontmatter=self.frontmatter,
|
|
instructions=instructions
|
|
if instructions is not None
|
|
else self.instructions,
|
|
path=self.path,
|
|
disclosure_level=level,
|
|
scripts=scripts if scripts is not None else self.scripts,
|
|
references=references if references is not None else self.references,
|
|
source=self.source,
|
|
namespace=self.namespace,
|
|
)
|
|
|
|
@property
|
|
def id(self) -> str:
|
|
"""技能的唯一标识符(强制使用所在文件夹的名称)"""
|
|
return self.path.name
|
|
|
|
def to_xml(self) -> str:
|
|
"""将技能转化为结构化的 XML 格式,供大模型友好读取"""
|
|
xml = f"<skill>\n <name>{self.id}</name>\n <description>{self.description}</description>\n" # noqa: E501
|
|
if self.instructions:
|
|
xml += f" <instructions>\n{self.instructions}\n </instructions>\n"
|
|
if self.scripts:
|
|
xml += (
|
|
" <available_scripts>\n"
|
|
+ "\n".join(f" <script>{s}</script>" for s in self.scripts)
|
|
+ "\n </available_scripts>\n"
|
|
)
|
|
if self.references:
|
|
xml += (
|
|
" <available_references>\n"
|
|
+ "\n".join(f" <reference>{r}</reference>" for r in self.references)
|
|
+ "\n </available_references>\n"
|
|
)
|
|
xml += "</skill>"
|
|
return xml
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""技能的显示名称(来自 YAML,可能不符合规范)"""
|
|
return self.frontmatter.name
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return self.frontmatter.description
|
|
|
|
|
|
class SkillSource(BaseModel):
|
|
"""显式定义的技能源 (动态解析器)"""
|
|
|
|
fetch_all: bool = Field(default=False)
|
|
"""是否拉取全局所有已注册的技能"""
|
|
scan_dir: Path | None = Field(default=None)
|
|
"""扫描特定物理目录下的所有技能"""
|
|
exclude_skills: list[str] | None = Field(default=None)
|
|
"""需要排除的技能 ID 列表"""
|
|
|
|
@classmethod
|
|
def all(cls, exclude: list[str] | None = None) -> "SkillSource":
|
|
"""声明式:获取系统全局挂载目录下的所有技能"""
|
|
return cls(fetch_all=True, exclude_skills=exclude)
|
|
|
|
@classmethod
|
|
def from_dir(
|
|
cls, path: str | Path, exclude: list[str] | None = None
|
|
) -> "SkillSource":
|
|
"""声明式:获取指定物理目录下的所有技能(支持私有独立技能库)"""
|
|
from pathlib import Path
|
|
|
|
return cls(scan_dir=Path(path), exclude_skills=exclude)
|
|
|
|
|
|
class SkillBlueprintBuilder:
|
|
"""环境蓝图组装器:将 YAML 字典剥离解析为沙箱 Blueprint 和环境变量要求"""
|
|
|
|
@classmethod
|
|
def build(cls, values: dict[str, Any]) -> tuple[SandboxBlueprint, list[str]]:
|
|
env_setup_data = values.get("env_setup", {})
|
|
python_packages = env_setup_data.get("python_packages", [])
|
|
system_packages = env_setup_data.get("system_packages", [])
|
|
node_packages = env_setup_data.get("node_packages", [])
|
|
bins = env_setup_data.get("bins", [])
|
|
install_scripts = env_setup_data.get("install_scripts", [])
|
|
required_envs = values.get("required_envs", [])
|
|
|
|
metadata = values.get("metadata", {})
|
|
if isinstance(metadata, str):
|
|
import json
|
|
|
|
try:
|
|
metadata = json.loads(metadata)
|
|
except Exception:
|
|
metadata = {}
|
|
|
|
if isinstance(metadata, dict):
|
|
for data in metadata.values():
|
|
if not isinstance(data, dict):
|
|
continue
|
|
requires = data.get("requires", {})
|
|
if isinstance(requires, dict):
|
|
for bin_key in ("bins", "anyBins"):
|
|
b = requires.get(bin_key, [])
|
|
if isinstance(b, list):
|
|
bins.extend(b)
|
|
envs = requires.get("env", [])
|
|
if isinstance(envs, list):
|
|
required_envs.extend(envs)
|
|
|
|
installs = data.get("install", [])
|
|
if isinstance(installs, list):
|
|
for inst in installs:
|
|
if not isinstance(inst, dict):
|
|
continue
|
|
kind = inst.get("kind", "").lower()
|
|
if kind in ("python", "pip", "uv"):
|
|
pkg = inst.get("package", "")
|
|
if isinstance(pkg, str) and pkg:
|
|
python_packages.extend(pkg.split())
|
|
elif kind in ("node", "npm"):
|
|
pkg = inst.get("package", "")
|
|
if isinstance(pkg, str) and pkg:
|
|
node_packages.append(pkg)
|
|
elif kind in ("apt"):
|
|
pkg = inst.get("formula") or inst.get("package") or ""
|
|
if isinstance(pkg, str) and pkg:
|
|
pkg_name = pkg.split("/")[-1]
|
|
system_packages.append(pkg_name)
|
|
elif kind == "go":
|
|
mod = inst.get("module", "")
|
|
if isinstance(mod, str) and mod:
|
|
install_scripts.append(f"go install {mod}")
|
|
|
|
key = "allowed-tools"
|
|
alt_key = "allowed_tools"
|
|
raw_tools = values.get(key) or values.get(alt_key)
|
|
tools_list = (
|
|
raw_tools.split()
|
|
if isinstance(raw_tools, str)
|
|
else (raw_tools if isinstance(raw_tools, list) else [])
|
|
)
|
|
|
|
import re
|
|
|
|
for tool in tools_list:
|
|
if isinstance(tool, str) and tool.startswith("Bash("):
|
|
match = re.search(r"Bash\((.*?)\)", tool)
|
|
if match:
|
|
inner = match.group(1)
|
|
cmd = inner.split(":")[0]
|
|
if cmd not in bins:
|
|
bins.append(cmd)
|
|
|
|
from zhenxun.services.ai.sandbox.models import (
|
|
AptSetup,
|
|
NodeSetup,
|
|
PythonSetup,
|
|
ShellSetup,
|
|
)
|
|
|
|
steps = []
|
|
if system_packages:
|
|
steps.append(AptSetup(packages=list(dict.fromkeys(system_packages))))
|
|
if python_packages:
|
|
steps.append(PythonSetup(packages=list(dict.fromkeys(python_packages))))
|
|
if node_packages:
|
|
steps.append(NodeSetup(packages=list(dict.fromkeys(node_packages))))
|
|
if install_scripts:
|
|
steps.append(ShellSetup(scripts=install_scripts))
|
|
|
|
blueprint = SandboxBlueprint(setup_steps=steps)
|
|
return blueprint, list(dict.fromkeys(required_envs))
|