dv-pipecat-ai 0.0.85.dev698__py3-none-any.whl → 0.0.85.dev814__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.85.dev698.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/METADATA +23 -18
- {dv_pipecat_ai-0.0.85.dev698.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/RECORD +45 -43
- pipecat/adapters/services/aws_nova_sonic_adapter.py +116 -6
- pipecat/pipeline/runner.py +6 -2
- pipecat/pipeline/task.py +40 -55
- pipecat/processors/aggregators/llm_context.py +40 -2
- pipecat/processors/frameworks/rtvi.py +1 -0
- pipecat/runner/daily.py +59 -20
- pipecat/runner/run.py +149 -67
- pipecat/runner/types.py +5 -5
- pipecat/services/assemblyai/models.py +6 -0
- pipecat/services/assemblyai/stt.py +13 -5
- pipecat/services/asyncai/tts.py +3 -0
- pipecat/services/aws/llm.py +33 -16
- pipecat/services/aws/nova_sonic/context.py +69 -0
- pipecat/services/aws/nova_sonic/llm.py +199 -89
- pipecat/services/aws/stt.py +2 -0
- pipecat/services/aws_nova_sonic/context.py +8 -12
- pipecat/services/cartesia/stt.py +77 -70
- pipecat/services/cartesia/tts.py +3 -1
- pipecat/services/deepgram/flux/stt.py +4 -0
- pipecat/services/elevenlabs/tts.py +82 -41
- pipecat/services/fish/tts.py +3 -0
- pipecat/services/google/stt.py +4 -0
- pipecat/services/lmnt/tts.py +2 -0
- pipecat/services/neuphonic/tts.py +3 -0
- pipecat/services/openai/tts.py +37 -6
- pipecat/services/piper/tts.py +7 -9
- pipecat/services/playht/tts.py +3 -0
- pipecat/services/rime/tts.py +9 -8
- pipecat/services/riva/stt.py +3 -1
- pipecat/services/salesforce/__init__.py +9 -0
- pipecat/services/salesforce/llm.py +465 -0
- pipecat/services/sarvam/tts.py +87 -10
- pipecat/services/speechmatics/stt.py +3 -1
- pipecat/services/stt_service.py +23 -10
- pipecat/services/tts_service.py +64 -13
- pipecat/transports/base_input.py +3 -0
- pipecat/transports/base_output.py +71 -77
- pipecat/transports/smallwebrtc/connection.py +5 -0
- pipecat/transports/smallwebrtc/request_handler.py +42 -0
- pipecat/utils/string.py +1 -0
- {dv_pipecat_ai-0.0.85.dev698.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.85.dev698.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/licenses/LICENSE +0 -0
- {dv_pipecat_ai-0.0.85.dev698.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/top_level.txt +0 -0
pipecat/pipeline/task.py
CHANGED
|
@@ -269,6 +269,9 @@ class PipelineTask(BasePipelineTask):
|
|
|
269
269
|
# StopFrame) has been received at the end of the pipeline.
|
|
270
270
|
self._pipeline_end_event = asyncio.Event()
|
|
271
271
|
|
|
272
|
+
# This event is set when the pipeline truly finishes.
|
|
273
|
+
self._pipeline_finished_event = asyncio.Event()
|
|
274
|
+
|
|
272
275
|
# This is the final pipeline. It is composed of a source processor,
|
|
273
276
|
# followed by the user pipeline, and ending with a sink processor. The
|
|
274
277
|
# source allows us to receive and react to upstream frames, and the sink
|
|
@@ -401,11 +404,7 @@ class PipelineTask(BasePipelineTask):
|
|
|
401
404
|
await self.queue_frame(EndFrame())
|
|
402
405
|
|
|
403
406
|
async def cancel(self):
|
|
404
|
-
"""
|
|
405
|
-
|
|
406
|
-
Cancels all running tasks and stops frame processing without
|
|
407
|
-
waiting for completion.
|
|
408
|
-
"""
|
|
407
|
+
"""Request the running pipeline to cancel."""
|
|
409
408
|
if not self._finished:
|
|
410
409
|
await self._cancel()
|
|
411
410
|
|
|
@@ -417,51 +416,38 @@ class PipelineTask(BasePipelineTask):
|
|
|
417
416
|
"""
|
|
418
417
|
if self.has_finished():
|
|
419
418
|
return
|
|
420
|
-
cleanup_pipeline = True
|
|
421
|
-
try:
|
|
422
|
-
# Setup processors.
|
|
423
|
-
await self._setup(params)
|
|
424
419
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
# controlled source processor).
|
|
428
|
-
push_task = await self._create_tasks()
|
|
429
|
-
await push_task
|
|
420
|
+
# Setup processors.
|
|
421
|
+
await self._setup(params)
|
|
430
422
|
|
|
431
|
-
|
|
432
|
-
|
|
423
|
+
# Create all main tasks and wait for the main push task. This is the
|
|
424
|
+
# task that pushes frames to the very beginning of our pipeline (i.e. to
|
|
425
|
+
# our controlled source processor).
|
|
426
|
+
await self._create_tasks()
|
|
433
427
|
|
|
434
|
-
|
|
435
|
-
|
|
428
|
+
try:
|
|
429
|
+
# Wait for pipeline to finish.
|
|
430
|
+
await self._wait_for_pipeline_finished()
|
|
436
431
|
except asyncio.CancelledError:
|
|
437
|
-
|
|
438
|
-
#
|
|
432
|
+
logger.debug(f"Pipeline task {self} got cancelled from outside...")
|
|
433
|
+
# We have been cancelled from outside, let's just cancel everything.
|
|
434
|
+
await self._cancel()
|
|
435
|
+
# Wait again for pipeline to finish. This time we have really
|
|
436
|
+
# cancelled, so it should really finish.
|
|
437
|
+
await self._wait_for_pipeline_finished()
|
|
438
|
+
# Re-raise in case there's more cleanup to do.
|
|
439
439
|
raise
|
|
440
440
|
finally:
|
|
441
441
|
# We can reach this point for different reasons:
|
|
442
442
|
#
|
|
443
|
-
# 1. The task has finished
|
|
444
|
-
# 2. By
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
# `self._cancelled` is true.
|
|
452
|
-
#
|
|
453
|
-
# Case (3) will raise the exception above (because we are cancelling
|
|
454
|
-
# the asyncio task). This will be then captured by the
|
|
455
|
-
# `PipelineRunner` which will call `PipelineTask.cancel()` and
|
|
456
|
-
# therefore becoming case (2).
|
|
457
|
-
if self._finished or self._cancelled:
|
|
458
|
-
logger.debug(f"Pipeline task {self} is finishing cleanup...")
|
|
459
|
-
await self._cancel_tasks()
|
|
460
|
-
await self._cleanup(cleanup_pipeline)
|
|
461
|
-
if self._check_dangling_tasks:
|
|
462
|
-
self._print_dangling_tasks()
|
|
463
|
-
self._finished = True
|
|
464
|
-
logger.debug(f"Pipeline task {self} has finished")
|
|
443
|
+
# 1. The pipeline task has finished (try case).
|
|
444
|
+
# 2. By an asyncio task cancellation (except case).
|
|
445
|
+
logger.debug(f"Pipeline task {self} is finishing...")
|
|
446
|
+
await self._cancel_tasks()
|
|
447
|
+
if self._check_dangling_tasks:
|
|
448
|
+
self._print_dangling_tasks()
|
|
449
|
+
self._finished = True
|
|
450
|
+
logger.debug(f"Pipeline task {self} has finished")
|
|
465
451
|
|
|
466
452
|
async def queue_frame(self, frame: Frame):
|
|
467
453
|
"""Queue a single frame to be pushed down the pipeline.
|
|
@@ -489,19 +475,7 @@ class PipelineTask(BasePipelineTask):
|
|
|
489
475
|
if not self._cancelled:
|
|
490
476
|
logger.debug(f"Canceling pipeline task {self}", call_id=self._conversation_id)
|
|
491
477
|
self._cancelled = True
|
|
492
|
-
|
|
493
|
-
# Make sure everything is cleaned up downstream. This is sent
|
|
494
|
-
# out-of-band from the main streaming task which is what we want since
|
|
495
|
-
# we want to cancel right away.
|
|
496
|
-
await self._pipeline.queue_frame(cancel_frame)
|
|
497
|
-
# Wait for CancelFrame to make it through the pipeline.
|
|
498
|
-
await self._wait_for_pipeline_end(cancel_frame)
|
|
499
|
-
# Only cancel the push task, we don't want to be able to process any
|
|
500
|
-
# other frame after cancel. Everything else will be cancelled in
|
|
501
|
-
# run().
|
|
502
|
-
if self._process_push_task:
|
|
503
|
-
await self._task_manager.cancel_task(self._process_push_task)
|
|
504
|
-
self._process_push_task = None
|
|
478
|
+
await self.queue_frame(CancelFrame())
|
|
505
479
|
|
|
506
480
|
async def _create_tasks(self):
|
|
507
481
|
"""Create and start all pipeline processing tasks."""
|
|
@@ -603,6 +577,17 @@ class PipelineTask(BasePipelineTask):
|
|
|
603
577
|
|
|
604
578
|
self._pipeline_end_event.clear()
|
|
605
579
|
|
|
580
|
+
# We are really done.
|
|
581
|
+
self._pipeline_finished_event.set()
|
|
582
|
+
|
|
583
|
+
async def _wait_for_pipeline_finished(self):
|
|
584
|
+
await self._pipeline_finished_event.wait()
|
|
585
|
+
self._pipeline_finished_event.clear()
|
|
586
|
+
# Make sure we wait for the main task to complete.
|
|
587
|
+
if self._process_push_task:
|
|
588
|
+
await self._process_push_task
|
|
589
|
+
self._process_push_task = None
|
|
590
|
+
|
|
606
591
|
async def _setup(self, params: PipelineTaskParams):
|
|
607
592
|
"""Set up the pipeline task and all processors."""
|
|
608
593
|
mgr_params = TaskManagerParams(loop=params.loop)
|
|
@@ -15,9 +15,10 @@ service-specific adapter.
|
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import base64
|
|
18
|
+
import copy
|
|
18
19
|
import io
|
|
19
20
|
from dataclasses import dataclass
|
|
20
|
-
from typing import Any, List, Optional, TypeAlias, Union
|
|
21
|
+
from typing import TYPE_CHECKING, Any, List, Optional, TypeAlias, Union
|
|
21
22
|
|
|
22
23
|
from loguru import logger
|
|
23
24
|
from openai._types import NOT_GIVEN as OPEN_AI_NOT_GIVEN
|
|
@@ -31,6 +32,9 @@ from PIL import Image
|
|
|
31
32
|
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
|
32
33
|
from pipecat.frames.frames import AudioRawFrame
|
|
33
34
|
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
|
37
|
+
|
|
34
38
|
# "Re-export" types from OpenAI that we're using as universal context types.
|
|
35
39
|
# NOTE: if universal message types need to someday diverge from OpenAI's, we
|
|
36
40
|
# should consider managing our own definitions. But we should do so carefully,
|
|
@@ -65,6 +69,26 @@ class LLMContext:
|
|
|
65
69
|
and content formatting.
|
|
66
70
|
"""
|
|
67
71
|
|
|
72
|
+
@staticmethod
|
|
73
|
+
def from_openai_context(openai_context: "OpenAILLMContext") -> "LLMContext":
|
|
74
|
+
"""Create a universal LLM context from an OpenAI-specific context.
|
|
75
|
+
|
|
76
|
+
NOTE: this should only be used internally, for facilitating migration
|
|
77
|
+
from OpenAILLMContext to LLMContext. New user code should use
|
|
78
|
+
LLMContext directly.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
openai_context: The OpenAI LLM context to convert.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
New LLMContext instance with converted messages and settings.
|
|
85
|
+
"""
|
|
86
|
+
return LLMContext(
|
|
87
|
+
messages=openai_context.get_messages(),
|
|
88
|
+
tools=openai_context.tools,
|
|
89
|
+
tool_choice=openai_context.tool_choice,
|
|
90
|
+
)
|
|
91
|
+
|
|
68
92
|
def __init__(
|
|
69
93
|
self,
|
|
70
94
|
messages: Optional[List[LLMContextMessage]] = None,
|
|
@@ -82,6 +106,19 @@ class LLMContext:
|
|
|
82
106
|
self._tools: ToolsSchema | NotGiven = LLMContext._normalize_and_validate_tools(tools)
|
|
83
107
|
self._tool_choice: LLMContextToolChoice | NotGiven = tool_choice
|
|
84
108
|
|
|
109
|
+
@property
|
|
110
|
+
def messages(self) -> List[LLMContextMessage]:
|
|
111
|
+
"""Get the current messages list.
|
|
112
|
+
|
|
113
|
+
NOTE: This is equivalent to calling `get_messages()` with no filter. If
|
|
114
|
+
you want to filter out LLM-specific messages that don't pertain to your
|
|
115
|
+
LLM, use `get_messages()` directly.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
List of conversation messages.
|
|
119
|
+
"""
|
|
120
|
+
return self.get_messages()
|
|
121
|
+
|
|
85
122
|
def get_messages(self, llm_specific_filter: Optional[str] = None) -> List[LLMContextMessage]:
|
|
86
123
|
"""Get the current messages list.
|
|
87
124
|
|
|
@@ -89,7 +126,8 @@ class LLMContext:
|
|
|
89
126
|
llm_specific_filter: Optional filter to return LLM-specific
|
|
90
127
|
messages for the given LLM, in addition to the standard
|
|
91
128
|
messages. If messages end up being filtered, an error will be
|
|
92
|
-
logged
|
|
129
|
+
logged; this is intended to catch accidental use of
|
|
130
|
+
incompatible LLM-specific messages.
|
|
93
131
|
|
|
94
132
|
Returns:
|
|
95
133
|
List of conversation messages.
|
|
@@ -1018,6 +1018,7 @@ class RTVIObserver(BaseObserver):
|
|
|
1018
1018
|
|
|
1019
1019
|
if (
|
|
1020
1020
|
isinstance(frame, (UserStartedSpeakingFrame, UserStoppedSpeakingFrame))
|
|
1021
|
+
and (direction == FrameDirection.DOWNSTREAM)
|
|
1021
1022
|
and self._params.user_speaking_enabled
|
|
1022
1023
|
):
|
|
1023
1024
|
await self._handle_interruptions(frame)
|
pipecat/runner/daily.py
CHANGED
|
@@ -76,12 +76,14 @@ class DailyRoomConfig(BaseModel):
|
|
|
76
76
|
async def configure(
|
|
77
77
|
aiohttp_session: aiohttp.ClientSession,
|
|
78
78
|
*,
|
|
79
|
+
api_key: Optional[str] = None,
|
|
79
80
|
room_exp_duration: Optional[float] = 2.0,
|
|
80
81
|
token_exp_duration: Optional[float] = 2.0,
|
|
81
82
|
sip_caller_phone: Optional[str] = None,
|
|
82
83
|
sip_enable_video: Optional[bool] = False,
|
|
83
84
|
sip_num_endpoints: Optional[int] = 1,
|
|
84
85
|
sip_codecs: Optional[Dict[str, List[str]]] = None,
|
|
86
|
+
room_properties: Optional[DailyRoomProperties] = None,
|
|
85
87
|
) -> DailyRoomConfig:
|
|
86
88
|
"""Configure Daily room URL and token with optional SIP capabilities.
|
|
87
89
|
|
|
@@ -91,6 +93,7 @@ async def configure(
|
|
|
91
93
|
|
|
92
94
|
Args:
|
|
93
95
|
aiohttp_session: HTTP session for making API requests.
|
|
96
|
+
api_key: Daily API key.
|
|
94
97
|
room_exp_duration: Room expiration time in hours.
|
|
95
98
|
token_exp_duration: Token expiration time in hours.
|
|
96
99
|
sip_caller_phone: Phone number or identifier for SIP display name.
|
|
@@ -99,6 +102,10 @@ async def configure(
|
|
|
99
102
|
sip_num_endpoints: Number of allowed SIP endpoints.
|
|
100
103
|
sip_codecs: Codecs to support for audio and video. If None, uses Daily defaults.
|
|
101
104
|
Example: {"audio": ["OPUS"], "video": ["H264"]}
|
|
105
|
+
room_properties: Optional DailyRoomProperties to use instead of building from
|
|
106
|
+
individual parameters. When provided, this overrides room_exp_duration and
|
|
107
|
+
SIP-related parameters. If not provided, properties are built from the
|
|
108
|
+
individual parameters as before.
|
|
102
109
|
|
|
103
110
|
Returns:
|
|
104
111
|
DailyRoomConfig: Object with room_url, token, and optional sip_endpoint.
|
|
@@ -115,18 +122,48 @@ async def configure(
|
|
|
115
122
|
# SIP-enabled room
|
|
116
123
|
sip_config = await configure(session, sip_caller_phone="+15551234567")
|
|
117
124
|
print(f"SIP endpoint: {sip_config.sip_endpoint}")
|
|
125
|
+
|
|
126
|
+
# Custom room properties with recording enabled
|
|
127
|
+
custom_props = DailyRoomProperties(
|
|
128
|
+
enable_recording="cloud",
|
|
129
|
+
max_participants=2,
|
|
130
|
+
)
|
|
131
|
+
config = await configure(session, room_properties=custom_props)
|
|
118
132
|
"""
|
|
119
133
|
# Check for required API key
|
|
120
|
-
api_key = os.getenv("DAILY_API_KEY")
|
|
134
|
+
api_key = api_key or os.getenv("DAILY_API_KEY")
|
|
121
135
|
if not api_key:
|
|
122
136
|
raise Exception(
|
|
123
137
|
"DAILY_API_KEY environment variable is required. "
|
|
124
138
|
"Get your API key from https://dashboard.daily.co/developers"
|
|
125
139
|
)
|
|
126
140
|
|
|
141
|
+
# Warn if both room_properties and individual parameters are provided
|
|
142
|
+
if room_properties is not None:
|
|
143
|
+
individual_params_provided = any(
|
|
144
|
+
[
|
|
145
|
+
room_exp_duration != 2.0,
|
|
146
|
+
token_exp_duration != 2.0,
|
|
147
|
+
sip_caller_phone is not None,
|
|
148
|
+
sip_enable_video is not False,
|
|
149
|
+
sip_num_endpoints != 1,
|
|
150
|
+
sip_codecs is not None,
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
if individual_params_provided:
|
|
154
|
+
logger.warning(
|
|
155
|
+
"Both room_properties and individual parameters (room_exp_duration, token_exp_duration, "
|
|
156
|
+
"sip_*) were provided. The room_properties will be used and individual parameters "
|
|
157
|
+
"will be ignored."
|
|
158
|
+
)
|
|
159
|
+
|
|
127
160
|
# Determine if SIP mode is enabled
|
|
128
161
|
sip_enabled = sip_caller_phone is not None
|
|
129
162
|
|
|
163
|
+
# If room_properties is provided, check if it has SIP configuration
|
|
164
|
+
if room_properties and room_properties.sip:
|
|
165
|
+
sip_enabled = True
|
|
166
|
+
|
|
130
167
|
daily_rest_helper = DailyRESTHelper(
|
|
131
168
|
daily_api_key=api_key,
|
|
132
169
|
daily_api_url=os.getenv("DAILY_API_URL", "https://api.daily.co/v1"),
|
|
@@ -150,27 +187,29 @@ async def configure(
|
|
|
150
187
|
room_name = f"{room_prefix}-{uuid.uuid4().hex[:8]}"
|
|
151
188
|
logger.info(f"Creating new Daily room: {room_name}")
|
|
152
189
|
|
|
153
|
-
#
|
|
154
|
-
|
|
190
|
+
# Use provided room_properties or build from parameters
|
|
191
|
+
if room_properties is None:
|
|
192
|
+
# Calculate expiration time
|
|
193
|
+
expiration_time = time.time() + (room_exp_duration * 60 * 60)
|
|
155
194
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
# Add SIP configuration if enabled
|
|
163
|
-
if sip_enabled:
|
|
164
|
-
sip_params = DailyRoomSipParams(
|
|
165
|
-
display_name=sip_caller_phone,
|
|
166
|
-
video=sip_enable_video,
|
|
167
|
-
sip_mode="dial-in",
|
|
168
|
-
num_endpoints=sip_num_endpoints,
|
|
169
|
-
codecs=sip_codecs,
|
|
195
|
+
# Create room properties
|
|
196
|
+
room_properties = DailyRoomProperties(
|
|
197
|
+
exp=expiration_time,
|
|
198
|
+
eject_at_room_exp=True,
|
|
170
199
|
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
200
|
+
|
|
201
|
+
# Add SIP configuration if enabled
|
|
202
|
+
if sip_enabled:
|
|
203
|
+
sip_params = DailyRoomSipParams(
|
|
204
|
+
display_name=sip_caller_phone,
|
|
205
|
+
video=sip_enable_video,
|
|
206
|
+
sip_mode="dial-in",
|
|
207
|
+
num_endpoints=sip_num_endpoints,
|
|
208
|
+
codecs=sip_codecs,
|
|
209
|
+
)
|
|
210
|
+
room_properties.sip = sip_params
|
|
211
|
+
room_properties.enable_dialout = True # Enable outbound calls if needed
|
|
212
|
+
room_properties.start_video_off = not sip_enable_video # Voice-only by default
|
|
174
213
|
|
|
175
214
|
# Create room parameters
|
|
176
215
|
room_params = DailyRoomParams(name=room_name, properties=room_properties)
|