Skip to content

Commit afa681a

Browse files
committed
Pass a default and an extended card to the starlette app. Also add tests
1 parent 15b9ce8 commit afa681a

File tree

7 files changed

+326
-99
lines changed

7 files changed

+326
-99
lines changed

examples/helloworld/__main__.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from a2a.server.request_handlers import DefaultRequestHandler
77
from a2a.server.tasks import InMemoryTaskStore
88
from a2a.types import (
9-
AgentAuthentication,
109
AgentCapabilities,
1110
AgentCard,
1211
AgentSkill,
@@ -30,17 +29,30 @@
3029
examples=['super hi', 'give me a super hello'],
3130
)
3231

33-
agent_card = AgentCard(
32+
# This will be the public-facing agent card
33+
public_agent_card = AgentCard(
3434
name='Hello World Agent',
3535
description='Just a hello world agent',
3636
url='http://localhost:9999/',
3737
version='1.0.0',
3838
defaultInputModes=['text'],
3939
defaultOutputModes=['text'],
4040
capabilities=AgentCapabilities(streaming=True),
41-
skills=[skill, extended_skill], # Include both skills
42-
authentication=AgentAuthentication(schemes=['public']),
43-
# Adding this line to enable extended card support:
41+
skills=[skill], # Only the basic skill for the public card
42+
supportsAuthenticatedExtendedCard=True,
43+
)
44+
45+
# This will be the authenticated extended agent card
46+
# It includes the additional 'extended_skill'
47+
specific_extended_agent_card = AgentCard(
48+
name='Hello World Agent - Extended Edition', # Different name for clarity
49+
description='The full-featured hello world agent for authenticated users.',
50+
url='http://localhost:9999/',
51+
version='1.0.1', # Could even be a different version
52+
defaultInputModes=['text'],
53+
defaultOutputModes=['text'],
54+
capabilities=AgentCapabilities(streaming=True), # Could have different capabilities
55+
skills=[skill, extended_skill], # Both skills for the extended card
4456
supportsAuthenticatedExtendedCard=True,
4557
)
4658

@@ -49,9 +61,9 @@
4961
task_store=InMemoryTaskStore(),
5062
)
5163

52-
server = A2AStarletteApplication(
53-
agent_card=agent_card, http_handler=request_handler
54-
)
64+
server = A2AStarletteApplication(agent_card=public_agent_card,
65+
http_handler=request_handler,
66+
extended_agent_card=specific_extended_agent_card)
5567
import uvicorn
5668

5769
uvicorn.run(server.build(), host='0.0.0.0', port=9999)

examples/helloworld/test_client.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
from a2a.client import A2AClient
1+
import logging # Import the logging module
22
from typing import Any
3-
import httpx
43
from uuid import uuid4
4+
5+
import httpx
6+
7+
from a2a.client import A2AClient
58
from a2a.types import (
69
SendMessageRequest,
710
MessageSendParams,
@@ -10,6 +13,9 @@
1013

1114

1215
async def main() -> None:
16+
# Configure logging to show INFO level messages
17+
logging.basicConfig(level=logging.INFO)
18+
1319
async with httpx.AsyncClient() as httpx_client:
1420
client = await A2AClient.get_client_from_agent_card_url(
1521
httpx_client, 'http://localhost:9999'

src/a2a/client/client.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import json
2+
import logging
23
from collections.abc import AsyncGenerator
34
from typing import Any
45
from uuid import uuid4
56

67
import httpx
78
from httpx_sse import SSEError, aconnect_sse
9+
from pydantic import ValidationError
810

911
from a2a.client.errors import A2AClientHTTPError, A2AClientJSONError
1012
from a2a.types import (AgentCard, CancelTaskRequest, CancelTaskResponse,
@@ -17,6 +19,7 @@
1719
SetTaskPushNotificationConfigResponse)
1820
from a2a.utils.telemetry import SpanKind, trace_class
1921

22+
logger = logging.getLogger(__name__)
2023

2124
class A2ACardResolver:
2225
"""Agent Card resolver."""
@@ -122,9 +125,7 @@ async def get_agent_card(
122125
'Successfully fetched extended agent card data: %s',
123126
extended_agent_card_data,
124127
) # Added for verbosity
125-
print(
126-
f'DEBUG: Fetched extended agent card data:\n{json.dumps(extended_agent_card_data, indent=2)}'
127-
) # Added for direct output
128+
128129
# This new card data replaces the old one entirely
129130
agent_card = AgentCard.model_validate(extended_agent_card_data)
130131
logger.info(

src/a2a/server/apps/starlette_app.py

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,23 @@ class A2AStarletteApplication:
3434
(SSE).
3535
"""
3636

37-
def __init__(self, agent_card: AgentCard, http_handler: RequestHandler):
37+
def __init__(
38+
self,
39+
agent_card: AgentCard,
40+
http_handler: RequestHandler,
41+
extended_agent_card: AgentCard | None = None,
42+
):
3843
"""Initializes the A2AStarletteApplication.
3944
4045
Args:
4146
agent_card: The AgentCard describing the agent's capabilities.
4247
http_handler: The handler instance responsible for processing A2A
4348
requests via http.
49+
extended_agent_card: An optional, distinct AgentCard to be served
50+
at the authenticated extended card endpoint.
4451
"""
4552
self.agent_card = agent_card
53+
self.extended_agent_card = extended_agent_card
4654
self.handler = JSONRPCHandler(
4755
agent_card=agent_card, request_handler=http_handler
4856
)
@@ -264,44 +272,11 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
264272
Returns:
265273
A JSONResponse containing the agent card data.
266274
"""
267-
268-
# Construct the public view of the agent card.
269-
public_card_data = {
270-
'version': self.agent_card.version,
271-
'name': self.agent_card.name,
272-
'providerName': self.agent_card.provider.organization
273-
if self.agent_card.provider
274-
else None,
275-
'url': self.agent_card.url,
276-
'authentication': self.agent_card.authentication.model_dump(
277-
mode='json', exclude_none=True
278-
)
279-
if self.agent_card.authentication
280-
else None, # authentication is a single object, can be None if made Optional
281-
'skills': [
282-
f.model_dump(mode='json', exclude_none=True)
283-
for f in self.agent_card.skills
284-
if f.id == 'hello_world' # Explicitly filter for public skills
285-
]
286-
if self.agent_card.skills
287-
else [], # Default to empty list if no skills
288-
'capabilities': self.agent_card.capabilities.model_dump(
289-
mode='json', exclude_none=True
290-
),
291-
'supportsAuthenticatedExtendedCard': (
292-
self.agent_card.supportsAuthenticatedExtendedCard
293-
),
294-
# Include other fields from types.py AgentCard designated as public
295-
'description': self.agent_card.description,
296-
'documentationUrl': self.agent_card.documentationUrl,
297-
'defaultInputModes': self.agent_card.defaultInputModes,
298-
'defaultOutputModes': self.agent_card.defaultOutputModes,
299-
}
300-
# Filter out None values from the public card data.
301-
public_card_data_cleaned = {
302-
k: v for k, v in public_card_data.items() if v is not None
303-
}
304-
return JSONResponse(public_card_data_cleaned)
275+
# The public agent card is a direct serialization of the agent_card
276+
# provided at initialization.
277+
return JSONResponse(
278+
self.agent_card.model_dump(mode='json', exclude_none=True)
279+
)
305280

306281
async def _handle_get_authenticated_extended_agent_card(
307282
self, request: Request
@@ -313,11 +288,15 @@ async def _handle_get_authenticated_extended_agent_card(
313288
status_code=404,
314289
)
315290

316-
# Authentication and authorization are NOT YET IMPLEMENTED for this endpoint.
317-
# As per current requirements, if 'supportsAuthenticatedExtendedCard' is true,
318-
# this endpoint returns the complete agent card.
319-
# In the future, proper authentication checks will be added here, and the
320-
# returned card may be filtered based on the client's authorization scopes.
291+
# If an explicit extended_agent_card is provided, serve that.
292+
if self.extended_agent_card:
293+
return JSONResponse(
294+
self.extended_agent_card.model_dump(
295+
mode='json', exclude_none=True
296+
)
297+
)
298+
# Otherwise, if supportsAuthenticatedExtendedCard is true but no specific
299+
# extended card is set, serve the main agent_card.
321300
return JSONResponse(
322301
self.agent_card.model_dump(mode='json', exclude_none=True)
323302
)

tests/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Running the tests
2+
3+
1. Run the tests
4+
```bash
5+
uv run pytest -v -s client/test_client.py
6+
```
7+
8+
In case of failures, you can cleanup the cache:
9+
10+
1. `uv clean`
11+
2. `rm -fR .pytest_cache .venv __pycache__`

0 commit comments

Comments
 (0)