Skip to content

Commit 01b3b2c

Browse files
authored
refactor(client)!: reorganize ClientFactory API (#947)
#### Replace `connect` class method with `create_from_url` instance method 1. `connect` implies some persistent connection, in fact the only difference with `create` is I/O during agent card resolution (also optional, as it accepted both URL or agent card itself). 2. Contained logic which was useful for a pre-configured factory instance (like agent card resolution). 3. It's a separate `async` method and `create` is kept without I/O. #### Added a utility `create_client` module function One-line entry point similar to the former `connect` to simplify migration, but doesn't contain any domain logic and just does dispatching between URL and agent card.
1 parent 546fb86 commit 01b3b2c

6 files changed

Lines changed: 214 additions & 117 deletions

File tree

itk/main.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from pyproto import instruction_pb2
1414

15-
from a2a.client import ClientConfig, ClientFactory
15+
from a2a.client import ClientConfig, create_client
1616
from a2a.compat.v0_3 import a2a_v0_3_pb2_grpc
1717
from a2a.compat.v0_3.grpc_handler import CompatGrpcHandler
1818
from a2a.server.agent_execution import AgentExecutor, RequestContext
@@ -128,10 +128,7 @@ async def handle_call_agent(call: instruction_pb2.CallAgent) -> list[str]:
128128
)
129129

130130
try:
131-
client = await ClientFactory.connect(
132-
call.agent_card_uri,
133-
client_config=config,
134-
)
131+
client = await create_client(call.agent_card_uri, client_config=config)
135132

136133
# Wrap nested instruction
137134
async with client:

samples/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import grpc
1010
import httpx
1111

12-
from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
12+
from a2a.client import A2ACardResolver, ClientConfig, create_client
1313
from a2a.types import Message, Part, Role, SendMessageRequest, TaskState
1414

1515

@@ -79,7 +79,7 @@ async def main() -> None:
7979
print('\n✓ Agent Card Found:')
8080
print(f' Name: {card.name}')
8181

82-
client = await ClientFactory.connect(card, client_config=config)
82+
client = await create_client(card, client_config=config)
8383

8484
actual_transport = getattr(client, '_transport', client)
8585
print(f' Picked Transport: {actual_transport.__class__.__name__}')

src/a2a/client/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
ClientCallContext,
1313
ClientConfig,
1414
)
15-
from a2a.client.client_factory import ClientFactory, minimal_agent_card
15+
from a2a.client.client_factory import (
16+
ClientFactory,
17+
create_client,
18+
minimal_agent_card,
19+
)
1620
from a2a.client.errors import (
1721
A2AClientError,
1822
A2AClientTimeoutError,
@@ -36,6 +40,7 @@
3640
'ClientFactory',
3741
'CredentialService',
3842
'InMemoryContextCredentialStore',
43+
'create_client',
3944
'create_text_message_object',
4045
'minimal_agent_card',
4146
]

src/a2a/client/client_factory.py

Lines changed: 89 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44

55
from collections.abc import Callable
6-
from typing import TYPE_CHECKING, Any, cast
6+
from typing import TYPE_CHECKING, Any
77

88
import httpx
99

@@ -56,32 +56,35 @@
5656

5757

5858
class ClientFactory:
59-
"""ClientFactory is used to generate the appropriate client for the agent.
59+
"""Factory for creating clients that communicate with A2A agents.
6060
61-
The factory is configured with a `ClientConfig` and optionally a list of
62-
`Consumer`s to use for all generated `Client`s. The expected use is:
63-
64-
.. code-block:: python
61+
The factory is configured with a `ClientConfig` and optionally custom
62+
transport producers registered via `register`. Example usage:
6563
6664
factory = ClientFactory(config)
67-
# Optionally register custom client implementations
68-
factory.register('my_customer_transport', NewCustomTransportClient)
69-
# Then with an agent card make a client with additional interceptors
65+
# Optionally register custom transport implementations
66+
factory.register('my_custom_transport', custom_transport_producer)
67+
# Create a client from an AgentCard
7068
client = factory.create(card, interceptors)
69+
# Or resolve an AgentCard from a URL and create a client
70+
client = await factory.create_from_url('https://example.com')
7171
72-
Now the client can be used consistently regardless of the transport. This
72+
The client can be used consistently regardless of the transport. This
7373
aligns the client configuration with the server's capabilities.
7474
"""
7575

7676
def __init__(
7777
self,
78-
config: ClientConfig,
78+
config: ClientConfig | None = None,
7979
):
80-
client = config.httpx_client or httpx.AsyncClient()
81-
client.headers.setdefault(VERSION_HEADER, PROTOCOL_VERSION_CURRENT)
82-
config.httpx_client = client
80+
config = config or ClientConfig()
81+
httpx_client = config.httpx_client or httpx.AsyncClient()
82+
httpx_client.headers.setdefault(
83+
VERSION_HEADER, PROTOCOL_VERSION_CURRENT
84+
)
8385

8486
self._config = config
87+
self._httpx_client = httpx_client
8588
self._registry: dict[str, TransportProducer] = {}
8689
self._register_defaults(config.supported_protocol_bindings)
8790

@@ -112,13 +115,13 @@ def jsonrpc_transport_producer(
112115
)
113116

114117
return CompatJsonRpcTransport(
115-
cast('httpx.AsyncClient', config.httpx_client),
118+
self._httpx_client,
116119
card,
117120
url,
118121
)
119122

120123
return JsonRpcTransport(
121-
cast('httpx.AsyncClient', config.httpx_client),
124+
self._httpx_client,
122125
card,
123126
url,
124127
)
@@ -151,13 +154,13 @@ def rest_transport_producer(
151154
)
152155

153156
return CompatRestTransport(
154-
cast('httpx.AsyncClient', config.httpx_client),
157+
self._httpx_client,
155158
card,
156159
url,
157160
)
158161

159162
return RestTransport(
160-
cast('httpx.AsyncClient', config.httpx_client),
163+
self._httpx_client,
161164
card,
162165
url,
163166
)
@@ -252,73 +255,45 @@ def _find_best_interface(
252255

253256
return best_gt_1_0 or best_ge_0_3 or best_no_version
254257

255-
@classmethod
256-
async def connect( # noqa: PLR0913
257-
cls,
258-
agent: str | AgentCard,
259-
client_config: ClientConfig | None = None,
258+
async def create_from_url(
259+
self,
260+
url: str,
260261
interceptors: list[ClientCallInterceptor] | None = None,
261262
relative_card_path: str | None = None,
262263
resolver_http_kwargs: dict[str, Any] | None = None,
263-
extra_transports: dict[str, TransportProducer] | None = None,
264264
signature_verifier: Callable[[AgentCard], None] | None = None,
265265
) -> Client:
266-
"""Convenience method for constructing a client.
267-
268-
Constructs a client that connects to the specified agent. Note that
269-
creating multiple clients via this method is less efficient than
270-
constructing an instance of ClientFactory and reusing that.
271-
272-
.. code-block:: python
266+
"""Create a `Client` by resolving an `AgentCard` from a URL.
273267
274-
# This will search for an AgentCard at /.well-known/agent-card.json
275-
my_agent_url = 'https://travel.agents.example.com'
276-
client = await ClientFactory.connect(my_agent_url)
268+
Resolves the agent card from the given URL using the factory's
269+
configured httpx client, then creates a client via `create`.
277270
271+
If the agent card is already available, use `create` directly
272+
instead.
278273
279274
Args:
280-
agent: The base URL of the agent, or the AgentCard to connect to.
281-
client_config: The ClientConfig to use when connecting to the agent.
282-
283-
interceptors: A list of interceptors to use for each request. These
284-
are used for things like attaching credentials or http headers
285-
to all outbound requests.
286-
relative_card_path: If the agent field is a URL, this value is used as
287-
the relative path when resolving the agent card. See
288-
A2AAgentCardResolver.get_agent_card for more details.
289-
resolver_http_kwargs: Dictionary of arguments to provide to the httpx
290-
client when resolving the agent card. This value is provided to
291-
A2AAgentCardResolver.get_agent_card as the http_kwargs parameter.
292-
extra_transports: Additional transport protocols to enable when
293-
constructing the client.
294-
signature_verifier: A callable used to verify the agent card's signatures.
275+
url: The base URL of the agent. The agent card will be fetched
276+
from `<url>/.well-known/agent-card.json` by default.
277+
interceptors: A list of interceptors to use for each request.
278+
These are used for things like attaching credentials or http
279+
headers to all outbound requests.
280+
relative_card_path: The relative path when resolving the agent
281+
card. See `A2ACardResolver.get_agent_card` for details.
282+
resolver_http_kwargs: Dictionary of arguments to provide to the
283+
httpx client when resolving the agent card.
284+
signature_verifier: A callable used to verify the agent card's
285+
signatures.
295286
296287
Returns:
297288
A `Client` object.
298289
"""
299-
client_config = client_config or ClientConfig()
300-
if isinstance(agent, str):
301-
if not client_config.httpx_client:
302-
async with httpx.AsyncClient() as client:
303-
resolver = A2ACardResolver(client, agent)
304-
card = await resolver.get_agent_card(
305-
relative_card_path=relative_card_path,
306-
http_kwargs=resolver_http_kwargs,
307-
signature_verifier=signature_verifier,
308-
)
309-
else:
310-
resolver = A2ACardResolver(client_config.httpx_client, agent)
311-
card = await resolver.get_agent_card(
312-
relative_card_path=relative_card_path,
313-
http_kwargs=resolver_http_kwargs,
314-
signature_verifier=signature_verifier,
315-
)
316-
else:
317-
card = agent
318-
factory = cls(client_config)
319-
for label, generator in (extra_transports or {}).items():
320-
factory.register(label, generator)
321-
return factory.create(card, interceptors)
290+
resolver = A2ACardResolver(self._httpx_client, url)
291+
card = await resolver.get_agent_card(
292+
relative_card_path=relative_card_path,
293+
http_kwargs=resolver_http_kwargs,
294+
signature_verifier=signature_verifier,
295+
)
296+
return self.create(card, interceptors)
322297

323298
def register(self, label: str, generator: TransportProducer) -> None:
324299
"""Register a new transport producer for a given transport label."""
@@ -389,6 +364,48 @@ def create(
389364
)
390365

391366

367+
async def create_client( # noqa: PLR0913
368+
agent: str | AgentCard,
369+
client_config: ClientConfig | None = None,
370+
interceptors: list[ClientCallInterceptor] | None = None,
371+
relative_card_path: str | None = None,
372+
resolver_http_kwargs: dict[str, Any] | None = None,
373+
signature_verifier: Callable[[AgentCard], None] | None = None,
374+
) -> Client:
375+
"""Create a `Client` for an agent from a URL or `AgentCard`.
376+
377+
Convenience function that constructs a `ClientFactory` internally.
378+
For reusing a factory across multiple agents or registering custom
379+
transports, use `ClientFactory` directly instead.
380+
381+
Args:
382+
agent: The base URL of the agent, or an `AgentCard` to use
383+
directly.
384+
client_config: Optional `ClientConfig`. A default config is
385+
created if not provided.
386+
interceptors: A list of interceptors to use for each request.
387+
relative_card_path: The relative path when resolving the agent
388+
card. Only used when `agent` is a URL.
389+
resolver_http_kwargs: Dictionary of arguments to provide to the
390+
httpx client when resolving the agent card.
391+
signature_verifier: A callable used to verify the agent card's
392+
signatures.
393+
394+
Returns:
395+
A `Client` object.
396+
"""
397+
factory = ClientFactory(client_config)
398+
if isinstance(agent, str):
399+
return await factory.create_from_url(
400+
agent,
401+
interceptors=interceptors,
402+
relative_card_path=relative_card_path,
403+
resolver_http_kwargs=resolver_http_kwargs,
404+
signature_verifier=signature_verifier,
405+
)
406+
return factory.create(agent, interceptors)
407+
408+
392409
def minimal_agent_card(
393410
url: str, transports: list[str] | None = None
394411
) -> AgentCard:

0 commit comments

Comments
 (0)