|
3 | 3 | import logging |
4 | 4 |
|
5 | 5 | from collections.abc import Callable |
6 | | -from typing import TYPE_CHECKING, Any, cast |
| 6 | +from typing import TYPE_CHECKING, Any |
7 | 7 |
|
8 | 8 | import httpx |
9 | 9 |
|
|
56 | 56 |
|
57 | 57 |
|
58 | 58 | class ClientFactory: |
59 | | - """ClientFactory is used to generate the appropriate client for the agent. |
| 59 | + """Factory for creating clients that communicate with A2A agents. |
60 | 60 |
|
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: |
65 | 63 |
|
66 | 64 | 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 |
70 | 68 | 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') |
71 | 71 |
|
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 |
73 | 73 | aligns the client configuration with the server's capabilities. |
74 | 74 | """ |
75 | 75 |
|
76 | 76 | def __init__( |
77 | 77 | self, |
78 | | - config: ClientConfig, |
| 78 | + config: ClientConfig | None = None, |
79 | 79 | ): |
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 | + ) |
83 | 85 |
|
84 | 86 | self._config = config |
| 87 | + self._httpx_client = httpx_client |
85 | 88 | self._registry: dict[str, TransportProducer] = {} |
86 | 89 | self._register_defaults(config.supported_protocol_bindings) |
87 | 90 |
|
@@ -112,13 +115,13 @@ def jsonrpc_transport_producer( |
112 | 115 | ) |
113 | 116 |
|
114 | 117 | return CompatJsonRpcTransport( |
115 | | - cast('httpx.AsyncClient', config.httpx_client), |
| 118 | + self._httpx_client, |
116 | 119 | card, |
117 | 120 | url, |
118 | 121 | ) |
119 | 122 |
|
120 | 123 | return JsonRpcTransport( |
121 | | - cast('httpx.AsyncClient', config.httpx_client), |
| 124 | + self._httpx_client, |
122 | 125 | card, |
123 | 126 | url, |
124 | 127 | ) |
@@ -151,13 +154,13 @@ def rest_transport_producer( |
151 | 154 | ) |
152 | 155 |
|
153 | 156 | return CompatRestTransport( |
154 | | - cast('httpx.AsyncClient', config.httpx_client), |
| 157 | + self._httpx_client, |
155 | 158 | card, |
156 | 159 | url, |
157 | 160 | ) |
158 | 161 |
|
159 | 162 | return RestTransport( |
160 | | - cast('httpx.AsyncClient', config.httpx_client), |
| 163 | + self._httpx_client, |
161 | 164 | card, |
162 | 165 | url, |
163 | 166 | ) |
@@ -252,73 +255,45 @@ def _find_best_interface( |
252 | 255 |
|
253 | 256 | return best_gt_1_0 or best_ge_0_3 or best_no_version |
254 | 257 |
|
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, |
260 | 261 | interceptors: list[ClientCallInterceptor] | None = None, |
261 | 262 | relative_card_path: str | None = None, |
262 | 263 | resolver_http_kwargs: dict[str, Any] | None = None, |
263 | | - extra_transports: dict[str, TransportProducer] | None = None, |
264 | 264 | signature_verifier: Callable[[AgentCard], None] | None = None, |
265 | 265 | ) -> 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. |
273 | 267 |
|
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`. |
277 | 270 |
|
| 271 | + If the agent card is already available, use `create` directly |
| 272 | + instead. |
278 | 273 |
|
279 | 274 | 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. |
295 | 286 |
|
296 | 287 | Returns: |
297 | 288 | A `Client` object. |
298 | 289 | """ |
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) |
322 | 297 |
|
323 | 298 | def register(self, label: str, generator: TransportProducer) -> None: |
324 | 299 | """Register a new transport producer for a given transport label.""" |
@@ -389,6 +364,48 @@ def create( |
389 | 364 | ) |
390 | 365 |
|
391 | 366 |
|
| 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 | + |
392 | 409 | def minimal_agent_card( |
393 | 410 | url: str, transports: list[str] | None = None |
394 | 411 | ) -> AgentCard: |
|
0 commit comments