Skip to content
Open
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
85 changes: 85 additions & 0 deletions specification/proposals/elemental/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# A2UI Elemental developer guide

A2UI Elemental is a model-optimized declarative UI format that uses plain HTML5-like
custom-element markup (no custom JavaScript or CSS) as the inference format. A host-side
compiler parses this markup and compiles it into standard A2UI v1.0 wire protocol payloads.

This guide covers the standalone developer scripts in
`specification/proposals/elemental/scripts/` for compiling, decompiling, and generating
prompts from a catalog.

> **Note:** A2UI Elemental is `@experimental` and may change without notice. Unlike A2UI
> Express, it is **not** gated behind an environment variable — no `A2UI_*_ENABLED` flag is
> required.

---

## Prerequisites

The scripts load the experimental Elemental code from the repo source and resolve the rest
of the A2UI SDK (and its dependencies such as `google-adk` and `antlr4`) from the
`a2ui_agent` project. The simplest way to run them is with [`uv`](https://docs.astral.sh/uv/),
which provisions the project environment automatically:

```bash
cd specification/proposals/elemental
```

All commands below run from that directory and use
`uv run --project ../../../agent_sdks/python/a2ui_agent` so the correct environment is used
regardless of your global Python setup.

---

## CLI utility reference

### Prompt generation

Generate the model system prompt contract, containing the HTML5 markup rules and the
per-component TypeScript/TSX interface signatures compiled from the active catalog schema:

```bash
uv run --project ../../../agent_sdks/python/a2ui_agent scripts/run_prompt_generator.py \
--catalog ../../v1_0/catalogs/basic/catalog.json
```

### Markup compiler (Elemental → A2UI JSON)

Compile an A2UI Elemental markup file (optionally wrapped in `<a2ui>` sentinels, i.e. the
shape a model emits) into standard, pretty-printed A2UI v1.0 JSON:

```bash
uv run --project ../../../agent_sdks/python/a2ui_agent scripts/run_compiler.py \
path/to/sample.elemental \
--surface-id "dashboard_surface"
```

`--surface-id` is optional: when provided it overrides the surface id, otherwise the id is
taken from the `<body>` element (falling back to `main` if absent).

### Decompiler (A2UI JSON → Elemental)

Convert a standard A2UI v1.0 JSON example (either a gallery example with a `messages`
array or a raw envelope) back into A2UI Elemental markup, wrapped in the `<a2ui>` sentinel:

```bash
uv run --project ../../../agent_sdks/python/a2ui_agent scripts/run_decompiler.py \
../../v1_0/catalogs/basic/examples/01_flight-status.json
```

---

## End-to-end round trip

Decompile a standard example to Elemental, then compile it back to verify fidelity:

```bash
uv run --project ../../../agent_sdks/python/a2ui_agent scripts/run_decompiler.py \
../../v1_0/catalogs/basic/examples/01_flight-status.json > /tmp/flight.elemental

uv run --project ../../../agent_sdks/python/a2ui_agent scripts/run_compiler.py \
/tmp/flight.elemental
```

The decompiler emits the surface (components + data model) as `<a2ui>`-wrapped markup;
the compiler parses it back into a standard `createSurface` payload.
167 changes: 167 additions & 0 deletions specification/proposals/elemental/scripts/run_compiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Command-line tool to compile A2UI Elemental markup into standard A2UI v1.0 JSON.

Loads an A2UI Elemental file (HTML5-like markup, optionally wrapped in the
`<a2ui>` sentinel and a markdown code fence, i.e. the exact shape a model emits),
parses and compiles it against the specified catalog schema, and prints the
pretty-printed standard A2UI JSON message on stdout.
"""

import argparse
import json
import os
import sys

sys.path.insert(
0,
os.path.abspath(
os.path.join(
os.path.dirname(__file__),
"..",
"..",
"..",
"..",
"agent_sdks",
"python",
"a2ui_agent",
"src",
)
),
)
import json
from a2ui.core.catalog import Catalog
Comment on lines +45 to +47

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

The standard library module json is imported twice in this file (at line 26 and line 46). We should remove the redundant import at line 46 to adhere to PEP 8 style guidelines.

Suggested change
)
import json
from a2ui.core.catalog import Catalog
)
from a2ui.core.catalog import Catalog
References
  1. Imports should be grouped and duplicate imports should be avoided. (link)

from a2ui.inference_formats.experimental.elemental.parser import ElementalParser


def _force_surface_id(envelope: dict, surface_id: str) -> None:
"""Overrides the surfaceId on whichever surface operation the envelope carries."""
for op_key in (
"createSurface",
"updateComponents",
"updateDataModel",
"deleteSurface",
):
op = envelope.get(op_key)
if isinstance(op, dict):
op["surfaceId"] = surface_id


def compile_elemental_file(
elemental_path: str, catalog_path: str, surface_id=None
) -> dict:
Comment on lines +64 to +66

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

The return type annotation of compile_elemental_file is specified as dict. However, at line 119, it can return envelopes (which is a list[dict]) if there are multiple envelopes. This mismatch will cause type checkers like mypy to fail. We should update the return type annotation to Union[dict, List[dict]].

Suggested change
def compile_elemental_file(
elemental_path: str, catalog_path: str, surface_id=None
) -> dict:
def compile_elemental_file(
elemental_path: str, catalog_path: str, surface_id=None
) -> "Union[dict, List[dict]]":

"""Compiles an A2UI Elemental markup file into standard JSON.

Args:
elemental_path: Path to the A2UI Elemental markup file.
catalog_path: Path to the catalog JSON schema.
surface_id: Optional surface identifier. When provided it overrides the
`id` encoded in the `<body>` element; when omitted, the `<body>` id is
used (falling back to "main" if absent).

Returns:
The compiled A2UI v1.0 JSON envelope.

Raises:
FileNotFoundError: If the Elemental or catalog file does not exist.
ValueError: If no A2UI payload could be compiled from the input.
"""
if not os.path.exists(elemental_path):
raise FileNotFoundError(f"Elemental file not found: {elemental_path}")
if not os.path.exists(catalog_path):
raise FileNotFoundError(f"Catalog schema not found: {catalog_path}")

with open(elemental_path, "r", encoding="utf-8") as f:
elemental_text = f.read()

with open(catalog_path, "r", encoding="utf-8") as f:
catalog_dict = json.load(f)
catalog = Catalog.from_json(catalog_dict, spec_version="1.0")

# The parser locates UI blocks by the <a2ui> sentinel. If the input is a
# bare <body> block (no sentinel), wrap it so it can still be compiled.
if "<a2ui>" not in elemental_text:
elemental_text = f"<a2ui>\n{elemental_text}\n</a2ui>"
Comment on lines +97 to +98

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

If the input text contains <A2UI> or <a2Ui> (case-insensitive), the check "<a2ui>" not in elemental_text will evaluate to True, and it will wrap the text in <a2ui>... </a2ui> again. This will result in double wrapping (e.g., <a2ui><A2UI>...</A2UI></a2ui>), which might fail parsing or produce unexpected results. To make it robust against case variations in LLM outputs, we should do a case-insensitive check.

Suggested change
if "<a2ui>" not in elemental_text:
elemental_text = f"<a2ui>\n{elemental_text}\n</a2ui>"
if "<a2ui>" not in elemental_text.lower():
elemental_text = f"<a2ui>\n{elemental_text}\n</a2ui>"


parser = ElementalParser(catalog, surface_id or "main")
parts = parser.parse_response(elemental_text)

envelopes = []
for part in parts:
if getattr(part, "a2ui_json", None):
envelopes.extend(part.a2ui_json)

if not envelopes:
raise ValueError(
"No A2UI payload was produced. Ensure the input is valid A2UI"
" Elemental markup wrapped in <a2ui> ... </a2ui>."
)

# An explicit --surface-id overrides the id encoded in the <body> element.
if surface_id is not None:
for envelope in envelopes:
_force_surface_id(envelope, surface_id)

return envelopes[0] if len(envelopes) == 1 else envelopes


def main():
"""CLI entrypoint for the compiler."""
parser = argparse.ArgumentParser(
description="Compile A2UI Elemental markup into standard A2UI v1.0 wire JSON."
)
parser.add_argument(
"elemental_file", help="Path to the A2UI Elemental markup file to compile."
)
parser.add_argument(
"--surface-id",
default=None,
help=(
"Override the surface id. If omitted, the id is taken from the <body>"
" element (or 'main' when absent)."
),
)
parser.add_argument(
"--catalog",
default=os.path.join(
os.path.dirname(__file__),
"..",
"..",
"..",
"v1_0",
"catalogs",
"basic",
"catalog.json",
),
help="Path to the catalog JSON schema (default: basic catalog).",
)

args = parser.parse_args()

try:
compiled = compile_elemental_file(
args.elemental_file, args.catalog, args.surface_id
)
print(json.dumps(compiled, indent=2))
sys.exit(0)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
main()
Loading
Loading