Skip to content

Commit e8233d1

Browse files
committed
Merge branch 'main' into make-fastapi-package-optional
2 parents 180898b + dc95e2a commit e8233d1

File tree

24 files changed

+843
-481
lines changed

24 files changed

+843
-481
lines changed

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
- name: Set postgres for tests
3535
run: |
3636
sudo apt-get update && sudo apt-get install -y postgresql-client
37-
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d a2a_test -f ${{ github.workspace }}/docker/postgres/init.sql
37+
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d a2a_test -f ${{ github.workspace }}/tests/docker/postgres/init.sql
3838
export POSTGRES_TEST_DSN="postgresql+asyncpg://postgres:postgres@localhost:5432/a2a_test"
3939
4040
- name: Install uv

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## [0.2.12](https://github.com/a2aproject/a2a-python/compare/v0.2.11...v0.2.12) (2025-07-14)
4+
5+
6+
### Features
7+
8+
* add `metadata` property to `RequestContext` ([#302](https://github.com/a2aproject/a2a-python/issues/302)) ([e781ced](https://github.com/a2aproject/a2a-python/commit/e781ced3b082ef085f9aeef02ceebb9b35c68280))
9+
* add A2ABaseModel ([#292](https://github.com/a2aproject/a2a-python/issues/292)) ([24f2eb0](https://github.com/a2aproject/a2a-python/commit/24f2eb0947112539cbd4e493c98d0d9dadc87f05))
10+
* add support for notification tokens in PushNotificationSender ([#266](https://github.com/a2aproject/a2a-python/issues/266)) ([75aa4ed](https://github.com/a2aproject/a2a-python/commit/75aa4ed866a6b4005e59eb000e965fb593e0888f))
11+
* Update A2A types from specification 🤖 ([#289](https://github.com/a2aproject/a2a-python/issues/289)) ([ecb321a](https://github.com/a2aproject/a2a-python/commit/ecb321a354d691ca90b52cc39e0a397a576fd7d7))
12+
13+
14+
### Bug Fixes
15+
16+
* add proper a2a request body documentation to Swagger UI ([#276](https://github.com/a2aproject/a2a-python/issues/276)) ([4343be9](https://github.com/a2aproject/a2a-python/commit/4343be99ad0df5eb6908867b71d55b1f7d0fafc6)), closes [#274](https://github.com/a2aproject/a2a-python/issues/274)
17+
* Handle asyncio.cancellederror and raise to propagate back ([#293](https://github.com/a2aproject/a2a-python/issues/293)) ([9d6cb68](https://github.com/a2aproject/a2a-python/commit/9d6cb68a1619960b9c9fd8e7aa08ffb27047343f))
18+
* Improve error handling in task creation ([#294](https://github.com/a2aproject/a2a-python/issues/294)) ([6412c75](https://github.com/a2aproject/a2a-python/commit/6412c75413e26489bd3d33f59e41b626a71807d3))
19+
* Resolve dependency issue with sql stores ([#303](https://github.com/a2aproject/a2a-python/issues/303)) ([2126828](https://github.com/a2aproject/a2a-python/commit/2126828b5cb6291f47ca15d56c0e870950f17536))
20+
* Send push notifications for message/send ([#298](https://github.com/a2aproject/a2a-python/issues/298)) ([0274112](https://github.com/a2aproject/a2a-python/commit/0274112bb5b077c17b344da3a65277f2ad67d38f))
21+
* **server:** Improve event consumer error handling ([#282](https://github.com/a2aproject/a2a-python/issues/282)) ([a5786a1](https://github.com/a2aproject/a2a-python/commit/a5786a112779a21819d28e4dfee40fa11f1bb49a))
22+
323
## [0.2.11](https://github.com/a2aproject/a2a-python/compare/v0.2.10...v0.2.11) (2025-07-07)
424

525

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/agent_execution/context.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import uuid
22

3+
from typing import Any
4+
35
from a2a.server.context import ServerCallContext
46
from a2a.types import (
57
InvalidParamsError,
@@ -134,6 +136,13 @@ def call_context(self) -> ServerCallContext | None:
134136
"""The server call context associated with this request."""
135137
return self._call_context
136138

139+
@property
140+
def metadata(self) -> dict[str, Any]:
141+
"""Metadata associated with the request, if available."""
142+
if not self._params:
143+
return {}
144+
return self._params.metadata or {}
145+
137146
def _check_or_generate_task_id(self) -> None:
138147
"""Ensures a task ID is present, generating one if necessary."""
139148
if not self._params:

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/__init__.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Components for managing tasks within the A2A server."""
22

3+
import logging
4+
35
from a2a.server.tasks.base_push_notification_sender import (
46
BasePushNotificationSender,
57
)
6-
from a2a.server.tasks.database_task_store import DatabaseTaskStore
78
from a2a.server.tasks.inmemory_push_notification_config_store import (
89
InMemoryPushNotificationConfigStore,
910
)
@@ -18,6 +19,30 @@
1819
from a2a.server.tasks.task_updater import TaskUpdater
1920

2021

22+
logger = logging.getLogger(__name__)
23+
24+
try:
25+
from a2a.server.tasks.database_task_store import (
26+
DatabaseTaskStore, # type: ignore
27+
)
28+
except ImportError as e:
29+
_original_error = e
30+
# If the database task store is not available, we can still use in-memory stores.
31+
logger.debug(
32+
'DatabaseTaskStore not loaded. This is expected if database dependencies are not installed. Error: %s',
33+
e,
34+
)
35+
36+
class DatabaseTaskStore: # type: ignore
37+
"""Placeholder for DatabaseTaskStore when dependencies are not installed."""
38+
39+
def __init__(self, *args, **kwargs):
40+
raise ImportError(
41+
'To use DatabaseTaskStore, its dependencies must be installed. '
42+
'You can install them with \'pip install "a2a-sdk[sql]"\''
43+
) from _original_error
44+
45+
2146
__all__ = [
2247
'BasePushNotificationSender',
2348
'DatabaseTaskStore',

0 commit comments

Comments
 (0)