Files
ai-video-fullstack/backend/services/pipecat/call_lifecycle.py
Xin Wang bdf3d3dd9c Refactor workflow agent and routing components for improved functionality
- 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.
2026-07-17 22:37:15 +08:00

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)