Skip to content

Commit b5b3401

Browse files
committed
Merge branch 'main' into make-fastapi-package-optional
2 parents 8f5e5a2 + dec4b48 commit b5b3401

File tree

9 files changed

+89
-26
lines changed

9 files changed

+89
-26
lines changed

.github/release-please.yml

Lines changed: 0 additions & 4 deletions
This file was deleted.

.github/release-trigger.yml

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
on:
2+
push:
3+
branches:
4+
- main
5+
6+
permissions:
7+
contents: write
8+
pull-requests: write
9+
10+
name: release-please
11+
12+
jobs:
13+
release-please:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: googleapis/release-please-action@v4
17+
with:
18+
token: ${{ secrets.A2A_BOT_PAT }}
19+
release-type: python

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* **deps:** Make opentelemetry an optional dependency ([#369](https://github.com/a2aproject/a2a-python/issues/369))
99
* **spec:** Update Agent Card Well-Known Path to `/.well-known/agent-card.json` ([#320](https://github.com/a2aproject/a2a-python/issues/320))
1010
* Remove custom `__getattr__` and `__setattr__` for `camelCase` fields in `types.py` ([#335](https://github.com/a2aproject/a2a-python/issues/335))
11+
* Use Script [`refactor_camel_to_snake.sh`](https://github.com/a2aproject/a2a-samples/blob/main/samples/python/refactor_camel_to_snake.sh) to convert your codebase to the new field names.
1112
* Add mTLS to SecuritySchemes, add oauth2 metadata url field, allow Skills to specify Security ([#362](https://github.com/a2aproject/a2a-python/issues/362))
1213
* Support for serving agent card at deprecated path ([#352](https://github.com/a2aproject/a2a-python/issues/352))
1314

src/a2a/client/transports/grpc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async def send_message(
8787
metadata=proto_utils.ToProto.metadata(request.metadata),
8888
)
8989
)
90-
if response.task:
90+
if response.HasField('task'):
9191
return proto_utils.FromProto.task(response.task)
9292
return proto_utils.FromProto.message(response.msg)
9393

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

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import logging
22

3-
from collections.abc import AsyncIterator
4-
from contextlib import asynccontextmanager
53
from typing import TYPE_CHECKING, Any
64

75

@@ -36,6 +34,28 @@
3634
logger = logging.getLogger(__name__)
3735

3836

37+
class A2AFastAPI(FastAPI):
38+
"""A FastAPI application that adds A2A-specific OpenAPI components."""
39+
40+
_a2a_components_added: bool = False
41+
42+
def openapi(self) -> dict[str, Any]:
43+
"""Generates the OpenAPI schema for the application."""
44+
openapi_schema = super().openapi()
45+
if not self._a2a_components_added:
46+
a2a_request_schema = A2ARequest.model_json_schema(
47+
ref_template='#/components/schemas/{model}'
48+
)
49+
defs = a2a_request_schema.pop('$defs', {})
50+
component_schemas = openapi_schema.setdefault(
51+
'components', {}
52+
).setdefault('schemas', {})
53+
component_schemas.update(defs)
54+
component_schemas['A2ARequest'] = a2a_request_schema
55+
self._a2a_components_added = True
56+
return openapi_schema
57+
58+
3959
class A2AFastAPIApplication(JSONRPCApplication):
4060
"""A FastAPI application implementing the A2A protocol server endpoints.
4161
@@ -139,23 +159,7 @@ def build(
139159
Returns:
140160
A configured FastAPI application instance.
141161
"""
142-
143-
@asynccontextmanager
144-
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
145-
a2a_request_schema = A2ARequest.model_json_schema(
146-
ref_template='#/components/schemas/{model}'
147-
)
148-
defs = a2a_request_schema.pop('$defs', {})
149-
openapi_schema = app.openapi()
150-
component_schemas = openapi_schema.setdefault(
151-
'components', {}
152-
).setdefault('schemas', {})
153-
component_schemas.update(defs)
154-
component_schemas['A2ARequest'] = a2a_request_schema
155-
156-
yield
157-
158-
app = FastAPI(lifespan=lifespan, **kwargs)
162+
app = A2AFastAPI(**kwargs)
159163

160164
self.add_routes_to_app(
161165
app, agent_card_url, rpc_url, extended_agent_card_url

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@
2222
_package_starlette_installed = False
2323

2424
from a2a.server.apps.jsonrpc.jsonrpc_app import (
25+
CallContextBuilder,
2526
JSONRPCApplication,
2627
)
28+
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
29+
from a2a.types import AgentCard
2730
from a2a.utils.constants import (
2831
AGENT_CARD_WELL_KNOWN_PATH,
2932
DEFAULT_RPC_URL,

tests/client/test_grpc_client.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
TaskStatus,
1919
TextPart,
2020
)
21-
from a2a.utils import proto_utils
21+
from a2a.utils import get_text_parts, proto_utils
2222

2323

2424
# Fixtures
@@ -112,6 +112,28 @@ async def test_send_message_task_response(
112112
assert response.id == sample_task.id
113113

114114

115+
@pytest.mark.asyncio
116+
async def test_send_message_message_response(
117+
grpc_transport: GrpcTransport,
118+
mock_grpc_stub: AsyncMock,
119+
sample_message_send_params: MessageSendParams,
120+
sample_message: Message,
121+
):
122+
"""Test send_message that returns a Message."""
123+
mock_grpc_stub.SendMessage.return_value = a2a_pb2.SendMessageResponse(
124+
msg=proto_utils.ToProto.message(sample_message)
125+
)
126+
127+
response = await grpc_transport.send_message(sample_message_send_params)
128+
129+
mock_grpc_stub.SendMessage.assert_awaited_once()
130+
assert isinstance(response, Message)
131+
assert response.message_id == sample_message.message_id
132+
assert get_text_parts(response.parts) == get_text_parts(
133+
sample_message.parts
134+
)
135+
136+
115137
@pytest.mark.asyncio
116138
async def test_get_task(
117139
grpc_transport: GrpcTransport, mock_grpc_stub: AsyncMock, sample_task: Task

tests/server/apps/jsonrpc/test_serialization.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from unittest import mock
22

33
import pytest
4+
from fastapi import FastAPI
45

56
from pydantic import ValidationError
67
from starlette.testclient import TestClient
@@ -183,3 +184,21 @@ def test_handle_unicode_characters(agent_card_with_api_key: AgentCard):
183184
data = response.json()
184185
assert 'error' not in data or data['error'] is None
185186
assert data['result']['parts'][0]['text'] == f'Received: {unicode_text}'
187+
188+
189+
def test_fastapi_sub_application(agent_card_with_api_key: AgentCard):
190+
"""
191+
Tests that the A2AFastAPIApplication endpoint correctly passes the url in sub-application.
192+
"""
193+
handler = mock.AsyncMock()
194+
sub_app_instance = A2AFastAPIApplication(agent_card_with_api_key, handler)
195+
app_instance = FastAPI()
196+
app_instance.mount('/a2a', sub_app_instance.build())
197+
client = TestClient(app_instance)
198+
199+
response = client.get('/a2a/openapi.json')
200+
assert response.status_code == 200
201+
response_data = response.json()
202+
203+
assert 'servers' in response_data
204+
assert response_data['servers'] == [{'url': '/a2a'}]

0 commit comments

Comments
 (0)