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.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 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): 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 async def select_edge(self, **_kwargs): self.calls += 1 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) 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_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()