Skip to content

Commit 08afb30

Browse files
committed
Support A2UI JSON
1 parent be87778 commit 08afb30

2 files changed

Lines changed: 87 additions & 11 deletions

File tree

src/select_ai/agent/a2a/server.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from a2a.server.routes import create_jsonrpc_routes
2323
from a2a.server.tasks import TaskUpdater
2424
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
25+
from google.protobuf.json_format import ParseDict
2526
from starlette.applications import Starlette
2627
from starlette.responses import JSONResponse
2728
from starlette.routing import Route
@@ -32,11 +33,9 @@
3233
from select_ai.agent.a2a.task_store import OracleTaskStore
3334
from select_ai.version import __version__
3435

35-
_A2UI_MIME_TYPE = "application/a2ui+json"
3636

37-
38-
def _a2ui_payload(result: str | None) -> dict | None:
39-
"""Return an A2UI response envelope, if ``RUN_TEAM`` returned one."""
37+
def _message_parts(result: str | None):
38+
"""Convert a serialized A2A message into its constituent parts."""
4039
if not result:
4140
return None
4241
try:
@@ -45,11 +44,24 @@ def _a2ui_payload(result: str | None) -> dict | None:
4544
return None
4645
if not isinstance(payload, dict):
4746
return None
48-
if payload.get("metadata", {}).get("mimeType") != _A2UI_MIME_TYPE:
49-
return None
50-
if not isinstance(payload.get("data"), list):
47+
message_parts = payload.get("parts")
48+
if payload.get("kind") != "message" or not isinstance(message_parts, list):
5149
return None
52-
return payload
50+
parts = []
51+
for part in message_parts:
52+
if not isinstance(part, dict):
53+
return None
54+
if part.get("kind") == "text" and isinstance(part.get("text"), str):
55+
parts.append(new_text_part(part["text"]))
56+
elif part.get("kind") == "data" and "data" in part:
57+
output_part = new_data_part(part["data"])
58+
if isinstance(part.get("metadata"), dict):
59+
ParseDict(part["metadata"], output_part.metadata)
60+
parts.append(output_part)
61+
else:
62+
# Avoid silently discarding an unsupported part type.
63+
return None
64+
return parts or None
5365

5466

5567
class DatabaseTeamExecutor(AgentExecutor):
@@ -81,11 +93,11 @@ async def execute(self, context, event_queue):
8193
prompt=context.get_user_input(),
8294
params={"conversation_id": conversation_id},
8395
)
84-
a2ui_payload = _a2ui_payload(result)
96+
message_parts = _message_parts(result)
8597
await updater.add_artifact(
8698
parts=(
87-
[new_data_part(a2ui_payload)]
88-
if a2ui_payload is not None
99+
message_parts
100+
if message_parts is not None
89101
else [new_text_part(result or "")]
90102
),
91103
name="database-agent-result",

tests/a2a/test_a2ui_parts.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# -----------------------------------------------------------------------------
2+
# Copyright (c) 2026, Oracle and/or its affiliates.
3+
#
4+
# Licensed under the Universal Permissive License v 1.0 as shown at
5+
# https://oss.oracle.com/licenses/upl.
6+
# -----------------------------------------------------------------------------
7+
8+
import json
9+
10+
import pytest
11+
from google.protobuf.json_format import MessageToDict
12+
13+
pytest.importorskip("a2a")
14+
15+
from select_ai.agent.a2a.server import _message_parts
16+
17+
18+
def test_a2a_message_parts_are_forwarded_with_metadata():
19+
result = json.dumps(
20+
{
21+
"kind": "message",
22+
"parts": [
23+
{"kind": "text", "text": "A2UI visualization ready."},
24+
{
25+
"kind": "data",
26+
"data": {
27+
"version": "v0.9",
28+
"createSurface": {"surfaceId": "smoke-test"},
29+
},
30+
"metadata": {"mimeType": "application/json+a2ui"},
31+
},
32+
],
33+
}
34+
)
35+
36+
parts = _message_parts(result)
37+
38+
assert [
39+
MessageToDict(part, preserving_proto_field_name=True) for part in parts
40+
] == [
41+
{"text": "A2UI visualization ready."},
42+
{
43+
"data": {
44+
"version": "v0.9",
45+
"createSurface": {"surfaceId": "smoke-test"},
46+
},
47+
"metadata": {"mimeType": "application/json+a2ui"},
48+
},
49+
]
50+
51+
52+
def test_a2a_text_message_is_forwarded():
53+
result = json.dumps(
54+
{
55+
"kind": "message",
56+
"parts": [{"kind": "text", "text": "ordinary response"}],
57+
}
58+
)
59+
60+
parts = _message_parts(result)
61+
62+
assert [
63+
MessageToDict(part, preserving_proto_field_name=True) for part in parts
64+
] == [{"text": "ordinary response"}]

0 commit comments

Comments
 (0)