Skip to content

feat(samples): add community templates sample app with version 0.1 templates and interactive studio - #2382

Open
jacobsimionato wants to merge 21 commits into
a2ui-project:mainfrom
jacobsimionato:feat/templates-community-sample
Open

feat(samples): add community templates sample app with version 0.1 templates and interactive studio#2382
jacobsimionato wants to merge 21 commits into
a2ui-project:mainfrom
jacobsimionato:feat/templates-community-sample

Conversation

@jacobsimionato

Copy link
Copy Markdown
Collaborator

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

  • Self-Contained Architecture:
    • All 11 sample template YAML definitions live directly inside samples/community/templates/templates/ with explicit version: "0.1".
    • Clean separation from SDK internals and specification docs.
  • FastAPI Backend (server.py):
    • Ingests local YAML templates and registers dynamic resolver templates.
    • 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.
    • Live Gemini API integration using TemplateInferenceFormat.
  • React 19 + Vite Frontend (client/):
    • Dynamic Template 3-Stage Studio:
      • Stage 1 (LLM Input): Interactive form inputs generating the compact Express DSL signature emitted by an LLM.
      • Stage 2 (Server Resolver / Blueprint): Live code view of the underlying YAML blueprint or Python generator function.
      • Stage 3 (Resolved Output): Live rendered output displaying resolved data figures.
    • Template Library Studio: Interactive browser for all registered templates with line-numbered YAML declaration view, sample parameter inspector, and live inflated preview.
    • Inspection Drawer: Full-height drawer showing side-by-side raw LLM Express DSL and expanded A2UI JSON messages.
    • Standard Yarn workspace integration conforming to AGENTS.md script targets (build, lint, lint:fix, format, format:check, test).

Testing & Verification

  • Playwright End-to-End Test Suite (test_e2e.mjs):
    • Verifies all 8 preset scenarios: Verified Salary, Payroll Summary, User Profile, Team Roster, Team Goals, Feedback Board, Competency Panel.
    • Verifies format drawer displays raw Express DSL and expanded JSON.
    • Verifies Dynamic Template 3-Stage Studio interactive inputs and execution.
    • Verifies live Gemini LLM generation and card rendering.
  • All tests pass with code 0.

Stack Context

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread samples/community/templates/server.py Outdated
Comment thread agent_sdks/python/a2ui_agent/pyproject.toml
Comment on lines +616 to +652
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."
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
  1. 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 prevent TypeError crashes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
  1. 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 prevent TypeError crashes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Updated TemplateProcessor.expand_template to inspect render_fn signature and defensively filter passed_params when VAR_KEYWORD (**kwargs) is not accepted.

Comment on lines +262 to +263
if (res.ok) {
const data = await res.json();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jacobsimionato
jacobsimionato force-pushed the feat/templates-community-sample branch from 016c81a to 5e79cfc Compare August 25, 2026 04:05
@jacobsimionato

Copy link
Copy Markdown
Collaborator Author

Addressed reviewer feedback and synced with upstream schema updates:

  1. Error Handling: Updated App.tsx in handleResolveDynamicTemplate to check if (!res.ok) throw new Error(Server returned status ${res.status}) so that backend failures trigger error states rather than failing silently.
  2. Local Template Updates: Synced section_card.yaml (type: child for headerAction) and two_column_layout.yaml (headerChild sampleData).
  3. Verification: 100% of Playwright E2E preset, inspector, library, and dynamic studio tests pass.

@jacobsimionato
jacobsimionato force-pushed the feat/templates-community-sample branch 3 times, most recently from 9c5b47f to 59e5116 Compare August 25, 2026 04:34
@jacobsimionato
jacobsimionato force-pushed the feat/templates-community-sample branch 5 times, most recently from 5eecc5e to 3d646c1 Compare August 25, 2026 05:47
@jacobsimionato
jacobsimionato force-pushed the feat/templates-community-sample branch from 3d646c1 to baa8533 Compare August 26, 2026 01:29
@jacobsimionato
jacobsimionato force-pushed the feat/templates-community-sample branch 5 times, most recently from 673ef10 to 7a1d5e6 Compare August 26, 2026 03:36
@jacobsimionato
jacobsimionato force-pushed the feat/templates-community-sample branch from 7a1d5e6 to 48ae813 Compare August 26, 2026 03:53
@jacobsimionato
jacobsimionato force-pushed the feat/templates-community-sample branch from 48ae813 to 9265a7b Compare August 26, 2026 04:01
@jacobsimionato
jacobsimionato force-pushed the feat/templates-community-sample branch from 9265a7b to d745da8 Compare August 26, 2026 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant