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:
129
backend/services/workflow_router.py
Normal file
129
backend/services/workflow_router.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Pre-response LLM routing for Workflow Agent 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_AGENT = "workflow_stay_on_current_agent"
|
||||
MAX_ROUTING_HISTORY_ENTRIES = 20
|
||||
|
||||
|
||||
class WorkflowLLMRouter:
|
||||
"""Select one LLM edge before the conversational LLM is allowed to reply."""
|
||||
|
||||
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_AGENT
|
||||
|
||||
names = {edge_name(edge) for edge in edges}
|
||||
stay_name = STAY_ON_CURRENT_AGENT
|
||||
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": "所有转移条件都不满足,继续由当前 Agent 处理用户消息。",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
ordered_conditions = "\n".join(
|
||||
f"{index + 1}. {edge_description(edge)}"
|
||||
for index, edge in enumerate(edges)
|
||||
)
|
||||
router_prompt = (
|
||||
"你是工作流路由器,不是对话助手。收到一轮完整用户输入后,"
|
||||
"必须且只能调用一个提供的函数,禁止输出任何口头回复。\n"
|
||||
"按给出的顺序判断转移条件;选择第一个明确满足的转移函数。"
|
||||
"如果没有条件满足,调用留在当前 Agent 的函数。\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 未返回函数调用,留在当前 Agent")
|
||||
return STAY_ON_CURRENT_AGENT
|
||||
selected = str(tool_calls[0].function.name or "")
|
||||
if selected == stay_name:
|
||||
return STAY_ON_CURRENT_AGENT
|
||||
if selected not in names:
|
||||
logger.warning(f"Workflow 路由 LLM 返回未知函数:{selected}")
|
||||
return STAY_ON_CURRENT_AGENT
|
||||
return selected
|
||||
except Exception as exc: # noqa: BLE001 - routing failure must not end the call
|
||||
logger.warning(f"Workflow LLM 边判断失败,留在当前 Agent:{exc}")
|
||||
return None
|
||||
finally:
|
||||
await client.close()
|
||||
Reference in New Issue
Block a user