From 1624d7a474f7e70bd6614b129237b8a7f9674be4 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Thu, 23 Apr 2026 15:19:32 -0400 Subject: [PATCH] feat(types): add is_given TypeGuard helpers for NotGiven sentinels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pyright can't narrow identity checks against module-level NotGiven sentinels (they aren't typed as singletons), which leaves many NotGiven-bearing unions stuck as unnarrowed types throughout the codebase. Introduce is_given TypeGuard helpers so narrowing works via isinstance under the hood. Each helper is co-located with the NotGiven flavor it guards: - services/settings.py: upgrade the existing is_given to a TypeGuard. - processors/aggregators/llm_context.py: add an is_given for LLMContext's NotGiven. Treat LLMContext's re-exported types (LLMStandardMessage, LLMContextToolChoice, NOT_GIVEN, NotGiven) as LLMContext's own — independent definitions that happen to coincide with OpenAI's as an implementation detail. - adapters/services/anthropic_adapter.py: add is_given for anthropic's NotGiven. - adapters/services/open_ai_adapter.py: add is_given for openai's NotGiven. --- .../adapters/services/anthropic_adapter.py | 25 +++++++++++- .../adapters/services/open_ai_adapter.py | 27 ++++++++++++- .../processors/aggregators/llm_context.py | 39 ++++++++++++++++--- src/pipecat/services/settings.py | 11 +++++- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/src/pipecat/adapters/services/anthropic_adapter.py b/src/pipecat/adapters/services/anthropic_adapter.py index 067d1eb22..2baa341f1 100644 --- a/src/pipecat/adapters/services/anthropic_adapter.py +++ b/src/pipecat/adapters/services/anthropic_adapter.py @@ -9,7 +9,7 @@ import copy import json from dataclasses import dataclass -from typing import Any, TypedDict +from typing import Any, TypedDict, TypeGuard, TypeVar from anthropic import NOT_GIVEN, NotGiven from anthropic.types.message_param import MessageParam @@ -26,6 +26,29 @@ from pipecat.processors.aggregators.llm_context import ( LLMStandardMessage, ) +_T = TypeVar("_T") + + +def is_given(value: _T | NotGiven) -> TypeGuard[_T]: + """Check whether a value was explicitly provided. + + Typically used when checking whether a parameter or field typed with + Anthropic's ``NotGiven`` was set:: + + if is_given(system): + ... + + Also acts as a type guard: inside a true branch, the value is narrowed + to exclude ``NotGiven`` (e.g. ``str | NotGiven`` becomes ``str``). + + Args: + value: The value to check. + + Returns: + ``True`` if *value* is anything other than ``NOT_GIVEN``. + """ + return not isinstance(value, NotGiven) + class AnthropicLLMInvocationParams(TypedDict): """Context-based parameters for invoking Anthropic's LLM API.""" diff --git a/src/pipecat/adapters/services/open_ai_adapter.py b/src/pipecat/adapters/services/open_ai_adapter.py index 335843d16..212e4f8aa 100644 --- a/src/pipecat/adapters/services/open_ai_adapter.py +++ b/src/pipecat/adapters/services/open_ai_adapter.py @@ -6,7 +6,7 @@ """OpenAI LLM adapter for Pipecat.""" -from typing import Any, TypedDict +from typing import Any, TypedDict, TypeGuard, TypeVar from openai._types import NotGiven as OpenAINotGiven from openai.types.chat import ( @@ -25,6 +25,31 @@ from pipecat.processors.aggregators.llm_context import ( NotGiven, ) +_T = TypeVar("_T") + + +def is_given(value: _T | OpenAINotGiven) -> TypeGuard[_T]: + """Check whether a value was explicitly provided. + + Typically used when checking whether a parameter or field typed with + OpenAI's ``NotGiven`` was set:: + + if is_given(tool_choice): + ... + + Also acts as a type guard: inside a true branch, the value is narrowed + to exclude ``OpenAINotGiven`` (e.g. + ``ChatCompletionToolChoiceOptionParam | OpenAINotGiven`` becomes + ``ChatCompletionToolChoiceOptionParam``). + + Args: + value: The value to check. + + Returns: + ``True`` if *value* is anything other than ``NOT_GIVEN``. + """ + return not isinstance(value, OpenAINotGiven) + class OpenAILLMInvocationParams(TypedDict): """Context-based parameters for invoking OpenAI ChatCompletion API.""" diff --git a/src/pipecat/processors/aggregators/llm_context.py b/src/pipecat/processors/aggregators/llm_context.py index b34dbfaec..ecf7e9239 100644 --- a/src/pipecat/processors/aggregators/llm_context.py +++ b/src/pipecat/processors/aggregators/llm_context.py @@ -21,7 +21,7 @@ import io import wave from collections.abc import Callable from dataclasses import dataclass -from typing import Any, TypeAlias +from typing import Any, TypeAlias, TypeGuard, TypeVar from loguru import logger from openai._types import NOT_GIVEN as OPEN_AI_NOT_GIVEN @@ -36,16 +36,45 @@ from pipecat.adapters.schemas.tools_schema import ToolsSchema from pipecat.frames.frames import AudioRawFrame # "Re-export" types from OpenAI that we're using as universal context types. -# NOTE: if universal message types need to someday diverge from OpenAI's, we -# should consider managing our own definitions. But we should do so carefully, -# as the OpenAI messages are somewhat of a standard and we want to continue -# supporting them. +# NOTE: these are aliased to OpenAI's today, but callers should treat them as +# LLMContext's own types — independent definitions that happen to coincide +# with OpenAI's as an implementation detail. If universal context types need +# to someday diverge from OpenAI's, we should consider managing our own +# definitions (but with care, since OpenAI's types are somewhat of a standard +# and we want to continue supporting them). In the meantime, code at the +# LLMContext/OpenAI boundary should use explicit casts rather than rely on +# the aliasing. LLMStandardMessage = ChatCompletionMessageParam LLMContextToolChoice = ChatCompletionToolChoiceOptionParam NOT_GIVEN = OPEN_AI_NOT_GIVEN NotGiven = OpenAINotGiven +_T = TypeVar("_T") + + +def is_given(value: _T | NotGiven) -> TypeGuard[_T]: + """Check whether a value was explicitly provided. + + Typically used when checking whether a ``NotGiven``-valued field or + parameter was set:: + + if is_given(context.tools): + ... + + Also acts as a type guard: inside a true branch, the value is narrowed + to exclude ``NotGiven`` (e.g. ``ToolsSchema | NotGiven`` becomes + ``ToolsSchema``). + + Args: + value: The value to check. + + Returns: + ``True`` if *value* is anything other than ``NOT_GIVEN``. + """ + return not isinstance(value, NotGiven) + + @dataclass class LLMSpecificMessage: """A container for a context message that is specific to a particular LLM service. diff --git a/src/pipecat/services/settings.py b/src/pipecat/services/settings.py index 585053277..cc8d95958 100644 --- a/src/pipecat/services/settings.py +++ b/src/pipecat/services/settings.py @@ -39,7 +39,7 @@ from __future__ import annotations import copy from collections.abc import Mapping from dataclasses import dataclass, field, fields -from typing import TYPE_CHECKING, Any, ClassVar, TypeVar +from typing import TYPE_CHECKING, Any, ClassVar, TypeGuard, TypeVar from loguru import logger @@ -88,7 +88,10 @@ Valid only in delta-mode settings objects. Must never appear in a service's """ -def is_given(value: Any) -> bool: +_T = TypeVar("_T") + + +def is_given(value: _T | _NotGiven) -> TypeGuard[_T]: """Check whether a delta field was explicitly provided. Typically used when processing a delta to decide whether a field @@ -98,6 +101,10 @@ def is_given(value: Any) -> bool: # caller wants to change the voice ... + Also acts as a type guard: inside a true branch, the value is narrowed + to exclude ``_NotGiven`` (e.g. ``str | None | _NotGiven`` becomes + ``str | None``). + For store-mode objects this always returns ``True`` (since ``validate_complete`` ensures no ``NOT_GIVEN`` fields remain).