feat(samples): add community templates sample app with version 0.1 templates and interactive studio - #2382
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a server-side UI template expansion system for the A2UI Python Agent SDK, enabling declarative static templates and dynamic server-side resolvers that expand into standard Basic Catalog primitives. Key feedback includes resolving a missing inspect import in the demo server and a missing nest-asyncio dependency in pyproject.toml. Additionally, the core SDK should avoid hardcoding loop variable names in validation, defensively filter parameters before unpacking them into resolver and render functions to prevent TypeError crashes, and ensure the React client handles non-ok fetch responses during dynamic template resolution.
| expr_pattern = re.compile(r"\$\{([\w\.]+)\}") | ||
|
|
||
| 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: | ||
| parts = path.split(".") | ||
| root = parts[0] | ||
| if root not in self.parameters: | ||
| if ( | ||
| root.startswith("__") | ||
| or root == "item" | ||
| or root == "skill" | ||
| or root == "member" | ||
| ): | ||
| return | ||
| raise ValueError( | ||
| f"Template '{self.template_id}': Component references parameter" | ||
| f" '{root}' in '{path}', but it is not declared in template" | ||
| " parameters." | ||
| ) |
There was a problem hiding this comment.
Hardcoding specific loop variable names like "skill" and "member" in the core SDK's template validation logic is a maintainability issue and restricts users from using custom loop variable names (e.g., as: foo with ${foo.bar} would fail validation).
Instead, we should dynamically collect all loop variable names defined via as within the template layout and treat them as valid root parameters during validation. To avoid redundantly recreating helper functions on every validation call, these helpers should be defined at the module level.
expr_pattern = re.compile(r"\$\{([\w\.]+)\}")
loop_vars = {"item"}
for comp in self.components:
_collect_loop_vars(comp.to_dict() if isinstance(comp, TemplateComponent) else comp, loop_vars)
for comp in self.components:
_check_val(comp.to_dict() if isinstance(comp, TemplateComponent) else comp, expr_pattern, loop_vars, self.parameters, self.template_id)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.
There was a problem hiding this comment.
Great recommendation! Implemented a dynamic _collect_loop_vars static helper that recursively traverses the layout to collect all custom loop variables defined via as (defaulting to item), removing all hardcoded loop variable names.
| """Executes the resolver synchronously or asynchronously.""" | ||
| if self.resolver is None: | ||
| return {} | ||
| res = self.resolver(**passed_params) |
There was a problem hiding this comment.
Unpacking **passed_params directly into the resolver function can raise a TypeError if the caller passes extra parameters that are not defined in the resolver's signature (and the resolver does not accept **kwargs). We should verify the type of passed_params (e.g., using isinstance(passed_params, dict)) before performing key-based lookups or assignments, and defensively filter passed_params to only include keys accepted by the resolver function.
| res = self.resolver(**passed_params) | |
| if isinstance(passed_params, dict): | |
| sig = inspect.signature(self.resolver) | |
| has_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()) | |
| if has_kwargs: | |
| filtered_params = passed_params | |
| else: | |
| filtered_params = { | |
| k: v for k, v in passed_params.items() | |
| if k in sig.parameters | |
| } | |
| res = self.resolver(**filtered_params) | |
| elif isinstance(passed_params, list): | |
| res = self.resolver(*passed_params) | |
| else: | |
| res = self.resolver(passed_params) |
References
- When validation checks or function arguments can be represented as either a dictionary (named) or a list (positional), always verify the type (e.g., using
isinstance(args, dict)) before performing key-based lookups or assignments to preventTypeErrorcrashes.
There was a problem hiding this comment.
Done! Updated DynamicTemplate.resolve() to inspect the resolver signature and defensively filter passed_params to only accepted parameters when the resolver does not accept **kwargs.
|
|
||
| # Mode A: Programmatic Render Function (returns AST tree or list directly) | ||
| if dynamic_tmpl.render_fn is not None: | ||
| res = dynamic_tmpl.render_fn(**passed_params) |
There was a problem hiding this comment.
Similarly to the resolver, unpacking **passed_params directly into render_fn can raise a TypeError if there are extra parameters passed that are not accepted by the function signature. We should verify the type of passed_params (e.g., using isinstance(passed_params, dict)) before performing key-based lookups or assignments, and defensively filter the parameters before calling render_fn.
| res = dynamic_tmpl.render_fn(**passed_params) | |
| if isinstance(passed_params, dict): | |
| import inspect | |
| sig = inspect.signature(dynamic_tmpl.render_fn) | |
| has_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()) | |
| if has_kwargs: | |
| filtered_params = passed_params | |
| else: | |
| filtered_params = { | |
| k: v for k, v in passed_params.items() | |
| if k in sig.parameters | |
| } | |
| res = dynamic_tmpl.render_fn(**filtered_params) | |
| elif isinstance(passed_params, list): | |
| res = dynamic_tmpl.render_fn(*passed_params) | |
| else: | |
| res = dynamic_tmpl.render_fn(passed_params) |
References
- When validation checks or function arguments can be represented as either a dictionary (named) or a list (positional), always verify the type (e.g., using
isinstance(args, dict)) before performing key-based lookups or assignments to preventTypeErrorcrashes.
There was a problem hiding this comment.
Done! Updated TemplateProcessor.expand_template to inspect render_fn signature and defensively filter passed_params when VAR_KEYWORD (**kwargs) is not accepted.
| if (res.ok) { | ||
| const data = await res.json(); |
There was a problem hiding this comment.
In handleResolveDynamicTemplate, if the fetch request returns a non-ok status (e.g., 400 or 500), the error is not handled or thrown, resulting in a silent failure where the loading spinner disappears but no error is logged or displayed. We should check res.ok and throw an error to trigger the catch block.
if (!res.ok) {
throw new Error("Server returned status " + res.status);
}
const data = await res.json();
There was a problem hiding this comment.
Done! Added if (!res.ok) throw new Error(\Server returned status ${res.status}`)inhandleResolveDynamicTemplate` to ensure errors trigger the catch block and update UI state appropriately.
016c81a to
5e79cfc
Compare
|
Addressed reviewer feedback and synced with upstream schema updates:
|
9c5b47f to
59e5116
Compare
5eecc5e to
3d646c1
Compare
3d646c1 to
baa8533
Compare
…definition specification
673ef10 to
7a1d5e6
Compare
…cursion, and max depth
7a1d5e6 to
48ae813
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
48ae813 to
9265a7b
Compare
…mplates and interactive studio
9265a7b to
d745da8
Compare
Summary
[Part 3 of 4 in Templates Stack - Depends on #2381]
This PR adds the A2UI Community Templates Sample App under
samples/community/templates/.The sample app demonstrates end-to-end server-side template expansion, dynamic server resolvers, and programmatic Python templates with an interactive 3-stage studio and live Gemini LLM generation.
Architectural Highlights
samples/community/templates/templates/with explicitversion: "0.1".server.py):EmployeeSalaryCard: Data-binding mode template querying simulated confidential HR databases.PayrollSummary: Programmatic dynamic template performing Python arithmetic and loops to output a dynamic compensation table.TemplateInferenceFormat.client/):AGENTS.mdscript targets (build,lint,lint:fix,format,format:check,test).Testing & Verification
test_e2e.mjs):Stack Context