diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryAutoConfiguration.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryAutoConfiguration.java index d3dc6d36fee..a6942b07d87 100644 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryAutoConfiguration.java +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryAutoConfiguration.java @@ -21,24 +21,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.chat.memory.ChatMemory; -import org.springframework.ai.chat.memory.MessageWindowChatMemory; -import org.springframework.ai.chat.memory.jdbc.JdbcChatMemory; -import org.springframework.ai.chat.memory.jdbc.JdbcChatMemoryConfig; import org.springframework.ai.chat.memory.jdbc.JdbcChatMemoryRepository; import org.springframework.ai.model.chat.memory.autoconfigure.ChatMemoryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionMessage; -import org.springframework.boot.autoconfigure.condition.ConditionOutcome; -import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.context.annotation.Conditional; -import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.jdbc.core.JdbcTemplate; /** @@ -59,55 +50,13 @@ JdbcChatMemoryRepository chatMemoryRepository(JdbcTemplate jdbcTemplate) { return JdbcChatMemoryRepository.builder().jdbcTemplate(jdbcTemplate).build(); } - /** - * @deprecated in favor of building a {@link MessageWindowChatMemory} (or other - * {@link ChatMemory} implementations) with a {@link JdbcChatMemoryRepository} - * instance. - */ @Bean @ConditionalOnMissingBean - @Deprecated - JdbcChatMemory chatMemory(JdbcTemplate jdbcTemplate) { - var config = JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build(); - return JdbcChatMemory.create(config); - } - - @Bean - @ConditionalOnMissingBean - @Conditional(OnSchemaInitializationEnabledCondition.class) + @ConditionalOnProperty(prefix = JdbcChatMemoryProperties.CONFIG_PREFIX, name = "initialize-schema", + havingValue = "true", matchIfMissing = true) JdbcChatMemoryDataSourceScriptDatabaseInitializer jdbcChatMemoryScriptDatabaseInitializer(DataSource dataSource) { logger.debug("Initializing schema for JdbcChatMemoryRepository"); return new JdbcChatMemoryDataSourceScriptDatabaseInitializer(dataSource); } - /** - * Condition to check if the schema initialization is enabled, supporting both - * deprecated and new property. - * - * @deprecated to be removed in 1.0.0-RC1. - */ - @Deprecated - static class OnSchemaInitializationEnabledCondition extends SpringBootCondition { - - @Override - public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { - Boolean initializeSchemaEnabled = context.getEnvironment() - .getProperty("spring.ai.chat.memory.jdbc.initialize-schema", Boolean.class); - - if (initializeSchemaEnabled != null) { - return new ConditionOutcome(initializeSchemaEnabled, - ConditionMessage.forCondition("Enable JDBC Chat Memory Schema Initialization") - .because("spring.ai.chat.memory.jdbc.initialize-schema is " + initializeSchemaEnabled)); - } - - initializeSchemaEnabled = context.getEnvironment() - .getProperty(JdbcChatMemoryProperties.CONFIG_PREFIX + ".initialize-schema", Boolean.class, true); - - return new ConditionOutcome(initializeSchemaEnabled, ConditionMessage - .forCondition("Enable JDBC Chat Memory Schema Initialization") - .because(JdbcChatMemoryProperties.CONFIG_PREFIX + ".initialize-schema is " + initializeSchemaEnabled)); - } - - } - } diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json deleted file mode 100644 index eceef48bb83..00000000000 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "groups": [], - "properties": [ - { - "name": "spring.ai.chat.memory.jdbc.initialize-schema", - "type": "java.lang.Boolean", - "description": "Whether to initialize the schema on startup.", - "deprecation": { - "replacement": "spring.ai.chat.memory.repository.jdbc.initialize-schema" - } - } - ], - "hints": [] -} \ No newline at end of file diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java index 5656b680c38..5961f25ee6e 100644 --- a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPostgresqlAutoConfigurationIT.java @@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test; import org.springframework.ai.chat.memory.ChatMemory; -import org.springframework.ai.chat.memory.jdbc.JdbcChatMemory; import org.springframework.ai.chat.memory.jdbc.JdbcChatMemoryRepository; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; @@ -57,58 +56,16 @@ void jdbcChatMemoryScriptDatabaseInitializer_shouldBeLoaded() { @Test void jdbcChatMemoryScriptDatabaseInitializer_shouldNotBeLoaded() { - this.contextRunner.withPropertyValues("spring.ai.chat.memory.jdbc.initialize-schema=false") - .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isFalse()); this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=false") .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isFalse()); } @Test - void initializeSchemaEnabledWithDeprecatedProperty() { - this.contextRunner - .withPropertyValues("spring.ai.chat.memory.jdbc.initialize-schema=true", - "spring.ai.chat.memory.repository.jdbc.initialize-schema=false") - .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue()); - } - - @Test - void initializeSchemaEnabledWithNewProperty() { + void initializeSchemaEnabledWithProperty() { this.contextRunner.withPropertyValues("spring.ai.chat.memory.repository.jdbc.initialize-schema=true") .run(context -> assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue()); } - @Test - void addGetAndClear_shouldAllExecute() { - this.contextRunner.withPropertyValues("spring.ai.chat.memory.jdbc.initialize-schema=true").run(context -> { - var chatMemory = context.getBean(JdbcChatMemory.class); - var conversationId = UUID.randomUUID().toString(); - var userMessage = new UserMessage("Message from the user"); - - chatMemory.add(conversationId, userMessage); - - assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).hasSize(1); - assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).isEqualTo(List.of(userMessage)); - - var assistantMessage = new AssistantMessage("Message from the assistant"); - - chatMemory.add(conversationId, List.of(assistantMessage)); - - assertThat(chatMemory.get(conversationId)).hasSize(2); - assertThat(chatMemory.get(conversationId)).isEqualTo(List.of(userMessage, assistantMessage)); - chatMemory.clear(conversationId); - - assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).isEmpty(); - - var multipleMessages = List.of(new UserMessage("Message from the user 1"), - new AssistantMessage("Message from the assistant 1")); - - chatMemory.add(conversationId, multipleMessages); - - assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).hasSize(multipleMessages.size()); - assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).isEqualTo(multipleMessages); - }); - } - @Test void useAutoConfiguredJdbcChatMemoryRepository() { this.contextRunner.run(context -> { diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemory.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemory.java deleted file mode 100644 index 66384bd5799..00000000000 --- a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemory.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.jdbc; - -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; - -import org.springframework.ai.chat.memory.ChatMemory; -import org.springframework.ai.chat.memory.MessageWindowChatMemory; -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.jdbc.core.BatchPreparedStatementSetter; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; - -/** - * An implementation of {@link ChatMemory} for JDBC. Creating an instance of - * JdbcChatMemory example: - * JdbcChatMemory.create(JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build()); - * - * @author Jonathan Leijendekker - * @author Linar Abzaltdinov - * @since 1.0.0 - * @deprecated in favor of building a {@link MessageWindowChatMemory} (or other - * {@link ChatMemory} implementations) with a {@link JdbcChatMemoryRepository} instance. - */ -@Deprecated -public class JdbcChatMemory implements ChatMemory { - - private static final String QUERY_ADD = """ - INSERT INTO ai_chat_memory (conversation_id, content, type, "timestamp") VALUES (?, ?, ?, ?)"""; - - private static final String QUERY_GET = """ - SELECT content, type FROM ai_chat_memory WHERE conversation_id = ? ORDER BY "timestamp" DESC LIMIT ?"""; - - private static final String QUERY_CLEAR = "DELETE FROM ai_chat_memory WHERE conversation_id = ?"; - - private final JdbcTemplate jdbcTemplate; - - public JdbcChatMemory(JdbcChatMemoryConfig config) { - this.jdbcTemplate = config.getJdbcTemplate(); - } - - public static JdbcChatMemory create(JdbcChatMemoryConfig config) { - return new JdbcChatMemory(config); - } - - @Override - public void add(String conversationId, List messages) { - this.jdbcTemplate.batchUpdate(QUERY_ADD, new AddBatchPreparedStatement(conversationId, messages)); - } - - @Override - public List get(String conversationId, int lastN) { - List messages = this.jdbcTemplate.query(QUERY_GET, new MessageRowMapper(), conversationId, lastN); - Collections.reverse(messages); - return messages; - } - - @Override - public void clear(String conversationId) { - this.jdbcTemplate.update(QUERY_CLEAR, conversationId); - } - - private record AddBatchPreparedStatement(String conversationId, List messages, - AtomicLong instantSeq) implements BatchPreparedStatementSetter { - - private AddBatchPreparedStatement(String conversationId, List messages) { - this(conversationId, messages, new AtomicLong(Instant.now().toEpochMilli())); - } - - @Override - public void setValues(PreparedStatement ps, int i) throws SQLException { - var message = this.messages.get(i); - - ps.setString(1, this.conversationId); - ps.setString(2, message.getText()); - ps.setString(3, message.getMessageType().name()); - ps.setTimestamp(4, new Timestamp(instantSeq.getAndIncrement())); - } - - @Override - public int getBatchSize() { - return this.messages.size(); - } - } - - private static class MessageRowMapper implements RowMapper { - - @Override - public Message mapRow(ResultSet rs, int i) throws SQLException { - var content = rs.getString(1); - var type = MessageType.valueOf(rs.getString(2)); - - return switch (type) { - case USER -> new UserMessage(content); - case ASSISTANT -> new AssistantMessage(content); - case SYSTEM -> new SystemMessage(content); - default -> null; - }; - } - - } - -} diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryConfig.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryConfig.java deleted file mode 100644 index 0211f05b853..00000000000 --- a/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryConfig.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.jdbc; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.util.Assert; - -/** - * Configuration for {@link JdbcChatMemory}. - * - * @author Jonathan Leijendekker - * @since 1.0.0 - * @deprecated in favor of using {@link JdbcChatMemoryRepository#builder()}. - */ -@Deprecated -public final class JdbcChatMemoryConfig { - - private final JdbcTemplate jdbcTemplate; - - private JdbcChatMemoryConfig(Builder builder) { - this.jdbcTemplate = builder.jdbcTemplate; - } - - public static Builder builder() { - return new Builder(); - } - - JdbcTemplate getJdbcTemplate() { - return this.jdbcTemplate; - } - - public static final class Builder { - - private JdbcTemplate jdbcTemplate; - - private Builder() { - } - - public Builder jdbcTemplate(JdbcTemplate jdbcTemplate) { - Assert.notNull(jdbcTemplate, "jdbc template must not be null"); - - this.jdbcTemplate = jdbcTemplate; - return this; - } - - public JdbcChatMemoryConfig build() { - Assert.notNull(this.jdbcTemplate, "jdbc template must not be null"); - - return new JdbcChatMemoryConfig(this); - } - - } - -} diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryConfigTest.java b/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryConfigTest.java deleted file mode 100644 index c553abb7509..00000000000 --- a/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryConfigTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2025-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.chat.memory.jdbc; - -import org.junit.jupiter.api.Test; - -import org.springframework.jdbc.core.JdbcTemplate; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; - -/** - * @author Jonathan Leijendekker - */ -class JdbcChatMemoryConfigTest { - - @Test - void setValues() { - var jdbcTemplate = mock(JdbcTemplate.class); - var config = JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build(); - - assertThat(config.getJdbcTemplate()).isEqualTo(jdbcTemplate); - } - - @Test - void setJdbcTemplateToNull_shouldThrow() { - assertThatThrownBy(() -> JdbcChatMemoryConfig.builder().jdbcTemplate(null)); - } - - @Test - void buildWithNullJdbcTemplate_shouldThrow() { - assertThatThrownBy(() -> JdbcChatMemoryConfig.builder().build()); - } - -} diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryIT.java b/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryIT.java deleted file mode 100644 index 24c9b2400f2..00000000000 --- a/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryIT.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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.jdbc; - -import java.sql.Timestamp; -import java.util.List; -import java.util.UUID; - -import javax.sql.DataSource; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; -import org.testcontainers.utility.MountableFile; - -import org.springframework.ai.chat.memory.ChatMemory; -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.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Primary; -import org.springframework.jdbc.core.JdbcTemplate; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Jonathan Leijendekker - * @author Linar Abzaltdinov - */ -@Testcontainers -class JdbcChatMemoryIT { - - @Container - @SuppressWarnings("resource") - static PostgreSQLContainer postgresContainer = new PostgreSQLContainer<>("postgres:17") - .withDatabaseName("chat_memory_test") - .withUsername("postgres") - .withPassword("postgres") - .withCopyFileToContainer( - MountableFile.forClasspathResource("org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql"), - "/docker-entrypoint-initdb.d/schema.sql"); - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withUserConfiguration(TestApplication.class) - .withPropertyValues(String.format("app.datasource.url=%s", postgresContainer.getJdbcUrl()), - String.format("app.datasource.username=%s", postgresContainer.getUsername()), - String.format("app.datasource.password=%s", postgresContainer.getPassword())); - - @BeforeAll - static void beforeAll() { - - } - - @Test - void correctChatMemoryInstance() { - this.contextRunner.run(context -> { - var chatMemory = context.getBean(ChatMemory.class); - - assertThat(chatMemory).isInstanceOf(JdbcChatMemory.class); - }); - } - - @ParameterizedTest - @CsvSource({ "Message from assistant,ASSISTANT", "Message from user,USER", "Message from system,SYSTEM" }) - void add_shouldInsertSingleMessage(String content, MessageType messageType) { - this.contextRunner.run(context -> { - var chatMemory = context.getBean(ChatMemory.class); - 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); - }; - - chatMemory.add(conversationId, message); - - var jdbcTemplate = context.getBean(JdbcTemplate.class); - var query = "SELECT conversation_id, content, type, \"timestamp\" FROM ai_chat_memory WHERE conversation_id = ?"; - var result = jdbcTemplate.queryForMap(query, conversationId); - - assertThat(result.size()).isEqualTo(4); - assertThat(result.get("conversation_id")).isEqualTo(conversationId); - assertThat(result.get("content")).isEqualTo(message.getText()); - assertThat(result.get("type")).isEqualTo(messageType.name()); - assertThat(result.get("timestamp")).isInstanceOf(Timestamp.class); - }); - } - - @Test - void add_shouldInsertMessages() { - this.contextRunner.run(context -> { - var chatMemory = context.getBean(ChatMemory.class); - 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)); - - chatMemory.add(conversationId, messages); - - var jdbcTemplate = context.getBean(JdbcTemplate.class); - var query = "SELECT conversation_id, content, type, \"timestamp\" FROM ai_chat_memory WHERE conversation_id = ?"; - var results = jdbcTemplate.queryForList(query, conversationId); - - assertThat(results.size()).isEqualTo(messages.size()); - - for (var i = 0; i < messages.size(); i++) { - var message = messages.get(i); - var result = results.get(i); - - assertThat(result.get("conversation_id")).isNotNull(); - assertThat(result.get("conversation_id")).isEqualTo(conversationId); - assertThat(result.get("content")).isEqualTo(message.getText()); - assertThat(result.get("type")).isEqualTo(message.getMessageType().name()); - assertThat(result.get("timestamp")).isInstanceOf(Timestamp.class); - } - }); - } - - @Test - void get_shouldReturnMessages() { - this.contextRunner.run(context -> { - var chatMemory = context.getBean(ChatMemory.class); - var conversationId = UUID.randomUUID().toString(); - var messages = List.of(new SystemMessage("Message from system - " + conversationId), - new UserMessage("Message from user 1 - " + conversationId), - new AssistantMessage("Message from assistant 1 - " + conversationId), - new UserMessage("Message from user 2 - " + conversationId), - new AssistantMessage("Message from assistant 2 - " + conversationId)); - - chatMemory.add(conversationId, messages); - - var results = chatMemory.get(conversationId, Integer.MAX_VALUE); - - assertThat(results.size()).isEqualTo(messages.size()); - assertThat(results).isEqualTo(messages); - }); - } - - @Test - void get_afterMultipleAdds_shouldReturnMessagesInSameOrder() { - this.contextRunner.run(context -> { - var chatMemory = context.getBean(ChatMemory.class); - var conversationId = UUID.randomUUID().toString(); - var userMessage = new UserMessage("Message from user - " + conversationId); - var assistantMessage = new AssistantMessage("Message from assistant - " + conversationId); - - chatMemory.add(conversationId, userMessage); - chatMemory.add(conversationId, assistantMessage); - - var results = chatMemory.get(conversationId, Integer.MAX_VALUE); - - assertThat(results.size()).isEqualTo(2); - assertThat(results).isEqualTo(List.of(userMessage, assistantMessage)); - }); - } - - @Test - void clear_shouldDeleteMessages() { - this.contextRunner.run(context -> { - var chatMemory = context.getBean(ChatMemory.class); - 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)); - - chatMemory.add(conversationId, messages); - - chatMemory.clear(conversationId); - - var jdbcTemplate = context.getBean(JdbcTemplate.class); - var count = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM ai_chat_memory WHERE conversation_id = ?", - Integer.class, conversationId); - - assertThat(count).isZero(); - }); - } - - @SpringBootConfiguration - @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class }) - static class TestApplication { - - @Bean - public ChatMemory chatMemory(JdbcTemplate jdbcTemplate) { - var config = JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build(); - - return JdbcChatMemory.create(config); - } - - @Bean - public JdbcTemplate jdbcTemplate(DataSource dataSource) { - return new JdbcTemplate(dataSource); - } - - @Bean - @Primary - @ConfigurationProperties("app.datasource") - public DataSourceProperties dataSourceProperties() { - return new DataSourceProperties(); - } - - @Bean - public DataSource dataSource(DataSourceProperties dataSourceProperties) { - return dataSourceProperties.initializeDataSourceBuilder().build(); - } - - } - -} 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 df5201495b4..11dd57d8167 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 @@ -1,7 +1,16 @@ [[ChatMemory]] = Chat Memory -Large language models (LLMs) are stateless, meaning they do not retain information about previous interactions. This can be a limitation when you want to maintain context or state across multiple interactions. To address this, Spring AI provides a `ChatMemory` abstraction that allows you to store and retrieve information across multiple interactions with the LLM. +Large language models (LLMs) are stateless, meaning they do not retain information about previous interactions. This can be a limitation when you want to maintain context or state across multiple interactions. To address this, Spring AI provides chat memory features that allow you to store and retrieve information across multiple interactions with the LLM. + +The `ChatMemory` abstraction allows you to implement various types of memory to support different use cases. The underlying storage of the messages is handled by the `ChatMemoryRepository`, whose sole responsibility is to store and retrieve messages. It's up to the `ChatMemory` implementation to decide which messages to keep and when to remove them. Examples of strategies could include keeping the last N messages, keeping messages for a certain time period, or keeping messages up to a certain token limit. + +Before choosing a memory type, it's important to understand the difference between chat memory and chat history. + +* *Chat Memory*. The information that a large-language model retains and uses to maintain contextual awareness throughout a conversation. +* *Chat History*. The entire conversation history, including all messages exchanged between the user and the model. + +The `ChatMemory` abstraction is designed to manage the _chat memory_. It allows you to store and retrieve messages that are relevant to the current conversation context. However, it is not the best fit for storing the _chat history_. If you need to maintain a complete record of all the messages exchanged, you should consider using a different approach, such as relying on Spring Data for efficient storage and retrieval of the complete chat history. == Quick Start diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatclient.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatclient.adoc index e5776c287d4..3af0b275aa3 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatclient.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chatclient.adoc @@ -565,36 +565,4 @@ in Assistant Messages | `ToolCall` === JdbcChatMemory -To create a `JdbcChatMemory`: - -[source,java] ----- -JdbcChatMemory.create(JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build()); ----- - -The `JdbcChatMemory` can also be autoconfigured (given that you have the `JdbcTemplate` bean) by adding the following dependency to your project: - -to your Maven `pom.xml` file: - -[source,xml] ----- - - org.springframework.ai - spring-ai-starter-model-chat-memory-jdbc - ----- - -or to your Gradle `build.gradle` file: - -[source,groovy] ----- -dependencies { - implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-jdbc' -} ----- - -The autoconfiguration will automatically create the `ai_chat_memory` table by default based on the JDBC driver. Currently, only `postgresql` and `mariadb` are supported. - -To disable schema initialization, set the property `spring.ai.chat.memory.jdbc.initialize-schema` to `false`. - -There are instances where you are using a database migration tool like Liquibase or Flyway to manage your database schema. In that case, you may disable schema initialization and just refer to link:https://github.com/spring-projects/spring-ai/tree/main/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc[these sql files] and add them to your migration script. +IMPORTANT: Refer to the new xref:api/chat-memory.adoc#_jdbc_repository[JDBC Chat Memory Repository] documentation for the current features and capabilities. 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 index 8b289d1b276..25c32f034db 100644 --- 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 @@ -40,7 +40,7 @@ */ public final class MessageWindowChatMemory implements ChatMemory { - private static final int DEFAULT_MAX_MESSAGES = 200; + private static final int DEFAULT_MAX_MESSAGES = 20; private static final ChatMemoryRepository DEFAULT_CHAT_MEMORY_REPOSITORY = new InMemoryChatMemoryRepository();