messages) {
+ Assert.hasText(conversationId, "conversationId cannot be null or empty");
+ Assert.notNull(messages, "messages cannot be null");
+ Assert.noNullElements(messages, "messages cannot contain null elements");
+ this.chatMemoryStore.put(conversationId, messages);
+ }
+
+ @Override
+ public void deleteByConversationId(String conversationId) {
+ Assert.hasText(conversationId, "conversationId cannot be null or empty");
+ this.chatMemoryStore.remove(conversationId);
+ }
+
+}
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/chat/memory/MessageWindowChatMemory.java b/spring-ai-model/src/main/java/org/springframework/ai/chat/memory/MessageWindowChatMemory.java
new file mode 100644
index 00000000000..8b289d1b276
--- /dev/null
+++ b/spring-ai-model/src/main/java/org/springframework/ai/chat/memory/MessageWindowChatMemory.java
@@ -0,0 +1,150 @@
+/*
+ * 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;
+
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.util.Assert;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A chat memory implementation that maintains a message window of a specified size,
+ * ensuring that the total number of messages does not exceed the specified limit. When
+ * the number of messages exceeds the maximum size, older messages are evicted.
+ *
+ * Messages of type {@link SystemMessage} are treated specially: if a new
+ * {@link SystemMessage} is added, all previous {@link SystemMessage} instances are
+ * removed from the memory. Also, if the total number of messages exceeds the limit, the
+ * {@link SystemMessage} messages are preserved while evicting other types of messages.
+ *
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+public final class MessageWindowChatMemory implements ChatMemory {
+
+ private static final int DEFAULT_MAX_MESSAGES = 200;
+
+ private static final ChatMemoryRepository DEFAULT_CHAT_MEMORY_REPOSITORY = new InMemoryChatMemoryRepository();
+
+ private final ChatMemoryRepository chatMemoryRepository;
+
+ private final int maxMessages;
+
+ private MessageWindowChatMemory(ChatMemoryRepository chatMemoryRepository, int maxMessages) {
+ Assert.notNull(chatMemoryRepository, "chatMemoryRepository cannot be null");
+ Assert.isTrue(maxMessages > 0, "maxMessages must be greater than 0");
+ this.chatMemoryRepository = chatMemoryRepository;
+ this.maxMessages = maxMessages;
+ }
+
+ @Override
+ public void add(String conversationId, List messages) {
+ Assert.hasText(conversationId, "conversationId cannot be null or empty");
+ Assert.notNull(messages, "messages cannot be null");
+ Assert.noNullElements(messages, "messages cannot contain null elements");
+
+ List memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId);
+ List processedMessages = process(memoryMessages, messages);
+ this.chatMemoryRepository.saveAll(conversationId, processedMessages);
+ }
+
+ @Override
+ public List get(String conversationId) {
+ Assert.hasText(conversationId, "conversationId cannot be null or empty");
+ return this.chatMemoryRepository.findByConversationId(conversationId);
+ }
+
+ @Override
+ @Deprecated // in favor of get(conversationId)
+ public List get(String conversationId, int lastN) {
+ return get(conversationId);
+ }
+
+ @Override
+ public void clear(String conversationId) {
+ Assert.hasText(conversationId, "conversationId cannot be null or empty");
+ this.chatMemoryRepository.deleteByConversationId(conversationId);
+ }
+
+ private List process(List memoryMessages, List newMessages) {
+ List processedMessages = new ArrayList<>();
+
+ Set memoryMessagesSet = new HashSet<>(memoryMessages);
+ boolean hasNewSystemMessage = newMessages.stream()
+ .filter(SystemMessage.class::isInstance)
+ .anyMatch(message -> !memoryMessagesSet.contains(message));
+
+ memoryMessages.stream()
+ .filter(message -> !(hasNewSystemMessage && message instanceof SystemMessage))
+ .forEach(processedMessages::add);
+
+ processedMessages.addAll(newMessages);
+
+ if (processedMessages.size() <= this.maxMessages) {
+ return processedMessages;
+ }
+
+ int messagesToRemove = processedMessages.size() - this.maxMessages;
+
+ List trimmedMessages = new ArrayList<>();
+ int removed = 0;
+ for (Message message : processedMessages) {
+ if (message instanceof SystemMessage || removed >= messagesToRemove) {
+ trimmedMessages.add(message);
+ }
+ else {
+ removed++;
+ }
+ }
+
+ return trimmedMessages;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private ChatMemoryRepository chatMemoryRepository = DEFAULT_CHAT_MEMORY_REPOSITORY;
+
+ private int maxMessages = DEFAULT_MAX_MESSAGES;
+
+ private Builder() {
+ }
+
+ public Builder chatMemoryRepository(ChatMemoryRepository chatMemoryRepository) {
+ this.chatMemoryRepository = chatMemoryRepository;
+ return this;
+ }
+
+ public Builder maxMessages(int maxMessages) {
+ this.maxMessages = maxMessages;
+ return this;
+ }
+
+ public MessageWindowChatMemory build() {
+ return new MessageWindowChatMemory(chatMemoryRepository, maxMessages);
+ }
+
+ }
+
+}
diff --git a/spring-ai-model/src/main/java/org/springframework/ai/chat/memory/package-info.java b/spring-ai-model/src/main/java/org/springframework/ai/chat/memory/package-info.java
new file mode 100644
index 00000000000..2dd55ff2556
--- /dev/null
+++ b/spring-ai-model/src/main/java/org/springframework/ai/chat/memory/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;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/spring-ai-model/src/test/java/org/springframework/ai/chat/memory/InMemoryChatMemoryRepositoryTests.java b/spring-ai-model/src/test/java/org/springframework/ai/chat/memory/InMemoryChatMemoryRepositoryTests.java
new file mode 100644
index 00000000000..6fc4315c19e
--- /dev/null
+++ b/spring-ai-model/src/test/java/org/springframework/ai/chat/memory/InMemoryChatMemoryRepositoryTests.java
@@ -0,0 +1,154 @@
+/*
+ * 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;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.UserMessage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Unit tests for {@link InMemoryChatMemoryRepository}.
+ *
+ * @author Thomas Vitale
+ */
+public class InMemoryChatMemoryRepositoryTests {
+
+ private final InMemoryChatMemoryRepository chatMemoryRepository = new InMemoryChatMemoryRepository();
+
+ @Test
+ void findConversationIds() {
+ String conversationId1 = UUID.randomUUID().toString();
+ String conversationId2 = UUID.randomUUID().toString();
+ List messages1 = List.of(new UserMessage("Hello"));
+ List messages2 = List.of(new AssistantMessage("Hi there"));
+
+ chatMemoryRepository.saveAll(conversationId1, messages1);
+ chatMemoryRepository.saveAll(conversationId2, messages2);
+
+ assertThat(chatMemoryRepository.findConversationIds()).containsExactlyInAnyOrder(conversationId1,
+ conversationId2);
+
+ chatMemoryRepository.deleteByConversationId(conversationId1);
+ assertThat(chatMemoryRepository.findConversationIds()).containsExactlyInAnyOrder(conversationId2);
+ }
+
+ @Test
+ void saveMessagesAndFindMultipleMessagesInConversation() {
+ String conversationId = UUID.randomUUID().toString();
+ List messages = List.of(new AssistantMessage("I, Robot"), new UserMessage("Hello"));
+
+ chatMemoryRepository.saveAll(conversationId, messages);
+
+ assertThat(chatMemoryRepository.findByConversationId(conversationId)).containsAll(messages);
+
+ chatMemoryRepository.deleteByConversationId(conversationId);
+
+ assertThat(chatMemoryRepository.findByConversationId(conversationId)).isEmpty();
+ }
+
+ @Test
+ void saveMessagesAndFindSingleMessageInConversation() {
+ String conversationId = UUID.randomUUID().toString();
+ Message message = new UserMessage("Hello");
+ List messages = List.of(message);
+
+ chatMemoryRepository.saveAll(conversationId, messages);
+
+ assertThat(chatMemoryRepository.findByConversationId(conversationId)).contains(message);
+
+ chatMemoryRepository.deleteByConversationId(conversationId);
+
+ assertThat(chatMemoryRepository.findByConversationId(conversationId)).isEmpty();
+ }
+
+ @Test
+ void findNonExistingConversation() {
+ String conversationId = UUID.randomUUID().toString();
+
+ assertThat(chatMemoryRepository.findByConversationId(conversationId)).isEmpty();
+ }
+
+ @Test
+ void subsequentSaveOverwritesPreviousVersion() {
+ String conversationId = UUID.randomUUID().toString();
+ List firstMessages = List.of(new UserMessage("Hello"));
+ List secondMessages = List.of(new AssistantMessage("Hi there"));
+
+ chatMemoryRepository.saveAll(conversationId, firstMessages);
+ chatMemoryRepository.saveAll(conversationId, secondMessages);
+
+ assertThat(chatMemoryRepository.findByConversationId(conversationId)).containsExactlyElementsOf(secondMessages);
+ }
+
+ @Test
+ void nullConversationIdNotAllowed() {
+ assertThatThrownBy(() -> chatMemoryRepository.saveAll(null, List.of(new UserMessage("Hello"))))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemoryRepository.findByConversationId(null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemoryRepository.deleteByConversationId(null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+ }
+
+ @Test
+ void emptyConversationIdNotAllowed() {
+ assertThatThrownBy(() -> chatMemoryRepository.saveAll("", List.of(new UserMessage("Hello"))))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemoryRepository.findByConversationId(""))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemoryRepository.deleteByConversationId(""))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+ }
+
+ @Test
+ void nullMessagesNotAllowed() {
+ String conversationId = UUID.randomUUID().toString();
+ assertThatThrownBy(() -> chatMemoryRepository.saveAll(conversationId, null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("messages cannot be null");
+ }
+
+ @Test
+ void messagesWithNullElementsNotAllowed() {
+ String conversationId = UUID.randomUUID().toString();
+ List messagesWithNull = new ArrayList<>();
+ messagesWithNull.add(null);
+
+ assertThatThrownBy(() -> chatMemoryRepository.saveAll(conversationId, messagesWithNull))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("messages cannot contain null elements");
+ }
+
+}
diff --git a/spring-ai-model/src/test/java/org/springframework/ai/chat/memory/MessageWindowChatMemoryTests.java b/spring-ai-model/src/test/java/org/springframework/ai/chat/memory/MessageWindowChatMemoryTests.java
new file mode 100644
index 00000000000..6684e48d3d5
--- /dev/null
+++ b/spring-ai-model/src/test/java/org/springframework/ai/chat/memory/MessageWindowChatMemoryTests.java
@@ -0,0 +1,294 @@
+/*
+ * 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;
+
+import org.junit.jupiter.api.Test;
+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.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Unit tests for {@link MessageWindowChatMemory}.
+ *
+ * @author Thomas Vitale
+ */
+public class MessageWindowChatMemoryTests {
+
+ private final MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder().build();
+
+ @Test
+ void zeroMaxMessagesNotAllowed() {
+ assertThatThrownBy(() -> MessageWindowChatMemory.builder().maxMessages(0).build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("maxMessages must be greater than 0");
+ }
+
+ @Test
+ void negativeMaxMessagesNotAllowed() {
+ assertThatThrownBy(() -> MessageWindowChatMemory.builder().maxMessages(-1).build())
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("maxMessages must be greater than 0");
+ }
+
+ @Test
+ void handleMultipleMessagesInConversation() {
+ String conversationId = UUID.randomUUID().toString();
+ List messages = List.of(new AssistantMessage("I, Robot"), new UserMessage("Hello"));
+
+ chatMemory.add(conversationId, messages);
+
+ assertThat(chatMemory.get(conversationId)).containsAll(messages);
+
+ chatMemory.clear(conversationId);
+
+ assertThat(chatMemory.get(conversationId)).isEmpty();
+ }
+
+ @Test
+ void handleSingleMessageInConversation() {
+ String conversationId = UUID.randomUUID().toString();
+ Message message = new UserMessage("Hello");
+
+ chatMemory.add(conversationId, message);
+
+ assertThat(chatMemory.get(conversationId)).contains(message);
+
+ chatMemory.clear(conversationId);
+
+ assertThat(chatMemory.get(conversationId)).isEmpty();
+ }
+
+ @Test
+ void nullConversationIdNotAllowed() {
+ assertThatThrownBy(() -> chatMemory.add(null, List.of(new UserMessage("Hello"))))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemory.add(null, new UserMessage("Hello")))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemory.get(null)).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemory.clear(null)).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+ }
+
+ @Test
+ void emptyConversationIdNotAllowed() {
+ assertThatThrownBy(() -> chatMemory.add("", List.of(new UserMessage("Hello"))))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemory.add(null, new UserMessage("Hello")))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemory.get("")).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+
+ assertThatThrownBy(() -> chatMemory.clear("")).isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("conversationId cannot be null or empty");
+ }
+
+ @Test
+ void nullMessagesNotAllowed() {
+ String conversationId = UUID.randomUUID().toString();
+ assertThatThrownBy(() -> chatMemory.add(conversationId, (List) null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("messages cannot be null");
+ }
+
+ @Test
+ void nullMessageNotAllowed() {
+ String conversationId = UUID.randomUUID().toString();
+ assertThatThrownBy(() -> chatMemory.add(conversationId, (Message) null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("message cannot be null");
+ }
+
+ @Test
+ void messagesWithNullElementsNotAllowed() {
+ String conversationId = UUID.randomUUID().toString();
+ List messagesWithNull = new ArrayList<>();
+ messagesWithNull.add(null);
+
+ assertThatThrownBy(() -> chatMemory.add(conversationId, messagesWithNull))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("messages cannot contain null elements");
+ }
+
+ @Test
+ void customMaxMessages() {
+ String conversationId = UUID.randomUUID().toString();
+ int customMaxMessages = 2;
+
+ MessageWindowChatMemory customChatMemory = MessageWindowChatMemory.builder()
+ .maxMessages(customMaxMessages)
+ .build();
+
+ List messages = List.of(new UserMessage("Message 1"), new AssistantMessage("Response 1"),
+ new UserMessage("Message 2"), new AssistantMessage("Response 2"), new UserMessage("Message 3"));
+
+ customChatMemory.add(conversationId, messages);
+ List result = customChatMemory.get(conversationId);
+
+ assertThat(result).hasSize(2);
+ }
+
+ @Test
+ void noEvictionWhenMessagesWithinLimit() {
+ int limit = 3;
+ MessageWindowChatMemory customChatMemory = MessageWindowChatMemory.builder().maxMessages(limit).build();
+
+ String conversationId = UUID.randomUUID().toString();
+ List memoryMessages = new ArrayList<>(
+ List.of(new UserMessage("Hello"), new AssistantMessage("Hi there")));
+ customChatMemory.add(conversationId, memoryMessages);
+
+ List newMessages = new ArrayList<>(List.of(new UserMessage("How are you?")));
+ customChatMemory.add(conversationId, newMessages);
+
+ List result = customChatMemory.get(conversationId);
+
+ assertThat(result).hasSize(limit);
+ assertThat(result).containsExactly(new UserMessage("Hello"), new AssistantMessage("Hi there"),
+ new UserMessage("How are you?"));
+ }
+
+ @Test
+ void evictionWhenMessagesExceedLimit() {
+ int limit = 2;
+ MessageWindowChatMemory customChatMemory = MessageWindowChatMemory.builder().maxMessages(limit).build();
+
+ String conversationId = UUID.randomUUID().toString();
+ List memoryMessages = new ArrayList<>(
+ List.of(new UserMessage("Message 1"), new AssistantMessage("Response 1")));
+ customChatMemory.add(conversationId, memoryMessages);
+
+ List newMessages = new ArrayList<>(
+ List.of(new UserMessage("Message 2"), new AssistantMessage("Response 2")));
+ customChatMemory.add(conversationId, newMessages);
+
+ List result = customChatMemory.get(conversationId);
+
+ assertThat(result).hasSize(limit);
+ assertThat(result).containsExactly(new UserMessage("Message 2"), new AssistantMessage("Response 2"));
+ }
+
+ @Test
+ void systemMessageIsPreservedDuringEviction() {
+ int limit = 3;
+ MessageWindowChatMemory customChatMemory = MessageWindowChatMemory.builder().maxMessages(limit).build();
+
+ String conversationId = UUID.randomUUID().toString();
+ List memoryMessages = new ArrayList<>(List.of(new SystemMessage("System instruction"),
+ new UserMessage("Message 1"), new AssistantMessage("Response 1")));
+ customChatMemory.add(conversationId, memoryMessages);
+
+ List newMessages = new ArrayList<>(
+ List.of(new UserMessage("Message 2"), new AssistantMessage("Response 2")));
+ customChatMemory.add(conversationId, newMessages);
+
+ List result = customChatMemory.get(conversationId);
+
+ assertThat(result).hasSize(limit);
+ assertThat(result).containsExactly(new SystemMessage("System instruction"), new UserMessage("Message 2"),
+ new AssistantMessage("Response 2"));
+ }
+
+ @Test
+ void multipleSystemMessagesArePreservedDuringEviction() {
+ int limit = 3;
+ MessageWindowChatMemory customChatMemory = MessageWindowChatMemory.builder().maxMessages(limit).build();
+
+ String conversationId = UUID.randomUUID().toString();
+ List memoryMessages = new ArrayList<>(
+ List.of(new SystemMessage("System instruction 1"), new SystemMessage("System instruction 2"),
+ new UserMessage("Message 1"), new AssistantMessage("Response 1")));
+ customChatMemory.add(conversationId, memoryMessages);
+
+ List newMessages = new ArrayList<>(
+ List.of(new UserMessage("Message 2"), new AssistantMessage("Response 2")));
+ customChatMemory.add(conversationId, newMessages);
+
+ List result = customChatMemory.get(conversationId);
+
+ assertThat(result).hasSize(limit);
+ assertThat(result).containsExactly(new SystemMessage("System instruction 1"),
+ new SystemMessage("System instruction 2"), new AssistantMessage("Response 2"));
+ }
+
+ @Test
+ void emptyMessageList() {
+ String conversationId = UUID.randomUUID().toString();
+
+ List result = this.chatMemory.get(conversationId);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ void oldSystemMessagesAreRemovedWhenNewOneAdded() {
+ int limit = 2;
+ MessageWindowChatMemory customChatMemory = MessageWindowChatMemory.builder().maxMessages(limit).build();
+
+ String conversationId = UUID.randomUUID().toString();
+ List memoryMessages = new ArrayList<>(
+ List.of(new SystemMessage("System instruction 1"), new SystemMessage("System instruction 2")));
+ customChatMemory.add(conversationId, memoryMessages);
+
+ List newMessages = new ArrayList<>(List.of(new SystemMessage("System instruction 3")));
+ customChatMemory.add(conversationId, newMessages);
+
+ List result = customChatMemory.get(conversationId);
+
+ assertThat(result).hasSize(1);
+ assertThat(result).containsExactly(new SystemMessage("System instruction 3"));
+ }
+
+ @Test
+ void mixedMessagesWithLimitEqualToSystemMessageCount() {
+ int limit = 2;
+ MessageWindowChatMemory customChatMemory = MessageWindowChatMemory.builder().maxMessages(limit).build();
+
+ String conversationId = UUID.randomUUID().toString();
+ List memoryMessages = new ArrayList<>(
+ List.of(new SystemMessage("System instruction 1"), new SystemMessage("System instruction 2")));
+ customChatMemory.add(conversationId, memoryMessages);
+
+ List newMessages = new ArrayList<>(
+ List.of(new UserMessage("Message 1"), new AssistantMessage("Response 1")));
+ customChatMemory.add(conversationId, newMessages);
+
+ List result = customChatMemory.get(conversationId);
+
+ assertThat(result).hasSize(2);
+ assertThat(result).containsExactly(new SystemMessage("System instruction 1"),
+ new SystemMessage("System instruction 2"));
+ }
+
+}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-model-anthropic/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-anthropic/pom.xml
index 6a80aa6cd39..0129169caf6 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-model-anthropic/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-anthropic/pom.xml
@@ -59,6 +59,12 @@
spring-ai-autoconfigure-model-chat-client
${project.parent.version}