Skip to content

Commit c0910e1

Browse files
ehsavoiefjuma
andauthored
feat: add support for JSON+HTTP/REST (#229)
* Create JSON+HTTP/REST Client. * Integrate PR from @ronantakizawa using regexp for server routing. * Update server code to use proper JSON (de)serialization with Proto. Signed-off-by: Emmanuel Hugonnet <[email protected]> Co-authored-by: Farah Juma <[email protected]>
1 parent 1adb96c commit c0910e1

File tree

57 files changed

+3796
-82
lines changed

Some content is hidden

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

57 files changed

+3796
-82
lines changed

.github/workflows/run-tck.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ env:
1717
UV_SYSTEM_PYTHON: 1
1818
# SUT_JSONRPC_URL to use for the TCK and the server agent
1919
SUT_JSONRPC_URL: http://localhost:9999
20+
# Slow system on CI
21+
TCK_STREAMING_TIMEOUT: 5.0
2022

2123
# Only run the latest job
2224
concurrency:
@@ -55,7 +57,7 @@ jobs:
5557
- name: Build with Maven, skipping tests
5658
run: mvn -B install -DskipTests
5759
- name: Start SUT
58-
run: SUT_GRPC_URL=${{ env.SUT_JSONRPC_URL }} mvn -B quarkus:dev & #SUT_JSONRPC_URL already set
60+
run: SUT_GRPC_URL=${{ env.SUT_JSONRPC_URL }} SUT_REST_URL=${{ env.SUT_JSONRPC_URL }} mvn -B quarkus:dev & #SUT_JSONRPC_URL already set
5961
working-directory: tck
6062
- name: Wait for SUT to start
6163
run: |
@@ -93,5 +95,5 @@ jobs:
9395
9496
- name: Run TCK
9597
run: |
96-
./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category all --transports jsonrpc,grpc --compliance-report report.json
98+
./run_tck.py --sut-url ${{ env.SUT_JSONRPC_URL }} --category all --transports jsonrpc,grpc,rest --compliance-report report.json
9799
working-directory: tck/a2a-tck

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,3 @@ nb-configuration.xml
4444
# TLS Certificates
4545
.certs/
4646
nbproject/
47-

README.md

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,18 @@ To use the reference implementation with the gRPC protocol, add the following de
8181
Note that you can add more than one of the above dependencies to your project depending on the transports
8282
you'd like to support.
8383

84-
Support for the HTTP+JSON/REST transport will be coming soon.
84+
To use the reference implementation with the HTTP+JSON/REST protocol, add the following dependency to your project:
85+
86+
> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.*
87+
88+
```xml
89+
<dependency>
90+
<groupId>io.github.a2asdk</groupId>
91+
<artifactId>a2a-java-sdk-reference-rest</artifactId>
92+
<!-- Use a released version from https://github.com/a2aproject/a2a-java/releases -->
93+
<version>${io.a2a.sdk.version}</version>
94+
</dependency>
95+
```
8596

8697
### 2. Add a class that creates an A2A Agent Card
8798

@@ -117,7 +128,7 @@ public class WeatherAgentCardProducer {
117128
.tags(Collections.singletonList("weather"))
118129
.examples(List.of("weather in LA, CA"))
119130
.build()))
120-
.protocolVersion("0.2.5")
131+
.protocolVersion("0.3.0")
121132
.build();
122133
}
123134
}
@@ -247,7 +258,7 @@ By default, the sdk-client is coming with the JSONRPC transport dependency. Desp
247258
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).
248259

249260

250-
If you want to use another transport (such as GRPC or HTTP+JSON), you'll need to add a relevant dependency:
261+
If you want to use the gRPC transport, you'll need to add a relevant dependency:
251262

252263
----
253264
> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.*
@@ -262,7 +273,21 @@ If you want to use another transport (such as GRPC or HTTP+JSON), you'll need to
262273
</dependency>
263274
```
264275

265-
Support for the HTTP+JSON/REST transport will be coming soon.
276+
277+
If you want to use the HTTP+JSON/REST transport, you'll need to add a relevant dependency:
278+
279+
----
280+
> *⚠️ The `io.github.a2asdk` `groupId` below is temporary and will likely change for future releases.*
281+
----
282+
283+
```xml
284+
<dependency>
285+
<groupId>io.github.a2asdk</groupId>
286+
<artifactId>a2a-java-sdk-client-transport-rest</artifactId>
287+
<!-- Use a released version from https://github.com/a2aproject/a2a-java/releases -->
288+
<version>${io.a2a.sdk.version}</version>
289+
</dependency>
290+
```
266291

267292
### Sample Usage
268293

@@ -360,6 +385,29 @@ Client client = Client
360385
.build();
361386
```
362387

388+
389+
##### HTTP+JSON/REST Transport Configuration
390+
391+
For the HTTP+JSON/REST transport, if you'd like to use the default `JdkA2AHttpClient`, provide a `RestTransportConfig` created with its default constructor.
392+
393+
To use a custom HTTP client implementation, simply create a `RestTransportConfig` as follows:
394+
395+
```java
396+
// Create a custom HTTP client
397+
A2AHttpClient customHttpClient = ...
398+
399+
// Configure the client settings
400+
ClientConfig clientConfig = new ClientConfig.Builder()
401+
.setAcceptedOutputModes(List.of("text"))
402+
.build();
403+
404+
Client client = Client
405+
.builder(agentCard)
406+
.clientConfig(clientConfig)
407+
.withTransport(RestTransport.class, new RestTransportConfig(customHttpClient))
408+
.build();
409+
```
410+
363411
##### Multiple Transport Configurations
364412

365413
You can specify configuration for multiple transports, the appropriate configuration
@@ -371,6 +419,7 @@ Client client = Client
371419
.builder(agentCard)
372420
.withTransport(GrpcTransport.class, new GrpcTransportConfig(channelFactory))
373421
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
422+
.withTransport(RestTransport.class, new RestTransportConfig())
374423
.build();
375424
```
376425

client/base/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
<artifactId>a2a-java-sdk-client-transport-grpc</artifactId>
3636
<scope>test</scope>
3737
</dependency>
38+
<dependency>
39+
<groupId>${project.groupId}</groupId>
40+
<artifactId>a2a-java-sdk-client-transport-rest</artifactId>
41+
<scope>test</scope>
42+
</dependency>
3843
<dependency>
3944
<groupId>${project.groupId}</groupId>
4045
<artifactId>a2a-java-sdk-common</artifactId>
@@ -54,6 +59,11 @@
5459
<artifactId>mockserver-netty</artifactId>
5560
<scope>test</scope>
5661
</dependency>
62+
<dependency>
63+
<groupId>org.slf4j</groupId>
64+
<artifactId>slf4j-jdk14</artifactId>
65+
<scope>test</scope>
66+
</dependency>
5767
</dependencies>
5868

5969
</project>

client/transport/grpc/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@
3333
<groupId>${project.groupId}</groupId>
3434
<artifactId>a2a-java-sdk-client-transport-spi</artifactId>
3535
</dependency>
36+
<dependency>
37+
<groupId>io.grpc</groupId>
38+
<artifactId>grpc-protobuf</artifactId>
39+
</dependency>
40+
<dependency>
41+
<groupId>io.grpc</groupId>
42+
<artifactId>grpc-stub</artifactId>
43+
</dependency>
3644
<dependency>
3745
<groupId>org.junit.jupiter</groupId>
3846
<artifactId>junit-jupiter-api</artifactId>

client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import static io.a2a.client.transport.jsonrpc.JsonMessages.AGENT_CARD;
44
import static io.a2a.client.transport.jsonrpc.JsonMessages.AGENT_CARD_SUPPORTS_EXTENDED;
5-
import static io.a2a.client.transport.jsonrpc.JsonMessages.AUTHENTICATION_EXTENDED_AGENT_CARD;
65
import static io.a2a.client.transport.jsonrpc.JsonMessages.CANCEL_TASK_TEST_REQUEST;
76
import static io.a2a.client.transport.jsonrpc.JsonMessages.CANCEL_TASK_TEST_RESPONSE;
87
import static io.a2a.client.transport.jsonrpc.JsonMessages.GET_AUTHENTICATED_EXTENDED_AGENT_CARD_REQUEST;
@@ -50,7 +49,6 @@
5049
import io.a2a.spec.FilePart;
5150
import io.a2a.spec.FileWithBytes;
5251
import io.a2a.spec.FileWithUri;
53-
import io.a2a.spec.GetAuthenticatedExtendedCardResponse;
5452
import io.a2a.spec.GetTaskPushNotificationConfigParams;
5553
import io.a2a.spec.Message;
5654
import io.a2a.spec.MessageSendConfiguration;

client/transport/rest/pom.xml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?xml version="1.0"?>
2+
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>io.github.a2asdk</groupId>
9+
<artifactId>a2a-java-sdk-parent</artifactId>
10+
<version>0.3.0.Beta1-SNAPSHOT</version>
11+
<relativePath>../../../pom.xml</relativePath>
12+
</parent>
13+
<artifactId>a2a-java-sdk-client-transport-rest</artifactId>
14+
<packaging>jar</packaging>
15+
16+
<name>Java SDK A2A Client Transport: JSON+HTTP/REST</name>
17+
<description>Java SDK for the Agent2Agent Protocol (A2A) - JSON+HTTP/REST Client Transport</description>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>${project.groupId}</groupId>
22+
<artifactId>a2a-java-sdk-common</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>${project.groupId}</groupId>
26+
<artifactId>a2a-java-sdk-spec</artifactId>
27+
</dependency>
28+
<dependency>
29+
<groupId>${project.groupId}</groupId>
30+
<artifactId>a2a-java-sdk-spec-grpc</artifactId>
31+
</dependency>
32+
<dependency>
33+
<groupId>${project.groupId}</groupId>
34+
<artifactId>a2a-java-sdk-client-transport-spi</artifactId>
35+
</dependency>
36+
<dependency>
37+
<groupId>io.github.a2asdk</groupId>
38+
<artifactId>a2a-java-sdk-http-client</artifactId>
39+
</dependency>
40+
<dependency>
41+
<groupId>com.google.protobuf</groupId>
42+
<artifactId>protobuf-java-util</artifactId>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.junit.jupiter</groupId>
46+
<artifactId>junit-jupiter-api</artifactId>
47+
<scope>test</scope>
48+
</dependency>
49+
50+
<dependency>
51+
<groupId>org.mock-server</groupId>
52+
<artifactId>mockserver-netty</artifactId>
53+
<scope>test</scope>
54+
</dependency>
55+
<dependency>
56+
<groupId>org.slf4j</groupId>
57+
<artifactId>slf4j-jdk14</artifactId>
58+
<scope>test</scope>
59+
</dependency>
60+
</dependencies>
61+
62+
</project>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.a2a.client.transport.rest;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.JsonNode;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
7+
import io.a2a.client.http.A2AHttpResponse;
8+
import io.a2a.spec.A2AClientException;
9+
import io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError;
10+
import io.a2a.spec.ContentTypeNotSupportedError;
11+
import io.a2a.spec.InternalError;
12+
import io.a2a.spec.InvalidAgentResponseError;
13+
import io.a2a.spec.InvalidParamsError;
14+
import io.a2a.spec.InvalidRequestError;
15+
import io.a2a.spec.JSONParseError;
16+
import io.a2a.spec.MethodNotFoundError;
17+
import io.a2a.spec.PushNotificationNotSupportedError;
18+
import io.a2a.spec.TaskNotCancelableError;
19+
import io.a2a.spec.TaskNotFoundError;
20+
import io.a2a.spec.UnsupportedOperationError;
21+
import java.util.logging.Level;
22+
import java.util.logging.Logger;
23+
24+
/**
25+
* Utility class to A2AHttpResponse to appropriate A2A error types
26+
*/
27+
public class RestErrorMapper {
28+
29+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule());
30+
31+
public static A2AClientException mapRestError(A2AHttpResponse response) {
32+
return RestErrorMapper.mapRestError(response.body(), response.status());
33+
}
34+
35+
public static A2AClientException mapRestError(String body, int code) {
36+
try {
37+
if (body != null && !body.isBlank()) {
38+
JsonNode node = OBJECT_MAPPER.readTree(body);
39+
String className = node.findValue("error").asText();
40+
String errorMessage = node.findValue("message").asText();
41+
return mapRestError(className, errorMessage, code);
42+
}
43+
return mapRestError("", "", code);
44+
} catch (JsonProcessingException ex) {
45+
Logger.getLogger(RestErrorMapper.class.getName()).log(Level.SEVERE, null, ex);
46+
return new A2AClientException("Failed to parse error response: " + ex.getMessage());
47+
}
48+
}
49+
50+
public static A2AClientException mapRestError(String className, String errorMessage, int code) {
51+
switch (className) {
52+
case "io.a2a.spec.TaskNotFoundError":
53+
return new A2AClientException(errorMessage, new TaskNotFoundError());
54+
case "io.a2a.spec.AuthenticatedExtendedCardNotConfiguredError":
55+
return new A2AClientException(errorMessage, new AuthenticatedExtendedCardNotConfiguredError());
56+
case "io.a2a.spec.ContentTypeNotSupportedError":
57+
return new A2AClientException(errorMessage, new ContentTypeNotSupportedError(null, null, errorMessage));
58+
case "io.a2a.spec.InternalError":
59+
return new A2AClientException(errorMessage, new InternalError(errorMessage));
60+
case "io.a2a.spec.InvalidAgentResponseError":
61+
return new A2AClientException(errorMessage, new InvalidAgentResponseError(null, null, errorMessage));
62+
case "io.a2a.spec.InvalidParamsError":
63+
return new A2AClientException(errorMessage, new InvalidParamsError());
64+
case "io.a2a.spec.InvalidRequestError":
65+
return new A2AClientException(errorMessage, new InvalidRequestError());
66+
case "io.a2a.spec.JSONParseError":
67+
return new A2AClientException(errorMessage, new JSONParseError());
68+
case "io.a2a.spec.MethodNotFoundError":
69+
return new A2AClientException(errorMessage, new MethodNotFoundError());
70+
case "io.a2a.spec.PushNotificationNotSupportedError":
71+
return new A2AClientException(errorMessage, new PushNotificationNotSupportedError());
72+
case "io.a2a.spec.TaskNotCancelableError":
73+
return new A2AClientException(errorMessage, new TaskNotCancelableError());
74+
case "io.a2a.spec.UnsupportedOperationError":
75+
return new A2AClientException(errorMessage, new UnsupportedOperationError());
76+
default:
77+
return new A2AClientException(errorMessage);
78+
}
79+
}
80+
}

0 commit comments

Comments
 (0)