diff --git a/licenses.json b/licenses.json index bc63f7e2..380712a9 100644 --- a/licenses.json +++ b/licenses.json @@ -24,6 +24,11 @@ "Name": "argcomplete", "URL": "https://github.com/kislyuk/argcomplete" }, + { + "License": "MIT", + "Name": "ast_serialize", + "URL": "https://github.com/mypyc/ast_serialize" + }, { "License": "MIT", "Name": "attrs", @@ -99,6 +104,11 @@ "Name": "langcodes", "URL": "https://github.com/georgkrause/langcodes" }, + { + "License": "MIT", + "Name": "librt", + "URL": "https://github.com/mypyc/librt" + }, { "License": "MIT License", "Name": "licensecheck", @@ -119,6 +129,16 @@ "Name": "mdurl", "URL": "https://github.com/executablebooks/mdurl" }, + { + "License": "MIT", + "Name": "mypy", + "URL": "https://www.mypy-lang.org/" + }, + { + "License": "MIT", + "Name": "mypy_extensions", + "URL": "https://github.com/python/mypy_extensions" + }, { "License": "BSD License", "Name": "nodeenv", @@ -129,6 +149,11 @@ "Name": "packaging", "URL": "https://github.com/pypa/packaging" }, + { + "License": "Mozilla Public License 2.0 (MPL 2.0)", + "Name": "pathspec", + "URL": "https://python-path-specification.readthedocs.io/en/latest/index.html" + }, { "License": "MIT", "Name": "platformdirs", @@ -239,6 +264,11 @@ "Name": "ty", "URL": "https://github.com/astral-sh/ty/" }, + { + "License": "PSF-2.0", + "Name": "typing_extensions", + "URL": "https://github.com/python/typing_extensions" + }, { "License": "MIT", "Name": "url-normalize", diff --git a/pyproject.toml b/pyproject.toml index c9a94352..bf84423d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,10 @@ exclude = [ "poly.docs" = [ "*.md", ] +"poly.types" = [ + "*.pyi", + "**/*.pyi", +] [tool.ruff] target-version = "py312" @@ -164,6 +168,7 @@ dev = [ "tomli-w>=1.0.0", "pip-licenses>=5.5.1", "licensecheck>=2024.3", + "mypy>=1.0.0", ] [project.scripts] diff --git a/scripts/sync_runtime_stubs.py b/scripts/sync_runtime_stubs.py index ff1d2f18..56bc735f 100755 --- a/scripts/sync_runtime_stubs.py +++ b/scripts/sync_runtime_stubs.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 """Sync type stubs from genai_lambda_runtime into src/poly/types/. -Extracts public API signatures (classes, methods, properties, type aliases, -exceptions) from the runtime source and writes stub-only .py files with -``...`` bodies. Internal helpers (prefixed with ``_``) and implementation -details are stripped. +Uses mypy's ``stubgen`` to generate .pyi stubs, then post-processes them: +- Renames .pyi → .py +- Rewrites ``runtime.`` / ``utils.`` imports to relative +- Adds copyright header and noqa directives +- Removes internal-only modules Usage: python scripts/sync_runtime_stubs.py [--runtime-path PATH] @@ -16,568 +17,234 @@ import argparse import ast +import json +import re +import subprocess import sys +import tempfile from pathlib import Path +STUB_DIR = Path(__file__).resolve().parent.parent / "src" / "poly" / "types" + STUB_HEADER = """\ # Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore """ -STUB_DIR = Path(__file__).resolve().parent.parent / "src" / "poly" / "types" - -STUB_FILES = [ - "attachment.py", - "conv_utils.py", - "conversation.py", - "external_events.py", - "flow.py", - "history.py", - "log_utils.py", - "memory.py", - "sms.py", - "value_extraction.py", - "value_extraction_types.py", - "webchat.py", - "agentic_dial.py", - "emails.py", - "entity_validator.py", -] - - -def _has_private_name(name: str) -> bool: - """Return True if name starts with _ (private/internal).""" - return name.startswith("_") and not (name.startswith("__") and name.endswith("__")) - - -def _format_annotation(node: ast.expr | None) -> str | None: - """Convert an AST annotation node to source text.""" - if node is None: - return None - return ast.unparse(node) - - -def _format_arg(arg: ast.arg) -> str: - """Format a function argument with optional annotation.""" - ann = _format_annotation(arg.annotation) - if ann: - return f"{arg.arg}: {ann}" - return arg.arg - - -def _build_signature(func: ast.FunctionDef | ast.AsyncFunctionDef) -> str: - """Build a stub-style function signature.""" - args = func.args - parts: list[str] = [] - - for arg in args.posonlyargs: - parts.append(_format_arg(arg)) - if args.posonlyargs: - parts.append("/") - - num_defaults = len(args.defaults) - num_args = len(args.args) - for i, arg in enumerate(args.args): - formatted = _format_arg(arg) - default_idx = i - (num_args - num_defaults) - if default_idx >= 0: - formatted += " = ..." - parts.append(formatted) - - if args.vararg: - parts.append(f"*{_format_arg(args.vararg)}") - elif args.kwonlyargs: - parts.append("*") - - kw_defaults = args.kw_defaults - for i, arg in enumerate(args.kwonlyargs): - formatted = _format_arg(arg) - if kw_defaults[i] is not None: - formatted += " = ..." - parts.append(formatted) - - if args.kwarg: - parts.append(f"**{_format_arg(args.kwarg)}") - - sig = ", ".join(parts) - ret = _format_annotation(func.returns) - if ret: - return f"({sig}) -> {ret}" - return f"({sig})" - - -def _get_docstring(node: ast.AST) -> str | None: - """Extract docstring from a node.""" - if ( - isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Module)) - and node.body - and isinstance(node.body[0], ast.Expr) - and isinstance(node.body[0].value, ast.Constant) - ): - val = node.body[0].value - if isinstance(val.value, str): - return val.value - return None - - -def _extract_class_vars(cls_node: ast.ClassDef) -> list[str]: - """Extract annotated class variables.""" - lines = [] - for stmt in cls_node.body: - if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): - name = stmt.target.id - if _has_private_name(name): - continue - ann = _format_annotation(stmt.annotation) - lines.append(f" {name}: {ann}") - return lines - - -def _extract_method_stub(func: ast.FunctionDef | ast.AsyncFunctionDef) -> str | None: - """Generate a stub line for a method.""" - name = func.name - if name.startswith("_") and not (name.startswith("__") and name.endswith("__")): - return None - - is_property = any( - (isinstance(d, ast.Name) and d.id == "property") - or (isinstance(d, ast.Attribute) and d.attr == "property") - for d in func.decorator_list - ) - is_staticmethod = any( - isinstance(d, ast.Name) and d.id == "staticmethod" for d in func.decorator_list - ) - is_classmethod = any( - isinstance(d, ast.Name) and d.id == "classmethod" for d in func.decorator_list - ) - - sig = _build_signature(func) - prefix = "async def" if isinstance(func, ast.AsyncFunctionDef) else "def" - - docstring = _get_docstring(func) - - lines = [] - if is_property: - lines.append(" @property") - elif is_staticmethod: - lines.append(" @staticmethod") - elif is_classmethod: - lines.append(" @classmethod") - - if docstring: - first_line = docstring.strip().split("\n")[0] - lines.append(f" {prefix} {name}{sig}:") - lines.append(f' """{first_line}"""') - else: - lines.append(f" {prefix} {name}{sig}: ...") - - return "\n".join(lines) - - -def _is_dataclass(cls_node: ast.ClassDef) -> bool: - """Check if a class is decorated with @dataclass.""" - for d in cls_node.decorator_list: - if isinstance(d, ast.Name) and d.id == "dataclass": - return True - if isinstance(d, ast.Call) and isinstance(d.func, ast.Name) and d.func.id == "dataclass": - return True - return False +# Regex patterns for import rewriting +_FROM_RUNTIME_RE = re.compile(r"^from runtime\.(\S+)", re.MULTILINE) +_IMPORT_RUNTIME_RE = re.compile(r"^import runtime\.(\w+)", re.MULTILINE) +# Imports that should be dropped entirely from stubs +_DROP_IMPORT_RE = re.compile( + r"^from (?:_typeshed|constants|utils\.api_connector|utils\.secret_vault) .*\n", + re.MULTILINE, +) +# _typeshed.Incomplete -> Any +_INCOMPLETE_RE = re.compile(r"\bIncomplete\b") +# Types from dropped imports that should become Any +_UNRESOLVABLE_TYPES = re.compile(r"\bHandoffMethod\b|\bApiIntegrations\b") -def _synthesize_dataclass_init(cls_node: ast.ClassDef) -> str | None: - """Build an __init__ stub from dataclass field annotations. +def _relativize_imports(source: str, rel_path: str) -> str: + """Rewrite absolute runtime/utils imports to relative ones. - Fields with a default value or default_factory get ``= ...`` in the - signature. Returns None when no annotated fields are found. + *rel_path* is the file path relative to the runtime root + (e.g. "conversation.py" or "integrations/integrations.py"). """ - params = ["self"] - for stmt in cls_node.body: - if not (isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name)): - continue - name = stmt.target.id - if _has_private_name(name): - continue - ann = _format_annotation(stmt.annotation) - if stmt.value is not None: - params.append(f"{name}: {ann} = ...") - else: - params.append(f"{name}: {ann}") - - if len(params) == 1: - return None - - sig = ", ".join(params) - return f" def __init__({sig}) -> None: ..." - - -def _extract_class_stub(cls_node: ast.ClassDef) -> str: - """Generate a full class stub.""" - bases = [ast.unparse(b) for b in cls_node.bases] - bases_str = f"({', '.join(bases)})" if bases else "" - - docstring = _get_docstring(cls_node) - doc_section = "" - if docstring: - first_line = docstring.strip().split("\n")[0] - doc_section = f' """{first_line}"""\n\n' - - class_vars = _extract_class_vars(cls_node) - cv_section = "\n".join(class_vars) + "\n" if class_vars else "" - - is_dc = _is_dataclass(cls_node) - has_explicit_init = any( - isinstance(s, ast.FunctionDef) and s.name == "__init__" for s in cls_node.body + source_pkg_parts = list(Path(rel_path).parent.parts) + depth = len(source_pkg_parts) + + def _rewrite_from(m: re.Match) -> str: + """Rewrite ``from runtime.X.Y import`` to relative form.""" + mod_tail = m.group(1) # e.g. "integrations.integration" + mod_parts = mod_tail.split(".") + + # Find shared prefix with source package + common = 0 + for a, b in zip(source_pkg_parts, mod_parts): + if a == b: + common += 1 + else: + break + + ups = len(source_pkg_parts) - common + dots = "." * (ups + 1) + remainder = ".".join(mod_parts[common:]) + rel = f"{dots}{remainder}" if remainder else dots + return f"from {rel}" + + def _rewrite_import(m: re.Match) -> str: + """Rewrite ``import runtime.X`` to ``from . import X``.""" + mod_name = m.group(1) + if depth == 0: + return f"from . import {mod_name}" + dots = "." * (depth + 1) + return f"from {dots} import {mod_name}" + + source = _FROM_RUNTIME_RE.sub(_rewrite_from, source) + source = _IMPORT_RUNTIME_RE.sub(_rewrite_import, source) + # Also handle from utils.X imports + source = re.sub( + r"^from utils\.(\S+)", + lambda m: _rewrite_from( + type(m)(m.re, f"runtime.{m.group(1)}", m.string, m.start(), m.end()) + ) + if False + else f"from {'.' * (depth + 1)}{m.group(1)}", + source, + flags=re.MULTILINE, ) + return source - methods = [] - # Synthesize __init__ for dataclasses that don't define one explicitly - if is_dc and not has_explicit_init: - init_stub = _synthesize_dataclass_init(cls_node) - if init_stub: - methods.append(init_stub) - - for stmt in cls_node.body: - if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): - stub = _extract_method_stub(stmt) - if stub: - methods.append(stub) - - methods_section = "\n".join(methods) + "\n" if methods else "" - - body = doc_section + cv_section + methods_section - if not body.strip(): - body = " ...\n" - - return f"class {cls_node.name}{bases_str}:\n{body}" - - -def _extract_top_level_assignments(tree: ast.Module) -> list[str]: - """Extract public type aliases and constants.""" - lines = [] - for stmt in tree.body: - if isinstance(stmt, ast.Assign): - for target in stmt.targets: - if isinstance(target, ast.Name) and not _has_private_name(target.id): - lines.append(ast.unparse(stmt)) - elif isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): - if not _has_private_name(stmt.target.id): - lines.append(ast.unparse(stmt)) - return lines - - -def _get_all_list(tree: ast.Module) -> list[str] | None: - """Extract __all__ if defined.""" - for stmt in tree.body: - if isinstance(stmt, ast.Assign): - for target in stmt.targets: - if isinstance(target, ast.Name) and target.id == "__all__": - if isinstance(stmt.value, ast.List): - return [ - elt.value for elt in stmt.value.elts if isinstance(elt, ast.Constant) - ] - return None - - -def _filter_import_names(names: list[ast.alias]) -> list[ast.alias]: - """Filter out private names from import lists.""" - return [alias for alias in names if not _has_private_name(alias.name)] - - -# Modules that only contain internal implementation details — skip entirely. -_INTERNAL_MODULES = frozenset( - { - "runtime.state_utils", - "runtime.llm_client", - "utils.api_connector", - "utils.secret_vault", - } -) - -# Stdlib/third-party modules whose types appear in public signatures. -_ALLOWED_STDLIB_MODULES = frozenset( - { - "abc", - "collections.abc", - "datetime", - "enum", - "re", - "typing", - "pydantic", - } -) - - -def _collect_import_stmts(stmts: list[ast.stmt]) -> list[ast.stmt]: - """Collect import statements, including those inside TYPE_CHECKING blocks.""" - result = [] - for stmt in stmts: - if isinstance(stmt, (ast.Import, ast.ImportFrom)): - result.append(stmt) - elif ( - isinstance(stmt, ast.If) - and isinstance(stmt.test, ast.Name) - and stmt.test.id == "TYPE_CHECKING" - ): - result.extend(_collect_import_stmts(stmt.body)) - return result +def _load_imports_json(python_root: Path) -> dict[str, list[str]]: + """Load imports.json and return a mapping of stub rel_path → __all__ names. -def _expand_module_import(module: str, alias: str, body_text: str) -> str | None: - """Convert ``import runtime.X as Y`` to ``from .X import A, B, C``. + *python_root* is the ``python/`` directory containing both ``runtime/`` + and ``utils/`` alongside ``assets/imports.json``. - Scans *body_text* for ``Y.name`` references and emits a relative - ``from`` import for each attribute used. Also rewrites those - ``Y.name`` references in body_text to just ``name``. - Returns (import_line, new_body_text) or None if no attributes are found. - """ - import re as _re - - pattern = _re.compile(r"(? .foo - rel_module = "." + module.split(".", 1)[1] if "." in module else module - import_line = f"from {rel_module} import {', '.join(names)}" - # Rewrite body: extraction_types.EntityConfig -> EntityConfig - new_body = pattern.sub(r"\1", body_text) - return import_line, new_body - - -def _relativize_module(module: str) -> str: - """Convert ``runtime.X`` or ``utils.X`` to ``.X`` for relative imports.""" - if module.startswith(("runtime.", "utils.")): - return "." + module.split(".", 1)[1] - return module - - -def _extract_imports(tree: ast.Module, source_path: Path) -> list[str]: - """Extract imports needed for the stub. - - Includes: - - typing imports - - runtime.* imports (relativized to .X) - - stdlib/pydantic imports used in type annotations - - imports from ``if TYPE_CHECKING:`` blocks (promoted to unconditional) - - ``import runtime.X as Y`` forms are tagged for deferred expansion - (needs body_text to resolve which attributes are used). + The keys in imports.json use ``runtime/X.py`` or ``utils/X.py`` form; + we strip the top-level package prefix to get the stub-relative path. """ - imports = [] - for stmt in _collect_import_stmts(tree.body): - if isinstance(stmt, ast.Import): - for alias in stmt.names: - if alias.name.startswith("runtime"): - imports.append( - ("module_alias", alias.name, alias.asname or alias.name.rsplit(".", 1)[-1]) - ) - elif alias.name in _ALLOWED_STDLIB_MODULES: - imports.append(ast.unparse(stmt)) - elif isinstance(stmt, ast.ImportFrom): - if not stmt.module: - continue - if stmt.module in _INTERNAL_MODULES: - continue - - is_runtime = stmt.module.startswith("runtime.") or stmt.module == "runtime" - is_typing = stmt.module.startswith("typing") - is_stdlib = any( - stmt.module == m or stmt.module.startswith(m + ".") for m in _ALLOWED_STDLIB_MODULES - ) - - if is_runtime or is_typing or is_stdlib: - public_names = _filter_import_names(stmt.names) - if not public_names: - continue - rel_module = _relativize_module(stmt.module) - filtered = ast.ImportFrom( - module=rel_module, - names=public_names, - level=0 if rel_module.startswith(".") else stmt.level, - ) - imports.append(ast.unparse(filtered)) - return imports - - -def _name_used_in(name: str, body_text: str) -> bool: - """Check if an imported name is actually referenced in the body. - - Uses word-boundary matching to avoid false positives like - ``import re`` matching ``"score"``. - """ - import re as _re - - return bool( - _re.search(r"(? "conversation.py" + # "utils/secret_vault.py" -> "secret_vault.py" + rel = key.split("/", 1)[1] if "/" in key else key + result.setdefault(rel, []).extend(names) + return result -def _prune_imports(import_lines: list[str], body_text: str) -> list[str]: - """Keep only imports whose names actually appear in the stub body.""" - pruned = [] - for line in import_lines: - # `import foo as bar` or `import foo` — keep if the bound name appears - if line.startswith("import "): - # e.g. "import runtime.external_events as external_events" - parts = line.split() - bound_name = parts[-1] # alias or last dotted segment - if _name_used_in(bound_name, body_text): - pruned.append(line) - continue - - # `from X import a, b, c` — keep only names that appear in body - try: - node = ast.parse(line).body[0] - except SyntaxError: - pruned.append(line) - continue - - if not isinstance(node, ast.ImportFrom): - pruned.append(line) - continue - - used = [ - alias for alias in node.names if _name_used_in(alias.asname or alias.name, body_text) - ] - if not used: - continue - filtered = ast.ImportFrom(module=node.module, names=used, level=node.level) - pruned.append(ast.unparse(filtered)) - - return pruned - - -def generate_stub(source_path: Path) -> str: - """Generate a stub file from a runtime source file.""" - source = source_path.read_text() - tree = ast.parse(source) - - candidate_imports = _extract_imports(tree, source_path) - all_list = _get_all_list(tree) - - classes = [] - for stmt in tree.body: - if isinstance(stmt, ast.ClassDef) and not _has_private_name(stmt.name): - classes.append(_extract_class_stub(stmt)) - - # Collect assignments, separating those that reference class names - # (type aliases like VoiceType = ...) to place them after class definitions. - class_names = {stmt.name for stmt in tree.body if isinstance(stmt, ast.ClassDef)} - assignments_before = [] - assignments_after = [] - for stmt in tree.body: - line = None - if isinstance(stmt, ast.Assign): - for target in stmt.targets: - if isinstance(target, ast.Name) and not _has_private_name(target.id): - line = ast.unparse(stmt) - elif isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name): - if not _has_private_name(stmt.target.id): - line = ast.unparse(stmt) - - if line is None: - continue - - unparsed_value = ast.unparse(stmt.value) if hasattr(stmt, "value") and stmt.value else "" - if any(cn in unparsed_value for cn in class_names): - assignments_after.append(line) - else: - assignments_before.append(line) - - # Derive __all__ from public classes and type aliases if source doesn't define one. - if all_list is None: - public_class_names = [ - stmt.name - for stmt in tree.body - if isinstance(stmt, ast.ClassDef) and not _has_private_name(stmt.name) - ] - public_assignment_names = [] - for stmt in tree.body: - if isinstance(stmt, ast.Assign): - for target in stmt.targets: - if isinstance(target, ast.Name) and not _has_private_name(target.id): - public_assignment_names.append(target.id) - all_list = public_class_names + public_assignment_names - - # Build body text (everything except imports) to prune unused imports. - body_parts = [] - if assignments_before: - body_parts.append("\n".join(assignments_before)) - if classes: - body_parts.append("\n\n".join(classes)) - if assignments_after: - body_parts.append("\n".join(assignments_after)) - body_text = "\n".join(body_parts) - - # Resolve deferred module-alias imports and rewrite body references. - resolved_imports = [] - for item in candidate_imports: - if isinstance(item, tuple) and item[0] == "module_alias": - _, module, alias = item - import_line, body_text = _expand_module_import(module, alias, body_text) - if import_line: - resolved_imports.append(import_line) - else: - resolved_imports.append(item) - - imports = _prune_imports(resolved_imports, body_text) - - # Rebuild body_parts from (possibly rewritten) body_text - # Split back into sections for assembly - all_body_lines = body_text.split("\n") if body_text.strip() else [] - - # Assemble final stub - parts = [STUB_HEADER] - - if imports: - parts.append("\n".join(imports)) - - if all_list: - items = ", ".join(f'"{name}"' for name in all_list) - parts.append(f"\n__all__ = [{items}]") - - if all_body_lines: - parts.append("\n".join(all_body_lines)) - - return "\n\n".join(parts) + "\n" +def _ensure_any_imported(source: str) -> str: + """Add ``Any`` to the typing import if not already present.""" + if "from typing import" in source: + return re.sub( + r"from typing import (.+)", + lambda m: f"from typing import {m.group(1)}" + if "Any" in m.group(1) + else f"from typing import Any, {m.group(1)}", + source, + count=1, + ) + return "from typing import Any\n" + source + + +def _postprocess(source: str, rel_path: str, all_names: list[str] | None = None) -> str: + """Apply all post-processing to a stubgen output file.""" + # Drop imports from modules we don't ship + source = _DROP_IMPORT_RE.sub("", source) + # Replace unresolvable types and Incomplete with Any + needs_any = False + for pattern in (_INCOMPLETE_RE, _UNRESOLVABLE_TYPES): + if pattern.search(source): + source = pattern.sub("Any", source) + needs_any = True + if needs_any: + source = _ensure_any_imported(source) + # Relativize runtime/utils imports + source = _relativize_imports(source, rel_path) + # Inject __all__ from imports.json, filtered to names available in the stub + if all_names: + tree = ast.parse(source) + available: set[str] = set() + for node in ast.iter_child_nodes(tree): + if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): + available.add(node.name) + elif isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name): + available.add(target.id) + elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): + available.add(node.target.id) + elif isinstance(node, (ast.Import, ast.ImportFrom)): + for alias in node.names: + available.add(alias.asname or alias.name) + filtered = [n for n in all_names if n in available] + if filtered: + all_line = "__all__ = " + repr(filtered) + "\n\n" + source = all_line + source + # Add header + source = STUB_HEADER + source + return source def main() -> None: - parser = argparse.ArgumentParser(description="Sync runtime type stubs") + parser = argparse.ArgumentParser(description="Sync runtime type stubs using stubgen") parser.add_argument( - "--runtime-path", + "--python-root", type=Path, - default=Path(__file__).resolve().parent.parent.parent - / "genai_lambda_runtime" - / "python" - / "runtime", - help="Path to the genai_lambda_runtime/python/runtime directory", + default=Path(__file__).resolve().parent.parent.parent / "genai_lambda_runtime" / "python", + help="Path to the genai_lambda_runtime/python directory", ) args = parser.parse_args() - runtime_path: Path = args.runtime_path - if not runtime_path.is_dir(): - print(f"Error: runtime path not found: {runtime_path}", file=sys.stderr) + python_root: Path = args.python_root + if not python_root.is_dir(): + print(f"Error: python root not found: {python_root}", file=sys.stderr) sys.exit(1) - STUB_DIR.mkdir(parents=True, exist_ok=True) + # imports.json drives __all__ generation (not which files to stub) + imports_map = _load_imports_json(python_root) + + # Stub all of runtime/ plus individual utils files from imports.json + runtime_dir = python_root / "runtime" + if not runtime_dir.is_dir(): + print(f"Error: runtime directory not found: {runtime_dir}", file=sys.stderr) + sys.exit(1) + + sources: list[str] = [str(runtime_dir)] + # Add individual utils/ files referenced in imports.json + imports_file = python_root / "assets" / "imports.json" + if imports_file.exists(): + with open(imports_file, encoding="utf-8") as f: + for key in json.load(f): + if key.startswith("utils/"): + source_file = python_root / key + if source_file.exists(): + sources.append(str(source_file)) + + # Run stubgen + with tempfile.TemporaryDirectory() as tmpdir: + cmd = ["uv", "run", "stubgen", "-o", tmpdir] + sources + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"stubgen failed:\n{result.stderr}", file=sys.stderr) + sys.exit(1) + print(result.stdout.strip()) + + # stubgen nests output under the source tree structure; + # find the common root (python/) and process each package separately + tmpdir_path = Path(tmpdir) + updated = 0 + + # Process each top-level package (runtime/, utils/) that has stubs + for pkg in ("runtime", "utils"): + stub_pkg = tmpdir_path / "python" / pkg + if not stub_pkg.is_dir(): + stub_pkg = tmpdir_path / pkg + if not stub_pkg.is_dir(): + continue + + for pyi_file in sorted(stub_pkg.rglob("*.pyi")): + rel = pyi_file.relative_to(stub_pkg) + # imports_map keys use .py paths + rel_py = str(rel.with_suffix(".py")) - updated = 0 - for filename in STUB_FILES: - source = runtime_path / filename - if not source.exists(): - print(f" SKIP {filename} (not found in runtime)") - continue + source = pyi_file.read_text(encoding="utf-8") + all_names = imports_map.get(rel_py) + processed = _postprocess(source, rel_py, all_names) - stub_content = generate_stub(source) - dest = STUB_DIR / filename - dest.write_text(stub_content) - print(f" OK {filename}") - updated += 1 + dest = STUB_DIR / rel + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(processed, encoding="utf-8") + print(f" OK {rel}") + updated += 1 - print(f"\nSynced {updated}/{len(STUB_FILES)} stub files to {STUB_DIR}") + print(f"\nSynced {updated} stub files to {STUB_DIR}") if __name__ == "__main__": diff --git a/src/poly/resources/safety_filters.py b/src/poly/resources/safety_filters.py index b8b5ee51..c46b3f3f 100644 --- a/src/poly/resources/safety_filters.py +++ b/src/poly/resources/safety_filters.py @@ -9,13 +9,13 @@ from google.protobuf.message import Message +import poly.resources.resource_utils as utils from poly.handlers.protobuf.channels_pb2 import Channel_UpdateSafetyFilters, ChannelType from poly.handlers.protobuf.content_filter_settings_pb2 import ( AzureContentFilter, AzureContentFilterCategory, ContentFilterSettings_UpdateContentFilterSettings, ) -import poly.resources.resource_utils as utils from poly.resources.resource import ResourceMapping, YamlResource PRECISION_MAPPING = {"LOOSE": "lenient", "MEDIUM": "medium", "STRICT": "strict"} diff --git a/src/poly/tests/project_test.py b/src/poly/tests/project_test.py index 89929e37..e8b83633 100644 --- a/src/poly/tests/project_test.py +++ b/src/poly/tests/project_test.py @@ -32,10 +32,10 @@ SettingsRole, SettingsRules, SMSTemplate, - Topic, TestCase, TestCaseAssertion, TestCaseTags, + Topic, TranscriptCorrection, Translation, Variable, diff --git a/src/poly/tests/resources_test.py b/src/poly/tests/resources_test.py index 9e95970d..2cb92a6f 100644 --- a/src/poly/tests/resources_test.py +++ b/src/poly/tests/resources_test.py @@ -7,10 +7,9 @@ import unittest import yaml - -import poly.resources.resource_utils as resource_utils from jsonschema import ValidationError +import poly.resources.resource_utils as resource_utils from poly.handlers.sync_client import SyncClientHandler from poly.resources.agent_settings import ( SettingsPersonality, @@ -53,7 +52,6 @@ FunctionParameters, FunctionType, ) - from poly.resources.handoff import Handoff from poly.resources.keyphrase_boosting import KeyphraseBoosting from poly.resources.languages import ( @@ -74,11 +72,6 @@ VoiceSafetyFilters, ) from poly.resources.sms import EnvPhoneNumbers, SMSTemplate -from poly.resources.topic import ( - FUNCTION_REGEX, - Topic, -) -from poly.resources.transcript_correction import RegularExpressionRule, TranscriptCorrection from poly.resources.test_suite import ( FunctionCallArgumentAssertion, FunctionCallAssertion, @@ -86,6 +79,11 @@ TestCaseAssertion, TestCaseTags, ) +from poly.resources.topic import ( + FUNCTION_REGEX, + Topic, +) +from poly.resources.transcript_correction import RegularExpressionRule, TranscriptCorrection from poly.resources.translations import Translation from poly.resources.variable import Variable from poly.resources.variant_attributes import Variant, VariantAttribute diff --git a/src/poly/types/__init__.py b/src/poly/types/__init__.py index 742efb25..57ff0d5c 100644 --- a/src/poly/types/__init__.py +++ b/src/poly/types/__init__.py @@ -1 +1,4 @@ # Copyright PolyAI Limited +# flake8: noqa +# ruff: noqa +# type: ignore diff --git a/src/poly/types/__init__.pyi b/src/poly/types/__init__.pyi new file mode 100644 index 00000000..742efb25 --- /dev/null +++ b/src/poly/types/__init__.pyi @@ -0,0 +1 @@ +# Copyright PolyAI Limited diff --git a/src/poly/types/agentic_dial.py b/src/poly/types/agentic_dial.py deleted file mode 100644 index 9d137abb..00000000 --- a/src/poly/types/agentic_dial.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -from collections.abc import Iterator - - -__all__ = [ - "Destination", - "AgenticDialConfig", - "AgenticDialData", - "MessageToParent", - "MessageToChild", - "Destinations", - "AgenticDial", -] - - -class Destination: - """Agentic dial destination configuration""" - - name: str - phone_number: str - sip_headers: dict[str, str] - - def __init__(self, name: str, phone_number: str, sip_headers: dict[str, str] = ...) -> None: ... - @classmethod - def from_dict(cls, data: dict) -> Destination: - """Create a Destination from a dictionary.""" - - def to_dict(self) -> dict: - """Convert to a dictionary.""" - - -class AgenticDialConfig: - """Agentic dial configuration""" - - destinations: list[Destination] - - def __init__(self, destinations: list[Destination] = ...) -> None: ... - @classmethod - def from_dict(cls, data: dict) -> AgenticDialConfig: - """Create an AgenticDialConfig from a dictionary.""" - - -class AgenticDialData: - """Agentic dial runtime data.""" - - config: AgenticDialConfig - active_dial_destinations: list[str] - dial_id: str | None - - def __init__( - self, - config: AgenticDialConfig = ..., - active_dial_destinations: list[str] = ..., - dial_id: str | None = ..., - ) -> None: ... - @classmethod - def from_dict(cls, data: dict) -> AgenticDialData: - """Create an AgenticDialData from a dictionary.""" - - -class MessageToParent: - """A message to send to the parent agent.""" - - content: str - - def __init__(self, content: str) -> None: ... - - -class MessageToChild: - """A message to send to a child agent.""" - - destination: str - content: str - - def __init__(self, destination: str, content: str) -> None: ... - - -class Destinations: - """Manages a collection of agentic dial destinations.""" - - def __init__(self, destinations: list[Destination]): ... - def __iter__(self) -> Iterator[Destination]: - """Iterate over all destinations.""" - - def __getitem__(self, name: str) -> Destination: - """Get a destination by name.""" - - def add( - self, *, name: str, phone_number: str, sip_headers: dict[str, str] | None = ... - ) -> None: - """Add a single destination.""" - - def clear(self) -> None: - """Clear all destinations.""" - - -class AgenticDial: - """Manages agentic dial functionality.""" - - def __init__(self, data: AgenticDialData | None): ... - @property - def active_destinations(self) -> list[str]: - """List of active destination names, i.e. those that have been dialed.""" - - def send_to_parent(self, content: str) -> None: - """Send a message to the parent agent.""" - - def send_to_child(self, destination: str, content: str) -> None: - """Send a message to a child agent.""" diff --git a/src/poly/types/agentic_dial.pyi b/src/poly/types/agentic_dial.pyi new file mode 100644 index 00000000..cfb90e79 --- /dev/null +++ b/src/poly/types/agentic_dial.pyi @@ -0,0 +1,56 @@ +# Copyright PolyAI Limited +__all__ = ["AgenticDial", "Destination", "Destinations"] + +from typing import Any +from collections.abc import Iterator +from dataclasses import dataclass, field + +@dataclass +class Destination: + name: str + phone_number: str + sip_headers: dict[str, str] = field(default_factory=dict) + @classmethod + def from_dict(cls, data: dict) -> Destination: ... + def to_dict(self) -> dict: ... + +@dataclass +class AgenticDialConfig: + destinations: list[Destination] = field(default_factory=list) + @classmethod + def from_dict(cls, data: dict) -> AgenticDialConfig: ... + +@dataclass +class AgenticDialData: + config: AgenticDialConfig = field(default_factory=AgenticDialConfig) + active_dial_destinations: list[str] = field(default_factory=list) + dial_id: str | None = ... + @classmethod + def from_dict(cls, data: dict) -> AgenticDialData: ... + +@dataclass +class MessageToParent: + content: str + +@dataclass +class MessageToChild: + destination: str + content: str + +class Destinations: + def __init__(self, destinations: list[Destination]) -> None: ... + def __iter__(self) -> Iterator[Destination]: ... + def __getitem__(self, name: str) -> Destination: ... + def add( + self, *, name: str, phone_number: str, sip_headers: dict[str, str] | None = None + ) -> None: ... + def clear(self) -> None: ... + +class AgenticDial: + destinations: Any + def __init__(self, data: AgenticDialData | None) -> None: ... + @property + def active_destinations(self) -> list[str]: ... + def send_to_parent(self, content: str) -> None: ... + def send_to_child(self, destination: str, content: str) -> None: ... + def unsubscribe_from_destination(self, destination: str) -> None: ... diff --git a/src/poly/types/analytics.pyi b/src/poly/types/analytics.pyi new file mode 100644 index 00000000..5ce8b14c --- /dev/null +++ b/src/poly/types/analytics.pyi @@ -0,0 +1,22 @@ +# Copyright PolyAI Limited +from dataclasses import dataclass + +@dataclass +class APIRequestMetadata: + url: str + method: str + response_time: float + status_code: int + error: dict = ... + def to_json_str(self) -> str: ... + +@dataclass +class AnalyticsEvent: + name: str + value: str + timestamp_str: str = ... + def __post_init__(self) -> None: ... + @classmethod + def from_dict(cls, data: dict) -> AnalyticsEvent: ... + +def response_to_analytics_events(response: list[dict]) -> list[AnalyticsEvent]: ... diff --git a/src/poly/types/attachment.py b/src/poly/types/attachment.py deleted file mode 100644 index 34ead0e1..00000000 --- a/src/poly/types/attachment.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -import typing - - -__all__ = ["Attachment"] - - -class Attachment: - """An attachment to an Agent Response.""" - - def __init__( - self, - content_url: str, - content_type: typing.Literal["image", "weblink", "unspecified"], - title: str | None = ..., - preview_image_url: str | None = ..., - call_to_action: str | None = ..., - ): ... - def to_dict(self): - """Convert the Attachment to a dictionary.""" diff --git a/src/poly/types/attachment.pyi b/src/poly/types/attachment.pyi new file mode 100644 index 00000000..bf92d67f --- /dev/null +++ b/src/poly/types/attachment.pyi @@ -0,0 +1,21 @@ +# Copyright PolyAI Limited +__all__ = ["Attachment"] + +from typing import Any +import typing + +class Attachment: + content_url: Any + content_type: Any + title: Any + preview_image_url: Any + call_to_action: Any + def __init__( + self, + content_url: str, + content_type: typing.Literal["image", "weblink", "unspecified"], + title: str | None = None, + preview_image_url: str | None = None, + call_to_action: str | None = None, + ) -> None: ... + def to_dict(self): ... diff --git a/src/poly/types/conv_utils.py b/src/poly/types/conv_utils.py deleted file mode 100644 index fe76ff04..00000000 --- a/src/poly/types/conv_utils.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -from typing import Literal -from .value_extraction_types import EntityConfig -from .history import AgentResponse, UserInput -from .value_extraction import Address - - -__all__ = ["Utils"] - - -class Utils: - """Utility class for the conv object.""" - - def __init__( - self, - account_id: str, - project_id: str, - client_env: str, - conversation_id: str, - turn_index: int, - language: str, - history: list[UserInput | AgentResponse], - transcript_alternatives: list[str], - vpc_enabled: bool = ..., - correlation_id: str | None = ..., - ): ... - def extract_address(self, addresses: list[Address] | None = ..., country: str = ...) -> Address: - """[Opt-in Feature] 🚧""" - - def extract_city( - self, - city_spellings: list[str] | None = ..., - states: list[str] | None = ..., - country: str = ..., - ) -> Address: - """[Opt-in Feature] 🚧""" - - def prompt_llm( - self, - prompt: str, - *, - show_history: bool = ..., - return_json: bool = ..., - model: Literal[ - "gpt-4o", - "gpt-4o-mini", - "gpt-4.1", - "gpt-4.1-mini", - "gpt-4.1-nano", - "gpt-5", - "gpt-5-mini", - "gpt-5-nano", - "gpt-5-chat", - "claude-sonnet-4", - "claude-3.5-haiku", - ] = ..., - ) -> str | dict: - """[Opt-in Feature] 🚧""" - - def validate_entity(self, value: str, entity_config: EntityConfig) -> _EntityValidationResponse: - """Validate an entity value against its configuration.""" - - def get_secret(self, secret_name: str) -> str | dict: - """Get secret value""" diff --git a/src/poly/types/conv_utils.pyi b/src/poly/types/conv_utils.pyi new file mode 100644 index 00000000..0901e6c9 --- /dev/null +++ b/src/poly/types/conv_utils.pyi @@ -0,0 +1,71 @@ +# Copyright PolyAI Limited +__all__ = ["Utils"] + +from . import value_extraction_types as extraction_types +from .history import AgentResponse as AgentResponse, UserInput as UserInput +from .value_extraction import Address as Address, _EntityValidationResponse +from typing import Any, Literal + +class PromptLLMCallLimitError(Exception): ... + +class Utils: + def __init__( + self, + account_id: str, + project_id: str, + client_env: str, + conversation_id: str, + turn_index: int, + language: str, + history: list[UserInput | AgentResponse], + transcript_alternatives: list[str], + vpc_enabled: bool = False, + correlation_id: str | None = None, + ) -> None: ... + EntityType: Any + NumericType: Any + NumericConfig: Any + QuantityConfig: Any + CurrencyConfig: Any + NameConfig: Any + FreeTextConfig: Any + AlphanumericConfig: Any + DateConfig: Any + EmailConfig: Any + TimeConfig: Any + PhoneNumberConfig: Any + EnumConfig: Any + EntityConfig: Any + def extract_address( + self, addresses: list[Address] | None = None, country: str = "US" + ) -> Address: ... + def extract_city( + self, + city_spellings: list[str] | None = None, + states: list[str] | None = None, + country: str = "US", + ) -> Address: ... + def prompt_llm( + self, + prompt: str, + *, + show_history: bool = False, + return_json: bool = False, + model: Literal[ + "gpt-4o", + "gpt-4o-mini", + "gpt-4.1", + "gpt-4.1-mini", + "gpt-4.1-nano", + "gpt-5", + "gpt-5-mini", + "gpt-5-nano", + "gpt-5-chat", + "claude-sonnet-4", + "claude-3.5-haiku", + ] = "gpt-4o", + ) -> str | dict: ... + def validate_entity( + self, value: str, entity_config: extraction_types.EntityConfig + ) -> _EntityValidationResponse: ... + def get_secret(self, secret_name: str) -> str | dict: ... diff --git a/src/poly/types/conversation.py b/src/poly/types/conversation.py deleted file mode 100644 index 51b9c9c4..00000000 --- a/src/poly/types/conversation.py +++ /dev/null @@ -1,831 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -from typing import Any, Literal, NewType -from .external_events import GenericExternalEvent, SMSReceived -from .agentic_dial import AgenticDialData -from .entity_validator import EntityValidationResult -from .history import AgentResponse, UserInput -from .integrations.integrations import Integrations -from .memory import Memory -from .sms import OutgoingSMS, OutgoingSMSTemplate, SMSTemplate -from .webchat import WebchatInterface -from .attachment import Attachment - - -__all__ = [ - "SMSIntegrationNotFound", - "SMSMissingAssistantAccess", - "MissingTemplate", - "MissingHandoff", - "TTSVoice", - "CustomVoice", - "ElevenLabsVoice", - "RimeVoice", - "EmotionKind", - "EmotionIntensity", - "Emotion", - "CartesiaVoice", - "PlayHTVoice", - "MinimaxVoice", - "HumeVoice", - "GoogleVoice", - "VoiceWeighting", - "FlowTransition", - "Variant", - "Entities", - "HandoffConfig", - "Handoff", - "ApiIntegrationData", - "ASRBiasing", - "State", - "ReadOnlyDict", - "TranslationReplacementProxy", - "RealtimeConfig", - "MetricEvent", - "FunctionExecutor", - "ApiExecutor", - "Conversation", - "EmotionKindValue", - "EmotionIntensityValue", - "VoiceType", - "SupportedLanguageCodes", -] - -SupportedLanguageCodes = Literal[ - "en-US", - "en-GB", - "es-US", - "es-ES", - "ar-SA", - "zh-CN", - "zh-TW", - "ja-JP", - "ko-KR", - "cs-CZ", - "nl-NL", - "en-AU", - "en-CA", - "en-IE", - "en-NZ", - "en-SG", - "fr-CA", - "fr-FR", - "fr-BE", - "de-DE", - "it-IT", - "pl-PL", - "pt-BR", - "pt-PT", - "sr-RS", - "es-ES", - "sv-SE", - "tr-TR", - "nl-BE", - "hr-HR", - "yue-HK", - "el-GR", - "hi-IN", - "bg-BG", - "bs-BA", - "sk-SK", -] - - -class SMSIntegrationNotFound(Exception): - """No integration with given provider fo[und in secret""" - - def __init__(self, secret_name: str, integration: str): ... - - -class SMSMissingAssistantAccess(Exception): - """No access for assistant on SMS secret""" - - def __init__(self, secret_name: str, assistant_id: str, integration: str): ... - - -class MissingTemplate(Exception): - """Template reference doesn't exist""" - - -class MissingHandoff(Exception): - """Handoff does not exist""" - - def __init__(self, handoff_destination: str): ... - - -class TTSVoice: - """Base class for TTS voice configurations""" - - def __init__(self, provider: str, provider_voice_id: str, config: dict = ...): ... - @property - def provider(self) -> str: - """The provider name.""" - - @property - def provider_voice_id(self) -> str: - """The unique identifier for the voice.""" - - def to_dict(self): - """Convert the TTSVoice to a dictionary.""" - - -class CustomVoice(TTSVoice): - """Voice configuration for a custom TTS provider.""" - - def __init__(self, provider: str, provider_voice_id: str, **kwargs): - """Initialize a voice from the given TTS provider.""" - - -class ElevenLabsVoice(TTSVoice): - """Voice configuration for ElevenLabs.""" - - def __init__( - self, - provider_voice_id: str, - similarity_boost: float | None = ..., - stability: float | None = ..., - model_id: Literal[ - "eleven_monolingual_v1", - "eleven_multilingual_v1", - "eleven_turbo_v2", - "eleven_turbo_v2_5", - "eleven_flash_v2_5", - ] - | None = ..., - speed: float | None = ..., - ): - """Initialize ElevenLabs voice.""" - - @property - def similarity_boost(self) -> float | None: - """The similarity boost factor.""" - - @property - def stability(self) -> float | None: - """The stability factor.""" - - @property - def speed(self) -> float | None: - """The speed factor.""" - - -class RimeVoice(TTSVoice): - """Rime voice config""" - - def __init__( - self, - provider_voice_id: str, - speech_alpha: float | None = ..., - model_id: Literal["mist", "mistv2"] | None = ..., - ): - """Initialize Rime voice.""" - - @property - def speech_alpha(self) -> float | None: - """speech pace""" - - -class EmotionKind: - """Enum for emotion kind""" - - -class EmotionIntensity: - """Enum for emotion intensity""" - - -class Emotion: - """Emotion for Cartesia voice""" - - def __init__( - self, kind: EmotionKindValue | None = ..., intensity: EmotionIntensityValue | None = ... - ): - """Initialize an Emotion instance.""" - - def to_dict(self) -> dict: - """Convert the emotion to a dictionary.""" - - -class CartesiaVoice(TTSVoice): - """Carteisa voice config""" - - def __init__( - self, - provider_voice_id: str, - speed: float | None = ..., - emotions: list[Emotion] | None = ..., - model_id: str | None = ..., - volume: float | None = ..., - emotion: str | None = ..., - language: str | None = ..., - ): - """Initialize Cartesia voice.""" - - @property - def emotions(self) -> list[Emotion] | None: - """speech emotion list""" - - @property - def speed(self) -> float | None: - """speech speed""" - - @property - def volume(self) -> float | None: - """speech volume (Sonic 3)""" - - @property - def emotion(self) -> str | None: - """emotion string (Sonic 3)""" - - @property - def language(self) -> str | None: - """language code (Sonic 3)""" - - -class PlayHTVoice(TTSVoice): - """Voice config for PlayHT""" - - def __init__( - self, - provider_voice_id: str, - speed: float | None = ..., - temperature: float | None = ..., - emotion: Literal[ - "female_happy", - "female_sad", - "female_angry", - "female_fearful", - "female_disgust", - "female_surprised", - "male_happy", - "male_sad", - "male_angry", - "male_fearful", - "male_disgust", - "male_surprised", - ] - | None = ..., - voice_guidance: int | None = ..., - style_guidance: int | None = ..., - voice_engine: Literal[ - "Play3.0-mini", "PlayDialog", "PlayHT2.0-turbo", "PlayHT2.0", "PlayHT1.0" - ] - | None = ..., - ): - """Initialize the PlayHT voice.""" - - @property - def temperature(self) -> float | None: - """The temperature.""" - - -class MinimaxVoice(TTSVoice): - """Voice config for Minimax""" - - def __init__( - self, - model_id: Literal["speech-02-hd", "speech-02-turbo", "speech-01-hd", "speech-01-turbo"], - voice_id: str, - speed: float | None = ..., - vol: float | None = ..., - pitch: float | None = ..., - emotion: Literal["happy", "sad", "angry", "fearful", "disgusted", "surprised", "neutral"] - | None = ..., - ): - """Initialise the Minimax TTS voice""" - - @property - def model_id(self) -> str: - """The model ID.""" - - @property - def speed(self) -> float | None: - """The speed of the generated speech.""" - - @property - def vol(self) -> float | None: - """The volume of the generated speech.""" - - @property - def pitch(self) -> float | None: - """The pitch of the generated speech.""" - - @property - def emotion(self) -> str | None: - """The emotion of the generated speech.""" - - -class HumeVoice(TTSVoice): - """Voice config for Hume""" - - def __init__( - self, - provider_voice_id: str, - voice_description: str | None = ..., - version: str | None = ..., - instant_mode: bool | None = ..., - provider: Literal["CUSTOM_VOICE", "HUME_AI"] | None = ..., - ): - """Initialize Hume voice.""" - - -class GoogleVoice(TTSVoice): - """Voice configuration for Google TTS.""" - - def __init__( - self, provider_voice_id: str, gender: Literal["male", "female", "neutral"] | None = ... - ): - """Initialize Google TTS voice.""" - - @property - def gender(self) -> str | None: - """The gender of the voice.""" - - -class VoiceWeighting: - """Weighting for a voice""" - - def __init__(self, voice: VoiceType, weight: float | None = ...): - """Create a VoiceWeighting for voice randomization.""" - - @property - def voice(self) -> VoiceType: - """The TTSVoice to use.""" - - @property - def weight(self) -> float | None: - """The weight for the voice.""" - - -class FlowTransition: - """Mutable object to trigger flow transitions""" - - goto_flow: str | None - exit_flow: bool - - def __init__(self, goto_flow: str | None = ..., exit_flow: bool = ...) -> None: ... - - -class Variant(dict): - """Variant object exposing variant attributes""" - - -class Entities(dict): - """Entities object exposing entities attributes""" - - -class HandoffConfig: - """Handoff configuration""" - - sip_type: HandoffMethod - sip_config: dict - sip_headers: dict - - def __init__( - self, sip_type: HandoffMethod, sip_config: dict = ..., sip_headers: dict = ... - ) -> None: ... - @classmethod - def from_dict(cls, d: dict): - """from_dict""" - - -class Handoff: - """Handoff response""" - - handoff: HandoffConfig - reason: str | None - destination: str | None - - def __init__( - self, handoff: HandoffConfig, reason: str | None, destination: str | None = ... - ) -> None: ... - @classmethod - def from_dict(cls, d: dict): - """from_dict""" - - def to_response(self) -> dict: - """Convert dataclass object into response format""" - - -class ApiIntegrationData: - """API integration data for runtime""" - - id: str - name: str - environments: dict[str, dict[str, str]] - operations: list[dict[str, str]] - - def __init__( - self, - id: str, - name: str, - environments: dict[str, dict[str, str]], - operations: list[dict[str, str]], - ) -> None: ... - def to_dict(self) -> dict[str, Any]: - """Convert to dictionary for JSON serialization""" - - @classmethod - def from_dict(cls, data: dict[str, Any]) -> ApiIntegrationData: - """Create from dictionary with defensive access""" - - -class ASRBiasing: - """ASR biasing configuration set by a function""" - - keywords: list[str] | None - custom_biases: dict[str, float] | None - - def __init__( - self, keywords: list[str] | None = ..., custom_biases: dict[str, float] | None = ... - ) -> None: ... - - -class State(dict): - """`dict` subclass with ergonomic attribute-style access""" - - def __getattr__(self, key: str) -> Any | None: - """Attribute access of values using keys""" - - def __setattr__(self, key: str, value: Any): - """Attribute style update of values using keys""" - - def __deepcopy__(self, memo): - """deepcopy""" - - def __reduce__(self): - """reduce""" - - -class ReadOnlyDict(dict): - """Read-only dictionary""" - - def __readonly__(self, *args, **kwargs): - """raise TypeError when trying to modify the dictionary""" - - def __init__(self, *args, **kwargs): ... - - -class TranslationReplacementProxy: - """Custom dictionary to support language translations replacements""" - - def __init__( - self, translations_config: dict[str, dict[str, str]] | None, language_code: str | None - ): ... - def __getattr__(self, name): - """__getattr__""" - - -class RealtimeConfig(ReadOnlyDict): - """Realtime config""" - - def __init__(self, **kwargs): - """init""" - - -class MetricEvent: - """Representation of a metric that has already been written to history.""" - - name: str - value: float | str | int | None - - def __init__(self, name: str, value: float | str | int | None) -> None: ... - - -class FunctionExecutor(dict): - """Function executor""" - - def __init__(self, conv: Conversation): ... - def __getattr__(self, name: str) -> Any: - """Dynamically import and return a function when accessed via dot notation.""" - - -class ApiExecutor: - """API executor""" - - def __init__(self, conv: Conversation, api_integrations: ApiIntegrations | None = ...): ... - def __getattr__(self, name: str) -> Any: - """Dynamically return API integration by name when accessed via dot notation.""" - - -class Conversation: - """Object exposing useful information from the conversation runtime""" - - def __init__( - self, - call_sid: str, - account_id: str, - project_id: str, - env: str, - sip_headers: dict[str, str], - state: State, - current_flow: str | None, - current_step: str | None, - flow_transition: FlowTransition, - caller_number: str | None, - callee_number: str | None, - variant: str | None = ..., - variants: dict[str, Variant] | None = ..., - sms_templates: dict[str, SMSTemplate] | None = ..., - language: str | None = ..., - turn_number: int | None = ..., - history: list[UserInput | AgentResponse] | None = ..., - transcript_alternatives: list[str] | None = ..., - handoffs: dict[str, HandoffConfig] | None = ..., - integration_attributes: dict[str, Any] | None = ..., - memory: Memory | None = ..., - metric_events: list[MetricEvent] | None = ..., - sms_received: list[SMSReceived] | None = ..., - realtime_config: dict[str, Any] | None = ..., - functions: dict[str, Any] | None = ..., - vpc_enabled: bool | None = ..., - generic_external_events: list[GenericExternalEvent] | None = ..., - channel_type: Literal["sms", "VOICE", "sip.polyai"] | None = ..., - entities: dict[str, EntityValidationResult] | None = ..., - apis: list[ApiIntegrationData] | None = ..., - variables: dict[str, str] | None = ..., - translations: dict[str, dict] | None = ..., - agentic_dial: AgenticDialData | None = ..., - provider_voice_id: str | None = ..., - integrations_config: dict[str, Any] | None = ..., - ): - """init""" - - @classmethod - def from_runtime_data( - cls, - runtime_data: dict[str, Any], - call_sid: str, - account_id: str, - project_id: str, - env: str, - flow_transition: FlowTransition, - vpc_enabled: bool = ..., - ) -> Conversation: - """Build Conversation object from runtime_data JSON dict""" - - @property - def id(self) -> str: - """The ID of the conversation""" - - @property - def account_id(self) -> str: - """The account ID""" - - @property - def project_id(self) -> str: - """The project ID""" - - @property - def env(self) -> str: - """The client environment this is executing in""" - - @property - def sip_headers(self) -> dict[str, str]: - """Dict mapping header names to values""" - - @property - def integration_attributes(self) -> dict[str, Any] | None: - """Attributes provided by an external integration.""" - - @property - def caller_number(self) -> str | None: - """The caller's phone number""" - - @property - def callee_number(self) -> str | None: - """The callee's phone number""" - - @property - def state(self) -> State: - """Dictionary of saved variables that persist through the conversation""" - - @property - def entities(self) -> Entities: - """The entities collected from the conversation""" - - @property - def current_flow(self) -> str | None: - """Name of the flow we are currently in""" - - @property - def current_step(self) -> str | None: - """Name of the step we are currently in""" - - @property - def sms_queue(self) -> list[OutgoingSMS | OutgoingSMSTemplate]: - """Queue of SMS messages to send""" - - @property - def metrics_queue(self) -> list[dict]: - """Queue of metrics to write""" - - @property - def variant_name(self) -> str | None: - """The name of the variant of the conversation""" - - @property - def variants(self) -> dict[str, Variant]: - """The variants of the conversation with their attributes""" - - @property - def variant(self) -> Variant | None: - """The variant of the conversation""" - - @property - def sms_templates(self) -> dict[str, SMSTemplate]: - """The SMS templates available to the conversation""" - - @property - def voice_change(self) -> TTSVoice | None: - """The request to change voice for the conversation""" - - @property - def language(self) -> str | None: - """The ISO 639 language code of the language set for the conversation""" - - @property - def history(self) -> list[UserInput | AgentResponse]: - """The history of the conversation so far""" - - @property - def handoffs(self) -> dict[str, HandoffConfig]: - """The handoffs available to the conversation""" - - @property - def transcript_alternatives(self) -> list[str]: - """List of transcription alternatives for the last user input,""" - - @property - def real_time_config(self) -> dict[str, Any | None]: - """The real time config for the conversation""" - - @property - def functions(self) -> FunctionExecutor: - """The functions available to the conversation""" - - @property - def api(self) -> ApiExecutor: - """Access to API integrations via conv.api.{api_name}.{operation}()""" - - @property - def generic_external_events(self) -> list[dict]: - """The generic external events available to the conversation initiated by the""" - - @property - def channel_type(self) -> str: - """The channel of the conversation e.g. 'sms', 'sip.polyai'""" - - @property - def attachments(self) -> list[Attachment]: - """List of attachments to be included with the next agent message.""" - - @property - def response_suggestions(self) -> list[str]: - """List of response suggestions (text strings) for the next agent message.""" - - @property - def webchat(self) -> WebchatInterface: - """Webchat-specific interface.""" - - @property - def translations(self) -> TranslationReplacementProxy: - """Return the localized translation of the accessed field name.""" - - @property - def provider_voice_id(self) -> str: - """The TTS provider voice ID set for the conversation""" - - @property - def integrations(self) -> Integrations: - """Access to external integrations via conv.integrations.{integration_name}()""" - - def send_email(self, to: str, body: str, subject: str = ...) -> None: - """Send an email""" - - def set_voice(self, voice: VoiceType): - """Change the voice for the current conversation moving forward.""" - - def set_language(self, language: SupportedLanguageCodes): - """Change the language for the current conversation moving forward.""" - - def set_asr_biasing( - self, keywords: list[str] | None = ..., custom_biases: dict[str, float] | None = ... - ) -> None: - """Set ASR biasing for the conversation.""" - - def clear_asr_biasing(self) -> None: - """Clear ASR biasing for future turns.""" - - def say(self, utterance: str): - """Set the next utterance to be said following the execution of the function""" - - def randomize_voice(self, voice_weights: list[VoiceWeighting]): - """Randomly selects a voice from a weighted list of voices.""" - - def goto_flow(self, flow_name: str): - """Trigger a transition to a flow""" - - def exit_flow(self): - """Trigger exiting the current flow""" - - def goto_csat_flow(self): - """Trigger a transition to the CSAT survey flow for voice.""" - - def set_variant(self, variant: str): - """Set the variant of the conversation""" - - def log_api_response(self, response: requests.models.Response, override_url: str = ...): - """Log api response for analytics""" - - def send_sms( - self, to_number: str, from_number: str, content: str, retry_count: int | None = ... - ) -> dict | None: - """Sends an SMS""" - - def send_whatsapp( - self, - to_number: str, - from_number: str, - content_id: str, - content: str | None = ..., - retry_count: int | None = ..., - ) -> dict | None: - """Sends a WhatsApp message""" - - def send_content_template( - self, - messaging_service_id: str, - to_number: str, - content_id: str, - content: str | None = ..., - whatsapp: bool | None = ..., - content_variables: dict | None = ..., - retry_count: int | None = ..., - ) -> dict | None: - """Sends a WhatsApp message""" - - def send_sms_template( - self, to_number: str, template: str, retry_count: int | None = ... - ) -> dict | None: - """Sends an SMS template""" - - def set_csat_eligibility(self, eligible: bool, reason: str | None = ...): - """Set whether this conversation is eligible for CSAT surveys.""" - - def set_csat_phone_number(self, phone_number: str): - """Set the phone number to use for CSAT SMS surveys.""" - - def set_csat_score(self, score: int): - """Set the CSAT score for this conversation.""" - - def set_csat_survey_entered(self): - """Mark that the CSAT survey was entered for this conversation.""" - - def add_attachments(self, attachments: list[Attachment]) -> None: - """Adds the given attachments to the agent message.""" - - def set_response_suggestions(self, suggestions: list[str]) -> None: - """Sets response suggestions for the agent message.""" - - def generate_external_event(self, *, send_to_llm: bool = ...) -> str: - """Generate an external event ID which can be sent to some external provider.""" - - @property - def metric_events(self) -> list[MetricEvent]: - """List of metric events already written in the conversation.""" - - def write_metric( - self, name: str, value: float | int | str | None = ..., *, write_once: bool = ... - ): - """Write a custom metric for call analytics.""" - - def call_handoff( - self, - destination: str, - reason: str = ..., - utterance: str = ..., - sip_headers: dict[str, str] | None = ..., - route: str | None = ..., - ): - """Trigger a transfer of the conversation to a live agent""" - - def discard_recording(self): - """Stop any recordings of the current conversation and prevent them from being""" - - -EmotionKindValue = NewType("EmotionKindValue", int) -EmotionIntensityValue = NewType("EmotionIntensityValue", int) -VoiceType = ( - CustomVoice - | ElevenLabsVoice - | PlayHTVoice - | CartesiaVoice - | RimeVoice - | MinimaxVoice - | HumeVoice - | GoogleVoice -) diff --git a/src/poly/types/conversation.pyi b/src/poly/types/conversation.pyi new file mode 100644 index 00000000..cd03906f --- /dev/null +++ b/src/poly/types/conversation.pyi @@ -0,0 +1,517 @@ +# Copyright PolyAI Limited +__all__ = [ + "SMSIntegrationNotFound", + "SMSMissingAssistantAccess", + "MissingTemplate", + "TTSVoice", + "CustomVoice", + "ElevenLabsVoice", + "RimeVoice", + "GoogleVoice", + "EmotionKindValue", + "EmotionIntensityValue", + "Emotion", + "EmotionIntensity", + "EmotionKind", + "CartesiaVoice", + "PlayHTVoice", + "MinimaxVoice", + "HumeVoice", + "VoiceType", + "VoiceWeighting", + "Variant", + "State", + "Conversation", + "MetricEvent", + "FunctionExecutor", + "ApiExecutor", + "Integrations", +] + +import requests +from . import external_events as external_events +from dataclasses import dataclass, field +from .agentic_dial import AgenticDialData +from .attachment import Attachment as Attachment +from .entity_validator import EntityValidationResult +from .history import AgentResponse, UserInput +from .integrations.integrations import Integrations +from .knowledge_base import KnowledgeBase +from .memory import Memory +from .sms import ( + OutgoingSMS, + OutgoingSMSTemplate as OutgoingSMSTemplate, + SMSCredentials, + SMSTemplate, +) +from .webchat import WebchatInterface +from typing import Any, Literal + +def best_effort_substitute(prompt: str, variables: dict) -> str: ... + +class SMSIntegrationNotFound(Exception): + def __init__(self, secret_name: str, integration: str) -> None: ... + +class SMSMissingAssistantAccess(Exception): + def __init__(self, secret_name: str, assistant_id: str, integration: str) -> None: ... + +class MissingTemplate(Exception): ... + +class MissingHandoff(Exception): + def __init__(self, handoff_destination: str) -> None: ... + +class TTSVoice: + def __init__(self, provider: str, provider_voice_id: str, config: dict = {}) -> None: ... + @property + def provider(self) -> str: ... + @property + def provider_voice_id(self) -> str: ... + def to_dict(self): ... + +class CustomVoice(TTSVoice): + def __init__(self, provider: str, provider_voice_id: str, **kwargs) -> None: ... + +class ElevenLabsVoice(TTSVoice): + def __init__( + self, + provider_voice_id: str, + similarity_boost: float | None = None, + stability: float | None = None, + model_id: Literal[ + "eleven_monolingual_v1", + "eleven_multilingual_v1", + "eleven_turbo_v2", + "eleven_turbo_v2_5", + "eleven_flash_v2_5", + ] + | None = "eleven_turbo_v2_5", + speed: float | None = None, + ) -> None: ... + @property + def similarity_boost(self) -> float | None: ... + @property + def stability(self) -> float | None: ... + @property + def speed(self) -> float | None: ... + +class RimeVoice(TTSVoice): + def __init__( + self, + provider_voice_id: str, + speech_alpha: float | None = 1.0, + model_id: Literal["mist", "mistv2"] | None = "mistv2", + ) -> None: ... + @property + def speech_alpha(self) -> float | None: ... + +EmotionKindValue: Any +EmotionIntensityValue: Any + +class EmotionKind: + ANGER: Any + POSITIVITY: Any + SURPRISE: Any + +class EmotionIntensity: + LOWEST: Any + LOW: Any + HIGH: Any + HIGHEST: Any + +class Emotion: + kind: Any + intensity: Any + def __init__( + self, kind: EmotionKindValue | None = None, intensity: EmotionIntensityValue | None = None + ) -> None: ... + def to_dict(self) -> dict: ... + +class CartesiaVoice(TTSVoice): + def __init__( + self, + provider_voice_id: str, + speed: float | None = 0, + emotions: list[Emotion] | None = None, + model_id: str | None = "sonic", + volume: float | None = None, + emotion: str | None = None, + language: str | None = None, + ) -> None: ... + @property + def emotions(self) -> list[Emotion] | None: ... + @property + def speed(self) -> float | None: ... + @property + def volume(self) -> float | None: ... + @property + def emotion(self) -> str | None: ... + @property + def language(self) -> str | None: ... + +class PlayHTVoice(TTSVoice): + def __init__( + self, + provider_voice_id: str, + speed: float | None = None, + temperature: float | None = None, + emotion: Literal[ + "female_happy", + "female_sad", + "female_angry", + "female_fearful", + "female_disgust", + "female_surprised", + "male_happy", + "male_sad", + "male_angry", + "male_fearful", + "male_disgust", + "male_surprised", + ] + | None = None, + voice_guidance: int | None = None, + style_guidance: int | None = None, + voice_engine: Literal[ + "Play3.0-mini", "PlayDialog", "PlayHT2.0-turbo", "PlayHT2.0", "PlayHT1.0" + ] + | None = None, + ) -> None: ... + @property + def temperature(self) -> float | None: ... + +class MinimaxVoice(TTSVoice): + def __init__( + self, + model_id: Literal["speech-02-hd", "speech-02-turbo", "speech-01-hd", "speech-01-turbo"], + voice_id: str, + speed: float | None = None, + vol: float | None = None, + pitch: float | None = None, + emotion: Literal["happy", "sad", "angry", "fearful", "disgusted", "surprised", "neutral"] + | None = None, + ) -> None: ... + @property + def model_id(self) -> str: ... + @property + def speed(self) -> float | None: ... + @property + def vol(self) -> float | None: ... + @property + def pitch(self) -> float | None: ... + @property + def emotion(self) -> str | None: ... + +class HumeVoice(TTSVoice): + def __init__( + self, + provider_voice_id: str, + voice_description: str | None = None, + version: str | None = "2", + instant_mode: bool | None = False, + provider: Literal["CUSTOM_VOICE", "HUME_AI"] | None = "CUSTOM_VOICE", + ) -> None: ... + +class GoogleVoice(TTSVoice): + def __init__( + self, provider_voice_id: str, gender: Literal["male", "female", "neutral"] | None = None + ) -> None: ... + @property + def gender(self) -> str | None: ... + +VoiceType = ( + CustomVoice + | ElevenLabsVoice + | PlayHTVoice + | CartesiaVoice + | RimeVoice + | MinimaxVoice + | HumeVoice + | GoogleVoice +) +SupportedLanguageCodes: Any + +class VoiceWeighting: + def __init__(self, voice: VoiceType, weight: float | None = None) -> None: ... + @property + def voice(self) -> VoiceType: ... + @property + def weight(self) -> float | None: ... + +@dataclass +class BackgroundTrack: + name: str + loudness: float = ... + +@dataclass +class FlowTransition: + goto_flow: str | None = ... + exit_flow: bool = ... + +class Variant(dict): + __getattr__: Any + +class Entities(dict): + __getattr__: Any + +@dataclass +class HandoffConfig: + sip_type: Any + sip_config: dict = field(default_factory=dict) + sip_headers: dict = field(default_factory=dict) + @classmethod + def from_dict(cls, d: dict): ... + +@dataclass +class Handoff: + handoff: HandoffConfig + reason: str | None + destination: str | None = ... + @classmethod + def from_dict(cls, d: dict): ... + def to_response(self) -> dict: ... + +@dataclass +class ApiIntegrationData: + id: str + name: str + environments: dict[str, dict[str, str]] + operations: list[dict[str, str]] + def to_dict(self) -> dict[str, Any]: ... + @classmethod + def from_dict(cls, data: dict[str, Any]) -> ApiIntegrationData: ... + +@dataclass +class ASRBiasing: + keywords: list[str] | None = ... + custom_biases: dict[str, float] | None = ... + +class State(dict): + def __getattr__(self, key: str) -> Any | None: ... + def __setattr__(self, key: str, value: Any): ... + def __deepcopy__(self, memo): ... + def __reduce__(self): ... + +class ReadOnlyDict(dict): + def __readonly__(self, *args, **kwargs) -> None: ... + __setitem__ = __readonly__ + __delitem__ = __readonly__ + clear = __readonly__ + pop = __readonly__ + popitem = __readonly__ + setdefault = __readonly__ + update = __readonly__ + def __init__(self, *args, **kwargs) -> None: ... + +class TranslationReplacementProxy: + def __init__( + self, translations_config: dict[str, dict[str, str]] | None, language_code: str | None + ) -> None: ... + def __getattr__(self, name): ... + +class RealtimeConfig(ReadOnlyDict): + def __init__(self, **kwargs) -> None: ... + +@dataclass +class MetricEvent: + name: str + value: float | str | int | None + +class FunctionExecutor(dict): + conv: Any + def __init__(self, conv: Conversation) -> None: ... + def __getattr__(self, name: str) -> Any: ... + +class ApiExecutor: + conv: Any + api_integrations: Any + def __init__(self, conv: Conversation, api_integrations: Any | None = None) -> None: ... + def __getattr__(self, name: str) -> Any: ... + +class Conversation: + utils: Any + memory: Any + log: Any + agentic_dial: Any + def __init__( + self, + call_sid: str, + account_id: str, + project_id: str, + env: str, + sip_headers: dict[str, str], + state: State, + current_flow: str | None, + current_step: str | None, + flow_transition: FlowTransition, + caller_number: str | None, + callee_number: str | None, + variant: str | None = None, + variants: dict[str, Variant] | None = None, + sms_templates: dict[str, SMSTemplate] | None = None, + language: str | None = None, + turn_number: int | None = None, + history: list[UserInput | AgentResponse] | None = None, + transcript_alternatives: list[str] | None = None, + handoffs: dict[str, HandoffConfig] | None = None, + integration_attributes: dict[str, Any] | None = None, + memory: Memory | None = None, + metric_events: list[MetricEvent] | None = None, + sms_received: list[external_events.SMSReceived] | None = None, + realtime_config: dict[str, Any] | None = None, + functions: dict[str, Any] | None = None, + vpc_enabled: bool | None = False, + generic_external_events: list[external_events.GenericExternalEvent] | None = None, + channel_type: Literal["sms", "VOICE", "sip.polyai"] | None = None, + entities: dict[str, EntityValidationResult] | None = None, + apis: list[ApiIntegrationData] | None = None, + variables: dict[str, str] | None = None, + translations: dict[str, dict] | None = None, + agentic_dial: AgenticDialData | None = None, + provider_voice_id: str | None = None, + integrations_config: dict[str, Any] | None = None, + ) -> None: ... + @classmethod + def from_runtime_data( + cls, + runtime_data: dict[str, Any], + call_sid: str, + account_id: str, + project_id: str, + env: str, + flow_transition: FlowTransition, + vpc_enabled: bool = False, + ) -> Conversation: ... + @property + def id(self) -> str: ... + @property + def account_id(self) -> str: ... + @property + def project_id(self) -> str: ... + @property + def env(self) -> str: ... + @property + def sip_headers(self) -> dict[str, str]: ... + @property + def integration_attributes(self) -> dict[str, Any] | None: ... + @property + def caller_number(self) -> str | None: ... + @property + def callee_number(self) -> str | None: ... + @property + def state(self) -> State: ... + @property + def entities(self) -> Entities: ... + @property + def current_flow(self) -> str | None: ... + @property + def current_step(self) -> str | None: ... + @property + def sms_queue(self) -> list[OutgoingSMS | OutgoingSMSTemplate]: ... + @property + def metrics_queue(self) -> list[dict]: ... + @property + def variant_name(self) -> str | None: ... + @property + def variants(self) -> dict[str, Variant]: ... + @property + def variant(self) -> Variant | None: ... + @property + def sms_templates(self) -> dict[str, SMSTemplate]: ... + @property + def voice_change(self) -> TTSVoice | None: ... + @property + def language(self) -> str | None: ... + @property + def history(self) -> list[UserInput | AgentResponse]: ... + @property + def handoffs(self) -> dict[str, HandoffConfig]: ... + @property + def transcript_alternatives(self) -> list[str]: ... + @property + def real_time_config(self) -> dict[str, Any | None]: ... + @property + def functions(self) -> FunctionExecutor: ... + @property + def api(self) -> ApiExecutor: ... + @property + def generic_external_events(self) -> list[dict]: ... + @property + def channel_type(self) -> str: ... + @property + def attachments(self) -> list[Attachment]: ... + @property + def response_suggestions(self) -> list[str]: ... + @property + def webchat(self) -> WebchatInterface: ... + @property + def translations(self) -> TranslationReplacementProxy: ... + @property + def provider_voice_id(self) -> str: ... + @property + def integrations(self) -> Integrations: ... + def send_email(self, to: str, body: str, subject: str = "") -> None: ... + def set_voice(self, voice: VoiceType): ... + def set_language(self, language: SupportedLanguageCodes): ... + def set_asr_biasing( + self, keywords: list[str] | None = None, custom_biases: dict[str, float] | None = None + ) -> None: ... + def clear_asr_biasing(self) -> None: ... + @property + def knowledge_base(self) -> KnowledgeBase: ... + def say(self, utterance: str): ... + def randomize_voice(self, voice_weights: list[VoiceWeighting]): ... + def goto_flow(self, flow_name: str): ... + def exit_flow(self) -> None: ... + def goto_csat_flow(self) -> None: ... + def set_variant(self, variant: str): ... + def log_api_response(self, response: requests.models.Response, override_url: str = None): ... + def send_sms( + self, to_number: str, from_number: str, content: str, retry_count: int | None = None + ) -> dict | None: ... + def send_whatsapp( + self, + to_number: str, + from_number: str, + content_id: str, + content: str | None = "", + retry_count: int | None = None, + ) -> dict | None: ... + def send_content_template( + self, + messaging_service_id: str, + to_number: str, + content_id: str, + content: str | None = "", + whatsapp: bool | None = False, + content_variables: dict | None = None, + retry_count: int | None = None, + ) -> dict | None: ... + def send_sms_template( + self, to_number: str, template: str, retry_count: int | None = None + ) -> dict | None: ... + def set_csat_eligibility(self, eligible: bool, reason: str | None = None): ... + def set_csat_phone_number(self, phone_number: str): ... + def set_csat_score(self, score: int): ... + def set_csat_survey_entered(self) -> None: ... + def set_background_track(self, name: str, loudness: float = -40): ... + def add_attachments(self, attachments: list[Attachment]) -> None: ... + def set_response_suggestions(self, suggestions: list[str]) -> None: ... + def generate_external_event(self, *, send_to_llm: bool = False) -> str: ... + @property + def metric_events(self) -> list[MetricEvent]: ... + def write_metric( + self, name: str, value: float | int | str | None = None, *, write_once: bool = False + ): ... + def call_handoff( + self, + destination: str, + reason: str = None, + utterance: str = None, + sip_headers: dict[str, str] | None = None, + route: str | None = None, + ): ... + def discard_recording(self) -> None: ... + +def retrieve_sms_credentials( + secret_name: str, secret_dict: dict[str, Any], project_id: str, integration: str +) -> SMSCredentials: ... diff --git a/src/poly/types/emails.py b/src/poly/types/emails.py deleted file mode 100644 index fdef2fdb..00000000 --- a/src/poly/types/emails.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -__all__ = ["OutgoingEmail"] - - -class OutgoingEmail: - """Sent email information""" - - to: str - body: str - subject: str - - def __init__(self, to: str, body: str, subject: str) -> None: ... - def asdict(self) -> dict: - """Returns the OutgoingEmail as a dictionary""" diff --git a/src/poly/types/emails.pyi b/src/poly/types/emails.pyi new file mode 100644 index 00000000..d6a13401 --- /dev/null +++ b/src/poly/types/emails.pyi @@ -0,0 +1,9 @@ +# Copyright PolyAI Limited +from dataclasses import dataclass + +@dataclass +class OutgoingEmail: + to: str + body: str + subject: str + def asdict(self) -> dict: ... diff --git a/src/poly/types/entity_validator.py b/src/poly/types/entity_validator.py deleted file mode 100644 index 4d33e388..00000000 --- a/src/poly/types/entity_validator.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -from .value_extraction_types import EntityType - - -__all__ = ["EntityValidationResult"] - - -class EntityValidationResult: - """Entity validation result""" - - id: str - name: str - valid: bool - value: str - type: EntityType - error: str | None - - def __init__( - self, id: str, name: str, valid: bool, value: str, type: EntityType, error: str | None = ... - ) -> None: ... - def to_dict(self) -> dict: - """Convert to dict""" - - @classmethod - def from_dict(cls, d: dict) -> EntityValidationResult: - """Convert from dict""" diff --git a/src/poly/types/entity_validator.pyi b/src/poly/types/entity_validator.pyi new file mode 100644 index 00000000..7dd8bc16 --- /dev/null +++ b/src/poly/types/entity_validator.pyi @@ -0,0 +1,15 @@ +# Copyright PolyAI Limited +from dataclasses import dataclass +from .value_extraction_types import EntityType + +@dataclass +class EntityValidationResult: + id: str + name: str + valid: bool + value: str + type: EntityType + error: str | None = ... + def to_dict(self) -> dict: ... + @classmethod + def from_dict(cls, d: dict) -> EntityValidationResult: ... diff --git a/src/poly/types/external_events.py b/src/poly/types/external_events.py deleted file mode 100644 index ade9f0e6..00000000 --- a/src/poly/types/external_events.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -import abc - - -__all__ = ["Event", "GenericExternalEvent", "SMSReceived", "ExternalEvents"] - - -class Event(abc.ABC): - """Base class for all event types.""" - - -class GenericExternalEvent(Event): - """A generic external event represented as a dictionary.""" - - ext_event_id: str - send_to_llm: bool - created_at: str | None - data: str | None - content_type: str | None - - def __init__( - self, - ext_event_id: str, - send_to_llm: bool, - created_at: str | None = ..., - data: str | None = ..., - content_type: str | None = ..., - ) -> None: ... - @classmethod - def from_dict(cls, d: dict): - """from_dict""" - - -class SMSReceived(Event): - """An SMS message received.""" - - from_number: str - to_number: str - text: str - - def __init__(self, from_number: str, to_number: str, text: str) -> None: ... - - -class ExternalEvents: - """Listen for events that are external to the agent, for example webhooks or""" - - def __init__(self, sms_received: list[SMSReceived]) -> None: ... - def listen_for_sms_next_turn(self, timeout: float = ...) -> None: - """listen for SMS in the next turn.""" - - def get_sms_received_history(self) -> list[SMSReceived]: - """Get the history of received SMS messages during the conversation.""" diff --git a/src/poly/types/external_events.pyi b/src/poly/types/external_events.pyi new file mode 100644 index 00000000..cc6b30ae --- /dev/null +++ b/src/poly/types/external_events.pyi @@ -0,0 +1,28 @@ +# Copyright PolyAI Limited +__all__ = ["ExternalEvents", "SMSReceived"] + +import abc +from dataclasses import dataclass + +class Event(abc.ABC): ... + +@dataclass +class GenericExternalEvent(Event): + ext_event_id: str + send_to_llm: bool + created_at: str | None = ... + data: str | None = ... + content_type: str | None = ... + @classmethod + def from_dict(cls, d: dict): ... + +@dataclass +class SMSReceived(Event): + from_number: str + to_number: str + text: str + +class ExternalEvents: + def __init__(self, sms_received: list[SMSReceived]) -> None: ... + def listen_for_sms_next_turn(self, timeout: float = 20) -> None: ... + def get_sms_received_history(self) -> list[SMSReceived]: ... diff --git a/src/poly/types/flow.py b/src/poly/types/flow.py deleted file mode 100644 index b7925cf0..00000000 --- a/src/poly/types/flow.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -from typing import Any -from .conversation import Conversation - - -__all__ = ["Transition", "StepTransition", "FlowFunctionExecutor", "Flow"] - - -class Transition: - """A flow transition triggered by a function""" - - exit_flow: bool - goto_flow: str | None - goto_step: str | None - - def __init__( - self, exit_flow: bool = ..., goto_flow: str | None = ..., goto_step: str | None = ... - ) -> None: ... - @classmethod - def from_dict(cls, d: dict) -> Transition: - """Construct from dict""" - - def is_noop(self) -> bool: - """Check if this transition does nothing""" - - -class StepTransition: - """Mutable object to trigger step transitions""" - - goto_step: str | None - - def __init__(self, goto_step: str | None = ...) -> None: ... - - -class FlowFunctionExecutor(dict): - """Flow function executor""" - - def __init__(self, conv: Conversation, flow: Flow): ... - def __getattr__(self, name: str) -> Any: - """Dynamically import and return a function when accessed via dot notation.""" - - -class Flow: - """Object for working within flows""" - - def __init__( - self, - current_step: str, - step_transition: StepTransition, - conv: Conversation, - function_dir: str, - ): - """init""" - - @property - def current_step(self) -> str: - """The name of the step we're currently in""" - - @property - def functions(self) -> FlowFunctionExecutor: - """The functions available to the flow""" - - def goto_step(self, step_name: str, label: str | None = ...): - """Trigger a transition to a different step""" diff --git a/src/poly/types/flow.pyi b/src/poly/types/flow.pyi new file mode 100644 index 00000000..964b5565 --- /dev/null +++ b/src/poly/types/flow.pyi @@ -0,0 +1,40 @@ +# Copyright PolyAI Limited +__all__ = ["Flow", "FlowFunctionExecutor"] + +from dataclasses import dataclass +from .conversation import Conversation as Conversation +from typing import Any + +@dataclass +class Transition: + exit_flow: bool = ... + goto_flow: str | None = ... + goto_step: str | None = ... + @classmethod + def from_dict(cls, d: dict) -> Transition: ... + def is_noop(self) -> bool: ... + +@dataclass +class StepTransition: + goto_step: str | None = ... + +class FlowFunctionExecutor(dict): + conv: Any + flow: Any + def __init__(self, conv: Conversation, flow: Flow) -> None: ... + def __getattr__(self, name: str) -> Any: ... + +class Flow: + function_dir: Any + def __init__( + self, + current_step: str, + step_transition: StepTransition, + conv: Conversation, + function_dir: str, + ) -> None: ... + @property + def current_step(self) -> str: ... + @property + def functions(self) -> FlowFunctionExecutor: ... + def goto_step(self, step_name: str, label: str | None = None): ... diff --git a/src/poly/types/history.py b/src/poly/types/history.py deleted file mode 100644 index 6bef8b2a..00000000 --- a/src/poly/types/history.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -__all__ = ["UserInput", "AgentResponse"] - - -class UserInput: - """Object representing a user turn""" - - def __init__(self, text: str): - """Initialize a UserInput event.""" - - @property - def text(self) -> str: - """The user's input text.""" - - @property - def role(self) -> str: - """The role of the event.""" - - def __repr__(self) -> str: - """User-friendly representation of the object""" - - def __eq__(self, other): - """Check equality based on text attribute.""" - - def to_dict(self) -> dict[str, str]: - """Convert the event to dictionary format""" - - def to_string(self) -> str: - """Convert the object into a user-friendly string""" - - -class AgentResponse: - """Object representing an agent turn""" - - def __init__(self, text: str): - """Initialize an AgentResponse event.""" - - @property - def text(self) -> str: - """The agent's response text.""" - - @property - def role(self) -> str: - """The role of the event.""" - - def __repr__(self) -> str: - """User-friendly representation of the object""" - - def __eq__(self, other): - """Check equality based on text attribute.""" - - def to_dict(self) -> dict[str, str]: - """Convert the event to dictionary format""" - - def to_string(self) -> str: - """User-friendly format for display""" diff --git a/src/poly/types/history.pyi b/src/poly/types/history.pyi new file mode 100644 index 00000000..147c51ed --- /dev/null +++ b/src/poly/types/history.pyi @@ -0,0 +1,22 @@ +# Copyright PolyAI Limited +__all__ = ["AgentResponse", "UserInput"] + +class UserInput: + def __init__(self, text: str) -> None: ... + @property + def text(self) -> str: ... + @property + def role(self) -> str: ... + def __eq__(self, other): ... + def to_dict(self) -> dict[str, str]: ... + def to_string(self) -> str: ... + +class AgentResponse: + def __init__(self, text: str) -> None: ... + @property + def text(self) -> str: ... + @property + def role(self) -> str: ... + def __eq__(self, other): ... + def to_dict(self) -> dict[str, str]: ... + def to_string(self) -> str: ... diff --git a/src/poly/types/integrations/__init__.py b/src/poly/types/integrations/__init__.py index 742efb25..c8ebe8d9 100644 --- a/src/poly/types/integrations/__init__.py +++ b/src/poly/types/integrations/__init__.py @@ -1 +1,7 @@ # Copyright PolyAI Limited +# flake8: noqa +# ruff: noqa +# type: ignore +from .integration import Integration as Integration, _registry as registry + +__all__ = ["Integration", "registry"] diff --git a/src/poly/types/integrations/__init__.pyi b/src/poly/types/integrations/__init__.pyi new file mode 100644 index 00000000..f49700cb --- /dev/null +++ b/src/poly/types/integrations/__init__.pyi @@ -0,0 +1,4 @@ +# Copyright PolyAI Limited +from .integration import Integration as Integration, _registry as registry + +__all__ = ["Integration", "registry"] diff --git a/src/poly/types/integrations/available_integrations/__init__.py b/src/poly/types/integrations/available_integrations/__init__.py index 742efb25..57ff0d5c 100644 --- a/src/poly/types/integrations/available_integrations/__init__.py +++ b/src/poly/types/integrations/available_integrations/__init__.py @@ -1 +1,4 @@ # Copyright PolyAI Limited +# flake8: noqa +# ruff: noqa +# type: ignore diff --git a/src/poly/types/integrations/available_integrations/__init__.pyi b/src/poly/types/integrations/available_integrations/__init__.pyi new file mode 100644 index 00000000..742efb25 --- /dev/null +++ b/src/poly/types/integrations/available_integrations/__init__.pyi @@ -0,0 +1 @@ +# Copyright PolyAI Limited diff --git a/src/poly/types/integrations/available_integrations/opentable.py b/src/poly/types/integrations/available_integrations/opentable.py deleted file mode 100644 index 9b6ebe80..00000000 --- a/src/poly/types/integrations/available_integrations/opentable.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore -from typing import Any - -from ..integration import Integration - -__all__ = ["OpenTable"] - - -class OpenTable(Integration): - """OpenTable integration class for proxying requests to the OpenTable API""" - - integration_id: str - integration_name: str - - def proxy_request( - self, - endpoint: str, - http_method: str, - base_url: str | None = ..., - headers: dict[str, str] | None = ..., - params: dict[str, str] | None = ..., - body: dict[str, Any] | None = ..., - timeout: int = ..., - ) -> Any: - """Proxy a request to the OpenTable API using the integration's authentication.""" diff --git a/src/poly/types/integrations/available_integrations/opentable.pyi b/src/poly/types/integrations/available_integrations/opentable.pyi new file mode 100644 index 00000000..4f7d34cd --- /dev/null +++ b/src/poly/types/integrations/available_integrations/opentable.pyi @@ -0,0 +1,81 @@ +# Copyright PolyAI Limited +__all__ = ["OpenTable"] + +import requests +from ..integration import Integration + +BASE_OPENTABLE_API_URL: str +V1_BASE_OPENTABLE_API_URL_SUFFIX: str +V2_BASE_OPENTABLE_API_URL_SUFFIX: str +OPENTABLE_AUTH_URL: str +OPENTABLE_SECRET_NAME: str + +class OpenTable(Integration): + integration_id: str + integration_name: str + def proxy_request( + self, + endpoint: str, + http_method: str, + base_url: str | None = None, + headers: dict[str, str] | None = None, + params: dict[str, str] | None = None, + body: dict[str, any] | None = None, + timeout: int = ..., + ) -> requests.Response: ... + def check_availability( + self, + restaurant_id: str, + party_size: int, + start_date_time: str, + forward_minutes: int | None = None, + backward_minutes: int | None = None, + include_experiences: bool = False, + ) -> requests.Response: ... + def lock_slot( + self, + restaurant_id: int, + party_size: int, + date_time: str, + table_type: str = "default", + dining_area_id: int | None = None, + experience_id: int | None = None, + ) -> requests.Response: ... + def make_reservation( + self, + restaurant_id: str, + reservation_token: str, + first_name: str, + last_name: str, + phone_number: str, + phone_country_code: int = 1, + special_request: str | None = None, + sms_notifications_opt_in: bool = True, + table_type: str = "default", + dining_area_id: int | None = None, + payments: dict | None = None, + ) -> requests.Response: ... + def lookup_bookings( + self, restaurant_id: str, phone_number: str, phone_country_code: int = 1 + ) -> requests.Response: ... + def update_reservation( + self, + restaurant_id: str, + reservation_id: str, + party_size: int | None = None, + date_time: str | None = None, + special_request: str | None = None, + ) -> requests.Response: ... + def cancel_booking(self, restaurant_id: str, reservation_id: str) -> requests.Response: ... + def get_experiences(self, restaurant_id: str) -> requests.Response: ... + def check_availability_v2( + self, + restaurant_id: str, + party_size: int, + start_date_time: str, + forward_minutes: int | None = None, + backward_minutes: int | None = None, + require_attributes: str | None = None, + include_credit_card_results: bool = False, + include_experiences: bool = False, + ) -> requests.Response: ... diff --git a/src/poly/types/integrations/available_integrations/tripleseat.py b/src/poly/types/integrations/available_integrations/tripleseat.py deleted file mode 100644 index d07cfba4..00000000 --- a/src/poly/types/integrations/available_integrations/tripleseat.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore -from typing import Any - -from ..integration import Integration - -__all__ = ["Tripleseat"] - - -class Tripleseat(Integration): - """Tripleseat integration class for proxying requests to the Tripleseat API""" - - integration_id: str - integration_name: str - - def get_bookings(self) -> Any: - """Get bookings from Tripleseat.""" - - def create_lead( - self, - public_key: str, - first_name: str | None = ..., - last_name: str | None = ..., - email_address: str | None = ..., - phone_number: str | None = ..., - location_id: str | None = ..., - additional_fields: dict | None = ..., - ) -> Any: - """Create a lead in Tripleseat.""" diff --git a/src/poly/types/integrations/available_integrations/tripleseat.pyi b/src/poly/types/integrations/available_integrations/tripleseat.pyi new file mode 100644 index 00000000..e89e9fcb --- /dev/null +++ b/src/poly/types/integrations/available_integrations/tripleseat.pyi @@ -0,0 +1,22 @@ +# Copyright PolyAI Limited +__all__ = ["Tripleseat"] + +import requests +from ..integration import Integration + +DEFAULT_PUBLIC_KEY: str + +class Tripleseat(Integration): + integration_id: str + integration_name: str + def get_bookings(self) -> requests.Response: ... + def create_lead( + self, + public_key: str = ..., + first_name: str | None = None, + last_name: str | None = None, + email_address: str | None = None, + phone_number: str | None = None, + location_id: str | None = None, + additional_fields: dict | None = None, + ) -> requests.Response: ... diff --git a/src/poly/types/integrations/integration.py b/src/poly/types/integrations/integration.py deleted file mode 100644 index 8333d199..00000000 --- a/src/poly/types/integrations/integration.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore -from typing import Any - -from ..log_utils import ConversationLogger - -__all__ = ["Integration"] - - -class Integration: - """Base class for all integrations""" - - integration_id: str - integration_name: str - - def __init__(self, log: ConversationLogger, proxy_request: Any): ... - def proxy_request( - self, - endpoint: str, - http_method: str, - headers: dict[str, str] | None = ..., - params: dict[str, str] | None = ..., - body: dict[str, Any] | None = ..., - ) -> Any: - """Proxy a request to the integration's API using the integration's authentication.""" diff --git a/src/poly/types/integrations/integration.pyi b/src/poly/types/integrations/integration.pyi new file mode 100644 index 00000000..039307b5 --- /dev/null +++ b/src/poly/types/integrations/integration.pyi @@ -0,0 +1,19 @@ +# Copyright PolyAI Limited +__all__ = ["Integration"] + +import requests +from ..log_utils import ConversationLogger as ConversationLogger + +class Integration: + integration_id: str + integration_name: str + def __init_subclass__(cls, **kwargs) -> None: ... + def __init__(self, log: ConversationLogger, proxy_request) -> None: ... + def proxy_request( + self, + endpoint: str, + http_method: str, + headers: dict[str, str] | None = None, + params: dict[str, str] | None = None, + body: dict[str, any] | None = None, + ) -> requests.Response: ... diff --git a/src/poly/types/integrations/integration_utils.pyi b/src/poly/types/integrations/integration_utils.pyi new file mode 100644 index 00000000..88db42b7 --- /dev/null +++ b/src/poly/types/integrations/integration_utils.pyi @@ -0,0 +1,22 @@ +# Copyright PolyAI Limited +from typing import Any +import requests + +VALID_HTTP_METHODS: Any +US_PROXY_BASE_URL: str +EU_PROXY_BASE_URL: str +DEFAULT_REQUEST_TIMEOUT_SECONDS: int + +def proxy_integration_request_to_paragon( + paragon_proxy_url: str, + paragon_connection_id: str, + paragon_project_id: str, + integration_token: str, + integration_id: str, + endpoint: str, + http_method: str, + headers: dict[str, str] | None = None, + params: dict[str, str] | None = None, + body: dict[str, any] | None = None, + request_timeout_seconds: int = ..., +) -> requests.Response: ... diff --git a/src/poly/types/integrations/integrations.py b/src/poly/types/integrations/integrations.py deleted file mode 100644 index 83ddfbb4..00000000 --- a/src/poly/types/integrations/integrations.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore -from typing import Any - -from .available_integrations.opentable import OpenTable -from .available_integrations.tripleseat import Tripleseat - -__all__ = ["Integrations"] - - -class Integrations: - """Integrations interface""" - - opentable: OpenTable - tripleseat: Tripleseat - - def __init__( - self, - log: Any, - paragon_connection_ids: dict[str, str] | None = ..., - paragon_project_id: str | None = ..., - integration_token: str | None = ..., - ): ... - def proxy_request( - self, - integration_id: str, - endpoint: str, - http_method: str, - headers: dict[str, str] | None = ..., - params: dict[str, str] | None = ..., - body: dict[str, Any] | None = ..., - ) -> Any: - """General method to proxy request through Paragon.""" diff --git a/src/poly/types/integrations/integrations.pyi b/src/poly/types/integrations/integrations.pyi new file mode 100644 index 00000000..a1731fe0 --- /dev/null +++ b/src/poly/types/integrations/integrations.pyi @@ -0,0 +1,27 @@ +# Copyright PolyAI Limited +__all__ = ["Integrations"] + +import requests +from .available_integrations.opentable import OpenTable as OpenTable +from .available_integrations.tripleseat import Tripleseat as Tripleseat +from ..log_utils import ConversationLogger as ConversationLogger + +class Integrations: + opentable: OpenTable + tripleseat: Tripleseat + def __init__( + self, + log: ConversationLogger, + paragon_connection_ids: dict[str, str] | None = None, + paragon_project_id: str | None = None, + integration_token: str | None = None, + ) -> None: ... + def proxy_request( + self, + integration_id: str, + endpoint: str, + http_method: str, + headers: dict[str, str] | None = None, + params: dict[str, str] | None = None, + body: dict[str, any] | None = None, + ) -> requests.Response: ... diff --git a/src/poly/types/knowledge_base.pyi b/src/poly/types/knowledge_base.pyi new file mode 100644 index 00000000..e5983f6f --- /dev/null +++ b/src/poly/types/knowledge_base.pyi @@ -0,0 +1,8 @@ +# Copyright PolyAI Limited +class KnowledgeBase: + def __init__(self) -> None: ... + @property + def disabled_topics(self) -> list[str]: ... + def disable_topics(self, topic_names: list[str]) -> None: ... + def enable_topics(self, topic_names: list[str]) -> None: ... + def enable_all_topics(self) -> None: ... diff --git a/src/poly/types/llm_client.pyi b/src/poly/types/llm_client.pyi new file mode 100644 index 00000000..d22c1c72 --- /dev/null +++ b/src/poly/types/llm_client.pyi @@ -0,0 +1,42 @@ +# Copyright PolyAI Limited +from dataclasses import dataclass +from typing import Literal + +class ChatCompletionError(Exception): + def __init__(self, message: str) -> None: ... + +@dataclass +class _InferenceConfig: + temperature: int | None = ... + top_p: float | None = ... + max_tokens: int | None = ... + response_format: dict | None = ... + +@dataclass +class _ModelPromptEvent: + content: str + type: Literal["prompt"] = ... + +@dataclass +class _ChatCompletionRequest: + provider_model_id: str + model_config: dict + inference_config: _InferenceConfig + events: list[_ModelPromptEvent] + +@dataclass +class _ChatCompletionResponse: + content: str + +class _LLMClient: + def __init__( + self, + account_id: str, + project_id: str, + client_env: str, + conversation_id: str, + correlation_id: str | None = None, + base_url: str = "https://api.internal.polyai.app", + timeout: int = 8, + ) -> None: ... + def chat_completion(self, request: _ChatCompletionRequest) -> _ChatCompletionResponse: ... diff --git a/src/poly/types/log_utils.py b/src/poly/types/log_utils.py deleted file mode 100644 index 3a01ecf3..00000000 --- a/src/poly/types/log_utils.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -__all__ = ["ConversationLogger"] - - -class ConversationLogger: - """Logging utility for Conversation objects""" - - def __init__(self): ... - def info(self, content: str, is_pii: bool = ..., **kwargs): - """Log an info message""" - - def warning(self, content: str, is_pii: bool = ..., **kwargs): - """Log a warning message""" - - def error(self, content: str, is_pii: bool = ..., **kwargs): - """Log an error message""" diff --git a/src/poly/types/log_utils.pyi b/src/poly/types/log_utils.pyi new file mode 100644 index 00000000..e87f951a --- /dev/null +++ b/src/poly/types/log_utils.pyi @@ -0,0 +1,8 @@ +# Copyright PolyAI Limited +__all__ = ["ConversationLogger"] + +class ConversationLogger: + def __init__(self) -> None: ... + def info(self, content: str, is_pii: bool = False, **kwargs): ... + def warning(self, content: str, is_pii: bool = False, **kwargs): ... + def error(self, content: str, is_pii: bool = False, **kwargs): ... diff --git a/src/poly/types/memory.py b/src/poly/types/memory.py deleted file mode 100644 index f385c75d..00000000 --- a/src/poly/types/memory.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -from typing import Any - - -__all__ = ["Memory"] - - -class Memory: - """An object exposing fields in the agent memory.""" - - def __init__(self, fields: dict[str, Any]): - """init""" - - def __getitem__(self, name: str) -> Any: - """Get a field from memory.""" - - def get(self, name: str, default: Any = ...) -> Any | None: - """Get a field from memory.""" - - def __contains__(self, name: str) -> bool: - """Check if a field is populated in memory.""" - - def __len__(self) -> int: - """Get the number of fields in memory.""" - - def fields(self) -> dict: - """Get all the fields from memory.""" - - def __setattr__(self, name: str, value: Any): - """WARNING: Do not use this method, this object is read-only.""" - - def __setitem__(self, name: str, value): - """WARNING: Do not use this method, this object is read-only.""" diff --git a/src/poly/types/memory.pyi b/src/poly/types/memory.pyi new file mode 100644 index 00000000..6973b040 --- /dev/null +++ b/src/poly/types/memory.pyi @@ -0,0 +1,14 @@ +# Copyright PolyAI Limited +__all__ = ["Memory"] + +from typing import Any + +class Memory: + def __init__(self, fields: dict[str, Any]) -> None: ... + def __getitem__(self, name: str) -> Any: ... + def get(self, name: str, default: Any = None) -> Any | None: ... + def __contains__(self, name: str) -> bool: ... + def __len__(self) -> int: ... + def fields(self) -> dict: ... + def __setattr__(self, name: str, value: Any): ... + def __setitem__(self, name: str, value): ... diff --git a/src/poly/types/secret_vault.py b/src/poly/types/secret_vault.pyi similarity index 59% rename from src/poly/types/secret_vault.py rename to src/poly/types/secret_vault.pyi index 43c15675..840b5d50 100644 --- a/src/poly/types/secret_vault.py +++ b/src/poly/types/secret_vault.pyi @@ -1,23 +1,22 @@ # Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore __all__ = ["InvalidInput", "MissingAccess", "SecretNotFound"] +from typing import Any -class SecretNotFound(Exception): - """Secret with specified name cannot be found""" +secret_client: Any +class SecretNotFound(Exception): def __init__(self, secret_name: str) -> None: ... - class MissingAccess(Exception): - """Assistant is not on access list for the secret""" - def __init__(self, assistant_id: str, secret_name: str) -> None: ... - class InvalidInput(Exception): - """Assistant is sending invalid input for secret access""" - def __init__(self, assistant_id: str, secret_name: str) -> None: ... + +class InternalError(Exception): + def __init__(self) -> None: ... + +def secret_vault(secret_name: str) -> str | dict: ... +def get_secret_vault_custom_secret(secret_name: str) -> dict: ... +def build_sms_secret_name() -> str: ... diff --git a/src/poly/types/sms.py b/src/poly/types/sms.py deleted file mode 100644 index d13b90a0..00000000 --- a/src/poly/types/sms.py +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -from typing import Protocol - - -__all__ = [ - "SMSClientFailure", - "SMSCredentials", - "SMSTemplate", - "OutgoingSMSTemplate", - "OutgoingSMS", - "SMSSentEvent", - "SMSClient", - "TwilioSMSClient", - "TelnyxSMSClient", - "SMSObj", -] - - -class SMSClientFailure(Exception): - """SMS Client Failure""" - - def __init__(self, integration: str, reason: str): ... - - -class SMSCredentials: - """SMS credentials""" - - account_sid: str - auth_token: str - - def __init__(self, account_sid: str, auth_token: str) -> None: ... - - -class SMSTemplate: - """SMS template""" - - name: str - content: str - phone_number: str - - def __init__(self, name: str, content: str, phone_number: str) -> None: ... - - -class OutgoingSMSTemplate: - """SMS template to send""" - - to_number: str - template: str - - def __init__(self, to_number: str, template: str) -> None: ... - - -class OutgoingSMS: - """SMS to send""" - - to_number: str - from_number: str - content: str - content_id: str | None - - def __init__( - self, to_number: str, from_number: str, content: str, content_id: str | None = ... - ) -> None: ... - - -class SMSSentEvent: - """Represents an attempt to send an SMS""" - - success: bool - sms: SMSObj - - def __init__(self, success: bool, sms: SMSObj) -> None: ... - @classmethod - def from_dict(cls, d) -> SMSSentEvent: - """Create an SMSSentEvent from a JSON dict""" - - def to_dict(self) -> dict: - """Convert SMSSentEvent to a JSON dict""" - - -class SMSClient(Protocol): - """SMS Client Protocol""" - - def send_sms(self, sms: OutgoingSMS) -> dict: - """Send SMS protocol""" - - def retry_send_sms(self, sms: OutgoingSMS, retry_count: int) -> dict: - """Send SMS protocol with retry""" - - -class TwilioSMSClient(SMSClient): - """Twilio SMS Client""" - - def __init__(self, sms_credentials: SMSCredentials): ... - def send_content_template(self, sms: OutgoingSMS, **kwargs) -> dict: - """Sends SMS using twilio API""" - - def send_sms(self, sms: OutgoingSMS) -> dict: - """Sends SMS using twilio API""" - - def retry_send_sms(self, sms: OutgoingSMS, retry_count: int): - """Sends SMS using Twilio API with retry.""" - - def retry_send_content_template(self, sms: OutgoingSMS, retry_count: int, **kwargs): - """Sends Content Template using Twilio API with retry.""" - - -class TelnyxSMSClient(SMSClient): - """Telnyx SMS Client""" - - def __init__(self, sms_credentials: SMSCredentials): ... - def send_sms(self, sms: OutgoingSMS) -> dict: - """Sends SMS using Telnyx API""" - - def retry_send_sms(self, sms: OutgoingSMS, retry_count: int) -> dict: - """Sends SMS using Telnyx API with retry""" - - -SMSObj = OutgoingSMS | OutgoingSMSTemplate diff --git a/src/poly/types/sms.pyi b/src/poly/types/sms.pyi new file mode 100644 index 00000000..7f563568 --- /dev/null +++ b/src/poly/types/sms.pyi @@ -0,0 +1,70 @@ +# Copyright PolyAI Limited +__all__ = [ + "OutgoingSMS", + "OutgoingSMSTemplate", + "SMSClientFailure", + "SMSCredentials", + "SMSTemplate", +] + +from collections.abc import Callable as Callable +from dataclasses import dataclass +from requests import Response as Response +from typing import Any, Protocol + +class SMSClientFailure(Exception): + def __init__(self, integration: str, reason: str) -> None: ... + +@dataclass +class SMSCredentials: + account_sid: str + auth_token: str + +@dataclass +class SMSTemplate: + name: str + content: str + phone_number: str + +@dataclass +class OutgoingSMSTemplate: + to_number: str + template: str + +@dataclass +class OutgoingSMS: + to_number: str + from_number: str + content: str + content_id: str | None = ... + +SMSObj = OutgoingSMS | OutgoingSMSTemplate + +def parse_sms_dict(d: dict) -> SMSObj: ... +@dataclass +class SMSSentEvent: + success: bool + sms: SMSObj + @classmethod + def from_dict(cls, d) -> SMSSentEvent: ... + def to_dict(self) -> dict: ... + +def fibonacci_backoff(n: int): ... + +class SMSClient(Protocol): + def send_sms(self, sms: OutgoingSMS) -> dict: ... + def retry_send_sms(self, sms: OutgoingSMS, retry_count: int) -> dict: ... + +class TwilioSMSClient(SMSClient): + sms_credentials: Any + def __init__(self, sms_credentials: SMSCredentials) -> None: ... + def send_content_template(self, sms: OutgoingSMS, **kwargs) -> dict: ... + def send_sms(self, sms: OutgoingSMS) -> dict: ... + def retry_send_sms(self, sms: OutgoingSMS, retry_count: int): ... + def retry_send_content_template(self, sms: OutgoingSMS, retry_count: int, **kwargs): ... + +class TelnyxSMSClient(SMSClient): + sms_credentials: Any + def __init__(self, sms_credentials: SMSCredentials) -> None: ... + def send_sms(self, sms: OutgoingSMS) -> dict: ... + def retry_send_sms(self, sms: OutgoingSMS, retry_count: int) -> dict: ... diff --git a/src/poly/types/state_utils.pyi b/src/poly/types/state_utils.pyi new file mode 100644 index 00000000..563c6dca --- /dev/null +++ b/src/poly/types/state_utils.pyi @@ -0,0 +1,8 @@ +# Copyright PolyAI Limited +import pickle +from typing import Any + +def encode_state_value(v: Any, pickler: type[pickle.Pickler] | None = None) -> str: ... +def pickle_state(state: dict, pickler: type[pickle.Pickler] | None = None) -> dict[str, str]: ... +def json_project_state(state: dict) -> dict[str, Any]: ... +def unpickle_state(d: dict[str, str]) -> dict[str, Any]: ... diff --git a/src/poly/types/value_extraction.py b/src/poly/types/value_extraction.py deleted file mode 100644 index f002f61a..00000000 --- a/src/poly/types/value_extraction.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -__all__ = ["ExtractionError", "Address"] - - -class ExtractionError(Exception): - """Error in retrieving extracted values from the conversation.""" - - def __init__(self, message: str): ... - - -class Address: - """Represents a structured address.""" - - street_number: str | None - street_name: str | None - city: str | None - state: str | None - postcode: str | None - country: str | None - - def __init__( - self, - street_number: str | None = ..., - street_name: str | None = ..., - city: str | None = ..., - state: str | None = ..., - postcode: str | None = ..., - country: str | None = ..., - ) -> None: ... diff --git a/src/poly/types/value_extraction.pyi b/src/poly/types/value_extraction.pyi new file mode 100644 index 00000000..13e948fe --- /dev/null +++ b/src/poly/types/value_extraction.pyi @@ -0,0 +1,59 @@ +# Copyright PolyAI Limited +__all__ = ["Address"] + +from dataclasses import dataclass +from .value_extraction_types import EntityConfig as EntityConfig + +class ExtractionError(Exception): + def __init__(self, message: str) -> None: ... + +@dataclass +class Address: + street_number: str | None = ... + street_name: str | None = ... + city: str | None = ... + state: str | None = ... + postcode: str | None = ... + country: str | None = ... + +@dataclass +class _AddressExtractionRequest: + hypotheses: list[str] + country: str = ... + language: str = ... + addresses: list[Address] | None = ... + states: list[str] | None = ... + spellings: list[str] | None = ... + +@dataclass +class _AddressExtractionResponse: + address: Address + extraction_info: str + +@dataclass +class _EntityValidationRequest: + value: str + entity_config: EntityConfig + +@dataclass +class _EntityValidationResponse: + is_valid: bool + message: str | None = ... + country_code: int | None = ... + number: str | None = ... + +class _ValueExtractionClient: + def __init__( + self, + account_id: str, + project_id: str, + client_env: str, + conversation_id: str, + turn_index: int, + correlation_id: str | None = None, + base_url: str = "https://api.internal.polyai.app", + timeout: int = 8, + ) -> None: ... + def extract_address(self, request: _AddressExtractionRequest) -> _AddressExtractionResponse: ... + def extract_city(self, request: _AddressExtractionRequest) -> _AddressExtractionResponse: ... + def validate_entity(self, request: _EntityValidationRequest) -> _EntityValidationResponse: ... diff --git a/src/poly/types/value_extraction_types.py b/src/poly/types/value_extraction_types.py deleted file mode 100644 index 4f783942..00000000 --- a/src/poly/types/value_extraction_types.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -import re -from datetime import date, time -from enum import StrEnum -from typing import Literal -from pydantic import BaseModel - - -__all__ = [ - "EntityType", - "NumericType", - "BaseRangeConfig", - "NonNegativeMaxRangeConfig", - "NumericConfig", - "QuantityConfig", - "CurrencyConfig", - "NameConfig", - "FreeTextConfig", - "AlphanumericConfig", - "DateConfig", - "EmailConfig", - "TimeConfig", - "PhoneNumberConfig", - "EnumConfig", - "EntityConfig", -] - - -class EntityType(StrEnum): - """Enum for entity types.""" - - -class NumericType(StrEnum): - """Enum for supported numeric types""" - - -class BaseRangeConfig(BaseModel): - """Base config with min/max validation.""" - - min_inclusive: float | None - max_inclusive: float | None - - -class NonNegativeMaxRangeConfig(BaseRangeConfig): - """Base config with non-negative min/max validation""" - - def validate_max(cls, v): - """Validates that max value is non-negative""" - - -class NumericConfig(BaseRangeConfig): - """Configuration for numeric entities.""" - - entity_type: Literal["numeric"] - numeric_type: NumericType - - def validate_min_max(cls, v, values): - """Validate that min is not greater than max.""" - - -class QuantityConfig(NonNegativeMaxRangeConfig): - """Configuration for quantity entities.""" - - entity_type: Literal["quantity"] - min_inclusive: int | None - max_inclusive: int | None - numeric_type: NumericType - - -class CurrencyConfig(NonNegativeMaxRangeConfig): - """Configuration for currency entities.""" - - entity_type: Literal["currency"] - min_inclusive: float | None - max_inclusive: float | None - numeric_type: NumericType - - -class NameConfig(BaseModel): - """Configuration for name entities.""" - - entity_type: Literal["name"] - - -class FreeTextConfig(BaseModel): - """Configuration for free text entities.""" - - entity_type: Literal["free_text"] - - -class AlphanumericConfig(BaseModel): - """Configuration for alphanumeric entities.""" - - custom_regex: str - entity_type: Literal["alphanumeric"] - capturing_group: int | None - - def validate_regex(cls, v): - """Validate and compile the regex pattern.""" - - def validate_capturing_group(cls, v): - """Validate capturing group.""" - - def get_compiled_regex(self) -> re.Pattern: - """Get the compiled regex pattern.""" - - -class DateConfig(BaseModel): - """Configuration for date entities.""" - - entity_type: Literal["date"] - day_first: bool | None - earliest_date_inclusive: date | None - latest_date_inclusive: date | None - - def parse_dates(cls, v, values): - """Parse date strings into date objects.""" - - def validate_dates(cls, v, values): - """Validate that earliest date is not after latest date.""" - - -class EmailConfig(AlphanumericConfig): - """Configuration for email entities.""" - - custom_regex: str - entity_type: Literal["email"] - capturing_group: int | None - - -class TimeConfig(BaseModel): - """Configuration for time entities.""" - - entity_type: Literal["time"] - format: str - earliest_time_inclusive: time | None - latest_time_inclusive: time | None - time_pivot: int - - def parse_times(cls, v, values): - """Parse time strings into time objects.""" - - def validate_times(cls, v, values): - """Validate that earliest time is not after latest time.""" - - def validate_time_pivot(cls, v, values): - """Validate that a time pivot, if provided is within a 12 hour range""" - - -class PhoneNumberConfig(BaseModel): - """Configuration for phone number entities.""" - - entity_type: Literal["phone_number"] - regions: set[str] | None - - def validate_regions(cls, v): - """Clean and validate region codes.""" - - -class EnumConfig(BaseModel): - """Configuration for Enum entities""" - - entity_type: Literal["enum"] - allowed_vals: set[str] | None - - def validate_allowed_vals(cls, v): - """Clean and validate allowed values.""" - - -EntityConfig = ( - NumericConfig - | QuantityConfig - | CurrencyConfig - | NameConfig - | FreeTextConfig - | AlphanumericConfig - | DateConfig - | EmailConfig - | TimeConfig - | PhoneNumberConfig - | EnumConfig -) diff --git a/src/poly/types/value_extraction_types.pyi b/src/poly/types/value_extraction_types.pyi new file mode 100644 index 00000000..7711f7ef --- /dev/null +++ b/src/poly/types/value_extraction_types.pyi @@ -0,0 +1,109 @@ +# Copyright PolyAI Limited +import re +from datetime import date, time +from enum import StrEnum +from pydantic import BaseModel +from typing import Literal + +class EntityType(StrEnum): + ADDRESS = "address" + ALPHANUMERIC = "alphanumeric" + DATE = "date" + EMAIL = "email" + NUMERIC = "numeric" + PHONE_NUMBER = "phone_number" + TIME = "time" + ENUM = "enum" + CURRENCY = "currency" + QUANTITY = "quantity" + NAME = "name" + FREE_TEXT = "free_text" + +class NumericType(StrEnum): + INT = "int" + FLOAT = "float" + +class BaseRangeConfig(BaseModel): + min_inclusive: float | None + max_inclusive: float | None + +class NonNegativeMaxRangeConfig(BaseRangeConfig): + def validate_max(cls, v): ... + +class NumericConfig(BaseRangeConfig): + entity_type: Literal["numeric"] + numeric_type: NumericType + def validate_min_max(cls, v, values): ... + +class QuantityConfig(NonNegativeMaxRangeConfig): + entity_type: Literal["quantity"] + min_inclusive: int | None + max_inclusive: int | None + numeric_type: NumericType + +class CurrencyConfig(NonNegativeMaxRangeConfig): + entity_type: Literal["currency"] + min_inclusive: float | None + max_inclusive: float | None + numeric_type: NumericType + +class NameConfig(BaseModel): + entity_type: Literal["name"] + +class FreeTextConfig(BaseModel): + entity_type: Literal["free_text"] + +class AlphanumericConfig(BaseModel): + custom_regex: str + entity_type: Literal["alphanumeric"] + capturing_group: int | None + def validate_regex(cls, v): ... + def validate_capturing_group(cls, v): ... + def get_compiled_regex(self) -> re.Pattern: ... + +class DateConfig(BaseModel): + entity_type: Literal["date"] + day_first: bool | None + earliest_date_inclusive: date | None + latest_date_inclusive: date | None + def parse_dates(cls, v, values): ... + def validate_dates(cls, v, values): ... + +class EmailConfig(AlphanumericConfig): + custom_regex: str + entity_type: Literal["email"] + capturing_group: int | None + +class TimeConfig(BaseModel): + entity_type: Literal["time"] + format: str + earliest_time_inclusive: time | None + latest_time_inclusive: time | None + time_pivot: int + def parse_times(cls, v, values): ... + def validate_times(cls, v, values): ... + def validate_time_pivot(cls, v, values): ... + +class PhoneNumberConfig(BaseModel): + entity_type: Literal["phone_number"] + regions: set[str] | None + def validate_regions(cls, v): ... + +class EnumConfig(BaseModel): + entity_type: Literal["enum"] + allowed_vals: set[str] | None + def validate_allowed_vals(cls, v): ... + +EntityConfig = ( + NumericConfig + | QuantityConfig + | CurrencyConfig + | NameConfig + | FreeTextConfig + | AlphanumericConfig + | DateConfig + | EmailConfig + | TimeConfig + | PhoneNumberConfig + | EnumConfig +) diff --git a/src/poly/types/webchat.py b/src/poly/types/webchat.py deleted file mode 100644 index f5db845a..00000000 --- a/src/poly/types/webchat.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright PolyAI Limited -# flake8: noqa -# ruff: noqa -# type: ignore - - -__all__ = ["ChatCallAction", "WebchatInterface"] - - -class ChatCallAction: - """A chat call action for an Agent Response.""" - - def __init__(self, contact_number: str, title: str | None = ...): ... - def to_dict(self): - """Convert the ChatCallAction to a dictionary.""" - - -class WebchatInterface: - """Webchat-specific methods and properties for the conversation.""" - - def __init__(self) -> None: ... - @property - def chat_call_actions(self) -> list[ChatCallAction]: - """List of chat call actions for the next agent message.""" - - def set_chat_call_actions(self, actions: list[ChatCallAction]) -> None: - """Sets chat call actions for the agent message.""" diff --git a/src/poly/types/webchat.pyi b/src/poly/types/webchat.pyi new file mode 100644 index 00000000..4f0590d7 --- /dev/null +++ b/src/poly/types/webchat.pyi @@ -0,0 +1,16 @@ +# Copyright PolyAI Limited +__all__ = ["WebchatInterface", "ChatCallAction"] + +from typing import Any + +class ChatCallAction: + contact_number: Any + title: Any + def __init__(self, contact_number: str, title: str | None = None) -> None: ... + def to_dict(self): ... + +class WebchatInterface: + def __init__(self) -> None: ... + @property + def chat_call_actions(self) -> list[ChatCallAction]: ... + def set_chat_call_actions(self, actions: list[ChatCallAction]) -> None: ... diff --git a/src/poly/utils.py b/src/poly/utils.py index 2cda34bf..601ae239 100644 --- a/src/poly/utils.py +++ b/src/poly/utils.py @@ -13,14 +13,13 @@ import re from typing import Callable, Optional -from poly.resources import Function, FunctionStep, Resource, ResourceMapping - -from poly.handlers.protobuf.commands_pb2 import Command from poly.handlers.protobuf.channels_pb2 import ( Channel_UpdateStatus, - WebChatChannel_UpdateStatus, ChannelStatus, + WebChatChannel_UpdateStatus, ) +from poly.handlers.protobuf.commands_pb2 import Command +from poly.resources import Function, FunctionStep, Resource, ResourceMapping logger = logging.getLogger(__name__) @@ -157,32 +156,46 @@ def _read_all_from_stub(source: str) -> list[str] | None: return None -def _load_file_class_maps() -> dict[str, list[str]]: +def _load_file_class_maps( + pkg: importlib.resources.abc.Traversable | None = None, + prefix: str = "", +) -> dict[str, list[str]]: """Discover exported names by reading __all__ from each type file. + Recurses into subpackages so nested modules like + ``integrations/integrations.py`` are included. + Returns: - A dictionary mapping filenames (e.g. "conversation.py") to the list of - names declared in that module's __all__. + A dictionary mapping dotted module paths (e.g. + ``"conversation"`` or ``"integrations.integrations"``) + to the list of names declared in that module's ``__all__``. """ + if pkg is None: + pkg = importlib.resources.files(_TYPES_PACKAGE) result: dict[str, list[str]] = {} - pkg = importlib.resources.files(_TYPES_PACKAGE) for resource in sorted(pkg.iterdir(), key=lambda r: r.name): name = resource.name - if not name.endswith(".py") or name.startswith("_"): + if name == "__pycache__": + continue + if resource.is_dir(): + sub_prefix = f"{prefix}{name}." if prefix else f"{name}." + result.update(_load_file_class_maps(resource, sub_prefix)) + continue + if not name.endswith(".pyi") or name.startswith("_"): continue names = _read_all_from_stub(resource.read_text(encoding="utf-8")) if names: - result[name] = names + module_name = name.removesuffix(".pyi") + result[f"{prefix}{module_name}"] = names return result def _gen_import_statements() -> str: """Import statements for _gen/__init__.py using _gen. absolute form.""" imports = [] - for file_path, names in _load_file_class_maps().items(): - module_name = os.path.basename(file_path).replace(".py", "") + for dotted_module, names in _load_file_class_maps().items(): imports.append( - f"""from _gen.{module_name} import ( + f"""from _gen.{dotted_module} import ( {", ".join(names)} )""" ) @@ -198,10 +211,10 @@ def create_import_file_contents() -> str: def _copy_types_tree(pkg: importlib.resources.abc.Traversable, dest_dir: str) -> None: - """Recursively copy .py stub files from a package into *dest_dir*. + """Recursively copy .pyi stub files from a package into *dest_dir*. Creates subdirectories as needed and rewrites ``runtime.``/``utils.`` - imports to relative form. + imports to relative form. Files are written as ``.pyi``. """ os.makedirs(dest_dir, exist_ok=True) for resource in sorted(pkg.iterdir(), key=lambda r: r.name): @@ -211,7 +224,7 @@ def _copy_types_tree(pkg: importlib.resources.abc.Traversable, dest_dir: str) -> if resource.is_dir(): _copy_types_tree(resource, os.path.join(dest_dir, name)) continue - if not name.endswith(".py") or (name.startswith("_") and name != "__init__.py"): + if not name.endswith(".pyi"): continue source = _relativize_stub_imports(resource.read_text(encoding="utf-8")) with open(os.path.join(dest_dir, name), "w", encoding="utf-8") as f: @@ -219,13 +232,13 @@ def _copy_types_tree(pkg: importlib.resources.abc.Traversable, dest_dir: str) -> def save_imports(base_path: str) -> None: - """Save the _gen package: __init__.py and importable stub .py files.""" + """Save the _gen package: __init__.py and .pyi stub files.""" gen_dir = os.path.join(base_path, "_gen") os.makedirs(gen_dir, exist_ok=True) - # Remove stale .pyi files if any exist from a previous generation + # Remove stale .py stub files from previous generation (now .pyi) for fname in os.listdir(gen_dir): - if fname.endswith(".pyi"): + if fname.endswith(".py") and fname != "__init__.py" and fname != "decorators.py": os.remove(os.path.join(gen_dir, fname)) # Copy type files (including subdirectories) into _gen/ diff --git a/uv.lock b/uv.lock index e75e52f6..0e7186d7 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.14.0" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] [[package]] name = "appdirs" @@ -20,6 +24,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" }, ] +[[package]] +name = "ast-serialize" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, + { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, + { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, + { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, + { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, + { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, + { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, +] + [[package]] name = "attrs" version = "23.2.0" @@ -226,6 +270,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/c1/d10b371bcba7abce05e2b33910e39c33cfa496a53f13640b7b8e10bb4d2b/langcodes-3.5.1-py3-none-any.whl", hash = "sha256:b6a9c25c603804e2d169165091d0cdb23934610524a21d226e4f463e8e958a72", size = 183050, upload-time = "2025-12-02T16:21:59.954Z" }, ] +[[package]] +name = "librt" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, + { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, + { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, + { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, + { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, + { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, + { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, + { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, + { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, +] + [[package]] name = "licensecheck" version = "2024.3" @@ -291,6 +369,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -309,6 +426,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "pip-licenses" version = "5.5.1" @@ -341,7 +467,7 @@ wheels = [ [[package]] name = "polyai-adk" -version = "0.23.1" +version = "0.25.7" source = { editable = "." } dependencies = [ { name = "argcomplete" }, @@ -361,6 +487,7 @@ dependencies = [ dev = [ { name = "coverage" }, { name = "licensecheck" }, + { name = "mypy" }, { name = "pip-licenses" }, { name = "pre-commit" }, { name = "pytest" }, @@ -375,6 +502,7 @@ requires-dist = [ { name = "jsonschema", specifier = ">=4.20.0" }, { name = "langcodes", specifier = "==3.5.1" }, { name = "licensecheck", marker = "extra == 'dev'", specifier = ">=2024.3" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pip-licenses", marker = "extra == 'dev'", specifier = ">=5.5.1" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.0.0" }, { name = "protobuf", specifier = ">=4.21.0" }, @@ -746,6 +874,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/7e/6bfd748a9f4ff9267ed3329b86a0f02cdf6ab49f87bc36c8a164852f99fc/ty-0.0.20-py3-none-win_arm64.whl", hash = "sha256:53f7a5c12c960e71f160b734f328eff9a35d578af4b67a36b0bb5990ac5cdc27", size = 10150143, upload-time = "2026-03-02T15:51:31.283Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + [[package]] name = "url-normalize" version = "2.2.1"