Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/spelling/line_forbidden.patterns
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@
(?!'")[‘’“”]

# "an" should only be before vowels.
\ban\s+(?![FHLMNRSX][A-Z0-9]+\b)(?!hour\b)(?!honest\b)([b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z]{1}\w*)
\ban\s+(?![FHLMNRSX][A-Z0-9]+\b)(?!hour\b)(?!honest\b)(?!httpx?\b)([b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z]{1}\w*)

# Don't use Google internal links
((corp|prod|sandbox).google.com|googleplex.com|https?://(?!localhost/)[0-9a-z][0-9a-z-]+/|(?:^|[^/.-])\b(?:go|b|cl|cr)/[a-z0-9_.-]+\b)
Expand Down
2 changes: 1 addition & 1 deletion .github/linters/.mypy.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[mypy]
exclude = examples/
disable_error_code = import-not-found
disable_error_code = import-not-found,annotation-unchecked

[mypy-examples.*]
follow_imports = skip
1 change: 1 addition & 0 deletions src/a2a/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""The A2A Python SDK."""
2 changes: 2 additions & 0 deletions src/a2a/client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Client-side components for interacting with an A2A agent."""

from a2a.client.client import A2ACardResolver, A2AClient
from a2a.client.errors import (
A2AClientError,
Expand Down
151 changes: 147 additions & 4 deletions src/a2a/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,34 @@ def __init__(
base_url: str,
agent_card_path: str = '/.well-known/agent.json',
):
"""Initializes the A2ACardResolver.

Args:
httpx_client: An async HTTP client instance (e.g., httpx.AsyncClient).
base_url: The base URL of the agent's host.
agent_card_path: The path to the agent card endpoint, relative to the base URL.
"""
self.base_url = base_url.rstrip('/')
self.agent_card_path = agent_card_path.lstrip('/')
self.httpx_client = httpx_client

async def get_agent_card(
self, http_kwargs: dict[str, Any] | None = None
) -> AgentCard:
"""Fetches the agent card from the specified URL.

Args:
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.get request.

Returns:
An `AgentCard` object representing the agent's capabilities.

Raises:
A2AClientHTTPError: If an HTTP error occurs during the request.
A2AClientJSONError: If the response body cannot be decoded as JSON
or validated against the AgentCard schema.
"""
try:
response = await self.httpx_client.get(
f'{self.base_url}/{self.agent_card_path}',
Expand All @@ -62,14 +83,26 @@ async def get_agent_card(

@trace_class(kind=SpanKind.CLIENT)
class A2AClient:
"""A2A Client."""
"""A2A Client for interacting with an A2A agent."""

def __init__(
self,
httpx_client: httpx.AsyncClient,
agent_card: AgentCard | None = None,
url: str | None = None,
):
"""Initializes the A2AClient.

Requires either an `AgentCard` or a direct `url` to the agent's RPC endpoint.

Args:
httpx_client: An async HTTP client instance (e.g., httpx.AsyncClient).
agent_card: The agent card object. If provided, `url` is taken from `agent_card.url`.
url: The direct URL to the agent's A2A RPC endpoint. Required if `agent_card` is None.

Raises:
ValueError: If neither `agent_card` nor `url` is provided.
"""
if agent_card:
self.url = agent_card.url
elif url:
Expand All @@ -86,7 +119,22 @@ async def get_client_from_agent_card_url(
agent_card_path: str = '/.well-known/agent.json',
http_kwargs: dict[str, Any] | None = None,
) -> 'A2AClient':
"""Get a A2A client for provided agent card URL."""
"""Fetches the AgentCard and initializes an A2A client.

Args:
httpx_client: An async HTTP client instance (e.g., httpx.AsyncClient).
base_url: The base URL of the agent's host.
agent_card_path: The path to the agent card endpoint, relative to the base URL.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.get request when fetching the agent card.

Returns:
An initialized `A2AClient` instance.

Raises:
A2AClientHTTPError: If an HTTP error occurs fetching the agent card.
A2AClientJSONError: If the agent card response is invalid.
"""
agent_card: AgentCard = await A2ACardResolver(
httpx_client, base_url=base_url, agent_card_path=agent_card_path
).get_agent_card(http_kwargs=http_kwargs)
Expand All @@ -98,6 +146,20 @@ async def send_message(
*,
http_kwargs: dict[str, Any] | None = None,
) -> SendMessageResponse:
"""Sends a non-streaming message request to the agent.

Args:
request: The `SendMessageRequest` object containing the message and configuration.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.post request.

Returns:
A `SendMessageResponse` object containing the agent's response (Task or Message) or an error.

Raises:
A2AClientHTTPError: If an HTTP error occurs during the request.
A2AClientJSONError: If the response body cannot be decoded as JSON or validated.
"""
if not request.id:
request.id = str(uuid4())

Expand All @@ -114,6 +176,23 @@ async def send_message_streaming(
*,
http_kwargs: dict[str, Any] | None = None,
) -> AsyncGenerator[SendStreamingMessageResponse]:
"""Sends a streaming message request to the agent and yields responses as they arrive.

This method uses Server-Sent Events (SSE) to receive a stream of updates from the agent.

Args:
request: The `SendStreamingMessageRequest` object containing the message and configuration.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.post request. A default `timeout=None` is set but can be overridden.

Yields:
`SendStreamingMessageResponse` objects as they are received in the SSE stream.
These can be Task, Message, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent.

Raises:
A2AClientHTTPError: If an HTTP or SSE protocol error occurs during the request.
A2AClientJSONError: If an SSE event data cannot be decoded as JSON or validated.
"""
if not request.id:
request.id = str(uuid4())

Expand Down Expand Up @@ -153,8 +232,16 @@ async def _send_request(
"""Sends a non-streaming JSON-RPC request to the agent.

Args:
rpc_request_payload: JSON RPC payload for sending the request
**kwargs: Additional keyword arguments to pass to the httpx client.
rpc_request_payload: JSON RPC payload for sending the request.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.post request.

Returns:
The JSON response payload as a dictionary.

Raises:
A2AClientHTTPError: If an HTTP error occurs during the request.
A2AClientJSONError: If the response body cannot be decoded as JSON.
"""
try:
response = await self.httpx_client.post(
Expand All @@ -177,6 +264,20 @@ async def get_task(
*,
http_kwargs: dict[str, Any] | None = None,
) -> GetTaskResponse:
"""Retrieves the current state and history of a specific task.

Args:
request: The `GetTaskRequest` object specifying the task ID and history length.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.post request.

Returns:
A `GetTaskResponse` object containing the Task or an error.

Raises:
A2AClientHTTPError: If an HTTP error occurs during the request.
A2AClientJSONError: If the response body cannot be decoded as JSON or validated.
"""
if not request.id:
request.id = str(uuid4())

Expand All @@ -193,6 +294,20 @@ async def cancel_task(
*,
http_kwargs: dict[str, Any] | None = None,
) -> CancelTaskResponse:
"""Requests the agent to cancel a specific task.

Args:
request: The `CancelTaskRequest` object specifying the task ID.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.post request.

Returns:
A `CancelTaskResponse` object containing the updated Task with canceled status or an error.

Raises:
A2AClientHTTPError: If an HTTP error occurs during the request.
A2AClientJSONError: If the response body cannot be decoded as JSON or validated.
"""
if not request.id:
request.id = str(uuid4())

Expand All @@ -209,6 +324,20 @@ async def set_task_callback(
*,
http_kwargs: dict[str, Any] | None = None,
) -> SetTaskPushNotificationConfigResponse:
"""Sets or updates the push notification configuration for a specific task.

Args:
request: The `SetTaskPushNotificationConfigRequest` object specifying the task ID and configuration.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.post request.

Returns:
A `SetTaskPushNotificationConfigResponse` object containing the confirmation or an error.

Raises:
A2AClientHTTPError: If an HTTP error occurs during the request.
A2AClientJSONError: If the response body cannot be decoded as JSON or validated.
"""
if not request.id:
request.id = str(uuid4())

Expand All @@ -225,6 +354,20 @@ async def get_task_callback(
*,
http_kwargs: dict[str, Any] | None = None,
) -> GetTaskPushNotificationConfigResponse:
"""Retrieves the push notification configuration for a specific task.

Args:
request: The `GetTaskPushNotificationConfigRequest` object specifying the task ID.
http_kwargs: Optional dictionary of keyword arguments to pass to the
underlying httpx.post request.

Returns:
A `GetTaskPushNotificationConfigResponse` object containing the configuration or an error.

Raises:
A2AClientHTTPError: If an HTTP error occurs during the request.
A2AClientJSONError: If the response body cannot be decoded as JSON or validated.
"""
if not request.id:
request.id = str(uuid4())

Expand Down
20 changes: 17 additions & 3 deletions src/a2a/client/errors.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
"""Custom exceptions for the A2A client."""


class A2AClientError(Exception):
"""Base exception for client A2A Client errors."""
"""Base exception for A2A Client errors."""


class A2AClientHTTPError(A2AClientError):
"""Client exception for HTTP errors."""
"""Client exception for HTTP errors received from the server."""

def __init__(self, status_code: int, message: str):
"""Initializes the A2AClientHTTPError.

Args:
status_code: The HTTP status code of the response.
message: A descriptive error message.
"""
self.status_code = status_code
self.message = message
super().__init__(f'HTTP Error {status_code}: {message}')


class A2AClientJSONError(A2AClientError):
"""Client exception for JSON errors."""
"""Client exception for JSON errors during response parsing or validation."""

def __init__(self, message: str):
"""Initializes the A2AClientJSONError.

Args:
message: A descriptive error message.
"""
self.message = message
super().__init__(f'JSON Error: {message}')
12 changes: 11 additions & 1 deletion src/a2a/client/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Helper functions for the A2A client."""

from uuid import uuid4

from a2a.types import Message, Part, Role, TextPart
Expand All @@ -6,7 +8,15 @@
def create_text_message_object(
role: Role = Role.user, content: str = ''
) -> Message:
"""Create a Message object for the given role and content."""
"""Create a Message object containing a single TextPart.

Args:
role: The role of the message sender (user or agent). Defaults to Role.user.
content: The text content of the message. Defaults to an empty string.

Returns:
A `Message` object with a new UUID messageId.
"""
return Message(
role=role, parts=[Part(TextPart(text=content))], messageId=str(uuid4())
)
1 change: 1 addition & 0 deletions src/a2a/server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Server-side components for implementing an A2A agent."""
2 changes: 2 additions & 0 deletions src/a2a/server/agent_execution/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Components for executing agent logic within the A2A server."""

from a2a.server.agent_execution.agent_executor import AgentExecutor
from a2a.server.agent_execution.context import RequestContext

Expand Down
30 changes: 27 additions & 3 deletions src/a2a/server/agent_execution/agent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,36 @@


class AgentExecutor(ABC):
"""Agent Executor interface."""
"""Agent Executor interface.

Implementations of this interface contain the core logic of the agent,
executing tasks based on requests and publishing updates to an event queue.
"""

@abstractmethod
async def execute(self, context: RequestContext, event_queue: EventQueue):
pass
"""Execute the agent's logic for a given request context.

The agent should read necessary information from the `context` and
publish `Task` or `Message` events, or `TaskStatusUpdateEvent` /
`TaskArtifactUpdateEvent` to the `event_queue`. This method should
return once the agent's execution for this request is complete or
yields control (e.g., enters an input-required state).

Args:
context: The request context containing the message, task ID, etc.
event_queue: The queue to publish events to.
"""

@abstractmethod
async def cancel(self, context: RequestContext, event_queue: EventQueue):
pass
"""Request the agent to cancel an ongoing task.

The agent should attempt to stop the task identified by the task_id
in the context and publish a `TaskStatusUpdateEvent` with state
`TaskState.canceled` to the `event_queue`.

Args:
context: The request context containing the task ID to cancel.
event_queue: The queue to publish the cancellation status update to.
"""
Loading