Refactor workflow agent and routing components for improved functionality
- Introduce WorkflowAgentStage to manage agent stage configurations and enhance interaction with the workflow engine. - Implement WorkflowEdgeEvaluator for priority-aware edge evaluation, improving routing decisions based on conditions and user turns. - Update WorkflowBrain to handle user turns and routing more effectively, ensuring agents cannot have only one default path. - Enhance CallEndCoordinator to track speech events and manage call termination based on queued speech. - Add new models and output handling for workflow interactions, improving clarity and maintainability. - Update tests to validate the new routing logic and agent behavior under various scenarios.
This commit is contained in:
@@ -28,7 +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
|
||||
from services.workflow.models import LLMRouteResult, RouteStatus
|
||||
|
||||
|
||||
class FakeLLM:
|
||||
@@ -47,6 +47,7 @@ class FakeCallEnd:
|
||||
self.finished = False
|
||||
self.response_started = False
|
||||
self.waited_for_text: bool | None = None
|
||||
self.tracked_speeches = 0
|
||||
|
||||
def begin(self, reason: str) -> None:
|
||||
self.ending = True
|
||||
@@ -58,6 +59,14 @@ class FakeCallEnd:
|
||||
def arm_after_speech(self) -> None:
|
||||
self.armed = True
|
||||
|
||||
def track_speech(self) -> None:
|
||||
self.tracked_speeches += 1
|
||||
|
||||
async def arm_after_tracked_speech(self) -> None:
|
||||
self.armed = True
|
||||
if self.tracked_speeches == 0:
|
||||
await self.finish()
|
||||
|
||||
async def finish_after_current_speech(self, *, has_text: bool) -> None:
|
||||
self.waited_for_text = has_text
|
||||
if has_text:
|
||||
@@ -824,7 +833,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def select_edge(self, **_kwargs):
|
||||
self.calls += 1
|
||||
return "goto_eat"
|
||||
return LLMRouteResult(
|
||||
status=RouteStatus.MATCHED,
|
||||
function_name="goto_eat",
|
||||
)
|
||||
|
||||
manager = FakeManager()
|
||||
router = FakeRouter()
|
||||
@@ -847,6 +859,45 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertTrue(any(isinstance(frame, LLMRunFrame) for frame in queued))
|
||||
self.assertIn("我想吃饭", brain._store.values["system__conversation_history"])
|
||||
|
||||
async def test_start_expression_condition_also_waits_for_user_turn(self):
|
||||
brain = WorkflowBrain(
|
||||
{
|
||||
"specVersion": 3,
|
||||
"settings": {},
|
||||
"nodes": [
|
||||
{"id": "start", "type": "start", "data": {}},
|
||||
{"id": "agent", "type": "agent", "data": {}},
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"id": "route",
|
||||
"source": "start",
|
||||
"target": "agent",
|
||||
"data": {
|
||||
"mode": "expression",
|
||||
"priority": 10,
|
||||
"expression": {
|
||||
"combinator": "and",
|
||||
"rules": [
|
||||
{
|
||||
"variable": "route",
|
||||
"operator": "eq",
|
||||
"value": "agent",
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
brain._store.values["route"] = "agent"
|
||||
|
||||
config = await brain._initial_node_config()
|
||||
|
||||
self.assertEqual(config["name"], "start")
|
||||
self.assertEqual(brain._state.status.value, "waiting_user")
|
||||
|
||||
async def test_automatic_node_can_follow_llm_condition(self):
|
||||
brain = WorkflowBrain(
|
||||
{
|
||||
@@ -896,7 +947,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
class FakeRouter:
|
||||
async def select_edge(self, **kwargs):
|
||||
self.node_name = kwargs["node_name"]
|
||||
return "goto_to_agent"
|
||||
return LLMRouteResult(
|
||||
status=RouteStatus.MATCHED,
|
||||
function_name="goto_to_agent",
|
||||
)
|
||||
|
||||
router = FakeRouter()
|
||||
brain._router = router
|
||||
@@ -961,7 +1015,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
class FakeRouter:
|
||||
def __init__(self):
|
||||
self.result = STAY_ON_CURRENT_NODE
|
||||
self.result = LLMRouteResult(status=RouteStatus.NO_MATCH)
|
||||
self.edge_ids = []
|
||||
|
||||
async def select_edge(self, **kwargs):
|
||||
@@ -975,7 +1029,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(router.edge_ids, ["llm"])
|
||||
self.assertEqual(selected["id"], "expression")
|
||||
|
||||
router.result = "goto_llm"
|
||||
router.result = LLMRouteResult(
|
||||
status=RouteStatus.MATCHED,
|
||||
function_name="goto_llm",
|
||||
)
|
||||
selected = await brain._select_edge("agent")
|
||||
self.assertEqual(selected["id"], "llm")
|
||||
|
||||
@@ -1183,21 +1240,19 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
brain._engine.data("agent")["entryMode"] = "generate"
|
||||
generate_config = brain._agent_config("agent")
|
||||
self.assertTrue(generate_config["respond_immediately"])
|
||||
self.assertFalse(generate_config["respond_immediately"])
|
||||
worker.frames.clear()
|
||||
await brain._manager.set_node_from_config(generate_config)
|
||||
self.assertTrue(any(isinstance(frame, LLMRunFrame) for frame in worker.frames))
|
||||
self.assertFalse(any(isinstance(frame, LLMRunFrame) for frame in worker.frames))
|
||||
await brain._after_node_activated(generate_config)
|
||||
self.assertTrue(any(isinstance(frame, LLMRunFrame) for frame in queued))
|
||||
|
||||
brain._engine.data("agent").update(
|
||||
{"entryMode": "fixed_speech", "entrySpeech": "您好,{{user_name}}"}
|
||||
)
|
||||
fixed_config = brain._agent_config("agent")
|
||||
self.assertFalse(fixed_config["respond_immediately"])
|
||||
self.assertEqual(
|
||||
fixed_config["pre_actions"][0]["type"],
|
||||
"workflow_fixed_speech",
|
||||
)
|
||||
self.assertEqual(fixed_config["pre_actions"][0]["text"], "您好,王先生")
|
||||
self.assertNotIn("pre_actions", fixed_config)
|
||||
self.assertEqual(
|
||||
fixed_config["task_messages"],
|
||||
[
|
||||
@@ -1216,10 +1271,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
{"role": "assistant", "content": "您好,王先生"},
|
||||
],
|
||||
)
|
||||
self.assertEqual(fixed_config["pre_actions"][0]["node_id"], "agent")
|
||||
worker.frames.clear()
|
||||
queued.clear()
|
||||
await brain._manager.set_node_from_config(fixed_config)
|
||||
await brain._after_node_activated(fixed_config)
|
||||
self.assertTrue(any(isinstance(frame, TTSSpeakFrame) for frame in queued))
|
||||
self.assertFalse(any(isinstance(frame, LLMRunFrame) for frame in worker.frames))
|
||||
context_updates = [
|
||||
@@ -1263,7 +1318,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
class FakeRouter:
|
||||
async def select_edge(self, **_kwargs):
|
||||
return "goto_finish"
|
||||
return LLMRouteResult(
|
||||
status=RouteStatus.MATCHED,
|
||||
function_name="goto_finish",
|
||||
)
|
||||
|
||||
brain._router = FakeRouter()
|
||||
handled = await brain.on_user_turn_end("我的需求已经说完了")
|
||||
|
||||
Reference in New Issue
Block a user