dv-pipecat-ai 0.0.82.dev815__py3-none-any.whl → 0.0.82.dev857__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 (106) hide show
  1. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/METADATA +8 -3
  2. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/RECORD +106 -79
  3. pipecat/adapters/base_llm_adapter.py +44 -6
  4. pipecat/adapters/services/anthropic_adapter.py +302 -2
  5. pipecat/adapters/services/aws_nova_sonic_adapter.py +40 -2
  6. pipecat/adapters/services/bedrock_adapter.py +40 -2
  7. pipecat/adapters/services/gemini_adapter.py +276 -6
  8. pipecat/adapters/services/open_ai_adapter.py +88 -7
  9. pipecat/adapters/services/open_ai_realtime_adapter.py +39 -1
  10. pipecat/audio/dtmf/__init__.py +0 -0
  11. pipecat/audio/dtmf/types.py +47 -0
  12. pipecat/audio/dtmf/utils.py +70 -0
  13. pipecat/audio/filters/aic_filter.py +199 -0
  14. pipecat/audio/utils.py +9 -7
  15. pipecat/extensions/ivr/__init__.py +0 -0
  16. pipecat/extensions/ivr/ivr_navigator.py +452 -0
  17. pipecat/frames/frames.py +156 -43
  18. pipecat/pipeline/llm_switcher.py +76 -0
  19. pipecat/pipeline/parallel_pipeline.py +3 -3
  20. pipecat/pipeline/service_switcher.py +144 -0
  21. pipecat/pipeline/task.py +68 -28
  22. pipecat/pipeline/task_observer.py +10 -0
  23. pipecat/processors/aggregators/dtmf_aggregator.py +2 -2
  24. pipecat/processors/aggregators/llm_context.py +277 -0
  25. pipecat/processors/aggregators/llm_response.py +48 -15
  26. pipecat/processors/aggregators/llm_response_universal.py +840 -0
  27. pipecat/processors/aggregators/openai_llm_context.py +3 -3
  28. pipecat/processors/dtmf_aggregator.py +0 -2
  29. pipecat/processors/filters/stt_mute_filter.py +0 -2
  30. pipecat/processors/frame_processor.py +18 -11
  31. pipecat/processors/frameworks/rtvi.py +17 -10
  32. pipecat/processors/metrics/sentry.py +2 -0
  33. pipecat/runner/daily.py +137 -36
  34. pipecat/runner/run.py +1 -1
  35. pipecat/runner/utils.py +7 -7
  36. pipecat/serializers/asterisk.py +20 -4
  37. pipecat/serializers/exotel.py +1 -1
  38. pipecat/serializers/plivo.py +1 -1
  39. pipecat/serializers/telnyx.py +1 -1
  40. pipecat/serializers/twilio.py +1 -1
  41. pipecat/services/__init__.py +2 -2
  42. pipecat/services/anthropic/llm.py +113 -28
  43. pipecat/services/asyncai/tts.py +4 -0
  44. pipecat/services/aws/llm.py +82 -8
  45. pipecat/services/aws/tts.py +0 -10
  46. pipecat/services/aws_nova_sonic/aws.py +5 -0
  47. pipecat/services/cartesia/tts.py +28 -16
  48. pipecat/services/cerebras/llm.py +15 -10
  49. pipecat/services/deepgram/stt.py +8 -0
  50. pipecat/services/deepseek/llm.py +13 -8
  51. pipecat/services/fireworks/llm.py +13 -8
  52. pipecat/services/fish/tts.py +8 -6
  53. pipecat/services/gemini_multimodal_live/gemini.py +5 -0
  54. pipecat/services/gladia/config.py +7 -1
  55. pipecat/services/gladia/stt.py +23 -15
  56. pipecat/services/google/llm.py +159 -59
  57. pipecat/services/google/llm_openai.py +18 -3
  58. pipecat/services/grok/llm.py +2 -1
  59. pipecat/services/llm_service.py +38 -3
  60. pipecat/services/mem0/memory.py +2 -1
  61. pipecat/services/mistral/llm.py +5 -6
  62. pipecat/services/nim/llm.py +2 -1
  63. pipecat/services/openai/base_llm.py +88 -26
  64. pipecat/services/openai/image.py +6 -1
  65. pipecat/services/openai_realtime_beta/openai.py +5 -2
  66. pipecat/services/openpipe/llm.py +6 -8
  67. pipecat/services/perplexity/llm.py +13 -8
  68. pipecat/services/playht/tts.py +9 -6
  69. pipecat/services/rime/tts.py +1 -1
  70. pipecat/services/sambanova/llm.py +18 -13
  71. pipecat/services/sarvam/tts.py +415 -10
  72. pipecat/services/speechmatics/stt.py +2 -2
  73. pipecat/services/tavus/video.py +1 -1
  74. pipecat/services/tts_service.py +15 -5
  75. pipecat/services/vistaar/llm.py +2 -5
  76. pipecat/transports/base_input.py +32 -19
  77. pipecat/transports/base_output.py +39 -5
  78. pipecat/transports/daily/__init__.py +0 -0
  79. pipecat/transports/daily/transport.py +2371 -0
  80. pipecat/transports/daily/utils.py +410 -0
  81. pipecat/transports/livekit/__init__.py +0 -0
  82. pipecat/transports/livekit/transport.py +1042 -0
  83. pipecat/transports/network/fastapi_websocket.py +12 -546
  84. pipecat/transports/network/small_webrtc.py +12 -922
  85. pipecat/transports/network/webrtc_connection.py +9 -595
  86. pipecat/transports/network/websocket_client.py +12 -481
  87. pipecat/transports/network/websocket_server.py +12 -487
  88. pipecat/transports/services/daily.py +9 -2334
  89. pipecat/transports/services/helpers/daily_rest.py +12 -396
  90. pipecat/transports/services/livekit.py +12 -975
  91. pipecat/transports/services/tavus.py +12 -757
  92. pipecat/transports/smallwebrtc/__init__.py +0 -0
  93. pipecat/transports/smallwebrtc/connection.py +612 -0
  94. pipecat/transports/smallwebrtc/transport.py +936 -0
  95. pipecat/transports/tavus/__init__.py +0 -0
  96. pipecat/transports/tavus/transport.py +770 -0
  97. pipecat/transports/websocket/__init__.py +0 -0
  98. pipecat/transports/websocket/client.py +494 -0
  99. pipecat/transports/websocket/fastapi.py +559 -0
  100. pipecat/transports/websocket/server.py +500 -0
  101. pipecat/transports/whatsapp/__init__.py +0 -0
  102. pipecat/transports/whatsapp/api.py +345 -0
  103. pipecat/transports/whatsapp/client.py +364 -0
  104. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/WHEEL +0 -0
  105. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/licenses/LICENSE +0 -0
  106. {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/top_level.txt +0 -0
pipecat/frames/frames.py CHANGED
@@ -12,7 +12,6 @@ and LLM processing.
12
12
  """
13
13
 
14
14
  from dataclasses import dataclass, field
15
- from enum import Enum
16
15
  from typing import (
17
16
  TYPE_CHECKING,
18
17
  Any,
@@ -27,6 +26,8 @@ from typing import (
27
26
  Tuple,
28
27
  )
29
28
 
29
+ from pipecat.adapters.schemas.tools_schema import ToolsSchema
30
+ from pipecat.audio.dtmf.types import KeypadEntry as NewKeypadEntry
30
31
  from pipecat.audio.interruptions.base_interruption_strategy import BaseInterruptionStrategy
31
32
  from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
32
33
  from pipecat.audio.vad.vad_analyzer import VADParams
@@ -36,12 +37,17 @@ from pipecat.utils.time import nanoseconds_to_str
36
37
  from pipecat.utils.utils import obj_count, obj_id
37
38
 
38
39
  if TYPE_CHECKING:
40
+ from pipecat.processors.aggregators.llm_context import LLMContext, NotGiven
39
41
  from pipecat.processors.frame_processor import FrameProcessor
40
42
 
41
43
 
42
- class KeypadEntry(str, Enum):
44
+ class DeprecatedKeypadEntry:
43
45
  """DTMF keypad entries for phone system integration.
44
46
 
47
+ .. deprecated:: 0.0.82
48
+ This class is deprecated and will be removed in a future version.
49
+ Instead, use `audio.dtmf.types.KeypadEntry`.
50
+
45
51
  Parameters:
46
52
  ONE: Number key 1.
47
53
  TWO: Number key 2.
@@ -57,18 +63,38 @@ class KeypadEntry(str, Enum):
57
63
  STAR: Star/asterisk key (*).
58
64
  """
59
65
 
60
- ONE = "1"
61
- TWO = "2"
62
- THREE = "3"
63
- FOUR = "4"
64
- FIVE = "5"
65
- SIX = "6"
66
- SEVEN = "7"
67
- EIGHT = "8"
68
- NINE = "9"
69
- ZERO = "0"
70
- POUND = "#"
71
- STAR = "*"
66
+ _enum = NewKeypadEntry
67
+
68
+ @classmethod
69
+ def _warn(cls):
70
+ import warnings
71
+
72
+ with warnings.catch_warnings():
73
+ warnings.simplefilter("always")
74
+ warnings.warn(
75
+ "`pipecat.frames.frames.KeypadEntry` is deprecated and will be removed in a future version. "
76
+ "Use `pipecat.audio.dtmf.types.KeypadEntry` instead.",
77
+ DeprecationWarning,
78
+ stacklevel=2,
79
+ )
80
+
81
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
82
+ """Allow the instance to be called as a function."""
83
+ self._warn()
84
+ return self._enum(*args, **kwargs)
85
+
86
+ def __getattr__(self, name):
87
+ """Retrieve an attribute from the underlying enum."""
88
+ self._warn()
89
+ return getattr(self._enum, name)
90
+
91
+ def __getitem__(self, name):
92
+ """Retrieve an item from the underlying enum."""
93
+ self._warn()
94
+ return self._enum[name]
95
+
96
+
97
+ KeypadEntry = DeprecatedKeypadEntry()
72
98
 
73
99
 
74
100
  def format_pts(pts: Optional[int]):
@@ -303,6 +329,11 @@ class TextFrame(DataFrame):
303
329
  """
304
330
 
305
331
  text: str
332
+ skip_tts: bool = field(init=False)
333
+
334
+ def __post_init__(self):
335
+ super().__post_init__()
336
+ self.skip_tts = False
306
337
 
307
338
  def __str__(self):
308
339
  pts = format_pts(self.pts)
@@ -403,6 +434,11 @@ class OpenAILLMContextAssistantTimestampFrame(DataFrame):
403
434
  timestamp: str
404
435
 
405
436
 
437
+ # A more universal (LLM-agnostic) name for
438
+ # OpenAILLMContextAssistantTimestampFrame, matching LLMContext
439
+ LLMContextAssistantTimestampFrame = OpenAILLMContextAssistantTimestampFrame
440
+
441
+
406
442
  @dataclass
407
443
  class TranscriptionMessage:
408
444
  """A message in a conversation transcript.
@@ -474,6 +510,20 @@ class TranscriptionUpdateFrame(DataFrame):
474
510
  return f"{self.name}(pts: {pts}, messages: {len(self.messages)})"
475
511
 
476
512
 
513
+ @dataclass
514
+ class LLMContextFrame(Frame):
515
+ """Frame containing a universal LLM context.
516
+
517
+ Used as a signal to LLM services to ingest the provided context and
518
+ generate a response based on it.
519
+
520
+ Parameters:
521
+ context: The LLM context containing messages, tools, and configuration.
522
+ """
523
+
524
+ context: "LLMContext"
525
+
526
+
477
527
  @dataclass
478
528
  class LLMMessagesFrame(DataFrame):
479
529
  """Frame containing LLM messages for chat completion.
@@ -500,15 +550,27 @@ class LLMMessagesFrame(DataFrame):
500
550
  super().__post_init__()
501
551
  import warnings
502
552
 
503
- warnings.simplefilter("always")
504
- warnings.warn(
505
- "LLMMessagesFrame is deprecated and will be removed in a future version. "
506
- "Instead, use either "
507
- "`LLMMessagesUpdateFrame` with `run_llm=True`, or "
508
- "`OpenAILLMContextFrame` with desired messages in a new context",
509
- DeprecationWarning,
510
- stacklevel=2,
511
- )
553
+ with warnings.catch_warnings():
554
+ warnings.simplefilter("always")
555
+ warnings.warn(
556
+ "LLMMessagesFrame is deprecated and will be removed in a future version. "
557
+ "Instead, use either "
558
+ "`LLMMessagesUpdateFrame` with `run_llm=True`, or "
559
+ "`OpenAILLMContextFrame` with desired messages in a new context",
560
+ DeprecationWarning,
561
+ stacklevel=2,
562
+ )
563
+
564
+
565
+ @dataclass
566
+ class LLMRunFrame(DataFrame):
567
+ """Frame to trigger LLM processing with current context.
568
+
569
+ A frame that instructs the LLM service to process the current context and
570
+ generate a response.
571
+ """
572
+
573
+ pass
512
574
 
513
575
 
514
576
  @dataclass
@@ -531,9 +593,8 @@ class LLMMessagesAppendFrame(DataFrame):
531
593
  class LLMMessagesUpdateFrame(DataFrame):
532
594
  """Frame containing LLM messages to replace current context.
533
595
 
534
- A frame containing a list of new LLM messages. These messages will
535
- replace the current context LLM messages and should generate a new
536
- LLMMessagesFrame.
596
+ A frame containing a list of new LLM messages to replace the current
597
+ context LLM messages.
537
598
 
538
599
  Parameters:
539
600
  messages: List of message dictionaries to replace current context.
@@ -556,7 +617,7 @@ class LLMSetToolsFrame(DataFrame):
556
617
  tools: List of tool/function definitions for the LLM.
557
618
  """
558
619
 
559
- tools: List[dict]
620
+ tools: List[dict] | ToolsSchema | "NotGiven"
560
621
 
561
622
 
562
623
  @dataclass
@@ -581,6 +642,21 @@ class LLMEnablePromptCachingFrame(DataFrame):
581
642
  enable: bool
582
643
 
583
644
 
645
+ @dataclass
646
+ class LLMConfigureOutputFrame(DataFrame):
647
+ """Frame to configure LLM output.
648
+
649
+ This frame is used to configure how the LLM produces output. For example, it
650
+ can tell the LLM to generate tokens that should be added to the context but
651
+ not spoken by the TTS service (if one is present in the pipeline).
652
+
653
+ Parameters:
654
+ skip_tts: Whether LLM tokens should skip the TTS service (if any).
655
+ """
656
+
657
+ skip_tts: bool
658
+
659
+
584
660
  @dataclass
585
661
  class TTSSpeakFrame(DataFrame):
586
662
  """Frame containing text that should be spoken by TTS.
@@ -617,7 +693,7 @@ class DTMFFrame:
617
693
  button: The DTMF keypad entry that was pressed.
618
694
  """
619
695
 
620
- button: KeypadEntry
696
+ button: NewKeypadEntry
621
697
 
622
698
 
623
699
  @dataclass
@@ -793,19 +869,6 @@ class StartInterruptionFrame(SystemFrame):
793
869
  pass
794
870
 
795
871
 
796
- @dataclass
797
- class StopInterruptionFrame(SystemFrame):
798
- """Frame indicating user stopped speaking (interruption ended).
799
-
800
- Emitted by the BaseInputTransport to indicate that a user has stopped
801
- speaking (i.e. no more interruptions). This is similar to
802
- UserStoppedSpeakingFrame except that it should be pushed concurrently
803
- with other frames (so the order is not guaranteed).
804
- """
805
-
806
- pass
807
-
808
-
809
872
  @dataclass
810
873
  class UserStartedSpeakingFrame(SystemFrame):
811
874
  """Frame indicating user has started speaking.
@@ -835,6 +898,16 @@ class UserStoppedSpeakingFrame(SystemFrame):
835
898
  emulated: bool = False
836
899
 
837
900
 
901
+ @dataclass
902
+ class UserSpeakingFrame(SystemFrame):
903
+ """Frame indicating the user is speaking.
904
+
905
+ Emitted by VAD to indicate the user is speaking.
906
+ """
907
+
908
+ pass
909
+
910
+
838
911
  @dataclass
839
912
  class EmulateUserStartedSpeakingFrame(SystemFrame):
840
913
  """Frame to emulate user started speaking behavior.
@@ -1055,6 +1128,23 @@ class TransportMessageUrgentFrame(SystemFrame):
1055
1128
  return f"{self.name}(message: {self.message})"
1056
1129
 
1057
1130
 
1131
+ @dataclass
1132
+ class InputTransportMessageUrgentFrame(TransportMessageUrgentFrame):
1133
+ """Frame for transport messages received from external sources.
1134
+
1135
+ This frame wraps incoming transport messages to distinguish them from outgoing
1136
+ urgent transport messages (TransportMessageUrgentFrame), preventing infinite
1137
+ message loops in the transport layer. It inherits the message payload from
1138
+ TransportMessageFrame while marking the message as having been received
1139
+ rather than generated locally.
1140
+
1141
+ Used by transport implementations to properly handle bidirectional message
1142
+ flow without creating feedback loops.
1143
+ """
1144
+
1145
+ pass
1146
+
1147
+
1058
1148
  @dataclass
1059
1149
  class UserImageRequestFrame(SystemFrame):
1060
1150
  """Frame requesting an image from a specific user.
@@ -1310,14 +1400,22 @@ class LLMFullResponseStartFrame(ControlFrame):
1310
1400
  more TextFrames and a final LLMFullResponseEndFrame.
1311
1401
  """
1312
1402
 
1313
- pass
1403
+ skip_tts: bool = field(init=False)
1404
+
1405
+ def __post_init__(self):
1406
+ super().__post_init__()
1407
+ self.skip_tts = False
1314
1408
 
1315
1409
 
1316
1410
  @dataclass
1317
1411
  class LLMFullResponseEndFrame(ControlFrame):
1318
1412
  """Frame indicating the end of an LLM response."""
1319
1413
 
1320
- pass
1414
+ skip_tts: bool = field(init=False)
1415
+
1416
+ def __post_init__(self):
1417
+ super().__post_init__()
1418
+ self.skip_tts = False
1321
1419
 
1322
1420
 
1323
1421
  @dataclass
@@ -1451,6 +1549,11 @@ class MixerEnableFrame(MixerControlFrame):
1451
1549
  class StartUserIdleProcessorFrame(SystemFrame):
1452
1550
  """Frame to start the UserIdleProcessor monitoring."""
1453
1551
 
1552
+
1553
+ @dataclass
1554
+ class ServiceSwitcherFrame(ControlFrame):
1555
+ """A base class for frames that control ServiceSwitcher behavior."""
1556
+
1454
1557
  pass
1455
1558
 
1456
1559
 
@@ -1466,3 +1569,13 @@ class WaitForDTMFFrame(ControlFrame):
1466
1569
  """Frame to stop the UserIdleProcessor monitoring."""
1467
1570
 
1468
1571
  pass
1572
+
1573
+
1574
+ @dataclass
1575
+ class ManuallySwitchServiceFrame(ServiceSwitcherFrame):
1576
+ """A frame to request a manual switch in the active service in a ServiceSwitcher.
1577
+
1578
+ Handled by ServiceSwitcherStrategyManual to switch the active service.
1579
+ """
1580
+
1581
+ service: "FrameProcessor"
@@ -0,0 +1,76 @@
1
+ #
2
+ # Copyright (c) 2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ """LLM switcher for switching between different LLMs at runtime, with different switching strategies."""
8
+
9
+ from typing import Any, List, Optional, Type
10
+
11
+ from pipecat.pipeline.service_switcher import ServiceSwitcher, StrategyType
12
+ from pipecat.processors.aggregators.llm_context import LLMContext
13
+ from pipecat.services.llm_service import LLMService
14
+
15
+
16
+ class LLMSwitcher(ServiceSwitcher[StrategyType]):
17
+ """A pipeline that switches between different LLMs at runtime."""
18
+
19
+ def __init__(self, llms: List[LLMService], strategy_type: Type[StrategyType]):
20
+ """Initialize the service switcher with a list of LLMs and a switching strategy."""
21
+ super().__init__(llms, strategy_type)
22
+
23
+ @property
24
+ def llms(self) -> List[LLMService]:
25
+ """Get the list of LLMs managed by this switcher."""
26
+ return self.services
27
+
28
+ @property
29
+ def active_llm(self) -> Optional[LLMService]:
30
+ """Get the currently active LLM, if any."""
31
+ return self.strategy.active_service
32
+
33
+ async def run_inference(self, context: LLMContext) -> Optional[str]:
34
+ """Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context, using the currently active LLM.
35
+
36
+ Args:
37
+ context: The LLM context containing conversation history.
38
+
39
+ Returns:
40
+ The LLM's response as a string, or None if no response is generated.
41
+ """
42
+ if self.active_llm:
43
+ return await self.active_llm.run_inference(context=context)
44
+ return None
45
+
46
+ def register_function(
47
+ self,
48
+ function_name: Optional[str],
49
+ handler: Any,
50
+ start_callback=None,
51
+ *,
52
+ cancel_on_interruption: bool = True,
53
+ ):
54
+ """Register a function handler for LLM function calls, on all LLMs, active or not.
55
+
56
+ Args:
57
+ function_name: The name of the function to handle. Use None to handle
58
+ all function calls with a catch-all handler.
59
+ handler: The function handler. Should accept a single FunctionCallParams
60
+ parameter.
61
+ start_callback: Legacy callback function (deprecated). Put initialization
62
+ code at the top of your handler instead.
63
+
64
+ .. deprecated:: 0.0.59
65
+ The `start_callback` parameter is deprecated and will be removed in a future version.
66
+
67
+ cancel_on_interruption: Whether to cancel this function call when an
68
+ interruption occurs. Defaults to True.
69
+ """
70
+ for llm in self.llms:
71
+ llm.register_function(
72
+ function_name=function_name,
73
+ handler=handler,
74
+ start_callback=start_callback,
75
+ cancel_on_interruption=cancel_on_interruption,
76
+ )
@@ -16,7 +16,7 @@ from typing import Dict, List
16
16
 
17
17
  from loguru import logger
18
18
 
19
- from pipecat.frames.frames import EndFrame, Frame, StartFrame
19
+ from pipecat.frames.frames import CancelFrame, EndFrame, Frame, StartFrame
20
20
  from pipecat.pipeline.base_pipeline import BasePipeline
21
21
  from pipecat.pipeline.pipeline import Pipeline, PipelineSink, PipelineSource
22
22
  from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, FrameProcessorSetup
@@ -141,7 +141,7 @@ class ParallelPipeline(BasePipeline):
141
141
  await super().process_frame(frame, direction)
142
142
 
143
143
  # Parallel pipeline synchronized frames.
144
- if isinstance(frame, (StartFrame, EndFrame)):
144
+ if isinstance(frame, (StartFrame, EndFrame, CancelFrame)):
145
145
  self._frame_counter[frame.id] = len(self._pipelines)
146
146
  await self.pause_processing_system_frames()
147
147
  await self.pause_processing_frames()
@@ -158,7 +158,7 @@ class ParallelPipeline(BasePipeline):
158
158
 
159
159
  async def _pipeline_sink_push_frame(self, frame: Frame, direction: FrameDirection):
160
160
  # Parallel pipeline synchronized frames.
161
- if isinstance(frame, (StartFrame, EndFrame)):
161
+ if isinstance(frame, (StartFrame, EndFrame, CancelFrame)):
162
162
  # Decrement counter.
163
163
  frame_counter = self._frame_counter.get(frame.id, 0)
164
164
  if frame_counter > 0:
@@ -0,0 +1,144 @@
1
+ #
2
+ # Copyright (c) 2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ """Service switcher for switching between different services at runtime, with different switching strategies."""
8
+
9
+ from typing import Any, Generic, List, Optional, Type, TypeVar
10
+
11
+ from pipecat.frames.frames import Frame, ManuallySwitchServiceFrame, ServiceSwitcherFrame
12
+ from pipecat.pipeline.parallel_pipeline import ParallelPipeline
13
+ from pipecat.processors.filters.function_filter import FunctionFilter
14
+ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
15
+
16
+
17
+ class ServiceSwitcherStrategy:
18
+ """Base class for service switching strategies."""
19
+
20
+ def __init__(self, services: List[FrameProcessor]):
21
+ """Initialize the service switcher strategy with a list of services."""
22
+ self.services = services
23
+ self.active_service: Optional[FrameProcessor] = None
24
+
25
+ def is_active(self, service: FrameProcessor) -> bool:
26
+ """Determine if the given service is the currently active one.
27
+
28
+ This method should be overridden by subclasses to implement specific logic.
29
+
30
+ Args:
31
+ service: The service to check.
32
+
33
+ Returns:
34
+ True if the given service is the active one, False otherwise.
35
+ """
36
+ raise NotImplementedError("Subclasses must implement this method.")
37
+
38
+ def handle_frame(self, frame: ServiceSwitcherFrame, direction: FrameDirection):
39
+ """Handle a frame that controls service switching.
40
+
41
+ This method can be overridden by subclasses to implement specific logic
42
+ for handling frames that control service switching.
43
+
44
+ Args:
45
+ frame: The frame to handle.
46
+ direction: The direction of the frame (upstream or downstream).
47
+ """
48
+ raise NotImplementedError("Subclasses must implement this method.")
49
+
50
+
51
+ class ServiceSwitcherStrategyManual(ServiceSwitcherStrategy):
52
+ """A strategy for switching between services manually.
53
+
54
+ This strategy allows the user to manually select which service is active.
55
+ The initial active service is the first one in the list.
56
+ """
57
+
58
+ def __init__(self, services: List[FrameProcessor]):
59
+ """Initialize the manual service switcher strategy with a list of services."""
60
+ super().__init__(services)
61
+ self.active_service = services[0] if services else None
62
+
63
+ def is_active(self, service: FrameProcessor) -> bool:
64
+ """Check if the given service is the currently active one.
65
+
66
+ Args:
67
+ service: The service to check.
68
+
69
+ Returns:
70
+ True if the given service is the active one, False otherwise.
71
+ """
72
+ return service == self.active_service
73
+
74
+ def handle_frame(self, frame: ServiceSwitcherFrame, direction: FrameDirection):
75
+ """Handle a frame that controls service switching.
76
+
77
+ Args:
78
+ frame: The frame to handle.
79
+ direction: The direction of the frame (upstream or downstream).
80
+ """
81
+ if isinstance(frame, ManuallySwitchServiceFrame):
82
+ self._set_active(frame.service)
83
+ else:
84
+ raise ValueError(f"Unsupported frame type: {type(frame)}")
85
+
86
+ def _set_active(self, service: FrameProcessor):
87
+ """Set the active service to the given one.
88
+
89
+ Args:
90
+ service: The service to set as active.
91
+ """
92
+ if service in self.services:
93
+ self.active_service = service
94
+ else:
95
+ raise ValueError(f"Service {service} is not in the list of available services.")
96
+
97
+
98
+ StrategyType = TypeVar("StrategyType", bound=ServiceSwitcherStrategy)
99
+
100
+
101
+ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
102
+ """A pipeline that switches between different services at runtime."""
103
+
104
+ def __init__(self, services: List[FrameProcessor], strategy_type: Type[StrategyType]):
105
+ """Initialize the service switcher with a list of services and a switching strategy."""
106
+ strategy = strategy_type(services)
107
+ super().__init__(*self._make_pipeline_definitions(services, strategy))
108
+ self.services = services
109
+ self.strategy = strategy
110
+
111
+ @staticmethod
112
+ def _make_pipeline_definitions(
113
+ services: List[FrameProcessor], strategy: ServiceSwitcherStrategy
114
+ ) -> List[Any]:
115
+ pipelines = []
116
+ for service in services:
117
+ pipelines.append(ServiceSwitcher._make_pipeline_definition(service, strategy))
118
+ return pipelines
119
+
120
+ @staticmethod
121
+ def _make_pipeline_definition(
122
+ service: FrameProcessor, strategy: ServiceSwitcherStrategy
123
+ ) -> Any:
124
+ async def filter(frame) -> bool:
125
+ _ = frame
126
+ return strategy.is_active(service)
127
+
128
+ return [
129
+ FunctionFilter(filter, direction=FrameDirection.DOWNSTREAM),
130
+ service,
131
+ FunctionFilter(filter, direction=FrameDirection.UPSTREAM),
132
+ ]
133
+
134
+ async def process_frame(self, frame: Frame, direction: FrameDirection):
135
+ """Process a frame, handling frames which affect service switching.
136
+
137
+ Args:
138
+ frame: The frame to process.
139
+ direction: The direction of the frame (upstream or downstream).
140
+ """
141
+ await super().process_frame(frame, direction)
142
+
143
+ if isinstance(frame, ServiceSwitcherFrame):
144
+ self.strategy.handle_frame(frame, direction)