feat: add deterministic message interaction stages
This commit is contained in:
@@ -8,12 +8,12 @@ from typing import Any
|
||||
|
||||
|
||||
SPEC_VERSION = "3"
|
||||
NODE_TYPES = {"start", "agent", "action", "handoff", "end"}
|
||||
NODE_TYPES = {"start", "agent", "message", "action", "handoff", "end"}
|
||||
EDGE_MODES = {"llm", "expression", "always"}
|
||||
AGENT_ENTRY_MODES = {"wait_user", "generate", "fixed_speech"}
|
||||
ACTION_RESULT_ASSIGNMENT_MODES = {"inherit", "override", "none"}
|
||||
ACTION_USER_INPUT_POLICIES = {"queue", "block"}
|
||||
AUTOMATIC_NODE_TYPES = {"start", "action", "handoff"}
|
||||
AUTOMATIC_NODE_TYPES = {"start", "message", "action", "handoff"}
|
||||
EXPRESSION_OPERATORS = {
|
||||
"eq",
|
||||
"neq",
|
||||
@@ -31,7 +31,7 @@ NODE_SPECS: list[dict[str, Any]] = [
|
||||
"name": "start",
|
||||
"displayName": "Start",
|
||||
"category": "control_node",
|
||||
"description": "初始化会话、动态变量和全局观察器,可播放固定开场白。",
|
||||
"description": "初始化会话、动态变量和全局观察器。",
|
||||
"icon": "Play",
|
||||
"accent": "mint",
|
||||
"addable": False,
|
||||
@@ -44,7 +44,6 @@ NODE_SPECS: list[dict[str, Any]] = [
|
||||
},
|
||||
"fields": [
|
||||
{"key": "name", "label": "节点名称", "type": "text", "default": "Start"},
|
||||
{"key": "greeting", "label": "固定开场白", "type": "textarea", "default": ""},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -67,6 +66,19 @@ NODE_SPECS: list[dict[str, Any]] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"displayName": "Message",
|
||||
"category": "interaction_node",
|
||||
"description": "固定播报,并可同时显示内置客户端消息、等待用户确认。",
|
||||
"icon": "MessageSquareText",
|
||||
"accent": "lavender",
|
||||
"addable": True,
|
||||
"constraints": {"minIncoming": 1, "minOutgoing": 0},
|
||||
"fields": [
|
||||
{"key": "name", "label": "节点名称", "type": "text", "default": "Message"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "action",
|
||||
"displayName": "Action",
|
||||
@@ -172,6 +184,17 @@ def _normalize_action_data(data: dict[str, Any]) -> None:
|
||||
)
|
||||
data.setdefault("resultAssignments", {})
|
||||
data.setdefault("userInputPolicy", "queue")
|
||||
data.pop("speech", None)
|
||||
|
||||
|
||||
def _normalize_message_data(data: dict[str, Any]) -> None:
|
||||
"""Fill the small built-in Message contract used by runtime and editor."""
|
||||
data.setdefault("speech", "")
|
||||
data.setdefault("showMessage", False)
|
||||
data.setdefault("title", "重要提示")
|
||||
data.setdefault("message", "")
|
||||
data.setdefault("confirmLabel", "确认")
|
||||
data.setdefault("requireConfirmation", False)
|
||||
|
||||
|
||||
def _normalize_settings(settings: dict[str, Any], *, global_prompt: str = "") -> None:
|
||||
@@ -200,8 +223,12 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
source.setdefault("edges", [])
|
||||
for node in source["nodes"]:
|
||||
data = node.setdefault("data", {})
|
||||
if node.get("type") == "agent":
|
||||
if node.get("type") == "start":
|
||||
data.pop("greeting", None)
|
||||
elif node.get("type") == "agent":
|
||||
_normalize_agent_data(data)
|
||||
elif node.get("type") == "message":
|
||||
_normalize_message_data(data)
|
||||
elif node.get("type") == "action":
|
||||
_normalize_action_data(data)
|
||||
return source
|
||||
@@ -218,6 +245,7 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"endCall": "end",
|
||||
"start": "start",
|
||||
"agent": "agent",
|
||||
"message": "message",
|
||||
"action": "action",
|
||||
"handoff": "handoff",
|
||||
"end": "end",
|
||||
@@ -236,10 +264,13 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
data.setdefault("scope", "session")
|
||||
elif new_type == "agent":
|
||||
_normalize_agent_data(data)
|
||||
elif new_type == "message":
|
||||
_normalize_message_data(data)
|
||||
elif new_type == "action":
|
||||
_normalize_action_data(data)
|
||||
elif new_type == "start":
|
||||
prompt = str(data.pop("prompt", "") or "").strip()
|
||||
data.pop("greeting", None)
|
||||
if prompt:
|
||||
start_prompt_nodes[str(node.get("id"))] = prompt
|
||||
for key in ("allowInterrupt", "addGlobalPrompt"):
|
||||
@@ -349,6 +380,49 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
data.get("entrySpeech") or ""
|
||||
).strip():
|
||||
errors.append(f"Agent 节点 {node_id} 的固定进入语不能为空")
|
||||
elif node_type == "message":
|
||||
data = node.get("data") or {}
|
||||
speech = data.get("speech")
|
||||
show_message = data.get("showMessage")
|
||||
require_confirmation = data.get("requireConfirmation")
|
||||
if not isinstance(speech, str):
|
||||
errors.append(f"Message 节点 {node_id} 的播报内容必须是文本")
|
||||
if not isinstance(show_message, bool):
|
||||
errors.append(f"Message 节点 {node_id} 的弹窗开关必须是布尔值")
|
||||
if not isinstance(require_confirmation, bool):
|
||||
errors.append(f"Message 节点 {node_id} 的确认开关必须是布尔值")
|
||||
if require_confirmation and show_message is not True:
|
||||
errors.append(f"Message 节点 {node_id} 等待确认时必须显示弹窗")
|
||||
if not str(speech or "").strip() and show_message is not True:
|
||||
errors.append(f"Message 节点 {node_id} 至少需要播报或显示弹窗")
|
||||
if show_message is True:
|
||||
title = data.get("title")
|
||||
message = data.get("message")
|
||||
confirm_label = data.get("confirmLabel")
|
||||
if (
|
||||
not isinstance(title, str)
|
||||
or not title.strip()
|
||||
or len(title) > 120
|
||||
):
|
||||
errors.append(
|
||||
f"Message 节点 {node_id} 的弹窗标题必须为 1-120 个字符"
|
||||
)
|
||||
if (
|
||||
not isinstance(message, str)
|
||||
or not message.strip()
|
||||
or len(message) > 2000
|
||||
):
|
||||
errors.append(
|
||||
f"Message 节点 {node_id} 的弹窗消息必须为 1-2000 个字符"
|
||||
)
|
||||
if (
|
||||
not isinstance(confirm_label, str)
|
||||
or not confirm_label.strip()
|
||||
or len(confirm_label) > 40
|
||||
):
|
||||
errors.append(
|
||||
f"Message 节点 {node_id} 的按钮文字必须为 1-40 个字符"
|
||||
)
|
||||
elif node_type == "action":
|
||||
data = node.get("data") or {}
|
||||
assignment_mode = data.get("resultAssignmentMode")
|
||||
@@ -477,7 +551,7 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
if node.get("type") != "agent"
|
||||
)
|
||||
if any(visit(node_id) for node_id in automatic_node_ids):
|
||||
errors.append("Start/Action/Handoff/End 之间不能形成无等待循环")
|
||||
errors.append("自动节点之间不能形成无等待循环")
|
||||
return list(dict.fromkeys(errors))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user