diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/pom.xml b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/pom.xml new file mode 100644 index 00000000000..040f2931e9c --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.1.0-SNAPSHOT + ../../../../../../pom.xml + + spring-ai-autoconfigure-model-chat-memory-repository-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-repository-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/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryAutoConfiguration.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryAutoConfiguration.java new file mode 100644 index 00000000000..9a378d32da9 --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/model/chat/memory/repository/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.repository.mongo.autoconfigure; + +import org.springframework.ai.chat.memory.repository.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.1.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/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryIndexCreator.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryIndexCreator.java new file mode 100644 index 00000000000..58c29b115ab --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryIndexCreator.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.repository.mongo.autoconfigure; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.ai.chat.memory.repository.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.1.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 + this.mongoTemplate.indexOps(Conversation.class) + .createIndex(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 + this.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"); + this.mongoTemplate.indexOps(Conversation.class).dropIndex(idx.getName()); + } + }); + this.mongoTemplate.indexOps(Conversation.class) + .createIndex(new Index().on("timestamp", Sort.Direction.ASC) + .expire(this.mongoChatMemoryProperties.getTtl())); + } + } + +} diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryProperties.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryProperties.java new file mode 100644 index 00000000000..a42d9bcb0d0 --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryProperties.java @@ -0,0 +1,62 @@ +/* + * 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.mongo.autoconfigure; + +import java.time.Duration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties for configuring the MongoDB ChatMemory repository. + * + * @author Łukasz Jernaś + * @since 1.1.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 this.ttl; + } + + public void setTtl(Duration ttl) { + this.ttl = ttl; + } + + public boolean isCreateIndices() { + return this.createIndices; + } + + public void setCreateIndices(boolean createIndices) { + this.createIndices = createIndices; + } + +} diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/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-mongodb/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..c6e1168dfd0 --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-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.repository.mongo.autoconfigure.MongoChatMemoryAutoConfiguration diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/test/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryAutoConfigurationIT.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/test/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryAutoConfigurationIT.java new file mode 100644 index 00000000000..c915637bdbd --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/test/java/org/springframework/ai/model/chat/memory/repository/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.repository.mongo.autoconfigure; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; + +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.memory.repository.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 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"); + + this.chatMemoryRepository.saveAll(conversationId, List.of(systemMessage)); + + assertThat(this.chatMemoryRepository.findConversationIds().contains(conversationId)).isTrue(); + + assertThat(this.chatMemoryRepository.findByConversationId(conversationId).size()).isEqualTo(1); + + this.chatMemoryRepository.deleteByConversationId(conversationId); + + assertThat(this.chatMemoryRepository.findByConversationId(conversationId).size()).isEqualTo(0); + + } + + @Test + void indicesShouldBeCreated() { + var conversationId = UUID.randomUUID().toString(); + var systemMessage = new SystemMessage("Some system message"); + + this.chatMemoryRepository.saveAll(conversationId, List.of(systemMessage)); + + assertThat(this.mongoTemplate.indexOps(Conversation.class).getIndexInfo().size()).isEqualTo(2); + } + +} diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/test/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryPropertiesTests.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/test/java/org/springframework/ai/model/chat/memory/repository/mongo/autoconfigure/MongoChatMemoryPropertiesTests.java new file mode 100644 index 00000000000..a1f92b2b995 --- /dev/null +++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-mongodb/src/test/java/org/springframework/ai/model/chat/memory/repository/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.repository.mongo.autoconfigure; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +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/repository/spring-ai-model-chat-memory-repository-mongodb/README.md b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/README.md new file mode 100644 index 00000000000..e779723c9cf --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-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/repository/spring-ai-model-chat-memory-repository-mongodb/pom.xml b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/pom.xml new file mode 100644 index 00000000000..2426efa8bb1 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/pom.xml @@ -0,0 +1,92 @@ + + + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.1.0-SNAPSHOT + ../../../pom.xml + + + spring-ai-model-chat-memory-repository-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/repository/spring-ai-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/chat/memory/repository/mongo/Conversation.java b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/chat/memory/repository/mongo/Conversation.java new file mode 100644 index 00000000000..04c3525ce1f --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/chat/memory/repository/mongo/Conversation.java @@ -0,0 +1,38 @@ +/* + * 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.repository.mongo; + +import java.time.Instant; +import java.util.Map; + +import org.jspecify.annotations.NullMarked; + +import org.springframework.data.mongodb.core.mapping.Document; + +/** + * A record representing a conversation in MongoDB. + * + * @author Lukasz Jernas + * @since 1.1.0 + */ +@Document("ai_chat_memory") +@NullMarked +public record Conversation(String conversationId, Message message, Instant timestamp) { + @NullMarked + public record Message(String content, String type, Map metadata) { + } +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/chat/memory/repository/mongo/MongoChatMemoryRepository.java b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/chat/memory/repository/mongo/MongoChatMemoryRepository.java new file mode 100644 index 00000000000..ed862714ca3 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/chat/memory/repository/mongo/MongoChatMemoryRepository.java @@ -0,0 +1,117 @@ +/* + * 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.repository.mongo; + +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; + +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 org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; + +/** + * An implementation of {@link ChatMemoryRepository} for MongoDB. + * + * @author Lukasz Jernas + * @since 1.1.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 this.mongoTemplate.query(Conversation.class).distinct("conversationId").as(String.class).all(); + } + + @Override + public List findByConversationId(String conversationId) { + var messages = this.mongoTemplate.query(Conversation.class) + .matching(Query.query(Criteria.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) { + deleteByConversationId(conversationId); + var conversations = messages.stream() + .map(message -> new Conversation(conversationId, + new Conversation.Message(message.getText(), message.getMessageType().name(), message.getMetadata()), + Instant.now())) + .toList(); + this.mongoTemplate.insert(conversations, Conversation.class); + + } + + @Override + public void deleteByConversationId(String conversationId) { + this.mongoTemplate.remove(Query.query(Criteria.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 final 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/repository/spring-ai-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/chat/memory/repository/mongo/package-info.java b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/chat/memory/repository/mongo/package-info.java new file mode 100644 index 00000000000..2459903190f --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/main/java/org/springframework/ai/chat/memory/repository/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.repository.mongo; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/test/java/org/springframework/ai/chat/memory/repository/mongo/MongoChatMemoryRepositoryIT.java b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/test/java/org/springframework/ai/chat/memory/repository/mongo/MongoChatMemoryRepositoryIT.java new file mode 100644 index 00000000000..9980593d1e7 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-mongodb/src/test/java/org/springframework/ai/chat/memory/repository/mongo/MongoChatMemoryRepositoryIT.java @@ -0,0 +1,159 @@ +/* + * 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.repository.mongo; + +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.testcontainers.containers.MongoDBContainer; +import org.testcontainers.junit.jupiter.Container; + +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.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 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(this.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); + }; + + this.chatMemoryRepository.saveAll(conversationId, List.of(message)); + + var result = this.mongoTemplate.query(Conversation.class) + .matching(Query.query(Criteria.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)); + + this.chatMemoryRepository.saveAll(conversationId, messages); + + var result = this.mongoTemplate.query(Conversation.class) + .matching(Query.query(Criteria.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)); + + this.chatMemoryRepository.saveAll(conversationId, messages); + + var results = this.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)); + + this.chatMemoryRepository.saveAll(conversationId, messages); + + this.chatMemoryRepository.deleteByConversationId(conversationId); + + var results = this.mongoTemplate.query(Conversation.class) + .matching(Query.query(Criteria.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 abb2bc543a3..d030c5ba8b6 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-cosmos-db memory/repository/spring-ai-model-chat-memory-repository-jdbc + memory/repository/spring-ai-model-chat-memory-repository-mongodb memory/repository/spring-ai-model-chat-memory-repository-neo4j @@ -91,6 +92,7 @@ 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-cosmos-db 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-mongodb 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 @@ -212,6 +214,7 @@ spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory 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-jdbc + spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-mongodb spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-neo4j spring-ai-spring-boot-starters/spring-ai-starter-mcp-client diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml index cd73aa67e48..687c74ece68 100644 --- a/spring-ai-bom/pom.xml +++ b/spring-ai-bom/pom.xml @@ -217,6 +217,12 @@ ${project.version} + + org.springframework.ai + spring-ai-model-chat-memory-repository-mongodb + ${project.version} + + org.springframework.ai spring-ai-model-chat-memory-repository-neo4j @@ -520,6 +526,12 @@ ${project.version} + + org.springframework.ai + spring-ai-autoconfigure-model-chat-memory-repository-mongodb + ${project.version} + + org.springframework.ai spring-ai-autoconfigure-model-chat-memory-repository-neo4j @@ -1139,6 +1151,12 @@ ${project.version} + + org.springframework.ai + spring-ai-starter-model-chat-memory-repository-mongodb + ${project.version} + + org.springframework.ai spring-ai-starter-model-chat-memory-repository-neo4j diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc index 4231a2dbcb6..788688df7db 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat-memory.adoc @@ -407,6 +407,74 @@ The auto-configuration will automatically create the specified database and cont You can customize the database and container names using the configuration properties mentioned above. +=== MongoChatMemoryRepository + +`MongoChatMemoryRepository` is a built-in implementation that uses MongoDB to store messages. It is suitable for applications that require a flexible, document-oriented database for chat memory persistence. + +First, add the following dependency to your project: + +[tabs] +====== +Maven:: ++ +[source, xml] +---- + + org.springframework.ai + spring-ai-starter-model-chat-memory-repository-mongodb + +---- + +Gradle:: ++ +[source,groovy] +---- +dependencies { + implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-repository-mongodb' +} +---- +====== + +Spring AI provides auto-configuration for the `MongoChatMemoryRepository`, which you can use directly in your application. + +[source,java] +---- +@Autowired +MongoChatMemoryRepository chatMemoryRepository; + +ChatMemory chatMemory = MessageWindowChatMemory.builder() + .chatMemoryRepository(chatMemoryRepository) + .maxMessages(10) + .build(); +---- + +If you'd rather create the `MongoChatMemoryRepository` manually, you can do so by providing a `MongoTemplate` instance: + +[source,java] +---- +ChatMemoryRepository chatMemoryRepository = MongoChatMemoryRepository.builder() + .mongoTemplate(mongoTemplate) + .build(); + +ChatMemory chatMemory = MessageWindowChatMemory.builder() + .chatMemoryRepository(chatMemoryRepository) + .maxMessages(10) + .build(); +---- + +==== Configuration Properties + +[cols="2,5,1",stripes=even] +|=== +|Property | Description | Default Value +| `spring.ai.chat.memory.repository.mongo.create-indices` | Should indices be created or recreated automatically on startup. Note: Changing the +* TTL value will drop the TTL index and recreate it | `false` +| `spring.ai.chat.memory.repository.mongo.ttl` | Time to live (TTL) for messages written in MongoDB, in seconds. If not set, messages will be stored indefinitely. | `0` +|=== + +==== Collection Initialization +The auto-configuration will automatically create the `ai_chat_memory` collection on startup if it does not already exist. + == Memory in Chat Client When using the ChatClient API, you can provide a `ChatMemory` implementation to maintain conversation context across multiple interactions. diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-mongodb/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-mongodb/pom.xml new file mode 100644 index 00000000000..87164f4028a --- /dev/null +++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-repository-mongodb/pom.xml @@ -0,0 +1,58 @@ + + + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.1.0-SNAPSHOT + ../../pom.xml + + spring-ai-starter-model-chat-memory-repository-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-repository-mongodb + ${project.parent.version} + + + + org.springframework.ai + spring-ai-model-chat-memory-repository-mongodb + ${project.parent.version} + + + +