Files
zhenxun_bot/zhenxun/services/ai/tools/providers/skills/models.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

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))