Skip to content

Commit 4be1002

Browse files
leijendarysobychacko
authored andcommitted
feat: JDBC implementation of ChatMemory
Signed-off-by: leijendary <[email protected]> Remove references to spring-ai-core module in jdbc chat memory
1 parent ef0a202 commit 4be1002

File tree

25 files changed

+1225
-44
lines changed

25 files changed

+1225
-44
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ One way to run integration tests on part of the code is to first do a quick comp
103103
```
104104
Then run the integration test for a specific module using the `-pl` option
105105
```shell
106-
./mvnw verify -Pintegration-tests -pl spring-ai-spring-boot-testcontainers
106+
./mvnw verify -Pintegration-tests -pl spring-ai-spring-boot-testcontainers
107107
```
108108

109109
### Documentation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.ai</groupId>
8+
<artifactId>spring-ai-parent</artifactId>
9+
<version>1.0.0-SNAPSHOT</version>
10+
<relativePath>../../../../../pom.xml</relativePath>
11+
</parent>
12+
<artifactId>spring-ai-autoconfigure-model-chat-memory-jdbc</artifactId>
13+
<packaging>jar</packaging>
14+
<name>Spring AI JDBC Chat Memory Auto Configuration</name>
15+
<description>Spring JDBC AI Chat Memory Auto Configuration</description>
16+
<url>https://github.com/spring-projects/spring-ai</url>
17+
18+
<scm>
19+
<url>https://github.com/spring-projects/spring-ai</url>
20+
<connection>git://github.com/spring-projects/spring-ai.git</connection>
21+
<developerConnection>[email protected]:spring-projects/spring-ai.git</developerConnection>
22+
</scm>
23+
24+
<dependencies>
25+
26+
<dependency>
27+
<groupId>org.springframework.ai</groupId>
28+
<artifactId>spring-ai-model-chat-memory-jdbc</artifactId>
29+
<version>${project.parent.version}</version>
30+
</dependency>
31+
32+
<!-- Boot dependencies -->
33+
<dependency>
34+
<groupId>org.springframework.boot</groupId>
35+
<artifactId>spring-boot-starter</artifactId>
36+
</dependency>
37+
38+
<dependency>
39+
<groupId>org.springframework.boot</groupId>
40+
<artifactId>spring-boot-configuration-processor</artifactId>
41+
<optional>true</optional>
42+
</dependency>
43+
44+
<!-- Test dependencies -->
45+
<dependency>
46+
<groupId>org.springframework.boot</groupId>
47+
<artifactId>spring-boot-starter-test</artifactId>
48+
<scope>test</scope>
49+
</dependency>
50+
51+
<dependency>
52+
<groupId>org.postgresql</groupId>
53+
<artifactId>postgresql</artifactId>
54+
<version>${postgresql.version}</version>
55+
<scope>test</scope>
56+
</dependency>
57+
58+
<dependency>
59+
<groupId>org.testcontainers</groupId>
60+
<artifactId>junit-jupiter</artifactId>
61+
<scope>test</scope>
62+
</dependency>
63+
64+
<dependency>
65+
<groupId>org.testcontainers</groupId>
66+
<artifactId>postgresql</artifactId>
67+
<scope>test</scope>
68+
</dependency>
69+
</dependencies>
70+
71+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.chat.memory.jdbc.autoconfigure;
18+
19+
import javax.sql.DataSource;
20+
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
24+
import org.springframework.ai.chat.memory.jdbc.JdbcChatMemory;
25+
import org.springframework.ai.chat.memory.jdbc.JdbcChatMemoryConfig;
26+
import org.springframework.boot.autoconfigure.AutoConfiguration;
27+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
28+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
29+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
30+
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
31+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
32+
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.jdbc.core.JdbcTemplate;
35+
36+
/**
37+
* @author Jonathan Leijendekker
38+
* @since 1.0.0
39+
*/
40+
@AutoConfiguration(after = JdbcTemplateAutoConfiguration.class)
41+
@ConditionalOnClass({ JdbcChatMemory.class, DataSource.class, JdbcTemplate.class })
42+
@EnableConfigurationProperties(JdbcChatMemoryProperties.class)
43+
public class JdbcChatMemoryAutoConfiguration {
44+
45+
private static final Logger logger = LoggerFactory.getLogger(JdbcChatMemoryAutoConfiguration.class);
46+
47+
@Bean
48+
@ConditionalOnMissingBean
49+
public JdbcChatMemory chatMemory(JdbcTemplate jdbcTemplate) {
50+
var config = JdbcChatMemoryConfig.builder().jdbcTemplate(jdbcTemplate).build();
51+
52+
return JdbcChatMemory.create(config);
53+
}
54+
55+
@Bean
56+
@ConditionalOnMissingBean
57+
@ConditionalOnProperty(value = "spring.ai.chat.memory.jdbc.initialize-schema", havingValue = "true",
58+
matchIfMissing = true)
59+
public DataSourceScriptDatabaseInitializer jdbcChatMemoryScriptDatabaseInitializer(DataSource dataSource) {
60+
logger.debug("Initializing JdbcChatMemory schema");
61+
62+
return new JdbcChatMemoryDataSourceScriptDatabaseInitializer(dataSource);
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.springframework.ai.model.chat.memory.jdbc.autoconfigure;
2+
3+
import java.util.List;
4+
5+
import javax.sql.DataSource;
6+
7+
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
8+
import org.springframework.boot.jdbc.init.PlatformPlaceholderDatabaseDriverResolver;
9+
import org.springframework.boot.sql.init.DatabaseInitializationMode;
10+
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
11+
12+
class JdbcChatMemoryDataSourceScriptDatabaseInitializer extends DataSourceScriptDatabaseInitializer {
13+
14+
private static final String SCHEMA_LOCATION = "classpath:org/springframework/ai/chat/memory/jdbc/schema-@@platform@@.sql";
15+
16+
public JdbcChatMemoryDataSourceScriptDatabaseInitializer(DataSource dataSource) {
17+
super(dataSource, getSettings(dataSource));
18+
}
19+
20+
static DatabaseInitializationSettings getSettings(DataSource dataSource) {
21+
var settings = new DatabaseInitializationSettings();
22+
settings.setSchemaLocations(resolveSchemaLocations(dataSource));
23+
settings.setMode(DatabaseInitializationMode.ALWAYS);
24+
settings.setContinueOnError(true);
25+
26+
return settings;
27+
}
28+
29+
static List<String> resolveSchemaLocations(DataSource dataSource) {
30+
var platformResolver = new PlatformPlaceholderDatabaseDriverResolver();
31+
32+
return platformResolver.resolveAll(dataSource, SCHEMA_LOCATION);
33+
}
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.chat.memory.jdbc.autoconfigure;
18+
19+
import org.springframework.boot.context.properties.ConfigurationProperties;
20+
21+
/**
22+
* @author Jonathan Leijendekker
23+
* @since 1.0.0
24+
*/
25+
@ConfigurationProperties(JdbcChatMemoryProperties.CONFIG_PREFIX)
26+
public class JdbcChatMemoryProperties {
27+
28+
public static final String CONFIG_PREFIX = "spring.ai.chat.memory.jdbc";
29+
30+
private boolean initializeSchema = true;
31+
32+
public boolean isInitializeSchema() {
33+
return this.initializeSchema;
34+
}
35+
36+
public void setInitializeSchema(boolean initializeSchema) {
37+
this.initializeSchema = initializeSchema;
38+
}
39+
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#
2+
# Copyright 2024-2025 the original author or authors.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
org.springframework.ai.model.chat.memory.jdbc.autoconfigure.JdbcChatMemoryAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2024-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.chat.memory.jdbc.autoconfigure;
18+
19+
import java.util.List;
20+
import java.util.UUID;
21+
22+
import org.junit.jupiter.api.Test;
23+
import org.testcontainers.containers.PostgreSQLContainer;
24+
import org.testcontainers.junit.jupiter.Container;
25+
import org.testcontainers.junit.jupiter.Testcontainers;
26+
import org.testcontainers.utility.DockerImageName;
27+
28+
import org.springframework.ai.chat.memory.jdbc.JdbcChatMemory;
29+
import org.springframework.ai.chat.messages.AssistantMessage;
30+
import org.springframework.ai.chat.messages.Message;
31+
import org.springframework.ai.chat.messages.UserMessage;
32+
import org.springframework.boot.autoconfigure.AutoConfigurations;
33+
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
34+
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
35+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
38+
39+
/**
40+
* @author Jonathan Leijendekker
41+
*/
42+
@Testcontainers
43+
class JdbcChatMemoryAutoConfigurationIT {
44+
45+
static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("postgres:17");
46+
47+
@Container
48+
@SuppressWarnings("resource")
49+
static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>(DEFAULT_IMAGE_NAME)
50+
.withDatabaseName("chat_memory_auto_configuration_test")
51+
.withUsername("postgres")
52+
.withPassword("postgres");
53+
54+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
55+
.withConfiguration(AutoConfigurations.of(JdbcChatMemoryAutoConfiguration.class,
56+
JdbcTemplateAutoConfiguration.class, DataSourceAutoConfiguration.class))
57+
.withPropertyValues(String.format("spring.datasource.url=%s", postgresContainer.getJdbcUrl()),
58+
String.format("spring.datasource.username=%s", postgresContainer.getUsername()),
59+
String.format("spring.datasource.password=%s", postgresContainer.getPassword()));
60+
61+
@Test
62+
void jdbcChatMemoryScriptDatabaseInitializer_shouldBeLoaded() {
63+
this.contextRunner.withPropertyValues("spring.ai.chat.memory.jdbc.initialize-schema=true").run(context -> {
64+
assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isTrue();
65+
});
66+
}
67+
68+
@Test
69+
void jdbcChatMemoryScriptDatabaseInitializer_shouldNotBeLoaded() {
70+
this.contextRunner.withPropertyValues("spring.ai.chat.memory.jdbc.initialize-schema=false").run(context -> {
71+
assertThat(context.containsBean("jdbcChatMemoryScriptDatabaseInitializer")).isFalse();
72+
});
73+
}
74+
75+
@Test
76+
void addGetAndClear_shouldAllExecute() {
77+
this.contextRunner.withPropertyValues("spring.ai.chat.memory.jdbc.initialize-schema=true").run(context -> {
78+
var chatMemory = context.getBean(JdbcChatMemory.class);
79+
var conversationId = UUID.randomUUID().toString();
80+
var userMessage = new UserMessage("Message from the user");
81+
82+
chatMemory.add(conversationId, userMessage);
83+
84+
assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).hasSize(1);
85+
assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).isEqualTo(List.of(userMessage));
86+
87+
chatMemory.clear(conversationId);
88+
89+
assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).isEmpty();
90+
91+
var multipleMessages = List.<Message>of(new UserMessage("Message from the user 1"),
92+
new AssistantMessage("Message from the assistant 1"));
93+
94+
chatMemory.add(conversationId, multipleMessages);
95+
96+
assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).hasSize(multipleMessages.size());
97+
assertThat(chatMemory.get(conversationId, Integer.MAX_VALUE)).isEqualTo(multipleMessages);
98+
});
99+
}
100+
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.springframework.ai.model.chat.memory.jdbc.autoconfigure;
2+
3+
import javax.sql.DataSource;
4+
5+
import org.junit.jupiter.api.Test;
6+
import org.testcontainers.containers.PostgreSQLContainer;
7+
import org.testcontainers.junit.jupiter.Container;
8+
import org.testcontainers.junit.jupiter.Testcontainers;
9+
import org.testcontainers.utility.DockerImageName;
10+
11+
import org.springframework.boot.autoconfigure.AutoConfigurations;
12+
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
13+
import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
14+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
18+
/**
19+
* @author Jonathan Leijendekker
20+
*/
21+
@Testcontainers
22+
class JdbcChatMemoryDataSourceScriptDatabaseInitializerTests {
23+
24+
static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("postgres:17");
25+
26+
@Container
27+
@SuppressWarnings("resource")
28+
static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>(DEFAULT_IMAGE_NAME)
29+
.withDatabaseName("chat_memory_initializer_test")
30+
.withUsername("postgres")
31+
.withPassword("postgres");
32+
33+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
34+
.withConfiguration(AutoConfigurations.of(JdbcChatMemoryAutoConfiguration.class,
35+
JdbcTemplateAutoConfiguration.class, DataSourceAutoConfiguration.class))
36+
.withPropertyValues(String.format("spring.datasource.url=%s", postgresContainer.getJdbcUrl()),
37+
String.format("spring.datasource.username=%s", postgresContainer.getUsername()),
38+
String.format("spring.datasource.password=%s", postgresContainer.getPassword()));
39+
40+
@Test
41+
void getSettings_shouldHaveSchemaLocations() {
42+
this.contextRunner.run(context -> {
43+
var dataSource = context.getBean(DataSource.class);
44+
var settings = JdbcChatMemoryDataSourceScriptDatabaseInitializer.getSettings(dataSource);
45+
46+
assertThat(settings.getSchemaLocations())
47+
.containsOnly("classpath:org/springframework/ai/chat/memory/jdbc/schema-postgresql.sql");
48+
});
49+
}
50+
51+
}

0 commit comments

Comments
 (0)