Skip to content

Commit e39a8e3

Browse files
committed
Merge branch 'main' into feature/async-tool-support
2 parents bbc2e3b + 50db344 commit e39a8e3

File tree

47 files changed

+3127
-61
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+3127
-61
lines changed

auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class OpenAiAudioSpeechProperties extends OpenAiParentProperties {
3636

3737
public static final String CONFIG_PREFIX = "spring.ai.openai.audio.speech";
3838

39-
public static final String DEFAULT_SPEECH_MODEL = OpenAiAudioApi.TtsModel.TTS_1.getValue();
39+
public static final String DEFAULT_SPEECH_MODEL = OpenAiAudioApi.TtsModel.GPT_4_O_MINI_TTS.getValue();
4040

4141
private static final Float SPEED = 1.0f;
4242

auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionProperties.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class OpenAiAudioTranscriptionProperties extends OpenAiParentProperties {
2626

2727
public static final String CONFIG_PREFIX = "spring.ai.openai.audio.transcription";
2828

29-
public static final String DEFAULT_TRANSCRIPTION_MODEL = OpenAiAudioApi.WhisperModel.WHISPER_1.getValue();
29+
public static final String DEFAULT_TRANSCRIPTION_MODEL = OpenAiAudioApi.TranscriptionModels.WHISPER_1.getValue();
3030

3131
private static final Double DEFAULT_TEMPERATURE = 0.7;
3232

memory/repository/spring-ai-model-chat-memory-repository-jdbc/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@
8585
<optional>true</optional>
8686
</dependency>
8787

88+
<dependency>
89+
<groupId>org.xerial</groupId>
90+
<artifactId>sqlite-jdbc</artifactId>
91+
<scope>test</scope>
92+
<optional>true</optional>
93+
</dependency>
94+
95+
<dependency>
96+
<groupId>com.h2database</groupId>
97+
<artifactId>h2</artifactId>
98+
<scope>test</scope>
99+
<optional>true</optional>
100+
</dependency>
101+
88102
<dependency>
89103
<groupId>org.springframework.boot</groupId>
90104
<artifactId>spring-boot-starter-test</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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.chat.memory.repository.jdbc;
18+
19+
/**
20+
* H2-specific SQL dialect for chat memory repository.
21+
*
22+
* @author Yanming Zhou
23+
*/
24+
public class H2ChatMemoryRepositoryDialect implements JdbcChatMemoryRepositoryDialect {
25+
26+
@Override
27+
public String getSelectMessagesSql() {
28+
return "SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY timestamp ASC";
29+
}
30+
31+
@Override
32+
public String getInsertMessageSql() {
33+
return "INSERT INTO SPRING_AI_CHAT_MEMORY (conversation_id, content, type, timestamp) VALUES (?, ?, ?, ?)";
34+
}
35+
36+
@Override
37+
public String getDeleteMessagesSql() {
38+
return "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ?";
39+
}
40+
41+
@Override
42+
public String getSelectConversationIdsSql() {
43+
return "SELECT DISTINCT conversation_id FROM SPRING_AI_CHAT_MEMORY";
44+
}
45+
46+
}

memory/repository/spring-ai-model-chat-memory-repository-jdbc/src/main/java/org/springframework/ai/chat/memory/repository/jdbc/JdbcChatMemoryRepository.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,13 @@ public static Builder builder() {
111111
}
112112

113113
private record AddBatchPreparedStatement(String conversationId, List<Message> messages,
114-
AtomicLong instantSeq) implements BatchPreparedStatementSetter {
114+
AtomicLong sequenceId) implements BatchPreparedStatementSetter {
115115

116116
private AddBatchPreparedStatement(String conversationId, List<Message> messages) {
117-
this(conversationId, messages, new AtomicLong(Instant.now().toEpochMilli()));
117+
// Use second-level granularity to ensure compatibility with all database
118+
// timestamp precisions. The timestamp serves as a sequence number for
119+
// message ordering, not as a precise temporal record.
120+
this(conversationId, messages, new AtomicLong(Instant.now().getEpochSecond()));
118121
}
119122

120123
@Override
@@ -124,7 +127,9 @@ public void setValues(PreparedStatement ps, int i) throws SQLException {
124127
ps.setString(1, this.conversationId);
125128
ps.setString(2, message.getText());
126129
ps.setString(3, message.getMessageType().name());
127-
ps.setTimestamp(4, new Timestamp(this.instantSeq.getAndIncrement()));
130+
// Convert seconds to milliseconds for Timestamp constructor.
131+
// Each message gets a unique second value, ensuring proper ordering.
132+
ps.setTimestamp(4, new Timestamp(this.sequenceId.getAndIncrement() * 1000L));
128133
}
129134

130135
@Override

memory/repository/spring-ai-model-chat-memory-repository-jdbc/src/main/java/org/springframework/ai/chat/memory/repository/jdbc/JdbcChatMemoryRepositoryDialect.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ public interface JdbcChatMemoryRepositoryDialect {
5353
*/
5454
String getDeleteMessagesSql();
5555

56-
/**
57-
* Optionally, dialect can provide more advanced SQL as needed.
58-
*/
59-
6056
/**
6157
* Detects the dialect from the DataSource.
6258
*/
@@ -78,6 +74,8 @@ static JdbcChatMemoryRepositoryDialect from(DataSource dataSource) {
7874
case "MySQL", "MariaDB" -> new MysqlChatMemoryRepositoryDialect();
7975
case "Microsoft SQL Server" -> new SqlServerChatMemoryRepositoryDialect();
8076
case "HSQL Database Engine" -> new HsqldbChatMemoryRepositoryDialect();
77+
case "SQLite" -> new SqliteChatMemoryRepositoryDialect();
78+
case "H2" -> new H2ChatMemoryRepositoryDialect();
8179
default -> // Add more as needed
8280
new PostgresChatMemoryRepositoryDialect();
8381
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.chat.memory.repository.jdbc;
18+
19+
/**
20+
* Sqlite dialect for chat memory repository.
21+
*
22+
* @author guan xu
23+
* @since 1.1.0
24+
*/
25+
public class SqliteChatMemoryRepositoryDialect implements JdbcChatMemoryRepositoryDialect {
26+
27+
@Override
28+
public String getSelectMessagesSql() {
29+
return "SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY timestamp";
30+
}
31+
32+
@Override
33+
public String getInsertMessageSql() {
34+
return "INSERT INTO SPRING_AI_CHAT_MEMORY (conversation_id, content, type, timestamp) VALUES (?, ?, ?, ?)";
35+
}
36+
37+
@Override
38+
public String getSelectConversationIdsSql() {
39+
return "SELECT DISTINCT conversation_id FROM SPRING_AI_CHAT_MEMORY";
40+
}
41+
42+
@Override
43+
public String getDeleteMessagesSql() {
44+
return "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ?";
45+
}
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE TABLE SPRING_AI_CHAT_MEMORY (
2+
conversation_id VARCHAR(36) NOT NULL,
3+
content LONGVARCHAR NOT NULL,
4+
type VARCHAR(10) NOT NULL,
5+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
6+
);
7+
8+
CREATE INDEX SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX ON SPRING_AI_CHAT_MEMORY(conversation_id, timestamp DESC);
9+
10+
ALTER TABLE SPRING_AI_CHAT_MEMORY ADD CONSTRAINT TYPE_CHECK CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE TABLE IF NOT EXISTS SPRING_AI_CHAT_MEMORY (
2+
conversation_id TEXT NOT NULL,
3+
content TEXT NOT NULL,
4+
type TEXT NOT NULL,
5+
timestamp INTEGER NOT NULL,
6+
CHECK (type IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL'))
7+
);
8+
9+
CREATE INDEX IF NOT EXISTS SPRING_AI_CHAT_MEMORY_CONVERSATION_ID_TIMESTAMP_IDX
10+
ON SPRING_AI_CHAT_MEMORY(conversation_id, timestamp);

memory/repository/spring-ai-model-chat-memory-repository-jdbc/src/test/java/org/springframework/ai/chat/memory/repository/jdbc/AbstractJdbcChatMemoryRepositoryIT.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.ai.chat.memory.repository.jdbc;
1818

19-
import java.sql.Timestamp;
2019
import java.util.List;
2120
import java.util.UUID;
2221
import java.util.stream.Collectors;
@@ -84,7 +83,7 @@ void saveMessagesSingleMessage(String content, MessageType messageType) {
8483
assertThat(result.get("conversation_id")).isEqualTo(conversationId);
8584
assertThat(result.get("content")).isEqualTo(message.getText());
8685
assertThat(result.get("type")).isEqualTo(messageType.name());
87-
assertThat(result.get("timestamp")).isInstanceOf(Timestamp.class);
86+
assertThat(result.get("timestamp")).isNotNull();
8887
}
8988

9089
@Test
@@ -114,7 +113,7 @@ void saveMessagesMultipleMessages() {
114113
assertThat(result.get("conversation_id")).isEqualTo(conversationId);
115114
assertThat(result.get("content")).isEqualTo(message.getText());
116115
assertThat(result.get("type")).isEqualTo(message.getMessageType().name());
117-
assertThat(result.get("timestamp")).isInstanceOf(Timestamp.class);
116+
assertThat(result.get("timestamp")).isNotNull();
118117
}
119118

120119
var count = this.chatMemoryRepository.findByConversationId(conversationId).size();
@@ -186,6 +185,30 @@ void testMessageOrder() {
186185
"4-Fourth message");
187186
}
188187

188+
@Test
189+
void testMessageOrderWithLargeBatch() {
190+
var conversationId = UUID.randomUUID().toString();
191+
192+
// Create a large batch of 50 messages to ensure timestamp ordering issues
193+
// are detected. With the old millisecond-precision code, MySQL/MariaDB's
194+
// second-precision TIMESTAMP columns would truncate all timestamps to the
195+
// same value, causing random ordering. This test validates the fix.
196+
List<Message> messages = new java.util.ArrayList<>();
197+
for (int i = 0; i < 50; i++) {
198+
messages.add(new UserMessage("Message " + i));
199+
}
200+
201+
this.chatMemoryRepository.saveAll(conversationId, messages);
202+
203+
List<Message> retrievedMessages = this.chatMemoryRepository.findByConversationId(conversationId);
204+
205+
// Verify we got all messages back in the exact order they were saved
206+
assertThat(retrievedMessages).hasSize(50);
207+
for (int i = 0; i < 50; i++) {
208+
assertThat(retrievedMessages.get(i).getText()).isEqualTo("Message " + i);
209+
}
210+
}
211+
189212
/**
190213
* Base configuration for all integration tests.
191214
*/

0 commit comments

Comments
 (0)