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.

Files changed (45) hide show
  1. {dv_pipecat_ai-0.0.85.dev698.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/METADATA +23 -18
  2. {dv_pipecat_ai-0.0.85.dev698.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/RECORD +45 -43
  3. pipecat/adapters/services/aws_nova_sonic_adapter.py +116 -6
  4. pipecat/pipeline/runner.py +6 -2
  5. pipecat/pipeline/task.py +40 -55
  6. pipecat/processors/aggregators/llm_context.py +40 -2
  7. pipecat/processors/frameworks/rtvi.py +1 -0
  8. pipecat/runner/daily.py +59 -20
  9. pipecat/runner/run.py +149 -67
  10. pipecat/runner/types.py +5 -5
  11. pipecat/services/assemblyai/models.py +6 -0
  12. pipecat/services/assemblyai/stt.py +13 -5
  13. pipecat/services/asyncai/tts.py +3 -0
  14. pipecat/services/aws/llm.py +33 -16
  15. pipecat/services/aws/nova_sonic/context.py +69 -0
  16. pipecat/services/aws/nova_sonic/llm.py +199 -89
  17. pipecat/services/aws/stt.py +2 -0
  18. pipecat/services/aws_nova_sonic/context.py +8 -12
  19. pipecat/services/cartesia/stt.py +77 -70
  20. pipecat/services/cartesia/tts.py +3 -1
  21. pipecat/services/deepgram/flux/stt.py +4 -0
  22. pipecat/services/elevenlabs/tts.py +82 -41
  23. pipecat/services/fish/tts.py +3 -0
  24. pipecat/services/google/stt.py +4 -0
  25. pipecat/services/lmnt/tts.py +2 -0
  26. pipecat/services/neuphonic/tts.py +3 -0
  27. pipecat/services/openai/tts.py +37 -6
  28. pipecat/services/piper/tts.py +7 -9
  29. pipecat/services/playht/tts.py +3 -0
  30. pipecat/services/rime/tts.py +9 -8
  31. pipecat/services/riva/stt.py +3 -1
  32. pipecat/services/salesforce/__init__.py +9 -0
  33. pipecat/services/salesforce/llm.py +465 -0
  34. pipecat/services/sarvam/tts.py +87 -10
  35. pipecat/services/speechmatics/stt.py +3 -1
  36. pipecat/services/stt_service.py +23 -10
  37. pipecat/services/tts_service.py +64 -13
  38. pipecat/transports/base_input.py +3 -0
  39. pipecat/transports/base_output.py +71 -77
  40. pipecat/transports/smallwebrtc/connection.py +5 -0
  41. pipecat/transports/smallwebrtc/request_handler.py +42 -0
  42. pipecat/utils/string.py +1 -0
  43. {dv_pipecat_ai-0.0.85.dev698.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/WHEEL +0 -0
  44. {dv_pipecat_ai-0.0.85.dev698.dist-info → dv_pipecat_ai-0.0.85.dev814.dist-info}/licenses/LICENSE +0 -0
  45. {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
- """Immediately stop the running pipeline.
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
- # Create all main tasks and wait of the main push task. This is the
426
- # task that pushes frames to the very beginning of our pipeline (our
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
- # We have already cleaned up the pipeline inside the task.
432
- cleanup_pipeline = False
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
- # Pipeline has finished nicely.
435
- self._finished = True
428
+ try:
429
+ # Wait for pipeline to finish.
430
+ await self._wait_for_pipeline_finished()
436
431
  except asyncio.CancelledError:
437
- # Raise exception back to the pipeline runner so it can cancel this
438
- # task properly.
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 properly (e.g. `EndFrame`).
444
- # 2. By calling `PipelineTask.cancel()`.
445
- # 3. By asyncio task cancellation.
446
- #
447
- # Case (1) will execute the code below without issues because
448
- # `self._finished` is true.
449
- #
450
- # Case (2) will execute the code below without issues because
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
- cancel_frame = CancelFrame()
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
- # Calculate expiration time
154
- expiration_time = time.time() + (room_exp_duration * 60 * 60)
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
- # Create room properties
157
- room_properties = DailyRoomProperties(
158
- exp=expiration_time,
159
- eject_at_room_exp=True,
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
- room_properties.sip = sip_params
172
- room_properties.enable_dialout = True # Enable outbound calls if needed
173
- room_properties.start_video_off = not sip_enable_video # Voice-only by default
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)