diff --git a/docs/examples/a2a_protocol_examples.md b/docs/examples/a2a_protocol_examples.md index 952f84e8..069620d6 100644 --- a/docs/examples/a2a_protocol_examples.md +++ b/docs/examples/a2a_protocol_examples.md @@ -129,7 +129,7 @@ if __name__ == "__main__": ## API Reference -### `serve_a2a(executor, agent_card=None, *, port=9000, host=None, ...)` +### `serve_a2a(executor, agent_card=None, *, port=None, host=None, ...)` Starts a Bedrock-compatible A2A server with `uvicorn`. @@ -137,7 +137,7 @@ Starts a Bedrock-compatible A2A server with `uvicorn`. |-----------|------|---------|-------------| | `executor` | `AgentExecutor` | required | An a2a-sdk `AgentExecutor` that implements the agent logic | | `agent_card` | `AgentCard` | `None` | Agent metadata. Auto-built from executor if omitted (works best with Strands) | -| `port` | `int` | `9000` | Port to serve on | +| `port` | `int \| None` | `None` | Port to serve on. Uses `PORT`, then `9000`, when omitted | | `host` | `str` | `None` | Host to bind to. Auto-detected: `0.0.0.0` in Docker, `127.0.0.1` otherwise | | `task_store` | `TaskStore` | `None` | Custom task store; defaults to `InMemoryTaskStore` | | `context_builder` | `CallContextBuilder` | `None` | Custom context builder; defaults to `BedrockCallContextBuilder` | diff --git a/src/bedrock_agentcore/runtime/a2a.py b/src/bedrock_agentcore/runtime/a2a.py index a8f68f08..4a05d871 100644 --- a/src/bedrock_agentcore/runtime/a2a.py +++ b/src/bedrock_agentcore/runtime/a2a.py @@ -241,6 +241,7 @@ def build_a2a_app( executor: Any, agent_card: Any = None, *, + runtime_url: Optional[str] = None, task_store: Any = None, context_builder: Any = None, ping_handler: Optional[Callable[[], PingStatus]] = None, @@ -251,6 +252,9 @@ def build_a2a_app( executor: An ``AgentExecutor`` that implements the agent logic. agent_card: Optional ``a2a.types.AgentCard`` describing the agent. If ``None``, one is built automatically by introspecting the executor. + runtime_url: URL advertised by an automatically generated agent card. + Defaults to ``http://localhost:9000/``. ``AGENTCORE_RUNTIME_URL`` + takes precedence when set. task_store: Optional ``TaskStore``; defaults to ``InMemoryTaskStore``. context_builder: Optional ``ServerCallContextBuilder``; defaults to ``BedrockCallContextBuilder``. @@ -269,7 +273,7 @@ def build_a2a_app( from starlette.responses import JSONResponse from starlette.routing import Route - runtime_url = os.environ.get(AGENTCORE_RUNTIME_URL_ENV, "http://localhost:9000/") + runtime_url = os.environ.get(AGENTCORE_RUNTIME_URL_ENV, runtime_url or "http://localhost:9000/") is_a2a_v1 = _is_a2a_v1() if agent_card is None: @@ -335,7 +339,7 @@ def serve_a2a( executor: Any, agent_card: Any = None, *, - port: int = 9000, + port: Optional[int] = None, host: Optional[str] = None, task_store: Any = None, context_builder: Any = None, @@ -348,7 +352,8 @@ def serve_a2a( executor: An ``AgentExecutor`` that implements the agent logic. agent_card: Optional ``a2a.types.AgentCard`` describing the agent. If ``None``, one is built automatically by introspecting the executor. - port: Port to serve on (default 9000). + port: Port to serve on. Defaults to the ``PORT`` environment variable, + or 9000 when it is unset. host: Host to bind to; auto-detected if ``None``. task_store: Optional ``TaskStore``; defaults to ``InMemoryTaskStore``. context_builder: Optional ``ServerCallContextBuilder``; defaults to @@ -360,9 +365,12 @@ def serve_a2a( import uvicorn + resolved_port = port if port is not None else int(os.environ.get("PORT", "9000")) + app = build_a2a_app( executor, agent_card, + runtime_url=f"http://localhost:{resolved_port}/", task_store=task_store, context_builder=context_builder, ping_handler=ping_handler, @@ -376,7 +384,7 @@ def serve_a2a( uvicorn_params: dict[str, Any] = { "host": host, - "port": port, + "port": resolved_port, "log_level": "info", } uvicorn_params.update(kwargs) diff --git a/tests/bedrock_agentcore/runtime/test_a2a.py b/tests/bedrock_agentcore/runtime/test_a2a.py index 04c0f6bb..6b804d8f 100644 --- a/tests/bedrock_agentcore/runtime/test_a2a.py +++ b/tests/bedrock_agentcore/runtime/test_a2a.py @@ -447,12 +447,31 @@ class TestServeA2A: @patch("uvicorn.run") def test_default_localhost(self, mock_uvicorn_run): with patch.dict("os.environ", {}, clear=False): + import os + + os.environ.pop("PORT", None) with patch("os.path.exists", return_value=False): serve_a2a(_EchoExecutor(), _make_agent_card()) kw = mock_uvicorn_run.call_args[1] assert kw["host"] == "127.0.0.1" assert kw["port"] == 9000 + @patch("uvicorn.run") + def test_port_from_environment(self, mock_uvicorn_run): + with patch.dict("os.environ", {"PORT": "9001"}, clear=True): + serve_a2a(_EchoExecutor()) + + app = mock_uvicorn_run.call_args.args[0] + response = TestClient(app).get("/.well-known/agent-card.json") + assert mock_uvicorn_run.call_args.kwargs["port"] == 9001 + assert _card_response_url(response.json()) == "http://localhost:9001/" + + @patch("uvicorn.run") + def test_explicit_port_overrides_environment(self, mock_uvicorn_run): + with patch.dict("os.environ", {"PORT": "9001"}): + serve_a2a(_EchoExecutor(), _make_agent_card(), port=8888) + assert mock_uvicorn_run.call_args.kwargs["port"] == 8888 + @patch("uvicorn.run") def test_docker_detection_dockerenv(self, mock_uvicorn_run): with patch("os.path.exists", return_value=True):