Files
ai-video-fullstack/backend/tests/test_brains.py
T
Xin Wang e78dc4088a Implement response handling and speech tracking in CallEnd functionality
- Add begin_response and finish_after_current_speech methods to CallEndCoordinator for better management of speech events.
- Update PromptBrain to utilize new methods, ensuring proper handling of generated closing speech and tool-only calls.
- Enhance tests to verify the correct behavior of speech tracking and response handling in various scenarios, including waiting for audio to finish before ending calls.
- Introduce a new test suite for CallEndCoordinator to validate the interaction with speech frames.
2026-07-14 13:43:42 +08:00

979 lines
34 KiB
Python

from __future__ import annotations
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from models import AssistantConfig, RuntimeTool
from pipecat.frames.frames import (
LLMContextFrame,
LLMFullResponseEndFrame,
LLMFullResponseStartFrame,
LLMMessagesAppendFrame,
LLMMessagesUpdateFrame,
LLMRunFrame,
LLMTextFrame,
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
class FakeLLM:
def __init__(self):
self.functions = {}
def register_function(self, name, handler):
self.functions[name] = handler
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
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
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
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_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_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_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(),
)
brain._tools.execute = execute
await brain._enter_action("lookup_action")
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"})
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_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",
"greeting": "欢迎,{{user_name}}",
},
},
{
"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, "欢迎,王先生")
greeting_message = {
"role": "system",
"content": f"{GREETING_CONTEXT_MARKER}\n欢迎,王先生",
}
brain.prepare_greeting_context(greeting, context)
self.assertEqual(context.get_messages(), [greeting_message])
await brain.on_connected()
self.assertEqual(brain._manager.current_node, "agent")
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"], [greeting_message])
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.assertTrue(generate_config["respond_immediately"])
worker.frames.clear()
await brain._manager.set_node_from_config(generate_config)
self.assertTrue(any(isinstance(frame, LLMRunFrame) for frame in worker.frames))
brain._engine.data("agent").update(
{"entryMode": "fixed_speech", "entrySpeech": "您好,{{user_name}}"}
)
fixed_config = brain._agent_config("agent")
self.assertFalse(fixed_config["respond_immediately"])
self.assertEqual(
fixed_config["pre_actions"][0]["type"],
"workflow_fixed_speech",
)
self.assertEqual(fixed_config["pre_actions"][0]["text"], "您好,王先生")
self.assertEqual(
fixed_config["task_messages"],
[
greeting_message,
{"role": "assistant", "content": "您好,王先生"},
],
)
self.assertEqual(
brain._agent_config(
"agent",
[{"role": "assistant", "content": "正在进入下一阶段"}],
)["task_messages"],
[
greeting_message,
{"role": "assistant", "content": "正在进入下一阶段"},
{"role": "assistant", "content": "您好,王先生"},
],
)
self.assertEqual(fixed_config["pre_actions"][0]["node_id"], "agent")
worker.frames.clear()
queued.clear()
await brain._manager.set_node_from_config(fixed_config)
self.assertTrue(any(isinstance(frame, TTSSpeakFrame) for frame in queued))
self.assertFalse(any(isinstance(frame, LLMRunFrame) for frame in worker.frames))
context_updates = [
frame
for frame in worker.frames
if isinstance(frame, LLMMessagesUpdateFrame)
]
self.assertEqual(
context_updates[-1].messages,
[
greeting_message,
{"role": "assistant", "content": "您好,王先生"},
],
)
self.assertFalse(
any(
isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("source") == "workflow-fixed-reply"
for frame in queued
)
)
await brain.on_client_ready()
fixed_reply_events = [
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("source") == "workflow-fixed-reply"
]
self.assertEqual(fixed_reply_events[0]["content"], "您好,王先生")
self.assertEqual(fixed_reply_events[0]["nodeId"], "agent")
self.assertIn("您好,王先生", brain._store.values["system__conversation_history"])
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 "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()