Content
# a2a4j-examples
[a2a4j](https://github.com/PheonixHkbxoic/a2a4j) example project
## What is the A2A Protocol?
The A2A (Agent2Agent) protocol is an open protocol launched by Google Cloud, aimed at facilitating interoperability between different AI agents. Its main goal is to allow these agents to communicate and collaborate effectively in a dynamic, multi-agent ecosystem, regardless of whether they are built by different vendors or use different technology frameworks.
## How does the A2A Protocol work?
1. Capability Description (Agent Card)
Each AI agent must provide a JSON-formatted "description" that clearly informs other agents about "what I can do" (e.g., analyze data, book flights), "what input formats are required," and "how to authenticate." This is akin to a business card for employees, allowing collaborators to quickly identify available resources. Other agents can easily find and understand its capabilities, eliminating significant communication barriers.
2. Task Dispatch and Tracking
When one agent wants to delegate a task to another agent, it is akin to issuing a "collaboration project intent." Once the other party agrees to take on the task, both parties will record a Task ID to track project progress and exchange materials until the task is completed.
For example, if a user asks the "Travel Planning Agent" to arrange an itinerary, that agent can send a task request to the "Flight Booking Agent" via A2A and receive real-time status updates (e.g., "Flight found, comparing prices"). Tasks can be completed instantly or involve complex processes lasting several days, with results (e.g., generated itineraries) returned in a standardized format.
3. Cross-Modal Communication
The protocol supports various data types, including text, images, and audio/video. For instance, in a medical scenario, an imaging analysis agent can directly pass CT images to a diagnostic agent without the need for intermediate format conversion.
4. Security Verification Mechanism
All communications are encrypted by default and authenticated through enterprise-level methods such as OAuth, ensuring that only authorized agents can participate in collaborations and preventing data leaks.
## Participants in A2A
There are three participants in the A2A protocol:
* User: The user (human or service) who uses the agent system to complete tasks.
* Client: Responsible for forwarding user requests.
* Server: Responsible for receiving tasks from the client. Developers must handle task requests by invoking third-party LLM APIs and responding to the client.
## Using a2a4j
### Prerequisites
* The a2a4j directory is developed using JDK8 and SpringBoot 2.7.18.
* Currently supports SpringMvc + Reactor + SSE.
* Future support for servlet and webflux.
* Future support for JDK17+, SpringBoot 3.X.
### Server Configuration
1. Add Maven dependency
```xml
<dependency>
<groupId>io.github.pheonixhkbxoic</groupId>
<artifactId>a2a4j-agent-mvc-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
```
2. Configure AgentCard instance
```java
@Bean
public AgentCard agentCard() {
AgentCapabilities capabilities = new AgentCapabilities();
AgentSkill skill = AgentSkill.builder()
.id("convert_currency")
.name("Currency Exchange Rates Tool")
.description("Helps with exchange values between various currencies")
.tags(Arrays.asList("currency conversion", "currency exchange"))
.examples(Collections.singletonList("What is exchange rate between USD and GBP?"))
.inputModes(Collections.singletonList("text"))
.outputModes(Collections.singletonList("text"))
.build();
AgentCard agentCard = new AgentCard();
agentCard.setName("Currency Agent");
agentCard.setDescription("current exchange");
agentCard.setUrl("http://127.0.0.1:" + port);
agentCard.setVersion("1.0.0");
agentCard.setCapabilities(capabilities);
agentCard.setSkills(Collections.singletonList(skill));
return agentCard;
}
```
The AgentCard is used to describe the capabilities of the current Agent Server. The client will connect to `http://{your_server_domain}/.well-known/agent.json` to retrieve the AgentCard upon startup.
3. Implement a custom task manager
```java
@Component
public class EchoTaskManager extends InMemoryTaskManager {
// wire agent
private final EchoAgent agent;
// agent support modes
private final List<String> supportModes = Arrays.asList("text", "file", "data");
public EchoTaskManager(@Autowired EchoAgent agent, @Autowired PushNotificationSenderAuth pushNotificationSenderAuth) {
this.agent = agent;
// must autowired, keep PushNotificationSenderAuth instance unique global
this.pushNotificationSenderAuth = pushNotificationSenderAuth;
}
@Override
public SendTaskResponse onSendTask(SendTaskRequest request) {
log.info("onSendTask request: {}", request);
TaskSendParams ps = request.getParams();
// 1. check request params
// 2. save task
// 2. agent invoke
List<Artifact> artifacts = this.agentInvoke(ps).block();
// 4. save and notification
Task taskCompleted = this.updateStore(ps.getId(), new TaskStatus(TaskState.COMPLETED), artifacts);
this.sendTaskNotification(taskCompleted);
Task taskSnapshot = this.appendTaskHistory(taskCompleted, 3);
return new SendTaskResponse(taskSnapshot);
}
@Override
public Mono<JsonRpcResponse> onSendTaskSubscribe(SendTaskStreamingRequest request) {
return Mono.fromCallable(() -> {
log.info("onSendTaskSubscribe request: {}", request);
TaskSendParams ps = request.getParams();
String taskId = ps.getId();
return null;
});
}
@Override
public Mono<JsonRpcResponse> onResubscribeTask(TaskResubscriptionRequest request) {
TaskIdParams params = request.getParams();
try {
this.initEventQueue(params.getId(), true);
} catch (Exception e) {
log.error("Error while reconnecting to SSE stream: {}", e.getMessage());
return Mono.just(new JsonRpcResponse(request.getId(), new InternalError("An error occurred while reconnecting to stream: " + e.getMessage())));
}
return Mono.empty();
}
// simulate agent invoke
private Mono<List<Artifact>> agentInvoke(TaskSendParams ps) {
}
private JsonRpcResponse<Object> validRequest(JsonRpcRequest<TaskSendParams> request) {
}
}
```
Notes:
* You need to extend the `InMemoryTaskManager` class and implement necessary methods such as `onSendTask`, `onSendTaskSubscribe`.
* You need to inject the `PushNotificationSenderAuth` instance if you want to send task status notifications to the notification listener server. This instance will expose the public key via `http://{your_server_domain}/.well-known/jwks.json`, and the notification listener will verify the notification request token and data through `PushNotificationReceiverAuth`.
* Create a custom class Agent to interact with the underlying LLM.
4. Code Reference
[a2a4j-examples agents/echo-agent](https://github.com/PheonixHkbxoic/a2a4j-examples/tree/main/agents/echo-agent)
### Client/Host Configuration
1. Add Maven dependency
```xml
<dependency>
<groupId>io.github.pheonixhkbxoic</groupId>
<artifactId>a2a4j-host-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
```
2. Configure relevant properties in the configuration file (e.g., application.xml)
```yaml
a2a4j:
host:
# can be null
notification:
url: http://127.0.0.1:8989/notify
agents:
agent-001:
baseUrl: http://127.0.0.1:8901
```
* You must configure the baseUrl of the Agent Server, and multiple can be configured.
* Optionally configure the baseUrl of the Notification Server.
3. Send task requests and handle responses
```java
@Slf4j
@RestController
public class AgentController {
@Resource
private List<A2AClient> clients;
@GetMapping("/chat")
public ResponseEntity<Object> chat(String userId, String sessionId, String prompts) {
A2AClient client = clients.get(0);
TaskSendParams params = TaskSendParams.builder()
.id(Uuid.uuid4hex())
.sessionId(sessionId)
.historyLength(3)
.acceptedOutputModes(Collections.singletonList("text"))
.message(new Message(Role.USER, Collections.singletonList(new TextPart(prompts)), null))
.pushNotification(client.getPushNotificationConfig())
.build();
log.info("params: {}", Util.toJson(params));
SendTaskResponse sendTaskResponse = client.sendTask(params);
JsonRpcError error = sendTaskResponse.getError();
if (error != null) {
return ResponseEntity.badRequest().body(error);
}
Task task = sendTaskResponse.getResult();
String answer = task.getArtifacts().stream()
.flatMap(t -> t.getParts().stream())
.filter(p -> new TextPart().getType().equals(p.getType()))
.map(p -> ((TextPart) p).getText())
.collect(Collectors.joining("\n"));
return ResponseEntity.ok(answer);
}
}
```
4. Code Reference
[a2a4j-examples hosts/standalone](https://github.com/PheonixHkbxoic/a2a4j-examples/tree/main/hosts/standalone)
### Notification Server Configuration
1. Add Maven dependency
```xml
<dependency>
<groupId>io.github.pheonixhkbxoic</groupId>
<artifactId>a2a4j-notification-mvc-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
```
2. Configure relevant properties in the configuration file (e.g., application.xml)
```yaml
a2a4j:
notification:
# default
endpoint: "/notify"
jwksUrls:
- http://127.0.0.1:8901/.well-known/jwks.json
```
* You must configure jwksUrls, and multiple can be configured.
* Optionally configure the endpoint; if not configured, it defaults to listening on `/notify`.
3. Create and instantiate a custom listener
```java
@Component
public class NotificationListener extends WebMvcNotificationAdapter {
public NotificationListener(@Autowired A2a4jNotificationProperties a2a4jNotificationProperties) {
super(a2a4jNotificationProperties.getEndpoint(), a2a4jNotificationProperties.getJwksUrls());
}
// TODO Implement methods to handle notifications, can use default implementation
}
```
Notes:
* You need to extend the `WebMvcNotificationAdapter` class.
* Inject configuration properties `@Autowired A2a4jNotificationProperties a2a4jNotificationProperties` and instantiate `PushNotificationReceiverAuth` by calling `super(a2a4jNotificationProperties.getEndpoint(), a2a4jNotificationProperties.getJwksUrls());` to listen on the specified address.
4. Code Reference
[a2a4j-examples notification-listener](https://github.com/PheonixHkbxoic/a2a4j-examples/tree/main/notification-listener)