Enhance greeting context management in Brain classes
- Introduce greeting context handling in BaseBrain and WorkflowBrain to manage assistant greetings effectively. - Implement prepare_greeting_context method to add greeting messages to the local context while preserving playback order. - Update pipeline event handling to ensure greeting timestamps are maintained until the client is ready. - Enhance tests to verify the correct behavior of greeting context management in various scenarios.
This commit is contained in:
@@ -9,6 +9,7 @@ from pipecat.frames.frames import (
|
||||
LLMContextFrame,
|
||||
LLMFullResponseEndFrame,
|
||||
LLMFullResponseStartFrame,
|
||||
LLMMessagesAppendFrame,
|
||||
LLMMessagesUpdateFrame,
|
||||
LLMRunFrame,
|
||||
LLMTextFrame,
|
||||
@@ -19,6 +20,7 @@ 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,
|
||||
@@ -227,6 +229,19 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
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(
|
||||
@@ -604,7 +619,10 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
{
|
||||
"id": "start",
|
||||
"type": "start",
|
||||
"data": {"name": "Start"},
|
||||
"data": {
|
||||
"name": "Start",
|
||||
"greeting": "欢迎,{{user_name}}",
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "agent",
|
||||
@@ -710,6 +728,14 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
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 = [
|
||||
@@ -758,7 +784,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
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.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(
|
||||
@@ -785,7 +811,21 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(fixed_config["pre_actions"][0]["text"], "您好,王先生")
|
||||
self.assertEqual(
|
||||
fixed_config["task_messages"],
|
||||
[{"role": "assistant", "content": "您好,王先生"}],
|
||||
[
|
||||
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()
|
||||
@@ -800,8 +840,19 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
]
|
||||
self.assertEqual(
|
||||
context_updates[-1].messages,
|
||||
[{"role": "assistant", "content": "您好,王先生"}],
|
||||
[
|
||||
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
|
||||
@@ -833,6 +884,22 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
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
|
||||
|
||||
108
backend/tests/test_pipeline_events.py
Normal file
108
backend/tests/test_pipeline_events.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from pipecat.frames.frames import OutputTransportMessageUrgentFrame
|
||||
from services.pipecat.pipeline_events import bind_cascade_pipeline_events
|
||||
|
||||
|
||||
class _EventSource:
|
||||
def __init__(self):
|
||||
self.handlers = {}
|
||||
|
||||
def event_handler(self, name):
|
||||
def decorator(handler):
|
||||
self.handlers[name] = handler
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class _Worker:
|
||||
def __init__(self):
|
||||
self.frames = []
|
||||
|
||||
async def queue_frame(self, frame):
|
||||
self.frames.append(frame)
|
||||
|
||||
|
||||
class _Brain:
|
||||
spec = SimpleNamespace(owns_context=True)
|
||||
|
||||
def __init__(self, worker):
|
||||
self.worker = worker
|
||||
self.prepared_greeting = ""
|
||||
|
||||
def prepare_greeting_context(self, greeting, _context):
|
||||
self.prepared_greeting = greeting
|
||||
|
||||
async def on_connected(self):
|
||||
pass
|
||||
|
||||
async def on_client_ready(self):
|
||||
for content, timestamp in (
|
||||
("Start Edge 过渡语", "2026-07-14T10:00:00.200+00:00"),
|
||||
("Agent 固定进入语", "2026-07-14T10:00:00.300+00:00"),
|
||||
):
|
||||
await self.worker.queue_frame(
|
||||
OutputTransportMessageUrgentFrame(
|
||||
message={
|
||||
"type": "transcript",
|
||||
"role": "assistant",
|
||||
"content": content,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PipelineEventTest(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_greeting_keeps_playback_timestamp_until_client_ready(self):
|
||||
transport = _EventSource()
|
||||
text_input = _EventSource()
|
||||
user_aggregator = _EventSource()
|
||||
assistant_aggregator = _EventSource()
|
||||
worker = _Worker()
|
||||
brain = _Brain(worker)
|
||||
|
||||
bind_cascade_pipeline_events(
|
||||
transport=transport,
|
||||
worker=worker,
|
||||
brain=brain,
|
||||
context=SimpleNamespace(),
|
||||
text_input=text_input,
|
||||
user_aggregator=user_aggregator,
|
||||
assistant_aggregator=assistant_aggregator,
|
||||
greeting="助手开场白",
|
||||
vision_enabled=False,
|
||||
vision_state={"client_id": None},
|
||||
)
|
||||
|
||||
greeting_time = "2026-07-14T10:00:00.100+00:00"
|
||||
with patch(
|
||||
"services.pipecat.pipeline_events.time_now_iso8601",
|
||||
return_value=greeting_time,
|
||||
) as clock:
|
||||
await transport.handlers["on_client_connected"](transport, object())
|
||||
await text_input.handlers["on_client_ready"](text_input)
|
||||
|
||||
transcripts = [
|
||||
frame.message
|
||||
for frame in worker.frames
|
||||
if isinstance(frame, OutputTransportMessageUrgentFrame)
|
||||
and frame.message.get("type") == "transcript"
|
||||
]
|
||||
ordered = sorted(transcripts, key=lambda message: message["timestamp"])
|
||||
self.assertEqual(
|
||||
[message["content"] for message in ordered],
|
||||
["助手开场白", "Start Edge 过渡语", "Agent 固定进入语"],
|
||||
)
|
||||
self.assertEqual(transcripts[0]["timestamp"], greeting_time)
|
||||
self.assertEqual(brain.prepared_greeting, "助手开场白")
|
||||
clock.assert_called_once_with()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user