Skip to content

Commit eb13687

Browse files
committed
feat(memory): First commit of file-based memory repository and autoconfiguration
Signed-off-by: dahlej <dahlej@gmail.com>
1 parent add7ca8 commit eb13687

File tree

16 files changed

+856
-0
lines changed

16 files changed

+856
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2023-2024 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
18+
xmlns="http://maven.apache.org/POM/4.0.0"
19+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
20+
<modelVersion>4.0.0</modelVersion>
21+
<parent>
22+
<groupId>org.springframework.ai</groupId>
23+
<artifactId>spring-ai-parent</artifactId>
24+
<version>1.0.0-SNAPSHOT</version>
25+
<relativePath>../../../../../../pom.xml</relativePath>
26+
</parent>
27+
<artifactId>spring-ai-autoconfigure-model-chat-memory-repository-file</artifactId>
28+
<packaging>jar</packaging>
29+
<name>Spring AI File Chat Memory Repository Auto Configuration</name>
30+
<description>Spring File AI Chat Memory Repository Auto Configuration</description>
31+
<url>https://github.com/spring-projects/spring-ai</url>
32+
33+
<scm>
34+
<url>https://github.com/spring-projects/spring-ai</url>
35+
<connection>git://github.com/spring-projects/spring-ai.git</connection>
36+
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
37+
</scm>
38+
39+
<dependencies>
40+
41+
<dependency>
42+
<groupId>org.springframework.ai</groupId>
43+
<artifactId>spring-ai-model-chat-memory-repository-file</artifactId>
44+
<version>${project.parent.version}</version>
45+
</dependency>
46+
47+
<dependency>
48+
<groupId>org.springframework.ai</groupId>
49+
<artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
50+
<version>${project.parent.version}</version>
51+
</dependency>
52+
53+
<!-- Boot dependencies -->
54+
<dependency>
55+
<groupId>org.springframework.boot</groupId>
56+
<artifactId>spring-boot-starter</artifactId>
57+
</dependency>
58+
59+
<dependency>
60+
<groupId>org.springframework.boot</groupId>
61+
<artifactId>spring-boot-configuration-processor</artifactId>
62+
<optional>true</optional>
63+
</dependency>
64+
65+
<dependency>
66+
<groupId>org.springframework.boot</groupId>
67+
<artifactId>spring-boot-autoconfigure-processor</artifactId>
68+
<optional>true</optional>
69+
</dependency>
70+
71+
<!-- Test dependencies -->
72+
<dependency>
73+
<groupId>org.springframework.boot</groupId>
74+
<artifactId>spring-boot-starter-test</artifactId>
75+
<scope>test</scope>
76+
</dependency>
77+
78+
<dependency>
79+
<groupId>org.testcontainers</groupId>
80+
<artifactId>junit-jupiter</artifactId>
81+
<scope>test</scope>
82+
</dependency>
83+
84+
</dependencies>
85+
86+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2025-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+
package org.springframework.ai.model.chat.memory.repository.file.autoconfigure;
17+
18+
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import org.springframework.ai.chat.memory.ChatMemoryRepository;
20+
import org.springframework.ai.chat.memory.repository.file.FileChatMemoryRepository;
21+
import org.springframework.ai.model.chat.memory.autoconfigure.ChatMemoryAutoConfiguration;
22+
import org.springframework.boot.autoconfigure.AutoConfiguration;
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
25+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
26+
import org.springframework.context.annotation.Bean;
27+
28+
import java.nio.file.Path;
29+
import java.nio.file.Paths;
30+
31+
/**
32+
* @author John Dahle
33+
*/
34+
@AutoConfiguration(before = ChatMemoryAutoConfiguration.class)
35+
@EnableConfigurationProperties(FileChatMemoryRepositoryProperties.class)
36+
@ConditionalOnProperty(prefix = FileChatMemoryRepositoryProperties.CONFIG_PREFIX, name = "enabled",
37+
havingValue = "true", matchIfMissing = true)
38+
public class FileChatMemoryRepositoryAutoConfiguration {
39+
40+
private final FileChatMemoryRepositoryProperties props;
41+
42+
public FileChatMemoryRepositoryAutoConfiguration(FileChatMemoryRepositoryProperties props) {
43+
this.props = props;
44+
}
45+
46+
@Bean
47+
@ConditionalOnMissingBean(ChatMemoryRepository.class)
48+
public FileChatMemoryRepository fileChatMemoryRepository(ObjectMapper objectMapper) {
49+
Path baseDir = Paths.get(props.getBaseDir());
50+
return new FileChatMemoryRepository(baseDir, objectMapper);
51+
}
52+
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2025-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+
package org.springframework.ai.model.chat.memory.repository.file.autoconfigure;
17+
18+
import org.springframework.boot.context.properties.ConfigurationProperties;
19+
20+
/**
21+
* @author John Dahle
22+
*/
23+
24+
@ConfigurationProperties(FileChatMemoryRepositoryProperties.CONFIG_PREFIX)
25+
public class FileChatMemoryRepositoryProperties {
26+
27+
public static final String CONFIG_PREFIX = "spring.ai.chat.memory.repository.file";
28+
private boolean enabled = true;
29+
private String baseDir = System.getProperty("user.home") + "/.spring-ai/chat-memory";
30+
31+
public boolean isEnabled() {
32+
return enabled;
33+
}
34+
35+
public void setEnabled(boolean enabled) {
36+
this.enabled = enabled;
37+
}
38+
39+
public String getBaseDir() {
40+
return baseDir;
41+
}
42+
43+
public void setBaseDir(String baseDir) {
44+
this.baseDir = baseDir;
45+
}
46+
47+
}
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.repository.file.autoconfigure.FileChatMemoryRepositoryAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Copyright 2023-2024 the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<project xmlns="http://maven.apache.org/POM/4.0.0"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
21+
<modelVersion>4.0.0</modelVersion>
22+
<parent>
23+
<groupId>org.springframework.ai</groupId>
24+
<artifactId>spring-ai-parent</artifactId>
25+
<version>1.0.0-SNAPSHOT</version>
26+
<relativePath>../../../pom.xml</relativePath>
27+
</parent>
28+
29+
<artifactId>spring-ai-model-chat-memory-repository-file</artifactId>
30+
<name>Spring File-based Chat Memory Repository</name>
31+
<description>Spring File-based Chat Memory Repository implementation</description>
32+
33+
<url>https://github.com/spring-projects/spring-ai</url>
34+
35+
<scm>
36+
<url>https://github.com/spring-projects/spring-ai</url>
37+
<connection>git://github.com/spring-projects/spring-ai.git</connection>
38+
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
39+
</scm>
40+
41+
<dependencies>
42+
<dependency>
43+
<groupId>org.springframework.ai</groupId>
44+
<artifactId>spring-ai-client-chat</artifactId>
45+
<version>${project.version}</version>
46+
</dependency>
47+
48+
<!-- TESTING -->
49+
<dependency>
50+
<groupId>org.springframework.boot</groupId>
51+
<artifactId>spring-boot-starter-test</artifactId>
52+
<scope>test</scope>
53+
</dependency>
54+
55+
<dependency>
56+
<groupId>org.springframework.ai</groupId>
57+
<artifactId>spring-ai-test</artifactId>
58+
<version>${project.version}</version>
59+
<scope>test</scope>
60+
</dependency>
61+
62+
</dependencies>
63+
64+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2025-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+
package org.springframework.ai.chat.memory.repository.file;
17+
18+
import com.fasterxml.jackson.core.type.TypeReference;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import org.springframework.ai.chat.memory.ChatMemoryRepository;
21+
import org.springframework.ai.chat.memory.repository.file.dto.MessageDto;
22+
import org.springframework.ai.chat.memory.repository.file.dto.MessageDtoMapper;
23+
import org.springframework.ai.chat.messages.Message;
24+
25+
import java.io.IOException;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import java.util.Collections;
29+
import java.util.List;
30+
import java.util.stream.Collectors;
31+
/**
32+
* @author John Dahle
33+
*/
34+
public class FileChatMemoryRepository implements ChatMemoryRepository {
35+
36+
private final Path baseDir;
37+
38+
private final ObjectMapper objectMapper;
39+
40+
public FileChatMemoryRepository(Path baseDir, ObjectMapper objectMapper) {
41+
this.baseDir = baseDir;
42+
this.objectMapper = objectMapper;
43+
try {
44+
Files.createDirectories(baseDir);
45+
}
46+
catch (IOException e) {
47+
throw new RuntimeException("Failed to create base directory: " + baseDir, e);
48+
}
49+
}
50+
51+
private Path fileFor(String conversationId) {
52+
return baseDir.resolve(conversationId + ".json");
53+
}
54+
55+
@Override
56+
public List<String> findConversationIds() {
57+
try (var stream = Files.list(baseDir)) {
58+
return stream.filter(p -> p.toString().endsWith(".json"))
59+
.map(p -> p.getFileName().toString().replaceFirst("\\.json$", ""))
60+
.collect(Collectors.toList());
61+
}
62+
catch (IOException e) {
63+
throw new RuntimeException("Failed to list conversation IDs", e);
64+
}
65+
}
66+
67+
@Override
68+
public List<Message> findByConversationId(String conversationId) {
69+
Path file = fileFor(conversationId);
70+
if (!Files.exists(file)) {
71+
return Collections.emptyList();
72+
}
73+
try {
74+
// 1. Read DTOs from disk
75+
List<MessageDto> dtos = objectMapper.readValue(file.toFile(), new TypeReference<List<MessageDto>>() {
76+
});
77+
// 2. Map them back to domain Messages
78+
return MessageDtoMapper.toDomainList(dtos);
79+
}
80+
catch (IOException e) {
81+
throw new RuntimeException("Failed to read messages for conversation: " + conversationId, e);
82+
}
83+
}
84+
85+
@Override
86+
public void saveAll(String conversationId, List<Message> messages) {
87+
try {
88+
// 1. Convert domain Messages into DTOs
89+
List<MessageDto> dtos = MessageDtoMapper.toDtoList(messages);
90+
// 2. Tell Jackson they’re DTOs and write them
91+
objectMapper.writerFor(new TypeReference<List<MessageDto>>() {
92+
}).writeValue(fileFor(conversationId).toFile(), dtos);
93+
}
94+
catch (IOException e) {
95+
throw new RuntimeException("Failed to write messages for conversation: " + conversationId, e);
96+
}
97+
}
98+
99+
@Override
100+
public void deleteByConversationId(String conversationId) {
101+
try {
102+
Files.deleteIfExists(fileFor(conversationId));
103+
}
104+
catch (IOException e) {
105+
throw new RuntimeException("Failed to delete conversation: " + conversationId, e);
106+
}
107+
}
108+
109+
}

0 commit comments

Comments
 (0)