Skip to content

Commit c87e87c

Browse files
refactor!: clean up of folder structure (#983)
# Description Refactors internal helpers modules so that the helpers name is used exclusively for the a2a.helpers package (customer-facing convenience functions). - Move a2a.client.helpers into a2a.client.card_resolver -- parse_agent_card and its backward-compat shims are implementation details of card resolution - Rename a2a.utils.helpers to a2a.utils.version_validator to reflect its actual content
1 parent f4a0bcd commit c87e87c

20 files changed

Lines changed: 1038 additions & 1158 deletions

scripts/test_minimal_install.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
'a2a.client.client',
3939
'a2a.client.client_factory',
4040
'a2a.client.errors',
41-
'a2a.client.helpers',
4241
'a2a.client.interceptors',
4342
'a2a.client.optionals',
4443
'a2a.client.transports',
@@ -52,7 +51,7 @@
5251
'a2a.utils',
5352
'a2a.utils.constants',
5453
'a2a.utils.error_handlers',
55-
'a2a.utils.helpers',
54+
'a2a.utils.version_validator',
5655
'a2a.utils.proto_utils',
5756
'a2a.utils.task',
5857
'a2a.helpers.agent_card',

src/a2a/client/card_resolver.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66

77
import httpx
88

9-
from google.protobuf.json_format import ParseError
9+
from google.protobuf.json_format import ParseDict, ParseError
1010

1111
from a2a.client.errors import AgentCardResolutionError
12-
from a2a.client.helpers import parse_agent_card
1312
from a2a.types.a2a_pb2 import (
1413
AgentCard,
1514
)
@@ -19,6 +18,111 @@
1918
logger = logging.getLogger(__name__)
2019

2120

21+
def parse_agent_card(agent_card_data: dict[str, Any]) -> AgentCard:
22+
"""Parse AgentCard JSON dictionary and handle backward compatibility."""
23+
_handle_extended_card_compatibility(agent_card_data)
24+
_handle_connection_fields_compatibility(agent_card_data)
25+
_handle_security_compatibility(agent_card_data)
26+
27+
return ParseDict(agent_card_data, AgentCard(), ignore_unknown_fields=True)
28+
29+
30+
def _handle_extended_card_compatibility(
31+
agent_card_data: dict[str, Any],
32+
) -> None:
33+
"""Map legacy supportsAuthenticatedExtendedCard to capabilities."""
34+
if agent_card_data.pop('supportsAuthenticatedExtendedCard', None):
35+
capabilities = agent_card_data.setdefault('capabilities', {})
36+
if 'extendedAgentCard' not in capabilities:
37+
capabilities['extendedAgentCard'] = True
38+
39+
40+
def _handle_connection_fields_compatibility(
41+
agent_card_data: dict[str, Any],
42+
) -> None:
43+
"""Map legacy connection and transport fields to supportedInterfaces."""
44+
main_url = agent_card_data.pop('url', None)
45+
main_transport = agent_card_data.pop('preferredTransport', 'JSONRPC')
46+
version = agent_card_data.pop('protocolVersion', '0.3.0')
47+
additional_interfaces = (
48+
agent_card_data.pop('additionalInterfaces', None) or []
49+
)
50+
51+
if 'supportedInterfaces' not in agent_card_data and main_url:
52+
supported_interfaces = []
53+
supported_interfaces.append(
54+
{
55+
'url': main_url,
56+
'protocolBinding': main_transport,
57+
'protocolVersion': version,
58+
}
59+
)
60+
supported_interfaces.extend(
61+
{
62+
'url': iface.get('url'),
63+
'protocolBinding': iface.get('transport'),
64+
'protocolVersion': version,
65+
}
66+
for iface in additional_interfaces
67+
)
68+
agent_card_data['supportedInterfaces'] = supported_interfaces
69+
70+
71+
def _map_legacy_security(
72+
sec_list: list[dict[str, list[str]]],
73+
) -> list[dict[str, Any]]:
74+
"""Convert a legacy security requirement list into the 1.0.0 Protobuf format."""
75+
return [
76+
{
77+
'schemes': {
78+
scheme_name: {'list': scopes}
79+
for scheme_name, scopes in sec_dict.items()
80+
}
81+
}
82+
for sec_dict in sec_list
83+
]
84+
85+
86+
def _handle_security_compatibility(agent_card_data: dict[str, Any]) -> None:
87+
"""Map legacy security requirements and schemas to their 1.0.0 Protobuf equivalents."""
88+
legacy_security = agent_card_data.pop('security', None)
89+
if (
90+
'securityRequirements' not in agent_card_data
91+
and legacy_security is not None
92+
):
93+
agent_card_data['securityRequirements'] = _map_legacy_security(
94+
legacy_security
95+
)
96+
97+
for skill in agent_card_data.get('skills', []):
98+
legacy_skill_sec = skill.pop('security', None)
99+
if 'securityRequirements' not in skill and legacy_skill_sec is not None:
100+
skill['securityRequirements'] = _map_legacy_security(
101+
legacy_skill_sec
102+
)
103+
104+
security_schemes = agent_card_data.get('securitySchemes', {})
105+
if security_schemes:
106+
type_mapping = {
107+
'apiKey': 'apiKeySecurityScheme',
108+
'http': 'httpAuthSecurityScheme',
109+
'oauth2': 'oauth2SecurityScheme',
110+
'openIdConnect': 'openIdConnectSecurityScheme',
111+
'mutualTLS': 'mtlsSecurityScheme',
112+
}
113+
for scheme in security_schemes.values():
114+
scheme_type = scheme.pop('type', None)
115+
if scheme_type in type_mapping:
116+
# Map legacy 'in' to modern 'location'
117+
if scheme_type == 'apiKey' and 'in' in scheme:
118+
scheme['location'] = scheme.pop('in')
119+
120+
mapped_name = type_mapping[scheme_type]
121+
new_scheme_wrapper = {mapped_name: scheme.copy()}
122+
scheme.clear()
123+
scheme.update(new_scheme_wrapper)
124+
125+
22126
class A2ACardResolver:
23127
"""Agent Card resolver."""
24128

src/a2a/client/helpers.py

Lines changed: 0 additions & 112 deletions
This file was deleted.

src/a2a/compat/v0_3/jsonrpc_adapter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
ServerCallContextBuilder,
4242
)
4343
from a2a.utils import constants
44-
from a2a.utils.helpers import validate_version
44+
from a2a.utils.version_validator import validate_version
4545

4646

4747
logger = logging.getLogger(__name__)

src/a2a/compat/v0_3/rest_handler.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@
2828
from a2a.compat.v0_3.request_handler import RequestHandler03
2929
from a2a.server.context import ServerCallContext
3030
from a2a.utils import constants
31-
from a2a.utils.helpers import (
32-
validate_version,
33-
)
3431
from a2a.utils.telemetry import SpanKind, trace_class
32+
from a2a.utils.version_validator import validate_version
3533

3634

3735
logger = logging.getLogger(__name__)

src/a2a/server/request_handlers/default_request_handler.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
TaskNotFoundError,
5959
UnsupportedOperationError,
6060
)
61-
from a2a.utils.helpers import maybe_await
6261
from a2a.utils.task import (
6362
apply_history_length,
6463
validate_history_length,
@@ -100,7 +99,7 @@ def __init__( # noqa: PLR0913
10099
request_context_builder: RequestContextBuilder | None = None,
101100
extended_agent_card: AgentCard | None = None,
102101
extended_card_modifier: Callable[
103-
[AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard
102+
[AgentCard, ServerCallContext], Awaitable[AgentCard]
104103
]
105104
| None = None,
106105
) -> None:
@@ -695,8 +694,8 @@ async def on_get_extended_agent_card(
695694
raise ExtendedAgentCardNotConfiguredError
696695

697696
if self.extended_card_modifier:
698-
return await maybe_await(
699-
self.extended_card_modifier(extended_card, context)
697+
extended_card = await self.extended_card_modifier(
698+
extended_card, context
700699
)
701700

702701
return extended_card

src/a2a/server/request_handlers/default_request_handler_v2.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
TaskNotCancelableError,
4848
TaskNotFoundError,
4949
)
50-
from a2a.utils.helpers import maybe_await
5150
from a2a.utils.task import (
5251
apply_history_length,
5352
validate_history_length,
@@ -93,7 +92,7 @@ def __init__( # noqa: PLR0913
9392
request_context_builder: RequestContextBuilder | None = None,
9493
extended_agent_card: AgentCard | None = None,
9594
extended_card_modifier: Callable[
96-
[AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard
95+
[AgentCard, ServerCallContext], Awaitable[AgentCard]
9796
]
9897
| None = None,
9998
) -> None:
@@ -467,8 +466,8 @@ async def on_get_extended_agent_card(
467466
raise ExtendedAgentCardNotConfiguredError
468467

469468
if self.extended_card_modifier:
470-
return await maybe_await(
471-
self.extended_card_modifier(extended_card, context)
469+
extended_card = await self.extended_card_modifier(
470+
extended_card, context
472471
)
473472

474473
return extended_card

src/a2a/server/routes/agent_card_routes.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,11 @@
2626
from a2a.server.request_handlers.response_helpers import agent_card_to_dict
2727
from a2a.types.a2a_pb2 import AgentCard
2828
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
29-
from a2a.utils.helpers import maybe_await
3029

3130

3231
def create_agent_card_routes(
3332
agent_card: AgentCard,
34-
card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard]
35-
| None = None,
33+
card_modifier: Callable[[AgentCard], Awaitable[AgentCard]] | None = None,
3634
card_url: str = AGENT_CARD_WELL_KNOWN_PATH,
3735
) -> list['Route']:
3836
"""Creates the Starlette Route for the A2A protocol agent card endpoint."""
@@ -45,7 +43,7 @@ def create_agent_card_routes(
4543
async def _get_agent_card(request: Request) -> Response:
4644
card_to_serve = agent_card
4745
if card_modifier:
48-
card_to_serve = await maybe_await(card_modifier(card_to_serve))
46+
card_to_serve = await card_modifier(card_to_serve)
4947
return JSONResponse(agent_card_to_dict(card_to_serve))
5048

5149
return [

src/a2a/server/routes/jsonrpc_dispatcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
TaskNotFoundError,
5050
UnsupportedOperationError,
5151
)
52-
from a2a.utils.helpers import validate_version
5352
from a2a.utils.telemetry import SpanKind, trace_class
53+
from a2a.utils.version_validator import validate_version
5454

5555

5656
INTERNAL_ERROR_CODE = -32603

src/a2a/server/routes/rest_dispatcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
InvalidRequestError,
2929
TaskNotFoundError,
3030
)
31-
from a2a.utils.helpers import validate_version
3231
from a2a.utils.telemetry import SpanKind, trace_class
32+
from a2a.utils.version_validator import validate_version
3333

3434

3535
if TYPE_CHECKING:

0 commit comments

Comments
 (0)