From 71e75843d136876804144e5f30de1983e79f4464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Jerna=C5=9B?= Date: Wed, 9 Apr 2025 14:43:05 +0200 Subject: [PATCH] Create Mongo Chat Memory repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Łukasz Jernaś --- .../pom.xml | 114 +++++++++++++ .../MongoChatMemoryAutoConfiguration.java | 44 +++++ .../MongoChatMemoryIndexCreator.java | 81 +++++++++ .../MongoChatMemoryProperties.java | 60 +++++++ ...ot.autoconfigure.AutoConfiguration.imports | 16 ++ .../MongoChatMemoryAutoConfigurationIT.java | 82 +++++++++ .../MongoChatMemoryPropertiesTests.java | 44 +++++ .../README.md | 1 + .../pom.xml | 92 ++++++++++ .../ai/chat/memory/mongo/Conversation.java | 30 ++++ .../mongo/MongoChatMemoryRepository.java | 114 +++++++++++++ .../ai/chat/memory/mongo/package-info.java | 22 +++ .../mongo/MongoChatMemoryRepositoryIT.java | 158 ++++++++++++++++++ pom.xml | 3 + spring-ai-bom/pom.xml | 18 ++ .../pom.xml | 58 +++++++ 16 files changed, 937 insertions(+) create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/pom.xml create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryAutoConfiguration.java create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryIndexCreator.java create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryProperties.java create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/test/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryAutoConfigurationIT.java create mode 100644 auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/test/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryPropertiesTests.java create mode 100644 memory/spring-ai-model-chat-memory-mongodb/README.md create mode 100644 memory/spring-ai-model-chat-memory-mongodb/pom.xml create mode 100644 memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/Conversation.java create mode 100644 memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/MongoChatMemoryRepository.java create mode 100644 memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/package-info.java create mode 100644 memory/spring-ai-model-chat-memory-mongodb/src/test/java/org/springframework/ai/chat/memory/mongo/MongoChatMemoryRepositoryIT.java create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-mongodb/pom.xml diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/pom.xml b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/pom.xml new file mode 100644 index 00000000000..88a62921bc5 --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.0.0-SNAPSHOT + ../../../../../pom.xml + + spring-ai-autoconfigure-model-chat-memory-mongodb + jar + Spring AI MongoDB Chat Memory Auto Configuration + Spring AI MongoDB Chat Memory 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-mongodb + ${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-starter-data-mongodb + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + + org.springframework.ai + spring-ai-test + ${project.parent.version} + test + + + + org.springframework.ai + spring-ai-autoconfigure-model-chat-client + ${project.parent.version} + test + + + + org.springframework.ai + spring-ai-openai + ${project.parent.version} + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.testcontainers + junit-jupiter + test + + + + org.testcontainers + mongodb + test + + + + org.mockito + mockito-core + test + + + diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryAutoConfiguration.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryAutoConfiguration.java new file mode 100644 index 00000000000..805c8e72d05 --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * 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.mongo.autoconfigure; + +import org.springframework.ai.chat.memory.mongo.MongoChatMemoryRepository; +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.data.mongo.MongoDataAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.data.mongodb.core.MongoTemplate; + +/** + * Spring Boot autoconfiguration for {@link MongoChatMemoryRepository}. + * + * @author Łukasz Jernaś + * @since 1.0.0 + */ +@AutoConfiguration(after = MongoDataAutoConfiguration.class, before = ChatMemoryAutoConfiguration.class) +@EnableConfigurationProperties(MongoChatMemoryProperties.class) +public class MongoChatMemoryAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + MongoChatMemoryRepository chatMemoryRepository(MongoTemplate mongoTemplate) { + return MongoChatMemoryRepository.builder().mongoTemplate(mongoTemplate).build(); + } + +} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryIndexCreator.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryIndexCreator.java new file mode 100644 index 00000000000..6dfd800a93f --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryIndexCreator.java @@ -0,0 +1,81 @@ +/* + * 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.mongo.autoconfigure; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.memory.mongo.Conversation; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.index.Index; +import org.springframework.stereotype.Component; + +/** + * Class responsible for creating MongoDB proper indices for the ChatMemory. Creates a + * main index on the conversationId and timestamp fields, and a TTL index on the timestamp + * field if the TTL is set in properties. + * + * @author Łukasz Jernaś + * @see MongoChatMemoryProperties + * @since 1.0.0 + */ +@Component +@ConditionalOnProperty(value = "spring.ai.chat.memory.repository.mongo.create-indices", havingValue = "true") +public class MongoChatMemoryIndexCreator { + + private static final Logger logger = LoggerFactory.getLogger(MongoChatMemoryIndexCreator.class); + + private final MongoTemplate mongoTemplate; + + private final MongoChatMemoryProperties mongoChatMemoryProperties; + + public MongoChatMemoryIndexCreator(MongoTemplate mongoTemplate, + MongoChatMemoryProperties mongoChatMemoryProperties) { + this.mongoTemplate = mongoTemplate; + this.mongoChatMemoryProperties = mongoChatMemoryProperties; + } + + @EventListener(ContextRefreshedEvent.class) + public void initIndicesAfterStartup() { + logger.info("Creating MongoDB indices for ChatMemory"); + // Create a main index + mongoTemplate.indexOps(Conversation.class) + .ensureIndex(new Index().on("conversationId", Sort.Direction.ASC).on("timestamp", Sort.Direction.DESC)); + + createOrUpdateTtlIndex(); + } + + private void createOrUpdateTtlIndex() { + if (!this.mongoChatMemoryProperties.getTtl().isZero()) { + // Check for existing TTL index + mongoTemplate.indexOps(Conversation.class).getIndexInfo().forEach(idx -> { + if (idx.getExpireAfter().isPresent() + && !idx.getExpireAfter().get().equals(this.mongoChatMemoryProperties.getTtl())) { + logger.warn("Dropping existing TTL index, because TTL is different"); + mongoTemplate.indexOps(Conversation.class).dropIndex(idx.getName()); + } + }); + mongoTemplate.indexOps(Conversation.class) + .ensureIndex(new Index().on("timestamp", Sort.Direction.ASC) + .expire(this.mongoChatMemoryProperties.getTtl())); + } + } + +} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryProperties.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryProperties.java new file mode 100644 index 00000000000..c009c95b0aa --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryProperties.java @@ -0,0 +1,60 @@ +/* + * 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.mongo.autoconfigure; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +/** + * @author Łukasz Jernaś + * @since 1.0.0 + */ +@ConfigurationProperties(MongoChatMemoryProperties.CONFIG_PREFIX) +public class MongoChatMemoryProperties { + + public static final String CONFIG_PREFIX = "spring.ai.chat.memory.repository.mongo"; + + /** + * If the indexes should be automatically created on app startup. Note: Changing the + * TTL value will drop the TTL index and recreate it. + */ + private boolean createIndices = false; + + /** + * The time to live (TTL) for the conversation documents in the database. The default + * value is 0, which means that the documents will not expire. + */ + private Duration ttl = Duration.ZERO; + + public Duration getTtl() { + return ttl; + } + + public void setTtl(Duration ttl) { + this.ttl = ttl; + } + + public boolean isCreateIndices() { + return createIndices; + } + + public void setCreateIndices(boolean createIndices) { + this.createIndices = createIndices; + } + +} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..820a378b576 --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/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.mongo.autoconfigure.MongoChatMemoryAutoConfiguration diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/test/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryAutoConfigurationIT.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/test/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryAutoConfigurationIT.java new file mode 100644 index 00000000000..e8a104d4957 --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/test/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryAutoConfigurationIT.java @@ -0,0 +1,82 @@ +/* + * 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.mongo.autoconfigure; + +import com.mongodb.client.MongoClient; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.memory.mongo.Conversation; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = { MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, + MongoChatMemoryAutoConfiguration.class, MongoChatMemoryIndexCreator.class }) +@TestPropertySource(properties = { "spring.data.mongodb.uri=spring.data.mongodb.uri=%s/ai_test", + "spring.ai.chat.memory.repository.mongo.create-indices=true" }) +class MongoChatMemoryAutoConfigurationIT { + + @Autowired + private ChatMemoryRepository chatMemoryRepository; + + @Autowired + private MongoTemplate mongoTemplate; + + @Container + @ServiceConnection + static MongoDBContainer mongoDbContainer = new MongoDBContainer("mongo:8.0.6"); + + @Test + void allMethodsShouldExecute() { + var conversationId = UUID.randomUUID().toString(); + var systemMessage = new SystemMessage("Some system message"); + + chatMemoryRepository.saveAll(conversationId, List.of(systemMessage)); + + assertThat(chatMemoryRepository.findConversationIds().contains(conversationId)).isTrue(); + + assertThat(chatMemoryRepository.findByConversationId(conversationId).size()).isEqualTo(1); + + chatMemoryRepository.deleteByConversationId(conversationId); + + assertThat(chatMemoryRepository.findByConversationId(conversationId).size()).isEqualTo(0); + + } + + @Test + void indicesShouldBeCreated() { + var conversationId = UUID.randomUUID().toString(); + var systemMessage = new SystemMessage("Some system message"); + + chatMemoryRepository.saveAll(conversationId, List.of(systemMessage)); + + assertThat(mongoTemplate.indexOps(Conversation.class).getIndexInfo().size()).isEqualTo(2); + } + +} diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/test/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryPropertiesTests.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/test/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryPropertiesTests.java new file mode 100644 index 00000000000..10ef5d88d96 --- /dev/null +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb/src/test/java/org/springframework/ai/model/chat/memory/mongo/autoconfigure/MongoChatMemoryPropertiesTests.java @@ -0,0 +1,44 @@ +/* + * 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.mongo.autoconfigure; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MongoChatMemoryPropertiesTests { + + @Test + void defaultValues_set() { + var properties = new MongoChatMemoryProperties(); + assertThat(properties.getTtl()).isEqualTo(Duration.ZERO); + assertThat(properties.isCreateIndices()).isFalse(); + } + + @Test + void overrideValues() { + var properties = new MongoChatMemoryProperties(); + properties.setTtl(Duration.ofMinutes(1)); + properties.setCreateIndices(true); + + assertThat(properties.getTtl()).isEqualTo(Duration.ofMinutes(1)); + assertThat(properties.isCreateIndices()).isTrue(); + } + +} diff --git a/memory/spring-ai-model-chat-memory-mongodb/README.md b/memory/spring-ai-model-chat-memory-mongodb/README.md new file mode 100644 index 00000000000..e779723c9cf --- /dev/null +++ b/memory/spring-ai-model-chat-memory-mongodb/README.md @@ -0,0 +1 @@ +[Chat Memory Documentation](https://docs.spring.io/spring-ai/reference/api/chat-memory.html#_chat_memory) diff --git a/memory/spring-ai-model-chat-memory-mongodb/pom.xml b/memory/spring-ai-model-chat-memory-mongodb/pom.xml new file mode 100644 index 00000000000..fe7872ea985 --- /dev/null +++ b/memory/spring-ai-model-chat-memory-mongodb/pom.xml @@ -0,0 +1,92 @@ + + + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.0.0-SNAPSHOT + ../../pom.xml + + + spring-ai-model-chat-memory-mongodb + Spring AI MongoDB Chat Memory + Spring AI MongoDB Chat Memory implementation + + + + org.springframework.ai + spring-ai-client-chat + ${project.version} + + + + + org.springframework.data + spring-data-mongodb + + + + org.mongodb + mongodb-driver-sync + + + + org.jspecify + jspecify + 1.0.0 + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.ai + spring-ai-test + ${project.version} + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.testcontainers + mongodb + test + + + + org.testcontainers + junit-jupiter + test + + + + + diff --git a/memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/Conversation.java b/memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/Conversation.java new file mode 100644 index 00000000000..5605217224f --- /dev/null +++ b/memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/Conversation.java @@ -0,0 +1,30 @@ +/* + * 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. + */ + +package org.springframework.ai.chat.memory.mongo; + +import org.jspecify.annotations.NullMarked; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.Instant; + +@Document("ai_chat_memory") +@NullMarked +public record Conversation(String conversationId, Message message, Instant timestamp) { + @NullMarked + public record Message(String content, String type) { + } +} diff --git a/memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/MongoChatMemoryRepository.java b/memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/MongoChatMemoryRepository.java new file mode 100644 index 00000000000..2200717d010 --- /dev/null +++ b/memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/MongoChatMemoryRepository.java @@ -0,0 +1,114 @@ +/* + * 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. + */ + +package org.springframework.ai.chat.memory.mongo; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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 org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoTemplate; + +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; + +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; + +/** + * An implementation of {@link ChatMemoryRepository} for MongoDB. + * + * @author Lukasz Jernas + * @since 1.0.0 + */ +public class MongoChatMemoryRepository implements ChatMemoryRepository { + + private static final Logger logger = LoggerFactory.getLogger(MongoChatMemoryRepository.class); + + private final MongoTemplate mongoTemplate; + + public MongoChatMemoryRepository(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + } + + @Override + public List findConversationIds() { + return mongoTemplate.query(Conversation.class).distinct("conversationId").as(String.class).all(); + } + + @Override + public List findByConversationId(String conversationId) { + var messages = mongoTemplate.query(Conversation.class) + .matching(query(where("conversationId").is(conversationId)).with(Sort.by("timestamp").descending())); + return messages.stream().map(MongoChatMemoryRepository::mapMessage).collect(Collectors.toList()); + } + + @Override + public void saveAll(String conversationId, List messages) { + var conversations = messages.stream() + .map(message -> new Conversation(conversationId, + new Conversation.Message(message.getText(), message.getMessageType().name()), Instant.now())) + .toList(); + mongoTemplate.insert(conversations, Conversation.class); + + } + + @Override + public void deleteByConversationId(String conversationId) { + mongoTemplate.remove(query(where("conversationId").is(conversationId)), Conversation.class); + } + + public static @Nullable Message mapMessage(Conversation conversation) { + return switch (conversation.message().type()) { + case "USER" -> new UserMessage(conversation.message().content()); + case "ASSISTANT" -> new AssistantMessage(conversation.message().content()); + case "SYSTEM" -> new SystemMessage(conversation.message().content()); + default -> { + logger.warn("Unsupported message type: {}", conversation.message().type()); + yield null; + } + }; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private MongoTemplate mongoTemplate; + + private Builder() { + } + + public Builder mongoTemplate(MongoTemplate mongoTemplate) { + this.mongoTemplate = mongoTemplate; + return this; + } + + public MongoChatMemoryRepository build() { + return new MongoChatMemoryRepository(this.mongoTemplate); + } + + } + +} diff --git a/memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/package-info.java b/memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/package-info.java new file mode 100644 index 00000000000..a68d01459dd --- /dev/null +++ b/memory/spring-ai-model-chat-memory-mongodb/src/main/java/org/springframework/ai/chat/memory/mongo/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.mongo; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/memory/spring-ai-model-chat-memory-mongodb/src/test/java/org/springframework/ai/chat/memory/mongo/MongoChatMemoryRepositoryIT.java b/memory/spring-ai-model-chat-memory-mongodb/src/test/java/org/springframework/ai/chat/memory/mongo/MongoChatMemoryRepositoryIT.java new file mode 100644 index 00000000000..74752fe31ef --- /dev/null +++ b/memory/spring-ai-model-chat-memory-mongodb/src/test/java/org/springframework/ai/chat/memory/mongo/MongoChatMemoryRepositoryIT.java @@ -0,0 +1,158 @@ +/* + * 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. + */ + +package org.springframework.ai.chat.memory.mongo; + +import org.junit.jupiter.api.Test; +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.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; + +/** + * Integration tests for {@link MongoChatMemoryRepository}. + * + * @author Łukasz Jernaś + */ +@SpringBootTest(classes = MongoChatMemoryRepositoryIT.TestConfiguration.class) +@TestPropertySource(properties = "spring.data.mongodb.uri=spring.data.mongodb.uri=%s/ai_test") +public class MongoChatMemoryRepositoryIT { + + @Autowired + private ChatMemoryRepository chatMemoryRepository; + + @Autowired + private MongoTemplate mongoTemplate; + + @Container + @ServiceConnection + static MongoDBContainer mongoDbContainer = new MongoDBContainer("mongo:8.0.6"); + + @Test + void correctChatMemoryRepositoryInstance() { + assertThat(chatMemoryRepository).isInstanceOf(ChatMemoryRepository.class); + } + + @ParameterizedTest + @CsvSource({ "Message from assistant,ASSISTANT", "Message from user,USER", "Message from system,SYSTEM" }) + void saveMessagesSingleMessage(String content, MessageType messageType) { + var conversationId = UUID.randomUUID().toString(); + var 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("Type not supported: " + messageType); + }; + + chatMemoryRepository.saveAll(conversationId, List.of(message)); + + var result = mongoTemplate.query(Conversation.class) + .matching(query(where("conversationId").is(conversationId))) + .first(); + + assertThat(result.isPresent()).isTrue(); + + assertThat(result.stream().count()).isEqualTo(1); + assertThat(result.get().conversationId()).isEqualTo(conversationId); + assertThat(result.get().message().content()).isEqualTo(message.getText()); + assertThat(result.get().message().type()).isEqualTo(messageType.toString()); + assertThat(result.get().timestamp()).isNotNull(); + } + + @Test + void saveMultipleMessages() { + var conversationId = UUID.randomUUID().toString(); + var messages = List.of(new AssistantMessage("Message from assistant - " + conversationId), + new UserMessage("Message from user - " + conversationId), + new SystemMessage("Message from system - " + conversationId)); + + chatMemoryRepository.saveAll(conversationId, messages); + + var result = mongoTemplate.query(Conversation.class) + .matching(query(where("conversationId").is(conversationId))) + .all(); + + assertThat(result.size()).isEqualTo(messages.size()); + + } + + @Test + void findByConversationId() { + var conversationId = UUID.randomUUID().toString(); + var messages = List.of(new AssistantMessage("Message from assistant - " + conversationId), + new UserMessage("Message from user - " + conversationId), + new SystemMessage("Message from system - " + conversationId)); + + chatMemoryRepository.saveAll(conversationId, messages); + + var results = chatMemoryRepository.findByConversationId(conversationId); + assertThat(results.size()).isEqualTo(messages.size()); + assertThat(results).isEqualTo(messages); + } + + @Test + void deleteMessagesByConversationId() { + var conversationId = UUID.randomUUID().toString(); + var messages = List.of(new AssistantMessage("Message from assistant - " + conversationId), + new UserMessage("Message from user - " + conversationId), + new SystemMessage("Message from system - " + conversationId)); + + chatMemoryRepository.saveAll(conversationId, messages); + + chatMemoryRepository.deleteByConversationId(conversationId); + + var results = mongoTemplate.query(Conversation.class) + .matching(query(where("conversationId").is(conversationId))) + .all(); + + assertThat(results.size()).isZero(); + } + + @SpringBootConfiguration + @ImportAutoConfiguration({ MongoAutoConfiguration.class, MongoDataAutoConfiguration.class }) + static class TestConfiguration { + + @Bean + ChatMemoryRepository chatMemoryRepository(MongoTemplate mongoTemplate) { + return MongoChatMemoryRepository.builder().mongoTemplate(mongoTemplate).build(); + } + + } + +} diff --git a/pom.xml b/pom.xml index 17113805d5d..d5f47eb26ad 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ memory/spring-ai-model-chat-memory-cassandra memory/spring-ai-model-chat-memory-jdbc memory/spring-ai-model-chat-memory-neo4j + memory/spring-ai-model-chat-memory-mongodb auto-configurations/common/spring-ai-autoconfigure-retry @@ -55,6 +56,7 @@ auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-cassandra auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-neo4j + auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-mongodb auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation @@ -181,6 +183,7 @@ spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock-converse spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-cassandra spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-jdbc + spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-mongodb spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-neo4j spring-ai-spring-boot-starters/spring-ai-starter-model-huggingface spring-ai-spring-boot-starters/spring-ai-starter-model-minimax diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml index 104c9adda8f..97171821247 100644 --- a/spring-ai-bom/pom.xml +++ b/spring-ai-bom/pom.xml @@ -211,6 +211,12 @@ ${project.version} + + org.springframework.ai + spring-ai-model-chat-memory-mongodb + ${project.version} + + org.springframework.ai spring-ai-model-chat-memory-neo4j @@ -487,6 +493,12 @@ ${project.version} + + org.springframework.ai + spring-ai-autoconfigure-model-chat-memory-mongodb + ${project.version} + + org.springframework.ai spring-ai-autoconfigure-model-chat-memory-neo4j @@ -1037,6 +1049,12 @@ ${project.version} + + org.springframework.ai + spring-ai-starter-model-chat-memory-mongodb + ${project.version} + + org.springframework.ai spring-ai-starter-model-chat-memory-neo4j diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-mongodb/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-mongodb/pom.xml new file mode 100644 index 00000000000..fcc84e63bd3 --- /dev/null +++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-mongodb/pom.xml @@ -0,0 +1,58 @@ + + + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.0.0-SNAPSHOT + ../../pom.xml + + spring-ai-starter-model-chat-memory-mongodb + jar + Spring AI Starter - MongoDB Chat Memory + Spring AI MongoDB Chat Memory 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-mongodb + ${project.parent.version} + + + + org.springframework.ai + spring-ai-model-chat-memory-mongodb + ${project.parent.version} + + + +