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 copy import deepcopy
|
||||||
from importlib.util import find_spec
|
from importlib.util import find_spec
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import FunctionType, TracebackType
|
from types import FunctionType, TracebackType, UnionType
|
||||||
from typing import Any, ForwardRef, cast, get_origin, get_type_hints
|
from typing import Any, ForwardRef, Literal, Union, cast, get_args, get_origin, get_type_hints
|
||||||
|
|
||||||
import typing_extensions
|
import typing_extensions
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -36,7 +36,9 @@ class SubmoduleExportPolicy:
|
|||||||
modules: dict[str, dict[str, dict[str, str]]]
|
modules: dict[str, dict[str, dict[str, str]]]
|
||||||
dependency_installations: tuple[OptionalDependencyInstallation, ...]
|
dependency_installations: tuple[OptionalDependencyInstallation, ...]
|
||||||
canonical_imports: tuple[dict[str, str], ...] = ()
|
canonical_imports: tuple[dict[str, str], ...] = ()
|
||||||
|
public_class_contracts: tuple[dict[str, Any], ...] = ()
|
||||||
public_properties: tuple[dict[str, Any], ...] = ()
|
public_properties: tuple[dict[str, Any], ...] = ()
|
||||||
|
public_type_aliases: tuple[dict[str, str], ...] = ()
|
||||||
public_typed_dicts: tuple[dict[str, Any], ...] = ()
|
public_typed_dicts: tuple[dict[str, Any], ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@@ -56,7 +58,9 @@ def load_submodule_export_policy(path: Path) -> SubmoduleExportPolicy:
|
|||||||
"canonical_imports",
|
"canonical_imports",
|
||||||
"modules",
|
"modules",
|
||||||
"optional_dependencies",
|
"optional_dependencies",
|
||||||
|
"public_class_contracts",
|
||||||
"public_properties",
|
"public_properties",
|
||||||
|
"public_type_aliases",
|
||||||
"public_typed_dicts",
|
"public_typed_dicts",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -164,7 +168,11 @@ def load_submodule_export_policy(path: Path) -> SubmoduleExportPolicy:
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
canonical_imports=_canonical_import_policy(value.get("canonical_imports", [])),
|
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_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", [])),
|
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):
|
if not isinstance(entry, dict):
|
||||||
raise ValueError("submodule export policy public_properties entries must be objects")
|
raise ValueError("submodule export policy public_properties entries must be objects")
|
||||||
owner_fields = {"class_name", "factory_name"} & set(entry)
|
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(
|
raise ValueError(
|
||||||
"submodule export policy public_properties entries must contain exactly "
|
"submodule export policy public_properties entries must contain exactly "
|
||||||
"module, names, and one of class_name or factory_name"
|
"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}"
|
f"{module_name}.{owner_name}"
|
||||||
)
|
)
|
||||||
identities.add(identity)
|
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)
|
return tuple(entries)
|
||||||
|
|
||||||
|
|
||||||
@@ -288,6 +365,33 @@ def _public_typed_dict_policy(value: object) -> tuple[dict[str, Any], ...]:
|
|||||||
return tuple(entries)
|
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:
|
def _add_legacy_literal_types(value: object) -> None:
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
if value.get("kind") == "literal" and "value" in value and "type" not in value:
|
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,
|
"name": value.name,
|
||||||
"value": _default_contract(value.value),
|
"value": _default_contract(value.value),
|
||||||
}
|
}
|
||||||
|
if isinstance(value, type):
|
||||||
|
return {
|
||||||
|
"kind": "type",
|
||||||
|
"identity": f"{value.__module__}.{value.__qualname__}",
|
||||||
|
}
|
||||||
if isinstance(value, tuple | list):
|
if isinstance(value, tuple | list):
|
||||||
return {
|
return {
|
||||||
"kind": "sequence",
|
"kind": "sequence",
|
||||||
@@ -723,6 +832,31 @@ def _merge_public_properties(
|
|||||||
return result
|
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]:
|
def _public_property_identity(entry: Mapping[str, Any]) -> tuple[str, str, str]:
|
||||||
if "class_name" in entry:
|
if "class_name" in entry:
|
||||||
return ("class_name", cast(str, entry["module"]), cast(str, entry["class_name"]))
|
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
|
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:
|
def _typed_dict_field_is_required(typed_dict: type, name: str, annotation: object) -> bool:
|
||||||
if isinstance(annotation, ForwardRef):
|
if isinstance(annotation, ForwardRef):
|
||||||
annotation_text = annotation.__forward_arg__
|
annotation_text = annotation.__forward_arg__
|
||||||
@@ -879,9 +1093,21 @@ def _optional_dependency_is_unsupported_for_contract(
|
|||||||
def _optional_dependency_for_binding(
|
def _optional_dependency_for_binding(
|
||||||
contract: Mapping[str, Any], module_name: str, binding_name: str
|
contract: Mapping[str, Any], module_name: str, binding_name: str
|
||||||
) -> str | None:
|
) -> 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
|
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(
|
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}
|
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:
|
if names and len(dependencies) == 1 and None not in dependencies:
|
||||||
return cast(str, next(iter(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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -1086,10 +1323,20 @@ def build_released_api_contract(
|
|||||||
updated["required_top_level_exports"] = ordered_exports
|
updated["required_top_level_exports"] = ordered_exports
|
||||||
updated["callables"] = callables
|
updated["callables"] = callables
|
||||||
updated["canonical_imports"] = canonical_imports
|
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(
|
updated["public_properties"] = _merge_public_properties(
|
||||||
contract.get("public_properties", []),
|
contract.get("public_properties", []),
|
||||||
release_policy.public_properties if release_policy is not None else (),
|
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(
|
updated["public_typed_dicts"] = _merge_public_typed_dicts(
|
||||||
contract.get("public_typed_dicts", []),
|
contract.get("public_typed_dicts", []),
|
||||||
_public_typed_dict_contract(release_policy.public_typed_dicts, agents_module)
|
_public_typed_dict_contract(release_policy.public_typed_dicts, agents_module)
|
||||||
@@ -1220,7 +1467,9 @@ def build_released_api_contract(
|
|||||||
"callables",
|
"callables",
|
||||||
"optional_dependency_unsupported_platforms",
|
"optional_dependency_unsupported_platforms",
|
||||||
"platform_import_errors",
|
"platform_import_errors",
|
||||||
|
"public_class_contracts",
|
||||||
"public_properties",
|
"public_properties",
|
||||||
|
"public_type_aliases",
|
||||||
"public_typed_dicts",
|
"public_typed_dicts",
|
||||||
"public_modules",
|
"public_modules",
|
||||||
"required_submodule_exports",
|
"required_submodule_exports",
|
||||||
@@ -1361,6 +1610,48 @@ def _validate_public_property_contract(
|
|||||||
return errors
|
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(
|
def _validate_public_typed_dict_contract(
|
||||||
contract: dict[str, Any],
|
contract: dict[str, Any],
|
||||||
agents_module: Any | None,
|
agents_module: Any | None,
|
||||||
@@ -1397,6 +1688,48 @@ def _validate_public_typed_dict_contract(
|
|||||||
return errors
|
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(
|
def _submodule_export_contract(
|
||||||
module: object,
|
module: object,
|
||||||
*,
|
*,
|
||||||
@@ -1495,6 +1828,13 @@ def validate_released_api_contract(
|
|||||||
errors.append(f"Invalid released optional dependency platform declarations: {error}")
|
errors.append(f"Invalid released optional dependency platform declarations: {error}")
|
||||||
unsupported_platforms = {}
|
unsupported_platforms = {}
|
||||||
|
|
||||||
|
errors.extend(
|
||||||
|
_validate_public_class_contract(
|
||||||
|
contract,
|
||||||
|
agents_module,
|
||||||
|
unsupported_platforms=unsupported_platforms,
|
||||||
|
)
|
||||||
|
)
|
||||||
errors.extend(
|
errors.extend(
|
||||||
_validate_public_property_contract(
|
_validate_public_property_contract(
|
||||||
contract,
|
contract,
|
||||||
@@ -1502,6 +1842,13 @@ def validate_released_api_contract(
|
|||||||
unsupported_platforms=unsupported_platforms,
|
unsupported_platforms=unsupported_platforms,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
errors.extend(
|
||||||
|
_validate_public_type_alias_contract(
|
||||||
|
contract,
|
||||||
|
agents_module,
|
||||||
|
unsupported_platforms=unsupported_platforms,
|
||||||
|
)
|
||||||
|
)
|
||||||
errors.extend(
|
errors.extend(
|
||||||
_validate_public_typed_dict_contract(
|
_validate_public_typed_dict_contract(
|
||||||
contract,
|
contract,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
from .events import VoiceStreamEvent, VoiceStreamEventAudio, VoiceStreamEventLifecycle
|
from .events import (
|
||||||
|
VoiceStreamEvent,
|
||||||
|
VoiceStreamEventAudio,
|
||||||
|
VoiceStreamEventError,
|
||||||
|
VoiceStreamEventLifecycle,
|
||||||
|
)
|
||||||
from .exceptions import STTWebsocketConnectionError
|
from .exceptions import STTWebsocketConnectionError
|
||||||
from .input import AudioInput, StreamedAudioInput
|
from .input import AudioInput, StreamedAudioInput
|
||||||
from .model import (
|
from .model import (
|
||||||
@@ -41,6 +46,7 @@ __all__ = [
|
|||||||
"OpenAISTTModel",
|
"OpenAISTTModel",
|
||||||
"OpenAITTSModel",
|
"OpenAITTSModel",
|
||||||
"VoiceStreamEventAudio",
|
"VoiceStreamEventAudio",
|
||||||
|
"VoiceStreamEventError",
|
||||||
"VoiceStreamEventLifecycle",
|
"VoiceStreamEventLifecycle",
|
||||||
"VoiceStreamEvent",
|
"VoiceStreamEvent",
|
||||||
"VoicePipeline",
|
"VoicePipeline",
|
||||||
|
|||||||
+359
@@ -197,6 +197,168 @@
|
|||||||
"canonical_name": "VercelSandboxClientOptions",
|
"canonical_name": "VercelSandboxClientOptions",
|
||||||
"module": "agents.extensions.sandbox",
|
"module": "agents.extensions.sandbox",
|
||||||
"name": "VercelSandboxClientOptions"
|
"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": {
|
"optional_dependencies": {
|
||||||
@@ -304,6 +466,86 @@
|
|||||||
"optional_bindings": {},
|
"optional_bindings": {},
|
||||||
"optional_exports": {}
|
"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": {
|
"agents.voice.testing": {
|
||||||
"optional_bindings": {
|
"optional_bindings": {
|
||||||
"STTCall": "numpy",
|
"STTCall": "numpy",
|
||||||
@@ -320,8 +562,73 @@
|
|||||||
"pcm16_samples": "numpy"
|
"pcm16_samples": "numpy"
|
||||||
},
|
},
|
||||||
"optional_exports": {}
|
"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": [
|
"public_properties": [
|
||||||
{
|
{
|
||||||
"class_name": "RunState",
|
"class_name": "RunState",
|
||||||
@@ -390,6 +697,41 @@
|
|||||||
"transcriptions"
|
"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",
|
"class_name": "RunloopPlatformClient",
|
||||||
"module": "agents.extensions.sandbox",
|
"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": [
|
"public_typed_dicts": [
|
||||||
{
|
{
|
||||||
"class_name": "ModelStepSpec",
|
"class_name": "ModelStepSpec",
|
||||||
@@ -463,6 +815,13 @@
|
|||||||
"playback_tracker",
|
"playback_tracker",
|
||||||
"call_id"
|
"call_id"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"class_name": "TTSCustomVoice",
|
||||||
|
"module": "agents.voice",
|
||||||
|
"names": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import abc
|
||||||
import builtins
|
import builtins
|
||||||
import importlib
|
import importlib
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -10,7 +12,7 @@ from importlib.metadata import version
|
|||||||
from inspect import Parameter, Signature
|
from inspect import Parameter, Signature
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any, Literal, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -25,7 +27,9 @@ from integration_tests._contract_support import (
|
|||||||
_parameter_contract,
|
_parameter_contract,
|
||||||
_public_class_member_contract,
|
_public_class_member_contract,
|
||||||
_validate_parameter_contract,
|
_validate_parameter_contract,
|
||||||
|
_validate_public_class_contract,
|
||||||
_validate_public_property_contract,
|
_validate_public_property_contract,
|
||||||
|
_validate_public_type_alias_contract,
|
||||||
_validate_public_typed_dict_contract,
|
_validate_public_typed_dict_contract,
|
||||||
build_released_api_contract,
|
build_released_api_contract,
|
||||||
load_api_contract,
|
load_api_contract,
|
||||||
@@ -41,14 +45,18 @@ def _release_policy(
|
|||||||
*,
|
*,
|
||||||
dependency_installations: tuple[OptionalDependencyInstallation, ...] = (),
|
dependency_installations: tuple[OptionalDependencyInstallation, ...] = (),
|
||||||
canonical_imports: tuple[dict[str, str], ...] = (),
|
canonical_imports: tuple[dict[str, str], ...] = (),
|
||||||
|
public_class_contracts: tuple[dict[str, Any], ...] = (),
|
||||||
public_properties: tuple[dict[str, Any], ...] = (),
|
public_properties: tuple[dict[str, Any], ...] = (),
|
||||||
|
public_type_aliases: tuple[dict[str, str], ...] = (),
|
||||||
public_typed_dicts: tuple[dict[str, Any], ...] = (),
|
public_typed_dicts: tuple[dict[str, Any], ...] = (),
|
||||||
) -> SubmoduleExportPolicy:
|
) -> SubmoduleExportPolicy:
|
||||||
return SubmoduleExportPolicy(
|
return SubmoduleExportPolicy(
|
||||||
modules=modules,
|
modules=modules,
|
||||||
dependency_installations=dependency_installations,
|
dependency_installations=dependency_installations,
|
||||||
canonical_imports=canonical_imports,
|
canonical_imports=canonical_imports,
|
||||||
|
public_class_contracts=public_class_contracts,
|
||||||
public_properties=public_properties,
|
public_properties=public_properties,
|
||||||
|
public_type_aliases=public_type_aliases,
|
||||||
public_typed_dicts=public_typed_dicts,
|
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]
|
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:
|
def test_released_api_contract_fixture_matches_installed_version() -> None:
|
||||||
contract = load_api_contract(CONTRACT)
|
contract = load_api_contract(CONTRACT)
|
||||||
assert contract["baseline"] == f"v{version('openai-agents')}"
|
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:
|
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
|
@property
|
||||||
def retained(self) -> str:
|
def retained(self) -> str:
|
||||||
return "value"
|
return "value"
|
||||||
|
|
||||||
class Released(ReleasedBase):
|
class Released(ReleasedBase):
|
||||||
|
def base_requirement(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def retained(self) -> str:
|
def retained(self) -> str:
|
||||||
return "value"
|
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"
|
"agents.ReleasedBase.removed removed or changed a released public property"
|
||||||
]
|
]
|
||||||
|
|
||||||
Changed = type(
|
class Changed(ReleasedBase):
|
||||||
"Changed",
|
def base_requirement(self) -> None:
|
||||||
(ReleasedBase,),
|
pass
|
||||||
{"retained": lambda self: "value"},
|
|
||||||
)
|
@abc.abstractmethod
|
||||||
|
def new_requirement(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
cast(type[Any], Changed)()
|
||||||
|
|
||||||
agents_module.Released = Changed
|
agents_module.Released = Changed
|
||||||
|
|
||||||
assert _validate_public_property_contract(contract, agents_module) == [
|
assert _validate_public_property_contract(contract, agents_module) == [
|
||||||
"agents.ReleasedBase.removed removed or changed a released public property",
|
"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",
|
"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(
|
def test_curated_public_property_contract_supports_factory_return_surfaces(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> 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(
|
def test_curated_public_typed_dict_contract_detects_field_shape_drift(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -1685,6 +1940,13 @@ def test_release_contract_policy_promotes_curated_public_state_surfaces(
|
|||||||
"name": "NewPublic",
|
"name": "NewPublic",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
public_class_contracts=(
|
||||||
|
{
|
||||||
|
"abstract": False,
|
||||||
|
"class_name": "NewPublic",
|
||||||
|
"module": "agents.submodule",
|
||||||
|
},
|
||||||
|
),
|
||||||
public_properties=(
|
public_properties=(
|
||||||
{
|
{
|
||||||
"class_name": "NewPublic",
|
"class_name": "NewPublic",
|
||||||
@@ -1727,6 +1989,13 @@ def test_release_contract_policy_promotes_curated_public_state_surfaces(
|
|||||||
"names": ["calls"],
|
"names": ["calls"],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
assert updated["public_class_contracts"] == [
|
||||||
|
{
|
||||||
|
"abstract": False,
|
||||||
|
"class_name": "NewPublic",
|
||||||
|
"module": "agents.submodule",
|
||||||
|
}
|
||||||
|
]
|
||||||
assert updated["public_typed_dicts"] == [
|
assert updated["public_typed_dicts"] == [
|
||||||
{
|
{
|
||||||
"class_name": "PublicState",
|
"class_name": "PublicState",
|
||||||
@@ -2428,9 +2697,14 @@ def test_load_submodule_export_policy_collects_artifact_installations(tmp_path:
|
|||||||
'{"ConditionalExport": "export_dependency"}}}, "optional_dependencies": '
|
'{"ConditionalExport": "export_dependency"}}}, "optional_dependencies": '
|
||||||
'{"binding_dependency": {"requirement": "binding-package>=1"}, '
|
'{"binding_dependency": {"requirement": "binding-package>=1"}, '
|
||||||
'"export_dependency": {"extra": "export-extra"}}, "public_properties": '
|
'"export_dependency": {"extra": "export-extra"}}, "public_properties": '
|
||||||
'[{"class_name": "ConditionalExport", "module": "agents.submodule", '
|
'[{"class_name": "ConditionalExport", '
|
||||||
'"names": ["status"]}, {"factory_name": "create_client", '
|
'"module": "agents.submodule", "names": ["status"]}, '
|
||||||
'"module": "agents.submodule", "names": ["calls"]}], "public_typed_dicts": '
|
'{"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", '
|
'[{"class_name": "ClientState", "module": "agents.submodule", '
|
||||||
'"names": ["status"]}]}',
|
'"names": ["status"]}]}',
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -2466,6 +2740,13 @@ def test_load_submodule_export_policy_collects_artifact_installations(tmp_path:
|
|||||||
"name": "ConditionalExport",
|
"name": "ConditionalExport",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
assert policy.public_class_contracts == (
|
||||||
|
{
|
||||||
|
"abstract": False,
|
||||||
|
"class_name": "ConditionalExport",
|
||||||
|
"module": "agents.submodule",
|
||||||
|
},
|
||||||
|
)
|
||||||
assert tuple(
|
assert tuple(
|
||||||
entry
|
entry
|
||||||
for entry in policy.public_properties
|
for entry in policy.public_properties
|
||||||
@@ -2488,6 +2769,12 @@ def test_load_submodule_export_policy_collects_artifact_installations(tmp_path:
|
|||||||
"names": ["calls"],
|
"names": ["calls"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
assert policy.public_type_aliases == (
|
||||||
|
{
|
||||||
|
"module": "agents.submodule",
|
||||||
|
"name": "PublicAlias",
|
||||||
|
},
|
||||||
|
)
|
||||||
assert policy.public_typed_dicts == (
|
assert policy.public_typed_dicts == (
|
||||||
{
|
{
|
||||||
"class_name": "ClientState",
|
"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:
|
def test_load_submodule_export_policy_collects_unsupported_platforms(tmp_path: Path) -> None:
|
||||||
policy_path = tmp_path / "policy.json"
|
policy_path = tmp_path / "policy.json"
|
||||||
policy_path.write_text(
|
policy_path.write_text(
|
||||||
@@ -2544,6 +2848,10 @@ def test_repository_release_policy_declares_v020_contract_surfaces() -> None:
|
|||||||
"agents.testing.model",
|
"agents.testing.model",
|
||||||
"agents.testing.sandbox",
|
"agents.testing.sandbox",
|
||||||
"agents.realtime.testing",
|
"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",
|
"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"))
|
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.realtime.testing",
|
||||||
"agents.testing",
|
"agents.testing",
|
||||||
"agents.testing.model",
|
"agents.testing.model",
|
||||||
"agents.testing.sandbox",
|
"agents.testing.sandbox",
|
||||||
"agents.voice.testing",
|
"agents.voice",
|
||||||
}
|
}
|
||||||
|
|
||||||
assert expected_modules <= policy.modules.keys()
|
documented_directive_modules = {
|
||||||
assert policy.modules["agents.voice.testing"] == {
|
line.removeprefix("::: ")
|
||||||
"optional_bindings": {
|
for path in (CONTRACT.parents[2] / "docs" / "ref" / "voice").rglob("*.md")
|
||||||
export: "numpy" for export in importlib.import_module("agents.voice.testing").__all__
|
for line in path.read_text(encoding="utf-8").splitlines()
|
||||||
},
|
if line.startswith("::: agents.voice")
|
||||||
"optional_exports": {},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
assert (
|
||||||
next(
|
next(
|
||||||
installation
|
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"))
|
policy = load_submodule_export_policy(CONTRACT.with_name("released_api_contract_policy.json"))
|
||||||
expected_modules = {
|
expected_modules = {
|
||||||
"agents.realtime.testing",
|
"agents.realtime.testing",
|
||||||
@@ -2659,6 +3000,133 @@ def test_repository_release_policy_declares_public_testing_state_surfaces() -> N
|
|||||||
"names": ["transcriptions"],
|
"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 == (
|
assert policy.public_typed_dicts == (
|
||||||
{
|
{
|
||||||
"class_name": "ModelStepSpec",
|
"class_name": "ModelStepSpec",
|
||||||
@@ -2692,6 +3160,11 @@ def test_repository_release_policy_declares_public_testing_state_surfaces() -> N
|
|||||||
"call_id",
|
"call_id",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"class_name": "TTSCustomVoice",
|
||||||
|
"module": "agents.voice",
|
||||||
|
"names": ["id"],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
for module_name in expected_modules:
|
for module_name in expected_modules:
|
||||||
module = importlib.import_module(module_name)
|
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)
|
canonical_module = importlib.import_module(canonical_module_name)
|
||||||
assert getattr(module, name) is getattr(canonical_module, canonical_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:
|
def test_voice_testing_start_sentinel_has_stable_contract_identity() -> None:
|
||||||
from agents.voice.testing import _START_NOT_CONFIGURED
|
from agents.voice.testing import _START_NOT_CONFIGURED
|
||||||
|
|||||||
Reference in New Issue
Block a user