fix: freeze the public voice API contract (#4578)
This commit is contained in:
@@ -12,8 +12,8 @@ from collections.abc import Callable, Iterable, Mapping
|
||||
from copy import deepcopy
|
||||
from importlib.util import find_spec
|
||||
from pathlib import Path
|
||||
from types import FunctionType, TracebackType
|
||||
from typing import Any, ForwardRef, cast, get_origin, get_type_hints
|
||||
from types import FunctionType, TracebackType, UnionType
|
||||
from typing import Any, ForwardRef, Literal, Union, cast, get_args, get_origin, get_type_hints
|
||||
|
||||
import typing_extensions
|
||||
from pydantic import BaseModel
|
||||
@@ -36,7 +36,9 @@ class SubmoduleExportPolicy:
|
||||
modules: dict[str, dict[str, dict[str, str]]]
|
||||
dependency_installations: tuple[OptionalDependencyInstallation, ...]
|
||||
canonical_imports: tuple[dict[str, str], ...] = ()
|
||||
public_class_contracts: tuple[dict[str, Any], ...] = ()
|
||||
public_properties: tuple[dict[str, Any], ...] = ()
|
||||
public_type_aliases: tuple[dict[str, str], ...] = ()
|
||||
public_typed_dicts: tuple[dict[str, Any], ...] = ()
|
||||
|
||||
|
||||
@@ -56,7 +58,9 @@ def load_submodule_export_policy(path: Path) -> SubmoduleExportPolicy:
|
||||
"canonical_imports",
|
||||
"modules",
|
||||
"optional_dependencies",
|
||||
"public_class_contracts",
|
||||
"public_properties",
|
||||
"public_type_aliases",
|
||||
"public_typed_dicts",
|
||||
}
|
||||
)
|
||||
@@ -164,7 +168,11 @@ def load_submodule_export_policy(path: Path) -> SubmoduleExportPolicy:
|
||||
)
|
||||
),
|
||||
canonical_imports=_canonical_import_policy(value.get("canonical_imports", [])),
|
||||
public_class_contracts=_public_class_contract_policy(
|
||||
value.get("public_class_contracts", [])
|
||||
),
|
||||
public_properties=_public_property_policy(value.get("public_properties", [])),
|
||||
public_type_aliases=_public_type_alias_policy(value.get("public_type_aliases", [])),
|
||||
public_typed_dicts=_public_typed_dict_policy(value.get("public_typed_dicts", [])),
|
||||
)
|
||||
|
||||
@@ -205,7 +213,8 @@ def _public_property_policy(value: object) -> tuple[dict[str, Any], ...]:
|
||||
if not isinstance(entry, dict):
|
||||
raise ValueError("submodule export policy public_properties entries must be objects")
|
||||
owner_fields = {"class_name", "factory_name"} & set(entry)
|
||||
if len(owner_fields) != 1 or set(entry) != {"module", "names", *owner_fields}:
|
||||
required_fields = {"module", "names", *owner_fields}
|
||||
if len(owner_fields) != 1 or set(entry) != required_fields:
|
||||
raise ValueError(
|
||||
"submodule export policy public_properties entries must contain exactly "
|
||||
"module, names, and one of class_name or factory_name"
|
||||
@@ -240,7 +249,75 @@ def _public_property_policy(value: object) -> tuple[dict[str, Any], ...]:
|
||||
f"{module_name}.{owner_name}"
|
||||
)
|
||||
identities.add(identity)
|
||||
entries.append({owner_field: owner_name, "module": module_name, "names": list(names)})
|
||||
normalized_entry = {
|
||||
owner_field: owner_name,
|
||||
"module": module_name,
|
||||
"names": list(names),
|
||||
}
|
||||
entries.append(normalized_entry)
|
||||
return tuple(entries)
|
||||
|
||||
|
||||
def _public_class_contract_policy(value: object) -> tuple[dict[str, Any], ...]:
|
||||
if not isinstance(value, list):
|
||||
raise ValueError("submodule export policy public_class_contracts must be a list")
|
||||
required_fields = {"class_name", "module"}
|
||||
contract_fields = {"abstract", "abstract_members"}
|
||||
entries: list[dict[str, Any]] = []
|
||||
identities: set[tuple[str, str]] = set()
|
||||
for entry in value:
|
||||
if (
|
||||
not isinstance(entry, dict)
|
||||
or not required_fields.issubset(entry)
|
||||
or not set(entry).issubset(required_fields | contract_fields)
|
||||
or not (set(entry) & contract_fields)
|
||||
):
|
||||
raise ValueError(
|
||||
"submodule export policy public_class_contracts entries must contain exactly "
|
||||
"module, class_name, and at least one of abstract or abstract_members"
|
||||
)
|
||||
module_name = entry["module"]
|
||||
class_name = entry["class_name"]
|
||||
if type(module_name) is not str or not module_name:
|
||||
raise ValueError(
|
||||
"submodule export policy public_class_contracts module must be a non-empty string"
|
||||
)
|
||||
if type(class_name) is not str or not class_name:
|
||||
raise ValueError(
|
||||
"submodule export policy public_class_contracts class_name must be a non-empty "
|
||||
"string"
|
||||
)
|
||||
if "abstract" in entry and type(entry["abstract"]) is not bool:
|
||||
raise ValueError(
|
||||
"submodule export policy public_class_contracts abstract must be a boolean"
|
||||
)
|
||||
abstract_members = entry.get("abstract_members")
|
||||
if "abstract_members" in entry and (
|
||||
not isinstance(abstract_members, list)
|
||||
or not abstract_members
|
||||
or not all(type(name) is str and name for name in abstract_members)
|
||||
or len(abstract_members) != len(set(abstract_members))
|
||||
):
|
||||
raise ValueError(
|
||||
"submodule export policy public_class_contracts abstract_members must be a "
|
||||
"non-empty list of unique non-empty strings"
|
||||
)
|
||||
identity = (module_name, class_name)
|
||||
if identity in identities:
|
||||
raise ValueError(
|
||||
"submodule export policy public_class_contracts must not repeat "
|
||||
f"{module_name}.{class_name}"
|
||||
)
|
||||
identities.add(identity)
|
||||
normalized_entry: dict[str, Any] = {
|
||||
"class_name": class_name,
|
||||
"module": module_name,
|
||||
}
|
||||
if "abstract" in entry:
|
||||
normalized_entry["abstract"] = entry["abstract"]
|
||||
if "abstract_members" in entry:
|
||||
normalized_entry["abstract_members"] = sorted(abstract_members)
|
||||
entries.append(normalized_entry)
|
||||
return tuple(entries)
|
||||
|
||||
|
||||
@@ -288,6 +365,33 @@ def _public_typed_dict_policy(value: object) -> tuple[dict[str, Any], ...]:
|
||||
return tuple(entries)
|
||||
|
||||
|
||||
def _public_type_alias_policy(value: object) -> tuple[dict[str, str], ...]:
|
||||
if not isinstance(value, list):
|
||||
raise ValueError("submodule export policy public_type_aliases must be a list")
|
||||
required_fields = {"module", "name"}
|
||||
entries: list[dict[str, str]] = []
|
||||
identities: set[tuple[str, str]] = set()
|
||||
for entry in value:
|
||||
if not isinstance(entry, dict) or set(entry) != required_fields:
|
||||
raise ValueError(
|
||||
"submodule export policy public_type_aliases entries must contain exactly "
|
||||
"module and name"
|
||||
)
|
||||
if not all(type(entry[field]) is str and entry[field] for field in required_fields):
|
||||
raise ValueError(
|
||||
"submodule export policy public_type_aliases values must be non-empty strings"
|
||||
)
|
||||
identity = (entry["module"], entry["name"])
|
||||
if identity in identities:
|
||||
raise ValueError(
|
||||
"submodule export policy public_type_aliases must not repeat "
|
||||
f"{entry['module']}.{entry['name']}"
|
||||
)
|
||||
identities.add(identity)
|
||||
entries.append({"module": entry["module"], "name": entry["name"]})
|
||||
return tuple(entries)
|
||||
|
||||
|
||||
def _add_legacy_literal_types(value: object) -> None:
|
||||
if isinstance(value, dict):
|
||||
if value.get("kind") == "literal" and "value" in value and "type" not in value:
|
||||
@@ -414,6 +518,11 @@ def _default_contract(value: object) -> dict[str, object]:
|
||||
"name": value.name,
|
||||
"value": _default_contract(value.value),
|
||||
}
|
||||
if isinstance(value, type):
|
||||
return {
|
||||
"kind": "type",
|
||||
"identity": f"{value.__module__}.{value.__qualname__}",
|
||||
}
|
||||
if isinstance(value, tuple | list):
|
||||
return {
|
||||
"kind": "sequence",
|
||||
@@ -723,6 +832,31 @@ def _merge_public_properties(
|
||||
return result
|
||||
|
||||
|
||||
def _merge_public_class_contracts(
|
||||
existing: Iterable[Mapping[str, Any]], promoted: Iterable[Mapping[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
result = [deepcopy(dict(entry)) for entry in existing]
|
||||
by_identity = {(entry["module"], entry["class_name"]): entry for entry in result}
|
||||
for entry_value in promoted:
|
||||
entry = deepcopy(dict(entry_value))
|
||||
identity = (entry["module"], entry["class_name"])
|
||||
previous = by_identity.get(identity)
|
||||
if previous is None:
|
||||
result.append(entry)
|
||||
by_identity[identity] = entry
|
||||
continue
|
||||
for field_name in ("abstract", "abstract_members"):
|
||||
if field_name not in entry:
|
||||
continue
|
||||
previous_value = previous.setdefault(field_name, entry[field_name])
|
||||
if previous_value != entry[field_name]:
|
||||
raise ValueError(
|
||||
"release policy public class contract conflicts with the released contract "
|
||||
f"for {entry['module']}.{entry['class_name']} field {field_name}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _public_property_identity(entry: Mapping[str, Any]) -> tuple[str, str, str]:
|
||||
if "class_name" in entry:
|
||||
return ("class_name", cast(str, entry["module"]), cast(str, entry["class_name"]))
|
||||
@@ -744,6 +878,86 @@ def _annotation_contract(annotation: object) -> str:
|
||||
return annotation_text
|
||||
|
||||
|
||||
def _sorted_type_alias_members(members: Iterable[dict[str, object]]) -> list[dict[str, object]]:
|
||||
return sorted(
|
||||
members,
|
||||
key=lambda member: (
|
||||
cast(str, member["kind"]),
|
||||
json.dumps(member, sort_keys=True, separators=(",", ":")),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _type_alias_definition(value: object) -> dict[str, object]:
|
||||
origin = get_origin(value)
|
||||
if origin is Literal:
|
||||
literal_values: list[dict[str, object]] = []
|
||||
for literal_value in get_args(value):
|
||||
literal_contract = _default_contract(literal_value)
|
||||
if literal_contract["kind"] not in {"literal", "enum"}:
|
||||
raise TypeError(
|
||||
"public type alias Literal members must use supported literal or enum values"
|
||||
)
|
||||
literal_values.append(literal_contract)
|
||||
return {
|
||||
"kind": "literal",
|
||||
"values": _sorted_type_alias_members(literal_values),
|
||||
}
|
||||
if origin in {Union, UnionType}:
|
||||
members = [_type_alias_definition(member) for member in get_args(value)]
|
||||
return {
|
||||
"kind": "union",
|
||||
"members": _sorted_type_alias_members(members),
|
||||
}
|
||||
if isinstance(value, type) and (
|
||||
value.__module__ == "agents" or value.__module__.startswith("agents.")
|
||||
):
|
||||
return {
|
||||
"kind": "type",
|
||||
"identity": f"{value.__module__}.{value.__qualname__}",
|
||||
}
|
||||
raise TypeError(f"unsupported public type alias member: {value!r}")
|
||||
|
||||
|
||||
def _public_type_alias_contract(
|
||||
policy_entries: Iterable[Mapping[str, str]],
|
||||
agents_module: Any | None,
|
||||
) -> list[dict[str, object]]:
|
||||
entries: list[dict[str, object]] = []
|
||||
missing = object()
|
||||
for policy_entry in policy_entries:
|
||||
module_name = policy_entry["module"]
|
||||
alias_name = policy_entry["name"]
|
||||
module = _import_contract_module(module_name, agents_module)
|
||||
alias = getattr(module, alias_name, missing)
|
||||
if alias is missing:
|
||||
raise ValueError(
|
||||
f"Cannot promote public type alias {module_name}.{alias_name} because it is missing"
|
||||
)
|
||||
try:
|
||||
definition = _type_alias_definition(alias)
|
||||
except TypeError as error:
|
||||
raise ValueError(
|
||||
f"Cannot promote public type alias {module_name}.{alias_name}: {error}"
|
||||
) from None
|
||||
entries.append({"definition": definition, "module": module_name, "name": alias_name})
|
||||
return entries
|
||||
|
||||
|
||||
def _merge_public_type_aliases(
|
||||
existing: Iterable[Mapping[str, Any]], promoted: Iterable[Mapping[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
result = [deepcopy(dict(entry)) for entry in existing]
|
||||
identities = {(entry["module"], entry["name"]) for entry in result}
|
||||
for entry_value in promoted:
|
||||
entry = deepcopy(dict(entry_value))
|
||||
identity = (entry["module"], entry["name"])
|
||||
if identity not in identities:
|
||||
result.append(entry)
|
||||
identities.add(identity)
|
||||
return result
|
||||
|
||||
|
||||
def _typed_dict_field_is_required(typed_dict: type, name: str, annotation: object) -> bool:
|
||||
if isinstance(annotation, ForwardRef):
|
||||
annotation_text = annotation.__forward_arg__
|
||||
@@ -879,9 +1093,21 @@ def _optional_dependency_is_unsupported_for_contract(
|
||||
def _optional_dependency_for_binding(
|
||||
contract: Mapping[str, Any], module_name: str, binding_name: str
|
||||
) -> str | None:
|
||||
return _optional_dependency_for_binding_in_modules(
|
||||
dependency = _optional_dependency_for_binding_in_modules(
|
||||
contract.get("required_submodule_exports", {}), module_name, binding_name
|
||||
)
|
||||
if dependency is not None:
|
||||
return dependency
|
||||
canonical_dependencies = {
|
||||
_optional_dependency_for_binding_in_modules(
|
||||
contract.get("required_submodule_exports", {}), entry["module"], entry["name"]
|
||||
)
|
||||
for entry in contract.get("canonical_imports", [])
|
||||
if entry["canonical_module"] == module_name and entry["canonical_name"] == binding_name
|
||||
}
|
||||
if canonical_dependencies and len(canonical_dependencies) == 1:
|
||||
return next(iter(canonical_dependencies))
|
||||
return None
|
||||
|
||||
|
||||
def _optional_dependency_for_binding_in_modules(
|
||||
@@ -913,6 +1139,17 @@ def _optional_dependency_for_module_import(
|
||||
dependencies = {optional_bindings.get(name) or optional_exports.get(name) for name in names}
|
||||
if names and len(dependencies) == 1 and None not in dependencies:
|
||||
return cast(str, next(iter(dependencies)))
|
||||
if names:
|
||||
return None
|
||||
canonical_dependencies = {
|
||||
_optional_dependency_for_binding(contract, entry["module"], entry["name"])
|
||||
for entry in contract.get("canonical_imports", [])
|
||||
if entry["canonical_module"] == module_name
|
||||
}
|
||||
if canonical_dependencies and len(canonical_dependencies) == 1:
|
||||
dependency = next(iter(canonical_dependencies))
|
||||
if dependency is not None:
|
||||
return dependency
|
||||
return None
|
||||
|
||||
|
||||
@@ -1086,10 +1323,20 @@ def build_released_api_contract(
|
||||
updated["required_top_level_exports"] = ordered_exports
|
||||
updated["callables"] = callables
|
||||
updated["canonical_imports"] = canonical_imports
|
||||
updated["public_class_contracts"] = _merge_public_class_contracts(
|
||||
contract.get("public_class_contracts", []),
|
||||
release_policy.public_class_contracts if release_policy is not None else (),
|
||||
)
|
||||
updated["public_properties"] = _merge_public_properties(
|
||||
contract.get("public_properties", []),
|
||||
release_policy.public_properties if release_policy is not None else (),
|
||||
)
|
||||
updated["public_type_aliases"] = _merge_public_type_aliases(
|
||||
contract.get("public_type_aliases", []),
|
||||
_public_type_alias_contract(release_policy.public_type_aliases, agents_module)
|
||||
if release_policy is not None
|
||||
else (),
|
||||
)
|
||||
updated["public_typed_dicts"] = _merge_public_typed_dicts(
|
||||
contract.get("public_typed_dicts", []),
|
||||
_public_typed_dict_contract(release_policy.public_typed_dicts, agents_module)
|
||||
@@ -1220,7 +1467,9 @@ def build_released_api_contract(
|
||||
"callables",
|
||||
"optional_dependency_unsupported_platforms",
|
||||
"platform_import_errors",
|
||||
"public_class_contracts",
|
||||
"public_properties",
|
||||
"public_type_aliases",
|
||||
"public_typed_dicts",
|
||||
"public_modules",
|
||||
"required_submodule_exports",
|
||||
@@ -1361,6 +1610,48 @@ def _validate_public_property_contract(
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_public_class_contract(
|
||||
contract: dict[str, Any],
|
||||
agents_module: Any | None,
|
||||
*,
|
||||
unsupported_platforms: Mapping[str, tuple[str, ...]] | None = None,
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
unsupported_platforms = unsupported_platforms or {}
|
||||
for entry in contract.get("public_class_contracts", []):
|
||||
module_name = entry["module"]
|
||||
class_name = entry["class_name"]
|
||||
optional_dependency = _optional_dependency_for_binding(contract, module_name, class_name)
|
||||
if optional_dependency is not None and not _optional_dependency_is_available_for_contract(
|
||||
optional_dependency, unsupported_platforms
|
||||
):
|
||||
continue
|
||||
try:
|
||||
module = _import_contract_module(module_name, agents_module)
|
||||
except Exception as error:
|
||||
errors.append(f"Failed to import released module {module_name}: {error!r}")
|
||||
continue
|
||||
class_value = getattr(module, class_name, None)
|
||||
if not isinstance(class_value, type):
|
||||
errors.append(f"Missing released public class {module_name}.{class_name}")
|
||||
continue
|
||||
if "abstract" in entry and inspect.isabstract(class_value) != entry["abstract"]:
|
||||
expected_state = "abstract" if entry["abstract"] else "concrete"
|
||||
current_state = "abstract" if inspect.isabstract(class_value) else "concrete"
|
||||
errors.append(
|
||||
f"{module_name}.{class_name} changed its released public class state: "
|
||||
f"expected {expected_state}, got {current_state}"
|
||||
)
|
||||
if "abstract_members" in entry:
|
||||
current_members = sorted(getattr(class_value, "__abstractmethods__", ()))
|
||||
if current_members != entry["abstract_members"]:
|
||||
errors.append(
|
||||
f"{module_name}.{class_name} changed its released public abstract members: "
|
||||
f"expected {entry['abstract_members']!r}, got {current_members!r}"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_public_typed_dict_contract(
|
||||
contract: dict[str, Any],
|
||||
agents_module: Any | None,
|
||||
@@ -1397,6 +1688,48 @@ def _validate_public_typed_dict_contract(
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_public_type_alias_contract(
|
||||
contract: dict[str, Any],
|
||||
agents_module: Any | None,
|
||||
*,
|
||||
unsupported_platforms: Mapping[str, tuple[str, ...]] | None = None,
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
unsupported_platforms = unsupported_platforms or {}
|
||||
missing = object()
|
||||
for entry in contract.get("public_type_aliases", []):
|
||||
module_name = entry["module"]
|
||||
alias_name = entry["name"]
|
||||
optional_dependency = _optional_dependency_for_binding(contract, module_name, alias_name)
|
||||
if optional_dependency is not None and not _optional_dependency_is_available_for_contract(
|
||||
optional_dependency, unsupported_platforms
|
||||
):
|
||||
continue
|
||||
try:
|
||||
module = _import_contract_module(module_name, agents_module)
|
||||
except Exception as error:
|
||||
errors.append(f"Failed to import released module {module_name}: {error!r}")
|
||||
continue
|
||||
alias = getattr(module, alias_name, missing)
|
||||
if alias is missing:
|
||||
errors.append(f"Missing released public type alias {module_name}.{alias_name}")
|
||||
continue
|
||||
try:
|
||||
current_definition = _type_alias_definition(alias)
|
||||
except TypeError as error:
|
||||
errors.append(
|
||||
f"{module_name}.{alias_name} no longer has a supported released public type "
|
||||
f"alias definition: {error}"
|
||||
)
|
||||
continue
|
||||
if current_definition != entry["definition"]:
|
||||
errors.append(
|
||||
f"{module_name}.{alias_name} changed its released public type alias: "
|
||||
f"expected {entry['definition']!r}, got {current_definition!r}"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def _submodule_export_contract(
|
||||
module: object,
|
||||
*,
|
||||
@@ -1495,6 +1828,13 @@ def validate_released_api_contract(
|
||||
errors.append(f"Invalid released optional dependency platform declarations: {error}")
|
||||
unsupported_platforms = {}
|
||||
|
||||
errors.extend(
|
||||
_validate_public_class_contract(
|
||||
contract,
|
||||
agents_module,
|
||||
unsupported_platforms=unsupported_platforms,
|
||||
)
|
||||
)
|
||||
errors.extend(
|
||||
_validate_public_property_contract(
|
||||
contract,
|
||||
@@ -1502,6 +1842,13 @@ def validate_released_api_contract(
|
||||
unsupported_platforms=unsupported_platforms,
|
||||
)
|
||||
)
|
||||
errors.extend(
|
||||
_validate_public_type_alias_contract(
|
||||
contract,
|
||||
agents_module,
|
||||
unsupported_platforms=unsupported_platforms,
|
||||
)
|
||||
)
|
||||
errors.extend(
|
||||
_validate_public_typed_dict_contract(
|
||||
contract,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
from .events import VoiceStreamEvent, VoiceStreamEventAudio, VoiceStreamEventLifecycle
|
||||
from .events import (
|
||||
VoiceStreamEvent,
|
||||
VoiceStreamEventAudio,
|
||||
VoiceStreamEventError,
|
||||
VoiceStreamEventLifecycle,
|
||||
)
|
||||
from .exceptions import STTWebsocketConnectionError
|
||||
from .input import AudioInput, StreamedAudioInput
|
||||
from .model import (
|
||||
@@ -41,6 +46,7 @@ __all__ = [
|
||||
"OpenAISTTModel",
|
||||
"OpenAITTSModel",
|
||||
"VoiceStreamEventAudio",
|
||||
"VoiceStreamEventError",
|
||||
"VoiceStreamEventLifecycle",
|
||||
"VoiceStreamEvent",
|
||||
"VoicePipeline",
|
||||
|
||||
+359
@@ -197,6 +197,168 @@
|
||||
"canonical_name": "VercelSandboxClientOptions",
|
||||
"module": "agents.extensions.sandbox",
|
||||
"name": "VercelSandboxClientOptions"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.input",
|
||||
"canonical_name": "AudioInput",
|
||||
"module": "agents.voice",
|
||||
"name": "AudioInput"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.input",
|
||||
"canonical_name": "StreamedAudioInput",
|
||||
"module": "agents.voice",
|
||||
"name": "StreamedAudioInput"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.model",
|
||||
"canonical_name": "STTModel",
|
||||
"module": "agents.voice",
|
||||
"name": "STTModel"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.model",
|
||||
"canonical_name": "STTModelSettings",
|
||||
"module": "agents.voice",
|
||||
"name": "STTModelSettings"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.model",
|
||||
"canonical_name": "TTSCustomVoice",
|
||||
"module": "agents.voice",
|
||||
"name": "TTSCustomVoice"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.model",
|
||||
"canonical_name": "TTSModel",
|
||||
"module": "agents.voice",
|
||||
"name": "TTSModel"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.model",
|
||||
"canonical_name": "TTSModelSettings",
|
||||
"module": "agents.voice",
|
||||
"name": "TTSModelSettings"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.model",
|
||||
"canonical_name": "TTSVoice",
|
||||
"module": "agents.voice",
|
||||
"name": "TTSVoice"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.model",
|
||||
"canonical_name": "VoiceModelProvider",
|
||||
"module": "agents.voice",
|
||||
"name": "VoiceModelProvider"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.result",
|
||||
"canonical_name": "StreamedAudioResult",
|
||||
"module": "agents.voice",
|
||||
"name": "StreamedAudioResult"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.workflow",
|
||||
"canonical_name": "SingleAgentVoiceWorkflow",
|
||||
"module": "agents.voice",
|
||||
"name": "SingleAgentVoiceWorkflow"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.models.openai_model_provider",
|
||||
"canonical_name": "OpenAIVoiceModelProvider",
|
||||
"module": "agents.voice",
|
||||
"name": "OpenAIVoiceModelProvider"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.models.openai_stt",
|
||||
"canonical_name": "OpenAISTTModel",
|
||||
"module": "agents.voice",
|
||||
"name": "OpenAISTTModel"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.models.openai_tts",
|
||||
"canonical_name": "OpenAITTSModel",
|
||||
"module": "agents.voice",
|
||||
"name": "OpenAITTSModel"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.events",
|
||||
"canonical_name": "VoiceStreamEventAudio",
|
||||
"module": "agents.voice",
|
||||
"name": "VoiceStreamEventAudio"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.events",
|
||||
"canonical_name": "VoiceStreamEventLifecycle",
|
||||
"module": "agents.voice",
|
||||
"name": "VoiceStreamEventLifecycle"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.events",
|
||||
"canonical_name": "VoiceStreamEvent",
|
||||
"module": "agents.voice",
|
||||
"name": "VoiceStreamEvent"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.events",
|
||||
"canonical_name": "VoiceStreamEventError",
|
||||
"module": "agents.voice",
|
||||
"name": "VoiceStreamEventError"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.pipeline",
|
||||
"canonical_name": "VoicePipeline",
|
||||
"module": "agents.voice",
|
||||
"name": "VoicePipeline"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.pipeline_config",
|
||||
"canonical_name": "VoicePipelineConfig",
|
||||
"module": "agents.voice",
|
||||
"name": "VoicePipelineConfig"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.utils",
|
||||
"canonical_name": "get_sentence_based_splitter",
|
||||
"module": "agents.voice",
|
||||
"name": "get_sentence_based_splitter"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.workflow",
|
||||
"canonical_name": "VoiceWorkflowHelper",
|
||||
"module": "agents.voice",
|
||||
"name": "VoiceWorkflowHelper"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.workflow",
|
||||
"canonical_name": "VoiceWorkflowBase",
|
||||
"module": "agents.voice",
|
||||
"name": "VoiceWorkflowBase"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.workflow",
|
||||
"canonical_name": "SingleAgentWorkflowCallbacks",
|
||||
"module": "agents.voice",
|
||||
"name": "SingleAgentWorkflowCallbacks"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.model",
|
||||
"canonical_name": "StreamedTranscriptionSession",
|
||||
"module": "agents.voice",
|
||||
"name": "StreamedTranscriptionSession"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.models.openai_stt",
|
||||
"canonical_name": "OpenAISTTTranscriptionSession",
|
||||
"module": "agents.voice",
|
||||
"name": "OpenAISTTTranscriptionSession"
|
||||
},
|
||||
{
|
||||
"canonical_module": "agents.voice.exceptions",
|
||||
"canonical_name": "STTWebsocketConnectionError",
|
||||
"module": "agents.voice",
|
||||
"name": "STTWebsocketConnectionError"
|
||||
}
|
||||
],
|
||||
"optional_dependencies": {
|
||||
@@ -304,6 +466,86 @@
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice": {
|
||||
"optional_bindings": {
|
||||
"AudioInput": "numpy",
|
||||
"OpenAISTTModel": "numpy",
|
||||
"OpenAISTTTranscriptionSession": "numpy",
|
||||
"OpenAITTSModel": "numpy",
|
||||
"OpenAIVoiceModelProvider": "numpy",
|
||||
"STTModel": "numpy",
|
||||
"STTModelSettings": "numpy",
|
||||
"STTWebsocketConnectionError": "numpy",
|
||||
"SingleAgentVoiceWorkflow": "numpy",
|
||||
"SingleAgentWorkflowCallbacks": "numpy",
|
||||
"StreamedAudioInput": "numpy",
|
||||
"StreamedAudioResult": "numpy",
|
||||
"StreamedTranscriptionSession": "numpy",
|
||||
"TTSCustomVoice": "numpy",
|
||||
"TTSModel": "numpy",
|
||||
"TTSModelSettings": "numpy",
|
||||
"TTSVoice": "numpy",
|
||||
"VoiceModelProvider": "numpy",
|
||||
"VoicePipeline": "numpy",
|
||||
"VoicePipelineConfig": "numpy",
|
||||
"VoiceStreamEvent": "numpy",
|
||||
"VoiceStreamEventAudio": "numpy",
|
||||
"VoiceStreamEventError": "numpy",
|
||||
"VoiceStreamEventLifecycle": "numpy",
|
||||
"VoiceWorkflowBase": "numpy",
|
||||
"VoiceWorkflowHelper": "numpy",
|
||||
"get_sentence_based_splitter": "numpy"
|
||||
},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.events": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.exceptions": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.input": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.imports": {
|
||||
"optional_bindings": {
|
||||
"np": "numpy",
|
||||
"npt": "numpy",
|
||||
"websockets": "numpy"
|
||||
},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.model": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.models.openai_model_provider": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.models.openai_stt": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.models.openai_tts": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.pipeline": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.pipeline_config": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.result": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.testing": {
|
||||
"optional_bindings": {
|
||||
"STTCall": "numpy",
|
||||
@@ -320,8 +562,73 @@
|
||||
"pcm16_samples": "numpy"
|
||||
},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.utils": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
},
|
||||
"agents.voice.workflow": {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {}
|
||||
}
|
||||
},
|
||||
"public_class_contracts": [
|
||||
{
|
||||
"abstract_members": [
|
||||
"create_session",
|
||||
"model_name",
|
||||
"transcribe"
|
||||
],
|
||||
"class_name": "STTModel",
|
||||
"module": "agents.voice.model"
|
||||
},
|
||||
{
|
||||
"abstract_members": [
|
||||
"close",
|
||||
"transcribe_turns"
|
||||
],
|
||||
"class_name": "StreamedTranscriptionSession",
|
||||
"module": "agents.voice.model"
|
||||
},
|
||||
{
|
||||
"abstract_members": [
|
||||
"model_name",
|
||||
"run"
|
||||
],
|
||||
"class_name": "TTSModel",
|
||||
"module": "agents.voice.model"
|
||||
},
|
||||
{
|
||||
"abstract_members": [
|
||||
"get_stt_model",
|
||||
"get_tts_model"
|
||||
],
|
||||
"class_name": "VoiceModelProvider",
|
||||
"module": "agents.voice.model"
|
||||
},
|
||||
{
|
||||
"abstract": false,
|
||||
"class_name": "OpenAISTTModel",
|
||||
"module": "agents.voice.models.openai_stt"
|
||||
},
|
||||
{
|
||||
"abstract": false,
|
||||
"class_name": "OpenAISTTTranscriptionSession",
|
||||
"module": "agents.voice.models.openai_stt"
|
||||
},
|
||||
{
|
||||
"abstract": false,
|
||||
"class_name": "OpenAITTSModel",
|
||||
"module": "agents.voice.models.openai_tts"
|
||||
},
|
||||
{
|
||||
"abstract_members": [
|
||||
"run"
|
||||
],
|
||||
"class_name": "VoiceWorkflowBase",
|
||||
"module": "agents.voice.workflow"
|
||||
}
|
||||
],
|
||||
"public_properties": [
|
||||
{
|
||||
"class_name": "RunState",
|
||||
@@ -390,6 +697,41 @@
|
||||
"transcriptions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"class_name": "STTModel",
|
||||
"module": "agents.voice.model",
|
||||
"names": [
|
||||
"model_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"class_name": "TTSModel",
|
||||
"module": "agents.voice.model",
|
||||
"names": [
|
||||
"model_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"class_name": "OpenAISTTModel",
|
||||
"module": "agents.voice.models.openai_stt",
|
||||
"names": [
|
||||
"model_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"class_name": "OpenAITTSModel",
|
||||
"module": "agents.voice.models.openai_tts",
|
||||
"names": [
|
||||
"model_name"
|
||||
]
|
||||
},
|
||||
{
|
||||
"class_name": "OpenAIVoiceModelProvider",
|
||||
"module": "agents.voice.models.openai_model_provider",
|
||||
"names": [
|
||||
"agent_registration"
|
||||
]
|
||||
},
|
||||
{
|
||||
"class_name": "RunloopPlatformClient",
|
||||
"module": "agents.extensions.sandbox",
|
||||
@@ -425,6 +767,16 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"public_type_aliases": [
|
||||
{
|
||||
"module": "agents.voice.model",
|
||||
"name": "TTSVoice"
|
||||
},
|
||||
{
|
||||
"module": "agents.voice.events",
|
||||
"name": "VoiceStreamEvent"
|
||||
}
|
||||
],
|
||||
"public_typed_dicts": [
|
||||
{
|
||||
"class_name": "ModelStepSpec",
|
||||
@@ -463,6 +815,13 @@
|
||||
"playback_tracker",
|
||||
"call_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"class_name": "TTSCustomVoice",
|
||||
"module": "agents.voice",
|
||||
"names": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import abc
|
||||
import builtins
|
||||
import importlib
|
||||
import inspect
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -10,7 +12,7 @@ from importlib.metadata import version
|
||||
from inspect import Parameter, Signature
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -25,7 +27,9 @@ from integration_tests._contract_support import (
|
||||
_parameter_contract,
|
||||
_public_class_member_contract,
|
||||
_validate_parameter_contract,
|
||||
_validate_public_class_contract,
|
||||
_validate_public_property_contract,
|
||||
_validate_public_type_alias_contract,
|
||||
_validate_public_typed_dict_contract,
|
||||
build_released_api_contract,
|
||||
load_api_contract,
|
||||
@@ -41,14 +45,18 @@ def _release_policy(
|
||||
*,
|
||||
dependency_installations: tuple[OptionalDependencyInstallation, ...] = (),
|
||||
canonical_imports: tuple[dict[str, str], ...] = (),
|
||||
public_class_contracts: tuple[dict[str, Any], ...] = (),
|
||||
public_properties: tuple[dict[str, Any], ...] = (),
|
||||
public_type_aliases: tuple[dict[str, str], ...] = (),
|
||||
public_typed_dicts: tuple[dict[str, Any], ...] = (),
|
||||
) -> SubmoduleExportPolicy:
|
||||
return SubmoduleExportPolicy(
|
||||
modules=modules,
|
||||
dependency_installations=dependency_installations,
|
||||
canonical_imports=canonical_imports,
|
||||
public_class_contracts=public_class_contracts,
|
||||
public_properties=public_properties,
|
||||
public_type_aliases=public_type_aliases,
|
||||
public_typed_dicts=public_typed_dicts,
|
||||
)
|
||||
|
||||
@@ -79,6 +87,59 @@ def test_literal_default_contract_preserves_exact_builtin_type(
|
||||
assert "changed its released positional parameter prefix" in errors[0]
|
||||
|
||||
|
||||
def test_type_default_contract_preserves_identity() -> None:
|
||||
assert _default_contract(int) == {
|
||||
"kind": "type",
|
||||
"identity": "builtins.int",
|
||||
}
|
||||
|
||||
def released_callable(value: type = int) -> None:
|
||||
_ = value
|
||||
|
||||
def changed_callable(value: type = float) -> None:
|
||||
_ = value
|
||||
|
||||
errors = _validate_parameter_contract(
|
||||
"Example",
|
||||
_parameter_contract(released_callable),
|
||||
_parameter_contract(changed_callable),
|
||||
)
|
||||
|
||||
assert len(errors) == 1
|
||||
assert "changed its released positional parameter prefix" in errors[0]
|
||||
|
||||
|
||||
def test_optional_dependency_for_module_import_uses_canonical_bindings() -> None:
|
||||
contract = {
|
||||
"required_submodule_exports": {
|
||||
"agents.voice": {
|
||||
"names": ["VoicePipeline"],
|
||||
"optional_bindings": {"VoicePipeline": "numpy"},
|
||||
"optional_exports": {},
|
||||
}
|
||||
},
|
||||
"canonical_imports": [
|
||||
{
|
||||
"canonical_module": "agents.voice.pipeline",
|
||||
"canonical_name": "VoicePipeline",
|
||||
"module": "agents.voice",
|
||||
"name": "VoicePipeline",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
assert (
|
||||
contract_support._optional_dependency_for_module_import(contract, "agents.voice.pipeline")
|
||||
== "numpy"
|
||||
)
|
||||
assert (
|
||||
contract_support._optional_dependency_for_binding(
|
||||
contract, "agents.voice.pipeline", "VoicePipeline"
|
||||
)
|
||||
== "numpy"
|
||||
)
|
||||
|
||||
|
||||
def test_released_api_contract_fixture_matches_installed_version() -> None:
|
||||
contract = load_api_contract(CONTRACT)
|
||||
assert contract["baseline"] == f"v{version('openai-agents')}"
|
||||
@@ -255,12 +316,19 @@ def test_public_class_member_contract_tracks_direct_callable_bindings() -> None:
|
||||
|
||||
|
||||
def test_curated_public_property_contract_detects_removed_or_changed_properties() -> None:
|
||||
class ReleasedBase:
|
||||
class ReleasedBase(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def base_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def retained(self) -> str:
|
||||
return "value"
|
||||
|
||||
class Released(ReleasedBase):
|
||||
def base_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def retained(self) -> str:
|
||||
return "value"
|
||||
@@ -293,21 +361,89 @@ def test_curated_public_property_contract_detects_removed_or_changed_properties(
|
||||
"agents.ReleasedBase.removed removed or changed a released public property"
|
||||
]
|
||||
|
||||
Changed = type(
|
||||
"Changed",
|
||||
(ReleasedBase,),
|
||||
{"retained": lambda self: "value"},
|
||||
)
|
||||
class Changed(ReleasedBase):
|
||||
def base_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def new_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
cast(type[Any], Changed)()
|
||||
|
||||
agents_module.Released = Changed
|
||||
|
||||
assert _validate_public_property_contract(contract, agents_module) == [
|
||||
"agents.ReleasedBase.removed removed or changed a released public property",
|
||||
"agents.Released.retained removed or changed a released public property",
|
||||
"agents.Released.concrete_only removed or changed a released public property",
|
||||
]
|
||||
|
||||
|
||||
def test_curated_public_class_contract_detects_abstract_member_and_state_changes() -> None:
|
||||
class ReleasedBase(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def base_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
class Released(ReleasedBase):
|
||||
def base_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
contract: dict[str, Any] = {
|
||||
"public_class_contracts": [
|
||||
{
|
||||
"abstract_members": ["base_requirement"],
|
||||
"class_name": "ReleasedBase",
|
||||
"module": "agents",
|
||||
},
|
||||
{
|
||||
"abstract": False,
|
||||
"class_name": "Released",
|
||||
"module": "agents",
|
||||
},
|
||||
]
|
||||
}
|
||||
agents_module = SimpleNamespace(__all__=[], ReleasedBase=ReleasedBase, Released=Released)
|
||||
|
||||
assert _validate_public_class_contract(contract, agents_module) == []
|
||||
|
||||
class ChangedBase(ReleasedBase):
|
||||
@abc.abstractmethod
|
||||
def new_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
class Changed(ChangedBase):
|
||||
def base_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
def new_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
class ExistingExternalSubclass(ChangedBase):
|
||||
def base_requirement(self) -> None:
|
||||
pass
|
||||
|
||||
assert not inspect.isabstract(Changed)
|
||||
with pytest.raises(TypeError):
|
||||
cast(type[Any], ExistingExternalSubclass)()
|
||||
|
||||
agents_module.ReleasedBase = ChangedBase
|
||||
agents_module.Released = Changed
|
||||
|
||||
assert _validate_public_class_contract(contract, agents_module) == [
|
||||
"agents.ReleasedBase changed its released public abstract members: expected "
|
||||
"['base_requirement'], got ['base_requirement', 'new_requirement']"
|
||||
]
|
||||
|
||||
agents_module.Released = ExistingExternalSubclass
|
||||
assert _validate_public_class_contract(contract, agents_module) == [
|
||||
"agents.ReleasedBase changed its released public abstract members: expected "
|
||||
"['base_requirement'], got ['base_requirement', 'new_requirement']",
|
||||
"agents.Released changed its released public class state: expected concrete, got abstract",
|
||||
]
|
||||
|
||||
|
||||
def test_curated_public_property_contract_supports_factory_return_surfaces(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -341,6 +477,125 @@ def test_curated_public_property_contract_supports_factory_return_surfaces(
|
||||
]
|
||||
|
||||
|
||||
def test_curated_public_type_alias_contract_records_and_validates_members(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
class EventA:
|
||||
pass
|
||||
|
||||
class EventB:
|
||||
pass
|
||||
|
||||
EventA.__module__ = "agents.aliases"
|
||||
EventA.__qualname__ = "EventA"
|
||||
EventB.__module__ = "agents.aliases"
|
||||
EventB.__qualname__ = "EventB"
|
||||
|
||||
agents_module = SimpleNamespace(__all__=[])
|
||||
aliases_module = SimpleNamespace(
|
||||
__all__=[],
|
||||
PublicAlias=Literal["b", "a"] | EventB | EventA,
|
||||
)
|
||||
modules = {
|
||||
"agents": agents_module,
|
||||
"agents.aliases": aliases_module,
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
contract_support,
|
||||
"_import_contract_module",
|
||||
lambda module_name, _agents_module: modules[module_name],
|
||||
)
|
||||
contract: dict[str, Any] = {
|
||||
"baseline": "v0.19.4",
|
||||
"baseline_commit": "a" * 40,
|
||||
"required_top_level_exports": [],
|
||||
"public_modules": ["agents"],
|
||||
"canonical_imports": [],
|
||||
"public_class_contracts": [],
|
||||
"public_properties": [],
|
||||
"public_type_aliases": [],
|
||||
"public_typed_dicts": [],
|
||||
"callables": {},
|
||||
}
|
||||
|
||||
updated = build_released_api_contract(
|
||||
contract,
|
||||
baseline="v0.20.0",
|
||||
baseline_commit="b" * 40,
|
||||
agents_module=agents_module,
|
||||
release_policy=_release_policy(
|
||||
{"agents.aliases": {"optional_bindings": {}, "optional_exports": {}}},
|
||||
public_type_aliases=(
|
||||
{
|
||||
"module": "agents.aliases",
|
||||
"name": "PublicAlias",
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
assert updated["public_type_aliases"] == [
|
||||
{
|
||||
"definition": {
|
||||
"kind": "union",
|
||||
"members": [
|
||||
{
|
||||
"kind": "literal",
|
||||
"values": [
|
||||
{
|
||||
"kind": "literal",
|
||||
"type": "builtins.str",
|
||||
"value": "a",
|
||||
},
|
||||
{
|
||||
"kind": "literal",
|
||||
"type": "builtins.str",
|
||||
"value": "b",
|
||||
},
|
||||
],
|
||||
},
|
||||
{"identity": "agents.aliases.EventA", "kind": "type"},
|
||||
{"identity": "agents.aliases.EventB", "kind": "type"},
|
||||
],
|
||||
},
|
||||
"module": "agents.aliases",
|
||||
"name": "PublicAlias",
|
||||
}
|
||||
]
|
||||
|
||||
aliases_module.PublicAlias = Literal["a", "b"] | EventA | EventB
|
||||
assert _validate_public_type_alias_contract(updated, agents_module) == []
|
||||
|
||||
aliases_module.PublicAlias = Literal["a"] | EventA
|
||||
errors = _validate_public_type_alias_contract(updated, agents_module)
|
||||
assert len(errors) == 1
|
||||
assert errors[0].startswith("agents.aliases.PublicAlias changed its released public type alias")
|
||||
|
||||
aliases_module.PublicAlias = list[str]
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=(
|
||||
r"agents\.aliases\.PublicAlias no longer has a supported released public type alias "
|
||||
r"definition: unsupported"
|
||||
),
|
||||
):
|
||||
build_released_api_contract(
|
||||
updated,
|
||||
baseline="v0.20.1",
|
||||
baseline_commit="c" * 40,
|
||||
agents_module=agents_module,
|
||||
release_policy=_release_policy(
|
||||
{"agents.aliases": {"optional_bindings": {}, "optional_exports": {}}},
|
||||
public_type_aliases=(
|
||||
{
|
||||
"module": "agents.aliases",
|
||||
"name": "PublicAlias",
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_curated_public_typed_dict_contract_detects_field_shape_drift(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
@@ -1685,6 +1940,13 @@ def test_release_contract_policy_promotes_curated_public_state_surfaces(
|
||||
"name": "NewPublic",
|
||||
},
|
||||
),
|
||||
public_class_contracts=(
|
||||
{
|
||||
"abstract": False,
|
||||
"class_name": "NewPublic",
|
||||
"module": "agents.submodule",
|
||||
},
|
||||
),
|
||||
public_properties=(
|
||||
{
|
||||
"class_name": "NewPublic",
|
||||
@@ -1727,6 +1989,13 @@ def test_release_contract_policy_promotes_curated_public_state_surfaces(
|
||||
"names": ["calls"],
|
||||
},
|
||||
]
|
||||
assert updated["public_class_contracts"] == [
|
||||
{
|
||||
"abstract": False,
|
||||
"class_name": "NewPublic",
|
||||
"module": "agents.submodule",
|
||||
}
|
||||
]
|
||||
assert updated["public_typed_dicts"] == [
|
||||
{
|
||||
"class_name": "PublicState",
|
||||
@@ -2428,9 +2697,14 @@ def test_load_submodule_export_policy_collects_artifact_installations(tmp_path:
|
||||
'{"ConditionalExport": "export_dependency"}}}, "optional_dependencies": '
|
||||
'{"binding_dependency": {"requirement": "binding-package>=1"}, '
|
||||
'"export_dependency": {"extra": "export-extra"}}, "public_properties": '
|
||||
'[{"class_name": "ConditionalExport", "module": "agents.submodule", '
|
||||
'"names": ["status"]}, {"factory_name": "create_client", '
|
||||
'"module": "agents.submodule", "names": ["calls"]}], "public_typed_dicts": '
|
||||
'[{"class_name": "ConditionalExport", '
|
||||
'"module": "agents.submodule", "names": ["status"]}, '
|
||||
'{"factory_name": "create_client", '
|
||||
'"module": "agents.submodule", "names": ["calls"]}], "public_class_contracts": '
|
||||
'[{"abstract": false, "class_name": "ConditionalExport", '
|
||||
'"module": "agents.submodule"}], "public_type_aliases": '
|
||||
'[{"module": "agents.submodule", "name": "PublicAlias"}], '
|
||||
'"public_typed_dicts": '
|
||||
'[{"class_name": "ClientState", "module": "agents.submodule", '
|
||||
'"names": ["status"]}]}',
|
||||
encoding="utf-8",
|
||||
@@ -2466,6 +2740,13 @@ def test_load_submodule_export_policy_collects_artifact_installations(tmp_path:
|
||||
"name": "ConditionalExport",
|
||||
},
|
||||
)
|
||||
assert policy.public_class_contracts == (
|
||||
{
|
||||
"abstract": False,
|
||||
"class_name": "ConditionalExport",
|
||||
"module": "agents.submodule",
|
||||
},
|
||||
)
|
||||
assert tuple(
|
||||
entry
|
||||
for entry in policy.public_properties
|
||||
@@ -2488,6 +2769,12 @@ def test_load_submodule_export_policy_collects_artifact_installations(tmp_path:
|
||||
"names": ["calls"],
|
||||
},
|
||||
)
|
||||
assert policy.public_type_aliases == (
|
||||
{
|
||||
"module": "agents.submodule",
|
||||
"name": "PublicAlias",
|
||||
},
|
||||
)
|
||||
assert policy.public_typed_dicts == (
|
||||
{
|
||||
"class_name": "ClientState",
|
||||
@@ -2497,6 +2784,23 @@ def test_load_submodule_export_policy_collects_artifact_installations(tmp_path:
|
||||
)
|
||||
|
||||
|
||||
def test_load_submodule_export_policy_rejects_invalid_class_abstract_state(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
policy_path = tmp_path / "policy.json"
|
||||
policy_path.write_text(
|
||||
'{"modules": {}, "optional_dependencies": {}, "public_class_contracts": '
|
||||
'[{"abstract": "false", "class_name": "Released", "module": "agents"}]}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="public_class_contracts abstract must be a boolean",
|
||||
):
|
||||
load_submodule_export_policy(policy_path)
|
||||
|
||||
|
||||
def test_load_submodule_export_policy_collects_unsupported_platforms(tmp_path: Path) -> None:
|
||||
policy_path = tmp_path / "policy.json"
|
||||
policy_path.write_text(
|
||||
@@ -2544,6 +2848,10 @@ def test_repository_release_policy_declares_v020_contract_surfaces() -> None:
|
||||
"agents.testing.model",
|
||||
"agents.testing.sandbox",
|
||||
"agents.realtime.testing",
|
||||
"agents.voice.model",
|
||||
"agents.voice.models.openai_model_provider",
|
||||
"agents.voice.models.openai_stt",
|
||||
"agents.voice.models.openai_tts",
|
||||
"agents.voice.testing",
|
||||
}
|
||||
) == (
|
||||
@@ -2580,23 +2888,56 @@ def test_repository_release_policy_declares_v020_contract_surfaces() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_repository_release_policy_declares_public_testing_modules() -> None:
|
||||
def test_repository_release_policy_declares_public_optional_modules() -> None:
|
||||
policy = load_submodule_export_policy(CONTRACT.with_name("released_api_contract_policy.json"))
|
||||
expected_modules = {
|
||||
documented_voice_modules = {
|
||||
"agents.voice.events",
|
||||
"agents.voice.exceptions",
|
||||
"agents.voice.input",
|
||||
"agents.voice.imports",
|
||||
"agents.voice.model",
|
||||
"agents.voice.models.openai_model_provider",
|
||||
"agents.voice.models.openai_stt",
|
||||
"agents.voice.models.openai_tts",
|
||||
"agents.voice.pipeline",
|
||||
"agents.voice.pipeline_config",
|
||||
"agents.voice.result",
|
||||
"agents.voice.testing",
|
||||
"agents.voice.utils",
|
||||
"agents.voice.workflow",
|
||||
}
|
||||
expected_modules = documented_voice_modules | {
|
||||
"agents.realtime.testing",
|
||||
"agents.testing",
|
||||
"agents.testing.model",
|
||||
"agents.testing.sandbox",
|
||||
"agents.voice.testing",
|
||||
"agents.voice",
|
||||
}
|
||||
|
||||
assert expected_modules <= policy.modules.keys()
|
||||
assert policy.modules["agents.voice.testing"] == {
|
||||
"optional_bindings": {
|
||||
export: "numpy" for export in importlib.import_module("agents.voice.testing").__all__
|
||||
},
|
||||
"optional_exports": {},
|
||||
documented_directive_modules = {
|
||||
line.removeprefix("::: ")
|
||||
for path in (CONTRACT.parents[2] / "docs" / "ref" / "voice").rglob("*.md")
|
||||
for line in path.read_text(encoding="utf-8").splitlines()
|
||||
if line.startswith("::: agents.voice")
|
||||
}
|
||||
|
||||
assert documented_directive_modules == documented_voice_modules
|
||||
assert expected_modules <= policy.modules.keys()
|
||||
for module_name in documented_voice_modules - {
|
||||
"agents.voice.imports",
|
||||
"agents.voice.testing",
|
||||
}:
|
||||
assert policy.modules[module_name] == {
|
||||
"optional_bindings": {},
|
||||
"optional_exports": {},
|
||||
}
|
||||
for module_name in ("agents.voice", "agents.voice.imports", "agents.voice.testing"):
|
||||
assert policy.modules[module_name] == {
|
||||
"optional_bindings": {
|
||||
export: "numpy" for export in importlib.import_module(module_name).__all__
|
||||
},
|
||||
"optional_exports": {},
|
||||
}
|
||||
assert (
|
||||
next(
|
||||
installation
|
||||
@@ -2607,7 +2948,7 @@ def test_repository_release_policy_declares_public_testing_modules() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_repository_release_policy_declares_public_testing_state_surfaces() -> None:
|
||||
def test_repository_release_policy_declares_public_state_surfaces() -> None:
|
||||
policy = load_submodule_export_policy(CONTRACT.with_name("released_api_contract_policy.json"))
|
||||
expected_modules = {
|
||||
"agents.realtime.testing",
|
||||
@@ -2659,6 +3000,133 @@ def test_repository_release_policy_declares_public_testing_state_surfaces() -> N
|
||||
"names": ["transcriptions"],
|
||||
},
|
||||
)
|
||||
assert tuple(
|
||||
entry
|
||||
for entry in policy.public_properties
|
||||
if entry["module"]
|
||||
in {
|
||||
"agents.voice.model",
|
||||
"agents.voice.models.openai_model_provider",
|
||||
"agents.voice.models.openai_stt",
|
||||
"agents.voice.models.openai_tts",
|
||||
}
|
||||
) == (
|
||||
{
|
||||
"class_name": "STTModel",
|
||||
"module": "agents.voice.model",
|
||||
"names": ["model_name"],
|
||||
},
|
||||
{
|
||||
"class_name": "TTSModel",
|
||||
"module": "agents.voice.model",
|
||||
"names": ["model_name"],
|
||||
},
|
||||
{
|
||||
"class_name": "OpenAISTTModel",
|
||||
"module": "agents.voice.models.openai_stt",
|
||||
"names": ["model_name"],
|
||||
},
|
||||
{
|
||||
"class_name": "OpenAITTSModel",
|
||||
"module": "agents.voice.models.openai_tts",
|
||||
"names": ["model_name"],
|
||||
},
|
||||
{
|
||||
"class_name": "OpenAIVoiceModelProvider",
|
||||
"module": "agents.voice.models.openai_model_provider",
|
||||
"names": ["agent_registration"],
|
||||
},
|
||||
)
|
||||
assert policy.public_class_contracts == (
|
||||
{
|
||||
"class_name": "STTModel",
|
||||
"module": "agents.voice.model",
|
||||
"abstract_members": ["create_session", "model_name", "transcribe"],
|
||||
},
|
||||
{
|
||||
"class_name": "StreamedTranscriptionSession",
|
||||
"module": "agents.voice.model",
|
||||
"abstract_members": ["close", "transcribe_turns"],
|
||||
},
|
||||
{
|
||||
"class_name": "TTSModel",
|
||||
"module": "agents.voice.model",
|
||||
"abstract_members": ["model_name", "run"],
|
||||
},
|
||||
{
|
||||
"class_name": "VoiceModelProvider",
|
||||
"module": "agents.voice.model",
|
||||
"abstract_members": ["get_stt_model", "get_tts_model"],
|
||||
},
|
||||
{
|
||||
"abstract": False,
|
||||
"class_name": "OpenAISTTModel",
|
||||
"module": "agents.voice.models.openai_stt",
|
||||
},
|
||||
{
|
||||
"abstract": False,
|
||||
"class_name": "OpenAISTTTranscriptionSession",
|
||||
"module": "agents.voice.models.openai_stt",
|
||||
},
|
||||
{
|
||||
"abstract": False,
|
||||
"class_name": "OpenAITTSModel",
|
||||
"module": "agents.voice.models.openai_tts",
|
||||
},
|
||||
{
|
||||
"class_name": "VoiceWorkflowBase",
|
||||
"module": "agents.voice.workflow",
|
||||
"abstract_members": ["run"],
|
||||
},
|
||||
)
|
||||
assert policy.public_type_aliases == (
|
||||
{
|
||||
"module": "agents.voice.model",
|
||||
"name": "TTSVoice",
|
||||
},
|
||||
{
|
||||
"module": "agents.voice.events",
|
||||
"name": "VoiceStreamEvent",
|
||||
},
|
||||
)
|
||||
type_aliases: dict[tuple[str, str], dict[str, Any]] = {
|
||||
(cast(str, entry["module"]), cast(str, entry["name"])): cast(
|
||||
dict[str, Any], entry["definition"]
|
||||
)
|
||||
for entry in contract_support._public_type_alias_contract(policy.public_type_aliases, None)
|
||||
}
|
||||
tts_voice = type_aliases[("agents.voice.model", "TTSVoice")]
|
||||
assert tts_voice["kind"] == "union"
|
||||
assert [
|
||||
value["value"]
|
||||
for member in tts_voice["members"]
|
||||
if member["kind"] == "literal"
|
||||
for value in member["values"]
|
||||
] == [
|
||||
"alloy",
|
||||
"ash",
|
||||
"ballad",
|
||||
"cedar",
|
||||
"coral",
|
||||
"echo",
|
||||
"fable",
|
||||
"marin",
|
||||
"nova",
|
||||
"onyx",
|
||||
"sage",
|
||||
"shimmer",
|
||||
"verse",
|
||||
]
|
||||
assert {member["identity"] for member in tts_voice["members"] if member["kind"] == "type"} == {
|
||||
"agents.voice.model.TTSCustomVoice"
|
||||
}
|
||||
voice_stream_event = type_aliases[("agents.voice.events", "VoiceStreamEvent")]
|
||||
assert voice_stream_event["kind"] == "union"
|
||||
assert {member["identity"] for member in voice_stream_event["members"]} == {
|
||||
"agents.voice.events.VoiceStreamEventAudio",
|
||||
"agents.voice.events.VoiceStreamEventError",
|
||||
"agents.voice.events.VoiceStreamEventLifecycle",
|
||||
}
|
||||
assert policy.public_typed_dicts == (
|
||||
{
|
||||
"class_name": "ModelStepSpec",
|
||||
@@ -2692,6 +3160,11 @@ def test_repository_release_policy_declares_public_testing_state_surfaces() -> N
|
||||
"call_id",
|
||||
],
|
||||
},
|
||||
{
|
||||
"class_name": "TTSCustomVoice",
|
||||
"module": "agents.voice",
|
||||
"names": ["id"],
|
||||
},
|
||||
)
|
||||
for module_name in expected_modules:
|
||||
module = importlib.import_module(module_name)
|
||||
@@ -2722,6 +3195,56 @@ def test_repository_release_policy_declares_public_testing_state_surfaces() -> N
|
||||
canonical_module = importlib.import_module(canonical_module_name)
|
||||
assert getattr(module, name) is getattr(canonical_module, canonical_name)
|
||||
|
||||
voice_canonical_modules = {
|
||||
"AudioInput": "agents.voice.input",
|
||||
"StreamedAudioInput": "agents.voice.input",
|
||||
"STTModel": "agents.voice.model",
|
||||
"STTModelSettings": "agents.voice.model",
|
||||
"TTSCustomVoice": "agents.voice.model",
|
||||
"TTSModel": "agents.voice.model",
|
||||
"TTSModelSettings": "agents.voice.model",
|
||||
"TTSVoice": "agents.voice.model",
|
||||
"VoiceModelProvider": "agents.voice.model",
|
||||
"StreamedAudioResult": "agents.voice.result",
|
||||
"SingleAgentVoiceWorkflow": "agents.voice.workflow",
|
||||
"OpenAIVoiceModelProvider": "agents.voice.models.openai_model_provider",
|
||||
"OpenAISTTModel": "agents.voice.models.openai_stt",
|
||||
"OpenAITTSModel": "agents.voice.models.openai_tts",
|
||||
"VoiceStreamEventAudio": "agents.voice.events",
|
||||
"VoiceStreamEventError": "agents.voice.events",
|
||||
"VoiceStreamEventLifecycle": "agents.voice.events",
|
||||
"VoiceStreamEvent": "agents.voice.events",
|
||||
"VoicePipeline": "agents.voice.pipeline",
|
||||
"VoicePipelineConfig": "agents.voice.pipeline_config",
|
||||
"get_sentence_based_splitter": "agents.voice.utils",
|
||||
"VoiceWorkflowHelper": "agents.voice.workflow",
|
||||
"VoiceWorkflowBase": "agents.voice.workflow",
|
||||
"SingleAgentWorkflowCallbacks": "agents.voice.workflow",
|
||||
"StreamedTranscriptionSession": "agents.voice.model",
|
||||
"OpenAISTTTranscriptionSession": "agents.voice.models.openai_stt",
|
||||
"STTWebsocketConnectionError": "agents.voice.exceptions",
|
||||
}
|
||||
expected_voice_canonical_imports = {
|
||||
("agents.voice", name, canonical_module_name, name)
|
||||
for name, canonical_module_name in voice_canonical_modules.items()
|
||||
}
|
||||
actual_voice_canonical_imports = {
|
||||
(
|
||||
entry["module"],
|
||||
entry["name"],
|
||||
entry["canonical_module"],
|
||||
entry["canonical_name"],
|
||||
)
|
||||
for entry in policy.canonical_imports
|
||||
if entry["module"] == "agents.voice"
|
||||
}
|
||||
|
||||
assert actual_voice_canonical_imports == expected_voice_canonical_imports
|
||||
for module_name, name, canonical_module_name, canonical_name in actual_voice_canonical_imports:
|
||||
module = importlib.import_module(module_name)
|
||||
canonical_module = importlib.import_module(canonical_module_name)
|
||||
assert getattr(module, name) is getattr(canonical_module, canonical_name)
|
||||
|
||||
|
||||
def test_voice_testing_start_sentinel_has_stable_contract_identity() -> None:
|
||||
from agents.voice.testing import _START_NOT_CONFIGURED
|
||||
|
||||
Reference in New Issue
Block a user