Skip to content

Commit 5e01ede

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

File tree

43 files changed

+532
-402
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

+532
-402
lines changed

README.md

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -243,21 +243,14 @@ 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.
247+
248+
If you want to use another transport (such as GRPC or HTTP+JSON), you'll need to add a relevant dependency:
247249

248250
----
249251
> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.*
250252
----
251253

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-
261254
```xml
262255
<dependency>
263256
<groupId>io.github.a2asdk</groupId>
@@ -305,14 +298,18 @@ Consumer<Throwable> errorHandler = error -> {
305298
...
306299
};
307300

308-
// Create the client using ClientFactory
309-
ClientFactory clientFactory = new ClientFactory(clientConfig);
310-
Client client = clientFactory.create(agentCard, consumers, errorHandler);
301+
// Create the client using the builder
302+
Client client = Client
303+
.from(finalAgentCard)
304+
.clientConfig(clientConfig)
305+
.addStreamConsumers(consumers)
306+
.streamErrorHandler(errorHandler)
307+
.build();
311308
```
312309

313310
#### Configuring Transport-Specific Settings
314311

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.
312+
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.
316313

317314
##### JSON-RPC Transport Configuration
318315

@@ -324,13 +321,16 @@ as follows:
324321
// Create a custom HTTP client
325322
A2AHttpClient customHttpClient = ...
326323

327-
// Create JSON-RPC transport configuration
328-
JSONRPCTransportConfig jsonrpcConfig = new JSONRPCTransportConfig(customHttpClient);
329-
330-
// Configure the client with transport-specific settings
324+
// Configure the client settings
331325
ClientConfig clientConfig = new ClientConfig.Builder()
332326
.setAcceptedOutputModes(List.of("text"))
333-
.setClientTransportConfigs(List.of(jsonrpcConfig))
327+
.build();
328+
329+
Client client = Client
330+
.from(agentCard)
331+
.clientConfig(clientConfig)
332+
.withJsonRpcTransport(new JSONRPCTransportConfigBuilder()
333+
.httpClient(customHttpClient).build())
334334
.build();
335335
```
336336

@@ -339,20 +339,19 @@ ClientConfig clientConfig = new ClientConfig.Builder()
339339
For the gRPC transport, you must configure a channel factory:
340340

341341
```java
342-
// Create a channel factory function that takes the agent URL and returns a Channel
343-
Function<String, Channel> channelFactory = agentUrl -> {
344-
return ManagedChannelBuilder.forTarget(agentUrl)
345-
...
346-
.build();
347-
};
348-
349-
// Create gRPC transport configuration
350-
GrpcTransportConfig grpcConfig = new GrpcTransportConfig(channelFactory);
342+
// Create a channel from agent URL
343+
Channel channel = ManagedChannelBuilder.forTarget(agentUrl).build();
351344

352345
// Configure the client with transport-specific settings
353346
ClientConfig clientConfig = new ClientConfig.Builder()
354347
.setAcceptedOutputModes(List.of("text"))
355-
.setClientTransportConfigs(List.of(grpcConfig))
348+
.build();
349+
350+
Client client = Client
351+
.from(agentCard)
352+
.clientConfig(clientConfig)
353+
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder()
354+
.channel(channel).build())
356355
.build();
357356
```
358357

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

364363
```java
365364
// 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();
365+
Client client = Client
366+
.from(agentCard)
367+
.withTransport(GrpcTransport.class, new GrpcTransportConfigBuilder().build())
368+
.withJsonRpcTransport(new JSONRPCTransportConfigBuilder().build())
369+
.build();
375370
```
376371

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

client/base/pom.xml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,24 @@
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>
24+
<version>${project.version}</version>
2425
</dependency>
2526
<dependency>
2627
<groupId>${project.groupId}</groupId>
27-
<artifactId>a2a-java-sdk-http-client</artifactId>
28+
<artifactId>a2a-java-sdk-client-transport-spi</artifactId>
29+
<version>${project.version}</version>
2830
</dependency>
2931
<dependency>
3032
<groupId>${project.groupId}</groupId>
31-
<artifactId>a2a-java-sdk-client-transport-spi</artifactId>
33+
<artifactId>a2a-java-sdk-client-transport-jsonrpc</artifactId>
34+
<version>${project.version}</version>
35+
</dependency>
36+
<dependency>
37+
<groupId>${project.groupId}</groupId>
38+
<artifactId>a2a-java-sdk-client-transport-grpc</artifactId>
39+
<version>${project.version}</version>
40+
<scope>test</scope>
3241
</dependency>
3342
<dependency>
3443
<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 from(AgentCard agentCard) {
50+
return new ClientBuilder(agentCard);
51+
}
4552

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

0 commit comments

Comments
 (0)