Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions itk/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from pyproto import instruction_pb2

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

try:
client = await ClientFactory.connect(
call.agent_card_uri,
client_config=config,
)
client = await create_client(call.agent_card_uri, client_config=config)

# Wrap nested instruction
async with client:
Expand Down
4 changes: 2 additions & 2 deletions samples/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import grpc
import httpx

from a2a.client import A2ACardResolver, ClientConfig, ClientFactory
from a2a.client import A2ACardResolver, ClientConfig, create_client
from a2a.types import Message, Part, Role, SendMessageRequest, TaskState


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

client = await ClientFactory.connect(card, client_config=config)
client = await create_client(card, client_config=config)

actual_transport = getattr(client, '_transport', client)
print(f' Picked Transport: {actual_transport.__class__.__name__}')
Expand Down
7 changes: 6 additions & 1 deletion src/a2a/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
ClientCallContext,
ClientConfig,
)
from a2a.client.client_factory import ClientFactory, minimal_agent_card
from a2a.client.client_factory import (
ClientFactory,
create_client,
minimal_agent_card,
)
from a2a.client.errors import (
A2AClientError,
A2AClientTimeoutError,
Expand All @@ -36,6 +40,7 @@
'ClientFactory',
'CredentialService',
'InMemoryContextCredentialStore',
'create_client',
'create_text_message_object',
'minimal_agent_card',
]
161 changes: 89 additions & 72 deletions src/a2a/client/client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging

from collections.abc import Callable
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any

import httpx

Expand Down Expand Up @@ -56,32 +56,35 @@


class ClientFactory:
"""ClientFactory is used to generate the appropriate client for the agent.
"""Factory for creating clients that communicate with A2A agents.

The factory is configured with a `ClientConfig` and optionally a list of
`Consumer`s to use for all generated `Client`s. The expected use is:

.. code-block:: python
The factory is configured with a `ClientConfig` and optionally custom
transport producers registered via `register`. Example usage:

factory = ClientFactory(config)
# Optionally register custom client implementations
factory.register('my_customer_transport', NewCustomTransportClient)
# Then with an agent card make a client with additional interceptors
# Optionally register custom transport implementations
factory.register('my_custom_transport', custom_transport_producer)
# Create a client from an AgentCard
client = factory.create(card, interceptors)
# Or resolve an AgentCard from a URL and create a client
client = await factory.create_from_url('https://example.com')

Now the client can be used consistently regardless of the transport. This
The client can be used consistently regardless of the transport. This
aligns the client configuration with the server's capabilities.
"""

def __init__(
self,
config: ClientConfig,
config: ClientConfig | None = None,
):
client = config.httpx_client or httpx.AsyncClient()
client.headers.setdefault(VERSION_HEADER, PROTOCOL_VERSION_CURRENT)
config.httpx_client = client
config = config or ClientConfig()
httpx_client = config.httpx_client or httpx.AsyncClient()
httpx_client.headers.setdefault(
VERSION_HEADER, PROTOCOL_VERSION_CURRENT
)

self._config = config
self._httpx_client = httpx_client
self._registry: dict[str, TransportProducer] = {}
self._register_defaults(config.supported_protocol_bindings)

Expand Down Expand Up @@ -112,13 +115,13 @@
)

return CompatJsonRpcTransport(
cast('httpx.AsyncClient', config.httpx_client),
self._httpx_client,
card,
url,
)

return JsonRpcTransport(
cast('httpx.AsyncClient', config.httpx_client),
self._httpx_client,
card,
url,
)
Expand Down Expand Up @@ -151,13 +154,13 @@
)

return CompatRestTransport(
cast('httpx.AsyncClient', config.httpx_client),
self._httpx_client,
card,
url,
)

return RestTransport(
cast('httpx.AsyncClient', config.httpx_client),
self._httpx_client,
card,
url,
)
Expand Down Expand Up @@ -252,73 +255,45 @@

return best_gt_1_0 or best_ge_0_3 or best_no_version

@classmethod
async def connect( # noqa: PLR0913
cls,
agent: str | AgentCard,
client_config: ClientConfig | None = None,
async def create_from_url(
self,
url: str,
interceptors: list[ClientCallInterceptor] | None = None,
relative_card_path: str | None = None,
resolver_http_kwargs: dict[str, Any] | None = None,
extra_transports: dict[str, TransportProducer] | None = None,
signature_verifier: Callable[[AgentCard], None] | None = None,
) -> Client:
"""Convenience method for constructing a client.

Constructs a client that connects to the specified agent. Note that
creating multiple clients via this method is less efficient than
constructing an instance of ClientFactory and reusing that.

.. code-block:: python
"""Create a `Client` by resolving an `AgentCard` from a URL.

# This will search for an AgentCard at /.well-known/agent-card.json
my_agent_url = 'https://travel.agents.example.com'
client = await ClientFactory.connect(my_agent_url)
Resolves the agent card from the given URL using the factory's
configured httpx client, then creates a client via `create`.

If the agent card is already available, use `create` directly
instead.

Args:
agent: The base URL of the agent, or the AgentCard to connect to.
client_config: The ClientConfig to use when connecting to the agent.

interceptors: A list of interceptors to use for each request. These
are used for things like attaching credentials or http headers
to all outbound requests.
relative_card_path: If the agent field is a URL, this value is used as
the relative path when resolving the agent card. See
A2AAgentCardResolver.get_agent_card for more details.
resolver_http_kwargs: Dictionary of arguments to provide to the httpx
client when resolving the agent card. This value is provided to
A2AAgentCardResolver.get_agent_card as the http_kwargs parameter.
extra_transports: Additional transport protocols to enable when
constructing the client.
signature_verifier: A callable used to verify the agent card's signatures.
url: The base URL of the agent. The agent card will be fetched
from `<url>/.well-known/agent-card.json` by default.
interceptors: A list of interceptors to use for each request.
These are used for things like attaching credentials or http
headers to all outbound requests.
relative_card_path: The relative path when resolving the agent
card. See `A2ACardResolver.get_agent_card` for details.
resolver_http_kwargs: Dictionary of arguments to provide to the
httpx client when resolving the agent card.
signature_verifier: A callable used to verify the agent card's
signatures.

Returns:
A `Client` object.
"""

Check notice on line 289 in src/a2a/client/client_factory.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/client/client_factory.py (369-396)
client_config = client_config or ClientConfig()
if isinstance(agent, str):
if not client_config.httpx_client:
async with httpx.AsyncClient() as client:
resolver = A2ACardResolver(client, agent)
card = await resolver.get_agent_card(
relative_card_path=relative_card_path,
http_kwargs=resolver_http_kwargs,
signature_verifier=signature_verifier,
)
else:
resolver = A2ACardResolver(client_config.httpx_client, agent)
card = await resolver.get_agent_card(
relative_card_path=relative_card_path,
http_kwargs=resolver_http_kwargs,
signature_verifier=signature_verifier,
)
else:
card = agent
factory = cls(client_config)
for label, generator in (extra_transports or {}).items():
factory.register(label, generator)
return factory.create(card, interceptors)
resolver = A2ACardResolver(self._httpx_client, url)
card = await resolver.get_agent_card(
relative_card_path=relative_card_path,
http_kwargs=resolver_http_kwargs,
signature_verifier=signature_verifier,
)
return self.create(card, interceptors)

def register(self, label: str, generator: TransportProducer) -> None:
"""Register a new transport producer for a given transport label."""
Expand Down Expand Up @@ -389,6 +364,48 @@
)


async def create_client( # noqa: PLR0913
agent: str | AgentCard,
client_config: ClientConfig | None = None,
interceptors: list[ClientCallInterceptor] | None = None,
relative_card_path: str | None = None,
resolver_http_kwargs: dict[str, Any] | None = None,
signature_verifier: Callable[[AgentCard], None] | None = None,
) -> Client:
"""Create a `Client` for an agent from a URL or `AgentCard`.

Convenience function that constructs a `ClientFactory` internally.
For reusing a factory across multiple agents or registering custom
transports, use `ClientFactory` directly instead.

Args:
agent: The base URL of the agent, or an `AgentCard` to use
directly.
client_config: Optional `ClientConfig`. A default config is
created if not provided.
interceptors: A list of interceptors to use for each request.
relative_card_path: The relative path when resolving the agent
card. Only used when `agent` is a URL.
resolver_http_kwargs: Dictionary of arguments to provide to the
httpx client when resolving the agent card.
signature_verifier: A callable used to verify the agent card's
signatures.

Returns:
A `Client` object.
"""

Check notice on line 396 in src/a2a/client/client_factory.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/client/client_factory.py (260-289)
factory = ClientFactory(client_config)
if isinstance(agent, str):
return await factory.create_from_url(
agent,
interceptors=interceptors,
relative_card_path=relative_card_path,
resolver_http_kwargs=resolver_http_kwargs,
signature_verifier=signature_verifier,
)
return factory.create(agent, interceptors)


def minimal_agent_card(
url: str, transports: list[str] | None = None
) -> AgentCard:
Expand Down
Loading
Loading