diff --git a/examples/spring-helloworld/pom.xml b/examples/spring-helloworld/pom.xml new file mode 100644 index 000000000..6b1432051 --- /dev/null +++ b/examples/spring-helloworld/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + io.github.a2asdk + a2a-java-sdk-parent + 0.2.3.Beta2-SNAPSHOT + ../../pom.xml + + + spring-helloworld + + + UTF-8 + 3.4.5 + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + io.github.a2asdk + a2a-java-sdk-server-spring + ${project.version} + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + diff --git a/examples/spring-helloworld/src/main/java/io/a2a/sdk/apps/spring/demo/SpringHelloworld.java b/examples/spring-helloworld/src/main/java/io/a2a/sdk/apps/spring/demo/SpringHelloworld.java new file mode 100644 index 000000000..a6aa6a87b --- /dev/null +++ b/examples/spring-helloworld/src/main/java/io/a2a/sdk/apps/spring/demo/SpringHelloworld.java @@ -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); + } +} diff --git a/examples/spring-helloworld/src/main/java/io/a2a/sdk/apps/spring/demo/config/A2AServerConfig.java b/examples/spring-helloworld/src/main/java/io/a2a/sdk/apps/spring/demo/config/A2AServerConfig.java new file mode 100644 index 000000000..083bed8c2 --- /dev/null +++ b/examples/spring-helloworld/src/main/java/io/a2a/sdk/apps/spring/demo/config/A2AServerConfig.java @@ -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(); + }; + }; + } +} diff --git a/examples/spring-helloworld/src/main/java/io/a2a/sdk/apps/spring/demo/endpoint/A2AServerController.java b/examples/spring-helloworld/src/main/java/io/a2a/sdk/apps/spring/demo/endpoint/A2AServerController.java new file mode 100644 index 000000000..3151426af --- /dev/null +++ b/examples/spring-helloworld/src/main/java/io/a2a/sdk/apps/spring/demo/endpoint/A2AServerController.java @@ -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> 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>> 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.>builder() + .data(error) + .build()); + } catch (Throwable t) { + JSONRPCErrorResponse error = new JSONRPCErrorResponse(new io.a2a.spec.InternalError(t.getMessage())); + return Flux.just(ServerSentEvent.>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>> processStreamingRequest(StreamingJSONRPCRequest request) { + Flow.Publisher> 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.>builder() + .data(generateErrorResponse(request, new UnsupportedOperationError())) + .build()); + } + + return Flux.create(sink -> { + publisher.subscribe(new Flow.Subscriber>() { + @Override + public void onSubscribe(Flow.Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + public void onNext(JSONRPCResponse item) { + sink.next(ServerSentEvent.>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); + } + +} diff --git a/examples/spring-helloworld/src/main/resources/application.yml b/examples/spring-helloworld/src/main/resources/application.yml new file mode 100644 index 000000000..6accbea51 --- /dev/null +++ b/examples/spring-helloworld/src/main/resources/application.yml @@ -0,0 +1,55 @@ +server: + port: 8089 + +spring: + application: + name: a2a-server + +management: + endpoints: + web: + exposure: + include: health + +logging: + level: + io.github.a2ap: DEBUG + org.springframework.web: INFO + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" + +# A2A specific configuration +a2a: + server: + enabled: true + id: "server-hello-world" + name: "A2A Java Server" + description: "A sample A2A agent implemented in Java" + version: "1.0.0" + url: "http://localhost:${server.port}/" + provider: + name: "A2A" + url: "https://github.com/a2aproject/a2a-java" + documentationUrl: "https://github.com/a2aproject/a2a-java" + capabilities: + streaming: true + pushNotifications: false + stateTransitionHistory: true + supportsAuthenticatedExtendedCard: true + defaultInputModes: + - "text" + defaultOutputModes: + - "text" + skills: + - name: "hello-world" + description: "A simple hello world skill" + tags: + - "greeting" + - "basic" + examples: + - "Say hello to me" + - "Greet me" + inputModes: + - "text" + outputModes: + - "text" diff --git a/pom.xml b/pom.xml index 9fd7cfeaf..ab72c66bb 100644 --- a/pom.xml +++ b/pom.xml @@ -271,10 +271,12 @@ common spec client + sdk-spring reference-impl tck examples/helloworld tests/server-common + examples/spring-helloworld @@ -303,4 +305,4 @@ - \ No newline at end of file + diff --git a/sdk-spring/README.md b/sdk-spring/README.md new file mode 100644 index 000000000..b98786434 --- /dev/null +++ b/sdk-spring/README.md @@ -0,0 +1,464 @@ +# A2A Java SDK - Spring Adapter + +This module provides a Spring Boot adapter for the A2A (Agent2Agent) protocol, allowing easy integration of A2A server functionality in Spring Boot applications. + +## Features + +- Complete JSON-RPC support +- Server-Sent Events (SSE) streaming response support +- Auto-configuration and Bean registration +- Configuration-based Agent Card setup +- Global exception handling +- Seamless integration with Spring Boot + +## Dependencies + +```xml + + io.a2a.sdk + a2a-java-sdk-server-spring + 0.2.4-SNAPSHOT + +``` + +## Quick Start + +### 1. Add Dependencies + +Add the following dependencies to your Spring Boot project's `pom.xml`: + +```xml + + + io.a2a.sdk + a2a-java-sdk-server-spring + 0.2.4-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-webflux + + +``` + +### 2. Configure Application Properties + +Configure your A2A server in `application.yml`: + +```yaml +server: + port: 8089 + +spring: + application: + name: a2a-server + +# A2A specific configuration +a2a: + server: + enabled: true + id: "my-agent-id" + name: "My A2A Agent" + description: "A sample A2A agent implemented in Java" + version: "1.0.0" + url: "http://localhost:${server.port}/" + provider: + name: "My Company" + url: "https://my-company.com" + documentationUrl: "https://my-company.com/docs" + capabilities: + streaming: true + pushNotifications: false + stateTransitionHistory: true + supportsAuthenticatedExtendedCard: true + defaultInputModes: + - "text" + defaultOutputModes: + - "text" + skills: + - name: "hello-world" + description: "A simple hello world skill" + tags: + - "greeting" + - "basic" + examples: + - "Say hello to me" + - "Greet me" + inputModes: + - "text" + outputModes: + - "text" +``` + +### 3. Implement Agent Logic + +Create a configuration class to provide the `AgentExecutor`: + +```java +@Configuration +public class A2AServerConfig { + + @Bean + public AgentExecutor agentExecutor() { + return new AgentExecutor() { + @Override + public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError { + // Echo the incoming message/task + eventQueue.enqueueEvent(context.getMessage() != null ? context.getMessage() : context.getTask()); + // Send a response + eventQueue.enqueueEvent(A2A.toAgentMessage("Hello World")); + } + + @Override + public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError { + TaskUpdater taskUpdater = new TaskUpdater(context, eventQueue); + taskUpdater.cancel(); + } + }; + } +} +``` + +### 4. Create Spring Boot Application + +Create a main application class: + +```java +@SpringBootApplication +public class A2AServerApplication { + + public static void main(String[] args) { + SpringApplication.run(A2AServerApplication.class, args); + } +} +``` + +### 5. Start the Application + +Start your Spring Boot application, and the A2A endpoints will be automatically available: + +- `POST /` - JSON-RPC endpoint +- `GET /.well-known/agent.json` - Agent Card endpoint +- `GET /agent/authenticatedExtendedCard` - Extended Agent Card endpoint + +## Endpoint Description + +### JSON-RPC Endpoint + +`POST /` - Handles all JSON-RPC requests + +Supported methods: +- `a2a.getTask` - Get task +- `a2a.cancelTask` - Cancel task +- `a2a.sendMessage` - Send message +- `a2a.sendStreamingMessage` - Send streaming message +- `a2a.resubscribeToTask` - Resubscribe to task +- `a2a.setTaskPushNotificationConfig` - Set push notification configuration +- `a2a.getTaskPushNotificationConfig` - Get push notification configuration + +### Agent Card Endpoint + +`GET /.well-known/agent.json` - Returns Agent Card information + +### Extended Agent Card Endpoint + +`GET /agent/authenticatedExtendedCard` - Returns authenticated extended Agent Card information + +## Streaming Responses + +For streaming requests (such as `sendStreamingMessage`), the endpoint will return responses in Server-Sent Events (SSE) format. + +## Exception Handling + +The module provides a global exception handler that automatically converts JSON parsing errors to appropriate JSON-RPC error responses. + +## Testing + +Run tests: + +```bash +mvn test +``` + +Tests include: +- Agent Card endpoint tests +- JSON-RPC request tests +- Error handling tests +- Streaming response tests + +## Configuration Options + +The A2A Spring adapter supports comprehensive configuration through Spring Boot's `application.yml` or `application.properties`. Here are the available configuration options: + +### Basic Configuration + +```yaml +a2a: + server: + enabled: true # Enable/disable A2A server + id: "my-agent-id" # Unique agent identifier + name: "My Agent" # Agent display name + description: "Agent description" # Agent description + version: "1.0.0" # Agent version + url: "http://localhost:8080/" # Agent base URL +``` + +### Provider Information + +```yaml +a2a: + server: + provider: + name: "My Company" + url: "https://my-company.com" + documentationUrl: "https://my-company.com/docs" +``` + +### Capabilities + +```yaml +a2a: + server: + capabilities: + streaming: true # Support streaming responses + pushNotifications: false # Support push notifications + stateTransitionHistory: true # Support state transition history + supportsAuthenticatedExtendedCard: true # Support authenticated extended card +``` + +### Input/Output Modes + +```yaml +a2a: + server: + defaultInputModes: + - "text" + - "json" + defaultOutputModes: + - "text" + - "json" +``` + +### Skills Configuration + +```yaml +a2a: + server: + skills: + - name: "skill-name" + description: "Skill description" + tags: + - "tag1" + - "tag2" + examples: + - "Example usage 1" + - "Example usage 2" + inputModes: + - "text" + outputModes: + - "text" +``` + +### Server Configuration + +```yaml +server: + port: 8080 # Custom port (optional) + +spring: + main: + web-application-type: servlet # Use servlet stack (required for SSE) +``` + +## Complete Example + +Here's a complete example of a simple A2A agent: + +### Project Structure + +``` +src/ +├── main/ +│ ├── java/ +│ │ └── com/example/ +│ │ ├── A2AServerApplication.java +│ │ └── config/ +│ │ └── A2AServerConfig.java +│ └── resources/ +│ └── application.yml +└── pom.xml +``` + +### pom.xml + +```xml + + + 4.0.0 + + com.example + my-a2a-agent + 1.0.0 + jar + + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + + + + io.a2a.sdk + a2a-java-sdk-server-spring + 0.2.4-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +``` + +### A2AServerApplication.java + +```java +package com.example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class A2AServerApplication { + + public static void main(String[] args) { + SpringApplication.run(A2AServerApplication.class, args); + } +} +``` + +### A2AServerConfig.java + +```java +package com.example.config; + +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.A2A; +import io.a2a.spec.JSONRPCError; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class A2AServerConfig { + + @Bean + public AgentExecutor agentExecutor() { + return new AgentExecutor() { + @Override + public void execute(RequestContext context, EventQueue eventQueue) throws JSONRPCError { + // Echo the incoming message/task + eventQueue.enqueueEvent(context.getMessage() != null ? context.getMessage() : context.getTask()); + // Send a response + eventQueue.enqueueEvent(A2A.toAgentMessage("Hello from A2A Agent!")); + } + + @Override + public void cancel(RequestContext context, EventQueue eventQueue) throws JSONRPCError { + TaskUpdater taskUpdater = new TaskUpdater(context, eventQueue); + taskUpdater.cancel(); + } + }; + } +} +``` + +### application.yml + +```yaml +server: + port: 8089 + +spring: + application: + name: my-a2a-agent + +a2a: + server: + enabled: true + id: "my-agent-id" + name: "My A2A Agent" + description: "A sample A2A agent implemented in Java" + version: "1.0.0" + url: "http://localhost:${server.port}/" + provider: + name: "My Company" + url: "https://my-company.com" + documentationUrl: "https://my-company.com/docs" + capabilities: + streaming: true + pushNotifications: false + stateTransitionHistory: true + supportsAuthenticatedExtendedCard: true + defaultInputModes: + - "text" + defaultOutputModes: + - "text" + skills: + - name: "hello-world" + description: "A simple hello world skill" + tags: + - "greeting" + - "basic" + examples: + - "Say hello to me" + - "Greet me" + inputModes: + - "text" + outputModes: + - "text" +``` + +### Running the Application + +1. Build the project: `mvn clean package` +2. Run the application: `java -jar target/my-a2a-agent-1.0.0.jar` +3. Test the endpoints: + - Agent Card: `curl http://localhost:8089/.well-known/agent.json` + - Send Message: `curl -X POST http://localhost:8089/ -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","method":"a2a.sendMessage","params":{"message":{"content":"Hello"}},"id":1}'` + +## Additional Examples + +See the `examples/spring-helloworld` directory for a complete working example application. + +## License + +This project is licensed under the Apache License 2.0. diff --git a/sdk-spring/pom.xml b/sdk-spring/pom.xml new file mode 100644 index 000000000..8b7827947 --- /dev/null +++ b/sdk-spring/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + + io.github.a2asdk + a2a-java-sdk-parent + 0.2.3.Beta2-SNAPSHOT + + a2a-java-sdk-server-spring + + jar + + Java A2A SDK Starter for Spring + Java SDK SpringBoot Starter for the Agent2Agent Protocol (A2A) - SDK - Spring + + + 3.4.5 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + ${project.groupId} + a2a-java-sdk-spec + ${project.version} + + + ${project.groupId} + a2a-java-sdk-server-common + ${project.version} + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-autoconfigure + + + diff --git a/sdk-spring/src/main/java/io/a2a/server/apps/spring/A2AServerProperties.java b/sdk-spring/src/main/java/io/a2a/server/apps/spring/A2AServerProperties.java new file mode 100644 index 000000000..421c892a7 --- /dev/null +++ b/sdk-spring/src/main/java/io/a2a/server/apps/spring/A2AServerProperties.java @@ -0,0 +1,585 @@ +/* + * Copyright 2024-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.a2a.server.apps.spring; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for A2A Server. + *

+ * This class provides configuration options for setting up an A2A server, including agent + * metadata, capabilities, and server settings. + * + *

+ * Properties can be configured in application.yml or application.properties:

+ * a2a:
+ *   server:
+ *     enabled: true
+ *     name: My A2A Agent
+ *     description: A sample A2A agent
+ *     version: 1.0.0
+ *     url: ...
+ *     capabilities:
+ *       streaming: true
+ *       push-notifications: false
+ *       state-transition-history: true
+ * 
+ * + */ +@ConfigurationProperties(prefix = "a2a.server") +public class A2AServerProperties implements Serializable { + + @Serial + private static final long serialVersionUID = -608274692651491547L; + + /** + * Whether the A2A server is enabled. + */ + private boolean enabled = false; + + /** + * The unique agent id + */ + private String id; + + /** + * The name of the agent. + */ + private String name; + + /** + * A description of what the agent does. + */ + private String description; + + /** + * The version of the agent. + */ + private String version; + + /** + * The base URL where the agent can be reached. + */ + private String url; + + /** + * Information about the provider of the agent. + */ + private Provider provider; + + /** + * An optional URL pointing to the agent's documentation. + */ + private String documentationUrl; + + /** + * Agent capabilities configuration. + */ + private Capabilities capabilities = new Capabilities(); + + /** + * Authentication details required to interact with the agent. + */ + private Authentication authentication = new Authentication(); + + /** + * Security scheme details used for authenticating with this agent. + */ + private Map securitySchemes = new HashMap<>(); + + /** + * Security requirements for contacting the agent. + */ + private List>> security = new ArrayList<>(); + + /** + * Default input modes supported by the agent (e.g., 'text', 'file', 'json'). + */ + private List defaultInputModes = List.of("text"); + + /** + * Default output modes supported by the agent (e.g., 'text', 'file', 'json'). + */ + private List defaultOutputModes = List.of("text"); + + /** + * List of specific skills offered by the agent. + */ + private List skills = new ArrayList<>(); + + /** + * Whether the agent supports authenticated extended card retrieval. + */ + private boolean supportsAuthenticatedExtendedCard = false; + + /** + * Returns whether the A2A server is enabled. + * + * @return true if enabled, false otherwise + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets whether the A2A server is enabled. + * + * @param enabled true to enable the server, false to disable + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Get agent id. + * @return agent id + */ + public String getId() { + return id; + } + + /** + * Set agent id. + * @param id agent id + */ + public void setId(String id) { + this.id = id; + } + + /** + * Returns the agent name. + * + * @return the agent name + */ + public String getName() { + return name; + } + + /** + * Sets the agent name. + * + * @param name the agent name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the agent description. + * + * @return the agent description + */ + public String getDescription() { + return description; + } + + /** + * Sets the agent description. + * + * @param description the agent description to set + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Returns the agent version. + * + * @return the agent version + */ + public String getVersion() { + return version; + } + + /** + * Sets the agent version. + * + * @param version the agent version to set + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * Returns the agent URL. + * + * @return the agent URL + */ + public String getUrl() { + return url; + } + + /** + * Sets the agent URL. + * + * @param url the agent URL to set + */ + public void setUrl(String url) { + this.url = url; + } + + public Provider getProvider() { + return provider; + } + + public void setProvider(Provider provider) { + this.provider = provider; + } + + public String getDocumentationUrl() { + return documentationUrl; + } + + public void setDocumentationUrl(String documentationUrl) { + this.documentationUrl = documentationUrl; + } + + public Authentication getAuthentication() { + return authentication; + } + + public void setAuthentication(Authentication authentication) { + this.authentication = authentication; + } + + public Map getSecuritySchemes() { + return securitySchemes; + } + + public void setSecuritySchemes(Map securitySchemes) { + this.securitySchemes = securitySchemes; + } + + public List>> getSecurity() { + return security; + } + + public void setSecurity(List>> security) { + this.security = security; + } + + public List getDefaultInputModes() { + return defaultInputModes; + } + + public void setDefaultInputModes(List defaultInputModes) { + this.defaultInputModes = defaultInputModes; + } + + public List getDefaultOutputModes() { + return defaultOutputModes; + } + + public void setDefaultOutputModes(List defaultOutputModes) { + this.defaultOutputModes = defaultOutputModes; + } + + public List getSkills() { + return skills; + } + + public void setSkills(List skills) { + this.skills = skills; + } + + /** + * Returns whether the agent supports authenticated extended card retrieval. + * + * @return true if authenticated extended card is supported, false otherwise + */ + public boolean isSupportsAuthenticatedExtendedCard() { + return supportsAuthenticatedExtendedCard; + } + + /** + * Sets whether the agent supports authenticated extended card retrieval. + * + * @param supportsAuthenticatedExtendedCard true to enable authenticated extended card support, false to disable + */ + public void setSupportsAuthenticatedExtendedCard(boolean supportsAuthenticatedExtendedCard) { + this.supportsAuthenticatedExtendedCard = supportsAuthenticatedExtendedCard; + } + + /** + * Returns the agent capabilities configuration. + * + * @return the capabilities configuration + */ + public Capabilities getCapabilities() { + return capabilities; + } + + /** + * Sets the agent capabilities configuration. + * + * @param capabilities the capabilities configuration to set + */ + public void setCapabilities(Capabilities capabilities) { + this.capabilities = capabilities; + } + + /** + * Configuration for agent capabilities. + *

+ * This class defines what features the agent supports, such as streaming responses, + * push notifications, and state history. + */ + public static class Capabilities implements Serializable { + + private static final long serialVersionUID = 2371695651871067858L; + + /** + * Whether the agent supports streaming responses. + */ + private boolean streaming = true; + + /** + * Whether the agent supports push notifications. + */ + private boolean pushNotifications = false; + + /** + * Whether the agent maintains state transition history. + */ + private boolean stateTransitionHistory = true; + + /** + * Returns whether streaming is supported. + * + * @return true if streaming is supported, false otherwise + */ + public boolean isStreaming() { + return streaming; + } + + /** + * Sets whether streaming is supported. + * + * @param streaming true to enable streaming support, false to disable + */ + public void setStreaming(boolean streaming) { + this.streaming = streaming; + } + + /** + * Returns whether push notifications are supported. + * + * @return true if push notifications are supported, false otherwise + */ + public boolean isPushNotifications() { + return pushNotifications; + } + + /** + * Sets whether push notifications are supported. + * + * @param pushNotifications true to enable push notification support, false to + * disable + */ + public void setPushNotifications(boolean pushNotifications) { + this.pushNotifications = pushNotifications; + } + + /** + * Returns whether state transition history is supported. + * + * @return true if state transition history is supported, false otherwise + */ + public boolean isStateTransitionHistory() { + return stateTransitionHistory; + } + + /** + * Sets whether state transition history is supported. + * + * @param stateTransitionHistory true to enable state transition history, false to + * disable + */ + public void setStateTransitionHistory(boolean stateTransitionHistory) { + this.stateTransitionHistory = stateTransitionHistory; + } + + } + + /** + * Configuration for agent provider information. + */ + public static class Provider implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * The name of the provider. + */ + private String name; + + /** + * The URL of the provider. + */ + private String url; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + } + + /** + * Configuration for agent authentication. + */ + public static class Authentication implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * The type of authentication. + */ + private String type; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } + + /** + * Configuration for security scheme. + */ + public static class SecurityScheme implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * The type of security scheme. + */ + private String type; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + } + + /** + * Configuration for agent skill. + */ + public static class Skill implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * The name of the skill. + */ + private String name; + + /** + * The description of the skill. + */ + private String description; + + /** + * Set of tag words describing classes of capabilities for this specific skill. + * Example: ["cooking", "customer support", "billing"] + */ + private List tags = new ArrayList<>(); + + /** + * The set of example scenarios that the skill can perform. + * Will be used by the client as a hint to understand how the skill can be used. + * Example: ["I need a recipe for bread"] + */ + private List examples; + + /** + * The input modes supported by the skill. + */ + private List inputModes = List.of("text"); + + /** + * The output modes supported by the skill. + */ + private List outputModes = List.of("text"); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public List getExamples() { + return examples; + } + + public void setExamples(List examples) { + this.examples = examples; + } + + public List getInputModes() { + return inputModes; + } + + public void setInputModes(List inputModes) { + this.inputModes = inputModes; + } + + public List getOutputModes() { + return outputModes; + } + + public void setOutputModes(List outputModes) { + this.outputModes = outputModes; + } + } + +} diff --git a/sdk-spring/src/main/java/io/a2a/server/apps/spring/A2ASpringAutoConfiguration.java b/sdk-spring/src/main/java/io/a2a/server/apps/spring/A2ASpringAutoConfiguration.java new file mode 100644 index 000000000..2a53044ee --- /dev/null +++ b/sdk-spring/src/main/java/io/a2a/server/apps/spring/A2ASpringAutoConfiguration.java @@ -0,0 +1,140 @@ +package io.a2a.server.apps.spring; + +import io.a2a.server.agentexecution.AgentExecutor; +import io.a2a.server.events.InMemoryQueueManager; +import io.a2a.server.events.QueueManager; +import io.a2a.server.requesthandlers.DefaultRequestHandler; +import io.a2a.server.requesthandlers.JSONRPCHandler; +import io.a2a.server.requesthandlers.RequestHandler; +import io.a2a.server.tasks.InMemoryPushNotifier; +import io.a2a.server.tasks.InMemoryTaskStore; +import io.a2a.server.tasks.PushNotifier; +import io.a2a.server.tasks.TaskStore; +import io.a2a.spec.AgentCapabilities; +import io.a2a.spec.AgentCard; +import io.a2a.spec.AgentProvider; +import io.a2a.spec.AgentSkill; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; + +import java.util.stream.Collectors; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Spring Boot auto-configuration for A2A (Agent2Agent) protocol. + * Automatically configures the necessary beans for A2A server functionality. + */ +@Configuration +@EnableConfigurationProperties(A2AServerProperties.class) +public class A2ASpringAutoConfiguration { + + @Bean(name = "a2aServerSelfCard") + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "a2a.server.enabled", havingValue = "true") + public AgentCard agentCard(final A2AServerProperties a2aServerProperties) { + AgentCard.Builder builder = new AgentCard.Builder() + .name(a2aServerProperties.getName()) + .url(a2aServerProperties.getUrl()) + .version(a2aServerProperties.getVersion()) + .description(a2aServerProperties.getDescription()); + + // Add provider information if exists + if (a2aServerProperties.getProvider() != null) { + builder.provider(new AgentProvider(a2aServerProperties.getProvider().getName(), a2aServerProperties.getProvider().getUrl())); + } + + // Add documentation URL if exists + if (a2aServerProperties.getDocumentationUrl() != null) { + builder.documentationUrl(a2aServerProperties.getDocumentationUrl()); + } + + // Add capabilities if exists + if (a2aServerProperties.getCapabilities() != null) { + builder.capabilities(new AgentCapabilities.Builder() + .streaming(a2aServerProperties.getCapabilities().isStreaming()) + .pushNotifications(a2aServerProperties.getCapabilities().isPushNotifications()) + .stateTransitionHistory(a2aServerProperties.getCapabilities().isStateTransitionHistory()) + .build()); + } + + // Add security requirements if exists + if (a2aServerProperties.getSecurity() != null && !a2aServerProperties.getSecurity().isEmpty()) { + builder.security(a2aServerProperties.getSecurity()); + } + + // Add default input modes if exists + if (a2aServerProperties.getDefaultInputModes() != null && !a2aServerProperties.getDefaultInputModes().isEmpty()) { + builder.defaultInputModes(a2aServerProperties.getDefaultInputModes()); + } + + // Add default output modes if exists + if (a2aServerProperties.getDefaultOutputModes() != null && !a2aServerProperties.getDefaultOutputModes().isEmpty()) { + builder.defaultOutputModes(a2aServerProperties.getDefaultOutputModes()); + } + + // Add skills list if exists + if (a2aServerProperties.getSkills() != null && !a2aServerProperties.getSkills().isEmpty()) { + builder.skills(a2aServerProperties.getSkills().stream() + .filter(skill -> skill != null && skill.getName() != null) + .map(skill -> new AgentSkill.Builder() + .id(skill.getName()) + .name(skill.getName()) + .description(skill.getDescription()) + .tags(skill.getTags()) + .examples(skill.getExamples()) + .inputModes(skill.getInputModes()) + .outputModes(skill.getOutputModes()) + .build()) + .collect(Collectors.toList())); + } + + // Add authenticated extended card support + builder.supportsAuthenticatedExtendedCard(a2aServerProperties.isSupportsAuthenticatedExtendedCard()); + + return builder.build(); + } + + @Bean + @ConditionalOnMissingBean + public Executor a2aExecutor() { + return ForkJoinPool.commonPool(); + } + + @Bean + @ConditionalOnMissingBean + public TaskStore taskStore() { + return new InMemoryTaskStore(); + } + + @Bean + @ConditionalOnMissingBean + public QueueManager queueManager() { + return new InMemoryQueueManager(); + } + + @Bean + @ConditionalOnMissingBean + public PushNotifier pushNotifier() { + return new InMemoryPushNotifier(); + } + + /** + * Configure RequestHandler Bean. + * This Bean handles all A2A requests. + */ + @Bean + @ConditionalOnMissingBean + public RequestHandler requestHandler(AgentExecutor agentExecutor, TaskStore taskStore, QueueManager queueManager, PushNotifier pushNotifier, Executor executor) { + return new DefaultRequestHandler(agentExecutor, taskStore, queueManager, pushNotifier, executor); + } + + @Bean + @ConditionalOnMissingBean + public JSONRPCHandler jsonrpcHandler(AgentCard agentCard, RequestHandler requestHandler) { + return new JSONRPCHandler(agentCard, requestHandler); + } +} diff --git a/sdk-spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sdk-spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..7216f4c58 --- /dev/null +++ b/sdk-spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +io.a2a.server.apps.spring.A2ASpringAutoConfiguration