diff --git a/README.md b/README.md
index 1f53d37cdc4..c77b604d526 100644
--- a/README.md
+++ b/README.md
@@ -103,7 +103,7 @@ One way to run integration tests on part of the code is to first do a quick comp
```
Then run the integration test for a specific module using the `-pl` option
```shell
-./mvnw verify -Pintegration-tests -pl spring-ai-spring-boot-testcontainers
+./mvnw verify -Pintegration-tests -pl spring-ai-spring-boot-testcontainers
```
### Documentation
diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/pom.xml b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/pom.xml
new file mode 100644
index 00000000000..69116d535b6
--- /dev/null
+++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai-parent
+ 1.0.0-SNAPSHOT
+ ../../../../../pom.xml
+
+ spring-ai-autoconfigure-model-chat-memory-jdbc
+ jar
+ Spring AI JDBC Chat Memory Auto Configuration
+ Spring JDBC AI 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-core
+ ${project.parent.version}
+
+
+
+ org.springframework.ai
+ spring-ai-model-chat-memory-jdbc
+ ${project.parent.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+
+
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
new file mode 100644
index 00000000000..bc811c3ded6
--- /dev/null
+++ 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
@@ -0,0 +1,65 @@
+/*
+ * 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.model.chat.memory.jdbc.autoconfigure;
+
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.chat.memory.jdbc.JdbcChatMemory;
+import org.springframework.ai.chat.memory.jdbc.JdbcChatMemoryConfig;
+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.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * @author Jonathan Leijendekker
+ * @since 1.0.0
+ */
+@AutoConfiguration(after = JdbcTemplateAutoConfiguration.class)
+@ConditionalOnClass({ JdbcChatMemory.class, DataSource.class, JdbcTemplate.class })
+@EnableConfigurationProperties(JdbcChatMemoryProperties.class)
+public class JdbcChatMemoryAutoConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(JdbcChatMemoryAutoConfiguration.class);
+
+ @Bean
+ @ConditionalOnMissingBean
+ public JdbcChatMemory chatMemory(JdbcTemplate jdbcTemplate) {
+ var config = JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build();
+
+ return JdbcChatMemory.create(config);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(value = "spring.ai.chat.memory.jdbc.initialize-schema", havingValue = "true",
+ matchIfMissing = true)
+ public DataSourceScriptDatabaseInitializer jdbcChatMemoryScriptDatabaseInitializer(DataSource dataSource) {
+ logger.debug("Initializing JdbcChatMemory schema");
+
+ return new JdbcChatMemoryDataSourceScriptDatabaseInitializer(dataSource);
+ }
+
+}
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/JdbcChatMemoryDataSourceScriptDatabaseInitializer.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/JdbcChatMemoryDataSourceScriptDatabaseInitializer.java
new file mode 100644
index 00000000000..2f1927048df
--- /dev/null
+++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializer.java
@@ -0,0 +1,35 @@
+package org.springframework.ai.model.chat.memory.jdbc.autoconfigure;
+
+import java.util.List;
+
+import javax.sql.DataSource;
+
+import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
+import org.springframework.boot.jdbc.init.PlatformPlaceholderDatabaseDriverResolver;
+import org.springframework.boot.sql.init.DatabaseInitializationMode;
+import org.springframework.boot.sql.init.DatabaseInitializationSettings;
+
+class JdbcChatMemoryDataSourceScriptDatabaseInitializer extends DataSourceScriptDatabaseInitializer {
+
+ private static final String SCHEMA_LOCATION = "classpath:org/springframework/ai/chat/memory/jdbc/schema-@@platform@@.sql";
+
+ public JdbcChatMemoryDataSourceScriptDatabaseInitializer(DataSource dataSource) {
+ super(dataSource, getSettings(dataSource));
+ }
+
+ static DatabaseInitializationSettings getSettings(DataSource dataSource) {
+ var settings = new DatabaseInitializationSettings();
+ settings.setSchemaLocations(resolveSchemaLocations(dataSource));
+ settings.setMode(DatabaseInitializationMode.ALWAYS);
+ settings.setContinueOnError(true);
+
+ return settings;
+ }
+
+ static List resolveSchemaLocations(DataSource dataSource) {
+ var platformResolver = new PlatformPlaceholderDatabaseDriverResolver();
+
+ return platformResolver.resolveAll(dataSource, SCHEMA_LOCATION);
+ }
+
+}
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/JdbcChatMemoryProperties.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/JdbcChatMemoryProperties.java
new file mode 100644
index 00000000000..1c33ffbb0a5
--- /dev/null
+++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryProperties.java
@@ -0,0 +1,40 @@
+/*
+ * 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.model.chat.memory.jdbc.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author Jonathan Leijendekker
+ * @since 1.0.0
+ */
+@ConfigurationProperties(JdbcChatMemoryProperties.CONFIG_PREFIX)
+public class JdbcChatMemoryProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.chat.memory.jdbc";
+
+ private boolean initializeSchema = true;
+
+ public boolean isInitializeSchema() {
+ return this.initializeSchema;
+ }
+
+ public void setInitializeSchema(boolean initializeSchema) {
+ this.initializeSchema = initializeSchema;
+ }
+
+}
diff --git a/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..6820c4237de
--- /dev/null
+++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/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.jdbc.autoconfigure.JdbcChatMemoryAutoConfiguration
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/JdbcChatMemoryAutoConfigurationIT.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/JdbcChatMemoryAutoConfigurationIT.java
new file mode 100644
index 00000000000..6f9573d3eb0
--- /dev/null
+++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryAutoConfigurationIT.java
@@ -0,0 +1,101 @@
+/*
+ * 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.model.chat.memory.jdbc.autoconfigure;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import org.springframework.ai.chat.memory.jdbc.JdbcChatMemory;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jonathan Leijendekker
+ */
+@Testcontainers
+class JdbcChatMemoryAutoConfigurationIT {
+
+ static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("postgres:17");
+
+ @Container
+ @SuppressWarnings("resource")
+ static PostgreSQLContainer> postgresContainer = new PostgreSQLContainer<>(DEFAULT_IMAGE_NAME)
+ .withDatabaseName("chat_memory_auto_configuration_test")
+ .withUsername("postgres")
+ .withPassword("postgres");
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(JdbcChatMemoryAutoConfiguration.class,
+ JdbcTemplateAutoConfiguration.class, DataSourceAutoConfiguration.class))
+ .withPropertyValues(String.format("spring.datasource.url=%s", postgresContainer.getJdbcUrl()),
+ String.format("spring.datasource.username=%s", postgresContainer.getUsername()),
+ String.format("spring.datasource.password=%s", postgresContainer.getPassword()));
+
+ @Test
+ void jdbcChatMemoryScriptDatabaseInitializer_shouldBeLoaded() {
+ this.contextRunner.withPropertyValues("spring.ai.chat.memory.jdbc.initialize-schema=true").run(context -> {
+ assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue();
+ });
+ }
+
+ @Test
+ void jdbcChatMemoryScriptDatabaseInitializer_shouldNotBeLoaded() {
+ this.contextRunner.withPropertyValues("spring.ai.chat.memory.jdbc.initialize-schema=false").run(context -> {
+ assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isFalse();
+ });
+ }
+
+ @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));
+
+ 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);
+ });
+ }
+
+}
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/JdbcChatMemoryDataSourceScriptDatabaseInitializerTests.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/JdbcChatMemoryDataSourceScriptDatabaseInitializerTests.java
new file mode 100644
index 00000000000..bcb1a9daacc
--- /dev/null
+++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryDataSourceScriptDatabaseInitializerTests.java
@@ -0,0 +1,51 @@
+package org.springframework.ai.model.chat.memory.jdbc.autoconfigure;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jonathan Leijendekker
+ */
+@Testcontainers
+class JdbcChatMemoryDataSourceScriptDatabaseInitializerTests {
+
+ static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("postgres:17");
+
+ @Container
+ @SuppressWarnings("resource")
+ static PostgreSQLContainer> postgresContainer = new PostgreSQLContainer<>(DEFAULT_IMAGE_NAME)
+ .withDatabaseName("chat_memory_initializer_test")
+ .withUsername("postgres")
+ .withPassword("postgres");
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(JdbcChatMemoryAutoConfiguration.class,
+ JdbcTemplateAutoConfiguration.class, DataSourceAutoConfiguration.class))
+ .withPropertyValues(String.format("spring.datasource.url=%s", postgresContainer.getJdbcUrl()),
+ String.format("spring.datasource.username=%s", postgresContainer.getUsername()),
+ String.format("spring.datasource.password=%s", postgresContainer.getPassword()));
+
+ @Test
+ void getSettings_shouldHaveSchemaLocations() {
+ this.contextRunner.run(context -> {
+ var dataSource = context.getBean(DataSource.class);
+ var settings = JdbcChatMemoryDataSourceScriptDatabaseInitializer.getSettings(dataSource);
+
+ assertThat(settings.getSchemaLocations())
+ .containsOnly("classpath:org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql");
+ });
+ }
+
+}
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/JdbcChatMemoryPropertiesTests.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/JdbcChatMemoryPropertiesTests.java
new file mode 100644
index 00000000000..196176149ff
--- /dev/null
+++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc/src/test/java/org/springframework/ai/model/chat/memory/jdbc/autoconfigure/JdbcChatMemoryPropertiesTests.java
@@ -0,0 +1,43 @@
+/*
+ * 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.model.chat.memory.jdbc.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jonathan Leijendekker
+ */
+class JdbcChatMemoryPropertiesTests {
+
+ @Test
+ void defaultValues() {
+ var props = new JdbcChatMemoryProperties();
+
+ assertThat(props.isInitializeSchema()).isTrue();
+ }
+
+ @Test
+ void customValues() {
+ var props = new JdbcChatMemoryProperties();
+ props.setInitializeSchema(false);
+
+ assertThat(props.isInitializeSchema()).isFalse();
+ }
+
+}
diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-milvus/pom.xml b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-milvus/pom.xml
index 6571316245c..dd9ce193b19 100644
--- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-milvus/pom.xml
+++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-milvus/pom.xml
@@ -27,8 +27,8 @@
spring-ai-autoconfigure-vector-store-milvus
jar
- Spring AI Auto Configuration for Weaviate vector store
- Spring AI Auto Configuration for Weaviate vector store
+ Spring AI Auto Configuration for Milvus vector store
+ Spring AI Auto Configuration for Mulvis vector store
https://github.com/spring-projects/spring-ai
diff --git a/memory/spring-ai-model-chat-memory-jdbc/README.md b/memory/spring-ai-model-chat-memory-jdbc/README.md
new file mode 100644
index 00000000000..8e100ad20a3
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/README.md
@@ -0,0 +1 @@
+[Chat Memory Documentation](https://docs.spring.io/spring-ai/reference/api/chatclient.html#_chat_memory)
diff --git a/memory/spring-ai-model-chat-memory-jdbc/pom.xml b/memory/spring-ai-model-chat-memory-jdbc/pom.xml
new file mode 100644
index 00000000000..98183015242
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/pom.xml
@@ -0,0 +1,95 @@
+
+
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai-parent
+ 1.0.0-SNAPSHOT
+ ../../pom.xml
+
+
+ spring-ai-model-chat-memory-jdbc
+ Spring AI JDBC Chat Memory
+ Spring AI JDBC Chat Memory implementation
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${project.parent.version}
+
+
+
+ org.springframework
+ spring-jdbc
+
+
+
+ com.zaxxer
+ HikariCP
+
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+ true
+
+
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ ${mariadb.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+
+ org.testcontainers
+ mariadb
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
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
new file mode 100644
index 00000000000..477f7509a19
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemory.java
@@ -0,0 +1,107 @@
+/*
+ * 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.util.List;
+
+import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.ai.chat.messages.*;
+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
+ * @since 1.0.0
+ */
+public class JdbcChatMemory implements ChatMemory {
+
+ private static final String QUERY_ADD = """
+ INSERT INTO ai_chat_memory (conversation_id, content, type) 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) {
+ return this.jdbcTemplate.query(QUERY_GET, new MessageRowMapper(), conversationId, lastN);
+ }
+
+ @Override
+ public void clear(String conversationId) {
+ this.jdbcTemplate.update(QUERY_CLEAR, conversationId);
+ }
+
+ private record AddBatchPreparedStatement(String conversationId,
+ List messages) implements BatchPreparedStatementSetter {
+ @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());
+ }
+
+ @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
new file mode 100644
index 00000000000..5a503aef051
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryConfig.java
@@ -0,0 +1,66 @@
+/*
+ * 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
+ */
+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/main/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRuntimeHints.java b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRuntimeHints.java
new file mode 100644
index 00000000000..eae3206f9a9
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRuntimeHints.java
@@ -0,0 +1,26 @@
+package org.springframework.ai.chat.memory.jdbc.aot.hint;
+
+import javax.sql.DataSource;
+
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+
+/**
+ * A {@link RuntimeHintsRegistrar} for JDBC Chat Memory hints
+ *
+ * @author Jonathan Leijendekker
+ */
+class JdbcChatMemoryRuntimeHints implements RuntimeHintsRegistrar {
+
+ @Override
+ public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
+ hints.reflection()
+ .registerType(DataSource.class, (hint) -> hint.withMembers(MemberCategory.INVOKE_DECLARED_METHODS));
+
+ hints.resources()
+ .registerPattern("org/springframework/ai/chat/memory/jdbc/schema-mariadb.sql")
+ .registerPattern("org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql");
+ }
+
+}
diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/META-INF/spring/aot.factories b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/META-INF/spring/aot.factories
new file mode 100644
index 00000000000..4b6f4a8f5ce
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/META-INF/spring/aot.factories
@@ -0,0 +1,2 @@
+org.springframework.aot.hint.RuntimeHintsRegistrar=\
+org.springframework.ai.chat.memory.jdbc.aot.hint.JdbcChatMemoryRuntimeHints
diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-mariadb.sql b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-mariadb.sql
new file mode 100644
index 00000000000..88c0ea11ba0
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-mariadb.sql
@@ -0,0 +1,10 @@
+CREATE TABLE IF NOT EXISTS ai_chat_memory (
+ conversation_id VARCHAR(36) NOT NULL,
+ content TEXT NOT NULL,
+ type VARCHAR(10) NOT NULL,
+ `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT type_check CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
+);
+
+CREATE INDEX IF NOT EXISTS ai_chat_memory_conversation_id_timestamp_idx
+ON ai_chat_memory(conversation_id, `timestamp` DESC);
diff --git a/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql
new file mode 100644
index 00000000000..11e60194b60
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/src/main/resources/org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS ai_chat_memory (
+ conversation_id VARCHAR(36) NOT NULL,
+ content TEXT NOT NULL,
+ type VARCHAR(10) NOT NULL CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')),
+ "timestamp" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX IF NOT EXISTS ai_chat_memory_conversation_id_timestamp_idx
+ON ai_chat_memory(conversation_id, "timestamp" DESC);
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
new file mode 100644
index 00000000000..7ae2c4477a1
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryConfigTest.java
@@ -0,0 +1,34 @@
+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
new file mode 100644
index 00000000000..1651bd49e87
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/JdbcChatMemoryIT.java
@@ -0,0 +1,211 @@
+/*
+ * 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.*;
+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
+ */
+@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 AssistantMessage("Message from assistant 1 - " + conversationId),
+ new AssistantMessage("Message from assistant 2 - " + conversationId),
+ new UserMessage("Message from user - " + conversationId),
+ new SystemMessage("Message from system - " + 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 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/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRuntimeHintsTest.java b/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRuntimeHintsTest.java
new file mode 100644
index 00000000000..1a6c7ff2d83
--- /dev/null
+++ b/memory/spring-ai-model-chat-memory-jdbc/src/test/java/org/springframework/ai/chat/memory/jdbc/aot/hint/JdbcChatMemoryRuntimeHintsTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.aot.hint;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.SpringFactoriesLoader;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Jonathan Leijendekker
+ */
+class JdbcChatMemoryRuntimeHintsTest {
+
+ private final RuntimeHints hints = new RuntimeHints();
+
+ private final JdbcChatMemoryRuntimeHints jdbcChatMemoryRuntimeHints = new JdbcChatMemoryRuntimeHints();
+
+ @Test
+ void aotFactoriesContainsRegistrar() {
+ var match = SpringFactoriesLoader.forResourceLocation("META-INF/spring/aot.factories")
+ .load(RuntimeHintsRegistrar.class)
+ .stream()
+ .anyMatch((registrar) -> registrar instanceof JdbcChatMemoryRuntimeHints);
+
+ assertThat(match).isTrue();
+ }
+
+ @ParameterizedTest
+ @MethodSource("getSchemaFileNames")
+ void jdbcSchemasHasHints(String schemaFileName) {
+ this.jdbcChatMemoryRuntimeHints.registerHints(this.hints, getClass().getClassLoader());
+
+ var predicate = RuntimeHintsPredicates.resource()
+ .forResource("org/springframework/ai/chat/memory/jdbc/" + schemaFileName);
+
+ assertThat(predicate).accepts(this.hints);
+ }
+
+ @Test
+ void dataSourceHasHints() {
+ this.jdbcChatMemoryRuntimeHints.registerHints(this.hints, getClass().getClassLoader());
+
+ assertThat(RuntimeHintsPredicates.reflection().onType(DataSource.class)).accepts(this.hints);
+ }
+
+ private static Stream getSchemaFileNames() throws IOException {
+ var resources = new PathMatchingResourcePatternResolver()
+ .getResources("classpath*:org/springframework/ai/chat/memory/jdbc/schema-*.sql");
+
+ return Arrays.stream(resources).map(Resource::getFilename);
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index df01bc41373..6a1a5e5de9e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,8 +40,9 @@
spring-ai-rag
advisors/spring-ai-advisors-vector-store
- memory/spring-ai-model-chat-memory-neo4j
memory/spring-ai-model-chat-memory-cassandra
+ memory/spring-ai-model-chat-memory-jdbc
+ memory/spring-ai-model-chat-memory-neo4j
auto-configurations/common/spring-ai-autoconfigure-retry
@@ -50,6 +51,7 @@
auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client
auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-cassandra
+ auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc
auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-neo4j
auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation
@@ -179,6 +181,7 @@
spring-ai-spring-boot-starters/spring-ai-starter-model-azure-openai
spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock
spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock-converse
+ spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-jdbc
spring-ai-spring-boot-starters/spring-ai-starter-model-huggingface
spring-ai-spring-boot-starters/spring-ai-starter-model-minimax
spring-ai-spring-boot-starters/spring-ai-starter-model-mistral-ai
@@ -198,7 +201,7 @@
spring-ai-spring-boot-starters/spring-ai-starter-mcp-client
spring-ai-spring-boot-starters/spring-ai-starter-mcp-server
spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux
- spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux
+ spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux
spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webmvc
spring-ai-integration-tests
@@ -293,6 +296,7 @@
1.13.0
0.5.0
2.10.1
+ 42.7.5
3.5.1
0.22.0
@@ -690,6 +694,9 @@
-->
+
+ org.springframework.ai.chat.memory/**/*IT.java
+
org.springframework.ai.anthropic/**/*IT.java
org.springframework.ai.azure.openai/**/*IT.java
@@ -713,7 +720,6 @@
org.springframework.ai.vectorstore**/CosmosDB**IT.java
org.springframework.ai.vectorstore.azure/**IT.java
- org.springframework.ai.chat.memory/**/Cassandra**IT.java
org.springframework.ai.vectorstore**/Cassandra**IT.java
org.springframework.ai.chroma/**IT.java
diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml
index 80f9a62ba31..1f1b0836bc4 100644
--- a/spring-ai-bom/pom.xml
+++ b/spring-ai-bom/pom.xml
@@ -148,13 +148,19 @@
${project.version}
+
+ org.springframework.ai
+ spring-ai-model-chat-memory-jdbc
+ ${project.version}
+
+
org.springframework.ai
spring-ai-model-chat-memory-neo4j
${project.version}
-
+
org.springframework.ai
@@ -973,6 +979,14 @@
${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-starter-model-chat-memory-jdbc
+ ${project.version}
+
+
@@ -994,7 +1008,7 @@
-
+
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 3fd13c44c89..7b445d067b3 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
@@ -366,46 +366,11 @@ In this configuration, the `MessageChatMemoryAdvisor` will be executed first, ad
xref:ROOT:api/retrieval-augmented-generation.adoc#_questionansweradvisor[Learn about Question Answer Advisor]
-=== Retrieval Augmented Generation
-
-Refer to the xref:_retrieval_augmented_generation[Retrieval Augmented Generation] guide.
-
-=== Chat Memory
-
-The interface `ChatMemory` represents a storage for chat conversation history. It provides methods to add messages to a conversation, retrieve messages from a conversation, and clear the conversation history.
-
-There are currently three implementations, `InMemoryChatMemory`, `CassandraChatMemory` and `Neo4jChatMemory`, that provide storage for chat conversation history, in-memory, persisted with `time-to-live` in Cassandra, and persisted without `time-to-live` in Neo4j correspondingly.
-
-To create a `CassandraChatMemory` with `time-to-live`:
-
-[source,java]
-----
-CassandraChatMemory.create(CassandraChatMemoryConfig.builder().withTimeToLive(Duration.ofDays(1)).build());
-----
-
-The Neo4j chat memory supports the following configuration parameters:
-
-[cols="2,5,1",stripes=even]
-|===
-|Property | Description | Default Value
-
-| `spring.ai.chat.memory.neo4j.messageLabel` | The label for the nodes that store messages | `Message`
-| `spring.ai.chat.memory.neo4j.sessionLabel` | The label for the nodes that store conversation sessions | `Session`
-| `spring.ai.chat.memory.neo4j.toolCallLabel` | The label for nodes that store tool calls, for example
-in Assistant Messages | `ToolCall`
-| `spring.ai.chat.memory.neo4j.metadataLabel` | The label for the node that store a message metadata | `Metadata`
-| `spring.ai.chat.memory.neo4j.toolResponseLabel` | The label for the nodes that store tool responses | `ToolResponse`
-| `spring.ai.chat.memory.neo4j.mediaLabel` | The label for the nodes that store the media associated to a message | `ToolResponse`
-
-
-|===
-
-
The following advisor implementations use the `ChatMemory` interface to advice the prompt with conversation history which differ in the details of how the memory is added to the prompt
* `MessageChatMemoryAdvisor` : Memory is retrieved and added as a collection of messages to the prompt
* `PromptChatMemoryAdvisor` : Memory is retrieved and added into the prompt's system text.
-* `VectorStoreChatMemoryAdvisor` : The constructor `VectorStoreChatMemoryAdvisor(VectorStore vectorStore, String defaultConversationId, int chatHistoryWindowSize, int order)` This constructor allows you to:
+* `VectorStoreChatMemoryAdvisor` : The constructor `VectorStoreChatMemoryAdvisor(VectorStore vectorStore, String defaultConversationId, int chatHistoryWindowSize, int order)` This constructor allows you to:
. Specify the VectorStore instance used for managing and querying documents.
. Set a default conversation ID to be used if none is provided in the context.
@@ -465,6 +430,10 @@ public class CustomerSupportAssistant {
xref:ROOT:api/retrieval-augmented-generation.adoc#_questionansweradvisor[Learn about Question Answer Advisor]
+=== Retrieval Augmented Generation
+
+Refer to the xref:ROOT:api/retrieval-augmented-generation.adoc[Retrieval Augmented Generation] guide.
+
=== Logging
The `SimpleLoggerAdvisor` is an advisor that logs the `request` and `response` data of the `ChatClient`.
@@ -515,3 +484,72 @@ SimpleLoggerAdvisor customLogger = new SimpleLoggerAdvisor(
This allows you to tailor the logged information to your specific needs.
TIP: Be cautious about logging sensitive information in production environments.
+
+== Chat Memory
+
+The interface `ChatMemory` represents a storage for chat conversation history. It provides methods to add messages to a conversation, retrieve messages from a conversation, and clear the conversation history.
+
+There are currently four implementations: `InMemoryChatMemory`, `CassandraChatMemory`, `Neo4jChatMemory`, and `JdbcChatMemory`, which provide storage for chat conversation history in-memory, persisted with `time-to-live` in Cassandra, and persisted without `time-to-live` in Neo4j and Jdbc, respectively.
+
+=== CassandraChatMemory
+
+To create a `CassandraChatMemory` with `time-to-live`:
+
+[source,java]
+----
+CassandraChatMemory.create(CassandraChatMemoryConfig.builder().withTimeToLive(Duration.ofDays(1)).build());
+----
+
+=== Neo4jChatMemory
+
+The Neo4j chat memory supports the following configuration parameters:
+
+[cols="2,5,1",stripes=even]
+|===
+|Property | Description | Default Value
+
+| `spring.ai.chat.memory.neo4j.messageLabel` | The label for the nodes that store messages | `Message`
+| `spring.ai.chat.memory.neo4j.sessionLabel` | The label for the nodes that store conversation sessions | `Session`
+| `spring.ai.chat.memory.neo4j.toolCallLabel` | The label for nodes that store tool calls, for example
+in Assistant Messages | `ToolCall`
+| `spring.ai.chat.memory.neo4j.metadataLabel` | The label for the node that store a message metadata | `Metadata`
+| `spring.ai.chat.memory.neo4j.toolResponseLabel` | The label for the nodes that store tool responses | `ToolResponse`
+| `spring.ai.chat.memory.neo4j.mediaLabel` | The label for the nodes that store the media associated to a message | `ToolResponse`
+
+|===
+
+=== 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.
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-jdbc/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-jdbc/pom.xml
new file mode 100644
index 00000000000..112be3cc248
--- /dev/null
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-jdbc/pom.xml
@@ -0,0 +1,58 @@
+
+
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai-parent
+ 1.0.0-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-starter-model-chat-memory-jdbc
+ jar
+ Spring AI Starter - JDBC Chat Memory
+ Spring AI JDBC 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-jdbc
+ ${project.parent.version}
+
+
+
+ org.springframework.ai
+ spring-ai-model-chat-memory-jdbc
+ ${project.parent.version}
+
+
+
+