dv-pipecat-ai 0.0.74.dev770__py3-none-any.whl → 0.0.82.dev776__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dv-pipecat-ai might be problematic. Click here for more details.

Files changed (244) hide show
  1. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
  2. dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
  3. pipecat/__init__.py +17 -0
  4. pipecat/adapters/base_llm_adapter.py +36 -1
  5. pipecat/adapters/schemas/direct_function.py +296 -0
  6. pipecat/adapters/schemas/function_schema.py +15 -6
  7. pipecat/adapters/schemas/tools_schema.py +55 -7
  8. pipecat/adapters/services/anthropic_adapter.py +22 -3
  9. pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
  10. pipecat/adapters/services/bedrock_adapter.py +22 -3
  11. pipecat/adapters/services/gemini_adapter.py +16 -3
  12. pipecat/adapters/services/open_ai_adapter.py +17 -2
  13. pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
  14. pipecat/audio/filters/base_audio_filter.py +30 -6
  15. pipecat/audio/filters/koala_filter.py +37 -2
  16. pipecat/audio/filters/krisp_filter.py +59 -6
  17. pipecat/audio/filters/noisereduce_filter.py +37 -0
  18. pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
  19. pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
  20. pipecat/audio/mixers/base_audio_mixer.py +30 -7
  21. pipecat/audio/mixers/soundfile_mixer.py +53 -6
  22. pipecat/audio/resamplers/base_audio_resampler.py +17 -9
  23. pipecat/audio/resamplers/resampy_resampler.py +26 -1
  24. pipecat/audio/resamplers/soxr_resampler.py +32 -1
  25. pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
  26. pipecat/audio/utils.py +194 -1
  27. pipecat/audio/vad/silero.py +60 -3
  28. pipecat/audio/vad/vad_analyzer.py +114 -30
  29. pipecat/clocks/base_clock.py +19 -0
  30. pipecat/clocks/system_clock.py +25 -0
  31. pipecat/extensions/voicemail/__init__.py +0 -0
  32. pipecat/extensions/voicemail/voicemail_detector.py +707 -0
  33. pipecat/frames/frames.py +590 -156
  34. pipecat/metrics/metrics.py +64 -1
  35. pipecat/observers/base_observer.py +58 -19
  36. pipecat/observers/loggers/debug_log_observer.py +56 -64
  37. pipecat/observers/loggers/llm_log_observer.py +8 -1
  38. pipecat/observers/loggers/transcription_log_observer.py +19 -7
  39. pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
  40. pipecat/observers/turn_tracking_observer.py +26 -1
  41. pipecat/pipeline/base_pipeline.py +5 -7
  42. pipecat/pipeline/base_task.py +52 -9
  43. pipecat/pipeline/parallel_pipeline.py +121 -177
  44. pipecat/pipeline/pipeline.py +129 -20
  45. pipecat/pipeline/runner.py +50 -1
  46. pipecat/pipeline/sync_parallel_pipeline.py +132 -32
  47. pipecat/pipeline/task.py +263 -280
  48. pipecat/pipeline/task_observer.py +85 -34
  49. pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
  50. pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
  51. pipecat/processors/aggregators/gated.py +25 -24
  52. pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
  53. pipecat/processors/aggregators/llm_response.py +398 -89
  54. pipecat/processors/aggregators/openai_llm_context.py +161 -13
  55. pipecat/processors/aggregators/sentence.py +25 -14
  56. pipecat/processors/aggregators/user_response.py +28 -3
  57. pipecat/processors/aggregators/vision_image_frame.py +24 -14
  58. pipecat/processors/async_generator.py +28 -0
  59. pipecat/processors/audio/audio_buffer_processor.py +78 -37
  60. pipecat/processors/consumer_processor.py +25 -6
  61. pipecat/processors/filters/frame_filter.py +23 -0
  62. pipecat/processors/filters/function_filter.py +30 -0
  63. pipecat/processors/filters/identity_filter.py +17 -2
  64. pipecat/processors/filters/null_filter.py +24 -1
  65. pipecat/processors/filters/stt_mute_filter.py +56 -21
  66. pipecat/processors/filters/wake_check_filter.py +46 -3
  67. pipecat/processors/filters/wake_notifier_filter.py +21 -3
  68. pipecat/processors/frame_processor.py +488 -131
  69. pipecat/processors/frameworks/langchain.py +38 -3
  70. pipecat/processors/frameworks/rtvi.py +719 -34
  71. pipecat/processors/gstreamer/pipeline_source.py +41 -0
  72. pipecat/processors/idle_frame_processor.py +26 -3
  73. pipecat/processors/logger.py +23 -0
  74. pipecat/processors/metrics/frame_processor_metrics.py +77 -4
  75. pipecat/processors/metrics/sentry.py +42 -4
  76. pipecat/processors/producer_processor.py +34 -14
  77. pipecat/processors/text_transformer.py +22 -10
  78. pipecat/processors/transcript_processor.py +48 -29
  79. pipecat/processors/user_idle_processor.py +31 -21
  80. pipecat/runner/__init__.py +1 -0
  81. pipecat/runner/daily.py +132 -0
  82. pipecat/runner/livekit.py +148 -0
  83. pipecat/runner/run.py +543 -0
  84. pipecat/runner/types.py +67 -0
  85. pipecat/runner/utils.py +515 -0
  86. pipecat/serializers/base_serializer.py +42 -0
  87. pipecat/serializers/exotel.py +17 -6
  88. pipecat/serializers/genesys.py +95 -0
  89. pipecat/serializers/livekit.py +33 -0
  90. pipecat/serializers/plivo.py +16 -15
  91. pipecat/serializers/protobuf.py +37 -1
  92. pipecat/serializers/telnyx.py +18 -17
  93. pipecat/serializers/twilio.py +32 -16
  94. pipecat/services/ai_service.py +5 -3
  95. pipecat/services/anthropic/llm.py +113 -43
  96. pipecat/services/assemblyai/models.py +63 -5
  97. pipecat/services/assemblyai/stt.py +64 -11
  98. pipecat/services/asyncai/__init__.py +0 -0
  99. pipecat/services/asyncai/tts.py +501 -0
  100. pipecat/services/aws/llm.py +185 -111
  101. pipecat/services/aws/stt.py +217 -23
  102. pipecat/services/aws/tts.py +118 -52
  103. pipecat/services/aws/utils.py +101 -5
  104. pipecat/services/aws_nova_sonic/aws.py +82 -64
  105. pipecat/services/aws_nova_sonic/context.py +15 -6
  106. pipecat/services/azure/common.py +10 -2
  107. pipecat/services/azure/image.py +32 -0
  108. pipecat/services/azure/llm.py +9 -7
  109. pipecat/services/azure/stt.py +65 -2
  110. pipecat/services/azure/tts.py +154 -23
  111. pipecat/services/cartesia/stt.py +125 -8
  112. pipecat/services/cartesia/tts.py +102 -38
  113. pipecat/services/cerebras/llm.py +15 -23
  114. pipecat/services/deepgram/stt.py +19 -11
  115. pipecat/services/deepgram/tts.py +36 -0
  116. pipecat/services/deepseek/llm.py +14 -23
  117. pipecat/services/elevenlabs/tts.py +330 -64
  118. pipecat/services/fal/image.py +43 -0
  119. pipecat/services/fal/stt.py +48 -10
  120. pipecat/services/fireworks/llm.py +14 -21
  121. pipecat/services/fish/tts.py +109 -9
  122. pipecat/services/gemini_multimodal_live/__init__.py +1 -0
  123. pipecat/services/gemini_multimodal_live/events.py +83 -2
  124. pipecat/services/gemini_multimodal_live/file_api.py +189 -0
  125. pipecat/services/gemini_multimodal_live/gemini.py +218 -21
  126. pipecat/services/gladia/config.py +17 -10
  127. pipecat/services/gladia/stt.py +82 -36
  128. pipecat/services/google/frames.py +40 -0
  129. pipecat/services/google/google.py +2 -0
  130. pipecat/services/google/image.py +39 -2
  131. pipecat/services/google/llm.py +176 -58
  132. pipecat/services/google/llm_openai.py +26 -4
  133. pipecat/services/google/llm_vertex.py +37 -15
  134. pipecat/services/google/rtvi.py +41 -0
  135. pipecat/services/google/stt.py +65 -17
  136. pipecat/services/google/test-google-chirp.py +45 -0
  137. pipecat/services/google/tts.py +390 -19
  138. pipecat/services/grok/llm.py +8 -6
  139. pipecat/services/groq/llm.py +8 -6
  140. pipecat/services/groq/stt.py +13 -9
  141. pipecat/services/groq/tts.py +40 -0
  142. pipecat/services/hamsa/__init__.py +9 -0
  143. pipecat/services/hamsa/stt.py +241 -0
  144. pipecat/services/heygen/__init__.py +5 -0
  145. pipecat/services/heygen/api.py +281 -0
  146. pipecat/services/heygen/client.py +620 -0
  147. pipecat/services/heygen/video.py +338 -0
  148. pipecat/services/image_service.py +5 -3
  149. pipecat/services/inworld/__init__.py +1 -0
  150. pipecat/services/inworld/tts.py +592 -0
  151. pipecat/services/llm_service.py +127 -45
  152. pipecat/services/lmnt/tts.py +80 -7
  153. pipecat/services/mcp_service.py +85 -44
  154. pipecat/services/mem0/memory.py +42 -13
  155. pipecat/services/minimax/tts.py +74 -15
  156. pipecat/services/mistral/__init__.py +0 -0
  157. pipecat/services/mistral/llm.py +185 -0
  158. pipecat/services/moondream/vision.py +55 -10
  159. pipecat/services/neuphonic/tts.py +275 -48
  160. pipecat/services/nim/llm.py +8 -6
  161. pipecat/services/ollama/llm.py +27 -7
  162. pipecat/services/openai/base_llm.py +54 -16
  163. pipecat/services/openai/image.py +30 -0
  164. pipecat/services/openai/llm.py +7 -5
  165. pipecat/services/openai/stt.py +13 -9
  166. pipecat/services/openai/tts.py +42 -10
  167. pipecat/services/openai_realtime_beta/azure.py +11 -9
  168. pipecat/services/openai_realtime_beta/context.py +7 -5
  169. pipecat/services/openai_realtime_beta/events.py +10 -7
  170. pipecat/services/openai_realtime_beta/openai.py +37 -18
  171. pipecat/services/openpipe/llm.py +30 -24
  172. pipecat/services/openrouter/llm.py +9 -7
  173. pipecat/services/perplexity/llm.py +15 -19
  174. pipecat/services/piper/tts.py +26 -12
  175. pipecat/services/playht/tts.py +227 -65
  176. pipecat/services/qwen/llm.py +8 -6
  177. pipecat/services/rime/tts.py +128 -17
  178. pipecat/services/riva/stt.py +160 -22
  179. pipecat/services/riva/tts.py +67 -2
  180. pipecat/services/sambanova/llm.py +19 -17
  181. pipecat/services/sambanova/stt.py +14 -8
  182. pipecat/services/sarvam/tts.py +60 -13
  183. pipecat/services/simli/video.py +82 -21
  184. pipecat/services/soniox/__init__.py +0 -0
  185. pipecat/services/soniox/stt.py +398 -0
  186. pipecat/services/speechmatics/stt.py +29 -17
  187. pipecat/services/stt_service.py +47 -11
  188. pipecat/services/tavus/video.py +94 -25
  189. pipecat/services/together/llm.py +8 -6
  190. pipecat/services/tts_service.py +77 -53
  191. pipecat/services/ultravox/stt.py +46 -43
  192. pipecat/services/vision_service.py +5 -3
  193. pipecat/services/websocket_service.py +12 -11
  194. pipecat/services/whisper/base_stt.py +58 -12
  195. pipecat/services/whisper/stt.py +69 -58
  196. pipecat/services/xtts/tts.py +59 -2
  197. pipecat/sync/base_notifier.py +19 -0
  198. pipecat/sync/event_notifier.py +24 -0
  199. pipecat/tests/utils.py +73 -5
  200. pipecat/transcriptions/language.py +24 -0
  201. pipecat/transports/base_input.py +112 -8
  202. pipecat/transports/base_output.py +235 -13
  203. pipecat/transports/base_transport.py +119 -0
  204. pipecat/transports/local/audio.py +76 -0
  205. pipecat/transports/local/tk.py +84 -0
  206. pipecat/transports/network/fastapi_websocket.py +174 -15
  207. pipecat/transports/network/small_webrtc.py +383 -39
  208. pipecat/transports/network/webrtc_connection.py +214 -8
  209. pipecat/transports/network/websocket_client.py +171 -1
  210. pipecat/transports/network/websocket_server.py +147 -9
  211. pipecat/transports/services/daily.py +792 -70
  212. pipecat/transports/services/helpers/daily_rest.py +122 -129
  213. pipecat/transports/services/livekit.py +339 -4
  214. pipecat/transports/services/tavus.py +273 -38
  215. pipecat/utils/asyncio/task_manager.py +92 -186
  216. pipecat/utils/base_object.py +83 -1
  217. pipecat/utils/network.py +2 -0
  218. pipecat/utils/string.py +114 -58
  219. pipecat/utils/text/base_text_aggregator.py +44 -13
  220. pipecat/utils/text/base_text_filter.py +46 -0
  221. pipecat/utils/text/markdown_text_filter.py +70 -14
  222. pipecat/utils/text/pattern_pair_aggregator.py +18 -14
  223. pipecat/utils/text/simple_text_aggregator.py +43 -2
  224. pipecat/utils/text/skip_tags_aggregator.py +21 -13
  225. pipecat/utils/time.py +36 -0
  226. pipecat/utils/tracing/class_decorators.py +32 -7
  227. pipecat/utils/tracing/conversation_context_provider.py +12 -2
  228. pipecat/utils/tracing/service_attributes.py +80 -64
  229. pipecat/utils/tracing/service_decorators.py +48 -21
  230. pipecat/utils/tracing/setup.py +13 -7
  231. pipecat/utils/tracing/turn_context_provider.py +12 -2
  232. pipecat/utils/tracing/turn_trace_observer.py +27 -0
  233. pipecat/utils/utils.py +14 -14
  234. dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
  235. pipecat/examples/daily_runner.py +0 -64
  236. pipecat/examples/run.py +0 -265
  237. pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
  238. pipecat/utils/asyncio/watchdog_event.py +0 -42
  239. pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
  240. pipecat/utils/asyncio/watchdog_queue.py +0 -48
  241. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
  242. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
  243. {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
  244. /pipecat/{examples → extensions}/__init__.py +0 -0
@@ -4,11 +4,18 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """Frame processing pipeline infrastructure for Pipecat.
8
+
9
+ This module provides the core frame processing system that enables building
10
+ audio/video processing pipelines. It includes frame processors, pipeline
11
+ management, and frame flow control mechanisms.
12
+ """
13
+
7
14
  import asyncio
8
15
  import traceback
9
16
  from dataclasses import dataclass
10
17
  from enum import Enum
11
- from typing import Awaitable, Callable, Coroutine, List, Optional, Sequence
18
+ from typing import Any, Awaitable, Callable, Coroutine, List, Optional, Sequence, Tuple
12
19
 
13
20
  from loguru import logger
14
21
 
@@ -28,51 +35,132 @@ from pipecat.frames.frames import (
28
35
  SystemFrame,
29
36
  )
30
37
  from pipecat.metrics.metrics import LLMTokenUsage, MetricsData
31
- from pipecat.observers.base_observer import BaseObserver, FramePushed
38
+ from pipecat.observers.base_observer import BaseObserver, FrameProcessed, FramePushed
32
39
  from pipecat.processors.metrics.frame_processor_metrics import FrameProcessorMetrics
33
40
  from pipecat.utils.asyncio.task_manager import BaseTaskManager
34
- from pipecat.utils.asyncio.watchdog_event import WatchdogEvent
35
- from pipecat.utils.asyncio.watchdog_queue import WatchdogQueue
36
41
  from pipecat.utils.base_object import BaseObject
37
42
 
38
43
 
39
44
  class FrameDirection(Enum):
45
+ """Direction of frame flow in the processing pipeline.
46
+
47
+ Parameters:
48
+ DOWNSTREAM: Frames flowing from input to output.
49
+ UPSTREAM: Frames flowing back from output to input.
50
+ """
51
+
40
52
  DOWNSTREAM = 1
41
53
  UPSTREAM = 2
42
54
 
43
55
 
56
+ FrameCallback = Callable[["FrameProcessor", Frame, FrameDirection], Awaitable[None]]
57
+
58
+
44
59
  @dataclass
45
60
  class FrameProcessorSetup:
61
+ """Configuration parameters for frame processor initialization.
62
+
63
+ Parameters:
64
+ clock: The clock instance for timing operations.
65
+ task_manager: The task manager for handling async operations.
66
+ observer: Optional observer for monitoring frame processing events.
67
+ """
68
+
46
69
  clock: BaseClock
47
70
  task_manager: BaseTaskManager
48
71
  observer: Optional[BaseObserver] = None
49
- watchdog_timers_enabled: bool = False
72
+
73
+
74
+ class FrameProcessorQueue(asyncio.PriorityQueue):
75
+ """A priority queue for systems frames and other frames.
76
+
77
+ This is a specialized queue for frame processors that separates and
78
+ prioritizes system frames over other frames. It ensures that `SystemFrame`
79
+ objects are processed before any other frames by using a priority queue.
80
+
81
+ """
82
+
83
+ HIGH_PRIORITY = 1
84
+ LOW_PRIORITY = 2
85
+
86
+ def __init__(self):
87
+ """Initialize the FrameProcessorQueue.
88
+
89
+ Args:
90
+ manager (BaseTaskManager): The task manager used by the internal watchdog queues.
91
+
92
+ """
93
+ super().__init__()
94
+ self.__high_counter = 0
95
+ self.__low_counter = 0
96
+
97
+ async def put(self, item: Tuple[Frame, FrameDirection, FrameCallback]):
98
+ """Put an item into the priority queue.
99
+
100
+ System frames (`SystemFrame`) have higher priority than any other
101
+ frames. If a non-frame item (e.g. a watchdog cancellation sentinel) is
102
+ provided it will have the highest priority.
103
+
104
+ Args:
105
+ item (Any): The item to enqueue.
106
+
107
+ """
108
+ frame, _, _ = item
109
+ if isinstance(frame, SystemFrame):
110
+ self.__high_counter += 1
111
+ await super().put((self.HIGH_PRIORITY, self.__high_counter, item))
112
+ else:
113
+ self.__low_counter += 1
114
+ await super().put((self.LOW_PRIORITY, self.__low_counter, item))
115
+
116
+ async def get(self) -> Any:
117
+ """Retrieve the next item from the queue.
118
+
119
+ System frames are prioritized. If both queues are empty, this method
120
+ waits until an item is available.
121
+
122
+ Returns:
123
+ Any: The next item from the system or main queue.
124
+
125
+ """
126
+ _, _, item = await super().get()
127
+ return item
50
128
 
51
129
 
52
130
  class FrameProcessor(BaseObject):
131
+ """Base class for all frame processors in the pipeline.
132
+
133
+ Frame processors are the building blocks of Pipecat pipelines, they can be
134
+ linked to form complex processing pipelines. They receive frames, process
135
+ them, and pass them to the next or previous processor in the chain. Each
136
+ frame processor guarantees frame ordering and processes frames in its own
137
+ task. System frames are also processed in a separate task which guarantees
138
+ frame priority.
139
+
140
+ """
141
+
53
142
  def __init__(
54
143
  self,
55
144
  *,
56
145
  name: Optional[str] = None,
57
- enable_watchdog_logging: Optional[bool] = None,
58
- enable_watchdog_timers: Optional[bool] = None,
146
+ enable_direct_mode: bool = False,
59
147
  metrics: Optional[FrameProcessorMetrics] = None,
60
- watchdog_timeout_secs: Optional[float] = None,
61
148
  **kwargs,
62
149
  ):
63
- super().__init__(name=name)
64
- self._parent: Optional["FrameProcessor"] = None
150
+ """Initialize the frame processor.
151
+
152
+ Args:
153
+ name: Optional name for this processor instance.
154
+ enable_direct_mode: Whether to process frames immediately or use internal queues.
155
+ metrics: Optional metrics collector for this processor.
156
+ **kwargs: Additional arguments passed to parent class.
157
+ """
158
+ super().__init__(name=name, **kwargs)
65
159
  self._prev: Optional["FrameProcessor"] = None
66
160
  self._next: Optional["FrameProcessor"] = None
67
161
 
68
- # Enable watchdog timers for all tasks created by this frame processor.
69
- self._enable_watchdog_timers = enable_watchdog_timers
70
-
71
- # Enable watchdog logging for all tasks created by this frame processor.
72
- self._enable_watchdog_logging = enable_watchdog_logging
73
-
74
- # Allow this frame processor to control their tasks timeout.
75
- self._watchdog_timeout_secs = watchdog_timeout_secs
162
+ # Enable direct mode to skip queues and process frames right away.
163
+ self._enable_direct_mode = enable_direct_mode
76
164
 
77
165
  # Clock
78
166
  self._clock: Optional[BaseClock] = None
@@ -104,201 +192,396 @@ class FrameProcessor(BaseObject):
104
192
  self._metrics = metrics or FrameProcessorMetrics()
105
193
  self._metrics.set_processor_name(self.name)
106
194
 
107
- # Processors have an input queue. The input queue will be processed
108
- # immediately (default) or it will block if `pause_processing_frames()`
109
- # is called. To resume processing frames we need to call
110
- # `resume_processing_frames()` which will wake up the event.
111
- self.__should_block_frames = False
112
- self.__input_event = None
195
+ # Processors have an input priority queue which stores any type of
196
+ # frames in order. System frames have higher priority than any other
197
+ # frames, so they will be returned first from the queue.
198
+ #
199
+ # If a system frame is obtained it will be processed immediately any
200
+ # other type of frame (data and control) will be put in a separate queue
201
+ # for later processing. This guarantees that each frame processor will
202
+ # always process system frames before any other frame in the queue.
203
+
204
+ # The input task that handles all types of frames. It processes system
205
+ # frames right away and queues non-system frames for later processing.
206
+ self.__should_block_system_frames = False
207
+ self.__input_event: Optional[asyncio.Event] = None
113
208
  self.__input_frame_task: Optional[asyncio.Task] = None
114
209
 
115
- # Every processor in Pipecat should only output frames from a single
116
- # task. This avoid problems like audio overlapping. System frames are the
117
- # exception to this rule. This create this task.
118
- self.__push_frame_task: Optional[asyncio.Task] = None
210
+ # The process task processes non-system frames. Non-system frames will
211
+ # be processed as soon as they are received by the processing task
212
+ # (default) or they will block if `pause_processing_frames()` is
213
+ # called. To resume processing frames we need to call
214
+ # `resume_processing_frames()` which will wake up the event.
215
+ self.__should_block_frames = False
216
+ self.__process_event: Optional[asyncio.Event] = None
217
+ self.__process_frame_task: Optional[asyncio.Task] = None
119
218
  self.logger = logger # Will later be replaced with a bound logger
120
219
 
121
220
  @property
122
221
  def id(self) -> int:
222
+ """Get the unique identifier for this processor.
223
+
224
+ Returns:
225
+ The unique integer ID of this processor.
226
+ """
123
227
  return self._id
124
228
 
125
229
  @property
126
230
  def name(self) -> str:
231
+ """Get the name of this processor.
232
+
233
+ Returns:
234
+ The name of this processor instance.
235
+ """
127
236
  return self._name
128
237
 
238
+ @property
239
+ def processors(self) -> List["FrameProcessor"]:
240
+ """Return the list of sub-processors contained within this processor.
241
+
242
+ Only compound processors (e.g. pipelines and parallel pipelines) have
243
+ sub-processors. Non-compound processors will return an empty list.
244
+
245
+ Returns:
246
+ The list of sub-processors if this is a compound processor.
247
+ """
248
+ return []
249
+
250
+ @property
251
+ def entry_processors(self) -> List["FrameProcessor"]:
252
+ """Return the list of entry processors for this processor.
253
+
254
+ Entry processors are the first processors in a compound processor
255
+ (e.g. pipelines, parallel pipelines). Note that pipelines can also be an
256
+ entry processor as pipelines are processors themselves. Non-compound
257
+ processors will simply return an empty list.
258
+
259
+ Returns:
260
+ The list of entry processors.
261
+ """
262
+ return []
263
+
264
+ @property
265
+ def next(self) -> Optional["FrameProcessor"]:
266
+ """Get the next processor.
267
+
268
+ Returns:
269
+ The next processor, or None if there's no next processor.
270
+ """
271
+ return self._next
272
+
273
+ @property
274
+ def previous(self) -> Optional["FrameProcessor"]:
275
+ """Get the previous processor.
276
+
277
+ Returns:
278
+ The previous processor, or None if there's no previous processor.
279
+ """
280
+ return self._prev
281
+
129
282
  @property
130
283
  def interruptions_allowed(self):
284
+ """Check if interruptions are allowed for this processor.
285
+
286
+ Returns:
287
+ True if interruptions are allowed.
288
+ """
131
289
  return self._allow_interruptions
132
290
 
133
291
  @property
134
292
  def metrics_enabled(self):
293
+ """Check if metrics collection is enabled.
294
+
295
+ Returns:
296
+ True if metrics collection is enabled.
297
+ """
135
298
  return self._enable_metrics
136
299
 
137
300
  @property
138
301
  def usage_metrics_enabled(self):
302
+ """Check if usage metrics collection is enabled.
303
+
304
+ Returns:
305
+ True if usage metrics collection is enabled.
306
+ """
139
307
  return self._enable_usage_metrics
140
308
 
141
309
  @property
142
310
  def report_only_initial_ttfb(self):
311
+ """Check if only initial TTFB should be reported.
312
+
313
+ Returns:
314
+ True if only initial time-to-first-byte should be reported.
315
+ """
143
316
  return self._report_only_initial_ttfb
144
317
 
145
318
  @property
146
319
  def interruption_strategies(self) -> Sequence[BaseInterruptionStrategy]:
320
+ """Get the interruption strategies for this processor.
321
+
322
+ Returns:
323
+ Sequence of interruption strategies.
324
+ """
147
325
  return self._interruption_strategies
148
326
 
149
327
  @property
150
328
  def task_manager(self) -> BaseTaskManager:
329
+ """Get the task manager for this processor.
330
+
331
+ Returns:
332
+ The task manager instance.
333
+
334
+ Raises:
335
+ Exception: If the task manager is not initialized.
336
+ """
151
337
  if not self._task_manager:
152
338
  raise Exception(f"{self} TaskManager is still not initialized.")
153
339
  return self._task_manager
154
340
 
341
+ def processors_with_metrics(self):
342
+ """Return processors that can generate metrics.
343
+
344
+ Recursively collects all processors that support metrics generation,
345
+ including those from nested processors.
346
+
347
+ Returns:
348
+ List of frame processors that can generate metrics.
349
+ """
350
+ return []
351
+
155
352
  def can_generate_metrics(self) -> bool:
353
+ """Check if this processor can generate metrics.
354
+
355
+ Returns:
356
+ True if this processor can generate metrics.
357
+ """
156
358
  return False
157
359
 
158
360
  def set_core_metrics_data(self, data: MetricsData):
361
+ """Set core metrics data for this processor.
362
+
363
+ Args:
364
+ data: The metrics data to set.
365
+ """
159
366
  self._metrics.set_core_metrics_data(data)
160
367
 
161
368
  async def start_ttfb_metrics(self):
369
+ """Start time-to-first-byte metrics collection."""
162
370
  if self.can_generate_metrics() and self.metrics_enabled:
163
371
  await self._metrics.start_ttfb_metrics(self._report_only_initial_ttfb)
164
372
 
165
373
  async def stop_ttfb_metrics(self):
374
+ """Stop time-to-first-byte metrics collection and push results."""
166
375
  if self.can_generate_metrics() and self.metrics_enabled:
167
376
  frame = await self._metrics.stop_ttfb_metrics()
168
377
  if frame:
169
378
  await self.push_frame(frame)
170
379
 
171
380
  async def start_processing_metrics(self):
381
+ """Start processing metrics collection."""
172
382
  if self.can_generate_metrics() and self.metrics_enabled:
173
383
  await self._metrics.start_processing_metrics()
174
384
 
175
385
  async def stop_processing_metrics(self):
386
+ """Stop processing metrics collection and push results."""
176
387
  if self.can_generate_metrics() and self.metrics_enabled:
177
388
  frame = await self._metrics.stop_processing_metrics()
178
389
  if frame:
179
390
  await self.push_frame(frame)
180
391
 
181
392
  async def start_llm_usage_metrics(self, tokens: LLMTokenUsage):
393
+ """Start LLM usage metrics collection.
394
+
395
+ Args:
396
+ tokens: Token usage information for the LLM.
397
+ """
182
398
  if self.can_generate_metrics() and self.usage_metrics_enabled:
183
399
  frame = await self._metrics.start_llm_usage_metrics(tokens)
184
400
  if frame:
185
401
  await self.push_frame(frame)
186
402
 
187
403
  async def start_tts_usage_metrics(self, text: str):
404
+ """Start TTS usage metrics collection.
405
+
406
+ Args:
407
+ text: The text being processed by TTS.
408
+ """
188
409
  if self.can_generate_metrics() and self.usage_metrics_enabled:
189
410
  frame = await self._metrics.start_tts_usage_metrics(text)
190
411
  if frame:
191
412
  await self.push_frame(frame)
192
413
 
193
414
  async def stop_all_metrics(self):
415
+ """Stop all active metrics collection."""
194
416
  await self.stop_ttfb_metrics()
195
417
  await self.stop_processing_metrics()
196
418
 
197
- def create_task(
198
- self,
199
- coroutine: Coroutine,
200
- name: Optional[str] = None,
201
- *,
202
- enable_watchdog_logging: Optional[bool] = None,
203
- enable_watchdog_timers: Optional[bool] = None,
204
- watchdog_timeout_secs: Optional[float] = None,
205
- ) -> asyncio.Task:
419
+ def create_task(self, coroutine: Coroutine, name: Optional[str] = None) -> asyncio.Task:
420
+ """Create a new task managed by this processor.
421
+
422
+ Args:
423
+ coroutine: The coroutine to run in the task.
424
+ name: Optional name for the task.
425
+
426
+ Returns:
427
+ The created asyncio task.
428
+ """
206
429
  if name:
207
430
  name = f"{self}::{name}"
208
431
  else:
209
432
  name = f"{self}::{coroutine.cr_code.co_name}"
210
- return self.task_manager.create_task(
211
- coroutine,
212
- name,
213
- enable_watchdog_logging=(
214
- enable_watchdog_logging
215
- if enable_watchdog_logging
216
- else self._enable_watchdog_logging
217
- ),
218
- enable_watchdog_timers=(
219
- enable_watchdog_timers if enable_watchdog_timers else self._enable_watchdog_timers
220
- ),
221
- watchdog_timeout=(
222
- watchdog_timeout_secs if watchdog_timeout_secs else self._watchdog_timeout_secs
223
- ),
224
- )
433
+ return self.task_manager.create_task(coroutine, name)
225
434
 
226
435
  async def cancel_task(self, task: asyncio.Task, timeout: Optional[float] = None):
436
+ """Cancel a task managed by this processor.
437
+
438
+ Args:
439
+ task: The task to cancel.
440
+ timeout: Optional timeout for task cancellation.
441
+ """
227
442
  await self.task_manager.cancel_task(task, timeout)
228
443
 
229
444
  async def wait_for_task(self, task: asyncio.Task, timeout: Optional[float] = None):
230
- await self.task_manager.wait_for_task(task, timeout)
445
+ """Wait for a task to complete.
446
+
447
+ .. deprecated:: 0.0.81
448
+ This function is deprecated, use `await task` or
449
+ `await asyncio.wait_for(task, timeout) instead.
450
+
451
+ Args:
452
+ task: The task to wait for.
453
+ timeout: Optional timeout for waiting.
454
+ """
455
+ import warnings
456
+
457
+ warnings.warn(
458
+ "`FrameProcessor.wait_for_task()` is deprecated. "
459
+ "Use `await task` or `await asyncio.wait_for(task, timeout)` instead.",
460
+ DeprecationWarning,
461
+ stacklevel=2,
462
+ )
231
463
 
232
- def reset_watchdog(self):
233
- self.task_manager.task_reset_watchdog()
464
+ if timeout:
465
+ await asyncio.wait_for(task, timeout)
466
+ else:
467
+ await task
234
468
 
235
469
  async def setup(self, setup: FrameProcessorSetup):
470
+ """Set up the processor with required components.
471
+
472
+ Args:
473
+ setup: Configuration object containing setup parameters.
474
+ """
236
475
  self._clock = setup.clock
237
476
  self._task_manager = setup.task_manager
238
477
  self._observer = setup.observer
239
- self._watchdog_timers_enabled = (
240
- self._enable_watchdog_timers
241
- if self._enable_watchdog_timers
242
- else setup.watchdog_timers_enabled
243
- )
478
+
479
+ # Create processing tasks.
480
+ self.__create_input_task()
481
+
244
482
  if self._metrics is not None:
245
483
  await self._metrics.setup(self._task_manager)
246
484
 
247
485
  async def cleanup(self):
486
+ """Clean up processor resources."""
248
487
  await super().cleanup()
249
488
  await self.__cancel_input_task()
250
- await self.__cancel_push_task()
489
+ await self.__cancel_process_task()
251
490
  if self._metrics is not None:
252
491
  await self._metrics.cleanup()
253
492
 
254
493
  def link(self, processor: "FrameProcessor"):
494
+ """Link this processor to the next processor in the pipeline.
495
+
496
+ Args:
497
+ processor: The processor to link to.
498
+ """
255
499
  self._next = processor
256
500
  processor._prev = self
257
501
  self.logger.debug(f"Linking {self} -> {self._next}")
258
502
 
259
- def get_event_loop(self) -> asyncio.AbstractEventLoop:
260
- return self.task_manager.get_event_loop()
261
-
262
- def set_parent(self, parent: "FrameProcessor"):
263
- self._parent = parent
503
+ def get_clock(self) -> BaseClock:
504
+ """Get the clock used by this processor.
264
505
 
265
- def get_parent(self) -> Optional["FrameProcessor"]:
266
- return self._parent
506
+ Returns:
507
+ The clock instance.
267
508
 
268
- def get_clock(self) -> BaseClock:
509
+ Raises:
510
+ Exception: If the clock is not initialized.
511
+ """
269
512
  if not self._clock:
270
513
  raise Exception(f"{self} Clock is still not initialized.")
271
514
  return self._clock
272
515
 
516
+ def get_event_loop(self) -> asyncio.AbstractEventLoop:
517
+ """Get the event loop used by this processor.
518
+
519
+ Returns:
520
+ The asyncio event loop.
521
+ """
522
+ return self.task_manager.get_event_loop()
523
+
273
524
  async def queue_frame(
274
525
  self,
275
526
  frame: Frame,
276
527
  direction: FrameDirection = FrameDirection.DOWNSTREAM,
277
- callback: Optional[
278
- Callable[["FrameProcessor", Frame, FrameDirection], Awaitable[None]]
279
- ] = None,
528
+ callback: Optional[FrameCallback] = None,
280
529
  ):
530
+ """Queue a frame for processing.
531
+
532
+ Args:
533
+ frame: The frame to queue.
534
+ direction: The direction of frame flow.
535
+ callback: Optional callback to call after processing.
536
+ """
281
537
  # If we are cancelling we don't want to process any other frame.
282
538
  if self._cancelling:
283
539
  return
284
540
 
285
- if isinstance(frame, SystemFrame):
286
- # We don't want to queue system frames.
287
- await self.process_frame(frame, direction)
541
+ if self._enable_direct_mode:
542
+ await self.__process_frame(frame, direction, callback)
288
543
  else:
289
- # We queue everything else.
290
544
  await self.__input_queue.put((frame, direction, callback))
291
545
 
292
546
  async def pause_processing_frames(self):
547
+ """Pause processing of queued frames."""
293
548
  self.logger.trace(f"{self}: pausing frame processing")
294
549
  self.__should_block_frames = True
295
550
 
551
+ async def pause_processing_system_frames(self):
552
+ """Pause processing of queued system frames."""
553
+ logger.trace(f"{self}: pausing system frame processing")
554
+ self.__should_block_system_frames = True
555
+
296
556
  async def resume_processing_frames(self):
557
+ """Resume processing of queued frames."""
297
558
  self.logger.trace(f"{self}: resuming frame processing")
559
+ if self.__process_event:
560
+ self.__process_event.set()
561
+
562
+ async def resume_processing_system_frames(self):
563
+ """Resume processing of queued system frames."""
564
+ self.logger.trace(f"{self}: resuming system frame processing")
298
565
  if self.__input_event:
299
566
  self.__input_event.set()
300
567
 
301
568
  async def process_frame(self, frame: Frame, direction: FrameDirection):
569
+ """Process a frame.
570
+
571
+ Args:
572
+ frame: The frame to process.
573
+ direction: The direction of frame flow.
574
+ """
575
+ if self._observer:
576
+ timestamp = self._clock.get_time() if self._clock else 0
577
+ data = FrameProcessed(
578
+ processor=self,
579
+ frame=frame,
580
+ direction=direction,
581
+ timestamp=timestamp,
582
+ )
583
+ await self._observer.on_process_frame(data)
584
+
302
585
  if isinstance(frame, StartFrame):
303
586
  await self.__start(frame)
304
587
  elif isinstance(frame, StartInterruptionFrame):
@@ -314,18 +597,33 @@ class FrameProcessor(BaseObject):
314
597
  await self.__resume(frame)
315
598
 
316
599
  async def push_error(self, error: ErrorFrame):
600
+ """Push an error frame upstream.
601
+
602
+ Args:
603
+ error: The error frame to push.
604
+ """
605
+ if not error.processor:
606
+ error.processor = self
317
607
  await self.push_frame(error, FrameDirection.UPSTREAM)
318
608
 
319
609
  async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
610
+ """Push a frame to the next processor in the pipeline.
611
+
612
+ Args:
613
+ frame: The frame to push.
614
+ direction: The direction to push the frame.
615
+ """
320
616
  if not self._check_started(frame):
321
617
  return
322
618
 
323
- if isinstance(frame, SystemFrame):
324
- await self.__internal_push_frame(frame, direction)
325
- else:
326
- await self.__push_queue.put((frame, direction))
619
+ await self.__internal_push_frame(frame, direction)
327
620
 
328
621
  async def __start(self, frame: StartFrame):
622
+ """Handle the start frame to initialize processor state.
623
+
624
+ Args:
625
+ frame: The start frame containing initialization parameters.
626
+ """
329
627
  self.__started = True
330
628
  self._allow_interruptions = frame.allow_interruptions
331
629
  self._enable_metrics = frame.enable_metrics
@@ -336,19 +634,32 @@ class FrameProcessor(BaseObject):
336
634
  self.logger = logger.bind(call_id=frame.metadata["call_id"])
337
635
  self._metrics.set_logger(self.logger)
338
636
  self._report_only_initial_ttfb = frame.report_only_initial_ttfb
339
- self.__create_input_task()
340
- self.__create_push_task()
637
+ self.__create_process_task()
341
638
 
342
639
  async def __cancel(self, frame: CancelFrame):
640
+ """Handle the cancel frame to stop processor operation.
641
+
642
+ Args:
643
+ frame: The cancel frame.
644
+ """
343
645
  self._cancelling = True
344
- await self.__cancel_input_task()
345
- await self.__cancel_push_task()
646
+ await self.__cancel_process_task()
346
647
 
347
648
  async def __pause(self, frame: FrameProcessorPauseFrame | FrameProcessorPauseUrgentFrame):
649
+ """Handle pause frame to pause processor operation.
650
+
651
+ Args:
652
+ frame: The pause frame.
653
+ """
348
654
  if frame.processor.name == self.name:
349
655
  await self.pause_processing_frames()
350
656
 
351
657
  async def __resume(self, frame: FrameProcessorResumeFrame | FrameProcessorResumeUrgentFrame):
658
+ """Handle resume frame to resume processor operation.
659
+
660
+ Args:
661
+ frame: The resume frame.
662
+ """
352
663
  if frame.processor.name == self.name:
353
664
  await self.resume_processing_frames()
354
665
 
@@ -357,29 +668,31 @@ class FrameProcessor(BaseObject):
357
668
  #
358
669
 
359
670
  async def _start_interruption(self):
671
+ """Start handling an interruption by cancelling current tasks."""
360
672
  try:
361
- # Cancel the push frame task. This will stop pushing frames downstream.
362
- await self.__cancel_push_task()
363
-
364
- # Cancel the input task. This will stop processing queued frames.
365
- await self.__cancel_input_task()
673
+ # Cancel the process task. This will stop processing queued frames.
674
+ await self.__cancel_process_task()
366
675
  except Exception as e:
367
676
  self.logger.exception(
368
677
  f"Uncaught exception in {self} when handling _start_interruption: {e}"
369
678
  )
370
679
  await self.push_error(ErrorFrame(str(e)))
371
680
 
372
- # Create a new input queue and task.
373
- self.__create_input_task()
374
-
375
- # Create a new output queue and task.
376
- self.__create_push_task()
681
+ # Create a new process queue and task.
682
+ self.__create_process_task()
377
683
 
378
684
  async def _stop_interruption(self):
685
+ """Stop handling an interruption."""
379
686
  # Nothing to do right now.
380
687
  pass
381
688
 
382
689
  async def __internal_push_frame(self, frame: Frame, direction: FrameDirection):
690
+ """Internal method to push frames to adjacent processors.
691
+
692
+ Args:
693
+ frame: The frame to push.
694
+ direction: The direction to push the frame.
695
+ """
383
696
  try:
384
697
  timestamp = self._clock.get_time() if self._clock else 0
385
698
  if direction == FrameDirection.DOWNSTREAM and self._next:
@@ -415,60 +728,104 @@ class FrameProcessor(BaseObject):
415
728
  await self.push_error(ErrorFrame(str(e)))
416
729
 
417
730
  def _check_started(self, frame: Frame):
731
+ """Check if the processor has been started.
732
+
733
+ Args:
734
+ frame: The frame being processed.
735
+
736
+ Returns:
737
+ True if the processor has been started.
738
+ """
418
739
  if not self.__started:
419
740
  logger.error(f"{self} Trying to process {frame} but StartFrame not received yet")
420
741
  return self.__started
421
742
 
422
743
  def __create_input_task(self):
744
+ """Create the frame input processing task."""
745
+ if self._enable_direct_mode:
746
+ return
747
+
423
748
  if not self.__input_frame_task:
424
- self.__should_block_frames = False
425
- if not self.__input_event:
426
- self.__input_event = WatchdogEvent(self.task_manager)
427
- self.__input_event.clear()
428
- self.__input_queue = WatchdogQueue(self.task_manager)
749
+ self.__input_event = asyncio.Event()
750
+ self.__input_queue = FrameProcessorQueue()
429
751
  self.__input_frame_task = self.create_task(self.__input_frame_task_handler())
430
752
 
431
753
  async def __cancel_input_task(self):
754
+ """Cancel the frame input processing task."""
432
755
  if self.__input_frame_task:
433
756
  await self.cancel_task(self.__input_frame_task)
434
757
  self.__input_frame_task = None
435
758
 
759
+ def __create_process_task(self):
760
+ """Create the non-system frame processing task."""
761
+ if self._enable_direct_mode:
762
+ return
763
+
764
+ if not self.__process_frame_task:
765
+ self.__should_block_frames = False
766
+ self.__process_event = asyncio.Event()
767
+ self.__process_queue = asyncio.Queue()
768
+ self.__process_frame_task = self.create_task(self.__process_frame_task_handler())
769
+
770
+ async def __cancel_process_task(self):
771
+ """Cancel the non-system frame processing task."""
772
+ if self.__process_frame_task:
773
+ await self.cancel_task(self.__process_frame_task)
774
+ self.__process_frame_task = None
775
+
776
+ async def __process_frame(
777
+ self, frame: Frame, direction: FrameDirection, callback: Optional[FrameCallback]
778
+ ):
779
+ try:
780
+ # Process the frame.
781
+ await self.process_frame(frame, direction)
782
+ # If this frame has an associated callback, call it now.
783
+ if callback:
784
+ await callback(self, frame, direction)
785
+ except Exception as e:
786
+ logger.exception(f"{self}: error processing frame: {e}")
787
+ await self.push_error(ErrorFrame(str(e)))
788
+
436
789
  async def __input_frame_task_handler(self):
437
- """Handle frames from the input queue."""
438
- my_queue = self.__input_queue
790
+ """Handle frames from the input queue.
791
+
792
+ It only processes system frames. Other frames are queue for another task
793
+ to execute.
794
+
795
+ """
439
796
  while True:
440
- if self.__should_block_frames and self.__input_event:
441
- logger.trace(f"{self}: frame processing paused")
797
+ if self.__should_block_system_frames and self.__input_event:
798
+ logger.trace(f"{self}: system frame processing paused")
442
799
  await self.__input_event.wait()
443
800
  self.__input_event.clear()
801
+ self.__should_block_system_frames = False
802
+ logger.trace(f"{self}: system frame processing resumed")
803
+
804
+ (frame, direction, callback) = await self.__input_queue.get()
805
+
806
+ if isinstance(frame, SystemFrame):
807
+ await self.__process_frame(frame, direction, callback)
808
+ elif self.__process_queue:
809
+ await self.__process_queue.put((frame, direction, callback))
810
+ else:
811
+ raise RuntimeError(
812
+ f"{self}: __process_queue is None when processing frame {frame.name}"
813
+ )
814
+
815
+ self.__input_queue.task_done()
816
+
817
+ async def __process_frame_task_handler(self):
818
+ """Handle non-system frames from the process queue."""
819
+ while True:
820
+ if self.__should_block_frames and self.__process_event:
821
+ logger.trace(f"{self}: frame processing paused")
822
+ await self.__process_event.wait()
823
+ self.__process_event.clear()
444
824
  self.__should_block_frames = False
445
825
  logger.trace(f"{self}: frame processing resumed")
446
826
 
447
- (frame, direction, callback) = await my_queue.get()
448
- try:
449
- # Process the frame.
450
- await self.process_frame(frame, direction)
451
- # If this frame has an associated callback, call it now.
452
- if callback:
453
- await callback(self, frame, direction)
454
- except Exception as e:
455
- self.logger.exception(f"{self}: error processing frame: {e}")
456
- await self.push_error(ErrorFrame(str(e)))
457
- finally:
458
- my_queue.task_done()
459
-
460
- def __create_push_task(self):
461
- if not self.__push_frame_task:
462
- self.__push_queue = WatchdogQueue(self.task_manager)
463
- self.__push_frame_task = self.create_task(self.__push_frame_task_handler())
464
-
465
- async def __cancel_push_task(self):
466
- if self.__push_frame_task:
467
- await self.cancel_task(self.__push_frame_task)
468
- self.__push_frame_task = None
469
-
470
- async def __push_frame_task_handler(self):
471
- while True:
472
- (frame, direction) = await self.__push_queue.get()
473
- await self.__internal_push_frame(frame, direction)
474
- self.__push_queue.task_done()
827
+ (frame, direction, callback) = await self.__process_queue.get()
828
+
829
+ await self.__process_frame(frame, direction, callback)
830
+
831
+ self.__process_queue.task_done()