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
+42 -49
View File
@@ -1,4 +1,4 @@
"""Workflow v3 node catalog, v2 compatibility normalization, and validation."""
"""Workflow v4 node catalog, normalization, and validation."""
from __future__ import annotations
@@ -14,7 +14,7 @@ from services.message_policy import (
)
SPEC_VERSION = "3"
SPEC_VERSION = "4"
NODE_TYPES = {
"start",
"agent",
@@ -28,7 +28,6 @@ EDGE_MODES = {"llm", "expression", "always"}
WORKFLOW_RUNTIME_MODES = {"pipeline", "realtime"}
WORKFLOW_LLM_ROUTING_MODES = {"llm_router", "edge_tool"}
AGENT_ENTRY_MODES = {"wait_user", "generate"}
ACTION_RESULT_ASSIGNMENT_MODES = {"inherit", "override", "none"}
ACTION_USER_INPUT_POLICIES = {"queue", "block"}
AUTOMATIC_NODE_TYPES = {
"start",
@@ -195,11 +194,8 @@ 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.pop("systemTools", None)
state_names = data.get("stateVariableNames")
data["stateVariableNames"] = list(
dict.fromkeys(str(name) for name in state_names or [] if str(name))
)
data.setdefault("toolBindings", [])
data.setdefault("builtInTools", {})
legacy_prompt = str(data.pop("visionFocusPrompt", "") or "").strip()
if "visionPrompt" not in data:
data["visionPrompt"] = legacy_prompt
@@ -215,35 +211,30 @@ def _normalize_agent_data(data: dict[str, Any]) -> None:
data.get("visionModelResourceId"),
data.get("visionPrompt"),
data.get("knowledgeBaseId"),
data.get("toolIds"),
data.get("toolBindings"),
)
)
data["inheritGlobalConfig"] = not has_node_overrides
def _normalize_action_data(data: dict[str, Any]) -> None:
"""Add Action defaults while preserving the behavior of saved graphs.
Older Action nodes always passed an empty mapping when no node-level
assignments were configured, so they did *not* inherit the reusable
tool's assignments. Infer ``none`` for that shape and ``override`` for
an existing non-empty mapping. Newly created nodes should persist their
intended mode explicitly.
"""
if "resultAssignmentMode" not in data:
assignments = data.get("resultAssignments")
data["resultAssignmentMode"] = (
"override" if isinstance(assignments, dict) and assignments else "none"
)
data.setdefault("resultAssignments", {})
"""Add deterministic Action defaults."""
data.setdefault(
"toolBinding",
{
"toolId": "",
"argumentBindings": {},
"templateBindings": {},
"resultAssignments": {},
},
)
data.setdefault("userInputPolicy", "queue")
data.pop("speech", None)
def _normalize_update_state_data(data: dict[str, Any]) -> None:
"""Keep deterministic state updates as one explicit assignment object."""
assignments = data.get("assignments")
data["assignments"] = dict(assignments) if isinstance(assignments, dict) else {}
"""Keep deterministic state updates as typed expression rules."""
data.setdefault("updates", [])
def _normalize_message_data(data: dict[str, Any]) -> None:
@@ -278,7 +269,8 @@ def _normalize_settings(settings: dict[str, Any], *, global_prompt: str = "") ->
settings.setdefault("visionPrompt", legacy_prompt)
if "visionPromptMode" not in settings:
settings["visionPromptMode"] = "guided" if legacy_prompt else "runtime"
settings.setdefault("toolIds", [])
settings.setdefault("toolBindings", [])
settings.setdefault("builtInTools", {})
settings.setdefault("knowledgeBaseId", "")
settings.setdefault("knowledgeMode", "automatic")
settings.setdefault("knowledgeTopN", 5)
@@ -288,7 +280,7 @@ def _normalize_settings(settings: dict[str, Any], *, global_prompt: str = "") ->
def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
"""Return a deep-copied v3 graph; preserve v3 IDs and migrate v2 semantics."""
"""Return a deep-copied v4 graph."""
source = deepcopy(graph or {})
if str(source.get("specVersion") or "") == SPEC_VERSION:
settings = source.setdefault("settings", {})
@@ -402,7 +394,7 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
settings = deepcopy(source.get("settings") or {})
_normalize_settings(settings, global_prompt=global_prompt)
return {
"specVersion": 3,
"specVersion": 4,
"settings": settings,
"nodes": mapped_nodes,
"edges": mapped_edges,
@@ -475,12 +467,11 @@ 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}")
state_names = data.get("stateVariableNames", [])
if not isinstance(state_names, list) or any(
not isinstance(name, str) for name in state_names
):
errors.append(f"Agent 节点 {node_id} 的状态变量授权必须是列表")
source = settings if data.get("inheritGlobalConfig", True) else data
if not isinstance(source.get("toolBindings"), list):
errors.append(f"Agent 节点 {node_id} 的工具绑定必须是列表")
if not isinstance(source.get("builtInTools"), dict):
errors.append(f"Agent 节点 {node_id} 的内建工具配置必须是对象")
vision_prompt_mode = str(source.get("visionPromptMode") or "runtime")
if vision_prompt_mode not in VISION_PROMPT_MODES:
errors.append(
@@ -562,13 +553,9 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
)
elif node_type == "action":
data = node.get("data") or {}
assignment_mode = data.get("resultAssignmentMode")
if assignment_mode not in ACTION_RESULT_ASSIGNMENT_MODES:
errors.append(
f"Action 节点 {node_id} 的结果变量模式无效:{assignment_mode}"
)
if not isinstance(data.get("resultAssignments"), dict):
errors.append(f"Action 节点 {node_id} 的结果变量映射必须是对象")
binding = data.get("toolBinding")
if not isinstance(binding, dict) or not binding.get("toolId"):
errors.append(f"Action 节点 {node_id} 必须配置工具绑定")
input_policy = data.get("userInputPolicy")
if input_policy not in ACTION_USER_INPUT_POLICIES:
errors.append(
@@ -580,10 +567,10 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
)
elif node_type == "update_state":
data = node.get("data") or {}
assignments = data.get("assignments")
if not isinstance(assignments, dict) or not assignments:
updates = data.get("updates")
if not isinstance(updates, list) or not updates:
errors.append(
f"Update State 节点 {node_id} 必须配置至少一个变量赋值"
f"Update State 节点 {node_id} 必须配置至少一条更新规则"
)
if counts["start"] != 1:
@@ -729,7 +716,11 @@ def graph_references(graph: dict[str, Any]) -> dict[str, set[str]]:
)
if value
}
tools: set[str] = {str(tool_id) for tool_id in settings.get("toolIds") or []}
tools: set[str] = {
str(binding.get("toolId") or "")
for binding in settings.get("toolBindings") or []
if isinstance(binding, dict) and binding.get("toolId")
}
knowledge: set[str] = (
{str(settings["knowledgeBaseId"])}
if settings.get("knowledgeBaseId")
@@ -753,10 +744,12 @@ def graph_references(graph: dict[str, Any]) -> dict[str, set[str]]:
):
if resource_id:
resources.add(str(resource_id))
for tool_id in data.get("toolIds") or []:
tools.add(str(tool_id))
for binding in data.get("toolBindings") or []:
if isinstance(binding, dict) and binding.get("toolId"):
tools.add(str(binding["toolId"]))
if data.get("knowledgeBaseId"):
knowledge.add(str(data["knowledgeBaseId"]))
if data.get("toolId"):
tools.add(str(data["toolId"]))
binding = data.get("toolBinding")
if isinstance(binding, dict) and binding.get("toolId"):
tools.add(str(binding["toolId"]))
return {"model_resources": resources, "tools": tools, "knowledge_bases": knowledge}