feat(workflow): enhance message stages and image routing

This commit is contained in:
Xin Wang
2026-08-03 12:38:02 +08:00
parent b0991f239e
commit 4c43e167db
13 changed files with 593 additions and 157 deletions

View File

@@ -1021,7 +1021,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
"type": "message",
"data": {
"speech": "请问您怎么称呼?",
"showMessage": False,
"completionPolicy": "playback",
},
},
{
@@ -1457,11 +1457,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
"type": "message",
"data": {
"speech": "请先确认 {{customer}} 的重要信息。",
"showMessage": True,
"title": "重要提示",
"message": "请核对客户信息。",
"confirmLabel": "确认",
"requireConfirmation": True,
"completionPolicy": "confirmation",
},
},
],
@@ -1585,6 +1584,131 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
self.assertTrue(result.succeeded)
self.assertEqual(input_states, [False, True])
async def test_interruptible_message_forwards_multimodal_input_once(self):
graph = {
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "message",
"type": "message",
"data": {
"speech": "请按提示操作,也可以直接告诉我需求。",
"completionPolicy": "interruptible",
},
},
{
"id": "agent",
"type": "agent",
"data": {
"prompt": "处理用户输入",
"contextPolicy": "fresh",
},
},
],
"edges": [
{
"id": "start-message",
"source": "start",
"target": "message",
"data": {"mode": "always"},
},
{
"id": "message-agent",
"source": "message",
"target": "agent",
"data": {"mode": "always"},
},
],
}
brain = WorkflowBrain(graph)
queued = []
input_states = []
class PlaybackCallEnd(FakeCallEnd):
def __init__(self):
super().__init__()
self.completion = None
def track_speech(self):
self.completion = asyncio.get_running_loop().create_future()
return self.completion
class FakeManager:
def __init__(self):
self.current_node = None
self.configs = []
async def initialize(self, config):
self.current_node = config["name"]
self.configs.append(config)
async def set_node_from_config(self, config):
self.current_node = config["name"]
self.configs.append(config)
async def queue_frame(frame):
queued.append(frame)
call_end = PlaybackCallEnd()
manager = FakeManager()
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=call_end,
set_input_enabled=input_states.append,
)
brain._manager = manager
await brain.on_connected()
await asyncio.sleep(0)
self.assertEqual(manager.current_node, "message")
self.assertIsNotNone(call_end.completion)
self.assertTrue(all(input_states))
image_message = {
"role": "user",
"content": [
{"type": "text", "text": "已发送一张图片"},
{
"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,AA=="},
},
],
}
await brain.on_user_turn_end(
"已发送一张图片",
user_message=image_message,
)
await asyncio.sleep(0)
self.assertEqual(manager.current_node, "agent")
self.assertEqual(
[config["name"] for config in manager.configs].count("agent"),
1,
)
self.assertTrue(call_end.completion.cancelled())
self.assertTrue(all(input_states))
self.assertEqual(
manager.configs[-1]["task_messages"],
[image_message],
)
self.assertEqual(
sum(isinstance(frame, LLMRunFrame) for frame in queued),
1,
)
self.assertTrue(
any(
isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("event") == "message_interrupted"
for frame in queued
)
)
async def test_message_between_agents_resumes_after_playback(self):
graph = {
"specVersion": 3,