diff --git a/pyrightconfig.json b/pyrightconfig.json index 31bac33c8..cd1a69be2 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -11,6 +11,7 @@ "src/pipecat/observers", "src/pipecat/pipeline", "src/pipecat/runner", + "src/pipecat/serializers", "src/pipecat/tests", "src/pipecat/transcriptions", "src/pipecat/turns", @@ -21,7 +22,6 @@ "src/pipecat/adapters", "src/pipecat/audio", "src/pipecat/processors", - "src/pipecat/serializers", "src/pipecat/services", "src/pipecat/transports", "tests" diff --git a/src/pipecat/serializers/exotel.py b/src/pipecat/serializers/exotel.py index ff2510f57..90d28270a 100644 --- a/src/pipecat/serializers/exotel.py +++ b/src/pipecat/serializers/exotel.py @@ -59,7 +59,9 @@ class ExotelFrameSerializer(FrameSerializer): call_sid: The associated Exotel Call SID (optional, not used in this implementation). params: Configuration parameters. """ - super().__init__(params or ExotelFrameSerializer.InputParams()) + params = params or ExotelFrameSerializer.InputParams() + super().__init__(params) + self._params: ExotelFrameSerializer.InputParams = params self._stream_sid = stream_sid self._call_sid = call_sid diff --git a/src/pipecat/serializers/genesys.py b/src/pipecat/serializers/genesys.py index e52abc6aa..9fca1f89a 100644 --- a/src/pipecat/serializers/genesys.py +++ b/src/pipecat/serializers/genesys.py @@ -166,7 +166,9 @@ class GenesysAudioHookSerializer(FrameSerializer): params: Configuration parameters. **kwargs: Additional arguments passed to BaseObject (e.g., name). """ - super().__init__(params or GenesysAudioHookSerializer.InputParams(), **kwargs) + params = params or GenesysAudioHookSerializer.InputParams() + super().__init__(params, **kwargs) + self._params: GenesysAudioHookSerializer.InputParams = params self._genesys_sample_rate = self._params.genesys_sample_rate self._sample_rate = 0 # Pipeline input rate, set in setup() diff --git a/src/pipecat/serializers/plivo.py b/src/pipecat/serializers/plivo.py index e86d18b8c..0df2d6753 100644 --- a/src/pipecat/serializers/plivo.py +++ b/src/pipecat/serializers/plivo.py @@ -8,6 +8,7 @@ import base64 import json +from typing import cast from loguru import logger @@ -71,7 +72,24 @@ class PlivoFrameSerializer(FrameSerializer): auth_token: Plivo auth token (required for auto hang-up). params: Configuration parameters. """ - super().__init__(params or PlivoFrameSerializer.InputParams()) + params = params or PlivoFrameSerializer.InputParams() + super().__init__(params) + self._params: PlivoFrameSerializer.InputParams = params + + # Validate hangup-related parameters if auto_hang_up is enabled + if self._params.auto_hang_up: + missing_credentials = [] + if not call_id: + missing_credentials.append("call_id") + if not auth_id: + missing_credentials.append("auth_id") + if not auth_token: + missing_credentials.append("auth_token") + + if missing_credentials: + raise ValueError( + f"auto_hang_up is enabled but missing required parameters: {', '.join(missing_credentials)}" + ) self._stream_id = stream_id self._call_id = call_id @@ -152,23 +170,11 @@ class PlivoFrameSerializer(FrameSerializer): try: import aiohttp - auth_id = self._auth_id - auth_token = self._auth_token - call_id = self._call_id - - if not call_id or not auth_id or not auth_token: - missing = [] - if not call_id: - missing.append("call_id") - if not auth_id: - missing.append("auth_id") - if not auth_token: - missing.append("auth_token") - - logger.warning( - f"Cannot hang up Plivo call: missing required parameters: {', '.join(missing)}" - ) - return + # __init__ guarantees these are non-None whenever auto_hang_up is True, + # which is the only path that reaches this method. + auth_id = cast(str, self._auth_id) + auth_token = cast(str, self._auth_token) + call_id = cast(str, self._call_id) # Plivo API endpoint for hanging up calls endpoint = f"https://api.plivo.com/v1/Account/{auth_id}/Call/{call_id}/" diff --git a/src/pipecat/serializers/protobuf.py b/src/pipecat/serializers/protobuf.py index 78d20fa24..44c660042 100644 --- a/src/pipecat/serializers/protobuf.py +++ b/src/pipecat/serializers/protobuf.py @@ -83,23 +83,24 @@ class ProtobufFrameSerializer(FrameSerializer): Serialized frame as bytes, or None if frame type is not serializable. """ # Wrapping this messages as a JSONFrame to send + serializable: Frame | MessageFrame = frame if isinstance(frame, (OutputTransportMessageFrame, OutputTransportMessageUrgentFrame)): if self.should_ignore_frame(frame): return None - frame = MessageFrame( + serializable = MessageFrame( data=json.dumps(frame.message), ) - proto_frame = frame_protos.Frame() - if type(frame) not in self.SERIALIZABLE_TYPES: - logger.warning(f"Frame type {type(frame)} is not serializable") + proto_frame = frame_protos.Frame() # type: ignore[attr-defined] + if type(serializable) not in self.SERIALIZABLE_TYPES: + logger.warning(f"Frame type {type(serializable)} is not serializable") return None # ignoring linter errors; we check that type(frame) is in this dict above - proto_optional_name = self.SERIALIZABLE_TYPES[type(frame)] # type: ignore + proto_optional_name = self.SERIALIZABLE_TYPES[type(serializable)] # type: ignore proto_attr = getattr(proto_frame, proto_optional_name) - for field in dataclasses.fields(frame): # type: ignore - value = getattr(frame, field.name) + for field in dataclasses.fields(serializable): # type: ignore + value = getattr(serializable, field.name) if value and hasattr(proto_attr, field.name): setattr(proto_attr, field.name, value) @@ -114,7 +115,7 @@ class ProtobufFrameSerializer(FrameSerializer): Returns: Deserialized frame instance, or None if deserialization fails. """ - proto = frame_protos.Frame.FromString(data) + proto = frame_protos.Frame.FromString(data) # type: ignore[attr-defined] which = proto.WhichOneof("frame") if which not in self.DESERIALIZABLE_FIELDS: logger.error("Unable to deserialize a valid frame") diff --git a/src/pipecat/serializers/telnyx.py b/src/pipecat/serializers/telnyx.py index 0d74664ab..4d769cada 100644 --- a/src/pipecat/serializers/telnyx.py +++ b/src/pipecat/serializers/telnyx.py @@ -8,10 +8,10 @@ import base64 import json +from typing import cast import aiohttp from loguru import logger -from pydantic import BaseModel from pipecat.audio.dtmf.types import KeypadEntry from pipecat.audio.utils import ( @@ -46,7 +46,7 @@ class TelnyxFrameSerializer(FrameSerializer): credentials to be provided. """ - class InputParams(BaseModel): + class InputParams(FrameSerializer.InputParams): """Configuration parameters for TelnyxFrameSerializer. Parameters: @@ -82,10 +82,26 @@ class TelnyxFrameSerializer(FrameSerializer): api_key: Your Telnyx API key (required for auto hang-up). params: Configuration parameters. """ + params = params or TelnyxFrameSerializer.InputParams() + super().__init__(params) + self._params: TelnyxFrameSerializer.InputParams = params + + # Validate hangup-related parameters if auto_hang_up is enabled + if self._params.auto_hang_up: + missing_credentials = [] + if not call_control_id: + missing_credentials.append("call_control_id") + if not api_key: + missing_credentials.append("api_key") + + if missing_credentials: + raise ValueError( + f"auto_hang_up is enabled but missing required parameters: {', '.join(missing_credentials)}" + ) + self._stream_id = stream_id self._call_control_id = call_control_id self._api_key = api_key - self._params = params or TelnyxFrameSerializer.InputParams() self._params.outbound_encoding = outbound_encoding self._params.inbound_encoding = inbound_encoding @@ -163,14 +179,10 @@ class TelnyxFrameSerializer(FrameSerializer): async def _hang_up_call(self): """Hang up the Telnyx call using Telnyx's REST API.""" try: - call_control_id = self._call_control_id - api_key = self._api_key - - if not call_control_id or not api_key: - logger.warning( - "Cannot hang up Telnyx call: call_control_id and api_key must be provided" - ) - return + # __init__ guarantees these are non-None whenever auto_hang_up is True, + # which is the only path that reaches this method. + call_control_id = cast(str, self._call_control_id) + api_key = cast(str, self._api_key) # Telnyx API endpoint for hanging up a call endpoint = f"https://api.telnyx.com/v2/calls/{call_control_id}/actions/hangup" diff --git a/src/pipecat/serializers/twilio.py b/src/pipecat/serializers/twilio.py index 857610b4e..d93543c8e 100644 --- a/src/pipecat/serializers/twilio.py +++ b/src/pipecat/serializers/twilio.py @@ -8,6 +8,7 @@ import base64 import json +from typing import cast from loguru import logger @@ -75,7 +76,9 @@ class TwilioFrameSerializer(FrameSerializer): edge: Twilio edge location (e.g., "sydney", "dublin"). Must be specified with region. params: Configuration parameters. """ - super().__init__(params or TwilioFrameSerializer.InputParams()) + params = params or TwilioFrameSerializer.InputParams() + super().__init__(params) + self._params: TwilioFrameSerializer.InputParams = params # Validate hangup-related parameters if auto_hang_up is enabled if self._params.auto_hang_up: @@ -178,9 +181,11 @@ class TwilioFrameSerializer(FrameSerializer): try: import aiohttp - account_sid = self._account_sid - auth_token = self._auth_token - call_sid = self._call_sid + # __init__ guarantees these are non-None whenever auto_hang_up is True, + # which is the only path that reaches this method. + account_sid = cast(str, self._account_sid) + auth_token = cast(str, self._auth_token) + call_sid = cast(str, self._call_sid) region = self._region edge = self._edge diff --git a/src/pipecat/serializers/vonage.py b/src/pipecat/serializers/vonage.py index d778cf62c..7d82a717c 100644 --- a/src/pipecat/serializers/vonage.py +++ b/src/pipecat/serializers/vonage.py @@ -54,7 +54,9 @@ class VonageFrameSerializer(FrameSerializer): Args: params: Configuration parameters. """ - super().__init__(params or VonageFrameSerializer.InputParams()) + params = params or VonageFrameSerializer.InputParams() + super().__init__(params) + self._params: VonageFrameSerializer.InputParams = params self._vonage_sample_rate = self._params.vonage_sample_rate self._sample_rate = 0 # Pipeline input rate