Skip to content

Commit 23bbe29

Browse files
committed
Merge remote-tracking branch 'upstream/1.0-dev' into guglielmoc/refactor_rest_server
2 parents 8c230f2 + 734d062 commit 23bbe29

3 files changed

Lines changed: 495 additions & 0 deletions

File tree

src/a2a/server/apps/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""HTTP application components for the A2A server."""
2+
3+
from a2a.server.apps.rest import A2ARESTFastAPIApplication
4+
5+
6+
__all__ = [
7+
'A2ARESTFastAPIApplication',
8+
]
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import logging
2+
3+
from collections.abc import Awaitable, Callable
4+
from typing import TYPE_CHECKING, Any
5+
6+
7+
if TYPE_CHECKING:
8+
from fastapi import APIRouter, FastAPI, Request, Response
9+
from fastapi.responses import JSONResponse
10+
from starlette.exceptions import HTTPException as StarletteHTTPException
11+
12+
_package_fastapi_installed = True
13+
else:
14+
try:
15+
from fastapi import APIRouter, FastAPI, Request, Response
16+
from fastapi.responses import JSONResponse
17+
from starlette.exceptions import HTTPException as StarletteHTTPException
18+
19+
_package_fastapi_installed = True
20+
except ImportError:
21+
APIRouter = Any
22+
FastAPI = Any
23+
Request = Any
24+
Response = Any
25+
StarletteHTTPException = Any
26+
27+
_package_fastapi_installed = False
28+
29+
30+
from a2a.compat.v0_3.rest_adapter import REST03Adapter
31+
from a2a.server.apps.rest.rest_adapter import RESTAdapter
32+
from a2a.server.context import ServerCallContext
33+
from a2a.server.request_handlers.request_handler import RequestHandler
34+
from a2a.server.routes import CallContextBuilder
35+
from a2a.types.a2a_pb2 import AgentCard
36+
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
37+
38+
39+
logger = logging.getLogger(__name__)
40+
41+
42+
_HTTP_TO_GRPC_STATUS_MAP = {
43+
400: 'INVALID_ARGUMENT',
44+
401: 'UNAUTHENTICATED',
45+
403: 'PERMISSION_DENIED',
46+
404: 'NOT_FOUND',
47+
405: 'UNIMPLEMENTED',
48+
409: 'ALREADY_EXISTS',
49+
415: 'INVALID_ARGUMENT',
50+
422: 'INVALID_ARGUMENT',
51+
500: 'INTERNAL',
52+
501: 'UNIMPLEMENTED',
53+
502: 'INTERNAL',
54+
503: 'UNAVAILABLE',
55+
504: 'DEADLINE_EXCEEDED',
56+
}
57+
58+
59+
class A2ARESTFastAPIApplication:
60+
"""A FastAPI application implementing the A2A protocol server REST endpoints.
61+
62+
Handles incoming REST requests, routes them to the appropriate
63+
handler methods, and manages response generation including Server-Sent Events
64+
(SSE).
65+
"""
66+
67+
def __init__( # noqa: PLR0913
68+
self,
69+
agent_card: AgentCard,
70+
http_handler: RequestHandler,
71+
extended_agent_card: AgentCard | None = None,
72+
context_builder: CallContextBuilder | None = None,
73+
card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard]
74+
| None = None,
75+
extended_card_modifier: Callable[
76+
[AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard
77+
]
78+
| None = None,
79+
enable_v0_3_compat: bool = False,
80+
):
81+
"""Initializes the A2ARESTFastAPIApplication.
82+
83+
Args:
84+
agent_card: The AgentCard describing the agent's capabilities.
85+
http_handler: The handler instance responsible for processing A2A
86+
requests via http.
87+
extended_agent_card: An optional, distinct AgentCard to be served
88+
at the authenticated extended card endpoint.
89+
context_builder: The CallContextBuilder used to construct the
90+
ServerCallContext passed to the http_handler. If None, no
91+
ServerCallContext is passed.
92+
card_modifier: An optional callback to dynamically modify the public
93+
agent card before it is served.
94+
extended_card_modifier: An optional callback to dynamically modify
95+
the extended agent card before it is served. It receives the
96+
call context.
97+
enable_v0_3_compat: If True, mounts backward-compatible v0.3 protocol
98+
endpoints under the '/v0.3' path prefix using REST03Adapter.
99+
"""
100+
if not _package_fastapi_installed:
101+
raise ImportError(
102+
'The `fastapi` package is required to use the'
103+
' `A2ARESTFastAPIApplication`. It can be added as a part of'
104+
' `a2a-sdk` optional dependencies, `a2a-sdk[http-server]`.'
105+
)
106+
self._adapter = RESTAdapter(
107+
agent_card=agent_card,
108+
http_handler=http_handler,
109+
extended_agent_card=extended_agent_card,
110+
context_builder=context_builder,
111+
card_modifier=card_modifier,
112+
extended_card_modifier=extended_card_modifier,
113+
)
114+
self.enable_v0_3_compat = enable_v0_3_compat
115+
self._v03_adapter = None
116+
117+
if self.enable_v0_3_compat:
118+
self._v03_adapter = REST03Adapter(
119+
agent_card=agent_card,
120+
http_handler=http_handler,
121+
extended_agent_card=extended_agent_card,
122+
context_builder=context_builder,
123+
card_modifier=card_modifier,
124+
extended_card_modifier=extended_card_modifier,
125+
)
126+
127+
def build(
128+
self,
129+
agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH,
130+
rpc_url: str = '',
131+
**kwargs: Any,
132+
) -> FastAPI:
133+
"""Builds and returns the FastAPI application instance.
134+
135+
Args:
136+
agent_card_url: The URL for the agent card endpoint.
137+
rpc_url: The URL for the A2A REST endpoint base path.
138+
**kwargs: Additional keyword arguments to pass to the FastAPI constructor.
139+
140+
Returns:
141+
A configured FastAPI application instance.
142+
"""
143+
app = FastAPI(**kwargs)
144+
145+
@app.exception_handler(StarletteHTTPException)
146+
async def http_exception_handler(
147+
request: Request, exc: StarletteHTTPException
148+
) -> Response:
149+
"""Catches framework-level HTTP exceptions.
150+
151+
For example, 404 Not Found for bad routes, 422 Unprocessable Entity
152+
for schema validation, and formats them into the A2A standard
153+
google.rpc.Status JSON format (AIP-193).
154+
"""
155+
grpc_status = _HTTP_TO_GRPC_STATUS_MAP.get(
156+
exc.status_code, 'UNKNOWN'
157+
)
158+
return JSONResponse(
159+
status_code=exc.status_code,
160+
content={
161+
'error': {
162+
'code': exc.status_code,
163+
'status': grpc_status,
164+
'message': str(exc.detail)
165+
if hasattr(exc, 'detail')
166+
else 'HTTP Exception',
167+
}
168+
},
169+
media_type='application/json',
170+
)
171+
172+
if self.enable_v0_3_compat and self._v03_adapter:
173+
v03_adapter = self._v03_adapter
174+
v03_router = APIRouter()
175+
for route, callback in v03_adapter.routes().items():
176+
v03_router.add_api_route(
177+
f'{rpc_url}{route[0]}', callback, methods=[route[1]]
178+
)
179+
app.include_router(v03_router)
180+
181+
router = APIRouter()
182+
for route, callback in self._adapter.routes().items():
183+
router.add_api_route(
184+
f'{rpc_url}{route[0]}', callback, methods=[route[1]]
185+
)
186+
187+
@router.get(f'{rpc_url}{agent_card_url}')
188+
async def get_agent_card(request: Request) -> Response:
189+
card = await self._adapter.handle_get_agent_card(request)
190+
return JSONResponse(card)
191+
192+
app.include_router(router)
193+
194+
return app

0 commit comments

Comments
 (0)