-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat(elemental): add CLI verification scripts and developer guide #2099
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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 | ||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The return type annotation of
Suggested change
|
||||||||||||||
| """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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the input text contains
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| 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() | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The standard library module
jsonis 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.References