Skip to content

Commit 3ac014a

Browse files
committed
feat(utils): add display_agent_card() utility for human-readable AgentCard inspection
1 parent 0bfec88 commit 3ac014a

4 files changed

Lines changed: 265 additions & 1 deletion

File tree

samples/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from a2a.client import A2ACardResolver, ClientConfig, create_client
1313
from a2a.types import Message, Part, Role, SendMessageRequest, TaskState
14+
from a2a.utils.agent_card import display_agent_card
1415

1516

1617
async def _handle_stream( # noqa: PLR0912
@@ -86,7 +87,7 @@ async def main() -> None:
8687
resolver = A2ACardResolver(httpx_client, args.url)
8788
card = await resolver.get_agent_card()
8889
print('\n✓ Agent Card Found:')
89-
print(f' Name: {card.name}')
90+
display_agent_card(card)
9091

9192
client = await create_client(card, client_config=config)
9293

src/a2a/utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Utility functions for the A2A Python SDK."""
22

33
from a2a.utils import proto_utils
4+
from a2a.utils.agent_card import display_agent_card
45
from a2a.utils.artifact import (
56
get_artifact_text,
67
new_artifact,
@@ -44,6 +45,7 @@
4445
'build_text_artifact',
4546
'completed_task',
4647
'create_task_obj',
48+
'display_agent_card',
4749
'get_artifact_text',
4850
'get_data_parts',
4951
'get_file_parts',

src/a2a/utils/agent_card.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Utility functions for inspecting AgentCard instances."""
2+
3+
from a2a.types.a2a_pb2 import AgentCard
4+
5+
6+
def display_agent_card(card: AgentCard) -> None:
7+
"""Print a human-readable summary of an AgentCard to stdout.
8+
9+
Args:
10+
card: The AgentCard proto message to display.
11+
"""
12+
width = 52
13+
sep = '=' * width
14+
thin = '-' * width
15+
16+
lines: list[str] = [sep, 'AgentCard'.center(width), sep]
17+
18+
lines += [
19+
'--- General ---',
20+
f'Name : {card.name}',
21+
f'Description : {card.description}',
22+
f'Version : {card.version}',
23+
]
24+
if card.documentation_url:
25+
lines.append(f'Docs URL : {card.documentation_url}')
26+
if card.icon_url:
27+
lines.append(f'Icon URL : {card.icon_url}')
28+
if card.HasField('provider'):
29+
lines.append(
30+
f'Provider : {card.provider.organization} ({card.provider.url})'
31+
)
32+
33+
lines += ['', '--- Interfaces ---']
34+
for i, iface in enumerate(card.supported_interfaces):
35+
binding = f'{iface.protocol_binding} {iface.protocol_version}'.strip()
36+
parts = [
37+
p
38+
for p in [binding, f'tenant={iface.tenant}' if iface.tenant else '']
39+
if p
40+
]
41+
suffix = f' ({", ".join(parts)})' if parts else ''
42+
line = f' [{i}] {iface.url}{suffix}'
43+
lines.append(line)
44+
45+
lines += [
46+
'',
47+
'--- Capabilities ---',
48+
f'Streaming : {card.capabilities.streaming}',
49+
f'Push notifications : {card.capabilities.push_notifications}',
50+
f'Extended agent card : {card.capabilities.extended_agent_card}',
51+
]
52+
53+
lines += [
54+
'',
55+
'--- I/O Modes ---',
56+
f'Input : {", ".join(card.default_input_modes) or "(none)"}',
57+
f'Output : {", ".join(card.default_output_modes) or "(none)"}',
58+
]
59+
60+
lines += ['', '--- Skills ---']
61+
if card.skills:
62+
for skill in card.skills:
63+
lines += [
64+
thin,
65+
f' ID : {skill.id}',
66+
f' Name : {skill.name}',
67+
f' Description : {skill.description}',
68+
f' Tags : {", ".join(skill.tags) or "(none)"}',
69+
]
70+
if skill.examples:
71+
for ex in skill.examples:
72+
lines.append(f' Example : {ex}')
73+
else:
74+
lines.append(' (none)')
75+
76+
lines.append(sep)
77+
print('\n'.join(lines))
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"""Tests for display_agent_card utility."""
2+
3+
import pytest
4+
5+
from a2a.types.a2a_pb2 import (
6+
AgentCapabilities,
7+
AgentCard,
8+
AgentInterface,
9+
AgentProvider,
10+
AgentSkill,
11+
)
12+
from a2a.utils.agent_card import display_agent_card
13+
14+
15+
@pytest.fixture
16+
def full_agent_card() -> AgentCard:
17+
return AgentCard(
18+
name='Sample Agent',
19+
description='A sample agent.',
20+
version='1.0.0',
21+
documentation_url='https://docs.example.com',
22+
icon_url='https://example.com/icon.png',
23+
provider=AgentProvider(
24+
organization='Example Org', url='https://example.com'
25+
),
26+
supported_interfaces=[
27+
AgentInterface(
28+
url='http://localhost:9999/a2a/jsonrpc',
29+
protocol_binding='JSONRPC',
30+
protocol_version='1.0',
31+
),
32+
AgentInterface(
33+
url='http://localhost:9999/a2a/rest',
34+
protocol_binding='HTTP+JSON',
35+
protocol_version='1.0',
36+
tenant='tenant-a',
37+
),
38+
],
39+
capabilities=AgentCapabilities(
40+
streaming=True,
41+
push_notifications=False,
42+
extended_agent_card=True,
43+
),
44+
default_input_modes=['text'],
45+
default_output_modes=['text', 'task-status'],
46+
skills=[
47+
AgentSkill(
48+
id='skill-1',
49+
name='My Skill',
50+
description='Does something useful.',
51+
tags=['foo', 'bar'],
52+
examples=['Do the thing', 'Another example'],
53+
),
54+
AgentSkill(
55+
id='skill-2',
56+
name='Other Skill',
57+
description='Does something else.',
58+
tags=['baz'],
59+
),
60+
],
61+
)
62+
63+
64+
class TestDisplayAgentCard:
65+
def test_full_card_output(
66+
self, full_agent_card: AgentCard, capsys: pytest.CaptureFixture[str]
67+
) -> None:
68+
"""Golden test: exact output for a fully-populated card."""
69+
display_agent_card(full_agent_card)
70+
assert capsys.readouterr().out == (
71+
'====================================================\n'
72+
' AgentCard \n'
73+
'====================================================\n'
74+
'--- General ---\n'
75+
'Name : Sample Agent\n'
76+
'Description : A sample agent.\n'
77+
'Version : 1.0.0\n'
78+
'Docs URL : https://docs.example.com\n'
79+
'Icon URL : https://example.com/icon.png\n'
80+
'Provider : Example Org (https://example.com)\n'
81+
'\n'
82+
'--- Interfaces ---\n'
83+
' [0] http://localhost:9999/a2a/jsonrpc (JSONRPC 1.0)\n'
84+
' [1] http://localhost:9999/a2a/rest (HTTP+JSON 1.0, tenant=tenant-a)\n'
85+
'\n'
86+
'--- Capabilities ---\n'
87+
'Streaming : True\n'
88+
'Push notifications : False\n'
89+
'Extended agent card : True\n'
90+
'\n'
91+
'--- I/O Modes ---\n'
92+
'Input : text\n'
93+
'Output : text, task-status\n'
94+
'\n'
95+
'--- Skills ---\n'
96+
'----------------------------------------------------\n'
97+
' ID : skill-1\n'
98+
' Name : My Skill\n'
99+
' Description : Does something useful.\n'
100+
' Tags : foo, bar\n'
101+
' Example : Do the thing\n'
102+
' Example : Another example\n'
103+
'----------------------------------------------------\n'
104+
' ID : skill-2\n'
105+
' Name : Other Skill\n'
106+
' Description : Does something else.\n'
107+
' Tags : baz\n'
108+
'====================================================\n'
109+
)
110+
111+
def test_empty_card_output(
112+
self, capsys: pytest.CaptureFixture[str]
113+
) -> None:
114+
"""Golden test: exact output for a card with only default/empty fields.
115+
116+
An empty supported_interfaces section signals a malformed card —
117+
the bare header with no entries is intentional and visible to the user.
118+
"""
119+
display_agent_card(AgentCard())
120+
assert capsys.readouterr().out == (
121+
'====================================================\n'
122+
' AgentCard \n'
123+
'====================================================\n'
124+
'--- General ---\n'
125+
'Name : \n'
126+
'Description : \n'
127+
'Version : \n'
128+
'\n'
129+
'--- Interfaces ---\n'
130+
'\n'
131+
'--- Capabilities ---\n'
132+
'Streaming : False\n'
133+
'Push notifications : False\n'
134+
'Extended agent card : False\n'
135+
'\n'
136+
'--- I/O Modes ---\n'
137+
'Input : (none)\n'
138+
'Output : (none)\n'
139+
'\n'
140+
'--- Skills ---\n'
141+
' (none)\n'
142+
'====================================================\n'
143+
)
144+
145+
def test_interface_without_protocol_version_has_no_trailing_space(
146+
self, capsys: pytest.CaptureFixture[str]
147+
) -> None:
148+
"""No trailing space in the binding field when protocol_version is not set."""
149+
card = AgentCard(
150+
supported_interfaces=[
151+
AgentInterface(
152+
url='127.0.0.1:50051',
153+
protocol_binding='GRPC',
154+
)
155+
]
156+
)
157+
display_agent_card(card)
158+
assert ' [0] 127.0.0.1:50051 (GRPC)' in capsys.readouterr().out
159+
160+
def test_interface_without_binding_or_version_has_no_parentheses(
161+
self, capsys: pytest.CaptureFixture[str]
162+
) -> None:
163+
"""No parentheses when neither protocol_binding nor protocol_version are set."""
164+
card = AgentCard(
165+
supported_interfaces=[AgentInterface(url='127.0.0.1:50051')]
166+
)
167+
display_agent_card(card)
168+
assert ' [0] 127.0.0.1:50051\n' in capsys.readouterr().out
169+
170+
def test_provider_both_fields_always_shown(
171+
self, capsys: pytest.CaptureFixture[str]
172+
) -> None:
173+
"""Both organization and url are shown when provider is set (both are required fields)."""
174+
card = AgentCard(
175+
provider=AgentProvider(
176+
organization='Example Org',
177+
url='https://example.com',
178+
),
179+
)
180+
display_agent_card(card)
181+
assert (
182+
'Provider : Example Org (https://example.com)'
183+
in capsys.readouterr().out
184+
)

0 commit comments

Comments
 (0)