feat: JDBC implementation of ChatMemory
Signed-off-by: leijendary <jonathanleijendekker@gmail.com> Remove references to spring-ai-core module in jdbc chat memory
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-parent</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../../../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-ai-autoconfigure-model-chat-memory-jdbc</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring AI JDBC Chat Memory Auto Configuration</name>
|
||||
<description>Spring JDBC AI Chat Memory Auto Configuration</description>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
<connection>git://github.com/spring-projects/spring-ai.git</connection>
|
||||
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-model-chat-memory-jdbc</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Boot dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgresql.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> resolveSchemaLocations(DataSource dataSource) {
|
||||
var platformResolver = new PlatformPlaceholderDatabaseDriverResolver();
|
||||
|
||||
return platformResolver.resolveAll(dataSource, SCHEMA_LOCATION);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.<Message>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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,8 +27,8 @@
|
||||
</parent>
|
||||
<artifactId>spring-ai-autoconfigure-vector-store-milvus</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring AI Auto Configuration for Weaviate vector store</name>
|
||||
<description>Spring AI Auto Configuration for Weaviate vector store</description>
|
||||
<name>Spring AI Auto Configuration for Milvus vector store</name>
|
||||
<description>Spring AI Auto Configuration for Mulvis vector store</description>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
|
||||
<scm>
|
||||
|
||||
1
memory/spring-ai-model-chat-memory-jdbc/README.md
Normal file
1
memory/spring-ai-model-chat-memory-jdbc/README.md
Normal file
@@ -0,0 +1 @@
|
||||
[Chat Memory Documentation](https://docs.spring.io/spring-ai/reference/api/chatclient.html#_chat_memory)
|
||||
95
memory/spring-ai-model-chat-memory-jdbc/pom.xml
Normal file
95
memory/spring-ai-model-chat-memory-jdbc/pom.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2023-2024 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.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-parent</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>spring-ai-model-chat-memory-jdbc</artifactId>
|
||||
<name>Spring AI JDBC Chat Memory</name>
|
||||
<description>Spring AI JDBC Chat Memory implementation</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-client-chat</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${postgresql.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<version>${mariadb.version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- TESTING -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>mariadb</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -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:
|
||||
* <code>JdbcChatMemory.create(JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build());</code>
|
||||
*
|
||||
* @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<Message> messages) {
|
||||
this.jdbcTemplate.batchUpdate(QUERY_ADD, new AddBatchPreparedStatement(conversationId, messages));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Message> 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<Message> 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<Message> {
|
||||
|
||||
@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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.aot.hint.RuntimeHintsRegistrar=\
|
||||
org.springframework.ai.chat.memory.jdbc.aot.hint.JdbcChatMemoryRuntimeHints
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.<Message>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.<Message>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.<Message>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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> getSchemaFileNames() throws IOException {
|
||||
var resources = new PathMatchingResourcePatternResolver()
|
||||
.getResources("classpath*:org/springframework/ai/chat/memory/jdbc/schema-*.sql");
|
||||
|
||||
return Arrays.stream(resources).map(Resource::getFilename);
|
||||
}
|
||||
|
||||
}
|
||||
12
pom.xml
12
pom.xml
@@ -40,8 +40,9 @@
|
||||
<module>spring-ai-rag</module>
|
||||
<module>advisors/spring-ai-advisors-vector-store</module>
|
||||
|
||||
<module>memory/spring-ai-model-chat-memory-neo4j</module>
|
||||
<module>memory/spring-ai-model-chat-memory-cassandra</module>
|
||||
<module>memory/spring-ai-model-chat-memory-jdbc</module>
|
||||
<module>memory/spring-ai-model-chat-memory-neo4j</module>
|
||||
|
||||
<module>auto-configurations/common/spring-ai-autoconfigure-retry</module>
|
||||
|
||||
@@ -50,6 +51,7 @@
|
||||
<module>auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client</module>
|
||||
|
||||
<module>auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-cassandra</module>
|
||||
<module>auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-jdbc</module>
|
||||
<module>auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory-neo4j</module>
|
||||
|
||||
<module>auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation</module>
|
||||
@@ -179,6 +181,7 @@
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-azure-openai</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock-converse</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-chat-memory-jdbc</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-huggingface</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-minimax</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-model-mistral-ai</module>
|
||||
@@ -198,7 +201,7 @@
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-mcp-client</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-mcp-server</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux</module>
|
||||
<module>spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webmvc</module>
|
||||
|
||||
<module>spring-ai-integration-tests</module>
|
||||
@@ -293,6 +296,7 @@
|
||||
<qdrant.version>1.13.0</qdrant.version>
|
||||
<typesense.version>0.5.0</typesense.version>
|
||||
<opensearch-client.version>2.10.1</opensearch-client.version>
|
||||
<postgresql.version>42.7.5</postgresql.version>
|
||||
<mariadb.version>3.5.1</mariadb.version>
|
||||
<commonmark.version>0.22.0</commonmark.version>
|
||||
|
||||
@@ -690,6 +694,9 @@
|
||||
</includes> -->
|
||||
|
||||
<excludes>
|
||||
<!-- Chat Memory -->
|
||||
<exclude>org.springframework.ai.chat.memory/**/*IT.java</exclude>
|
||||
|
||||
<!-- Models -->
|
||||
<exclude>org.springframework.ai.anthropic/**/*IT.java</exclude>
|
||||
<exclude>org.springframework.ai.azure.openai/**/*IT.java</exclude>
|
||||
@@ -713,7 +720,6 @@
|
||||
<!-- Vector Stores -->
|
||||
<exclude>org.springframework.ai.vectorstore**/CosmosDB**IT.java</exclude>
|
||||
<exclude>org.springframework.ai.vectorstore.azure/**IT.java</exclude>
|
||||
<exclude>org.springframework.ai.chat.memory/**/Cassandra**IT.java</exclude>
|
||||
<exclude>org.springframework.ai.vectorstore**/Cassandra**IT.java</exclude>
|
||||
<exclude>org.springframework.ai.chroma/**IT.java</exclude>
|
||||
<!-- <exclude>org.springframework.ai.vectorstore/**/**Chroma**IT.java</exclude> -->
|
||||
|
||||
@@ -148,13 +148,19 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-model-chat-memory-jdbc</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-model-chat-memory-neo4j</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI Models -->
|
||||
<!-- Spring AI Models -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
@@ -973,6 +979,14 @@
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring AI Spring Boot starters for Chat Memory -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-chat-memory-jdbc</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Utilities -->
|
||||
|
||||
<dependency>
|
||||
@@ -994,7 +1008,7 @@
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -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]
|
||||
----
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-chat-memory-jdbc</artifactId>
|
||||
</dependency>
|
||||
----
|
||||
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
~ Copyright 2023-2024 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.
|
||||
-->
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-parent</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-ai-starter-model-chat-memory-jdbc</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring AI Starter - JDBC Chat Memory</name>
|
||||
<description>Spring AI JDBC Chat Memory Starter</description>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/spring-projects/spring-ai</url>
|
||||
<connection>git://github.com/spring-projects/spring-ai.git</connection>
|
||||
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-autoconfigure-model-chat-memory-jdbc</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-model-chat-memory-jdbc</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
Reference in New Issue
Block a user