Files
ai-video-fullstack/backend/tests/test_brains.py
2026-08-03 10:55:57 +08:00

2463 lines
85 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import asyncio
import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from models import AssistantConfig, RuntimeTool
from pipecat.flows import FlowManager
from pipecat.frames.frames import (
LLMContextFrame,
LLMFullResponseEndFrame,
LLMFullResponseStartFrame,
LLMMessagesAppendFrame,
LLMMessagesUpdateFrame,
LLMRunFrame,
LLMTextFrame,
LLMUpdateSettingsFrame,
OutputTransportMessageUrgentFrame,
TTSSpeakFrame,
)
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.frame_processor import FrameDirection
from schemas import AssistantUpsert, REALTIME_CAPABLE_TYPES
from services.brains import BrainRuntime, SPECS, build_brain
from services.brains.base import GREETING_CONTEXT_MARKER
from services.brains.dify_llm import (
DifyLLMService,
last_user_text,
normalize_api_base,
)
from services.brains.workflow_brain import ConfiguredFlowManager, WorkflowBrain
from services.runtime_variables import prepare_dynamic_config
from services.action_runtime import ActionOutcome, ActionStatus
from services.workflow.models import (
LLMRouteResult,
RouteStatus,
WorkflowStatus,
)
class FakeLLM:
def __init__(self):
self.functions = {}
self.function_options = {}
def register_function(self, name, handler, **options):
self.functions[name] = handler
self.function_options[name] = options
class FakeCallEnd:
def __init__(self):
self.ending = False
self.reason = ""
self.armed = False
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
self.reason = reason
def begin_response(self) -> None:
self.response_started = True
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:
self.armed = True
else:
await self.finish()
async def finish(self) -> None:
self.finished = True
class FakeFunctionParams:
def __init__(self, arguments=None):
self.arguments = arguments or {}
self.result = None
self.properties = None
async def result_callback(self, result, properties=None):
self.result = result
self.properties = properties
async def noop_queue_frame(_frame):
return None
class BrainRegistryTests(unittest.TestCase):
def test_capability_matrix(self):
self.assertEqual(
{
name: spec.supported_runtime_modes
for name, spec in SPECS.items()
},
{
"prompt": frozenset({"pipeline", "realtime"}),
"workflow": frozenset({"pipeline"}),
"dify": frozenset({"pipeline"}),
"fastgpt": frozenset({"pipeline"}),
},
)
self.assertEqual(
REALTIME_CAPABLE_TYPES,
{
name
for name, spec in SPECS.items()
if "realtime" in spec.supported_runtime_modes
},
)
def test_unknown_brain_does_not_fallback_to_prompt(self):
with self.assertRaisesRegex(ValueError, "尚未实现"):
build_brain(AssistantConfig(type="opencode"))
def test_workflow_realtime_is_rejected_at_schema_boundary(self):
with self.assertRaises(ValueError):
AssistantUpsert(
name="workflow",
type="workflow",
runtimeMode="realtime",
)
def test_prompt_realtime_keeps_dynamic_variable_definitions(self):
assistant = AssistantUpsert(
name="realtime prompt",
type="prompt",
runtimeMode="realtime",
dynamicVariableDefinitions={
"user_name": {
"type": "string",
"required": True,
"default": None,
}
},
)
self.assertIn("user_name", assistant.dynamic_variable_definitions)
def test_workflow_keeps_dynamic_variables_and_tool_bindings(self):
assistant = AssistantUpsert(
name="workflow",
type="workflow",
toolIds=["tool_a"],
dynamicVariableDefinitions={
"customer": {"type": "string", "required": False, "default": "王先生"}
},
graph={},
)
self.assertEqual(assistant.tool_ids, ["tool_a"])
self.assertIn("customer", assistant.dynamic_variable_definitions)
class DifyHelpersTests(unittest.TestCase):
def test_normalize_api_base(self):
self.assertEqual(
normalize_api_base("https://api.dify.ai"),
"https://api.dify.ai/v1",
)
self.assertEqual(
normalize_api_base("https://example.test/v1/chat-messages"),
"https://example.test/v1",
)
def test_last_user_text(self):
self.assertEqual(
last_user_text(
[
{"role": "user", "content": "first"},
{"role": "assistant", "content": "answer"},
{
"role": "user",
"content": [{"type": "text", "text": "latest"}],
},
]
),
"latest",
)
class DifyLLMServiceTests(unittest.IsolatedAsyncioTestCase):
async def test_streams_sdk_events_and_keeps_conversation_id(self):
class FakeDifyClient:
requests = []
async def achat_messages(self, request, **_kwargs):
self.requests.append(request)
async def events():
yield SimpleNamespace(
event="message",
answer="你好",
conversation_id="conversation-1",
)
yield SimpleNamespace(
event="message_end",
conversation_id="conversation-1",
)
return events()
client = FakeDifyClient()
service = DifyLLMService(
AssistantConfig(type="dify"),
client=client,
user_id="test-user",
)
frames = []
async def push_frame(frame, *_args, **_kwargs):
frames.append(frame)
service.push_frame = push_frame
context = LLMContext(messages=[{"role": "user", "content": "问题"}])
await service.process_frame(
LLMContextFrame(context),
FrameDirection.DOWNSTREAM,
)
self.assertIsInstance(frames[0], LLMFullResponseStartFrame)
self.assertIsInstance(frames[1], LLMTextFrame)
self.assertEqual(frames[1].text, "你好")
self.assertIsInstance(frames[-1], LLMFullResponseEndFrame)
self.assertEqual(service._conversation_id, "conversation-1")
context.add_message({"role": "user", "content": "追问"})
await service.process_frame(
LLMContextFrame(context),
FrameDirection.DOWNSTREAM,
)
self.assertEqual(client.requests[-1].conversation_id, "conversation-1")
class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
async def test_startup_only_tool_is_not_registered_with_llm(self):
startup_tool = RuntimeTool(
id="opening_message",
name="开场确认",
function_name="show_message",
type="client",
)
conversation_tool = RuntimeTool(
id="lookup_order",
name="查询订单",
function_name="lookup_order",
type="http",
)
cfg = AssistantConfig(
type="prompt",
tools=[startup_tool, conversation_tool],
llm_tool_ids=[conversation_tool.id],
startup={
"actions": [
{
"id": "opening_message",
"phase": "opening",
"tool_id": startup_tool.id,
"required": True,
}
]
},
)
brain = build_brain(cfg)
llm = FakeLLM()
visible_schemas = []
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(messages=[]),
llm=llm,
queue_frame=noop_queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=visible_schemas.extend,
call_end=FakeCallEnd(),
),
)
self.assertIn(startup_tool.id, brain._tool_by_id)
self.assertNotIn(startup_tool.function_name, llm.functions)
self.assertIn(conversation_tool.function_name, llm.functions)
self.assertEqual(len(visible_schemas), 1)
async def test_preflight_runs_multiple_server_tools_in_order(self):
tools = [
RuntimeTool(
id=f"preflight_{index}",
name=f"预检动作 {index}",
function_name=f"preflight_{index}",
type="http",
)
for index in (1, 2)
]
cfg = AssistantConfig(
type="prompt",
tools=tools,
startup={
"actions": [
{
"id": tool.id,
"phase": "preflight",
"tool_id": tool.id,
"required": True,
}
for tool in tools
]
},
)
brain = build_brain(cfg)
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=noop_queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
),
)
brain._actions.execute = AsyncMock(
side_effect=[
ActionOutcome(
invocation_id=f"act_{index}",
status=ActionStatus.SUCCESS,
duration_ms=index,
)
for index in (1, 2)
]
)
await brain.run_preflight()
self.assertEqual(
[call.args[0].id for call in brain._actions.execute.await_args_list],
["preflight_1", "preflight_2"],
)
async def test_opening_stage_starts_speech_and_releases_on_confirmation(self):
tool = RuntimeTool(
id="opening_data",
name="加载开场数据",
function_name="load_opening_data",
type="http",
)
cfg = AssistantConfig(
type="prompt",
greeting="请阅读并确认重要信息",
tools=[tool],
startup={
"execution_mode": "sequential",
"opening_message": {
"title": "重要提示",
"message": "请确认已阅读。",
"confirm_label": "确认",
},
"actions": [
{
"id": "opening_data",
"phase": "opening",
"tool_id": "opening_data",
"arguments": {},
"required": True,
}
],
},
)
brain = build_brain(cfg)
input_states = []
queued = []
async def queue_frame(frame):
queued.append(frame)
confirmation_started = asyncio.Event()
user_confirmed = asyncio.Event()
client_calls = []
class FakeClientTools:
async def call(self, function_name, arguments, **options):
client_calls.append((function_name, arguments, options))
confirmation_started.set()
await user_confirmed.wait()
return {"status": "ok", "data": {"action": "confirmed"}}
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
client_tools=FakeClientTools(),
set_input_enabled=input_states.append,
),
)
called_tool_ids = []
async def execute(tool, *_args, **_kwargs):
called_tool_ids.append(tool.id)
return ActionOutcome(
invocation_id=f"act_{len(called_tool_ids)}",
status=ActionStatus.SUCCESS,
duration_ms=len(called_tool_ids),
)
brain._actions.execute = AsyncMock(side_effect=execute)
self.assertEqual(await brain.greeting(cfg), "")
await brain.on_connected(greeting_pending=False)
opening_task = asyncio.create_task(brain.on_client_ready())
await confirmation_started.wait()
self.assertEqual(input_states, [False])
self.assertEqual(called_tool_ids, [])
self.assertEqual(client_calls[0][0], "show_message")
self.assertFalse(client_calls[0][1]["dismissible"])
self.assertTrue(
any(
isinstance(frame, TTSSpeakFrame)
and frame.text == "请阅读并确认重要信息"
for frame in queued
)
)
self.assertTrue(
any(
isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "transcript"
and frame.message.get("content") == "请阅读并确认重要信息"
for frame in queued
)
)
user_confirmed.set()
await opening_task
self.assertEqual(input_states, [False, True])
self.assertEqual(called_tool_ids, ["opening_data"])
self.assertEqual(
len(
[
frame
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "startup-action-result"
]
),
1,
)
# Replayed client-ready must not execute startup actions twice.
await brain.on_client_ready()
self.assertEqual(brain._actions.execute.await_count, 1)
async def test_required_opening_failure_keeps_input_blocked_and_ends_call(self):
cfg = AssistantConfig(
type="prompt",
startup={
"opening_message": {
"title": "重要提示",
"message": "请确认已阅读。",
"confirm_label": "确认",
}
},
)
brain = build_brain(cfg)
call_end = FakeCallEnd()
input_states = []
async def queue_frame(_frame):
pass
class FailingClientTools:
async def call(self, *_args, **_kwargs):
return {"status": "error", "message": "客户端未显示消息"}
await brain.setup(
cfg,
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,
client_tools=FailingClientTools(),
set_input_enabled=input_states.append,
),
)
await brain.on_connected(greeting_pending=False)
await brain.on_client_ready()
self.assertEqual(input_states, [False])
self.assertTrue(call_end.ending)
self.assertTrue(call_end.finished)
self.assertEqual(call_end.reason, "startup_action_failed")
async def test_realtime_prompt_brain_renders_dynamic_variables(self):
cfg = prepare_dynamic_config(
AssistantConfig(
type="prompt",
runtimeMode="realtime",
prompt="服务用户 {{user_name}}",
greeting="您好,{{user_name}}",
dynamic_variable_definitions={
"user_name": {"type": "string", "required": True}
},
),
{"user_name": "王先生"},
assistant_id="asst_realtime",
)
brain = build_brain(cfg)
self.assertEqual(brain.system_prompt(cfg), "服务用户 王先生")
self.assertEqual(await brain.greeting(cfg), "您好,王先生")
context = LLMContext(
messages=[{"role": "system", "content": brain.system_prompt(cfg)}]
)
brain.prepare_greeting_context("您好,王先生", context)
self.assertEqual(
[message["role"] for message in context.get_messages()],
["system", "system"],
)
self.assertNotEqual(context.get_messages()[0]["role"], "assistant")
self.assertEqual(
context.get_messages()[1]["content"],
f"{GREETING_CONTEXT_MARKER}\n您好,王先生",
)
async def test_session_update_refreshes_prompt_without_running_llm(self):
cfg = prepare_dynamic_config(
AssistantConfig(
type="prompt",
runtimeMode="pipeline",
prompt="面板状态:{{panel_open}}",
dynamic_variable_definitions={
"panel_open": {"type": "boolean", "default": False}
},
),
{},
assistant_id="asst_session_update",
)
brain = build_brain(cfg)
prompts = []
queued_frames = []
async def queue_frame(frame):
queued_frames.append(frame)
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(
messages=[{"role": "system", "content": brain.system_prompt(cfg)}]
),
llm=FakeLLM(),
queue_frame=queue_frame,
set_system_prompt=prompts.append,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
),
)
result = await brain.on_session_update({"panel_open": True})
self.assertEqual(result.changed, ["panel_open"])
self.assertEqual(result.dynamic_variables, {"panel_open": True})
self.assertEqual(prompts, ["面板状态true"])
self.assertEqual(queued_frames, [])
async def test_end_call_tool_is_owned_by_prompt_brain(self):
brain = build_brain(
AssistantConfig(
type="prompt",
tools=[
RuntimeTool(
id="end-call",
name="结束通话",
function_name="end_call",
type="end_call",
definition={
"config": {
"message_type": "none",
"capture_reason": True,
}
},
)
],
)
)
llm = FakeLLM()
call_end = FakeCallEnd()
visible_tools = []
async def queue_frame(_frame):
pass
await brain.setup(
AssistantConfig(
type="prompt",
tools=[
RuntimeTool(
id="end-call",
name="结束通话",
function_name="end_call",
type="end_call",
definition={"config": {"capture_reason": True}},
)
],
),
BrainRuntime(
context=LLMContext(messages=[]),
llm=llm,
queue_frame=queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda tools: visible_tools.extend(tools or []),
call_end=call_end,
),
)
self.assertEqual(visible_tools[0].name, "end_call")
params = FakeFunctionParams({"reason": "用户已完成咨询"})
await brain.on_assistant_text_start("closing-turn")
await llm.functions["end_call"](params)
self.assertEqual(call_end.reason, "用户已完成咨询")
self.assertFalse(call_end.finished)
await brain.on_assistant_text_end("closing-turn", "", False)
self.assertTrue(call_end.finished)
self.assertFalse(call_end.waited_for_text)
self.assertTrue(call_end.response_started)
self.assertEqual(params.result["action"], "ending_call")
async def test_end_call_waits_for_prompt_generated_closing_speech(self):
tool = RuntimeTool(
id="end-call",
name="结束通话",
function_name="end_call",
type="end_call",
definition={"config": {"message_type": "none"}},
)
cfg = AssistantConfig(type="prompt", tools=[tool])
brain = build_brain(cfg)
llm = FakeLLM()
call_end = FakeCallEnd()
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(messages=[]),
llm=llm,
queue_frame=lambda _frame: None,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=call_end,
),
)
await brain.on_assistant_text_start("closing-turn")
await llm.functions["end_call"](FakeFunctionParams({}))
await brain.on_assistant_text_end(
"closing-turn",
"感谢您的来电。祝您生活愉快,再见。",
False,
)
self.assertFalse(call_end.finished)
self.assertTrue(call_end.armed)
self.assertTrue(call_end.waited_for_text)
async def test_http_tool_renders_secrets_and_updates_prompt_variable(self):
requests = []
class FakeResponse:
status_code = 200
content = b'{"order":{"status":"paid"}}'
def raise_for_status(self):
return None
def json(self):
return {"order": {"status": "paid"}}
class FakeClient:
def __init__(self, **_kwargs):
pass
async def __aenter__(self):
return self
async def __aexit__(self, *_args):
return None
async def request(self, method, url, **kwargs):
requests.append((method, url, kwargs))
return FakeResponse()
cfg = prepare_dynamic_config(
AssistantConfig(
type="prompt",
runtimeMode="pipeline",
prompt="订单状态:{{order_status}}",
dynamic_variable_definitions={
"order_status": {"type": "string", "default": "unknown"}
},
tools=[
RuntimeTool(
id="lookup",
name="查询订单",
function_name="lookup_order",
type="http",
description="查询订单状态",
definition={
"config": {
"method": "GET",
"url": "https://example.test/orders/{order_id}",
"headers": {"Authorization": "Bearer {{secret__token}}"},
"parameters": [
{
"name": "order_id",
"type": "string",
"location": "path",
"required": True,
},
{
"name": "Authorization",
"type": "string",
"location": "header",
"required": False,
},
],
"dynamic_variable_assignments": {
"order_status": "response.order.status"
},
}
},
secrets={"dynamic_variables": {"secret__token": "server-token"}},
)
],
),
{},
assistant_id="asst_1",
)
brain = build_brain(cfg)
llm = FakeLLM()
prompts = []
visible_tools = []
async def queue_frame(_frame):
pass
await brain.setup(
cfg,
BrainRuntime(
context=LLMContext(messages=[]),
llm=llm,
queue_frame=queue_frame,
set_system_prompt=prompts.append,
set_tools=lambda tools: visible_tools.extend(tools or []),
call_end=FakeCallEnd(),
),
)
params = FakeFunctionParams(
{"order_id": "A/1", "Authorization": "attacker-value"}
)
with patch("services.tool_executor.httpx.AsyncClient", FakeClient):
await llm.functions["lookup_order"](params)
self.assertEqual(requests[0][1], "https://example.test/orders/A%2F1")
self.assertEqual(
requests[0][2]["headers"]["Authorization"], "Bearer server-token"
)
self.assertEqual(params.result["updated_variables"], ["order_status"])
self.assertEqual(prompts[-1], "订单状态paid")
class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
async def test_flow_manager_dispatches_native_vision_without_auxiliary_handler(self):
manager = object.__new__(ConfiguredFlowManager)
fallback_transition = AsyncMock()
native_handler = AsyncMock()
native_enabled = {"value": True}
async def flow_handler(_args, _manager):
return {"status": "ok"}
setattr(
flow_handler,
"_workflow_native_vision_handler",
native_handler,
)
setattr(
flow_handler,
"_workflow_native_vision_enabled",
lambda: native_enabled["value"],
)
with patch.object(
FlowManager,
"_create_transition_func",
new=AsyncMock(return_value=fallback_transition),
):
transition = await manager._create_transition_func(
"fetch_user_image",
flow_handler,
)
params = SimpleNamespace()
await transition(params)
native_handler.assert_awaited_once_with(params)
fallback_transition.assert_not_awaited()
native_enabled["value"] = False
await transition(params)
fallback_transition.assert_awaited_once_with(params)
def test_client_tool_session_wait_disables_flow_timeout(self):
brain = WorkflowBrain(
{
"specVersion": 3,
"settings": {},
"nodes": [{"id": "start", "type": "start", "data": {}}],
"edges": [],
}
)
timeout_tool = RuntimeTool(
id="client_timeout",
name="限时等待",
function_name="wait_with_timeout",
type="client",
definition={
"type": "client",
"config": {"timeout_seconds": 7},
},
)
session_tool = RuntimeTool(
id="client_session",
name="会话内等待",
function_name="wait_for_session",
type="client",
definition={
"type": "client",
"config": {
"timeout_seconds": 7,
"response_wait_mode": "session",
},
},
)
self.assertEqual(brain._flow_tool(timeout_tool, "start").timeout_secs, 7.0)
self.assertIsNone(brain._flow_tool(session_tool, "start").timeout_secs)
async def test_session_update_refreshes_current_agent_without_routing(self):
cfg = prepare_dynamic_config(
AssistantConfig(
type="workflow",
graph={
"specVersion": 3,
"settings": {"globalPrompt": "面板 {{panel_open}}"},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "agent",
"type": "agent",
"data": {"prompt": "等待用户操作"},
},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "agent",
"data": {"mode": "always", "priority": 0},
}
],
},
dynamic_variable_definitions={
"panel_open": {"type": "boolean", "default": False}
},
),
{},
assistant_id="asst_workflow_session_update",
)
brain = WorkflowBrain(cfg)
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(),
)
brain._state.enter("agent", WorkflowStatus.WAITING_USER)
result = await brain.on_session_update({"panel_open": True})
self.assertEqual(result.changed, ["panel_open"])
prompt_updates = [
frame for frame in queued if isinstance(frame, LLMUpdateSettingsFrame)
]
self.assertEqual(len(prompt_updates), 1)
self.assertIn("面板 true", prompt_updates[0].delta.system_instruction)
self.assertFalse(any(isinstance(frame, LLMRunFrame) for frame in queued))
variable_event = next(
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "workflow-variables"
)
self.assertEqual(variable_event["reason"], "session_update")
self.assertEqual(variable_event["changed"], ["panel_open"])
async def test_agent_vision_tool_is_scoped_to_effective_stage(self):
brain = WorkflowBrain(
{
"specVersion": 3,
"settings": {
"globalPrompt": "全局规则",
"defaultLlmResourceId": "llm_global",
"visionEnabled": True,
"visionModelResourceId": "vision_global",
},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "agent",
"type": "agent",
"data": {
"prompt": "观察用户需要展示的物品",
"inheritGlobalConfig": True,
},
},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "agent",
"data": {"mode": "always", "priority": 0},
}
],
}
)
scopes = []
vision_function = object()
async def queue_frame(_frame):
pass
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(),
set_vision_scope=scopes.append,
vision_function=vision_function,
)
await brain._apply_agent_stage("agent")
inherited_config = brain._agent_config("agent")
self.assertIn(vision_function, inherited_config["functions"])
self.assertIn("fetch_user_image", inherited_config["role_message"])
self.assertEqual(
scopes[-1],
{
"enabled": True,
"vision_model_resource_id": "vision_global",
"llm_resource_id": "llm_global",
},
)
brain._engine.data("agent").update(
{
"inheritGlobalConfig": False,
"llmResourceId": "llm_agent",
"visionEnabled": False,
"visionModelResourceId": "",
}
)
await brain._apply_agent_stage("agent")
custom_config = brain._agent_config("agent")
self.assertNotIn(vision_function, custom_config["functions"])
self.assertNotIn("fetch_user_image", custom_config["role_message"])
self.assertFalse(scopes[-1]["enabled"])
async def test_initial_message_starts_without_workflow_greeting(self):
brain = WorkflowBrain(
{
"specVersion": 3,
"settings": {"globalPrompt": "全局规则"},
"nodes": [
{
"id": "start",
"type": "start",
"data": {"greeting": "欢迎使用"},
},
{
"id": "message",
"type": "message",
"data": {
"speech": "请问您怎么称呼?",
"showMessage": False,
},
},
{
"id": "agent",
"type": "agent",
"data": {"prompt": "收集用户信息"},
},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "message",
"data": {"mode": "always", "priority": 0},
},
{
"id": "after_message",
"source": "message",
"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()
self.assertNotIn("greeting", brain._engine.data("start"))
self.assertEqual(
await brain.greeting(
AssistantConfig(type="workflow", greeting="旧助手级开场白")
),
"",
)
await brain.on_connected()
self.assertEqual(brain._manager.current_node, "message")
for _ in range(3):
await asyncio.sleep(0)
self.assertEqual(brain._manager.current_node, "agent")
message_speech_frames = [
frame for frame in queued if isinstance(frame, TTSSpeakFrame)
]
self.assertEqual(len(message_speech_frames), 1)
self.assertEqual(message_speech_frames[0].text, "请问您怎么称呼?")
# Stray generic greeting notifications must not replay the Message.
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",
name="查询订单",
function_name="lookup_order",
type="http",
)
cfg = prepare_dynamic_config(
AssistantConfig(
type="workflow",
graph={
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "lookup_action",
"type": "action",
"data": {
"toolId": "lookup",
"resultAssignments": {
"order_status": "order.status"
},
},
},
],
"edges": [],
},
dynamic_variable_definitions={
"order_status": {"type": "string", "default": "pending"}
},
tools=[tool],
),
{},
assistant_id="asst_workflow_action",
)
brain = WorkflowBrain(cfg)
queued = []
async def queue_frame(frame):
queued.append(frame)
async def execute(_tool, _arguments, *, result_assignments=None):
self.assertEqual(result_assignments, {"order_status": "order.status"})
brain._store.assign("order_status", "paid")
return {
"status": "ok",
"updated_variables": ["order_status"],
}
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(),
session_id="conv_action",
)
brain._tools.execute = execute
outcome = await brain._enter_action("lookup_action")
self.assertEqual(outcome.status, ActionStatus.SUCCESS)
self.assertEqual(outcome.updated_variables, ("order_status",))
self.assertEqual(
brain._store.values["system__last_action_invocation_id"],
outcome.invocation_id,
)
variable_events = [
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "workflow-variables"
]
self.assertEqual(variable_events[-1]["reason"], "action")
self.assertEqual(variable_events[-1]["changed"], ["order_status"])
self.assertEqual(variable_events[-1]["variables"], {"order_status": "paid"})
completed_event = next(
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("event") == "action_completed"
)
self.assertEqual(completed_event["outcome"]["status"], "success")
self.assertEqual(completed_event["outcome"]["updatedVariables"], ["order_status"])
self.assertEqual(completed_event["sessionId"], "conv_action")
self.assertEqual(completed_event["workflowRevision"], brain._engine.revision)
self.assertNotIn("result", completed_event["outcome"])
async def test_action_result_assignment_modes_reach_tool_executor(self):
tool = RuntimeTool(
id="client_action",
name="客户端操作",
function_name="show_message",
type="client",
)
brain = WorkflowBrain(
AssistantConfig(
type="workflow",
graph={
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "inherit_action",
"type": "action",
"data": {
"toolId": "client_action",
"resultAssignmentMode": "inherit",
},
},
{
"id": "override_action",
"type": "action",
"data": {
"toolId": "client_action",
"resultAssignmentMode": "override",
"resultAssignments": {"choice": "action"},
},
},
{
"id": "none_action",
"type": "action",
"data": {
"toolId": "client_action",
"resultAssignmentMode": "none",
},
},
],
"edges": [],
},
tools=[tool],
)
)
captured = []
async def execute(_tool, _arguments, *, result_assignments=None):
captured.append(result_assignments)
return {"status": "ok", "updated_variables": []}
brain._runtime = BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=noop_queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
)
brain._tools.execute = execute
await brain._enter_action("inherit_action")
await brain._enter_action("override_action")
await brain._enter_action("none_action")
self.assertEqual(captured, [None, {"choice": "action"}, {}])
async def test_action_client_error_sets_error_status(self):
tool = RuntimeTool(
id="client_action",
name="客户端操作",
function_name="show_message",
type="client",
)
brain = WorkflowBrain(
AssistantConfig(
type="workflow",
graph={
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "action",
"type": "action",
"data": {"toolId": "client_action"},
},
],
"edges": [],
},
tools=[tool],
)
)
async def execute(_tool, _arguments, *, result_assignments=None):
return {
"status": "error",
"message": "用户关闭了确认弹窗",
"updated_variables": [],
}
brain._runtime = BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=noop_queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
)
brain._tools.execute = execute
outcome = await brain._enter_action("action")
self.assertEqual(outcome.status, ActionStatus.FAILURE)
self.assertEqual(outcome.error.code, "tool_error")
self.assertEqual(brain._store.values["system__last_action_status"], "error")
self.assertEqual(
brain._store.values["system__last_action_error"],
"用户关闭了确认弹窗",
)
async def test_cancelled_action_does_not_follow_failure_or_default_edge(self):
tool = RuntimeTool(
id="client_action",
name="客户端操作",
function_name="show_message",
type="client",
)
brain = WorkflowBrain(
AssistantConfig(
type="workflow",
graph={
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "action",
"type": "action",
"data": {"toolId": "client_action"},
},
{"id": "end", "type": "end", "data": {}},
],
"edges": [
{
"id": "after_action",
"source": "action",
"target": "end",
"data": {"mode": "always"},
}
],
},
tools=[tool],
)
)
queued = []
async def queue_frame(frame):
queued.append(frame)
async def execute(_tool, _arguments, *, result_assignments=None):
return {
"status": "error",
"message": "会话已结束",
"updated_variables": [],
}
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._tools.execute = execute
config = await brain._resolve_path("action")
self.assertEqual(config["name"], "action")
self.assertEqual(
brain._store.values["system__last_action_status"],
"cancelled",
)
self.assertFalse(
any(
isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("event") == "edge_selected"
for frame in queued
)
)
async def test_action_block_policy_only_suppresses_input_while_running(self):
tool = RuntimeTool(
id="client_action",
name="客户端操作",
function_name="show_message",
type="client",
)
brain = WorkflowBrain(
AssistantConfig(
type="workflow",
graph={
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "block_action",
"type": "action",
"data": {
"toolId": "client_action",
"userInputPolicy": "block",
},
},
{
"id": "queue_action",
"type": "action",
"data": {
"toolId": "client_action",
"userInputPolicy": "queue",
},
},
],
"edges": [],
},
tools=[tool],
)
)
input_states = []
async def execute(_tool, _arguments, *, result_assignments=None):
input_states.append("executing")
return {"status": "ok", "updated_variables": []}
brain._runtime = BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=noop_queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=FakeCallEnd(),
set_input_enabled=input_states.append,
)
brain._tools.execute = execute
await brain._enter_action("block_action")
self.assertEqual(input_states, [False, "executing", True])
input_states.clear()
await brain._enter_action("queue_action")
self.assertEqual(input_states, ["executing"])
async def test_message_starts_speech_and_releases_on_confirmation(self):
brain = WorkflowBrain(
AssistantConfig(
type="workflow",
graph={
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "message",
"type": "message",
"data": {
"speech": "请先确认 {{customer}} 的重要信息。",
"showMessage": True,
"title": "重要提示",
"message": "请核对客户信息。",
"confirmLabel": "确认",
"requireConfirmation": True,
},
},
],
"edges": [],
},
)
)
brain._store.values["customer"] = "王先生"
events = []
async def queue_frame(frame):
if isinstance(frame, TTSSpeakFrame):
events.append(("speech", frame.text))
class OrderedCallEnd(FakeCallEnd):
def __init__(self):
super().__init__()
self.playback_completion = None
def track_speech(self):
self.tracked_speeches += 1
events.append("tracked")
self.playback_completion = asyncio.get_running_loop().create_future()
return self.playback_completion
message_started = asyncio.Event()
user_confirmed = asyncio.Event()
class FakeClientTools:
async def call(self, function_name, arguments, **options):
self.function_name = function_name
self.arguments = arguments
self.options = options
events.append("message_displayed")
message_started.set()
await user_confirmed.wait()
return {"status": "ok", "data": {"action": "confirmed"}}
input_states = []
call_end = OrderedCallEnd()
client_tools = FakeClientTools()
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,
client_tools=client_tools,
set_input_enabled=input_states.append,
)
brain._message_stages.set_client_tools(client_tools)
message_task = asyncio.create_task(brain._enter_message("message"))
await message_started.wait()
self.assertEqual(
events,
[
"tracked",
("speech", "请先确认 王先生 的重要信息。"),
"message_displayed",
],
)
self.assertEqual(input_states, [False])
self.assertEqual(client_tools.function_name, "show_message")
self.assertEqual(client_tools.options["response_wait_mode"], "session")
user_confirmed.set()
result = await message_task
self.assertTrue(result.succeeded)
self.assertEqual(result.action, "confirmed")
self.assertFalse(call_end.playback_completion.done())
self.assertEqual(input_states, [False, True])
async def test_speech_only_message_waits_for_transport_playback(self):
brain = WorkflowBrain(
{
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "message",
"type": "message",
"data": {"speech": "正在为您准备服务。"},
},
],
"edges": [],
}
)
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
call_end = PlaybackCallEnd()
input_states = []
brain._runtime = BrainRuntime(
context=LLMContext(messages=[]),
llm=FakeLLM(),
queue_frame=noop_queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=call_end,
set_input_enabled=input_states.append,
)
message_task = asyncio.create_task(brain._enter_message("message"))
await asyncio.sleep(0)
self.assertFalse(message_task.done())
self.assertEqual(input_states, [False])
call_end.completion.set_result(None)
result = await message_task
self.assertTrue(result.succeeded)
self.assertEqual(input_states, [False, True])
async def test_message_between_agents_resumes_after_playback(self):
graph = {
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "opening",
"type": "message",
"data": {"speech": "欢迎使用。"},
},
{
"id": "agent1",
"type": "agent",
"data": {"prompt": "收集基本信息"},
},
{
"id": "middle",
"type": "message",
"data": {"speech": "现在进入信息确认。"},
},
{
"id": "agent2",
"type": "agent",
"data": {"prompt": "确认信息", "contextPolicy": "fresh"},
},
{
"id": "end",
"type": "end",
"data": {"scope": "session"},
},
],
"edges": [
{
"id": "start-opening",
"source": "start",
"target": "opening",
"data": {"mode": "always"},
},
{
"id": "opening-agent1",
"source": "opening",
"target": "agent1",
"data": {"mode": "always"},
},
{
"id": "agent1-middle",
"source": "agent1",
"target": "middle",
"data": {
"mode": "llm",
"priority": 10,
"condition": "基本信息已经收集完成",
},
},
{
"id": "middle-agent2",
"source": "middle",
"target": "agent2",
"data": {"mode": "always"},
},
{
"id": "agent2-end",
"source": "agent2",
"target": "end",
"data": {
"mode": "llm",
"priority": 10,
"condition": "用户确认可以结束通话",
},
},
],
}
brain = WorkflowBrain(graph)
queued = []
input_states = []
class PlaybackCallEnd(FakeCallEnd):
def __init__(self):
super().__init__()
self.completions = []
def track_speech(self):
completion = asyncio.get_running_loop().create_future()
self.completions.append(completion)
return 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()
class MatchingRouter:
async def select_edge(self, **kwargs):
edge = kwargs["edges"][0]
return LLMRouteResult(
status=RouteStatus.MATCHED,
function_name=kwargs["edge_name"](edge),
)
brain._router = MatchingRouter()
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, "opening")
self.assertEqual(len(call_end.completions), 1)
call_end.completions[0].set_result(None)
for _ in range(5):
await asyncio.sleep(0)
if manager.current_node == "agent1":
break
self.assertEqual(manager.current_node, "agent1")
# The user-turn processor must return while the second Message is
# still waiting for its transport playback boundary.
await asyncio.wait_for(
brain.on_user_turn_end("基本信息已经收集完成"),
timeout=0.1,
)
await asyncio.sleep(0)
self.assertEqual(manager.current_node, "middle")
self.assertEqual(len(call_end.completions), 2)
self.assertFalse(call_end.completions[1].done())
call_end.completions[1].set_result(None)
for _ in range(5):
await asyncio.sleep(0)
if manager.current_node == "agent2":
break
self.assertEqual(manager.current_node, "agent2")
self.assertTrue(any(isinstance(frame, LLMRunFrame) for frame in queued))
self.assertEqual(
manager.configs[-1]["task_messages"],
[
{"role": "user", "content": "基本信息已经收集完成"},
{"role": "assistant", "content": "现在进入信息确认。"},
],
)
await brain.on_assistant_text_end("agent2-turn", "信息确认完成", False)
await brain.on_user_turn_end("结束通话")
self.assertEqual(manager.current_node, "end")
self.assertTrue(call_end.finished)
async def test_nodes_without_outgoing_edges_remain_active(self):
queued = []
async def queue_frame(frame):
queued.append(frame)
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, current_node=None):
self.current_node = current_node
async def initialize(self, config):
self.current_node = config["name"]
start_brain = WorkflowBrain(
{
"specVersion": 3,
"settings": {},
"nodes": [{"id": "start", "type": "start", "data": {}}],
"edges": [],
}
)
start_brain._runtime = runtime
start_brain._manager = FakeManager()
await start_brain.on_connected()
self.assertEqual(start_brain._manager.current_node, "start")
queued.clear()
await start_brain.on_client_ready()
ready_messages = [
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
]
self.assertIn(
{"type": "node-active", "nodeId": "start"},
ready_messages,
)
self.assertTrue(
any(
message.get("type") == "workflow-variables"
and message.get("reason") == "client_ready"
and message.get("nodeId") == "start"
for message in ready_messages
)
)
agent_brain = WorkflowBrain(
{
"specVersion": 3,
"settings": {"globalPrompt": "全局规则"},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "agent",
"type": "agent",
"data": {"prompt": "持续回答"},
},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "agent",
"data": {"mode": "always", "priority": 0},
}
],
}
)
agent_brain._runtime = runtime
agent_brain._manager = FakeManager("agent")
queued.clear()
await agent_brain.on_client_ready()
self.assertTrue(
any(
isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message
== {"type": "node-active", "nodeId": "agent"}
for frame in queued
)
)
queued.clear()
handled = await agent_brain.on_user_turn_end("请继续回答")
self.assertTrue(handled)
self.assertEqual(agent_brain._manager.current_node, "agent")
self.assertTrue(any(isinstance(frame, LLMRunFrame) for frame in queued))
handoff_brain = WorkflowBrain(
{
"specVersion": 3,
"settings": {},
"nodes": [
{"id": "start", "type": "start", "data": {}},
{
"id": "handoff",
"type": "handoff",
"data": {"targetType": "human"},
},
],
"edges": [],
}
)
handoff_brain._runtime = runtime
handoff_config = await handoff_brain._resolve_path("handoff")
self.assertEqual(handoff_config["name"], "handoff")
self.assertTrue(
any(
isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "handoff-requested"
for frame in queued
)
)
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"},
},
{
"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
self.current_user_message = None
async def select_edge(self, **kwargs):
self.calls += 1
self.current_user_message = kwargs.get("current_user_message")
return LLMRouteResult(
status=RouteStatus.MATCHED,
function_name="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)
image_message = {
"role": "user",
"content": [
{"type": "text", "text": "我想吃饭"},
{
"type": "image_url",
"image_url": {"url": "data:image/jpeg;base64,AA=="},
},
],
}
handled = await brain.on_user_turn_end(
"我想吃饭",
user_message=image_message,
)
self.assertTrue(handled)
self.assertEqual(router.calls, 1)
self.assertEqual(router.current_user_message, image_message)
self.assertEqual(manager.current_node, "eat")
self.assertIn(
image_message,
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_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(
{
"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 LLMRouteResult(
status=RouteStatus.MATCHED,
function_name="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 = LLMRouteResult(status=RouteStatus.NO_MATCH)
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 = LLMRouteResult(
status=RouteStatus.MATCHED,
function_name="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,
"settings": {
"globalPrompt": "全局规则",
"defaultLlmResourceId": "llm_global",
"defaultAsrResourceId": "asr_global",
"defaultTtsResourceId": "tts_global",
"knowledgeBaseId": "kb_global",
"knowledgeMode": "automatic",
"enableInterrupt": False,
"turnConfig": {
"bargeIn": {"strategy": "transcription"},
"vad": {"confidence": 0.55},
},
},
"nodes": [
{
"id": "start",
"type": "start",
"data": {"name": "Start"},
},
{
"id": "agent",
"type": "agent",
"data": {
"name": "收集需求",
"prompt": "服务 {{user_name}}",
"contextPolicy": "fresh",
},
},
{
"id": "end",
"type": "end",
"data": {"name": "End", "message": "感谢来电", "scope": "session"},
},
],
"edges": [
{
"id": "begin",
"source": "start",
"target": "agent",
"data": {"mode": "always", "priority": 0},
},
{
"id": "finish",
"source": "agent",
"target": "end",
"data": {
"mode": "llm",
"priority": 10,
"condition": "需求已收集",
"transitionSpeech": "正在为你结束流程",
},
}
],
}
cfg = prepare_dynamic_config(
AssistantConfig(
type="workflow",
graph=graph,
dynamic_variable_definitions={
"user_name": {"type": "string", "required": True}
},
),
{"user_name": "王先生"},
assistant_id="asst_workflow",
)
brain = WorkflowBrain(cfg)
llm = FakeLLM()
context = LLMContext(messages=[])
queued = []
service_switches = []
knowledge_scopes = []
turn_configs = []
call_end = FakeCallEnd()
class FakeWorker:
def __init__(self):
self.frames = []
self.handlers = {}
def set_reached_downstream_filter(self, *_args):
pass
def event_handler(self, name):
def decorator(fn):
self.handlers[name] = fn
return fn
return decorator
async def queue_frame(self, frame):
self.frames.append(frame)
async def queue_frames(self, frames):
self.frames.extend(frames)
worker = FakeWorker()
pair = SimpleNamespace(
user=lambda: SimpleNamespace(_context=context),
assistant=lambda: SimpleNamespace(has_function_calls_in_progress=False),
)
async def queue_frame(frame):
queued.append(frame)
async def switch_services(llm_id, asr_id, tts_id):
service_switches.append((llm_id, asr_id, tts_id))
async def apply_turn_config(enable_interrupt, turn_config):
turn_configs.append((enable_interrupt, turn_config))
runtime = BrainRuntime(
context=context,
llm=llm,
queue_frame=queue_frame,
set_system_prompt=lambda _prompt: None,
set_tools=lambda _tools: None,
call_end=call_end,
worker=worker,
context_aggregator=pair,
switch_services=switch_services,
set_knowledge_scope=knowledge_scopes.append,
apply_turn_config=apply_turn_config,
)
await brain.setup(cfg, runtime)
greeting = await brain.greeting(cfg)
self.assertEqual(greeting, "")
self.assertEqual(context.get_messages(), [])
await brain.on_connected()
self.assertEqual(brain._manager.current_node, "agent")
await brain.on_client_ready()
variable_events = [
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "workflow-variables"
]
self.assertEqual(variable_events[0]["reason"], "initialized")
self.assertEqual(variable_events[0]["variables"], {"user_name": "王先生"})
self.assertNotIn("system__conversation_id", variable_events[0]["variables"])
self.assertEqual(
service_switches,
[("llm_global", "asr_global", "tts_global")],
)
self.assertEqual(knowledge_scopes[-1]["knowledge_base_id"], "kb_global")
self.assertEqual(turn_configs[-1][0], False)
self.assertEqual(turn_configs[-1][1]["vad"]["confidence"], 0.55)
brain._engine.data("agent").update(
{
"inheritGlobalConfig": False,
"llmResourceId": "llm_agent",
"asrResourceId": "asr_agent",
"ttsResourceId": "tts_agent",
"knowledgeBaseId": "kb_agent",
"knowledgeMode": "on_demand",
"enableInterrupt": True,
"turnConfig": {
"bargeIn": {"strategy": "vad"},
"turnDetection": {"strategy": "smart_turn"},
},
}
)
await brain._apply_agent_stage("agent")
self.assertEqual(
service_switches[-1],
("llm_agent", "asr_agent", "tts_agent"),
)
self.assertEqual(knowledge_scopes[-1]["knowledge_base_id"], "kb_agent")
self.assertEqual(turn_configs[-1][0], True)
self.assertEqual(
turn_configs[-1][1]["turnDetection"]["strategy"],
"smart_turn",
)
agent_config = brain._agent_config("agent")
self.assertIn("王先生", agent_config["role_message"])
self.assertIn("工作流路由已在用户一轮输入结束时完成", agent_config["role_message"])
self.assertEqual(agent_config["task_messages"], [])
self.assertFalse(agent_config["respond_immediately"])
self.assertFalse(any(isinstance(frame, LLMRunFrame) for frame in worker.frames))
self.assertEqual(
agent_config["context_strategy"].strategy.value,
"reset",
)
brain._engine.data("agent")["entryMode"] = "generate"
generate_config = brain._agent_config("agent")
self.assertFalse(generate_config["respond_immediately"])
worker.frames.clear()
await brain._manager.set_node_from_config(generate_config)
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")["entryMode"] = "wait_user"
self.assertEqual(
brain._agent_config(
"agent",
[{"role": "assistant", "content": "正在进入下一阶段"}],
)["task_messages"],
[{"role": "assistant", "content": "正在进入下一阶段"}],
)
self.assertFalse(
any(
function.name == "goto_finish"
for function in brain._agent_config("agent")["functions"]
)
)
await brain.on_assistant_text_end("old-turn", "需求已收集", False)
self.assertEqual(brain._manager.current_node, "agent")
class FakeRouter:
async def select_edge(self, **_kwargs):
return LLMRouteResult(
status=RouteStatus.MATCHED,
function_name="goto_finish",
)
brain._router = FakeRouter()
handled = await brain.on_user_turn_end("我的需求已经说完了")
self.assertTrue(handled)
self.assertEqual(brain._manager.current_node, "end")
self.assertIn("我的需求已经说完了", brain._store.values["system__conversation_history"])
self.assertTrue(call_end.ending)
self.assertTrue(call_end.armed)
self.assertTrue(any(getattr(frame, "text", "") == "感谢来电" for frame in queued))
transition_context_frames = [
frame
for frame in worker.frames
if isinstance(frame, LLMMessagesAppendFrame)
and frame.messages
== [{"role": "assistant", "content": "正在为你结束流程"}]
]
self.assertTrue(transition_context_frames)
transition_events = [
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("source") == "workflow-edge-transition"
]
self.assertEqual(transition_events[0]["content"], "正在为你结束流程")
self.assertEqual(transition_events[0]["nodeId"], "end")
assistant_transcripts = [
frame.message.get("content")
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "transcript"
and frame.message.get("role") == "assistant"
]
self.assertEqual(
assistant_transcripts,
["正在为你结束流程", "感谢来电"],
)
self.assertIn(
"正在为你结束流程",
brain._store.values["system__conversation_history"],
)
self.assertIn(
"感谢来电",
brain._store.values["system__conversation_history"],
)
if __name__ == "__main__":
unittest.main()