feat: add system tools and state updates
This commit is contained in:
@@ -11,15 +11,30 @@ from services.message_policy import (
|
||||
MESSAGE_CONFIRMATION,
|
||||
MESSAGE_PLAYBACK,
|
||||
)
|
||||
from services.system_tools import SYSTEM_TOOL_KINDS, normalize_system_tools
|
||||
|
||||
|
||||
SPEC_VERSION = "3"
|
||||
NODE_TYPES = {"start", "agent", "message", "action", "handoff", "end"}
|
||||
NODE_TYPES = {
|
||||
"start",
|
||||
"agent",
|
||||
"message",
|
||||
"action",
|
||||
"update_state",
|
||||
"handoff",
|
||||
"end",
|
||||
}
|
||||
EDGE_MODES = {"llm", "expression", "always"}
|
||||
AGENT_ENTRY_MODES = {"wait_user", "generate"}
|
||||
ACTION_RESULT_ASSIGNMENT_MODES = {"inherit", "override", "none"}
|
||||
ACTION_USER_INPUT_POLICIES = {"queue", "block"}
|
||||
AUTOMATIC_NODE_TYPES = {"start", "message", "action", "handoff"}
|
||||
AUTOMATIC_NODE_TYPES = {
|
||||
"start",
|
||||
"message",
|
||||
"action",
|
||||
"update_state",
|
||||
"handoff",
|
||||
}
|
||||
EXPRESSION_OPERATORS = {
|
||||
"eq",
|
||||
"neq",
|
||||
@@ -98,6 +113,24 @@ NODE_SPECS: list[dict[str, Any]] = [
|
||||
{"key": "name", "label": "节点名称", "type": "text", "default": "Action"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "update_state",
|
||||
"displayName": "Update State",
|
||||
"category": "execution_node",
|
||||
"description": "原子更新已声明的动态变量,然后使用新状态继续路由。",
|
||||
"icon": "Braces",
|
||||
"accent": "mint",
|
||||
"addable": True,
|
||||
"constraints": {"minIncoming": 1, "minOutgoing": 0},
|
||||
"fields": [
|
||||
{
|
||||
"key": "name",
|
||||
"label": "节点名称",
|
||||
"type": "text",
|
||||
"default": "Update State",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "handoff",
|
||||
"displayName": "Handoff",
|
||||
@@ -160,6 +193,11 @@ 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")))
|
||||
state_names = data.get("stateVariableNames")
|
||||
data["stateVariableNames"] = list(
|
||||
dict.fromkeys(str(name) for name in state_names or [] if str(name))
|
||||
)
|
||||
if "inheritGlobalConfig" not in data:
|
||||
has_node_overrides = any(
|
||||
(
|
||||
@@ -194,6 +232,12 @@ def _normalize_action_data(data: dict[str, Any]) -> None:
|
||||
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 {}
|
||||
|
||||
|
||||
def _normalize_message_data(data: dict[str, Any]) -> None:
|
||||
"""Fill the small built-in Message contract used by runtime and editor."""
|
||||
data.setdefault("speech", "")
|
||||
@@ -246,6 +290,8 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
_normalize_message_data(data)
|
||||
elif node.get("type") == "action":
|
||||
_normalize_action_data(data)
|
||||
elif node.get("type") == "update_state":
|
||||
_normalize_update_state_data(data)
|
||||
return source
|
||||
|
||||
nodes = source.get("nodes") or []
|
||||
@@ -262,6 +308,7 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"agent": "agent",
|
||||
"message": "message",
|
||||
"action": "action",
|
||||
"update_state": "update_state",
|
||||
"handoff": "handoff",
|
||||
"end": "end",
|
||||
}
|
||||
@@ -283,6 +330,8 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
_normalize_message_data(data)
|
||||
elif new_type == "action":
|
||||
_normalize_action_data(data)
|
||||
elif new_type == "update_state":
|
||||
_normalize_update_state_data(data)
|
||||
elif new_type == "start":
|
||||
prompt = str(data.pop("prompt", "") or "").strip()
|
||||
data.pop("greeting", None)
|
||||
@@ -390,6 +439,16 @@ 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
|
||||
):
|
||||
errors.append(f"Agent 节点 {node_id} 的状态变量授权必须是列表")
|
||||
elif node_type == "message":
|
||||
data = node.get("data") or {}
|
||||
speech = data.get("speech")
|
||||
@@ -447,6 +506,13 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
errors.append(
|
||||
f"Action 节点 {node_id} 的用户输入策略无效:{input_policy}"
|
||||
)
|
||||
elif node_type == "update_state":
|
||||
data = node.get("data") or {}
|
||||
assignments = data.get("assignments")
|
||||
if not isinstance(assignments, dict) or not assignments:
|
||||
errors.append(
|
||||
f"Update State 节点 {node_id} 必须配置至少一个变量赋值"
|
||||
)
|
||||
|
||||
if counts["start"] != 1:
|
||||
errors.append("工作流必须有且仅有一个 Start 节点")
|
||||
|
||||
Reference in New Issue
Block a user