a2ui-project / a2ui-project/a2ui

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

Đang mở
#2,525 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
component: agent_sdk P2 type: feature/enhancement
Ngôn ngữ chính
TypeScript
Star
16.4k
Fork
1.3k
Merge trung bình
2 ngày 13 giờ
Pull request đã merge (30 ngày)
134

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.