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
@@ -9,11 +9,22 @@
9
9
  import asyncio
10
10
  import inspect
11
11
  from dataclasses import dataclass
12
- from typing import Any, Awaitable, Callable, Dict, Mapping, Optional, Protocol, Sequence, Type
12
+ from typing import (
13
+ Any,
14
+ Awaitable,
15
+ Callable,
16
+ Dict,
17
+ Mapping,
18
+ Optional,
19
+ Protocol,
20
+ Sequence,
21
+ Type,
22
+ )
13
23
 
14
24
  from loguru import logger
15
25
 
16
26
  from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
27
+ from pipecat.adapters.schemas.direct_function import DirectFunction, DirectFunctionWrapper
17
28
  from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter
18
29
  from pipecat.frames.frames import (
19
30
  CancelFrame,
@@ -94,8 +105,9 @@ class FunctionCallRegistryItem:
94
105
  """
95
106
 
96
107
  function_name: Optional[str]
97
- handler: FunctionCallHandler
108
+ handler: FunctionCallHandler | "DirectFunctionWrapper"
98
109
  cancel_on_interruption: bool
110
+ handler_deprecated: bool
99
111
 
100
112
 
101
113
  @dataclass
@@ -128,18 +140,14 @@ class LLMService(AIService):
128
140
  parallel and sequential execution modes. Provides event handlers for
129
141
  completion timeouts and function call lifecycle events.
130
142
 
131
- Args:
132
- run_in_parallel: Whether to run function calls in parallel or sequentially.
133
- Defaults to True.
134
- **kwargs: Additional arguments passed to the parent AIService.
143
+ The service supports the following event handlers:
144
+
145
+ - on_completion_timeout: Called when an LLM completion timeout occurs
146
+ - on_function_calls_started: Called when function calls are received and
147
+ execution is about to start
135
148
 
136
- Event handlers:
137
- on_completion_timeout: Called when an LLM completion timeout occurs.
138
- on_function_calls_started: Called when function calls are received and
139
- execution is about to start.
149
+ Example::
140
150
 
141
- Example:
142
- ```python
143
151
  @task.event_handler("on_completion_timeout")
144
152
  async def on_completion_timeout(service):
145
153
  logger.warning("LLM completion timed out")
@@ -147,7 +155,6 @@ class LLMService(AIService):
147
155
  @task.event_handler("on_function_calls_started")
148
156
  async def on_function_calls_started(service, function_calls):
149
157
  logger.info(f"Starting {len(function_calls)} function calls")
150
- ```
151
158
  """
152
159
 
153
160
  # OpenAILLMAdapter is used as the default adapter since it aligns with most LLM implementations.
@@ -155,6 +162,13 @@ class LLMService(AIService):
155
162
  adapter_class: Type[BaseLLMAdapter] = OpenAILLMAdapter
156
163
 
157
164
  def __init__(self, run_in_parallel: bool = True, **kwargs):
165
+ """Initialize the LLM service.
166
+
167
+ Args:
168
+ run_in_parallel: Whether to run function calls in parallel or sequentially.
169
+ Defaults to True.
170
+ **kwargs: Additional arguments passed to the parent AIService.
171
+ """
158
172
  super().__init__(**kwargs)
159
173
  self._run_in_parallel = run_in_parallel
160
174
  self._start_callbacks = {}
@@ -162,6 +176,7 @@ class LLMService(AIService):
162
176
  self._functions: Dict[Optional[str], FunctionCallRegistryItem] = {}
163
177
  self._function_call_tasks: Dict[asyncio.Task, FunctionCallRunnerItem] = {}
164
178
  self._sequential_runner_task: Optional[asyncio.Task] = None
179
+ self._tracing_enabled: bool = False
165
180
 
166
181
  self._register_event_handler("on_function_calls_started")
167
182
  self._register_event_handler("on_completion_timeout")
@@ -204,6 +219,7 @@ class LLMService(AIService):
204
219
  await super().start(frame)
205
220
  if not self._run_in_parallel:
206
221
  await self._create_sequential_runner_task()
222
+ self._tracing_enabled = frame.enable_tracing
207
223
 
208
224
  async def stop(self, frame: EndFrame):
209
225
  """Stop the LLM service.
@@ -238,9 +254,11 @@ class LLMService(AIService):
238
254
  await self._handle_interruptions(frame)
239
255
 
240
256
  async def _handle_interruptions(self, _: StartInterruptionFrame):
257
+ # logger.info("In LLM Handling interruptions")
241
258
  for function_name, entry in self._functions.items():
242
259
  if entry.cancel_on_interruption:
243
260
  await self._cancel_function_call(function_name)
261
+ # logger.info("in LLM Interruptions handled")
244
262
 
245
263
  def register_function(
246
264
  self,
@@ -259,15 +277,32 @@ class LLMService(AIService):
259
277
  parameter.
260
278
  start_callback: Legacy callback function (deprecated). Put initialization
261
279
  code at the top of your handler instead.
280
+
281
+ .. deprecated:: 0.0.59
282
+ The `start_callback` parameter is deprecated and will be removed in a future version.
283
+
262
284
  cancel_on_interruption: Whether to cancel this function call when an
263
285
  interruption occurs. Defaults to True.
264
286
  """
287
+ signature = inspect.signature(handler)
288
+ handler_deprecated = len(signature.parameters) > 1
289
+ if handler_deprecated:
290
+ import warnings
291
+
292
+ with warnings.catch_warnings():
293
+ warnings.simplefilter("always")
294
+ warnings.warn(
295
+ "Function calls with parameters `(function_name, tool_call_id, arguments, llm, context, result_callback)` are deprecated, use a single `FunctionCallParams` parameter instead.",
296
+ DeprecationWarning,
297
+ )
298
+
265
299
  # Registering a function with the function_name set to None will run
266
300
  # that handler for all functions
267
301
  self._functions[function_name] = FunctionCallRegistryItem(
268
302
  function_name=function_name,
269
303
  handler=handler,
270
304
  cancel_on_interruption=cancel_on_interruption,
305
+ handler_deprecated=handler_deprecated,
271
306
  )
272
307
 
273
308
  # Start callbacks are now deprecated.
@@ -283,6 +318,31 @@ class LLMService(AIService):
283
318
 
284
319
  self._start_callbacks[function_name] = start_callback
285
320
 
321
+ def register_direct_function(
322
+ self,
323
+ handler: DirectFunction,
324
+ *,
325
+ cancel_on_interruption: bool = True,
326
+ ):
327
+ """Register a direct function handler for LLM function calls.
328
+
329
+ Direct functions have their metadata automatically extracted from their
330
+ signature and docstring, eliminating the need for accompanying
331
+ configurations (as FunctionSchemas or in provider-specific formats).
332
+
333
+ Args:
334
+ handler: The direct function to register. Must follow DirectFunction protocol.
335
+ cancel_on_interruption: Whether to cancel this function call when an
336
+ interruption occurs. Defaults to True.
337
+ """
338
+ wrapper = DirectFunctionWrapper(handler)
339
+ self._functions[wrapper.name] = FunctionCallRegistryItem(
340
+ function_name=wrapper.name,
341
+ handler=wrapper,
342
+ cancel_on_interruption=cancel_on_interruption,
343
+ handler_deprecated=False,
344
+ )
345
+
286
346
  def unregister_function(self, function_name: Optional[str]):
287
347
  """Remove a registered function handler.
288
348
 
@@ -293,6 +353,16 @@ class LLMService(AIService):
293
353
  if self._start_callbacks[function_name]:
294
354
  del self._start_callbacks[function_name]
295
355
 
356
+ def unregister_direct_function(self, handler: Any):
357
+ """Remove a registered direct function handler.
358
+
359
+ Args:
360
+ handler: The direct function handler to remove.
361
+ """
362
+ wrapper = DirectFunctionWrapper(handler)
363
+ del self._functions[wrapper.name]
364
+ # Note: no need to remove start callback here, as direct functions don't support start callbacks.
365
+
296
366
  def has_function(self, function_name: str):
297
367
  """Check if a function handler is registered.
298
368
 
@@ -307,6 +377,17 @@ class LLMService(AIService):
307
377
  return True
308
378
  return function_name in self._functions.keys()
309
379
 
380
+ def needs_mcp_alternate_schema(self) -> bool:
381
+ """Check if this LLM service requires alternate MCP schema.
382
+
383
+ Some LLM services have stricter JSON schema validation and require
384
+ certain properties to be removed or modified for compatibility.
385
+
386
+ Returns:
387
+ True if MCP schemas should be cleaned for this service, False otherwise.
388
+ """
389
+ return False
390
+
310
391
  async def run_function_calls(self, function_calls: Sequence[FunctionCallFromLLM]):
311
392
  """Execute a sequence of function calls from the LLM.
312
393
 
@@ -408,7 +489,7 @@ class LLMService(AIService):
408
489
  self._function_call_tasks[task] = runner_item
409
490
  # Since we run tasks sequentially we don't need to call
410
491
  # task.add_done_callback(self._function_call_task_finished).
411
- await self.wait_for_task(task)
492
+ await task
412
493
  del self._function_call_tasks[task]
413
494
 
414
495
  async def _run_function_call(self, runner_item: FunctionCallRunnerItem):
@@ -472,35 +553,40 @@ class LLMService(AIService):
472
553
  await self.push_frame(result_frame_downstream, FrameDirection.DOWNSTREAM)
473
554
  await self.push_frame(result_frame_upstream, FrameDirection.UPSTREAM)
474
555
 
475
- signature = inspect.signature(item.handler)
476
- if len(signature.parameters) > 1:
477
- import warnings
478
-
479
- with warnings.catch_warnings():
480
- warnings.simplefilter("always")
481
- warnings.warn(
482
- "Function calls with parameters `(function_name, tool_call_id, arguments, llm, context, result_callback)` are deprecated, use a single `FunctionCallParams` parameter instead.",
483
- DeprecationWarning,
484
- )
485
-
486
- await item.handler(
487
- runner_item.function_name,
488
- runner_item.tool_call_id,
489
- runner_item.arguments,
490
- self,
491
- runner_item.context,
492
- function_call_result_callback,
556
+ if isinstance(item.handler, DirectFunctionWrapper):
557
+ # Handler is a DirectFunctionWrapper
558
+ await item.handler.invoke(
559
+ args=runner_item.arguments,
560
+ params=FunctionCallParams(
561
+ function_name=runner_item.function_name,
562
+ tool_call_id=runner_item.tool_call_id,
563
+ arguments=runner_item.arguments,
564
+ llm=self,
565
+ context=runner_item.context,
566
+ result_callback=function_call_result_callback,
567
+ ),
493
568
  )
494
569
  else:
495
- params = FunctionCallParams(
496
- function_name=runner_item.function_name,
497
- tool_call_id=runner_item.tool_call_id,
498
- arguments=runner_item.arguments,
499
- llm=self,
500
- context=runner_item.context,
501
- result_callback=function_call_result_callback,
502
- )
503
- await item.handler(params)
570
+ # Handler is a FunctionCallHandler
571
+ if item.handler_deprecated:
572
+ await item.handler(
573
+ runner_item.function_name,
574
+ runner_item.tool_call_id,
575
+ runner_item.arguments,
576
+ self,
577
+ runner_item.context,
578
+ function_call_result_callback,
579
+ )
580
+ else:
581
+ params = FunctionCallParams(
582
+ function_name=runner_item.function_name,
583
+ tool_call_id=runner_item.tool_call_id,
584
+ arguments=runner_item.arguments,
585
+ llm=self,
586
+ context=runner_item.context,
587
+ result_callback=function_call_result_callback,
588
+ )
589
+ await item.handler(params)
504
590
 
505
591
  async def _cancel_function_call(self, function_name: Optional[str]):
506
592
  cancelled_tasks = set()
@@ -533,7 +619,3 @@ class LLMService(AIService):
533
619
  def _function_call_task_finished(self, task: asyncio.Task):
534
620
  if task in self._function_call_tasks:
535
621
  del self._function_call_tasks[task]
536
- # The task is finished so this should exit immediately. We need to
537
- # do this because otherwise the task manager would report a dangling
538
- # task if we don't remove it.
539
- asyncio.run_coroutine_threadsafe(self.wait_for_task(task), self.get_event_loop())
@@ -4,6 +4,8 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
+ """LMNT text-to-speech service implementation."""
8
+
7
9
  import json
8
10
  from typing import AsyncGenerator, Optional
9
11
 
@@ -27,7 +29,8 @@ from pipecat.utils.tracing.service_decorators import traced_tts
27
29
 
28
30
  # See .env.example for LMNT configuration needed
29
31
  try:
30
- import websockets
32
+ from websockets.asyncio.client import connect as websocket_connect
33
+ from websockets.protocol import State
31
34
  except ModuleNotFoundError as e:
32
35
  logger.error(f"Exception: {e}")
33
36
  logger.error("In order to use LMNT, you need to `pip install pipecat-ai[lmnt]`.")
@@ -35,6 +38,14 @@ except ModuleNotFoundError as e:
35
38
 
36
39
 
37
40
  def language_to_lmnt_language(language: Language) -> Optional[str]:
41
+ """Convert a Language enum to LMNT language code.
42
+
43
+ Args:
44
+ language: The Language enum value to convert.
45
+
46
+ Returns:
47
+ The corresponding LMNT language code, or None if not supported.
48
+ """
38
49
  BASE_LANGUAGES = {
39
50
  Language.DE: "de",
40
51
  Language.EN: "en",
@@ -71,6 +82,13 @@ def language_to_lmnt_language(language: Language) -> Optional[str]:
71
82
 
72
83
 
73
84
  class LmntTTSService(InterruptibleTTSService):
85
+ """LMNT real-time text-to-speech service.
86
+
87
+ Provides real-time text-to-speech synthesis using LMNT's WebSocket API.
88
+ Supports streaming audio generation with configurable voice models and
89
+ language settings.
90
+ """
91
+
74
92
  def __init__(
75
93
  self,
76
94
  *,
@@ -78,9 +96,19 @@ class LmntTTSService(InterruptibleTTSService):
78
96
  voice_id: str,
79
97
  sample_rate: Optional[int] = None,
80
98
  language: Language = Language.EN,
81
- model: str = "aurora",
99
+ model: str = "blizzard",
82
100
  **kwargs,
83
101
  ):
102
+ """Initialize the LMNT TTS service.
103
+
104
+ Args:
105
+ api_key: LMNT API key for authentication.
106
+ voice_id: ID of the voice to use for synthesis.
107
+ sample_rate: Audio sample rate. If None, uses default.
108
+ language: Language for synthesis. Defaults to English.
109
+ model: TTS model to use. Defaults to "blizzard".
110
+ **kwargs: Additional arguments passed to parent InterruptibleTTSService.
111
+ """
84
112
  super().__init__(
85
113
  push_stop_frames=True,
86
114
  pause_frame_processing=True,
@@ -99,35 +127,71 @@ class LmntTTSService(InterruptibleTTSService):
99
127
  self._receive_task = None
100
128
 
101
129
  def can_generate_metrics(self) -> bool:
130
+ """Check if this service can generate processing metrics.
131
+
132
+ Returns:
133
+ True, as LMNT service supports metrics generation.
134
+ """
102
135
  return True
103
136
 
104
137
  def language_to_service_language(self, language: Language) -> Optional[str]:
138
+ """Convert a Language enum to LMNT service language format.
139
+
140
+ Args:
141
+ language: The language to convert.
142
+
143
+ Returns:
144
+ The LMNT-specific language code, or None if not supported.
145
+ """
105
146
  return language_to_lmnt_language(language)
106
147
 
107
148
  async def start(self, frame: StartFrame):
149
+ """Start the LMNT TTS service.
150
+
151
+ Args:
152
+ frame: The start frame containing initialization parameters.
153
+ """
108
154
  await super().start(frame)
109
155
  await self._connect()
110
156
 
111
157
  async def stop(self, frame: EndFrame):
158
+ """Stop the LMNT TTS service.
159
+
160
+ Args:
161
+ frame: The end frame.
162
+ """
112
163
  await super().stop(frame)
113
164
  await self._disconnect()
114
165
 
115
166
  async def cancel(self, frame: CancelFrame):
167
+ """Cancel the LMNT TTS service.
168
+
169
+ Args:
170
+ frame: The cancel frame.
171
+ """
116
172
  await super().cancel(frame)
117
173
  await self._disconnect()
118
174
 
119
175
  async def push_frame(self, frame: Frame, direction: FrameDirection = FrameDirection.DOWNSTREAM):
176
+ """Push a frame downstream with special handling for stop conditions.
177
+
178
+ Args:
179
+ frame: The frame to push.
180
+ direction: The direction to push the frame.
181
+ """
120
182
  await super().push_frame(frame, direction)
121
183
  if isinstance(frame, (TTSStoppedFrame, StartInterruptionFrame)):
122
184
  self._started = False
123
185
 
124
186
  async def _connect(self):
187
+ """Connect to LMNT WebSocket and start receive task."""
125
188
  await self._connect_websocket()
126
189
 
127
190
  if self._websocket and not self._receive_task:
128
191
  self._receive_task = self.create_task(self._receive_task_handler(self._report_error))
129
192
 
130
193
  async def _disconnect(self):
194
+ """Disconnect from LMNT WebSocket and clean up tasks."""
131
195
  if self._receive_task:
132
196
  await self.cancel_task(self._receive_task)
133
197
  self._receive_task = None
@@ -137,7 +201,7 @@ class LmntTTSService(InterruptibleTTSService):
137
201
  async def _connect_websocket(self):
138
202
  """Connect to LMNT websocket."""
139
203
  try:
140
- if self._websocket and self._websocket.open:
204
+ if self._websocket and self._websocket.state is State.OPEN:
141
205
  return
142
206
 
143
207
  logger.debug("Connecting to LMNT")
@@ -153,7 +217,7 @@ class LmntTTSService(InterruptibleTTSService):
153
217
  }
154
218
 
155
219
  # Connect to LMNT's websocket directly
156
- self._websocket = await websockets.connect("wss://api.lmnt.com/v1/ai/speech/stream")
220
+ self._websocket = await websocket_connect("wss://api.lmnt.com/v1/ai/speech/stream")
157
221
 
158
222
  # Send initialization message
159
223
  await self._websocket.send(json.dumps(init_msg))
@@ -181,12 +245,14 @@ class LmntTTSService(InterruptibleTTSService):
181
245
  self._websocket = None
182
246
 
183
247
  def _get_websocket(self):
248
+ """Get the WebSocket connection if available."""
184
249
  if self._websocket:
185
250
  return self._websocket
186
251
  raise Exception("Websocket not connected")
187
252
 
188
253
  async def flush_audio(self):
189
- if not self._websocket or self._websocket.closed:
254
+ """Flush any pending audio synthesis."""
255
+ if not self._websocket or self._websocket.state is State.CLOSED:
190
256
  return
191
257
  await self._get_websocket().send(json.dumps({"flush": True}))
192
258
 
@@ -216,11 +282,18 @@ class LmntTTSService(InterruptibleTTSService):
216
282
 
217
283
  @traced_tts
218
284
  async def run_tts(self, text: str) -> AsyncGenerator[Frame, None]:
219
- """Generate TTS audio from text."""
285
+ """Generate TTS audio from text using LMNT's streaming API.
286
+
287
+ Args:
288
+ text: The text to synthesize into speech.
289
+
290
+ Yields:
291
+ Frame: Audio frames containing the synthesized speech.
292
+ """
220
293
  logger.debug(f"{self}: Generating TTS [{text}]")
221
294
 
222
295
  try:
223
- if not self._websocket or self._websocket.closed:
296
+ if not self._websocket or self._websocket.state is State.CLOSED:
224
297
  await self._connect()
225
298
 
226
299
  try: