- Introduce WorkflowAgentStage to manage agent stage configurations and enhance interaction with the workflow engine. - Implement WorkflowEdgeEvaluator for priority-aware edge evaluation, improving routing decisions based on conditions and user turns. - Update WorkflowBrain to handle user turns and routing more effectively, ensuring agents cannot have only one default path. - Enhance CallEndCoordinator to track speech events and manage call termination based on queued speech. - Add new models and output handling for workflow interactions, improving clarity and maintainability. - Update tests to validate the new routing logic and agent behavior under various scenarios.
96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
"""Shared call termination timing for prompt tools and workflow end nodes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
from loguru import logger
|
|
from pipecat.frames.frames import BotStartedSpeakingFrame, BotStoppedSpeakingFrame
|
|
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
|
|
|
|
|
class CallEndCoordinator:
|
|
"""End immediately or after the currently armed closing speech finishes."""
|
|
|
|
def __init__(self, queue_end: Callable[[str], Awaitable[None]]):
|
|
self._queue_end = queue_end
|
|
self._ending = False
|
|
self._armed = False
|
|
self._speaking = False
|
|
self._response_speech_started = False
|
|
self._tracked_speeches = 0
|
|
self._finish_after_tracked_speech = False
|
|
self._finished = False
|
|
self._reason = "completed"
|
|
|
|
@property
|
|
def ending(self) -> bool:
|
|
return self._ending
|
|
|
|
def begin(self, reason: str) -> None:
|
|
self._ending = True
|
|
self._reason = reason or "completed"
|
|
|
|
def begin_response(self) -> None:
|
|
"""Start tracking speech produced by one LLM response."""
|
|
self._response_speech_started = False
|
|
|
|
def arm_after_speech(self) -> None:
|
|
"""Wait for the next observed bot speech to finish."""
|
|
self._armed = True
|
|
|
|
def track_speech(self) -> None:
|
|
"""Register one fixed utterance before its TTSSpeakFrame is queued."""
|
|
self._tracked_speeches += 1
|
|
|
|
async def arm_after_tracked_speech(self) -> None:
|
|
"""Finish after every already queued fixed utterance has played."""
|
|
self._finish_after_tracked_speech = True
|
|
if self._tracked_speeches == 0:
|
|
await self.finish()
|
|
|
|
async def finish_after_current_speech(self, *, has_text: bool) -> None:
|
|
"""Finish now if speech is absent/done, otherwise wait for its stop."""
|
|
if not has_text:
|
|
await self.finish()
|
|
return
|
|
if self._response_speech_started and not self._speaking:
|
|
await self.finish()
|
|
return
|
|
self._armed = True
|
|
|
|
async def finish(self) -> None:
|
|
if self._finished:
|
|
return
|
|
self._finished = True
|
|
await self._queue_end(self._reason)
|
|
|
|
async def observe(self, frame) -> None:
|
|
if isinstance(frame, BotStartedSpeakingFrame):
|
|
self._speaking = True
|
|
self._response_speech_started = True
|
|
elif isinstance(frame, BotStoppedSpeakingFrame) and self._speaking:
|
|
self._speaking = False
|
|
if self._tracked_speeches > 0:
|
|
self._tracked_speeches -= 1
|
|
if (
|
|
self._finish_after_tracked_speech
|
|
and self._tracked_speeches == 0
|
|
):
|
|
logger.info("所有工作流结束语播报完毕,挂断通话")
|
|
await self.finish()
|
|
elif self._armed:
|
|
logger.info("结束语播报完毕,挂断通话")
|
|
await self.finish()
|
|
|
|
|
|
class EndCallAfterSpeechProcessor(FrameProcessor):
|
|
def __init__(self, coordinator: CallEndCoordinator):
|
|
super().__init__()
|
|
self._coordinator = coordinator
|
|
|
|
async def process_frame(self, frame, direction: FrameDirection):
|
|
await super().process_frame(frame, direction)
|
|
await self.push_frame(frame, direction)
|
|
await self._coordinator.observe(frame)
|