- Update WorkflowBrain to handle greeting playback more effectively, ensuring that the initial greeting completes before transitioning to the first node. - Introduce new methods for managing greeting states and conditions, enhancing the interaction flow for user turns. - Refactor WorkflowLLMRouter to improve routing logic and ensure proper handling of conditional paths. - Enhance tests to verify the correct behavior of greeting management and routing under various scenarios, including waiting for audio playback to finish. - Update frontend components to reflect changes in edge handling and improve user experience in workflow configurations.
132 lines
4.7 KiB
Python
132 lines
4.7 KiB
Python
"""Small LLM router for Workflow conditional edges.
|
|
|
|
The router deliberately uses a separate, short completion. Its only output is
|
|
a required function choice, so the current Agent cannot speak before the graph
|
|
has decided whether the user turn belongs to another node.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from collections.abc import Callable
|
|
from typing import Any
|
|
|
|
from loguru import logger
|
|
from models import AssistantConfig
|
|
from openai import AsyncOpenAI
|
|
|
|
|
|
STAY_ON_CURRENT_NODE = "workflow_stay_on_current_node"
|
|
# Compatibility alias for callers saved before all source nodes supported LLM edges.
|
|
STAY_ON_CURRENT_AGENT = STAY_ON_CURRENT_NODE
|
|
MAX_ROUTING_HISTORY_ENTRIES = 20
|
|
|
|
|
|
class WorkflowLLMRouter:
|
|
"""Select one LLM edge without allowing the router to speak."""
|
|
|
|
def __init__(self, cfg: AssistantConfig):
|
|
self._cfg = cfg
|
|
|
|
async def select_edge(
|
|
self,
|
|
*,
|
|
node_name: str,
|
|
node_prompt: str,
|
|
edges: list[dict[str, Any]],
|
|
history: list[dict[str, str]],
|
|
variables: dict[str, Any],
|
|
edge_name: Callable[[dict[str, Any]], str],
|
|
edge_description: Callable[[dict[str, Any]], str],
|
|
) -> str | None:
|
|
"""Return an edge function name, STAY, or None when routing failed."""
|
|
if not edges:
|
|
return STAY_ON_CURRENT_NODE
|
|
|
|
names = {edge_name(edge) for edge in edges}
|
|
stay_name = STAY_ON_CURRENT_NODE
|
|
while stay_name in names:
|
|
stay_name = f"_{stay_name}"
|
|
|
|
tools = [
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": edge_name(edge),
|
|
"description": edge_description(edge),
|
|
"parameters": {"type": "object", "properties": {}},
|
|
},
|
|
}
|
|
for edge in edges
|
|
]
|
|
tools.append(
|
|
{
|
|
"type": "function",
|
|
"function": {
|
|
"name": stay_name,
|
|
"description": "所有转移条件都不满足,留在当前节点。",
|
|
"parameters": {"type": "object", "properties": {}},
|
|
},
|
|
}
|
|
)
|
|
|
|
ordered_conditions = "\n".join(
|
|
f"{index + 1}. {edge_description(edge)}"
|
|
for index, edge in enumerate(edges)
|
|
)
|
|
router_prompt = (
|
|
"你是工作流路由器,不是对话助手。收到一轮完整用户输入后,"
|
|
"必须且只能调用一个提供的函数,禁止输出任何口头回复。\n"
|
|
"按给出的顺序判断转移条件;选择第一个明确满足的转移函数。"
|
|
"如果没有条件满足,调用留在当前节点的函数。\n\n"
|
|
f"当前节点:{node_name}\n"
|
|
f"当前节点任务:{node_prompt or '未配置'}\n"
|
|
f"转移条件:\n{ordered_conditions}"
|
|
)
|
|
recent_history = history[-MAX_ROUTING_HISTORY_ENTRIES:]
|
|
routing_input = json.dumps(
|
|
{
|
|
"conversation": recent_history,
|
|
"session_variables": variables,
|
|
},
|
|
ensure_ascii=False,
|
|
separators=(",", ":"),
|
|
)
|
|
extra_body = self._cfg.llm_values.get("extraBody")
|
|
request_extra = (
|
|
{"extra_body": extra_body} if isinstance(extra_body, dict) else {}
|
|
)
|
|
client = AsyncOpenAI(
|
|
api_key=self._cfg.llm_api_key,
|
|
base_url=self._cfg.llm_base_url,
|
|
timeout=15.0,
|
|
)
|
|
try:
|
|
response = await client.chat.completions.create(
|
|
model=self._cfg.model,
|
|
messages=[
|
|
{"role": "system", "content": router_prompt},
|
|
{"role": "user", "content": routing_input},
|
|
],
|
|
tools=tools,
|
|
tool_choice="required",
|
|
temperature=0,
|
|
**request_extra,
|
|
)
|
|
tool_calls = response.choices[0].message.tool_calls or []
|
|
if not tool_calls:
|
|
logger.warning("Workflow 路由 LLM 未返回函数调用,留在当前节点")
|
|
return STAY_ON_CURRENT_NODE
|
|
selected = str(tool_calls[0].function.name or "")
|
|
if selected == stay_name:
|
|
return STAY_ON_CURRENT_NODE
|
|
if selected not in names:
|
|
logger.warning(f"Workflow 路由 LLM 返回未知函数:{selected}")
|
|
return STAY_ON_CURRENT_NODE
|
|
return selected
|
|
except Exception as exc: # noqa: BLE001 - routing failure must not end the call
|
|
logger.warning(f"Workflow LLM 边判断失败,留在当前节点:{exc}")
|
|
return None
|
|
finally:
|
|
await client.close()
|