dv-pipecat-ai 0.0.85.dev5__py3-none-any.whl → 0.0.85.dev698__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 (157) hide show
  1. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/METADATA +78 -117
  2. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/RECORD +157 -123
  3. pipecat/adapters/base_llm_adapter.py +38 -1
  4. pipecat/adapters/services/anthropic_adapter.py +9 -14
  5. pipecat/adapters/services/aws_nova_sonic_adapter.py +5 -0
  6. pipecat/adapters/services/bedrock_adapter.py +236 -13
  7. pipecat/adapters/services/gemini_adapter.py +12 -8
  8. pipecat/adapters/services/open_ai_adapter.py +19 -7
  9. pipecat/adapters/services/open_ai_realtime_adapter.py +5 -0
  10. pipecat/audio/filters/krisp_viva_filter.py +193 -0
  11. pipecat/audio/filters/noisereduce_filter.py +15 -0
  12. pipecat/audio/turn/base_turn_analyzer.py +9 -1
  13. pipecat/audio/turn/smart_turn/base_smart_turn.py +14 -8
  14. pipecat/audio/turn/smart_turn/data/__init__.py +0 -0
  15. pipecat/audio/turn/smart_turn/data/smart-turn-v3.0.onnx +0 -0
  16. pipecat/audio/turn/smart_turn/http_smart_turn.py +6 -2
  17. pipecat/audio/turn/smart_turn/local_smart_turn.py +1 -1
  18. pipecat/audio/turn/smart_turn/local_smart_turn_v2.py +1 -1
  19. pipecat/audio/turn/smart_turn/local_smart_turn_v3.py +124 -0
  20. pipecat/audio/vad/data/README.md +10 -0
  21. pipecat/audio/vad/vad_analyzer.py +13 -1
  22. pipecat/extensions/voicemail/voicemail_detector.py +5 -5
  23. pipecat/frames/frames.py +120 -87
  24. pipecat/observers/loggers/debug_log_observer.py +3 -3
  25. pipecat/observers/loggers/llm_log_observer.py +7 -3
  26. pipecat/observers/loggers/user_bot_latency_log_observer.py +22 -10
  27. pipecat/pipeline/runner.py +12 -4
  28. pipecat/pipeline/service_switcher.py +64 -36
  29. pipecat/pipeline/task.py +85 -24
  30. pipecat/processors/aggregators/dtmf_aggregator.py +28 -22
  31. pipecat/processors/aggregators/{gated_openai_llm_context.py → gated_llm_context.py} +9 -9
  32. pipecat/processors/aggregators/gated_open_ai_llm_context.py +12 -0
  33. pipecat/processors/aggregators/llm_response.py +6 -7
  34. pipecat/processors/aggregators/llm_response_universal.py +19 -15
  35. pipecat/processors/aggregators/user_response.py +6 -6
  36. pipecat/processors/aggregators/vision_image_frame.py +24 -2
  37. pipecat/processors/audio/audio_buffer_processor.py +43 -8
  38. pipecat/processors/filters/stt_mute_filter.py +2 -0
  39. pipecat/processors/frame_processor.py +103 -17
  40. pipecat/processors/frameworks/langchain.py +8 -2
  41. pipecat/processors/frameworks/rtvi.py +209 -68
  42. pipecat/processors/frameworks/strands_agents.py +170 -0
  43. pipecat/processors/logger.py +2 -2
  44. pipecat/processors/transcript_processor.py +4 -4
  45. pipecat/processors/user_idle_processor.py +3 -6
  46. pipecat/runner/run.py +270 -50
  47. pipecat/runner/types.py +2 -0
  48. pipecat/runner/utils.py +51 -10
  49. pipecat/serializers/exotel.py +5 -5
  50. pipecat/serializers/livekit.py +20 -0
  51. pipecat/serializers/plivo.py +6 -9
  52. pipecat/serializers/protobuf.py +6 -5
  53. pipecat/serializers/telnyx.py +2 -2
  54. pipecat/serializers/twilio.py +43 -23
  55. pipecat/services/ai_service.py +2 -6
  56. pipecat/services/anthropic/llm.py +2 -25
  57. pipecat/services/asyncai/tts.py +2 -3
  58. pipecat/services/aws/__init__.py +1 -0
  59. pipecat/services/aws/llm.py +122 -97
  60. pipecat/services/aws/nova_sonic/__init__.py +0 -0
  61. pipecat/services/aws/nova_sonic/context.py +367 -0
  62. pipecat/services/aws/nova_sonic/frames.py +25 -0
  63. pipecat/services/aws/nova_sonic/llm.py +1155 -0
  64. pipecat/services/aws/stt.py +1 -3
  65. pipecat/services/aws_nova_sonic/__init__.py +19 -1
  66. pipecat/services/aws_nova_sonic/aws.py +11 -1151
  67. pipecat/services/aws_nova_sonic/context.py +13 -355
  68. pipecat/services/aws_nova_sonic/frames.py +13 -17
  69. pipecat/services/azure/realtime/__init__.py +0 -0
  70. pipecat/services/azure/realtime/llm.py +65 -0
  71. pipecat/services/azure/stt.py +15 -0
  72. pipecat/services/cartesia/tts.py +2 -2
  73. pipecat/services/deepgram/__init__.py +1 -0
  74. pipecat/services/deepgram/flux/__init__.py +0 -0
  75. pipecat/services/deepgram/flux/stt.py +636 -0
  76. pipecat/services/elevenlabs/__init__.py +2 -1
  77. pipecat/services/elevenlabs/stt.py +254 -276
  78. pipecat/services/elevenlabs/tts.py +5 -5
  79. pipecat/services/fish/tts.py +2 -2
  80. pipecat/services/gemini_multimodal_live/events.py +38 -524
  81. pipecat/services/gemini_multimodal_live/file_api.py +23 -173
  82. pipecat/services/gemini_multimodal_live/gemini.py +41 -1403
  83. pipecat/services/gladia/stt.py +56 -72
  84. pipecat/services/google/__init__.py +1 -0
  85. pipecat/services/google/gemini_live/__init__.py +3 -0
  86. pipecat/services/google/gemini_live/file_api.py +189 -0
  87. pipecat/services/google/gemini_live/llm.py +1582 -0
  88. pipecat/services/google/gemini_live/llm_vertex.py +184 -0
  89. pipecat/services/google/llm.py +15 -11
  90. pipecat/services/google/llm_openai.py +3 -3
  91. pipecat/services/google/llm_vertex.py +86 -16
  92. pipecat/services/google/tts.py +7 -3
  93. pipecat/services/heygen/api.py +2 -0
  94. pipecat/services/heygen/client.py +8 -4
  95. pipecat/services/heygen/video.py +2 -0
  96. pipecat/services/hume/__init__.py +5 -0
  97. pipecat/services/hume/tts.py +220 -0
  98. pipecat/services/inworld/tts.py +6 -6
  99. pipecat/services/llm_service.py +15 -5
  100. pipecat/services/lmnt/tts.py +2 -2
  101. pipecat/services/mcp_service.py +4 -2
  102. pipecat/services/mem0/memory.py +6 -5
  103. pipecat/services/mistral/llm.py +29 -8
  104. pipecat/services/moondream/vision.py +42 -16
  105. pipecat/services/neuphonic/tts.py +2 -2
  106. pipecat/services/openai/__init__.py +1 -0
  107. pipecat/services/openai/base_llm.py +27 -20
  108. pipecat/services/openai/realtime/__init__.py +0 -0
  109. pipecat/services/openai/realtime/context.py +272 -0
  110. pipecat/services/openai/realtime/events.py +1106 -0
  111. pipecat/services/openai/realtime/frames.py +37 -0
  112. pipecat/services/openai/realtime/llm.py +829 -0
  113. pipecat/services/openai/tts.py +16 -8
  114. pipecat/services/openai_realtime/__init__.py +27 -0
  115. pipecat/services/openai_realtime/azure.py +21 -0
  116. pipecat/services/openai_realtime/context.py +21 -0
  117. pipecat/services/openai_realtime/events.py +21 -0
  118. pipecat/services/openai_realtime/frames.py +21 -0
  119. pipecat/services/openai_realtime_beta/azure.py +16 -0
  120. pipecat/services/openai_realtime_beta/openai.py +17 -5
  121. pipecat/services/playht/tts.py +31 -4
  122. pipecat/services/rime/tts.py +3 -4
  123. pipecat/services/sarvam/tts.py +2 -6
  124. pipecat/services/simli/video.py +2 -2
  125. pipecat/services/speechmatics/stt.py +1 -7
  126. pipecat/services/stt_service.py +34 -0
  127. pipecat/services/tavus/video.py +2 -2
  128. pipecat/services/tts_service.py +9 -9
  129. pipecat/services/vision_service.py +7 -6
  130. pipecat/services/vistaar/llm.py +4 -0
  131. pipecat/tests/utils.py +4 -4
  132. pipecat/transcriptions/language.py +41 -1
  133. pipecat/transports/base_input.py +17 -42
  134. pipecat/transports/base_output.py +42 -26
  135. pipecat/transports/daily/transport.py +199 -26
  136. pipecat/transports/heygen/__init__.py +0 -0
  137. pipecat/transports/heygen/transport.py +381 -0
  138. pipecat/transports/livekit/transport.py +228 -63
  139. pipecat/transports/local/audio.py +6 -1
  140. pipecat/transports/local/tk.py +11 -2
  141. pipecat/transports/network/fastapi_websocket.py +1 -1
  142. pipecat/transports/smallwebrtc/connection.py +98 -19
  143. pipecat/transports/smallwebrtc/request_handler.py +204 -0
  144. pipecat/transports/smallwebrtc/transport.py +65 -23
  145. pipecat/transports/tavus/transport.py +23 -12
  146. pipecat/transports/websocket/client.py +41 -5
  147. pipecat/transports/websocket/fastapi.py +21 -11
  148. pipecat/transports/websocket/server.py +14 -7
  149. pipecat/transports/whatsapp/api.py +8 -0
  150. pipecat/transports/whatsapp/client.py +47 -0
  151. pipecat/utils/base_object.py +54 -22
  152. pipecat/utils/string.py +12 -1
  153. pipecat/utils/tracing/service_decorators.py +21 -21
  154. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/WHEEL +0 -0
  155. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/licenses/LICENSE +0 -0
  156. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/top_level.txt +0 -0
  157. /pipecat/services/{aws_nova_sonic → aws/nova_sonic}/ready.wav +0 -0
pipecat/frames/frames.py CHANGED
@@ -672,7 +672,7 @@ class TTSSpeakFrame(DataFrame):
672
672
 
673
673
 
674
674
  @dataclass
675
- class TransportMessageFrame(DataFrame):
675
+ class OutputTransportMessageFrame(DataFrame):
676
676
  """Frame containing transport-specific message data.
677
677
 
678
678
  Parameters:
@@ -685,6 +685,32 @@ class TransportMessageFrame(DataFrame):
685
685
  return f"{self.name}(message: {self.message})"
686
686
 
687
687
 
688
+ @dataclass
689
+ class TransportMessageFrame(OutputTransportMessageFrame):
690
+ """Frame containing transport-specific message data.
691
+
692
+ .. deprecated:: 0.0.87
693
+ This frame is deprecated and will be removed in a future version.
694
+ Instead, use `OutputTransportMessageFrame`.
695
+
696
+ Parameters:
697
+ message: The transport message payload.
698
+ """
699
+
700
+ def __post_init__(self):
701
+ super().__post_init__()
702
+ import warnings
703
+
704
+ with warnings.catch_warnings():
705
+ warnings.simplefilter("always")
706
+ warnings.warn(
707
+ "TransportMessageFrame is deprecated and will be removed in a future version. "
708
+ "Instead, use OutputTransportMessageFrame.",
709
+ DeprecationWarning,
710
+ stacklevel=2,
711
+ )
712
+
713
+
688
714
  @dataclass
689
715
  class DTMFFrame:
690
716
  """Base class for DTMF (Dual-Tone Multi-Frequency) keypad frames.
@@ -788,43 +814,6 @@ class FatalErrorFrame(ErrorFrame):
788
814
  fatal: bool = field(default=True, init=False)
789
815
 
790
816
 
791
- @dataclass
792
- class EndTaskFrame(SystemFrame):
793
- """Frame to request graceful pipeline task closure.
794
-
795
- This is used to notify the pipeline task that the pipeline should be
796
- closed nicely (flushing all the queued frames) by pushing an EndFrame
797
- downstream. This frame should be pushed upstream.
798
- """
799
-
800
- pass
801
-
802
-
803
- @dataclass
804
- class CancelTaskFrame(SystemFrame):
805
- """Frame to request immediate pipeline task cancellation.
806
-
807
- This is used to notify the pipeline task that the pipeline should be
808
- stopped immediately by pushing a CancelFrame downstream. This frame
809
- should be pushed upstream.
810
- """
811
-
812
- pass
813
-
814
-
815
- @dataclass
816
- class StopTaskFrame(SystemFrame):
817
- """Frame to request pipeline task stop while keeping processors running.
818
-
819
- This is used to notify the pipeline task that it should be stopped as
820
- soon as possible (flushing all the queued frames) but that the pipeline
821
- processors should be kept in a running state. This frame should be pushed
822
- upstream.
823
- """
824
-
825
- pass
826
-
827
-
828
817
  @dataclass
829
818
  class FrameProcessorPauseUrgentFrame(SystemFrame):
830
819
  """Frame to pause frame processing immediately.
@@ -857,7 +846,7 @@ class FrameProcessorResumeUrgentFrame(SystemFrame):
857
846
 
858
847
 
859
848
  @dataclass
860
- class StartInterruptionFrame(SystemFrame):
849
+ class InterruptionFrame(SystemFrame):
861
850
  """Frame indicating user started speaking (interruption detected).
862
851
 
863
852
  Emitted by the BaseInputTransport to indicate that a user has started
@@ -869,6 +858,34 @@ class StartInterruptionFrame(SystemFrame):
869
858
  pass
870
859
 
871
860
 
861
+ @dataclass
862
+ class StartInterruptionFrame(InterruptionFrame):
863
+ """Frame indicating user started speaking (interruption detected).
864
+
865
+ .. deprecated:: 0.0.85
866
+ This frame is deprecated and will be removed in a future version.
867
+ Instead, use `InterruptionFrame`.
868
+
869
+ Emitted by the BaseInputTransport to indicate that a user has started
870
+ speaking (i.e. is interrupting). This is similar to
871
+ UserStartedSpeakingFrame except that it should be pushed concurrently
872
+ with other frames (so the order is not guaranteed).
873
+ """
874
+
875
+ def __post_init__(self):
876
+ super().__post_init__()
877
+ import warnings
878
+
879
+ with warnings.catch_warnings():
880
+ warnings.simplefilter("always")
881
+ warnings.warn(
882
+ "StartInterruptionFrame is deprecated and will be removed in a future version. "
883
+ "Instead, use InterruptionFrame.",
884
+ DeprecationWarning,
885
+ stacklevel=2,
886
+ )
887
+
888
+
872
889
  @dataclass
873
890
  class UserStartedSpeakingFrame(SystemFrame):
874
891
  """Frame indicating user has started speaking.
@@ -944,20 +961,6 @@ class VADUserStoppedSpeakingFrame(SystemFrame):
944
961
  pass
945
962
 
946
963
 
947
- @dataclass
948
- class BotInterruptionFrame(SystemFrame):
949
- """Frame indicating the bot should be interrupted.
950
-
951
- Emitted when the bot should be interrupted. This will mainly cause the
952
- same actions as if the user interrupted except that the
953
- UserStartedSpeakingFrame and UserStoppedSpeakingFrame won't be generated.
954
- This frame should be pushed upstreams. It results in the BaseInputTransport
955
- starting an interruption by pushing a StartInterruptionFrame downstream.
956
- """
957
-
958
- pass
959
-
960
-
961
964
  @dataclass
962
965
  class BotStartedSpeakingFrame(SystemFrame):
963
966
  """Frame indicating the bot started speaking.
@@ -1115,8 +1118,8 @@ class STTMuteFrame(SystemFrame):
1115
1118
 
1116
1119
 
1117
1120
  @dataclass
1118
- class TransportMessageUrgentFrame(SystemFrame):
1119
- """Frame for urgent transport messages that need immediate processing.
1121
+ class InputTransportMessageFrame(SystemFrame):
1122
+ """Frame for transport messages received from external sources.
1120
1123
 
1121
1124
  Parameters:
1122
1125
  message: The urgent transport message payload.
@@ -1129,20 +1132,69 @@ class TransportMessageUrgentFrame(SystemFrame):
1129
1132
 
1130
1133
 
1131
1134
  @dataclass
1132
- class InputTransportMessageUrgentFrame(TransportMessageUrgentFrame):
1135
+ class InputTransportMessageUrgentFrame(InputTransportMessageFrame):
1133
1136
  """Frame for transport messages received from external sources.
1134
1137
 
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.
1138
+ .. deprecated:: 0.0.87
1139
+ This frame is deprecated and will be removed in a future version.
1140
+ Instead, use `InputTransportMessageFrame`.
1140
1141
 
1141
- Used by transport implementations to properly handle bidirectional message
1142
- flow without creating feedback loops.
1142
+ Parameters:
1143
+ message: The urgent transport message payload.
1143
1144
  """
1144
1145
 
1145
- pass
1146
+ def __post_init__(self):
1147
+ super().__post_init__()
1148
+ import warnings
1149
+
1150
+ with warnings.catch_warnings():
1151
+ warnings.simplefilter("always")
1152
+ warnings.warn(
1153
+ "InputTransportMessageUrgentFrame is deprecated and will be removed in a future version. "
1154
+ "Instead, use InputTransportMessageFrame.",
1155
+ DeprecationWarning,
1156
+ stacklevel=2,
1157
+ )
1158
+
1159
+
1160
+ @dataclass
1161
+ class OutputTransportMessageUrgentFrame(SystemFrame):
1162
+ """Frame for urgent transport messages that need to be sent immediately.
1163
+
1164
+ Parameters:
1165
+ message: The urgent transport message payload.
1166
+ """
1167
+
1168
+ message: Any
1169
+
1170
+ def __str__(self):
1171
+ return f"{self.name}(message: {self.message})"
1172
+
1173
+
1174
+ @dataclass
1175
+ class TransportMessageUrgentFrame(OutputTransportMessageUrgentFrame):
1176
+ """Frame for urgent transport messages that need to be sent immediately.
1177
+
1178
+ .. deprecated:: 0.0.87
1179
+ This frame is deprecated and will be removed in a future version.
1180
+ Instead, use `OutputTransportMessageUrgentFrame`.
1181
+
1182
+ Parameters:
1183
+ message: The urgent transport message payload.
1184
+ """
1185
+
1186
+ def __post_init__(self):
1187
+ super().__post_init__()
1188
+ import warnings
1189
+
1190
+ with warnings.catch_warnings():
1191
+ warnings.simplefilter("always")
1192
+ warnings.warn(
1193
+ "TransportMessageUrgentFrame is deprecated and will be removed in a future version. "
1194
+ "Instead, use OutputTransportMessageFrame.",
1195
+ DeprecationWarning,
1196
+ stacklevel=2,
1197
+ )
1146
1198
 
1147
1199
 
1148
1200
  @dataclass
@@ -1253,23 +1305,6 @@ class UserImageRawFrame(InputImageRawFrame):
1253
1305
  return f"{self.name}(pts: {pts}, user: {self.user_id}, source: {self.transport_source}, size: {self.size}, format: {self.format}, request: {self.request})"
1254
1306
 
1255
1307
 
1256
- @dataclass
1257
- class VisionImageRawFrame(InputImageRawFrame):
1258
- """Image frame for vision/image analysis with associated text prompt.
1259
-
1260
- An image with an associated text to ask for a description of it.
1261
-
1262
- Parameters:
1263
- text: Optional text prompt describing what to analyze in the image.
1264
- """
1265
-
1266
- text: Optional[str] = None
1267
-
1268
- def __str__(self):
1269
- pts = format_pts(self.pts)
1270
- return f"{self.name}(pts: {pts}, text: [{self.text}], size: {self.size}, format: {self.format})"
1271
-
1272
-
1273
1308
  @dataclass
1274
1309
  class InputDTMFFrame(DTMFFrame, SystemFrame):
1275
1310
  """DTMF keypress input frame from transport."""
@@ -1571,15 +1606,13 @@ class MixerEnableFrame(MixerControlFrame):
1571
1606
 
1572
1607
 
1573
1608
  @dataclass
1574
- class StartUserIdleProcessorFrame(SystemFrame):
1575
- """Frame to start the UserIdleProcessor monitoring."""
1609
+ class ServiceSwitcherFrame(ControlFrame):
1610
+ """A base class for frames that affect ServiceSwitcher behavior."""
1576
1611
 
1577
1612
 
1578
1613
  @dataclass
1579
- class ServiceSwitcherFrame(ControlFrame):
1580
- """A base class for frames that control ServiceSwitcher behavior."""
1581
-
1582
- pass
1614
+ class StartUserIdleProcessorFrame(SystemFrame):
1615
+ """Frame to start the UserIdleProcessor monitoring."""
1583
1616
 
1584
1617
 
1585
1618
  @dataclass
@@ -54,7 +54,7 @@ class DebugLogObserver(BaseObserver):
54
54
 
55
55
  Log frames with specific source/destination filters::
56
56
 
57
- from pipecat.frames.frames import StartInterruptionFrame, UserStartedSpeakingFrame, LLMTextFrame
57
+ from pipecat.frames.frames import InterruptionFrame, UserStartedSpeakingFrame, LLMTextFrame
58
58
  from pipecat.observers.loggers.debug_log_observer import DebugLogObserver, FrameEndpoint
59
59
  from pipecat.transports.base_output import BaseOutputTransport
60
60
  from pipecat.services.stt_service import STTService
@@ -62,8 +62,8 @@ class DebugLogObserver(BaseObserver):
62
62
  observers=[
63
63
  DebugLogObserver(
64
64
  frame_types={
65
- # Only log StartInterruptionFrame when source is BaseOutputTransport
66
- StartInterruptionFrame: (BaseOutputTransport, FrameEndpoint.SOURCE),
65
+ # Only log InterruptionFrame when source is BaseOutputTransport
66
+ InterruptionFrame: (BaseOutputTransport, FrameEndpoint.SOURCE),
67
67
  # Only log UserStartedSpeakingFrame when destination is STTService
68
68
  UserStartedSpeakingFrame: (STTService, FrameEndpoint.DESTINATION),
69
69
  # Log LLMTextFrame regardless of source or destination type
@@ -11,6 +11,7 @@ from loguru import logger
11
11
  from pipecat.frames.frames import (
12
12
  FunctionCallInProgressFrame,
13
13
  FunctionCallResultFrame,
14
+ LLMContextFrame,
14
15
  LLMFullResponseEndFrame,
15
16
  LLMFullResponseStartFrame,
16
17
  LLMMessagesFrame,
@@ -79,10 +80,13 @@ class LLMLogObserver(BaseObserver):
79
80
  f"🧠 {arrow} {dst} LLM MESSAGES FRAME: {frame.messages} at {time_sec:.2f}s"
80
81
  )
81
82
  # Log OpenAILLMContextFrame (input)
82
- elif isinstance(frame, OpenAILLMContextFrame):
83
- logger.debug(
84
- f"🧠 {arrow} {dst} LLM CONTEXT FRAME: {frame.context.messages} at {time_sec:.2f}s"
83
+ elif isinstance(frame, (LLMContextFrame, OpenAILLMContextFrame)):
84
+ messages = (
85
+ frame.context.messages
86
+ if isinstance(frame, OpenAILLMContextFrame)
87
+ else frame.context.get_messages()
85
88
  )
89
+ logger.debug(f"🧠 {arrow} {dst} LLM CONTEXT FRAME: {messages} at {time_sec:.2f}s")
86
90
  # Log function call result (input)
87
91
  elif isinstance(frame, FunctionCallResultFrame):
88
92
  logger.debug(
@@ -61,17 +61,29 @@ class UserBotLatencyLogObserver(BaseObserver):
61
61
  elif isinstance(data.frame, UserStoppedSpeakingFrame):
62
62
  self._user_stopped_time = time.time()
63
63
  elif isinstance(data.frame, (EndFrame, CancelFrame)):
64
- if self._latencies:
65
- avg_latency = mean(self._latencies)
66
- min_latency = min(self._latencies)
67
- max_latency = max(self._latencies)
68
- logger.info(
69
- f"⏱️ LATENCY FROM USER STOPPED SPEAKING TO BOT STARTED SPEAKING - Avg: {avg_latency:.3f}s, Min: {min_latency:.3f}s, Max: {max_latency:.3f}s"
70
- )
64
+ self._log_summary()
71
65
  elif isinstance(data.frame, BotStartedSpeakingFrame) and self._user_stopped_time:
72
66
  latency = time.time() - self._user_stopped_time
73
67
  self._user_stopped_time = 0
74
68
  self._latencies.append(latency)
75
- logger.debug(
76
- f"⏱️ LATENCY FROM USER STOPPED SPEAKING TO BOT STARTED SPEAKING: {latency:.3f}s"
77
- )
69
+ self._log_latency(latency)
70
+
71
+ def _log_summary(self):
72
+ if not self._latencies:
73
+ return
74
+ avg_latency = mean(self._latencies)
75
+ min_latency = min(self._latencies)
76
+ max_latency = max(self._latencies)
77
+ logger.info(
78
+ f"⏱️ LATENCY FROM USER STOPPED SPEAKING TO BOT STARTED SPEAKING - Avg: {avg_latency:.3f}s, Min: {min_latency:.3f}s, Max: {max_latency:.3f}s"
79
+ )
80
+
81
+ def _log_latency(self, latency: float):
82
+ """Log the latency.
83
+
84
+ Args:
85
+ latency: The latency to log.
86
+ """
87
+ logger.debug(
88
+ f"⏱️ LATENCY FROM USER STOPPED SPEAKING TO BOT STARTED SPEAKING: {latency:.3f}s"
89
+ )
@@ -106,13 +106,21 @@ class PipelineRunner(BaseObject):
106
106
 
107
107
  def _setup_sigint(self):
108
108
  """Set up signal handlers for graceful shutdown."""
109
- loop = asyncio.get_running_loop()
110
- loop.add_signal_handler(signal.SIGINT, lambda *args: self._sig_handler())
109
+ try:
110
+ loop = asyncio.get_running_loop()
111
+ loop.add_signal_handler(signal.SIGINT, lambda *args: self._sig_handler())
112
+ except NotImplementedError:
113
+ # Windows fallback
114
+ signal.signal(signal.SIGINT, lambda s, f: self._sig_handler())
111
115
 
112
116
  def _setup_sigterm(self):
113
117
  """Set up signal handlers for graceful shutdown."""
114
- loop = asyncio.get_running_loop()
115
- loop.add_signal_handler(signal.SIGTERM, lambda *args: self._sig_handler())
118
+ try:
119
+ loop = asyncio.get_running_loop()
120
+ loop.add_signal_handler(signal.SIGTERM, lambda *args: self._sig_handler())
121
+ except NotImplementedError:
122
+ # Windows fallback
123
+ signal.signal(signal.SIGTERM, lambda s, f: self._sig_handler())
116
124
 
117
125
  def _sig_handler(self):
118
126
  """Handle interrupt signals by cancelling all tasks."""
@@ -6,9 +6,15 @@
6
6
 
7
7
  """Service switcher for switching between different services at runtime, with different switching strategies."""
8
8
 
9
+ from dataclasses import dataclass
9
10
  from typing import Any, Generic, List, Optional, Type, TypeVar
10
11
 
11
- from pipecat.frames.frames import Frame, ManuallySwitchServiceFrame, ServiceSwitcherFrame
12
+ from pipecat.frames.frames import (
13
+ ControlFrame,
14
+ Frame,
15
+ ManuallySwitchServiceFrame,
16
+ ServiceSwitcherFrame,
17
+ )
12
18
  from pipecat.pipeline.parallel_pipeline import ParallelPipeline
13
19
  from pipecat.processors.filters.function_filter import FunctionFilter
14
20
  from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
@@ -22,19 +28,6 @@ class ServiceSwitcherStrategy:
22
28
  self.services = services
23
29
  self.active_service: Optional[FrameProcessor] = None
24
30
 
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
31
  def handle_frame(self, frame: ServiceSwitcherFrame, direction: FrameDirection):
39
32
  """Handle a frame that controls service switching.
40
33
 
@@ -60,17 +53,6 @@ class ServiceSwitcherStrategyManual(ServiceSwitcherStrategy):
60
53
  super().__init__(services)
61
54
  self.active_service = services[0] if services else None
62
55
 
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
56
  def handle_frame(self, frame: ServiceSwitcherFrame, direction: FrameDirection):
75
57
  """Handle a frame that controls service switching.
76
58
 
@@ -79,20 +61,21 @@ class ServiceSwitcherStrategyManual(ServiceSwitcherStrategy):
79
61
  direction: The direction of the frame (upstream or downstream).
80
62
  """
81
63
  if isinstance(frame, ManuallySwitchServiceFrame):
82
- self._set_active(frame.service)
64
+ self._set_active_if_available(frame.service)
83
65
  else:
84
66
  raise ValueError(f"Unsupported frame type: {type(frame)}")
85
67
 
86
- def _set_active(self, service: FrameProcessor):
87
- """Set the active service to the given one.
68
+ def _set_active_if_available(self, service: FrameProcessor):
69
+ """Set the active service to the given one, if it is in the list of available services.
70
+
71
+ If it's not in the list, the request is ignored, as it may have been
72
+ intended for another ServiceSwitcher in the pipeline.
88
73
 
89
74
  Args:
90
75
  service: The service to set as active.
91
76
  """
92
77
  if service in self.services:
93
78
  self.active_service = service
94
- else:
95
- raise ValueError(f"Service {service} is not in the list of available services.")
96
79
 
97
80
 
98
81
  StrategyType = TypeVar("StrategyType", bound=ServiceSwitcherStrategy)
@@ -108,6 +91,43 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
108
91
  self.services = services
109
92
  self.strategy = strategy
110
93
 
94
+ class ServiceSwitcherFilter(FunctionFilter):
95
+ """An internal filter that allows frames to pass through to the wrapped service only if it's the active service."""
96
+
97
+ def __init__(
98
+ self,
99
+ wrapped_service: FrameProcessor,
100
+ active_service: FrameProcessor,
101
+ direction: FrameDirection,
102
+ ):
103
+ """Initialize the service switcher filter with a strategy and direction."""
104
+
105
+ async def filter(_: Frame) -> bool:
106
+ return self._wrapped_service == self._active_service
107
+
108
+ super().__init__(filter, direction)
109
+ self._wrapped_service = wrapped_service
110
+ self._active_service = active_service
111
+
112
+ async def process_frame(self, frame, direction):
113
+ """Process a frame through the filter, handling special internal filter-updating frames."""
114
+ if isinstance(frame, ServiceSwitcher.ServiceSwitcherFilterFrame):
115
+ self._active_service = frame.active_service
116
+ # Two ServiceSwitcherFilters "sandwich" a service. Push the
117
+ # frame only to update the other side of the sandwich, but
118
+ # otherwise don't let it leave the sandwich.
119
+ if direction == self._direction:
120
+ await self.push_frame(frame, direction)
121
+ return
122
+
123
+ await super().process_frame(frame, direction)
124
+
125
+ @dataclass
126
+ class ServiceSwitcherFilterFrame(ControlFrame):
127
+ """An internal frame used by ServiceSwitcher to filter frames based on active service."""
128
+
129
+ active_service: FrameProcessor
130
+
111
131
  @staticmethod
112
132
  def _make_pipeline_definitions(
113
133
  services: List[FrameProcessor], strategy: ServiceSwitcherStrategy
@@ -121,14 +141,18 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
121
141
  def _make_pipeline_definition(
122
142
  service: FrameProcessor, strategy: ServiceSwitcherStrategy
123
143
  ) -> Any:
124
- async def filter(frame) -> bool:
125
- _ = frame
126
- return strategy.is_active(service)
127
-
128
144
  return [
129
- FunctionFilter(filter, direction=FrameDirection.DOWNSTREAM),
145
+ ServiceSwitcher.ServiceSwitcherFilter(
146
+ wrapped_service=service,
147
+ active_service=strategy.active_service,
148
+ direction=FrameDirection.DOWNSTREAM,
149
+ ),
130
150
  service,
131
- FunctionFilter(filter, direction=FrameDirection.UPSTREAM),
151
+ ServiceSwitcher.ServiceSwitcherFilter(
152
+ wrapped_service=service,
153
+ active_service=strategy.active_service,
154
+ direction=FrameDirection.UPSTREAM,
155
+ ),
132
156
  ]
133
157
 
134
158
  async def process_frame(self, frame: Frame, direction: FrameDirection):
@@ -142,3 +166,7 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
142
166
 
143
167
  if isinstance(frame, ServiceSwitcherFrame):
144
168
  self.strategy.handle_frame(frame, direction)
169
+ service_switcher_filter_frame = ServiceSwitcher.ServiceSwitcherFilterFrame(
170
+ active_service=self.strategy.active_service
171
+ )
172
+ await super().process_frame(service_switcher_filter_frame, direction)