feat(toolset): 工具系统重构为 toolset 统一管理,新增系统预置工具集
将工具管理从"agent 挂单个 tool"改为"agent 挂 toolset"模式: - 三个系统预置工具集(system_basic/system_chat/system_workflow)入 DB - 新增 send_file 工具(系统对话工具集)、修复 approval actor 调用 bug - 后端 agent 加载全部走 toolset 链路,移除 load_tools_from_list - 前端工具集中心卡片展示 + agent 配置改为 toolset 多选 - resource API 增加 category 过滤与系统 toolset 保护 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,7 @@ class GlobalStateMachine:
|
||||
for scope, name_to_cls in tm.tool_mapper.items()
|
||||
},
|
||||
system_tools_by_scope=system_tools_by_scope,
|
||||
all_funcs=dict(tm._all_funcs),
|
||||
)
|
||||
|
||||
def _publish_snapshot(self) -> None:
|
||||
@@ -270,22 +271,31 @@ class GlobalStateMachine:
|
||||
tools: List[str],
|
||||
description: Optional[str] = None,
|
||||
owner_id: Optional[str] = None,
|
||||
is_system: bool = False,
|
||||
category: str = "user",
|
||||
) -> Dict[str, Any]:
|
||||
"""新增/更新一个自定义工具组:仅允许引用非 system/非 mcp 的工具。"""
|
||||
# 校验:只能放第三方(非 system / 非 mcp)工具
|
||||
invalid = [
|
||||
t for t in tools if not self._global_tool_manager.is_third_party_tool(t)
|
||||
]
|
||||
if invalid:
|
||||
raise ValueError(
|
||||
f"自定义工具组只允许包含第三方工具,以下不合法:{invalid}"
|
||||
)
|
||||
"""新增/更新一个工具集。
|
||||
|
||||
- 系统 toolset(``is_system=True``)允许包含 system 工具,由启动期种子写入;
|
||||
API 调用方一般不应直接传 ``is_system=True``
|
||||
- 用户 toolset(``is_system=False``)只允许引用非 system / 非 mcp 的工具
|
||||
"""
|
||||
if not is_system:
|
||||
invalid = [
|
||||
t for t in tools if not self._global_tool_manager.is_third_party_tool(t)
|
||||
]
|
||||
if invalid:
|
||||
raise ValueError(
|
||||
f"用户工具集只允许包含第三方工具,以下不合法:{invalid}"
|
||||
)
|
||||
saved = await self.postgres_database.upsert_custom_toolset.remote(
|
||||
toolset_id=toolset_id,
|
||||
name=name,
|
||||
tools=list(tools),
|
||||
description=description,
|
||||
owner_id=owner_id,
|
||||
is_system=is_system,
|
||||
category=category,
|
||||
)
|
||||
self._custom_toolsets[toolset_id] = saved
|
||||
self._global_tool_manager.rebuild_custom_toolsets(self._custom_toolsets)
|
||||
|
||||
@@ -68,6 +68,8 @@ class GSMSnapshot:
|
||||
# 客户端按名字 + ``tool_funcs`` 在自己进程里重建 FunctionToolset,
|
||||
# 避开把不可序列化/版本耦合的 toolset 实例塞进快照的坑。
|
||||
system_tools_by_scope: Dict[str, List[str]] = field(default_factory=dict)
|
||||
# 全部插件函数(system + 第三方),用于 toolset 装配时统一查表。
|
||||
all_funcs: Dict[str, Callable[..., Any]] = field(default_factory=dict)
|
||||
|
||||
|
||||
_local_cache: Dict[str, Any] = {"version": -1, "snapshot": None}
|
||||
@@ -141,18 +143,22 @@ def reset_local_cache() -> None:
|
||||
|
||||
|
||||
def build_toolsets_for_scope(
|
||||
snapshot: GSMSnapshot, scope: str
|
||||
snapshot: GSMSnapshot,
|
||||
scope: str,
|
||||
toolset_ids: Optional[List[str]] = None,
|
||||
) -> List[Any]:
|
||||
"""在调用方进程里按 ``snapshot`` 现场组装 FunctionToolset 列表。
|
||||
|
||||
复刻 ``GlobalToolManager.get_toolsets_for_scope`` 的合并逻辑:
|
||||
新模型下"系统工具集"也存在 ``custom_toolsets`` 里(``is_system=True``),
|
||||
所以本函数只按 ``toolset_ids`` 在 ``custom_toolsets`` 中按需挑选并装配;
|
||||
所有工具函数(system + 第三方)都从 ``snapshot.all_funcs`` 统一查表。
|
||||
|
||||
- 系统 toolset:按 ``default`` + ``scope`` 两个 bucket 拼装
|
||||
- 自定义 toolset:``custom_toolsets`` 里所有有效项
|
||||
|
||||
返回的 toolset 是 *进程局部* 的——pydantic-ai FunctionToolset 实例不能跨进程
|
||||
共享,但函数对象本身已经躺在 snapshot 里被 cloudpickle 还原过,
|
||||
重新 ``FunctionToolset(tools=[...])`` 几乎零代价。
|
||||
Args:
|
||||
snapshot: 当前 GSM 快照。
|
||||
scope: 调用方所属 scope(保留参数:未来可按 scope 过滤系统 toolset
|
||||
的可见性,目前仅用于命名/日志)。
|
||||
toolset_ids: agent 配置的 toolset 列表;为 None 表示返回全部 toolset
|
||||
(兼容老调用,但建议传入显式列表)。
|
||||
"""
|
||||
try:
|
||||
from pydantic_ai.toolsets import FunctionToolset
|
||||
@@ -161,32 +167,24 @@ def build_toolsets_for_scope(
|
||||
return []
|
||||
|
||||
result: List[Any] = []
|
||||
for bucket in ("default", scope):
|
||||
names = snapshot.system_tools_by_scope.get(bucket) or []
|
||||
funcs = [snapshot.tool_funcs[n] for n in names if n in snapshot.tool_funcs]
|
||||
if not funcs:
|
||||
target_ids = (
|
||||
list(toolset_ids)
|
||||
if toolset_ids is not None
|
||||
else list(snapshot.custom_toolsets.keys())
|
||||
)
|
||||
for toolset_id in target_ids:
|
||||
defn = snapshot.custom_toolsets.get(toolset_id)
|
||||
if not defn:
|
||||
continue
|
||||
try:
|
||||
result.append(
|
||||
FunctionToolset(tools=funcs, id=f"system::{bucket}")
|
||||
)
|
||||
except Exception as e: # pragma: no cover - 防御
|
||||
_logger.error(f"build system toolset {bucket} failed: {e}")
|
||||
|
||||
for toolset_id, defn in snapshot.custom_toolsets.items():
|
||||
names = defn.get("tools") or []
|
||||
funcs = [
|
||||
snapshot.third_party_funcs[n]
|
||||
for n in names
|
||||
if n in snapshot.third_party_funcs
|
||||
]
|
||||
funcs = [snapshot.all_funcs[n] for n in names if n in snapshot.all_funcs]
|
||||
if not funcs:
|
||||
continue
|
||||
try:
|
||||
result.append(
|
||||
FunctionToolset(tools=funcs, id=f"custom::{toolset_id}")
|
||||
FunctionToolset(tools=funcs, id=f"toolset::{toolset_id}")
|
||||
)
|
||||
except Exception as e: # pragma: no cover - 防御
|
||||
_logger.error(f"build custom toolset {toolset_id} failed: {e}")
|
||||
_logger.error(f"build toolset {toolset_id} failed: {e}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -29,6 +29,7 @@ class GlobalToolManager:
|
||||
_system_toolsets: Dict[str, Any]
|
||||
_custom_toolsets: Dict[str, Any]
|
||||
_third_party_funcs: Dict[str, Callable]
|
||||
_all_funcs: Dict[str, Callable]
|
||||
tool_mapper: Dict[str, Dict[str, Type[BaseToolData]]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -39,6 +40,7 @@ class GlobalToolManager:
|
||||
self._retrieval_toolsets = {}
|
||||
self._custom_toolsets = {}
|
||||
self._third_party_funcs = {}
|
||||
self._all_funcs = {}
|
||||
self.tool_mapper = defaultdict(dict)
|
||||
|
||||
tool_plugin_dir = (
|
||||
@@ -91,6 +93,8 @@ class GlobalToolManager:
|
||||
if category == "mcp":
|
||||
continue
|
||||
|
||||
self._all_funcs[plugin_name] = tool_func
|
||||
|
||||
scopes = [s for s in action_scopes if s] or ["default"]
|
||||
|
||||
if is_system:
|
||||
@@ -138,7 +142,11 @@ class GlobalToolManager:
|
||||
logger.error(f"Failed to build retrieval toolset {scope}: {e}")
|
||||
|
||||
def rebuild_custom_toolsets(self, custom_defs: Dict[str, Dict[str, Any]]) -> None:
|
||||
"""根据 DB 中的自定义工具组定义重建 custom FunctionToolset。"""
|
||||
"""根据 DB 中的 toolset 定义重建 FunctionToolset。
|
||||
|
||||
系统 toolset(is_system=True)允许包含 system 工具,用户 toolset 只取得到 callable
|
||||
的工具(理论上业务层已校验只包含第三方工具)。
|
||||
"""
|
||||
FunctionToolset = self._import_function_toolset()
|
||||
if FunctionToolset is None:
|
||||
self._custom_toolsets = {}
|
||||
@@ -146,22 +154,21 @@ class GlobalToolManager:
|
||||
new_map: Dict[str, Any] = {}
|
||||
for toolset_id, defn in custom_defs.items():
|
||||
tools_names = defn.get("tools") or []
|
||||
funcs = [
|
||||
self._third_party_funcs[n]
|
||||
for n in tools_names
|
||||
if n in self._third_party_funcs
|
||||
]
|
||||
funcs = [self._all_funcs[n] for n in tools_names if n in self._all_funcs]
|
||||
if not funcs:
|
||||
continue
|
||||
try:
|
||||
new_map[toolset_id] = FunctionToolset(
|
||||
tools=funcs,
|
||||
id=f"custom::{toolset_id}",
|
||||
id=f"toolset::{toolset_id}",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to build custom toolset {toolset_id}: {e}")
|
||||
logger.error(f"Failed to build toolset {toolset_id}: {e}")
|
||||
self._custom_toolsets = new_map
|
||||
|
||||
def get_toolset_by_id(self, toolset_id: str) -> Any | None:
|
||||
return self._custom_toolsets.get(toolset_id)
|
||||
|
||||
@staticmethod
|
||||
def _import_function_toolset():
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user