refactor: remove legacy assistant compatibility

This commit is contained in:
Xin Wang
2026-08-18 10:07:18 +08:00
parent abfcd4cd19
commit d003ee77a5
40 changed files with 505 additions and 729 deletions
+47 -241
View File
@@ -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)