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:
|
||||
|
||||
@@ -45,14 +45,12 @@ class ConsciousnessNode:
|
||||
global_state_machine: GlobalStateMachine,
|
||||
provider_title: str,
|
||||
model_id: str,
|
||||
tools_list: list[str] = None,
|
||||
toolsets=None,
|
||||
locale: str | None = None,
|
||||
custom_system_prompt: str | None = None,
|
||||
) -> None:
|
||||
system_prompt: str = agent_prompt("consciousness_node", locale=locale, custom_system_prompt=custom_system_prompt)
|
||||
output_type = Union[ForregulatoryNode, ForWorkflow, ForWorkflowEngine]
|
||||
from kilostar.utils.get_tool import load_tools_from_list
|
||||
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
|
||||
|
||||
snapshot = await fetch_snapshot(gsm_actor=global_state_machine)
|
||||
@@ -62,7 +60,6 @@ class ConsciousnessNode:
|
||||
raise ValueError(t("provider_not_registered", locale=locale, provider_title=provider_title))
|
||||
agent_factory = AgentFactory()
|
||||
|
||||
callables = load_tools_from_list(tools_list)
|
||||
self.agent = agent_factory.create_agent(
|
||||
provider=provider,
|
||||
model_id=model_id,
|
||||
@@ -70,7 +67,6 @@ class ConsciousnessNode:
|
||||
system_prompt=system_prompt,
|
||||
deps_type=ConsciousnessNodeDeps,
|
||||
agent_name="consciousness_node",
|
||||
tools=callables,
|
||||
toolsets=toolsets,
|
||||
)
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ class RegulatoryNode:
|
||||
global_state_machine: GlobalStateMachine,
|
||||
provider_title: str,
|
||||
model_id: str,
|
||||
tools_list: list[str] = None,
|
||||
toolsets=None,
|
||||
locale: str | None = None,
|
||||
custom_system_prompt: str | None = None,
|
||||
@@ -60,7 +59,7 @@ class RegulatoryNode:
|
||||
global_state_machine: 全局状态机
|
||||
provider_title: 供应商名
|
||||
model_id: 模型id
|
||||
tools_list: 工具列表
|
||||
toolsets: 已装配好的 FunctionToolset 列表
|
||||
locale: 语言代码(zh/en),控制system prompt语言
|
||||
custom_system_prompt: 管理员自定义追加提示词(可选)
|
||||
Returns:
|
||||
@@ -68,7 +67,6 @@ class RegulatoryNode:
|
||||
"""
|
||||
system_prompt: str = agent_prompt("regulatory_node", locale=locale, custom_system_prompt=custom_system_prompt)
|
||||
output_type = Union[MessageResponse]
|
||||
from kilostar.utils.get_tool import load_tools_from_list
|
||||
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
|
||||
|
||||
# 走 Object Store 快照而不是 actor RPC:高频读路径不再受单 actor 串行限制
|
||||
@@ -79,7 +77,6 @@ class RegulatoryNode:
|
||||
raise ValueError(t("provider_not_registered", locale=locale, provider_title=provider_title))
|
||||
agent_factory = AgentFactory()
|
||||
|
||||
callables = load_tools_from_list(tools_list)
|
||||
self.agent = agent_factory.create_agent(
|
||||
provider=provider,
|
||||
model_id=model_id,
|
||||
@@ -87,7 +84,6 @@ class RegulatoryNode:
|
||||
system_prompt=system_prompt,
|
||||
deps_type=RegulatoryNodeDeps,
|
||||
agent_name="regulatory_node",
|
||||
tools=callables,
|
||||
toolsets=toolsets,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import String, Text, DateTime, func
|
||||
from sqlalchemy import String, Text, DateTime, Boolean, func, text
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
@@ -9,10 +9,12 @@ from .base import BaseDataModel
|
||||
|
||||
|
||||
class CustomToolsetModel(BaseDataModel):
|
||||
"""用户自定义工具组:把若干个非 system / 非 mcp 的工具插件打包成一个 toolset。
|
||||
"""工具集:把若干个工具插件打包成一个 toolset。
|
||||
|
||||
``tools`` 字段保存工具名列表(即 ``plugin/tool_plugin/`` 下的目录名);
|
||||
GSM 启动时按列表把对应工具函数装进同一个 ``FunctionToolset``。
|
||||
系统预置工具集(is_system=True)由启动期种子写入,前端不允许修改/删除。
|
||||
用户自定义工具集(is_system=False)只能包含第三方工具。
|
||||
|
||||
``tools`` 字段保存工具名列表(即 ``plugin/tool_plugin/`` 下的目录名)。
|
||||
"""
|
||||
|
||||
__tablename__ = "custom_toolset"
|
||||
@@ -22,7 +24,23 @@ class CustomToolsetModel(BaseDataModel):
|
||||
description: Mapped[Optional[str]] = mapped_column(Text)
|
||||
owner_id: Mapped[Optional[str]] = mapped_column(String(64), index=True)
|
||||
tools: Mapped[List[str]] = mapped_column(
|
||||
JSONB, default=list, comment="工具名列表,仅允许非 system/非 mcp 的工具"
|
||||
JSONB, default=list, comment="工具名列表"
|
||||
)
|
||||
is_system: Mapped[bool] = mapped_column(
|
||||
Boolean,
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=text("false"),
|
||||
index=True,
|
||||
comment="是否系统预置工具集(不可修改/删除)",
|
||||
)
|
||||
category: Mapped[str] = mapped_column(
|
||||
String(32),
|
||||
nullable=False,
|
||||
default="user",
|
||||
server_default=text("'user'"),
|
||||
index=True,
|
||||
comment="分类:system_basic/system_chat/system_workflow/user",
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
|
||||
@@ -33,7 +33,7 @@ class SystemNodeConfigModel(BaseDataModel):
|
||||
provider_title: Mapped[str] = mapped_column(String(50), nullable=False)
|
||||
model_id: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
tools: Mapped[Optional[List[str]]] = mapped_column(
|
||||
JSONB, default=list, comment="节点可调用的工具标识列表"
|
||||
JSONB, default=list, comment="节点挂载的工具集 ID 列表(custom_toolset.toolset_id)"
|
||||
)
|
||||
persona_id: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("persona_template.template_id", ondelete="SET NULL"),
|
||||
|
||||
@@ -7,7 +7,7 @@ from kilostar.core.postgres_database.model.custom_toolset import CustomToolsetMo
|
||||
|
||||
|
||||
class CustomToolsetDatabase:
|
||||
"""用户自定义工具组 DAO。``tools`` 字段是工具名列表,业务层负责保证只放非 system/非 mcp 的工具。"""
|
||||
"""工具集 DAO。包含系统预置 toolset(is_system=True)和用户自定义 toolset。"""
|
||||
|
||||
def __init__(self, async_session_maker):
|
||||
self.async_session_maker = async_session_maker
|
||||
@@ -20,6 +20,8 @@ class CustomToolsetDatabase:
|
||||
"description": row.description,
|
||||
"owner_id": row.owner_id,
|
||||
"tools": list(row.tools or []),
|
||||
"is_system": bool(row.is_system),
|
||||
"category": row.category,
|
||||
}
|
||||
|
||||
@database_exception
|
||||
@@ -30,6 +32,8 @@ class CustomToolsetDatabase:
|
||||
tools: List[str],
|
||||
description: Optional[str] = None,
|
||||
owner_id: Optional[str] = None,
|
||||
is_system: bool = False,
|
||||
category: str = "user",
|
||||
) -> Dict[str, Any]:
|
||||
async with self.async_session_maker() as session:
|
||||
stmt = select(CustomToolsetModel).where(
|
||||
@@ -41,6 +45,8 @@ class CustomToolsetDatabase:
|
||||
row.description = description
|
||||
row.owner_id = owner_id
|
||||
row.tools = list(tools)
|
||||
row.is_system = is_system
|
||||
row.category = category
|
||||
else:
|
||||
row = CustomToolsetModel(
|
||||
toolset_id=toolset_id,
|
||||
@@ -48,6 +54,8 @@ class CustomToolsetDatabase:
|
||||
description=description,
|
||||
owner_id=owner_id,
|
||||
tools=list(tools),
|
||||
is_system=is_system,
|
||||
category=category,
|
||||
)
|
||||
session.add(row)
|
||||
await session.commit()
|
||||
|
||||
Reference in New Issue
Block a user