Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d356c0b
feat: Update to support python 3.12
pstephengoogle May 22, 2025
fd0ec5a
Change to 3.10 and provided detailed description about event queue usage
pstephengoogle May 22, 2025
6bd1d44
fix merge conflict
pstephengoogle May 22, 2025
b7964cf
Fix typo
pstephengoogle May 22, 2025
22a6230
Add gRPC based support in the SDK.
pstephengoogle Jun 4, 2025
f81a002
Update pyproject.toml
pstephengoogle Jun 4, 2025
cd752b9
feat: Update to support python 3.12
pstephengoogle May 22, 2025
850ef3c
Change to 3.10 and provided detailed description about event queue usage
pstephengoogle May 22, 2025
85472d9
fix merge conflict
pstephengoogle May 22, 2025
3526a2a
Fix typo
pstephengoogle May 22, 2025
d7e3bce
Add gRPC based support in the SDK.
pstephengoogle Jun 4, 2025
a068140
Update pyproject.toml
pstephengoogle Jun 4, 2025
27b1339
Add spelling fixes/excludes
holtskinner Jun 4, 2025
fccf31e
Merge branch 'main' into updates
holtskinner Jun 4, 2025
693bfc6
Add grpc directory to jscpd ignore
holtskinner Jun 4, 2025
e94a9fc
spelling/linting
holtskinner Jun 4, 2025
feaa218
Exclude grpc/ directory
holtskinner Jun 4, 2025
52117a7
Update JSCPD to ignore `/src/a2a/grpc/**`
holtskinner Jun 4, 2025
50539f4
Add google.api dependencies
pstephengoogle Jun 5, 2025
7cdef10
Merge branch 'updates' of https://github.com/google/a2a-python into u…
pstephengoogle Jun 5, 2025
29ec4d4
Fix lint/typing errors
pstephengoogle Jun 6, 2025
4d72700
Fix more lint errors
pstephengoogle Jun 6, 2025
a427f15
Yet more lint/mypy fixes
pstephengoogle Jun 6, 2025
83fdeaf
Update Linter and nox formatter to exclude grpc/ directory
holtskinner Jun 6, 2025
a19fddb
Fix formatting
holtskinner Jun 6, 2025
7bd25ab
Ignore Docstring error in `proto_utils.py`
holtskinner Jun 6, 2025
c83f329
Lint fixes (ruff `unsafe-fixes`)
holtskinner Jun 6, 2025
feff2d7
Fix ruff errors
pstephengoogle Jun 6, 2025
a44ab73
Additional ruff/mypy fixes
pstephengoogle Jun 6, 2025
434b7fa
Update .ruff.toml to exclude a few rules for a few files
pstephengoogle Jun 6, 2025
2e0c176
More ruff rules
pstephengoogle Jun 6, 2025
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
5 changes: 5 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ ACard
AClient
AError
AFast
AGrpc
ARequest
ARun
AServer
AServers
AService
AStarlette
EUR
GBP
INR
JPY
JSONRPCt
Llm
RUF
aconnect
adk
agentic
aio
autouse
cla
cls
Expand All @@ -34,6 +38,7 @@ linting
oauthoidc
opensource
protoc
pyi
pyversions
socio
sse
Expand Down
3 changes: 1 addition & 2 deletions .github/actions/spelling/excludes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@
\.zip$
^\.github/actions/spelling/
^\.github/workflows/
^\Qsrc/a2a/auth/__init__.py\E$
^\Qsrc/a2a/server/request_handlers/context.py\E$
CHANGELOG.md
noxfile.py
^src/a2a/grpc/
2 changes: 1 addition & 1 deletion .github/linters/.jscpd.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"ignore": ["**/.github/**", "**/.git/**", "**/tests/**"],
"ignore": ["**/.github/**", "**/.git/**", "**/tests/**", "**/src/a2a/grpc/**", "**/.nox/**", "**/.venv/**"],
"threshold": 3,
"reporters": ["html", "markdown"]
}
8 changes: 7 additions & 1 deletion .github/linters/.ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ exclude = [
"venv",
"*/migrations/*",
"noxfile.py",
"src/a2a/grpc/**",
]

[lint.isort]
Expand Down Expand Up @@ -137,9 +138,14 @@ inline-quotes = "single"
"SLF001",
]
"types.py" = ["D", "E501", "N815"] # Ignore docstring and annotation issues in types.py
"proto_utils.py" = ["D102", "PLR0911"]
"helpers.py" = ["ANN001", "ANN201", "ANN202"]

[format]
exclude = ["types.py"]
exclude = [
"types.py",
"src/a2a/grpc/**",
]
docstring-code-format = true
docstring-code-line-length = "dynamic" # Or set to 80
quote-style = "single"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,5 @@ jobs:
VALIDATE_GIT_COMMITLINT: false
PYTHON_MYPY_CONFIG_FILE: .mypy.ini
FILTER_REGEX_INCLUDE: ".*src/**/*"
FILTER_REGEX_EXCLUDE: ".*src/a2a/grpc/**/*"
PYTHON_RUFF_CONFIG_FILE: .ruff.toml
5 changes: 4 additions & 1 deletion buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ managed:
plugins:
# Generate python protobuf related code
# Generates *_pb2.py files, one for each .proto
- remote: buf.build/protocolbuffers/python
- remote: buf.build/protocolbuffers/python:v29.3
out: src/a2a/grpc
# Generate python service code.
# Generates *_pb2_grpc.py
- remote: buf.build/grpc/python
out: src/a2a/grpc
# Generates *_pb2.pyi files.
- remote: buf.build/protocolbuffers/pyi:v29.3
out: src/a2a/grpc
5 changes: 4 additions & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,17 @@ def format(session) -> None:
}
)

lint_paths_py = [f for f in changed_files if f.endswith('.py')]
lint_paths_py = [
f for f in changed_files if f.endswith('.py') and 'grpc/' not in f
]

if not lint_paths_py:
session.log('No changed Python files to lint.')
return

session.install(
'types-requests',
'types-protobuf',
'pyupgrade',
'autoflake',
'ruff',
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ dependencies = [
"fastapi>=0.115.12",
"httpx>=0.28.1",
"httpx-sse>=0.4.0",
"google-api-core>=1.26.0",
"opentelemetry-api>=1.33.0",
"opentelemetry-sdk>=1.33.0",
"pydantic>=2.11.3",
"sse-starlette>=2.3.3",
"starlette>=0.46.2",
"grpcio>=1.60",
"grpcio-tools>=1.60",
"grpcio_reflection>=1.7.0",
]

classifiers = [
Expand Down
2 changes: 2 additions & 0 deletions src/a2a/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
A2AClientHTTPError,
A2AClientJSONError,
)
from a2a.client.grpc_client import A2AGrpcClient
from a2a.client.helpers import create_text_message_object


Expand All @@ -15,5 +16,6 @@
'A2AClientError',
'A2AClientHTTPError',
'A2AClientJSONError',
'A2AGrpcClient',
'create_text_message_object',
]
190 changes: 190 additions & 0 deletions src/a2a/client/grpc_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import logging

from collections.abc import AsyncGenerator

import grpc

from a2a.grpc import a2a_pb2, a2a_pb2_grpc
from a2a.types import (
AgentCard,
Message,
MessageSendParams,
Task,
TaskArtifactUpdateEvent,
TaskIdParams,
TaskPushNotificationConfig,
TaskQueryParams,
TaskStatusUpdateEvent,
)
from a2a.utils import proto_utils
from a2a.utils.telemetry import SpanKind, trace_class


logger = logging.getLogger(__name__)


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

def __init__(
self,
grpc_stub: a2a_pb2_grpc.A2AServiceStub,
agent_card: AgentCard,
):
"""Initializes the A2AGrpcClient.

Requires an `AgentCard`

Args:
grpc_stub: A grpc client stub.
agent_card: The agent card object.
"""
self.agent_card = agent_card
self.stub = grpc_stub

async def send_message(
self,
request: MessageSendParams,
) -> Task | Message:
"""Sends a non-streaming message request to the agent.

Args:
request: The `MessageSendParams` object containing the message and configuration.

Returns:
A `Task` or `Message` object containing the agent's response.
"""
response = await self.stub.SendMessage(
a2a_pb2.SendMessageRequest(
request=proto_utils.ToProto.message(request.message),
configuration=proto_utils.ToProto.message_send_configuration(
request.configuration
),
metadata=proto_utils.ToProto.metadata(request.metadata),
)
)
if response.task:
return proto_utils.FromProto.task(response.task)
return proto_utils.FromProto.message(response.msg)

async def send_message_streaming(
self,
request: MessageSendParams,
) -> AsyncGenerator[
Message | Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent
]:
"""Sends a streaming message request to the agent and yields responses as they arrive.

This method uses gRPC streams to receive a stream of updates from the
agent.

Args:
request: The `MessageSendParams` object containing the message and configuration.

Yields:
`Message` or `Task` or `TaskStatusUpdateEvent` or
`TaskArtifactUpdateEvent` objects as they are received in the
stream.
"""
stream = self.stub.SendStreamingMessage(
a2a_pb2.SendMessageRequest(
request=proto_utils.ToProto.message(request.message),
configuration=proto_utils.ToProto.message_send_configuration(
request.configuration
),
metadata=proto_utils.ToProto.metadata(request.metadata),
)
)
while True:
response = await stream.read()
if response == grpc.aio.EOF:
break
if response.HasField('msg'):
yield proto_utils.FromProto.message(response.msg)
elif response.HasField('task'):
yield proto_utils.FromProto.task(response.task)
elif response.HasField('status_update'):
yield proto_utils.FromProto.task_status_update_event(
response.status_update
)
elif response.HasField('artifact_update'):
yield proto_utils.FromProto.task_artifact_update_event(
response.artifact_update
)

async def get_task(
self,
request: TaskQueryParams,
) -> Task:
"""Retrieves the current state and history of a specific task.

Args:
request: The `TaskQueryParams` object specifying the task ID

Returns:
A `Task` object containing the Task or None.
"""
task = await self.stub.GetTask(
a2a_pb2.GetTaskRequest(name=f'tasks/{request.id}')
)
return proto_utils.FromProto.task(task)

async def cancel_task(
self,
request: TaskIdParams,
) -> Task:
"""Requests the agent to cancel a specific task.

Args:
request: The `TaskIdParams` object specifying the task ID.

Returns:
A `Task` object containing the updated Task
"""
task = await self.stub.CancelTask(
a2a_pb2.CancelTaskRequest(name=f'tasks/{request.id}')
)
return proto_utils.FromProto.task(task)

async def set_task_callback(
self,
request: TaskPushNotificationConfig,
) -> TaskPushNotificationConfig:
"""Sets or updates the push notification configuration for a specific task.

Args:
request: The `TaskPushNotificationConfig` object specifying the task ID and configuration.

Returns:
A `TaskPushNotificationConfig` object containing the config.
"""
config = await self.stub.CreateTaskPushNotification(
a2a_pb2.CreateTaskPushNotificationRequest(
parent='',
config_id='',
config=proto_utils.ToProto.task_push_notification_config(
request
),
)
)
return proto_utils.FromProto.task_push_notification_config(config)

async def get_task_callback(
self,
request: TaskIdParams, # TODO: Update to a push id params
) -> TaskPushNotificationConfig:
"""Retrieves the push notification configuration for a specific task.

Args:
request: The `TaskIdParams` object specifying the task ID.

Returns:
A `TaskPushNotificationConfig` object containing the configuration.
"""
config = await self.stub.GetTaskPushNotification(
a2a_pb2.GetTaskPushNotificationRequest(
name=f'tasks/{request.id}/pushNotification/undefined',
)
)
return proto_utils.FromProto.task_push_notification_config(config)
Empty file added src/a2a/grpc/__init__.py
Empty file.
Loading