Skip to content

Commit 749d102

Browse files
committed
feat: Add GetExtendedAgentCard Support to RequestHandlers
1 parent ca7edc3 commit 749d102

32 files changed

Lines changed: 565 additions & 425 deletions

itk/main.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,16 @@ async def main_async(http_port: int, grpc_port: int) -> None:
292292
handler = DefaultRequestHandler(
293293
agent_executor=V10AgentExecutor(),
294294
task_store=task_store,
295+
agent_card=agent_card,
296+
queue_manager=InMemoryQueueManager(),
297+
)
298+
299+
handler_extended = DefaultRequestHandler(
300+
agent_executor=V10AgentExecutor(),
301+
task_store=task_store,
302+
agent_card=agent_card,
295303
queue_manager=InMemoryQueueManager(),
304+
extended_agent_card=agent_card,
296305
)
297306

298307
app = FastAPI()
@@ -301,9 +310,7 @@ async def main_async(http_port: int, grpc_port: int) -> None:
301310
agent_card=agent_card, card_url='/.well-known/agent-card.json'
302311
)
303312
jsonrpc_routes = create_jsonrpc_routes(
304-
agent_card=agent_card,
305-
request_handler=handler,
306-
extended_agent_card=agent_card,
313+
request_handler=handler_extended,
307314
rpc_url='/',
308315
enable_v0_3_compat=True,
309316
)
@@ -313,7 +320,6 @@ async def main_async(http_port: int, grpc_port: int) -> None:
313320
)
314321

315322
rest_routes = create_rest_routes(
316-
agent_card=agent_card,
317323
request_handler=handler,
318324
enable_v0_3_compat=True,
319325
)
@@ -323,7 +329,7 @@ async def main_async(http_port: int, grpc_port: int) -> None:
323329

324330
compat_servicer = CompatGrpcHandler(agent_card, handler)
325331
a2a_v0_3_pb2_grpc.add_A2AServiceServicer_to_server(compat_servicer, server)
326-
servicer = GrpcHandler(agent_card, handler)
332+
servicer = GrpcHandler(handler)
327333
a2a_pb2_grpc.add_A2AServiceServicer_to_server(servicer, server)
328334

329335
server.add_insecure_port(f'127.0.0.1:{grpc_port}')

samples/hello_world_agent.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,17 +191,17 @@ async def serve(
191191

192192
task_store = InMemoryTaskStore()
193193
request_handler = DefaultRequestHandler(
194-
agent_executor=SampleAgentExecutor(), task_store=task_store
194+
agent_executor=SampleAgentExecutor(),
195+
task_store=task_store,
196+
agent_card=agent_card,
195197
)
196198

197199
rest_routes = create_rest_routes(
198-
agent_card=agent_card,
199200
request_handler=request_handler,
200201
path_prefix='/a2a/rest',
201202
enable_v0_3_compat=True,
202203
)
203204
jsonrpc_routes = create_jsonrpc_routes(
204-
agent_card=agent_card,
205205
request_handler=request_handler,
206206
rpc_url='/a2a/jsonrpc',
207207
enable_v0_3_compat=True,
@@ -216,7 +216,7 @@ async def serve(
216216

217217
grpc_server = grpc.aio.server()
218218
grpc_server.add_insecure_port(f'{host}:{grpc_port}')
219-
servicer = GrpcHandler(agent_card, request_handler)
219+
servicer = GrpcHandler(request_handler)
220220
a2a_pb2_grpc.add_A2AServiceServicer_to_server(servicer, grpc_server)
221221

222222
compat_grpc_server = grpc.aio.server()

src/a2a/server/request_handlers/default_request_handler.py

Lines changed: 97 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import asyncio
22
import logging
33

4-
from collections.abc import AsyncGenerator
4+
from collections.abc import AsyncGenerator, Awaitable, Callable
55
from typing import cast
66

77
from a2a.server.agent_execution import (
@@ -31,8 +31,10 @@
3131
TaskStore,
3232
)
3333
from a2a.types.a2a_pb2 import (
34+
AgentCard,
3435
CancelTaskRequest,
3536
DeleteTaskPushNotificationConfigRequest,
37+
GetExtendedAgentCardRequest,
3638
GetTaskPushNotificationConfigRequest,
3739
GetTaskRequest,
3840
ListTaskPushNotificationConfigsRequest,
@@ -47,13 +49,16 @@
4749
TaskState,
4850
)
4951
from a2a.utils.errors import (
52+
ExtendedAgentCardNotConfiguredError,
5053
InternalError,
5154
InvalidParamsError,
55+
PushNotificationConfigNotFoundError,
5256
PushNotificationNotSupportedError,
5357
TaskNotCancelableError,
5458
TaskNotFoundError,
5559
UnsupportedOperationError,
5660
)
61+
from a2a.utils.helpers import maybe_await, validate
5762
from a2a.utils.task import (
5863
apply_history_length,
5964
validate_history_length,
@@ -88,27 +93,43 @@ def __init__( # noqa: PLR0913
8893
self,
8994
agent_executor: AgentExecutor,
9095
task_store: TaskStore,
96+
agent_card: AgentCard,
9197
queue_manager: QueueManager | None = None,
9298
push_config_store: PushNotificationConfigStore | None = None,
9399
push_sender: PushNotificationSender | None = None,
94100
request_context_builder: RequestContextBuilder | None = None,
101+
extended_agent_card: AgentCard | None = None,
102+
card_modifier: Callable[[AgentCard], Awaitable[AgentCard] | AgentCard]
103+
| None = None,
104+
extended_card_modifier: Callable[
105+
[AgentCard, ServerCallContext], Awaitable[AgentCard] | AgentCard
106+
]
107+
| None = None,
95108
) -> None:
96109
"""Initializes the DefaultRequestHandler.
97110
98111
Args:
99112
agent_executor: The `AgentExecutor` instance to run agent logic.
100113
task_store: The `TaskStore` instance to manage task persistence.
114+
agent_card: The AgentCard describing the agent's capabilities.
101115
queue_manager: The `QueueManager` instance to manage event queues. Defaults to `InMemoryQueueManager`.
102116
push_config_store: The `PushNotificationConfigStore` instance for managing push notification configurations. Defaults to None.
103117
push_sender: The `PushNotificationSender` instance for sending push notifications. Defaults to None.
104118
request_context_builder: The `RequestContextBuilder` instance used
105119
to build request contexts. Defaults to `SimpleRequestContextBuilder`.
120+
extended_agent_card: An optional, distinct AgentCard to be served at the authenticated extended card endpoint.
121+
card_modifier: An optional callback to dynamically modify the public agent card before it is served.
122+
extended_card_modifier: An optional callback to dynamically modify the extended agent card before it is served. It receives the call context.
106123
"""
107124
self.agent_executor = agent_executor
108125
self.task_store = task_store
126+
self._agent_card = agent_card
109127
self._queue_manager = queue_manager or InMemoryQueueManager()
110128
self._push_config_store = push_config_store
111129
self._push_sender = push_sender
130+
self.extended_agent_card = extended_agent_card
131+
self.card_modifier = card_modifier
132+
self.extended_card_modifier = extended_card_modifier
112133
self._request_context_builder = (
113134
request_context_builder
114135
or SimpleRequestContextBuilder(
@@ -122,6 +143,11 @@ def __init__( # noqa: PLR0913
122143
# asyncio tasks and to surface unexpected exceptions.
123144
self._background_tasks = set()
124145

146+
@property
147+
def agent_card(self) -> AgentCard:
148+
"""The agent card to be served by default."""
149+
return self._agent_card
150+
125151
@validate_request_params
126152
async def on_get_task(
127153
self,
@@ -396,6 +422,10 @@ async def push_notification_callback(event: Event) -> None:
396422
return result
397423

398424
@validate_request_params
425+
@validate(
426+
lambda self: self.agent_card.capabilities.streaming,
427+
'Streaming is not supported by the agent',
428+
)
399429
async def on_message_send_stream(
400430
self,
401431
params: SendMessageRequest,
@@ -485,6 +515,14 @@ async def _cleanup_producer(
485515
self._running_agents.pop(task_id, None)
486516

487517
@validate_request_params
518+
@validate(
519+
lambda self: (
520+
self.agent_card.capabilities.push_notifications
521+
and self._push_config_store
522+
),
523+
error_message='Push notifications are not supported by the agent',
524+
error_type=PushNotificationNotSupportedError,
525+
)
488526
async def on_create_task_push_notification_config(
489527
self,
490528
params: TaskPushNotificationConfig,
@@ -494,15 +532,12 @@ async def on_create_task_push_notification_config(
494532
495533
Requires a `PushNotifier` to be configured.
496534
"""
497-
if not self._push_config_store:
498-
raise PushNotificationNotSupportedError
499-
500535
task_id = params.task_id
501536
task: Task | None = await self.task_store.get(task_id, context)
502537
if not task:
503538
raise TaskNotFoundError
504539

505-
await self._push_config_store.set_info(
540+
await self._push_config_store.set_info( # type: ignore[union-attr]
506541
task_id,
507542
params,
508543
context,
@@ -511,6 +546,14 @@ async def on_create_task_push_notification_config(
511546
return params
512547

513548
@validate_request_params
549+
@validate(
550+
lambda self: (
551+
self.agent_card.capabilities.push_notifications
552+
and self._push_config_store
553+
),
554+
error_message='Push notifications are not supported by the agent',
555+
error_type=PushNotificationNotSupportedError,
556+
)
514557
async def on_get_task_push_notification_config(
515558
self,
516559
params: GetTaskPushNotificationConfigRequest,
@@ -520,26 +563,27 @@ async def on_get_task_push_notification_config(
520563
521564
Requires a `PushConfigStore` to be configured.
522565
"""
523-
if not self._push_config_store:
524-
raise PushNotificationNotSupportedError
525-
526566
task_id = params.task_id
527567
config_id = params.id
528568
task: Task | None = await self.task_store.get(task_id, context)
529569
if not task:
530570
raise TaskNotFoundError
531571

532572
push_notification_configs: list[TaskPushNotificationConfig] = (
533-
await self._push_config_store.get_info(task_id, context) or []
573+
await self._push_config_store.get_info(task_id, context) or [] # type: ignore[union-attr]
534574
)
535575

536576
for config in push_notification_configs:
537577
if config.id == config_id:
538578
return config
539579

540-
raise InternalError(message='Push notification config not found')
580+
raise PushNotificationConfigNotFoundError
541581

542582
@validate_request_params
583+
@validate(
584+
lambda self: self.agent_card.capabilities.streaming,
585+
'Streaming is not supported by the agent',
586+
)
543587
async def on_subscribe_to_task(
544588
self,
545589
params: SubscribeToTaskRequest,
@@ -583,6 +627,14 @@ async def on_subscribe_to_task(
583627
yield event
584628

585629
@validate_request_params
630+
@validate(
631+
lambda self: (
632+
self.agent_card.capabilities.push_notifications
633+
and self._push_config_store
634+
),
635+
error_message='Push notifications are not supported by the agent',
636+
error_type=PushNotificationNotSupportedError,
637+
)
586638
async def on_list_task_push_notification_configs(
587639
self,
588640
params: ListTaskPushNotificationConfigsRequest,
@@ -592,15 +644,12 @@ async def on_list_task_push_notification_configs(
592644
593645
Requires a `PushConfigStore` to be configured.
594646
"""
595-
if not self._push_config_store:
596-
raise PushNotificationNotSupportedError
597-
598647
task_id = params.task_id
599648
task: Task | None = await self.task_store.get(task_id, context)
600649
if not task:
601650
raise TaskNotFoundError
602651

603-
push_notification_config_list = await self._push_config_store.get_info(
652+
push_notification_config_list = await self._push_config_store.get_info( # type: ignore[union-attr]
604653
task_id, context
605654
)
606655

@@ -609,6 +658,14 @@ async def on_list_task_push_notification_configs(
609658
)
610659

611660
@validate_request_params
661+
@validate(
662+
lambda self: (
663+
self.agent_card.capabilities.push_notifications
664+
and self._push_config_store
665+
),
666+
error_message='Push notifications are not supported by the agent',
667+
error_type=PushNotificationNotSupportedError,
668+
)
612669
async def on_delete_task_push_notification_config(
613670
self,
614671
params: DeleteTaskPushNotificationConfigRequest,
@@ -618,13 +675,35 @@ async def on_delete_task_push_notification_config(
618675
619676
Requires a `PushConfigStore` to be configured.
620677
"""
621-
if not self._push_config_store:
622-
raise PushNotificationNotSupportedError
623-
624678
task_id = params.task_id
625679
config_id = params.id
626680
task: Task | None = await self.task_store.get(task_id, context)
627681
if not task:
628682
raise TaskNotFoundError
629683

630-
await self._push_config_store.delete_info(task_id, context, config_id)
684+
await self._push_config_store.delete_info(task_id, context, config_id) # type: ignore[union-attr]
685+
686+
@validate_request_params
687+
@validate(
688+
lambda self: self.agent_card.capabilities.extended_agent_card,
689+
error_message='The agent does not have an extended agent card configured',
690+
error_type=ExtendedAgentCardNotConfiguredError,
691+
)
692+
async def on_get_extended_agent_card(
693+
self,
694+
params: GetExtendedAgentCardRequest,
695+
context: ServerCallContext,
696+
) -> AgentCard:
697+
"""Default handler for 'GetExtendedAgentCard'.
698+
699+
Requires `capabilities.extended_agent_card` to be true.
700+
"""
701+
card = self.extended_agent_card or self.agent_card
702+
703+
if self.extended_card_modifier:
704+
return await maybe_await(self.extended_card_modifier(card, context))
705+
706+
if self.card_modifier:
707+
return await maybe_await(self.card_modifier(card))
708+
709+
return card

0 commit comments

Comments
 (0)