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.
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/METADATA +137 -93
- dv_pipecat_ai-0.0.82.dev776.dist-info/RECORD +340 -0
- pipecat/__init__.py +17 -0
- pipecat/adapters/base_llm_adapter.py +36 -1
- pipecat/adapters/schemas/direct_function.py +296 -0
- pipecat/adapters/schemas/function_schema.py +15 -6
- pipecat/adapters/schemas/tools_schema.py +55 -7
- pipecat/adapters/services/anthropic_adapter.py +22 -3
- pipecat/adapters/services/aws_nova_sonic_adapter.py +23 -3
- pipecat/adapters/services/bedrock_adapter.py +22 -3
- pipecat/adapters/services/gemini_adapter.py +16 -3
- pipecat/adapters/services/open_ai_adapter.py +17 -2
- pipecat/adapters/services/open_ai_realtime_adapter.py +23 -3
- pipecat/audio/filters/base_audio_filter.py +30 -6
- pipecat/audio/filters/koala_filter.py +37 -2
- pipecat/audio/filters/krisp_filter.py +59 -6
- pipecat/audio/filters/noisereduce_filter.py +37 -0
- pipecat/audio/interruptions/base_interruption_strategy.py +25 -5
- pipecat/audio/interruptions/min_words_interruption_strategy.py +21 -4
- pipecat/audio/mixers/base_audio_mixer.py +30 -7
- pipecat/audio/mixers/soundfile_mixer.py +53 -6
- pipecat/audio/resamplers/base_audio_resampler.py +17 -9
- pipecat/audio/resamplers/resampy_resampler.py +26 -1
- pipecat/audio/resamplers/soxr_resampler.py +32 -1
- pipecat/audio/resamplers/soxr_stream_resampler.py +101 -0
- pipecat/audio/utils.py +194 -1
- pipecat/audio/vad/silero.py +60 -3
- pipecat/audio/vad/vad_analyzer.py +114 -30
- pipecat/clocks/base_clock.py +19 -0
- pipecat/clocks/system_clock.py +25 -0
- pipecat/extensions/voicemail/__init__.py +0 -0
- pipecat/extensions/voicemail/voicemail_detector.py +707 -0
- pipecat/frames/frames.py +590 -156
- pipecat/metrics/metrics.py +64 -1
- pipecat/observers/base_observer.py +58 -19
- pipecat/observers/loggers/debug_log_observer.py +56 -64
- pipecat/observers/loggers/llm_log_observer.py +8 -1
- pipecat/observers/loggers/transcription_log_observer.py +19 -7
- pipecat/observers/loggers/user_bot_latency_log_observer.py +32 -5
- pipecat/observers/turn_tracking_observer.py +26 -1
- pipecat/pipeline/base_pipeline.py +5 -7
- pipecat/pipeline/base_task.py +52 -9
- pipecat/pipeline/parallel_pipeline.py +121 -177
- pipecat/pipeline/pipeline.py +129 -20
- pipecat/pipeline/runner.py +50 -1
- pipecat/pipeline/sync_parallel_pipeline.py +132 -32
- pipecat/pipeline/task.py +263 -280
- pipecat/pipeline/task_observer.py +85 -34
- pipecat/pipeline/to_be_updated/merge_pipeline.py +32 -2
- pipecat/processors/aggregators/dtmf_aggregator.py +29 -22
- pipecat/processors/aggregators/gated.py +25 -24
- pipecat/processors/aggregators/gated_openai_llm_context.py +22 -2
- pipecat/processors/aggregators/llm_response.py +398 -89
- pipecat/processors/aggregators/openai_llm_context.py +161 -13
- pipecat/processors/aggregators/sentence.py +25 -14
- pipecat/processors/aggregators/user_response.py +28 -3
- pipecat/processors/aggregators/vision_image_frame.py +24 -14
- pipecat/processors/async_generator.py +28 -0
- pipecat/processors/audio/audio_buffer_processor.py +78 -37
- pipecat/processors/consumer_processor.py +25 -6
- pipecat/processors/filters/frame_filter.py +23 -0
- pipecat/processors/filters/function_filter.py +30 -0
- pipecat/processors/filters/identity_filter.py +17 -2
- pipecat/processors/filters/null_filter.py +24 -1
- pipecat/processors/filters/stt_mute_filter.py +56 -21
- pipecat/processors/filters/wake_check_filter.py +46 -3
- pipecat/processors/filters/wake_notifier_filter.py +21 -3
- pipecat/processors/frame_processor.py +488 -131
- pipecat/processors/frameworks/langchain.py +38 -3
- pipecat/processors/frameworks/rtvi.py +719 -34
- pipecat/processors/gstreamer/pipeline_source.py +41 -0
- pipecat/processors/idle_frame_processor.py +26 -3
- pipecat/processors/logger.py +23 -0
- pipecat/processors/metrics/frame_processor_metrics.py +77 -4
- pipecat/processors/metrics/sentry.py +42 -4
- pipecat/processors/producer_processor.py +34 -14
- pipecat/processors/text_transformer.py +22 -10
- pipecat/processors/transcript_processor.py +48 -29
- pipecat/processors/user_idle_processor.py +31 -21
- pipecat/runner/__init__.py +1 -0
- pipecat/runner/daily.py +132 -0
- pipecat/runner/livekit.py +148 -0
- pipecat/runner/run.py +543 -0
- pipecat/runner/types.py +67 -0
- pipecat/runner/utils.py +515 -0
- pipecat/serializers/base_serializer.py +42 -0
- pipecat/serializers/exotel.py +17 -6
- pipecat/serializers/genesys.py +95 -0
- pipecat/serializers/livekit.py +33 -0
- pipecat/serializers/plivo.py +16 -15
- pipecat/serializers/protobuf.py +37 -1
- pipecat/serializers/telnyx.py +18 -17
- pipecat/serializers/twilio.py +32 -16
- pipecat/services/ai_service.py +5 -3
- pipecat/services/anthropic/llm.py +113 -43
- pipecat/services/assemblyai/models.py +63 -5
- pipecat/services/assemblyai/stt.py +64 -11
- pipecat/services/asyncai/__init__.py +0 -0
- pipecat/services/asyncai/tts.py +501 -0
- pipecat/services/aws/llm.py +185 -111
- pipecat/services/aws/stt.py +217 -23
- pipecat/services/aws/tts.py +118 -52
- pipecat/services/aws/utils.py +101 -5
- pipecat/services/aws_nova_sonic/aws.py +82 -64
- pipecat/services/aws_nova_sonic/context.py +15 -6
- pipecat/services/azure/common.py +10 -2
- pipecat/services/azure/image.py +32 -0
- pipecat/services/azure/llm.py +9 -7
- pipecat/services/azure/stt.py +65 -2
- pipecat/services/azure/tts.py +154 -23
- pipecat/services/cartesia/stt.py +125 -8
- pipecat/services/cartesia/tts.py +102 -38
- pipecat/services/cerebras/llm.py +15 -23
- pipecat/services/deepgram/stt.py +19 -11
- pipecat/services/deepgram/tts.py +36 -0
- pipecat/services/deepseek/llm.py +14 -23
- pipecat/services/elevenlabs/tts.py +330 -64
- pipecat/services/fal/image.py +43 -0
- pipecat/services/fal/stt.py +48 -10
- pipecat/services/fireworks/llm.py +14 -21
- pipecat/services/fish/tts.py +109 -9
- pipecat/services/gemini_multimodal_live/__init__.py +1 -0
- pipecat/services/gemini_multimodal_live/events.py +83 -2
- pipecat/services/gemini_multimodal_live/file_api.py +189 -0
- pipecat/services/gemini_multimodal_live/gemini.py +218 -21
- pipecat/services/gladia/config.py +17 -10
- pipecat/services/gladia/stt.py +82 -36
- pipecat/services/google/frames.py +40 -0
- pipecat/services/google/google.py +2 -0
- pipecat/services/google/image.py +39 -2
- pipecat/services/google/llm.py +176 -58
- pipecat/services/google/llm_openai.py +26 -4
- pipecat/services/google/llm_vertex.py +37 -15
- pipecat/services/google/rtvi.py +41 -0
- pipecat/services/google/stt.py +65 -17
- pipecat/services/google/test-google-chirp.py +45 -0
- pipecat/services/google/tts.py +390 -19
- pipecat/services/grok/llm.py +8 -6
- pipecat/services/groq/llm.py +8 -6
- pipecat/services/groq/stt.py +13 -9
- pipecat/services/groq/tts.py +40 -0
- pipecat/services/hamsa/__init__.py +9 -0
- pipecat/services/hamsa/stt.py +241 -0
- pipecat/services/heygen/__init__.py +5 -0
- pipecat/services/heygen/api.py +281 -0
- pipecat/services/heygen/client.py +620 -0
- pipecat/services/heygen/video.py +338 -0
- pipecat/services/image_service.py +5 -3
- pipecat/services/inworld/__init__.py +1 -0
- pipecat/services/inworld/tts.py +592 -0
- pipecat/services/llm_service.py +127 -45
- pipecat/services/lmnt/tts.py +80 -7
- pipecat/services/mcp_service.py +85 -44
- pipecat/services/mem0/memory.py +42 -13
- pipecat/services/minimax/tts.py +74 -15
- pipecat/services/mistral/__init__.py +0 -0
- pipecat/services/mistral/llm.py +185 -0
- pipecat/services/moondream/vision.py +55 -10
- pipecat/services/neuphonic/tts.py +275 -48
- pipecat/services/nim/llm.py +8 -6
- pipecat/services/ollama/llm.py +27 -7
- pipecat/services/openai/base_llm.py +54 -16
- pipecat/services/openai/image.py +30 -0
- pipecat/services/openai/llm.py +7 -5
- pipecat/services/openai/stt.py +13 -9
- pipecat/services/openai/tts.py +42 -10
- pipecat/services/openai_realtime_beta/azure.py +11 -9
- pipecat/services/openai_realtime_beta/context.py +7 -5
- pipecat/services/openai_realtime_beta/events.py +10 -7
- pipecat/services/openai_realtime_beta/openai.py +37 -18
- pipecat/services/openpipe/llm.py +30 -24
- pipecat/services/openrouter/llm.py +9 -7
- pipecat/services/perplexity/llm.py +15 -19
- pipecat/services/piper/tts.py +26 -12
- pipecat/services/playht/tts.py +227 -65
- pipecat/services/qwen/llm.py +8 -6
- pipecat/services/rime/tts.py +128 -17
- pipecat/services/riva/stt.py +160 -22
- pipecat/services/riva/tts.py +67 -2
- pipecat/services/sambanova/llm.py +19 -17
- pipecat/services/sambanova/stt.py +14 -8
- pipecat/services/sarvam/tts.py +60 -13
- pipecat/services/simli/video.py +82 -21
- pipecat/services/soniox/__init__.py +0 -0
- pipecat/services/soniox/stt.py +398 -0
- pipecat/services/speechmatics/stt.py +29 -17
- pipecat/services/stt_service.py +47 -11
- pipecat/services/tavus/video.py +94 -25
- pipecat/services/together/llm.py +8 -6
- pipecat/services/tts_service.py +77 -53
- pipecat/services/ultravox/stt.py +46 -43
- pipecat/services/vision_service.py +5 -3
- pipecat/services/websocket_service.py +12 -11
- pipecat/services/whisper/base_stt.py +58 -12
- pipecat/services/whisper/stt.py +69 -58
- pipecat/services/xtts/tts.py +59 -2
- pipecat/sync/base_notifier.py +19 -0
- pipecat/sync/event_notifier.py +24 -0
- pipecat/tests/utils.py +73 -5
- pipecat/transcriptions/language.py +24 -0
- pipecat/transports/base_input.py +112 -8
- pipecat/transports/base_output.py +235 -13
- pipecat/transports/base_transport.py +119 -0
- pipecat/transports/local/audio.py +76 -0
- pipecat/transports/local/tk.py +84 -0
- pipecat/transports/network/fastapi_websocket.py +174 -15
- pipecat/transports/network/small_webrtc.py +383 -39
- pipecat/transports/network/webrtc_connection.py +214 -8
- pipecat/transports/network/websocket_client.py +171 -1
- pipecat/transports/network/websocket_server.py +147 -9
- pipecat/transports/services/daily.py +792 -70
- pipecat/transports/services/helpers/daily_rest.py +122 -129
- pipecat/transports/services/livekit.py +339 -4
- pipecat/transports/services/tavus.py +273 -38
- pipecat/utils/asyncio/task_manager.py +92 -186
- pipecat/utils/base_object.py +83 -1
- pipecat/utils/network.py +2 -0
- pipecat/utils/string.py +114 -58
- pipecat/utils/text/base_text_aggregator.py +44 -13
- pipecat/utils/text/base_text_filter.py +46 -0
- pipecat/utils/text/markdown_text_filter.py +70 -14
- pipecat/utils/text/pattern_pair_aggregator.py +18 -14
- pipecat/utils/text/simple_text_aggregator.py +43 -2
- pipecat/utils/text/skip_tags_aggregator.py +21 -13
- pipecat/utils/time.py +36 -0
- pipecat/utils/tracing/class_decorators.py +32 -7
- pipecat/utils/tracing/conversation_context_provider.py +12 -2
- pipecat/utils/tracing/service_attributes.py +80 -64
- pipecat/utils/tracing/service_decorators.py +48 -21
- pipecat/utils/tracing/setup.py +13 -7
- pipecat/utils/tracing/turn_context_provider.py +12 -2
- pipecat/utils/tracing/turn_trace_observer.py +27 -0
- pipecat/utils/utils.py +14 -14
- dv_pipecat_ai-0.0.74.dev770.dist-info/RECORD +0 -319
- pipecat/examples/daily_runner.py +0 -64
- pipecat/examples/run.py +0 -265
- pipecat/utils/asyncio/watchdog_async_iterator.py +0 -72
- pipecat/utils/asyncio/watchdog_event.py +0 -42
- pipecat/utils/asyncio/watchdog_priority_queue.py +0 -48
- pipecat/utils/asyncio/watchdog_queue.py +0 -48
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.74.dev770.dist-info → dv_pipecat_ai-0.0.82.dev776.dist-info}/top_level.txt +0 -0
- /pipecat/{examples → extensions}/__init__.py +0 -0
pipecat/services/llm_service.py
CHANGED
|
@@ -9,11 +9,22 @@
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import inspect
|
|
11
11
|
from dataclasses import dataclass
|
|
12
|
-
from typing import
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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())
|
pipecat/services/lmnt/tts.py
CHANGED
|
@@ -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
|
|
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 = "
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
296
|
+
if not self._websocket or self._websocket.state is State.CLOSED:
|
|
224
297
|
await self._connect()
|
|
225
298
|
|
|
226
299
|
try:
|