Skip to content

Commit 2b58616

Browse files
authored
feat: add slimrpc multicast support with unified client factory (#18)
Introduce multicast transport and client classes that leverage slim-bindings group channels to query multiple A2A agents simultaneously. The SRPCClientFactory auto-detects multicast from comma-separated names in the agent card URL, providing a single create() entry point for both unicast and multicast clients. Fixes: #17 Signed-off-by: Mauro Sardara <msardara@cisco.com>
1 parent fd71310 commit 2b58616

6 files changed

Lines changed: 2109 additions & 1124 deletions

File tree

examples/echo_agent/client.py

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from a2a.client import (
1111
A2ACardResolver,
1212
Client,
13-
ClientFactory,
1413
create_text_message_object,
1514
minimal_agent_card,
1615
)
@@ -22,7 +21,10 @@
2221
from slima2a import setup_slim_client
2322
from slima2a.client_transport import (
2423
ClientConfig,
24+
MultiAgentClientFactory,
25+
MulticastClient,
2526
slimrpc_channel_factory,
27+
slimrpc_group_channel_factory,
2628
)
2729

2830
BASE_URL = "http://localhost:9999"
@@ -65,39 +67,44 @@ async def main() -> None:
6567
streaming=args.stream,
6668
httpx_client=httpx_client,
6769
slimrpc_channel_factory=slimrpc_channel_factory(slim_local_app, conn_id),
70+
slimrpc_group_channel_factory=slimrpc_group_channel_factory(
71+
slim_local_app, conn_id
72+
),
6873
)
69-
client_factory = ClientFactory(client_config)
74+
client_factory = MultiAgentClientFactory(client_config)
7075

7176
if args.a2a_version == "v0":
7277
from slima2a.compat.v3_0.client_transport import SRPCCompatTransport
7378

74-
# mypy: the register API expects a different callable type; safe to ignore here.
75-
client_factory.register("slimrpc", SRPCCompatTransport.create) # type: ignore
76-
else:
77-
from slima2a.client_transport import SRPCTransport
79+
client_factory.register("slimrpc", SRPCCompatTransport.create, multiagent=True) # type: ignore
7880

79-
# mypy: the register API expects a different callable type; safe to ignore here.
80-
client_factory.register("slimrpc", SRPCTransport.create) # type: ignore
81+
agent_names = [f"agntcy/demo/{name.strip()}" for name in args.agents.split(",")]
8182

8283
match args.type:
8384
case "slimrpc":
84-
agent_card = minimal_agent_card("agntcy/demo/echo_agent", ["slimrpc"])
85+
cards = [minimal_agent_card(name, ["slimrpc"]) for name in agent_names]
8586
case "starlette":
86-
agent_card = await fetch_agent_card(
87-
resolver=A2ACardResolver(
88-
httpx_client=httpx_client,
89-
base_url=BASE_URL,
87+
cards = [
88+
await fetch_agent_card(
89+
resolver=A2ACardResolver(
90+
httpx_client=httpx_client,
91+
base_url=BASE_URL,
92+
)
9093
)
91-
)
94+
]
9295
case _:
9396
raise ValueError(f"Invalid client type: {args.type}")
9497

95-
client = client_factory.create(card=agent_card)
96-
logger.info("A2AClient initialized.")
98+
client = client_factory.create(card=cards)
9799

98-
response_text = await send_message(client, args.text)
99-
print(f"> {args.text}")
100-
print(response_text)
100+
if isinstance(client, MulticastClient):
101+
print(f"> {args.text} (multicast to {agent_names})")
102+
await send_message_multicast(client, args.text)
103+
else:
104+
logger.info("A2AClient initialized.")
105+
response_text = await send_message(client, args.text)
106+
print(f"> {args.text}")
107+
print(response_text)
101108

102109

103110
def parse_arguments() -> argparse.Namespace:
@@ -133,6 +140,13 @@ def parse_arguments() -> argparse.Namespace:
133140
default="v1",
134141
choices=["v0", "v1"],
135142
)
143+
parser.add_argument(
144+
"--agents",
145+
type=str,
146+
required=False,
147+
default="echo_agent",
148+
help="Comma-separated agent names (e.g. echo_agent_1,echo_agent_2). Multiple names triggers multicast.",
149+
)
136150

137151
args = parser.parse_args()
138152

@@ -180,5 +194,41 @@ async def send_message(
180194
return output
181195

182196

197+
async def send_message_multicast(
198+
client: MulticastClient,
199+
text: str,
200+
) -> None:
201+
message = create_text_message_object(content=text)
202+
request = SendMessageRequest(message=message)
203+
204+
try:
205+
async for source, response in client.send_message(request):
206+
output = ""
207+
which = response.WhichOneof("payload")
208+
if which == "message":
209+
for part in response.message.parts:
210+
if part.WhichOneof("content") == "text":
211+
output += part.text
212+
elif which == "task":
213+
if response.task.artifacts:
214+
for artifact in response.task.artifacts:
215+
for part in artifact.parts:
216+
if part.WhichOneof("content") == "text":
217+
output += part.text
218+
elif which == "artifact_update":
219+
artifact = response.artifact_update.artifact
220+
for part in artifact.parts:
221+
if part.WhichOneof("content") == "text":
222+
output += part.text
223+
if output:
224+
print(f" [{source}] {output}")
225+
except Exception as e:
226+
logger.error(
227+
f"failed sending multicast message: {e}",
228+
exc_info=True,
229+
)
230+
raise RuntimeError("failed sending multicast message") from e
231+
232+
183233
if __name__ == "__main__":
184234
asyncio.run(main())

examples/echo_agent/server.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import argparse
22
import asyncio
3+
import contextlib
34
import logging
45
import sys
56
from pathlib import Path
@@ -59,7 +60,7 @@ async def main() -> None:
5960
service, local_app, local_name, conn_id = await setup_slim_client(
6061
namespace="agntcy",
6162
group="demo",
62-
name="echo_agent",
63+
name=args.name,
6364
secret="my_shared_secret_for_testing_purposes_only",
6465
)
6566

@@ -106,6 +107,7 @@ def parse_arguments() -> argparse.Namespace:
106107
parser = argparse.ArgumentParser()
107108

108109
parser.add_argument("--type", type=str, required=False, default="slimrpc")
110+
parser.add_argument("--name", type=str, required=False, default="echo_agent")
109111

110112
parser.add_argument(
111113
"--log-level",
@@ -131,4 +133,5 @@ def parse_arguments() -> argparse.Namespace:
131133

132134

133135
if __name__ == "__main__":
134-
asyncio.run(main())
136+
with contextlib.suppress(KeyboardInterrupt):
137+
asyncio.run(main())

0 commit comments

Comments
 (0)