no need to call start_watchdog() only reset_watchdog()

This commit is contained in:
Aleix Conchillo Flaqué
2025-06-24 16:43:10 -07:00
parent 202055a9b8
commit eb5ecab104
41 changed files with 341 additions and 211 deletions

View File

@@ -21,6 +21,7 @@ from pipecat.frames.frames import (
from pipecat.pipeline.base_pipeline import BasePipeline from pipecat.pipeline.base_pipeline import BasePipeline
from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.pipeline import Pipeline
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, FrameProcessorSetup from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, FrameProcessorSetup
from pipecat.utils.watchdog_queue import WatchdogQueue
class ParallelPipelineSource(FrameProcessor): class ParallelPipelineSource(FrameProcessor):
@@ -83,8 +84,8 @@ class ParallelPipeline(BasePipeline):
self._up_task = None self._up_task = None
self._down_task = None self._down_task = None
self._up_queue = asyncio.Queue() self._up_queue = WatchdogQueue(self)
self._down_queue = asyncio.Queue() self._down_queue = WatchdogQueue(self)
self._pipelines = [] self._pipelines = []
@@ -202,18 +203,14 @@ class ParallelPipeline(BasePipeline):
async def _process_up_queue(self): async def _process_up_queue(self):
while True: while True:
frame = await self._up_queue.get() frame = await self._up_queue.get()
self.start_watchdog()
await self._parallel_push_frame(frame, FrameDirection.UPSTREAM) await self._parallel_push_frame(frame, FrameDirection.UPSTREAM)
self._up_queue.task_done() self._up_queue.task_done()
self.reset_watchdog()
async def _process_down_queue(self): async def _process_down_queue(self):
running = True running = True
while running: while running:
frame = await self._down_queue.get() frame = await self._down_queue.get()
self.start_watchdog()
endframe_counter = self._endframe_counter.get(frame.id, 0) endframe_counter = self._endframe_counter.get(frame.id, 0)
# If we have a counter, decrement it. # If we have a counter, decrement it.
@@ -228,5 +225,3 @@ class ParallelPipeline(BasePipeline):
running = not (endframe_counter == 0 and isinstance(frame, EndFrame)) running = not (endframe_counter == 0 and isinstance(frame, EndFrame))
self._down_queue.task_done() self._down_queue.task_done()
self.reset_watchdog()

View File

@@ -41,6 +41,8 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, F
from pipecat.utils.asyncio import WATCHDOG_TIMEOUT, BaseTaskManager, TaskManager, TaskManagerParams from pipecat.utils.asyncio import WATCHDOG_TIMEOUT, BaseTaskManager, TaskManager, TaskManagerParams
from pipecat.utils.tracing.setup import is_tracing_available from pipecat.utils.tracing.setup import is_tracing_available
from pipecat.utils.tracing.turn_trace_observer import TurnTraceObserver from pipecat.utils.tracing.turn_trace_observer import TurnTraceObserver
from pipecat.utils.watchdog_queue import WatchdogQueue
from pipecat.utils.watchdog_reseter import WatchdogReseter
HEARTBEAT_SECONDS = 1.0 HEARTBEAT_SECONDS = 1.0
HEARTBEAT_MONITOR_SECONDS = HEARTBEAT_SECONDS * 10 HEARTBEAT_MONITOR_SECONDS = HEARTBEAT_SECONDS * 10
@@ -131,7 +133,7 @@ class PipelineTaskSink(FrameProcessor):
await self._down_queue.put(frame) await self._down_queue.put(frame)
class PipelineTask(BasePipelineTask): class PipelineTask(WatchdogReseter, BasePipelineTask):
"""Manages the execution of a pipeline, handling frame processing and task lifecycle. """Manages the execution of a pipeline, handling frame processing and task lifecycle.
It has a couple of event handlers `on_frame_reached_upstream` and It has a couple of event handlers `on_frame_reached_upstream` and
@@ -261,18 +263,18 @@ class PipelineTask(BasePipelineTask):
self._cancelled = False self._cancelled = False
# This queue receives frames coming from the pipeline upstream. # This queue receives frames coming from the pipeline upstream.
self._up_queue = asyncio.Queue() self._up_queue = WatchdogQueue(self)
# This queue receives frames coming from the pipeline downstream. # This queue receives frames coming from the pipeline downstream.
self._down_queue = asyncio.Queue() self._down_queue = WatchdogQueue(self)
# This queue is the queue used to push frames to the pipeline. # This queue is the queue used to push frames to the pipeline.
self._push_queue = asyncio.Queue() self._push_queue = WatchdogQueue(self)
# This is the heartbeat queue. When a heartbeat frame is received in the # This is the heartbeat queue. When a heartbeat frame is received in the
# down queue we add it to the heartbeat queue for processing. # down queue we add it to the heartbeat queue for processing.
self._heartbeat_queue = asyncio.Queue() self._heartbeat_queue = WatchdogQueue(self)
# This is the idle queue. When frames are received downstream they are # This is the idle queue. When frames are received downstream they are
# put in the queue. If no frame is received the pipeline is considered # put in the queue. If no frame is received the pipeline is considered
# idle. # idle.
self._idle_queue = asyncio.Queue() self._idle_queue = WatchdogQueue(self)
# This event is used to indicate a finalize frame (e.g. EndFrame, # This event is used to indicate a finalize frame (e.g. EndFrame,
# StopFrame) has been received in the down queue. # StopFrame) has been received in the down queue.
self._pipeline_end_event = asyncio.Event() self._pipeline_end_event = asyncio.Event()
@@ -424,6 +426,9 @@ class PipelineTask(BasePipelineTask):
for frame in frames: for frame in frames:
await self.queue_frame(frame) await self.queue_frame(frame)
def reset_watchdog(self):
self._task_manager.reset_watchdog(asyncio.current_task())
async def _cancel(self): async def _cancel(self):
if not self._cancelled: if not self._cancelled:
logger.debug(f"Canceling pipeline task {self}") logger.debug(f"Canceling pipeline task {self}")
@@ -526,8 +531,6 @@ class PipelineTask(BasePipelineTask):
await self._pipeline.cleanup() await self._pipeline.cleanup()
await self._sink.cleanup() await self._sink.cleanup()
await self._task_manager.cleanup()
async def _process_push_queue(self): async def _process_push_queue(self):
"""This is the task that runs the pipeline for the first time by sending """This is the task that runs the pipeline for the first time by sending
a StartFrame and by pushing any other frames queued by the user. It runs a StartFrame and by pushing any other frames queued by the user. It runs

View File

@@ -12,6 +12,8 @@ from attr import dataclass
from pipecat.observers.base_observer import BaseObserver, FramePushed from pipecat.observers.base_observer import BaseObserver, FramePushed
from pipecat.utils.asyncio import BaseTaskManager from pipecat.utils.asyncio import BaseTaskManager
from pipecat.utils.watchdog_queue import WatchdogQueue
from pipecat.utils.watchdog_reseter import WatchdogReseter
@dataclass @dataclass
@@ -26,7 +28,7 @@ class Proxy:
observer: BaseObserver observer: BaseObserver
class TaskObserver(BaseObserver): class TaskObserver(WatchdogReseter, BaseObserver):
"""This is a pipeline frame observer that is meant to be used as a proxy to """This is a pipeline frame observer that is meant to be used as a proxy to
the user provided observers. That is, this is the observer that should be the user provided observers. That is, this is the observer that should be
passed to the frame processors. Then, every time a frame is pushed this passed to the frame processors. Then, every time a frame is pushed this
@@ -89,11 +91,14 @@ class TaskObserver(BaseObserver):
for proxy in self._proxies.values(): for proxy in self._proxies.values():
await proxy.queue.put(data) await proxy.queue.put(data)
def reset_watchdog(self):
self._task_manager.reset_watchdog(asyncio.current_task())
def _started(self) -> bool: def _started(self) -> bool:
return self._proxies is not None return self._proxies is not None
def _create_proxy(self, observer: BaseObserver) -> Proxy: def _create_proxy(self, observer: BaseObserver) -> Proxy:
queue = asyncio.Queue() queue = WatchdogQueue(self)
task = self._task_manager.create_task( task = self._task_manager.create_task(
self._proxy_task_handler(queue, observer), self._proxy_task_handler(queue, observer),
f"TaskObserver::{observer}::_proxy_task_handler", f"TaskObserver::{observer}::_proxy_task_handler",

View File

@@ -119,6 +119,7 @@ class DTMFAggregator(FrameProcessor):
await asyncio.wait_for(self._digit_event.wait(), timeout=self._idle_timeout) await asyncio.wait_for(self._digit_event.wait(), timeout=self._idle_timeout)
self._digit_event.clear() self._digit_event.clear()
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.reset_watchdog()
if self._aggregation: if self._aggregation:
await self._flush_aggregation() await self._flush_aggregation()

View File

@@ -470,6 +470,7 @@ class LLMUserContextAggregator(LLMContextResponseAggregator):
) )
self._emulating_vad = False self._emulating_vad = False
finally: finally:
self.reset_watchdog()
self._aggregation_event.clear() self._aggregation_event.clear()
async def _maybe_emulate_user_speaking(self): async def _maybe_emulate_user_speaking(self):

View File

@@ -10,6 +10,7 @@ from typing import Awaitable, Callable, Optional
from pipecat.frames.frames import CancelFrame, EndFrame, Frame, StartFrame from pipecat.frames.frames import CancelFrame, EndFrame, Frame, StartFrame
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.processors.producer_processor import ProducerProcessor, identity_transformer from pipecat.processors.producer_processor import ProducerProcessor, identity_transformer
from pipecat.utils.watchdog_queue import WatchdogQueue
class ConsumerProcessor(FrameProcessor): class ConsumerProcessor(FrameProcessor):
@@ -31,7 +32,7 @@ class ConsumerProcessor(FrameProcessor):
super().__init__(**kwargs) super().__init__(**kwargs)
self._transformer = transformer self._transformer = transformer
self._direction = direction self._direction = direction
self._queue: asyncio.Queue = producer.add_consumer() self._queue: WatchdogQueue = producer.add_consumer(self)
self._consumer_task: Optional[asyncio.Task] = None self._consumer_task: Optional[asyncio.Task] = None
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):
@@ -61,7 +62,5 @@ class ConsumerProcessor(FrameProcessor):
async def _consumer_task_handler(self): async def _consumer_task_handler(self):
while True: while True:
frame = await self._queue.get() frame = await self._queue.get()
self.start_watchdog()
new_frame = await self._transformer(frame) new_frame = await self._transformer(frame)
await self.push_frame(new_frame, self._direction) await self.push_frame(new_frame, self._direction)
self.reset_watchdog()

View File

@@ -31,6 +31,9 @@ from pipecat.observers.base_observer import BaseObserver, FramePushed
from pipecat.processors.metrics.frame_processor_metrics import FrameProcessorMetrics from pipecat.processors.metrics.frame_processor_metrics import FrameProcessorMetrics
from pipecat.utils.asyncio import BaseTaskManager from pipecat.utils.asyncio import BaseTaskManager
from pipecat.utils.base_object import BaseObject from pipecat.utils.base_object import BaseObject
from pipecat.utils.watchdog_event import WatchdogEvent
from pipecat.utils.watchdog_queue import WatchdogQueue
from pipecat.utils.watchdog_reseter import WatchdogReseter
class FrameDirection(Enum): class FrameDirection(Enum):
@@ -45,13 +48,13 @@ class FrameProcessorSetup:
observer: Optional[BaseObserver] = None observer: Optional[BaseObserver] = None
class FrameProcessor(BaseObject): class FrameProcessor(WatchdogReseter, BaseObject):
def __init__( def __init__(
self, self,
*, *,
name: Optional[str] = None, name: Optional[str] = None,
metrics: Optional[FrameProcessorMetrics] = None,
enable_watchdog_logging: Optional[bool] = None, enable_watchdog_logging: Optional[bool] = None,
metrics: Optional[FrameProcessorMetrics] = None,
watchdog_timeout_secs: Optional[float] = None, watchdog_timeout_secs: Optional[float] = None,
**kwargs, **kwargs,
): ):
@@ -101,7 +104,7 @@ class FrameProcessor(BaseObject):
# is called. To resume processing frames we need to call # is called. To resume processing frames we need to call
# `resume_processing_frames()` which will wake up the event. # `resume_processing_frames()` which will wake up the event.
self.__should_block_frames = False self.__should_block_frames = False
self.__input_event = asyncio.Event() self.__input_event = WatchdogEvent(self)
self.__input_frame_task: Optional[asyncio.Task] = None self.__input_frame_task: Optional[asyncio.Task] = None
# Every processor in Pipecat should only output frames from a single # Every processor in Pipecat should only output frames from a single
@@ -210,9 +213,6 @@ class FrameProcessor(BaseObject):
async def wait_for_task(self, task: asyncio.Task, timeout: Optional[float] = None): async def wait_for_task(self, task: asyncio.Task, timeout: Optional[float] = None):
await self.get_task_manager().wait_for_task(task, timeout) await self.get_task_manager().wait_for_task(task, timeout)
def start_watchdog(self):
self.get_task_manager().start_watchdog(asyncio.current_task())
def reset_watchdog(self): def reset_watchdog(self):
self.get_task_manager().reset_watchdog(asyncio.current_task()) self.get_task_manager().reset_watchdog(asyncio.current_task())
@@ -397,7 +397,7 @@ class FrameProcessor(BaseObject):
if not self.__input_frame_task: if not self.__input_frame_task:
self.__should_block_frames = False self.__should_block_frames = False
self.__input_event.clear() self.__input_event.clear()
self.__input_queue = asyncio.Queue() self.__input_queue = WatchdogQueue(self)
self.__input_frame_task = self.create_task(self.__input_frame_task_handler()) self.__input_frame_task = self.create_task(self.__input_frame_task_handler())
async def __cancel_input_task(self): async def __cancel_input_task(self):
@@ -416,7 +416,6 @@ class FrameProcessor(BaseObject):
(frame, direction, callback) = await self.__input_queue.get() (frame, direction, callback) = await self.__input_queue.get()
try: try:
self.start_watchdog()
# Process the frame. # Process the frame.
await self.process_frame(frame, direction) await self.process_frame(frame, direction)
# If this frame has an associated callback, call it now. # If this frame has an associated callback, call it now.
@@ -427,11 +426,10 @@ class FrameProcessor(BaseObject):
await self.push_error(ErrorFrame(str(e))) await self.push_error(ErrorFrame(str(e)))
finally: finally:
self.__input_queue.task_done() self.__input_queue.task_done()
self.reset_watchdog()
def __create_push_task(self): def __create_push_task(self):
if not self.__push_frame_task: if not self.__push_frame_task:
self.__push_queue = asyncio.Queue() self.__push_queue = WatchdogQueue(self)
self.__push_frame_task = self.create_task(self.__push_frame_task_handler()) self.__push_frame_task = self.create_task(self.__push_frame_task_handler())
async def __cancel_push_task(self): async def __cancel_push_task(self):
@@ -442,7 +440,5 @@ class FrameProcessor(BaseObject):
async def __push_frame_task_handler(self): async def __push_frame_task_handler(self):
while True: while True:
(frame, direction) = await self.__push_queue.get() (frame, direction) = await self.__push_queue.get()
self.start_watchdog()
await self.__internal_push_frame(frame, direction) await self.__internal_push_frame(frame, direction)
self.__push_queue.task_done() self.__push_queue.task_done()
self.reset_watchdog()

View File

@@ -68,6 +68,7 @@ from pipecat.transports.base_input import BaseInputTransport
from pipecat.transports.base_output import BaseOutputTransport from pipecat.transports.base_output import BaseOutputTransport
from pipecat.transports.base_transport import BaseTransport from pipecat.transports.base_transport import BaseTransport
from pipecat.utils.string import match_endofsentence from pipecat.utils.string import match_endofsentence
from pipecat.utils.watchdog_queue import WatchdogQueue
RTVI_PROTOCOL_VERSION = "0.3.0" RTVI_PROTOCOL_VERSION = "0.3.0"
@@ -650,11 +651,11 @@ class RTVIProcessor(FrameProcessor):
self._registered_services: Dict[str, RTVIService] = {} self._registered_services: Dict[str, RTVIService] = {}
# A task to process incoming action frames. # A task to process incoming action frames.
self._action_queue = asyncio.Queue() self._action_queue = WatchdogQueue(self)
self._action_task: Optional[asyncio.Task] = None self._action_task: Optional[asyncio.Task] = None
# A task to process incoming transport messages. # A task to process incoming transport messages.
self._message_queue = asyncio.Queue() self._message_queue = WatchdogQueue(self)
self._message_task: Optional[asyncio.Task] = None self._message_task: Optional[asyncio.Task] = None
self._register_event_handler("on_bot_started") self._register_event_handler("on_bot_started")
@@ -783,18 +784,14 @@ class RTVIProcessor(FrameProcessor):
async def _action_task_handler(self): async def _action_task_handler(self):
while True: while True:
frame = await self._action_queue.get() frame = await self._action_queue.get()
self.start_watchdog()
await self._handle_action(frame.message_id, frame.rtvi_action_run) await self._handle_action(frame.message_id, frame.rtvi_action_run)
self._action_queue.task_done() self._action_queue.task_done()
self.reset_watchdog()
async def _message_task_handler(self): async def _message_task_handler(self):
while True: while True:
message = await self._message_queue.get() message = await self._message_queue.get()
self.start_watchdog()
await self._handle_message(message) await self._handle_message(message)
self._message_queue.task_done() self._message_queue.task_done()
self.reset_watchdog()
async def _handle_transport_message(self, frame: TransportMessageUrgentFrame): async def _handle_transport_message(self, frame: TransportMessageUrgentFrame):
try: try:

View File

@@ -9,6 +9,8 @@ import asyncio
from loguru import logger from loguru import logger
from pipecat.utils.asyncio import TaskManager from pipecat.utils.asyncio import TaskManager
from pipecat.utils.watchdog_queue import WatchdogQueue
from pipecat.utils.watchdog_reseter import WatchdogReseter
try: try:
import sentry_sdk import sentry_sdk
@@ -20,7 +22,7 @@ except ModuleNotFoundError as e:
from pipecat.processors.metrics.frame_processor_metrics import FrameProcessorMetrics from pipecat.processors.metrics.frame_processor_metrics import FrameProcessorMetrics
class SentryMetrics(FrameProcessorMetrics): class SentryMetrics(WatchdogReseter, FrameProcessorMetrics):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._ttfb_metrics_tx = None self._ttfb_metrics_tx = None
@@ -28,13 +30,12 @@ class SentryMetrics(FrameProcessorMetrics):
self._sentry_available = sentry_sdk.is_initialized() self._sentry_available = sentry_sdk.is_initialized()
if not self._sentry_available: if not self._sentry_available:
logger.warning("Sentry SDK not initialized. Sentry features will be disabled.") logger.warning("Sentry SDK not initialized. Sentry features will be disabled.")
self._sentry_queue = asyncio.Queue()
self._sentry_task = None self._sentry_task = None
async def setup(self, task_manager: TaskManager): async def setup(self, task_manager: TaskManager):
await super().setup(task_manager) await super().setup(task_manager)
if self._sentry_available: if self._sentry_available:
self._sentry_queue = asyncio.Queue() self._sentry_queue = WatchdogQueue(self)
self._sentry_task = self.task_manager.create_task( self._sentry_task = self.task_manager.create_task(
self._sentry_task_handler(), name=f"{self}::_sentry_task_handler" self._sentry_task_handler(), name=f"{self}::_sentry_task_handler"
) )
@@ -48,6 +49,10 @@ class SentryMetrics(FrameProcessorMetrics):
logger.trace(f"{self} Flushing Sentry metrics") logger.trace(f"{self} Flushing Sentry metrics")
sentry_sdk.flush(timeout=5.0) sentry_sdk.flush(timeout=5.0)
def reset_watchdog(self):
if self._task_manager:
self._task_manager.reset_watchdog(asyncio.current_task())
async def start_ttfb_metrics(self, report_only_initial_ttfb): async def start_ttfb_metrics(self, report_only_initial_ttfb):
await super().start_ttfb_metrics(report_only_initial_ttfb) await super().start_ttfb_metrics(report_only_initial_ttfb)
@@ -93,3 +98,4 @@ class SentryMetrics(FrameProcessorMetrics):
if tx: if tx:
await self.task_manager.get_event_loop().run_in_executor(None, tx.finish) await self.task_manager.get_event_loop().run_in_executor(None, tx.finish)
running = tx is not None running = tx is not None
self._sentry_queue.task_done()

View File

@@ -9,6 +9,7 @@ from typing import Awaitable, Callable, List
from pipecat.frames.frames import Frame from pipecat.frames.frames import Frame
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.utils.watchdog_queue import WatchdogQueue
async def identity_transformer(frame: Frame): async def identity_transformer(frame: Frame):
@@ -36,14 +37,14 @@ class ProducerProcessor(FrameProcessor):
self._passthrough = passthrough self._passthrough = passthrough
self._consumers: List[asyncio.Queue] = [] self._consumers: List[asyncio.Queue] = []
def add_consumer(self): def add_consumer(self, consumer: FrameProcessor):
""" """
Adds a new consumer and returns its associated queue. Adds a new consumer and returns its associated queue.
Returns: Returns:
asyncio.Queue: The queue for the newly added consumer. asyncio.Queue: The queue for the newly added consumer.
""" """
queue = asyncio.Queue() queue = WatchdogQueue(consumer)
self._consumers.append(queue) self._consumers.append(queue)
return queue return queue

View File

@@ -47,6 +47,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.llm_service import FunctionCallFromLLM, LLMService from pipecat.services.llm_service import FunctionCallFromLLM, LLMService
from pipecat.utils.tracing.service_decorators import traced_llm from pipecat.utils.tracing.service_decorators import traced_llm
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
try: try:
from anthropic import NOT_GIVEN, AsyncAnthropic, NotGiven from anthropic import NOT_GIVEN, AsyncAnthropic, NotGiven
@@ -203,7 +204,7 @@ class AnthropicLLMService(LLMService):
json_accumulator = "" json_accumulator = ""
function_calls = [] function_calls = []
async for event in response: async for event in WatchdogAsyncIterator(response, reseter=self):
# Aggregate streaming content, create frames, trigger events # Aggregate streaming content, create frames, trigger events
if event.type == "content_block_delta": if event.type == "content_block_delta":

View File

@@ -189,17 +189,16 @@ class AssemblyAISTTService(STTService):
try: try:
while self._connected: while self._connected:
try: try:
message = await self._websocket.recv() message = await asyncio.wait_for(self._websocket.recv(), timeout=1.0)
self.start_watchdog()
data = json.loads(message) data = json.loads(message)
await self._handle_message(data) await self._handle_message(data)
except asyncio.TimeoutError:
self.reset_watchdog()
except websockets.exceptions.ConnectionClosedOK: except websockets.exceptions.ConnectionClosedOK:
break break
except Exception as e: except Exception as e:
logger.error(f"Error processing WebSocket message: {e}") logger.error(f"Error processing WebSocket message: {e}")
break break
finally:
self.reset_watchdog()
except Exception as e: except Exception as e:
logger.error(f"Fatal error in receive handler: {e}") logger.error(f"Fatal error in receive handler: {e}")

View File

@@ -711,6 +711,8 @@ class AWSBedrockLLMService(LLMService):
function_calls = [] function_calls = []
for event in response["stream"]: for event in response["stream"]:
self.reset_watchdog()
# Handle text content # Handle text content
if "contentBlockDelta" in event: if "contentBlockDelta" in event:
delta = event["contentBlockDelta"]["delta"] delta = event["contentBlockDelta"]["delta"]
@@ -762,6 +764,7 @@ class AWSBedrockLLMService(LLMService):
completion_tokens += usage.get("outputTokens", 0) completion_tokens += usage.get("outputTokens", 0)
cache_read_input_tokens += usage.get("cacheReadInputTokens", 0) cache_read_input_tokens += usage.get("cacheReadInputTokens", 0)
cache_creation_input_tokens += usage.get("cacheWriteInputTokens", 0) cache_creation_input_tokens += usage.get("cacheWriteInputTokens", 0)
await self.run_function_calls(function_calls) await self.run_function_calls(function_calls)
except asyncio.CancelledError: except asyncio.CancelledError:
# If we're interrupted, we won't get a complete usage report. So set our flag to use the # If we're interrupted, we won't get a complete usage report. So set our flag to use the

View File

@@ -284,9 +284,7 @@ class AWSTranscribeSTTService(STTService):
break break
try: try:
response = await self._ws_client.recv() response = await asyncio.wait_for(self._ws_client.recv(), timeout=1.0)
self.start_watchdog()
headers, payload = decode_event(response) headers, payload = decode_event(response)
@@ -337,6 +335,8 @@ class AWSTranscribeSTTService(STTService):
else: else:
logger.debug(f"{self} Other message type received: {headers}") logger.debug(f"{self} Other message type received: {headers}")
logger.debug(f"{self} Payload: {payload}") logger.debug(f"{self} Payload: {payload}")
except asyncio.TimeoutError:
self.reset_watchdog()
except websockets.exceptions.ConnectionClosed as e: except websockets.exceptions.ConnectionClosed as e:
logger.error( logger.error(
f"{self} WebSocket connection closed in receive loop with code {e.code}: {e.reason}" f"{self} WebSocket connection closed in receive loop with code {e.code}: {e.reason}"
@@ -345,5 +345,3 @@ class AWSTranscribeSTTService(STTService):
except Exception as e: except Exception as e:
logger.error(f"{self} Unexpected error in receive loop: {e}") logger.error(f"{self} Unexpected error in receive loop: {e}")
break break
finally:
self.reset_watchdog()

View File

@@ -697,9 +697,9 @@ class AWSNovaSonicLLMService(LLMService):
try: try:
while self._stream and not self._disconnecting: while self._stream and not self._disconnecting:
output = await self._stream.await_output() output = await self._stream.await_output()
result = await output[1].receive() result = await asyncio.wait_for(output[1].receive(), timeout=1.0)
self.start_watchdog() self.reset_watchdog()
if result.value and result.value.bytes_: if result.value and result.value.bytes_:
response_data = result.value.bytes_.decode("utf-8") response_data = result.value.bytes_.decode("utf-8")
@@ -728,13 +728,12 @@ class AWSNovaSonicLLMService(LLMService):
elif "completionEnd" in event_json: elif "completionEnd" in event_json:
# Handle the LLM completion ending # Handle the LLM completion ending
await self._handle_completion_end_event(event_json) await self._handle_completion_end_event(event_json)
except asyncio.TimeoutError:
self.reset_watchdog()
except Exception as e: except Exception as e:
logger.error(f"{self} error processing responses: {e}") logger.error(f"{self} error processing responses: {e}")
if self._wants_connection: if self._wants_connection:
await self.reset_conversation() await self.reset_conversation()
finally:
self.reset_watchdog()
async def _handle_completion_start_event(self, event_json): async def _handle_completion_start_event(self, event_json):
pass pass

View File

@@ -30,6 +30,7 @@ from pipecat.transcriptions.language import Language
from pipecat.utils.text.base_text_aggregator import BaseTextAggregator from pipecat.utils.text.base_text_aggregator import BaseTextAggregator
from pipecat.utils.text.skip_tags_aggregator import SkipTagsAggregator from pipecat.utils.text.skip_tags_aggregator import SkipTagsAggregator
from pipecat.utils.tracing.service_decorators import traced_tts from pipecat.utils.tracing.service_decorators import traced_tts
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
# See .env.example for Cartesia configuration needed # See .env.example for Cartesia configuration needed
try: try:
@@ -255,7 +256,7 @@ class CartesiaTTSService(AudioContextWordTTSService):
self._context_id = None self._context_id = None
async def _receive_messages(self): async def _receive_messages(self):
async for message in self._get_websocket(): async for message in WatchdogAsyncIterator(self._get_websocket(), reseter=self):
msg = json.loads(message) msg = json.loads(message)
if not msg or not self.audio_context_available(msg["context_id"]): if not msg or not self.audio_context_available(msg["context_id"]):
continue continue

View File

@@ -33,6 +33,7 @@ from pipecat.services.tts_service import (
) )
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.tracing.service_decorators import traced_tts from pipecat.utils.tracing.service_decorators import traced_tts
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
# See .env.example for ElevenLabs configuration needed # See .env.example for ElevenLabs configuration needed
try: try:
@@ -394,7 +395,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
self._started = False self._started = False
async def _receive_messages(self): async def _receive_messages(self):
async for message in self._get_websocket(): async for message in WatchdogAsyncIterator(self._get_websocket(), reseter=self):
msg = json.loads(message) msg = json.loads(message)
received_ctx_id = msg.get("contextId") received_ctx_id = msg.get("contextId")
@@ -426,7 +427,8 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
async def _keepalive_task_handler(self): async def _keepalive_task_handler(self):
while True: while True:
await asyncio.sleep(10) self.reset_watchdog()
await asyncio.sleep(4)
try: try:
if self._websocket and self._websocket.open: if self._websocket and self._websocket.open:
if self._context_id: if self._context_id:

View File

@@ -60,7 +60,8 @@ from pipecat.services.openai.llm import (
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.string import match_endofsentence from pipecat.utils.string import match_endofsentence
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_gemini_live, traced_stt, traced_tts from pipecat.utils.tracing.service_decorators import traced_gemini_live, traced_stt
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
from . import events from . import events
@@ -686,9 +687,7 @@ class GeminiMultimodalLiveLLMService(LLMService):
# #
async def _receive_task_handler(self): async def _receive_task_handler(self):
async for message in self._websocket: async for message in WatchdogAsyncIterator(self._websocket, reseter=self):
self.start_watchdog()
evt = events.parse_server_event(message) evt = events.parse_server_event(message)
# logger.debug(f"Received event: {message[:500]}") # logger.debug(f"Received event: {message[:500]}")
# logger.debug(f"Received event: {evt}") # logger.debug(f"Received event: {evt}")
@@ -711,8 +710,6 @@ class GeminiMultimodalLiveLLMService(LLMService):
# errors are fatal, so exit the receive loop # errors are fatal, so exit the receive loop
return return
self.reset_watchdog()
# #
# #
# #

View File

@@ -27,6 +27,7 @@ from pipecat.services.stt_service import STTService
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_stt from pipecat.utils.tracing.service_decorators import traced_stt
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
try: try:
import websockets import websockets
@@ -501,9 +502,7 @@ class GladiaSTTService(STTService):
async def _receive_task_handler(self): async def _receive_task_handler(self):
try: try:
async for message in self._websocket: async for message in WatchdogAsyncIterator(self._websocket, reseter=self):
self.start_watchdog()
content = json.loads(message) content = json.loads(message)
# Handle audio chunk acknowledgments # Handle audio chunk acknowledgments
@@ -568,8 +567,6 @@ class GladiaSTTService(STTService):
pass pass
except Exception as e: except Exception as e:
logger.error(f"Error in Gladia WebSocket handler: {e}") logger.error(f"Error in Gladia WebSocket handler: {e}")
finally:
self.reset_watchdog()
async def _maybe_reconnect(self) -> bool: async def _maybe_reconnect(self) -> bool:
"""Handle exponential backoff reconnection logic.""" """Handle exponential backoff reconnection logic."""

View File

@@ -48,6 +48,7 @@ from pipecat.services.openai.llm import (
OpenAIUserContextAggregator, OpenAIUserContextAggregator,
) )
from pipecat.utils.tracing.service_decorators import traced_llm from pipecat.utils.tracing.service_decorators import traced_llm
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
# Suppress gRPC fork warnings # Suppress gRPC fork warnings
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
@@ -557,7 +558,7 @@ class GoogleLLMService(LLMService):
) )
function_calls = [] function_calls = []
async for chunk in response: async for chunk in WatchdogAsyncIterator(response, reseter=self):
# Stop TTFB metrics after the first chunk # Stop TTFB metrics after the first chunk
await self.stop_ttfb_metrics() await self.stop_ttfb_metrics()
if chunk.usage_metadata: if chunk.usage_metadata:

View File

@@ -11,6 +11,7 @@ from openai import AsyncStream
from openai.types.chat import ChatCompletionChunk from openai.types.chat import ChatCompletionChunk
from pipecat.services.llm_service import FunctionCallFromLLM from pipecat.services.llm_service import FunctionCallFromLLM
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
# Suppress gRPC fork warnings # Suppress gRPC fork warnings
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
@@ -53,7 +54,7 @@ class GoogleLLMOpenAIBetaService(OpenAILLMService):
context context
) )
async for chunk in chunk_stream: async for chunk in WatchdogAsyncIterator(chunk_stream, reseter=self):
if chunk.usage: if chunk.usage:
tokens = LLMTokenUsage( tokens = LLMTokenUsage(
prompt_tokens=chunk.usage.prompt_tokens, prompt_tokens=chunk.usage.prompt_tokens,

View File

@@ -10,6 +10,7 @@ import os
import time import time
from pipecat.utils.tracing.service_decorators import traced_stt from pipecat.utils.tracing.service_decorators import traced_stt
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
# Suppress gRPC fork warnings # Suppress gRPC fork warnings
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false" os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
@@ -747,8 +748,6 @@ class GoogleSTTService(STTService):
try: try:
while True: while True:
try: try:
self.start_watchdog()
if self._request_queue.empty(): if self._request_queue.empty():
# wait for 10ms in case we don't have audio # wait for 10ms in case we don't have audio
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
@@ -763,8 +762,6 @@ class GoogleSTTService(STTService):
# Process responses # Process responses
await self._process_responses(streaming_recognize) await self._process_responses(streaming_recognize)
self.reset_watchdog()
# If we're here, check if we need to reconnect # If we're here, check if we need to reconnect
if (int(time.time() * 1000) - self._stream_start_time) > self.STREAMING_LIMIT: if (int(time.time() * 1000) - self._stream_start_time) > self.STREAMING_LIMIT:
logger.debug("Reconnecting stream after timeout") logger.debug("Reconnecting stream after timeout")
@@ -779,8 +776,6 @@ class GoogleSTTService(STTService):
await asyncio.sleep(1) # Brief delay before reconnecting await asyncio.sleep(1) # Brief delay before reconnecting
self._stream_start_time = int(time.time() * 1000) self._stream_start_time = int(time.time() * 1000)
finally:
self.reset_watchdog()
except Exception as e: except Exception as e:
logger.error(f"Error in streaming task: {e}") logger.error(f"Error in streaming task: {e}")
@@ -804,17 +799,13 @@ class GoogleSTTService(STTService):
async def _process_responses(self, streaming_recognize): async def _process_responses(self, streaming_recognize):
"""Process streaming recognition responses.""" """Process streaming recognition responses."""
try: try:
async for response in streaming_recognize: async for response in WatchdogAsyncIterator(streaming_recognize, reseter=self):
self.start_watchdog()
# Check streaming limit # Check streaming limit
if (int(time.time() * 1000) - self._stream_start_time) > self.STREAMING_LIMIT: if (int(time.time() * 1000) - self._stream_start_time) > self.STREAMING_LIMIT:
logger.debug("Stream timeout reached in response processing") logger.debug("Stream timeout reached in response processing")
self.reset_watchdog()
break break
if not response.results: if not response.results:
self.reset_watchdog()
continue continue
for result in response.results: for result in response.results:
@@ -856,11 +847,8 @@ class GoogleSTTService(STTService):
result=result, result=result,
) )
) )
self.reset_watchdog()
except Exception as e: except Exception as e:
logger.error(f"Error processing Google STT responses: {e}") logger.error(f"Error processing Google STT responses: {e}")
self.reset_watchdog()
# Re-raise the exception to let it propagate (e.g. in the case of a # Re-raise the exception to let it propagate (e.g. in the case of a
# timeout, propagate to _stream_audio to reconnect) # timeout, propagate to _stream_audio to reconnect)
raise raise

View File

@@ -36,6 +36,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.llm_service import FunctionCallFromLLM, LLMService from pipecat.services.llm_service import FunctionCallFromLLM, LLMService
from pipecat.utils.tracing.service_decorators import traced_llm from pipecat.utils.tracing.service_decorators import traced_llm
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
class BaseOpenAILLMService(LLMService): class BaseOpenAILLMService(LLMService):
@@ -192,7 +193,7 @@ class BaseOpenAILLMService(LLMService):
context context
) )
async for chunk in chunk_stream: async for chunk in WatchdogAsyncIterator(chunk_stream, reseter=self):
if chunk.usage: if chunk.usage:
tokens = LLMTokenUsage( tokens = LLMTokenUsage(
prompt_tokens=chunk.usage.prompt_tokens, prompt_tokens=chunk.usage.prompt_tokens,

View File

@@ -52,7 +52,8 @@ from pipecat.services.llm_service import FunctionCallFromLLM, LLMService
from pipecat.services.openai.llm import OpenAIContextAggregatorPair from pipecat.services.openai.llm import OpenAIContextAggregatorPair
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_openai_realtime, traced_stt, traced_tts from pipecat.utils.tracing.service_decorators import traced_openai_realtime, traced_stt
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
from . import events from . import events
from .context import ( from .context import (
@@ -369,8 +370,7 @@ class OpenAIRealtimeBetaLLMService(LLMService):
# #
async def _receive_task_handler(self): async def _receive_task_handler(self):
async for message in self._websocket: async for message in WatchdogAsyncIterator(self._websocket, reseter=self):
self.start_watchdog()
evt = events.parse_server_event(message) evt = events.parse_server_event(message)
if evt.type == "session.created": if evt.type == "session.created":
await self._handle_evt_session_created(evt) await self._handle_evt_session_created(evt)
@@ -401,7 +401,6 @@ class OpenAIRealtimeBetaLLMService(LLMService):
await self._handle_evt_error(evt) await self._handle_evt_error(evt)
# errors are fatal, so exit the receive loop # errors are fatal, so exit the receive loop
return return
self.reset_watchdog()
@traced_openai_realtime(operation="llm_setup") @traced_openai_realtime(operation="llm_setup")
async def _handle_evt_session_created(self, evt): async def _handle_evt_session_created(self, evt):

View File

@@ -23,6 +23,7 @@ from pipecat.services.stt_service import SegmentedSTTService, STTService
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_stt from pipecat.utils.tracing.service_decorators import traced_stt
from pipecat.utils.watchdog_queue import WatchdogQueue
try: try:
import riva.client import riva.client
@@ -198,7 +199,7 @@ class RivaSTTService(STTService):
self._thread_task = self.create_task(self._thread_task_handler()) self._thread_task = self.create_task(self._thread_task_handler())
if not self._response_task: if not self._response_task:
self._response_queue = asyncio.Queue() self._response_queue = WatchdogQueue(self)
self._response_task = self.create_task(self._response_task_handler()) self._response_task = self.create_task(self._response_task_handler())
async def stop(self, frame: EndFrame): async def stop(self, frame: EndFrame):
@@ -224,13 +225,12 @@ class RivaSTTService(STTService):
streaming_config=self._config, streaming_config=self._config,
) )
for response in responses: for response in responses:
self.start_watchdog() self.reset_watchdog()
if not response.results: if not response.results:
continue continue
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self._response_queue.put(response), self.get_event_loop() self._response_queue.put(response), self.get_event_loop()
) )
self.reset_watchdog()
async def _thread_task_handler(self): async def _thread_task_handler(self):
try: try:
@@ -285,9 +285,8 @@ class RivaSTTService(STTService):
async def _response_task_handler(self): async def _response_task_handler(self):
while True: while True:
response = await self._response_queue.get() response = await self._response_queue.get()
self.start_watchdog()
await self._handle_response(response) await self._handle_response(response)
self.reset_watchdog() self._response_queue.task_done()
async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]: async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
await self.start_ttfb_metrics() await self.start_ttfb_metrics()

View File

@@ -19,6 +19,7 @@ from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.llm_service import FunctionCallFromLLM from pipecat.services.llm_service import FunctionCallFromLLM
from pipecat.services.openai.llm import OpenAILLMService from pipecat.services.openai.llm import OpenAILLMService
from pipecat.utils.tracing.service_decorators import traced_llm from pipecat.utils.tracing.service_decorators import traced_llm
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
class SambaNovaLLMService(OpenAILLMService): # type: ignore class SambaNovaLLMService(OpenAILLMService): # type: ignore
@@ -94,7 +95,7 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore
context context
) )
async for chunk in chunk_stream: async for chunk in WatchdogAsyncIterator(chunk_stream, reseter=self):
if chunk.usage: if chunk.usage:
tokens = LLMTokenUsage( tokens = LLMTokenUsage(
prompt_tokens=chunk.usage.prompt_tokens, prompt_tokens=chunk.usage.prompt_tokens,

View File

@@ -18,6 +18,7 @@ from pipecat.frames.frames import (
TTSAudioRawFrame, TTSAudioRawFrame,
) )
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, StartFrame from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, StartFrame
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
try: try:
from av.audio.frame import AudioFrame from av.audio.frame import AudioFrame
@@ -61,8 +62,8 @@ class SimliVideoService(FrameProcessor):
async def _consume_and_process_audio(self): async def _consume_and_process_audio(self):
await self._pipecat_resampler_event.wait() await self._pipecat_resampler_event.wait()
async for audio_frame in self._simli_client.getAudioStreamIterator(): audio_iterator = self._simli_client.getAudioStreamIterator()
self.start_watchdog() async for audio_frame in WatchdogAsyncIterator(audio_iterator, reseter=self):
resampled_frames = self._pipecat_resampler.resample(audio_frame) resampled_frames = self._pipecat_resampler.resample(audio_frame)
for resampled_frame in resampled_frames: for resampled_frame in resampled_frames:
audio_array = resampled_frame.to_ndarray() audio_array = resampled_frame.to_ndarray()
@@ -75,12 +76,11 @@ class SimliVideoService(FrameProcessor):
num_channels=1, num_channels=1,
), ),
) )
self.reset_watchdog()
async def _consume_and_process_video(self): async def _consume_and_process_video(self):
await self._pipecat_resampler_event.wait() await self._pipecat_resampler_event.wait()
async for video_frame in self._simli_client.getVideoStreamIterator(targetFormat="rgb24"): video_iterator = self._simli_client.getVideoStreamIterator(targetFormat="rgb24")
self.start_watchdog() async for video_frame in WatchdogAsyncIterator(video_iterator, reseter=self):
# Process the video frame # Process the video frame
convertedFrame: OutputImageRawFrame = OutputImageRawFrame( convertedFrame: OutputImageRawFrame = OutputImageRawFrame(
image=video_frame.to_rgb().to_image().tobytes(), image=video_frame.to_rgb().to_image().tobytes(),
@@ -89,7 +89,6 @@ class SimliVideoService(FrameProcessor):
) )
convertedFrame.pts = video_frame.pts convertedFrame.pts = video_frame.pts
await self.push_frame(convertedFrame) await self.push_frame(convertedFrame)
self.reset_watchdog()
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):
await super().process_frame(frame, direction) await super().process_frame(frame, direction)

View File

@@ -27,6 +27,7 @@ from pipecat.frames.frames import (
from pipecat.processors.frame_processor import FrameDirection, FrameProcessorSetup from pipecat.processors.frame_processor import FrameDirection, FrameProcessorSetup
from pipecat.services.ai_service import AIService from pipecat.services.ai_service import AIService
from pipecat.transports.services.tavus import TavusCallbacks, TavusParams, TavusTransportClient from pipecat.transports.services.tavus import TavusCallbacks, TavusParams, TavusTransportClient
from pipecat.utils.watchdog_queue import WatchdogQueue
class TavusVideoService(AIService): class TavusVideoService(AIService):
@@ -71,7 +72,7 @@ class TavusVideoService(AIService):
self._resampler = create_default_resampler() self._resampler = create_default_resampler()
self._audio_buffer = bytearray() self._audio_buffer = bytearray()
self._queue = asyncio.Queue() self._queue = WatchdogQueue(self)
self._send_task: Optional[asyncio.Task] = None self._send_task: Optional[asyncio.Task] = None
# This is the custom track destination expected by Tavus # This is the custom track destination expected by Tavus
self._transport_destination: Optional[str] = "stream" self._transport_destination: Optional[str] = "stream"
@@ -188,7 +189,7 @@ class TavusVideoService(AIService):
async def _create_send_task(self): async def _create_send_task(self):
if not self._send_task: if not self._send_task:
self._queue = asyncio.Queue() self._queue = WatchdogQueue(self)
self._send_task = self.create_task(self._send_task_handler()) self._send_task = self.create_task(self._send_task_handler())
async def _cancel_send_task(self): async def _cancel_send_task(self):
@@ -217,7 +218,6 @@ class TavusVideoService(AIService):
async def _send_task_handler(self): async def _send_task_handler(self):
while True: while True:
frame = await self._queue.get() frame = await self._queue.get()
self.start_watchdog()
if isinstance(frame, OutputAudioRawFrame) and self._client: if isinstance(frame, OutputAudioRawFrame) and self._client:
await self._client.write_audio_frame(frame) await self._client.write_audio_frame(frame)
self.reset_watchdog() self._queue.task_done()

View File

@@ -39,6 +39,7 @@ from pipecat.utils.text.base_text_aggregator import BaseTextAggregator
from pipecat.utils.text.base_text_filter import BaseTextFilter from pipecat.utils.text.base_text_filter import BaseTextFilter
from pipecat.utils.text.simple_text_aggregator import SimpleTextAggregator from pipecat.utils.text.simple_text_aggregator import SimpleTextAggregator
from pipecat.utils.time import seconds_to_nanoseconds from pipecat.utils.time import seconds_to_nanoseconds
from pipecat.utils.watchdog_queue import WatchdogQueue
class TTSService(AIService): class TTSService(AIService):
@@ -315,6 +316,8 @@ class TTSService(AIService):
if has_started: if has_started:
await self.push_frame(TTSStoppedFrame()) await self.push_frame(TTSStoppedFrame())
has_started = False has_started = False
finally:
self.reset_watchdog()
class WordTTSService(TTSService): class WordTTSService(TTSService):
@@ -327,7 +330,7 @@ class WordTTSService(TTSService):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._initial_word_timestamp = -1 self._initial_word_timestamp = -1
self._words_queue = asyncio.Queue() self._words_queue = WatchdogQueue(self)
self._words_task = None self._words_task = None
self._llm_response_started: bool = False self._llm_response_started: bool = False
@@ -578,7 +581,7 @@ class AudioContextWordTTSService(WebsocketWordTTSService):
def _create_audio_context_task(self): def _create_audio_context_task(self):
if not self._audio_context_task: if not self._audio_context_task:
self._contexts_queue = asyncio.Queue() self._contexts_queue = WatchdogQueue(self)
self._contexts: Dict[str, asyncio.Queue] = {} self._contexts: Dict[str, asyncio.Queue] = {}
self._audio_context_task = self.create_task(self._audio_context_task_handler()) self._audio_context_task = self.create_task(self._audio_context_task_handler())
@@ -620,10 +623,12 @@ class AudioContextWordTTSService(WebsocketWordTTSService):
while running: while running:
try: try:
frame = await asyncio.wait_for(queue.get(), timeout=AUDIO_CONTEXT_TIMEOUT) frame = await asyncio.wait_for(queue.get(), timeout=AUDIO_CONTEXT_TIMEOUT)
self.reset_watchdog()
if frame: if frame:
await self.push_frame(frame) await self.push_frame(frame)
running = frame is not None running = frame is not None
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.reset_watchdog()
# We didn't get audio, so let's consider this context finished. # We didn't get audio, so let's consider this context finished.
logger.trace(f"{self} time out on audio context {context_id}") logger.trace(f"{self} time out on audio context {context_id}")
break break

View File

@@ -368,8 +368,6 @@ class BaseInputTransport(FrameProcessor):
self._audio_in_queue.get(), timeout=AUDIO_INPUT_TIMEOUT_SECS self._audio_in_queue.get(), timeout=AUDIO_INPUT_TIMEOUT_SECS
) )
self.start_watchdog()
# If an audio filter is available, run it before VAD. # If an audio filter is available, run it before VAD.
if self._params.audio_in_filter: if self._params.audio_in_filter:
frame.audio = await self._params.audio_in_filter.filter(frame.audio) frame.audio = await self._params.audio_in_filter.filter(frame.audio)

View File

@@ -40,6 +40,7 @@ from pipecat.frames.frames import (
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.transports.base_transport import TransportParams from pipecat.transports.base_transport import TransportParams
from pipecat.utils.time import nanoseconds_to_seconds from pipecat.utils.time import nanoseconds_to_seconds
from pipecat.utils.watchdog_priority_queue import WatchdogPriorityQueue
BOT_VAD_STOP_SECS = 0.35 BOT_VAD_STOP_SECS = 0.35
@@ -441,8 +442,10 @@ class BaseOutputTransport(FrameProcessor):
frame = await asyncio.wait_for( frame = await asyncio.wait_for(
self._audio_queue.get(), timeout=vad_stop_secs self._audio_queue.get(), timeout=vad_stop_secs
) )
self._transport.reset_watchdog()
yield frame yield frame
except asyncio.TimeoutError: except asyncio.TimeoutError:
self._transport.reset_watchdog()
# Notify the bot stopped speaking upstream if necessary. # Notify the bot stopped speaking upstream if necessary.
await self._bot_stopped_speaking() await self._bot_stopped_speaking()
@@ -452,11 +455,13 @@ class BaseOutputTransport(FrameProcessor):
while True: while True:
try: try:
frame = self._audio_queue.get_nowait() frame = self._audio_queue.get_nowait()
self._transport.reset_watchdog()
if isinstance(frame, OutputAudioRawFrame): if isinstance(frame, OutputAudioRawFrame):
frame.audio = await self._mixer.mix(frame.audio) frame.audio = await self._mixer.mix(frame.audio)
last_frame_time = time.time() last_frame_time = time.time()
yield frame yield frame
except asyncio.QueueEmpty: except asyncio.QueueEmpty:
self._transport.reset_watchdog()
# Notify the bot stopped speaking upstream if necessary. # Notify the bot stopped speaking upstream if necessary.
diff_time = time.time() - last_frame_time diff_time = time.time() - last_frame_time
if diff_time > vad_stop_secs: if diff_time > vad_stop_secs:
@@ -597,7 +602,7 @@ class BaseOutputTransport(FrameProcessor):
def _create_clock_task(self): def _create_clock_task(self):
if not self._clock_task: if not self._clock_task:
self._clock_queue = asyncio.PriorityQueue() self._clock_queue = WatchdogPriorityQueue(self._transport)
self._clock_task = self._transport.create_task(self._clock_task_handler()) self._clock_task = self._transport.create_task(self._clock_task_handler())
async def _cancel_clock_task(self): async def _cancel_clock_task(self):

View File

@@ -26,11 +26,12 @@ from pipecat.frames.frames import (
TransportMessageFrame, TransportMessageFrame,
TransportMessageUrgentFrame, TransportMessageUrgentFrame,
) )
from pipecat.processors.frame_processor import FrameDirection, FrameProcessorSetup from pipecat.processors.frame_processor import FrameDirection
from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType from pipecat.serializers.base_serializer import FrameSerializer, FrameSerializerType
from pipecat.transports.base_input import BaseInputTransport from pipecat.transports.base_input import BaseInputTransport
from pipecat.transports.base_output import BaseOutputTransport from pipecat.transports.base_output import BaseOutputTransport
from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
try: try:
from fastapi import WebSocket from fastapi import WebSocket
@@ -178,12 +179,10 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
async def _receive_messages(self): async def _receive_messages(self):
try: try:
async for message in self._client.receive(): async for message in WatchdogAsyncIterator(self._client.receive(), reseter=self):
if not self._params.serializer: if not self._params.serializer:
continue continue
self.start_watchdog()
frame = await self._params.serializer.deserialize(message) frame = await self._params.serializer.deserialize(message)
if not frame: if not frame:
@@ -193,13 +192,9 @@ class FastAPIWebsocketInputTransport(BaseInputTransport):
await self.push_audio_frame(frame) await self.push_audio_frame(frame)
else: else:
await self.push_frame(frame) await self.push_frame(frame)
self.reset_watchdog()
except Exception as e: except Exception as e:
logger.error(f"{self} exception receiving data: {e.__class__.__name__} ({e})") logger.error(f"{self} exception receiving data: {e.__class__.__name__} ({e})")
self.reset_watchdog()
await self._client.trigger_client_disconnected() await self._client.trigger_client_disconnected()
async def _monitor_websocket(self): async def _monitor_websocket(self):

View File

@@ -33,6 +33,7 @@ from pipecat.transports.base_input import BaseInputTransport
from pipecat.transports.base_output import BaseOutputTransport from pipecat.transports.base_output import BaseOutputTransport
from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection from pipecat.transports.network.webrtc_connection import SmallWebRTCConnection
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
try: try:
import cv2 import cv2
@@ -422,19 +423,18 @@ class SmallWebRTCInputTransport(BaseInputTransport):
async def _receive_audio(self): async def _receive_audio(self):
try: try:
async for audio_frame in self._client.read_audio_frame(): audio_iterator = self._client.read_audio_frame()
self.start_watchdog() async for audio_frame in WatchdogAsyncIterator(audio_iterator, reseter=self):
if audio_frame: if audio_frame:
await self.push_audio_frame(audio_frame) await self.push_audio_frame(audio_frame)
self.reset_watchdog()
except Exception as e: except Exception as e:
logger.error(f"{self} exception receiving data: {e.__class__.__name__} ({e})") logger.error(f"{self} exception receiving data: {e.__class__.__name__} ({e})")
async def _receive_video(self): async def _receive_video(self):
try: try:
async for video_frame in self._client.read_video_frame(): video_iterator = self._client.read_video_frame()
self.start_watchdog() async for video_frame in WatchdogAsyncIterator(video_iterator, reseter=self):
if video_frame: if video_frame:
await self.push_video_frame(video_frame) await self.push_video_frame(video_frame)
@@ -453,7 +453,6 @@ class SmallWebRTCInputTransport(BaseInputTransport):
await self.push_video_frame(image_frame) await self.push_video_frame(image_frame)
# Remove from pending requests # Remove from pending requests
del self._image_requests[req_id] del self._image_requests[req_id]
self.reset_watchdog()
except Exception as e: except Exception as e:
logger.error(f"{self} exception receiving data: {e.__class__.__name__} ({e})") logger.error(f"{self} exception receiving data: {e.__class__.__name__} ({e})")

View File

@@ -40,6 +40,8 @@ from pipecat.transports.base_input import BaseInputTransport
from pipecat.transports.base_output import BaseOutputTransport from pipecat.transports.base_output import BaseOutputTransport
from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.utils.asyncio import BaseTaskManager from pipecat.utils.asyncio import BaseTaskManager
from pipecat.utils.watchdog_queue import WatchdogQueue
from pipecat.utils.watchdog_reseter import WatchdogReseter
try: try:
from daily import ( from daily import (
@@ -250,7 +252,7 @@ class DailyAudioTrack:
track: CustomAudioTrack track: CustomAudioTrack
class DailyTransportClient(EventHandler): class DailyTransportClient(WatchdogReseter, EventHandler):
"""Core client for interacting with Daily's API. """Core client for interacting with Daily's API.
Manages the connection to Daily rooms and handles all low-level API interactions. Manages the connection to Daily rooms and handles all low-level API interactions.
@@ -320,9 +322,9 @@ class DailyTransportClient(EventHandler):
# waits for it to finish using completions (and a future) we will # waits for it to finish using completions (and a future) we will
# deadlock because completions use event handlers (which are holding the # deadlock because completions use event handlers (which are holding the
# GIL). # GIL).
self._event_queue = asyncio.Queue() self._event_queue = WatchdogQueue(self)
self._audio_queue = asyncio.Queue() self._audio_queue = WatchdogQueue(self)
self._video_queue = asyncio.Queue() self._video_queue = WatchdogQueue(self)
self._event_task = None self._event_task = None
self._audio_task = None self._audio_task = None
self._video_task = None self._video_task = None
@@ -395,6 +397,10 @@ class DailyTransportClient(EventHandler):
if not frame.transport_destination and self._camera: if not frame.transport_destination and self._camera:
self._camera.write_frame(frame.image) self._camera.write_frame(frame.image)
def reset_watchdog(self):
if self._task_manager:
self._task_manager.reset_watchdog(asyncio.current_task())
async def setup(self, setup: FrameProcessorSetup): async def setup(self, setup: FrameProcessorSetup):
if self._task_manager: if self._task_manager:
return return
@@ -934,6 +940,7 @@ class DailyTransportClient(EventHandler):
await self._joined_event.wait() await self._joined_event.wait()
(callback, *args) = await queue.get() (callback, *args) = await queue.get()
await callback(*args) await callback(*args)
queue.task_done()
def _get_event_loop(self) -> asyncio.AbstractEventLoop: def _get_event_loop(self) -> asyncio.AbstractEventLoop:
if not self._task_manager: if not self._task_manager:

View File

@@ -28,6 +28,7 @@ from pipecat.transports.base_input import BaseInputTransport
from pipecat.transports.base_output import BaseOutputTransport from pipecat.transports.base_output import BaseOutputTransport
from pipecat.transports.base_transport import BaseTransport, TransportParams from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.utils.asyncio import BaseTaskManager from pipecat.utils.asyncio import BaseTaskManager
from pipecat.utils.watchdog_async_iterator import WatchdogAsyncIterator
try: try:
from livekit import rtc from livekit import rtc
@@ -341,8 +342,9 @@ class LiveKitTransportClient:
logger.warning(f"Received unexpected event type: {type(event)}") logger.warning(f"Received unexpected event type: {type(event)}")
async def get_next_audio_frame(self): async def get_next_audio_frame(self):
frame, participant_id = await self._audio_queue.get() while True:
return frame, participant_id frame, participant_id = await self._audio_queue.get()
yield frame, participant_id
def __str__(self): def __str__(self):
return f"{self._transport_name}::LiveKitTransportClient" return f"{self._transport_name}::LiveKitTransportClient"
@@ -413,9 +415,8 @@ class LiveKitInputTransport(BaseInputTransport):
async def _audio_in_task_handler(self): async def _audio_in_task_handler(self):
logger.info("Audio input task started") logger.info("Audio input task started")
while True: audio_iterator = self._client.get_next_audio_frame()
audio_data = await self._client.get_next_audio_frame() async for audio_data in WatchdogAsyncIterator(audio_iterator, reseter=self):
self.start_watchdog()
if audio_data: if audio_data:
audio_frame_event, participant_id = audio_data audio_frame_event, participant_id = audio_data
pipecat_audio_frame = await self._convert_livekit_audio_to_pipecat( pipecat_audio_frame = await self._convert_livekit_audio_to_pipecat(
@@ -428,7 +429,6 @@ class LiveKitInputTransport(BaseInputTransport):
num_channels=pipecat_audio_frame.num_channels, num_channels=pipecat_audio_frame.num_channels,
) )
await self.push_audio_frame(input_audio_frame) await self.push_audio_frame(input_audio_frame)
self.reset_watchdog()
async def _convert_livekit_audio_to_pipecat( async def _convert_livekit_audio_to_pipecat(
self, audio_frame_event: rtc.AudioFrameEvent self, audio_frame_event: rtc.AudioFrameEvent

View File

@@ -27,10 +27,6 @@ class BaseTaskManager(ABC):
def setup(self, params: TaskManagerParams): def setup(self, params: TaskManagerParams):
pass pass
@abstractmethod
async def cleanup(self):
pass
@abstractmethod @abstractmethod
def get_event_loop(self) -> asyncio.AbstractEventLoop: def get_event_loop(self) -> asyncio.AbstractEventLoop:
pass pass
@@ -97,14 +93,6 @@ class BaseTaskManager(ABC):
"""Returns the list of currently created/registered tasks.""" """Returns the list of currently created/registered tasks."""
pass pass
@abstractmethod
def start_watchdog(self, task: asyncio.Task):
"""Starts the given task watchdog timer. If not reset, a warning will be
logged indicating the task is stalling.
"""
pass
@abstractmethod @abstractmethod
def reset_watchdog(self, task: asyncio.Task): def reset_watchdog(self, task: asyncio.Task):
"""Resets the given task watchdog timer. If not reset, a warning will be """Resets the given task watchdog timer. If not reset, a warning will be
@@ -117,31 +105,21 @@ class BaseTaskManager(ABC):
@dataclass @dataclass
class TaskData: class TaskData:
task: asyncio.Task task: asyncio.Task
watchdog_start: asyncio.Event
watchdog_timer: asyncio.Event watchdog_timer: asyncio.Event
enable_watchdog_logging: bool enable_watchdog_logging: bool
watchdog_timeout: float watchdog_timeout: float
watchdog_task: Optional[asyncio.Task]
class TaskManager(BaseTaskManager): class TaskManager(BaseTaskManager):
def __init__(self) -> None: def __init__(self) -> None:
self._tasks: Dict[str, TaskData] = {} self._tasks: Dict[str, TaskData] = {}
self._params: Optional[TaskManagerParams] = None self._params: Optional[TaskManagerParams] = None
self._watchdog_tasks: List[asyncio.Task] = []
def setup(self, params: TaskManagerParams): def setup(self, params: TaskManagerParams):
if not self._params: if not self._params:
self._params = params self._params = params
async def cleanup(self):
for task in self._watchdog_tasks:
try:
task.cancel()
await task
except asyncio.CancelledError:
# This is expected, no need to re-raise.
pass
def get_event_loop(self) -> asyncio.AbstractEventLoop: def get_event_loop(self) -> asyncio.AbstractEventLoop:
if not self._params: if not self._params:
raise Exception("TaskManager is not setup: unable to get event loop") raise Exception("TaskManager is not setup: unable to get event loop")
@@ -189,7 +167,6 @@ class TaskManager(BaseTaskManager):
self._add_task( self._add_task(
TaskData( TaskData(
task=task, task=task,
watchdog_start=asyncio.Event(),
watchdog_timer=asyncio.Event(), watchdog_timer=asyncio.Event(),
enable_watchdog_logging=( enable_watchdog_logging=(
enable_watchdog_logging enable_watchdog_logging
@@ -199,6 +176,7 @@ class TaskManager(BaseTaskManager):
watchdog_timeout=( watchdog_timeout=(
watchdog_timeout if watchdog_timeout else self._params.watchdog_timeout watchdog_timeout if watchdog_timeout else self._params.watchdog_timeout
), ),
watchdog_task=None,
) )
) )
logger.trace(f"{name}: task created") logger.trace(f"{name}: task created")
@@ -231,7 +209,7 @@ class TaskManager(BaseTaskManager):
except Exception as e: except Exception as e:
logger.exception(f"{name}: unexpected exception while stopping task: {e}") logger.exception(f"{name}: unexpected exception while stopping task: {e}")
finally: finally:
self._remove_task(task) await self._remove_task(task)
async def cancel_task(self, task: asyncio.Task, timeout: Optional[float] = None): async def cancel_task(self, task: asyncio.Task, timeout: Optional[float] = None):
"""Cancels the given asyncio Task and awaits its completion with an """Cancels the given asyncio Task and awaits its completion with an
@@ -265,36 +243,19 @@ class TaskManager(BaseTaskManager):
logger.critical(f"{name}: fatal base exception while cancelling task: {e}") logger.critical(f"{name}: fatal base exception while cancelling task: {e}")
raise raise
finally: finally:
self._remove_task(task) await self._remove_task(task)
def current_tasks(self) -> Sequence[asyncio.Task]: def current_tasks(self) -> Sequence[asyncio.Task]:
"""Returns the list of currently created/registered tasks.""" """Returns the list of currently created/registered tasks."""
return [data.task for data in self._tasks.values()] return [data.task for data in self._tasks.values()]
def start_watchdog(self, task: asyncio.Task):
"""Starts the given task watchdog timer. If not reset, a warning will be
logged indicating the task is stalling. If the timer was already started
a warning will be logged.
"""
name = task.get_name()
if name in self._tasks:
if self._tasks[name].watchdog_start.is_set():
logger.warning(f"Watchdog timer for task {name} already started")
else:
self._tasks[name].watchdog_timer.clear()
self._tasks[name].watchdog_start.set()
else:
logger.warning(f"Unable to start watchdog timer: task {name} does not exist")
def reset_watchdog(self, task: asyncio.Task): def reset_watchdog(self, task: asyncio.Task):
"""Resets the given task watchdog timer. If not reset, a warning will be """Resets the given task watchdog timer. If not reset on time, a warning
logged indicating the task is stalling. will be logged indicating the task is stalling.
""" """
name = task.get_name() name = task.get_name()
if name in self._tasks: if name in self._tasks:
self._tasks[name].watchdog_start.clear()
self._tasks[name].watchdog_timer.set() self._tasks[name].watchdog_timer.set()
else: else:
logger.warning(f"Unable to reset watchdog timer: task {name} does not exist") logger.warning(f"Unable to reset watchdog timer: task {name} does not exist")
@@ -302,44 +263,40 @@ class TaskManager(BaseTaskManager):
def _add_task(self, task_data: TaskData): def _add_task(self, task_data: TaskData):
name = task_data.task.get_name() name = task_data.task.get_name()
self._tasks[name] = task_data self._tasks[name] = task_data
watchdog_task = self.get_event_loop().create_task( watchdog_task = self.get_event_loop().create_task(self._watchdog_task_handler(task_data))
self._watchdog_task_handler(self._tasks[name]) task_data.watchdog_task = watchdog_task
)
self._watchdog_tasks.append(watchdog_task)
def _remove_task(self, task: asyncio.Task): async def _remove_task(self, task: asyncio.Task):
name = task.get_name() name = task.get_name()
try: try:
task_data = self._tasks[name]
if task_data.watchdog_task:
try:
task_data.watchdog_task.cancel()
await task_data.watchdog_task
except asyncio.CancelledError:
pass
task_data.watchdog_task = None
del self._tasks[name] del self._tasks[name]
except KeyError as e: except KeyError as e:
logger.trace(f"{name}: unable to remove task (already removed?): {e}") logger.trace(f"{name}: unable to remove task (already removed?): {e}")
async def _watchdog_task_handler(self, task_data: TaskData): async def _watchdog_task_handler(self, task_data: TaskData):
name = task_data.task.get_name() name = task_data.task.get_name()
start = task_data.watchdog_start
timer = task_data.watchdog_timer timer = task_data.watchdog_timer
enable_watchdog_logging = task_data.enable_watchdog_logging enable_watchdog_logging = task_data.enable_watchdog_logging
watchdog_timeout = task_data.watchdog_timeout watchdog_timeout = task_data.watchdog_timeout
async def wait_for_reset():
waiting = True
while waiting:
try:
start_time = time.time()
await asyncio.wait_for(timer.wait(), timeout=watchdog_timeout)
total_time = time.time() - start_time
if enable_watchdog_logging:
logger.debug(f"{name} task processing time: {total_time:.20f}")
waiting = False
except asyncio.TimeoutError:
logger.warning(
f"{name}: task is taking too long {WATCHDOG_TIMEOUT} second(s) (forgot to reset watchdog?)"
)
finally:
timer.clear()
while True: while True:
# Wait for the user to start the watchdog timer. try:
await start.wait() start_time = time.time()
# Now, waiting for the task to finish. await asyncio.wait_for(timer.wait(), timeout=watchdog_timeout)
await wait_for_reset() total_time = time.time() - start_time
if enable_watchdog_logging:
logger.debug(f"{name} task processing time: {total_time:.20f}")
except asyncio.TimeoutError:
logger.warning(
f"{name}: task is taking too long {WATCHDOG_TIMEOUT} second(s) (forgot to reset watchdog?)"
)
finally:
timer.clear()

View File

@@ -0,0 +1,60 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio
from typing import AsyncIterator, Optional
from pipecat.utils.watchdog_reseter import WatchdogReseter
class WatchdogAsyncIterator:
"""An asynchronous iterator that monitors activity and resets the current
task watchdog timer. This is necessary to avoid task watchdog timers to
expire while we are waiting to get an item from the iterator.
"""
def __init__(self, async_iterable, *, reseter: WatchdogReseter, timeout: float = 2.0):
self._async_iterable = async_iterable
self._reseter = reseter
self._timeout = timeout
self._iter: Optional[AsyncIterator] = None
self._current_anext_task: Optional[asyncio.Task] = None
def __aiter__(self):
return self
async def __anext__(self):
if not self._iter:
self._iter = await self._ensure_async_iterator(self._async_iterable)
while True:
try:
if not self._current_anext_task:
self._current_anext_task = asyncio.create_task(self._iter.__anext__())
item = await asyncio.wait_for(
asyncio.shield(self._current_anext_task),
timeout=self._timeout,
)
self._reseter.reset_watchdog()
# The task has finish, so we will create a new one for th next item.
self._current_anext_task = None
return item
except asyncio.TimeoutError:
self._reseter.reset_watchdog()
except StopAsyncIteration:
self._current_anext_task = None
raise
async def _ensure_async_iterator(self, obj) -> AsyncIterator:
aiter = obj.__aiter__()
if asyncio.iscoroutine(aiter):
aiter = await aiter
return aiter

View File

@@ -0,0 +1,31 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio
from pipecat.utils.watchdog_reseter import WatchdogReseter
class WatchdogEvent(asyncio.Event):
"""An asynchronous event that resets the current task watchdog timer. This
is necessary to avoid task watchdog timers to expire while we are waiting on
the event.
"""
def __init__(self, reseter: WatchdogReseter, timeout: float = 2.0) -> None:
super().__init__()
self._reseter = reseter
self._timeout = timeout
async def wait(self):
while True:
try:
await asyncio.wait_for(super().wait(), timeout=self._timeout)
self._reseter.reset_watchdog()
return True
except asyncio.TimeoutError:
self._reseter.reset_watchdog()

View File

@@ -0,0 +1,35 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio
from pipecat.utils.watchdog_reseter import WatchdogReseter
class WatchdogPriorityQueue(asyncio.PriorityQueue):
"""An asynchronous priority queue that resets the current task watchdog
timer. This is necessary to avoid task watchdog timers to expire while we
are waiting to get an item from the queue.
"""
def __init__(self, reseter: WatchdogReseter, maxsize: int = 0, timeout: float = 2.0) -> None:
super().__init__(maxsize)
self._reseter = reseter
self._timeout = timeout
async def get(self):
while True:
try:
item = await asyncio.wait_for(super().get(), timeout=self._timeout)
self._reseter.reset_watchdog()
return item
except asyncio.TimeoutError:
self._reseter.reset_watchdog()
def task_done(self):
self._reseter.reset_watchdog()
super().task_done()

View File

@@ -0,0 +1,35 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
import asyncio
from pipecat.utils.watchdog_reseter import WatchdogReseter
class WatchdogQueue(asyncio.Queue):
"""An asynchronous queue that resets the current task watchdog timer. This
is necessary to avoid task watchdog timers to expire while we are waiting to
get an item from the queue.
"""
def __init__(self, reseter: WatchdogReseter, maxsize: int = 0, timeout: float = 2.0) -> None:
super().__init__(maxsize)
self._reseter = reseter
self._timeout = timeout
async def get(self):
while True:
try:
item = await asyncio.wait_for(super().get(), timeout=self._timeout)
self._reseter.reset_watchdog()
return item
except asyncio.TimeoutError:
self._reseter.reset_watchdog()
def task_done(self):
self._reseter.reset_watchdog()
super().task_done()

View File

@@ -0,0 +1,13 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
from abc import ABC, abstractmethod
class WatchdogReseter(ABC):
@abstractmethod
def reset_watchdog(self):
pass