Skip to content

Commit 41f3bd3

Browse files
jer96jer
authored andcommitted
feat(a2a): support mounts for containerized deployments (#524)
* feat(a2a): support mounts for containerized deployments * feat(a2a): escape hatch for load balancers which strip paths * feat(a2a): formatting --------- Co-authored-by: jer <jerebill@amazon.com>
1 parent a89fed5 commit 41f3bd3

File tree

3 files changed

+412
-10
lines changed

3 files changed

+412
-10
lines changed

src/strands/multiagent/a2a/server.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import logging
88
from typing import Any, Literal
9+
from urllib.parse import urlparse
910

1011
import uvicorn
1112
from a2a.server.apps import A2AFastAPIApplication, A2AStarletteApplication
@@ -31,6 +32,8 @@ def __init__(
3132
# AgentCard
3233
host: str = "0.0.0.0",
3334
port: int = 9000,
35+
http_url: str | None = None,
36+
serve_at_root: bool = False,
3437
version: str = "0.0.1",
3538
skills: list[AgentSkill] | None = None,
3639
):
@@ -40,13 +43,34 @@ def __init__(
4043
agent: The Strands Agent to wrap with A2A compatibility.
4144
host: The hostname or IP address to bind the A2A server to. Defaults to "0.0.0.0".
4245
port: The port to bind the A2A server to. Defaults to 9000.
46+
http_url: The public HTTP URL where this agent will be accessible. If provided,
47+
this overrides the generated URL from host/port and enables automatic
48+
path-based mounting for load balancer scenarios.
49+
Example: "http://my-alb.amazonaws.com/agent1"
50+
serve_at_root: If True, forces the server to serve at root path regardless of
51+
http_url path component. Use this when your load balancer strips path prefixes.
52+
Defaults to False.
4353
version: The version of the agent. Defaults to "0.0.1".
4454
skills: The list of capabilities or functions the agent can perform.
4555
"""
4656
self.host = host
4757
self.port = port
48-
self.http_url = f"http://{self.host}:{self.port}/"
4958
self.version = version
59+
60+
if http_url:
61+
# Parse the provided URL to extract components for mounting
62+
self.public_base_url, self.mount_path = self._parse_public_url(http_url)
63+
self.http_url = http_url.rstrip("/") + "/"
64+
65+
# Override mount path if serve_at_root is requested
66+
if serve_at_root:
67+
self.mount_path = ""
68+
else:
69+
# Fall back to constructing the URL from host and port
70+
self.public_base_url = f"http://{host}:{port}"
71+
self.http_url = f"{self.public_base_url}/"
72+
self.mount_path = ""
73+
5074
self.strands_agent = agent
5175
self.name = self.strands_agent.name
5276
self.description = self.strands_agent.description
@@ -58,6 +82,25 @@ def __init__(
5882
self._agent_skills = skills
5983
logger.info("Strands' integration with A2A is experimental. Be aware of frequent breaking changes.")
6084

85+
def _parse_public_url(self, url: str) -> tuple[str, str]:
86+
"""Parse the public URL into base URL and mount path components.
87+
88+
Args:
89+
url: The full public URL (e.g., "http://my-alb.amazonaws.com/agent1")
90+
91+
Returns:
92+
tuple: (base_url, mount_path) where base_url is the scheme+netloc
93+
and mount_path is the path component
94+
95+
Example:
96+
_parse_public_url("http://my-alb.amazonaws.com/agent1")
97+
Returns: ("http://my-alb.amazonaws.com", "/agent1")
98+
"""
99+
parsed = urlparse(url.rstrip("/"))
100+
base_url = f"{parsed.scheme}://{parsed.netloc}"
101+
mount_path = parsed.path if parsed.path != "/" else ""
102+
return base_url, mount_path
103+
61104
@property
62105
def public_agent_card(self) -> AgentCard:
63106
"""Get the public AgentCard for this agent.
@@ -119,24 +162,42 @@ def agent_skills(self, skills: list[AgentSkill]) -> None:
119162
def to_starlette_app(self) -> Starlette:
120163
"""Create a Starlette application for serving this agent via HTTP.
121164
122-
This method creates a Starlette application that can be used to serve
123-
the agent via HTTP using the A2A protocol.
165+
Automatically handles path-based mounting if a mount path was derived
166+
from the http_url parameter.
124167
125168
Returns:
126169
Starlette: A Starlette application configured to serve this agent.
127170
"""
128-
return A2AStarletteApplication(agent_card=self.public_agent_card, http_handler=self.request_handler).build()
171+
a2a_app = A2AStarletteApplication(agent_card=self.public_agent_card, http_handler=self.request_handler).build()
172+
173+
if self.mount_path:
174+
# Create parent app and mount the A2A app at the specified path
175+
parent_app = Starlette()
176+
parent_app.mount(self.mount_path, a2a_app)
177+
logger.info("Mounting A2A server at path: %s", self.mount_path)
178+
return parent_app
179+
180+
return a2a_app
129181

130182
def to_fastapi_app(self) -> FastAPI:
131183
"""Create a FastAPI application for serving this agent via HTTP.
132184
133-
This method creates a FastAPI application that can be used to serve
134-
the agent via HTTP using the A2A protocol.
185+
Automatically handles path-based mounting if a mount path was derived
186+
from the http_url parameter.
135187
136188
Returns:
137189
FastAPI: A FastAPI application configured to serve this agent.
138190
"""
139-
return A2AFastAPIApplication(agent_card=self.public_agent_card, http_handler=self.request_handler).build()
191+
a2a_app = A2AFastAPIApplication(agent_card=self.public_agent_card, http_handler=self.request_handler).build()
192+
193+
if self.mount_path:
194+
# Create parent app and mount the A2A app at the specified path
195+
parent_app = FastAPI()
196+
parent_app.mount(self.mount_path, a2a_app)
197+
logger.info("Mounting A2A server at path: %s", self.mount_path)
198+
return parent_app
199+
200+
return a2a_app
140201

141202
def serve(
142203
self,

src/strands/session/repository_session_manager.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,7 @@ def initialize(self, agent: "Agent", **kwargs: Any) -> None:
133133
agent.state = AgentState(session_agent.state)
134134

135135
# Restore the conversation manager to its previous state, and get the optional prepend messages
136-
prepend_messages = agent.conversation_manager.restore_from_session(
137-
session_agent.conversation_manager_state
138-
)
136+
prepend_messages = agent.conversation_manager.restore_from_session(session_agent.conversation_manager_state)
139137

140138
if prepend_messages is None:
141139
prepend_messages = []

0 commit comments

Comments
 (0)