From 31ff07916f7223f2dfb69b664d1224e6981b3708 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Wed, 29 Apr 2026 12:48:16 -0400 Subject: [PATCH] fix: clear 10 more services from pyright ignore list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A second pass over the low-error-count files in the ignore list. Drops 10 files (77 → 67) and full-pyright errors from 580 → 555. Default pyright stays clean. Three coherent shapes plus a handful of one-offs: `Language | str | None` → `Language | None` at STT frame boundaries. `assert_given(self._settings.language)` returns `Language | str | None` (strips `_NotGiven`, keeps the rest), but `TranscriptionFrame.language` expects `Language | None`. In practice both `_settings.language` and SDK-supplied codes resolve to a `Language` enum value, but technically they could be raw strings — and `Language` is a StrEnum, so downstream consumers (which mostly compare/serialize as strings) handle either. Used `cast("Language | None", ...)` at each call site rather than a runtime-validating helper, so an unrecognised code (e.g. one we haven't added to the enum yet) still flows through unchanged. Cleared azure/stt.py, aws/stt.py, gradium/stt.py; mistral/stt.py keeps the cast at the SDK boundary (storing under `_detected_language: Language | None`) but stays in the ignore list because of two unrelated Optional-access errors. aiobotocore `async with` stub gap. `aioboto3.Session().client(...)` is an async context manager at runtime but its stubs don't advertise `__aenter__`/`__aexit__` to pyright. Scoped `# pyright: ignore[reportGeneralTypeIssues]` on the two affected sites: aws/agent_core.py and aws/tts.py. aws/tts.py also had a latent bug on the no-`AudioStream` path: the original code set `audio_data = None` and then crashed in `resample(...)` and `len(audio_data)` below; replaced with an early `return` after logging — matches the convention elsewhere (OpenAI TTS, etc.) of not recording usage metrics on the error path. heygen `event_id: str | None` → `str` at transport→client boundary. Three call sites in transports/heygen/transport.py passed `self._event_id` (`str | None`) into client methods that take `str`. Added a guard at each: `agent_speak_end` and `interrupt` only fire when `_event_id` is set; `write_audio_frame` warn-and-drops when there's no active bot event rather than sending a malformed message. `OpenAIResponsesLLMInvocationParams` TypedDict. `get_llm_invocation_params` always sets both `input` and `tools` in the same dict literal, but the TypedDict was `total=False` so direct subscript access (`invocation_params["input"]`) tripped `reportTypedDictNotRequiredAccess` in services/openai/responses/llm.py. Marked both keys `Required[...]`; `instructions` stays non-required since it's only added when a system instruction is present. Latent bug in heygen/api_interactive_avatar.py: the code accessed `request_data.voice.voiceId` and `request_data.voice.elevenlabsSettings`, but those names are Pydantic *aliases*; the actual attribute names (used for attribute access) are `voice_id` and `elevenlabs_settings`. Switched to the field names — those camelCase accesses would have raised AttributeError at runtime if `voice` was set. Other small fixes: - assemblyai/stt.py: the deprecated `connection_params=` init path was reading `formatted_finals` and `word_finalization_max_wait_time` off `AssemblyAIConnectionParams`, but those fields were never on the deprecated input model — they were added to Settings later. Removed the reads (with a comment noting they're only available via the canonical `settings=...` API); the deprecated input model is unchanged. - rtvi/processor.py: two `about: Mapping[str, Any] = None` parameter signatures — declared `Mapping`, defaulted to `None`, and both function bodies already handled the None case. Widened to `Mapping[str, Any] | None = None`. - aws/stt.py: `subprotocols=["mqtt"]` failed against websockets' `Sequence[Subprotocol] | None` (Subprotocol is a NewType wrapper). Wrapped: `subprotocols=[Subprotocol("mqtt")]`. Files dropped from the ignore list (77 → 67): processors/frameworks/rtvi/processor.py, services/assemblyai/stt.py, services/aws/agent_core.py, services/aws/stt.py, services/aws/tts.py, services/azure/stt.py, services/gradium/stt.py, services/heygen/api_interactive_avatar.py, services/openai/responses/llm.py, transports/heygen/transport.py. --- pyrightconfig.json | 10 ---------- .../services/open_ai_responses_adapter.py | 8 +++++--- .../processors/frameworks/rtvi/processor.py | 4 ++-- src/pipecat/services/assemblyai/stt.py | 9 +++++---- src/pipecat/services/aws/agent_core.py | 6 +++++- src/pipecat/services/aws/stt.py | 11 ++++++++--- src/pipecat/services/aws/tts.py | 8 ++++++-- src/pipecat/services/azure/stt.py | 16 +++++++++++----- src/pipecat/services/gradium/stt.py | 16 +++++++++++----- .../services/heygen/api_interactive_avatar.py | 4 ++-- src/pipecat/services/mistral/stt.py | 9 ++++++--- src/pipecat/transports/heygen/transport.py | 13 ++++++++++--- 12 files changed, 71 insertions(+), 43 deletions(-) diff --git a/pyrightconfig.json b/pyrightconfig.json index 0e54af4c7..c928acdac 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -17,18 +17,12 @@ "src/pipecat/processors/frame_processor.py", "src/pipecat/processors/frameworks/langchain.py", "src/pipecat/processors/frameworks/rtvi/observer.py", - "src/pipecat/processors/frameworks/rtvi/processor.py", "src/pipecat/processors/frameworks/strands_agents.py", "src/pipecat/services/anthropic/llm.py", - "src/pipecat/services/assemblyai/stt.py", - "src/pipecat/services/aws/agent_core.py", "src/pipecat/services/aws/llm.py", "src/pipecat/services/aws/nova_sonic/llm.py", "src/pipecat/services/aws/sagemaker/bidi_client.py", - "src/pipecat/services/aws/stt.py", - "src/pipecat/services/aws/tts.py", "src/pipecat/services/aws/utils.py", - "src/pipecat/services/azure/stt.py", "src/pipecat/services/azure/tts.py", "src/pipecat/services/deepgram/flux/base.py", "src/pipecat/services/deepgram/flux/sagemaker/stt.py", @@ -42,8 +36,6 @@ "src/pipecat/services/google/llm.py", "src/pipecat/services/google/stt.py", "src/pipecat/services/google/tts.py", - "src/pipecat/services/gradium/stt.py", - "src/pipecat/services/heygen/api_interactive_avatar.py", "src/pipecat/services/heygen/client.py", "src/pipecat/services/heygen/video.py", "src/pipecat/services/inworld/realtime/llm.py", @@ -57,7 +49,6 @@ "src/pipecat/services/openai/base_llm.py", "src/pipecat/services/openai/image.py", "src/pipecat/services/openai/realtime/llm.py", - "src/pipecat/services/openai/responses/llm.py", "src/pipecat/services/openai/stt.py", "src/pipecat/services/rime/tts.py", "src/pipecat/services/sambanova/llm.py", @@ -72,7 +63,6 @@ "src/pipecat/services/xai/realtime/llm.py", "src/pipecat/transports/base_output.py", "src/pipecat/transports/daily/transport.py", - "src/pipecat/transports/heygen/transport.py", "src/pipecat/transports/lemonslice/transport.py", "src/pipecat/transports/livekit/transport.py", "src/pipecat/transports/smallwebrtc/connection.py", diff --git a/src/pipecat/adapters/services/open_ai_responses_adapter.py b/src/pipecat/adapters/services/open_ai_responses_adapter.py index d19e7d117..6fa03d2d7 100644 --- a/src/pipecat/adapters/services/open_ai_responses_adapter.py +++ b/src/pipecat/adapters/services/open_ai_responses_adapter.py @@ -6,7 +6,7 @@ """OpenAI Responses API adapter for Pipecat.""" -from typing import Any, TypedDict, cast +from typing import Any, Required, TypedDict, cast from openai._types import NotGiven as OpenAINotGiven from openai.types.responses import FunctionToolParam, ResponseInputItemParam, ToolParam @@ -23,8 +23,10 @@ from pipecat.processors.aggregators.llm_context import ( class OpenAIResponsesLLMInvocationParams(TypedDict, total=False): """Context-based parameters for invoking OpenAI Responses API.""" - input: list[ResponseInputItemParam] - tools: list[ToolParam] | OpenAINotGiven + # `input` and `tools` are always populated by `get_llm_invocation_params`; + # `instructions` is only set when a system instruction is present. + input: Required[list[ResponseInputItemParam]] + tools: Required[list[ToolParam] | OpenAINotGiven] instructions: str diff --git a/src/pipecat/processors/frameworks/rtvi/processor.py b/src/pipecat/processors/frameworks/rtvi/processor.py index 5586ec8ae..5f5e02568 100644 --- a/src/pipecat/processors/frameworks/rtvi/processor.py +++ b/src/pipecat/processors/frameworks/rtvi/processor.py @@ -102,7 +102,7 @@ class RTVIProcessor(FrameProcessor): self._client_ready = True await self._call_event_handler("on_client_ready") - async def set_bot_ready(self, about: Mapping[str, Any] = None): + async def set_bot_ready(self, about: Mapping[str, Any] | None = None): """Mark the bot as ready and send the bot-ready message. Args: @@ -404,7 +404,7 @@ class RTVIProcessor(FrameProcessor): ) await self.push_frame(frame) - async def _send_bot_ready(self, about: Mapping[str, Any] = None): + async def _send_bot_ready(self, about: Mapping[str, Any] | None = None): """Send the bot-ready message to the client. Args: diff --git a/src/pipecat/services/assemblyai/stt.py b/src/pipecat/services/assemblyai/stt.py index f87a5a0e5..5eda4b7f6 100644 --- a/src/pipecat/services/assemblyai/stt.py +++ b/src/pipecat/services/assemblyai/stt.py @@ -233,10 +233,11 @@ class AssemblyAISTTService(WebsocketSTTService): sample_rate = connection_params.sample_rate encoding = connection_params.encoding default_settings.model = connection_params.speech_model - default_settings.formatted_finals = connection_params.formatted_finals - default_settings.word_finalization_max_wait_time = ( - connection_params.word_finalization_max_wait_time - ) + # Note: `formatted_finals` and `word_finalization_max_wait_time` + # were added to Settings after this deprecated input model + # was frozen and have no equivalent on + # AssemblyAIConnectionParams; they are only configurable via + # the canonical `settings=...` API. default_settings.end_of_turn_confidence_threshold = ( connection_params.end_of_turn_confidence_threshold ) diff --git a/src/pipecat/services/aws/agent_core.py b/src/pipecat/services/aws/agent_core.py index 2f1560b7c..d43374b4a 100644 --- a/src/pipecat/services/aws/agent_core.py +++ b/src/pipecat/services/aws/agent_core.py @@ -201,7 +201,11 @@ class AWSAgentCoreProcessor(FrameProcessor): if not payload: return - async with self._aws_session.client("bedrock-agentcore", **self._aws_params) as client: + # aioboto3's `client()` is an async context manager but its stubs don't + # advertise `__aenter__` / `__aexit__` in a way pyright can see. + async with self._aws_session.client( # pyright: ignore[reportGeneralTypeIssues] + "bedrock-agentcore", **self._aws_params + ) as client: # Invoke the AgentCore agent response = await client.invoke_agent_runtime( agentRuntimeArn=self._agentArn, payload=payload.encode() diff --git a/src/pipecat/services/aws/stt.py b/src/pipecat/services/aws/stt.py index aa1fdf9bf..c482b1b00 100644 --- a/src/pipecat/services/aws/stt.py +++ b/src/pipecat/services/aws/stt.py @@ -16,7 +16,7 @@ import random import string from collections.abc import AsyncGenerator from dataclasses import dataclass -from typing import Any +from typing import Any, cast from loguru import logger @@ -38,6 +38,7 @@ from pipecat.utils.time import time_now_iso8601 from pipecat.utils.tracing.service_decorators import traced_stt try: + from websockets import Subprotocol from websockets.asyncio.client import connect as websocket_connect from websockets.protocol import State except ModuleNotFoundError as e: @@ -314,7 +315,7 @@ class AWSTranscribeSTTService(WebsocketSTTService): self._websocket = await websocket_connect( presigned_url, additional_headers=additional_headers, - subprotocols=["mqtt"], + subprotocols=[Subprotocol("mqtt")], ping_interval=None, ping_timeout=None, compression=None, @@ -534,7 +535,11 @@ class AWSTranscribeSTTService(WebsocketSTTService): is_final = not result.get("IsPartial", True) if transcript: - language = assert_given(self._settings.language) + # Technically `_settings.language` could be a raw string, but + # Language is a StrEnum so downstream handles either. + language = cast( + "Language | None", assert_given(self._settings.language) + ) if is_final: await self.push_frame( TranscriptionFrame( diff --git a/src/pipecat/services/aws/tts.py b/src/pipecat/services/aws/tts.py index a683a94ea..ec44202f9 100644 --- a/src/pipecat/services/aws/tts.py +++ b/src/pipecat/services/aws/tts.py @@ -345,7 +345,11 @@ class AWSPollyTTSService(TTSService): # Filter out None values filtered_params = {k: v for k, v in params.items() if v is not None} - async with self._aws_session.client("polly", **self._aws_params) as polly: + # aioboto3's `client()` is an async context manager but its stubs + # don't advertise `__aenter__` / `__aexit__` to pyright. + async with self._aws_session.client( # pyright: ignore[reportGeneralTypeIssues] + "polly", **self._aws_params + ) as polly: response = await polly.synthesize_speech(**filtered_params) if "AudioStream" in response: # Get the streaming body and read it @@ -353,7 +357,7 @@ class AWSPollyTTSService(TTSService): audio_data = await stream.read() else: logger.error(f"{self} No audio stream in response") - audio_data = None + return audio_data = await self._resampler.resample(audio_data, 16000, self.sample_rate) diff --git a/src/pipecat/services/azure/stt.py b/src/pipecat/services/azure/stt.py index e130fe980..be06e07cd 100644 --- a/src/pipecat/services/azure/stt.py +++ b/src/pipecat/services/azure/stt.py @@ -13,7 +13,7 @@ Speech SDK for real-time audio transcription. import asyncio from collections.abc import AsyncGenerator from dataclasses import dataclass -from typing import Any +from typing import Any, cast from loguru import logger @@ -280,8 +280,11 @@ class AzureSTTService(STTService): def _on_handle_recognized(self, event): if event.result.reason == ResultReason.RecognizedSpeech and len(event.result.text) > 0: - language = getattr(event.result, "language", None) or assert_given( - self._settings.language + # Technically either source could be a raw string, but Language is + # a StrEnum so downstream handles either. + language = cast( + "Language | None", + getattr(event.result, "language", None) or assert_given(self._settings.language), ) frame = TranscriptionFrame( event.result.text, @@ -297,8 +300,11 @@ class AzureSTTService(STTService): def _on_handle_recognizing(self, event): if event.result.reason == ResultReason.RecognizingSpeech and len(event.result.text) > 0: - language = getattr(event.result, "language", None) or assert_given( - self._settings.language + # Technically either source could be a raw string, but Language is + # a StrEnum so downstream handles either. + language = cast( + "Language | None", + getattr(event.result, "language", None) or assert_given(self._settings.language), ) frame = InterimTranscriptionFrame( event.result.text, diff --git a/src/pipecat/services/gradium/stt.py b/src/pipecat/services/gradium/stt.py index e58383b45..0e284d05b 100644 --- a/src/pipecat/services/gradium/stt.py +++ b/src/pipecat/services/gradium/stt.py @@ -15,7 +15,7 @@ import base64 import json from collections.abc import AsyncGenerator from dataclasses import dataclass, field -from typing import Any +from typing import Any, cast from loguru import logger from pydantic import BaseModel @@ -401,8 +401,10 @@ class GradiumSTTService(WebsocketSTTService): json_config = {} if self._json_config: json_config = json.loads(self._json_config) - language = assert_given(self._settings.language) - if language: + # Technically `_settings.language` could be a raw string, but + # Language is a StrEnum so downstream handles either. + language = cast("Language | None", assert_given(self._settings.language)) + if language is not None: gradium_language = language_to_gradium_language(language) if gradium_language: json_config["language"] = gradium_language @@ -482,12 +484,14 @@ class GradiumSTTService(WebsocketSTTService): """ self._accumulated_text.append(text) accumulated = " ".join(self._accumulated_text) + # Technically `_settings.language` could be a raw string, but Language + # is a StrEnum so downstream handles either. await self.push_frame( InterimTranscriptionFrame( text=accumulated, user_id=self._user_id, timestamp=time_now_iso8601(), - language=assert_given(self._settings.language), + language=cast("Language | None", assert_given(self._settings.language)), ) ) await self.stop_processing_metrics() @@ -519,7 +523,9 @@ class GradiumSTTService(WebsocketSTTService): text = " ".join(self._accumulated_text) self._accumulated_text.clear() logger.debug(f"Final transcription: [{text}]") - language = assert_given(self._settings.language) + # Technically `_settings.language` could be a raw string, but Language + # is a StrEnum so downstream handles either. + language = cast("Language | None", assert_given(self._settings.language)) await self.push_frame( TranscriptionFrame( text, diff --git a/src/pipecat/services/heygen/api_interactive_avatar.py b/src/pipecat/services/heygen/api_interactive_avatar.py index 4c50ceaa3..ff6a401d0 100644 --- a/src/pipecat/services/heygen/api_interactive_avatar.py +++ b/src/pipecat/services/heygen/api_interactive_avatar.py @@ -210,11 +210,11 @@ class HeyGenApi(BaseAvatarApi): "quality": request_data.quality, "avatar_id": request_data.avatar_id, "voice": { - "voice_id": request_data.voice.voiceId if request_data.voice else None, + "voice_id": request_data.voice.voice_id if request_data.voice else None, "rate": request_data.voice.rate if request_data.voice else None, "emotion": request_data.voice.emotion if request_data.voice else None, "elevenlabs_settings": ( - request_data.voice.elevenlabsSettings if request_data.voice else None + request_data.voice.elevenlabs_settings if request_data.voice else None ), }, "knowledge_id": request_data.knowledge_id, diff --git a/src/pipecat/services/mistral/stt.py b/src/pipecat/services/mistral/stt.py index 85d218628..bbbeddf04 100644 --- a/src/pipecat/services/mistral/stt.py +++ b/src/pipecat/services/mistral/stt.py @@ -12,7 +12,7 @@ Voxtral Realtime transcription API using the Mistral SDK's RealtimeConnection. from collections.abc import AsyncGenerator from dataclasses import dataclass -from typing import Any +from typing import Any, cast from loguru import logger @@ -30,6 +30,7 @@ from pipecat.processors.frame_processor import FrameDirection from pipecat.services.settings import STTSettings, assert_given from pipecat.services.stt_latency import MISTRAL_TTFS_P99 from pipecat.services.stt_service import STTService +from pipecat.transcriptions.language import Language from pipecat.utils.time import time_now_iso8601 from pipecat.utils.tracing.service_decorators import traced_stt @@ -132,7 +133,7 @@ class MistralSTTService(STTService): self._connection: RealtimeConnection | None = None self._receive_task = None self._accumulated_text = "" - self._detected_language: str | None = None + self._detected_language: Language | None = None def can_generate_metrics(self) -> bool: """Check if the service can generate processing metrics. @@ -278,7 +279,9 @@ class MistralSTTService(STTService): self._accumulated_text = "" elif isinstance(event, TranscriptionStreamLanguage): - self._detected_language = event.audio_language + # Technically the SDK could emit a code we haven't added yet, + # but Language is a StrEnum so downstream handles either. + self._detected_language = cast("Language | None", event.audio_language) elif isinstance(event, RealtimeTranscriptionError): error_msg = event.error.message if event.error else "Unknown error" diff --git a/src/pipecat/transports/heygen/transport.py b/src/pipecat/transports/heygen/transport.py index 82a0ae3d0..0558445af 100644 --- a/src/pipecat/transports/heygen/transport.py +++ b/src/pipecat/transports/heygen/transport.py @@ -239,8 +239,9 @@ class HeyGenOutputTransport(BaseOutputTransport): logger.warning("self._event_id is already defined!") self._event_id = str(frame.id) elif isinstance(frame, BotStoppedSpeakingFrame): - await self._client.agent_speak_end(self._event_id) - self._event_id = None + if self._event_id is not None: + await self._client.agent_speak_end(self._event_id) + self._event_id = None await super().push_frame(frame, direction) async def process_frame(self, frame: Frame, direction: FrameDirection): @@ -261,7 +262,8 @@ class HeyGenOutputTransport(BaseOutputTransport): """ await super().process_frame(frame, direction) if isinstance(frame, InterruptionFrame): - await self._client.interrupt(self._event_id) + if self._event_id is not None: + await self._client.interrupt(self._event_id) await self.push_frame(frame, direction) if isinstance(frame, UserStartedSpeakingFrame): await self._client.start_agent_listening() @@ -281,6 +283,11 @@ class HeyGenOutputTransport(BaseOutputTransport): audio = frame.audio if frame.sample_rate != HEY_GEN_SAMPLE_RATE: audio = await self._resampler.resample(audio, frame.sample_rate, HEY_GEN_SAMPLE_RATE) + if self._event_id is None: + # No active bot-speech event — drop the chunk rather than send a + # message the HeyGen API will reject. + logger.warning(f"{self}: dropping audio frame because no event_id is set") + return False await self._client.agent_speak(bytes(audio), self._event_id) return True