Skip to content

Commit 7af5e84

Browse files
committed
feat: Provide a more friendly builder-based way to configure transport configuration
1 parent 26ab76d commit 7af5e84

File tree

43 files changed

+520
-396
lines changed

Some content is hidden

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

43 files changed

+520
-396
lines changed

README.md

Lines changed: 38 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,40 @@ 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 JSONRPCTransportConfigBuilder().build())
308+
.addStreamConsumers(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 JSONRPCTransportConfigBuilder()
336+
.httpClient(customHttpClient).build())
334337
.build();
335338
```
336339

@@ -346,13 +349,16 @@ Function<String, Channel> channelFactory = agentUrl -> {
346349
.build();
347350
};
348351

349-
// Create gRPC transport configuration
350-
GrpcTransportConfig grpcConfig = new GrpcTransportConfig(channelFactory);
351-
352352
// Configure the client with transport-specific settings
353353
ClientConfig clientConfig = new ClientConfig.Builder()
354354
.setAcceptedOutputModes(List.of("text"))
355-
.setClientTransportConfigs(List.of(grpcConfig))
355+
.build();
356+
357+
Client client = Client
358+
.builder(agentCard)
359+
.clientConfig(clientConfig)
360+
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder()
361+
.channelFactory(channelFactory).build())
356362
.build();
357363
```
358364

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

364370
```java
365371
// 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();
372+
Client client = Client
373+
.builder(agentCard)
374+
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().build())
375+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfigBuilder().build())
376+
.build();
375377
```
376378

377379
#### 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: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
return clientTransportProvider.create(clientTransportConfig, agentCard, agentInterface.url());
100+
}
101+
102+
private Map<String, String> getServerPreferredTransports() {
103+
Map<String, String> serverPreferredTransports = new LinkedHashMap<>();
104+
serverPreferredTransports.put(agentCard.preferredTransport(), agentCard.url());
105+
if (agentCard.additionalInterfaces() != null) {
106+
for (AgentInterface agentInterface : agentCard.additionalInterfaces()) {
107+
serverPreferredTransports.putIfAbsent(agentInterface.transport(), agentInterface.url());
108+
}
109+
}
110+
return serverPreferredTransports;
111+
}
112+
113+
private List<String> getClientPreferredTransports() {
114+
List<String> supportedClientTransports = new ArrayList<>();
115+
116+
if (clientTransports.isEmpty()) {
117+
// default to JSONRPC if not specified
118+
supportedClientTransports.add(TransportProtocol.JSONRPC.asString());
119+
} else {
120+
clientTransports.forEach((aClass, clientTransportConfig) -> supportedClientTransports.add(transportProtocolMapping.get(aClass)));
121+
}
122+
return supportedClientTransports;
123+
}
124+
125+
private AgentInterface findBestClientTransport() throws A2AClientException {
126+
// Retrieve transport supported by the A2A server
127+
Map<String, String> serverPreferredTransports = getServerPreferredTransports();
128+
129+
// Retrieve transport configured for this client (using withTransport methods)
130+
List<String> clientPreferredTransports = getClientPreferredTransports();
131+
132+
String transportProtocol = null;
133+
String transportUrl = null;
134+
if (clientConfig.isUseClientPreference()) {
135+
for (String clientPreferredTransport : clientPreferredTransports) {
136+
if (serverPreferredTransports.containsKey(clientPreferredTransport)) {
137+
transportProtocol = clientPreferredTransport;
138+
transportUrl = serverPreferredTransports.get(transportProtocol);
139+
break;
140+
}
141+
}
142+
} else {
143+
for (Map.Entry<String, String> transport : serverPreferredTransports.entrySet()) {
144+
if (clientPreferredTransports.contains(transport.getKey())) {
145+
transportProtocol = transport.getKey();
146+
transportUrl = transport.getValue();
147+
break;
148+
}
149+
}
150+
}
151+
if (transportProtocol == null || transportUrl == null) {
152+
throw new A2AClientException("No compatible transport found");
153+
}
154+
if (! transportProviderRegistry.containsKey(transportProtocol)) {
155+
throw new A2AClientException("No client available for " + transportProtocol);
156+
}
157+
158+
return new AgentInterface(transportProtocol, transportUrl);
159+
}
160+
}

0 commit comments

Comments
 (0)