refactor: remove legacy assistant compatibility
This commit is contained in:
+47
-241
@@ -10,7 +10,6 @@ from models import VISION_PROMPT_MODES
|
||||
from services.message_policy import (
|
||||
MESSAGE_COMPLETION_POLICIES,
|
||||
MESSAGE_CONFIRMATION,
|
||||
MESSAGE_PLAYBACK,
|
||||
)
|
||||
|
||||
|
||||
@@ -170,236 +169,9 @@ def node_types_response() -> dict[str, Any]:
|
||||
return {"specVersion": SPEC_VERSION, "nodeTypes": NODE_SPECS}
|
||||
|
||||
|
||||
def _edge_data_v3(edge: dict) -> dict:
|
||||
data = deepcopy(edge.get("data") or {})
|
||||
data.pop("transitionSpeech", None)
|
||||
data.pop("transition_speech", None)
|
||||
if data.get("mode") in EDGE_MODES:
|
||||
data.setdefault("priority", 10)
|
||||
return data
|
||||
condition = str(data.pop("condition", "") or "").strip()
|
||||
data.update(
|
||||
{
|
||||
"mode": "llm" if condition else "always",
|
||||
"priority": 10,
|
||||
"condition": condition,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
def _normalize_agent_data(data: dict[str, Any]) -> None:
|
||||
"""Keep Agent entry focused on conversation, not fixed interaction."""
|
||||
data.setdefault("contextPolicy", "inherit")
|
||||
if data.get("entryMode") not in AGENT_ENTRY_MODES:
|
||||
data["entryMode"] = "wait_user"
|
||||
data.pop("entrySpeech", None)
|
||||
data.setdefault("toolBindings", [])
|
||||
data.setdefault("builtInTools", {})
|
||||
legacy_prompt = str(data.pop("visionFocusPrompt", "") or "").strip()
|
||||
if "visionPrompt" not in data:
|
||||
data["visionPrompt"] = legacy_prompt
|
||||
if "visionPromptMode" not in data:
|
||||
data["visionPromptMode"] = "guided" if legacy_prompt else "runtime"
|
||||
if "inheritGlobalConfig" not in data:
|
||||
has_node_overrides = any(
|
||||
(
|
||||
data.get("llmResourceId"),
|
||||
data.get("asrResourceId"),
|
||||
data.get("ttsResourceId"),
|
||||
data.get("visionEnabled"),
|
||||
data.get("visionModelResourceId"),
|
||||
data.get("visionPrompt"),
|
||||
data.get("knowledgeBaseId"),
|
||||
data.get("toolBindings"),
|
||||
)
|
||||
)
|
||||
data["inheritGlobalConfig"] = not has_node_overrides
|
||||
|
||||
|
||||
def _normalize_action_data(data: dict[str, Any]) -> None:
|
||||
"""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 typed expression rules."""
|
||||
data.setdefault("updates", [])
|
||||
|
||||
|
||||
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("title", "重要提示")
|
||||
data.setdefault("message", "")
|
||||
data.setdefault("confirmLabel", "确认")
|
||||
if "completionPolicy" not in data:
|
||||
data["completionPolicy"] = (
|
||||
MESSAGE_CONFIRMATION
|
||||
if data.get("requireConfirmation") is True
|
||||
else MESSAGE_PLAYBACK
|
||||
)
|
||||
data.pop("requireConfirmation", None)
|
||||
data.pop("showMessage", None)
|
||||
|
||||
|
||||
def _normalize_settings(settings: dict[str, Any], *, global_prompt: str = "") -> None:
|
||||
# Missing values belong to graphs saved before workflow runtime/routing
|
||||
# became explicit. Preserve their dedicated-router behavior.
|
||||
settings.setdefault("runtimeMode", "pipeline")
|
||||
settings.setdefault("llmRoutingMode", "llm_router")
|
||||
settings.setdefault("globalPrompt", global_prompt)
|
||||
settings.setdefault("defaultRealtimeResourceId", "")
|
||||
settings.setdefault("defaultLlmResourceId", "")
|
||||
settings.setdefault("defaultAsrResourceId", "")
|
||||
settings.setdefault("defaultTtsResourceId", "")
|
||||
settings.setdefault("visionEnabled", False)
|
||||
settings.setdefault("visionModelResourceId", "")
|
||||
legacy_prompt = str(settings.pop("visionFocusPrompt", "") or "").strip()
|
||||
settings.setdefault("visionPrompt", legacy_prompt)
|
||||
if "visionPromptMode" not in settings:
|
||||
settings["visionPromptMode"] = "guided" if legacy_prompt else "runtime"
|
||||
settings.setdefault("toolBindings", [])
|
||||
settings.setdefault("builtInTools", {})
|
||||
settings.setdefault("knowledgeBaseId", "")
|
||||
settings.setdefault("knowledgeMode", "automatic")
|
||||
settings.setdefault("knowledgeTopN", 5)
|
||||
settings.setdefault("knowledgeScoreThreshold", 0.0)
|
||||
settings.setdefault("enableInterrupt", True)
|
||||
settings.setdefault("turnConfig", {})
|
||||
|
||||
|
||||
def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""Return a deep-copied v4 graph."""
|
||||
source = deepcopy(graph or {})
|
||||
if str(source.get("specVersion") or "") == SPEC_VERSION:
|
||||
settings = source.setdefault("settings", {})
|
||||
_normalize_settings(settings)
|
||||
source.setdefault("nodes", [])
|
||||
source.setdefault("edges", [])
|
||||
for edge in source["edges"]:
|
||||
edge["data"] = _edge_data_v3(edge)
|
||||
for node in source["nodes"]:
|
||||
data = node.setdefault("data", {})
|
||||
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)
|
||||
elif node.get("type") == "update_state":
|
||||
_normalize_update_state_data(data)
|
||||
return source
|
||||
|
||||
nodes = source.get("nodes") or []
|
||||
edges = source.get("edges") or []
|
||||
global_prompt = ""
|
||||
mapped_nodes: list[dict] = []
|
||||
start_prompt_nodes: dict[str, str] = {}
|
||||
|
||||
type_map = {
|
||||
"startCall": "start",
|
||||
"agentNode": "agent",
|
||||
"endCall": "end",
|
||||
"start": "start",
|
||||
"agent": "agent",
|
||||
"message": "message",
|
||||
"action": "action",
|
||||
"update_state": "update_state",
|
||||
"handoff": "handoff",
|
||||
"end": "end",
|
||||
}
|
||||
for node in nodes:
|
||||
old_type = str(node.get("type") or "")
|
||||
data = deepcopy(node.get("data") or {})
|
||||
if old_type == "globalNode":
|
||||
global_prompt = str(data.get("prompt") or "")
|
||||
continue
|
||||
new_type = type_map.get(old_type, old_type)
|
||||
migrated = deepcopy(node)
|
||||
migrated["type"] = new_type
|
||||
if new_type == "end":
|
||||
data["message"] = data.pop("message", data.pop("prompt", ""))
|
||||
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 == "update_state":
|
||||
_normalize_update_state_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"):
|
||||
data.pop(key, None)
|
||||
migrated["data"] = data
|
||||
mapped_nodes.append(migrated)
|
||||
|
||||
mapped_edges: list[dict] = []
|
||||
for edge in edges:
|
||||
migrated = deepcopy(edge)
|
||||
migrated["data"] = _edge_data_v3(migrated)
|
||||
mapped_edges.append(migrated)
|
||||
|
||||
# A v2 Start was conversational. Insert a synthetic Agent so its prompt remains active.
|
||||
for start_id, prompt in start_prompt_nodes.items():
|
||||
synthetic_id = f"{start_id}-migrated-agent"
|
||||
start_node = next((n for n in mapped_nodes if n.get("id") == start_id), None)
|
||||
position = (start_node or {}).get("position") or {"x": 100, "y": 120}
|
||||
mapped_nodes.append(
|
||||
{
|
||||
"id": synthetic_id,
|
||||
"type": "agent",
|
||||
"position": {"x": position.get("x", 100) + 300, "y": position.get("y", 120)},
|
||||
"data": {
|
||||
"name": "迁移的开场 Agent",
|
||||
"prompt": prompt,
|
||||
"contextPolicy": "inherit",
|
||||
"inheritGlobalConfig": True,
|
||||
"entryMode": "wait_user",
|
||||
},
|
||||
}
|
||||
)
|
||||
for edge in mapped_edges:
|
||||
if edge.get("source") == start_id:
|
||||
edge["source"] = synthetic_id
|
||||
edge["data"] = _edge_data_v3(edge)
|
||||
if str(edge["data"].get("condition") or "").strip():
|
||||
edge["data"]["mode"] = "llm"
|
||||
mapped_edges.append(
|
||||
{
|
||||
"id": f"e-{start_id}-{synthetic_id}",
|
||||
"source": start_id,
|
||||
"target": synthetic_id,
|
||||
"data": {"mode": "always", "priority": 0},
|
||||
}
|
||||
)
|
||||
|
||||
settings = deepcopy(source.get("settings") or {})
|
||||
_normalize_settings(settings, global_prompt=global_prompt)
|
||||
return {
|
||||
"specVersion": 4,
|
||||
"settings": settings,
|
||||
"nodes": mapped_nodes,
|
||||
"edges": mapped_edges,
|
||||
**({"viewport": deepcopy(source["viewport"])} if source.get("viewport") else {}),
|
||||
}
|
||||
def copy_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""Return an isolated copy of a current Workflow graph."""
|
||||
return deepcopy(graph or {})
|
||||
|
||||
|
||||
def _validate_expression(expression: Any) -> list[str]:
|
||||
@@ -421,16 +193,50 @@ def _validate_expression(expression: Any) -> list[str]:
|
||||
|
||||
|
||||
def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
graph = normalize_graph(graph)
|
||||
nodes = graph.get("nodes") or []
|
||||
edges = graph.get("edges") or []
|
||||
if not nodes:
|
||||
return []
|
||||
|
||||
graph = copy_graph(graph)
|
||||
errors: list[str] = []
|
||||
settings = graph.get("settings") or {}
|
||||
runtime_mode = str(settings.get("runtimeMode") or "pipeline")
|
||||
llm_routing_mode = str(settings.get("llmRoutingMode") or "llm_router")
|
||||
if str(graph.get("specVersion") or "") != SPEC_VERSION:
|
||||
errors.append(f"工作流只支持 specVersion {SPEC_VERSION}")
|
||||
nodes = graph.get("nodes")
|
||||
edges = graph.get("edges")
|
||||
settings = graph.get("settings")
|
||||
if not isinstance(nodes, list) or not nodes:
|
||||
errors.append("工作流必须包含节点")
|
||||
nodes = []
|
||||
if not isinstance(edges, list):
|
||||
errors.append("工作流 edges 必须是列表")
|
||||
edges = []
|
||||
if not isinstance(settings, dict):
|
||||
errors.append("工作流 settings 必须是对象")
|
||||
settings = {}
|
||||
|
||||
required_settings = {
|
||||
"runtimeMode",
|
||||
"llmRoutingMode",
|
||||
"globalPrompt",
|
||||
"defaultRealtimeResourceId",
|
||||
"defaultLlmResourceId",
|
||||
"defaultAsrResourceId",
|
||||
"defaultTtsResourceId",
|
||||
"visionEnabled",
|
||||
"visionModelResourceId",
|
||||
"visionPromptMode",
|
||||
"visionPrompt",
|
||||
"toolBindings",
|
||||
"builtInTools",
|
||||
"knowledgeBaseId",
|
||||
"knowledgeMode",
|
||||
"knowledgeTopN",
|
||||
"knowledgeScoreThreshold",
|
||||
"enableInterrupt",
|
||||
"turnConfig",
|
||||
}
|
||||
missing_settings = sorted(required_settings - settings.keys())
|
||||
if missing_settings:
|
||||
errors.append("工作流 settings 缺少字段:" + ", ".join(missing_settings))
|
||||
|
||||
runtime_mode = str(settings.get("runtimeMode") or "")
|
||||
llm_routing_mode = str(settings.get("llmRoutingMode") or "")
|
||||
if runtime_mode not in WORKFLOW_RUNTIME_MODES:
|
||||
errors.append(f"工作流运行模式无效:{runtime_mode}")
|
||||
if llm_routing_mode not in WORKFLOW_LLM_ROUTING_MODES:
|
||||
@@ -699,7 +505,7 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
|
||||
def graph_references(graph: dict[str, Any]) -> dict[str, set[str]]:
|
||||
"""Collect externally referenced IDs for save/runtime validation."""
|
||||
normalized = normalize_graph(graph)
|
||||
normalized = copy_graph(graph)
|
||||
settings = normalized.get("settings") or {}
|
||||
resources = {
|
||||
str(value)
|
||||
|
||||
Reference in New Issue
Block a user