Skip to content

Commit c6f12bb

Browse files
committed
feat: implement schema packaging and runtime loading
- Add a base InferenceStrategy class - Add PackSpecsBuildHook to copy JSON schemas into the package during build time. - Update pyproject.toml to include assets and configure the build hook. - Implement A2uiSchemaManager for robust schema loading, pruning, and prompt generation.
1 parent 20d6471 commit c6f12bb

14 files changed

Lines changed: 870 additions & 58 deletions

File tree

.github/workflows/python_a2ui_agent_build_and_test.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@ jobs:
4646
working-directory: a2a_agents/python/a2ui_agent
4747
run: uv run pyink --check .
4848

49+
- name: Run unit tests
50+
working-directory: a2a_agents/python/a2ui_agent
51+
run: uv run --with pytest pytest tests/
52+
4953
- name: Build the python SDK
5054
working-directory: a2a_agents/python/a2ui_agent
5155
run: uv build .
5256

53-
- name: Run unit tests
57+
- name: Run validation scripts on assets packing
5458
working-directory: a2a_agents/python/a2ui_agent
55-
run: uv run --with pytest pytest tests/
59+
run: uv run python tests/integration/verify_load_real.py
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/a2ui/assets/**/*.json
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import os
2+
import shutil
3+
import sys
4+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
5+
6+
7+
class PackSpecsBuildHook(BuildHookInterface):
8+
9+
def initialize(self, version, build_data):
10+
project_root = self.root
11+
12+
# Add src to sys.path to import the constant
13+
src_path = os.path.join(project_root, "src")
14+
if src_path not in sys.path:
15+
sys.path.insert(0, src_path)
16+
17+
from a2ui.inference.schema.manager import (
18+
SPEC_VERSION_MAP,
19+
A2UI_ASSET_PACKAGE,
20+
SPECIFICATION_DIR,
21+
find_repo_root,
22+
)
23+
24+
# project root is in a2a_agents/python/a2ui_agent
25+
# Dynamically find repo root by looking for SPECIFICATION_DIR
26+
repo_root = find_repo_root(project_root)
27+
if not repo_root:
28+
# Check for PKG-INFO which implies a packaged state (sdist).
29+
# If PKG-INFO is present, trust the bundled assets.
30+
if os.path.exists(os.path.join(project_root, "PKG-INFO")):
31+
print("Repository root not found, but PKG-INFO present (sdist). Skipping copy.")
32+
return
33+
34+
raise RuntimeError(
35+
f"Could not find repository root (looked for '{SPECIFICATION_DIR}'"
36+
" directory)."
37+
)
38+
39+
# Target directory: src/a2ui/assets
40+
target_base = os.path.join(
41+
project_root, "src", A2UI_ASSET_PACKAGE.replace(".", os.sep)
42+
)
43+
44+
for ver, schema_map in SPEC_VERSION_MAP.items():
45+
target_dir = os.path.join(target_base, ver)
46+
os.makedirs(target_dir, exist_ok=True)
47+
48+
for _schema_key, source_rel_path in schema_map.items():
49+
source_path = os.path.join(repo_root, source_rel_path)
50+
51+
if not os.path.exists(source_path):
52+
print(
53+
f"WARNING: Source schema file not found at {source_path}. Build might"
54+
" produce incomplete wheel if not running from monorepo root."
55+
)
56+
continue
57+
58+
filename = os.path.basename(source_path)
59+
dst_file = os.path.join(target_dir, filename)
60+
61+
print(f"Copying {source_path} -> {dst_file}")
62+
shutil.copy2(source_path, dst_file)

a2a_agents/python/a2ui_agent/pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ build-backend = "hatchling.build"
1717

1818
[tool.hatch.build.targets.wheel]
1919
packages = ["src/a2ui"]
20+
artifacts = ["src/a2ui/assets/**"]
21+
22+
[tool.hatch.build.targets.sdist]
23+
artifacts = ["src/a2ui/assets/**"]
24+
25+
[tool.hatch.build.hooks.custom]
26+
path = "pack_specs_hook.py"
2027

2128
[[tool.uv.index]]
2229
url = "https://pypi.org/simple"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from abc import ABC, abstractmethod
16+
from typing import List
17+
18+
19+
class InferenceStrategy(ABC):
20+
21+
@abstractmethod
22+
def generate_system_prompt(
23+
self,
24+
role_description: str,
25+
workflow_description: str = "",
26+
ui_description: str = "",
27+
selected_components: List[str] = [],
28+
include_examples: bool = False,
29+
) -> str:
30+
"""
31+
Abstract method to be implemented by subclasses.
32+
"""
33+
pass
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import json
16+
import os
17+
import importlib.resources
18+
from typing import List, Dict, Any
19+
20+
from abc import ABC, abstractmethod
21+
22+
ENCODING = "utf-8"
23+
24+
25+
class A2uiSchemaLoader(ABC):
26+
"""Abstract base class for loading schema files."""
27+
28+
@abstractmethod
29+
def load(self, filename: str) -> Any:
30+
"""Loads a JSON file."""
31+
pass
32+
33+
34+
class FileSystemLoader(A2uiSchemaLoader):
35+
"""Loads schema files from the local filesystem.
36+
37+
This loader assumes that all referenced schema files are located in the
38+
same flat directory structure.
39+
"""
40+
41+
def __init__(self, base_dir: str):
42+
self.base_dir = base_dir
43+
44+
def load(self, filename: str) -> Any:
45+
path = os.path.join(self.base_dir, filename)
46+
with open(path, "r", encoding=ENCODING) as f:
47+
return json.load(f)
48+
49+
50+
class PackageLoader(A2uiSchemaLoader):
51+
"""Loads schema files from package resources.
52+
53+
This loader assumes that all referenced schema files are located in the
54+
same flat package structure.
55+
"""
56+
57+
def __init__(self, package_path: str):
58+
self.package_path = package_path
59+
60+
def load(self, filename: str) -> Any:
61+
try:
62+
traversable = importlib.resources.files(self.package_path)
63+
resource_path = traversable.joinpath(filename)
64+
with resource_path.open("r", encoding=ENCODING) as f:
65+
return json.load(f)
66+
except Exception as e:
67+
raise IOError(
68+
f"Could not load package resource {filename} in {self.package_path}: {e}"
69+
) from e

0 commit comments

Comments
 (0)