Enhance workflow routing and agent configuration management
- Introduce WorkflowLLMRouter for pre-response LLM routing, allowing agents to determine the appropriate function to call based on user input. - Implement UserTurnRoutingProcessor to manage user turns before reaching the LLM, ensuring proper routing and handling of user messages. - Refactor WorkflowBrain to integrate new routing logic and enhance agent stage configuration, including entry modes and resource management. - Update service factory to support dynamic LLM resource configuration based on workflow settings. - Add tests for new routing functionality and ensure proper handling of user messages in various scenarios.
This commit is contained in:
@@ -10,6 +10,8 @@ from typing import Any
|
||||
SPEC_VERSION = "3"
|
||||
NODE_TYPES = {"start", "agent", "action", "handoff", "end"}
|
||||
EDGE_MODES = {"llm", "expression", "always"}
|
||||
AGENT_ENTRY_MODES = {"wait_user", "generate", "fixed_speech"}
|
||||
AUTOMATIC_NODE_TYPES = {"start", "action", "handoff"}
|
||||
EXPRESSION_OPERATORS = {
|
||||
"eq",
|
||||
"neq",
|
||||
@@ -34,7 +36,7 @@ NODE_SPECS: list[dict[str, Any]] = [
|
||||
"constraints": {
|
||||
"minIncoming": 0,
|
||||
"maxIncoming": 0,
|
||||
"minOutgoing": 1,
|
||||
"minOutgoing": 0,
|
||||
"minInstances": 1,
|
||||
"maxInstances": 1,
|
||||
},
|
||||
@@ -51,7 +53,7 @@ NODE_SPECS: list[dict[str, Any]] = [
|
||||
"icon": "Bot",
|
||||
"accent": "sky",
|
||||
"addable": True,
|
||||
"constraints": {"minIncoming": 1, "minOutgoing": 1},
|
||||
"constraints": {"minIncoming": 1, "minOutgoing": 0},
|
||||
"fields": [
|
||||
{"key": "name", "label": "节点名称", "type": "text", "default": "Agent"},
|
||||
{
|
||||
@@ -84,7 +86,7 @@ NODE_SPECS: list[dict[str, Any]] = [
|
||||
"icon": "PhoneForwarded",
|
||||
"accent": "lavender",
|
||||
"addable": True,
|
||||
"constraints": {"minIncoming": 1, "minOutgoing": 1},
|
||||
"constraints": {"minIncoming": 1, "minOutgoing": 0},
|
||||
"fields": [
|
||||
{"key": "name", "label": "节点名称", "type": "text", "default": "Handoff"},
|
||||
{"key": "target", "label": "转交目标", "type": "text", "default": ""},
|
||||
@@ -132,13 +134,49 @@ def _edge_data_v3(edge: dict, source_type: str) -> dict:
|
||||
return data
|
||||
|
||||
|
||||
def _normalize_agent_data(data: dict[str, Any]) -> None:
|
||||
"""Add v3 Agent defaults without changing existing node-level behavior."""
|
||||
data.setdefault("contextPolicy", "inherit")
|
||||
data.setdefault("entryMode", "wait_user")
|
||||
data.setdefault("entrySpeech", "")
|
||||
if "inheritGlobalConfig" not in data:
|
||||
has_node_overrides = any(
|
||||
(
|
||||
data.get("llmResourceId"),
|
||||
data.get("asrResourceId"),
|
||||
data.get("ttsResourceId"),
|
||||
data.get("knowledgeBaseId"),
|
||||
data.get("toolIds"),
|
||||
)
|
||||
)
|
||||
data["inheritGlobalConfig"] = not has_node_overrides
|
||||
|
||||
|
||||
def _normalize_settings(settings: dict[str, Any], *, global_prompt: str = "") -> None:
|
||||
settings.setdefault("globalPrompt", global_prompt)
|
||||
settings.setdefault("defaultLlmResourceId", "")
|
||||
settings.setdefault("defaultAsrResourceId", "")
|
||||
settings.setdefault("defaultTtsResourceId", "")
|
||||
settings.setdefault("toolIds", [])
|
||||
settings.setdefault("knowledgeBaseId", "")
|
||||
settings.setdefault("knowledgeMode", "automatic")
|
||||
settings.setdefault("knowledgeTopN", 5)
|
||||
settings.setdefault("knowledgeScoreThreshold", 0.0)
|
||||
|
||||
|
||||
def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"""Return a deep-copied v3 graph; preserve v3 IDs and migrate v2 semantics."""
|
||||
source = deepcopy(graph or {})
|
||||
if str(source.get("specVersion") or "") == SPEC_VERSION:
|
||||
source.setdefault("settings", {})
|
||||
settings = source.setdefault("settings", {})
|
||||
_normalize_settings(settings)
|
||||
source.setdefault("nodes", [])
|
||||
source.setdefault("edges", [])
|
||||
for node in source["nodes"]:
|
||||
if node.get("type") != "agent":
|
||||
continue
|
||||
data = node.setdefault("data", {})
|
||||
_normalize_agent_data(data)
|
||||
return source
|
||||
|
||||
nodes = source.get("nodes") or []
|
||||
@@ -171,9 +209,7 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
data["message"] = data.pop("message", data.pop("prompt", ""))
|
||||
data.setdefault("scope", "session")
|
||||
elif new_type == "agent":
|
||||
data.setdefault("contextPolicy", "inherit")
|
||||
data.setdefault("toolIds", [])
|
||||
data.setdefault("knowledgeMode", "disabled")
|
||||
_normalize_agent_data(data)
|
||||
elif new_type == "start":
|
||||
prompt = str(data.pop("prompt", "") or "").strip()
|
||||
if prompt:
|
||||
@@ -207,8 +243,9 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
"name": "迁移的开场 Agent",
|
||||
"prompt": prompt,
|
||||
"contextPolicy": "inherit",
|
||||
"toolIds": [],
|
||||
"knowledgeMode": "disabled",
|
||||
"inheritGlobalConfig": True,
|
||||
"entryMode": "wait_user",
|
||||
"entrySpeech": "",
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -228,9 +265,7 @@ def normalize_graph(graph: dict[str, Any] | None) -> dict[str, Any]:
|
||||
)
|
||||
|
||||
settings = deepcopy(source.get("settings") or {})
|
||||
settings.setdefault("globalPrompt", global_prompt)
|
||||
settings.setdefault("defaultAsrResourceId", "")
|
||||
settings.setdefault("defaultTtsResourceId", "")
|
||||
_normalize_settings(settings, global_prompt=global_prompt)
|
||||
return {
|
||||
"specVersion": 3,
|
||||
"settings": settings,
|
||||
@@ -281,11 +316,18 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
node_by_id[node_id] = node
|
||||
counts[node_type] += 1
|
||||
|
||||
if node_type == "agent":
|
||||
data = node.get("data") or {}
|
||||
entry_mode = data.get("entryMode", "wait_user")
|
||||
if entry_mode not in AGENT_ENTRY_MODES:
|
||||
errors.append(f"Agent 节点 {node_id} 的进入模式无效:{entry_mode}")
|
||||
elif entry_mode == "fixed_speech" and not str(
|
||||
data.get("entrySpeech") or ""
|
||||
).strip():
|
||||
errors.append(f"Agent 节点 {node_id} 的固定进入语不能为空")
|
||||
|
||||
if counts["start"] != 1:
|
||||
errors.append("工作流必须有且仅有一个 Start 节点")
|
||||
if counts["end"] < 1:
|
||||
errors.append("工作流至少需要一个 End 节点")
|
||||
|
||||
incoming: dict[str, int] = defaultdict(int)
|
||||
outgoing: dict[str, int] = defaultdict(int)
|
||||
adj: dict[str, list[str]] = defaultdict(list)
|
||||
@@ -313,7 +355,8 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
if mode == "llm" and not str(data.get("condition") or "").strip():
|
||||
errors.append(f"LLM 判断边缺少自然语言条件:{edge_id}")
|
||||
if mode == "expression":
|
||||
errors.extend(f"边 {edge_id}:{item}" for item in _validate_expression(data.get("expression")))
|
||||
expression_errors = _validate_expression(data.get("expression"))
|
||||
errors.extend(f"边 {edge_id}:{item}" for item in expression_errors)
|
||||
try:
|
||||
priority = int(data.get("priority", 10))
|
||||
except (TypeError, ValueError):
|
||||
@@ -329,7 +372,9 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
incoming[target_id] += 1
|
||||
outgoing[source_id] += 1
|
||||
adj[source_id].append(target_id)
|
||||
if node_by_id[source_id].get("type") != "agent" and node_by_id[target_id].get("type") != "agent":
|
||||
source_is_automatic = node_by_id[source_id].get("type") != "agent"
|
||||
target_is_automatic = node_by_id[target_id].get("type") != "agent"
|
||||
if source_is_automatic and target_is_automatic:
|
||||
auto_adj[source_id].append(target_id)
|
||||
|
||||
for node_id, node in node_by_id.items():
|
||||
@@ -347,10 +392,18 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
errors.append(f"节点 {node_id} 的{label}不能少于 {lo}")
|
||||
if hi is not None and actual > hi:
|
||||
errors.append(f"节点 {node_id} 的{label}不能多于 {hi}")
|
||||
if node.get("type") in {"start", "action", "handoff"} and always_counts[node_id] != 1:
|
||||
errors.append(f"自动节点 {node_id} 必须有且仅有一条默认边")
|
||||
node_type = node.get("type")
|
||||
if (
|
||||
node_type in AUTOMATIC_NODE_TYPES
|
||||
and outgoing[node_id] > 0
|
||||
and always_counts[node_id] != 1
|
||||
):
|
||||
errors.append(f"自动节点 {node_id} 存在出边时必须有且仅有一条默认边")
|
||||
|
||||
start_id = next((nid for nid, n in node_by_id.items() if n.get("type") == "start"), None)
|
||||
start_id = next(
|
||||
(node_id for node_id, node in node_by_id.items() if node.get("type") == "start"),
|
||||
None,
|
||||
)
|
||||
if start_id:
|
||||
reached = {start_id}
|
||||
queue = deque([start_id])
|
||||
@@ -380,7 +433,12 @@ def validate_graph(graph: dict[str, Any]) -> list[str]:
|
||||
visited.add(node_id)
|
||||
return False
|
||||
|
||||
if any(visit(node_id) for node_id, node in node_by_id.items() if node.get("type") != "agent"):
|
||||
automatic_node_ids = (
|
||||
node_id
|
||||
for node_id, node in node_by_id.items()
|
||||
if node.get("type") != "agent"
|
||||
)
|
||||
if any(visit(node_id) for node_id in automatic_node_ids):
|
||||
errors.append("Start/Action/Handoff/End 之间不能形成无等待循环")
|
||||
return list(dict.fromkeys(errors))
|
||||
|
||||
@@ -392,22 +450,35 @@ def graph_references(graph: dict[str, Any]) -> dict[str, set[str]]:
|
||||
resources = {
|
||||
str(value)
|
||||
for value in (
|
||||
settings.get("defaultLlmResourceId"),
|
||||
settings.get("defaultAsrResourceId"),
|
||||
settings.get("defaultTtsResourceId"),
|
||||
)
|
||||
if value
|
||||
}
|
||||
tools: set[str] = set()
|
||||
knowledge: set[str] = set()
|
||||
tools: set[str] = {str(tool_id) for tool_id in settings.get("toolIds") or []}
|
||||
knowledge: set[str] = (
|
||||
{str(settings["knowledgeBaseId"])}
|
||||
if settings.get("knowledgeBaseId")
|
||||
else set()
|
||||
)
|
||||
for node in normalized.get("nodes") or []:
|
||||
data = node.get("data") or {}
|
||||
for resource_id in (data.get("asrResourceId"), data.get("ttsResourceId")):
|
||||
if resource_id:
|
||||
resources.add(str(resource_id))
|
||||
for tool_id in data.get("toolIds") or []:
|
||||
tools.add(str(tool_id))
|
||||
inherits_global = (
|
||||
node.get("type") == "agent" and data.get("inheritGlobalConfig", True)
|
||||
)
|
||||
if not inherits_global:
|
||||
for resource_id in (
|
||||
data.get("llmResourceId"),
|
||||
data.get("asrResourceId"),
|
||||
data.get("ttsResourceId"),
|
||||
):
|
||||
if resource_id:
|
||||
resources.add(str(resource_id))
|
||||
for tool_id in data.get("toolIds") or []:
|
||||
tools.add(str(tool_id))
|
||||
if data.get("knowledgeBaseId"):
|
||||
knowledge.add(str(data["knowledgeBaseId"]))
|
||||
if data.get("toolId"):
|
||||
tools.add(str(data["toolId"]))
|
||||
if data.get("knowledgeBaseId"):
|
||||
knowledge.add(str(data["knowledgeBaseId"]))
|
||||
return {"model_resources": resources, "tools": tools, "knowledge_bases": knowledge}
|
||||
|
||||
Reference in New Issue
Block a user