From befb4f6a47fc2477b667559bae6d5ba2e8c2bd27 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Thu, 19 Mar 2026 17:31:03 +0000 Subject: [PATCH 1/6] wip --- samples/hello_world_agent.py | 16 +- src/a2a/compat/v0_3/jsonrpc_adapter.py | 2 +- src/a2a/compat/v0_3/rest_adapter.py | 5 +- src/a2a/server/apps/__init__.py | 10 - src/a2a/server/apps/jsonrpc/__init__.py | 20 -- src/a2a/server/apps/jsonrpc/fastapi_app.py | 148 --------- src/a2a/server/apps/jsonrpc/starlette_app.py | 169 ----------- src/a2a/server/apps/rest/fastapi_app.py | 2 +- src/a2a/server/apps/rest/rest_adapter.py | 5 +- src/a2a/server/routes/__init__.py | 20 ++ src/a2a/server/routes/agent_card_routes.py | 85 ++++++ .../jsonrpc_dispatcher.py} | 53 +--- src/a2a/server/routes/jsonrpc_routes.py | 107 +++++++ tck/sut_agent.py | 22 +- tests/__init__.py | 1 + tests/compat/v0_3/test_jsonrpc_app_compat.py | 11 +- .../cross_version/client_server/server_0_3.py | 15 +- .../cross_version/client_server/server_1_0.py | 21 +- tests/integration/test_agent_card.py | 17 +- .../test_client_server_integration.py | 65 ++-- tests/integration/test_end_to_end.py | 14 +- tests/integration/test_tenant.py | 17 +- tests/integration/test_version_header.py | 17 +- tests/server/apps/jsonrpc/test_fastapi_app.py | 79 ----- .../server/apps/jsonrpc/test_serialization.py | 280 ------------------ .../server/apps/jsonrpc/test_starlette_app.py | 81 ----- tests/server/routes/test_agent_card_routes.py | 105 +++++++ .../test_jsonrpc_dispatcher.py} | 239 +++------------ tests/server/routes/test_jsonrpc_routes.py | 96 ++++++ tests/server/test_integration.py | 102 ++++--- 30 files changed, 668 insertions(+), 1156 deletions(-) delete mode 100644 src/a2a/server/apps/jsonrpc/__init__.py delete mode 100644 src/a2a/server/apps/jsonrpc/fastapi_app.py delete mode 100644 src/a2a/server/apps/jsonrpc/starlette_app.py create mode 100644 src/a2a/server/routes/__init__.py create mode 100644 src/a2a/server/routes/agent_card_routes.py rename src/a2a/server/{apps/jsonrpc/jsonrpc_app.py => routes/jsonrpc_dispatcher.py} (93%) create mode 100644 src/a2a/server/routes/jsonrpc_routes.py create mode 100644 tests/__init__.py delete mode 100644 tests/server/apps/jsonrpc/test_fastapi_app.py delete mode 100644 tests/server/apps/jsonrpc/test_serialization.py delete mode 100644 tests/server/apps/jsonrpc/test_starlette_app.py create mode 100644 tests/server/routes/test_agent_card_routes.py rename tests/server/{apps/jsonrpc/test_jsonrpc_app.py => routes/test_jsonrpc_dispatcher.py} (51%) create mode 100644 tests/server/routes/test_jsonrpc_routes.py diff --git a/samples/hello_world_agent.py b/samples/hello_world_agent.py index 38dfdf56..e46b9ede 100644 --- a/samples/hello_world_agent.py +++ b/samples/hello_world_agent.py @@ -11,12 +11,13 @@ from a2a.compat.v0_3.grpc_handler import CompatGrpcHandler from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext -from a2a.server.apps import A2AFastAPIApplication, A2ARESTFastAPIApplication +from a2a.server.apps import A2ARESTFastAPIApplication from a2a.server.events.event_queue import EventQueue from a2a.server.request_handlers import GrpcHandler from a2a.server.request_handlers.default_request_handler import ( DefaultRequestHandler, ) +from a2a.server.routes import AgentCardRoutes, JsonRpcRoutes from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore from a2a.server.tasks.task_updater import TaskUpdater from a2a.types import ( @@ -197,14 +198,17 @@ async def serve( ) rest_app = rest_app_builder.build() - jsonrpc_app_builder = A2AFastAPIApplication( + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=request_handler, + rpc_url='/a2a/jsonrpc/', + ) + agent_card_routes = AgentCardRoutes( agent_card=agent_card, - http_handler=request_handler, - enable_v0_3_compat=True, ) - app = FastAPI() - jsonrpc_app_builder.add_routes_to_app(app, rpc_url='/a2a/jsonrpc/') + app.routes.extend(jsonrpc_routes.routes) + app.routes.extend(agent_card_routes.routes) app.mount('/a2a/rest', rest_app) grpc_server = grpc.aio.server() diff --git a/src/a2a/compat/v0_3/jsonrpc_adapter.py b/src/a2a/compat/v0_3/jsonrpc_adapter.py index 30a04dd9..073c7854 100644 --- a/src/a2a/compat/v0_3/jsonrpc_adapter.py +++ b/src/a2a/compat/v0_3/jsonrpc_adapter.py @@ -10,8 +10,8 @@ if TYPE_CHECKING: from starlette.requests import Request - from a2a.server.apps.jsonrpc.jsonrpc_app import CallContextBuilder from a2a.server.request_handlers.request_handler import RequestHandler + from a2a.server.routes import CallContextBuilder from a2a.types.a2a_pb2 import AgentCard _package_starlette_installed = True diff --git a/src/a2a/compat/v0_3/rest_adapter.py b/src/a2a/compat/v0_3/rest_adapter.py index b0296e40..8cae6b63 100644 --- a/src/a2a/compat/v0_3/rest_adapter.py +++ b/src/a2a/compat/v0_3/rest_adapter.py @@ -33,12 +33,9 @@ from a2a.compat.v0_3 import conversions from a2a.compat.v0_3.rest_handler import REST03Handler -from a2a.server.apps.jsonrpc.jsonrpc_app import ( - CallContextBuilder, - DefaultCallContextBuilder, -) from a2a.server.apps.rest.rest_adapter import RESTAdapterInterface from a2a.server.context import ServerCallContext +from a2a.server.routes import CallContextBuilder, DefaultCallContextBuilder from a2a.utils.error_handlers import ( rest_error_handler, rest_stream_error_handler, diff --git a/src/a2a/server/apps/__init__.py b/src/a2a/server/apps/__init__.py index 579deaa5..1cdb3295 100644 --- a/src/a2a/server/apps/__init__.py +++ b/src/a2a/server/apps/__init__.py @@ -1,18 +1,8 @@ """HTTP application components for the A2A server.""" -from a2a.server.apps.jsonrpc import ( - A2AFastAPIApplication, - A2AStarletteApplication, - CallContextBuilder, - JSONRPCApplication, -) from a2a.server.apps.rest import A2ARESTFastAPIApplication __all__ = [ - 'A2AFastAPIApplication', 'A2ARESTFastAPIApplication', - 'A2AStarletteApplication', - 'CallContextBuilder', - 'JSONRPCApplication', ] diff --git a/src/a2a/server/apps/jsonrpc/__init__.py b/src/a2a/server/apps/jsonrpc/__init__.py deleted file mode 100644 index 1121fdbc..00000000 --- a/src/a2a/server/apps/jsonrpc/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -"""A2A JSON-RPC Applications.""" - -from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication -from a2a.server.apps.jsonrpc.jsonrpc_app import ( - CallContextBuilder, - DefaultCallContextBuilder, - JSONRPCApplication, - StarletteUserProxy, -) -from a2a.server.apps.jsonrpc.starlette_app import A2AStarletteApplication - - -__all__ = [ - 'A2AFastAPIApplication', - 'A2AStarletteApplication', - 'CallContextBuilder', - 'DefaultCallContextBuilder', - 'JSONRPCApplication', - 'StarletteUserProxy', -] diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py deleted file mode 100644 index 0ec9d1ab..00000000 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ /dev/null @@ -1,148 +0,0 @@ -import logging - -from collections.abc import Awaitable, Callable -from typing import TYPE_CHECKING, Any - - -if TYPE_CHECKING: - from fastapi import FastAPI - - _package_fastapi_installed = True -else: - try: - from fastapi import FastAPI - - _package_fastapi_installed = True - except ImportError: - FastAPI = Any - - _package_fastapi_installed = False - -from a2a.server.apps.jsonrpc.jsonrpc_app import ( - CallContextBuilder, - JSONRPCApplication, -) -from a2a.server.context import ServerCallContext -from a2a.server.request_handlers.request_handler import RequestHandler -from a2a.types.a2a_pb2 import AgentCard -from a2a.utils.constants import ( - AGENT_CARD_WELL_KNOWN_PATH, - DEFAULT_RPC_URL, -) - - -logger = logging.getLogger(__name__) - - -class A2AFastAPIApplication(JSONRPCApplication): - """A FastAPI application implementing the A2A protocol server endpoints. - - Handles incoming JSON-RPC requests, routes them to the appropriate - handler methods, and manages response generation including Server-Sent Events - (SSE). - """ - - def __init__( # noqa: PLR0913 - self, - agent_card: AgentCard, - http_handler: RequestHandler, - extended_agent_card: AgentCard | None = None, - context_builder: CallContextBuilder | None = None, - card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard] - | None = None, - extended_card_modifier: Callable[ - [AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard - ] - | None = None, - max_content_length: int | None = 10 * 1024 * 1024, # 10MB - enable_v0_3_compat: bool = False, - ) -> None: - """Initializes the A2AFastAPIApplication. - - Args: - agent_card: The AgentCard describing the agent's capabilities. - http_handler: The handler instance responsible for processing A2A - requests via http. - extended_agent_card: An optional, distinct AgentCard to be served - at the authenticated extended card endpoint. - context_builder: The CallContextBuilder used to construct the - ServerCallContext passed to the http_handler. If None, no - ServerCallContext is passed. - card_modifier: An optional callback to dynamically modify the public - agent card before it is served. - extended_card_modifier: An optional callback to dynamically modify - the extended agent card before it is served. It receives the - call context. - max_content_length: The maximum allowed content length for incoming - requests. Defaults to 10MB. Set to None for unbounded maximum. - enable_v0_3_compat: Whether to enable v0.3 backward compatibility on the same endpoint. - """ - if not _package_fastapi_installed: - raise ImportError( - 'The `fastapi` package is required to use the `A2AFastAPIApplication`.' - ' It can be added as a part of `a2a-sdk` optional dependencies,' - ' `a2a-sdk[http-server]`.' - ) - super().__init__( - agent_card=agent_card, - http_handler=http_handler, - extended_agent_card=extended_agent_card, - context_builder=context_builder, - card_modifier=card_modifier, - extended_card_modifier=extended_card_modifier, - max_content_length=max_content_length, - enable_v0_3_compat=enable_v0_3_compat, - ) - - def add_routes_to_app( - self, - app: FastAPI, - agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH, - rpc_url: str = DEFAULT_RPC_URL, - ) -> None: - """Adds the routes to the FastAPI application. - - Args: - app: The FastAPI application to add the routes to. - agent_card_url: The URL for the agent card endpoint. - rpc_url: The URL for the A2A JSON-RPC endpoint. - """ - app.post( - rpc_url, - openapi_extra={ - 'requestBody': { - 'content': { - 'application/json': { - 'schema': { - '$ref': '#/components/schemas/A2ARequest' - } - } - }, - 'required': True, - 'description': 'A2ARequest', - } - }, - )(self._handle_requests) - app.get(agent_card_url)(self._handle_get_agent_card) - - def build( - self, - agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH, - rpc_url: str = DEFAULT_RPC_URL, - **kwargs: Any, - ) -> FastAPI: - """Builds and returns the FastAPI application instance. - - Args: - agent_card_url: The URL for the agent card endpoint. - rpc_url: The URL for the A2A JSON-RPC endpoint. - **kwargs: Additional keyword arguments to pass to the FastAPI constructor. - - Returns: - A configured FastAPI application instance. - """ - app = FastAPI(**kwargs) - - self.add_routes_to_app(app, agent_card_url, rpc_url) - - return app diff --git a/src/a2a/server/apps/jsonrpc/starlette_app.py b/src/a2a/server/apps/jsonrpc/starlette_app.py deleted file mode 100644 index 553fa250..00000000 --- a/src/a2a/server/apps/jsonrpc/starlette_app.py +++ /dev/null @@ -1,169 +0,0 @@ -import logging - -from collections.abc import Awaitable, Callable -from typing import TYPE_CHECKING, Any - - -if TYPE_CHECKING: - from starlette.applications import Starlette - from starlette.routing import Route - - _package_starlette_installed = True - -else: - try: - from starlette.applications import Starlette - from starlette.routing import Route - - _package_starlette_installed = True - except ImportError: - Starlette = Any - Route = Any - - _package_starlette_installed = False - -from a2a.server.apps.jsonrpc.jsonrpc_app import ( - CallContextBuilder, - JSONRPCApplication, -) -from a2a.server.context import ServerCallContext -from a2a.server.request_handlers.request_handler import RequestHandler -from a2a.types.a2a_pb2 import AgentCard -from a2a.utils.constants import ( - AGENT_CARD_WELL_KNOWN_PATH, - DEFAULT_RPC_URL, -) - - -logger = logging.getLogger(__name__) - - -class A2AStarletteApplication(JSONRPCApplication): - """A Starlette application implementing the A2A protocol server endpoints. - - Handles incoming JSON-RPC requests, routes them to the appropriate - handler methods, and manages response generation including Server-Sent Events - (SSE). - """ - - def __init__( # noqa: PLR0913 - self, - agent_card: AgentCard, - http_handler: RequestHandler, - extended_agent_card: AgentCard | None = None, - context_builder: CallContextBuilder | None = None, - card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard] - | None = None, - extended_card_modifier: Callable[ - [AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard - ] - | None = None, - max_content_length: int | None = 10 * 1024 * 1024, # 10MB - enable_v0_3_compat: bool = False, - ) -> None: - """Initializes the A2AStarletteApplication. - - Args: - agent_card: The AgentCard describing the agent's capabilities. - http_handler: The handler instance responsible for processing A2A - requests via http. - extended_agent_card: An optional, distinct AgentCard to be served - at the authenticated extended card endpoint. - context_builder: The CallContextBuilder used to construct the - ServerCallContext passed to the http_handler. If None, no - ServerCallContext is passed. - card_modifier: An optional callback to dynamically modify the public - agent card before it is served. - extended_card_modifier: An optional callback to dynamically modify - the extended agent card before it is served. It receives the - call context. - max_content_length: The maximum allowed content length for incoming - requests. Defaults to 10MB. Set to None for unbounded maximum. - enable_v0_3_compat: Whether to enable v0.3 backward compatibility on the same endpoint. - """ - if not _package_starlette_installed: - raise ImportError( - 'Packages `starlette` and `sse-starlette` are required to use the' - ' `A2AStarletteApplication`. It can be added as a part of `a2a-sdk`' - ' optional dependencies, `a2a-sdk[http-server]`.' - ) - super().__init__( - agent_card=agent_card, - http_handler=http_handler, - extended_agent_card=extended_agent_card, - context_builder=context_builder, - card_modifier=card_modifier, - extended_card_modifier=extended_card_modifier, - max_content_length=max_content_length, - enable_v0_3_compat=enable_v0_3_compat, - ) - - def routes( - self, - agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH, - rpc_url: str = DEFAULT_RPC_URL, - ) -> list[Route]: - """Returns the Starlette Routes for handling A2A requests. - - Args: - agent_card_url: The URL path for the agent card endpoint. - rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests). - - Returns: - A list of Starlette Route objects. - """ - return [ - Route( - rpc_url, - self._handle_requests, - methods=['POST'], - name='a2a_handler', - ), - Route( - agent_card_url, - self._handle_get_agent_card, - methods=['GET'], - name='agent_card', - ), - ] - - def add_routes_to_app( - self, - app: Starlette, - agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH, - rpc_url: str = DEFAULT_RPC_URL, - ) -> None: - """Adds the routes to the Starlette application. - - Args: - app: The Starlette application to add the routes to. - agent_card_url: The URL path for the agent card endpoint. - rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests). - """ - routes = self.routes( - agent_card_url=agent_card_url, - rpc_url=rpc_url, - ) - app.routes.extend(routes) - - def build( - self, - agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH, - rpc_url: str = DEFAULT_RPC_URL, - **kwargs: Any, - ) -> Starlette: - """Builds and returns the Starlette application instance. - - Args: - agent_card_url: The URL path for the agent card endpoint. - rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests). - **kwargs: Additional keyword arguments to pass to the Starlette constructor. - - Returns: - A configured Starlette application instance. - """ - app = Starlette(**kwargs) - - self.add_routes_to_app(app, agent_card_url, rpc_url) - - return app diff --git a/src/a2a/server/apps/rest/fastapi_app.py b/src/a2a/server/apps/rest/fastapi_app.py index ea9a501b..4feac907 100644 --- a/src/a2a/server/apps/rest/fastapi_app.py +++ b/src/a2a/server/apps/rest/fastapi_app.py @@ -28,10 +28,10 @@ from a2a.compat.v0_3.rest_adapter import REST03Adapter -from a2a.server.apps.jsonrpc.jsonrpc_app import CallContextBuilder from a2a.server.apps.rest.rest_adapter import RESTAdapter from a2a.server.context import ServerCallContext from a2a.server.request_handlers.request_handler import RequestHandler +from a2a.server.routes import CallContextBuilder from a2a.types.a2a_pb2 import AgentCard from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH diff --git a/src/a2a/server/apps/rest/rest_adapter.py b/src/a2a/server/apps/rest/rest_adapter.py index 6b8abb99..ebf996a4 100644 --- a/src/a2a/server/apps/rest/rest_adapter.py +++ b/src/a2a/server/apps/rest/rest_adapter.py @@ -33,16 +33,13 @@ _package_starlette_installed = False -from a2a.server.apps.jsonrpc import ( - CallContextBuilder, - DefaultCallContextBuilder, -) from a2a.server.context import ServerCallContext from a2a.server.request_handlers.request_handler import RequestHandler from a2a.server.request_handlers.response_helpers import ( agent_card_to_dict, ) from a2a.server.request_handlers.rest_handler import RESTHandler +from a2a.server.routes import CallContextBuilder, DefaultCallContextBuilder from a2a.types.a2a_pb2 import AgentCard from a2a.utils.error_handlers import ( rest_error_handler, diff --git a/src/a2a/server/routes/__init__.py b/src/a2a/server/routes/__init__.py new file mode 100644 index 00000000..ec65d8b3 --- /dev/null +++ b/src/a2a/server/routes/__init__.py @@ -0,0 +1,20 @@ +"""A2A Routes.""" + +from a2a.server.routes.agent_card_routes import AgentCardRoutes +from a2a.server.routes.jsonrpc_dispatcher import ( + CallContextBuilder, + DefaultCallContextBuilder, + JsonRpcDispatcher, + StarletteUserProxy, +) +from a2a.server.routes.jsonrpc_routes import JsonRpcRoutes + + +__all__ = [ + 'AgentCardRoutes', + 'CallContextBuilder', + 'DefaultCallContextBuilder', + 'JsonRpcDispatcher', + 'JsonRpcRoutes', + 'StarletteUserProxy', +] diff --git a/src/a2a/server/routes/agent_card_routes.py b/src/a2a/server/routes/agent_card_routes.py new file mode 100644 index 00000000..30d635f1 --- /dev/null +++ b/src/a2a/server/routes/agent_card_routes.py @@ -0,0 +1,85 @@ +import logging + +from collections.abc import Awaitable, Callable, Sequence +from typing import TYPE_CHECKING, Any + + +if TYPE_CHECKING: + from starlette.middleware import Middleware + from starlette.requests import Request + from starlette.responses import JSONResponse, Response + from starlette.routing import Route + + _package_starlette_installed = True +else: + try: + from starlette.middleware import Middleware + from starlette.requests import Request + from starlette.responses import JSONResponse, Response + from starlette.routing import Route + + _package_starlette_installed = True + except ImportError: + Middleware = Any + Route = Any + Request = Any + Response = Any + JSONResponse = Any + + _package_starlette_installed = False + +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 + + +logger = logging.getLogger(__name__) + + +class AgentCardRoutes: + """Provides the Starlette Route for the A2A protocol agent card endpoint.""" + + def __init__( + self, + agent_card: AgentCard, + card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard] + | None = None, + card_url: str = AGENT_CARD_WELL_KNOWN_PATH, + middleware: Sequence['Middleware'] | None = None, + ) -> None: + """Initializes the AgentCardRoute. + + Args: + agent_card: The AgentCard describing the agent's capabilities. + card_modifier: An optional callback to dynamically modify the public + agent card before it is served. + card_url: The URL for the agent card endpoint. + middleware: An optional list of Starlette middleware to apply to the + agent card endpoint. + """ + if not _package_starlette_installed: + raise ImportError( + 'The `starlette` package is required to use the `AgentCardRoutes`.' + ' `a2a-sdk[http-server]`.' + ) + + self.agent_card = agent_card + self.card_modifier = card_modifier + + async def get_agent_card(request: Request) -> Response: + card_to_serve = self.agent_card + if self.card_modifier: + card_to_serve = await maybe_await( + self.card_modifier(card_to_serve) + ) + return JSONResponse(agent_card_to_dict(card_to_serve)) + + self.routes = [ + Route( + path=card_url, + endpoint=get_agent_card, + methods=['GET'], + middleware=middleware, + ) + ] diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/routes/jsonrpc_dispatcher.py similarity index 93% rename from src/a2a/server/apps/jsonrpc/jsonrpc_app.py rename to src/a2a/server/routes/jsonrpc_dispatcher.py index 21947076..14a0cc0b 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/routes/jsonrpc_dispatcher.py @@ -31,7 +31,6 @@ from a2a.server.request_handlers.jsonrpc_handler import JSONRPCHandler from a2a.server.request_handlers.request_handler import RequestHandler from a2a.server.request_handlers.response_helpers import ( - agent_card_to_dict, build_error_response, ) from a2a.types import A2ARequest @@ -49,14 +48,12 @@ TaskPushNotificationConfig, ) from a2a.utils.constants import ( - AGENT_CARD_WELL_KNOWN_PATH, - DEFAULT_RPC_URL, + DEFAULT_MAX_CONTENT_LENGTH, ) from a2a.utils.errors import ( A2AError, UnsupportedOperationError, ) -from a2a.utils.helpers import maybe_await INTERNAL_ERROR_CODE = -32603 @@ -167,7 +164,7 @@ def build(self, request: Request) -> ServerCallContext: ) -class JSONRPCApplication(ABC): +class JsonRpcDispatcher: """Base class for A2A JSONRPC applications. Handles incoming JSON-RPC requests, routes them to the appropriate @@ -204,10 +201,10 @@ def __init__( # noqa: PLR0913 [AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard ] | None = None, - max_content_length: int | None = 10 * 1024 * 1024, # 10MB enable_v0_3_compat: bool = False, + max_content_length: int | None = DEFAULT_MAX_CONTENT_LENGTH, ) -> None: - """Initializes the JSONRPCApplication. + """Initializes the JsonRpcDispatcher. Args: agent_card: The AgentCard describing the agent's capabilities. @@ -230,7 +227,7 @@ def __init__( # noqa: PLR0913 if not _package_starlette_installed: raise ImportError( 'Packages `starlette` and `sse-starlette` are required to use the' - ' `JSONRPCApplication`. They can be added as a part of `a2a-sdk`' + ' `JsonRpcDispatcher`. They can be added as a part of `a2a-sdk`' ' optional dependencies, `a2a-sdk[http-server]`.' ) @@ -600,43 +597,3 @@ async def event_generator( # handler_result is a dict (JSON-RPC response) return JSONResponse(handler_result, headers=headers) - - async def _handle_get_agent_card(self, request: Request) -> JSONResponse: - """Handles GET requests for the agent card endpoint. - - Args: - request: The incoming Starlette Request object. - - Returns: - A JSONResponse containing the agent card data. - """ - card_to_serve = self.agent_card - if self.card_modifier: - card_to_serve = await maybe_await(self.card_modifier(card_to_serve)) - - return JSONResponse( - agent_card_to_dict( - card_to_serve, - ) - ) - - @abstractmethod - def build( - self, - agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH, - rpc_url: str = DEFAULT_RPC_URL, - **kwargs: Any, - ) -> FastAPI | Starlette: - """Builds and returns the JSONRPC application instance. - - Args: - agent_card_url: The URL for the agent card endpoint. - rpc_url: The URL for the A2A JSON-RPC endpoint. - **kwargs: Additional keyword arguments to pass to the FastAPI constructor. - - Returns: - A configured JSONRPC application instance. - """ - raise NotImplementedError( - 'Subclasses must implement the build method to create the application instance.' - ) diff --git a/src/a2a/server/routes/jsonrpc_routes.py b/src/a2a/server/routes/jsonrpc_routes.py new file mode 100644 index 00000000..cc0e1261 --- /dev/null +++ b/src/a2a/server/routes/jsonrpc_routes.py @@ -0,0 +1,107 @@ +import logging + +from collections.abc import Awaitable, Callable, Sequence +from typing import TYPE_CHECKING, Any + + +if TYPE_CHECKING: + from starlette.middleware import Middleware + from starlette.routing import Route, Router + + _package_starlette_installed = True +else: + try: + from starlette.middleware import Middleware + from starlette.routing import Route, Router + + _package_starlette_installed = True + except ImportError: + Middleware = Any + Route = Any + Router = Any + + _package_starlette_installed = False + + +from a2a.server.context import ServerCallContext +from a2a.server.request_handlers.request_handler import RequestHandler +from a2a.server.routes.jsonrpc_dispatcher import ( + CallContextBuilder, + JsonRpcDispatcher, +) +from a2a.types.a2a_pb2 import AgentCard +from a2a.utils.constants import DEFAULT_RPC_URL + + +logger = logging.getLogger(__name__) + + +class JsonRpcRoutes: + """Provides the Starlette Route for the A2A protocol JSON-RPC endpoint. + + Handles incoming JSON-RPC requests, routes them to the appropriate + handler methods, and manages response generation including Server-Sent Events + (SSE). + """ + + def __init__( # noqa: PLR0913 + self, + agent_card: AgentCard, + request_handler: RequestHandler, + extended_agent_card: AgentCard | None = None, + context_builder: CallContextBuilder | None = None, + card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard] + | None = None, + extended_card_modifier: Callable[ + [AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard + ] + | None = None, + enable_v0_3_compat: bool = False, + rpc_url: str = DEFAULT_RPC_URL, + middleware: Sequence[Middleware] | None = None, + ) -> None: + """Initializes the JsonRpcRoute. + + Args: + agent_card: The AgentCard describing the agent's capabilities. + request_handler: The handler instance responsible for processing A2A + requests via http. + extended_agent_card: An optional, distinct AgentCard to be served + at the authenticated extended card endpoint. + context_builder: The CallContextBuilder used to construct the + ServerCallContext passed to the request_handler. If None, no + ServerCallContext is passed. + card_modifier: An optional callback to dynamically modify the public + agent card before it is served. + extended_card_modifier: An optional callback to dynamically modify + the extended agent card before it is served. It receives the + call context. + enable_v0_3_compat: Whether to enable v0.3 backward compatibility on the same endpoint. + rpc_url: The URL prefix for the RPC endpoints. + middleware: An optional list of Starlette middleware to apply to the routes. + """ + if not _package_starlette_installed: + raise ImportError( + 'The `starlette` package is required to use the `JsonRpcRoutes`.' + ' It can be added as a part of `a2a-sdk` optional dependencies,' + ' `a2a-sdk[http-server]`.' + ) + + self.dispatcher = JsonRpcDispatcher( + agent_card=agent_card, + http_handler=request_handler, + extended_agent_card=extended_agent_card, + context_builder=context_builder, + card_modifier=card_modifier, + extended_card_modifier=extended_card_modifier, + enable_v0_3_compat=enable_v0_3_compat, + ) + + self.routes = [ + Route( + path=rpc_url, + endpoint=self.dispatcher._handle_requests, # noqa: SLF001 + methods=['POST'], + middleware=middleware, + ) + ] diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 7196b828..95549343 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -18,13 +18,16 @@ from a2a.server.agent_execution.context import RequestContext from a2a.server.apps import ( A2ARESTFastAPIApplication, - A2AStarletteApplication, ) from a2a.server.events.event_queue import EventQueue from a2a.server.request_handlers.default_request_handler import ( DefaultRequestHandler, ) from a2a.server.request_handlers.grpc_handler import GrpcHandler +from a2a.server.routes import ( + AgentCardRoutes, + JsonRpcRoutes, +) from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore from a2a.server.tasks.task_store import TaskStore from a2a.types import ( @@ -196,15 +199,22 @@ def serve(task_store: TaskStore) -> None: task_store=task_store, ) - main_app = Starlette() - # JSONRPC - jsonrpc_server = A2AStarletteApplication( + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=request_handler, + rpc_url=JSONRPC_URL, + ) + # Agent Card + agent_card_routes = AgentCardRoutes( agent_card=agent_card, - http_handler=request_handler, ) - jsonrpc_server.add_routes_to_app(main_app, rpc_url=JSONRPC_URL) + routes = [ + *jsonrpc_routes.routes, + *agent_card_routes.routes, + ] + main_app = Starlette(routes=routes) # REST rest_server = A2ARESTFastAPIApplication( agent_card=agent_card, diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..792d6005 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/compat/v0_3/test_jsonrpc_app_compat.py b/tests/compat/v0_3/test_jsonrpc_app_compat.py index 4f09bb23..4b344c67 100644 --- a/tests/compat/v0_3/test_jsonrpc_app_compat.py +++ b/tests/compat/v0_3/test_jsonrpc_app_compat.py @@ -6,7 +6,8 @@ import pytest from starlette.testclient import TestClient -from a2a.server.apps.jsonrpc.starlette_app import A2AStarletteApplication +from starlette.applications import Starlette +from a2a.server.routes import JsonRpcRoutes from a2a.server.request_handlers.request_handler import RequestHandler from a2a.types.a2a_pb2 import ( AgentCard, @@ -50,16 +51,18 @@ def test_app(mock_handler): mock_agent_card.capabilities.streaming = False mock_agent_card.capabilities.push_notifications = True mock_agent_card.capabilities.extended_agent_card = True - return A2AStarletteApplication( + router = JsonRpcRoutes( agent_card=mock_agent_card, - http_handler=mock_handler, + request_handler=mock_handler, enable_v0_3_compat=True, + rpc_url='/', ) + return Starlette(routes=router.routes) @pytest.fixture def client(test_app): - return TestClient(test_app.build()) + return TestClient(test_app) def test_send_message_v03_compat( diff --git a/tests/integration/cross_version/client_server/server_0_3.py b/tests/integration/cross_version/client_server/server_0_3.py index 7bd5f7e7..96152c13 100644 --- a/tests/integration/cross_version/client_server/server_0_3.py +++ b/tests/integration/cross_version/client_server/server_0_3.py @@ -8,7 +8,7 @@ from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext -from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication +from a2a.server.apps.jsonrpc import A2AFastAPIApplication from a2a.server.apps.rest.fastapi_app import A2ARESTFastAPIApplication from a2a.server.events.event_queue import EventQueue from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager @@ -188,12 +188,13 @@ async def main_async(http_port: int, grpc_port: int): ) app = FastAPI() - app.mount( - '/jsonrpc', - A2AFastAPIApplication( - http_handler=handler, agent_card=agent_card - ).build(), - ) + jsonrpc_app = A2AFastAPIApplication( + agent_card=agent_card, + http_handler=handler, + extended_agent_card=agent_card, + ).build() + app.mount('/jsonrpc', jsonrpc_app) + app.mount( '/rest', A2ARESTFastAPIApplication( diff --git a/tests/integration/cross_version/client_server/server_1_0.py b/tests/integration/cross_version/client_server/server_1_0.py index e079fdf2..907c010f 100644 --- a/tests/integration/cross_version/client_server/server_1_0.py +++ b/tests/integration/cross_version/client_server/server_1_0.py @@ -5,7 +5,8 @@ import grpc from a2a.server.agent_execution import AgentExecutor, RequestContext -from a2a.server.apps import A2AFastAPIApplication, A2ARESTFastAPIApplication +from a2a.server.routes import AgentCardRoutes, JsonRpcRoutes +from a2a.server.apps import A2ARESTFastAPIApplication from a2a.server.events import EventQueue from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager from a2a.server.request_handlers import DefaultRequestHandler, GrpcHandler @@ -166,10 +167,20 @@ async def main_async(http_port: int, grpc_port: int): app = FastAPI() app.add_middleware(CustomLoggingMiddleware) - jsonrpc_app = A2AFastAPIApplication( - http_handler=handler, agent_card=agent_card, enable_v0_3_compat=True - ).build() - app.mount('/jsonrpc', jsonrpc_app) + agent_card_routes = AgentCardRoutes( + agent_card=agent_card, card_url='/.well-known/agent-card.json' + ) + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=handler, + extended_agent_card=agent_card, + rpc_url='/', + enable_v0_3_compat=True, + ) + app.mount( + '/jsonrpc', + FastAPI(routes=jsonrpc_routes.routes + agent_card_routes.routes), + ) app.mount( '/rest', diff --git a/tests/integration/test_agent_card.py b/tests/integration/test_agent_card.py index eb7c03f4..42aca384 100644 --- a/tests/integration/test_agent_card.py +++ b/tests/integration/test_agent_card.py @@ -4,7 +4,9 @@ from fastapi import FastAPI from a2a.server.agent_execution import AgentExecutor, RequestContext -from a2a.server.apps import A2AFastAPIApplication, A2ARESTFastAPIApplication +from starlette.applications import Starlette +from a2a.server.apps import A2ARESTFastAPIApplication +from a2a.server.routes import AgentCardRoutes, JsonRpcRoutes from a2a.server.events import EventQueue from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager from a2a.server.request_handlers import DefaultRequestHandler @@ -70,10 +72,15 @@ async def test_agent_card_integration(header_val: str | None) -> None: app = FastAPI() # Mount JSONRPC application - # In JSONRPCApplication, the default agent_card_url is AGENT_CARD_WELL_KNOWN_PATH - jsonrpc_app = A2AFastAPIApplication( - http_handler=handler, agent_card=agent_card - ).build() + jsonrpc_routes = [ + *AgentCardRoutes( + agent_card=agent_card, card_url='/.well-known/agent-card.json' + ).routes, + *JsonRpcRoutes( + agent_card=agent_card, request_handler=handler, rpc_url='/' + ).routes, + ] + jsonrpc_app = Starlette(routes=jsonrpc_routes) app.mount('/jsonrpc', jsonrpc_app) # Mount REST application diff --git a/tests/integration/test_client_server_integration.py b/tests/integration/test_client_server_integration.py index e239d780..f6f1b418 100644 --- a/tests/integration/test_client_server_integration.py +++ b/tests/integration/test_client_server_integration.py @@ -23,7 +23,9 @@ with_a2a_extensions, ) from a2a.client.transports import JsonRpcTransport, RestTransport -from a2a.server.apps import A2AFastAPIApplication, A2ARESTFastAPIApplication +from starlette.applications import Starlette +from a2a.server.apps import A2ARESTFastAPIApplication +from a2a.server.routes import AgentCardRoutes, JsonRpcRoutes from a2a.server.request_handlers import GrpcHandler, RequestHandler from a2a.types import a2a_pb2_grpc from a2a.types.a2a_pb2 import ( @@ -220,10 +222,14 @@ def http_base_setup(mock_request_handler: AsyncMock, agent_card: AgentCard): def jsonrpc_setup(http_base_setup) -> TransportSetup: """Sets up the JsonRpcTransport and in-memory server.""" mock_request_handler, agent_card = http_base_setup - app_builder = A2AFastAPIApplication( - agent_card, mock_request_handler, extended_agent_card=agent_card + agent_card_routes = AgentCardRoutes(agent_card=agent_card, card_url='/') + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=mock_request_handler, + extended_agent_card=agent_card, + rpc_url='/', ) - app = app_builder.build() + app = Starlette(routes=[*agent_card_routes.routes, *jsonrpc_routes.routes]) httpx_client = httpx.AsyncClient(transport=httpx.ASGITransport(app=app)) factory = ClientFactory( config=ClientConfig( @@ -619,12 +625,16 @@ async def test_json_transport_get_signed_base_card( }, ) - app_builder = A2AFastAPIApplication( - agent_card, - mock_request_handler, - card_modifier=signer, # Sign the base card + agent_card_routes = AgentCardRoutes( + agent_card=agent_card, card_url='/', card_modifier=signer ) - app = app_builder.build() + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=mock_request_handler, + extended_agent_card=agent_card, + rpc_url='/', + ) + app = Starlette(routes=[*agent_card_routes.routes, *jsonrpc_routes.routes]) httpx_client = httpx.AsyncClient(transport=httpx.ASGITransport(app=app)) agent_url = agent_card.supported_interfaces[0].url @@ -639,7 +649,8 @@ async def test_json_transport_get_signed_base_card( # Verification happens here result = await resolver.get_agent_card( - signature_verifier=signature_verifier + relative_card_path='/', + signature_verifier=signature_verifier, ) # Create transport with the verified card @@ -684,15 +695,15 @@ async def test_client_get_signed_extended_card( }, ) - app_builder = A2AFastAPIApplication( - agent_card, - mock_request_handler, + agent_card_routes = AgentCardRoutes(agent_card=agent_card, card_url='/') + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=mock_request_handler, extended_agent_card=extended_agent_card, - extended_card_modifier=lambda card, ctx: signer( - card - ), # Sign the extended card + extended_card_modifier=lambda card, ctx: signer(card), + rpc_url='/', ) - app = app_builder.build() + app = Starlette(routes=[*agent_card_routes.routes, *jsonrpc_routes.routes]) httpx_client = httpx.AsyncClient(transport=httpx.ASGITransport(app=app)) transport = JsonRpcTransport( @@ -753,16 +764,17 @@ async def test_client_get_signed_base_and_extended_cards( }, ) - app_builder = A2AFastAPIApplication( - agent_card, - mock_request_handler, + agent_card_routes = AgentCardRoutes( + agent_card=agent_card, card_url='/', card_modifier=signer + ) + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=mock_request_handler, extended_agent_card=extended_agent_card, - card_modifier=signer, # Sign the base card - extended_card_modifier=lambda card, ctx: signer( - card - ), # Sign the extended card + extended_card_modifier=lambda card, ctx: signer(card), + rpc_url='/', ) - app = app_builder.build() + app = Starlette(routes=[*agent_card_routes.routes, *jsonrpc_routes.routes]) httpx_client = httpx.AsyncClient(transport=httpx.ASGITransport(app=app)) agent_url = agent_card.supported_interfaces[0].url @@ -777,7 +789,8 @@ async def test_client_get_signed_base_and_extended_cards( # 1. Fetch base card base_card = await resolver.get_agent_card( - signature_verifier=signature_verifier + relative_card_path='/', + signature_verifier=signature_verifier, ) # 2. Create transport with base card diff --git a/tests/integration/test_end_to_end.py b/tests/integration/test_end_to_end.py index ddf9edbf..f75e8c9d 100644 --- a/tests/integration/test_end_to_end.py +++ b/tests/integration/test_end_to_end.py @@ -10,7 +10,9 @@ from a2a.client.client import ClientConfig from a2a.client.client_factory import ClientFactory from a2a.server.agent_execution import AgentExecutor, RequestContext -from a2a.server.apps import A2AFastAPIApplication, A2ARESTFastAPIApplication +from starlette.applications import Starlette +from a2a.server.apps import A2ARESTFastAPIApplication +from a2a.server.routes import JsonRpcRoutes, AgentCardRoutes from a2a.server.events import EventQueue from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager from a2a.server.request_handlers import DefaultRequestHandler, GrpcHandler @@ -192,10 +194,14 @@ def rest_setup(agent_card, base_e2e_setup) -> ClientSetup: @pytest.fixture def jsonrpc_setup(agent_card, base_e2e_setup) -> ClientSetup: task_store, handler = base_e2e_setup - app_builder = A2AFastAPIApplication( - agent_card, handler, extended_agent_card=agent_card + agent_card_routes = AgentCardRoutes(agent_card=agent_card, card_url='/') + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=handler, + extended_agent_card=agent_card, + rpc_url='/', ) - app = app_builder.build() + app = Starlette(routes=[*agent_card_routes.routes, *jsonrpc_routes.routes]) httpx_client = httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url='http://testserver' ) diff --git a/tests/integration/test_tenant.py b/tests/integration/test_tenant.py index 903b90a2..21698b4f 100644 --- a/tests/integration/test_tenant.py +++ b/tests/integration/test_tenant.py @@ -19,7 +19,8 @@ from a2a.client import ClientConfig, ClientFactory from a2a.utils.constants import TransportProtocol -from a2a.server.apps.jsonrpc.starlette_app import A2AStarletteApplication +from a2a.server.routes import AgentCardRoutes, JsonRpcRoutes +from starlette.applications import Starlette from a2a.server.request_handlers.request_handler import RequestHandler from a2a.server.context import ServerCallContext @@ -197,10 +198,18 @@ def jsonrpc_agent_card(self): @pytest.fixture def server_app(self, jsonrpc_agent_card, mock_handler): - app = A2AStarletteApplication( + agent_card_routes = AgentCardRoutes( + agent_card=jsonrpc_agent_card, card_url='/' + ) + jsonrpc_routes = JsonRpcRoutes( agent_card=jsonrpc_agent_card, - http_handler=mock_handler, - ).build(rpc_url='/jsonrpc') + request_handler=mock_handler, + extended_agent_card=jsonrpc_agent_card, + rpc_url='/jsonrpc', + ) + app = Starlette( + routes=[*agent_card_routes.routes, *jsonrpc_routes.routes] + ) return app @pytest.mark.asyncio diff --git a/tests/integration/test_version_header.py b/tests/integration/test_version_header.py index 40aa9144..754b1416 100644 --- a/tests/integration/test_version_header.py +++ b/tests/integration/test_version_header.py @@ -4,7 +4,8 @@ from starlette.testclient import TestClient from a2a.server.agent_execution import AgentExecutor, RequestContext -from a2a.server.apps import A2AFastAPIApplication, A2ARESTFastAPIApplication +from a2a.server.apps import A2ARESTFastAPIApplication +from a2a.server.routes import AgentCardRoutes, JsonRpcRoutes from a2a.server.events import EventQueue from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager from a2a.server.request_handlers import DefaultRequestHandler @@ -56,10 +57,16 @@ async def mock_on_message_send_stream(*args, **kwargs): handler.on_message_send_stream = mock_on_message_send_stream app = FastAPI() - jsonrpc_app = A2AFastAPIApplication( - http_handler=handler, agent_card=agent_card, enable_v0_3_compat=True - ).build() - app.mount('/jsonrpc', jsonrpc_app) + agent_card_routes = AgentCardRoutes(agent_card=agent_card, card_url='/') + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=handler, + extended_agent_card=agent_card, + rpc_url='/jsonrpc', + enable_v0_3_compat=True, + ) + app.routes.extend(agent_card_routes.routes) + app.routes.extend(jsonrpc_routes.routes) rest_app = A2ARESTFastAPIApplication( http_handler=handler, agent_card=agent_card, enable_v0_3_compat=True ).build() diff --git a/tests/server/apps/jsonrpc/test_fastapi_app.py b/tests/server/apps/jsonrpc/test_fastapi_app.py deleted file mode 100644 index 11831df5..00000000 --- a/tests/server/apps/jsonrpc/test_fastapi_app.py +++ /dev/null @@ -1,79 +0,0 @@ -from typing import Any -from unittest.mock import MagicMock - -import pytest - -from a2a.server.apps.jsonrpc import fastapi_app -from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication -from a2a.server.request_handlers.request_handler import ( - RequestHandler, # For mock spec -) -from a2a.types.a2a_pb2 import AgentCard # For mock spec - - -# --- A2AFastAPIApplication Tests --- - - -class TestA2AFastAPIApplicationOptionalDeps: - # Running tests in this class requires the optional dependency fastapi to be - # present in the test environment. - - @pytest.fixture(scope='class', autouse=True) - def ensure_pkg_fastapi_is_present(self): - try: - import fastapi as _fastapi # noqa: F401 - except ImportError: - pytest.fail( - f'Running tests in {self.__class__.__name__} requires' - ' the optional dependency fastapi to be present in the test' - ' environment. Run `uv sync --dev ...` before running the test' - ' suite.' - ) - - @pytest.fixture(scope='class') - def mock_app_params(self) -> dict: - # Mock http_handler - mock_handler = MagicMock(spec=RequestHandler) - # Mock agent_card with essential attributes accessed in __init__ - mock_agent_card = MagicMock(spec=AgentCard) - # Ensure 'url' attribute exists on the mock_agent_card, as it's accessed - # in __init__ - mock_agent_card.url = 'http://example.com' - # Ensure 'capabilities.extended_agent_card' attribute exists - return {'agent_card': mock_agent_card, 'http_handler': mock_handler} - - @pytest.fixture(scope='class') - def mark_pkg_fastapi_not_installed(self): - pkg_fastapi_installed_flag = fastapi_app._package_fastapi_installed - fastapi_app._package_fastapi_installed = False - yield - fastapi_app._package_fastapi_installed = pkg_fastapi_installed_flag - - def test_create_a2a_fastapi_app_with_present_deps_succeeds( - self, mock_app_params: dict - ): - try: - _app = A2AFastAPIApplication(**mock_app_params) - except ImportError: - pytest.fail( - 'With the fastapi package present, creating a' - ' A2AFastAPIApplication instance should not raise ImportError' - ) - - def test_create_a2a_fastapi_app_with_missing_deps_raises_importerror( - self, - mock_app_params: dict, - mark_pkg_fastapi_not_installed: Any, - ): - with pytest.raises( - ImportError, - match=( - 'The `fastapi` package is required to use the' - ' `A2AFastAPIApplication`' - ), - ): - _app = A2AFastAPIApplication(**mock_app_params) - - -if __name__ == '__main__': - pytest.main([__file__]) diff --git a/tests/server/apps/jsonrpc/test_serialization.py b/tests/server/apps/jsonrpc/test_serialization.py deleted file mode 100644 index 825f8e2a..00000000 --- a/tests/server/apps/jsonrpc/test_serialization.py +++ /dev/null @@ -1,280 +0,0 @@ -"""Tests for JSON-RPC serialization behavior.""" - -from unittest import mock - -import pytest -from starlette.testclient import TestClient - -from a2a.server.apps import A2AFastAPIApplication, A2AStarletteApplication -from a2a.server.jsonrpc_models import JSONParseError -from a2a.types import ( - InvalidRequestError, -) -from a2a.types.a2a_pb2 import ( - AgentCapabilities, - AgentInterface, - AgentCard, - AgentSkill, - APIKeySecurityScheme, - Message, - Part, - Role, - SecurityRequirement, - SecurityScheme, -) - - -@pytest.fixture -def minimal_agent_card(): - """Provides a minimal AgentCard for testing.""" - return AgentCard( - name='TestAgent', - description='A test agent.', - supported_interfaces=[ - AgentInterface( - url='http://example.com/agent', protocol_binding='HTTP+JSON' - ) - ], - version='1.0.0', - capabilities=AgentCapabilities(), - default_input_modes=['text/plain'], - default_output_modes=['text/plain'], - skills=[ - AgentSkill( - id='skill-1', - name='Test Skill', - description='A test skill', - tags=['test'], - ) - ], - ) - - -@pytest.fixture -def agent_card_with_api_key(): - """Provides an AgentCard with an APIKeySecurityScheme for testing serialization.""" - api_key_scheme = APIKeySecurityScheme( - name='X-API-KEY', - location='header', - ) - - security_scheme = SecurityScheme(api_key_security_scheme=api_key_scheme) - - card = AgentCard( - name='APIKeyAgent', - description='An agent that uses API Key auth.', - supported_interfaces=[ - AgentInterface( - url='http://example.com/apikey-agent', - protocol_binding='HTTP+JSON', - ) - ], - version='1.0.0', - capabilities=AgentCapabilities(), - default_input_modes=['text/plain'], - default_output_modes=['text/plain'], - ) - # Add security scheme to the map - card.security_schemes['api_key_auth'].CopyFrom(security_scheme) - - return card - - -def test_starlette_agent_card_serialization(minimal_agent_card: AgentCard): - """Tests that the A2AStarletteApplication endpoint correctly serializes agent card.""" - handler = mock.AsyncMock() - app_instance = A2AStarletteApplication(minimal_agent_card, handler) - client = TestClient(app_instance.build()) - - response = client.get('/.well-known/agent-card.json') - assert response.status_code == 200 - response_data = response.json() - - assert response_data['name'] == 'TestAgent' - assert response_data['description'] == 'A test agent.' - assert ( - response_data['supportedInterfaces'][0]['url'] - == 'http://example.com/agent' - ) - assert response_data['version'] == '1.0.0' - - -def test_starlette_agent_card_with_api_key_scheme( - agent_card_with_api_key: AgentCard, -): - """Tests that the A2AStarletteApplication endpoint correctly serializes API key schemes.""" - handler = mock.AsyncMock() - app_instance = A2AStarletteApplication(agent_card_with_api_key, handler) - client = TestClient(app_instance.build()) - - response = client.get('/.well-known/agent-card.json') - assert response.status_code == 200 - response_data = response.json() - - # Check security schemes are serialized - assert 'securitySchemes' in response_data - assert 'api_key_auth' in response_data['securitySchemes'] - - -def test_fastapi_agent_card_serialization(minimal_agent_card: AgentCard): - """Tests that the A2AFastAPIApplication endpoint correctly serializes agent card.""" - handler = mock.AsyncMock() - app_instance = A2AFastAPIApplication(minimal_agent_card, handler) - client = TestClient(app_instance.build()) - - response = client.get('/.well-known/agent-card.json') - assert response.status_code == 200 - response_data = response.json() - - assert response_data['name'] == 'TestAgent' - assert response_data['description'] == 'A test agent.' - - -def test_handle_invalid_json(minimal_agent_card: AgentCard): - """Test handling of malformed JSON.""" - handler = mock.AsyncMock() - app_instance = A2AStarletteApplication(minimal_agent_card, handler) - client = TestClient(app_instance.build()) - - response = client.post( - '/', - content='{ "jsonrpc": "2.0", "method": "test", "id": 1, "params": { "key": "value" }', - ) - assert response.status_code == 200 - data = response.json() - assert data['error']['code'] == JSONParseError().code - - -def test_handle_oversized_payload(minimal_agent_card: AgentCard): - """Test handling of oversized JSON payloads.""" - handler = mock.AsyncMock() - app_instance = A2AStarletteApplication(minimal_agent_card, handler) - client = TestClient(app_instance.build()) - - large_string = 'a' * 11 * 1_000_000 # 11MB string - payload = { - 'jsonrpc': '2.0', - 'method': 'test', - 'id': 1, - 'params': {'data': large_string}, - } - - response = client.post('/', json=payload) - assert response.status_code == 200 - data = response.json() - assert data['error']['code'] == -32600 - - -@pytest.mark.parametrize( - 'max_content_length', - [ - None, - 11 * 1024 * 1024, - 30 * 1024 * 1024, - ], -) -def test_handle_oversized_payload_with_max_content_length( - minimal_agent_card: AgentCard, - max_content_length: int | None, -): - """Test handling of JSON payloads with sizes within custom max_content_length.""" - handler = mock.AsyncMock() - app_instance = A2AStarletteApplication( - minimal_agent_card, handler, max_content_length=max_content_length - ) - client = TestClient(app_instance.build()) - - large_string = 'a' * 11 * 1_000_000 # 11MB string - payload = { - 'jsonrpc': '2.0', - 'method': 'test', - 'id': 1, - 'params': {'data': large_string}, - } - - response = client.post('/', json=payload) - assert response.status_code == 200 - data = response.json() - # When max_content_length is set, requests up to that size should not be - # rejected due to payload size. The request might fail for other reasons, - # but it shouldn't be an InvalidRequestError related to the content length. - if max_content_length is not None: - assert data['error']['code'] != -32600 - - -def test_handle_unicode_characters(minimal_agent_card: AgentCard): - """Test handling of unicode characters in JSON payload.""" - handler = mock.AsyncMock() - app_instance = A2AStarletteApplication(minimal_agent_card, handler) - client = TestClient(app_instance.build()) - - unicode_text = 'こんにちは世界' # "Hello world" in Japanese - - # Mock a handler response - handler.on_message_send.return_value = Message( - role=Role.ROLE_AGENT, - parts=[Part(text=f'Received: {unicode_text}')], - message_id='response-unicode', - ) - - unicode_payload = { - 'jsonrpc': '2.0', - 'method': 'SendMessage', - 'id': 'unicode_test', - 'params': { - 'message': { - 'role': 'ROLE_USER', - 'parts': [{'text': unicode_text}], - 'messageId': 'msg-unicode', - } - }, - } - - response = client.post('/', json=unicode_payload) - - # We are testing that the server can correctly deserialize the unicode payload - assert response.status_code == 200 - data = response.json() - # Check that we got a result (handler was called) - if 'result' in data: - # Response should contain the unicode text - result = data['result'] - if 'message' in result: - assert ( - result['message']['parts'][0]['text'] - == f'Received: {unicode_text}' - ) - elif 'parts' in result: - assert result['parts'][0]['text'] == f'Received: {unicode_text}' - - -def test_fastapi_sub_application(minimal_agent_card: AgentCard): - """ - Tests that the A2AFastAPIApplication endpoint correctly passes the url in sub-application. - """ - from fastapi import FastAPI - - handler = mock.AsyncMock() - sub_app_instance = A2AFastAPIApplication(minimal_agent_card, handler) - app_instance = FastAPI() - app_instance.mount('/a2a', sub_app_instance.build()) - client = TestClient(app_instance) - - response = client.get('/a2a/openapi.json') - assert response.status_code == 200 - response_data = response.json() - - # The generated a2a.json (OpenAPI 2.0 / Swagger) does not typically include a 'servers' block - # unless specifically configured or converted to OpenAPI 3.0. - # FastAPI usually generates OpenAPI 3.0 schemas which have 'servers'. - # When we inject the raw Swagger 2.0 schema, it won't have 'servers'. - # We check if it is indeed the injected schema by checking for 'swagger': '2.0' - # or by checking for 'basePath' if we want to test path correctness. - - if response_data.get('swagger') == '2.0': - # It's the injected Swagger 2.0 schema - pass - else: - # It's an auto-generated OpenAPI 3.0+ schema (fallback or otherwise) - assert 'servers' in response_data - assert response_data['servers'] == [{'url': '/a2a'}] diff --git a/tests/server/apps/jsonrpc/test_starlette_app.py b/tests/server/apps/jsonrpc/test_starlette_app.py deleted file mode 100644 index fa686871..00000000 --- a/tests/server/apps/jsonrpc/test_starlette_app.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Any -from unittest.mock import MagicMock - -import pytest - -from a2a.server.apps.jsonrpc import starlette_app -from a2a.server.apps.jsonrpc.starlette_app import A2AStarletteApplication -from a2a.server.request_handlers.request_handler import ( - RequestHandler, # For mock spec -) -from a2a.types.a2a_pb2 import AgentCard # For mock spec - - -# --- A2AStarletteApplication Tests --- - - -class TestA2AStarletteApplicationOptionalDeps: - # Running tests in this class requires optional dependencies starlette and - # sse-starlette to be present in the test environment. - - @pytest.fixture(scope='class', autouse=True) - def ensure_pkg_starlette_is_present(self): - try: - import sse_starlette as _sse_starlette # noqa: F401 - import starlette as _starlette # noqa: F401 - except ImportError: - pytest.fail( - f'Running tests in {self.__class__.__name__} requires' - ' optional dependencies starlette and sse-starlette to be' - ' present in the test environment. Run `uv sync --dev ...`' - ' before running the test suite.' - ) - - @pytest.fixture(scope='class') - def mock_app_params(self) -> dict: - # Mock http_handler - mock_handler = MagicMock(spec=RequestHandler) - # Mock agent_card with essential attributes accessed in __init__ - mock_agent_card = MagicMock(spec=AgentCard) - # Ensure 'url' attribute exists on the mock_agent_card, as it's accessed - # in __init__ - mock_agent_card.url = 'http://example.com' - # Ensure 'capabilities.extended_agent_card' attribute exists - return {'agent_card': mock_agent_card, 'http_handler': mock_handler} - - @pytest.fixture(scope='class') - def mark_pkg_starlette_not_installed(self): - pkg_starlette_installed_flag = ( - starlette_app._package_starlette_installed - ) - starlette_app._package_starlette_installed = False - yield - starlette_app._package_starlette_installed = ( - pkg_starlette_installed_flag - ) - - def test_create_a2a_starlette_app_with_present_deps_succeeds( - self, mock_app_params: dict - ): - try: - _app = A2AStarletteApplication(**mock_app_params) - except ImportError: - pytest.fail( - 'With packages starlette and see-starlette present, creating an' - ' A2AStarletteApplication instance should not raise ImportError' - ) - - def test_create_a2a_starlette_app_with_missing_deps_raises_importerror( - self, - mock_app_params: dict, - mark_pkg_starlette_not_installed: Any, - ): - with pytest.raises( - ImportError, - match='Packages `starlette` and `sse-starlette` are required', - ): - _app = A2AStarletteApplication(**mock_app_params) - - -if __name__ == '__main__': - pytest.main([__file__]) diff --git a/tests/server/routes/test_agent_card_routes.py b/tests/server/routes/test_agent_card_routes.py new file mode 100644 index 00000000..8f86ec93 --- /dev/null +++ b/tests/server/routes/test_agent_card_routes.py @@ -0,0 +1,105 @@ +# ruff: noqa: INP001 +import asyncio +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import pytest +from starlette.testclient import TestClient +from starlette.middleware import Middleware + +from a2a.server.routes.agent_card_routes import AgentCardRoutes +from a2a.types.a2a_pb2 import AgentCard + + +@pytest.fixture +def agent_card(): + return AgentCard() + + +def test_get_agent_card_success(agent_card): + """Tests that the agent card route returns the card correctly.""" + routes = AgentCardRoutes(agent_card=agent_card).routes + + from starlette.applications import Starlette + + app = Starlette(routes=routes) + client = TestClient(app) + + response = client.get('/.well-known/agent-card.json') + assert response.status_code == 200 + assert response.headers['content-type'] == 'application/json' + assert response.json() == {} # Empty card serializes to empty dict/json + + +def test_get_agent_card_with_modifier(agent_card): + """Tests that card_modifier is called and modifies the response.""" + + # To test modification, let's assume we can mock the dict conversion or just see if the modifier runs. + # Actually card_modifier receives AgentCard and returns AgentCard. + async def modifier(card: AgentCard) -> AgentCard: + # Clone or modify + modified = AgentCard() + # Set some field if possible, or just return a different instance to verify. + # Since Protobuf objects have fields, let's look at one we can set. + # Usually they have fields like 'url' in v0.3 or others. + # Let's just return a MagicMock or set Something that shows up in dict if we know it. + # Wait, if we return a different object, we can verify it. + # Let's try to mock the conversion or just verify it was called. + return card + + mock_modifier = AsyncMock(side_effect=modifier) + routes = AgentCardRoutes( + agent_card=agent_card, card_modifier=mock_modifier + ).routes + + from starlette.applications import Starlette + + app = Starlette(routes=routes) + client = TestClient(app) + + response = client.get('/.well-known/agent-card.json') + assert response.status_code == 200 + assert mock_modifier.called + + +def test_agent_card_custom_url(agent_card): + """Tests that custom card_url is respected.""" + custom_url = '/custom/path/agent.json' + routes = AgentCardRoutes(agent_card=agent_card, card_url=custom_url).routes + + from starlette.applications import Starlette + + app = Starlette(routes=routes) + client = TestClient(app) + + # Check that default returns 404 + assert client.get('/.well-known/agent-card.json').status_code == 404 + # Check that custom returns 200 + assert client.get(custom_url).status_code == 200 + + +def test_agent_card_with_middleware(agent_card): + """Tests that middleware is applied to the routes.""" + middleware_called = False + + class MyMiddleware: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + nonlocal middleware_called + middleware_called = True + await self.app(scope, receive, send) + + routes = AgentCardRoutes( + agent_card=agent_card, middleware=[Middleware(MyMiddleware)] + ).routes + + from starlette.applications import Starlette + + app = Starlette(routes=routes) + client = TestClient(app) + + response = client.get('/.well-known/agent-card.json') + assert response.status_code == 200 + assert middleware_called is True diff --git a/tests/server/apps/jsonrpc/test_jsonrpc_app.py b/tests/server/routes/test_jsonrpc_dispatcher.py similarity index 51% rename from tests/server/apps/jsonrpc/test_jsonrpc_app.py rename to tests/server/routes/test_jsonrpc_dispatcher.py index be54958b..7241cac4 100644 --- a/tests/server/apps/jsonrpc/test_jsonrpc_app.py +++ b/tests/server/routes/test_jsonrpc_dispatcher.py @@ -1,38 +1,37 @@ # ruff: noqa: INP001 +import json from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest - from starlette.responses import JSONResponse from starlette.testclient import TestClient - -# Attempt to import StarletteBaseUser, fallback to MagicMock if not available try: from starlette.authentication import BaseUser as StarletteBaseUser except ImportError: StarletteBaseUser = MagicMock() # type: ignore from a2a.extensions.common import HTTP_EXTENSION_HEADER -from a2a.server.apps.jsonrpc import ( - jsonrpc_app, # Keep this import for optional deps test -) -from a2a.server.apps.jsonrpc.jsonrpc_app import ( - JSONRPCApplication, - StarletteUserProxy, -) -from a2a.server.apps.jsonrpc.starlette_app import A2AStarletteApplication from a2a.server.context import ServerCallContext -from a2a.server.request_handlers.request_handler import ( - RequestHandler, -) # For mock spec +from a2a.server.request_handlers.request_handler import RequestHandler from a2a.types.a2a_pb2 import ( AgentCard, Message, Part, Role, ) +from a2a.server.routes import jsonrpc_dispatcher +from a2a.server.routes.jsonrpc_dispatcher import ( + CallContextBuilder, + DefaultCallContextBuilder, + JsonRpcDispatcher, + StarletteUserProxy, +) +from a2a.server.routes.jsonrpc_routes import JsonRpcRoutes +from a2a.server.routes.agent_card_routes import AgentCardRoutes +from a2a.server.jsonrpc_models import JSONRPCError +from a2a.utils.errors import A2AError # --- StarletteUserProxy Tests --- @@ -58,12 +57,7 @@ def test_starlette_user_proxy_user_name(self): assert proxy.user_name == 'Test User DisplayName' def test_starlette_user_proxy_user_name_raises_attribute_error(self): - """ - Tests that if the underlying starlette user object is missing the - display_name attribute, the proxy currently raises an AttributeError. - """ starlette_user_mock = MagicMock(spec=StarletteBaseUser) - # Ensure display_name is not present on the mock to trigger AttributeError del starlette_user_mock.display_name proxy = StarletteUserProxy(starlette_user_mock) @@ -71,13 +65,12 @@ def test_starlette_user_proxy_user_name_raises_attribute_error(self): _ = proxy.user_name -# --- JSONRPCApplication Tests (Selected) --- +# --- JsonRpcDispatcher Tests --- @pytest.fixture def mock_handler(): handler = AsyncMock(spec=RequestHandler) - # Return a proto Message object directly - the handler wraps it in SendMessageResponse handler.on_message_send.return_value = Message( message_id='test', role=Role.ROLE_AGENT, @@ -90,23 +83,26 @@ def mock_handler(): def test_app(mock_handler): mock_agent_card = MagicMock(spec=AgentCard) mock_agent_card.url = 'http://mockurl.com' - # Set up capabilities.streaming to avoid validation issues mock_agent_card.capabilities = MagicMock() mock_agent_card.capabilities.streaming = False - return A2AStarletteApplication( - agent_card=mock_agent_card, http_handler=mock_handler + + jsonrpc_routes = JsonRpcRoutes( + agent_card=mock_agent_card, request_handler=mock_handler, rpc_url='/' ) + from starlette.applications import Starlette + + return Starlette(routes=jsonrpc_routes.routes) + @pytest.fixture def client(test_app): - return TestClient(test_app.build(), headers={'A2A-Version': '1.0'}) + return TestClient(test_app, headers={'A2A-Version': '1.0'}) def _make_send_message_request( text: str = 'hi', tenant: str | None = None ) -> dict: - """Helper to create a JSON-RPC send message request.""" params: dict[str, Any] = { 'message': { 'messageId': '1', @@ -125,113 +121,39 @@ def _make_send_message_request( } -class TestJSONRPCApplicationSetup: # Renamed to avoid conflict - def test_jsonrpc_app_build_method_abstract_raises_typeerror( - self, - ): # Renamed test - mock_handler = MagicMock(spec=RequestHandler) - # Mock agent_card with essential attributes accessed in JSONRPCApplication.__init__ - mock_agent_card = MagicMock(spec=AgentCard) - # Ensure 'url' attribute exists on the mock_agent_card, as it's accessed in __init__ - mock_agent_card.url = 'http://mockurl.com' - # Ensure 'supportsAuthenticatedExtendedCard' attribute exists - - # This will fail at definition time if an abstract method is not implemented - with pytest.raises( - TypeError, - match=r".*abstract class IncompleteJSONRPCApp .* abstract method '?build'?", - ): - - class IncompleteJSONRPCApp(JSONRPCApplication): - # Intentionally not implementing 'build' - def some_other_method(self): - pass - - IncompleteJSONRPCApp( - agent_card=mock_agent_card, http_handler=mock_handler - ) # type: ignore[abstract] - - -class TestJSONRPCApplicationOptionalDeps: - # Running tests in this class requires optional dependencies starlette and - # sse-starlette to be present in the test environment. - - @pytest.fixture(scope='class', autouse=True) - def ensure_pkg_starlette_is_present(self): - try: - import sse_starlette as _sse_starlette # noqa: F401, PLC0415 - import starlette as _starlette # noqa: F401, PLC0415 - except ImportError: - pytest.fail( - f'Running tests in {self.__class__.__name__} requires' - ' optional dependencies starlette and sse-starlette to be' - ' present in the test environment. Run `uv sync --dev ...`' - ' before running the test suite.' - ) - +class TestJsonRpcDispatcherOptionalDependencies: @pytest.fixture(scope='class') def mock_app_params(self) -> dict: - # Mock http_handler mock_handler = MagicMock(spec=RequestHandler) - # Mock agent_card with essential attributes accessed in __init__ mock_agent_card = MagicMock(spec=AgentCard) - # Ensure 'url' attribute exists on the mock_agent_card, as it's accessed - # in __init__ mock_agent_card.url = 'http://example.com' - # Ensure 'supportsAuthenticatedExtendedCard' attribute exists return {'agent_card': mock_agent_card, 'http_handler': mock_handler} @pytest.fixture(scope='class') def mark_pkg_starlette_not_installed(self): - pkg_starlette_installed_flag = jsonrpc_app._package_starlette_installed - jsonrpc_app._package_starlette_installed = False + pkg_starlette_installed_flag = ( + jsonrpc_dispatcher._package_starlette_installed + ) + jsonrpc_dispatcher._package_starlette_installed = False yield - jsonrpc_app._package_starlette_installed = pkg_starlette_installed_flag - - def test_create_jsonrpc_based_app_with_present_deps_succeeds( - self, mock_app_params: dict - ): - class MockJSONRPCApp(JSONRPCApplication): - def build( # type: ignore[override] - self, - agent_card_url='/.well-known/agent.json', - rpc_url='/', - **kwargs, - ): - return object() # type: ignore[return-value] - - try: - _app = MockJSONRPCApp(**mock_app_params) - except ImportError: - pytest.fail( - 'With packages starlette and see-starlette present, creating a' - ' JSONRPCApplication-based instance should not raise' - ' ImportError' - ) + jsonrpc_dispatcher._package_starlette_installed = ( + pkg_starlette_installed_flag + ) - def test_create_jsonrpc_based_app_with_missing_deps_raises_importerror( + def test_create_dispatcher_with_missing_deps_raises_importerror( self, mock_app_params: dict, mark_pkg_starlette_not_installed: Any ): - class MockJSONRPCApp(JSONRPCApplication): - def build( # type: ignore[override] - self, - agent_card_url='/.well-known/agent.json', - rpc_url='/', - **kwargs, - ): - return object() # type: ignore[return-value] - with pytest.raises( ImportError, match=( 'Packages `starlette` and `sse-starlette` are required to use' - ' the `JSONRPCApplication`' + ' the `JsonRpcDispatcher`' ), ): - _app = MockJSONRPCApp(**mock_app_params) + JsonRpcDispatcher(**mock_app_params) -class TestJSONRPCApplicationExtensions: +class TestJsonRpcDispatcherExtensions: def test_request_with_single_extension(self, client, mock_handler): headers = {HTTP_EXTENSION_HEADER: 'foo'} response = client.post( @@ -261,24 +183,6 @@ def test_request_with_comma_separated_extensions( call_context = mock_handler.on_message_send.call_args[0][1] assert call_context.requested_extensions == {'foo', 'bar'} - def test_request_with_comma_separated_extensions_no_space( - self, client, mock_handler - ): - headers = [ - (HTTP_EXTENSION_HEADER, 'foo, bar'), - (HTTP_EXTENSION_HEADER, 'baz'), - ] - response = client.post( - '/', - headers=headers, - json=_make_send_message_request(), - ) - response.raise_for_status() - - mock_handler.on_message_send.assert_called_once() - call_context = mock_handler.on_message_send.call_args[0][1] - assert call_context.requested_extensions == {'foo', 'bar', 'baz'} - def test_method_added_to_call_context_state(self, client, mock_handler): response = client.post( '/', @@ -290,29 +194,10 @@ def test_method_added_to_call_context_state(self, client, mock_handler): call_context = mock_handler.on_message_send.call_args[0][1] assert call_context.state['method'] == 'SendMessage' - def test_request_with_multiple_extension_headers( - self, client, mock_handler - ): - headers = [ - (HTTP_EXTENSION_HEADER, 'foo'), - (HTTP_EXTENSION_HEADER, 'bar'), - ] - response = client.post( - '/', - headers=headers, - json=_make_send_message_request(), - ) - response.raise_for_status() - - mock_handler.on_message_send.assert_called_once() - call_context = mock_handler.on_message_send.call_args[0][1] - assert call_context.requested_extensions == {'foo', 'bar'} - def test_response_with_activated_extensions(self, client, mock_handler): def side_effect(request, context: ServerCallContext): context.activated_extensions.add('foo') context.activated_extensions.add('baz') - # Return a proto Message object directly return Message( message_id='test', role=Role.ROLE_AGENT, @@ -335,7 +220,7 @@ def side_effect(request, context: ServerCallContext): } -class TestJSONRPCApplicationTenant: +class TestJsonRpcDispatcherTenant: def test_tenant_extraction_from_params(self, client, mock_handler): tenant_id = 'my-tenant-123' response = client.post( @@ -362,20 +247,23 @@ def test_no_tenant_extraction(self, client, mock_handler): assert call_context.tenant == '' -class TestJSONRPCApplicationV03Compat: +class TestJsonRpcDispatcherV03Compat: def test_v0_3_compat_flag_routes_to_adapter(self, mock_handler): mock_agent_card = MagicMock(spec=AgentCard) mock_agent_card.url = 'http://mockurl.com' mock_agent_card.capabilities = MagicMock() mock_agent_card.capabilities.streaming = False - app = A2AStarletteApplication( + from starlette.applications import Starlette + + jsonrpc_routes = JsonRpcRoutes( agent_card=mock_agent_card, - http_handler=mock_handler, + request_handler=mock_handler, enable_v0_3_compat=True, + rpc_url='/', ) - - client = TestClient(app.build()) + app = Starlette(routes=jsonrpc_routes.routes) + client = TestClient(app) request_data = { 'jsonrpc': '2.0', @@ -390,8 +278,11 @@ def test_v0_3_compat_flag_routes_to_adapter(self, mock_handler): }, } + dispatcher_instance = jsonrpc_routes.dispatcher with patch.object( - app._v03_adapter, 'handle_request', new_callable=AsyncMock + dispatcher_instance._v03_adapter, + 'handle_request', + new_callable=AsyncMock, ) as mock_handle: mock_handle.return_value = JSONResponse( {'jsonrpc': '2.0', 'id': '1', 'result': {}} @@ -403,42 +294,6 @@ def test_v0_3_compat_flag_routes_to_adapter(self, mock_handler): assert mock_handle.called assert mock_handle.call_args[1]['method'] == 'message/send' - def test_v0_3_compat_flag_disabled_rejects_v0_3_method(self, mock_handler): - mock_agent_card = MagicMock(spec=AgentCard) - mock_agent_card.url = 'http://mockurl.com' - mock_agent_card.capabilities = MagicMock() - mock_agent_card.capabilities.streaming = False - - app = A2AStarletteApplication( - agent_card=mock_agent_card, - http_handler=mock_handler, - enable_v0_3_compat=False, - ) - - client = TestClient(app.build()) - - request_data = { - 'jsonrpc': '2.0', - 'id': '1', - 'method': 'message/send', - 'params': { - 'message': { - 'messageId': 'msg-1', - 'role': 'ROLE_USER', - 'parts': [{'text': 'Hello'}], - } - }, - } - - response = client.post('/', json=request_data) - - assert response.status_code == 200 - # Should return MethodNotFoundError because the v0.3 method is not recognized - # without the adapter enabled. - resp_json = response.json() - assert 'error' in resp_json - assert resp_json['error']['code'] == -32601 - if __name__ == '__main__': pytest.main([__file__]) diff --git a/tests/server/routes/test_jsonrpc_routes.py b/tests/server/routes/test_jsonrpc_routes.py new file mode 100644 index 00000000..b4cd2f2b --- /dev/null +++ b/tests/server/routes/test_jsonrpc_routes.py @@ -0,0 +1,96 @@ +# ruff: noqa: INP001 +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import pytest +from starlette.testclient import TestClient +from starlette.middleware import Middleware + +from a2a.server.routes.jsonrpc_routes import JsonRpcRoutes +from a2a.server.request_handlers.request_handler import RequestHandler +from a2a.types.a2a_pb2 import AgentCard + + +@pytest.fixture +def agent_card(): + return AgentCard() + + +@pytest.fixture +def mock_handler(): + return AsyncMock(spec=RequestHandler) + + +def test_routes_creation(agent_card, mock_handler): + """Tests that JsonRpcRoutes creates Route objects list.""" + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, request_handler=mock_handler + ) + + assert hasattr(jsonrpc_routes, 'routes') + assert isinstance(jsonrpc_routes.routes, list) + assert len(jsonrpc_routes.routes) == 1 + + from starlette.routing import Route + + assert isinstance(jsonrpc_routes.routes[0], Route) + assert jsonrpc_routes.routes[0].methods == {'POST'} + + +def test_jsonrpc_custom_url(agent_card, mock_handler): + """Tests that custom rpc_url is respected for routing.""" + custom_url = '/custom/api/jsonrpc' + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, request_handler=mock_handler, rpc_url=custom_url + ) + + from starlette.applications import Starlette + + app = Starlette(routes=jsonrpc_routes.routes) + client = TestClient(app) + + # Check that default path returns 404 + assert client.post('/a2a/jsonrpc', json={}).status_code == 404 + + # Check that custom path routes to dispatcher (which will return JSON-RPC response, even if error) + response = client.post( + custom_url, json={'jsonrpc': '2.0', 'id': '1', 'method': 'foo'} + ) + assert response.status_code == 200 + resp_json = response.json() + assert 'error' in resp_json + # Method not found error from dispatcher + assert resp_json['error']['code'] == -32601 + + +def test_jsonrpc_with_middleware(agent_card, mock_handler): + """Tests that middleware is applied to the route.""" + middleware_called = False + + class MyMiddleware: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + nonlocal middleware_called + middleware_called = True + await self.app(scope, receive, send) + + jsonrpc_routes = JsonRpcRoutes( + agent_card=agent_card, + request_handler=mock_handler, + middleware=[Middleware(MyMiddleware)], + rpc_url='/', + ) + + from starlette.applications import Starlette + + app = Starlette(routes=jsonrpc_routes.routes) + client = TestClient(app) + + # Call to trigger middleware + # Empty JSON might raise error, let's send a base valid format for dispatcher + client.post( + '/', json={'jsonrpc': '2.0', 'id': '1', 'method': 'SendMessage'} + ) + assert middleware_called is True diff --git a/tests/server/test_integration.py b/tests/server/test_integration.py index 525c8e12..0cc5524d 100644 --- a/tests/server/test_integration.py +++ b/tests/server/test_integration.py @@ -18,10 +18,8 @@ from starlette.routing import Route from starlette.testclient import TestClient -from a2a.server.apps import ( - A2AFastAPIApplication, - A2AStarletteApplication, -) +from a2a.server.routes import AgentCardRoutes, JsonRpcRoutes + from a2a.server.context import ServerCallContext from a2a.server.jsonrpc_models import ( InternalError, @@ -148,14 +146,48 @@ def handler(): return handler +class AppBuilder: + def __init__(self, agent_card, handler, card_modifier=None): + self.agent_card = agent_card + self.handler = handler + self.card_modifier = card_modifier + + def build( + self, + rpc_url='/', + agent_card_url=AGENT_CARD_WELL_KNOWN_PATH, + middleware=None, + routes=None, + ): + from starlette.applications import Starlette + + app_instance = Starlette(middleware=middleware, routes=routes or []) + + # Agent card router + card_routes = AgentCardRoutes( + self.agent_card, + card_url=agent_card_url, + card_modifier=self.card_modifier, + ) + app_instance.routes.extend(card_routes.routes) + + # JSON-RPC router + rpc_routes = JsonRpcRoutes( + self.agent_card, self.handler, rpc_url=rpc_url + ) + app_instance.routes.extend(rpc_routes.routes) + + return app_instance + + @pytest.fixture def app(agent_card: AgentCard, handler: mock.AsyncMock): - return A2AStarletteApplication(agent_card, handler) + return AppBuilder(agent_card, handler) @pytest.fixture -def client(app: A2AStarletteApplication, **kwargs): - """Create a test client with the Starlette app.""" +def client(app, **kwargs): + """Create a test client with the app builder.""" return TestClient(app.build(**kwargs), headers={'A2A-Version': '1.0'}) @@ -172,9 +204,7 @@ def test_agent_card_endpoint(client: TestClient, agent_card: AgentCard): assert 'streaming' in data['capabilities'] -def test_agent_card_custom_url( - app: A2AStarletteApplication, agent_card: AgentCard -): +def test_agent_card_custom_url(app, agent_card: AgentCard): """Test the agent card endpoint with a custom URL.""" client = TestClient(app.build(agent_card_url='/my-agent')) response = client.get('/my-agent') @@ -183,9 +213,7 @@ def test_agent_card_custom_url( assert data['name'] == agent_card.name -def test_starlette_rpc_endpoint_custom_url( - app: A2AStarletteApplication, handler: mock.AsyncMock -): +def test_starlette_rpc_endpoint_custom_url(app, handler: mock.AsyncMock): """Test the RPC endpoint with a custom URL.""" # Provide a valid Task object as the return value task_status = MINIMAL_TASK_STATUS @@ -208,9 +236,7 @@ def test_starlette_rpc_endpoint_custom_url( assert data['result']['id'] == 'task1' -def test_fastapi_rpc_endpoint_custom_url( - app: A2AFastAPIApplication, handler: mock.AsyncMock -): +def test_fastapi_rpc_endpoint_custom_url(app, handler: mock.AsyncMock): """Test the RPC endpoint with a custom URL.""" # Provide a valid Task object as the return value task_status = MINIMAL_TASK_STATUS @@ -233,9 +259,7 @@ def test_fastapi_rpc_endpoint_custom_url( assert data['result']['id'] == 'task1' -def test_starlette_build_with_extra_routes( - app: A2AStarletteApplication, agent_card: AgentCard -): +def test_starlette_build_with_extra_routes(app, agent_card: AgentCard): """Test building the app with additional routes.""" def custom_handler(request): @@ -243,7 +267,7 @@ def custom_handler(request): extra_route = Route('/hello', custom_handler, methods=['GET']) test_app = app.build(routes=[extra_route]) - client = TestClient(test_app) + client = TestClient(test_app, headers={'A2A-Version': '1.0'}) # Test the added route response = client.get('/hello') @@ -257,9 +281,7 @@ def custom_handler(request): assert data['name'] == agent_card.name -def test_fastapi_build_with_extra_routes( - app: A2AFastAPIApplication, agent_card: AgentCard -): +def test_fastapi_build_with_extra_routes(app, agent_card: AgentCard): """Test building the app with additional routes.""" def custom_handler(request): @@ -281,9 +303,7 @@ def custom_handler(request): assert data['name'] == agent_card.name -def test_fastapi_build_custom_agent_card_path( - app: A2AFastAPIApplication, agent_card: AgentCard -): +def test_fastapi_build_custom_agent_card_path(app, agent_card: AgentCard): """Test building the app with a custom agent card path.""" test_app = app.build(agent_card_url='/agent-card') @@ -471,7 +491,7 @@ def test_get_push_notification_config( handler.on_get_task_push_notification_config.assert_awaited_once() -def test_server_auth(app: A2AStarletteApplication, handler: mock.AsyncMock): +def test_server_auth(app, handler: mock.AsyncMock): class TestAuthMiddleware(AuthenticationBackend): async def authenticate( self, conn: HTTPConnection @@ -534,9 +554,7 @@ async def authenticate( @pytest.mark.asyncio -async def test_message_send_stream( - app: A2AStarletteApplication, handler: mock.AsyncMock -) -> None: +async def test_message_send_stream(app, handler: mock.AsyncMock) -> None: """Test streaming message sending.""" # Setup mock streaming response @@ -614,9 +632,7 @@ async def stream_generator(): @pytest.mark.asyncio -async def test_task_resubscription( - app: A2AStarletteApplication, handler: mock.AsyncMock -) -> None: +async def test_task_resubscription(app, handler: mock.AsyncMock) -> None: """Test task resubscription streaming.""" # Setup mock streaming response @@ -751,9 +767,7 @@ async def modifier(card: AgentCard) -> AgentCard: modified_card.name = 'Dynamically Modified Agent' return modified_card - app_instance = A2AStarletteApplication( - agent_card, handler, card_modifier=modifier - ) + app_instance = AppBuilder(agent_card, handler, card_modifier=modifier) client = TestClient(app_instance.build()) response = client.get(AGENT_CARD_WELL_KNOWN_PATH) @@ -776,9 +790,7 @@ def modifier(card: AgentCard) -> AgentCard: modified_card.name = 'Dynamically Modified Agent' return modified_card - app_instance = A2AStarletteApplication( - agent_card, handler, card_modifier=modifier - ) + app_instance = AppBuilder(agent_card, handler, card_modifier=modifier) client = TestClient(app_instance.build()) response = client.get(AGENT_CARD_WELL_KNOWN_PATH) @@ -801,9 +813,7 @@ async def modifier(card: AgentCard) -> AgentCard: modified_card.name = 'Dynamically Modified Agent' return modified_card - app_instance = A2AFastAPIApplication( - agent_card, handler, card_modifier=modifier - ) + app_instance = AppBuilder(agent_card, handler, card_modifier=modifier) client = TestClient(app_instance.build()) response = client.get(AGENT_CARD_WELL_KNOWN_PATH) @@ -823,9 +833,7 @@ def modifier(card: AgentCard) -> AgentCard: modified_card.name = 'Dynamically Modified Agent' return modified_card - app_instance = A2AFastAPIApplication( - agent_card, handler, card_modifier=modifier - ) + app_instance = AppBuilder(agent_card, handler, card_modifier=modifier) client = TestClient(app_instance.build()) response = client.get(AGENT_CARD_WELL_KNOWN_PATH) @@ -937,7 +945,7 @@ def test_agent_card_backward_compatibility_supports_extended_card( ): """Test that supportsAuthenticatedExtendedCard is injected when extended_agent_card is True.""" agent_card.capabilities.extended_agent_card = True - app_instance = A2AStarletteApplication(agent_card, handler) + app_instance = AppBuilder(agent_card, handler) client = TestClient(app_instance.build()) response = client.get(AGENT_CARD_WELL_KNOWN_PATH) assert response.status_code == 200 @@ -950,7 +958,7 @@ def test_agent_card_backward_compatibility_no_extended_card( ): """Test that supportsAuthenticatedExtendedCard is absent when extended_agent_card is False.""" agent_card.capabilities.extended_agent_card = False - app_instance = A2AStarletteApplication(agent_card, handler) + app_instance = AppBuilder(agent_card, handler) client = TestClient(app_instance.build()) response = client.get(AGENT_CARD_WELL_KNOWN_PATH) assert response.status_code == 200 From 71a9285df9e94d754cee684ef9c476997223823b Mon Sep 17 00:00:00 2001 From: Guglielmo Colombo Date: Thu, 19 Mar 2026 21:10:39 +0100 Subject: [PATCH 2/6] Update src/a2a/server/routes/agent_card_routes.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/a2a/server/routes/agent_card_routes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/a2a/server/routes/agent_card_routes.py b/src/a2a/server/routes/agent_card_routes.py index 30d635f1..7b63b5c0 100644 --- a/src/a2a/server/routes/agent_card_routes.py +++ b/src/a2a/server/routes/agent_card_routes.py @@ -60,8 +60,9 @@ def __init__( """ if not _package_starlette_installed: raise ImportError( - 'The `starlette` package is required to use the `AgentCardRoutes`.' - ' `a2a-sdk[http-server]`.' + 'The `starlette` package is required to use `AgentCardRoutes`. ' + 'It can be installed as part of `a2a-sdk` optional dependencies, `a2a-sdk[http-server]`.' + ) ) self.agent_card = agent_card From fee5d5e67a8e441bf13abe31ba07d8830e92e3fd Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Thu, 19 Mar 2026 20:29:36 +0000 Subject: [PATCH 3/6] fix suggestions --- src/a2a/server/routes/agent_card_routes.py | 17 +++++------ src/a2a/server/routes/jsonrpc_dispatcher.py | 29 ------------------- src/a2a/utils/constants.py | 1 - tests/server/routes/test_agent_card_routes.py | 9 +----- tests/server/routes/test_jsonrpc_routes.py | 5 +--- 5 files changed, 9 insertions(+), 52 deletions(-) diff --git a/src/a2a/server/routes/agent_card_routes.py b/src/a2a/server/routes/agent_card_routes.py index 7b63b5c0..067477bd 100644 --- a/src/a2a/server/routes/agent_card_routes.py +++ b/src/a2a/server/routes/agent_card_routes.py @@ -63,24 +63,21 @@ def __init__( 'The `starlette` package is required to use `AgentCardRoutes`. ' 'It can be installed as part of `a2a-sdk` optional dependencies, `a2a-sdk[http-server]`.' ) - ) self.agent_card = agent_card self.card_modifier = card_modifier - async def get_agent_card(request: Request) -> Response: - card_to_serve = self.agent_card - if self.card_modifier: - card_to_serve = await maybe_await( - self.card_modifier(card_to_serve) - ) - return JSONResponse(agent_card_to_dict(card_to_serve)) - self.routes = [ Route( path=card_url, - endpoint=get_agent_card, + endpoint=self._get_agent_card, methods=['GET'], middleware=middleware, ) ] + + async def _get_agent_card(self, request: Request) -> Response: + card_to_serve = self.agent_card + if self.card_modifier: + card_to_serve = await maybe_await(self.card_modifier(card_to_serve)) + return JSONResponse(agent_card_to_dict(card_to_serve)) diff --git a/src/a2a/server/routes/jsonrpc_dispatcher.py b/src/a2a/server/routes/jsonrpc_dispatcher.py index 14a0cc0b..970d0620 100644 --- a/src/a2a/server/routes/jsonrpc_dispatcher.py +++ b/src/a2a/server/routes/jsonrpc_dispatcher.py @@ -47,9 +47,6 @@ SubscribeToTaskRequest, TaskPushNotificationConfig, ) -from a2a.utils.constants import ( - DEFAULT_MAX_CONTENT_LENGTH, -) from a2a.utils.errors import ( A2AError, UnsupportedOperationError, @@ -202,7 +199,6 @@ def __init__( # noqa: PLR0913 ] | None = None, enable_v0_3_compat: bool = False, - max_content_length: int | None = DEFAULT_MAX_CONTENT_LENGTH, ) -> None: """Initializes the JsonRpcDispatcher. @@ -220,8 +216,6 @@ def __init__( # noqa: PLR0913 extended_card_modifier: An optional callback to dynamically modify the extended agent card before it is served. It receives the call context. - max_content_length: The maximum allowed content length for incoming - requests. Defaults to 10MB. Set to None for unbounded maximum. enable_v0_3_compat: Whether to enable v0.3 backward compatibility on the same endpoint. """ if not _package_starlette_installed: @@ -242,7 +236,6 @@ def __init__( # noqa: PLR0913 extended_card_modifier=extended_card_modifier, ) self._context_builder = context_builder or DefaultCallContextBuilder() - self._max_content_length = max_content_length self.enable_v0_3_compat = enable_v0_3_compat self._v03_adapter: JSONRPC03Adapter | None = None @@ -298,22 +291,6 @@ def _generate_error_response( status_code=200, ) - def _allowed_content_length(self, request: Request) -> bool: - """Checks if the request content length is within the allowed maximum. - - Args: - request: The incoming Starlette Request object. - - Returns: - False if the content length is larger than the allowed maximum, True otherwise. - """ - if self._max_content_length is not None: - with contextlib.suppress(ValueError): - content_length = int(request.headers.get('content-length', '0')) - if content_length and content_length > self._max_content_length: - return False - return True - async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911, PLR0912 """Handles incoming POST requests to the main A2A endpoint. @@ -344,12 +321,6 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911 request_id, str | int ): request_id = None - # Treat payloads lager than allowed as invalid request (-32600) before routing - if not self._allowed_content_length(request): - return self._generate_error_response( - request_id, - InvalidRequestError(message='Payload too large'), - ) logger.debug('Request body: %s', body) # 1) Validate base JSON-RPC structure only (-32600 on failure) try: diff --git a/src/a2a/utils/constants.py b/src/a2a/utils/constants.py index 6cee2a05..5497d8a2 100644 --- a/src/a2a/utils/constants.py +++ b/src/a2a/utils/constants.py @@ -20,7 +20,6 @@ class TransportProtocol(str, Enum): GRPC = 'GRPC' -DEFAULT_MAX_CONTENT_LENGTH = 10 * 1024 * 1024 # 10MB JSONRPC_PARSE_ERROR_CODE = -32700 VERSION_HEADER = 'A2A-Version' diff --git a/tests/server/routes/test_agent_card_routes.py b/tests/server/routes/test_agent_card_routes.py index 8f86ec93..01ccce8c 100644 --- a/tests/server/routes/test_agent_card_routes.py +++ b/tests/server/routes/test_agent_card_routes.py @@ -6,6 +6,7 @@ import pytest from starlette.testclient import TestClient from starlette.middleware import Middleware +from starlette.applications import Starlette from a2a.server.routes.agent_card_routes import AgentCardRoutes from a2a.types.a2a_pb2 import AgentCard @@ -20,8 +21,6 @@ def test_get_agent_card_success(agent_card): """Tests that the agent card route returns the card correctly.""" routes = AgentCardRoutes(agent_card=agent_card).routes - from starlette.applications import Starlette - app = Starlette(routes=routes) client = TestClient(app) @@ -52,8 +51,6 @@ async def modifier(card: AgentCard) -> AgentCard: agent_card=agent_card, card_modifier=mock_modifier ).routes - from starlette.applications import Starlette - app = Starlette(routes=routes) client = TestClient(app) @@ -67,8 +64,6 @@ def test_agent_card_custom_url(agent_card): custom_url = '/custom/path/agent.json' routes = AgentCardRoutes(agent_card=agent_card, card_url=custom_url).routes - from starlette.applications import Starlette - app = Starlette(routes=routes) client = TestClient(app) @@ -95,8 +90,6 @@ async def __call__(self, scope, receive, send): agent_card=agent_card, middleware=[Middleware(MyMiddleware)] ).routes - from starlette.applications import Starlette - app = Starlette(routes=routes) client = TestClient(app) diff --git a/tests/server/routes/test_jsonrpc_routes.py b/tests/server/routes/test_jsonrpc_routes.py index b4cd2f2b..5d1b01d9 100644 --- a/tests/server/routes/test_jsonrpc_routes.py +++ b/tests/server/routes/test_jsonrpc_routes.py @@ -5,6 +5,7 @@ import pytest from starlette.testclient import TestClient from starlette.middleware import Middleware +from starlette.applications import Starlette from a2a.server.routes.jsonrpc_routes import JsonRpcRoutes from a2a.server.request_handlers.request_handler import RequestHandler @@ -44,8 +45,6 @@ def test_jsonrpc_custom_url(agent_card, mock_handler): agent_card=agent_card, request_handler=mock_handler, rpc_url=custom_url ) - from starlette.applications import Starlette - app = Starlette(routes=jsonrpc_routes.routes) client = TestClient(app) @@ -83,8 +82,6 @@ async def __call__(self, scope, receive, send): rpc_url='/', ) - from starlette.applications import Starlette - app = Starlette(routes=jsonrpc_routes.routes) client = TestClient(app) From 23e149daef73add8387fdff1fd8985e31818e680 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Fri, 20 Mar 2026 08:19:10 +0000 Subject: [PATCH 4/6] revert test --- .../cross_version/client_server/server_0_3.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/integration/cross_version/client_server/server_0_3.py b/tests/integration/cross_version/client_server/server_0_3.py index 96152c13..7bd5f7e7 100644 --- a/tests/integration/cross_version/client_server/server_0_3.py +++ b/tests/integration/cross_version/client_server/server_0_3.py @@ -8,7 +8,7 @@ from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext -from a2a.server.apps.jsonrpc import A2AFastAPIApplication +from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication from a2a.server.apps.rest.fastapi_app import A2ARESTFastAPIApplication from a2a.server.events.event_queue import EventQueue from a2a.server.events.in_memory_queue_manager import InMemoryQueueManager @@ -188,13 +188,12 @@ async def main_async(http_port: int, grpc_port: int): ) app = FastAPI() - jsonrpc_app = A2AFastAPIApplication( - agent_card=agent_card, - http_handler=handler, - extended_agent_card=agent_card, - ).build() - app.mount('/jsonrpc', jsonrpc_app) - + app.mount( + '/jsonrpc', + A2AFastAPIApplication( + http_handler=handler, agent_card=agent_card + ).build(), + ) app.mount( '/rest', A2ARESTFastAPIApplication( From b3c201e5a00d77d1ac2a9c9e1fe28da3d3780641 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Fri, 20 Mar 2026 08:26:49 +0000 Subject: [PATCH 5/6] revert wrong changes --- tests/__init__.py | 1 - tests/compat/v0_3/test_jsonrpc_app_compat.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index 792d6005..e69de29b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +0,0 @@ -# diff --git a/tests/compat/v0_3/test_jsonrpc_app_compat.py b/tests/compat/v0_3/test_jsonrpc_app_compat.py index 4b344c67..f9581845 100644 --- a/tests/compat/v0_3/test_jsonrpc_app_compat.py +++ b/tests/compat/v0_3/test_jsonrpc_app_compat.py @@ -51,13 +51,13 @@ def test_app(mock_handler): mock_agent_card.capabilities.streaming = False mock_agent_card.capabilities.push_notifications = True mock_agent_card.capabilities.extended_agent_card = True - router = JsonRpcRoutes( + jsonrpc_routes = JsonRpcRoutes( agent_card=mock_agent_card, request_handler=mock_handler, enable_v0_3_compat=True, rpc_url='/', ) - return Starlette(routes=router.routes) + return Starlette(routes=jsonrpc_routes.routes) @pytest.fixture From 3c962e2263076a1d68a9679b9bea1215047b7df6 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Fri, 20 Mar 2026 08:52:36 +0000 Subject: [PATCH 6/6] make method public --- src/a2a/server/routes/jsonrpc_dispatcher.py | 2 +- src/a2a/server/routes/jsonrpc_routes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/a2a/server/routes/jsonrpc_dispatcher.py b/src/a2a/server/routes/jsonrpc_dispatcher.py index 970d0620..1ce5f0fe 100644 --- a/src/a2a/server/routes/jsonrpc_dispatcher.py +++ b/src/a2a/server/routes/jsonrpc_dispatcher.py @@ -291,7 +291,7 @@ def _generate_error_response( status_code=200, ) - async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911, PLR0912 + async def handle_requests(self, request: Request) -> Response: # noqa: PLR0911, PLR0912 """Handles incoming POST requests to the main A2A endpoint. Parses the request body as JSON, validates it against A2A request types, diff --git a/src/a2a/server/routes/jsonrpc_routes.py b/src/a2a/server/routes/jsonrpc_routes.py index cc0e1261..73bca828 100644 --- a/src/a2a/server/routes/jsonrpc_routes.py +++ b/src/a2a/server/routes/jsonrpc_routes.py @@ -100,7 +100,7 @@ def __init__( # noqa: PLR0913 self.routes = [ Route( path=rpc_url, - endpoint=self.dispatcher._handle_requests, # noqa: SLF001 + endpoint=self.dispatcher.handle_requests, methods=['POST'], middleware=middleware, )