docs(spec): add proposals for client-expanded templates and Python fluent builders - #2383
docs(spec): add proposals for client-expanded templates and Python fluent builders#2383jacobsimionato wants to merge 26 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a server-side UI template expansion engine for the A2UI Python Agent SDK, adding declarative static templates, dynamic server resolvers, and synchronous expansion capabilities. The feedback highlights a few key areas for improvement: ensuring the resolve method in models.py strictly returns a dictionary to prevent downstream type errors, updating the validation guard in processor.py to correctly handle dictionary-based expressions for primitive types, and refactoring nested helper functions in both models.py and processor.py to the module or class level to eliminate redundant recreation overhead during recursive execution.
| return dict(res_val) if isinstance(res_val, dict) else res_val | ||
| return dict(res) if isinstance(res, dict) else res |
There was a problem hiding this comment.
The resolve method is annotated to return Dict[str, Any], but if the resolver returns None or a non-dictionary type, it returns res_val or res directly. This will cause a TypeError downstream in processor.py when attempting to unpack the resolved data (e.g., {**passed_params, **resolved_data}).
Ensure that resolve always returns a dictionary, falling back to an empty dictionary {} if the resolver output is invalid.
| return dict(res_val) if isinstance(res_val, dict) else res_val | |
| return dict(res) if isinstance(res, dict) else res | |
| return dict(res_val) if isinstance(res_val, dict) else {} | |
| return dict(res) if isinstance(res, dict) else {} |
| if ( | ||
| isinstance(p_meta_dict, dict) | ||
| and p_type not in ["child", "children", "action"] | ||
| and not ( | ||
| isinstance(params[p_name], str) | ||
| and ( | ||
| params[p_name].startswith("${") | ||
| or params[p_name].startswith("__") | ||
| ) | ||
| ) | ||
| ): |
There was a problem hiding this comment.
When a template parameter is a primitive type (like string, number, or boolean), the agent or LLM may pass a dynamic DataBinding (e.g., {"path": "/foo"}), a Concat expression, or a FormatExpr instead of a literal value.
Currently, the validation guard only skips validation if the parameter is a string starting with ${ or __. If a dictionary-based expression is passed, jsonschema.validate will run and fail because a dictionary does not match the primitive type schema (e.g., string).
Update the guard to also skip validation if the parameter value is a dictionary representing an A2UI expression (containing keys like path, concat, format, or param).
is_expression = isinstance(params[p_name], dict) and any(
k in params[p_name] for k in ["path", "concat", "format", "param"]
)
if (
isinstance(p_meta_dict, dict)
and p_type not in ["child", "children", "action"]
and not is_expression
and not (
isinstance(params[p_name], str)
and (
params[p_name].startswith("${")
or params[p_name].startswith("__")
)
)
):| def map_id(internal_id: str) -> str: | ||
| if internal_id == "root": | ||
| return instance_id | ||
| return f"{instance_id}_{internal_id}" | ||
|
|
||
| def resolve_slot(val: Any) -> Tuple[bool, Any]: | ||
| if isinstance(val, (ParamRef, TemplateLoop)): | ||
| val = val.to_dict() | ||
| if ( | ||
| isinstance(val, dict) | ||
| and "param" in val | ||
| and "template" not in val | ||
| and "item" not in val | ||
| ): | ||
| p_path = val["param"] | ||
| found, res = resolve_param_path(p_path, params) | ||
| if found: | ||
| return True, res | ||
| if "default" in val: | ||
| return True, val["default"] | ||
| elif isinstance(val, str): | ||
| if val.startswith("${") and val.endswith("}"): | ||
| p_path = val[2:-1] | ||
| found, res = resolve_param_path(p_path, params) | ||
| if found: | ||
| return True, res | ||
| elif val.startswith("__PARAM__"): | ||
| p_name = val[9:] | ||
| found, res = resolve_param_path(p_name, params) | ||
| if found: | ||
| return True, res | ||
| return False, val | ||
|
|
||
| def map_child_list(child_list: Any) -> Any: |
There was a problem hiding this comment.
Avoid defining helper functions like map_id, resolve_slot, and map_child_list inside the _expand_static_layout method. Since this method is called frequently (and recursively) during template expansion, redefining these functions on every invocation introduces unnecessary performance overhead and memory allocation.
Consider refactoring these helpers to the module level or as private methods of the TemplateProcessor class, passing instance_id and params as arguments.
References
- Avoid defining helper functions inside loops or nested functions where they are redundantly recreated on every iteration. Define them at the module level to improve performance and maintainability.
| def check_val(val: Any) -> None: | ||
| if isinstance(val, dict): | ||
| if "param" in val and isinstance(val["param"], str): | ||
| check_path(val["param"]) | ||
| if "concat" in val and isinstance(val["concat"], list): | ||
| for elem in val["concat"]: | ||
| check_val(elem) | ||
| if "format" in val and "args" in val and isinstance(val["args"], dict): | ||
| for v in val["args"].values(): | ||
| check_val(v) | ||
| for v in val.values(): | ||
| check_val(v) | ||
| elif isinstance(val, list): | ||
| for item in val: | ||
| check_val(item) | ||
| elif isinstance(val, str): | ||
| for m in expr_pattern.finditer(val): | ||
| check_path(m.group(1)) | ||
|
|
||
| def check_path(path: str) -> None: |
There was a problem hiding this comment.
The helper functions check_val and check_path are defined inside the validate_definition method, causing them to be redundantly recreated every time a template is validated.
To improve performance and maintainability, refactor these helpers to the module level or as private methods of the StaticTemplate class (e.g., _check_val and _check_path).
References
- Avoid defining helper functions inside loops or nested functions where they are redundantly recreated on every iteration. Define them at the module level to improve performance and maintainability.
f8c9d7f to
384db67
Compare
a5a3c99 to
5551454
Compare
909da7f to
643cd8b
Compare
643cd8b to
452c99a
Compare
…definition specification
0381f15 to
c1c481f
Compare
…cursion, and max depth
c1c481f to
21db203
Compare
…th version 0.1 validation
…processor, and test_format
…tion/proposals/templates/schema
…at, and fix relative doc link
…alogs, and imports in templates
21db203 to
358294b
Compare
…mplates and interactive studio
358294b to
9de65d1
Compare
Summary
[Part 4 of 4 in Templates Stack - Depends on #2382]
This PR adds two future architecture proposals to the A2UI Templates roadmap under
specification/proposals/templates/:client_expanded_templates.md)python_fluent_template_builders.md)Proposed Architecture 1: Client-Expanded Templates & MCP Federation
This proposal details how A2UI templates can be transported as compact JSON definitions over the wire and expanded directly on the client by the A2UI renderer/framework:
createSurface(templates: [...])or dynamically streamed via a newupdateTemplatesprotocol message.catalogId, components reference remote templates viatemplateSource: "mcp://github-server/templates".{hostId}_{slot}_{index}_{type}), providing direct two-way reactivity with the client's localDataModel.template_definition.json, no executable code or arbitrary scripts cross the wire.Proposed Architecture 2: Python Fluent Template Builders
This proposal details an ergonomic, catalog-driven code generation system for authoring type-safe templates in Python:
a2ui-build-builders) distributed viaa2ui-agent-sdkanda2ui-corethat ingests any A2UIcatalog.jsonschema and synthesizes strongly typed dataclasses (Card,Column,Row,Text,Button, etc.).a2ui.template.builder, so general developers have zero build steps.base.py,models.py, serialization protocol) and auto-generated catalog modules (components.py,types.py).**kwargs) eliminate typos likealgn="center", whileLiteralunions enforce valid variant enums in IDE hover popups.Stack Context