feat: organize tools and enable qwen realtime calls
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user