From 7d5108a734ca5aaee9dbde9620d01ab4ec5d1984 Mon Sep 17 00:00:00 2001 From: Sagar Verma Date: Sun, 16 Aug 2026 21:17:54 +0530 Subject: [PATCH 1/2] BAEL-9644: TodoWriteTool in Spring AI changes --- spring-ai-modules/pom.xml | 1 + .../spring-ai-todowrite-tool/pom.xml | 83 +++++++++++++++++++ .../springai/todowritetool/Application.java | 33 ++++++++ .../springai/todowritetool/TodoItem.java | 4 + .../springai/todowritetool/TodoService.java | 21 +++++ .../springai/todowritetool/TodoWriteTool.java | 30 +++++++ .../config/TodoAgentService.java | 31 +++++++ .../src/main/resources/application.properties | 3 + .../ApplicationIntegrationTest.java | 14 ++++ .../TodoAgentServiceIntegrationTest.java | 32 +++++++ .../todowritetool/TodoWriteToolUnitTest.java | 27 ++++++ .../resources/application-test.properties | 1 + 12 files changed, 280 insertions(+) create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/pom.xml create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/Application.java create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoItem.java create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoService.java create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoWriteTool.java create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/config/TodoAgentService.java create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/main/resources/application.properties create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/ApplicationIntegrationTest.java create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoAgentServiceIntegrationTest.java create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoWriteToolUnitTest.java create mode 100644 spring-ai-modules/spring-ai-todowrite-tool/src/test/resources/application-test.properties diff --git a/spring-ai-modules/pom.xml b/spring-ai-modules/pom.xml index 341aed83f4ed..acb011928872 100644 --- a/spring-ai-modules/pom.xml +++ b/spring-ai-modules/pom.xml @@ -35,5 +35,6 @@ spring-ai-vector-stores spring-ai-mcp-annotations spring-ai-subagent-orchestrator + spring-ai-todowrite-tool diff --git a/spring-ai-modules/spring-ai-todowrite-tool/pom.xml b/spring-ai-modules/spring-ai-todowrite-tool/pom.xml new file mode 100644 index 000000000000..bf557caf805d --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + + com.baeldung + spring-ai-modules + 0.0.1 + ../pom.xml + + + spring-ai-todowrite-tool + spring-ai-todowrite-tool + spring-ai-todowrite-tool + + + 21 + 2.0.0-M5 + 4.0.6 + 6.0.3 + 6.0.3 + 2.0.17 + 1.5.18 + 3.5.5 + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + + + + org.springframework.ai + spring-ai-starter-model-openai + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.platform + junit-platform-launcher + ${junit-platform.version} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + + diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/Application.java b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/Application.java new file mode 100644 index 000000000000..5829c970c3e8 --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/Application.java @@ -0,0 +1,33 @@ +package com.baeldung.springai.todowritetool; + +import com.baeldung.springai.todowritetool.config.TodoAgentService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; + +@SpringBootApplication +public class Application { + + private static final Logger logger = LoggerFactory.getLogger(Application.class); + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + @Profile("!test") + CommandLineRunner demo(TodoAgentService todoAgentService) { + return args -> { + String response = todoAgentService.ask( + """ + Track these steps as a todo list: set up the project, write the tool, add tests + """ + ); + logger.info("{}", response); + }; + } +} diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoItem.java b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoItem.java new file mode 100644 index 000000000000..8389050170c0 --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoItem.java @@ -0,0 +1,4 @@ +package com.baeldung.springai.todowritetool; + +public record TodoItem(String id, String content, String status, String priority) { +} diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoService.java b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoService.java new file mode 100644 index 000000000000..66d291e3089c --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoService.java @@ -0,0 +1,21 @@ +package com.baeldung.springai.todowritetool; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.stereotype.Service; + +@Service +public class TodoService { + + private final AtomicReference> todos = new AtomicReference<>(List.of()); + + public List write(List updatedTodos) { + todos.set(updatedTodos); + return updatedTodos; + } + + public List read() { + return todos.get(); + } +} diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoWriteTool.java b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoWriteTool.java new file mode 100644 index 000000000000..a4ce241dabab --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoWriteTool.java @@ -0,0 +1,30 @@ +package com.baeldung.springai.todowritetool; + +import java.util.List; + +import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.tool.annotation.ToolParam; +import org.springframework.stereotype.Component; + +@Component +public class TodoWriteTool { + + private final TodoService todoService; + + public TodoWriteTool(TodoService todoService) { + this.todoService = todoService; + } + + @Tool(description = "Create or update the structured todo list for the " + + "current session, replacing any previous list") + public List todoWrite( + @ToolParam(description = "The full list of todo items, including " + + "unchanged ones") List todos) { + return todoService.write(todos); + } + + @Tool(description = "Read the current todo list for the session") + public List todoRead() { + return todoService.read(); + } +} diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/config/TodoAgentService.java b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/config/TodoAgentService.java new file mode 100644 index 000000000000..f84432fc48ab --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/config/TodoAgentService.java @@ -0,0 +1,31 @@ +package com.baeldung.springai.todowritetool.config; + +import com.baeldung.springai.todowritetool.TodoWriteTool; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.stereotype.Service; + +@Service +public class TodoAgentService { + + private final ChatClient chatClient; + + public TodoAgentService(ChatClient.Builder chatClientBuilder, TodoWriteTool todoWriteTool) { + this.chatClient = chatClientBuilder.clone() + .defaultSystem(""" + You are a task-tracking assistant. + When the user asks you to track, plan, or list steps, you MUST call the todoWrite tool + with a complete todo list. Use string ids such as "1", "2", "3", statuses + pending/in_progress/completed, and priorities high/medium/low. + After the tool returns, briefly confirm what was recorded. + """) + .defaultTools(todoWriteTool) + .build(); + } + + public String ask(String userMessage) { + return chatClient.prompt() + .user(userMessage) + .call() + .content(); + } +} diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/main/resources/application.properties b/spring-ai-modules/spring-ai-todowrite-tool/src/main/resources/application.properties new file mode 100644 index 000000000000..3dfacd68cc2f --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/main/resources/application.properties @@ -0,0 +1,3 @@ +spring.application.name=spring-ai-todowrite-tool +spring.ai.openai.api-key=${OPENAI_API_KEY} +spring.ai.openai.chat.options.model=gpt-4.1-mini diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/ApplicationIntegrationTest.java b/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/ApplicationIntegrationTest.java new file mode 100644 index 000000000000..9737edfc3a89 --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/ApplicationIntegrationTest.java @@ -0,0 +1,14 @@ +package com.baeldung.springai.todowritetool; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class ApplicationIntegrationTest { + + @Test + void contextLoads() { + } +} diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoAgentServiceIntegrationTest.java b/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoAgentServiceIntegrationTest.java new file mode 100644 index 000000000000..85eff525a5d5 --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoAgentServiceIntegrationTest.java @@ -0,0 +1,32 @@ +package com.baeldung.springai.todowritetool; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.baeldung.springai.todowritetool.config.TodoAgentService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +@SpringBootTest +@ActiveProfiles("test") +@TestPropertySource(locations = "classpath:application-test.properties") +class TodoAgentServiceIntegrationTest { + + @Autowired + private TodoAgentService todoAgentService; + + @Autowired + private TodoService todoService; + + @Test + void whenUserAsksToTrackSteps_thenTodoListIsPopulated() { + String response = todoAgentService.ask( + "Track these steps as a todo list: set up the project, " + + "write the tool, add tests"); + + assertThat(response).isNotBlank(); + assertThat(todoService.read()).isNotEmpty(); + } +} diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoWriteToolUnitTest.java b/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoWriteToolUnitTest.java new file mode 100644 index 000000000000..799872e05c5a --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoWriteToolUnitTest.java @@ -0,0 +1,27 @@ +package com.baeldung.springai.todowritetool; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +class TodoWriteToolUnitTest { + + @Test + void whenTodoWriteIsCalled_thenTodoReadReturnsSameList() { + TodoService todoService = new TodoService(); + TodoWriteTool todoWriteTool = new TodoWriteTool(todoService); + List todos = List.of( + new TodoItem("1", "Set up project", "completed", "high"), + new TodoItem("2", "Write TodoWriteTool", "in_progress", "high"), + new TodoItem("3", "Add tests", "pending", "medium")); + + List written = todoWriteTool.todoWrite(todos); + + assertThat(written).hasSize(3); + assertThat(todoWriteTool.todoRead()) + .extracting(TodoItem::status) + .containsExactly("completed", "in_progress", "pending"); + } +} diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/test/resources/application-test.properties b/spring-ai-modules/spring-ai-todowrite-tool/src/test/resources/application-test.properties new file mode 100644 index 000000000000..b659627b64d1 --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/test/resources/application-test.properties @@ -0,0 +1 @@ +spring.ai.openai.api-key=dummy-key-for-tests From cbe17d360bc76fd196955800b5e063a9d7d1d8bf Mon Sep 17 00:00:00 2001 From: Sagar Verma Date: Tue, 25 Aug 2026 16:52:03 +0530 Subject: [PATCH 2/2] feedback comment fixed --- .../com/baeldung/springai/todowritetool/TodoItem.java | 4 +++- .../springai/todowritetool/TodoWriteToolUnitTest.java | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoItem.java b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoItem.java index 8389050170c0..f8ad87c14b27 100644 --- a/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoItem.java +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoItem.java @@ -1,4 +1,6 @@ package com.baeldung.springai.todowritetool; -public record TodoItem(String id, String content, String status, String priority) { +public record TodoItem(String id, String content, Status status, Priority priority) { + public enum Status { pending, in_progress, completed } + public enum Priority { low, medium, high } } diff --git a/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoWriteToolUnitTest.java b/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoWriteToolUnitTest.java index 799872e05c5a..f9dadfd80fa2 100644 --- a/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoWriteToolUnitTest.java +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/test/java/com/baeldung/springai/todowritetool/TodoWriteToolUnitTest.java @@ -13,15 +13,15 @@ void whenTodoWriteIsCalled_thenTodoReadReturnsSameList() { TodoService todoService = new TodoService(); TodoWriteTool todoWriteTool = new TodoWriteTool(todoService); List todos = List.of( - new TodoItem("1", "Set up project", "completed", "high"), - new TodoItem("2", "Write TodoWriteTool", "in_progress", "high"), - new TodoItem("3", "Add tests", "pending", "medium")); + new TodoItem("1", "Set up project", TodoItem.Status.completed, TodoItem.Priority.high), + new TodoItem("2", "Write TodoWriteTool", TodoItem.Status.in_progress, TodoItem.Priority.high), + new TodoItem("3", "Add tests", TodoItem.Status.pending, TodoItem.Priority.medium)); List written = todoWriteTool.todoWrite(todos); assertThat(written).hasSize(3); assertThat(todoWriteTool.todoRead()) .extracting(TodoItem::status) - .containsExactly("completed", "in_progress", "pending"); + .containsExactly(TodoItem.Status.completed, TodoItem.Status.in_progress, TodoItem.Status.pending); } }