From 6dad8fcf134f9ef11f56694b6959d3a36f0836ad Mon Sep 17 00:00:00 2001 From: a2a-bot Date: Wed, 2 Jul 2025 16:52:07 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20Update=20A2A=20types=20from=20speci?= =?UTF-8?q?fication=20=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/a2a/types.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/a2a/types.py b/src/a2a/types.py index 7f952369..edfcb5c8 100644 --- a/src/a2a/types.py +++ b/src/a2a/types.py @@ -921,6 +921,20 @@ class UnsupportedOperationError(BaseModel): """ +class WellKnownUris(BaseModel): + """ + Well-known URIs used in the A2A protocol. + https://datatracker.ietf.org/doc/html/rfc8615 + """ + + AGENT_CARD_WELL_KNOWN_URI: Literal['/.well-known/agent.json'] = ( + '/.well-known/agent.json' + ) + """ + The well-known URI at which an AgentCard should be hosted. + """ + + class A2AError( RootModel[ JSONParseError @@ -1568,6 +1582,9 @@ class AgentCard(BaseModel): - Skills: A set of capabilities the agent can perform - Default modalities/content types supported by the agent. - Authentication requirements + + The AgentCard SHOULD be hosted at the well-known URI specified by the + `WellKnownUris.AGENT_CARD_WELL_KNOWN_URI` constant ("/.well-known/agent.json"). """ additionalInterfaces: list[AgentInterface] | None = None From 0e1a5092211975f116b1c2766a297e0fa3ddfd0b Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Wed, 2 Jul 2025 11:57:21 -0500 Subject: [PATCH 2/3] Exclude Types from spelling --- .github/actions/spelling/excludes.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 43e50473..15f8f791 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -90,3 +90,4 @@ noxfile.py ^src/a2a/grpc/ ^tests/ .pre-commit-config.yaml +^src/a2a/types.py From 64be5e4bdda9cf3554173e15ec6dddbc4ef26d89 Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Wed, 2 Jul 2025 12:03:48 -0500 Subject: [PATCH 3/3] Change References to hard-coded path --- src/a2a/client/client.py | 5 +-- src/a2a/server/apps/jsonrpc/fastapi_app.py | 6 ++-- src/a2a/server/apps/jsonrpc/jsonrpc_app.py | 35 ++++++++++++------- src/a2a/server/apps/jsonrpc/starlette_app.py | 8 ++--- tests/client/test_client.py | 16 ++++++--- .../server/apps/jsonrpc/test_serialization.py | 8 +++-- tests/server/test_integration.py | 7 ++-- 7 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/a2a/client/client.py b/src/a2a/client/client.py index e29ef8a7..8dce83d4 100644 --- a/src/a2a/client/client.py +++ b/src/a2a/client/client.py @@ -26,6 +26,7 @@ SendStreamingMessageResponse, SetTaskPushNotificationConfigRequest, SetTaskPushNotificationConfigResponse, + WellKnownUris, ) from a2a.utils.telemetry import SpanKind, trace_class @@ -40,7 +41,7 @@ def __init__( self, httpx_client: httpx.AsyncClient, base_url: str, - agent_card_path: str = '/.well-known/agent.json', + agent_card_path: str = WellKnownUris.AGENT_CARD_WELL_KNOWN_URI, ): """Initializes the A2ACardResolver. @@ -184,7 +185,7 @@ async def _apply_interceptors( async def get_client_from_agent_card_url( httpx_client: httpx.AsyncClient, base_url: str, - agent_card_path: str = '/.well-known/agent.json', + agent_card_path: str = WellKnownUris.AGENT_CARD_WELL_KNOWN_URI, http_kwargs: dict[str, Any] | None = None, ) -> 'A2AClient': """Fetches the public AgentCard and initializes an A2A client. diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py index e961e868..aa583ec7 100644 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ b/src/a2a/server/apps/jsonrpc/fastapi_app.py @@ -9,7 +9,7 @@ JSONRPCApplication, ) from a2a.server.request_handlers.jsonrpc_handler import RequestHandler -from a2a.types import AgentCard +from a2a.types import AgentCard, WellKnownUris logger = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def __init__( def add_routes_to_app( self, app: FastAPI, - agent_card_url: str = '/.well-known/agent.json', + agent_card_url: str = WellKnownUris.AGENT_CARD_WELL_KNOWN_URI, rpc_url: str = '/', extended_agent_card_url: str = '/agent/authenticatedExtendedCard', ) -> None: @@ -83,7 +83,7 @@ async def get_extended_agent_card(request: Request) -> Response: def build( self, - agent_card_url: str = '/.well-known/agent.json', + agent_card_url: str = WellKnownUris.AGENT_CARD_WELL_KNOWN_URI, rpc_url: str = '/', extended_agent_card_url: str = '/agent/authenticatedExtendedCard', **kwargs: Any, diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py index 37d28d53..4cc654bc 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py @@ -41,6 +41,7 @@ SetTaskPushNotificationConfigRequest, TaskResubscriptionRequest, UnsupportedOperationError, + WellKnownUris, ) from a2a.utils.errors import MethodNotImplementedError @@ -299,24 +300,32 @@ async def _process_non_streaming_request( request_obj, context ) case SetTaskPushNotificationConfigRequest(): - handler_result = await self.handler.set_push_notification_config( - request_obj, - context, + handler_result = ( + await self.handler.set_push_notification_config( + request_obj, + context, + ) ) case GetTaskPushNotificationConfigRequest(): - handler_result = await self.handler.get_push_notification_config( - request_obj, - context, + handler_result = ( + await self.handler.get_push_notification_config( + request_obj, + context, + ) ) case ListTaskPushNotificationConfigRequest(): - handler_result = await self.handler.list_push_notification_config( - request_obj, - context, + handler_result = ( + await self.handler.list_push_notification_config( + request_obj, + context, + ) ) case DeleteTaskPushNotificationConfigRequest(): - handler_result = await self.handler.delete_push_notification_config( - request_obj, - context, + handler_result = ( + await self.handler.delete_push_notification_config( + request_obj, + context, + ) ) case _: logger.error( @@ -424,7 +433,7 @@ async def _handle_get_authenticated_extended_agent_card( @abstractmethod def build( self, - agent_card_url: str = '/.well-known/agent.json', + agent_card_url: str = WellKnownUris.AGENT_CARD_WELL_KNOWN_URI, rpc_url: str = '/', **kwargs: Any, ) -> FastAPI | Starlette: diff --git a/src/a2a/server/apps/jsonrpc/starlette_app.py b/src/a2a/server/apps/jsonrpc/starlette_app.py index 1826841b..c4d19ead 100644 --- a/src/a2a/server/apps/jsonrpc/starlette_app.py +++ b/src/a2a/server/apps/jsonrpc/starlette_app.py @@ -10,7 +10,7 @@ JSONRPCApplication, ) from a2a.server.request_handlers.jsonrpc_handler import RequestHandler -from a2a.types import AgentCard +from a2a.types import AgentCard, WellKnownUris logger = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def __init__( def routes( self, - agent_card_url: str = '/.well-known/agent.json', + agent_card_url: str = WellKnownUris.AGENT_CARD_WELL_KNOWN_URI, rpc_url: str = '/', extended_agent_card_url: str = '/agent/authenticatedExtendedCard', ) -> list[Route]: @@ -95,7 +95,7 @@ def routes( def add_routes_to_app( self, app: Starlette, - agent_card_url: str = '/.well-known/agent.json', + agent_card_url: str = WellKnownUris.AGENT_CARD_WELL_KNOWN_URI, rpc_url: str = '/', extended_agent_card_url: str = '/agent/authenticatedExtendedCard', ) -> None: @@ -116,7 +116,7 @@ def add_routes_to_app( def build( self, - agent_card_url: str = '/.well-known/agent.json', + agent_card_url: str = WellKnownUris.AGENT_CARD_WELL_KNOWN_URI, rpc_url: str = '/', extended_agent_card_url: str = '/agent/authenticatedExtendedCard', **kwargs: Any, diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 5b6e9491..10e74b2f 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -46,6 +46,7 @@ TaskNotCancelableError, TaskPushNotificationConfig, TaskQueryParams, + WellKnownUris, ) @@ -127,8 +128,7 @@ async def async_iterable_from_list( class TestA2ACardResolver: BASE_URL = 'http://example.com' - AGENT_CARD_PATH = '/.well-known/agent.json' - FULL_AGENT_CARD_URL = f'{BASE_URL}{AGENT_CARD_PATH}' + FULL_AGENT_CARD_URL = f'{BASE_URL}{WellKnownUris.AGENT_CARD_WELL_KNOWN_URI}' EXTENDED_AGENT_CARD_PATH = ( '/agent/authenticatedExtendedCard' # Default path ) @@ -153,20 +153,26 @@ async def test_init_parameters_stored_correctly( httpx_client=mock_httpx_client, base_url=base_url, ) - assert resolver_default_path.agent_card_path == '.well-known/agent.json' + assert ( + resolver_default_path.agent_card_path + == WellKnownUris.AGENT_CARD_WELL_KNOWN_URI.lstrip('/') + ) @pytest.mark.asyncio async def test_init_strips_slashes(self, mock_httpx_client: AsyncMock): resolver = A2ACardResolver( httpx_client=mock_httpx_client, base_url='http://example.com/', # With trailing slash - agent_card_path='/.well-known/agent.json/', # With leading/trailing slash + agent_card_path=f'{WellKnownUris.AGENT_CARD_WELL_KNOWN_URI}/', # With leading/trailing slash ) assert ( resolver.base_url == 'http://example.com' ) # Trailing slash stripped # constructor lstrips agent_card_path, but keeps trailing if provided - assert resolver.agent_card_path == '.well-known/agent.json/' + assert ( + resolver.agent_card_path + == WellKnownUris.AGENT_CARD_WELL_KNOWN_URI.lstrip('/') + '/' + ) @pytest.mark.asyncio async def test_get_agent_card_success_public_only( diff --git a/tests/server/apps/jsonrpc/test_serialization.py b/tests/server/apps/jsonrpc/test_serialization.py index ea3da1c0..ab0f0a0a 100644 --- a/tests/server/apps/jsonrpc/test_serialization.py +++ b/tests/server/apps/jsonrpc/test_serialization.py @@ -1,6 +1,8 @@ from unittest import mock import pytest + +from pydantic import ValidationError from starlette.testclient import TestClient from a2a.server.apps import A2AFastAPIApplication, A2AStarletteApplication @@ -10,8 +12,8 @@ AgentCard, In, SecurityScheme, + WellKnownUris, ) -from pydantic import ValidationError @pytest.fixture @@ -52,7 +54,7 @@ def test_starlette_agent_card_with_api_key_scheme_alias( app_instance = A2AStarletteApplication(agent_card_with_api_key, handler) client = TestClient(app_instance.build()) - response = client.get('/.well-known/agent.json') + response = client.get(WellKnownUris.AGENT_CARD_WELL_KNOWN_URI) assert response.status_code == 200 response_data = response.json() @@ -84,7 +86,7 @@ def test_fastapi_agent_card_with_api_key_scheme_alias( app_instance = A2AFastAPIApplication(agent_card_with_api_key, handler) client = TestClient(app_instance.build()) - response = client.get('/.well-known/agent.json') + response = client.get(WellKnownUris.AGENT_CARD_WELL_KNOWN_URI) assert response.status_code == 200 response_data = response.json() diff --git a/tests/server/test_integration.py b/tests/server/test_integration.py index 5581711e..de242cdb 100644 --- a/tests/server/test_integration.py +++ b/tests/server/test_integration.py @@ -43,6 +43,7 @@ TaskStatus, TextPart, UnsupportedOperationError, + WellKnownUris, ) from a2a.utils.errors import MethodNotImplementedError @@ -147,7 +148,7 @@ def client(app: A2AStarletteApplication, **kwargs): def test_agent_card_endpoint(client: TestClient, agent_card: AgentCard): """Test the agent card endpoint returns expected data.""" - response = client.get('/.well-known/agent.json') + response = client.get(WellKnownUris.AGENT_CARD_WELL_KNOWN_URI) assert response.status_code == 200 data = response.json() assert data['name'] == agent_card.name @@ -315,7 +316,7 @@ def custom_handler(request): assert response.json() == {'message': 'Hello'} # Ensure default routes still work - response = client.get('/.well-known/agent.json') + response = client.get(WellKnownUris.AGENT_CARD_WELL_KNOWN_URI) assert response.status_code == 200 data = response.json() assert data['name'] == agent_card.name @@ -339,7 +340,7 @@ def custom_handler(request): assert response.json() == {'message': 'Hello'} # Ensure default routes still work - response = client.get('/.well-known/agent.json') + response = client.get(WellKnownUris.AGENT_CARD_WELL_KNOWN_URI) assert response.status_code == 200 data = response.json() assert data['name'] == agent_card.name