From 27acba1e0b48aa79d7356e6d359a370d0772500b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ra=C4=8Dinsk=C3=BD?= Date: Fri, 18 Jul 2025 14:18:27 +0200 Subject: [PATCH 1/6] fix: openapi working in sub-app --- src/a2a/server/apps/jsonrpc/fastapi_app.py | 49 +++++++++++++--------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py index 74060688..212810b7 100644 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ b/src/a2a/server/apps/jsonrpc/fastapi_app.py @@ -1,11 +1,16 @@ import logging +import sys -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager from typing import Any from fastapi import FastAPI + +if sys.version_info < (3, 12): # pragma: no cover + from typing_extensions import override +else: # pragma: no cover + from typing import override + from a2a.server.apps.jsonrpc.jsonrpc_app import ( CallContextBuilder, JSONRPCApplication, @@ -22,6 +27,28 @@ logger = logging.getLogger(__name__) +class A2AFastAPI(FastAPI): + """A FastAPI application that adds A2A-specific OpenAPI components.""" + + a2a_components_added: bool = False + + @override + def openapi(self) -> dict[str, Any]: + openapi_schema = super().openapi() + if not self.a2a_components_added: + a2a_request_schema = A2ARequest.model_json_schema( + ref_template='#/components/schemas/{model}' + ) + defs = a2a_request_schema.pop('$defs', {}) + component_schemas = openapi_schema.setdefault( + 'components', {} + ).setdefault('schemas', {}) + component_schemas.update(defs) + component_schemas['A2ARequest'] = a2a_request_schema + self.a2a_components_added = True + return openapi_schema + + class A2AFastAPIApplication(JSONRPCApplication): """A FastAPI application implementing the A2A protocol server endpoints. @@ -112,23 +139,7 @@ def build( Returns: A configured FastAPI application instance. """ - - @asynccontextmanager - async def lifespan(app: FastAPI) -> AsyncIterator[None]: - a2a_request_schema = A2ARequest.model_json_schema( - ref_template='#/components/schemas/{model}' - ) - defs = a2a_request_schema.pop('$defs', {}) - openapi_schema = app.openapi() - component_schemas = openapi_schema.setdefault( - 'components', {} - ).setdefault('schemas', {}) - component_schemas.update(defs) - component_schemas['A2ARequest'] = a2a_request_schema - - yield - - app = FastAPI(lifespan=lifespan, **kwargs) + app = A2AFastAPI(**kwargs) self.add_routes_to_app( app, agent_card_url, rpc_url, extended_agent_card_url From 38fe9f676f020c2208f6281c5c59f15ed599c920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ra=C4=8Dinsk=C3=BD?= Date: Fri, 18 Jul 2025 14:22:34 +0200 Subject: [PATCH 2/6] Update src/a2a/server/apps/jsonrpc/fastapi_app.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/a2a/server/apps/jsonrpc/fastapi_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py index 212810b7..e0c6a02b 100644 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ b/src/a2a/server/apps/jsonrpc/fastapi_app.py @@ -30,7 +30,7 @@ class A2AFastAPI(FastAPI): """A FastAPI application that adds A2A-specific OpenAPI components.""" - a2a_components_added: bool = False + _a2a_components_added: bool = False @override def openapi(self) -> dict[str, Any]: From c89c4cdb9b6644479a8d9cc97f8347aff275585e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ra=C4=8Dinsk=C3=BD?= Date: Fri, 18 Jul 2025 14:22:56 +0200 Subject: [PATCH 3/6] fix: openapi working in sub-app --- src/a2a/server/apps/jsonrpc/fastapi_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py index e0c6a02b..168d1ff8 100644 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ b/src/a2a/server/apps/jsonrpc/fastapi_app.py @@ -35,7 +35,7 @@ class A2AFastAPI(FastAPI): @override def openapi(self) -> dict[str, Any]: openapi_schema = super().openapi() - if not self.a2a_components_added: + if not self._a2a_components_added: a2a_request_schema = A2ARequest.model_json_schema( ref_template='#/components/schemas/{model}' ) @@ -45,7 +45,7 @@ def openapi(self) -> dict[str, Any]: ).setdefault('schemas', {}) component_schemas.update(defs) component_schemas['A2ARequest'] = a2a_request_schema - self.a2a_components_added = True + self._a2a_components_added = True return openapi_schema From 6a5482fa553e215d8a70fc6625986cb0788957cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ra=C4=8Dinsk=C3=BD?= Date: Fri, 18 Jul 2025 14:30:56 +0200 Subject: [PATCH 4/6] fix: openapi working in sub-app --- src/a2a/server/apps/jsonrpc/fastapi_app.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py index 168d1ff8..3c700944 100644 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ b/src/a2a/server/apps/jsonrpc/fastapi_app.py @@ -1,16 +1,9 @@ import logging -import sys from typing import Any from fastapi import FastAPI - -if sys.version_info < (3, 12): # pragma: no cover - from typing_extensions import override -else: # pragma: no cover - from typing import override - from a2a.server.apps.jsonrpc.jsonrpc_app import ( CallContextBuilder, JSONRPCApplication, @@ -32,8 +25,8 @@ class A2AFastAPI(FastAPI): _a2a_components_added: bool = False - @override def openapi(self) -> dict[str, Any]: + """Generates the OpenAPI schema for the application.""" openapi_schema = super().openapi() if not self._a2a_components_added: a2a_request_schema = A2ARequest.model_json_schema( From ede4bf460fcaee9bf19b8eace1100d5770e5bb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ra=C4=8Dinsk=C3=BD?= Date: Mon, 21 Jul 2025 15:04:31 +0200 Subject: [PATCH 5/6] test: openapi working in sub-app --- .../server/apps/jsonrpc/test_serialization.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/server/apps/jsonrpc/test_serialization.py b/tests/server/apps/jsonrpc/test_serialization.py index 3091c0cd..435fbfde 100644 --- a/tests/server/apps/jsonrpc/test_serialization.py +++ b/tests/server/apps/jsonrpc/test_serialization.py @@ -1,6 +1,7 @@ from unittest import mock import pytest +from fastapi import FastAPI from pydantic import ValidationError from starlette.testclient import TestClient @@ -184,3 +185,23 @@ def test_handle_unicode_characters(agent_card_with_api_key: AgentCard): data = response.json() assert 'error' not in data or data['error'] is None assert data['result']['parts'][0]['text'] == f'Received: {unicode_text}' + + +def test_fastapi_sub_application( + agent_card_with_api_key: AgentCard, +): + """ + Tests that the A2AFastAPIApplication endpoint correctly passes the url in sub-application. + """ + handler = mock.AsyncMock() + sub_app_instance = A2AFastAPIApplication(agent_card_with_api_key, 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() + + assert 'servers' in response_data + assert response_data['servers'] == [{'url': '/a2a'}] From 3f7267b93808dc1257d58c24f2fb9a18781e73e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Ra=C4=8Dinsk=C3=BD?= Date: Mon, 21 Jul 2025 15:05:07 +0200 Subject: [PATCH 6/6] test: openapi working in sub-app --- tests/server/apps/jsonrpc/test_serialization.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/server/apps/jsonrpc/test_serialization.py b/tests/server/apps/jsonrpc/test_serialization.py index 435fbfde..eb78d6b4 100644 --- a/tests/server/apps/jsonrpc/test_serialization.py +++ b/tests/server/apps/jsonrpc/test_serialization.py @@ -187,9 +187,7 @@ def test_handle_unicode_characters(agent_card_with_api_key: AgentCard): assert data['result']['parts'][0]['text'] == f'Received: {unicode_text}' -def test_fastapi_sub_application( - agent_card_with_api_key: AgentCard, -): +def test_fastapi_sub_application(agent_card_with_api_key: AgentCard): """ Tests that the A2AFastAPIApplication endpoint correctly passes the url in sub-application. """