diff --git a/spring-ai-modules/pom.xml b/spring-ai-modules/pom.xml
index a632d87ad676..4c61c0f18bc2 100644
--- a/spring-ai-modules/pom.xml
+++ b/spring-ai-modules/pom.xml
@@ -20,6 +20,7 @@
spring-ai-2
spring-ai-3
spring-ai-4
+ spring-ai-a2a
spring-ai-agent-skills
spring-ai-agentic-patterns
spring-ai-anthropic-agent-skills
diff --git a/spring-ai-modules/spring-ai-a2a/pom.xml b/spring-ai-modules/spring-ai-a2a/pom.xml
new file mode 100644
index 000000000000..3a3110efa429
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/pom.xml
@@ -0,0 +1,91 @@
+
+
+ 4.0.0
+
+
+ com.baeldung
+ spring-ai-modules
+ 0.0.1
+ ../pom.xml
+
+
+ com.baeldung
+ spring-ai-a2a
+ 0.0.1
+ spring-ai-a2a
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webmvc
+
+
+ org.springframework.ai
+ spring-ai-starter-model-openai
+ ${spring-ai.version}
+
+
+ org.springaicommunity
+ spring-ai-a2a-server-autoconfigure
+ ${spring-ai-a2a-server-config.version}
+
+
+ io.github.a2asdk
+ a2a-java-sdk-client
+ ${a2a-client.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webmvc-test
+ test
+
+
+
+
+ 21
+ 6.0.3
+ 4.0.6
+ 2.0.0
+ 0.3.0
+ 0.3.3.Final
+
+
+
+
+ skills-matcher-server
+
+ true
+
+
+ com.baeldung.a2a.server.skillsmatcher.SkillsMatcherServer
+
+
+
+ salary-evaluator-server
+
+ com.baeldung.a2a.server.salaryevaluator.SalaryEvaluatorServer
+
+
+
+ job-screening-orchestrator-client
+
+ com.baeldung.a2a.client.orchestrator.jobscreening.JobScreeningOrchestrator
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ ${spring.boot.mainclass}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/AgentRegistry.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/AgentRegistry.java
new file mode 100644
index 000000000000..17a7bb3c3348
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/AgentRegistry.java
@@ -0,0 +1,39 @@
+package com.baeldung.a2a.client.orchestrator.jobscreening;
+
+import io.a2a.A2A;
+import io.a2a.spec.AgentCard;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Component
+class AgentRegistry {
+
+ private final Map agentCards = new HashMap<>();
+
+ AgentRegistry(@Value("${remote.agents.urls}") List agentUrls) throws URISyntaxException {
+ for (String url : agentUrls) {
+ String path = new URI(url).getPath();
+ AgentCard card = A2A.getAgentCard(url, path + ".well-known/agent-card.json", null);
+ agentCards.put(card.name(), card);
+ }
+ }
+
+ AgentCard get(String agentName) {
+ return agentCards.get(agentName);
+ }
+
+ String describeAgents() {
+ return agentCards
+ .values()
+ .stream()
+ .map(card -> "- " + card.name() + ": " + card.description())
+ .collect(Collectors.joining("\n"));
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/ChatClientConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/ChatClientConfiguration.java
new file mode 100644
index 000000000000..f45cc5d465af
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/ChatClientConfiguration.java
@@ -0,0 +1,29 @@
+package com.baeldung.a2a.client.orchestrator.jobscreening;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class ChatClientConfiguration {
+
+ @Bean
+ ChatClient chatClient(
+ ChatClient.Builder chatClientBuilder,
+ AgentRegistry agentRegistry,
+ RemoteAgentTools remoteAgentTools
+ ) {
+ return chatClientBuilder
+ .defaultSystem("""
+ You are a job-screening orchestrator for recruiters.
+ You do not evaluate candidates yourself. Instead, you delegate
+ to the following remote agents:
+
+ %s
+
+ Once all agents have responded, combine their responses into a short screening summary.
+ """.formatted(agentRegistry.describeAgents()))
+ .defaultTools(remoteAgentTools)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningController.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningController.java
new file mode 100644
index 000000000000..34597fd6cd5a
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningController.java
@@ -0,0 +1,39 @@
+package com.baeldung.a2a.client.orchestrator.jobscreening;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+class JobScreeningController {
+
+ private final ChatClient chatClient;
+
+ JobScreeningController(ChatClient chatClient) {
+ this.chatClient = chatClient;
+ }
+
+ @PostMapping("/screenings")
+ ScreeningResponse screenCandidate(@RequestBody ScreeningRequest screeningRequest) {
+ String verdict = chatClient
+ .prompt()
+ .user(screeningRequest.toString())
+ .call()
+ .content();
+ return new ScreeningResponse(verdict);
+ }
+
+ record ScreeningRequest(
+ String name,
+ String email,
+ String jobTitle,
+ String requiredSkills,
+ String candidateSkills,
+ int expectedSalary
+ ) {}
+
+ record ScreeningResponse(
+ String verdict
+ ) {}
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningOrchestrator.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningOrchestrator.java
new file mode 100644
index 000000000000..31a9c9a855c4
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningOrchestrator.java
@@ -0,0 +1,15 @@
+package com.baeldung.a2a.client.orchestrator.jobscreening;
+
+import org.springaicommunity.a2a.server.autoconfigure.A2AServerAutoConfiguration;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.PropertySource;
+
+@SpringBootApplication(exclude = A2AServerAutoConfiguration.class)
+@PropertySource("classpath:application-job-screening-orchestrator.properties")
+class JobScreeningOrchestrator {
+
+ public static void main(String[] args) {
+ SpringApplication.run(JobScreeningOrchestrator.class, args);
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentClient.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentClient.java
new file mode 100644
index 000000000000..f7a5d139f407
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentClient.java
@@ -0,0 +1,71 @@
+package com.baeldung.a2a.client.orchestrator.jobscreening;
+
+import io.a2a.A2A;
+import io.a2a.client.Client;
+import io.a2a.client.ClientEvent;
+import io.a2a.client.TaskEvent;
+import io.a2a.client.config.ClientConfig;
+import io.a2a.client.transport.jsonrpc.JSONRPCTransport;
+import io.a2a.client.transport.jsonrpc.JSONRPCTransportConfig;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.Artifact;
+import io.a2a.spec.Message;
+import io.a2a.spec.Part;
+import io.a2a.spec.TextPart;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+@Component
+class RemoteAgentClient {
+
+ private final AgentRegistry agentRegistry;
+
+ RemoteAgentClient(AgentRegistry agentRegistry) {
+ this.agentRegistry = agentRegistry;
+ }
+
+ String sendMessage(String agentName, String task)
+ throws ExecutionException, InterruptedException, TimeoutException {
+ AgentCard agentCard = agentRegistry.get(agentName);
+
+ CompletableFuture response = new CompletableFuture<>();
+ BiConsumer responseConsumer = (event, card) -> {
+ TaskEvent taskEvent = (TaskEvent) event;
+ response.complete(taskEvent.getTask()
+ .getArtifacts()
+ .stream()
+ .map(Artifact::parts)
+ .map(this::extractText)
+ .collect(Collectors.joining("\n")));
+ };
+
+ Client client = Client.builder(agentCard)
+ .clientConfig(new ClientConfig.Builder()
+ .setAcceptedOutputModes(List.of("text"))
+ .build())
+ .withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig())
+ .addConsumers(List.of(responseConsumer))
+ .streamingErrorHandler(response::completeExceptionally)
+ .build();
+
+ Message message = A2A.toUserMessage(task);
+ client.sendMessage(message);
+ return response.get(60, TimeUnit.SECONDS);
+ }
+
+ private String extractText(List> parts) {
+ return parts
+ .stream()
+ .filter(TextPart.class::isInstance)
+ .map(TextPart.class::cast)
+ .map(TextPart::getText)
+ .collect(Collectors.joining("\n"));
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentTools.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentTools.java
new file mode 100644
index 000000000000..3fe53ae923a1
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentTools.java
@@ -0,0 +1,29 @@
+package com.baeldung.a2a.client.orchestrator.jobscreening;
+
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+@Service
+class RemoteAgentTools {
+
+ private final RemoteAgentClient remoteAgentClient;
+
+ RemoteAgentTools(RemoteAgentClient remoteAgentClient) {
+ this.remoteAgentClient = remoteAgentClient;
+ }
+
+ @Tool(
+ name = "send-message-to-agent",
+ description = "Sends a task to a remote agent and returns its response."
+ )
+ String sendMessageToAgent(
+ @ToolParam(description = "Name of the remote agent") String agentName,
+ @ToolParam(description = "The task to perform") String task
+ ) throws ExecutionException, InterruptedException, TimeoutException {
+ return remoteAgentClient.sendMessage(agentName, task);
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerServer.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerServer.java
new file mode 100644
index 000000000000..27302c37da0d
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerServer.java
@@ -0,0 +1,14 @@
+package com.baeldung.a2a.server.backgroundchecker;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.PropertySource;
+
+@SpringBootApplication
+@PropertySource("classpath:application-background-checker-server.properties")
+public class BackgroundCheckerServer {
+
+ public static void main(String[] args) {
+ SpringApplication.run(BackgroundCheckerServer.class, args);
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerTools.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerTools.java
new file mode 100644
index 000000000000..06cd4c9021b2
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerTools.java
@@ -0,0 +1,47 @@
+package com.baeldung.a2a.server.backgroundchecker;
+
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.stereotype.Service;
+
+import java.util.Set;
+
+@Service
+class BackgroundCheckerTools {
+
+ private static final Set TRUSTED_EMAIL_DOMAINS = Set.of(
+ "gmail.com",
+ "baeldung.com"
+ );
+
+ @Tool(
+ name = "check-background",
+ description = "Runs a background check for a candidate"
+ )
+ BackgroundCheckResult checkBackground(
+ @ToolParam(description = "The candidate's full name") String candidateName,
+ @ToolParam(description = "The candidate's email address") String email
+ ) {
+ String domain = extractDomain(email);
+ Verdict verdict = TRUSTED_EMAIL_DOMAINS.contains(domain)
+ ? Verdict.CLEAR
+ : Verdict.NEEDS_REVIEW;
+ return new BackgroundCheckResult(verdict);
+ }
+
+ private String extractDomain(String email) {
+ int atIndex = email.lastIndexOf('@');
+ return atIndex < 0
+ ? ""
+ : email.substring(atIndex + 1).trim().toLowerCase();
+ }
+
+ record BackgroundCheckResult(
+ Verdict verdict
+ ) {}
+
+ enum Verdict {
+ CLEAR,
+ NEEDS_REVIEW
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/ChatClientConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/ChatClientConfiguration.java
new file mode 100644
index 000000000000..f4612c4c9a51
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/ChatClientConfiguration.java
@@ -0,0 +1,24 @@
+package com.baeldung.a2a.server.backgroundchecker;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class ChatClientConfiguration {
+
+ @Bean
+ ChatClient chatClient(
+ ChatClient.Builder chatClientBuilder,
+ BackgroundCheckerTools backgroundCheckerTools
+ ) {
+ return chatClientBuilder
+ .defaultSystem("""
+ You are a background-check assistant for recruiters.
+ Use the check-background tool to run a background check on a
+ candidate using their name and email, then summarize the result.
+ """)
+ .defaultTools(backgroundCheckerTools)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/ServerConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/ServerConfiguration.java
new file mode 100644
index 000000000000..cfd8a3f22a2e
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/ServerConfiguration.java
@@ -0,0 +1,55 @@
+package com.baeldung.a2a.server.backgroundchecker;
+
+import io.a2a.server.agentexecution.AgentExecutor;
+import io.a2a.spec.AgentCapabilities;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.AgentSkill;
+import org.springaicommunity.a2a.server.executor.DefaultAgentExecutor;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+@Configuration
+class ServerConfiguration {
+
+ @Bean
+ AgentExecutor agentExecutor(ChatClient chatClient) {
+ return new DefaultAgentExecutor(chatClient, (client, requestContext) -> {
+ String userMessage = DefaultAgentExecutor.extractTextFromMessage(requestContext.getMessage());
+ return client
+ .prompt()
+ .user(userMessage)
+ .call()
+ .content();
+ });
+ }
+
+ @Bean
+ AgentCard agentCard(
+ @Value("${server.host}") String host,
+ @Value("${server.port}") int port
+ ) {
+ return new AgentCard.Builder()
+ .name("Background Check Agent")
+ .description("Runs a background check on a candidate")
+ .url(String.format("http://%s:%d/", host, port))
+ .version("1.0.0")
+ .capabilities(new AgentCapabilities
+ .Builder()
+ .streaming(false)
+ .build())
+ .defaultInputModes(List.of("text"))
+ .defaultOutputModes(List.of("text"))
+ .skills(List.of(new AgentSkill.Builder()
+ .id("background_check")
+ .name("Background Check")
+ .description("Runs a background check on a candidate using their name and email")
+ .tags(List.of("hiring", "recruiting", "compliance"))
+ .build()))
+ .protocolVersion("1.0.1")
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ChatClientConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ChatClientConfiguration.java
new file mode 100644
index 000000000000..12952b8617f8
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ChatClientConfiguration.java
@@ -0,0 +1,24 @@
+package com.baeldung.a2a.server.salaryevaluator;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class ChatClientConfiguration {
+
+ @Bean
+ ChatClient chatClient(
+ ChatClient.Builder chatClientBuilder,
+ SalaryEvaluatorTools salaryEvaluatorTools
+ ) {
+ return chatClientBuilder
+ .defaultSystem("""
+ You are a salary-evaluation assistant for recruiters.
+ Use the evaluate-salary tool to compare a candidate's expected
+ salary against the job title they've applied for.
+ """)
+ .defaultTools(salaryEvaluatorTools)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorServer.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorServer.java
new file mode 100644
index 000000000000..e80269affd57
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorServer.java
@@ -0,0 +1,14 @@
+package com.baeldung.a2a.server.salaryevaluator;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.PropertySource;
+
+@SpringBootApplication
+@PropertySource("classpath:application-salary-evaluator-server.properties")
+public class SalaryEvaluatorServer {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SalaryEvaluatorServer.class, args);
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorTools.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorTools.java
new file mode 100644
index 000000000000..15d201f167cc
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorTools.java
@@ -0,0 +1,45 @@
+package com.baeldung.a2a.server.salaryevaluator;
+
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.stereotype.Service;
+
+@Service
+class SalaryEvaluatorTools {
+
+ @Tool(
+ name = "evaluate-salary",
+ description = "Compares a candidate's expected salary for a given job title"
+ )
+ SalaryEvaluationResult evaluateSalary(
+ @ToolParam(description = "The job title being applied for") String jobTitle,
+ @ToolParam(description = "Candidate's expected annual salary") int expectedSalary
+ ) {
+ SalaryRange salaryRange = budgetLookup(jobTitle);
+ Verdict verdict = expectedSalary > salaryRange.max()
+ ? Verdict.ABOVE_BUDGET
+ : expectedSalary < salaryRange.min()
+ ? Verdict.BELOW_BUDGET
+ : Verdict.WITHIN_BUDGET;
+ return new SalaryEvaluationResult(verdict);
+ }
+
+ private SalaryRange budgetLookup(String jobTitle) {
+ return new SalaryRange(80000, 120000);
+ }
+
+ record SalaryRange(
+ int min,
+ int max
+ ) {}
+
+ record SalaryEvaluationResult(
+ Verdict verdict
+ ) {}
+
+ enum Verdict {
+ WITHIN_BUDGET,
+ ABOVE_BUDGET,
+ BELOW_BUDGET
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ServerConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ServerConfiguration.java
new file mode 100644
index 000000000000..04b5fab9f1d8
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ServerConfiguration.java
@@ -0,0 +1,55 @@
+package com.baeldung.a2a.server.salaryevaluator;
+
+import io.a2a.server.agentexecution.AgentExecutor;
+import io.a2a.spec.AgentCapabilities;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.AgentSkill;
+import org.springaicommunity.a2a.server.executor.DefaultAgentExecutor;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+@Configuration
+class ServerConfiguration {
+
+ @Bean
+ AgentExecutor agentExecutor(ChatClient chatClient) {
+ return new DefaultAgentExecutor(chatClient, (client, requestContext) -> {
+ String userMessage = DefaultAgentExecutor.extractTextFromMessage(requestContext.getMessage());
+ return client
+ .prompt()
+ .user(userMessage)
+ .call()
+ .content();
+ });
+ }
+
+ @Bean
+ AgentCard agentCard(
+ @Value("${server.host}") String host,
+ @Value("${server.port}") int port
+ ) {
+ return new AgentCard.Builder()
+ .name("Salary Evaluator Agent")
+ .description("Checks if a candidate's expected salary fits a job title's budget range")
+ .url(String.format("http://%s:%d/", host, port))
+ .version("1.0.0")
+ .capabilities(new AgentCapabilities
+ .Builder()
+ .streaming(false)
+ .build())
+ .defaultInputModes(List.of("text"))
+ .defaultOutputModes(List.of("text"))
+ .skills(List.of(new AgentSkill.Builder()
+ .id("salary_evaluation")
+ .name("Salary Evaluation")
+ .description("Compares a candidate's expected salary against a job title's budget range")
+ .tags(List.of("hiring", "recruiting", "compensation"))
+ .build()))
+ .protocolVersion("1.0.1")
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ChatClientConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ChatClientConfiguration.java
new file mode 100644
index 000000000000..c24d90000ce2
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ChatClientConfiguration.java
@@ -0,0 +1,24 @@
+package com.baeldung.a2a.server.skillsmatcher;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class ChatClientConfiguration {
+
+ @Bean
+ ChatClient chatClient(
+ ChatClient.Builder chatClientBuilder,
+ SkillsMatcherTools skillsMatcherTools
+ ) {
+ return chatClientBuilder
+ .defaultSystem("""
+ You are a skills-matching assistant for recruiters.
+ Use the match-skills tool to compare a candidate's skills
+ against a job's required skills, then summarize the result.
+ """)
+ .defaultTools(skillsMatcherTools)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ServerConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ServerConfiguration.java
new file mode 100644
index 000000000000..60f8ae3e238e
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ServerConfiguration.java
@@ -0,0 +1,55 @@
+package com.baeldung.a2a.server.skillsmatcher;
+
+import io.a2a.server.agentexecution.AgentExecutor;
+import io.a2a.spec.AgentCapabilities;
+import io.a2a.spec.AgentCard;
+import io.a2a.spec.AgentSkill;
+import org.springaicommunity.a2a.server.executor.DefaultAgentExecutor;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+@Configuration
+class ServerConfiguration {
+
+ @Bean
+ AgentExecutor agentExecutor(ChatClient chatClient) {
+ return new DefaultAgentExecutor(chatClient, (client, requestContext) -> {
+ String userMessage = DefaultAgentExecutor.extractTextFromMessage(requestContext.getMessage());
+ return client
+ .prompt()
+ .user(userMessage)
+ .call()
+ .content();
+ });
+ }
+
+ @Bean
+ AgentCard agentCard(
+ @Value("${server.host}") String host,
+ @Value("${server.port}") int port
+ ) {
+ return new AgentCard.Builder()
+ .name("Skills Matcher Agent")
+ .description("Evaluates how well a candidate's skills match a job's required skills")
+ .url(String.format("http://%s:%d/", host, port))
+ .version("1.0.0")
+ .capabilities(new AgentCapabilities
+ .Builder()
+ .streaming(false)
+ .build())
+ .defaultInputModes(List.of("text"))
+ .defaultOutputModes(List.of("text"))
+ .skills(List.of(new AgentSkill.Builder()
+ .id("skills_matching")
+ .name("Skills Matching")
+ .description("Compares candidate skills to job requirements and scores the fit")
+ .tags(List.of("hiring", "recruiting"))
+ .build()))
+ .protocolVersion("1.0.1")
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherServer.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherServer.java
new file mode 100644
index 000000000000..351c30fed8af
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherServer.java
@@ -0,0 +1,14 @@
+package com.baeldung.a2a.server.skillsmatcher;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.PropertySource;
+
+@SpringBootApplication
+@PropertySource("classpath:application-skills-matcher-server.properties")
+public class SkillsMatcherServer {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SkillsMatcherServer.class, args);
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherTools.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherTools.java
new file mode 100644
index 000000000000..bbb2c0bee8f0
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherTools.java
@@ -0,0 +1,62 @@
+package com.baeldung.a2a.server.skillsmatcher;
+
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Service
+class SkillsMatcherTools {
+
+ @Tool(
+ name = "match-skills",
+ description = "Compares a candidate's skills against a job's required skills and returns a fit score"
+ )
+ SkillsMatchResult matchSkills(
+ @ToolParam(description = "Candidate skills, comma-separated") String candidateSkills,
+ @ToolParam(description = "Required job skills, comma-separated") String requiredSkills
+ ) {
+ Set candidateSkillSet = normalize(candidateSkills);
+ Set requiredSkillSet = normalize(requiredSkills);
+
+ Set matchedSkillSet = new HashSet<>(candidateSkillSet);
+ matchedSkillSet.retainAll(requiredSkillSet);
+
+ Set missingSkillSet = new HashSet<>(requiredSkillSet);
+ missingSkillSet.removeAll(candidateSkillSet);
+
+ int score = requiredSkillSet.isEmpty() ? 0 : (matchedSkillSet.size() * 100) / requiredSkillSet.size();
+ Verdict verdict = score >= 75
+ ? Verdict.STRONG_MATCH
+ : score >= 40
+ ? Verdict.PARTIAL_MATCH
+ : Verdict.WEAK_MATCH;
+
+ return new SkillsMatchResult(score, verdict, matchedSkillSet, missingSkillSet);
+ }
+
+ private Set normalize(String csv) {
+ return Arrays.stream(csv.split(","))
+ .map(String::trim)
+ .map(String::toLowerCase)
+ .filter(s -> !s.isEmpty())
+ .collect(Collectors.toSet());
+ }
+
+ record SkillsMatchResult(
+ int score,
+ Verdict verdict,
+ Set matchedSkills,
+ Set missingSkills
+ ) {}
+
+ enum Verdict {
+ STRONG_MATCH,
+ PARTIAL_MATCH,
+ WEAK_MATCH
+ }
+}
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/resources/application-background-checker-server.properties b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-background-checker-server.properties
new file mode 100644
index 000000000000..ea7410ff61c0
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-background-checker-server.properties
@@ -0,0 +1,5 @@
+server.host=localhost
+server.port=8083
+
+spring.ai.openai.api-key=${OPENAI_API_KEY}
+spring.ai.openai.chat.model=gpt-5.5
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/resources/application-job-screening-orchestrator.properties b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-job-screening-orchestrator.properties
new file mode 100644
index 000000000000..f242593dae60
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-job-screening-orchestrator.properties
@@ -0,0 +1,6 @@
+server.port=8080
+
+spring.ai.openai.api-key=${OPENAI_API_KEY}
+spring.ai.openai.chat.model=gpt-5.5
+
+remote.agents.urls=http://localhost:8081,http://localhost:8082,http://localhost:8083
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/resources/application-salary-evaluator-server.properties b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-salary-evaluator-server.properties
new file mode 100644
index 000000000000..b0019c806654
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-salary-evaluator-server.properties
@@ -0,0 +1,5 @@
+server.host=localhost
+server.port=8082
+
+spring.ai.openai.api-key=${OPENAI_API_KEY}
+spring.ai.openai.chat.model=gpt-5.5
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/resources/application-skills-matcher-server.properties b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-skills-matcher-server.properties
new file mode 100644
index 000000000000..0bbebb2388df
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-skills-matcher-server.properties
@@ -0,0 +1,5 @@
+server.host=localhost
+server.port=8081
+
+spring.ai.openai.api-key=${OPENAI_API_KEY}
+spring.ai.openai.chat.model=gpt-5.5
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/main/resources/logback-spring.xml b/spring-ai-modules/spring-ai-a2a/src/main/resources/logback-spring.xml
new file mode 100644
index 000000000000..449efbdaebb0
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/main/resources/logback-spring.xml
@@ -0,0 +1,15 @@
+
+
+
+ [%d{yyyy-MM-dd HH:mm:ss}] [%p] [%c{1}] - %m%n
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-ai-modules/spring-ai-a2a/src/test/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningLiveTest.java b/spring-ai-modules/spring-ai-a2a/src/test/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningLiveTest.java
new file mode 100644
index 000000000000..776339419215
--- /dev/null
+++ b/spring-ai-modules/spring-ai-a2a/src/test/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningLiveTest.java
@@ -0,0 +1,73 @@
+package com.baeldung.a2a.client.orchestrator.jobscreening;
+
+import com.baeldung.a2a.server.backgroundchecker.BackgroundCheckerServer;
+import com.baeldung.a2a.server.salaryevaluator.SalaryEvaluatorServer;
+import com.baeldung.a2a.server.skillsmatcher.SkillsMatcherServer;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(classes = JobScreeningOrchestrator.class)
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
+class JobScreeningLiveTest {
+
+ private static ConfigurableApplicationContext skillsMatcherServerContext;
+ private static ConfigurableApplicationContext salaryEvaluatorServerContext;
+ private static ConfigurableApplicationContext backgroundCheckerServerContext;
+
+ @Autowired
+ private AgentRegistry agentRegistry;
+
+ @Autowired
+ private JobScreeningController jobScreeningController;
+
+ @BeforeAll
+ static void startRemoteAgentServers() {
+ backgroundCheckerServerContext = SpringApplication.run(BackgroundCheckerServer.class);
+ salaryEvaluatorServerContext = SpringApplication.run(SalaryEvaluatorServer.class);
+ skillsMatcherServerContext = SpringApplication.run(SkillsMatcherServer.class);
+ }
+
+ @AfterAll
+ static void stopRemoteAgentServers() {
+ backgroundCheckerServerContext.close();
+ salaryEvaluatorServerContext.close();
+ skillsMatcherServerContext.close();
+ }
+
+ @Test
+ void whenApplicationStarts_thenAllAgentCardsFetched() {
+ String agentDescriptions = agentRegistry.describeAgents();
+
+ assertThat(agentDescriptions)
+ .contains("Background Check Agent")
+ .contains("Salary Evaluator Agent")
+ .contains("Skills Matcher Agent");
+ }
+
+ @Test
+ void whenCandidateScreened_thenScreeningVerdictReturned() {
+ var screeningRequest = new JobScreeningController.ScreeningRequest(
+ "John Doe",
+ "john.doe@baeldung.com",
+ "Backend Developer",
+ "Java, Spring Boot, AWS, Kafka",
+ "Java, Spring Boot, Azure, Kafka",
+ 110000
+ );
+
+ var screeningResponse = jobScreeningController.screenCandidate(screeningRequest);
+
+ assertThat(screeningResponse.verdict())
+ .isNotBlank()
+ .containsAnyOf("75%", "3 out of 4 skills")
+ .containsIgnoringCase("AWS");
+ }
+}
\ No newline at end of file