From 0320514c122f840e606b9928f3b17c07aafaa4a0 Mon Sep 17 00:00:00 2001 From: Manfred Ng Date: Sat, 28 Mar 2026 13:01:28 +0000 Subject: [PATCH 1/2] BAEL-9647: Anthropic Agent Skills Support in Spring AI --- .../spring-ai-agent-skills/pom.xml | 65 +++++++++++++++++++ .../anthropic/AgentSkillsController.java | 42 ++++++++++++ .../anthropic/AgentSkillsService.java | 56 ++++++++++++++++ .../anthropic/AnthropicDocument.java | 4 ++ .../agentskills/anthropic/Application.java | 12 ++++ .../agentskills/anthropic/MonthlySale.java | 7 ++ .../anthropic/MonthlySalesService.java | 41 ++++++++++++ .../agentskills/anthropic/ReportRequest.java | 6 ++ .../main/resources/application-anthropic.yml | 8 +++ .../src/main/resources/application.yml | 3 + .../AgentSkillsControllerIntegrationTest.java | 55 ++++++++++++++++ 11 files changed, 299 insertions(+) create mode 100644 spring-ai-modules/spring-ai-agent-skills/pom.xml create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsController.java create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsService.java create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AnthropicDocument.java create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/Application.java create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/MonthlySale.java create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/MonthlySalesService.java create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/ReportRequest.java create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/main/resources/application-anthropic.yml create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/main/resources/application.yml create mode 100644 spring-ai-modules/spring-ai-agent-skills/src/test/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsControllerIntegrationTest.java diff --git a/spring-ai-modules/spring-ai-agent-skills/pom.xml b/spring-ai-modules/spring-ai-agent-skills/pom.xml new file mode 100644 index 000000000000..31a662800a00 --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + com.baeldung + spring-ai-modules + 0.0.1 + ../pom.xml + + + spring-ai-agent-skills + spring-ai-agent-skills + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.ai + spring-ai-starter-model-anthropic + + + org.apache.tika + tika-core + ${tika.version} + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + 21 + 1.1.4 + 3.5.13 + 2.0.17 + 1.5.18 + 3.3.0 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsController.java b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsController.java new file mode 100644 index 000000000000..555107e1c7ca --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsController.java @@ -0,0 +1,42 @@ +package com.baeldung.springai.agentskills.anthropic; + +import javax.validation.Valid; + +import org.apache.tika.Tika; + +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/agent-skills") +@Validated +public class AgentSkillsController { + + private final AgentSkillsService agentSkillsService; + private final Tika tika = new Tika(); + + public AgentSkillsController(AgentSkillsService agentSkillsService) { + this.agentSkillsService = agentSkillsService; + } + + @GetMapping("/report") + public ResponseEntity genReport(@RequestBody @Valid ReportRequest reportRequest) { + AnthropicDocument document = agentSkillsService.genReport(reportRequest); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(tika.detect(document.content()))) + .header(HttpHeaders.CONTENT_DISPOSITION, + ContentDisposition.attachment() + .filename(document.fileName()) + .build() + .toString()) + .body(document.content()); + } + +} diff --git a/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsService.java b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsService.java new file mode 100644 index 000000000000..d9adb75dd853 --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsService.java @@ -0,0 +1,56 @@ +package com.baeldung.springai.agentskills.anthropic; + +import java.util.List; + +import org.springframework.ai.anthropic.AnthropicChatModel; +import org.springframework.ai.anthropic.AnthropicChatOptions; +import org.springframework.ai.anthropic.AnthropicSkillsResponseHelper; +import org.springframework.ai.anthropic.api.AnthropicApi; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.stereotype.Service; + +@Service +public class AgentSkillsService { + + private final AnthropicChatModel chatModel; + private final AnthropicApi anthropicApi; + private final MonthlySalesService monthlySalesService; + + public AgentSkillsService(AnthropicChatModel chatModel, AnthropicApi anthropicApi, MonthlySalesService monthlySalesService) { + this.chatModel = chatModel; + this.anthropicApi = anthropicApi; + this.monthlySalesService = monthlySalesService; + } + + public AnthropicDocument genReport(ReportRequest reportRequest) { + ChatResponse response = ChatClient.create(chatModel) + .prompt() + .system("Given the dataset of monthly sales for our product: " + monthlySalesService.getMonthlySalesForYear(2025)) + .user(reportRequest.prompt()) + .options(AnthropicChatOptions.builder() + .model("claude-sonnet-4-5") + .skill(AnthropicApi.AnthropicSkill.DOCX) + .skill(AnthropicApi.AnthropicSkill.PDF) + .skill(AnthropicApi.AnthropicSkill.PPTX) + .skill(AnthropicApi.AnthropicSkill.XLSX) + .maxTokens(4096) + .build()) + .call() + .chatResponse(); + + List fileIds = AnthropicSkillsResponseHelper.extractFileIds(response); + if (fileIds.isEmpty()) { + throw new IllegalStateException("No document was generated by the DOCX skill"); + } + + return downloadReport(fileIds.get(0)); + } + + public AnthropicDocument downloadReport(String fileId) { + AnthropicApi.FileMetadata metadata = anthropicApi.getFileMetadata(fileId); + byte[] content = anthropicApi.downloadFile(fileId); + return new AnthropicDocument(metadata.filename(), content); + } + +} diff --git a/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AnthropicDocument.java b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AnthropicDocument.java new file mode 100644 index 000000000000..9b05f18921de --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/AnthropicDocument.java @@ -0,0 +1,4 @@ +package com.baeldung.springai.agentskills.anthropic; + +public record AnthropicDocument(String fileName, byte[] content) { +} \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/Application.java b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/Application.java new file mode 100644 index 000000000000..a89b1ca1f0b4 --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/Application.java @@ -0,0 +1,12 @@ +package com.baeldung.springai.agentskills.anthropic; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/MonthlySale.java b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/MonthlySale.java new file mode 100644 index 000000000000..857037672249 --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/MonthlySale.java @@ -0,0 +1,7 @@ +package com.baeldung.springai.agentskills.anthropic; + +import java.math.BigDecimal; +import java.time.Month; + +public record MonthlySale(String product, int year, Month month, BigDecimal amount) { +} diff --git a/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/MonthlySalesService.java b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/MonthlySalesService.java new file mode 100644 index 000000000000..db5a3d3261c9 --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/MonthlySalesService.java @@ -0,0 +1,41 @@ +package com.baeldung.springai.agentskills.anthropic; + +import java.math.BigDecimal; +import java.time.Month; +import java.util.List; + +import org.springframework.stereotype.Service; + +@Service +public class MonthlySalesService { + + public List getMonthlySalesForYear(int year) { + return List.of( + new MonthlySale("Product A", year, Month.JANUARY, new BigDecimal("1200")), + new MonthlySale("Product A", year, Month.FEBRUARY, new BigDecimal("1325")), + new MonthlySale("Product A", year, Month.MARCH, new BigDecimal("1410")), + new MonthlySale("Product A", year, Month.APRIL, new BigDecimal("1380")), + new MonthlySale("Product A", year, Month.MAY, new BigDecimal("1495")), + new MonthlySale("Product A", year, Month.JUNE, new BigDecimal("1560")), + new MonthlySale("Product A", year, Month.JULY, new BigDecimal("1620")), + new MonthlySale("Product A", year, Month.AUGUST, new BigDecimal("1585")), + new MonthlySale("Product A", year, Month.SEPTEMBER, new BigDecimal("1660")), + new MonthlySale("Product A", year, Month.OCTOBER, new BigDecimal("1715")), + new MonthlySale("Product A", year, Month.NOVEMBER, new BigDecimal("1780")), + new MonthlySale("Product A", year, Month.DECEMBER, new BigDecimal("1850")), + new MonthlySale("Product B", year, Month.JANUARY, new BigDecimal("950")), + new MonthlySale("Product B", year, Month.FEBRUARY, new BigDecimal("990")), + new MonthlySale("Product B", year, Month.MARCH, new BigDecimal("1045")), + new MonthlySale("Product B", year, Month.APRIL, new BigDecimal("1015")), + new MonthlySale("Product B", year, Month.MAY, new BigDecimal("1090")), + new MonthlySale("Product B", year, Month.JUNE, new BigDecimal("1135")), + new MonthlySale("Product B", year, Month.JULY, new BigDecimal("1180")), + new MonthlySale("Product B", year, Month.AUGUST, new BigDecimal("1160")), + new MonthlySale("Product B", year, Month.SEPTEMBER, new BigDecimal("1215")), + new MonthlySale("Product B", year, Month.OCTOBER, new BigDecimal("1270")), + new MonthlySale("Product B", year, Month.NOVEMBER, new BigDecimal("1330")), + new MonthlySale("Product B", year, Month.DECEMBER, new BigDecimal("1395")) + ); + } + +} diff --git a/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/ReportRequest.java b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/ReportRequest.java new file mode 100644 index 000000000000..29d6999bc846 --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/main/java/com/baeldung/springai/agentskills/anthropic/ReportRequest.java @@ -0,0 +1,6 @@ +package com.baeldung.springai.agentskills.anthropic; + +import javax.validation.constraints.NotNull; + +public record ReportRequest(@NotNull String prompt) { +} diff --git a/spring-ai-modules/spring-ai-agent-skills/src/main/resources/application-anthropic.yml b/spring-ai-modules/spring-ai-agent-skills/src/main/resources/application-anthropic.yml new file mode 100644 index 000000000000..c4b337a8578d --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/main/resources/application-anthropic.yml @@ -0,0 +1,8 @@ +spring: + + application: + name: agentskills-anthropic + + ai: + anthropic: + api-key: "${ANTHROPIC_API_KEY}" diff --git a/spring-ai-modules/spring-ai-agent-skills/src/main/resources/application.yml b/spring-ai-modules/spring-ai-agent-skills/src/main/resources/application.yml new file mode 100644 index 000000000000..c81b1a1fd268 --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + active: anthropic \ No newline at end of file diff --git a/spring-ai-modules/spring-ai-agent-skills/src/test/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsControllerIntegrationTest.java b/spring-ai-modules/spring-ai-agent-skills/src/test/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsControllerIntegrationTest.java new file mode 100644 index 000000000000..3f27109c733f --- /dev/null +++ b/spring-ai-modules/spring-ai-agent-skills/src/test/java/com/baeldung/springai/agentskills/anthropic/AgentSkillsControllerIntegrationTest.java @@ -0,0 +1,55 @@ +package com.baeldung.springai.agentskills.anthropic; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest(properties = { + "spring.profiles.active=test", + "spring.ai.anthropic.api-key=test-key" +}) +@AutoConfigureMockMvc +class AgentSkillsControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private AgentSkillsService agentSkillsService; + + @Test + void whenReportRequestIsValid_thenEndpointReturnsGeneratedDocument() throws Exception { + byte[] documentContent = "%PDF-1.7\nMock PDF".getBytes(); + ReportRequest reportRequest = new ReportRequest("Generate a monthly sales summary"); + AnthropicDocument generatedDocument = new AnthropicDocument("sales-report.pdf", documentContent); + + when(agentSkillsService.genReport(reportRequest)).thenReturn(generatedDocument); + + mockMvc.perform(get("/agent-skills/report") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "prompt": "Generate a monthly sales summary" + } + """)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_PDF)) + .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"sales-report.pdf\"")) + .andExpect(content().bytes(documentContent)); + + verify(agentSkillsService).genReport(reportRequest); + } + +} From 83f9e54ce1776399cf5278940001f1d6d7f67311 Mon Sep 17 00:00:00 2001 From: Manfred Ng Date: Fri, 24 Apr 2026 17:46:32 +0100 Subject: [PATCH 2/2] BAEL-9647: Anthropic Agent Skills Support in Spring AI --- spring-ai-modules/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/spring-ai-modules/pom.xml b/spring-ai-modules/pom.xml index 09fc0ed8f1ae..0bafd5b562c5 100644 --- a/spring-ai-modules/pom.xml +++ b/spring-ai-modules/pom.xml @@ -21,6 +21,7 @@ spring-ai-3 spring-ai-4 spring-ai-agentic-patterns + spring-ai-agent-skills spring-ai-chat-stream spring-ai-introduction spring-ai-mcp