Refactor workflow routing and greeting management in Brain classes

- Update WorkflowBrain to handle greeting playback more effectively, ensuring that the initial greeting completes before transitioning to the first node.
- Introduce new methods for managing greeting states and conditions, enhancing the interaction flow for user turns.
- Refactor WorkflowLLMRouter to improve routing logic and ensure proper handling of conditional paths.
- Enhance tests to verify the correct behavior of greeting management and routing under various scenarios, including waiting for audio playback to finish.
- Update frontend components to reflect changes in edge handling and improve user experience in workflow configurations.
This commit is contained in:
Xin Wang
2026-07-17 22:01:42 +08:00
parent 34c0d12d2a
commit 162a3d8bec
15 changed files with 826 additions and 147 deletions

View File

@@ -3,6 +3,8 @@
from loguru import logger
from pipecat.frames.frames import (
BotStartedSpeakingFrame,
BotStoppedSpeakingFrame,
EndFrame,
LLMMessagesAppendFrame,
OutputTransportMessageUrgentFrame,
@@ -33,6 +35,28 @@ def bind_cascade_pipeline_events(
pending_text_inputs: list[str] = []
greeting_transcript_sent = False
greeting_timestamp = ""
greeting_playback_pending = False
greeting_playback_started = False
# FlowManager already observes downstream frames for its own actions. Add
# to that filter instead of replacing it, then use the real transport
# playback boundary to release Workflow startup.
worker.add_reached_downstream_filter(
(BotStartedSpeakingFrame, BotStoppedSpeakingFrame)
)
@worker.event_handler("on_frame_reached_downstream")
async def on_frame_reached_downstream(_worker, frame):
nonlocal greeting_playback_pending, greeting_playback_started
if not greeting_playback_pending:
return
if isinstance(frame, BotStartedSpeakingFrame):
greeting_playback_started = True
return
if isinstance(frame, BotStoppedSpeakingFrame) and greeting_playback_started:
greeting_playback_pending = False
greeting_playback_started = False
await brain.on_greeting_finished()
async def queue_transcript(role: str, content: str, timestamp: str) -> None:
if not content:
@@ -132,7 +156,7 @@ def bind_cascade_pipeline_events(
@transport.event_handler("on_client_connected")
async def on_client_connected(_transport, _client):
nonlocal greeting_timestamp
nonlocal greeting_timestamp, greeting_playback_pending
if vision_enabled:
try:
vision_state["client_id"] = get_transport_client_id(
@@ -145,16 +169,24 @@ def bind_cascade_pipeline_events(
)
except Exception as exc: # noqa: BLE001 - media availability is optional
logger.warning(f"视觉理解摄像头捕获初始化失败: {exc}")
if greeting:
has_greeting = bool(greeting.strip())
if has_greeting:
# Preserve the actual playback order. The transcript is delivered
# later on client-ready, but the preview sorts by this timestamp.
greeting_timestamp = greeting_timestamp or time_now_iso8601()
if brain.spec.owns_context:
brain.prepare_greeting_context(greeting, context)
greeting_playback_pending = True
# Initialize the Workflow before the greeting is queued so a very
# short TTS response cannot finish before the brain arms its startup
# gate. Other brain types simply ignore greeting_pending.
await brain.on_connected(greeting_pending=has_greeting)
if has_greeting:
await worker.queue_frame(
TTSSpeakFrame(greeting, append_to_context=False)
)
await brain.on_connected()
@transport.event_handler("on_client_disconnected")
async def on_client_disconnected(_transport, _client):