feat(elemental): add CLI verification scripts and developer guide#2099
feat(elemental): add CLI verification scripts and developer guide#2099dongai-z wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
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.
| messages = ex_data.get("messages") | ||
| if messages is None: | ||
| messages = [ex_data] |
There was a problem hiding this comment.
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.
| 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
- 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.
| def compile_elemental_file( | ||
| elemental_path: str, catalog_path: str, surface_id=None | ||
| ) -> dict: |
There was a problem hiding this comment.
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]].
| 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]]": |
| if "<a2ui>" not in elemental_text: | ||
| elemental_text = f"<a2ui>\n{elemental_text}\n</a2ui>" |
There was a problem hiding this comment.
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.
| 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>" |
| ) | ||
| import json | ||
| from a2ui.core.catalog import Catalog |
There was a problem hiding this comment.
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.
| ) | |
| import json | |
| from a2ui.core.catalog import Catalog | |
| ) | |
| from a2ui.core.catalog import Catalog |
References
- Imports should be grouped and duplicate imports should be avoided. (link)
| ) | ||
| import json | ||
| from a2ui.core.catalog import Catalog |
There was a problem hiding this comment.
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.
| ) | |
| import json | |
| from a2ui.core.catalog import Catalog | |
| ) | |
| from a2ui.core.catalog import Catalog |
References
- Imports should be grouped and duplicate imports should be avoided. (link)
| 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)) |
There was a problem hiding this comment.
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
- 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.
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 mirrorspecification/proposals/express/scripts/andspecification/proposals/express/README.md.What's added
specification/proposals/elemental/scripts/run_compiler.py— Elemental markup → standard A2UI v1.0 JSONspecification/proposals/elemental/scripts/run_decompiler.py— A2UI JSON (gallery example or raw envelope) → Elemental markupspecification/proposals/elemental/scripts/run_prompt_generator.py— catalog → Elemental system prompt (markup rules + per-component TSX interfaces)specification/proposals/elemental/README.md— developer guide withuv runinvocationsDesign
The scripts follow the existing Express script conventions: Apache 2.0 header, module docstring,
sys.pathinsertion ofagent_sdks/python/a2ui_agent/src, defaultv1_0/catalogs/basic/catalog.json,Error:-to-stderr withsys.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:
spec_version="1.0"(Express uses"0.9.1") — the default catalog and examples are v1.0.run_decompiler.pymergesupdateDataModelinto the reconstructedcreateSurface(Express only readsupdateComponents) — Elemental renders the data model as a<script type="application/json">block, so dropping it would lose surface data.run_decompiler.pywraps output in the<a2ui>sentinel viawrap_decompiled_blocks— this matches the shape a model emits and whatrun_compiler.pyaccepts back.run_compiler.pyusesElementalParser(Express callsExpressCompilerdirectly) —ElementalCompiler.compileexpects 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):Round-trips the
01_flight-statusexample (22 components + full data model preserved) and generates the prompt (18 component interfaces).Testing
uv runagainst the basic v1.0 catalog.pyinkandprettierformatting checks pass.Notes
@experimental.A2UI_EXPRESS_ENABLED).Pre-launch Checklist
One time:
For this PR:
If you need help, consider asking for advice on the discussion board.