Skip to content
Closed
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
69 changes: 69 additions & 0 deletions examples/spring-helloworld/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-parent</artifactId>
<version>0.2.3.Beta2-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>spring-helloworld</artifactId>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.4.5</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.a2asdk</groupId>
<artifactId>a2a-java-sdk-server-spring</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.a2a.sdk.apps.spring.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
*
*/
@SpringBootApplication
public class SpringHelloworld {

public static void main(String[] args) {
SpringApplication.run(SpringHelloworld.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.a2a.sdk.apps.spring.demo.config;

import io.a2a.A2A;
import io.a2a.server.agentexecution.AgentExecutor;
import io.a2a.server.agentexecution.RequestContext;
import io.a2a.server.events.EventQueue;
import io.a2a.server.tasks.TaskUpdater;
import io.a2a.spec.JSONRPCError;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* A2A server config
*/
@Configuration
public class A2AServerConfig {

@Bean
public AgentExecutor agentExecutor() {
return new AgentExecutor() {
@Override
public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
eventQueue.enqueueEvent(context.getMessage() != null ? context.getMessage() : context.getTask());
eventQueue.enqueueEvent(A2A.toAgentMessage("Hello World"));
}

@Override
public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError {
TaskUpdater taskUpdater = new TaskUpdater(context, eventQueue);
taskUpdater.cancel();
};
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package io.a2a.sdk.apps.spring.demo.endpoint;

import jakarta.annotation.Resource;
import java.util.concurrent.Flow;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;

import io.a2a.server.ExtendedAgentCard;
import io.a2a.server.requesthandlers.JSONRPCHandler;
import io.a2a.spec.AgentCard;
import io.a2a.spec.CancelTaskRequest;
import io.a2a.spec.GetTaskPushNotificationConfigRequest;
import io.a2a.spec.GetTaskRequest;
import io.a2a.spec.IdJsonMappingException;
import io.a2a.spec.InvalidParamsError;
import io.a2a.spec.InvalidParamsJsonMappingException;
import io.a2a.spec.InvalidRequestError;
import io.a2a.spec.JSONErrorResponse;
import io.a2a.spec.JSONParseError;
import io.a2a.spec.JSONRPCError;
import io.a2a.spec.JSONRPCErrorResponse;
import io.a2a.spec.JSONRPCRequest;
import io.a2a.spec.JSONRPCResponse;
import io.a2a.spec.MethodNotFoundError;
import io.a2a.spec.MethodNotFoundJsonMappingException;
import io.a2a.spec.NonStreamingJSONRPCRequest;
import io.a2a.spec.SendMessageRequest;
import io.a2a.spec.SendStreamingMessageRequest;
import io.a2a.spec.SetTaskPushNotificationConfigRequest;
import io.a2a.spec.StreamingJSONRPCRequest;
import io.a2a.spec.TaskResubscriptionRequest;
import io.a2a.spec.UnsupportedOperationError;
import io.a2a.util.Utils;
import reactor.core.publisher.Flux;

/**
* Spring Boot REST controller for A2A (Agent2Agent) protocol endpoints.
* Provides endpoints for JSON-RPC communication and agent card retrieval.
*/
@RestController
@RequestMapping("/")
public class A2AServerController {

@Resource
private JSONRPCHandler jsonRpcHandler;

@Autowired(required = false)
@ExtendedAgentCard
private AgentCard extendedAgentCard;

/**
* Handles incoming POST requests to the main A2A endpoint.
* Dispatches the request to the appropriate JSON-RPC handler method.
*
* @param requestBody the JSON-RPC request body as string
* @return the JSON-RPC response
*/
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<JSONRPCResponse<?>> handleNonStreamingRequests(@RequestBody String requestBody) {
try {
NonStreamingJSONRPCRequest<?> request = Utils.OBJECT_MAPPER.readValue(requestBody, NonStreamingJSONRPCRequest.class);
JSONRPCResponse<?> response = processNonStreamingRequest(request);
return ResponseEntity.ok(response);
} catch (JsonProcessingException e) {
JSONRPCErrorResponse error = handleError(e);
return ResponseEntity.ok(error);
} catch (Throwable t) {
JSONRPCErrorResponse error = new JSONRPCErrorResponse(new io.a2a.spec.InternalError(t.getMessage()));
return ResponseEntity.ok(error);
}
}

/**
* Handles incoming POST requests for streaming operations using Server-Sent Events (SSE).
* Dispatches the request to the appropriate JSON-RPC handler method.
*
* @param requestBody the JSON-RPC request body as string
* @return a Flux of ServerSentEvent containing the streaming responses
*/
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<JSONRPCResponse<?>>> handleStreamingRequests(@RequestBody String requestBody) {
try {
StreamingJSONRPCRequest<?> request = Utils.OBJECT_MAPPER.readValue(requestBody, StreamingJSONRPCRequest.class);
return processStreamingRequest(request);
} catch (JsonProcessingException e) {
JSONRPCErrorResponse error = handleError(e);
return Flux.just(ServerSentEvent.<JSONRPCResponse<?>>builder()
.data(error)
.build());
} catch (Throwable t) {
JSONRPCErrorResponse error = new JSONRPCErrorResponse(new io.a2a.spec.InternalError(t.getMessage()));
return Flux.just(ServerSentEvent.<JSONRPCResponse<?>>builder()
.data(error)
.build());
}
}

/**
* Handles incoming GET requests to the agent card endpoint.
* Returns the agent card in JSON format.
*
* @return the agent card
*/
@GetMapping(path = "/.well-known/agent.json", produces = MediaType.APPLICATION_JSON_VALUE)
public AgentCard getAgentCard() {
return jsonRpcHandler.getAgentCard();
}

/**
* Handles incoming GET requests to the authenticated extended agent card endpoint.
* Returns the extended agent card in JSON format.
*
* @return the authenticated extended agent card
*/
@GetMapping(path = "/agent/authenticatedExtendedCard", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> getAuthenticatedExtendedAgentCard() {
// TODO: Add authentication for this endpoint
// https://github.com/a2aproject/a2a-java/issues/77
if (!jsonRpcHandler.getAgentCard().supportsAuthenticatedExtendedCard()) {
JSONErrorResponse errorResponse = new JSONErrorResponse("Extended agent card not supported or not enabled.");
return ResponseEntity.status(404).body(errorResponse);
}
if (extendedAgentCard == null) {
JSONErrorResponse errorResponse = new JSONErrorResponse("Authenticated extended agent card is supported but not configured on the server.");
return ResponseEntity.status(404).body(errorResponse);
}
return ResponseEntity.ok(extendedAgentCard);
}

private JSONRPCResponse<?> processNonStreamingRequest(NonStreamingJSONRPCRequest<?> request) {
if (request instanceof GetTaskRequest) {
return jsonRpcHandler.onGetTask((GetTaskRequest) request);
} else if (request instanceof CancelTaskRequest) {
return jsonRpcHandler.onCancelTask((CancelTaskRequest) request);
} else if (request instanceof SetTaskPushNotificationConfigRequest) {
return jsonRpcHandler.setPushNotification((SetTaskPushNotificationConfigRequest) request);
} else if (request instanceof GetTaskPushNotificationConfigRequest) {
return jsonRpcHandler.getPushNotification((GetTaskPushNotificationConfigRequest) request);
} else if (request instanceof SendMessageRequest) {
return jsonRpcHandler.onMessageSend((SendMessageRequest) request);
} else {
return generateErrorResponse(request, new UnsupportedOperationError());
}
}

private Flux<ServerSentEvent<JSONRPCResponse<?>>> processStreamingRequest(StreamingJSONRPCRequest<?> request) {
Flow.Publisher<? extends JSONRPCResponse<?>> publisher;
if (request instanceof SendStreamingMessageRequest) {
publisher = jsonRpcHandler.onMessageSendStream((SendStreamingMessageRequest) request);
} else if (request instanceof TaskResubscriptionRequest) {
publisher = jsonRpcHandler.onResubscribeToTask((TaskResubscriptionRequest) request);
} else {
return Flux.just(ServerSentEvent.<JSONRPCResponse<?>>builder()
.data(generateErrorResponse(request, new UnsupportedOperationError()))
.build());
}

return Flux.create(sink -> {
publisher.subscribe(new Flow.Subscriber<JSONRPCResponse<?>>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}

@Override
public void onNext(JSONRPCResponse<?> item) {
sink.next(ServerSentEvent.<JSONRPCResponse<?>>builder()
.data(item)
.build());
}

@Override
public void onError(Throwable throwable) {
sink.error(throwable);
}

@Override
public void onComplete() {
sink.complete();
}
});
});
}

private JSONRPCErrorResponse handleError(JsonProcessingException exception) {
Object id = null;
JSONRPCError jsonRpcError = null;
if (exception.getCause() instanceof JsonParseException) {
jsonRpcError = new JSONParseError();
} else if (exception instanceof com.fasterxml.jackson.core.io.JsonEOFException) {
jsonRpcError = new JSONParseError(exception.getMessage());
} else if (exception instanceof MethodNotFoundJsonMappingException err) {
id = err.getId();
jsonRpcError = new MethodNotFoundError();
} else if (exception instanceof InvalidParamsJsonMappingException err) {
id = err.getId();
jsonRpcError = new InvalidParamsError();
} else if (exception instanceof IdJsonMappingException err) {
id = err.getId();
jsonRpcError = new InvalidRequestError();
} else {
jsonRpcError = new InvalidRequestError();
}
return new JSONRPCErrorResponse(id, jsonRpcError);
}

private JSONRPCResponse<?> generateErrorResponse(JSONRPCRequest<?> request, JSONRPCError error) {
return new JSONRPCErrorResponse(request.getId(), error);
}

}
Loading