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.
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/METADATA +8 -3
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/RECORD +106 -79
- pipecat/adapters/base_llm_adapter.py +44 -6
- pipecat/adapters/services/anthropic_adapter.py +302 -2
- pipecat/adapters/services/aws_nova_sonic_adapter.py +40 -2
- pipecat/adapters/services/bedrock_adapter.py +40 -2
- pipecat/adapters/services/gemini_adapter.py +276 -6
- pipecat/adapters/services/open_ai_adapter.py +88 -7
- pipecat/adapters/services/open_ai_realtime_adapter.py +39 -1
- pipecat/audio/dtmf/__init__.py +0 -0
- pipecat/audio/dtmf/types.py +47 -0
- pipecat/audio/dtmf/utils.py +70 -0
- pipecat/audio/filters/aic_filter.py +199 -0
- pipecat/audio/utils.py +9 -7
- pipecat/extensions/ivr/__init__.py +0 -0
- pipecat/extensions/ivr/ivr_navigator.py +452 -0
- pipecat/frames/frames.py +156 -43
- pipecat/pipeline/llm_switcher.py +76 -0
- pipecat/pipeline/parallel_pipeline.py +3 -3
- pipecat/pipeline/service_switcher.py +144 -0
- pipecat/pipeline/task.py +68 -28
- pipecat/pipeline/task_observer.py +10 -0
- pipecat/processors/aggregators/dtmf_aggregator.py +2 -2
- pipecat/processors/aggregators/llm_context.py +277 -0
- pipecat/processors/aggregators/llm_response.py +48 -15
- pipecat/processors/aggregators/llm_response_universal.py +840 -0
- pipecat/processors/aggregators/openai_llm_context.py +3 -3
- pipecat/processors/dtmf_aggregator.py +0 -2
- pipecat/processors/filters/stt_mute_filter.py +0 -2
- pipecat/processors/frame_processor.py +18 -11
- pipecat/processors/frameworks/rtvi.py +17 -10
- pipecat/processors/metrics/sentry.py +2 -0
- pipecat/runner/daily.py +137 -36
- pipecat/runner/run.py +1 -1
- pipecat/runner/utils.py +7 -7
- pipecat/serializers/asterisk.py +20 -4
- pipecat/serializers/exotel.py +1 -1
- pipecat/serializers/plivo.py +1 -1
- pipecat/serializers/telnyx.py +1 -1
- pipecat/serializers/twilio.py +1 -1
- pipecat/services/__init__.py +2 -2
- pipecat/services/anthropic/llm.py +113 -28
- pipecat/services/asyncai/tts.py +4 -0
- pipecat/services/aws/llm.py +82 -8
- pipecat/services/aws/tts.py +0 -10
- pipecat/services/aws_nova_sonic/aws.py +5 -0
- pipecat/services/cartesia/tts.py +28 -16
- pipecat/services/cerebras/llm.py +15 -10
- pipecat/services/deepgram/stt.py +8 -0
- pipecat/services/deepseek/llm.py +13 -8
- pipecat/services/fireworks/llm.py +13 -8
- pipecat/services/fish/tts.py +8 -6
- pipecat/services/gemini_multimodal_live/gemini.py +5 -0
- pipecat/services/gladia/config.py +7 -1
- pipecat/services/gladia/stt.py +23 -15
- pipecat/services/google/llm.py +159 -59
- pipecat/services/google/llm_openai.py +18 -3
- pipecat/services/grok/llm.py +2 -1
- pipecat/services/llm_service.py +38 -3
- pipecat/services/mem0/memory.py +2 -1
- pipecat/services/mistral/llm.py +5 -6
- pipecat/services/nim/llm.py +2 -1
- pipecat/services/openai/base_llm.py +88 -26
- pipecat/services/openai/image.py +6 -1
- pipecat/services/openai_realtime_beta/openai.py +5 -2
- pipecat/services/openpipe/llm.py +6 -8
- pipecat/services/perplexity/llm.py +13 -8
- pipecat/services/playht/tts.py +9 -6
- pipecat/services/rime/tts.py +1 -1
- pipecat/services/sambanova/llm.py +18 -13
- pipecat/services/sarvam/tts.py +415 -10
- pipecat/services/speechmatics/stt.py +2 -2
- pipecat/services/tavus/video.py +1 -1
- pipecat/services/tts_service.py +15 -5
- pipecat/services/vistaar/llm.py +2 -5
- pipecat/transports/base_input.py +32 -19
- pipecat/transports/base_output.py +39 -5
- pipecat/transports/daily/__init__.py +0 -0
- pipecat/transports/daily/transport.py +2371 -0
- pipecat/transports/daily/utils.py +410 -0
- pipecat/transports/livekit/__init__.py +0 -0
- pipecat/transports/livekit/transport.py +1042 -0
- pipecat/transports/network/fastapi_websocket.py +12 -546
- pipecat/transports/network/small_webrtc.py +12 -922
- pipecat/transports/network/webrtc_connection.py +9 -595
- pipecat/transports/network/websocket_client.py +12 -481
- pipecat/transports/network/websocket_server.py +12 -487
- pipecat/transports/services/daily.py +9 -2334
- pipecat/transports/services/helpers/daily_rest.py +12 -396
- pipecat/transports/services/livekit.py +12 -975
- pipecat/transports/services/tavus.py +12 -757
- pipecat/transports/smallwebrtc/__init__.py +0 -0
- pipecat/transports/smallwebrtc/connection.py +612 -0
- pipecat/transports/smallwebrtc/transport.py +936 -0
- pipecat/transports/tavus/__init__.py +0 -0
- pipecat/transports/tavus/transport.py +770 -0
- pipecat/transports/websocket/__init__.py +0 -0
- pipecat/transports/websocket/client.py +494 -0
- pipecat/transports/websocket/fastapi.py +559 -0
- pipecat/transports/websocket/server.py +500 -0
- pipecat/transports/whatsapp/__init__.py +0 -0
- pipecat/transports/whatsapp/api.py +345 -0
- pipecat/transports/whatsapp/client.py +364 -0
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/WHEEL +0 -0
- {dv_pipecat_ai-0.0.82.dev815.dist-info → dv_pipecat_ai-0.0.82.dev857.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
535
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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)
|