Skip to content

Commit c4eba81

Browse files
authored
feat: add A2A Card to Agent builder (#2165)
* feat: add A2A Card to Agent builder * docs: add from_card section
1 parent 150d777 commit c4eba81

File tree

3 files changed

+113
-24
lines changed

3 files changed

+113
-24
lines changed

autogen/a2a/client.py

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from a2a.client import ClientFactory as A2AClientFactory
1414
from a2a.types import AgentCard, Message, Task, TaskIdParams, TaskQueryParams, TaskState
1515
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH, EXTENDED_AGENT_CARD_PATH, PREV_AGENT_CARD_WELL_KNOWN_PATH
16+
from typing_extensions import Self
1617

1718
from autogen import ConversableAgent
1819
from autogen.agentchat.group import ContextVariables
@@ -55,9 +56,14 @@ def __init__(
5556
max_reconnects: int = 3,
5657
polling_interval: float = 0.5,
5758
) -> None:
58-
self.url = url
59+
self.url = url # make it public for backward compatibility
5960

6061
self._httpx_client_factory = client or EmptyClientFactory()
62+
self._card_resolver = A2ACardResolver(
63+
httpx_client=self._httpx_client_factory(),
64+
base_url=url,
65+
)
66+
6167
self._max_reconnects = max_reconnects
6268
self._polling_interval = polling_interval
6369

@@ -66,7 +72,7 @@ def __init__(
6672
self.__llm_config: dict[str, Any] = {}
6773

6874
self._client_config = client_config or ClientConfig()
69-
self.__agent_card: AgentCard | None = None
75+
self._agent_card: AgentCard | None = None
7076

7177
self.replace_reply_func(
7278
ConversableAgent.generate_oai_reply,
@@ -77,6 +83,48 @@ def __init__(
7783
A2aRemoteAgent.a_generate_remote_reply,
7884
)
7985

86+
@classmethod
87+
def from_card(
88+
cls,
89+
card: AgentCard,
90+
*,
91+
silent: bool | None = None,
92+
client: ClientFactory | None = None,
93+
client_config: ClientConfig | None = None,
94+
max_reconnects: int = 3,
95+
polling_interval: float = 0.5,
96+
) -> Self:
97+
"""Creates an A2aRemoteAgent instance from an existing AgentCard.
98+
99+
This method allows you to instantiate an A2aRemoteAgent directly using a pre-existing
100+
AgentCard, such as one retrieved from a discovery service or constructed manually.
101+
The resulting agent will use the data from the given card and avoid redundant card
102+
fetching. The agent's registryURL is set to "UNKNOWN" since it is assumed to be derived
103+
from the card.
104+
105+
Args:
106+
card: The agent card containing metadata and configuration for the remote agent.
107+
silent: whether to print the message sent. If None, will use the value of silent in each function.
108+
client: An optional HTTPX client instance factory.
109+
client_config: A2A Client configuration options.
110+
max_reconnects: Maximum number of reconnection attempts before giving up.
111+
polling_interval: Time in seconds between polling operations. Works for A2A Servers doesn't support streaming.
112+
113+
Returns:
114+
Self: An instance of the A2aRemoteAgent configured with the provided card.
115+
"""
116+
instance = cls(
117+
url="UNKNOWN",
118+
name=card.name,
119+
silent=silent,
120+
client=client,
121+
client_config=client_config,
122+
max_reconnects=max_reconnects,
123+
polling_interval=polling_interval,
124+
)
125+
instance._agent_card = card
126+
return instance
127+
80128
def generate_remote_reply(
81129
self,
82130
messages: list[dict[str, Any]] | None = None,
@@ -94,8 +142,8 @@ async def a_generate_remote_reply(
94142
if messages is None:
95143
messages = self._oai_messages[sender]
96144

97-
if not self.__agent_card:
98-
self.__agent_card = await self._get_agent_card()
145+
if not self._agent_card:
146+
self._agent_card = await self._get_agent_card()
99147

100148
initial_message = request_message_to_a2a(
101149
request_message=RequestMessage(
@@ -108,9 +156,9 @@ async def a_generate_remote_reply(
108156

109157
self._client_config.httpx_client = self._httpx_client_factory()
110158
async with self._client_config.httpx_client:
111-
agent_client = A2AClientFactory(self._client_config).create(self.__agent_card)
159+
agent_client = A2AClientFactory(self._client_config).create(self._agent_card)
112160

113-
if self.__agent_card.capabilities.streaming:
161+
if self._agent_card.capabilities.streaming:
114162
reply = await self._ask_streaming(agent_client, initial_message)
115163
return self._apply_reply(reply, sender)
116164

@@ -142,9 +190,9 @@ async def _ask_streaming(self, client: Client, message: Message) -> ResponseMess
142190
if task and connection_attemps < self._max_reconnects:
143191
pass
144192

145-
if not self.__agent_card:
193+
if not self._agent_card:
146194
raise A2aClientError("Failed to connect to the agent: agent card not found") from e
147-
raise A2aClientError(f"Failed to connect to the agent: {pformat(self.__agent_card.model_dump())}") from e
195+
raise A2aClientError(f"Failed to connect to the agent: {pformat(self._agent_card.model_dump())}") from e
148196

149197
task = cast(Task, task)
150198
while connection_attemps < self._max_reconnects:
@@ -159,11 +207,9 @@ async def _ask_streaming(self, client: Client, message: Message) -> ResponseMess
159207
if connection_attemps < self._max_reconnects:
160208
pass
161209

162-
if not self.__agent_card:
210+
if not self._agent_card:
163211
raise A2aClientError("Failed to connect to the agent: agent card not found") from e
164-
raise A2aClientError(
165-
f"Failed to connect to the agent: {pformat(self.__agent_card.model_dump())}"
166-
) from e
212+
raise A2aClientError(f"Failed to connect to the agent: {pformat(self._agent_card.model_dump())}") from e
167213

168214
return None
169215

@@ -175,9 +221,9 @@ async def _ask_polling(self, client: Client, message: Message) -> ResponseMessag
175221
return result
176222
break
177223
except httpx.ConnectError as e:
178-
if not self.__agent_card:
224+
if not self._agent_card:
179225
raise A2aClientError("Failed to connect to the agent: agent card not found") from e
180-
raise A2aClientError(f"Failed to connect to the agent: {pformat(self.__agent_card.model_dump())}") from e
226+
raise A2aClientError(f"Failed to connect to the agent: {pformat(self._agent_card.model_dump())}") from e
181227

182228
started_task, connection_attemps = cast(Task, started_task), 0
183229
while connection_attemps < self._max_reconnects:
@@ -190,10 +236,10 @@ async def _ask_polling(self, client: Client, message: Message) -> ResponseMessag
190236
if connection_attemps < self._max_reconnects:
191237
pass
192238

193-
if not self.__agent_card:
239+
if not self._agent_card:
194240
raise A2aClientError("Failed to connect to the agent: agent card not found") from e
195241
raise A2aClientError(
196-
f"Failed to connect to the agent: {pformat(self.__agent_card.model_dump())}"
242+
f"Failed to connect to the agent: {pformat(self._agent_card.model_dump())}"
197243
) from e
198244

199245
else:
@@ -231,27 +277,27 @@ async def _get_agent_card(
231277
self,
232278
auth_http_kwargs: dict[str, Any] | None = None,
233279
) -> AgentCard:
234-
resolver = A2ACardResolver(httpx_client=self._httpx_client_factory(), base_url=self.url)
235-
236280
card: AgentCard | None = None
237281

238282
try:
239-
logger.info(f"Attempting to fetch public agent card from: {self.url}{AGENT_CARD_WELL_KNOWN_PATH}")
283+
logger.info(
284+
f"Attempting to fetch public agent card from: {self._card_resolver.base_url}{AGENT_CARD_WELL_KNOWN_PATH}"
285+
)
240286

241287
try:
242-
card = await resolver.get_agent_card(relative_card_path=AGENT_CARD_WELL_KNOWN_PATH)
288+
card = await self._card_resolver.get_agent_card(relative_card_path=AGENT_CARD_WELL_KNOWN_PATH)
243289
except A2AClientHTTPError as e_public:
244290
if e_public.status_code == 404:
245291
logger.info(
246-
f"Attempting to fetch public agent card from: {self.url}{PREV_AGENT_CARD_WELL_KNOWN_PATH}"
292+
f"Attempting to fetch public agent card from: {self._card_resolver.base_url}{PREV_AGENT_CARD_WELL_KNOWN_PATH}"
247293
)
248-
card = await resolver.get_agent_card(relative_card_path=PREV_AGENT_CARD_WELL_KNOWN_PATH)
294+
card = await self._card_resolver.get_agent_card(relative_card_path=PREV_AGENT_CARD_WELL_KNOWN_PATH)
249295
else:
250296
raise e_public
251297

252298
if card.supports_authenticated_extended_card:
253299
try:
254-
card = await resolver.get_agent_card(
300+
card = await self._card_resolver.get_agent_card(
255301
relative_card_path=EXTENDED_AGENT_CARD_PATH,
256302
http_kwargs=auth_http_kwargs,
257303
)

test/a2a/test_client.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from unittest.mock import AsyncMock
77

88
import pytest
9-
from a2a.types import DataPart, TextPart
9+
from a2a.types import AgentCapabilities, AgentCard, DataPart, TextPart # type: ignore
1010

1111
from autogen import ConversableAgent
1212
from autogen.a2a import A2aRemoteAgent, MockClient
@@ -110,3 +110,22 @@ async def test_answer_with_dict(data: dict[str, Any] | DataPart) -> None:
110110
"role": "assistant",
111111
"name": "test-agent",
112112
}
113+
114+
115+
def test_build_agent_from_card() -> None:
116+
card = AgentCard(
117+
name="Test Agent",
118+
description="A test agent",
119+
url="http://test.example.com",
120+
version="0.1.0",
121+
default_input_modes=["text"],
122+
default_output_modes=["text"],
123+
capabilities=AgentCapabilities(streaming=False),
124+
skills=[],
125+
supports_authenticated_extended_card=False,
126+
)
127+
agent = A2aRemoteAgent.from_card(card)
128+
129+
assert agent.name == "Test Agent"
130+
assert agent.url == "UNKNOWN"
131+
assert agent._agent_card == card

website/docs/user-guide/a2a/client.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,30 @@ async def review_code():
7171
asyncio.run(review_code())
7272
```
7373

74+
### Creating from AgentCard
75+
76+
If you already have an `AgentCard` (e.g., fetched from a discovery service or another source), you can create an `A2aRemoteAgent` instance directly from it using the `from_card` classmethod. This avoids redundant fetching of the agent card and allows you to use pre-validated card information.
77+
78+
```python linenums="1"
79+
from a2a.types import AgentCard, AgentCapabilities
80+
81+
# Create or fetch an AgentCard
82+
card = AgentCard(
83+
name="python_coder",
84+
url="http://localhost:8000",
85+
description="A Python coding assistant",
86+
version="0.1.0",
87+
default_input_modes=["text"],
88+
default_output_modes=["text"],
89+
capabilities=AgentCapabilities(streaming=True),
90+
skills=[],
91+
supports_authenticated_extended_card=False,
92+
)
93+
94+
# Create the remote agent from the card
95+
remote_agent = A2aRemoteAgent.from_card(card)
96+
```
97+
7498
## Advanced Usage
7599

76100
### Custom HTTP Client

0 commit comments

Comments
 (0)