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..f8ad87c14b27 --- /dev/null +++ b/spring-ai-modules/spring-ai-todowrite-tool/src/main/java/com/baeldung/springai/todowritetool/TodoItem.java @@ -0,0 +1,6 @@ +package com.baeldung.springai.todowritetool; + +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/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..f9dadfd80fa2 --- /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", 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(TodoItem.Status.completed, TodoItem.Status.in_progress, TodoItem.Status.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