From 31f67cb1b5e2d7b59f5482ab5b0f7e1900b06a37 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Tue, 17 Mar 2026 10:35:01 +0000 Subject: [PATCH 1/4] test --- src/a2a/server/apps/jsonrpc/fastapi_app.py | 40 +--------------------- src/a2a/server/apps/rest/fastapi_app.py | 37 +++++++++++++++++++- tck/sut_agent.py | 6 ++-- 3 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py index 20acfc575..c632b4423 100644 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ b/src/a2a/server/apps/jsonrpc/fastapi_app.py @@ -35,44 +35,6 @@ logger = logging.getLogger(__name__) - -class A2AFastAPI(FastAPI): - """A FastAPI application that adds A2A-specific OpenAPI components.""" - - _a2a_components_added: bool = False - - def openapi(self) -> dict[str, Any]: - """Generates the OpenAPI schema for the application.""" - if self.openapi_schema: - return self.openapi_schema - - # Try to use the a2a.json schema generated from the proto file - # if available, instead of generating one from the python types. - try: - from a2a import types # noqa: PLC0415 - - schema_file = importlib.resources.files(types).joinpath('a2a.json') - if schema_file.is_file(): - self.openapi_schema = json.loads( - schema_file.read_text(encoding='utf-8') - ) - if self.openapi_schema: - return self.openapi_schema - except Exception: # noqa: BLE001 - logger.warning( - "Could not load 'a2a.json' from 'a2a.types'. Falling back to auto-generation." - ) - - openapi_schema = super().openapi() - if not self._a2a_components_added: - # A2ARequest is now a Union type of proto messages, so we can't use - # model_json_schema. Instead, we just mark it as added without - # adding the schema since proto types don't have Pydantic schemas. - # The OpenAPI schema will still be functional for the endpoints. - self._a2a_components_added = True - return openapi_schema - - class A2AFastAPIApplication(JSONRPCApplication): """A FastAPI application implementing the A2A protocol server endpoints. @@ -180,7 +142,7 @@ def build( Returns: A configured FastAPI application instance. """ - app = A2AFastAPI(**kwargs) + app = FastAPI(**kwargs) self.add_routes_to_app(app, agent_card_url, rpc_url) diff --git a/src/a2a/server/apps/rest/fastapi_app.py b/src/a2a/server/apps/rest/fastapi_app.py index ea9a501b9..61d21848c 100644 --- a/src/a2a/server/apps/rest/fastapi_app.py +++ b/src/a2a/server/apps/rest/fastapi_app.py @@ -55,6 +55,41 @@ 504: 'DEADLINE_EXCEEDED', } +class A2AFastAPI(FastAPI): + """A FastAPI application that adds A2A-specific OpenAPI components.""" + + _a2a_components_added: bool = False + + def openapi(self) -> dict[str, Any]: + """Generates the OpenAPI schema for the application.""" + if self.openapi_schema: + return self.openapi_schema + + # Try to use the a2a.json schema generated from the proto file + # if available, instead of generating one from the python types. + try: + from a2a import types # noqa: PLC0415 + + schema_file = importlib.resources.files(types).joinpath('a2a.json') + if schema_file.is_file(): + self.openapi_schema = json.loads( + schema_file.read_text(encoding='utf-8') + ) + if self.openapi_schema: + return self.openapi_schema + except Exception: # noqa: BLE001 + logger.warning( + "Could not load 'a2a.json' from 'a2a.types'. Falling back to auto-generation." + ) + + openapi_schema = super().openapi() + if not self._a2a_components_added: + # A2ARequest is now a Union type of proto messages, so we can't use + # model_json_schema. Instead, we just mark it as added without + # adding the schema since proto types don't have Pydantic schemas. + # The OpenAPI schema will still be functional for the endpoints. + self._a2a_components_added = True + return openapi_schema class A2ARESTFastAPIApplication: """A FastAPI application implementing the A2A protocol server REST endpoints. @@ -140,7 +175,7 @@ def build( Returns: A configured FastAPI application instance. """ - app = FastAPI(**kwargs) + app = A2AFastAPI(**kwargs) @app.exception_handler(StarletteHTTPException) async def http_exception_handler( diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 8f2f09379..9dddbaa5a 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -10,6 +10,7 @@ from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext from a2a.server.apps import A2AStarletteApplication +from a2a.server.apps.rest.fastapi_app import A2ARESTFastAPIApplication from a2a.server.events.event_queue import EventQueue from a2a.server.request_handlers.default_request_handler import ( DefaultRequestHandler, @@ -32,6 +33,7 @@ JSONRPC_URL = '/a2a/jsonrpc' +REST_URL = '/a2a/rest' logging.basicConfig(level=logging.INFO) logger = logging.getLogger('SUTAgent') @@ -172,12 +174,12 @@ def serve(task_store: TaskStore) -> None: task_store=task_store, ) - server = A2AStarletteApplication( + server = A2ARESTFastAPIApplication( agent_card=agent_card, http_handler=request_handler, ) - app = server.build(rpc_url=JSONRPC_URL) + app = server.build(rpc_url=REST_URL) logger.info('Starting HTTP server on port %s...', http_port) uvicorn.run(app, host='127.0.0.1', port=http_port, log_level='info') From 64c280df9a950abdedf3d35ebc8c9acdc9da58e0 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Tue, 17 Mar 2026 11:43:38 +0000 Subject: [PATCH 2/4] feat: Allow `A2AFastAPI` to prefix OpenAPI paths with `rpc_url` and refactor import statements. --- src/a2a/server/apps/jsonrpc/fastapi_app.py | 3 +-- src/a2a/server/apps/rest/fastapi_app.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py index c632b4423..0ec9d1ab2 100644 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ b/src/a2a/server/apps/jsonrpc/fastapi_app.py @@ -1,5 +1,3 @@ -import importlib.resources -import json import logging from collections.abc import Awaitable, Callable @@ -35,6 +33,7 @@ logger = logging.getLogger(__name__) + class A2AFastAPIApplication(JSONRPCApplication): """A FastAPI application implementing the A2A protocol server endpoints. diff --git a/src/a2a/server/apps/rest/fastapi_app.py b/src/a2a/server/apps/rest/fastapi_app.py index 61d21848c..aac13b382 100644 --- a/src/a2a/server/apps/rest/fastapi_app.py +++ b/src/a2a/server/apps/rest/fastapi_app.py @@ -1,3 +1,5 @@ +import importlib.resources +import json import logging from collections.abc import Awaitable, Callable @@ -55,10 +57,12 @@ 504: 'DEADLINE_EXCEEDED', } + class A2AFastAPI(FastAPI): """A FastAPI application that adds A2A-specific OpenAPI components.""" _a2a_components_added: bool = False + rpc_url: str = '' def openapi(self) -> dict[str, Any]: """Generates the OpenAPI schema for the application.""" @@ -75,6 +79,11 @@ def openapi(self) -> dict[str, Any]: self.openapi_schema = json.loads( schema_file.read_text(encoding='utf-8') ) + if self.rpc_url and self.openapi_schema: + paths = self.openapi_schema.get('paths', {}) + self.openapi_schema['paths'] = { + f'{self.rpc_url}{k}': v for k, v in paths.items() + } if self.openapi_schema: return self.openapi_schema except Exception: # noqa: BLE001 @@ -84,13 +93,10 @@ def openapi(self) -> dict[str, Any]: openapi_schema = super().openapi() if not self._a2a_components_added: - # A2ARequest is now a Union type of proto messages, so we can't use - # model_json_schema. Instead, we just mark it as added without - # adding the schema since proto types don't have Pydantic schemas. - # The OpenAPI schema will still be functional for the endpoints. self._a2a_components_added = True return openapi_schema + class A2ARESTFastAPIApplication: """A FastAPI application implementing the A2A protocol server REST endpoints. @@ -176,6 +182,7 @@ def build( A configured FastAPI application instance. """ app = A2AFastAPI(**kwargs) + app.rpc_url = rpc_url @app.exception_handler(StarletteHTTPException) async def http_exception_handler( From c8d9e171fe8b82522d5afbc43513faa677588d24 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Tue, 17 Mar 2026 13:52:30 +0000 Subject: [PATCH 3/4] test --- tck/sut_agent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 7196b828b..9946ac475 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -203,7 +203,7 @@ def serve(task_store: TaskStore) -> None: agent_card=agent_card, http_handler=request_handler, ) - jsonrpc_server.add_routes_to_app(main_app, rpc_url=JSONRPC_URL) + json_rpc_app = jsonrpc_server.build(rpc_url=JSONRPC_URL) # REST rest_server = A2ARESTFastAPIApplication( @@ -212,6 +212,7 @@ def serve(task_store: TaskStore) -> None: ) rest_app = rest_server.build(rpc_url=REST_URL) main_app.mount('', rest_app) + main_app.mount('', json_rpc_app) config = uvicorn.Config( main_app, host='127.0.0.1', port=http_port, log_level='info' From eeafb8a3b2b5a3b71f68b790a829dcc6fea4cbb4 Mon Sep 17 00:00:00 2001 From: guglielmoc Date: Tue, 17 Mar 2026 14:55:19 +0000 Subject: [PATCH 4/4] tests --- tck/sut_agent.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 9946ac475..5af6309db 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -8,6 +8,7 @@ import grpc.aio import uvicorn +from fastapi import FastAPI from starlette.applications import Starlette import a2a.compat.v0_3.a2a_v0_3_pb2_grpc as a2a_v0_3_grpc @@ -19,6 +20,7 @@ from a2a.server.apps import ( A2ARESTFastAPIApplication, A2AStarletteApplication, + A2AFastAPIApplication, ) from a2a.server.events.event_queue import EventQueue from a2a.server.request_handlers.default_request_handler import ( @@ -196,10 +198,10 @@ def serve(task_store: TaskStore) -> None: task_store=task_store, ) - main_app = Starlette() + main_app = FastAPI() # JSONRPC - jsonrpc_server = A2AStarletteApplication( + jsonrpc_server = A2AFastAPIApplication( agent_card=agent_card, http_handler=request_handler, ) @@ -210,9 +212,10 @@ def serve(task_store: TaskStore) -> None: agent_card=agent_card, http_handler=request_handler, ) - rest_app = rest_server.build(rpc_url=REST_URL) - main_app.mount('', rest_app) - main_app.mount('', json_rpc_app) + rest_app = rest_server.build() + + main_app.mount(REST_URL,rest_app) + main_app.mount('',json_rpc_app) config = uvicorn.Config( main_app, host='127.0.0.1', port=http_port, log_level='info'