Skip to content

Commit 04a3626

Browse files
add fastapi jsonrpc app
1 parent 20f0826 commit 04a3626

File tree

4 files changed

+204
-113
lines changed

4 files changed

+204
-113
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""A2A JSON-RPC Applications."""
2+
3+
from .jsonrpc_app import JSONRPCApplication, CallContextBuilder
4+
from .starlette_app import A2AStarletteApplication
5+
from .fastapi_app import A2AFastAPIApplication
6+
7+
__all__ = [
8+
'JSONRPCApplication',
9+
'CallContextBuilder',
10+
'A2AStarletteApplication',
11+
'A2AFastAPIApplication',
12+
]
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import logging
2+
from typing import Any
3+
4+
from fastapi import FastAPI, Request
5+
6+
from a2a.server.apps.jsonrpc import JSONRPCApplication, CallContextBuilder
7+
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
8+
from a2a.types import AgentCard
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class A2AFastAPIApplication(JSONRPCApplication):
14+
"""A FastAPI application implementing the A2A protocol server endpoints.
15+
16+
Handles incoming JSON-RPC requests, routes them to the appropriate
17+
handler methods, and manages response generation including Server-Sent Events
18+
(SSE)."""
19+
20+
def __init__(self, agent_card: AgentCard, http_handler: RequestHandler):
21+
"""Initializes the A2A FastAPI application.
22+
23+
Args:
24+
agent_card: The AgentCard describing the agent's capabilities.
25+
http_handler: The handler instance responsible for processing A2A requests via http.
26+
"""
27+
super().__init__(agent_card, http_handler)
28+
29+
def build(
30+
self,
31+
agent_card_url: str = '/.well-known/agent.json',
32+
rpc_url: str = '/',
33+
**kwargs: Any,
34+
) -> FastAPI:
35+
"""Builds and returns the FastAPI application instance.
36+
37+
Args:
38+
agent_card_url: The URL for the agent card endpoint.
39+
rpc_url: The URL for the A2A JSON-RPC endpoint
40+
**kwargs: Additional keyword arguments to pass to the FastAPI constructor.
41+
42+
Returns:
43+
A configured FastAPI application instance.
44+
"""
45+
app = FastAPI(**kwargs)
46+
47+
@app.post(rpc_url)
48+
async def handle_a2a_request(request: Request):
49+
return await self._handle_requests(request)
50+
51+
@app.get(agent_card_url)
52+
async def get_agent_card(request: Request):
53+
return await self._handle_get_agent_card(request)
54+
55+
return app

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

Lines changed: 38 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,40 @@
33
import traceback
44
from abc import ABC, abstractmethod
55
from collections.abc import AsyncGenerator
6-
from typing import Any
6+
from typing import Any, Union
77

8+
from fastapi import FastAPI
89
from pydantic import ValidationError
910
from sse_starlette.sse import EventSourceResponse
1011
from starlette.applications import Starlette
1112
from starlette.requests import Request
1213
from starlette.responses import JSONResponse, Response
13-
from starlette.routing import Route
1414

1515
from a2a.server.context import ServerCallContext
1616
from a2a.server.request_handlers.jsonrpc_handler import JSONRPCHandler
1717
from a2a.server.request_handlers.request_handler import RequestHandler
18-
from a2a.types import (A2AError, A2ARequest, AgentCard, CancelTaskRequest,
19-
GetTaskPushNotificationConfigRequest, GetTaskRequest,
20-
InternalError, InvalidRequestError, JSONParseError,
21-
JSONRPCError, JSONRPCErrorResponse, JSONRPCResponse,
22-
SendMessageRequest, SendStreamingMessageRequest,
23-
SendStreamingMessageResponse,
24-
SetTaskPushNotificationConfigRequest,
25-
TaskResubscriptionRequest, UnsupportedOperationError)
18+
from a2a.server.context import ServerCallContext
19+
from a2a.types import (
20+
A2AError,
21+
A2ARequest,
22+
AgentCard,
23+
CancelTaskRequest,
24+
GetTaskPushNotificationConfigRequest,
25+
GetTaskRequest,
26+
InternalError,
27+
InvalidRequestError,
28+
JSONParseError,
29+
JSONRPCError,
30+
JSONRPCErrorResponse,
31+
JSONRPCResponse,
32+
SendMessageRequest,
33+
SendStreamingMessageRequest,
34+
SendStreamingMessageResponse,
35+
SetTaskPushNotificationConfigRequest,
36+
TaskResubscriptionRequest,
37+
UnsupportedOperationError,
38+
)
39+
2640
from a2a.utils.errors import MethodNotImplementedError
2741

2842
logger = logging.getLogger(__name__)
@@ -36,19 +50,19 @@ def build(self, request: Request) -> ServerCallContext:
3650
"""Builds a ServerCallContext from a Starlette Request."""
3751

3852

39-
class A2AStarletteApplication:
40-
"""A Starlette application implementing the A2A protocol server endpoints.
53+
class JSONRPCApplication(ABC):
54+
"""Base class for A2A applications.
4155
42-
Handles incoming JSON-RPC requests, routes them to the appropriate
43-
handler methods, and manages response generation including Server-Sent Events
44-
(SSE).
56+
Args:
57+
agent_card: The AgentCard describing the agent's capabilities.
58+
http_handler: The handler instance responsible for processing A2A
59+
requests via http.
4560
"""
4661

4762
def __init__(
4863
self,
4964
agent_card: AgentCard,
5065
http_handler: RequestHandler,
51-
extended_agent_card: AgentCard | None = None,
5266
context_builder: CallContextBuilder | None = None,
5367
):
5468
"""Initializes the A2AStarletteApplication.
@@ -57,24 +71,14 @@ def __init__(
5771
agent_card: The AgentCard describing the agent's capabilities.
5872
http_handler: The handler instance responsible for processing A2A
5973
requests via http.
60-
extended_agent_card: An optional, distinct AgentCard to be served
61-
at the authenticated extended card endpoint.
6274
context_builder: The CallContextBuilder used to construct the
6375
ServerCallContext passed to the http_handler. If None, no
6476
ServerCallContext is passed.
6577
"""
6678
self.agent_card = agent_card
67-
self.extended_agent_card = extended_agent_card
6879
self.handler = JSONRPCHandler(
6980
agent_card=agent_card, request_handler=http_handler
7081
)
71-
if (
72-
self.agent_card.supportsAuthenticatedExtendedCard
73-
and self.extended_agent_card is None
74-
):
75-
logger.error(
76-
'AgentCard.supportsAuthenticatedExtendedCard is True, but no extended_agent_card was provided. The /agent/authenticatedExtendedCard endpoint will return 404.'
77-
)
7882
self._context_builder = context_builder
7983

8084
def _generate_error_response(
@@ -317,104 +321,25 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
317321
Returns:
318322
A JSONResponse containing the agent card data.
319323
"""
320-
# The public agent card is a direct serialization of the agent_card
321-
# provided at initialization.
322324
return JSONResponse(
323325
self.agent_card.model_dump(mode='json', exclude_none=True)
324326
)
325327

326-
async def _handle_get_authenticated_extended_agent_card(
327-
self, request: Request
328-
) -> JSONResponse:
329-
"""Handles GET requests for the authenticated extended agent card."""
330-
if not self.agent_card.supportsAuthenticatedExtendedCard:
331-
return JSONResponse(
332-
{'error': 'Extended agent card not supported or not enabled.'},
333-
status_code=404,
334-
)
335-
336-
# If an explicit extended_agent_card is provided, serve that.
337-
if self.extended_agent_card:
338-
return JSONResponse(
339-
self.extended_agent_card.model_dump(
340-
mode='json', exclude_none=True
341-
)
342-
)
343-
# If supportsAuthenticatedExtendedCard is true, but no specific
344-
# extended_agent_card was provided during server initialization,
345-
# return a 404
346-
return JSONResponse(
347-
{'error': 'Authenticated extended agent card is supported but not configured on the server.'},
348-
status_code=404,
349-
)
350-
351-
def routes(
352-
self,
353-
agent_card_url: str = '/.well-known/agent.json',
354-
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
355-
rpc_url: str = '/',
356-
) -> list[Route]:
357-
"""Returns the Starlette Routes for handling A2A requests.
358-
359-
Args:
360-
agent_card_url: The URL path for the agent card endpoint.
361-
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
362-
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
363-
364-
Returns:
365-
A list of Starlette Route objects.
366-
"""
367-
app_routes = [
368-
Route(
369-
rpc_url,
370-
self._handle_requests,
371-
methods=['POST'],
372-
name='a2a_handler',
373-
),
374-
Route(
375-
agent_card_url,
376-
self._handle_get_agent_card,
377-
methods=['GET'],
378-
name='agent_card',
379-
),
380-
]
381-
382-
if self.agent_card.supportsAuthenticatedExtendedCard:
383-
app_routes.append(
384-
Route(
385-
extended_agent_card_url,
386-
self._handle_get_authenticated_extended_agent_card,
387-
methods=['GET'],
388-
name='authenticated_extended_agent_card',
389-
)
390-
)
391-
return app_routes
392-
328+
@abstractmethod
393329
def build(
394330
self,
395331
agent_card_url: str = '/.well-known/agent.json',
396-
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
397332
rpc_url: str = '/',
398333
**kwargs: Any,
399-
) -> Starlette:
400-
"""Builds and returns the Starlette application instance.
334+
) -> Union[Starlette, FastAPI]:
335+
"""Builds and returns the FastAPI application instance.
401336
402337
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-
**kwargs: Additional keyword arguments to pass to the Starlette
407-
constructor.
338+
agent_card_url: The URL for the agent card endpoint.
339+
rpc_url: The URL for the A2A JSON-RPC endpoint
340+
**kwargs: Additional keyword arguments to pass to the FastAPI constructor.
408341
409342
Returns:
410-
A configured Starlette application instance.
343+
A configured FastAPI application instance.
411344
"""
412-
app_routes = self.routes(
413-
agent_card_url, extended_agent_card_url, rpc_url
414-
)
415-
if 'routes' in kwargs:
416-
kwargs['routes'].extend(app_routes)
417-
else:
418-
kwargs['routes'] = app_routes
419-
420-
return Starlette(**kwargs)
345+
pass
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import logging
2+
3+
from typing import Any
4+
from abc import ABC, abstractmethod
5+
6+
from starlette.applications import Starlette
7+
from starlette.routing import Route
8+
9+
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
10+
from a2a.server.apps.jsonrpc import JSONRPCApplication, CallContextBuilder
11+
from a2a.types import AgentCard
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+
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+
context_builder: The CallContextBuilder used to construct the
38+
ServerCallContext passed to the http_handler. If None, no
39+
ServerCallContext is passed.
40+
"""
41+
super().__init__(
42+
agent_card=agent_card,
43+
http_handler=http_handler,
44+
context_builder=context_builder
45+
)
46+
47+
def routes(
48+
self,
49+
agent_card_url: str = '/.well-known/agent.json',
50+
rpc_url: str = '/',
51+
) -> list[Route]:
52+
"""Returns the Starlette Routes for handling A2A requests.
53+
54+
Args:
55+
agent_card_url: The URL path for the agent card endpoint.
56+
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
57+
58+
Returns:
59+
A list of Starlette Route objects.
60+
"""
61+
return [
62+
Route(
63+
rpc_url,
64+
self._handle_requests,
65+
methods=['POST'],
66+
name='a2a_handler',
67+
),
68+
Route(
69+
agent_card_url,
70+
self._handle_get_agent_card,
71+
methods=['GET'],
72+
name='agent_card',
73+
),
74+
]
75+
76+
def build(
77+
self,
78+
agent_card_url: str = '/.well-known/agent.json',
79+
rpc_url: str = '/',
80+
**kwargs: Any,
81+
) -> Starlette:
82+
"""Builds and returns the Starlette application instance.
83+
84+
Args:
85+
agent_card_url: The URL path for the agent card endpoint.
86+
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
87+
**kwargs: Additional keyword arguments to pass to the Starlette
88+
constructor.
89+
90+
Returns:
91+
A configured Starlette application instance.
92+
"""
93+
routes = self.routes(agent_card_url, rpc_url)
94+
if 'routes' in kwargs:
95+
kwargs['routes'] += routes
96+
else:
97+
kwargs['routes'] = routes
98+
99+
return Starlette(**kwargs)

0 commit comments

Comments
 (0)