From 49cf64e8ba42c4e509fffa30592c233139464285 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Thu, 16 Jul 2026 03:20:43 +0530 Subject: [PATCH 01/10] implement skills-matcher A2A server --- spring-ai-modules/pom.xml | 1 + spring-ai-modules/spring-ai-a2a/pom.xml | 51 +++++++++++++++ .../a2a/server/skillsmatcher/Application.java | 15 +++++ .../ChatClientConfiguration.java | 20 ++++++ .../skillsmatcher/ServerConfiguration.java | 45 +++++++++++++ .../skillsmatcher/SkillsMatcherTools.java | 63 +++++++++++++++++++ ...plication-skills-matcher-server.properties | 6 ++ .../src/main/resources/logback-spring.xml | 15 +++++ 8 files changed, 216 insertions(+) create mode 100644 spring-ai-modules/spring-ai-a2a/pom.xml create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ChatClientConfiguration.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ServerConfiguration.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherTools.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/resources/application-skills-matcher-server.properties create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/resources/logback-spring.xml 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..3a27eb45e874 --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/pom.xml @@ -0,0 +1,51 @@ + + + 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} + + + + + 21 + 4.0.6 + 2.0.0 + 0.3.0 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java new file mode 100644 index 000000000000..7deb03f8bbbb --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java @@ -0,0 +1,15 @@ +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") +class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.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/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..0fbb6f40fafc --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ChatClientConfiguration.java @@ -0,0 +1,20 @@ +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..afda4e79578b --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ServerConfiguration.java @@ -0,0 +1,45 @@ +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, (chat, ctx) -> { + String userMessage = DefaultAgentExecutor.extractTextFromMessage(ctx.getMessage()); + return chat.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("http://" + host + ":" + port + "/a2a/") + .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("0.3.0") + .build(); + } +} \ 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..05f29983ab2a --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherTools.java @@ -0,0 +1,63 @@ +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-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..86662c439ec3 --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-skills-matcher-server.properties @@ -0,0 +1,6 @@ +server.host=localhost +server.port=8081 +server.servlet.context-path=/a2a + +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 From 2878900a8034c6ba6a48a042df80cdd738acb8b1 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Thu, 16 Jul 2026 03:23:42 +0530 Subject: [PATCH 02/10] implement salary-evaluator A2A server --- .../server/salaryevaluator/Application.java | 15 ++++++ .../ChatClientConfiguration.java | 20 ++++++++ .../salaryevaluator/SalaryEvaluatorTools.java | 49 +++++++++++++++++++ .../salaryevaluator/ServerConfiguration.java | 45 +++++++++++++++++ ...ication-salary-evaluator-server.properties | 6 +++ 5 files changed, 135 insertions(+) create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ChatClientConfiguration.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorTools.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ServerConfiguration.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/resources/application-salary-evaluator-server.properties diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java new file mode 100644 index 000000000000..ff0cbe15e96b --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java @@ -0,0 +1,15 @@ +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") +class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.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/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..7e069f5e5eed --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ChatClientConfiguration.java @@ -0,0 +1,20 @@ +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/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..9af7eb5afb01 --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorTools.java @@ -0,0 +1,49 @@ +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) { + if (jobTitle.toLowerCase().contains("backend")) + return new SalaryRange(80000, 120000); + return new SalaryRange(40000, 60000); + } + + 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..0b4d1ad47df6 --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ServerConfiguration.java @@ -0,0 +1,45 @@ +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, (chat, ctx) -> { + String userMessage = DefaultAgentExecutor.extractTextFromMessage(ctx.getMessage()); + return chat.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("Evaluates whether a candidate's expected salary fits a job's budgeted salary range") + .url("http://" + host + ":" + port + "/a2a/") + .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's budgeted range") + .tags(List.of("hiring", "recruiting", "compensation")) + .build())) + .protocolVersion("0.3.0") + .build(); + } +} \ 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..47f40ccae2d9 --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-salary-evaluator-server.properties @@ -0,0 +1,6 @@ +server.host=localhost +server.port=8082 +server.servlet.context-path=/a2a + +spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.openai.chat.model=gpt-5.5 \ No newline at end of file From b3bd12a773aab9ed3a99cc47a98fa971116ef47f Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Thu, 16 Jul 2026 03:25:37 +0530 Subject: [PATCH 03/10] implement job-screening orchestrator A2A client --- spring-ai-modules/spring-ai-a2a/pom.xml | 6 ++ .../jobscreening/AgentRegistry.java | 39 +++++++++ .../jobscreening/Application.java | 16 ++++ .../jobscreening/ChatClientConfiguration.java | 29 +++++++ .../jobscreening/JobScreeningController.java | 39 +++++++++ .../jobscreening/RemoteAgentTools.java | 80 +++++++++++++++++++ ...tion-job-screening-orchestrator.properties | 6 ++ 7 files changed, 215 insertions(+) create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/AgentRegistry.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/ChatClientConfiguration.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningController.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentTools.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/resources/application-job-screening-orchestrator.properties diff --git a/spring-ai-modules/spring-ai-a2a/pom.xml b/spring-ai-modules/spring-ai-a2a/pom.xml index 3a27eb45e874..31183d155699 100644 --- a/spring-ai-modules/spring-ai-a2a/pom.xml +++ b/spring-ai-modules/spring-ai-a2a/pom.xml @@ -30,6 +30,11 @@ spring-ai-a2a-server-autoconfigure ${spring-ai-a2a-server-config.version} + + io.github.a2asdk + a2a-java-sdk-client + ${a2a-client.version} + @@ -37,6 +42,7 @@ 4.0.6 2.0.0 0.3.0 + 0.3.3.Final 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/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java new file mode 100644 index 000000000000..b6fa0b62c43a --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java @@ -0,0 +1,16 @@ +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 Application { + + public static void main(String[] args) { + SpringApplication.run(Application.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/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..463ae8f0376a --- /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 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/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..f4961debad4e --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentTools.java @@ -0,0 +1,80 @@ +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.MessageEvent; +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.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Service; + +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; + +@Service +class RemoteAgentTools { + + private final AgentRegistry agentRegistry; + + RemoteAgentTools(AgentRegistry agentRegistry) { + this.agentRegistry = agentRegistry; + } + + @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 { + 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/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..0d1ffb875897 --- /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/a2a/,http://localhost:8082/a2a/ \ No newline at end of file From 89790fd6675b1f7b57e875a10e2550f8b3056b55 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Thu, 16 Jul 2026 03:35:22 +0530 Subject: [PATCH 04/10] fix: unable to find a single main class --- spring-ai-modules/spring-ai-a2a/pom.xml | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/spring-ai-modules/spring-ai-a2a/pom.xml b/spring-ai-modules/spring-ai-a2a/pom.xml index 31183d155699..27b978e2cd81 100644 --- a/spring-ai-modules/spring-ai-a2a/pom.xml +++ b/spring-ai-modules/spring-ai-a2a/pom.xml @@ -45,11 +45,38 @@ 0.3.3.Final + + + skills-matcher-server + + true + + + com.baeldung.a2a.server.skillsmatcher.Application + + + + salary-evaluator-server + + com.baeldung.a2a.server.salaryevaluator.Application + + + + job-screening-orchestrator-client + + com.baeldung.a2a.client.orchestrator.jobscreening.Application + + + + org.springframework.boot spring-boot-maven-plugin + + ${spring.boot.mainclass} + From c3cde9696a10345aaba43e1c2563556d0b82eb76 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Tue, 21 Jul 2026 20:23:40 +0530 Subject: [PATCH 05/10] refacotoring w.r.t to indentation --- .../jobscreening/Application.java | 1 - .../jobscreening/JobScreeningController.java | 6 ++--- .../server/salaryevaluator/Application.java | 1 - .../ChatClientConfiguration.java | 8 ++++-- .../salaryevaluator/SalaryEvaluatorTools.java | 10 +++---- .../salaryevaluator/ServerConfiguration.java | 26 +++++++++++++------ .../a2a/server/skillsmatcher/Application.java | 1 - .../ChatClientConfiguration.java | 8 ++++-- .../skillsmatcher/ServerConfiguration.java | 22 +++++++++++----- .../skillsmatcher/SkillsMatcherTools.java | 3 +-- 10 files changed, 52 insertions(+), 34 deletions(-) diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java index b6fa0b62c43a..f6622feceb03 100644 --- a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java @@ -12,5 +12,4 @@ class Application { public static void main(String[] args) { SpringApplication.run(Application.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/JobScreeningController.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningController.java index 463ae8f0376a..bb440a596e0c 100644 --- 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 @@ -29,11 +29,9 @@ record ScreeningRequest( 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/server/salaryevaluator/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java index ff0cbe15e96b..d0fa2f7c4c7d 100644 --- a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java @@ -11,5 +11,4 @@ class Application { public static void main(String[] args) { SpringApplication.run(Application.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/ChatClientConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ChatClientConfiguration.java index 7e069f5e5eed..12952b8617f8 100644 --- 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 @@ -8,12 +8,16 @@ class ChatClientConfiguration { @Bean - ChatClient chatClient(ChatClient.Builder chatClientBuilder, SalaryEvaluatorTools salaryEvaluatorTools) { + 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.""") + salary against the job title they've applied for. + """) .defaultTools(salaryEvaluatorTools) .build(); } 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 index 9af7eb5afb01..15d201f167cc 100644 --- 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 @@ -25,21 +25,17 @@ SalaryEvaluationResult evaluateSalary( } private SalaryRange budgetLookup(String jobTitle) { - if (jobTitle.toLowerCase().contains("backend")) - return new SalaryRange(80000, 120000); - return new SalaryRange(40000, 60000); + return new SalaryRange(80000, 120000); } record SalaryRange( int min, int max - ){ - } + ) {} record SalaryEvaluationResult( Verdict verdict - ) { - } + ) {} enum Verdict { WITHIN_BUDGET, 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 index 0b4d1ad47df6..0836c20eb784 100644 --- 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 @@ -17,26 +17,36 @@ class ServerConfiguration { @Bean AgentExecutor agentExecutor(ChatClient chatClient) { - return new DefaultAgentExecutor(chatClient, (chat, ctx) -> { - String userMessage = DefaultAgentExecutor.extractTextFromMessage(ctx.getMessage()); - return chat.prompt().user(userMessage).call().content(); + 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) { + AgentCard agentCard( + @Value("${server.host}") String host, + @Value("${server.port}") int port + ) { return new AgentCard.Builder() .name("Salary Evaluator Agent") - .description("Evaluates whether a candidate's expected salary fits a job's budgeted salary range") - .url("http://" + host + ":" + port + "/a2a/") + .description("Checks if a candidate's expected salary fits a job title's budget range") + .url(String.format("http://%s:%d/a2a/", host, port)) .version("1.0.0") - .capabilities(new AgentCapabilities.Builder().streaming(false).build()) + .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's budgeted range") + .description("Compares a candidate's expected salary against a job title's budget range") .tags(List.of("hiring", "recruiting", "compensation")) .build())) .protocolVersion("0.3.0") diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java index 7deb03f8bbbb..1827d2d96252 100644 --- a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java @@ -11,5 +11,4 @@ class Application { public static void main(String[] args) { SpringApplication.run(Application.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/ChatClientConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ChatClientConfiguration.java index 0fbb6f40fafc..c24d90000ce2 100644 --- 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 @@ -8,12 +8,16 @@ class ChatClientConfiguration { @Bean - ChatClient chatClient(ChatClient.Builder chatClientBuilder, SkillsMatcherTools skillsMatcherTools) { + 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.""") + against a job's required skills, then summarize the result. + """) .defaultTools(skillsMatcherTools) .build(); } 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 index afda4e79578b..8fa924355f22 100644 --- 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 @@ -17,20 +17,30 @@ class ServerConfiguration { @Bean AgentExecutor agentExecutor(ChatClient chatClient) { - return new DefaultAgentExecutor(chatClient, (chat, ctx) -> { - String userMessage = DefaultAgentExecutor.extractTextFromMessage(ctx.getMessage()); - return chat.prompt().user(userMessage).call().content(); + 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) { + 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("http://" + host + ":" + port + "/a2a/") + .url(String.format("http://%s:%d/a2a/", host, port)) .version("1.0.0") - .capabilities(new AgentCapabilities.Builder().streaming(false).build()) + .capabilities(new AgentCapabilities + .Builder() + .streaming(false) + .build()) .defaultInputModes(List.of("text")) .defaultOutputModes(List.of("text")) .skills(List.of(new AgentSkill.Builder() 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 index 05f29983ab2a..bbb2c0bee8f0 100644 --- 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 @@ -52,8 +52,7 @@ record SkillsMatchResult( Verdict verdict, Set matchedSkills, Set missingSkills - ) { - } + ) {} enum Verdict { STRONG_MATCH, From d20f4b18d40887a49d0ee076973bc6591b16fafe Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Tue, 21 Jul 2026 20:33:46 +0530 Subject: [PATCH 06/10] add background-checker A2A server --- .../jobscreening/JobScreeningController.java | 2 + .../server/backgroundchecker/Application.java | 14 +++++ .../BackgroundCheckerTools.java | 47 ++++++++++++++++ .../ChatClientConfiguration.java | 24 ++++++++ .../ServerConfiguration.java | 55 +++++++++++++++++++ ...ation-background-checker-server.properties | 6 ++ ...tion-job-screening-orchestrator.properties | 2 +- 7 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/Application.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerTools.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/ChatClientConfiguration.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/ServerConfiguration.java create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/resources/application-background-checker-server.properties 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 index bb440a596e0c..34597fd6cd5a 100644 --- 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 @@ -25,6 +25,8 @@ ScreeningResponse screenCandidate(@RequestBody ScreeningRequest screeningRequest } record ScreeningRequest( + String name, + String email, String jobTitle, String requiredSkills, String candidateSkills, diff --git a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/Application.java new file mode 100644 index 000000000000..2e0c9ccf6519 --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/Application.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") +class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.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..9375860cf3f9 --- /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/a2a/", 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("0.3.0") + .build(); + } +} \ 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..8a50718946c2 --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/src/main/resources/application-background-checker-server.properties @@ -0,0 +1,6 @@ +server.host=localhost +server.port=8083 +server.servlet.context-path=/a2a + +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 index 0d1ffb875897..b8a52212f062 100644 --- 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 @@ -3,4 +3,4 @@ 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/a2a/,http://localhost:8082/a2a/ \ No newline at end of file +remote.agents.urls=http://localhost:8081/a2a/,http://localhost:8082/a2a/,http://localhost:8083/a2a/ \ No newline at end of file From 83b85bdc6d09c6cf95e1215c29662e3670191937 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Tue, 21 Jul 2026 22:49:46 +0530 Subject: [PATCH 07/10] make remote agent tool calling class lightweight --- .../jobscreening/RemoteAgentClient.java | 71 +++++++++++++++++++ .../jobscreening/RemoteAgentTools.java | 59 ++------------- 2 files changed, 75 insertions(+), 55 deletions(-) create mode 100644 spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentClient.java 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 index f4961debad4e..3fe53ae923a1 100644 --- 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 @@ -1,37 +1,19 @@ 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.MessageEvent; -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.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; -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; @Service class RemoteAgentTools { - private final AgentRegistry agentRegistry; + private final RemoteAgentClient remoteAgentClient; - RemoteAgentTools(AgentRegistry agentRegistry) { - this.agentRegistry = agentRegistry; + RemoteAgentTools(RemoteAgentClient remoteAgentClient) { + this.remoteAgentClient = remoteAgentClient; } @Tool( @@ -42,39 +24,6 @@ String sendMessageToAgent( @ToolParam(description = "Name of the remote agent") String agentName, @ToolParam(description = "The task to perform") 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")); + return remoteAgentClient.sendMessage(agentName, task); } } \ No newline at end of file From e2047195698a59aa9d55cf34b3a038c3fb98d5a7 Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Thu, 23 Jul 2026 16:20:57 +0530 Subject: [PATCH 08/10] remove context path and increase protocol version --- .../a2a/server/backgroundchecker/ServerConfiguration.java | 4 ++-- .../a2a/server/salaryevaluator/ServerConfiguration.java | 4 ++-- .../a2a/server/skillsmatcher/ServerConfiguration.java | 4 ++-- .../application-background-checker-server.properties | 1 - .../application-job-screening-orchestrator.properties | 2 +- .../resources/application-salary-evaluator-server.properties | 1 - .../resources/application-skills-matcher-server.properties | 1 - 7 files changed, 7 insertions(+), 10 deletions(-) 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 index 9375860cf3f9..cfd8a3f22a2e 100644 --- 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 @@ -35,7 +35,7 @@ AgentCard agentCard( return new AgentCard.Builder() .name("Background Check Agent") .description("Runs a background check on a candidate") - .url(String.format("http://%s:%d/a2a/", host, port)) + .url(String.format("http://%s:%d/", host, port)) .version("1.0.0") .capabilities(new AgentCapabilities .Builder() @@ -49,7 +49,7 @@ AgentCard agentCard( .description("Runs a background check on a candidate using their name and email") .tags(List.of("hiring", "recruiting", "compliance")) .build())) - .protocolVersion("0.3.0") + .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/ServerConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/ServerConfiguration.java index 0836c20eb784..04b5fab9f1d8 100644 --- 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 @@ -35,7 +35,7 @@ AgentCard agentCard( 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/a2a/", host, port)) + .url(String.format("http://%s:%d/", host, port)) .version("1.0.0") .capabilities(new AgentCapabilities .Builder() @@ -49,7 +49,7 @@ AgentCard agentCard( .description("Compares a candidate's expected salary against a job title's budget range") .tags(List.of("hiring", "recruiting", "compensation")) .build())) - .protocolVersion("0.3.0") + .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/ServerConfiguration.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/ServerConfiguration.java index 8fa924355f22..60f8ae3e238e 100644 --- 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 @@ -35,7 +35,7 @@ AgentCard agentCard( 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/a2a/", host, port)) + .url(String.format("http://%s:%d/", host, port)) .version("1.0.0") .capabilities(new AgentCapabilities .Builder() @@ -49,7 +49,7 @@ AgentCard agentCard( .description("Compares candidate skills to job requirements and scores the fit") .tags(List.of("hiring", "recruiting")) .build())) - .protocolVersion("0.3.0") + .protocolVersion("1.0.1") .build(); } } \ 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 index 8a50718946c2..ea7410ff61c0 100644 --- 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 @@ -1,6 +1,5 @@ server.host=localhost server.port=8083 -server.servlet.context-path=/a2a 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 index b8a52212f062..f242593dae60 100644 --- 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 @@ -3,4 +3,4 @@ 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/a2a/,http://localhost:8082/a2a/,http://localhost:8083/a2a/ \ No newline at end of file +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 index 47f40ccae2d9..b0019c806654 100644 --- 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 @@ -1,6 +1,5 @@ server.host=localhost server.port=8082 -server.servlet.context-path=/a2a 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 index 86662c439ec3..0bbebb2388df 100644 --- 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 @@ -1,6 +1,5 @@ server.host=localhost server.port=8081 -server.servlet.context-path=/a2a spring.ai.openai.api-key=${OPENAI_API_KEY} spring.ai.openai.chat.model=gpt-5.5 \ No newline at end of file From 67d0a7a6dc82037a7d54006a4c834a8b5404e97c Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Fri, 31 Jul 2026 23:11:57 +0530 Subject: [PATCH 09/10] add live test --- spring-ai-modules/spring-ai-a2a/pom.xml | 13 +- spring-ai-modules/spring-ai-a2a/project.md | 851 ++++++++++++++++++ ...ion.java => JobScreeningOrchestrator.java} | 4 +- ...tion.java => BackgroundCheckerServer.java} | 4 +- ...cation.java => SalaryEvaluatorServer.java} | 4 +- ...lication.java => SkillsMatcherServer.java} | 4 +- .../jobscreening/JobScreeningLiveTest.java | 73 ++ 7 files changed, 942 insertions(+), 11 deletions(-) create mode 100644 spring-ai-modules/spring-ai-a2a/project.md rename spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/{Application.java => JobScreeningOrchestrator.java} (83%) rename spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/{Application.java => BackgroundCheckerServer.java} (78%) rename spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/{Application.java => SalaryEvaluatorServer.java} (78%) rename spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/{Application.java => SkillsMatcherServer.java} (79%) create mode 100644 spring-ai-modules/spring-ai-a2a/src/test/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningLiveTest.java diff --git a/spring-ai-modules/spring-ai-a2a/pom.xml b/spring-ai-modules/spring-ai-a2a/pom.xml index 27b978e2cd81..3a3110efa429 100644 --- a/spring-ai-modules/spring-ai-a2a/pom.xml +++ b/spring-ai-modules/spring-ai-a2a/pom.xml @@ -35,10 +35,17 @@ 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 @@ -52,19 +59,19 @@ true - com.baeldung.a2a.server.skillsmatcher.Application + com.baeldung.a2a.server.skillsmatcher.SkillsMatcherServer salary-evaluator-server - com.baeldung.a2a.server.salaryevaluator.Application + com.baeldung.a2a.server.salaryevaluator.SalaryEvaluatorServer job-screening-orchestrator-client - com.baeldung.a2a.client.orchestrator.jobscreening.Application + com.baeldung.a2a.client.orchestrator.jobscreening.JobScreeningOrchestrator diff --git a/spring-ai-modules/spring-ai-a2a/project.md b/spring-ai-modules/spring-ai-a2a/project.md new file mode 100644 index 000000000000..eeb3a632f5c7 --- /dev/null +++ b/spring-ai-modules/spring-ai-a2a/project.md @@ -0,0 +1,851 @@ + + +===== pom.xml ===== + + + + 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} + + + + + 21 + 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} + + + + + + + +===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/AgentRegistry.java ===== + +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")); + } +} + +===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java ===== + +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 Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} + +===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/ChatClientConfiguration.java ===== + +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(); + } +} + +===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningController.java ===== + +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 + ) {} +} + +===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentClient.java ===== + +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")); + } +} + +===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentTools.java ===== + +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); + } +} + +===== src/main/java/com/baeldung/a2a/server/backgroundchecker/Application.java ===== + +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") +class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} + +===== src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerTools.java ===== + +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 + } +} + +===== src/main/java/com/baeldung/a2a/server/backgroundchecker/ChatClientConfiguration.java ===== + +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(); + } +} + +===== src/main/java/com/baeldung/a2a/server/backgroundchecker/ServerConfiguration.java ===== + +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/a2a/", 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("0.3.0") + .build(); + } +} + +===== src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java ===== + +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") +class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} + +===== src/main/java/com/baeldung/a2a/server/salaryevaluator/ChatClientConfiguration.java ===== + +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(); + } +} + +===== src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorTools.java ===== + +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 + } +} + +===== src/main/java/com/baeldung/a2a/server/salaryevaluator/ServerConfiguration.java ===== + +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/a2a/", 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("0.3.0") + .build(); + } +} + +===== src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java ===== + +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") +class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} + +===== src/main/java/com/baeldung/a2a/server/skillsmatcher/ChatClientConfiguration.java ===== + +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(); + } +} + +===== src/main/java/com/baeldung/a2a/server/skillsmatcher/ServerConfiguration.java ===== + +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/a2a/", 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("0.3.0") + .build(); + } +} + +===== src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherTools.java ===== + +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 + } +} + +===== src/main/resources/application-background-checker-server.properties ===== + +server.host=localhost +server.port=8083 +server.servlet.context-path=/a2a + +spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.openai.chat.model=gpt-5.5 + +===== src/main/resources/application-job-screening-orchestrator.properties ===== + +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/a2a/,http://localhost:8082/a2a/,http://localhost:8083/a2a/ + +===== src/main/resources/application-salary-evaluator-server.properties ===== + +server.host=localhost +server.port=8082 +server.servlet.context-path=/a2a + +spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.openai.chat.model=gpt-5.5 + +===== src/main/resources/application-skills-matcher-server.properties ===== + +server.host=localhost +server.port=8081 +server.servlet.context-path=/a2a + +spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.openai.chat.model=gpt-5.5 + +===== src/main/resources/logback-spring.xml ===== + + + + + [%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/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningOrchestrator.java similarity index 83% rename from spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java rename to spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningOrchestrator.java index f6622feceb03..31a9c9a855c4 100644 --- a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningOrchestrator.java @@ -7,9 +7,9 @@ @SpringBootApplication(exclude = A2AServerAutoConfiguration.class) @PropertySource("classpath:application-job-screening-orchestrator.properties") -class Application { +class JobScreeningOrchestrator { public static void main(String[] args) { - SpringApplication.run(Application.class, 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/server/backgroundchecker/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerServer.java similarity index 78% rename from spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/Application.java rename to spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerServer.java index 2e0c9ccf6519..27302c37da0d 100644 --- a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/Application.java +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerServer.java @@ -6,9 +6,9 @@ @SpringBootApplication @PropertySource("classpath:application-background-checker-server.properties") -class Application { +public class BackgroundCheckerServer { public static void main(String[] args) { - SpringApplication.run(Application.class, 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/salaryevaluator/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorServer.java similarity index 78% rename from spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java rename to spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorServer.java index d0fa2f7c4c7d..e80269affd57 100644 --- a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorServer.java @@ -6,9 +6,9 @@ @SpringBootApplication @PropertySource("classpath:application-salary-evaluator-server.properties") -class Application { +public class SalaryEvaluatorServer { public static void main(String[] args) { - SpringApplication.run(Application.class, 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/skillsmatcher/Application.java b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherServer.java similarity index 79% rename from spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java rename to spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherServer.java index 1827d2d96252..351c30fed8af 100644 --- a/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java +++ b/spring-ai-modules/spring-ai-a2a/src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherServer.java @@ -6,9 +6,9 @@ @SpringBootApplication @PropertySource("classpath:application-skills-matcher-server.properties") -class Application { +public class SkillsMatcherServer { public static void main(String[] args) { - SpringApplication.run(Application.class, args); + SpringApplication.run(SkillsMatcherServer.class, args); } } \ 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 From c1d09b102277446bb79d9cb6734940c42362e97e Mon Sep 17 00:00:00 2001 From: Hardik Singh Behl Date: Fri, 31 Jul 2026 23:13:35 +0530 Subject: [PATCH 10/10] remove project.md --- spring-ai-modules/spring-ai-a2a/project.md | 851 --------------------- 1 file changed, 851 deletions(-) delete mode 100644 spring-ai-modules/spring-ai-a2a/project.md diff --git a/spring-ai-modules/spring-ai-a2a/project.md b/spring-ai-modules/spring-ai-a2a/project.md deleted file mode 100644 index eeb3a632f5c7..000000000000 --- a/spring-ai-modules/spring-ai-a2a/project.md +++ /dev/null @@ -1,851 +0,0 @@ - - -===== pom.xml ===== - - - - 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} - - - - - 21 - 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} - - - - - - - -===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/AgentRegistry.java ===== - -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")); - } -} - -===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/Application.java ===== - -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 Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} - -===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/ChatClientConfiguration.java ===== - -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(); - } -} - -===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/JobScreeningController.java ===== - -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 - ) {} -} - -===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentClient.java ===== - -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")); - } -} - -===== src/main/java/com/baeldung/a2a/client/orchestrator/jobscreening/RemoteAgentTools.java ===== - -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); - } -} - -===== src/main/java/com/baeldung/a2a/server/backgroundchecker/Application.java ===== - -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") -class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} - -===== src/main/java/com/baeldung/a2a/server/backgroundchecker/BackgroundCheckerTools.java ===== - -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 - } -} - -===== src/main/java/com/baeldung/a2a/server/backgroundchecker/ChatClientConfiguration.java ===== - -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(); - } -} - -===== src/main/java/com/baeldung/a2a/server/backgroundchecker/ServerConfiguration.java ===== - -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/a2a/", 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("0.3.0") - .build(); - } -} - -===== src/main/java/com/baeldung/a2a/server/salaryevaluator/Application.java ===== - -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") -class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} - -===== src/main/java/com/baeldung/a2a/server/salaryevaluator/ChatClientConfiguration.java ===== - -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(); - } -} - -===== src/main/java/com/baeldung/a2a/server/salaryevaluator/SalaryEvaluatorTools.java ===== - -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 - } -} - -===== src/main/java/com/baeldung/a2a/server/salaryevaluator/ServerConfiguration.java ===== - -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/a2a/", 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("0.3.0") - .build(); - } -} - -===== src/main/java/com/baeldung/a2a/server/skillsmatcher/Application.java ===== - -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") -class Application { - - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } -} - -===== src/main/java/com/baeldung/a2a/server/skillsmatcher/ChatClientConfiguration.java ===== - -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(); - } -} - -===== src/main/java/com/baeldung/a2a/server/skillsmatcher/ServerConfiguration.java ===== - -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/a2a/", 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("0.3.0") - .build(); - } -} - -===== src/main/java/com/baeldung/a2a/server/skillsmatcher/SkillsMatcherTools.java ===== - -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 - } -} - -===== src/main/resources/application-background-checker-server.properties ===== - -server.host=localhost -server.port=8083 -server.servlet.context-path=/a2a - -spring.ai.openai.api-key=${OPENAI_API_KEY} -spring.ai.openai.chat.model=gpt-5.5 - -===== src/main/resources/application-job-screening-orchestrator.properties ===== - -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/a2a/,http://localhost:8082/a2a/,http://localhost:8083/a2a/ - -===== src/main/resources/application-salary-evaluator-server.properties ===== - -server.host=localhost -server.port=8082 -server.servlet.context-path=/a2a - -spring.ai.openai.api-key=${OPENAI_API_KEY} -spring.ai.openai.chat.model=gpt-5.5 - -===== src/main/resources/application-skills-matcher-server.properties ===== - -server.host=localhost -server.port=8081 -server.servlet.context-path=/a2a - -spring.ai.openai.api-key=${OPENAI_API_KEY} -spring.ai.openai.chat.model=gpt-5.5 - -===== src/main/resources/logback-spring.xml ===== - - - - - [%d{yyyy-MM-dd HH:mm:ss}] [%p] [%c{1}] - %m%n - - - - - - - - - - - \ No newline at end of file