diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 88beeb1c6..33df56a62 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -55,7 +55,7 @@ jobs: run: | echo "$HOME/.cargo/bin" >> $GITHUB_PATH - name: Install dependencies - run: uv sync --dev --extra sql --extra encryption + run: uv sync --dev --extra sql --extra encryption --extra grpc - name: Run tests and check coverage run: uv run pytest --cov=a2a --cov-report=xml --cov-fail-under=89 - name: Show coverage summary in log diff --git a/pyproject.toml b/pyproject.toml index a55f2074b..6d57a796a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,16 +11,11 @@ dependencies = [ "fastapi>=0.115.2", "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", - "starlette", - "grpcio>=1.60", - "grpcio-tools>=1.60", - "grpcio_reflection>=1.7.0", - "protobuf==5.29.5", + "starlette" ] classifiers = [ @@ -42,6 +37,7 @@ mysql = ["sqlalchemy[asyncio,aiomysql]>=2.0.0"] sqlite = ["sqlalchemy[asyncio,aiosqlite]>=2.0.0"] sql = ["sqlalchemy[asyncio,postgresql-asyncpg,aiomysql,aiosqlite]>=2.0.0"] encryption = ["cryptography>=43.0.0"] +grpc = ["grpcio>=1.60", "grpcio-tools>=1.60", "grpcio_reflection>=1.7.0", "protobuf==5.29.5", "google-api-core>=1.26.0"] [project.urls] homepage = "https://a2a-protocol.org/" diff --git a/src/a2a/server/request_handlers/__init__.py b/src/a2a/server/request_handlers/__init__.py index 8cf2fe8ce..6e5603de9 100644 --- a/src/a2a/server/request_handlers/__init__.py +++ b/src/a2a/server/request_handlers/__init__.py @@ -1,9 +1,10 @@ """Request handler components for the A2A server.""" +import logging + from a2a.server.request_handlers.default_request_handler import ( DefaultRequestHandler, ) -from a2a.server.request_handlers.grpc_handler import GrpcHandler from a2a.server.request_handlers.jsonrpc_handler import JSONRPCHandler from a2a.server.request_handlers.request_handler import RequestHandler from a2a.server.request_handlers.response_helpers import ( @@ -12,6 +13,28 @@ ) +logger = logging.getLogger(__name__) + +try: + from a2a.server.request_handlers.grpc_handler import ( + GrpcHandler, # type: ignore + ) +except ImportError as e: + _original_error = e + logger.debug( + 'GrpcHandler not loaded. This is expected if gRPC dependencies are not installed. Error: %s', + ) + + class GrpcHandler: # type: ignore + """Placeholder for GrpcHandler when dependencies are not installed.""" + + def __init__(self, *args, **kwargs): + raise ImportError( + 'To use GrpcHandler, its dependencies must be installed. ' + 'You can install them with \'pip install "a2a-sdk[grpc]"\'' + ) from _original_error + + __all__ = [ 'DefaultRequestHandler', 'GrpcHandler', diff --git a/src/a2a/server/request_handlers/grpc_handler.py b/src/a2a/server/request_handlers/grpc_handler.py index d2323115c..987a0d6df 100644 --- a/src/a2a/server/request_handlers/grpc_handler.py +++ b/src/a2a/server/request_handlers/grpc_handler.py @@ -5,8 +5,16 @@ from abc import ABC, abstractmethod from collections.abc import AsyncIterable -import grpc -import grpc.aio + +try: + import grpc + import grpc.aio +except ImportError as e: + raise ImportError( + 'GrpcHandler requires grpcio and grpcio-tools to be installed. ' + 'Install with: ' + "'pip install a2a-sdk[grpc]'" + ) from e import a2a.grpc.a2a_pb2_grpc as a2a_grpc diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index 0871f05cb..423a83882 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -28,8 +28,8 @@ def message(cls, message: types.Message | None) -> a2a_pb2.Message | None: return a2a_pb2.Message( message_id=message.messageId, content=[ToProto.part(p) for p in message.parts], - context_id=message.contextId, - task_id=message.taskId, + context_id=message.contextId or '', + task_id=message.taskId or '', role=cls.role(message.role), metadata=ToProto.metadata(message.metadata), ) @@ -438,8 +438,8 @@ def message(cls, message: a2a_pb2.Message) -> types.Message: return types.Message( messageId=message.message_id, parts=[FromProto.part(p) for p in message.content], - contextId=message.context_id, - taskId=message.task_id, + contextId=message.context_id or None, + taskId=message.task_id or None, role=FromProto.role(message.role), metadata=FromProto.metadata(message.metadata), ) diff --git a/uv.lock b/uv.lock index 70a6421e3..7243f408d 100644 --- a/uv.lock +++ b/uv.lock @@ -11,15 +11,10 @@ name = "a2a-sdk" source = { editable = "." } dependencies = [ { name = "fastapi" }, - { name = "google-api-core" }, - { name = "grpcio" }, - { name = "grpcio-reflection" }, - { name = "grpcio-tools" }, { name = "httpx" }, { name = "httpx-sse" }, { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, - { name = "protobuf" }, { name = "pydantic" }, { name = "sse-starlette" }, { name = "starlette" }, @@ -29,6 +24,13 @@ dependencies = [ encryption = [ { name = "cryptography" }, ] +grpc = [ + { name = "google-api-core" }, + { name = "grpcio" }, + { name = "grpcio-reflection" }, + { name = "grpcio-tools" }, + { name = "protobuf" }, +] mysql = [ { name = "sqlalchemy", extra = ["aiomysql", "asyncio"] }, ] @@ -62,15 +64,15 @@ dev = [ requires-dist = [ { name = "cryptography", marker = "extra == 'encryption'", specifier = ">=43.0.0" }, { name = "fastapi", specifier = ">=0.115.2" }, - { name = "google-api-core", specifier = ">=1.26.0" }, - { name = "grpcio", specifier = ">=1.60" }, - { name = "grpcio-reflection", specifier = ">=1.7.0" }, - { name = "grpcio-tools", specifier = ">=1.60" }, + { name = "google-api-core", marker = "extra == 'grpc'", specifier = ">=1.26.0" }, + { name = "grpcio", marker = "extra == 'grpc'", specifier = ">=1.60" }, + { name = "grpcio-reflection", marker = "extra == 'grpc'", specifier = ">=1.7.0" }, + { name = "grpcio-tools", marker = "extra == 'grpc'", specifier = ">=1.60" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "httpx-sse", specifier = ">=0.4.0" }, { name = "opentelemetry-api", specifier = ">=1.33.0" }, { name = "opentelemetry-sdk", specifier = ">=1.33.0" }, - { name = "protobuf", specifier = "==5.29.5" }, + { name = "protobuf", marker = "extra == 'grpc'", specifier = "==5.29.5" }, { name = "pydantic", specifier = ">=2.11.3" }, { name = "sqlalchemy", extras = ["aiomysql", "aiosqlite", "asyncio", "postgresql-asyncpg"], marker = "extra == 'sql'", specifier = ">=2.0.0" }, { name = "sqlalchemy", extras = ["aiomysql", "asyncio"], marker = "extra == 'mysql'", specifier = ">=2.0.0" }, @@ -79,7 +81,7 @@ requires-dist = [ { name = "sse-starlette" }, { name = "starlette" }, ] -provides-extras = ["encryption", "mysql", "postgresql", "sql", "sqlite"] +provides-extras = ["encryption", "grpc", "mysql", "postgresql", "sql", "sqlite"] [package.metadata.requires-dev] dev = [