Skip to content

Commit 2c57faf

Browse files
Updated agent card engpoints
1 parent 7530164 commit 2c57faf

File tree

2 files changed

+85
-12
lines changed

2 files changed

+85
-12
lines changed

src/a2a/server/apps/rest/fastapi_app.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import logging
22

3+
from collections.abc import Callable
34
from typing import Any
45

56
from fastapi import APIRouter, FastAPI, Request, Response
67

78
from a2a.server.apps.jsonrpc.jsonrpc_app import CallContextBuilder
89
from a2a.server.apps.rest.rest_adapter import RESTAdapter
10+
from a2a.server.context import ServerCallContext
911
from a2a.server.request_handlers.request_handler import RequestHandler
1012
from a2a.types import AgentCard
1113
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
@@ -22,11 +24,17 @@ class A2ARESTFastAPIApplication:
2224
(SSE).
2325
"""
2426

25-
def __init__(
27+
def __init__( # noqa: PLR0913
2628
self,
2729
agent_card: AgentCard,
2830
http_handler: RequestHandler,
31+
extended_agent_card: AgentCard | None = None,
2932
context_builder: CallContextBuilder | None = None,
33+
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
34+
extended_card_modifier: Callable[
35+
[AgentCard, ServerCallContext], AgentCard
36+
]
37+
| None = None,
3038
):
3139
"""Initializes the A2ARESTFastAPIApplication.
3240
@@ -39,11 +47,19 @@ def __init__(
3947
context_builder: The CallContextBuilder used to construct the
4048
ServerCallContext passed to the http_handler. If None, no
4149
ServerCallContext is passed.
50+
card_modifier: An optional callback to dynamically modify the public
51+
agent card before it is served.
52+
extended_card_modifier: An optional callback to dynamically modify
53+
the extended agent card before it is served. It receives the
54+
call context.
4255
"""
4356
self._adapter = RESTAdapter(
4457
agent_card=agent_card,
4558
http_handler=http_handler,
59+
extended_agent_card=extended_agent_card,
4660
context_builder=context_builder,
61+
card_modifier=card_modifier,
62+
extended_card_modifier=extended_card_modifier,
4763
)
4864

4965
def build(

src/a2a/server/apps/rest/rest_adapter.py

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,26 +33,50 @@ class RESTAdapter:
3333
manages response generation including Server-Sent Events (SSE).
3434
"""
3535

36-
def __init__(
36+
def __init__( # noqa: PLR0913
3737
self,
3838
agent_card: AgentCard,
3939
http_handler: RequestHandler,
40+
extended_agent_card: AgentCard | None = None,
4041
context_builder: CallContextBuilder | None = None,
42+
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
43+
extended_card_modifier: Callable[
44+
[AgentCard, ServerCallContext], AgentCard
45+
]
46+
| None = None,
4147
):
4248
"""Initializes the RESTApplication.
4349
4450
Args:
4551
agent_card: The AgentCard describing the agent's capabilities.
4652
http_handler: The handler instance responsible for processing A2A
4753
requests via http.
54+
extended_agent_card: An optional, distinct AgentCard to be served
55+
at the authenticated extended card endpoint.
4856
context_builder: The CallContextBuilder used to construct the
4957
ServerCallContext passed to the http_handler. If None, no
5058
ServerCallContext is passed.
59+
card_modifier: An optional callback to dynamically modify the public
60+
agent card before it is served.
61+
extended_card_modifier: An optional callback to dynamically modify
62+
the extended agent card before it is served. It receives the
63+
call context.
5164
"""
5265
self.agent_card = agent_card
66+
self.extended_agent_card = extended_agent_card
67+
self.card_modifier = card_modifier
68+
self.extended_card_modifier = extended_card_modifier
5369
self.handler = RESTHandler(
5470
agent_card=agent_card, request_handler=http_handler
5571
)
72+
if (
73+
self.agent_card.supports_authenticated_extended_card
74+
and self.extended_agent_card is None
75+
and self.extended_card_modifier is None
76+
):
77+
logger.error(
78+
'AgentCard.supports_authenticated_extended_card is True, but no extended_agent_card was provided. The /agent/authenticatedExtendedCard endpoint will return 404.'
79+
)
5680
self._context_builder = context_builder or DefaultCallContextBuilder()
5781

5882
@rest_error_handler
@@ -84,35 +108,41 @@ async def event_generator(
84108
)
85109

86110
@rest_error_handler
87-
async def handle_get_agent_card(self, request: Request) -> JSONResponse:
111+
async def handle_get_agent_card(
112+
self, request: Request, call_context: ServerCallContext | None = None
113+
) -> JSONResponse | Response:
88114
"""Handles GET requests for the agent card endpoint.
89115
90116
Args:
91117
request: The incoming Starlette Request object.
118+
call_context: ServerCallContext
92119
93120
Returns:
94121
A JSONResponse containing the agent card data.
95122
"""
96-
if self.agent_card.supports_authenticated_extended_card:
97-
return await self.handle_authenticated_agent_card(request) # type: ignore[return-value]
123+
card_to_serve = self.agent_card
124+
if self.card_modifier:
125+
card_to_serve = self.card_modifier(card_to_serve)
98126

99-
# The public agent card is a direct serialization of the agent_card
100-
# provided at initialization.
101127
return JSONResponse(
102-
self.agent_card.model_dump(mode='json', exclude_none=True)
128+
card_to_serve.model_dump(
129+
exclude_none=True,
130+
by_alias=True,
131+
)
103132
)
104133

105134
@rest_error_handler
106135
async def handle_authenticated_agent_card(
107-
self, request: Request
108-
) -> JSONResponse:
136+
self, request: Request, call_context: ServerCallContext | None = None
137+
) -> JSONResponse | Response:
109138
"""Hook for per credential agent card response.
110139
111140
If a dynamic card is needed based on the credentials provided in the request
112141
override this method and return the customized content.
113142
114143
Args:
115144
request: The incoming Starlette Request object.
145+
call_context: ServerCallContext
116146
117147
Returns:
118148
A JSONResponse containing the authenticated card.
@@ -123,8 +153,29 @@ async def handle_authenticated_agent_card(
123153
message='Authenticated card not supported'
124154
)
125155
)
156+
card_to_serve = self.extended_agent_card
157+
158+
if self.extended_card_modifier:
159+
context = self._context_builder.build(request)
160+
# If no base extended card is provided, pass the public card to the modifier
161+
base_card = card_to_serve if card_to_serve else self.agent_card
162+
card_to_serve = self.extended_card_modifier(base_card, context)
163+
164+
if card_to_serve:
165+
return JSONResponse(
166+
card_to_serve.model_dump(
167+
exclude_none=True,
168+
by_alias=True,
169+
)
170+
)
171+
# If supports_authenticated_extended_card is true, but no
172+
# extended_agent_card was provided, and no modifier produced a card,
173+
# return a 404.
126174
return JSONResponse(
127-
self.agent_card.model_dump(mode='json', exclude_none=True)
175+
{
176+
'error': 'Authenticated extended agent card is supported but not configured on the server.'
177+
},
178+
status_code=404,
128179
)
129180

130181
def routes(self) -> dict[tuple[str, str], Callable[[Request], Any]]:
@@ -177,7 +228,13 @@ def routes(self) -> dict[tuple[str, str], Callable[[Request], Any]]:
177228
('/v1/tasks', 'GET'): functools.partial(
178229
self._handle_request, self.handler.list_tasks
179230
),
180-
('/v1/card', 'GET'): self.handle_get_agent_card,
231+
('v1/well_known/agent_json', 'GET'): functools.partial(
232+
self._handle_request, self.handle_get_agent_card
233+
),
181234
}
235+
if self.agent_card.supports_authenticated_extended_card:
236+
routes[('/v1/card', 'GET')] = functools.partial(
237+
self._handle_request, self.handle_authenticated_agent_card
238+
)
182239

183240
return routes

0 commit comments

Comments
 (0)