a2ui-project / a2ui-project/a2ui

feat(builder): support data model updates in fluent builder API

Open
#2,525 0 comments 0 reactions 0 assignees View on GitHub
component: agent_sdk P2 type: feature/enhancement
Dominant language
TypeScript
Stars
16.4k
Forks
1.3k
Avg merge
2d 13h
Merged PRs (30d)
134

Description

## Background

The A2UI v0.9.1 protocol defines the `updateDataModel` message to initialize and modify the untyped data model associated with a surface ([a2ui_protocol.md](specification/v0_9_1/docs/a2ui_protocol.md#updatedatamodel)):

```json
{
"version": "v0.9.1",
"updateDataModel": {
"surfaceId": "user_profile_card",
"path": "/user/name",
"value": "Jane Doe"
}
}
```

The fluent builder API currently provides envelope helpers for component trees (`create_surface` and `update_components`). However, it lacks a dedicated helper to emit incremental `updateDataModel` messages. When an agent needs to update state reactively (for example, updating a counter, updating status fields, or streaming data into bound text nodes), developers currently have to construct the envelope dictionary by hand.

Additionally, `create_surface` accepts a `data_model` dictionary, but iterates over top-level keys as individual subpaths (`/key`) rather than allowing the caller to set the root data model at `"/"` directly.

---

## Proposed API

### 1. Standalone `update_data_model` helper

Add a top-level helper function in `a2ui.builder.base`:

```python
_UNDEFINED = object()

def update_data_model(
surface_id: str,
value: Any = _UNDEFINED,
*,
path: str = "/",
) -> list[dict[str, Any]]:
"""Creates an updateDataModel envelope message.

Args:
surface_id: The target surface identifier.
value: The data to set at the path. If omitted, signals key removal per protocol.
path: JSON Pointer location within the data model. Defaults to "/".

Returns:
A list containing the updateDataModel envelope message.
"""
norm_path = path if path.startswith("/") else f"/{path}"
payload: dict[str, Any] = {
"surfaceId": surface_id,
"path": norm_path,
}
if value is not _UNDEFINED:
payload["value"] = value

return [{"updateDataModel": payload}]
```

*Notes on the return type*:
* Returning `list[dict[str, Any]]` matches `create_surface` and `update_components`, which both return lists of protocol messages ready to yield or transmit.
* Alternatively, a single envelope dictionary `{"updateDataModel": payload}` could be returned, or both could be supported (e.g. `to_data_update` on a container vs `update_data_model` functional helper).

### 2. Initial data model in `create_surface`

Refactor the `data_model` parameter in `create_surface` and `ComponentTree.to_surface` to support setting the root data model:

```python
def create_surface(
surface_id: str,
root: ComponentBuilderNode,
*,
catalog_id: str | None = None,
data_model: Any = None,
) -> list[dict[str, Any]]:
```

Behavior when `data_model` is provided:
* If `data_model` is a dictionary where all keys begin with `"/"`, emit one `updateDataModel` message per key-value entry (treating each key as a JSON Pointer path).
* Otherwise, emit a single `updateDataModel` message with `path="/"` and `value=data_model`. This allows initializing the entire data model with a standard dictionary in a single message.

Example usage:

```python
from a2ui.builder.base import create_surface, update_data_model
from a2ui.builder.catalogs.basic import Card, Text

tree = Card(child=Text(text=bind("/user/name")))

# Create surface with initial root data model:
messages = create_surface(
"main",
root=tree,
data_model={"user": {"name": "Alice"}},
)

# Later, update a specific path incrementally:
patch = update_data_model("main", value="Bob", path="/user/name")

# Or delete a key per v0.9.1 specification (omitting value):
removal = update_data_model("main", path="/user/temp")
```

---

## Implementation suggestions

1. **Deletion semantics**: In the A2UI v0.9.1 JSON schema (`server_to_client.json`), the `value` field is optional. If present, the value at `path` is replaced or created; if omitted, the key at `path` is removed. Using a sentinel like `_UNDEFINED = object()` allows distinguishing between an omitted value (deletion) and an explicit `None` (JSON `null`).
2. **Path normalization**: Normalize paths so they always have a leading `/` (e.g. `"user/name"` -> `"/user/name"`).
3. **Exports**: Export `update_data_model` from `a2ui.builder` and include it in `__all__`.
4. **CLI code generators**: Update Python emitters in Dart CLI (`dart/a2ui_cli/lib/src/emitters/python/python_emitter.dart`) and TypeScript CLI (`javascript/a2ui_cli/src/emitters/python/python-emitter.ts`) to re-export `update_data_model` from generated catalog packages.
5. **Documentation**: Document `update_data_model` and `data_model` parameter in `agent_sdks/python/a2ui_agent/src/a2ui/builder/README.md`.

---

## Checklist

- [ ] Add `update_data_model` in `agent_sdks/python/a2ui_agent/src/a2ui/builder/base.py` and export in `__init__.py`.
- [ ] Update `ComponentTree.to_surface` and `create_surface` data model handling.
- [ ] Add unit tests in `agent_sdks/python/a2ui_agent/tests/test_pydantic_builders.py`.
- [ ] Update Dart CLI Python emitter and tests.
- [ ] Update TypeScript CLI Python emitter and tests.
- [ ] Update documentation in `builder/README.md`.

Contributor guide

Open the contributing guide

Research direction

The main implementation is in `agent_sdks/python/a2ui_agent/src/a2ui/builder/base.py`. Start by reading the existing `create_surface` and `update_components` helpers to understand the envelope pattern. Add the `update_data_model` function as described, then update `create_surface`'s data model handling. Run the unit tests in `agent_sdks/python/a2ui_agent/tests/test_pydantic_builders.py`. Also update the CLI emitters in the Dart and TypeScript CLI codebases as listed in the checklist.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart, python, typescript
Domain
api, backend-api-design, tooling
Issue type
Feature
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.