Skip to content

Commit d633bac

Browse files
committed
feat: Support for serving agent card at deprecated path
1 parent c94d6aa commit d633bac

File tree

7 files changed

+108
-20
lines changed

7 files changed

+108
-20
lines changed

src/a2a/server/apps/jsonrpc/fastapi_app.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
AGENT_CARD_WELL_KNOWN_PATH,
1717
DEFAULT_RPC_URL,
1818
EXTENDED_AGENT_CARD_PATH,
19+
PREV_AGENT_CARD_WELL_KNOWN_PATH,
1920
)
2021

2122

@@ -89,6 +90,12 @@ def add_routes_to_app(
8990
)(self._handle_requests)
9091
app.get(agent_card_url)(self._handle_get_agent_card)
9192

93+
# add deprecated path only if the agent_card_url uses default well-known path
94+
if agent_card_url == AGENT_CARD_WELL_KNOWN_PATH:
95+
app.get(PREV_AGENT_CARD_WELL_KNOWN_PATH, include_in_schema=False)(
96+
self.handle_deprecated_agent_card_path
97+
)
98+
9299
if self.agent_card.supports_authenticated_extended_card:
93100
app.get(extended_agent_card_url)(
94101
self._handle_get_authenticated_extended_agent_card

src/a2a/server/apps/jsonrpc/jsonrpc_app.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,15 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
436436
)
437437
)
438438

439+
async def handle_deprecated_agent_card_path(
440+
self, request: Request
441+
) -> JSONResponse:
442+
"""Handles GET requests for the deprecated agent card endpoint."""
443+
logger.warning(
444+
'Deprecated agent card endpoint accessed. /.well-known/agent.json endpoint will be removed in the future'
445+
)
446+
return await self._handle_get_agent_card(request)
447+
439448
async def _handle_get_authenticated_extended_agent_card(
440449
self, request: Request
441450
) -> JSONResponse:

src/a2a/server/apps/jsonrpc/starlette_app.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
AGENT_CARD_WELL_KNOWN_PATH,
1616
DEFAULT_RPC_URL,
1717
EXTENDED_AGENT_CARD_PATH,
18+
PREV_AGENT_CARD_WELL_KNOWN_PATH,
1819
)
1920

2021

@@ -86,6 +87,17 @@ def routes(
8687
),
8788
]
8889

90+
# add deprecated path only if the agent_card_url uses default well-known path
91+
if agent_card_url == AGENT_CARD_WELL_KNOWN_PATH:
92+
app_routes.append(
93+
Route(
94+
PREV_AGENT_CARD_WELL_KNOWN_PATH,
95+
self.handle_deprecated_agent_card_path,
96+
methods=['GET'],
97+
name='agent_card_path_deprecated',
98+
)
99+
)
100+
89101
if self.agent_card.supports_authenticated_extended_card:
90102
app_routes.append(
91103
Route(

src/a2a/utils/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
AGENT_CARD_WELL_KNOWN_PATH,
1010
DEFAULT_RPC_URL,
1111
EXTENDED_AGENT_CARD_PATH,
12+
PREV_AGENT_CARD_WELL_KNOWN_PATH,
1213
)
1314
from a2a.utils.helpers import (
1415
append_artifact_to_task,
@@ -34,6 +35,7 @@
3435
'AGENT_CARD_WELL_KNOWN_PATH',
3536
'DEFAULT_RPC_URL',
3637
'EXTENDED_AGENT_CARD_PATH',
38+
'PREV_AGENT_CARD_WELL_KNOWN_PATH',
3739
'append_artifact_to_task',
3840
'are_modalities_compatible',
3941
'build_text_artifact',

src/a2a/utils/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Constants for well-known URIs used throughout the A2A Python SDK."""
22

3-
AGENT_CARD_WELL_KNOWN_PATH = '/.well-known/agent.json'
3+
AGENT_CARD_WELL_KNOWN_PATH = '/.well-known/agent-card.json'
4+
PREV_AGENT_CARD_WELL_KNOWN_PATH = '/.well-known/agent.json'
45
EXTENDED_AGENT_CARD_PATH = '/agent/authenticatedExtendedCard'
56
DEFAULT_RPC_URL = '/'

tests/client/test_client.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
TaskPushNotificationConfig,
4949
TaskQueryParams,
5050
)
51-
51+
from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH
5252

5353
AGENT_CARD = AgentCard(
5454
name='Hello World Agent',
@@ -128,7 +128,7 @@ async def async_iterable_from_list(
128128

129129
class TestA2ACardResolver:
130130
BASE_URL = 'http://example.com'
131-
AGENT_CARD_PATH = '/.well-known/agent.json'
131+
AGENT_CARD_PATH = AGENT_CARD_WELL_KNOWN_PATH
132132
FULL_AGENT_CARD_URL = f'{BASE_URL}{AGENT_CARD_PATH}'
133133
EXTENDED_AGENT_CARD_PATH = (
134134
'/agent/authenticatedExtendedCard' # Default path
@@ -154,7 +154,10 @@ async def test_init_parameters_stored_correctly(
154154
httpx_client=mock_httpx_client,
155155
base_url=base_url,
156156
)
157-
assert resolver_default_path.agent_card_path == '.well-known/agent.json'
157+
assert (
158+
'/' + resolver_default_path.agent_card_path
159+
== AGENT_CARD_WELL_KNOWN_PATH
160+
)
158161

159162
@pytest.mark.asyncio
160163
async def test_init_strips_slashes(self, mock_httpx_client: AsyncMock):

tests/server/test_integration.py

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@
4545
UnsupportedOperationError,
4646
)
4747
from a2a.utils.errors import MethodNotImplementedError
48-
48+
from a2a.utils import (
49+
AGENT_CARD_WELL_KNOWN_PATH,
50+
PREV_AGENT_CARD_WELL_KNOWN_PATH,
51+
)
4952

5053
# === TEST SETUP ===
5154

@@ -147,7 +150,7 @@ def client(app: A2AStarletteApplication, **kwargs):
147150

148151
def test_agent_card_endpoint(client: TestClient, agent_card: AgentCard):
149152
"""Test the agent card endpoint returns expected data."""
150-
response = client.get('/.well-known/agent.json')
153+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
151154
assert response.status_code == 200
152155
data = response.json()
153156
assert data['name'] == agent_card.name
@@ -169,6 +172,36 @@ def test_authenticated_extended_agent_card_endpoint_not_supported(
169172
assert response.status_code == 404 # Starlette's default for no route
170173

171174

175+
def test_agent_card_default_endpoint_has_deprecated_route(
176+
agent_card: AgentCard, handler: mock.AsyncMock
177+
):
178+
"""Test agent card deprecated route is available for default route."""
179+
app_instance = A2AStarletteApplication(agent_card, handler)
180+
client = TestClient(app_instance.build())
181+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
182+
assert response.status_code == 200
183+
data = response.json()
184+
assert data['name'] == agent_card.name
185+
response = client.get(PREV_AGENT_CARD_WELL_KNOWN_PATH)
186+
assert response.status_code == 200
187+
data = response.json()
188+
assert data['name'] == agent_card.name
189+
190+
191+
def test_agent_card_custom_endpoint_has_no_deprecated_route(
192+
agent_card: AgentCard, handler: mock.AsyncMock
193+
):
194+
"""Test agent card deprecated route is not available for custom route."""
195+
app_instance = A2AStarletteApplication(agent_card, handler)
196+
client = TestClient(app_instance.build(agent_card_url='/my-agent'))
197+
response = client.get('/my-agent')
198+
assert response.status_code == 200
199+
data = response.json()
200+
assert data['name'] == agent_card.name
201+
response = client.get(PREV_AGENT_CARD_WELL_KNOWN_PATH)
202+
assert response.status_code == 404
203+
204+
172205
def test_authenticated_extended_agent_card_endpoint_not_supported_fastapi(
173206
agent_card: AgentCard, handler: mock.AsyncMock
174207
):
@@ -253,9 +286,7 @@ def test_starlette_rpc_endpoint_custom_url(
253286
"""Test the RPC endpoint with a custom URL."""
254287
# Provide a valid Task object as the return value
255288
task_status = TaskStatus(**MINIMAL_TASK_STATUS)
256-
task = Task(
257-
id='task1', context_id='ctx1', state='completed', status=task_status
258-
)
289+
task = Task(id='task1', context_id='ctx1', status=task_status)
259290
handler.on_get_task.return_value = task
260291
client = TestClient(app.build(rpc_url='/api/rpc'))
261292
response = client.post(
@@ -278,9 +309,7 @@ def test_fastapi_rpc_endpoint_custom_url(
278309
"""Test the RPC endpoint with a custom URL."""
279310
# Provide a valid Task object as the return value
280311
task_status = TaskStatus(**MINIMAL_TASK_STATUS)
281-
task = Task(
282-
id='task1', context_id='ctx1', state='completed', status=task_status
283-
)
312+
task = Task(id='task1', context_id='ctx1', status=task_status)
284313
handler.on_get_task.return_value = task
285314
client = TestClient(app.build(rpc_url='/api/rpc'))
286315
response = client.post(
@@ -315,7 +344,7 @@ def custom_handler(request):
315344
assert response.json() == {'message': 'Hello'}
316345

317346
# Ensure default routes still work
318-
response = client.get('/.well-known/agent.json')
347+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
319348
assert response.status_code == 200
320349
data = response.json()
321350
assert data['name'] == agent_card.name
@@ -339,11 +368,40 @@ def custom_handler(request):
339368
assert response.json() == {'message': 'Hello'}
340369

341370
# Ensure default routes still work
342-
response = client.get('/.well-known/agent.json')
371+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
372+
assert response.status_code == 200
373+
data = response.json()
374+
assert data['name'] == agent_card.name
375+
376+
# check if deprecated agent card path route is available with default well-known path
377+
response = client.get(PREV_AGENT_CARD_WELL_KNOWN_PATH)
378+
assert response.status_code == 200
379+
data = response.json()
380+
assert data['name'] == agent_card.name
381+
382+
383+
def test_fastapi_build_custom_agent_card_path(
384+
app: A2AFastAPIApplication, agent_card: AgentCard
385+
):
386+
"""Test building the app with additional routes."""
387+
388+
test_app = app.build(agent_card_url='/agent-card')
389+
client = TestClient(test_app)
390+
391+
# Ensure custom card path works
392+
response = client.get('/agent-card')
343393
assert response.status_code == 200
344394
data = response.json()
345395
assert data['name'] == agent_card.name
346396

397+
# Ensure default agent card location is not available
398+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
399+
assert response.status_code == 404
400+
401+
# check if deprecated agent card path route is not available
402+
response = client.get(PREV_AGENT_CARD_WELL_KNOWN_PATH)
403+
assert response.status_code == 404
404+
347405

348406
# === REQUEST METHODS TESTS ===
349407

@@ -395,9 +453,7 @@ def test_cancel_task(client: TestClient, handler: mock.AsyncMock):
395453
# Setup mock response
396454
task_status = TaskStatus(**MINIMAL_TASK_STATUS)
397455
task_status.state = TaskState.canceled # 'cancelled' #
398-
task = Task(
399-
id='task1', context_id='ctx1', state='cancelled', status=task_status
400-
)
456+
task = Task(id='task1', context_id='ctx1', status=task_status)
401457
handler.on_cancel_task.return_value = task
402458

403459
# Send request
@@ -425,9 +481,7 @@ def test_get_task(client: TestClient, handler: mock.AsyncMock):
425481
"""Test getting a task."""
426482
# Setup mock response
427483
task_status = TaskStatus(**MINIMAL_TASK_STATUS)
428-
task = Task(
429-
id='task1', context_id='ctx1', state='completed', status=task_status
430-
)
484+
task = Task(id='task1', context_id='ctx1', status=task_status)
431485
handler.on_get_task.return_value = task # JSONRPCResponse(root=task)
432486

433487
# Send request

0 commit comments

Comments
 (0)