Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions scripts/test_minimal_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
'a2a.client.client',
'a2a.client.client_factory',
'a2a.client.errors',
'a2a.client.helpers',
'a2a.client.interceptors',
'a2a.client.optionals',
'a2a.client.transports',
Expand All @@ -52,7 +51,7 @@
'a2a.utils',
'a2a.utils.constants',
'a2a.utils.error_handlers',
'a2a.utils.helpers',
'a2a.utils.version_validator',
'a2a.utils.proto_utils',
'a2a.utils.task',
'a2a.helpers.agent_card',
Expand Down
108 changes: 106 additions & 2 deletions src/a2a/client/card_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@

import httpx

from google.protobuf.json_format import ParseError
from google.protobuf.json_format import ParseDict, ParseError

from a2a.client.errors import AgentCardResolutionError
from a2a.client.helpers import parse_agent_card
from a2a.types.a2a_pb2 import (
AgentCard,
)
Expand All @@ -19,6 +18,111 @@
logger = logging.getLogger(__name__)


def parse_agent_card(agent_card_data: dict[str, Any]) -> AgentCard:
"""Parse AgentCard JSON dictionary and handle backward compatibility."""
_handle_extended_card_compatibility(agent_card_data)
_handle_connection_fields_compatibility(agent_card_data)
_handle_security_compatibility(agent_card_data)

return ParseDict(agent_card_data, AgentCard(), ignore_unknown_fields=True)


def _handle_extended_card_compatibility(
agent_card_data: dict[str, Any],
) -> None:
"""Map legacy supportsAuthenticatedExtendedCard to capabilities."""
if agent_card_data.pop('supportsAuthenticatedExtendedCard', None):
capabilities = agent_card_data.setdefault('capabilities', {})
if 'extendedAgentCard' not in capabilities:
capabilities['extendedAgentCard'] = True


def _handle_connection_fields_compatibility(
agent_card_data: dict[str, Any],
) -> None:
"""Map legacy connection and transport fields to supportedInterfaces."""
main_url = agent_card_data.pop('url', None)
main_transport = agent_card_data.pop('preferredTransport', 'JSONRPC')
version = agent_card_data.pop('protocolVersion', '0.3.0')
additional_interfaces = (
agent_card_data.pop('additionalInterfaces', None) or []
)

if 'supportedInterfaces' not in agent_card_data and main_url:
supported_interfaces = []
supported_interfaces.append(
{
'url': main_url,
'protocolBinding': main_transport,
'protocolVersion': version,
}
)
supported_interfaces.extend(
{
'url': iface.get('url'),
'protocolBinding': iface.get('transport'),
'protocolVersion': version,
}
for iface in additional_interfaces
)
agent_card_data['supportedInterfaces'] = supported_interfaces


def _map_legacy_security(
sec_list: list[dict[str, list[str]]],
) -> list[dict[str, Any]]:
"""Convert a legacy security requirement list into the 1.0.0 Protobuf format."""
return [
{
'schemes': {
scheme_name: {'list': scopes}
for scheme_name, scopes in sec_dict.items()
}
}
for sec_dict in sec_list
]


def _handle_security_compatibility(agent_card_data: dict[str, Any]) -> None:
"""Map legacy security requirements and schemas to their 1.0.0 Protobuf equivalents."""
legacy_security = agent_card_data.pop('security', None)
if (
'securityRequirements' not in agent_card_data
and legacy_security is not None
):
agent_card_data['securityRequirements'] = _map_legacy_security(
legacy_security
)

for skill in agent_card_data.get('skills', []):
legacy_skill_sec = skill.pop('security', None)
if 'securityRequirements' not in skill and legacy_skill_sec is not None:
skill['securityRequirements'] = _map_legacy_security(
legacy_skill_sec
)

security_schemes = agent_card_data.get('securitySchemes', {})
if security_schemes:
type_mapping = {
'apiKey': 'apiKeySecurityScheme',
'http': 'httpAuthSecurityScheme',
'oauth2': 'oauth2SecurityScheme',
'openIdConnect': 'openIdConnectSecurityScheme',
'mutualTLS': 'mtlsSecurityScheme',
}
for scheme in security_schemes.values():
scheme_type = scheme.pop('type', None)
if scheme_type in type_mapping:
# Map legacy 'in' to modern 'location'
if scheme_type == 'apiKey' and 'in' in scheme:
scheme['location'] = scheme.pop('in')

mapped_name = type_mapping[scheme_type]
new_scheme_wrapper = {mapped_name: scheme.copy()}
scheme.clear()
scheme.update(new_scheme_wrapper)


class A2ACardResolver:
"""Agent Card resolver."""

Expand Down
112 changes: 0 additions & 112 deletions src/a2a/client/helpers.py

This file was deleted.

2 changes: 1 addition & 1 deletion src/a2a/compat/v0_3/jsonrpc_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
ServerCallContextBuilder,
)
from a2a.utils import constants
from a2a.utils.helpers import validate_version
from a2a.utils.version_validator import validate_version


logger = logging.getLogger(__name__)
Expand Down
4 changes: 1 addition & 3 deletions src/a2a/compat/v0_3/rest_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@
from a2a.compat.v0_3.request_handler import RequestHandler03
from a2a.server.context import ServerCallContext
from a2a.utils import constants
from a2a.utils.helpers import (
validate_version,
)
from a2a.utils.telemetry import SpanKind, trace_class
from a2a.utils.version_validator import validate_version


logger = logging.getLogger(__name__)
Expand Down
7 changes: 3 additions & 4 deletions src/a2a/server/request_handlers/default_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
TaskNotFoundError,
UnsupportedOperationError,
)
from a2a.utils.helpers import maybe_await
from a2a.utils.task import (
apply_history_length,
validate_history_length,
Expand Down Expand Up @@ -95,29 +94,29 @@
task_store: TaskStore,
agent_card: AgentCard,
queue_manager: QueueManager | None = None,
push_config_store: PushNotificationConfigStore | None = None,
push_sender: PushNotificationSender | None = None,
request_context_builder: RequestContextBuilder | None = None,
extended_agent_card: AgentCard | None = None,
extended_card_modifier: Callable[
[AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard
[AgentCard, ServerCallContext], Awaitable[AgentCard]
]
Comment thread
guglielmo-san marked this conversation as resolved.
| None = None,
) -> None:
"""Initializes the DefaultRequestHandler.

Args:
agent_executor: The `AgentExecutor` instance to run agent logic.
task_store: The `TaskStore` instance to manage task persistence.
agent_card: The `AgentCard` describing the agent's capabilities.
queue_manager: The `QueueManager` instance to manage event queues. Defaults to `InMemoryQueueManager`.
push_config_store: The `PushNotificationConfigStore` instance for managing push notification configurations. Defaults to None.
push_sender: The `PushNotificationSender` instance for sending push notifications. Defaults to None.
request_context_builder: The `RequestContextBuilder` instance used
to build request contexts. Defaults to `SimpleRequestContextBuilder`.
extended_agent_card: An optional, distinct `AgentCard` to be served at the extended card endpoint.
extended_card_modifier: An optional callback to dynamically modify the extended `AgentCard` before it is served.
"""

Check notice on line 119 in src/a2a/server/request_handlers/default_request_handler.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/request_handlers/default_request_handler_v2.py (90-99)
self.agent_executor = agent_executor
self.task_store = task_store
self._agent_card = agent_card
Expand Down Expand Up @@ -665,38 +664,38 @@

Requires a `PushConfigStore` to be configured.
"""
if not self._push_config_store:
raise PushNotificationNotSupportedError

task_id = params.task_id
config_id = params.id
task: Task | None = await self.task_store.get(task_id, context)
if not task:
raise TaskNotFoundError

await self._push_config_store.delete_info(task_id, context, config_id)

@validate_request_params
@validate(
lambda self: self._agent_card.capabilities.extended_agent_card,
error_message='The agent does not support authenticated extended cards',
)
async def on_get_extended_agent_card(
self,
params: GetExtendedAgentCardRequest,
context: ServerCallContext,
) -> AgentCard:
"""Default handler for 'GetExtendedAgentCard'.

Requires `capabilities.extended_agent_card` to be true.
"""
extended_card = self.extended_agent_card
if not extended_card:
raise ExtendedAgentCardNotConfiguredError

if self.extended_card_modifier:
return await maybe_await(
self.extended_card_modifier(extended_card, context)
extended_card = await self.extended_card_modifier(
extended_card, context
)

return extended_card

Check notice on line 701 in src/a2a/server/request_handlers/default_request_handler.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/request_handlers/default_request_handler_v2.py (439-473)
7 changes: 3 additions & 4 deletions src/a2a/server/request_handlers/default_request_handler_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
TaskNotCancelableError,
TaskNotFoundError,
)
from a2a.utils.helpers import maybe_await
from a2a.utils.task import (
apply_history_length,
validate_history_length,
Expand Down Expand Up @@ -88,16 +87,16 @@
agent_card: AgentCard,
queue_manager: Any
| None = None, # Kept for backward compat in signature
push_config_store: PushNotificationConfigStore | None = None,
push_sender: PushNotificationSender | None = None,
request_context_builder: RequestContextBuilder | None = None,
extended_agent_card: AgentCard | None = None,
extended_card_modifier: Callable[
[AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard
[AgentCard, ServerCallContext], Awaitable[AgentCard]
Comment thread
guglielmo-san marked this conversation as resolved.
]
| None = None,
) -> None:
self.agent_executor = agent_executor

Check notice on line 99 in src/a2a/server/request_handlers/default_request_handler_v2.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/request_handlers/default_request_handler.py (97-119)
self.task_store = task_store
self._agent_card = agent_card
self._push_config_store = push_config_store
Expand Down Expand Up @@ -437,38 +436,38 @@
params: DeleteTaskPushNotificationConfigRequest,
context: ServerCallContext,
) -> None:
if not self._push_config_store:
raise PushNotificationNotSupportedError

task_id = params.task_id
config_id = params.id
task: Task | None = await self.task_store.get(task_id, context)
if not task:
raise TaskNotFoundError

await self._push_config_store.delete_info(task_id, context, config_id)

@validate_request_params
@validate(
lambda self: self._agent_card.capabilities.extended_agent_card,
error_message='The agent does not support authenticated extended cards',
)
async def on_get_extended_agent_card(
self,
params: GetExtendedAgentCardRequest,
context: ServerCallContext,
) -> AgentCard:
"""Default handler for 'GetExtendedAgentCard'.

Requires `capabilities.extended_agent_card` to be true.
"""
extended_card = self.extended_agent_card
if not extended_card:
raise ExtendedAgentCardNotConfiguredError

if self.extended_card_modifier:
return await maybe_await(
self.extended_card_modifier(extended_card, context)
extended_card = await self.extended_card_modifier(
extended_card, context
)

return extended_card

Check notice on line 473 in src/a2a/server/request_handlers/default_request_handler_v2.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/request_handlers/default_request_handler.py (667-701)
6 changes: 2 additions & 4 deletions src/a2a/server/routes/agent_card_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,11 @@
from a2a.server.request_handlers.response_helpers import agent_card_to_dict
from a2a.types.a2a_pb2 import AgentCard
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
from a2a.utils.helpers import maybe_await


def create_agent_card_routes(
agent_card: AgentCard,
card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard]
| None = None,
card_modifier: Callable[[AgentCard], Awaitable[AgentCard]] | None = None,
Comment thread
guglielmo-san marked this conversation as resolved.
card_url: str = AGENT_CARD_WELL_KNOWN_PATH,
) -> list['Route']:
"""Creates the Starlette Route for the A2A protocol agent card endpoint."""
Expand All @@ -45,7 +43,7 @@ def create_agent_card_routes(
async def _get_agent_card(request: Request) -> Response:
card_to_serve = agent_card
if card_modifier:
card_to_serve = await maybe_await(card_modifier(card_to_serve))
card_to_serve = await card_modifier(card_to_serve)
return JSONResponse(agent_card_to_dict(card_to_serve))

return [
Expand Down
2 changes: 1 addition & 1 deletion src/a2a/server/routes/jsonrpc_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
TaskNotFoundError,
UnsupportedOperationError,
)
from a2a.utils.helpers import validate_version
from a2a.utils.telemetry import SpanKind, trace_class
from a2a.utils.version_validator import validate_version


INTERNAL_ERROR_CODE = -32603
Expand Down
2 changes: 1 addition & 1 deletion src/a2a/server/routes/rest_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
InvalidRequestError,
TaskNotFoundError,
)
from a2a.utils.helpers import validate_version
from a2a.utils.telemetry import SpanKind, trace_class
from a2a.utils.version_validator import validate_version


if TYPE_CHECKING:
Expand Down
Loading
Loading