Skip to content

Commit bfc7401

Browse files
authored
Merge branch 'main' into manual-update-spec
2 parents 0d9cf0e + 2444034 commit bfc7401

File tree

8 files changed

+186
-18
lines changed

8 files changed

+186
-18
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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
AGENT_CARD_WELL_KNOWN_PATH,
5353
DEFAULT_RPC_URL,
5454
EXTENDED_AGENT_CARD_PATH,
55+
PREV_AGENT_CARD_WELL_KNOWN_PATH,
5556
)
5657
from a2a.utils.errors import MethodNotImplementedError
5758

@@ -436,6 +437,15 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
436437
)
437438
)
438439

440+
async def handle_deprecated_agent_card_path(
441+
self, request: Request
442+
) -> JSONResponse:
443+
"""Handles GET requests for the deprecated agent card endpoint."""
444+
logger.warning(
445+
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."
446+
)
447+
return await self._handle_get_agent_card(request)
448+
439449
async def _handle_get_authenticated_extended_agent_card(
440450
self, request: Request
441451
) -> 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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
TaskPushNotificationConfig,
4949
TaskQueryParams,
5050
)
51+
from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH
5152

5253

5354
AGENT_CARD = AgentCard(
@@ -128,7 +129,7 @@ async def async_iterable_from_list(
128129

129130
class TestA2ACardResolver:
130131
BASE_URL = 'http://example.com'
131-
AGENT_CARD_PATH = '/.well-known/agent.json'
132+
AGENT_CARD_PATH = AGENT_CARD_WELL_KNOWN_PATH
132133
FULL_AGENT_CARD_URL = f'{BASE_URL}{AGENT_CARD_PATH}'
133134
EXTENDED_AGENT_CARD_PATH = (
134135
'/agent/authenticatedExtendedCard' # Default path
@@ -154,7 +155,10 @@ async def test_init_parameters_stored_correctly(
154155
httpx_client=mock_httpx_client,
155156
base_url=base_url,
156157
)
157-
assert resolver_default_path.agent_card_path == '.well-known/agent.json'
158+
assert (
159+
'/' + resolver_default_path.agent_card_path
160+
== AGENT_CARD_WELL_KNOWN_PATH
161+
)
158162

159163
@pytest.mark.asyncio
160164
async def test_init_strips_slashes(self, mock_httpx_client: AsyncMock):

tests/server/request_handlers/test_default_request_handler.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import logging
23
import time
34

45
from unittest.mock import (
@@ -50,6 +51,9 @@
5051
TextPart,
5152
UnsupportedOperationError,
5253
)
54+
from a2a.utils import (
55+
new_task,
56+
)
5357

5458

5559
class DummyAgentExecutor(AgentExecutor):
@@ -579,6 +583,79 @@ async def test_on_message_send_task_id_mismatch():
579583
assert 'Task ID mismatch' in exc_info.value.error.message # type: ignore
580584

581585

586+
class HelloAgentExecutor(AgentExecutor):
587+
async def execute(self, context: RequestContext, event_queue: EventQueue):
588+
task = context.current_task
589+
if not task:
590+
assert context.message is not None, (
591+
'A message is required to create a new task'
592+
)
593+
task = new_task(context.message) # type: ignore
594+
await event_queue.enqueue_event(task)
595+
updater = TaskUpdater(event_queue, task.id, task.context_id)
596+
597+
try:
598+
parts = [Part(root=TextPart(text='I am working'))]
599+
await updater.update_status(
600+
TaskState.working,
601+
message=updater.new_agent_message(parts),
602+
)
603+
except Exception as e:
604+
# Stop processing when the event loop is closed
605+
logging.warning('Error: %s', e)
606+
return
607+
await updater.add_artifact(
608+
[Part(root=TextPart(text='Hello world!'))],
609+
name='conversion_result',
610+
)
611+
await updater.complete()
612+
613+
async def cancel(self, context: RequestContext, event_queue: EventQueue):
614+
pass
615+
616+
617+
@pytest.mark.asyncio
618+
async def test_on_message_send_non_blocking():
619+
task_store = InMemoryTaskStore()
620+
push_store = InMemoryPushNotificationConfigStore()
621+
622+
request_handler = DefaultRequestHandler(
623+
agent_executor=HelloAgentExecutor(),
624+
task_store=task_store,
625+
push_config_store=push_store,
626+
)
627+
params = MessageSendParams(
628+
message=Message(
629+
role=Role.user,
630+
message_id='msg_push',
631+
parts=[Part(root=TextPart(text='Hi'))],
632+
),
633+
configuration=MessageSendConfiguration(
634+
blocking=False, accepted_output_modes=['text/plain']
635+
),
636+
)
637+
638+
result = await request_handler.on_message_send(
639+
params, create_server_call_context()
640+
)
641+
642+
assert result is not None
643+
assert isinstance(result, Task)
644+
assert result.status.state == TaskState.submitted
645+
646+
# Polling for 500ms until task is completed.
647+
task: Task | None = None
648+
for _ in range(5):
649+
await asyncio.sleep(0.1)
650+
task = await task_store.get(result.id)
651+
assert task is not None
652+
if task.status.state == TaskState.completed:
653+
break
654+
655+
assert task is not None
656+
assert task.status.state == TaskState.completed
657+
658+
582659
@pytest.mark.asyncio
583660
async def test_on_message_send_interrupted_flow():
584661
"""Test on_message_send when flow is interrupted (e.g., auth_required)."""

tests/server/test_integration.py

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
TextPart,
4545
UnsupportedOperationError,
4646
)
47+
from a2a.utils import (
48+
AGENT_CARD_WELL_KNOWN_PATH,
49+
PREV_AGENT_CARD_WELL_KNOWN_PATH,
50+
)
4751
from a2a.utils.errors import MethodNotImplementedError
4852

4953

@@ -147,7 +151,7 @@ def client(app: A2AStarletteApplication, **kwargs):
147151

148152
def test_agent_card_endpoint(client: TestClient, agent_card: AgentCard):
149153
"""Test the agent card endpoint returns expected data."""
150-
response = client.get('/.well-known/agent.json')
154+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
151155
assert response.status_code == 200
152156
data = response.json()
153157
assert data['name'] == agent_card.name
@@ -169,6 +173,36 @@ def test_authenticated_extended_agent_card_endpoint_not_supported(
169173
assert response.status_code == 404 # Starlette's default for no route
170174

171175

176+
def test_agent_card_default_endpoint_has_deprecated_route(
177+
agent_card: AgentCard, handler: mock.AsyncMock
178+
):
179+
"""Test agent card deprecated route is available for default route."""
180+
app_instance = A2AStarletteApplication(agent_card, handler)
181+
client = TestClient(app_instance.build())
182+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
183+
assert response.status_code == 200
184+
data = response.json()
185+
assert data['name'] == agent_card.name
186+
response = client.get(PREV_AGENT_CARD_WELL_KNOWN_PATH)
187+
assert response.status_code == 200
188+
data = response.json()
189+
assert data['name'] == agent_card.name
190+
191+
192+
def test_agent_card_custom_endpoint_has_no_deprecated_route(
193+
agent_card: AgentCard, handler: mock.AsyncMock
194+
):
195+
"""Test agent card deprecated route is not available for custom route."""
196+
app_instance = A2AStarletteApplication(agent_card, handler)
197+
client = TestClient(app_instance.build(agent_card_url='/my-agent'))
198+
response = client.get('/my-agent')
199+
assert response.status_code == 200
200+
data = response.json()
201+
assert data['name'] == agent_card.name
202+
response = client.get(PREV_AGENT_CARD_WELL_KNOWN_PATH)
203+
assert response.status_code == 404
204+
205+
172206
def test_authenticated_extended_agent_card_endpoint_not_supported_fastapi(
173207
agent_card: AgentCard, handler: mock.AsyncMock
174208
):
@@ -253,9 +287,7 @@ def test_starlette_rpc_endpoint_custom_url(
253287
"""Test the RPC endpoint with a custom URL."""
254288
# Provide a valid Task object as the return value
255289
task_status = TaskStatus(**MINIMAL_TASK_STATUS)
256-
task = Task(
257-
id='task1', context_id='ctx1', state='completed', status=task_status
258-
)
290+
task = Task(id='task1', context_id='ctx1', status=task_status)
259291
handler.on_get_task.return_value = task
260292
client = TestClient(app.build(rpc_url='/api/rpc'))
261293
response = client.post(
@@ -278,9 +310,7 @@ def test_fastapi_rpc_endpoint_custom_url(
278310
"""Test the RPC endpoint with a custom URL."""
279311
# Provide a valid Task object as the return value
280312
task_status = TaskStatus(**MINIMAL_TASK_STATUS)
281-
task = Task(
282-
id='task1', context_id='ctx1', state='completed', status=task_status
283-
)
313+
task = Task(id='task1', context_id='ctx1', status=task_status)
284314
handler.on_get_task.return_value = task
285315
client = TestClient(app.build(rpc_url='/api/rpc'))
286316
response = client.post(
@@ -315,7 +345,7 @@ def custom_handler(request):
315345
assert response.json() == {'message': 'Hello'}
316346

317347
# Ensure default routes still work
318-
response = client.get('/.well-known/agent.json')
348+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
319349
assert response.status_code == 200
320350
data = response.json()
321351
assert data['name'] == agent_card.name
@@ -339,11 +369,40 @@ def custom_handler(request):
339369
assert response.json() == {'message': 'Hello'}
340370

341371
# Ensure default routes still work
342-
response = client.get('/.well-known/agent.json')
372+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
343373
assert response.status_code == 200
344374
data = response.json()
345375
assert data['name'] == agent_card.name
346376

377+
# check if deprecated agent card path route is available with default well-known path
378+
response = client.get(PREV_AGENT_CARD_WELL_KNOWN_PATH)
379+
assert response.status_code == 200
380+
data = response.json()
381+
assert data['name'] == agent_card.name
382+
383+
384+
def test_fastapi_build_custom_agent_card_path(
385+
app: A2AFastAPIApplication, agent_card: AgentCard
386+
):
387+
"""Test building the app with a custom agent card path."""
388+
389+
test_app = app.build(agent_card_url='/agent-card')
390+
client = TestClient(test_app)
391+
392+
# Ensure custom card path works
393+
response = client.get('/agent-card')
394+
assert response.status_code == 200
395+
data = response.json()
396+
assert data['name'] == agent_card.name
397+
398+
# Ensure default agent card location is not available
399+
response = client.get(AGENT_CARD_WELL_KNOWN_PATH)
400+
assert response.status_code == 404
401+
402+
# check if deprecated agent card path route is not available
403+
response = client.get(PREV_AGENT_CARD_WELL_KNOWN_PATH)
404+
assert response.status_code == 404
405+
347406

348407
# === REQUEST METHODS TESTS ===
349408

@@ -395,9 +454,7 @@ def test_cancel_task(client: TestClient, handler: mock.AsyncMock):
395454
# Setup mock response
396455
task_status = TaskStatus(**MINIMAL_TASK_STATUS)
397456
task_status.state = TaskState.canceled # 'cancelled' #
398-
task = Task(
399-
id='task1', context_id='ctx1', state='cancelled', status=task_status
400-
)
457+
task = Task(id='task1', context_id='ctx1', status=task_status)
401458
handler.on_cancel_task.return_value = task
402459

403460
# Send request
@@ -425,9 +482,7 @@ def test_get_task(client: TestClient, handler: mock.AsyncMock):
425482
"""Test getting a task."""
426483
# Setup mock response
427484
task_status = TaskStatus(**MINIMAL_TASK_STATUS)
428-
task = Task(
429-
id='task1', context_id='ctx1', state='completed', status=task_status
430-
)
485+
task = Task(id='task1', context_id='ctx1', status=task_status)
431486
handler.on_get_task.return_value = task # JSONRPCResponse(root=task)
432487

433488
# Send request

0 commit comments

Comments
 (0)