Skip to content

Commit 7025cf1

Browse files
fix(python-sdk): support relative data binding paths in v0.9 streaming parser (#1179)
1 parent ec8e019 commit 7025cf1

3 files changed

Lines changed: 104 additions & 5 deletions

File tree

agent_sdks/python/src/a2ui/parser/streaming.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def __new__(cls, catalog: "A2uiCatalog" = None, *args, **kwargs):
7676
return super().__new__(cls)
7777

7878
def __init__(self, catalog: "A2uiCatalog" = None):
79+
self._version = getattr(catalog, "version", None) if catalog else None
7980
self._ref_fields_map = extract_component_ref_fields(catalog) if catalog else {}
8081
self._required_fields_map = (
8182
extract_component_required_fields(catalog) if catalog else {}
@@ -1007,11 +1008,12 @@ def traverse(obj, parent_key=None):
10071008
):
10081009
path = obj["path"]
10091010
key = path.lstrip("/")
1010-
if "componentId" not in obj:
1011-
obj.clear()
1012-
obj.update({"path": "/" + key})
1013-
else:
1014-
# If not in data model, still ensure path has leading slash if it's a bindable object
1011+
if self._version != VERSION_0_9:
1012+
if "componentId" not in obj:
1013+
obj.clear()
1014+
obj.update({"path": "/" + key})
1015+
elif self._version != VERSION_0_9:
1016+
# If not in data model, still ensure path has leading slash if it's a bindable object (v0.8 only)
10151017
current_path = obj.get("path")
10161018
if current_path is not None:
10171019
if not isinstance(current_path, str) or not current_path.startswith("/"):

agent_sdks/python/tests/parser/test_streaming_v08.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,36 @@ def test_streaming_msg_type_deduplication(mock_catalog):
273273

274274
# After completion, msg_types is reset
275275
assert parser.msg_types == []
276+
277+
278+
def test_v08_path_heuristic_adds_slash(mock_catalog):
279+
"""Tests that v0.8 adds a leading slash to relative paths."""
280+
parser = A2uiStreamParser(catalog=mock_catalog)
281+
# Disable validation for simplicity
282+
parser._validator = None
283+
284+
# 1. Send beginRendering first to avoid buffering
285+
chunk_br = (
286+
A2UI_OPEN_TAG
287+
+ '[{"beginRendering": {"surfaceId": "s1", "root": "root"}}]'
288+
+ A2UI_CLOSE_TAG
289+
)
290+
list(parser.process_chunk(chunk_br))
291+
292+
# 2. Send surfaceUpdate with a relative path
293+
chunk_su = (
294+
A2UI_OPEN_TAG
295+
+ '[{"surfaceUpdate": {"surfaceId": "s1", "components": [{"id": "root",'
296+
' "component": {"Text": {"text": {"path": "some/relative/path"}}}}]}}]'
297+
+ A2UI_CLOSE_TAG
298+
)
299+
300+
messages = []
301+
for part in parser.process_chunk(chunk_su):
302+
if part.a2ui_json:
303+
messages.extend(part.a2ui_json)
304+
305+
# The path should have been prefixed with a slash
306+
assert len(messages) > 0
307+
comp = messages[0][MSG_TYPE_SURFACE_UPDATE]["components"][0]
308+
assert comp["component"]["Text"]["text"]["path"] == "/some/relative/path"

agent_sdks/python/tests/parser/test_streaming_v09.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,67 @@ def test_streaming_msg_type_deduplication(mock_catalog):
388388

389389
# After completion, msg_types is reset
390390
assert not parser.msg_types
391+
392+
393+
def test_v09_path_heuristic_relative_path(mock_catalog):
394+
"""Tests that v0.9 allows relative paths (no leading slash)."""
395+
parser = A2uiStreamParser(catalog=mock_catalog)
396+
# Disable validation to avoid needing full catalog for this test
397+
parser._validator = None
398+
399+
# 1. Create surface
400+
chunk_cs = (
401+
A2UI_OPEN_TAG
402+
+ '[{"version": "v0.9", "createSurface": {"surfaceId": "s1", "catalogId": "c1"}}]'
403+
+ A2UI_CLOSE_TAG
404+
)
405+
list(parser.process_chunk(chunk_cs))
406+
407+
# 2. Update components with a relative path
408+
chunk_uc = (
409+
A2UI_OPEN_TAG
410+
+ '[{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components":'
411+
' [{"id": "root", "component": "Text", "text": {"path":'
412+
' "some/relative/path"}}]}}]'
413+
+ A2UI_CLOSE_TAG
414+
)
415+
416+
messages = []
417+
for part in parser.process_chunk(chunk_uc):
418+
if part.a2ui_json:
419+
messages.extend(part.a2ui_json)
420+
421+
assert len(messages) > 0
422+
comp = messages[0][MSG_TYPE_UPDATE_COMPONENTS]["components"][0]
423+
assert comp["text"]["path"] == "some/relative/path"
424+
425+
426+
def test_v09_path_heuristic_absolute_path(mock_catalog):
427+
"""Tests that v0.9 still supports absolute paths (leading slash)."""
428+
parser = A2uiStreamParser(catalog=mock_catalog)
429+
parser._validator = None
430+
431+
# 1. Create surface
432+
chunk_cs = (
433+
A2UI_OPEN_TAG
434+
+ '[{"version": "v0.9", "createSurface": {"surfaceId": "s1", "catalogId": "c1"}}]'
435+
+ A2UI_CLOSE_TAG
436+
)
437+
list(parser.process_chunk(chunk_cs))
438+
439+
# 2. Update components with an absolute path
440+
chunk_uc = (
441+
A2UI_OPEN_TAG
442+
+ '[{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components":'
443+
' [{"id": "root", "component": "Text", "text": {"path": "/absolute/path"}}]}}]'
444+
+ A2UI_CLOSE_TAG
445+
)
446+
447+
messages = []
448+
for part in parser.process_chunk(chunk_uc):
449+
if part.a2ui_json:
450+
messages.extend(part.a2ui_json)
451+
452+
assert len(messages) > 0
453+
comp = messages[0][MSG_TYPE_UPDATE_COMPONENTS]["components"][0]
454+
assert comp["text"]["path"] == "/absolute/path"

0 commit comments

Comments
 (0)