Refactor workflow routing and greeting management in Brain classes
- 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.
This commit is contained in:
@@ -28,6 +28,7 @@ from services.brains.dify_llm import (
|
||||
)
|
||||
from services.brains.workflow_brain import WorkflowBrain
|
||||
from services.runtime_variables import prepare_dynamic_config
|
||||
from services.workflow_router import STAY_ON_CURRENT_NODE
|
||||
|
||||
|
||||
class FakeLLM:
|
||||
@@ -460,6 +461,86 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
|
||||
class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_initial_fixed_speech_waits_for_start_greeting_to_finish(self):
|
||||
brain = WorkflowBrain(
|
||||
{
|
||||
"specVersion": 3,
|
||||
"settings": {"globalPrompt": "全局规则"},
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"type": "start",
|
||||
"data": {"greeting": "欢迎使用"},
|
||||
},
|
||||
{
|
||||
"id": "agent",
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"prompt": "收集用户信息",
|
||||
"entryMode": "fixed_speech",
|
||||
"entrySpeech": "请问您怎么称呼?",
|
||||
},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "begin",
|
||||
"source": "start",
|
||||
"target": "agent",
|
||||
"data": {"mode": "always", "priority": 0},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
queued = []
|
||||
|
||||
async def queue_frame(frame):
|
||||
queued.append(frame)
|
||||
|
||||
class FakeManager:
|
||||
def __init__(self):
|
||||
self.current_node = None
|
||||
|
||||
async def initialize(self, config):
|
||||
self.current_node = config["name"]
|
||||
|
||||
async def set_node_from_config(self, config):
|
||||
self.current_node = config["name"]
|
||||
for action in config.get("pre_actions", []):
|
||||
await action["handler"](action, self)
|
||||
|
||||
brain._runtime = BrainRuntime(
|
||||
context=LLMContext(messages=[]),
|
||||
llm=FakeLLM(),
|
||||
queue_frame=queue_frame,
|
||||
set_system_prompt=lambda _prompt: None,
|
||||
set_tools=lambda _tools: None,
|
||||
call_end=FakeCallEnd(),
|
||||
)
|
||||
brain._manager = FakeManager()
|
||||
|
||||
await brain.on_connected(greeting_pending=True)
|
||||
|
||||
self.assertEqual(brain._manager.current_node, "start")
|
||||
self.assertFalse(any(isinstance(frame, TTSSpeakFrame) for frame in queued))
|
||||
|
||||
await brain.on_greeting_finished()
|
||||
|
||||
self.assertEqual(brain._manager.current_node, "agent")
|
||||
fixed_speech_frames = [
|
||||
frame for frame in queued if isinstance(frame, TTSSpeakFrame)
|
||||
]
|
||||
self.assertEqual(len(fixed_speech_frames), 1)
|
||||
self.assertEqual(fixed_speech_frames[0].text, "请问您怎么称呼?")
|
||||
|
||||
# Playback notifications may be duplicated by a transport reconnect;
|
||||
# the initial entry behavior must still run only once.
|
||||
await brain.on_greeting_finished()
|
||||
self.assertEqual(
|
||||
len([frame for frame in queued if isinstance(frame, TTSSpeakFrame)]),
|
||||
1,
|
||||
)
|
||||
|
||||
async def test_action_publishes_updated_session_variables(self):
|
||||
tool = RuntimeTool(
|
||||
id="lookup",
|
||||
@@ -652,6 +733,261 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
)
|
||||
|
||||
async def test_start_llm_conditions_wait_for_and_route_first_user_turn(self):
|
||||
brain = WorkflowBrain(
|
||||
{
|
||||
"specVersion": 3,
|
||||
"settings": {"globalPrompt": "全局规则"},
|
||||
"nodes": [
|
||||
{
|
||||
"id": "start",
|
||||
"type": "start",
|
||||
"data": {"name": "Start", "greeting": "你想做什么?"},
|
||||
},
|
||||
{
|
||||
"id": "eat",
|
||||
"type": "agent",
|
||||
"data": {
|
||||
"name": "点饭",
|
||||
"prompt": "帮助用户点饭",
|
||||
"contextPolicy": "fresh",
|
||||
"entryMode": "wait_user",
|
||||
},
|
||||
},
|
||||
{"id": "drink", "type": "agent", "data": {}},
|
||||
{"id": "run", "type": "agent", "data": {}},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "eat",
|
||||
"source": "start",
|
||||
"target": "eat",
|
||||
"data": {
|
||||
"mode": "llm",
|
||||
"priority": 10,
|
||||
"condition": "用户想吃饭",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "drink",
|
||||
"source": "start",
|
||||
"target": "drink",
|
||||
"data": {
|
||||
"mode": "llm",
|
||||
"priority": 20,
|
||||
"condition": "用户想喝水",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "run",
|
||||
"source": "start",
|
||||
"target": "run",
|
||||
"data": {
|
||||
"mode": "llm",
|
||||
"priority": 30,
|
||||
"condition": "用户想跑步",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
queued = []
|
||||
|
||||
async def queue_frame(frame):
|
||||
queued.append(frame)
|
||||
|
||||
brain._runtime = BrainRuntime(
|
||||
context=LLMContext(messages=[]),
|
||||
llm=FakeLLM(),
|
||||
queue_frame=queue_frame,
|
||||
set_system_prompt=lambda _prompt: None,
|
||||
set_tools=lambda _tools: None,
|
||||
call_end=FakeCallEnd(),
|
||||
)
|
||||
|
||||
class FakeManager:
|
||||
def __init__(self):
|
||||
self.current_node = None
|
||||
self.config = None
|
||||
|
||||
async def initialize(self, config):
|
||||
self.current_node = config["name"]
|
||||
self.config = config
|
||||
|
||||
async def set_node_from_config(self, config):
|
||||
self.current_node = config["name"]
|
||||
self.config = config
|
||||
|
||||
class FakeRouter:
|
||||
def __init__(self):
|
||||
self.calls = 0
|
||||
|
||||
async def select_edge(self, **_kwargs):
|
||||
self.calls += 1
|
||||
return "goto_eat"
|
||||
|
||||
manager = FakeManager()
|
||||
router = FakeRouter()
|
||||
brain._manager = manager
|
||||
brain._router = router
|
||||
|
||||
await brain.on_connected()
|
||||
self.assertEqual(manager.current_node, "start")
|
||||
self.assertEqual(router.calls, 0)
|
||||
|
||||
handled = await brain.on_user_turn_end("我想吃饭")
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(router.calls, 1)
|
||||
self.assertEqual(manager.current_node, "eat")
|
||||
self.assertIn(
|
||||
{"role": "user", "content": "我想吃饭"},
|
||||
manager.config["task_messages"],
|
||||
)
|
||||
self.assertTrue(any(isinstance(frame, LLMRunFrame) for frame in queued))
|
||||
self.assertIn("我想吃饭", brain._store.values["system__conversation_history"])
|
||||
|
||||
async def test_automatic_node_can_follow_llm_condition(self):
|
||||
brain = WorkflowBrain(
|
||||
{
|
||||
"specVersion": 3,
|
||||
"settings": {"globalPrompt": "全局规则"},
|
||||
"nodes": [
|
||||
{"id": "start", "type": "start", "data": {}},
|
||||
{
|
||||
"id": "handoff",
|
||||
"type": "handoff",
|
||||
"data": {"name": "人工转接", "targetType": "human"},
|
||||
},
|
||||
{
|
||||
"id": "agent",
|
||||
"type": "agent",
|
||||
"data": {"name": "继续服务", "prompt": "继续处理"},
|
||||
},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "to-agent",
|
||||
"source": "handoff",
|
||||
"target": "agent",
|
||||
"data": {
|
||||
"mode": "llm",
|
||||
"priority": 10,
|
||||
"condition": "转接后仍需 AI 继续服务",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
queued = []
|
||||
|
||||
async def queue_frame(frame):
|
||||
queued.append(frame)
|
||||
|
||||
brain._runtime = BrainRuntime(
|
||||
context=LLMContext(messages=[]),
|
||||
llm=FakeLLM(),
|
||||
queue_frame=queue_frame,
|
||||
set_system_prompt=lambda _prompt: None,
|
||||
set_tools=lambda _tools: None,
|
||||
call_end=FakeCallEnd(),
|
||||
)
|
||||
|
||||
class FakeRouter:
|
||||
async def select_edge(self, **kwargs):
|
||||
self.node_name = kwargs["node_name"]
|
||||
return "goto_to_agent"
|
||||
|
||||
router = FakeRouter()
|
||||
brain._router = router
|
||||
|
||||
config = await brain._resolve_path("handoff")
|
||||
|
||||
self.assertEqual(config["name"], "agent")
|
||||
self.assertEqual(router.node_name, "人工转接")
|
||||
|
||||
async def test_mixed_edge_conditions_follow_priority(self):
|
||||
brain = WorkflowBrain(
|
||||
{
|
||||
"specVersion": 3,
|
||||
"settings": {},
|
||||
"nodes": [
|
||||
{"id": "start", "type": "start", "data": {}},
|
||||
{"id": "agent", "type": "agent", "data": {}},
|
||||
{"id": "llm-target", "type": "end", "data": {}},
|
||||
{"id": "expression-target", "type": "end", "data": {}},
|
||||
{"id": "default-target", "type": "end", "data": {}},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "llm",
|
||||
"source": "agent",
|
||||
"target": "llm-target",
|
||||
"data": {
|
||||
"mode": "llm",
|
||||
"priority": 10,
|
||||
"condition": "大模型条件成立",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "expression",
|
||||
"source": "agent",
|
||||
"target": "expression-target",
|
||||
"data": {
|
||||
"mode": "expression",
|
||||
"priority": 20,
|
||||
"expression": {
|
||||
"combinator": "and",
|
||||
"rules": [
|
||||
{
|
||||
"variable": "route",
|
||||
"operator": "eq",
|
||||
"value": "expression",
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"source": "agent",
|
||||
"target": "default-target",
|
||||
"data": {"mode": "always", "priority": 30},
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
brain._store.values["route"] = "expression"
|
||||
|
||||
class FakeRouter:
|
||||
def __init__(self):
|
||||
self.result = STAY_ON_CURRENT_NODE
|
||||
self.edge_ids = []
|
||||
|
||||
async def select_edge(self, **kwargs):
|
||||
self.edge_ids = [edge["id"] for edge in kwargs["edges"]]
|
||||
return self.result
|
||||
|
||||
router = FakeRouter()
|
||||
brain._router = router
|
||||
|
||||
selected = await brain._select_edge("agent")
|
||||
self.assertEqual(router.edge_ids, ["llm"])
|
||||
self.assertEqual(selected["id"], "expression")
|
||||
|
||||
router.result = "goto_llm"
|
||||
selected = await brain._select_edge("agent")
|
||||
self.assertEqual(selected["id"], "llm")
|
||||
|
||||
expression_edge = next(
|
||||
edge for edge in brain._engine.edges if edge["id"] == "expression"
|
||||
)
|
||||
expression_edge["data"]["priority"] = 5
|
||||
router.edge_ids = []
|
||||
selected = await brain._select_edge("agent")
|
||||
self.assertEqual(selected["id"], "expression")
|
||||
self.assertEqual(router.edge_ids, [])
|
||||
|
||||
async def test_transition_and_end_are_owned_by_workflow_brain(self):
|
||||
graph = {
|
||||
"specVersion": 3,
|
||||
|
||||
Reference in New Issue
Block a user