Skip to content

feat(elemental): add CLI verification scripts and developer guide#2099

Open
dongai-z wants to merge 1 commit into
a2ui-project:mainfrom
AGenUI:feat/elemental_docs
Open

feat(elemental): add CLI verification scripts and developer guide#2099
dongai-z wants to merge 1 commit into
a2ui-project:mainfrom
AGenUI:feat/elemental_docs

Conversation

@dongai-z

@dongai-z dongai-z commented Jul 26, 2026

Copy link
Copy Markdown

Summary

This adds the CLI tooling and a developer guide for the A2UI Elemental proposal, bringing it to parity with A2UI Express. The Elemental format implementation (inference_formats/experimental/elemental) and spec (a2ui_elemental.md, implementation_plan.md) already exist, but there was no runnable way to compile, decompile, or generate prompts from the command line. These scripts mirror specification/proposals/express/scripts/ and specification/proposals/express/README.md.

What's added

  • specification/proposals/elemental/scripts/run_compiler.py — Elemental markup → standard A2UI v1.0 JSON
  • specification/proposals/elemental/scripts/run_decompiler.py — A2UI JSON (gallery example or raw envelope) → Elemental markup
  • specification/proposals/elemental/scripts/run_prompt_generator.py — catalog → Elemental system prompt (markup rules + per-component TSX interfaces)
  • specification/proposals/elemental/README.md — developer guide with uv run invocations

Design

The scripts follow the existing Express script conventions: Apache 2.0 header, module docstring, sys.path insertion of agent_sdks/python/a2ui_agent/src, default v1_0/catalogs/basic/catalog.json, Error:-to-stderr with sys.exit(0/1), and stdout output. They are catalog-agnostic (no hardcoded component names).

Differences from the Express scripts

These follow from Elemental's semantics:

  1. spec_version="1.0" (Express uses "0.9.1") — the default catalog and examples are v1.0.
  2. run_decompiler.py merges updateDataModel into the reconstructed createSurface (Express only reads updateComponents) — Elemental renders the data model as a <script type="application/json"> block, so dropping it would lose surface data.
  3. run_decompiler.py wraps output in the <a2ui> sentinel via wrap_decompiled_blocks — this matches the shape a model emits and what run_compiler.py accepts back.
  4. run_compiler.py uses ElementalParser (Express calls ExpressCompiler directly) — ElementalCompiler.compile expects a bare <body> and does not unwrap the <a2ui> sentinel, so the parser layer is required to accept model-shaped input.

Verification

Run from specification/proposals/elemental (no environment variable required — Elemental is not gated):

cd specification/proposals/elemental
EX=../../v1_0/catalogs/basic/examples/01_flight-status.json
uv run --project ../../../agent_sdks/python/a2ui_agent scripts/run_decompiler.py "$EX" > /tmp/flight.elemental
uv run --project ../../../agent_sdks/python/a2ui_agent scripts/run_compiler.py /tmp/flight.elemental
uv run --project ../../../agent_sdks/python/a2ui_agent scripts/run_prompt_generator.py

Round-trips the 01_flight-status example (22 components + full data model preserved) and generates the prompt (18 component interfaces).

Testing

  • Ran all three scripts via uv run against the basic v1.0 catalog.
  • pyink and prettier formatting checks pass.

Notes

  • A2UI Elemental is @experimental.
  • No environment gate (unlike Express's A2UI_EXPRESS_ENABLED).
  • Scripts require Python 3.8+.

Pre-launch Checklist

One time:

For this PR:

  • I have updated the relevant CHANGELOG.md file.
  • I updated/added relevant documentation.
  • My code changes (if any) have tests.
  • If my branch is on a fork, I have verified that scripts/e2e_test.sh passes.

If you need help, consider asking for advice on the discussion board.

@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 A2UI Elemental, a model-optimized declarative UI format, along with a developer guide and three CLI utility scripts for compiling, decompiling, and generating system prompts. The feedback highlights several areas for improvement, including handling cases where input JSON is a list or contains non-dictionary elements to prevent AttributeError crashes, correcting the return type annotation of the compiler function, ensuring case-insensitive detection of the sentinel to avoid double wrapping, and removing duplicate json imports in the script files.

Comment on lines +84 to +86
messages = ex_data.get("messages")
if messages is None:
messages = [ex_data]

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.

high

If ex_data is a list (which is a common format for A2UI examples, as seen in catalog.json's examples), calling ex_data.get("messages") will raise an AttributeError: 'list' object has no attribute 'get'. We should check if ex_data is a list first to handle both gallery wrappers and raw lists of envelopes robustly.

Suggested change
messages = ex_data.get("messages")
if messages is None:
messages = [ex_data]
if isinstance(ex_data, list):
messages = ex_data
else:
messages = ex_data.get("messages")
if messages is None:
messages = [ex_data]
References
  1. When handling JSON payloads that can be either a list of objects or a single object, use type checking (such as isinstance(..., list)) to safely handle both cases and avoid indexing errors.

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

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]]":

Comment on lines +97 to +98
if "<a2ui>" not in elemental_text:
elemental_text = f"<a2ui>\n{elemental_text}\n</a2ui>"

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>"

Comment on lines +45 to +47
)
import json
from a2ui.core.catalog import Catalog

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)

Comment on lines +45 to +47
)
import json
from a2ui.core.catalog import Catalog

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)

Comment on lines +88 to +103
for msg in messages:
if "createSurface" in msg:
create = msg["createSurface"]
surface_id = create.get("surfaceId", surface_id)
catalog_id = create.get("catalogId", catalog_id)
if create.get("components"):
components_list = create["components"]
if create.get("dataModel"):
data_model = create["dataModel"]
if "updateComponents" in msg:
update = msg["updateComponents"]
components_list = update.get("components", components_list)
surface_id = update.get("surfaceId", surface_id)
if "updateDataModel" in msg:
update_dm = msg["updateDataModel"]
data_model = update_dm.get("value", update_dm.get("contents", data_model))

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 msg is a dictionary, but createSurface, updateComponents, or updateDataModel are not dictionaries (e.g., they are null, strings, or lists due to malformed input), calling .get() on them will raise an AttributeError. To make the script more robust and prevent crashes on malformed or unexpected JSON inputs, we should verify that these objects are dictionaries before calling .get().

    for msg in messages:
        if not isinstance(msg, dict):
            continue
        if "createSurface" in msg:
            create = msg["createSurface"]
            if isinstance(create, dict):
                surface_id = create.get("surfaceId", surface_id)
                catalog_id = create.get("catalogId", catalog_id)
                if create.get("components"):
                    components_list = create["components"]
                if create.get("dataModel"):
                    data_model = create["dataModel"]
        if "updateComponents" in msg:
            update = msg["updateComponents"]
            if isinstance(update, dict):
                components_list = update.get("components", components_list)
                surface_id = update.get("surfaceId", surface_id)
        if "updateDataModel" in msg:
            update_dm = msg["updateDataModel"]
            if isinstance(update_dm, dict):
                data_model = update_dm.get("value", update_dm.get("contents", data_model))
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.

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