diff --git a/.github/workflows/python_a2ui_agent_build_and_test.yml b/.github/workflows/python_a2ui_agent_build_and_test.yml index 4d3f44e8e..4ecd60d27 100644 --- a/.github/workflows/python_a2ui_agent_build_and_test.yml +++ b/.github/workflows/python_a2ui_agent_build_and_test.yml @@ -46,10 +46,14 @@ jobs: working-directory: a2a_agents/python/a2ui_agent run: uv run pyink --check . + - name: Run unit tests + working-directory: a2a_agents/python/a2ui_agent + run: uv run --with pytest pytest tests/ + - name: Build the python SDK working-directory: a2a_agents/python/a2ui_agent run: uv build . - - name: Run unit tests + - name: Run validation scripts on assets packing working-directory: a2a_agents/python/a2ui_agent - run: uv run --with pytest pytest tests/ + run: uv run python tests/integration/verify_load_real.py diff --git a/a2a_agents/python/a2ui_agent/.gitignore b/a2a_agents/python/a2ui_agent/.gitignore new file mode 100644 index 000000000..22b91a08b --- /dev/null +++ b/a2a_agents/python/a2ui_agent/.gitignore @@ -0,0 +1 @@ +src/a2ui/assets/**/*.json diff --git a/a2a_agents/python/a2ui_agent/pack_specs_hook.py b/a2a_agents/python/a2ui_agent/pack_specs_hook.py new file mode 100644 index 000000000..f846f2a7c --- /dev/null +++ b/a2a_agents/python/a2ui_agent/pack_specs_hook.py @@ -0,0 +1,62 @@ +import os +import shutil +import sys +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + + +class PackSpecsBuildHook(BuildHookInterface): + + def initialize(self, version, build_data): + project_root = self.root + + # Add src to sys.path to import the constant + src_path = os.path.join(project_root, "src") + if src_path not in sys.path: + sys.path.insert(0, src_path) + + from a2ui.inference.schema.manager import ( + SPEC_VERSION_MAP, + A2UI_ASSET_PACKAGE, + SPECIFICATION_DIR, + find_repo_root, + ) + + # project root is in a2a_agents/python/a2ui_agent + # Dynamically find repo root by looking for SPECIFICATION_DIR + repo_root = find_repo_root(project_root) + if not repo_root: + # Check for PKG-INFO which implies a packaged state (sdist). + # If PKG-INFO is present, trust the bundled assets. + if os.path.exists(os.path.join(project_root, "PKG-INFO")): + print("Repository root not found, but PKG-INFO present (sdist). Skipping copy.") + return + + raise RuntimeError( + f"Could not find repository root (looked for '{SPECIFICATION_DIR}'" + " directory)." + ) + + # Target directory: src/a2ui/assets + target_base = os.path.join( + project_root, "src", A2UI_ASSET_PACKAGE.replace(".", os.sep) + ) + + for ver, schema_map in SPEC_VERSION_MAP.items(): + target_dir = os.path.join(target_base, ver) + os.makedirs(target_dir, exist_ok=True) + + for _schema_key, source_rel_path in schema_map.items(): + source_path = os.path.join(repo_root, source_rel_path) + + if not os.path.exists(source_path): + print( + f"WARNING: Source schema file not found at {source_path}. Build might" + " produce incomplete wheel if not running from monorepo root." + ) + continue + + filename = os.path.basename(source_path) + dst_file = os.path.join(target_dir, filename) + + print(f"Copying {source_path} -> {dst_file}") + shutil.copy2(source_path, dst_file) diff --git a/a2a_agents/python/a2ui_agent/pyproject.toml b/a2a_agents/python/a2ui_agent/pyproject.toml index 513db8577..960a031dd 100644 --- a/a2a_agents/python/a2ui_agent/pyproject.toml +++ b/a2a_agents/python/a2ui_agent/pyproject.toml @@ -12,11 +12,18 @@ dependencies = [ ] [build-system] -requires = ["hatchling"] +requires = ["hatchling", "jsonschema"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/a2ui"] +artifacts = ["src/a2ui/assets/**"] + +[tool.hatch.build.targets.sdist] +artifacts = ["src/a2ui/assets/**"] + +[tool.hatch.build.hooks.custom] +path = "pack_specs_hook.py" [[tool.uv.index]] url = "https://pypi.org/simple" diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/core/validator.py b/a2a_agents/python/a2ui_agent/src/a2ui/core/validator.py new file mode 100644 index 000000000..c918a6a03 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/core/validator.py @@ -0,0 +1,32 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, Dict +import jsonschema + + +class A2uiValidator: + """ + Validates JSON instances against a bundled A2UI schema. + """ + + def __init__(self, schema: Dict[str, Any]): + self._validator = jsonschema.validators.validator_for(schema)(schema) + + def validate(self, instance: Any) -> None: + """ + Validates the given instance against the bundled schema. + Raises jsonschema.exceptions.ValidationError if validation fails. + """ + self._validator.validate(instance) diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/__init__.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/__init__.py new file mode 100644 index 000000000..4df27a209 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/inference_strategy.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/inference_strategy.py new file mode 100644 index 000000000..db737324e --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/inference_strategy.py @@ -0,0 +1,33 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from abc import ABC, abstractmethod +from typing import List + + +class InferenceStrategy(ABC): + + @abstractmethod + def generate_system_prompt( + self, + role_description: str, + workflow_description: str = "", + ui_description: str = "", + selected_components: List[str] = [], + examples: str = "", + ) -> str: + """ + Abstract method to be implemented by subclasses. + """ + pass diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/__init__.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/__init__.py new file mode 100644 index 000000000..4df27a209 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/loader.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/loader.py new file mode 100644 index 000000000..5ef09c175 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/loader.py @@ -0,0 +1,69 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +import importlib.resources +from typing import List, Dict, Any + +from abc import ABC, abstractmethod + +ENCODING = "utf-8" + + +class A2uiSchemaLoader(ABC): + """Abstract base class for loading schema files.""" + + @abstractmethod + def load(self, filename: str) -> Any: + """Loads a JSON file.""" + pass + + +class FileSystemLoader(A2uiSchemaLoader): + """Loads schema files from the local filesystem. + + This loader assumes that all referenced schema files are located in the + same flat directory structure. + """ + + def __init__(self, base_dir: str): + self.base_dir = base_dir + + def load(self, filename: str) -> Any: + path = os.path.join(self.base_dir, filename) + with open(path, "r", encoding=ENCODING) as f: + return json.load(f) + + +class PackageLoader(A2uiSchemaLoader): + """Loads schema files from package resources. + + This loader assumes that all referenced schema files are located in the + same flat package structure. + """ + + def __init__(self, package_path: str): + self.package_path = package_path + + def load(self, filename: str) -> Any: + try: + traversable = importlib.resources.files(self.package_path) + resource_path = traversable.joinpath(filename) + with resource_path.open("r", encoding=ENCODING) as f: + return json.load(f) + except Exception as e: + raise IOError( + f"Could not load package resource {filename} in {self.package_path}: {e}" + ) from e diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py new file mode 100644 index 000000000..5b9e86fcf --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py @@ -0,0 +1,412 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy +import json +import logging +import os +import importlib.resources +from typing import List, Dict, Any, Tuple, Set +from ..inference_strategy import InferenceStrategy +from .loader import A2uiSchemaLoader, PackageLoader, FileSystemLoader +from a2ui.core.validator import A2uiValidator + +# Helper constants for schema management shared by build hook and runtime + +# Mapping of version to the relative path of the source schema in the monorepo +# Paths are relative to the repository root +A2UI_ASSET_PACKAGE = "a2ui.assets" +SERVER_TO_CLIENT_SCHEMA_KEY = "server_to_client" +COMMON_TYPES_SCHEMA_KEY = "common_types" +CATALOG_SCHEMA_KEY = "catalog" +CATALOG_COMPONENTS_KEY = "components" +CATALOG_STYLES_KEY = "styles" + +SPEC_VERSION_MAP = { + "0.8": { + SERVER_TO_CLIENT_SCHEMA_KEY: "specification/v0_8/json/server_to_client.json", + CATALOG_SCHEMA_KEY: "specification/v0_8/json/standard_catalog_definition.json", + }, + "0.9": { + SERVER_TO_CLIENT_SCHEMA_KEY: "specification/v0_9/json/server_to_client.json", + CATALOG_SCHEMA_KEY: "specification/v0_9/json/standard_catalog.json", + COMMON_TYPES_SCHEMA_KEY: "specification/v0_9/json/common_types.json", + }, +} + +SPECIFICATION_DIR = "specification" + + +class A2uiSchemaManager(InferenceStrategy): + """Manages A2UI schema pruning and prompt injection.""" + + def __init__(self, version: str, custom_catalog_path: str = None): + self.version = version + self.server_to_client_schema = None + self.catalog_schema = None + self.common_types_schema = None + self._load_schemas(version, custom_catalog_path) + self._bundled_schema = self.bundle_schemas() + self._validator = A2uiValidator(self._bundled_schema) + + @property + def bundled_schema(self): + return self._bundled_schema + + @property + def validator(self): + return self._validator + + def _load_schemas(self, version: str, custom_catalog_path: str = None): + """ + Loads separate schema components. + """ + if version in SPEC_VERSION_MAP: + spec_map = SPEC_VERSION_MAP[version] + + self.server_to_client_schema = self._load_schema( + version, spec_map.get(SERVER_TO_CLIENT_SCHEMA_KEY) + ) + if COMMON_TYPES_SCHEMA_KEY in spec_map: + self.common_types_schema = self._load_schema( + version, spec_map.get(COMMON_TYPES_SCHEMA_KEY) + ) + + if custom_catalog_path: + self.catalog_schema = self._load_from_path(custom_catalog_path) + elif CATALOG_SCHEMA_KEY in spec_map: + self.catalog_schema = self._load_schema( + version, spec_map.get(CATALOG_SCHEMA_KEY) + ) + else: + raise ValueError(f"Could not load catalog schema for version {version}") + else: + raise ValueError( + f"Unknown A2UI specification version: {version}. Supported:" + f" {list(SPEC_VERSION_MAP.keys())}" + ) + + def _load_schema(self, version: str, path: str) -> Dict: + if not path: + return {} + + filename = os.path.basename(path) + + # 1. Try to load from package resources + try: + loader = PackageLoader(f"{A2UI_ASSET_PACKAGE}.{version}") + return loader.load(filename) + except Exception as e: + logging.debug("Could not load schema '%s' from package: %s", filename, e) + + # 2. Fallback: Local Assets + # This handles cases where assets might be present in src but not installed + try: + potential_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", + "assets", + version, + filename, + ) + ) + loader = FileSystemLoader(os.path.dirname(potential_path)) + return loader.load(filename) + except Exception as e: + logging.debug("Could not load schema '%s' from local assets: %s", filename, e) + + # 3. Fallback: Source Repository (specification/...) + # This handles cases where we are running directly from source tree + # And assets are not yet copied to src/a2ui/assets + # schema_manager.py is at a2a_agents/python/a2ui_agent/src/a2ui/inference/schema/manager.py + # Dynamically find repo root by looking for "specification" directory + try: + repo_root = find_repo_root(os.path.dirname(__file__)) + except Exception as e: + logging.debug("Could not find repo root: %s", e) + + if repo_root: + source_path = os.path.join(repo_root, path) + if os.path.exists(source_path): + loader = FileSystemLoader(os.path.dirname(source_path)) + return loader.load(filename) + + raise IOError( + f"Could not load schema component {filename} for version {version}" + ) from e + + def _load_from_path(self, path: str) -> Dict: + try: + loader = FileSystemLoader(os.path.dirname(path)) + return loader.load(os.path.basename(path)) + except Exception as e: + raise ValueError(f"Failed to load custom catalog at {path}: {e}") + + def get_pruned_catalog(self, selected_components: List[str] = None) -> Dict: + """Dynamically filters the catalog to reduce context window tokens.""" + if not self.catalog_schema: + return {} + + schema = copy.deepcopy(self.catalog_schema) + + # If no components selected, return full catalog + if not selected_components: + return schema + + if CATALOG_COMPONENTS_KEY in schema and isinstance( + schema[CATALOG_COMPONENTS_KEY], dict + ): + all_comps = schema[CATALOG_COMPONENTS_KEY] + schema[CATALOG_COMPONENTS_KEY] = { + k: v for k, v in all_comps.items() if k in selected_components + } + + # Filter anyComponent oneOf if it exists + # Path: $defs -> anyComponent -> oneOf + if "$defs" in schema and "anyComponent" in schema["$defs"]: + any_comp = schema["$defs"]["anyComponent"] + if "oneOf" in any_comp and isinstance(any_comp["oneOf"], list): + filtered_one_of = [] + for item in any_comp["oneOf"]: + if "$ref" in item: + ref = item["$ref"] + if ref.startswith(f"#/{CATALOG_COMPONENTS_KEY}/"): + comp_name = ref.split("/")[-1] + if comp_name in selected_components: + filtered_one_of.append(item) + else: + logging.warning(f"Skipping unknown ref format: {ref}") + else: + logging.warning(f"Skipping non-ref item in anyComponent oneOf: {item}") + + any_comp["oneOf"] = filtered_one_of + + return schema + + def generate_system_prompt( + self, + role_description: str, + workflow_description: str = "", + ui_description: str = "", + selected_components: List[str] = [], + examples: str = "", + ) -> str: + """Assembles the final system instruction for the LLM.""" + catalog_str = json.dumps(self.get_pruned_catalog(selected_components), indent=2) + server_client_str = ( + json.dumps(self.server_to_client_schema, indent=2) + if self.server_to_client_schema + else "{}" + ) + + parts = [role_description] + if workflow_description: + parts.append(f"## Workflow Description:\n{workflow_description}") + if ui_description: + parts.append(f"## UI Description:\n{ui_description}") + + if examples: + parts.append(f"## Examples:\n{examples}") + + parts.append("---BEGIN A2UI JSON SCHEMA---") + parts.append(f"### Server To Client Schema:\n{server_client_str}") + + if self.common_types_schema: + common_str = json.dumps(self.common_types_schema, indent=2) + parts.append(f"### Common Types Schema:\n{common_str}") + + parts.append(f"### Catalog Schema:\n{catalog_str}") + parts.append("---END A2UI JSON SCHEMA---") + + return "\n\n".join(parts) + + def bundle_schemas(self) -> Dict[str, Any]: + """ + Bundles the loaded schemas into a single self-contained schema. + Returns: + A dictionary representing the bundled schema. + """ + bundled = [] + if self.version == "0.8": + bundled = self._bundle_0_8( + self.server_to_client_schema, + self.catalog_schema, + self.common_types_schema, + ) + else: + # Default to 0.9+ behavior (using $defs) + bundled = self._bundle_0_9( + self.server_to_client_schema, + self.catalog_schema, + self.common_types_schema, + ) + # LLM is instructed to generate a list of messages, so we wrap the bundled schema in an array. + return { + "type": "array", + "items": bundled, + } + + def _inject_additional_properties( + self, + schema: Dict[str, Any], + source_properties: Dict[str, Any], + mapping: Dict[str, str] = None, + ) -> Tuple[Dict[str, Any], Set[str]]: + """ + Recursively injects properties from source_properties into nodes with additionalProperties=True. + Args: + schema: The target schema to traverse and patch. + source_properties: A dictionary of top-level property groups (e.g., "components", "styles") from the source schema. + Returns: + A tuple containing: + - The patched schema. + - A set of keys from source_properties that were injected. + """ + injected_keys = set() + + def recursive_inject(obj): + if isinstance(obj, dict): + new_obj = {} + for k, v in obj.items(): + if isinstance(v, dict) and v.get("additionalProperties") is True: + if k in source_properties: + injected_keys.add(k) + new_node = dict(v) + new_node["additionalProperties"] = False + new_node["properties"] = { + **new_node.get("properties", {}), + **source_properties[k], + } + new_obj[k] = new_node + else: # No matching source group, keep as is but recurse children + new_obj[k] = recursive_inject(v) + else: # Not a node with additionalProperties, recurse children + new_obj[k] = recursive_inject(v) + return new_obj + elif isinstance(obj, list): + return [recursive_inject(i) for i in obj] + return obj + + return recursive_inject(schema), injected_keys + + def _bundle_0_9( + self, + server_to_client: Dict[str, Any], + catalog: Dict[str, Any], + common: Dict[str, Any] = None, + ) -> Dict[str, Any]: + if not server_to_client: + return {} + + bundled = copy.deepcopy(server_to_client) + + # Collect source properties and merge schemas + source_properties = {} + schemas_to_merge = [] + # merged in order so catalog overrides common. + if common: + schemas_to_merge.append(common) + if catalog: + schemas_to_merge.append(catalog) + + if "$defs" not in bundled: + bundled["$defs"] = {} + + for schema in schemas_to_merge: + source_properties.update(schema) + + # Merge $defs + if "$defs" in schema: + bundled["$defs"].update(schema["$defs"]) + + # Merge other top-level properties + for k, v in schema.items(): + if k not in ["$schema", "title", "$id", "description", "$defs"]: + bundled[k] = v + + bundled["$id"] = "https://a2ui.dev/specification/0.9/server_to_client_bundled.json" + bundled["title"] = "Bundled A2UI Message Schema" + bundled["description"] = ( + "A self-contained bundled schema including server messages, catalog components," + " and common types." + ) + + bundled, injected_keys = self._inject_additional_properties( + bundled, source_properties + ) + + # Cleanup injected keys from bundled root to avoid duplication + for k in injected_keys: + if k in bundled: + del bundled[k] + + # Recursively strip external file references + def rewrite_refs(obj): + if isinstance(obj, dict): + new_obj = {} + for k, v in obj.items(): + if k == "$ref" and isinstance(v, str): + if ".json" in v: + ref_parts = v.split(".json") + fragment = ref_parts[-1] + if not fragment: + fragment = "#" + new_obj[k] = fragment + else: + new_obj[k] = v + else: + new_obj[k] = rewrite_refs(v) + return new_obj + elif isinstance(obj, list): + return [rewrite_refs(i) for i in obj] + return obj + + return rewrite_refs(bundled) + + def _bundle_0_8( + self, + server_to_client: Dict[str, Any], + catalog: Dict[str, Any], + common: Dict[str, Any] = None, + ) -> Dict[str, Any]: + if not server_to_client: + return {} + + bundled = copy.deepcopy(server_to_client) + + # Prepare catalog components and styles for injection + source_properties = {} + if catalog: + if CATALOG_COMPONENTS_KEY in catalog: + # Special mapping for v0.8: "components" -> "component" + source_properties["component"] = catalog[CATALOG_COMPONENTS_KEY] + if CATALOG_STYLES_KEY in catalog: + source_properties[CATALOG_STYLES_KEY] = catalog[CATALOG_STYLES_KEY] + + bundled, _ = self._inject_additional_properties(bundled, source_properties) + + return bundled + + +def find_repo_root(start_path: str) -> str | None: + """Finds the repository root by looking for the 'specification' directory.""" + current = os.path.abspath(start_path) + while True: + if os.path.isdir(os.path.join(current, SPECIFICATION_DIR)): + return current + parent = os.path.dirname(current) + if parent == current: + return None + current = parent diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/__init__.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/__init__.py new file mode 100644 index 000000000..4df27a209 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/manager.py b/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/manager.py new file mode 100644 index 000000000..e64a6c8d3 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/src/a2ui/inference/template/manager.py @@ -0,0 +1,30 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..inference_strategy import InferenceStrategy +from typing import List + + +class A2uiTemplateManager(InferenceStrategy): + + def generate_system_prompt( + self, + role_description: str, + workflow_description: str = "", + ui_description: str = "", + selected_components: List[str] = [], + examples: str = "", + ) -> str: + # TODO: Implementation logic for Template Manager + return "" diff --git a/a2a_agents/python/a2ui_agent/tests/core/test_validator.py b/a2a_agents/python/a2ui_agent/tests/core/test_validator.py new file mode 100644 index 000000000..d1b353895 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/tests/core/test_validator.py @@ -0,0 +1,44 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +import jsonschema +from a2ui.core.validator import A2uiValidator + + +@pytest.fixture +def mock_bundled_schema(): + return { + "type": "array", + "items": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {"foo": {"type": "string"}}, + "required": ["foo"], + }, + } + + +def test_validation_success(mock_bundled_schema): + validator = A2uiValidator(mock_bundled_schema) + instance = [{"foo": "bar"}] + validator.validate(instance) + + +def test_validation_failure(mock_bundled_schema): + validator = A2uiValidator(mock_bundled_schema) + instance = [{"foo": 123}] # Wrong type + + with pytest.raises(jsonschema.exceptions.ValidationError): + validator.validate(instance) diff --git a/a2a_agents/python/a2ui_agent/tests/inference/test_schema_bundling.py b/a2a_agents/python/a2ui_agent/tests/inference/test_schema_bundling.py new file mode 100644 index 000000000..0dbc72e52 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/tests/inference/test_schema_bundling.py @@ -0,0 +1,310 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import pytest +from unittest.mock import MagicMock +from a2ui.inference.schema.manager import A2uiSchemaManager + + +class TestSchemaBundling: + + @pytest.fixture + def manager_0_9(self): + m = A2uiSchemaManager("0.9") + m.server_to_client_schema = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "A2UI Message Schema", + "oneOf": [ + {"$ref": "#/$defs/CreateSurfaceMessage"}, + {"$ref": "#/$defs/UpdateComponentsMessage"}, + ], + "$defs": { + "CreateSurfaceMessage": { + "type": "object", + "properties": { + "createSurface": { + "type": "object", + "properties": { + "surfaceId": { + "type": "string", + }, + "theme": {"type": "object", "additionalProperties": True}, + }, + "required": ["surfaceId"], + "additionalProperties": False, + } + }, + "required": ["createSurface"], + "additionalProperties": False, + }, + "UpdateComponentsMessage": { + "type": "object", + "properties": { + "updateComponents": { + "type": "object", + "properties": { + "surfaceId": { + "type": "string", + }, + "components": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "standard_catalog.json#/$defs/anyComponent" + }, + }, + }, + "required": ["surfaceId", "components"], + "additionalProperties": False, + } + }, + "required": ["updateComponents"], + "additionalProperties": False, + }, + "UpdateDataModelMessage": { + "type": "object", + "properties": { + "updateDataModel": { + "type": "object", + "properties": { + "surfaceId": { + "type": "string", + }, + "value": {"additionalProperties": True}, + }, + "required": ["surfaceId"], + "additionalProperties": False, + } + }, + "required": ["updateDataModel"], + "additionalProperties": False, + }, + }, + } + m.catalog_schema = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "A2UI Standard Catalog", + "catalogId": "https://a2ui.dev/specification/v0_9/standard_catalog.json", + "components": { + "Text": { + "type": "object", + "allOf": [ + {"$ref": "common_types.json#/$defs/ComponentCommon"}, + {"$ref": "#/$defs/CatalogComponentCommon"}, + { + "type": "object", + "properties": { + "component": {"const": "Text"}, + "text": {"$ref": "common_types.json#/$defs/DynamicString"}, + "variant": { + "type": "string", + "enum": [ + "h1", + "h2", + "h3", + "h4", + "h5", + "caption", + "body", + ], + }, + }, + "required": ["component", "text"], + }, + ], + }, + "Image": {}, + "Icon": {}, + }, + "theme": {"primaryColor": {"type": "string"}, "iconUrl": {"type": "string"}}, + "$defs": { + "CatalogComponentCommon": { + "type": "object", + "properties": {"weight": {"type": "number"}}, + }, + "anyComponent": { + "oneOf": [ + {"$ref": "#/components/Text"}, + {"$ref": "#/components/Image"}, + {"$ref": "#/components/Icon"}, + ], + "discriminator": {"propertyName": "component"}, + }, + }, + } + m.common_types_schema = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "A2UI Common Types", + "$defs": { + "ComponentId": { + "type": "string", + }, + "AccessibilityAttributes": { + "type": "object", + "properties": { + "label": { + "$ref": "#/$defs/DynamicString", + } + }, + }, + "ComponentCommon": { + "type": "object", + "properties": {"id": {"$ref": "#/$defs/ComponentId"}}, + "required": ["id"], + }, + "DataBinding": {"type": "object"}, + "DynamicString": { + "oneOf": [{"type": "string"}, {"$ref": "#/$defs/DataBinding"}] + }, + }, + } + return m + + @pytest.fixture + def manager_0_8(self): + m = A2uiSchemaManager("0.8") + m.server_to_client_schema = { + "title": "A2UI Message Schema", + "description": "Describes a JSON payload for an A2UI message.", + "type": "object", + "additionalProperties": False, + "properties": { + "beginRendering": { + "type": "object", + "additionalProperties": False, + "properties": { + "surfaceId": {"type": "string"}, + "styles": { + "type": "object", + "description": "Styling information for the UI.", + "additionalProperties": True, + }, + }, + "required": ["surfaceId"], + }, + "surfaceUpdate": { + "type": "object", + "additionalProperties": False, + "properties": { + "surfaceId": { + "type": "string", + }, + "components": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": False, + "properties": { + "id": { + "type": "string", + }, + "component": { + "type": "object", + "description": "A wrapper object.", + "additionalProperties": True, + }, + }, + "required": ["id", "component"], + }, + }, + }, + }, + "required": ["surfaceId", "components"], + }, + } + m.catalog_schema = { + "components": {"Text": {"type": "object"}, "Button": {"type": "object"}}, + "styles": {"font": {"type": "string"}, "primaryColor": {"type": "string"}}, + } + return m + + def test_bundle_0_9(self, manager_0_9): + bundled_wrapped = manager_0_9.bundle_schemas() + bundled = bundled_wrapped["items"] + + assert bundled["$schema"] == "https://json-schema.org/draft/2020-12/schema" + assert bundled["title"] == "Bundled A2UI Message Schema" + assert ( + bundled["description"] + == "A self-contained bundled schema including server messages, catalog" + " components, and common types." + ) + assert ( + bundled["$id"] + == "https://a2ui.dev/specification/0.9/server_to_client_bundled.json" + ) + + assert "components" in bundled + assert "Text" in bundled["components"] + + # Verify dynamic cleanup: theme should be removed from root as it was injected + assert "theme" not in bundled + + # Verify definitions are merged + assert "CreateSurfaceMessage" in bundled["$defs"] + assert "UpdateComponentsMessage" in bundled["$defs"] + assert "CatalogComponentCommon" in bundled["$defs"] + assert "DynamicString" in bundled["$defs"] + assert "DataBinding" in bundled["$defs"] + assert "ComponentId" in bundled["$defs"] + assert "AccessibilityAttributes" in bundled["$defs"] + assert "ComponentCommon" in bundled["$defs"] + + # Check injection in CreateSurfaceMessage -> theme + create_surface = bundled["$defs"]["CreateSurfaceMessage"]["properties"][ + "createSurface" + ] + theme_node = create_surface["properties"]["theme"] + assert theme_node["additionalProperties"] is False + assert "primaryColor" in theme_node["properties"] + assert "iconUrl" in theme_node["properties"] + + # Check injection in UpdateDataModelMessage -> updateDataModel -> value + update_data_model = bundled["$defs"]["UpdateDataModelMessage"]["properties"][ + "updateDataModel" + ] + value_node = update_data_model["properties"]["value"] + assert value_node["additionalProperties"] is True + + def test_bundle_0_8(self, manager_0_8): + bundled_wrapped = manager_0_8.bundle_schemas() + bundled = bundled_wrapped["items"] + + # Verify styles injection + styles_node = bundled["properties"]["beginRendering"]["properties"]["styles"] + assert styles_node["additionalProperties"] is False + assert "font" in styles_node["properties"] + assert "primaryColor" in styles_node["properties"] + + # Verify component injection + component_node = bundled["properties"]["surfaceUpdate"]["properties"]["components"][ + "items" + ]["properties"]["component"] + assert component_node["additionalProperties"] is False + assert "Text" in component_node["properties"] + assert "Button" in component_node["properties"] + + def test_bundle_external_file_stripping(self): + m = A2uiSchemaManager("0.9") + m.server_to_client_schema = { + "$defs": {"A": {"$ref": "other.json#/$defs/B"}, "X": {"$ref": "other.json"}} + } + m.catalog_schema = {} + m.common_types_schema = {} + + bundled_wrapped = m.bundle_schemas() + bundled = bundled_wrapped["items"] + assert bundled["$defs"]["A"]["$ref"] == "#/$defs/B" + assert bundled["$defs"]["X"]["$ref"] == "#" diff --git a/a2a_agents/python/a2ui_agent/tests/inference/test_schema_manager.py b/a2a_agents/python/a2ui_agent/tests/inference/test_schema_manager.py new file mode 100644 index 000000000..3cd35cd8b --- /dev/null +++ b/a2a_agents/python/a2ui_agent/tests/inference/test_schema_manager.py @@ -0,0 +1,289 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import pytest +import json +from unittest.mock import patch, MagicMock +from a2ui.inference.schema.manager import A2uiSchemaManager + +test_version = "0.8" + + +@pytest.fixture +def mock_importlib_resources(): + with patch("importlib.resources.files") as mock_files: + yield mock_files + + +def test_schema_manager_init_valid_version(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + + def files_side_effect(package): + if package == f"a2ui.assets.{test_version}": + return mock_traversable + return MagicMock() + + mock_files.side_effect = files_side_effect + + # Mock file open calls for server_to_client and catalog + def joinpath_side_effect(path): + mock_file = MagicMock() + if path == "server_to_client.json": + content = '{"version": "0.8", "defs": "server_defs"}' + elif path == "standard_catalog_definition.json": + content = '{"version": "0.8", "components": {"Text": {}}}' + else: + content = "{}" + + mock_file.open.return_value.__enter__.return_value = io.StringIO(content) + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + manager = A2uiSchemaManager(test_version) + + assert manager.server_to_client_schema["defs"] == "server_defs" + assert manager.catalog_schema["version"] == test_version + assert "Text" in manager.catalog_schema["components"] + + +def test_schema_manager_fallback_local_assets(mock_importlib_resources): + # Force importlib to fail + mock_importlib_resources.side_effect = FileNotFoundError("Package not found") + + with ( + patch("os.path.exists") as mock_exists, + patch("builtins.open", new_callable=MagicMock) as mock_open, + ): + + def open_side_effect(path, *args, **kwargs): + path_str = str(path) + if "server_to_client.json" in path_str: + return io.StringIO('{"defs": "local_server"}') + elif "standard_catalog_definition.json" in path_str: + return io.StringIO('{"components": {"LocalText": {}}}') + raise FileNotFoundError(path) + + mock_open.side_effect = open_side_effect + + manager = A2uiSchemaManager(test_version) + + assert manager.server_to_client_schema["defs"] == "local_server" + assert "LocalText" in manager.catalog_schema["components"] + + +def test_schema_manager_init_invalid_version(): + with pytest.raises(ValueError, match="Unknown A2UI specification version"): + A2uiSchemaManager("invalid_version") + + +def test_schema_manager_init_custom_catalog(tmp_path, mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + if path == "server_to_client.json": + mock_file.open.return_value.__enter__.return_value = io.StringIO("{}") + else: + mock_file.open.return_value.__enter__.return_value = io.StringIO("{}") + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + d = tmp_path / "custom_catalog.json" + d.write_text('{"components": {"Custom": {}}}', encoding="utf-8") + + manager = A2uiSchemaManager(test_version, custom_catalog_path=str(d)) + + assert "Custom" in manager.catalog_schema["components"] + + +def test_get_pruned_catalog(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + if path == "standard_catalog_definition.json": + content = json.dumps({"components": {"Text": {}, "Button": {}, "Image": {}}}) + else: + content = "{}" + mock_file.open.return_value.__enter__.return_value = io.StringIO(content) + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + manager = A2uiSchemaManager(test_version) + + # Test with explicit selection + pruned = manager.get_pruned_catalog(["Text", "Button"]) + assert "Text" in pruned["components"] + assert "Button" in pruned["components"] + assert "Image" not in pruned["components"] + + # Test with None (default) + full_catalog_none = manager.get_pruned_catalog() + assert "Text" in full_catalog_none["components"] + assert "Button" in full_catalog_none["components"] + assert "Image" in full_catalog_none["components"] + + # Test with empty list + full_catalog_empty = manager.get_pruned_catalog([]) + assert "Text" in full_catalog_empty["components"] + assert "Button" in full_catalog_empty["components"] + assert "Image" in full_catalog_empty["components"] + + +def test_get_pruned_catalog_filters_any_component(mock_importlib_resources): + manager = A2uiSchemaManager("0.8") + manager.catalog_schema = { + "version": "0.8", + "$defs": { + "anyComponent": { + "oneOf": [ + {"$ref": "#/components/Text"}, + {"$ref": "#/components/Button"}, + {"$ref": "#/components/Image"}, + ] + } + }, + "components": {"Text": {}, "Button": {}, "Image": {}}, + } + + selected = ["Text"] + pruned = manager.get_pruned_catalog(selected) + + assert "Text" in pruned["components"] + assert "Button" not in pruned["components"] + assert "Image" not in pruned["components"] + + if "$defs" in pruned and "anyComponent" in pruned["$defs"]: + any_comp_one_of = pruned["$defs"]["anyComponent"]["oneOf"] + assert len(any_comp_one_of) == 1 + assert any_comp_one_of[0]["$ref"] == "#/components/Text" + + +def test_generate_system_prompt(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + if path == "server_to_client.json": + content = '{"type": "server_schema"}' + elif path == "standard_catalog_definition.json": + content = '{"components": {"Text": {}}}' + else: + content = "{}" + mock_file.open.return_value.__enter__.return_value = io.StringIO(content) + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + manager = A2uiSchemaManager("0.8") + prompt = manager.generate_system_prompt( + "You are a helpful assistant.", + "Manage workflow.", + "Render UI.", + ["Text"], + ) + + assert "You are a helpful assistant." in prompt + assert "## Workflow Description:" in prompt + assert "Manage workflow." in prompt + assert "## UI Description:" in prompt + assert "Render UI." in prompt + assert "---BEGIN A2UI JSON SCHEMA---" in prompt + assert "### Server To Client Schema:" in prompt + assert '"type": "server_schema"' in prompt + assert "### Catalog Schema:" in prompt + assert "---END A2UI JSON SCHEMA---" in prompt + assert '"Text":{}' in prompt.replace(" ", "") + + +def test_generate_system_prompt_with_examples(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + mock_file.open.return_value.__enter__.return_value = io.StringIO("{}") + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + manager = A2uiSchemaManager("0.8") + + # Test with examples + examples = json.dumps([{"description": "example1", "code": "..."}]) + prompt = manager.generate_system_prompt("Role description", examples=examples) + assert "## Examples:" in prompt + assert '"example1"' in prompt + + # Test without examples + prompt_no_examples = manager.generate_system_prompt("Role description") + assert "## Examples:" not in prompt_no_examples + + +def test_generate_system_prompt_v0_9_common_types(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + + def joinpath_side_effect(path): + mock_file = MagicMock() + content = "{}" + if path == "common_types.json": + content = '{"types": {"Common": {}}}' + elif path == "server_to_client.json": + content = '{"type": "server_schema"}' + + mock_file.open.return_value.__enter__.return_value = io.StringIO(content) + return mock_file + + mock_traversable.joinpath.side_effect = joinpath_side_effect + + # Initialize with version 0.9 which expects common types + manager = A2uiSchemaManager("0.9") + + prompt = manager.generate_system_prompt("Role") + + assert "### Common Types Schema:" in prompt + assert '"types":{"Common":{}}' in prompt.replace(" ", "").replace("\n", "") + + +def test_generate_system_prompt_minimal_args(mock_importlib_resources): + mock_files = mock_importlib_resources + mock_traversable = MagicMock() + mock_files.return_value = mock_traversable + mock_traversable.joinpath.return_value.open.return_value.__enter__.return_value = ( + io.StringIO("{}") + ) + + manager = A2uiSchemaManager("0.8") + prompt = manager.generate_system_prompt("Just Role") + + # Check that optional sections are missing + assert "## Workflow Description:" not in prompt + assert "## UI Description:" not in prompt + assert "## Examples:" not in prompt + assert "Just Role" in prompt + assert "---BEGIN A2UI JSON SCHEMA---" in prompt diff --git a/a2a_agents/python/a2ui_agent/tests/integration/verify_load_real.py b/a2a_agents/python/a2ui_agent/tests/integration/verify_load_real.py new file mode 100644 index 000000000..a40f67ee0 --- /dev/null +++ b/a2a_agents/python/a2ui_agent/tests/integration/verify_load_real.py @@ -0,0 +1,599 @@ +import sys + +from a2ui.inference.schema.manager import A2uiSchemaManager + + +def verify(): + print('Verifying A2uiSchemaManager...') + try: + manager = A2uiSchemaManager('0.8') + print( + f"Successfully loaded 0.8: {len(manager.catalog_schema.get('components', {}))}" + ' components' + ) + print( + 'Components found:' + f" {list(manager.catalog_schema.get('components', {}).keys())[:5]}..." + ) + + a2ui_message = [ + {'beginRendering': {'surfaceId': 'contact-card', 'root': 'main_card'}}, + { + 'surfaceUpdate': { + 'surfaceId': 'contact-card', + 'components': [ + { + 'id': 'profile_image', + 'component': { + 'Image': { + 'url': {'path': 'imageUrl'}, + 'usageHint': 'avatar', + 'fit': 'cover', + } + }, + }, + { + 'id': 'user_heading', + 'weight': 1, + 'component': { + 'Text': {'text': {'path': 'name'}, 'usageHint': 'h2'} + }, + }, + { + 'id': 'description_text_1', + 'component': {'Text': {'text': {'path': 'title'}}}, + }, + { + 'id': 'description_text_2', + 'component': {'Text': {'text': {'path': 'team'}}}, + }, + { + 'id': 'description_column', + 'component': { + 'Column': { + 'children': { + 'explicitList': [ + 'user_heading', + 'description_text_1', + 'description_text_2', + ] + }, + 'alignment': 'center', + } + }, + }, + { + 'id': 'calendar_icon', + 'component': { + 'Icon': {'name': {'literalString': 'calendarToday'}} + }, + }, + { + 'id': 'calendar_primary_text', + 'component': { + 'Text': {'usageHint': 'h5', 'text': {'path': 'calendar'}} + }, + }, + { + 'id': 'calendar_secondary_text', + 'component': {'Text': {'text': {'literalString': 'Calendar'}}}, + }, + { + 'id': 'calendar_text_column', + 'component': { + 'Column': { + 'children': { + 'explicitList': [ + 'calendar_primary_text', + 'calendar_secondary_text', + ] + }, + 'distribution': 'start', + 'alignment': 'start', + } + }, + }, + { + 'id': 'info_row_1', + 'component': { + 'Row': { + 'children': { + 'explicitList': [ + 'calendar_icon', + 'calendar_text_column', + ] + }, + 'distribution': 'start', + 'alignment': 'start', + } + }, + }, + { + 'id': 'location_icon', + 'component': { + 'Icon': {'name': {'literalString': 'locationOn'}} + }, + }, + { + 'id': 'location_primary_text', + 'component': { + 'Text': {'usageHint': 'h5', 'text': {'path': 'location'}} + }, + }, + { + 'id': 'location_secondary_text', + 'component': {'Text': {'text': {'literalString': 'Location'}}}, + }, + { + 'id': 'location_text_column', + 'component': { + 'Column': { + 'children': { + 'explicitList': [ + 'location_primary_text', + 'location_secondary_text', + ] + }, + 'distribution': 'start', + 'alignment': 'start', + } + }, + }, + { + 'id': 'info_row_2', + 'component': { + 'Row': { + 'children': { + 'explicitList': [ + 'location_icon', + 'location_text_column', + ] + }, + 'distribution': 'start', + 'alignment': 'start', + } + }, + }, + { + 'id': 'mail_icon', + 'component': {'Icon': {'name': {'literalString': 'mail'}}}, + }, + { + 'id': 'mail_primary_text', + 'component': { + 'Text': {'usageHint': 'h5', 'text': {'path': 'email'}} + }, + }, + { + 'id': 'mail_secondary_text', + 'component': {'Text': {'text': {'literalString': 'Email'}}}, + }, + { + 'id': 'mail_text_column', + 'component': { + 'Column': { + 'children': { + 'explicitList': [ + 'mail_primary_text', + 'mail_secondary_text', + ] + }, + 'distribution': 'start', + 'alignment': 'start', + } + }, + }, + { + 'id': 'info_row_3', + 'component': { + 'Row': { + 'children': { + 'explicitList': ['mail_icon', 'mail_text_column'] + }, + 'distribution': 'start', + 'alignment': 'start', + } + }, + }, + {'id': 'div', 'component': {'Divider': {}}}, + { + 'id': 'call_icon', + 'component': {'Icon': {'name': {'literalString': 'call'}}}, + }, + { + 'id': 'call_primary_text', + 'component': { + 'Text': {'usageHint': 'h5', 'text': {'path': 'mobile'}} + }, + }, + { + 'id': 'call_secondary_text', + 'component': {'Text': {'text': {'literalString': 'Mobile'}}}, + }, + { + 'id': 'call_text_column', + 'component': { + 'Column': { + 'children': { + 'explicitList': [ + 'call_primary_text', + 'call_secondary_text', + ] + }, + 'distribution': 'start', + 'alignment': 'start', + } + }, + }, + { + 'id': 'info_row_4', + 'component': { + 'Row': { + 'children': { + 'explicitList': ['call_icon', 'call_text_column'] + }, + 'distribution': 'start', + 'alignment': 'start', + } + }, + }, + { + 'id': 'info_rows_column', + 'weight': 1, + 'component': { + 'Column': { + 'children': { + 'explicitList': [ + 'info_row_1', + 'info_row_2', + 'info_row_3', + 'info_row_4', + ] + }, + 'alignment': 'stretch', + } + }, + }, + { + 'id': 'button_1_text', + 'component': {'Text': {'text': {'literalString': 'Follow'}}}, + }, + { + 'id': 'button_1', + 'component': { + 'Button': { + 'child': 'button_1_text', + 'primary': True, + 'action': {'name': 'follow_contact'}, + } + }, + }, + { + 'id': 'button_2_text', + 'component': {'Text': {'text': {'literalString': 'Message'}}}, + }, + { + 'id': 'button_2', + 'component': { + 'Button': { + 'child': 'button_2_text', + 'primary': False, + 'action': {'name': 'send_message'}, + } + }, + }, + { + 'id': 'action_buttons_row', + 'component': { + 'Row': { + 'children': {'explicitList': ['button_1', 'button_2']}, + 'distribution': 'center', + 'alignment': 'center', + } + }, + }, + { + 'id': 'link_text', + 'component': { + 'Text': { + 'text': { + 'literalString': '[View Full Profile](/profile)' + } + } + }, + }, + { + 'id': 'link_text_wrapper', + 'component': { + 'Row': { + 'children': {'explicitList': ['link_text']}, + 'distribution': 'center', + 'alignment': 'center', + } + }, + }, + { + 'id': 'main_column', + 'component': { + 'Column': { + 'children': { + 'explicitList': [ + 'profile_image', + 'description_column', + 'div', + 'info_rows_column', + 'action_buttons_row', + 'link_text_wrapper', + ] + }, + 'alignment': 'stretch', + } + }, + }, + { + 'id': 'main_card', + 'component': {'Card': {'child': 'main_column'}}, + }, + ], + } + }, + { + 'dataModelUpdate': { + 'surfaceId': 'contact-card', + 'path': '/', + 'contents': [ + {'key': 'name', 'valueString': 'Casey Smith'}, + {'key': 'title', 'valueString': 'Digital Marketing Specialist'}, + {'key': 'team', 'valueString': 'Growth Team'}, + {'key': 'location', 'valueString': 'New York'}, + {'key': 'email', 'valueString': 'casey.smith@example.com'}, + {'key': 'mobile', 'valueString': '+1 (415) 222-3333'}, + {'key': 'calendar', 'valueString': 'In a meeting'}, + { + 'key': 'imageUrl', + 'valueString': 'http://localhost:10003/static/profile2.png', + }, + ], + } + }, + ] + manager.validator.validate(a2ui_message) + print('Validation successful') + + except Exception as e: + print(f'Failed to load 0.8: {e}') + sys.exit(1) + + try: + manager = A2uiSchemaManager('0.9') + print( + f"Successfully loaded 0.9: {len(manager.catalog_schema.get('components', {}))}" + ' components' + ) + print( + 'Components found:' + f" {list(manager.catalog_schema.get('components', {}).keys())}..." + ) + a2ui_message = [ + { + 'createSurface': { + 'surfaceId': 'contact_form_1', + 'catalogId': ( + 'https://a2ui.dev/specification/v0_9/standard_catalog.json' + ), + } + }, + { + 'updateComponents': { + 'surfaceId': 'contact_form_1', + 'components': [ + {'id': 'root', 'component': 'Card', 'child': 'form_container'}, + { + 'id': 'form_container', + 'component': 'Column', + 'children': [ + 'header_row', + 'name_row', + 'email_group', + 'phone_group', + 'pref_group', + 'divider_1', + 'newsletter_checkbox', + 'submit_button', + ], + 'justify': 'start', + 'align': 'stretch', + }, + { + 'id': 'header_row', + 'component': 'Row', + 'children': ['header_icon', 'header_text'], + 'align': 'center', + }, + {'id': 'header_icon', 'component': 'Icon', 'name': 'mail'}, + { + 'id': 'header_text', + 'component': 'Text', + 'text': '# Contact Us', + 'variant': 'h2', + }, + { + 'id': 'name_row', + 'component': 'Row', + 'children': ['first_name_group', 'last_name_group'], + 'justify': 'spaceBetween', + }, + { + 'id': 'first_name_group', + 'component': 'Column', + 'children': ['first_name_label', 'first_name_field'], + 'weight': 1, + }, + { + 'id': 'first_name_label', + 'component': 'Text', + 'text': 'First Name', + 'variant': 'caption', + }, + { + 'id': 'first_name_field', + 'component': 'TextField', + 'label': 'First Name', + 'value': {'path': '/contact/firstName'}, + 'variant': 'shortText', + }, + { + 'id': 'last_name_group', + 'component': 'Column', + 'children': ['last_name_label', 'last_name_field'], + 'weight': 1, + }, + { + 'id': 'last_name_label', + 'component': 'Text', + 'text': 'Last Name', + 'variant': 'caption', + }, + { + 'id': 'last_name_field', + 'component': 'TextField', + 'label': 'Last Name', + 'value': {'path': '/contact/lastName'}, + 'variant': 'shortText', + }, + { + 'id': 'email_group', + 'component': 'Column', + 'children': ['email_label', 'email_field'], + }, + { + 'id': 'email_label', + 'component': 'Text', + 'text': 'Email Address', + 'variant': 'caption', + }, + { + 'id': 'email_field', + 'component': 'TextField', + 'label': 'Email', + 'value': {'path': '/contact/email'}, + 'variant': 'shortText', + 'checks': [ + { + 'call': 'required', + 'args': [{'path': '/contact/email'}], + 'message': 'Email is required.', + }, + { + 'call': 'email', + 'args': [{'path': '/contact/email'}], + 'message': 'Please enter a valid email address.', + }, + ], + }, + { + 'id': 'phone_group', + 'component': 'Column', + 'children': ['phone_label', 'phone_field'], + }, + { + 'id': 'phone_label', + 'component': 'Text', + 'text': 'Phone Number', + 'variant': 'caption', + }, + { + 'id': 'phone_field', + 'component': 'TextField', + 'label': 'Phone', + 'value': {'path': '/contact/phone'}, + 'variant': 'shortText', + 'checks': [{ + 'call': 'regex', + 'args': [{'path': '/contact/phone'}, '^\\d{10}$'], + 'message': 'Phone number must be 10 digits.', + }], + }, + { + 'id': 'pref_group', + 'component': 'Column', + 'children': ['pref_label', 'pref_picker'], + }, + { + 'id': 'pref_label', + 'component': 'Text', + 'text': 'Preferred Contact Method', + 'variant': 'caption', + }, + { + 'id': 'pref_picker', + 'component': 'ChoicePicker', + 'variant': 'mutuallyExclusive', + 'options': [ + {'label': 'Email', 'value': 'email'}, + {'label': 'Phone', 'value': 'phone'}, + {'label': 'SMS', 'value': 'sms'}, + ], + 'value': {'path': '/contact/preference'}, + }, + {'id': 'divider_1', 'component': 'Divider', 'axis': 'horizontal'}, + { + 'id': 'newsletter_checkbox', + 'component': 'CheckBox', + 'label': 'Subscribe to our newsletter', + 'value': {'path': '/contact/subscribe'}, + }, + { + 'id': 'submit_button_label', + 'component': 'Text', + 'text': 'Send Message', + }, + { + 'id': 'submit_button', + 'component': 'Button', + 'child': 'submit_button_label', + 'variant': 'primary', + 'action': { + 'event': { + 'name': 'submitContactForm', + 'context': { + 'formId': 'contact_form_1', + 'clientTime': { + 'call': 'now', + 'returnType': 'string', + }, + 'isNewsletterSubscribed': { + 'path': '/contact/subscribe' + }, + }, + } + }, + }, + ], + } + }, + { + 'updateDataModel': { + 'surfaceId': 'contact_form_1', + 'path': '/contact', + 'value': { + 'firstName': 'John', + 'lastName': 'Doe', + 'email': 'john.doe@example.com', + 'phone': '1234567890', + 'preference': ['email'], + 'subscribe': True, + }, + } + }, + {'deleteSurface': {'surfaceId': 'contact_form_1'}}, + ] + manager.validator.validate(a2ui_message) + print('Validation successful') + except Exception as e: + print(f'Failed to load 0.9: {e}') + sys.exit(1) + + +if __name__ == '__main__': + verify() diff --git a/a2a_agents/python/a2ui_agent/uv.lock b/a2a_agents/python/a2ui_agent/uv.lock index 3c413688b..62f5effd3 100644 --- a/a2a_agents/python/a2ui_agent/uv.lock +++ b/a2a_agents/python/a2ui_agent/uv.lock @@ -485,7 +485,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -509,7 +509,7 @@ wheels = [ [[package]] name = "google-adk" -version = "1.22.1" +version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiosqlite" }, @@ -556,9 +556,9 @@ dependencies = [ { name = "watchdog" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/7e/1fe2704d8079d93bffb5888aa2bd1081251855e8bf14d97f648abd9fd7fa/google_adk-1.22.1.tar.gz", hash = "sha256:4590df0a9340cf05cf5a9899986dfcc3db1c624c6165d76c04be16de535e6404", size = 2046783, upload-time = "2026-01-12T20:50:08.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/25/a8c7058812ae3a6046c1c909da31b4c95a6534f555ec50730fe215b2592c/google_adk-1.23.0.tar.gz", hash = "sha256:07829b3198d412ecddb8b102c6bc9511607a234989b7659be102d806e4c92258", size = 2072294, upload-time = "2026-01-22T23:26:52.352Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/ea/5fee720ca26eff38e338ff0dc1eae4abd06fc712b841333689f5caf4c55f/google_adk-1.22.1-py3-none-any.whl", hash = "sha256:65c921a1343220eb7823ec8972479c046e7d9464f17c0829fb5508551678a9ef", size = 2368855, upload-time = "2026-01-12T20:50:06.087Z" }, + { url = "https://files.pythonhosted.org/packages/60/36/2abbcaad2bd4691863ac05189070c1e9f8d12ec16194f41a975c984883af/google_adk-1.23.0-py3-none-any.whl", hash = "sha256:94b77c9afa39042e77a35c2b3dad7e122d940e065cb5a9ba9e7b5de73786f993", size = 2418796, upload-time = "2026-01-22T23:26:50.289Z" }, ] [[package]] @@ -994,7 +994,7 @@ wheels = [ [[package]] name = "google-genai" -version = "1.59.0" +version = "1.60.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1008,9 +1008,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/34/c03bcbc759d67ac3d96077838cdc1eac85417de6ea3b65b313fe53043eee/google_genai-1.59.0.tar.gz", hash = "sha256:0b7a2dc24582850ae57294209d8dfc2c4f5fcfde0a3f11d81dc5aca75fb619e2", size = 487374, upload-time = "2026-01-15T20:29:46.619Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/3f/a753be0dcee352b7d63bc6d1ba14a72591d63b6391dac0cdff7ac168c530/google_genai-1.60.0.tar.gz", hash = "sha256:9768061775fddfaecfefb0d6d7a6cabefb3952ebd246cd5f65247151c07d33d1", size = 487721, upload-time = "2026-01-21T22:17:30.398Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/53/6d00692fe50d73409b3406ae90c71bc4499c8ae7fac377ba16e283da917c/google_genai-1.59.0-py3-none-any.whl", hash = "sha256:59fc01a225d074fe9d1e626c3433da292f33249dadce4deb34edea698305a6df", size = 719099, upload-time = "2026-01-15T20:29:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/31/e5/384b1f383917b5f0ae92e28f47bc27b16e3d26cd9bacb25e9f8ecab3c8fe/google_genai-1.60.0-py3-none-any.whl", hash = "sha256:967338378ffecebec19a8ed90cf8797b26818bacbefd7846a9280beb1099f7f3", size = 719431, upload-time = "2026-01-21T22:17:28.086Z" }, ] [[package]] @@ -1703,11 +1703,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -1844,11 +1844,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -2043,11 +2043,11 @@ crypto = [ [[package]] name = "pyparsing" -version = "3.3.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] @@ -2420,51 +2420,58 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.45" +version = "2.0.46" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912, upload-time = "2025-12-09T21:05:16.737Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/70/75b1387d72e2847220441166c5eb4e9846dd753895208c13e6d66523b2d9/sqlalchemy-2.0.45-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c64772786d9eee72d4d3784c28f0a636af5b0a29f3fe26ff11f55efe90c0bd85", size = 2154148, upload-time = "2025-12-10T20:03:21.023Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a4/7805e02323c49cb9d1ae5cd4913b28c97103079765f520043f914fca4cb3/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ae64ebf7657395824a19bca98ab10eb9a3ecb026bf09524014f1bb81cb598d4", size = 3233051, upload-time = "2025-12-09T22:06:04.768Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ec/32ae09139f61bef3de3142e85c47abdee8db9a55af2bb438da54a4549263/sqlalchemy-2.0.45-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f02325709d1b1a1489f23a39b318e175a171497374149eae74d612634b234c0", size = 3232781, upload-time = "2025-12-09T22:09:54.435Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/bf7b869b6f5585eac34222e1cf4405f4ba8c3b85dd6b1af5d4ce8bca695f/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2c3684fca8a05f0ac1d9a21c1f4a266983a7ea9180efb80ffeb03861ecd01a0", size = 3182096, upload-time = "2025-12-09T22:06:06.169Z" }, - { url = "https://files.pythonhosted.org/packages/21/6a/c219720a241bb8f35c88815ccc27761f5af7fdef04b987b0e8a2c1a6dcaa/sqlalchemy-2.0.45-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040f6f0545b3b7da6b9317fc3e922c9a98fc7243b2a1b39f78390fc0942f7826", size = 3205109, upload-time = "2025-12-09T22:09:55.969Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c4/6ccf31b2bc925d5d95fab403ffd50d20d7c82b858cf1a4855664ca054dce/sqlalchemy-2.0.45-cp310-cp310-win32.whl", hash = "sha256:830d434d609fe7bfa47c425c445a8b37929f140a7a44cdaf77f6d34df3a7296a", size = 2114240, upload-time = "2025-12-09T21:29:54.007Z" }, - { url = "https://files.pythonhosted.org/packages/de/29/a27a31fca07316def418db6f7c70ab14010506616a2decef1906050a0587/sqlalchemy-2.0.45-cp310-cp310-win_amd64.whl", hash = "sha256:0209d9753671b0da74da2cfbb9ecf9c02f72a759e4b018b3ab35f244c91842c7", size = 2137615, upload-time = "2025-12-09T21:29:55.85Z" }, - { url = "https://files.pythonhosted.org/packages/a2/1c/769552a9d840065137272ebe86ffbb0bc92b0f1e0a68ee5266a225f8cd7b/sqlalchemy-2.0.45-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e90a344c644a4fa871eb01809c32096487928bd2038bf10f3e4515cb688cc56", size = 2153860, upload-time = "2025-12-10T20:03:23.843Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f8/9be54ff620e5b796ca7b44670ef58bc678095d51b0e89d6e3102ea468216/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8c8b41b97fba5f62349aa285654230296829672fc9939cd7f35aab246d1c08b", size = 3309379, upload-time = "2025-12-09T22:06:07.461Z" }, - { url = "https://files.pythonhosted.org/packages/f6/2b/60ce3ee7a5ae172bfcd419ce23259bb874d2cddd44f67c5df3760a1e22f9/sqlalchemy-2.0.45-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12c694ed6468333a090d2f60950e4250b928f457e4962389553d6ba5fe9951ac", size = 3309948, upload-time = "2025-12-09T22:09:57.643Z" }, - { url = "https://files.pythonhosted.org/packages/a3/42/bac8d393f5db550e4e466d03d16daaafd2bad1f74e48c12673fb499a7fc1/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f7d27a1d977a1cfef38a0e2e1ca86f09c4212666ce34e6ae542f3ed0a33bc606", size = 3261239, upload-time = "2025-12-09T22:06:08.879Z" }, - { url = "https://files.pythonhosted.org/packages/6f/12/43dc70a0528c59842b04ea1c1ed176f072a9b383190eb015384dd102fb19/sqlalchemy-2.0.45-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d62e47f5d8a50099b17e2bfc1b0c7d7ecd8ba6b46b1507b58cc4f05eefc3bb1c", size = 3284065, upload-time = "2025-12-09T22:09:59.454Z" }, - { url = "https://files.pythonhosted.org/packages/cf/9c/563049cf761d9a2ec7bc489f7879e9d94e7b590496bea5bbee9ed7b4cc32/sqlalchemy-2.0.45-cp311-cp311-win32.whl", hash = "sha256:3c5f76216e7b85770d5bb5130ddd11ee89f4d52b11783674a662c7dd57018177", size = 2113480, upload-time = "2025-12-09T21:29:57.03Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fa/09d0a11fe9f15c7fa5c7f0dd26be3d235b0c0cbf2f9544f43bc42efc8a24/sqlalchemy-2.0.45-cp311-cp311-win_amd64.whl", hash = "sha256:a15b98adb7f277316f2c276c090259129ee4afca783495e212048daf846654b2", size = 2138407, upload-time = "2025-12-09T21:29:58.556Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760, upload-time = "2025-12-09T22:11:02.66Z" }, - { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268, upload-time = "2025-12-09T22:13:49.054Z" }, - { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144, upload-time = "2025-12-09T22:11:04.14Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907, upload-time = "2025-12-09T22:13:50.598Z" }, - { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182, upload-time = "2025-12-09T21:39:30.824Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200, upload-time = "2025-12-09T21:39:32.321Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082, upload-time = "2025-12-09T22:11:06.167Z" }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131, upload-time = "2025-12-09T22:13:52.626Z" }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389, upload-time = "2025-12-09T22:11:08.093Z" }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054, upload-time = "2025-12-09T22:13:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299, upload-time = "2025-12-09T21:39:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264, upload-time = "2025-12-09T21:39:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998, upload-time = "2025-12-09T22:13:28.622Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434, upload-time = "2025-12-09T22:13:30.188Z" }, - { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404, upload-time = "2025-12-09T22:11:09.662Z" }, - { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057, upload-time = "2025-12-09T22:13:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279, upload-time = "2025-12-09T22:11:11.1Z" }, - { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508, upload-time = "2025-12-09T22:13:57.932Z" }, - { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204, upload-time = "2025-12-09T21:39:38.365Z" }, - { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785, upload-time = "2025-12-09T21:39:39.503Z" }, - { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029, upload-time = "2025-12-09T22:13:32.09Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142, upload-time = "2025-12-09T22:13:33.739Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672, upload-time = "2025-12-09T21:54:52.608Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/26/66ba59328dc25e523bfcb0f8db48bdebe2035e0159d600e1f01c0fc93967/sqlalchemy-2.0.46-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:895296687ad06dc9b11a024cf68e8d9d3943aa0b4964278d2553b86f1b267735", size = 2155051, upload-time = "2026-01-21T18:27:28.965Z" }, + { url = "https://files.pythonhosted.org/packages/21/cd/9336732941df972fbbfa394db9caa8bb0cf9fe03656ec728d12e9cbd6edc/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab65cb2885a9f80f979b85aa4e9c9165a31381ca322cbde7c638fe6eefd1ec39", size = 3234666, upload-time = "2026-01-21T18:32:28.72Z" }, + { url = "https://files.pythonhosted.org/packages/38/62/865ae8b739930ec433cd4123760bee7f8dafdc10abefd725a025604fb0de/sqlalchemy-2.0.46-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52fe29b3817bd191cc20bad564237c808967972c97fa683c04b28ec8979ae36f", size = 3232917, upload-time = "2026-01-21T18:44:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/38/805904b911857f2b5e00fdea44e9570df62110f834378706939825579296/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:09168817d6c19954d3b7655da6ba87fcb3a62bb575fb396a81a8b6a9fadfe8b5", size = 3185790, upload-time = "2026-01-21T18:32:30.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/3260bb53aabd2d274856337456ea52f6a7eccf6cce208e558f870cec766b/sqlalchemy-2.0.46-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be6c0466b4c25b44c5d82b0426b5501de3c424d7a3220e86cd32f319ba56798e", size = 3207206, upload-time = "2026-01-21T18:44:55.93Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b3/67c432d7f9d88bb1a61909b67e29f6354d59186c168fb5d381cf438d3b73/sqlalchemy-2.0.46-cp310-cp310-win32.whl", hash = "sha256:1bc3f601f0a818d27bfe139f6766487d9c88502062a2cd3a7ee6c342e81d5047", size = 2115296, upload-time = "2026-01-21T18:33:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8c/25fb284f570f9d48e6c240f0269a50cec9cf009a7e08be4c0aaaf0654972/sqlalchemy-2.0.46-cp310-cp310-win_amd64.whl", hash = "sha256:e0c05aff5c6b1bb5fb46a87e0f9d2f733f83ef6cbbbcd5c642b6c01678268061", size = 2138540, upload-time = "2026-01-21T18:33:14.22Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" }, + { url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" }, + { url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" }, + { url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" }, + { url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" }, + { url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" }, + { url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" }, + { url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" }, + { url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" }, + { url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" }, + { url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" }, + { url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" }, ] [[package]] diff --git a/samples/agent/adk/contact_lookup/a2ui_schema.py b/samples/agent/adk/contact_lookup/a2ui_schema.py deleted file mode 100644 index 4b6038fdc..000000000 --- a/samples/agent/adk/contact_lookup/a2ui_schema.py +++ /dev/null @@ -1,788 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -# a2ui_schema.py - -A2UI_SCHEMA = r''' -{ - "title": "A2UI Message Schema", - "description": "Describes a JSON payload for an A2UI (Agent to UI) message, which is used to dynamically construct and update user interfaces. A message MUST contain exactly ONE of the action properties: 'beginRendering', 'surfaceUpdate', 'dataModelUpdate', or 'deleteSurface'.", - "type": "object", - "properties": { - "beginRendering": { - "type": "object", - "description": "Signals the client to begin rendering a surface with a root component and specific styles.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be rendered." - }, - "root": { - "type": "string", - "description": "The ID of the root component to render." - }, - "styles": { - "type": "object", - "description": "Styling information for the UI.", - "properties": { - "font": { - "type": "string", - "description": "The primary font for the UI." - }, - "primaryColor": { - "type": "string", - "description": "The primary UI color as a hexadecimal code (e.g., '#00BFFF').", - "pattern": "^#[0-9a-fA-F]{6}$" - } - } - } - }, - "required": ["root", "surfaceId"] - }, - "surfaceUpdate": { - "type": "object", - "description": "Updates a surface with a new set of components.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be updated. If you are adding a new surface this *must* be a new, unique identified that has never been used for any existing surfaces shown." - }, - "components": { - "type": "array", - "description": "A list containing all UI components for the surface.", - "minItems": 1, - "items": { - "type": "object", - "description": "Represents a *single* component in a UI widget tree. This component could be one of many supported types.", - "properties": { - "id": { - "type": "string", - "description": "The unique identifier for this component." - }, - "weight": { - "type": "number", - "description": "The relative weight of this component within a Row or Column. This corresponds to the CSS 'flex-grow' property. Note: this may ONLY be set when the component is a direct descendant of a Row or Column." - }, - "component": { - "type": "object", - "description": "A wrapper object that MUST contain exactly one key, which is the name of the component type (e.g., 'Heading'). The value is an object containing the properties for that specific component.", - "properties": { - "Text": { - "type": "object", - "properties": { - "text": { - "type": "object", - "description": "The text content to display. This can be a literal string or a reference to a value in the data model ('path', e.g., '/doc/title'). While simple Markdown formatting is supported (i.e. without HTML, images, or links), utilizing dedicated UI components is generally preferred for a richer and more structured presentation.", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "usageHint": { - "type": "string", - "description": "A hint for the base text style. One of:\n- `h1`: Largest heading.\n- `h2`: Second largest heading.\n- `h3`: Third largest heading.\n- `h4`: Fourth largest heading.\n- `h5`: Fifth largest heading.\n- `caption`: Small text for captions.\n- `body`: Standard body text.", - "enum": [ - "h1", - "h2", - "h3", - "h4", - "h5", - "caption", - "body" - ] - } - }, - "required": ["text"] - }, - "Image": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the image to display. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/thumbnail/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "fit": { - "type": "string", - "description": "Specifies how the image should be resized to fit its container. This corresponds to the CSS 'object-fit' property.", - "enum": [ - "contain", - "cover", - "fill", - "none", - "scale-down" - ] - }, - "usageHint": { - "type": "string", - "description": "A hint for the image size and style. One of:\n- `icon`: Small square icon.\n- `avatar`: Circular avatar image.\n- `smallFeature`: Small feature image.\n- `mediumFeature`: Medium feature image.\n- `largeFeature`: Large feature image.\n- `header`: Full-width, full bleed, header image.", - "enum": [ - "icon", - "avatar", - "smallFeature", - "mediumFeature", - "largeFeature", - "header" - ] - } - }, - "required": ["url"] - }, - "Icon": { - "type": "object", - "properties": { - "name": { - "type": "object", - "description": "The name of the icon to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/form/submit').", - "properties": { - "literalString": { - "type": "string", - "enum": [ - "accountCircle", - "add", - "arrowBack", - "arrowForward", - "attachFile", - "calendarToday", - "call", - "camera", - "check", - "close", - "delete", - "download", - "edit", - "event", - "error", - "favorite", - "favoriteOff", - "folder", - "help", - "home", - "info", - "locationOn", - "lock", - "lockOpen", - "mail", - "menu", - "moreVert", - "moreHoriz", - "notificationsOff", - "notifications", - "payment", - "person", - "phone", - "photo", - "print", - "refresh", - "search", - "send", - "settings", - "share", - "shoppingCart", - "star", - "starHalf", - "starOff", - "upload", - "visibility", - "visibilityOff", - "warning" - ] - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["name"] - }, - "Video": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the video to display. This can be a literal string or a reference to a value in the data model ('path', e.g. '/video/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["url"] - }, - "AudioPlayer": { - "type": "object", - "properties": { - "url": { - "type": "object", - "description": "The URL of the audio to be played. This can be a literal string ('literal') or a reference to a value in the data model ('path', e.g. '/song/url').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "description": { - "type": "object", - "description": "A description of the audio, such as a title or summary. This can be a literal string or a reference to a value in the data model ('path', e.g. '/song/title').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["url"] - }, - "Row": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (horizontally). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "center", - "end", - "spaceAround", - "spaceBetween", - "spaceEvenly", - "start" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (vertically). This corresponds to the CSS 'align-items' property.", - "enum": ["start", "center", "end", "stretch"] - } - }, - "required": ["children"] - }, - "Column": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "distribution": { - "type": "string", - "description": "Defines the arrangement of children along the main axis (vertically). This corresponds to the CSS 'justify-content' property.", - "enum": [ - "start", - "center", - "end", - "spaceBetween", - "spaceAround", - "spaceEvenly" - ] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis (horizontally). This corresponds to the CSS 'align-items' property.", - "enum": ["center", "end", "start", "stretch"] - } - }, - "required": ["children"] - }, - "List": { - "type": "object", - "properties": { - "children": { - "type": "object", - "description": "Defines the children. Use 'explicitList' for a fixed set of children, or 'template' to generate children from a data list.", - "properties": { - "explicitList": { - "type": "array", - "items": { - "type": "string" - } - }, - "template": { - "type": "object", - "description": "A template for generating a dynamic list of children from a data model list. `componentId` is the component to use as a template, and `dataBinding` is the path to the map of components in the data model. Values in the map will define the list of children.", - "properties": { - "componentId": { - "type": "string" - }, - "dataBinding": { - "type": "string" - } - }, - "required": ["componentId", "dataBinding"] - } - } - }, - "direction": { - "type": "string", - "description": "The direction in which the list items are laid out.", - "enum": ["vertical", "horizontal"] - }, - "alignment": { - "type": "string", - "description": "Defines the alignment of children along the cross axis.", - "enum": ["start", "center", "end", "stretch"] - } - }, - "required": ["children"] - }, - "Card": { - "type": "object", - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to be rendered inside the card." - } - }, - "required": ["child"] - }, - "Tabs": { - "type": "object", - "properties": { - "tabItems": { - "type": "array", - "description": "An array of objects, where each object defines a tab with a title and a child component.", - "items": { - "type": "object", - "properties": { - "title": { - "type": "object", - "description": "The tab title. Defines the value as either a literal value or a path to data model value (e.g. '/options/title').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "child": { - "type": "string" - } - }, - "required": ["title", "child"] - } - } - }, - "required": ["tabItems"] - }, - "Divider": { - "type": "object", - "properties": { - "axis": { - "type": "string", - "description": "The orientation of the divider.", - "enum": ["horizontal", "vertical"] - } - } - }, - "Modal": { - "type": "object", - "properties": { - "entryPointChild": { - "type": "string", - "description": "The ID of the component that opens the modal when interacted with (e.g., a button)." - }, - "contentChild": { - "type": "string", - "description": "The ID of the component to be displayed inside the modal." - } - }, - "required": ["entryPointChild", "contentChild"] - }, - "Button": { - "type": "object", - "properties": { - "child": { - "type": "string", - "description": "The ID of the component to display in the button, typically a Text component." - }, - "primary": { - "type": "boolean", - "description": "Indicates if this button should be styled as the primary action." - }, - "action": { - "type": "object", - "description": "The client-side action to be dispatched when the button is clicked. It includes the action's name and an optional context payload.", - "properties": { - "name": { - "type": "string" - }, - "context": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "value": { - "type": "object", - "description": "Defines the value to be included in the context as either a literal value or a path to a data model value (e.g. '/user/name').", - "properties": { - "path": { - "type": "string" - }, - "literalString": { - "type": "string" - }, - "literalNumber": { - "type": "number" - }, - "literalBoolean": { - "type": "boolean" - } - } - } - }, - "required": ["key", "value"] - } - } - }, - "required": ["name"] - } - }, - "required": ["child", "action"] - }, - "CheckBox": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text to display next to the checkbox. Defines the value as either a literal value or a path to data model ('path', e.g. '/option/label').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "value": { - "type": "object", - "description": "The current state of the checkbox (true for checked, false for unchecked). This can be a literal boolean ('literalBoolean') or a reference to a value in the data model ('path', e.g. '/filter/open').", - "properties": { - "literalBoolean": { - "type": "boolean" - }, - "path": { - "type": "string" - } - } - } - }, - "required": ["label", "value"] - }, - "TextField": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text label for the input field. This can be a literal string or a reference to a value in the data model ('path, e.g. '/user/name').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "text": { - "type": "object", - "description": "The value of the text field. This can be a literal string or a reference to a value in the data model ('path', e.g. '/user/name').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "textFieldType": { - "type": "string", - "description": "The type of input field to display.", - "enum": [ - "date", - "longText", - "number", - "shortText", - "obscured" - ] - }, - "validationRegexp": { - "type": "string", - "description": "A regular expression used for client-side validation of the input." - } - }, - "required": ["label"] - }, - "DateTimeInput": { - "type": "object", - "properties": { - "value": { - "type": "object", - "description": "The selected date and/or time value in ISO 8601 format. This can be a literal string ('literalString') or a reference to a value in the data model ('path', e.g. '/user/dob').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "enableDate": { - "type": "boolean", - "description": "If true, allows the user to select a date." - }, - "enableTime": { - "type": "boolean", - "description": "If true, allows the user to select a time." - } - }, - "required": ["value"] - }, - "MultipleChoice": { - "type": "object", - "properties": { - "selections": { - "type": "object", - "description": "The currently selected values for the component. This can be a literal array of strings or a path to an array in the data model('path', e.g. '/hotel/options').", - "properties": { - "literalArray": { - "type": "array", - "items": { - "type": "string" - } - }, - "path": { - "type": "string" - } - } - }, - "options": { - "type": "array", - "description": "An array of available options for the user to choose from.", - "items": { - "type": "object", - "properties": { - "label": { - "type": "object", - "description": "The text to display for this option. This can be a literal string or a reference to a value in the data model (e.g. '/option/label').", - "properties": { - "literalString": { - "type": "string" - }, - "path": { - "type": "string" - } - } - }, - "value": { - "type": "string", - "description": "The value to be associated with this option when selected." - } - }, - "required": ["label", "value"] - } - }, - "maxAllowedSelections": { - "type": "integer", - "description": "The maximum number of options that the user is allowed to select." - } - }, - "required": ["selections", "options"] - }, - "Slider": { - "type": "object", - "properties": { - "value": { - "type": "object", - "description": "The current value of the slider. This can be a literal number ('literalNumber') or a reference to a value in the data model ('path', e.g. '/restaurant/cost').", - "properties": { - "literalNumber": { - "type": "number" - }, - "path": { - "type": "string" - } - } - }, - "minValue": { - "type": "number", - "description": "The minimum value of the slider." - }, - "maxValue": { - "type": "number", - "description": "The maximum value of the slider." - } - }, - "required": ["value"] - } - } - } - }, - "required": ["id", "component"] - } - } - }, - "required": ["surfaceId", "components"] - }, - "dataModelUpdate": { - "type": "object", - "description": "Updates the data model for a surface.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface this data model update applies to." - }, - "path": { - "type": "string", - "description": "An optional path to a location within the data model (e.g., '/user/name'). If omitted, or set to '/', the entire data model will be replaced." - }, - "contents": { - "type": "array", - "description": "An array of data entries. Each entry must contain a 'key' and exactly one corresponding typed 'value*' property.", - "items": { - "type": "object", - "description": "A single data entry. Exactly one 'value*' property should be provided alongside the key.", - "properties": { - "key": { - "type": "string", - "description": "The key for this data entry." - }, - "valueString": { - "type": "string" - }, - "valueNumber": { - "type": "number" - }, - "valueBoolean": { - "type": "boolean" - }, - "valueMap": { - "description": "Represents a map as an adjacency list.", - "type": "array", - "items": { - "type": "object", - "description": "One entry in the map. Exactly one 'value*' property should be provided alongside the key.", - "properties": { - "key": { - "type": "string" - }, - "valueString": { - "type": "string" - }, - "valueNumber": { - "type": "number" - }, - "valueBoolean": { - "type": "boolean" - } - }, - "required": ["key"] - } - } - }, - "required": ["key"] - } - } - }, - "required": ["contents", "surfaceId"] - }, - "deleteSurface": { - "type": "object", - "description": "Signals the client to delete the surface identified by 'surfaceId'.", - "properties": { - "surfaceId": { - "type": "string", - "description": "The unique identifier for the UI surface to be deleted." - } - }, - "required": ["surfaceId"] - } - } -} -''' diff --git a/samples/agent/adk/contact_lookup/agent.py b/samples/agent/adk/contact_lookup/agent.py index d16b58a9e..bbd18ef06 100644 --- a/samples/agent/adk/contact_lookup/agent.py +++ b/samples/agent/adk/contact_lookup/agent.py @@ -22,7 +22,6 @@ from a2ui_examples import CONTACT_UI_EXAMPLES # Corrected imports from our new/refactored files -from a2ui_schema import A2UI_SCHEMA from google.adk.agents.llm_agent import LlmAgent from google.adk.artifacts import InMemoryArtifactService from google.adk.memory.in_memory_memory_service import InMemoryMemoryService @@ -30,12 +29,9 @@ from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types -from prompt_builder import ( - - get_text_prompt, - get_ui_prompt, -) +from prompt_builder import get_text_prompt, ROLE_DESCRIPTION, WORKFLOW_DESCRIPTION, UI_DESCRIPTION from tools import get_contact_info +from a2ui.inference.schema.manager import A2uiSchemaManager logger = logging.getLogger(__name__) @@ -48,6 +44,7 @@ class ContactAgent: def __init__(self, base_url: str, use_ui: bool = False): self.base_url = base_url self.use_ui = use_ui + self._schema_manager = A2uiSchemaManager("0.8") if use_ui else None self._agent = self._build_agent(use_ui) self._user_id = "remote_agent" self._runner = Runner( @@ -58,23 +55,6 @@ def __init__(self, base_url: str, use_ui: bool = False): memory_service=InMemoryMemoryService(), ) - # --- MODIFICATION: Wrap the schema --- - # Load the A2UI_SCHEMA string into a Python object for validation - try: - # First, load the schema for a *single message* - single_message_schema = json.loads(A2UI_SCHEMA) - - # The prompt instructs the LLM to return a *list* of messages. - # Therefore, our validation schema must be an *array* of the single message schema. - self.a2ui_schema_object = {"type": "array", "items": single_message_schema} - logger.info( - "A2UI_SCHEMA successfully loaded and wrapped in an array validator." - ) - except json.JSONDecodeError as e: - logger.error(f"CRITICAL: Failed to parse A2UI_SCHEMA: {e}") - self.a2ui_schema_object = None - # --- END MODIFICATION --- - def get_processing_message(self) -> str: return "Looking up contact information..." @@ -82,11 +62,16 @@ def _build_agent(self, use_ui: bool) -> LlmAgent: """Builds the LLM agent for the contact agent.""" LITELLM_MODEL = os.getenv("LITELLM_MODEL", "gemini/gemini-2.5-flash") - if use_ui: - instruction = get_ui_prompt(self.base_url, CONTACT_UI_EXAMPLES) - else: - # The text prompt function also returns a complete prompt. - instruction = get_text_prompt() + instruction = ( + self._schema_manager.generate_system_prompt( + role_description=ROLE_DESCRIPTION, + workflow_description=WORKFLOW_DESCRIPTION, + ui_description=UI_DESCRIPTION, + examples=CONTACT_UI_EXAMPLES, + ) + if use_ui + else get_text_prompt() + ) return LlmAgent( model=LiteLlm(model=LITELLM_MODEL), @@ -120,7 +105,7 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]: current_query_text = query # Ensure schema was loaded - if self.use_ui and self.a2ui_schema_object is None: + if self.use_ui and self._schema_manager.bundled_schema is None: logger.error( "--- ContactAgent.stream: A2UI_SCHEMA is not loaded. " "Cannot perform UI validation. ---" @@ -224,9 +209,14 @@ async def stream(self, query, session_id) -> AsyncIterable[dict[str, Any]]: logger.info( "--- ContactAgent.stream: Validating against A2UI_SCHEMA... ---" ) - jsonschema.validate( - instance=parsed_json_data, schema=self.a2ui_schema_object - ) + if self._schema_manager.validator: + self._schema_manager.validator.validate(parsed_json_data) + else: + # Fallback if validator failed to init but UI is on (shouldn't happen ideally) + logger.warning( + "Validator not initialized, skipping detailed schema validation." + ) + # We could choose to fail here, but let's just warn to avoid blocking if just validation setup failed. # --- End New Validation Steps --- logger.info( diff --git a/samples/agent/adk/contact_lookup/prompt_builder.py b/samples/agent/adk/contact_lookup/prompt_builder.py index 3fe269e65..980f7ecbd 100644 --- a/samples/agent/adk/contact_lookup/prompt_builder.py +++ b/samples/agent/adk/contact_lookup/prompt_builder.py @@ -13,81 +13,35 @@ # limitations under the License. from a2ui_examples import CONTACT_UI_EXAMPLES -from a2ui_schema import A2UI_SCHEMA +from a2ui.inference.schema.manager import A2uiSchemaManager -# This is the agent's master instruction, separate from the UI prompt formatting. -AGENT_INSTRUCTION = """ - You are a helpful contact lookup assistant. Your goal is to help users find colleagues using a rich UI. +ROLE_DESCRIPTION="You are a helpful contact lookup assistant. Your final output MUST be a a2ui UI JSON response." - To achieve this, you MUST follow this logic: - - 1. **For finding contacts (e.g., "Who is Alex Jordan?"):** - a. You MUST call the `get_contact_info` tool. Extract the name and department. - b. After receiving the data: - i. If the tool returns a **single contact**, you MUST use the `CONTACT_CARD_EXAMPLE` template. - ii. If the tool returns **multiple contacts**, you MUST use the `CONTACT_LIST_EXAMPLE` template. - iii. If the tool returns an **empty list**, respond with text only and an empty JSON list: "I couldn't find anyone by that name.---a2ui_JSON---[]" - - 2. **For handling a profile view (e.g., "WHO_IS: Alex Jordan..."):** - a. You MUST call the `get_contact_info` tool with the specific name. - b. This will return a single contact. You MUST use the `CONTACT_CARD_EXAMPLE` template. - - 3. **For handling actions (e.g., "USER_WANTS_TO_EMAIL: ..."):** - a. You MUST use the `ACTION_CONFIRMATION_EXAMPLE` template. - b. Populate the `dataModelUpdate.contents` with a confirmation title and message. +WORKFLOW_DESCRIPTION=""" +To generate the response, you MUST follow these rules: +1. Your response MUST be in two parts, separated by the delimiter: `---a2ui_JSON---`. +2. The first part is your conversational text response (e.g., "Here is the contact you requested..."). +3. The second part is a single, raw JSON object which is a list of A2UI messages. +4. The JSON part MUST validate against the A2UI JSON SCHEMA provided below. +5. Buttons that represent the main action on a card or view (e.g., 'Follow', 'Email', 'Search') SHOULD include the `"primary": true` attribute. """ - -def get_ui_prompt(base_url: str, examples: str) -> str: - """ - Constructs the full prompt with UI instructions, rules, examples, and schema. - - Args: - base_url: The base URL for resolving static assets like logos. - examples: A string containing the specific UI examples for the agent's task. - - Returns: - A formatted string to be used as the system prompt for the LLM. - """ - - # --- THIS IS THE FIX --- - # We no longer call .format() on the examples, as it breaks the JSON. - formatted_examples = examples - # --- END FIX --- - - return f""" - You are a helpful contact lookup assistant. Your final output MUST be a a2ui UI JSON response. - - To generate the response, you MUST follow these rules: - 1. Your response MUST be in two parts, separated by the delimiter: `---a2ui_JSON---`. - 2. The first part is your conversational text response (e.g., "Here is the contact you requested..."). - 3. The second part is a single, raw JSON object which is a list of A2UI messages. - 4. The JSON part MUST validate against the A2UI JSON SCHEMA provided below. - 5. Buttons that represent the main action on a card or view (e.g., 'Follow', 'Email', 'Search') SHOULD include the `"primary": true` attribute. - - --- UI TEMPLATE RULES --- - - **For finding contacts (e.g., "Who is Alex Jordan?"):** - a. You MUST call the `get_contact_info` tool. - b. If the tool returns a **single contact**, you MUST use the `CONTACT_CARD_EXAMPLE` template. Populate the `dataModelUpdate.contents` with the contact's details (name, title, email, etc.). - c. If the tool returns **multiple contacts**, you MUST use the `CONTACT_LIST_EXAMPLE` template. Populate the `dataModelUpdate.contents` with the list of contacts for the "contacts" key. - d. If the tool returns an **empty list**, respond with text only and an empty JSON list: "I couldn't find anyone by that name.---a2ui_JSON---[]" - - - **For handling a profile view (e.g., "WHO_IS: Alex Jordan..."):** - a. You MUST call the `get_contact_info` tool with the specific name. - b. This will return a single contact. You MUST use the `CONTACT_CARD_EXAMPLE` template. - - - **For handling actions (e.g., "follow_contact"):** - a. You MUST use the `FOLLOW_SUCCESS_EXAMPLE` template. - b. This will render a new card with a "Successfully Followed" message. - c. Respond with a text confirmation like "You are now following this contact." along with the JSON. - - {formatted_examples} - - ---BEGIN A2UI JSON SCHEMA--- - {A2UI_SCHEMA} - ---END A2UI JSON SCHEMA--- - """ - +UI_DESCRIPTION=""" +- **For finding contacts (e.g., "Who is Alex Jordan?"):** + a. You MUST call the `get_contact_info` tool. + b. If the tool returns a **single contact**, you MUST use the `CONTACT_CARD_EXAMPLE` template. Populate the `dataModelUpdate.contents` with the contact's details (name, title, email, etc.). + c. If the tool returns **multiple contacts**, you MUST use the `CONTACT_LIST_EXAMPLE` template. Populate the `dataModelUpdate.contents` with the list of contacts for the "contacts" key. + d. If the tool returns an **empty list**, respond with text only and an empty JSON list: "I couldn't find anyone by that name.---a2ui_JSON---[]" + +- **For handling a profile view (e.g., "WHO_IS: Alex Jordan..."):** + a. You MUST call the `get_contact_info` tool with the specific name. + b. This will return a single contact. You MUST use the `CONTACT_CARD_EXAMPLE` template. + +- **For handling actions (e.g., "follow_contact"):** + a. You MUST use the `FOLLOW_SUCCESS_EXAMPLE` template. + b. This will render a new card with a "Successfully Followed" message. + c. Respond with a text confirmation like "You are now following this contact." along with the JSON. +""" def get_text_prompt() -> str: """ @@ -109,9 +63,13 @@ def get_text_prompt() -> str: if __name__ == "__main__": - # Example of how to use the prompt builder - my_base_url = "http://localhost:8000" - contact_prompt = get_ui_prompt(my_base_url, CONTACT_UI_EXAMPLES) + # Example of how to use the A2UI Schema Manager to generate a system prompt + contact_prompt = A2uiSchemaManager("0.8").generate_system_prompt( + role_description=ROLE_DESCRIPTION, + workflow_description=WORKFLOW_DESCRIPTION, + ui_description=UI_DESCRIPTION, + examples=CONTACT_UI_EXAMPLES, + ) print(contact_prompt) with open("generated_prompt.txt", "w") as f: f.write(contact_prompt) diff --git a/samples/agent/adk/uv.lock b/samples/agent/adk/uv.lock index 48cf6be77..bbba9aba0 100644 --- a/samples/agent/adk/uv.lock +++ b/samples/agent/adk/uv.lock @@ -52,6 +52,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "pyink", specifier = ">=24.10.0" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, ] @@ -1145,7 +1146,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140, upload-time = "2025-12-04T14:23:01.282Z" }, { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219, upload-time = "2025-12-04T14:50:08.309Z" }, { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211, upload-time = "2025-12-04T14:57:43.968Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311, upload-time = "2025-12-04T15:07:14.697Z" }, { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833, upload-time = "2025-12-04T14:26:03.669Z" }, { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256, upload-time = "2025-12-04T15:04:25.276Z" }, { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483, upload-time = "2025-12-04T14:27:30.804Z" }, @@ -1153,7 +1153,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671, upload-time = "2025-12-04T14:23:05.267Z" }, { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360, upload-time = "2025-12-04T14:50:10.026Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160, upload-time = "2025-12-04T14:57:45.41Z" }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388, upload-time = "2025-12-04T15:07:15.789Z" }, { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166, upload-time = "2025-12-04T14:26:05.099Z" }, { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193, upload-time = "2025-12-04T15:04:27.041Z" }, { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653, upload-time = "2025-12-04T14:27:32.366Z" }, @@ -1161,7 +1160,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638, upload-time = "2025-12-04T14:25:20.941Z" }, { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145, upload-time = "2025-12-04T14:50:11.039Z" }, { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236, upload-time = "2025-12-04T14:57:47.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506, upload-time = "2025-12-04T15:07:16.906Z" }, { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783, upload-time = "2025-12-04T14:26:06.225Z" }, { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857, upload-time = "2025-12-04T15:04:28.484Z" }, { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034, upload-time = "2025-12-04T14:27:33.531Z" },