diff --git a/src/a2a/server/request_handlers/rest_handler.py b/src/a2a/server/request_handlers/rest_handler.py index b809dcb5b..04d2ebce3 100644 --- a/src/a2a/server/request_handlers/rest_handler.py +++ b/src/a2a/server/request_handlers/rest_handler.py @@ -293,7 +293,7 @@ async def list_tasks( proto_utils.parse_params(request.query_params, params) result = await self.request_handler.on_list_tasks(params, context) - return MessageToDict(result) + return MessageToDict(result, always_print_fields_with_no_presence=True) async def list_push_notifications( self, diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 8f2f09379..7196b828b 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -5,15 +5,26 @@ from datetime import datetime, timezone +import grpc.aio import uvicorn +from starlette.applications import Starlette + +import a2a.compat.v0_3.a2a_v0_3_pb2_grpc as a2a_v0_3_grpc +import a2a.types.a2a_pb2_grpc as a2a_grpc + +from a2a.compat.v0_3.grpc_handler import CompatGrpcHandler from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext -from a2a.server.apps import A2AStarletteApplication +from a2a.server.apps import ( + A2ARESTFastAPIApplication, + A2AStarletteApplication, +) from a2a.server.events.event_queue import EventQueue from a2a.server.request_handlers.default_request_handler import ( DefaultRequestHandler, ) +from a2a.server.request_handlers.grpc_handler import GrpcHandler from a2a.server.tasks.inmemory_task_store import InMemoryTaskStore from a2a.server.tasks.task_store import TaskStore from a2a.types import ( @@ -32,6 +43,7 @@ JSONRPC_URL = '/a2a/jsonrpc' +REST_URL = '/a2a/rest' logging.basicConfig(level=logging.INFO) logger = logging.getLogger('SUTAgent') @@ -133,6 +145,8 @@ def serve(task_store: TaskStore) -> None: """Sets up the A2A service and starts the HTTP server.""" http_port = int(os.environ.get('HTTP_PORT', '41241')) + grpc_port = int(os.environ.get('GRPC_PORT', '50051')) + agent_card = AgentCard( name='SUT Agent', description='An agent to be used as SUT against TCK tests.', @@ -140,7 +154,17 @@ def serve(task_store: TaskStore) -> None: AgentInterface( url=f'http://localhost:{http_port}{JSONRPC_URL}', protocol_binding='JSONRPC', - protocol_version='0.3.0', + protocol_version='1.0.0', + ), + AgentInterface( + url=f'http://localhost:{http_port}{REST_URL}', + protocol_binding='REST', + protocol_version='1.0.0', + ), + AgentInterface( + url=f'http://localhost:{grpc_port}', + protocol_binding='GRPC', + protocol_version='1.0.0', ), ], provider=AgentProvider( @@ -172,15 +196,49 @@ def serve(task_store: TaskStore) -> None: task_store=task_store, ) - server = A2AStarletteApplication( + main_app = Starlette() + + # JSONRPC + jsonrpc_server = A2AStarletteApplication( agent_card=agent_card, http_handler=request_handler, ) + jsonrpc_server.add_routes_to_app(main_app, rpc_url=JSONRPC_URL) - app = server.build(rpc_url=JSONRPC_URL) + # REST + rest_server = A2ARESTFastAPIApplication( + agent_card=agent_card, + http_handler=request_handler, + ) + rest_app = rest_server.build(rpc_url=REST_URL) + main_app.mount('', rest_app) + + config = uvicorn.Config( + main_app, host='127.0.0.1', port=http_port, log_level='info' + ) + uvicorn_server = uvicorn.Server(config) + + # GRPC + grpc_server = grpc.aio.server() + grpc_server.add_insecure_port(f'[::]:{grpc_port}') + servicer = GrpcHandler(agent_card, request_handler) + compat_servicer = CompatGrpcHandler(agent_card, request_handler) + a2a_grpc.add_A2AServiceServicer_to_server(servicer, grpc_server) + a2a_v0_3_grpc.add_A2AServiceServicer_to_server(compat_servicer, grpc_server) + + logger.info( + 'Starting HTTP server on port %s and gRPC on port %s...', + http_port, + grpc_port, + ) - logger.info('Starting HTTP server on port %s...', http_port) - uvicorn.run(app, host='127.0.0.1', port=http_port, log_level='info') + loop = asyncio.get_event_loop() + loop.run_until_complete(grpc_server.start()) + loop.run_until_complete( + asyncio.gather( + uvicorn_server.serve(), grpc_server.wait_for_termination() + ) + ) def main() -> None: