Skip to content

Commit 5686c8e

Browse files
authored
feat: Provide a more friendly builder-based way to configure transpor… (#238)
…t configuration This PR is a proposal to make the transport configuration specific to a given transport implementation. This one will ease developer experience by providing a builder for configuring a Client in a more stricter way compare to what exists today. Also, this will help at getting rid of strong coupling with different transport implementations. Two benefits: Get rid of dependencies which may not be required (grpc dependencies if you only go for jsonrpc) Easier to provide new transport implementation (such as HTTP+JSON). # Description Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Follow the [`CONTRIBUTING` Guide](../CONTRIBUTING.md). - [x] Make your Pull Request title in the <https://www.conventionalcommits.org/> specification. - Important Prefixes for [release-please](https://github.com/googleapis/release-please): - `fix:` which represents bug fixes, and correlates to a [SemVer](https://semver.org/) patch. - `feat:` represents a new feature, and correlates to a SemVer minor. - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. - [x] Ensure the tests pass - [x] Appropriate READMEs were updated (if necessary) Fixes #<issue_number_goes_here> 🦕
1 parent aa75bf9 commit 5686c8e

File tree

44 files changed

+534
-411
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+534
-411
lines changed

README.md

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ To make use of the Java `Client`:
225225

226226
### 1. Add the A2A Java SDK Client dependency to your project
227227

228-
Adding a dependency on `a2a-java-sdk-client` will provide access to a `ClientFactory`
228+
Adding a dependency on `a2a-java-sdk-client` will provide access to a `ClientBuilder`
229229
that you can use to create your A2A `Client`.
230230

231231
----
@@ -243,21 +243,16 @@ that you can use to create your A2A `Client`.
243243

244244
### 2. Add one or more dependencies on the A2A Java SDK Client Transport(s) you'd like to use
245245

246-
You need to add a dependency on at least one of the following client transport modules:
246+
By default, the sdk-client is coming with the JSONRPC transport dependency. Despite the fact that the JSONRPC transport
247+
dependency is included by default, you still need to add the transport to the Client as described in [JSON-RPC Transport section](#json-rpc-transport-configuration).
248+
249+
250+
If you want to use another transport (such as GRPC or HTTP+JSON), you'll need to add a relevant dependency:
247251

248252
----
249253
> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.*
250254
----
251255

252-
```xml
253-
<dependency>
254-
<groupId>io.github.a2asdk</groupId>
255-
<artifactId>a2a-java-sdk-client-transport-jsonrpc</artifactId>
256-
<!-- Use a released version from https://github.com/a2aproject/a2a-java/releases -->
257-
<version>${io.a2a.sdk.version}</version>
258-
</dependency>
259-
```
260-
261256
```xml
262257
<dependency>
263258
<groupId>io.github.a2asdk</groupId>
@@ -271,13 +266,13 @@ Support for the HTTP+JSON/REST transport will be coming soon.
271266

272267
### Sample Usage
273268

274-
#### Create a Client using the ClientFactory
269+
#### Create a Client using the ClientBuilder
275270

276271
```java
277272
// First, get the agent card for the A2A server agent you want to connect to
278273
AgentCard agentCard = new A2ACardResolver("http://localhost:1234").getAgentCard();
279274

280-
// Specify configuration for the ClientFactory
275+
// Specify configuration for the ClientBuilder
281276
ClientConfig clientConfig = new ClientConfig.Builder()
282277
.setAcceptedOutputModes(List.of("text"))
283278
.build();
@@ -305,32 +300,39 @@ Consumer<Throwable> errorHandler = error -> {
305300
...
306301
};
307302

308-
// Create the client using ClientFactory
309-
ClientFactory clientFactory = new ClientFactory(clientConfig);
310-
Client client = clientFactory.create(agentCard, consumers, errorHandler);
303+
// Create the client using the builder
304+
Client client = Client
305+
.builder(agentCard)
306+
.clientConfig(clientConfig)
307+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
308+
.addConsumers(consumers)
309+
.streamingErrorHandler(errorHandler)
310+
.build();
311311
```
312312

313313
#### Configuring Transport-Specific Settings
314314

315-
Different transport protocols can be configured with specific settings using `ClientTransportConfig` implementations. The A2A Java SDK provides `JSONRPCTransportConfig` for the JSON-RPC transport and `GrpcTransportConfig` for the gRPC transport.
315+
Different transport protocols can be configured with specific settings using specific `ClientTransportConfig` implementations. The A2A Java SDK provides `JSONRPCTransportConfig` for the JSON-RPC transport and `GrpcTransportConfig` for the gRPC transport.
316316

317317
##### JSON-RPC Transport Configuration
318318

319319
For the JSON-RPC transport, if you'd like to use the default `JdkA2AHttpClient`, no additional
320-
configuration is needed. To use a custom HTTP client instead, simply create a `JSONRPCTransportConfig`
320+
configuration is needed. To use a custom HTTP client implementation, simply create a `JSONRPCTransportConfig`
321321
as follows:
322322

323323
```java
324324
// Create a custom HTTP client
325325
A2AHttpClient customHttpClient = ...
326326

327-
// Create JSON-RPC transport configuration
328-
JSONRPCTransportConfig jsonrpcConfig = new JSONRPCTransportConfig(customHttpClient);
329-
330-
// Configure the client with transport-specific settings
327+
// Configure the client settings
331328
ClientConfig clientConfig = new ClientConfig.Builder()
332329
.setAcceptedOutputModes(List.of("text"))
333-
.setClientTransportConfigs(List.of(jsonrpcConfig))
330+
.build();
331+
332+
Client client = Client
333+
.builder(agentCard)
334+
.clientConfig(clientConfig)
335+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig(customHttpClient))
334336
.build();
335337
```
336338

@@ -346,13 +348,15 @@ Function<String, Channel> channelFactory = agentUrl -> {
346348
.build();
347349
};
348350

349-
// Create gRPC transport configuration
350-
GrpcTransportConfig grpcConfig = new GrpcTransportConfig(channelFactory);
351-
352351
// Configure the client with transport-specific settings
353352
ClientConfig clientConfig = new ClientConfig.Builder()
354353
.setAcceptedOutputModes(List.of("text"))
355-
.setClientTransportConfigs(List.of(grpcConfig))
354+
.build();
355+
356+
Client client = Client
357+
.builder(agentCard)
358+
.clientConfig(clientConfig)
359+
.withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory))
356360
.build();
357361
```
358362

@@ -363,15 +367,11 @@ will be used based on the selected transport:
363367

364368
```java
365369
// Configure both JSON-RPC and gRPC transports
366-
List<ClientTransportConfig> transportConfigs = List.of(
367-
new JSONRPCTransportConfig(...),
368-
new GrpcTransportConfig(...)
369-
);
370-
371-
ClientConfig clientConfig = new ClientConfig.Builder()
372-
.setAcceptedOutputModes(List.of("text"))
373-
.setClientTransportConfigs(transportConfigs)
374-
.build();
370+
Client client = Client
371+
.builder(agentCard)
372+
.withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory))
373+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
374+
.build();
375375
```
376376

377377
#### Send a message to the A2A server agent

client/base/pom.xml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,20 @@
2020
<dependencies>
2121
<dependency>
2222
<groupId>${project.groupId}</groupId>
23-
<artifactId>a2a-java-sdk-client-config</artifactId>
23+
<artifactId>a2a-java-sdk-http-client</artifactId>
2424
</dependency>
2525
<dependency>
2626
<groupId>${project.groupId}</groupId>
27-
<artifactId>a2a-java-sdk-http-client</artifactId>
27+
<artifactId>a2a-java-sdk-client-transport-spi</artifactId>
2828
</dependency>
2929
<dependency>
3030
<groupId>${project.groupId}</groupId>
31-
<artifactId>a2a-java-sdk-client-transport-spi</artifactId>
31+
<artifactId>a2a-java-sdk-client-transport-jsonrpc</artifactId>
32+
</dependency>
33+
<dependency>
34+
<groupId>${project.groupId}</groupId>
35+
<artifactId>a2a-java-sdk-client-transport-grpc</artifactId>
36+
<scope>test</scope>
3237
</dependency>
3338
<dependency>
3439
<groupId>${project.groupId}</groupId>

client/base/src/main/java/io/a2a/A2A.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import java.util.Collections;
44
import java.util.Map;
55

6-
import io.a2a.client.A2ACardResolver;
6+
import io.a2a.client.http.A2ACardResolver;
77
import io.a2a.client.http.A2AHttpClient;
88
import io.a2a.client.http.JdkA2AHttpClient;
99
import io.a2a.spec.A2AClientError;

client/base/src/main/java/io/a2a/client/AbstractClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import java.util.function.BiConsumer;
88
import java.util.function.Consumer;
99

10-
import io.a2a.client.config.ClientCallContext;
10+
import io.a2a.client.transport.spi.interceptors.ClientCallContext;
1111
import io.a2a.spec.A2AClientException;
1212
import io.a2a.spec.AgentCard;
1313
import io.a2a.spec.DeleteTaskPushNotificationConfigParams;
@@ -387,4 +387,4 @@ public Consumer<Throwable> getStreamingErrorHandler() {
387387
return streamingErrorHandler;
388388
}
389389

390-
}
390+
}

client/base/src/main/java/io/a2a/client/Client.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import java.util.function.BiConsumer;
66
import java.util.function.Consumer;
77

8-
import io.a2a.client.config.ClientCallContext;
98
import io.a2a.client.config.ClientConfig;
9+
import io.a2a.client.transport.spi.interceptors.ClientCallContext;
1010
import io.a2a.client.transport.spi.ClientTransport;
1111
import io.a2a.spec.A2AClientError;
1212
import io.a2a.spec.A2AClientException;
@@ -28,20 +28,27 @@
2828
import io.a2a.spec.TaskQueryParams;
2929
import io.a2a.spec.TaskStatusUpdateEvent;
3030

31+
import static io.a2a.util.Assert.checkNotNullParam;
32+
3133
public class Client extends AbstractClient {
3234

3335
private final ClientConfig clientConfig;
3436
private final ClientTransport clientTransport;
3537
private AgentCard agentCard;
3638

37-
public Client(AgentCard agentCard, ClientConfig clientConfig, ClientTransport clientTransport,
39+
Client(AgentCard agentCard, ClientConfig clientConfig, ClientTransport clientTransport,
3840
List<BiConsumer<ClientEvent, AgentCard>> consumers, Consumer<Throwable> streamingErrorHandler) {
3941
super(consumers, streamingErrorHandler);
42+
checkNotNullParam("agentCard", agentCard);
43+
4044
this.agentCard = agentCard;
4145
this.clientConfig = clientConfig;
4246
this.clientTransport = clientTransport;
4347
}
4448

49+
public static ClientBuilder builder(AgentCard agentCard) {
50+
return new ClientBuilder(agentCard);
51+
}
4552

4653
@Override
4754
public void sendMessage(Message request, ClientCallContext context) throws A2AClientException {
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package io.a2a.client;
2+
3+
import io.a2a.client.config.ClientConfig;
4+
import io.a2a.client.transport.spi.ClientTransport;
5+
import io.a2a.client.transport.spi.ClientTransportConfig;
6+
import io.a2a.client.transport.spi.ClientTransportConfigBuilder;
7+
import io.a2a.client.transport.spi.ClientTransportProvider;
8+
import io.a2a.spec.A2AClientException;
9+
import io.a2a.spec.AgentCard;
10+
import io.a2a.spec.AgentInterface;
11+
import io.a2a.spec.TransportProtocol;
12+
13+
import java.util.ArrayList;
14+
import java.util.HashMap;
15+
import java.util.LinkedHashMap;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.ServiceLoader;
19+
import java.util.function.BiConsumer;
20+
import java.util.function.Consumer;
21+
22+
public class ClientBuilder {
23+
24+
private static final Map<String, ClientTransportProvider<? extends ClientTransport, ? extends ClientTransportConfig<?>>> transportProviderRegistry = new HashMap<>();
25+
private static final Map<Class<? extends ClientTransport>, String> transportProtocolMapping = new HashMap<>();
26+
27+
static {
28+
ServiceLoader<ClientTransportProvider> loader = ServiceLoader.load(ClientTransportProvider.class);
29+
for (ClientTransportProvider<?, ?> transport : loader) {
30+
transportProviderRegistry.put(transport.getTransportProtocol(), transport);
31+
transportProtocolMapping.put(transport.getTransportProtocolClass(), transport.getTransportProtocol());
32+
}
33+
}
34+
35+
private final AgentCard agentCard;
36+
37+
private final List<BiConsumer<ClientEvent, AgentCard>> consumers = new ArrayList<>();
38+
private Consumer<Throwable> streamErrorHandler;
39+
private ClientConfig clientConfig;
40+
41+
private final Map<Class<? extends ClientTransport>, ClientTransportConfig<? extends ClientTransport>> clientTransports = new LinkedHashMap<>();
42+
43+
ClientBuilder(AgentCard agentCard) {
44+
this.agentCard = agentCard;
45+
}
46+
47+
public <T extends ClientTransport> ClientBuilder withTransport(Class<T> clazz, ClientTransportConfigBuilder<? extends ClientTransportConfig<T>, ?> configBuilder) {
48+
return withTransport(clazz, configBuilder.build());
49+
}
50+
51+
public <T extends ClientTransport> ClientBuilder withTransport(Class<T> clazz, ClientTransportConfig<T> config) {
52+
clientTransports.put(clazz, config);
53+
54+
return this;
55+
}
56+
57+
public ClientBuilder addConsumer(BiConsumer<ClientEvent, AgentCard> consumer) {
58+
this.consumers.add(consumer);
59+
return this;
60+
}
61+
62+
public ClientBuilder addConsumers(List<BiConsumer<ClientEvent, AgentCard>> consumers) {
63+
this.consumers.addAll(consumers);
64+
return this;
65+
}
66+
67+
public ClientBuilder streamingErrorHandler(Consumer<Throwable> streamErrorHandler) {
68+
this.streamErrorHandler = streamErrorHandler;
69+
return this;
70+
}
71+
72+
public ClientBuilder clientConfig(ClientConfig clientConfig) {
73+
this.clientConfig = clientConfig;
74+
return this;
75+
}
76+
77+
public Client build() throws A2AClientException {
78+
if (this.clientConfig == null) {
79+
this.clientConfig = new ClientConfig.Builder().build();
80+
}
81+
82+
ClientTransport clientTransport = buildClientTransport();
83+
84+
return new Client(agentCard, clientConfig, clientTransport, consumers, streamErrorHandler);
85+
}
86+
87+
@SuppressWarnings("unchecked")
88+
private ClientTransport buildClientTransport() throws A2AClientException {
89+
// Get the preferred transport
90+
AgentInterface agentInterface = findBestClientTransport();
91+
Class<? extends ClientTransport> transportProtocolClass = transportProviderRegistry.get(agentInterface.transport()).getTransportProtocolClass();
92+
93+
// Get the transport provider associated to the protocol
94+
ClientTransportProvider clientTransportProvider = transportProviderRegistry.get(agentInterface.transport());
95+
96+
// Retrieve the configuration associated to the preferred transport
97+
ClientTransportConfig<? extends ClientTransport> clientTransportConfig = clientTransports.get(transportProtocolClass);
98+
99+
if (clientTransportConfig == null) {
100+
throw new A2AClientException("Missing required TransportConfig for " + agentInterface.transport());
101+
}
102+
103+
return clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url());
104+
}
105+
106+
private Map<String, String> getServerPreferredTransports() {
107+
Map<String, String> serverPreferredTransports = new LinkedHashMap<>();
108+
serverPreferredTransports.put(agentCard.preferredTransport(), agentCard.url());
109+
if (agentCard.additionalInterfaces() != null) {
110+
for (AgentInterface agentInterface : agentCard.additionalInterfaces()) {
111+
serverPreferredTransports.putIfAbsent(agentInterface.transport(), agentInterface.url());
112+
}
113+
}
114+
return serverPreferredTransports;
115+
}
116+
117+
private List<String> getClientPreferredTransports() {
118+
List<String> supportedClientTransports = new ArrayList<>();
119+
120+
if (clientTransports.isEmpty()) {
121+
// default to JSONRPC if not specified
122+
supportedClientTransports.add(TransportProtocol.JSONRPC.asString());
123+
} else {
124+
clientTransports.forEach((aClass, clientTransportConfig) -> supportedClientTransports.add(transportProtocolMapping.get(aClass)));
125+
}
126+
return supportedClientTransports;
127+
}
128+
129+
private AgentInterface findBestClientTransport() throws A2AClientException {
130+
// Retrieve transport supported by the A2A server
131+
Map<String, String> serverPreferredTransports = getServerPreferredTransports();
132+
133+
// Retrieve transport configured for this client (using withTransport methods)
134+
List<String> clientPreferredTransports = getClientPreferredTransports();
135+
136+
String transportProtocol = null;
137+
String transportUrl = null;
138+
if (clientConfig.isUseClientPreference()) {
139+
for (String clientPreferredTransport : clientPreferredTransports) {
140+
if (serverPreferredTransports.containsKey(clientPreferredTransport)) {
141+
transportProtocol = clientPreferredTransport;
142+
transportUrl = serverPreferredTransports.get(transportProtocol);
143+
break;
144+
}
145+
}
146+
} else {
147+
for (Map.Entry<String, String> transport : serverPreferredTransports.entrySet()) {
148+
if (clientPreferredTransports.contains(transport.getKey())) {
149+
transportProtocol = transport.getKey();
150+
transportUrl = transport.getValue();
151+
break;
152+
}
153+
}
154+
}
155+
if (transportProtocol == null || transportUrl == null) {
156+
throw new A2AClientException("No compatible transport found");
157+
}
158+
if (! transportProviderRegistry.containsKey(transportProtocol)) {
159+
throw new A2AClientException("No client available for " + transportProtocol);
160+
}
161+
162+
return new AgentInterface(transportProtocol, transportUrl);
163+
}
164+
}

0 commit comments

Comments
 (0)