Skip to content
Closed
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
72 changes: 21 additions & 51 deletions specification/v0_9/docs/a2ui_protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,9 @@ flowchart TD

```

## Data Model Representation: Binding, Scope, and Interpolation
## Data Model Representation: Binding and Scope

This section describes how UI components **represent** and reference data from the Data Model. A2UI relies on a strictly defined relationship between the UI structure (Components) and the state (Data Model), defining the mechanics of path resolution, variable scope during iteration, and interpolation.
This section describes how UI components **represent** and reference data from the Data Model. A2UI relies on a strictly defined relationship between the UI structure (Components) and the state (Data Model), defining the mechanics of path resolution, variable scope during iteration.

### Path Resolution & Scope

Expand Down Expand Up @@ -342,22 +342,6 @@ When a container component (such as `Column`, `Row`, or `List`) utilizes the **T
}
```

#### Client-Side Functions
Copy link
Collaborator

Choose a reason for hiding this comment

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

@gspencergoog this defines a DSL to call functions from within string templates. I think if we do decide to support a syntax like this, we should move this specification to the "standard catalog" section and make it specific to the string_format function etc. So maybe string_format happens to use some magical DSL and be able to refer to the data model and other client-side functions directly (we will expose the necessary rendering framework APIs to make this feasible to implement). A different catalog can invent some other magical DSL. But the core A2UI specification is more purely JSON-based.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, this section should be removed. I agree that it should only be part of the string_format function's definition, and not general for all strings. (I thought I had already removed that when I added string_format).


Results of client-side functions can be interpolated. Function calls are identified by the presence of parentheses `()`.

- `${now()}`: A function with no arguments.
- `${formatDate(${/currentDate}, 'yyyy-MM-dd')}`: A function with positional arguments.

Arguments can be **Literals** (quoted strings, numbers, or booleans), or **Nested Expressions**.

#### Nested Interpolation

Expressions can be nested using additional `${...}` wrappers inside an outer expression to make bindings explicit or to chain function calls.

- **Explicit Binding**: `${formatDate(${/currentDate}, 'yyyy-MM-dd')}`
- **Nested Functions**: `${upper(${now()})}`

#### Type Conversion

When a non-string value is interpolated, the client converts it to a string:
Expand Down Expand Up @@ -568,61 +552,47 @@ The [`standard_catalog.json`] provides the baseline set of components and functi

### Functions

#### Validation Functions

| Function | Description |
| :---------------- | :----------------------------------------------------------------------- |
| **required** | Checks that the value is not null, undefined, or empty. |
| **regex** | Checks that the value matches a regular expression string. |
| **length** | Checks string length constraints. |
| **numeric** | Checks numeric range constraints. |
| **email** | Checks that the value is a valid email address. |
| **string_format** | Does string interpolation of data model values and registered functions. |

### The `string_format` function

The `string_format` function supports embedding dynamic expressions directly within string properties. This allows for mixing static text with data model values and function results.

#### _Syntax_

Interpolated expressions are enclosed in `${...}`. To include a literal `${` in a string, it must be escaped as `\${`.
#### String Formatting Functions

#### _Data Model Binding_

Values from the data model can be interpolated using their JSON Pointer path.

- `${/user/profile/name}`: Absolute path.
- `${firstName}`: Relative path (resolved against the current collection scope).
| Function | Description |
| :---------------- | :---------------------------------------------------------------------------------- |
| **formatString** | Performs string interpolation on a template string using named arguments. The template contains placeholders in the `${key}` format, which are replaced by the corresponding values from the `args` object. To include a literal `${` in a string, it must be escaped as `\${`. |
| **formatNumber** | Formats a number with the specified grouping and decimal precision. |
| **formatCurrency**| Formats a number as a currency string. |
| **formatDate** | Formats a timestamp into a string using a pattern. |
| **pluralize** | Returns the singular string if the count is 1, otherwise returns the plural string. |

**Example:**

```json
{
"id": "user_welcome",
"id": "receipt",
"component": "Text",
"text": {
"call": "string_format",
"call": "formatString",
"args": {
"value": "Hello, ${/user/firstName}! Welcome back to ${/appName}."
"template": "Bought ${quantity} ${item} on ${date} for ${price}.",
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see what you're going for here, but it seems like it's a lot more verbose than the previous design, and still requires parsing/validating of the template to interpolate the values, and escaping of the template markers. I realize that it's a simpler parser, but it's not that much simpler (and that's not really where we want to optimize complexity).

An equivalent string_format argument would be:

"Bought ${formatNumber(${/transaction/count}, 0)} ${pluralize(${/transaction/count}, 'apple', 'apples')}  on ${formatDate(${/transaction/timestamp}, 'MMM d, yyyy')} for ${formatCurrency{${/transaction/amount}, 'USD')}"

To me, this seems easier/smaller to generate, since it doesn't require the extra argument names, "call", and "args" objects and eliminates much of the nesting. It has only marginally harder parsing complexity (which we'll put into a reusable SDK function anyhow).

"args": {
"quantity": { "call": "formatNumber", "args": { "value": { "path": "/transaction/count" }, "decimals": 0 } },
"item": { "call": "pluralize", "args": { "value": { "path": "/transaction/count" }, "singular": "apple", "plural": "apples" } },
"date": { "call": "formatDate", "args": { "value": { "path": "/transaction/timestamp" }, "pattern": "MMM d, yyyy" } },
"price": { "call": "formatCurrency", "args": { "value": { "path": "/transaction/amount" }, "currencyCode": "USD" } }
}
}
}
}
```

#### _Client-Side Functions_

Results of client-side functions can be interpolated. Function calls are identified by the presence of parentheses `()`.

- `${now()}`: A function with no arguments.
- `${formatDate(${/currentDate}, 'yyyy-MM-dd')}`: A function with positional arguments.

Arguments can be **Literals** (quoted strings, numbers, or booleans), or **Nested Expressions**.

#### _Nested Interpolation_

Expressions can be nested using additional `${...}` wrappers inside an outer expression to make bindings explicit or to chain function calls.

- **Explicit Binding**: `${formatDate(${/currentDate}, 'yyyy-MM-dd')}`
- **Nested Functions**: `${upper(${now()})}`

#### _Type Conversion_

When a non-string value is interpolated, the client converts it to a string:
Expand Down
6 changes: 2 additions & 4 deletions specification/v0_9/docs/evolution_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,8 @@ Specifying an unknown surfaceId will cause an error. It is recommended that clie

**v0.9:**

- **String Formatting**: Introduced the `string_format` function, which supports `${expression}` syntax for interpolation.
- **Unified Expression Language**: Allows embedding JSON Pointer paths (absolute and relative) and client-side function calls directly within the format string.
- **Nesting**: Supports recursive nesting of expressions (e.g., `${formatDate(${/timestamp}, 'yyyy-MM-dd')}`).
- **Reason**: Improves readability for complex strings. Instead of generating complex nested JSON objects (like chained concatenations) to combine strings and data, the model can write natural-looking template literals within the `string_format` function.
- **String Formatting**: Introduced the `formatString` function, which supports `${expression}` syntax for interpolation.
- **Reason**: Allows generating complex strings with functions for formatting.

### 5.4. Data Synchronization

Expand Down
107 changes: 97 additions & 10 deletions specification/v0_9/json/standard_catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -716,20 +716,107 @@
}
},
{
"name": "string_format",
"description": "Performs string interpolation of data model values and other functions in the catalog functions list and returns the resulting string. The value string can contain interpolated expressions in the `${expression}` format. Supported expression types include: JSON Pointer paths to the data model (e.g., `${/absolute/path}` or `${relative/path}`), and client-side function calls (e.g., `${now()}`). Function arguments must be literals (quoted strings, numbers, booleans) or nested expressions (e.g., `${formatDate(${/currentDate}, 'MM-dd')}`). To include a literal `${` sequence, escape it as `\\${`.",
"name": "formatString",
"description": "Performs string interpolation on a template string using named arguments. The template can contain placeholders in the `${key}` format, which are replaced by the corresponding values from the `args` object.",
"returnType": "string",
"parameters": {
"allOf": [
{ "$ref": "#/$defs/valueParam" },
{
"type": "object",
"required": [ "template", "args" ],
"properties": {
"template": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "The template string containing `${key}` placeholders."
},
"args": {
"type": "object",
"properties": {
"value": { "type": "string" }
}
"description": "A map of named values to be interpolated into the template. Keys must match the placeholders in the template.",
"additionalProperties": true
}
],
"unevaluatedProperties": false
}
}
},
{
"name": "formatNumber",
Copy link
Collaborator

Choose a reason for hiding this comment

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

I prefer this lower-magic version of string interpolation function calling overall. But I still wonder if there is some middle ground where you need to explicitly pass arguments to stringFormat, but these specific formatting options for certain types are still built into the formatString function rather than separated out, which makes the "call sites" super complex.

E.g. trying to take inspiration from Python:

{
  "id": "receipt",
  "component": "Text",
  "text": {
    "call": "formatString",
    "args": {
      "template": "Bought ${quantity} ${item} on ${date:%m %d, %Y} for ${price.2f}.",
      "args": {
        "quantity": {path: "/transaction/count"},
        "item": {path: "/transaction/name"},
        "date": {path: "/transaction/timestamp"},
        "price": {path: "/transaction/amount"},
      }
    }
  }
}

At least this way, the core spec and data flow is still pretty simple and synchronous, but the standard catalog just happens to have a slightly magical function that can understand some fancy formatting placeholders.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That was my original proposal, but I thought we wanted to do the dart / calling functions approach for string formatting. I'd actually prefer just a single string_format function if that what you prefer. @gspencergoog thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I talked this over with Jacob, and I still think that I'd rather use the more "modern" version of string interpolation that just takes a single string:

  • It it just simpler to construct if the LLM needs to construct one string instead of a string and also a separate set of nested args.
  • They both have equivalent functionality, and don't provide any extra validation, since any of these options need to have validation that involves validating the template or string against the args anyhow.
  • Having a fixed set of built in formatting options is not as flexible in the long run, although I guess there's nothing saying we couldn't have some built in ones along with the ability to interpolate function outputs.
  • Also, with a single string there's no chance of having a mismatch between the number of arguments and the substitution points.

So, how about this compromise: We keep the args in the string, but include some basic formatters that don't need function calls?

Like so:

{
  "id": "receipt",
  "component": "Text",
  "text": {
    "call": "formatString",
    "args": {
      "template": "Bought ${/transaction/quantity} ${/transaction/name} on ${/transaction/timestamp:%m %d, %Y} for ${/transaction/amount:.2f}.",
    }
  }
}

In the end, I think the best solution will be whatever generates the best results from the various LLMs, and we kind of need to run an implementation through evals to know that definitively.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Just wanted to add: if we remove mention of the DSL above from the core A2UI specification, and just include it in the definition for the stringFormat function in the standard catalog, then developers using custom catalogs would be completely free to implement your exact proposal, @wrenj .

So people using the standard catalog get a convenient string format syntax that is (hopefully!) simple enough for humans and LLMs to write directly. But Catalog creators have the hooks they need to innovate with other approaches, and they are not tethered to the DSL we are using in the standard catalog.

Re the actual DSL (sorry, not sure if it technically counts as a language), maybe we should try to stick more closely to the Python fstring format, e.g. by using {/dataModelReference} instead of ${/dataModelReference}, so the LLM can leverage its knowledge there. Though this might encourage it to hallucinate python code and formatting options. Just seems confusing to be quite close to Python, but then arbitrarily make some different design decisions.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Re the actual DSL (sorry, not sure if it technically counts as a language), maybe we should try to stick more closely to the Python fstring format,

It was intended to be close to TypeScript, JavaScript, Dart, Bash, Kotlin, PHP and Groovy. I figured that was a larger learning base than Python and Rust.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It was intended to be close to TypeScript, JavaScript, Dart, Bash, Kotlin, PHP and Groovy. I figured that was a larger learning base than Python and Rust.

Oh that makes perfect sense. My language knowledge is not as broad as yours :-D. I was referring to the colon syntax in {/transaction/amount:.2f} which I suggested based on Python, but probably its the same as other languages.

Overall, I don't feel strongly other than borrowing familiar syntax ideas, which we already have!

"description": "Formats a number with the specified grouping and decimal precision.",
"returnType": "string",
"parameters": {
"type": "object",
"required": [ "value" ],
"properties": {
"value": {
"$ref": "common_types.json#/$defs/DynamicNumber",
"description": "The number to format."
},
"decimals": {
"$ref": "common_types.json#/$defs/DynamicNumber",
"description": "Optional. The number of decimal places to show. Defaults to 0 or 2 depending on locale."
},
"useGrouping": {
"$ref": "common_types.json#/$defs/DynamicBoolean",
"description": "Optional. If true, uses locale-specific grouping separators (e.g. '1,000'). If false, returns raw digits (e.g. '1000'). Defaults to true."
}
}
}
},
{
"name": "formatCurrency",
"description": "Formats a number as a currency string.",
"returnType": "string",
"parameters": {
"type": "object",
"required": [ "value" ],
"properties": {
"value": {
"$ref": "common_types.json#/$defs/DynamicNumber",
"description": "The monetary amount."
},
"currencyCode": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "Optional. The ISO 4217 currency code (e.g., 'USD', 'EUR'). If omitted, uses the device's default currency."
Copy link
Collaborator

Choose a reason for hiding this comment

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

How does the LLM know the device's default currency to be able to put in the relevant value?

}
}
}
},
{
"name": "formatDate",
"description": "Formats a timestamp into a string using a pattern.",
"returnType": "string",
"parameters": {
"type": "object",
"required": [ "value", "pattern" ],
"properties": {
"value": {
"$ref": "common_types.json#/$defs/DynamicDate",
"description": "The date to format."
},
"pattern": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "A Unicode TR35 date pattern string.\n\nToken Reference:\n- Year: 'yy' (26), 'yyyy' (2026)\n- Month: 'M' (1), 'MM' (01), 'MMM' (Jan), 'MMMM' (January)\n- Day: 'd' (1), 'dd' (01), 'E' (Tue), 'EEEE' (Tuesday)\n- Hour (12h): 'h' (1-12), 'hh' (01-12) - requires 'a' for AM/PM\n- Hour (24h): 'H' (0-23), 'HH' (00-23) - Military Time\n- Minute: 'mm' (00-59)\n- Second: 'ss' (00-59)\n- Period: 'a' (AM/PM)\n\nExamples:\n- 'MMM dd, yyyy' -> 'Jan 16, 2026'\n- 'HH:mm' -> '14:30' (Military)\n- 'h:mm a' -> '2:30 PM'\n- 'EEEE, d MMMM' -> 'Friday, 16 January'"
}
}
}
},
{
"name": "pluralize",
"description": "Returns the singular string if the count is 1, otherwise returns the plural string.",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that this definition is only sufficient for English text. Other languages will need more complex rules, and more arguments.

"returnType": "string",
"parameters": {
"type": "object",
"required": [ "value", "singular", "plural" ],
"properties": {
"value": {
"$ref": "common_types.json#/$defs/DynamicNumber",
"description": "The numeric value to check."
},
"singular": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "The string to return if count is exactly 1."
},
"plural": {
"$ref": "common_types.json#/$defs/DynamicString",
"description": "The string to return if count is not 1."
}
}
}
}
],
Expand Down
Loading