diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/pom.xml b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/pom.xml new file mode 100644 index 00000000000..b4805b36429 --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/pom.xml @@ -0,0 +1,86 @@ + + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.0.0-SNAPSHOT + ../../../../../../pom.xml + + spring-ai-autoconfigure-model-chat-memory-repository-file + jar + Spring AI File Chat Memory Repository Auto Configuration + Spring File AI Chat Memory Repository Auto Configuration + https://github.com/spring-projects/spring-ai + + + https://github.com/spring-projects/spring-ai + git://github.com/spring-projects/spring-ai.git + git@github.com:spring-projects/spring-ai.git + + + + + + org.springframework.ai + spring-ai-model-chat-memory-repository-file + ${project.parent.version} + + + + org.springframework.ai + spring-ai-autoconfigure-model-chat-memory + ${project.parent.version} + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.testcontainers + junit-jupiter + test + + + + + diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/src/main/java/org/springframework/ai/model/chat/memory/repository/file/autoconfigure/FileChatMemoryRepositoryAutoConfiguration.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/src/main/java/org/springframework/ai/model/chat/memory/repository/file/autoconfigure/FileChatMemoryRepositoryAutoConfiguration.java new file mode 100644 index 00000000000..9e221c52fe6 --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/src/main/java/org/springframework/ai/model/chat/memory/repository/file/autoconfigure/FileChatMemoryRepositoryAutoConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.model.chat.memory.repository.file.autoconfigure; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.memory.repository.file.FileChatMemoryRepository; +import org.springframework.ai.model.chat.memory.autoconfigure.ChatMemoryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * @author John Dahle + */ +@AutoConfiguration(before = ChatMemoryAutoConfiguration.class) +@EnableConfigurationProperties(FileChatMemoryRepositoryProperties.class) +@ConditionalOnProperty(prefix = FileChatMemoryRepositoryProperties.CONFIG_PREFIX, name = "enabled", + havingValue = "true", matchIfMissing = true) +public class FileChatMemoryRepositoryAutoConfiguration { + + private final FileChatMemoryRepositoryProperties props; + + public FileChatMemoryRepositoryAutoConfiguration(FileChatMemoryRepositoryProperties props) { + this.props = props; + } + + @Bean + @ConditionalOnMissingBean(ChatMemoryRepository.class) + public FileChatMemoryRepository fileChatMemoryRepository(ObjectMapper objectMapper) { + Path baseDir = Paths.get(props.getBaseDir()); + return new FileChatMemoryRepository(baseDir, objectMapper); + } + +} diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/src/main/java/org/springframework/ai/model/chat/memory/repository/file/autoconfigure/FileChatMemoryRepositoryProperties.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/src/main/java/org/springframework/ai/model/chat/memory/repository/file/autoconfigure/FileChatMemoryRepositoryProperties.java new file mode 100644 index 00000000000..649abf2aad2 --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/src/main/java/org/springframework/ai/model/chat/memory/repository/file/autoconfigure/FileChatMemoryRepositoryProperties.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.model.chat.memory.repository.file.autoconfigure; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author John Dahle + */ + +@ConfigurationProperties(FileChatMemoryRepositoryProperties.CONFIG_PREFIX) +public class FileChatMemoryRepositoryProperties { + + public static final String CONFIG_PREFIX = "spring.ai.chat.memory.repository.file"; + private boolean enabled = true; + private String baseDir = System.getProperty("user.home") + "/.spring-ai/chat-memory"; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getBaseDir() { + return baseDir; + } + + public void setBaseDir(String baseDir) { + this.baseDir = baseDir; + } + +} diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..a214e883aba --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,16 @@ +# +# Copyright 2024-2025 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +org.springframework.ai.model.chat.memory.repository.file.autoconfigure.FileChatMemoryRepositoryAutoConfiguration diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/pom.xml b/memory/repository/spring-ai-model-chat-memory-repository-file/pom.xml new file mode 100644 index 00000000000..d583972f2e7 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/pom.xml @@ -0,0 +1,64 @@ + + + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.0.0-SNAPSHOT + ../../../pom.xml + + + spring-ai-model-chat-memory-repository-file + Spring File-based Chat Memory Repository + Spring File-based Chat Memory Repository implementation + + https://github.com/spring-projects/spring-ai + + + https://github.com/spring-projects/spring-ai + git://github.com/spring-projects/spring-ai.git + git@github.com:spring-projects/spring-ai.git + + + + + org.springframework.ai + spring-ai-client-chat + ${project.version} + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.ai + spring-ai-test + ${project.version} + test + + + + + diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/FileChatMemoryRepository.java b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/FileChatMemoryRepository.java new file mode 100644 index 00000000000..ff00a1a460e --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/FileChatMemoryRepository.java @@ -0,0 +1,109 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.chat.memory.repository.file; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.memory.repository.file.dto.MessageDto; +import org.springframework.ai.chat.memory.repository.file.dto.MessageDtoMapper; +import org.springframework.ai.chat.messages.Message; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +/** + * @author John Dahle + */ +public class FileChatMemoryRepository implements ChatMemoryRepository { + + private final Path baseDir; + + private final ObjectMapper objectMapper; + + public FileChatMemoryRepository(Path baseDir, ObjectMapper objectMapper) { + this.baseDir = baseDir; + this.objectMapper = objectMapper; + try { + Files.createDirectories(baseDir); + } + catch (IOException e) { + throw new RuntimeException("Failed to create base directory: " + baseDir, e); + } + } + + private Path fileFor(String conversationId) { + return baseDir.resolve(conversationId + ".json"); + } + + @Override + public List findConversationIds() { + try (var stream = Files.list(baseDir)) { + return stream.filter(p -> p.toString().endsWith(".json")) + .map(p -> p.getFileName().toString().replaceFirst("\\.json$", "")) + .collect(Collectors.toList()); + } + catch (IOException e) { + throw new RuntimeException("Failed to list conversation IDs", e); + } + } + + @Override + public List findByConversationId(String conversationId) { + Path file = fileFor(conversationId); + if (!Files.exists(file)) { + return Collections.emptyList(); + } + try { + // 1. Read DTOs from disk + List dtos = objectMapper.readValue(file.toFile(), new TypeReference>() { + }); + // 2. Map them back to domain Messages + return MessageDtoMapper.toDomainList(dtos); + } + catch (IOException e) { + throw new RuntimeException("Failed to read messages for conversation: " + conversationId, e); + } + } + + @Override + public void saveAll(String conversationId, List messages) { + try { + // 1. Convert domain Messages into DTOs + List dtos = MessageDtoMapper.toDtoList(messages); + // 2. Tell Jackson they’re DTOs and write them + objectMapper.writerFor(new TypeReference>() { + }).writeValue(fileFor(conversationId).toFile(), dtos); + } + catch (IOException e) { + throw new RuntimeException("Failed to write messages for conversation: " + conversationId, e); + } + } + + @Override + public void deleteByConversationId(String conversationId) { + try { + Files.deleteIfExists(fileFor(conversationId)); + } + catch (IOException e) { + throw new RuntimeException("Failed to delete conversation: " + conversationId, e); + } + } + +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/AssistantMessageDto.java b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/AssistantMessageDto.java new file mode 100644 index 00000000000..b81756f2a20 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/AssistantMessageDto.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.chat.memory.repository.file.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; + +/** + * @author John Dahle + */ + +@JsonTypeName("assistant") +public record AssistantMessageDto(@JsonProperty("text") String text) implements MessageDto { + + @JsonCreator + public AssistantMessageDto { + } // Jackson will call this record‐canonical ctor + + @Override + public Message toDomain() { + return new AssistantMessage(text); + } +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/MessageDto.java b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/MessageDto.java new file mode 100644 index 00000000000..ce3fcf87829 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/MessageDto.java @@ -0,0 +1,33 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.chat.memory.repository.file.dto; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.ai.chat.messages.Message; + +/** + * @author John Dahle + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "kind") +@JsonSubTypes({ @JsonSubTypes.Type(value = AssistantMessageDto.class, name = "assistant"), + @JsonSubTypes.Type(value = UserMessageDto.class, name = "user"), + @JsonSubTypes.Type(value = SystemMessageDto.class, name = "system") }) +public sealed interface MessageDto permits AssistantMessageDto, UserMessageDto, SystemMessageDto { + + Message toDomain(); + +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/MessageDtoMapper.java b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/MessageDtoMapper.java new file mode 100644 index 00000000000..4c2aeb62efc --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/MessageDtoMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.chat.memory.repository.file.dto; + +import org.springframework.ai.chat.messages.Message; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author John Dahle + */ +public final class MessageDtoMapper { + + private MessageDtoMapper() { + /* no-op */ } + + public static MessageDto toDto(Message msg) { + return switch (msg.getMessageType()) { + case ASSISTANT -> new AssistantMessageDto(msg.getText()); + case USER -> new UserMessageDto(msg.getText()); + case SYSTEM -> new SystemMessageDto(msg.getText()); + default -> throw new IllegalArgumentException("Unsupported message type: " + msg.getMessageType()); + }; + } + + public static Message toDomain(MessageDto dto) { + return dto.toDomain(); + } + + public static List toDtoList(List messages) { + return messages.stream().map(MessageDtoMapper::toDto).collect(Collectors.toList()); + } + + public static List toDomainList(List dtos) { + return dtos.stream().map(MessageDto::toDomain).collect(Collectors.toList()); + } + +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/SystemMessageDto.java b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/SystemMessageDto.java new file mode 100644 index 00000000000..2b2d627ce9f --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/SystemMessageDto.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.chat.memory.repository.file.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; + +/** + * @author John Dahle + */ +@JsonTypeName("system") +public record SystemMessageDto(@JsonProperty("text") String text) implements MessageDto { + + @JsonCreator + public SystemMessageDto { + } + + @Override + public Message toDomain() { + return new SystemMessage(text); + } +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/UserMessageDto.java b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/UserMessageDto.java new file mode 100644 index 00000000000..4eb1d37381f --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/dto/UserMessageDto.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.chat.memory.repository.file.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.UserMessage; + +/** + * @author John Dahle + */ +@JsonTypeName("user") +public record UserMessageDto(@JsonProperty("text") String text) implements MessageDto { + @JsonCreator + public UserMessageDto { + } + + @Override + public Message toDomain() { + return new UserMessage(text); + } +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/package-info.java b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/package-info.java new file mode 100644 index 00000000000..60758af4de8 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/src/main/java/org/springframework/ai/chat/memory/repository/file/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@NonNullApi +@NonNullFields +package org.springframework.ai.chat.memory.repository.file; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/src/test/java/org/springframework/ai/chat/memory/repository/file/FileChatMemoryRepositoryDtoIT.java b/memory/repository/spring-ai-model-chat-memory-repository-file/src/test/java/org/springframework/ai/chat/memory/repository/file/FileChatMemoryRepositoryDtoIT.java new file mode 100644 index 00000000000..e30cc14c4ab --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/src/test/java/org/springframework/ai/chat/memory/repository/file/FileChatMemoryRepositoryDtoIT.java @@ -0,0 +1,76 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.chat.memory.repository.file; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author John Dahle + */ +class FileChatMemoryRepositoryDtoIT { + + private static final String SAMPLE_JSON = """ + [ + { + "kind": "assistant", + "text": "Hello from assistant" + }, + { + "kind": "user", + "text": "Hello from user" + }, + { + "kind": "system", + "text": "System initialization" + } + ] + """; + + @Test + void loadSampleMemoryIncludingSystem(@TempDir Path tempDir) throws Exception { + String conversationId = "sample-convo"; + Path file = tempDir.resolve(conversationId + ".json"); + Files.writeString(file, SAMPLE_JSON); + ChatMemoryRepository repo = new FileChatMemoryRepository(tempDir, new ObjectMapper()); + List ids = repo.findConversationIds(); + assertThat(ids).containsExactly(conversationId); + List messages = repo.findByConversationId(conversationId); + assertThat(messages).hasSize(3); + assertThat(messages.get(0)).isInstanceOf(AssistantMessage.class) + .extracting(Message::getText) + .isEqualTo("Hello from assistant"); + assertThat(messages.get(1)).isInstanceOf(UserMessage.class) + .extracting(Message::getText) + .isEqualTo("Hello from user"); + assertThat(messages.get(2)).isInstanceOf(SystemMessage.class) + .extracting(Message::getText) + .isEqualTo("System initialization"); + } + +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-file/src/test/java/org/springframework/ai/chat/memory/repository/file/FileChatMemoryRepositoryIT.java b/memory/repository/spring-ai-model-chat-memory-repository-file/src/test/java/org/springframework/ai/chat/memory/repository/file/FileChatMemoryRepositoryIT.java new file mode 100644 index 00000000000..5d3c390b1f4 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-file/src/test/java/org/springframework/ai/chat/memory/repository/file/FileChatMemoryRepositoryIT.java @@ -0,0 +1,116 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.ai.chat.memory.repository.file; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.MessageType; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +/** + * @author John Dahle + */ +@SpringBootTest(classes = FileChatMemoryRepositoryIT.TestConfig.class) +public class FileChatMemoryRepositoryIT { + + @TempDir + static Path tempDir; + + @Autowired + private ChatMemoryRepository chatMemoryRepository; + + @Test + void correctChatMemoryRepositoryInstance() { + assertThat(chatMemoryRepository).isInstanceOf(FileChatMemoryRepository.class); + } + + @ParameterizedTest + @CsvSource({ "Message from assistant,ASSISTANT", "Message from user,USER", "Message from system,SYSTEM" }) + void saveMessagesSingleMessage(String content, MessageType messageType) { + String conversationId = UUID.randomUUID().toString(); + Message message = switch (messageType) { + case ASSISTANT -> new AssistantMessage(content + " - " + conversationId); + case USER -> new UserMessage(content + " - " + conversationId); + case SYSTEM -> new SystemMessage(content + " - " + conversationId); + default -> throw new IllegalArgumentException("Unsupported type: " + messageType); + }; + + chatMemoryRepository.saveAll(conversationId, List.of(message)); + List results = chatMemoryRepository.findByConversationId(conversationId); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getText()).isEqualTo(message.getText()); + assertThat(results.get(0).getMessageType()).isEqualTo(messageType); + } + + @Test + void deleteMessagesByConversationId() { + String conversationId = UUID.randomUUID().toString(); + List messages = List.of(new AssistantMessage("Assistant"), new UserMessage("User"), + new SystemMessage("System")); + + chatMemoryRepository.saveAll(conversationId, messages); + assertThat(chatMemoryRepository.findByConversationId(conversationId)).hasSize(3); + + chatMemoryRepository.deleteByConversationId(conversationId); + assertThat(chatMemoryRepository.findByConversationId(conversationId)).isEmpty(); + } + + @Configuration + static class TestConfig { + + @Bean + public ChatMemoryRepository chatMemoryRepository() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + // Attach @class metadata only for Message types + mapper.addMixIn(Message.class, MessageMixin.class); + + // Register concrete Message subtypes + mapper.registerSubtypes(AssistantMessage.class, UserMessage.class, SystemMessage.class); + + // Prepare the temporary directory + Files.createDirectories(tempDir); + return new FileChatMemoryRepository(tempDir, mapper); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class") + private abstract static class MessageMixin { + + } + + } + +} diff --git a/pom.xml b/pom.xml index 599bf8b4237..1153943adfc 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ memory/repository/spring-ai-model-chat-memory-repository-cassandra memory/repository/spring-ai-model-chat-memory-repository-jdbc memory/repository/spring-ai-model-chat-memory-repository-neo4j + memory/repository/spring-ai-model-chat-memory-repository-file @@ -89,6 +90,7 @@ auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-cassandra auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-jdbc + auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-file auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-neo4j auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation @@ -195,9 +197,11 @@ spring-ai-spring-boot-starters/spring-ai-starter-model-deepseek spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-cassandra + spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-file spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-jdbc spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-neo4j + spring-ai-spring-boot-starters/spring-ai-starter-mcp-client spring-ai-spring-boot-starters/spring-ai-starter-mcp-server spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-file/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-file/pom.xml new file mode 100644 index 00000000000..1610338d8e9 --- /dev/null +++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-file/pom.xml @@ -0,0 +1,64 @@ + + + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.0.0-SNAPSHOT + ../../pom.xml + + spring-ai-starter-model-chat-memory-repository-file + jar + Spring AI Starter - JDBC Chat Memory Repository + Spring AI JDBC Chat Memory Repository Starter + https://github.com/spring-projects/spring-ai + + + https://github.com/spring-projects/spring-ai + git://github.com/spring-projects/spring-ai.git + git@github.com:spring-projects/spring-ai.git + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.ai + spring-ai-autoconfigure-model-chat-memory + ${project.parent.version} + + + + org.springframework.ai + spring-ai-autoconfigure-model-chat-memory-repository-file + ${project.parent.version} + + + + org.springframework.ai + spring-ai-model-chat-memory-repository-file + ${project.parent.version} + + + +