refactor: unify system tools as resources
This commit is contained in:
@@ -44,7 +44,7 @@ from services.message_stage import (
|
||||
MessageStageSpec,
|
||||
)
|
||||
from services.runtime_variables import DynamicVariableError, DynamicVariableStore
|
||||
from services.system_tools import SYSTEM_TOOL_KINDS, state_update_properties
|
||||
from services.system_tools import state_update_properties, system_tool_kind
|
||||
from services.tool_executor import ToolExecutionError, ToolExecutor
|
||||
from services.tool_policy import policy_for_tool
|
||||
|
||||
@@ -128,11 +128,16 @@ class PromptBrain(BaseBrain):
|
||||
if llm_tool_ids is not None and tool.id not in llm_tool_ids:
|
||||
continue
|
||||
if tool.type == "system":
|
||||
schema, handler = self._make_end_call_tool(tool, runtime)
|
||||
schema, handler = self._make_system_tool(tool, runtime)
|
||||
elif tool.type in {"http", "mcp", "client"}:
|
||||
schema, handler = self._make_remote_tool(tool, runtime)
|
||||
else:
|
||||
continue
|
||||
if schema.name in registered_names:
|
||||
logger.warning(
|
||||
f"跳过工具 {tool.id}: 函数名 {schema.name} 已被占用"
|
||||
)
|
||||
continue
|
||||
schemas.append(schema)
|
||||
registered_names.add(schema.name)
|
||||
policy = policy_for_tool(tool)
|
||||
@@ -141,19 +146,6 @@ class PromptBrain(BaseBrain):
|
||||
handler,
|
||||
cancel_on_interruption=policy.cancel_on_interruption,
|
||||
)
|
||||
for kind in cfg.system_tools or []:
|
||||
if kind not in SYSTEM_TOOL_KINDS:
|
||||
logger.warning(f"忽略未知系统工具: {kind}")
|
||||
continue
|
||||
schema, handler = self._make_system_tool(kind, runtime)
|
||||
if schema.name in registered_names:
|
||||
logger.warning(
|
||||
f"跳过系统工具 {schema.name}: 与已绑定工具函数名冲突"
|
||||
)
|
||||
continue
|
||||
registered_names.add(schema.name)
|
||||
schemas.append(schema)
|
||||
runtime.llm.register_function(schema.name, handler)
|
||||
runtime.set_tools(schemas)
|
||||
|
||||
async def run_preflight(self) -> None:
|
||||
@@ -598,50 +590,23 @@ class PromptBrain(BaseBrain):
|
||||
)
|
||||
return schema, end_call
|
||||
|
||||
# ---------- 内置系统工具(助手配置 system_tools 开关) ----------
|
||||
# ---------- System 工具资源 ----------
|
||||
|
||||
def _make_system_tool(self, kind: str, runtime: BrainRuntime):
|
||||
def _make_system_tool(self, tool, runtime: BrainRuntime):
|
||||
kind = system_tool_kind(tool.definition or {})
|
||||
if not kind:
|
||||
raise ValueError(f"系统工具 {tool.id} 缺少有效 kind")
|
||||
if kind == "end_conversation":
|
||||
return self._make_end_conversation_tool(runtime)
|
||||
return self._make_end_call_tool(tool, runtime)
|
||||
if kind == "update_state":
|
||||
return self._make_update_state_tool(runtime)
|
||||
return self._make_update_state_tool(tool, runtime)
|
||||
if kind == "skip_turn":
|
||||
return self._make_skip_turn_tool()
|
||||
return self._make_skip_turn_tool(tool)
|
||||
if kind == "request_human_handoff":
|
||||
return self._make_handoff_tool(runtime)
|
||||
return self._make_handoff_tool(tool, runtime)
|
||||
raise ValueError(f"未知系统工具: {kind}")
|
||||
|
||||
def _make_end_conversation_tool(self, runtime: BrainRuntime):
|
||||
"""结束本次对话,等待模型已生成的告别语播完后再挂断。"""
|
||||
|
||||
async def end_conversation(params: FunctionCallParams) -> None:
|
||||
reason = str(
|
||||
params.arguments.get("reason") or "end_conversation"
|
||||
).strip()
|
||||
self._waiting_for_generated_end_speech = True
|
||||
runtime.call_end.begin(reason)
|
||||
await params.result_callback(
|
||||
{"status": "success", "action": "ending_call"},
|
||||
properties=FunctionCallResultProperties(run_llm=False),
|
||||
)
|
||||
|
||||
schema = FunctionSchema(
|
||||
name="end_conversation",
|
||||
description=(
|
||||
"礼貌地结束本次对话。当用户明确告别、表示任务已完成"
|
||||
"或要求挂断时调用。"
|
||||
),
|
||||
properties={
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "结束对话的简短原因。",
|
||||
}
|
||||
},
|
||||
required=[],
|
||||
)
|
||||
return schema, end_conversation
|
||||
|
||||
def _make_update_state_tool(self, runtime: BrainRuntime):
|
||||
def _make_update_state_tool(self, tool, runtime: BrainRuntime):
|
||||
"""更新已声明的动态变量(会话状态),并让模型继续当前回答。"""
|
||||
|
||||
writable = state_update_properties(self._cfg.dynamic_variable_definitions)
|
||||
@@ -676,8 +641,8 @@ class PromptBrain(BaseBrain):
|
||||
)
|
||||
|
||||
schema = FunctionSchema(
|
||||
name="update_state",
|
||||
description=(
|
||||
name=tool.function_name,
|
||||
description=tool.description or (
|
||||
"静默更新本次对话中已经声明并明确列出的动态变量。"
|
||||
"只提交本轮获得或确认的信息,更新后继续当前回答。"
|
||||
),
|
||||
@@ -686,7 +651,7 @@ class PromptBrain(BaseBrain):
|
||||
)
|
||||
return schema, update_state
|
||||
|
||||
def _make_skip_turn_tool(self):
|
||||
def _make_skip_turn_tool(self, tool):
|
||||
"""跳过当前轮次,不生成任何语音回复。"""
|
||||
|
||||
async def skip_turn(params: FunctionCallParams) -> None:
|
||||
@@ -700,8 +665,8 @@ class PromptBrain(BaseBrain):
|
||||
)
|
||||
|
||||
schema = FunctionSchema(
|
||||
name="skip_turn",
|
||||
description=(
|
||||
name=tool.function_name,
|
||||
description=tool.description or (
|
||||
"跳过当前轮次,不生成任何语音回复。"
|
||||
"仅当用户明确要求稍等、话还没说完,或输入可确认只是噪音时调用。"
|
||||
),
|
||||
@@ -715,7 +680,7 @@ class PromptBrain(BaseBrain):
|
||||
)
|
||||
return schema, skip_turn
|
||||
|
||||
def _make_handoff_tool(self, runtime: BrainRuntime):
|
||||
def _make_handoff_tool(self, tool, runtime: BrainRuntime):
|
||||
"""提交人工接管请求;请求完成前保持当前 AI 会话可用。"""
|
||||
|
||||
async def request_human_handoff(params: FunctionCallParams) -> None:
|
||||
@@ -741,8 +706,8 @@ class PromptBrain(BaseBrain):
|
||||
)
|
||||
|
||||
schema = FunctionSchema(
|
||||
name="request_human_handoff",
|
||||
description=(
|
||||
name=tool.function_name,
|
||||
description=tool.description or (
|
||||
"提交人工接管请求。当用户明确要求人工服务、投诉升级或 AI 无法"
|
||||
"解决时调用。该工具只提交请求,不代表人工已经接通;调用后继续"
|
||||
"回复用户并说明正在等待人工响应。"
|
||||
|
||||
@@ -57,7 +57,7 @@ from services.message_stage import (
|
||||
MessageStageSpec,
|
||||
)
|
||||
from services.runtime_variables import DynamicVariableError, DynamicVariableStore
|
||||
from services.system_tools import SYSTEM_TOOL_KINDS, state_update_properties
|
||||
from services.system_tools import state_update_properties, system_tool_kind
|
||||
from services.tool_executor import ToolExecutionError, ToolExecutor
|
||||
from services.tool_policy import policy_for_tool
|
||||
from services.workflow.agent import WorkflowAgentStage
|
||||
@@ -564,22 +564,21 @@ class WorkflowBrain(BaseBrain):
|
||||
|
||||
for tool_id in stage.tool_ids:
|
||||
tool = self._tool_by_id.get(str(tool_id))
|
||||
if tool and tool.type in {"http", "mcp", "client"}:
|
||||
if not tool:
|
||||
continue
|
||||
if tool.type == "system":
|
||||
append_function(
|
||||
self._workflow_system_tool(
|
||||
tool,
|
||||
node_id=node_id,
|
||||
state_variable_names=stage.state_variable_names,
|
||||
)
|
||||
)
|
||||
elif tool.type in {"http", "mcp", "client"}:
|
||||
append_function(self._flow_tool(tool, node_id))
|
||||
append_function(self._knowledge_function(node_id))
|
||||
if stage.vision_enabled and self._require_runtime().vision_function:
|
||||
append_function(self._require_runtime().vision_function)
|
||||
for kind in stage.system_tools:
|
||||
if kind not in SYSTEM_TOOL_KINDS:
|
||||
logger.warning(f"忽略 Agent {node_id} 的未知系统工具: {kind}")
|
||||
continue
|
||||
append_function(
|
||||
self._workflow_system_tool(
|
||||
kind,
|
||||
node_id=node_id,
|
||||
state_variable_names=stage.state_variable_names,
|
||||
)
|
||||
)
|
||||
return self._require_agent_stage().node_config(
|
||||
node_id,
|
||||
functions=functions,
|
||||
@@ -738,27 +737,30 @@ class WorkflowBrain(BaseBrain):
|
||||
|
||||
def _workflow_system_tool(
|
||||
self,
|
||||
kind: str,
|
||||
tool: RuntimeTool,
|
||||
*,
|
||||
node_id: str,
|
||||
state_variable_names: tuple[str, ...],
|
||||
) -> FlowsFunctionSchema:
|
||||
"""Build one platform-owned tool scoped to the active Agent node."""
|
||||
kind = system_tool_kind(tool.definition or {})
|
||||
if kind == "update_state":
|
||||
return self._workflow_update_state_tool(
|
||||
tool,
|
||||
node_id,
|
||||
state_variable_names=state_variable_names,
|
||||
)
|
||||
if kind == "skip_turn":
|
||||
return self._workflow_skip_turn_tool()
|
||||
return self._workflow_skip_turn_tool(tool)
|
||||
if kind == "request_human_handoff":
|
||||
return self._workflow_handoff_tool(node_id)
|
||||
return self._workflow_handoff_tool(tool, node_id)
|
||||
if kind == "end_conversation":
|
||||
return self._workflow_end_conversation_tool()
|
||||
raise ValueError(f"未知系统工具: {kind}")
|
||||
return self._workflow_end_conversation_tool(tool, node_id)
|
||||
raise ValueError(f"系统工具 {tool.id} 缺少有效 kind")
|
||||
|
||||
def _workflow_update_state_tool(
|
||||
self,
|
||||
tool: RuntimeTool,
|
||||
node_id: str,
|
||||
*,
|
||||
state_variable_names: tuple[str, ...],
|
||||
@@ -791,8 +793,8 @@ class WorkflowBrain(BaseBrain):
|
||||
}
|
||||
|
||||
return FlowsFunctionSchema(
|
||||
name="update_state",
|
||||
description=(
|
||||
name=tool.function_name,
|
||||
description=tool.description or (
|
||||
"静默更新当前阶段明确授权的动态变量。"
|
||||
"只提交本轮获得或确认的信息,更新后继续当前回答。"
|
||||
),
|
||||
@@ -805,7 +807,7 @@ class WorkflowBrain(BaseBrain):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _workflow_skip_turn_tool() -> FlowsFunctionSchema:
|
||||
def _workflow_skip_turn_tool(tool: RuntimeTool) -> FlowsFunctionSchema:
|
||||
async def handler(args, _flow_manager):
|
||||
reason = str((args or {}).get("reason") or "").strip()
|
||||
result = {"status": "success", "action": "skip_turn"}
|
||||
@@ -815,8 +817,8 @@ class WorkflowBrain(BaseBrain):
|
||||
|
||||
setattr(handler, "_suppress_followup_llm", True)
|
||||
return FlowsFunctionSchema(
|
||||
name="skip_turn",
|
||||
description=(
|
||||
name=tool.function_name,
|
||||
description=tool.description or (
|
||||
"跳过当前轮次,不生成任何语音回复。"
|
||||
"仅当用户明确要求稍等、话还没说完,或输入可确认只是噪音时调用。"
|
||||
),
|
||||
@@ -830,7 +832,9 @@ class WorkflowBrain(BaseBrain):
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
def _workflow_handoff_tool(self, node_id: str) -> FlowsFunctionSchema:
|
||||
def _workflow_handoff_tool(
|
||||
self, tool: RuntimeTool, node_id: str
|
||||
) -> FlowsFunctionSchema:
|
||||
async def handler(args, _flow_manager):
|
||||
reason = str((args or {}).get("reason") or "human_handoff").strip()
|
||||
await self._require_runtime().queue_frame(
|
||||
@@ -852,8 +856,8 @@ class WorkflowBrain(BaseBrain):
|
||||
}
|
||||
|
||||
return FlowsFunctionSchema(
|
||||
name="request_human_handoff",
|
||||
description=(
|
||||
name=tool.function_name,
|
||||
description=tool.description or (
|
||||
"提交人工接管请求。当用户明确要求人工服务、投诉升级或 AI 无法"
|
||||
"解决时调用。该工具只提交请求,不代表人工已经接通;调用后继续"
|
||||
"回复用户并说明正在等待人工响应。"
|
||||
@@ -865,27 +869,55 @@ class WorkflowBrain(BaseBrain):
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
def _workflow_end_conversation_tool(self) -> FlowsFunctionSchema:
|
||||
def _workflow_end_conversation_tool(
|
||||
self, tool: RuntimeTool, node_id: str
|
||||
) -> FlowsFunctionSchema:
|
||||
config = (tool.definition or {}).get("config") or {}
|
||||
message_type = str(config.get("message_type") or "none")
|
||||
custom_message = str(config.get("custom_message") or "").strip()
|
||||
capture_reason = bool(config.get("capture_reason", True))
|
||||
|
||||
async def handler(args, _flow_manager):
|
||||
reason = str((args or {}).get("reason") or "end_conversation").strip()
|
||||
self._waiting_for_generated_end_speech = True
|
||||
self._require_runtime().call_end.begin(reason)
|
||||
uses_custom_message = message_type == "custom" and bool(custom_message)
|
||||
self._waiting_for_generated_end_speech = not uses_custom_message
|
||||
runtime = self._require_runtime()
|
||||
runtime.call_end.begin(reason)
|
||||
if uses_custom_message:
|
||||
await self._queue_visible_speech(
|
||||
custom_message,
|
||||
source="workflow-system-tool",
|
||||
node_id=node_id,
|
||||
)
|
||||
arm_tracked = getattr(
|
||||
runtime.call_end,
|
||||
"arm_after_tracked_speech",
|
||||
None,
|
||||
)
|
||||
if callable(arm_tracked):
|
||||
await arm_tracked()
|
||||
else:
|
||||
runtime.call_end.arm_after_speech()
|
||||
return {"status": "success", "action": "ending_call"}
|
||||
|
||||
setattr(handler, "_suppress_followup_llm", True)
|
||||
return FlowsFunctionSchema(
|
||||
name="end_conversation",
|
||||
description=(
|
||||
name=tool.function_name,
|
||||
description=tool.description or (
|
||||
"礼貌地结束本次对话。当用户明确告别、表示任务已完成"
|
||||
"或要求挂断时调用。"
|
||||
),
|
||||
properties={
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "结束对话的简短原因。",
|
||||
properties=(
|
||||
{
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "结束对话的简短原因。",
|
||||
}
|
||||
}
|
||||
},
|
||||
required=[],
|
||||
if capture_reason
|
||||
else {}
|
||||
),
|
||||
required=["reason"] if capture_reason else [],
|
||||
handler=handler,
|
||||
)
|
||||
|
||||
|
||||
@@ -266,7 +266,6 @@ async def resolve_runtime_config(
|
||||
enableInterrupt=assistant.enable_interrupt,
|
||||
turnConfig=assistant.turn_config or {},
|
||||
startup=assistant.startup or {},
|
||||
system_tools=assistant.system_tools or [],
|
||||
tools=runtime_tools,
|
||||
llm_tool_ids=llm_tool_ids,
|
||||
knowledge_base_id=assistant.knowledge_base_id,
|
||||
|
||||
@@ -11,7 +11,6 @@ from services.message_policy import (
|
||||
MESSAGE_CONFIRMATION,
|
||||
MESSAGE_PLAYBACK,
|
||||
)
|
||||
from services.system_tools import SYSTEM_TOOL_KINDS, normalize_system_tools
|
||||
|
||||
|
||||
SPEC_VERSION = "3"
|
||||
@@ -193,7 +192,7 @@ def _normalize_agent_data(data: dict[str, Any]) -> None:
|
||||
if data.get("entryMode") not in AGENT_ENTRY_MODES:
|
||||
data["entryMode"] = "wait_user"
|
||||
data.pop("entrySpeech", None)
|
||||
data["systemTools"] = list(normalize_system_tools(data.get("systemTools")))
|
||||
data.pop("systemTools", None)
|
||||
state_names = data.get("stateVariableNames")
|
||||
data["stateVariableNames"] = list(
|
||||
dict.fromkeys(str(name) for name in state_names or [] if str(name))
|
||||
@@ -439,11 +438,6 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
entry_mode = data.get("entryMode", "wait_user")
|
||||
if entry_mode not in AGENT_ENTRY_MODES:
|
||||
errors.append(f"Agent 节点 {node_id} 的进入模式无效:{entry_mode}")
|
||||
system_tools = data.get("systemTools", [])
|
||||
if not isinstance(system_tools, list) or any(
|
||||
tool not in SYSTEM_TOOL_KINDS for tool in system_tools
|
||||
):
|
||||
errors.append(f"Agent 节点 {node_id} 的系统工具配置无效")
|
||||
state_names = data.get("stateVariableNames", [])
|
||||
if not isinstance(state_names, list) or any(
|
||||
not isinstance(name, str) for name in state_names
|
||||
|
||||
@@ -6,25 +6,56 @@ from collections.abc import Iterable, Mapping
|
||||
from typing import Any
|
||||
|
||||
|
||||
SYSTEM_TOOL_KINDS = frozenset(
|
||||
{
|
||||
"end_conversation",
|
||||
"update_state",
|
||||
"skip_turn",
|
||||
"request_human_handoff",
|
||||
}
|
||||
)
|
||||
SYSTEM_TOOL_SPECS: dict[str, dict[str, str]] = {
|
||||
"end_conversation": {
|
||||
"id": "tool_end_call_default",
|
||||
"name": "结束对话",
|
||||
"function_name": "end_conversation",
|
||||
"description": (
|
||||
"礼貌地结束本次对话。当用户明确告别、表示任务已完成"
|
||||
"或要求挂断时调用。"
|
||||
),
|
||||
},
|
||||
"update_state": {
|
||||
"id": "tool_update_state_default",
|
||||
"name": "更新状态",
|
||||
"function_name": "update_state",
|
||||
"description": (
|
||||
"静默更新本次对话中已经声明并明确授权的动态变量。"
|
||||
"只提交本轮获得或确认的信息,更新后继续当前回答。"
|
||||
),
|
||||
},
|
||||
"skip_turn": {
|
||||
"id": "tool_skip_turn_default",
|
||||
"name": "跳过本轮",
|
||||
"function_name": "skip_turn",
|
||||
"description": (
|
||||
"跳过当前轮次,不生成任何语音回复。"
|
||||
"仅当用户明确要求稍等、话还没说完,或输入可确认只是噪音时调用。"
|
||||
),
|
||||
},
|
||||
"request_human_handoff": {
|
||||
"id": "tool_request_human_handoff_default",
|
||||
"name": "转接人工",
|
||||
"function_name": "request_human_handoff",
|
||||
"description": (
|
||||
"提交人工接管请求。当用户明确要求人工服务、投诉升级或 AI 无法"
|
||||
"解决时调用。该工具只提交请求,不代表人工已经接通;调用后继续"
|
||||
"回复用户并说明正在等待人工响应。"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
SYSTEM_TOOL_KINDS = frozenset(SYSTEM_TOOL_SPECS)
|
||||
|
||||
|
||||
def normalize_system_tools(values: Iterable[Any] | None) -> tuple[str, ...]:
|
||||
"""Return known tool names once each while preserving editor order."""
|
||||
return tuple(
|
||||
dict.fromkeys(
|
||||
str(value)
|
||||
for value in values or ()
|
||||
if str(value) in SYSTEM_TOOL_KINDS
|
||||
)
|
||||
)
|
||||
def system_tool_kind(definition: Mapping[str, Any] | None) -> str | None:
|
||||
"""Return the supported platform behavior declared by a System resource."""
|
||||
config = (definition or {}).get("config")
|
||||
if not isinstance(config, Mapping):
|
||||
return None
|
||||
kind = str(config.get("kind") or "")
|
||||
return kind if kind in SYSTEM_TOOL_KINDS else None
|
||||
|
||||
|
||||
def state_update_properties(
|
||||
|
||||
@@ -11,7 +11,6 @@ from typing import Any
|
||||
|
||||
from services.node_specs import normalize_graph
|
||||
from services.runtime_variables import DynamicVariableStore
|
||||
from services.system_tools import normalize_system_tools
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -25,7 +24,6 @@ class AgentStageConfig:
|
||||
vision_enabled: bool
|
||||
vision_model_resource_id: str | None
|
||||
tool_ids: tuple[str, ...]
|
||||
system_tools: tuple[str, ...]
|
||||
state_variable_names: tuple[str, ...]
|
||||
knowledge_base_id: str | None
|
||||
knowledge_mode: str
|
||||
@@ -148,9 +146,8 @@ class WorkflowEngine:
|
||||
str(source.get("visionModelResourceId") or "") or None
|
||||
),
|
||||
tool_ids=tuple(str(tool_id) for tool_id in source.get("toolIds") or []),
|
||||
# System tools and their writable state scope always belong to the
|
||||
# Agent node. They are permissions, not inheritable model config.
|
||||
system_tools=normalize_system_tools(data.get("systemTools")),
|
||||
# Variable write scope always belongs to the Agent node, even when
|
||||
# its selected tool resources come from Workflow defaults.
|
||||
state_variable_names=tuple(
|
||||
dict.fromkeys(
|
||||
str(name)
|
||||
|
||||
Reference in New Issue
Block a user