|
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 | 78 | config: ClientConfig, |
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 | + httpx_client = config.httpx_client or httpx.AsyncClient() |
| 81 | + httpx_client.headers.setdefault( |
| 82 | + VERSION_HEADER, PROTOCOL_VERSION_CURRENT |
| 83 | + ) |
| 84 | + config.httpx_client = httpx_client |
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 |
|
| 91 | + def register(self, label: str, generator: TransportProducer) -> None: |
| 92 | + """Register a new transport producer for a given transport label.""" |
| 93 | + self._registry[label] = generator |
| 94 | + |
| 95 | + def create( |
| 96 | + self, |
| 97 | + card: AgentCard, |
| 98 | + interceptors: list[ClientCallInterceptor] | None = None, |
| 99 | + ) -> Client: |
| 100 | + """Create a new `Client` for the provided `AgentCard`. |
| 101 | +
|
| 102 | + Args: |
| 103 | + card: An `AgentCard` defining the characteristics of the agent. |
| 104 | + interceptors: A list of interceptors to use for each request. These |
| 105 | + are used for things like attaching credentials or http headers |
| 106 | + to all outbound requests. |
| 107 | +
|
| 108 | + Returns: |
| 109 | + A `Client` object. |
| 110 | +
|
| 111 | + Raises: |
| 112 | + If there is no valid matching of the client configuration with the |
| 113 | + server configuration, a `ValueError` is raised. |
| 114 | + """ |
| 115 | + client_set = self._config.supported_protocol_bindings or [ |
| 116 | + TransportProtocol.JSONRPC |
| 117 | + ] |
| 118 | + transport_protocol = None |
| 119 | + selected_interface = None |
| 120 | + if self._config.use_client_preference: |
| 121 | + for protocol_binding in client_set: |
| 122 | + selected_interface = ClientFactory._find_best_interface( |
| 123 | + list(card.supported_interfaces), |
| 124 | + protocol_bindings=[protocol_binding], |
| 125 | + ) |
| 126 | + if selected_interface: |
| 127 | + transport_protocol = protocol_binding |
| 128 | + break |
| 129 | + else: |
| 130 | + for supported_interface in card.supported_interfaces: |
| 131 | + if supported_interface.protocol_binding in client_set: |
| 132 | + transport_protocol = supported_interface.protocol_binding |
| 133 | + selected_interface = ClientFactory._find_best_interface( |
| 134 | + list(card.supported_interfaces), |
| 135 | + protocol_bindings=[transport_protocol], |
| 136 | + ) |
| 137 | + break |
| 138 | + if not transport_protocol or not selected_interface: |
| 139 | + raise ValueError('no compatible transports found.') |
| 140 | + if transport_protocol not in self._registry: |
| 141 | + raise ValueError(f'no client available for {transport_protocol}') |
| 142 | + |
| 143 | + transport = self._registry[transport_protocol]( |
| 144 | + card, selected_interface.url, self._config |
| 145 | + ) |
| 146 | + |
| 147 | + if selected_interface.tenant: |
| 148 | + transport = TenantTransportDecorator( |
| 149 | + transport, selected_interface.tenant |
| 150 | + ) |
| 151 | + |
| 152 | + return BaseClient( |
| 153 | + card, |
| 154 | + self._config, |
| 155 | + transport, |
| 156 | + interceptors or [], |
| 157 | + ) |
| 158 | + |
| 159 | + async def create_from_url( |
| 160 | + self, |
| 161 | + url: str, |
| 162 | + interceptors: list[ClientCallInterceptor] | None = None, |
| 163 | + relative_card_path: str | None = None, |
| 164 | + resolver_http_kwargs: dict[str, Any] | None = None, |
| 165 | + signature_verifier: Callable[[AgentCard], None] | None = None, |
| 166 | + ) -> Client: |
| 167 | + """Create a `Client` by resolving an `AgentCard` from a URL. |
| 168 | +
|
| 169 | + Resolves the agent card from the given URL using the factory's |
| 170 | + configured httpx client, then creates a client via `create`. |
| 171 | +
|
| 172 | + If the agent card is already available, use `create` directly |
| 173 | + instead. |
| 174 | +
|
| 175 | + Args: |
| 176 | + url: The base URL of the agent. The agent card will be fetched |
| 177 | + from `<url>/.well-known/agent-card.json` by default. |
| 178 | + interceptors: A list of interceptors to use for each request. |
| 179 | + These are used for things like attaching credentials or http |
| 180 | + headers to all outbound requests. |
| 181 | + relative_card_path: The relative path when resolving the agent |
| 182 | + card. See `A2ACardResolver.get_agent_card` for details. |
| 183 | + resolver_http_kwargs: Dictionary of arguments to provide to the |
| 184 | + httpx client when resolving the agent card. |
| 185 | + signature_verifier: A callable used to verify the agent card's |
| 186 | + signatures. |
| 187 | +
|
| 188 | + Returns: |
| 189 | + A `Client` object. |
| 190 | + """ |
| 191 | + resolver = A2ACardResolver(self._httpx_client, url) |
| 192 | + card = await resolver.get_agent_card( |
| 193 | + relative_card_path=relative_card_path, |
| 194 | + http_kwargs=resolver_http_kwargs, |
| 195 | + signature_verifier=signature_verifier, |
| 196 | + ) |
| 197 | + return self.create(card, interceptors) |
| 198 | + |
88 | 199 | def _register_defaults(self, supported: list[str]) -> None: |
89 | 200 | # Empty support list implies JSON-RPC only. |
90 | 201 |
|
@@ -252,141 +363,47 @@ def _find_best_interface( |
252 | 363 |
|
253 | 364 | return best_gt_1_0 or best_ge_0_3 or best_no_version |
254 | 365 |
|
255 | | - @classmethod |
256 | | - async def connect( # noqa: PLR0913 |
257 | | - cls, |
258 | | - agent: str | AgentCard, |
259 | | - client_config: ClientConfig | None = None, |
260 | | - interceptors: list[ClientCallInterceptor] | None = None, |
261 | | - relative_card_path: str | None = None, |
262 | | - resolver_http_kwargs: dict[str, Any] | None = None, |
263 | | - extra_transports: dict[str, TransportProducer] | None = None, |
264 | | - signature_verifier: Callable[[AgentCard], None] | None = None, |
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 |
273 | | -
|
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) |
277 | | -
|
278 | | -
|
279 | | - 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. |
295 | | -
|
296 | | - Returns: |
297 | | - A `Client` object. |
298 | | - """ |
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) |
322 | | - |
323 | | - def register(self, label: str, generator: TransportProducer) -> None: |
324 | | - """Register a new transport producer for a given transport label.""" |
325 | | - self._registry[label] = generator |
326 | | - |
327 | | - def create( |
328 | | - self, |
329 | | - card: AgentCard, |
330 | | - interceptors: list[ClientCallInterceptor] | None = None, |
331 | | - ) -> Client: |
332 | | - """Create a new `Client` for the provided `AgentCard`. |
333 | | -
|
334 | | - Args: |
335 | | - card: An `AgentCard` defining the characteristics of the agent. |
336 | | - interceptors: A list of interceptors to use for each request. These |
337 | | - are used for things like attaching credentials or http headers |
338 | | - to all outbound requests. |
339 | 366 |
|
340 | | - Returns: |
341 | | - A `Client` object. |
342 | | -
|
343 | | - Raises: |
344 | | - If there is no valid matching of the client configuration with the |
345 | | - server configuration, a `ValueError` is raised. |
346 | | - """ |
347 | | - client_set = self._config.supported_protocol_bindings or [ |
348 | | - TransportProtocol.JSONRPC |
349 | | - ] |
350 | | - transport_protocol = None |
351 | | - selected_interface = None |
352 | | - if self._config.use_client_preference: |
353 | | - for protocol_binding in client_set: |
354 | | - selected_interface = ClientFactory._find_best_interface( |
355 | | - list(card.supported_interfaces), |
356 | | - protocol_bindings=[protocol_binding], |
357 | | - ) |
358 | | - if selected_interface: |
359 | | - transport_protocol = protocol_binding |
360 | | - break |
361 | | - else: |
362 | | - for supported_interface in card.supported_interfaces: |
363 | | - if supported_interface.protocol_binding in client_set: |
364 | | - transport_protocol = supported_interface.protocol_binding |
365 | | - selected_interface = ClientFactory._find_best_interface( |
366 | | - list(card.supported_interfaces), |
367 | | - protocol_bindings=[transport_protocol], |
368 | | - ) |
369 | | - break |
370 | | - if not transport_protocol or not selected_interface: |
371 | | - raise ValueError('no compatible transports found.') |
372 | | - if transport_protocol not in self._registry: |
373 | | - raise ValueError(f'no client available for {transport_protocol}') |
374 | | - |
375 | | - transport = self._registry[transport_protocol]( |
376 | | - card, selected_interface.url, self._config |
377 | | - ) |
378 | | - |
379 | | - if selected_interface.tenant: |
380 | | - transport = TenantTransportDecorator( |
381 | | - transport, selected_interface.tenant |
382 | | - ) |
383 | | - |
384 | | - return BaseClient( |
385 | | - card, |
386 | | - self._config, |
387 | | - transport, |
388 | | - interceptors or [], |
| 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 or ClientConfig()) |
| 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, |
389 | 405 | ) |
| 406 | + return factory.create(agent, interceptors) |
390 | 407 |
|
391 | 408 |
|
392 | 409 | def minimal_agent_card( |
|
0 commit comments