feat: organize tools and enable qwen realtime calls

This commit is contained in:
Xin Wang
2026-08-14 00:15:00 +08:00
parent eb111a3890
commit 5be21f5ccc
71 changed files with 5771 additions and 2026 deletions
+50 -58
View File
@@ -11,7 +11,7 @@ from typing import Any
from uuid import uuid4
from loguru import logger
from models import AssistantConfig, RuntimeTool
from models import AssistantConfig, RuntimeTool, RuntimeToolBinding
from db.session import SessionLocal
from pipecat.flows import (
ContextStrategy,
@@ -61,7 +61,8 @@ from services.message_stage import (
confirmation_context_message,
)
from services.runtime_variables import DynamicVariableError, DynamicVariableStore
from services.system_tools import state_update_properties, system_tool_kind
from services.system_tools import runtime_system_tools, state_update_properties, system_tool_kind
from services.state_expressions import evaluate_update_rules
from services.tool_executor import ToolExecutionError, ToolExecutor
from services.tool_policy import policy_for_tool
from services.workflow.agent import WorkflowAgentStage
@@ -654,20 +655,26 @@ class WorkflowBrain(BaseBrain):
registered_names.add(function_name)
functions.append(function)
for tool_id in stage.tool_ids:
tool = self._tool_by_id.get(str(tool_id))
for raw_binding in stage.tool_bindings:
binding = RuntimeToolBinding(**raw_binding)
tool = self._tool_by_id.get(binding.tool_id)
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))
if (
tool.type == "client"
and self._require_runtime().client_tools is not None
and getattr(
self._require_runtime().client_tools,
"supports",
lambda _name: True,
)(tool.function_name) is False
):
logger.warning(f"跳过客户端未上报的工具: {tool.function_name}")
continue
if tool.type in {"http", "mcp", "client"}:
append_function(self._flow_tool(tool, node_id, binding))
for tool in runtime_system_tools(stage.built_in_tools):
append_function(self._workflow_system_tool(tool, node_id=node_id))
append_function(self._knowledge_function(node_id))
if stage.vision_enabled and self._require_runtime().vision_function:
vision_function = self._require_runtime().vision_function
@@ -765,9 +772,13 @@ class WorkflowBrain(BaseBrain):
"respond_immediately": False,
}
def _flow_tool(self, tool: RuntimeTool, node_id: str) -> FlowsFunctionSchema:
properties, required = self._tools.schema_parts(tool)
self._tools.register_secrets(tool)
def _flow_tool(
self,
tool: RuntimeTool,
node_id: str,
binding: RuntimeToolBinding,
) -> FlowsFunctionSchema:
properties, required = self._tools.schema_parts(tool, binding)
policy = policy_for_tool(tool)
async def handler(args, _flow_manager):
@@ -784,7 +795,11 @@ class WorkflowBrain(BaseBrain):
toolType=tool.type,
)
try:
result = await self._tools.execute(tool, dict(args or {}))
result = await self._tools.execute(
tool,
dict(args or {}),
binding=binding,
)
except ToolExecutionError as exc:
await self._emit_trace(
"tool_failed",
@@ -935,7 +950,6 @@ class WorkflowBrain(BaseBrain):
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 {})
@@ -943,7 +957,6 @@ class WorkflowBrain(BaseBrain):
schema = self._workflow_update_state_tool(
tool,
node_id,
state_variable_names=state_variable_names,
)
elif kind == "skip_turn":
schema = self._workflow_skip_turn_tool(tool)
@@ -962,22 +975,19 @@ class WorkflowBrain(BaseBrain):
self,
tool: RuntimeTool,
node_id: str,
*,
state_variable_names: tuple[str, ...],
) -> FlowsFunctionSchema:
allowed = frozenset(state_variable_names)
config = (tool.definition or {}).get("config") or {}
rules = list(config.get("updates") or [])
async def handler(args, _flow_manager):
values = dict(args or {})
unauthorized = sorted(set(values) - allowed)
if unauthorized:
return {
"status": "error",
"message": "状态变量未获当前节点授权: " + ",".join(unauthorized),
}
try:
values = evaluate_update_rules(
rules,
dict(self._store.values),
dict(args or {}),
)
changed = self._store.assign_declared_many(values)
except DynamicVariableError as exc:
except (DynamicVariableError, ValueError) as exc:
return {"status": "error", "message": f"状态更新失败: {exc}"}
result = {
"status": "success",
@@ -1009,7 +1019,7 @@ class WorkflowBrain(BaseBrain):
),
properties=state_update_properties(
self._cfg.dynamic_variable_definitions if self._cfg else {},
allowed_names=state_variable_names,
rules=rules,
),
required=[],
handler=handler,
@@ -1311,11 +1321,9 @@ class WorkflowBrain(BaseBrain):
"""Apply one deterministic, atomic dynamic-variable update."""
self._state.enter(node_id, WorkflowStatus.RUNNING_ACTION)
await self._emit_node_active(node_id)
raw_assignments = self._engine.data(node_id).get("assignments") or {}
rendered = self._store.render_data(raw_assignments)
if not isinstance(rendered, dict):
raise DynamicVariableError("Update State 节点赋值必须是对象")
changed = self._store.assign_declared_many(rendered)
updates = self._engine.data(node_id).get("updates") or []
values = evaluate_update_rules(updates, dict(self._store.values))
changed = self._store.assign_declared_many(values)
if changed:
await self._emit_variables(
reason="update_state",
@@ -1461,8 +1469,10 @@ class WorkflowBrain(BaseBrain):
data = self._engine.data(node_id)
runtime = self._require_runtime()
invocation_id = self._actions.new_invocation_id()
tool_id = str(data.get("toolId") or "")
tool = self._tool_by_id.get(tool_id)
raw_binding = data.get("toolBinding") or {}
binding = RuntimeToolBinding(**raw_binding)
tool_id = binding.tool_id
tool = self._tool_by_id.get(binding.tool_id)
try:
stage_result = await self._action_stages.run(
ActionStageSpec(
@@ -1470,8 +1480,7 @@ class WorkflowBrain(BaseBrain):
StageAction(
id=node_id,
tool=tool,
arguments=data.get("arguments") or {},
result_assignments=self._action_result_assignments(data),
binding=binding,
invocation_id=invocation_id,
),
),
@@ -1633,23 +1642,6 @@ class WorkflowBrain(BaseBrain):
}[outcome.status]
await self._emit_trace(event, nodeId=node_id, outcome=outcome.trace_payload())
@staticmethod
def _action_result_assignments(
data: dict[str, Any],
) -> dict[str, str] | None:
"""Resolve node mapping semantics for ToolExecutor.
``None`` means inherit the reusable tool's mapping, while an empty
dictionary explicitly disables all result assignments.
"""
mode = str(data.get("resultAssignmentMode") or "none")
if mode == "inherit":
return None
if mode == "override":
assignments = data.get("resultAssignments")
return dict(assignments) if isinstance(assignments, dict) else {}
return {}
async def _enter_handoff(self, node_id: str) -> None:
self._state.enter(node_id, WorkflowStatus.HANDOFF)
await self._emit_node_active(node_id)