Skip to content

Commit 0e66e1f

Browse files
feat!: Add FastAPI JSONRPC Application (#104)
# Description ## Summary This PR introduces a `A2AFastAPIApplication` class that enables serving A2A endpoints using a FastAPI application, while preserving compatibility with the existing JSONRPC Starlette-based architecture. ### Motivation While the SDK currently provides a Starlette-based server app for A2A agent communication using JSONRPC, many production Python APIs are built with FastAPI due to its support for performance, extensibility, automatic OpenAPI schema generation, and strong async capabilities. Providing native FastAPI support enables seamless integration into such environments without requiring users to manually wrap the existing Starlette app. This addition does **not** introduce any breaking changes. FastAPI is treated as an optional integration, and the core A2A logic continues to rely on shared base classes and request handlers. ### Implementation Details * Introduced `JSONRPCApplication`, an abstract base class that encapsulates the shared request-handling logic for both Starlette and FastAPI JSONRPC applications. This isolates the common behavior, with the only required customization being the `build(...)` method used to register routes and return the appropriate application instance. * Added `A2AFastAPIApplication`, a concrete implementation of `JSONRPCApplication` that constructs a FastAPI app with the appropriate routes: * `POST /` (or custom RPC endpoint) for handling A2A JSON-RPC messages. * `GET /.well-known/agent.json` (or custom) for serving the agent card. * All request processing is shared via `_handle_requests` and `_handle_get_agent_card`. * SSE streaming support is preserved for `SendStreamingMessageRequest` and `TaskResubscriptionRequest`. * Added `fastapi` to the dependencies in `pyproject.toml`. ### Usage Example ```python from a2a.server.apps.jsonrpc import A2AFastAPIApplication app = A2AFastAPIApplication(agent_card=my_agent_card, http_handler=my_handler).build() ``` ### Hello World Example (`examples/helloword/`) 1. Change `A2AStarletteApplication` to `A2AFastAPIApplication` 2. Start the server ```bash uv run . ``` ![fastapi_app_running](https://github.com/user-attachments/assets/cb31d08e-5aba-4e08-ad92-ef1c8e0d3c69) ![fastapi_app_docs](https://github.com/user-attachments/assets/a0aa2dbe-abea-4b8f-9c5c-448569f44bc5) 3. Run the test client ```bash uv run test_client.py ``` ![fastapi_results_server](https://github.com/user-attachments/assets/372566ca-9b8c-4cb8-b9d1-7331bdcb05c2) ![fastapi_results_client](https://github.com/user-attachments/assets/cfbe944c-92b3-4abc-9fe7-4afca8b2bd4e) Continues #26 Fixes #21 🦕
1 parent 1f0a769 commit 0e66e1f

File tree

9 files changed

+835
-541
lines changed

9 files changed

+835
-541
lines changed

.github/actions/spelling/allow.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
ACard
22
AClient
33
AError
4+
AFast
45
ARequest
56
ARun
67
AServer

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ authors = [{ name = "Google LLC", email = "googleapis-packages@google.com" }]
88
requires-python = ">=3.10"
99
keywords = ["A2A", "A2A SDK", "A2A Protocol", "Agent2Agent"]
1010
dependencies = [
11+
"fastapi>=0.115.12",
1112
"httpx>=0.28.1",
1213
"httpx-sse>=0.4.0",
1314
"opentelemetry-api>=1.33.0",

src/a2a/server/apps/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
"""HTTP application components for the A2A server."""
22

3-
from a2a.server.apps.starlette_app import A2AStarletteApplication
3+
from .jsonrpc import (
4+
A2AFastAPIApplication,
5+
A2AStarletteApplication,
6+
CallContextBuilder,
7+
JSONRPCApplication,
8+
)
49

510

6-
__all__ = ['A2AStarletteApplication']
11+
__all__ = [
12+
'A2AFastAPIApplication',
13+
'A2AStarletteApplication',
14+
'CallContextBuilder',
15+
'JSONRPCApplication',
16+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""A2A JSON-RPC Applications."""
2+
3+
from .fastapi_app import A2AFastAPIApplication
4+
from .jsonrpc_app import CallContextBuilder, JSONRPCApplication
5+
from .starlette_app import A2AStarletteApplication
6+
7+
8+
__all__ = [
9+
'A2AFastAPIApplication',
10+
'A2AStarletteApplication',
11+
'CallContextBuilder',
12+
'JSONRPCApplication',
13+
]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import logging
2+
3+
from typing import Any
4+
5+
from fastapi import FastAPI, Request
6+
7+
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
8+
from a2a.types import AgentCard
9+
10+
from .jsonrpc_app import CallContextBuilder, JSONRPCApplication
11+
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class A2AFastAPIApplication(JSONRPCApplication):
17+
"""A FastAPI application implementing the A2A protocol server endpoints.
18+
19+
Handles incoming JSON-RPC requests, routes them to the appropriate
20+
handler methods, and manages response generation including Server-Sent Events
21+
(SSE).
22+
"""
23+
24+
def __init__(
25+
self,
26+
agent_card: AgentCard,
27+
http_handler: RequestHandler,
28+
extended_agent_card: AgentCard | None = None,
29+
context_builder: CallContextBuilder | None = None,
30+
):
31+
"""Initializes the A2AStarletteApplication.
32+
33+
Args:
34+
agent_card: The AgentCard describing the agent's capabilities.
35+
http_handler: The handler instance responsible for processing A2A
36+
requests via http.
37+
extended_agent_card: An optional, distinct AgentCard to be served
38+
at the authenticated extended card endpoint.
39+
context_builder: The CallContextBuilder used to construct the
40+
ServerCallContext passed to the http_handler. If None, no
41+
ServerCallContext is passed.
42+
"""
43+
super().__init__(
44+
agent_card=agent_card,
45+
http_handler=http_handler,
46+
extended_agent_card=extended_agent_card,
47+
context_builder=context_builder,
48+
)
49+
50+
def build(
51+
self,
52+
agent_card_url: str = '/.well-known/agent.json',
53+
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
54+
rpc_url: str = '/',
55+
**kwargs: Any,
56+
) -> FastAPI:
57+
"""Builds and returns the FastAPI application instance.
58+
59+
Args:
60+
agent_card_url: The URL for the agent card endpoint.
61+
rpc_url: The URL for the A2A JSON-RPC endpoint.
62+
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
63+
**kwargs: Additional keyword arguments to pass to the FastAPI constructor.
64+
65+
Returns:
66+
A configured FastAPI application instance.
67+
"""
68+
app = FastAPI(**kwargs)
69+
70+
@app.post(rpc_url)
71+
async def handle_a2a_request(request: Request):
72+
return await self._handle_requests(request)
73+
74+
@app.get(agent_card_url)
75+
async def get_agent_card(request: Request):
76+
return await self._handle_get_agent_card(request)
77+
78+
if self.agent_card.supportsAuthenticatedExtendedCard:
79+
80+
@app.get(extended_agent_card_url)
81+
async def get_extended_agent_card(request: Request):
82+
return await self._handle_get_authenticated_extended_agent_card(
83+
request
84+
)
85+
86+
return app

src/a2a/server/apps/starlette_app.py renamed to src/a2a/server/apps/jsonrpc/jsonrpc_app.py

Lines changed: 12 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from collections.abc import AsyncGenerator
88
from typing import Any
99

10+
from fastapi import FastAPI
1011
from pydantic import ValidationError
1112
from sse_starlette.sse import EventSourceResponse
1213
from starlette.applications import Starlette
1314
from starlette.authentication import BaseUser
1415
from starlette.requests import Request
1516
from starlette.responses import JSONResponse, Response
16-
from starlette.routing import Route
1717

1818
from a2a.auth.user import UnauthenticatedUser
1919
from a2a.auth.user import User as A2AUser
@@ -81,8 +81,8 @@ def build(self, request: Request) -> ServerCallContext:
8181
return ServerCallContext(user=user, state=state)
8282

8383

84-
class A2AStarletteApplication:
85-
"""A Starlette application implementing the A2A protocol server endpoints.
84+
class JSONRPCApplication(ABC):
85+
"""Base class for A2A JSONRPC applications.
8686
8787
Handles incoming JSON-RPC requests, routes them to the appropriate
8888
handler methods, and manages response generation including Server-Sent Events
@@ -391,73 +391,23 @@ async def _handle_get_authenticated_extended_agent_card(
391391
status_code=404,
392392
)
393393

394-
def routes(
395-
self,
396-
agent_card_url: str = '/.well-known/agent.json',
397-
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
398-
rpc_url: str = '/',
399-
) -> list[Route]:
400-
"""Returns the Starlette Routes for handling A2A requests.
401-
402-
Args:
403-
agent_card_url: The URL path for the agent card endpoint.
404-
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
405-
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
406-
407-
Returns:
408-
A list of Starlette Route objects.
409-
"""
410-
app_routes = [
411-
Route(
412-
rpc_url,
413-
self._handle_requests,
414-
methods=['POST'],
415-
name='a2a_handler',
416-
),
417-
Route(
418-
agent_card_url,
419-
self._handle_get_agent_card,
420-
methods=['GET'],
421-
name='agent_card',
422-
),
423-
]
424-
425-
if self.agent_card.supportsAuthenticatedExtendedCard:
426-
app_routes.append(
427-
Route(
428-
extended_agent_card_url,
429-
self._handle_get_authenticated_extended_agent_card,
430-
methods=['GET'],
431-
name='authenticated_extended_agent_card',
432-
)
433-
)
434-
return app_routes
435-
394+
@abstractmethod
436395
def build(
437396
self,
438397
agent_card_url: str = '/.well-known/agent.json',
439-
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
440398
rpc_url: str = '/',
441399
**kwargs: Any,
442-
) -> Starlette:
443-
"""Builds and returns the Starlette application instance.
400+
) -> FastAPI | Starlette:
401+
"""Builds and returns the JSONRPC application instance.
444402
445403
Args:
446-
agent_card_url: The URL path for the agent card endpoint.
447-
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
448-
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
449-
**kwargs: Additional keyword arguments to pass to the Starlette
450-
constructor.
404+
agent_card_url: The URL for the agent card endpoint.
405+
rpc_url: The URL for the A2A JSON-RPC endpoint
406+
**kwargs: Additional keyword arguments to pass to the FastAPI constructor.
451407
452408
Returns:
453-
A configured Starlette application instance.
409+
A configured JSONRPC application instance.
454410
"""
455-
app_routes = self.routes(
456-
agent_card_url, extended_agent_card_url, rpc_url
411+
raise NotImplementedError(
412+
'Subclasses must implement the build method to create the application instance.'
457413
)
458-
if 'routes' in kwargs:
459-
kwargs['routes'].extend(app_routes)
460-
else:
461-
kwargs['routes'] = app_routes
462-
463-
return Starlette(**kwargs)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import logging
2+
3+
from typing import Any
4+
5+
from starlette.applications import Starlette
6+
from starlette.routing import Route
7+
8+
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
9+
from a2a.types import AgentCard
10+
11+
from .jsonrpc_app import CallContextBuilder, JSONRPCApplication
12+
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class A2AStarletteApplication(JSONRPCApplication):
18+
"""A Starlette application implementing the A2A protocol server endpoints.
19+
20+
Handles incoming JSON-RPC requests, routes them to the appropriate
21+
handler methods, and manages response generation including Server-Sent Events
22+
(SSE).
23+
"""
24+
25+
def __init__(
26+
self,
27+
agent_card: AgentCard,
28+
http_handler: RequestHandler,
29+
extended_agent_card: AgentCard | None = None,
30+
context_builder: CallContextBuilder | None = None,
31+
):
32+
"""Initializes the A2AStarletteApplication.
33+
34+
Args:
35+
agent_card: The AgentCard describing the agent's capabilities.
36+
http_handler: The handler instance responsible for processing A2A
37+
requests via http.
38+
extended_agent_card: An optional, distinct AgentCard to be served
39+
at the authenticated extended card endpoint.
40+
context_builder: The CallContextBuilder used to construct the
41+
ServerCallContext passed to the http_handler. If None, no
42+
ServerCallContext is passed.
43+
"""
44+
super().__init__(
45+
agent_card=agent_card,
46+
http_handler=http_handler,
47+
extended_agent_card=extended_agent_card,
48+
context_builder=context_builder,
49+
)
50+
51+
def routes(
52+
self,
53+
agent_card_url: str = '/.well-known/agent.json',
54+
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
55+
rpc_url: str = '/',
56+
) -> list[Route]:
57+
"""Returns the Starlette Routes for handling A2A requests.
58+
59+
Args:
60+
agent_card_url: The URL path for the agent card endpoint.
61+
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
62+
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
63+
64+
Returns:
65+
A list of Starlette Route objects.
66+
"""
67+
app_routes = [
68+
Route(
69+
rpc_url,
70+
self._handle_requests,
71+
methods=['POST'],
72+
name='a2a_handler',
73+
),
74+
Route(
75+
agent_card_url,
76+
self._handle_get_agent_card,
77+
methods=['GET'],
78+
name='agent_card',
79+
),
80+
]
81+
82+
if self.agent_card.supportsAuthenticatedExtendedCard:
83+
app_routes.append(
84+
Route(
85+
extended_agent_card_url,
86+
self._handle_get_authenticated_extended_agent_card,
87+
methods=['GET'],
88+
name='authenticated_extended_agent_card',
89+
)
90+
)
91+
return app_routes
92+
93+
def build(
94+
self,
95+
agent_card_url: str = '/.well-known/agent.json',
96+
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
97+
rpc_url: str = '/',
98+
**kwargs: Any,
99+
) -> Starlette:
100+
"""Builds and returns the Starlette application instance.
101+
102+
Args:
103+
agent_card_url: The URL path for the agent card endpoint.
104+
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
105+
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
106+
**kwargs: Additional keyword arguments to pass to the Starlette
107+
constructor.
108+
109+
Returns:
110+
A configured Starlette application instance.
111+
"""
112+
app_routes = self.routes(
113+
agent_card_url, extended_agent_card_url, rpc_url
114+
)
115+
if 'routes' in kwargs:
116+
kwargs['routes'].extend(app_routes)
117+
else:
118+
kwargs['routes'] = app_routes
119+
120+
return Starlette(**kwargs)

0 commit comments

Comments
 (0)