Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 5 additions & 33 deletions src/a2a/server/apps/jsonrpc/fastapi_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
from fastapi import FastAPI

from a2a.server.apps.jsonrpc.jsonrpc_app import (
CallContextBuilder,
JSONRPCApplication,
)
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
from a2a.types import A2ARequest, AgentCard
from a2a.types import A2ARequest
from a2a.utils.constants import (
AGENT_CARD_WELL_KNOWN_PATH,
DEFAULT_RPC_URL,
Expand All @@ -31,32 +29,6 @@ class A2AFastAPIApplication(JSONRPCApplication):
(SSE).
"""

def __init__(
self,
agent_card: AgentCard,
http_handler: RequestHandler,
extended_agent_card: AgentCard | None = None,
context_builder: CallContextBuilder | None = None,
) -> None:
"""Initializes the A2AStarletteApplication.

Args:
agent_card: The AgentCard describing the agent's capabilities.
http_handler: The handler instance responsible for processing A2A
requests via http.
extended_agent_card: An optional, distinct AgentCard to be served
at the authenticated extended card endpoint.
context_builder: The CallContextBuilder used to construct the
ServerCallContext passed to the http_handler. If None, no
ServerCallContext is passed.
"""
super().__init__(
agent_card=agent_card,
http_handler=http_handler,
extended_agent_card=extended_agent_card,
context_builder=context_builder,
)

def add_routes_to_app(
self,
app: FastAPI,
Expand Down Expand Up @@ -90,13 +62,13 @@ def add_routes_to_app(
)(self._handle_requests)
app.get(agent_card_url)(self._handle_get_agent_card)

# add deprecated path only if the agent_card_url uses default well-known path
if agent_card_url == AGENT_CARD_WELL_KNOWN_PATH:
app.get(PREV_AGENT_CARD_WELL_KNOWN_PATH, include_in_schema=False)(
self.handle_deprecated_agent_card_path
# For backward compatibility, serve the agent card at the deprecated path as well.
# TODO: remove in a future release
app.get(PREV_AGENT_CARD_WELL_KNOWN_PATH)(
self._handle_get_agent_card
)

# TODO: deprecated endpoint to be removed in a future release
if self.agent_card.supports_authenticated_extended_card:
app.get(extended_agent_card_url)(
self._handle_get_authenticated_extended_agent_card
Expand Down
60 changes: 40 additions & 20 deletions src/a2a/server/apps/jsonrpc/jsonrpc_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import traceback

from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator
from collections.abc import AsyncGenerator, Callable
from typing import Any

from fastapi import FastAPI
Expand Down Expand Up @@ -123,12 +123,17 @@ class JSONRPCApplication(ABC):
(SSE).
"""

def __init__(
def __init__( # noqa: PLR0913
self,
agent_card: AgentCard,
http_handler: RequestHandler,
extended_agent_card: AgentCard | None = None,
context_builder: CallContextBuilder | None = None,
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
extended_card_modifier: Callable[
[AgentCard, ServerCallContext], AgentCard
]
| None = None,
) -> None:
"""Initializes the A2AStarletteApplication.

Expand All @@ -141,17 +146,26 @@ def __init__(
context_builder: The CallContextBuilder used to construct the
ServerCallContext passed to the http_handler. If None, no
ServerCallContext is passed.
card_modifier: An optional callback to dynamically modify the public
agent card before it is served.
extended_card_modifier: An optional callback to dynamically modify
the extended agent card before it is served. It receives the
call context.
"""
self.agent_card = agent_card
self.extended_agent_card = extended_agent_card
self.card_modifier = card_modifier
self.extended_card_modifier = extended_card_modifier
self.handler = JSONRPCHandler(
agent_card=agent_card,
request_handler=http_handler,
extended_agent_card=extended_agent_card,
extended_card_modifier=extended_card_modifier,
)
if (
self.agent_card.supports_authenticated_extended_card
and self.extended_agent_card is None
and self.extended_card_modifier is None
):
logger.error(
'AgentCard.supports_authenticated_extended_card is True, but no extended_agent_card was provided. The /agent/authenticatedExtendedCard endpoint will return 404.'
Expand Down Expand Up @@ -448,24 +462,23 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
Returns:
A JSONResponse containing the agent card data.
"""
# The public agent card is a direct serialization of the agent_card
# provided at initialization.
if request.url.path == PREV_AGENT_CARD_WELL_KNOWN_PATH:
logger.warning(
f"Deprecated agent card endpoint '{PREV_AGENT_CARD_WELL_KNOWN_PATH}' accessed. "
f"Please use '{AGENT_CARD_WELL_KNOWN_PATH}' instead. This endpoint will be removed in a future version."
)

card_to_serve = self.agent_card
if self.card_modifier:
card_to_serve = self.card_modifier(card_to_serve)

return JSONResponse(
self.agent_card.model_dump(
card_to_serve.model_dump(
exclude_none=True,
by_alias=True,
)
)

async def handle_deprecated_agent_card_path(
self, request: Request
) -> JSONResponse:
"""Handles GET requests for the deprecated agent card endpoint."""
logger.warning(
f"Deprecated agent card endpoint '{PREV_AGENT_CARD_WELL_KNOWN_PATH}' accessed. Please use '{AGENT_CARD_WELL_KNOWN_PATH}' instead. This endpoint will be removed in a future version."
)
return await self._handle_get_agent_card(request)

async def _handle_get_authenticated_extended_agent_card(
self, request: Request
) -> JSONResponse:
Expand All @@ -480,17 +493,24 @@ async def _handle_get_authenticated_extended_agent_card(
status_code=404,
)

# If an explicit extended_agent_card is provided, serve that.
if self.extended_agent_card:
card_to_serve = self.extended_agent_card

if self.extended_card_modifier:
context = self._context_builder.build(request)
# If no base extended card is provided, pass the public card to the modifier
base_card = card_to_serve if card_to_serve else self.agent_card
card_to_serve = self.extended_card_modifier(base_card, context)

if card_to_serve:
return JSONResponse(
self.extended_agent_card.model_dump(
card_to_serve.model_dump(
exclude_none=True,
by_alias=True,
)
)
# If supports_authenticated_extended_card is true, but no specific
# extended_agent_card was provided during server initialization,
# return a 404
# If supports_authenticated_extended_card is true, but no
# extended_agent_card was provided, and no modifier produced a card,
# return a 404.
return JSONResponse(
{
'error': 'Authenticated extended agent card is supported but not configured on the server.'
Expand Down
36 changes: 4 additions & 32 deletions src/a2a/server/apps/jsonrpc/starlette_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@
from starlette.routing import Route

from a2a.server.apps.jsonrpc.jsonrpc_app import (
CallContextBuilder,
JSONRPCApplication,
)
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
from a2a.types import AgentCard
from a2a.utils.constants import (
AGENT_CARD_WELL_KNOWN_PATH,
DEFAULT_RPC_URL,
Expand All @@ -30,32 +27,6 @@ class A2AStarletteApplication(JSONRPCApplication):
(SSE).
"""

def __init__(
self,
agent_card: AgentCard,
http_handler: RequestHandler,
extended_agent_card: AgentCard | None = None,
context_builder: CallContextBuilder | None = None,
) -> None:
"""Initializes the A2AStarletteApplication.

Args:
agent_card: The AgentCard describing the agent's capabilities.
http_handler: The handler instance responsible for processing A2A
requests via http.
extended_agent_card: An optional, distinct AgentCard to be served
at the authenticated extended card endpoint.
context_builder: The CallContextBuilder used to construct the
ServerCallContext passed to the http_handler. If None, no
ServerCallContext is passed.
"""
super().__init__(
agent_card=agent_card,
http_handler=http_handler,
extended_agent_card=extended_agent_card,
context_builder=context_builder,
)

def routes(
self,
agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH,
Expand Down Expand Up @@ -87,14 +58,15 @@ def routes(
),
]

# add deprecated path only if the agent_card_url uses default well-known path
if agent_card_url == AGENT_CARD_WELL_KNOWN_PATH:
# For backward compatibility, serve the agent card at the deprecated path as well.
# TODO: remove in a future release
app_routes.append(
Route(
PREV_AGENT_CARD_WELL_KNOWN_PATH,
self.handle_deprecated_agent_card_path,
self._handle_get_agent_card,
methods=['GET'],
name='agent_card_path_deprecated',
name='deprecated_agent_card',
)
)

Expand Down
11 changes: 10 additions & 1 deletion src/a2a/server/request_handlers/grpc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"'pip install a2a-sdk[grpc]'"
) from e

from collections.abc import Callable

import a2a.grpc.a2a_pb2_grpc as a2a_grpc

from a2a import types
Expand Down Expand Up @@ -87,6 +89,7 @@ def __init__(
agent_card: AgentCard,
request_handler: RequestHandler,
context_builder: CallContextBuilder | None = None,
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
):
"""Initializes the GrpcHandler.

Expand All @@ -96,10 +99,13 @@ def __init__(
delegate requests to.
context_builder: The CallContextBuilder object. If none the
DefaultCallContextBuilder is used.
card_modifier: An optional callback to dynamically modify the public
agent card before it is served.
"""
self.agent_card = agent_card
self.request_handler = request_handler
self.context_builder = context_builder or DefaultCallContextBuilder()
self.card_modifier = card_modifier

async def SendMessage(
self,
Expand Down Expand Up @@ -331,7 +337,10 @@ async def GetAgentCard(
context: grpc.aio.ServicerContext,
) -> a2a_pb2.AgentCard:
"""Get the agent card for the agent served."""
return proto_utils.ToProto.agent_card(self.agent_card)
card_to_serve = self.agent_card
if self.card_modifier:
card_to_serve = self.card_modifier(card_to_serve)
return proto_utils.ToProto.agent_card(card_to_serve)

async def abort_context(
self, error: ServerError, context: grpc.aio.ServicerContext
Expand Down
25 changes: 22 additions & 3 deletions src/a2a/server/request_handlers/jsonrpc_handler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from collections.abc import AsyncIterable
from collections.abc import AsyncIterable, Callable

from a2a.server.context import ServerCallContext
from a2a.server.request_handlers.request_handler import RequestHandler
Expand Down Expand Up @@ -62,17 +62,25 @@ def __init__(
agent_card: AgentCard,
request_handler: RequestHandler,
extended_agent_card: AgentCard | None = None,
extended_card_modifier: Callable[
[AgentCard, ServerCallContext], AgentCard
]
| None = None,
):
"""Initializes the JSONRPCHandler.

Args:
agent_card: The AgentCard describing the agent's capabilities.
request_handler: The underlying `RequestHandler` instance to delegate requests to.
extended_agent_card: An optional, distinct Extended AgentCard to be served
extended_card_modifier: An optional callback to dynamically modify
the extended agent card before it is served. It receives the
call context.
"""
self.agent_card = agent_card
self.request_handler = request_handler
self.extended_agent_card = extended_agent_card
self.extended_card_modifier = extended_card_modifier

async def on_message_send(
self,
Expand Down Expand Up @@ -417,16 +425,27 @@ async def get_authenticated_extended_card(
Returns:
A `GetAuthenticatedExtendedCardResponse` object containing the config or a JSON-RPC error.
"""
if self.extended_agent_card is None:
if (
self.extended_agent_card is None
and self.extended_card_modifier is None
):
return GetAuthenticatedExtendedCardResponse(
root=JSONRPCErrorResponse(
id=request.id,
error=AuthenticatedExtendedCardNotConfiguredError(),
)
)

base_card = self.extended_agent_card
if base_card is None:
base_card = self.agent_card

card_to_serve = base_card
if self.extended_card_modifier and context:
card_to_serve = self.extended_card_modifier(base_card, context)

return GetAuthenticatedExtendedCardResponse(
root=GetAuthenticatedExtendedCardSuccessResponse(
id=request.id, result=self.extended_agent_card
id=request.id, result=card_to_serve
)
)
Loading
Loading