Skip to content

Commit 3e85414

Browse files
committed
Merge branch 'main' into make-fastapi-package-optional
2 parents 180898b + 9d12c58 commit 3e85414

16 files changed

+1679
-222
lines changed

scripts/generate_types.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ uv run datamodel-codegen \
3232
--use-one-literal-as-default \
3333
--class-name A2A \
3434
--use-standard-collections \
35-
--use-subclass-enum
35+
--use-subclass-enum \
36+
--base-class a2a._base.A2ABaseModel
3637

3738
echo "Codegen finished successfully."

src/a2a/_base.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from pydantic import BaseModel, ConfigDict
2+
3+
4+
class A2ABaseModel(BaseModel):
5+
"""Base class for shared behavior across A2A data models.
6+
7+
Provides a common configuration (e.g., alias-based population) and
8+
serves as the foundation for future extensions or shared utilities.
9+
"""
10+
11+
model_config = ConfigDict(
12+
# SEE: https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name
13+
validate_by_name=True,
14+
validate_by_alias=True,
15+
)

src/a2a/grpc/a2a_pb2.py

Lines changed: 100 additions & 100 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from a2a.utils.constants import (
4040
AGENT_CARD_WELL_KNOWN_PATH,
4141
DEFAULT_RPC_URL,
42+
EXTENDED_AGENT_CARD_PATH,
4243
)
4344
from a2a.utils.errors import MethodNotImplementedError
4445

@@ -50,8 +51,10 @@
5051
from sse_starlette.sse import EventSourceResponse
5152
from starlette.applications import Starlette
5253
from starlette.authentication import BaseUser
54+
from starlette.exceptions import HTTPException
5355
from starlette.requests import Request
5456
from starlette.responses import JSONResponse, Response
57+
from starlette.status import HTTP_413_REQUEST_ENTITY_TOO_LARGE
5558

5659
_package_starlette_installed = True
5760
else:
@@ -60,8 +63,10 @@
6063
from sse_starlette.sse import EventSourceResponse
6164
from starlette.applications import Starlette
6265
from starlette.authentication import BaseUser
66+
from starlette.exceptions import HTTPException
6367
from starlette.requests import Request
6468
from starlette.responses import JSONResponse, Response
69+
from starlette.status import HTTP_413_REQUEST_ENTITY_TOO_LARGE
6570

6671
_package_starlette_installed = True
6772
except ImportError:
@@ -71,9 +76,11 @@
7176
EventSourceResponse = Any
7277
Starlette = Any
7378
BaseUser = Any
79+
HTTPException = Any
7480
Request = Any
7581
JSONResponse = Any
7682
Response = Any
83+
HTTP_413_REQUEST_ENTITY_TOO_LARGE = Any
7784

7885

7986
class StarletteUserProxy(A2AUser):
@@ -206,7 +213,7 @@ def _generate_error_response(
206213
status_code=200,
207214
)
208215

209-
async def _handle_requests(self, request: Request) -> Response:
216+
async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
210217
"""Handles incoming POST requests to the main A2A endpoint.
211218
212219
Parses the request body as JSON, validates it against A2A request types,
@@ -262,6 +269,15 @@ async def _handle_requests(self, request: Request) -> Response:
262269
request_id,
263270
A2AError(root=InvalidRequestError(data=json.loads(e.json()))),
264271
)
272+
except HTTPException as e:
273+
if e.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE:
274+
return self._generate_error_response(
275+
request_id,
276+
A2AError(
277+
root=InvalidRequestError(message='Payload too large')
278+
),
279+
)
280+
raise e
265281
except Exception as e:
266282
logger.error(f'Unhandled exception: {e}')
267283
traceback.print_exc()
@@ -468,13 +484,16 @@ def build(
468484
self,
469485
agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH,
470486
rpc_url: str = DEFAULT_RPC_URL,
487+
extended_agent_card_url: str = EXTENDED_AGENT_CARD_PATH,
471488
**kwargs: Any,
472489
) -> FastAPI | Starlette:
473490
"""Builds and returns the JSONRPC application instance.
474491
475492
Args:
476493
agent_card_url: The URL for the agent card endpoint.
477-
rpc_url: The URL for the A2A JSON-RPC endpoint
494+
rpc_url: The URL for the A2A JSON-RPC endpoint.
495+
extended_agent_card_url: The URL for the authenticated extended
496+
agent card endpoint.
478497
**kwargs: Additional keyword arguments to pass to the FastAPI constructor.
479498
480499
Returns:

src/a2a/server/events/event_consumer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from collections.abc import AsyncGenerator
66

7+
from pydantic import ValidationError
8+
79
from a2a.server.events.event_queue import Event, EventQueue
810
from a2a.types import (
911
InternalError,
@@ -138,6 +140,9 @@ async def consume_all(self) -> AsyncGenerator[Event]:
138140
# python 3.12 and get a queue empty error on an open queue
139141
if self.queue.is_closed():
140142
break
143+
except ValidationError as e:
144+
logger.error(f'Invalid event format received: {e}')
145+
continue
141146
except Exception as e:
142147
logger.error(
143148
f'Stopping event consumption due to exception: {e}'

src/a2a/server/request_handlers/default_request_handler.py

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@
3333
InvalidParamsError,
3434
ListTaskPushNotificationConfigParams,
3535
Message,
36-
MessageSendConfiguration,
3736
MessageSendParams,
38-
PushNotificationConfig,
3937
Task,
4038
TaskIdParams,
4139
TaskNotFoundError,
@@ -202,18 +200,6 @@ async def _setup_message_execution(
202200
)
203201

204202
task = task_manager.update_with_message(params.message, task)
205-
if self.should_add_push_info(params):
206-
assert self._push_config_store is not None
207-
assert isinstance(
208-
params.configuration, MessageSendConfiguration
209-
)
210-
assert isinstance(
211-
params.configuration.pushNotificationConfig,
212-
PushNotificationConfig,
213-
)
214-
await self._push_config_store.set_info(
215-
task.id, params.configuration.pushNotificationConfig
216-
)
217203

218204
# Build request context
219205
request_context = await self._request_context_builder.build(
@@ -228,6 +214,16 @@ async def _setup_message_execution(
228214
# Always assign a task ID. We may not actually upgrade to a task, but
229215
# dictating the task ID at this layer is useful for tracking running
230216
# agents.
217+
218+
if (
219+
self._push_config_store
220+
and params.configuration
221+
and params.configuration.pushNotificationConfig
222+
):
223+
await self._push_config_store.set_info(
224+
task_id, params.configuration.pushNotificationConfig
225+
)
226+
231227
queue = await self._queue_manager.create_or_tap(task_id)
232228
result_aggregator = ResultAggregator(task_manager)
233229
# TODO: to manage the non-blocking flows.
@@ -333,16 +329,6 @@ async def on_message_send_stream(
333329
if isinstance(event, Task):
334330
self._validate_task_id_match(task_id, event.id)
335331

336-
if (
337-
self._push_config_store
338-
and params.configuration
339-
and params.configuration.pushNotificationConfig
340-
):
341-
await self._push_config_store.set_info(
342-
task_id,
343-
params.configuration.pushNotificationConfig,
344-
)
345-
346332
await self._send_push_notification_if_needed(
347333
task_id, result_aggregator
348334
)
@@ -509,11 +495,3 @@ async def on_delete_task_push_notification_config(
509495
await self._push_config_store.delete_info(
510496
params.id, params.pushNotificationConfigId
511497
)
512-
513-
def should_add_push_info(self, params: MessageSendParams) -> bool:
514-
"""Determines if push notification info should be set for a task."""
515-
return bool(
516-
self._push_config_store
517-
and params.configuration
518-
and params.configuration.pushNotificationConfig
519-
)

src/a2a/server/tasks/base_push_notification_sender.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,13 @@ async def _dispatch_notification(
5252
) -> bool:
5353
url = push_info.url
5454
try:
55+
headers = None
56+
if push_info.token:
57+
headers = {'X-A2A-Notification-Token': push_info.token}
5558
response = await self._client.post(
56-
url, json=task.model_dump(mode='json', exclude_none=True)
59+
url,
60+
json=task.model_dump(mode='json', exclude_none=True),
61+
headers=headers
5762
)
5863
response.raise_for_status()
5964
logger.info(

0 commit comments

Comments
 (0)