Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions spring-ai-modules/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@
<module>spring-ai-vector-stores</module>
<module>spring-ai-mcp-annotations</module>
<module>spring-ai-subagent-orchestrator</module>
<module>spring-ai-todowrite-tool</module>
</modules>
</project>
83 changes: 83 additions & 0 deletions spring-ai-modules/spring-ai-todowrite-tool/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.baeldung</groupId>
<artifactId>spring-ai-modules</artifactId>
<version>0.0.1</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>spring-ai-todowrite-tool</artifactId>
<name>spring-ai-todowrite-tool</name>
<description>spring-ai-todowrite-tool</description>

<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0-M5</spring-ai.version>
<spring-boot.version>4.0.6</spring-boot.version>
<junit-jupiter.version>6.0.3</junit-jupiter.version>
<junit-platform.version>6.0.3</junit-platform.version>
<org.slf4j.version>2.0.17</org.slf4j.version>
<logback.version>1.5.18</logback.version>
<maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>
</properties>

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>${junit-platform.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -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);
};
}
}
Original file line number Diff line number Diff line change
@@ -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 }
}
Original file line number Diff line number Diff line change
@@ -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<List<TodoItem>> todos = new AtomicReference<>(List.of());

public List<TodoItem> write(List<TodoItem> updatedTodos) {
todos.set(updatedTodos);
return updatedTodos;
}

public List<TodoItem> read() {
return todos.get();
}
}
Original file line number Diff line number Diff line change
@@ -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<TodoItem> todoWrite(
@ToolParam(description = "The full list of todo items, including "
+ "unchanged ones") List<TodoItem> todos) {
return todoService.write(todos);
}

@Tool(description = "Read the current todo list for the session")
public List<TodoItem> todoRead() {
return todoService.read();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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() {
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<TodoItem> 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<TodoItem> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
spring.ai.openai.api-key=dummy-key-for-tests