a2ui-project / a2ui-project/a2ui

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

Aperta
#2,525 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
component: agent_sdk P2 type: feature/enhancement
Lingua principale
TypeScript
Stelle
16.4k
Fork
1.3k
Merge medio
2g 13h
PR unite (30g)
134

Descrizione

## 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`.

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.