Skip to content

Commit df9c198

Browse files
Merge branch 'main' into sdk_buf
2 parents 667825a + 0e66e1f commit df9c198

File tree

10 files changed

+839
-542
lines changed

10 files changed

+839
-542
lines changed

.github/actions/spelling/allow.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
ACard
22
AClient
33
AError
4+
AFast
45
ARequest
56
ARun
67
AServer

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ authors = [{ name = "Google LLC", email = "[email protected]" }]
88
requires-python = ">=3.10"
99
keywords = ["A2A", "A2A SDK", "A2A Protocol", "Agent2Agent"]
1010
dependencies = [
11+
"fastapi>=0.115.12",
1112
"httpx>=0.28.1",
1213
"httpx-sse>=0.4.0",
1314
"opentelemetry-api>=1.33.0",

src/a2a/server/apps/__init__.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
"""HTTP application components for the A2A server."""
22

3-
from a2a.server.apps.starlette_app import A2AStarletteApplication
3+
from .jsonrpc import (
4+
A2AFastAPIApplication,
5+
A2AStarletteApplication,
6+
CallContextBuilder,
7+
JSONRPCApplication,
8+
)
49

510

6-
__all__ = ['A2AStarletteApplication']
11+
__all__ = [
12+
'A2AFastAPIApplication',
13+
'A2AStarletteApplication',
14+
'CallContextBuilder',
15+
'JSONRPCApplication',
16+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""A2A JSON-RPC Applications."""
2+
3+
from .fastapi_app import A2AFastAPIApplication
4+
from .jsonrpc_app import CallContextBuilder, JSONRPCApplication
5+
from .starlette_app import A2AStarletteApplication
6+
7+
8+
__all__ = [
9+
'A2AFastAPIApplication',
10+
'A2AStarletteApplication',
11+
'CallContextBuilder',
12+
'JSONRPCApplication',
13+
]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import logging
2+
3+
from typing import Any
4+
5+
from fastapi import FastAPI, Request
6+
7+
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
8+
from a2a.types import AgentCard
9+
10+
from .jsonrpc_app import CallContextBuilder, JSONRPCApplication
11+
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class A2AFastAPIApplication(JSONRPCApplication):
17+
"""A FastAPI application implementing the A2A protocol server endpoints.
18+
19+
Handles incoming JSON-RPC requests, routes them to the appropriate
20+
handler methods, and manages response generation including Server-Sent Events
21+
(SSE).
22+
"""
23+
24+
def __init__(
25+
self,
26+
agent_card: AgentCard,
27+
http_handler: RequestHandler,
28+
extended_agent_card: AgentCard | None = None,
29+
context_builder: CallContextBuilder | None = None,
30+
):
31+
"""Initializes the A2AStarletteApplication.
32+
33+
Args:
34+
agent_card: The AgentCard describing the agent's capabilities.
35+
http_handler: The handler instance responsible for processing A2A
36+
requests via http.
37+
extended_agent_card: An optional, distinct AgentCard to be served
38+
at the authenticated extended card endpoint.
39+
context_builder: The CallContextBuilder used to construct the
40+
ServerCallContext passed to the http_handler. If None, no
41+
ServerCallContext is passed.
42+
"""
43+
super().__init__(
44+
agent_card=agent_card,
45+
http_handler=http_handler,
46+
extended_agent_card=extended_agent_card,
47+
context_builder=context_builder,
48+
)
49+
50+
def build(
51+
self,
52+
agent_card_url: str = '/.well-known/agent.json',
53+
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
54+
rpc_url: str = '/',
55+
**kwargs: Any,
56+
) -> FastAPI:
57+
"""Builds and returns the FastAPI application instance.
58+
59+
Args:
60+
agent_card_url: The URL for the agent card endpoint.
61+
rpc_url: The URL for the A2A JSON-RPC endpoint.
62+
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
63+
**kwargs: Additional keyword arguments to pass to the FastAPI constructor.
64+
65+
Returns:
66+
A configured FastAPI application instance.
67+
"""
68+
app = FastAPI(**kwargs)
69+
70+
@app.post(rpc_url)
71+
async def handle_a2a_request(request: Request):
72+
return await self._handle_requests(request)
73+
74+
@app.get(agent_card_url)
75+
async def get_agent_card(request: Request):
76+
return await self._handle_get_agent_card(request)
77+
78+
if self.agent_card.supportsAuthenticatedExtendedCard:
79+
80+
@app.get(extended_agent_card_url)
81+
async def get_extended_agent_card(request: Request):
82+
return await self._handle_get_authenticated_extended_agent_card(
83+
request
84+
)
85+
86+
return app

src/a2a/server/apps/starlette_app.py renamed to src/a2a/server/apps/jsonrpc/jsonrpc_app.py

Lines changed: 12 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from collections.abc import AsyncGenerator
88
from typing import Any
99

10+
from fastapi import FastAPI
1011
from pydantic import ValidationError
1112
from sse_starlette.sse import EventSourceResponse
1213
from starlette.applications import Starlette
1314
from starlette.authentication import BaseUser
1415
from starlette.requests import Request
1516
from starlette.responses import JSONResponse, Response
16-
from starlette.routing import Route
1717

1818
from a2a.auth.user import UnauthenticatedUser
1919
from a2a.auth.user import User as A2AUser
@@ -81,8 +81,8 @@ def build(self, request: Request) -> ServerCallContext:
8181
return ServerCallContext(user=user, state=state)
8282

8383

84-
class A2AStarletteApplication:
85-
"""A Starlette application implementing the A2A protocol server endpoints.
84+
class JSONRPCApplication(ABC):
85+
"""Base class for A2A JSONRPC applications.
8686
8787
Handles incoming JSON-RPC requests, routes them to the appropriate
8888
handler methods, and manages response generation including Server-Sent Events
@@ -391,73 +391,23 @@ async def _handle_get_authenticated_extended_agent_card(
391391
status_code=404,
392392
)
393393

394-
def routes(
395-
self,
396-
agent_card_url: str = '/.well-known/agent.json',
397-
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
398-
rpc_url: str = '/',
399-
) -> list[Route]:
400-
"""Returns the Starlette Routes for handling A2A requests.
401-
402-
Args:
403-
agent_card_url: The URL path for the agent card endpoint.
404-
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
405-
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
406-
407-
Returns:
408-
A list of Starlette Route objects.
409-
"""
410-
app_routes = [
411-
Route(
412-
rpc_url,
413-
self._handle_requests,
414-
methods=['POST'],
415-
name='a2a_handler',
416-
),
417-
Route(
418-
agent_card_url,
419-
self._handle_get_agent_card,
420-
methods=['GET'],
421-
name='agent_card',
422-
),
423-
]
424-
425-
if self.agent_card.supportsAuthenticatedExtendedCard:
426-
app_routes.append(
427-
Route(
428-
extended_agent_card_url,
429-
self._handle_get_authenticated_extended_agent_card,
430-
methods=['GET'],
431-
name='authenticated_extended_agent_card',
432-
)
433-
)
434-
return app_routes
435-
394+
@abstractmethod
436395
def build(
437396
self,
438397
agent_card_url: str = '/.well-known/agent.json',
439-
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
440398
rpc_url: str = '/',
441399
**kwargs: Any,
442-
) -> Starlette:
443-
"""Builds and returns the Starlette application instance.
400+
) -> FastAPI | Starlette:
401+
"""Builds and returns the JSONRPC application instance.
444402
445403
Args:
446-
agent_card_url: The URL path for the agent card endpoint.
447-
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
448-
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
449-
**kwargs: Additional keyword arguments to pass to the Starlette
450-
constructor.
404+
agent_card_url: The URL for the agent card endpoint.
405+
rpc_url: The URL for the A2A JSON-RPC endpoint
406+
**kwargs: Additional keyword arguments to pass to the FastAPI constructor.
451407
452408
Returns:
453-
A configured Starlette application instance.
409+
A configured JSONRPC application instance.
454410
"""
455-
app_routes = self.routes(
456-
agent_card_url, extended_agent_card_url, rpc_url
411+
raise NotImplementedError(
412+
'Subclasses must implement the build method to create the application instance.'
457413
)
458-
if 'routes' in kwargs:
459-
kwargs['routes'].extend(app_routes)
460-
else:
461-
kwargs['routes'] = app_routes
462-
463-
return Starlette(**kwargs)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import logging
2+
3+
from typing import Any
4+
5+
from starlette.applications import Starlette
6+
from starlette.routing import Route
7+
8+
from a2a.server.request_handlers.jsonrpc_handler import RequestHandler
9+
from a2a.types import AgentCard
10+
11+
from .jsonrpc_app import CallContextBuilder, JSONRPCApplication
12+
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
class A2AStarletteApplication(JSONRPCApplication):
18+
"""A Starlette application implementing the A2A protocol server endpoints.
19+
20+
Handles incoming JSON-RPC requests, routes them to the appropriate
21+
handler methods, and manages response generation including Server-Sent Events
22+
(SSE).
23+
"""
24+
25+
def __init__(
26+
self,
27+
agent_card: AgentCard,
28+
http_handler: RequestHandler,
29+
extended_agent_card: AgentCard | None = None,
30+
context_builder: CallContextBuilder | None = None,
31+
):
32+
"""Initializes the A2AStarletteApplication.
33+
34+
Args:
35+
agent_card: The AgentCard describing the agent's capabilities.
36+
http_handler: The handler instance responsible for processing A2A
37+
requests via http.
38+
extended_agent_card: An optional, distinct AgentCard to be served
39+
at the authenticated extended card endpoint.
40+
context_builder: The CallContextBuilder used to construct the
41+
ServerCallContext passed to the http_handler. If None, no
42+
ServerCallContext is passed.
43+
"""
44+
super().__init__(
45+
agent_card=agent_card,
46+
http_handler=http_handler,
47+
extended_agent_card=extended_agent_card,
48+
context_builder=context_builder,
49+
)
50+
51+
def routes(
52+
self,
53+
agent_card_url: str = '/.well-known/agent.json',
54+
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
55+
rpc_url: str = '/',
56+
) -> list[Route]:
57+
"""Returns the Starlette Routes for handling A2A requests.
58+
59+
Args:
60+
agent_card_url: The URL path for the agent card endpoint.
61+
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
62+
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
63+
64+
Returns:
65+
A list of Starlette Route objects.
66+
"""
67+
app_routes = [
68+
Route(
69+
rpc_url,
70+
self._handle_requests,
71+
methods=['POST'],
72+
name='a2a_handler',
73+
),
74+
Route(
75+
agent_card_url,
76+
self._handle_get_agent_card,
77+
methods=['GET'],
78+
name='agent_card',
79+
),
80+
]
81+
82+
if self.agent_card.supportsAuthenticatedExtendedCard:
83+
app_routes.append(
84+
Route(
85+
extended_agent_card_url,
86+
self._handle_get_authenticated_extended_agent_card,
87+
methods=['GET'],
88+
name='authenticated_extended_agent_card',
89+
)
90+
)
91+
return app_routes
92+
93+
def build(
94+
self,
95+
agent_card_url: str = '/.well-known/agent.json',
96+
extended_agent_card_url: str = '/agent/authenticatedExtendedCard',
97+
rpc_url: str = '/',
98+
**kwargs: Any,
99+
) -> Starlette:
100+
"""Builds and returns the Starlette application instance.
101+
102+
Args:
103+
agent_card_url: The URL path for the agent card endpoint.
104+
rpc_url: The URL path for the A2A JSON-RPC endpoint (POST requests).
105+
extended_agent_card_url: The URL for the authenticated extended agent card endpoint.
106+
**kwargs: Additional keyword arguments to pass to the Starlette
107+
constructor.
108+
109+
Returns:
110+
A configured Starlette application instance.
111+
"""
112+
app_routes = self.routes(
113+
agent_card_url, extended_agent_card_url, rpc_url
114+
)
115+
if 'routes' in kwargs:
116+
kwargs['routes'].extend(app_routes)
117+
else:
118+
kwargs['routes'] = app_routes
119+
120+
return Starlette(**kwargs)

src/a2a/server/tasks/task_updater.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async def update_status(
6868
async def add_artifact(
6969
self,
7070
parts: list[Part],
71-
artifact_id: str = str(uuid.uuid4()),
71+
artifact_id: str | None = None,
7272
name: str | None = None,
7373
metadata: dict[str, Any] | None = None,
7474
):
@@ -82,6 +82,9 @@ async def add_artifact(
8282
append: Optional boolean indicating if this chunk appends to a previous one.
8383
last_chunk: Optional boolean indicating if this is the last chunk.
8484
"""
85+
if not artifact_id:
86+
artifact_id = str(uuid.uuid4())
87+
8588
await self.event_queue.enqueue_event(
8689
TaskArtifactUpdateEvent(
8790
taskId=self.task_id,

0 commit comments

Comments
 (0)