Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/python_agent_sdk_build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ jobs:
working-directory: agent_sdks/python/a2ui_core
run: uv run --with pytest pytest tests/

- name: Typecheck Core
working-directory: agent_sdks/python/a2ui_core
run: uv run mypy src/ tests/

- name: Run conformance YAML validation
working-directory: agent_sdks/conformance
run: uv run pytest
Expand Down
16 changes: 16 additions & 0 deletions agent_sdks/python/a2ui_core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ dependencies = [
dev = [
"pytest>=8.0.0",
"pyink>=24.10.0",
"mypy>=1.14.0",
"types-jsonschema>=4.26.0",
]

[build-system]
Expand All @@ -40,3 +42,17 @@ packages = ["src/a2ui"]
[[tool.uv.index]]
url = "https://pypi.org/simple"
default = true

[tool.mypy]
mypy_path = ["src", "scripts"]
explicit_package_bases = true
strict = true
plugins = [
"pydantic.mypy"
]

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
warn_required_dynamic_aliases = true
warn_untyped_fields = true
30 changes: 17 additions & 13 deletions agent_sdks/python/a2ui_core/scripts/generate_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import json
import os
import re
from typing import Any, Dict, List
from typing import Any, Dict, List, Optional

# Base directories
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
Expand Down Expand Up @@ -86,16 +86,18 @@ def map_json_type_to_python(prop_name: str, prop: Dict[str, Any]) -> str:
return f"Literal[{cval}]"

if "$ref" in prop:
ref = prop["$ref"]
ref = str(prop["$ref"])
if "common_types.json" in ref:
ref_name = ref.split("/")[-1]
return ref_name
return str(ref_name)
elif ref.startswith("#/"):
return ref.split("/")[-1]
return str(ref.split("/")[-1])
return "Any"

if "oneOf" in prop or "anyOf" in prop:
union_items = prop.get("oneOf") or prop.get("anyOf")
if not union_items:
return "Any"
mapped_items = []
for item in union_items:
mapped = map_json_type_to_python(prop_name, item)
Expand Down Expand Up @@ -227,7 +229,7 @@ def compile_component_to_pydantic(
name: str,
schema: Dict[str, Any],
base_class: str = "ComponentCommon",
common_data: Dict[str, Any] = None,
common_data: Optional[Dict[str, Any]] = None,
) -> str:
"""Generates Python Pydantic class string representing one Component."""
lines = [
Expand Down Expand Up @@ -264,7 +266,7 @@ def compile_component_to_pydantic(


def compile_object_def(
class_name: str, spec: Dict[str, Any], base_class: str = None
class_name: str, spec: Dict[str, Any], base_class: Optional[str] = None
) -> str:
"""Generates Python Pydantic class representing a standard JSON Schema object definition."""
add_props = spec.get("additionalProperties", False)
Expand Down Expand Up @@ -422,7 +424,7 @@ def generate_common_types(common_data: Dict[str, Any]) -> str:


def generate_basic_catalog_components(
catalog_data: Dict[str, Any], common_data: Dict[str, Any] = None
catalog_data: Dict[str, Any], common_data: Optional[Dict[str, Any]] = None
) -> tuple[str, List[str]]:
"""Generates components.py containing all component schemas extending CatalogComponentCommon."""
global ALLOW_INLINE_COMPILATION
Expand Down Expand Up @@ -616,7 +618,9 @@ def generate_server_to_client(s2c_data: Dict[str, Any]) -> tuple[str, List[str]]
f', alias="{envelope_key}"' if snake_envelope != envelope_key else ""
)
output.append(f"class {mname}(StrictBaseModel):")
output.append(f" version: Literal[SPEC_VERSION] = SPEC_VERSION")
output.append(
f' version: Literal["{SPEC_VERSION_DOT}"] = "{SPEC_VERSION_DOT}"'
)
output.append(f" {snake_envelope}: {payload_name} = Field(...{alias_opt})")
output.append("\n")

Expand Down Expand Up @@ -663,7 +667,7 @@ def generate_client_capabilities(capabilities_data: Dict[str, Any]) -> str:

output.append("class A2uiClientCapabilities(StrictBaseModel):")
output.append(
" v0_9: Optional[V09Capabilities] = Field(None, alias=SPEC_VERSION)"
f' v0_9: Optional[V09Capabilities] = Field(None, alias="{SPEC_VERSION_DOT}")'
)

code = "\n".join(output)
Expand Down Expand Up @@ -702,12 +706,12 @@ def generate_client_to_server(c2s_data: Dict[str, Any]) -> str:
output.append(f"A2uiClientError = Union[{', '.join(error_class_names)}]\n")

output.append("class A2uiClientActionMessage(StrictBaseModel):")
output.append(f" version: Literal[SPEC_VERSION] = SPEC_VERSION")
output.append(f' version: Literal["{SPEC_VERSION_DOT}"] = "{SPEC_VERSION_DOT}"')
output.append(" action: A2uiClientAction = Field(...)")
output.append("\n")

output.append("class A2uiClientErrorMessage(StrictBaseModel):")
output.append(f" version: Literal[SPEC_VERSION] = SPEC_VERSION")
output.append(f' version: Literal["{SPEC_VERSION_DOT}"] = "{SPEC_VERSION_DOT}"')
output.append(" error: A2uiClientError = Field(...)")
output.append("\n")

Expand All @@ -717,7 +721,7 @@ def generate_client_to_server(c2s_data: Dict[str, Any]) -> str:

# Client Data Model
output.append("class A2uiClientDataModel(StrictBaseModel):")
output.append(f" version: Literal[SPEC_VERSION] = SPEC_VERSION")
output.append(f' version: Literal["{SPEC_VERSION_DOT}"] = "{SPEC_VERSION_DOT}"')
output.append(
' surfaces: Dict[str, Dict[str, Any]] = Field(..., description="A map of surface IDs to their current data models.")\n'
)
Expand Down Expand Up @@ -769,7 +773,7 @@ def generate_schema_init(msg_names: List[str]) -> str:
return "\n".join(output)


def main():
def main() -> None:
print("Compiling modular and symmetrical A2UI schemas mirroring web_core...")

os.makedirs(SCHEMA_DIR, exist_ok=True)
Expand Down
104 changes: 54 additions & 50 deletions agent_sdks/python/a2ui_core/src/a2ui/core/basic_catalog/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,59 +13,59 @@
# limitations under the License.

from .components import (
AudioPlayerComponent,
ButtonComponent,
CardComponent,
CheckBoxComponent,
ChoicePickerComponent,
ColumnComponent,
DateTimeInputComponent,
DividerComponent,
IconComponent,
ImageComponent,
ListComponent,
ModalComponent,
RowComponent,
SliderComponent,
TabsComponent,
TextComponent,
TextFieldComponent,
VideoComponent,
AnyComponent,
AudioPlayerComponent as AudioPlayerComponent,
ButtonComponent as ButtonComponent,
CardComponent as CardComponent,
CheckBoxComponent as CheckBoxComponent,
ChoicePickerComponent as ChoicePickerComponent,
ColumnComponent as ColumnComponent,
DateTimeInputComponent as DateTimeInputComponent,
DividerComponent as DividerComponent,
IconComponent as IconComponent,
ImageComponent as ImageComponent,
ListComponent as ListComponent,
ModalComponent as ModalComponent,
RowComponent as RowComponent,
SliderComponent as SliderComponent,
TabsComponent as TabsComponent,
TextComponent as TextComponent,
TextFieldComponent as TextFieldComponent,
VideoComponent as VideoComponent,
AnyComponent as AnyComponent,
)
from .function_apis import (
RequiredApi,
RegexApi,
LengthApi,
NumericApi,
EmailApi,
FormatStringApi,
FormatNumberApi,
FormatCurrencyApi,
FormatDateApi,
PluralizeApi,
OpenUrlApi,
AndApi,
OrApi,
NotApi,
RequiredApi as RequiredApi,
RegexApi as RegexApi,
LengthApi as LengthApi,
NumericApi as NumericApi,
EmailApi as EmailApi,
FormatStringApi as FormatStringApi,
FormatNumberApi as FormatNumberApi,
FormatCurrencyApi as FormatCurrencyApi,
FormatDateApi as FormatDateApi,
PluralizeApi as PluralizeApi,
OpenUrlApi as OpenUrlApi,
AndApi as AndApi,
OrApi as OrApi,
NotApi as NotApi,
)
from .operator_apis import (
AddApi,
SubtractApi,
MultiplyApi,
DivideApi,
EqualsApi,
NotEqualsApi,
GreaterThanApi,
LessThanApi,
ContainsApi,
StartsWithApi,
EndsWithApi,
AddApi as AddApi,
SubtractApi as SubtractApi,
MultiplyApi as MultiplyApi,
DivideApi as DivideApi,
EqualsApi as EqualsApi,
NotEqualsApi as NotEqualsApi,
GreaterThanApi as GreaterThanApi,
LessThanApi as LessThanApi,
ContainsApi as ContainsApi,
StartsWithApi as StartsWithApi,
EndsWithApi as EndsWithApi,
)
from .styles import Theme
from .function_impls import BASIC_FUNCTION_IMPLEMENTATIONS
from ..schema.constants import SPEC_VERSION, SPEC_BASE_URL
from ..catalog import ModelCatalog
from .styles import Theme as Theme
from .function_impls import BASIC_FUNCTION_IMPLEMENTATIONS as BASIC_FUNCTION_IMPLEMENTATIONS
from ..schema.constants import SPEC_VERSION as SPEC_VERSION, SPEC_BASE_URL as SPEC_BASE_URL
from ..catalog import ModelCatalog as ModelCatalog


def _basic_catalog_id(spec_version: str) -> str:
Expand All @@ -74,10 +74,14 @@ def _basic_catalog_id(spec_version: str) -> str:
)


from typing import Dict, Type
from pydantic import BaseModel


class BasicCatalog(ModelCatalog):

def __init__(self):
components_map = {
def __init__(self) -> None:
components_map: Dict[str, Type[BaseModel]] = {
"Text": TextComponent,
"Image": ImageComponent,
"Icon": IconComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def matches_keyword(self, keyword: str) -> bool:
return True
return False

def skip_whitespace(self):
def skip_whitespace(self) -> None:
while not self.is_at_end() and self.peek().isspace():
self.advance()

Expand Down
Loading
Loading