Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
22 changes: 17 additions & 5 deletions src/a2a/server/apps/jsonrpc/fastapi_app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from collections.abc import AsyncIterator
from collections.abc import AsyncIterator, Callable
from contextlib import asynccontextmanager
from typing import Any

Expand All @@ -10,6 +10,7 @@
CallContextBuilder,
JSONRPCApplication,
)
from a2a.server.context import ServerCallContext
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
from a2a.types import A2ARequest, AgentCard
from a2a.utils.constants import (
Expand All @@ -31,12 +32,17 @@
(SSE).
"""

def __init__(

Check failure on line 35 in src/a2a/server/apps/jsonrpc/fastapi_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Ruff (PLR0913)

src/a2a/server/apps/jsonrpc/fastapi_app.py:35:9: PLR0913 Too many arguments in function definition (6 > 5)
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,
Comment thread
holtskinner marked this conversation as resolved.
Outdated
extended_card_modifier: Callable[
[AgentCard, ServerCallContext], AgentCard
]
| None = None,
) -> None:
"""Initializes the A2AStarletteApplication.

Expand All @@ -49,12 +55,19 @@
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.
"""
super().__init__(
agent_card=agent_card,
http_handler=http_handler,
extended_agent_card=extended_agent_card,
context_builder=context_builder,
card_modifier=card_modifier,
extended_card_modifier=extended_card_modifier,
)

def add_routes_to_app(

Check notice on line 73 in src/a2a/server/apps/jsonrpc/fastapi_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/apps/jsonrpc/starlette_app.py (35-73)
Expand Down Expand Up @@ -90,13 +103,12 @@
)(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.
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
Comment thread
dmandar marked this conversation as resolved.
Expand Down
57 changes: 38 additions & 19 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 @@
(SSE).
"""

def __init__(

Check failure on line 126 in src/a2a/server/apps/jsonrpc/jsonrpc_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Ruff (PLR0913)

src/a2a/server/apps/jsonrpc/jsonrpc_app.py:126:9: PLR0913 Too many arguments in function definition (6 > 5)
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,9 +146,16 @@
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

Check notice on line 155 in src/a2a/server/apps/jsonrpc/jsonrpc_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/apps/jsonrpc/starlette_app.py (35-64)
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,
Comment thread
dmandar marked this conversation as resolved.
request_handler=http_handler,
Expand All @@ -152,6 +164,7 @@
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 +461,23 @@
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 +492,24 @@
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
19 changes: 16 additions & 3 deletions src/a2a/server/apps/jsonrpc/starlette_app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging

from collections.abc import Callable
from typing import Any

from starlette.applications import Starlette
Expand All @@ -9,6 +10,7 @@
CallContextBuilder,
JSONRPCApplication,
)
from a2a.server.context import ServerCallContext
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
from a2a.types import AgentCard
from a2a.utils.constants import (
Expand All @@ -30,12 +32,17 @@
(SSE).
"""

def __init__(

Check failure on line 35 in src/a2a/server/apps/jsonrpc/starlette_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Ruff (PLR0913)

src/a2a/server/apps/jsonrpc/starlette_app.py:35:9: PLR0913 Too many arguments in function definition (6 > 5)
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 @@ -48,12 +55,19 @@
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
Comment thread
kthota-g marked this conversation as resolved.
Outdated
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.
"""
super().__init__(

Check notice on line 64 in src/a2a/server/apps/jsonrpc/starlette_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/apps/jsonrpc/jsonrpc_app.py (126-155)
agent_card=agent_card,
http_handler=http_handler,
extended_agent_card=extended_agent_card,
context_builder=context_builder,
card_modifier=card_modifier,
extended_card_modifier=extended_card_modifier,
)

def routes(

Check notice on line 73 in src/a2a/server/apps/jsonrpc/starlette_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/apps/jsonrpc/fastapi_app.py (35-73)
Expand Down Expand Up @@ -87,14 +101,13 @@
),
]

# add deprecated path only if the agent_card_url uses default well-known path
if agent_card_url == AGENT_CARD_WELL_KNOWN_PATH:
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
28 changes: 28 additions & 0 deletions tests/server/request_handlers/test_grpc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,34 @@ async def test_get_agent_card(
assert response.version == sample_agent_card.version


@pytest.mark.asyncio
async def test_get_agent_card_with_modifier(
mock_request_handler: AsyncMock,
sample_agent_card: types.AgentCard,
mock_grpc_context: AsyncMock,
):
"""Test GetAgentCard call with a card_modifier."""

def modifier(card: types.AgentCard) -> types.AgentCard:
modified_card = card.model_copy(deep=True)
modified_card.name = 'Modified gRPC Agent'
return modified_card

grpc_handler_modified = GrpcHandler(
agent_card=sample_agent_card,
request_handler=mock_request_handler,
card_modifier=modifier,
)

request_proto = a2a_pb2.GetAgentCardRequest()
response = await grpc_handler_modified.GetAgentCard(
request_proto, mock_grpc_context
)

assert response.name == 'Modified gRPC Agent'
assert response.version == sample_agent_card.version


@pytest.mark.asyncio
@pytest.mark.parametrize(
'server_error, grpc_status_code, error_message_part',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@
from collections.abc import AsyncGenerator

import pytest
import pytest_asyncio

from _pytest.mark.structures import ParameterSet
from sqlalchemy import select
from sqlalchemy.ext.asyncio import (
async_sessionmaker,
create_async_engine,
)


# Skip entire test module if SQLAlchemy is not installed
Expand All @@ -20,8 +12,17 @@
reason='Database tests require Cryptography. Install extra encryption',
)

import pytest_asyncio

from _pytest.mark.structures import ParameterSet

# Now safe to import SQLAlchemy-dependent modules
from cryptography.fernet import Fernet
from sqlalchemy import select
from sqlalchemy.ext.asyncio import (
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.inspection import inspect

from a2a.server.models import (
Expand Down
Loading
Loading