Skip to content

Commit d1d646e

Browse files
committed
Add functionality for extended agent card. Does not support authentication yet
1 parent 36bfdfd commit d1d646e

File tree

4 files changed

+151
-18
lines changed

4 files changed

+151
-18
lines changed

examples/helloworld/__main__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
AgentSkill,
1111
)
1212

13-
1413
if __name__ == '__main__':
1514
skill = AgentSkill(
1615
id='hello_world',
@@ -20,6 +19,14 @@
2019
examples=['hi', 'hello world'],
2120
)
2221

22+
extended_skill = AgentSkill(
23+
id='super_hello_world',
24+
name='Returns a SUPER Hello World',
25+
description='A more enthusiastic greeting, only for authenticated users.',
26+
tags=['hello world', 'super', 'extended'],
27+
examples=['super hi', 'give me a super hello'],
28+
)
29+
2330
agent_card = AgentCard(
2431
name='Hello World Agent',
2532
description='Just a hello world agent',
@@ -28,8 +35,10 @@
2835
defaultInputModes=['text'],
2936
defaultOutputModes=['text'],
3037
capabilities=AgentCapabilities(streaming=True),
31-
skills=[skill],
38+
skills=[skill, extended_skill], # Include both skills
3239
authentication=AgentAuthentication(schemes=['public']),
40+
# Adding this line to enable extended card support:
41+
supportsAuthenticatedExtendedCard=True,
3342
)
3443

3544
request_handler = DefaultRequestHandler(

src/a2a/client/client.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import json
2-
2+
import logging
33
from collections.abc import AsyncGenerator
44
from typing import Any
55
from uuid import uuid4
66

77
import httpx
8-
98
from httpx_sse import SSEError, aconnect_sse
109

1110
from a2a.client.errors import A2AClientHTTPError, A2AClientJSONError
@@ -34,30 +33,92 @@ def __init__(
3433
httpx_client: httpx.AsyncClient,
3534
base_url: str,
3635
agent_card_path: str = '/.well-known/agent.json',
36+
extended_agent_card_path: str = '/agent/authenticatedExtendedCard',
3737
):
3838
self.base_url = base_url.rstrip('/')
3939
self.agent_card_path = agent_card_path.lstrip('/')
40+
self.extended_agent_card_path = extended_agent_card_path.lstrip('/')
4041
self.httpx_client = httpx_client
4142

4243
async def get_agent_card(
4344
self, http_kwargs: dict[str, Any] | None = None
4445
) -> AgentCard:
46+
# Fetch the initial public agent card
47+
public_card_url = f'{self.base_url}/{self.agent_card_path}'
4548
try:
4649
response = await self.httpx_client.get(
47-
f'{self.base_url}/{self.agent_card_path}',
50+
public_card_url,
4851
**(http_kwargs or {}),
4952
)
5053
response.raise_for_status()
51-
return AgentCard.model_validate(response.json())
54+
public_agent_card_data = response.json()
55+
logger.info("Successfully fetched public agent card data: %s", public_agent_card_data) # Added for verbosity
56+
# print(f"DEBUG: Fetched public agent card data:\n{json.dumps(public_agent_card_data, indent=2)}") # Added for direct output
57+
agent_card = AgentCard.model_validate(public_agent_card_data)
5258
except httpx.HTTPStatusError as e:
53-
raise A2AClientHTTPError(e.response.status_code, str(e)) from e
59+
raise A2AClientHTTPError(
60+
e.response.status_code,
61+
f'Failed to fetch public agent card from {public_card_url}: {e}',
62+
) from e
5463
except json.JSONDecodeError as e:
55-
raise A2AClientJSONError(str(e)) from e
64+
raise A2AClientJSONError(
65+
f'Failed to parse JSON for public agent card from {public_card_url}: {e}'
66+
) from e
5667
except httpx.RequestError as e:
5768
raise A2AClientHTTPError(
58-
503, f'Network communication error: {e}'
69+
503,
70+
f'Network communication error fetching public agent card from {public_card_url}: {e}',
5971
) from e
6072

73+
# Check for supportsAuthenticatedExtendedCard
74+
if agent_card.supportsAuthenticatedExtendedCard:
75+
# Construct URL for the extended card.
76+
# The extended card URL is relative to the agent's base URL specified *in* the agent card.
77+
if not agent_card.url:
78+
logger.warning(
79+
"Agent card (from %s) indicates support for an extended card "
80+
"but does not specify its own base 'url' field. "
81+
"Cannot fetch extended card. Proceeding with public card.",
82+
public_card_url,
83+
)
84+
return agent_card
85+
86+
extended_card_base_url = agent_card.url.rstrip('/')
87+
full_extended_card_url = (
88+
f'{extended_card_base_url}/{self.extended_agent_card_path}'
89+
)
90+
91+
logger.info(
92+
'Attempting to fetch extended agent card from %s',
93+
full_extended_card_url,
94+
)
95+
try:
96+
# Make another GET request for the extended card
97+
# Note: Authentication headers will be added here when auth is implemented.
98+
extended_response = await self.httpx_client.get(
99+
full_extended_card_url,
100+
**(http_kwargs or {}), # Passing original http_kwargs
101+
)
102+
extended_response.raise_for_status()
103+
extended_agent_card_data = extended_response.json()
104+
logger.info("Successfully fetched extended agent card data: %s", extended_agent_card_data) # Added for verbosity
105+
print(f"DEBUG: Fetched extended agent card data:\n{json.dumps(extended_agent_card_data, indent=2)}") # Added for direct output
106+
# This new card data replaces the old one entirely
107+
agent_card = AgentCard.model_validate(extended_agent_card_data)
108+
logger.info(
109+
'Successfully fetched and using extended agent card from %s',
110+
full_extended_card_url,
111+
)
112+
except (httpx.HTTPStatusError, httpx.RequestError, json.JSONDecodeError, ValidationError) as e:
113+
logger.warning(
114+
'Failed to fetch or parse extended agent card from %s. Error: %s. '
115+
'Proceeding with the initially fetched public agent card.',
116+
full_extended_card_url, e
117+
)
118+
# Fallback to the already parsed public_agent_card (which is 'agent_card' at this point)
119+
120+
return agent_card
121+
61122

62123
class A2AClient:
63124
"""A2A Client."""

src/a2a/server/apps/starlette_app.py

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import json
22
import logging
33
import traceback
4-
54
from collections.abc import AsyncGenerator
65
from typing import Any
76

@@ -38,7 +37,6 @@
3837
)
3938
from a2a.utils.errors import MethodNotImplementedError
4039

41-
4240
logger = logging.getLogger(__name__)
4341

4442

@@ -247,25 +245,74 @@ async def event_generator(
247245

248246
async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
249247
"""Handles GET requests for the agent card."""
248+
# Construct the public view of the agent card.
249+
public_card_data = {
250+
"version": self.agent_card.version,
251+
"name": self.agent_card.name,
252+
"providerName": self.agent_card.provider.organization if self.agent_card.provider else None,
253+
"url": self.agent_card.url,
254+
"authentication": self.agent_card.authentication.model_dump(mode='json', exclude_none=True)
255+
if self.agent_card.authentication else None, # authentication is a single object, can be None if made Optional
256+
"skills": [
257+
f.model_dump(mode='json', exclude_none=True)
258+
for f in self.agent_card.skills if f.id == 'hello_world' # Explicitly filter for public skills
259+
]
260+
if self.agent_card.skills
261+
else [], # Default to empty list if no skills
262+
"capabilities": self.agent_card.capabilities.model_dump(
263+
mode='json', exclude_none=True
264+
),
265+
"supportsAuthenticatedExtendedCard": (
266+
self.agent_card.supportsAuthenticatedExtendedCard
267+
),
268+
# Include other fields from types.py AgentCard designated as public
269+
"description": self.agent_card.description,
270+
"documentationUrl": self.agent_card.documentationUrl,
271+
"defaultInputModes": self.agent_card.defaultInputModes,
272+
"defaultOutputModes": self.agent_card.defaultOutputModes,
273+
}
274+
# Filter out None values from the public card data.
275+
public_card_data_cleaned = {
276+
k: v for k, v in public_card_data.items() if v is not None
277+
}
278+
return JSONResponse(public_card_data_cleaned)
279+
280+
async def _handle_get_authenticated_extended_agent_card(
281+
self, request: Request
282+
) -> JSONResponse:
283+
"""Handles GET requests for the authenticated extended agent card."""
284+
if not self.agent_card.supportsAuthenticatedExtendedCard:
285+
return JSONResponse(
286+
{"error": "Extended agent card not supported or not enabled."},
287+
status_code=404,
288+
)
289+
290+
# Authentication and authorization are NOT YET IMPLEMENTED for this endpoint.
291+
# As per current requirements, if 'supportsAuthenticatedExtendedCard' is true,
292+
# this endpoint returns the complete agent card.
293+
# In the future, proper authentication checks will be added here, and the
294+
# returned card may be filtered based on the client's authorization scopes.
250295
return JSONResponse(
251296
self.agent_card.model_dump(mode='json', exclude_none=True)
252297
)
253298

254299
def routes(
255300
self,
256301
agent_card_url: str = '/.well-known/agent.json',
302+
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
257303
rpc_url: str = '/',
258304
) -> list[Route]:
259305
"""Returns the Starlette Routes for handling A2A requests.
260306
261307
Args:
262308
agent_card_url: The URL for the agent card endpoint.
263309
rpc_url: The URL for the A2A JSON-RPC endpoint
310+
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
264311
265312
Returns:
266313
The Starlette Routes serving A2A requests.
267314
"""
268-
return [
315+
app_routes = [
269316
Route(
270317
rpc_url,
271318
self._handle_requests,
@@ -280,9 +327,21 @@ def routes(
280327
),
281328
]
282329

330+
if self.agent_card.supportsAuthenticatedExtendedCard:
331+
app_routes.append(
332+
Route(
333+
extended_agent_card_url,
334+
self._handle_get_authenticated_extended_agent_card,
335+
methods=['GET'],
336+
name='authenticated_extended_agent_card',
337+
)
338+
)
339+
return app_routes
340+
283341
def build(
284342
self,
285343
agent_card_url: str = '/.well-known/agent.json',
344+
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
286345
rpc_url: str = '/',
287346
**kwargs: Any,
288347
) -> Starlette:
@@ -291,16 +350,17 @@ def build(
291350
Args:
292351
agent_card_url: The URL for the agent card endpoint.
293352
rpc_url: The URL for the A2A JSON-RPC endpoint
353+
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
294354
**kwargs: Additional keyword arguments to pass to the Starlette
295355
constructor.
296356
297357
Returns:
298358
A configured Starlette application instance.
299359
"""
300-
routes = self.routes(agent_card_url, rpc_url)
360+
app_routes = self.routes(agent_card_url, extended_agent_card_url, rpc_url)
301361
if 'routes' in kwargs:
302-
kwargs['routes'] += routes
362+
kwargs['routes'].extend(app_routes)
303363
else:
304-
kwargs['routes'] = routes
364+
kwargs['routes'] = app_routes
305365

306366
return Starlette(**kwargs)

src/a2a/types.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from __future__ import annotations
55

66
from enum import Enum
7-
from typing import Any, Literal
7+
from typing import Any, Literal, Optional
88

9-
from pydantic import BaseModel, RootModel
9+
from pydantic import BaseModel, Field, RootModel
1010

1111

1212
class A2A(RootModel[Any]):
@@ -710,7 +710,10 @@ class AgentCard(BaseModel):
710710
"""
711711
The version of the agent - format is up to the provider.
712712
"""
713-
713+
supportsAuthenticatedExtendedCard: Optional[bool] = Field(default=None)
714+
"""
715+
Optional field indicating there is an extended card available post authentication at the /agent/authenticatedExtendedCard endpoint.
716+
"""
714717

715718
class CancelTaskRequest(BaseModel):
716719
"""

0 commit comments

Comments
 (0)