diff --git a/memory/repository/spring-ai-model-chat-memory-repository-oracle/README.md b/memory/repository/spring-ai-model-chat-memory-repository-oracle/README.md new file mode 100644 index 0000000000..f3cee964ec --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-oracle/README.md @@ -0,0 +1,30 @@ +# Spring AI Oracle Chat Memory Repository + +[Chat Memory Documentation](https://docs.spring.io/spring-ai/reference/api/chat-memory.html#_chat_memory) + +## Test Notes + +### Use Existing Oracle Database + +If `ORACLE_DATABASE_URL` is set, tests use that database instead of Testcontainers. + +Optional credentials: + +- `ORACLE_DATABASE_USERNAME` +- `ORACLE_DATABASE_PASSWORD` + +### Rancher Desktop + Testcontainers + +With Rancher Desktop, Ryuk can fail to start unless you set: + +- `TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock` + +Example: + +```bash +TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock \ +mvn -pl memory/repository/spring-ai-model-chat-memory-repository-oracle \ + -Denforcer.skip=true \ + -Dmaven.build.cache.enabled=false \ + -Dtest=OracleChatMemoryRepositoryIT test +``` diff --git a/memory/repository/spring-ai-model-chat-memory-repository-oracle/pom.xml b/memory/repository/spring-ai-model-chat-memory-repository-oracle/pom.xml new file mode 100644 index 0000000000..8039f50242 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-oracle/pom.xml @@ -0,0 +1,66 @@ + + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 2.0.0-SNAPSHOT + ../../../pom.xml + + + spring-ai-model-chat-memory-repository-oracle + Spring AI Oracle Chat Memory + Spring AI Oracle Chat Memory implementation + + https://github.com/spring-projects/spring-ai + + + https://github.com/spring-projects/spring-ai + scm:git:git://github.com/spring-projects/spring-ai.git + scm:git:ssh://git@github.com/spring-projects/spring-ai.git + + + + + org.springframework.ai + spring-ai-model-chat-memory-repository-jdbc + ${project.version} + + + + org.springframework.boot + spring-boot-starter-jdbc + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.testcontainers + testcontainers + test + + + + org.testcontainers + testcontainers-oracle-free + test + + + + com.oracle.database.jdbc + ojdbc11 + 23.4.0.24.05 + test + true + + + + diff --git a/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/main/java/org/springframework/ai/chat/memory/repository/oracle/OracleChatMemoryRepository.java b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/main/java/org/springframework/ai/chat/memory/repository/oracle/OracleChatMemoryRepository.java new file mode 100644 index 0000000000..afa5170fa8 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/main/java/org/springframework/ai/chat/memory/repository/oracle/OracleChatMemoryRepository.java @@ -0,0 +1,162 @@ +/* + * Copyright 2023-present 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.repository.oracle; + +import java.util.List; + +import javax.sql.DataSource; + +import org.jspecify.annotations.Nullable; + +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository; +import org.springframework.ai.chat.messages.Message; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.util.Assert; + +/** + * An implementation of {@link ChatMemoryRepository} for Oracle. + * + * @since 2.0.0 + */ +public final class OracleChatMemoryRepository implements ChatMemoryRepository { + + /** Delegate repository implementation. */ + private final JdbcChatMemoryRepository delegate; + + private OracleChatMemoryRepository(final JdbcChatMemoryRepository delegateRepository) { + this.delegate = delegateRepository; + } + + @Override + public List findConversationIds() { + return this.delegate.findConversationIds(); + } + + @Override + public List findByConversationId(final String conversationId) { + return this.delegate.findByConversationId(conversationId); + } + + @Override + public void saveAll(final String conversationId, final List messages) { + this.delegate.saveAll(conversationId, messages); + } + + @Override + public void deleteByConversationId(final String conversationId) { + this.delegate.deleteByConversationId(conversationId); + } + + /** + * Create a builder for {@link OracleChatMemoryRepository}. + * @return a new builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for {@link OracleChatMemoryRepository}. + */ + public static final class Builder { + + private static final String DEFAULT_TABLE_NAME = "SPRING_AI_CHAT_MEMORY"; + + /** Data source used by the underlying JDBC repository. */ + private @Nullable DataSource dataSource; + + /** JDBC template used by the underlying JDBC repository. */ + private @Nullable JdbcTemplate jdbcTemplate; + + /** Optional transaction manager used by the repository. */ + private @Nullable PlatformTransactionManager transactionManager; + + /** Table name used by the repository. */ + private String tableName = DEFAULT_TABLE_NAME; + + private Builder() { + } + + /** + * Set the data source. + * @param dataSource data source to use + * @return this builder + */ + public Builder dataSource(final DataSource dataSource) { + this.dataSource = dataSource; + return this; + } + + /** + * Set the JDBC template. + * @param jdbcTemplate JDBC template to use + * @return this builder + */ + public Builder jdbcTemplate(final JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + return this; + } + + /** + * Set the transaction manager. + * @param transactionManager transaction manager to use + * @return this builder + */ + public Builder transactionManager(final PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + return this; + } + + /** + * Set the table name used to store chat memory. + * @param tableName table name to use + * @return this builder + */ + public Builder tableName(final String tableName) { + Assert.hasText(tableName, "tableName cannot be null or empty"); + this.tableName = tableName; + return this; + } + + /** + * Build the Oracle chat memory repository. + * @return a new Oracle chat memory repository + */ + public OracleChatMemoryRepository build() { + var dialect = new OracleRepositoryDialect(this.tableName); + var repository = JdbcChatMemoryRepository.builder().dialect(dialect); + + if (this.dataSource != null) { + repository.dataSource(this.dataSource); + } + + if (this.jdbcTemplate != null) { + repository.jdbcTemplate(this.jdbcTemplate); + } + + if (this.transactionManager != null) { + repository.transactionManager(this.transactionManager); + } + + return new OracleChatMemoryRepository(repository.build()); + } + + } + +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/main/java/org/springframework/ai/chat/memory/repository/oracle/OracleRepositoryDialect.java b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/main/java/org/springframework/ai/chat/memory/repository/oracle/OracleRepositoryDialect.java new file mode 100644 index 0000000000..5438015400 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/main/java/org/springframework/ai/chat/memory/repository/oracle/OracleRepositoryDialect.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023-present 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.repository.oracle; + +import java.util.regex.Pattern; + +import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepositoryDialect; +import org.springframework.util.Assert; + +/** + * Oracle SQL dialect for chat memory repository. + * + * @since 2.0.0 + */ +final class OracleRepositoryDialect implements JdbcChatMemoryRepositoryDialect { + + private static final Pattern TABLE_NAME_PATTERN = Pattern + .compile("^[A-Za-z][A-Za-z0-9_$#]*(\\.[A-Za-z][A-Za-z0-9_$#]*)?$"); + + private final String tableName; + + OracleRepositoryDialect(final String tableName) { + Assert.hasText(tableName, "tableName cannot be null or empty"); + Assert.isTrue(TABLE_NAME_PATTERN.matcher(tableName).matches(), + "tableName must be an Oracle identifier or schema-qualified identifier"); + this.tableName = tableName; + } + + @Override + public String getSelectMessagesSql() { + return "SELECT content, type FROM " + this.tableName + " WHERE CONVERSATION_ID = ? ORDER BY \"TIMESTAMP\""; + } + + @Override + public String getInsertMessageSql() { + return "INSERT INTO " + this.tableName + " (CONVERSATION_ID, CONTENT, TYPE, \"TIMESTAMP\") VALUES (?, ?, ?, ?)"; + } + + @Override + public String getSelectConversationIdsSql() { + return "SELECT DISTINCT conversation_id FROM " + this.tableName; + } + + @Override + public String getDeleteMessagesSql() { + return "DELETE FROM " + this.tableName + " WHERE CONVERSATION_ID = ?"; + } + +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/main/java/org/springframework/ai/chat/memory/repository/oracle/package-info.java b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/main/java/org/springframework/ai/chat/memory/repository/oracle/package-info.java new file mode 100644 index 0000000000..5ebe133259 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/main/java/org/springframework/ai/chat/memory/repository/oracle/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2023-present 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. + */ + +/** + * Oracle chat memory repository support. + */ +@NullMarked +package org.springframework.ai.chat.memory.repository.oracle; + +import org.jspecify.annotations.NullMarked; diff --git a/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/test/java/org/springframework/ai/chat/memory/repository/oracle/OracleChatMemoryRepositoryIT.java b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/test/java/org/springframework/ai/chat/memory/repository/oracle/OracleChatMemoryRepositoryIT.java new file mode 100644 index 0000000000..e6ad240063 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/test/java/org/springframework/ai/chat/memory/repository/oracle/OracleChatMemoryRepositoryIT.java @@ -0,0 +1,233 @@ +/* + * Copyright 2023-present 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.repository.oracle; + +import java.util.List; +import java.util.UUID; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.testcontainers.oracle.OracleContainer; + +import org.springframework.ai.chat.memory.ChatMemoryRepository; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.MessageType; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.JdbcTemplateAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link OracleChatMemoryRepository}. + */ +@SpringBootTest(classes = OracleChatMemoryRepositoryIT.TestConfiguration.class) +class OracleChatMemoryRepositoryIT { + + private static final String CHAT_MEMORY_TABLE = "SPRING_AI_CHAT_MEMORY_CUSTOM"; + + private static final String ORACLE_DATABASE_URL = System.getenv("ORACLE_DATABASE_URL"); + + private static final String ORACLE_DATABASE_USERNAME = System.getenv("ORACLE_DATABASE_USERNAME"); + + private static final String ORACLE_DATABASE_PASSWORD = System.getenv("ORACLE_DATABASE_PASSWORD"); + + private static final boolean USE_EXISTING_DATABASE = StringUtils.hasText(ORACLE_DATABASE_URL); + + private static final OracleContainer ORACLE_CONTAINER = USE_EXISTING_DATABASE ? null + : new OracleContainer("gvenzl/oracle-free:slim-faststart"); + + static { + if (!USE_EXISTING_DATABASE) { + ORACLE_CONTAINER.start(); + } + } + + @Autowired + private ChatMemoryRepository chatMemoryRepository; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @DynamicPropertySource + static void configureDatasourceProperties(DynamicPropertyRegistry registry) { + if (USE_EXISTING_DATABASE) { + registry.add("spring.datasource.url", () -> ORACLE_DATABASE_URL); + registry.add("spring.datasource.username", + () -> StringUtils.hasText(ORACLE_DATABASE_USERNAME) ? ORACLE_DATABASE_USERNAME : "test"); + registry.add("spring.datasource.password", + () -> StringUtils.hasText(ORACLE_DATABASE_PASSWORD) ? ORACLE_DATABASE_PASSWORD : "test"); + } + else { + registry.add("spring.datasource.url", ORACLE_CONTAINER::getJdbcUrl); + registry.add("spring.datasource.username", ORACLE_CONTAINER::getUsername); + registry.add("spring.datasource.password", ORACLE_CONTAINER::getPassword); + } + } + + @AfterAll + static void stopContainer() { + if (ORACLE_CONTAINER != null) { + ORACLE_CONTAINER.stop(); + } + } + + @BeforeEach + void resetSchema() { + this.jdbcTemplate.execute(""" + BEGIN + EXECUTE IMMEDIATE 'DROP TABLE %s'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -942 THEN + RAISE; + END IF; + END; + """.formatted(CHAT_MEMORY_TABLE)); + this.jdbcTemplate.execute(""" + CREATE TABLE %s ( + CONVERSATION_ID VARCHAR2(36 CHAR) NOT NULL, + CONTENT CLOB NOT NULL, + "TYPE" VARCHAR2(10 CHAR) NOT NULL CHECK ("TYPE" IN ('USER', 'ASSISTANT', 'SYSTEM', 'TOOL')), + "TIMESTAMP" TIMESTAMP NOT NULL + ) + """.formatted(CHAT_MEMORY_TABLE)); + this.jdbcTemplate.execute(("CREATE INDEX " + CHAT_MEMORY_TABLE + "_CONVERSATION_ID_TIMESTAMP_IDX ON " + + CHAT_MEMORY_TABLE + "(CONVERSATION_ID, \"TIMESTAMP\")")); + } + + @Test + void saveAndReadConversation() { + var conversationId = UUID.randomUUID().toString(); + var messages = List.of(new UserMessage("user message"), new AssistantMessage("assistant message"), + new SystemMessage("system message")); + + this.chatMemoryRepository.saveAll(conversationId, messages); + + assertThat(this.chatMemoryRepository.findConversationIds()).contains(conversationId); + assertThat(this.chatMemoryRepository.findByConversationId(conversationId)).isEqualTo(messages); + } + + @ParameterizedTest + @CsvSource({ "Message from assistant,ASSISTANT", "Message from user,USER", "Message from system,SYSTEM" }) + void saveSingleMessage(String content, MessageType messageType) { + 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); + case TOOL -> throw new IllegalArgumentException("TOOL message type not supported in this test"); + }; + + this.chatMemoryRepository.saveAll(conversationId, List.of(message)); + + assertThat(this.chatMemoryRepository.findConversationIds()).contains(conversationId); + assertThat(this.chatMemoryRepository.findByConversationId(conversationId)).containsExactly(message); + } + + @Test + void saveMultipleMessagesAndOverwriteConversation() { + var conversationId = UUID.randomUUID().toString(); + var messages = List.of(new AssistantMessage("assistant - " + conversationId), + new UserMessage("user - " + conversationId), new SystemMessage("system - " + conversationId)); + + this.chatMemoryRepository.saveAll(conversationId, messages); + assertThat(this.chatMemoryRepository.findByConversationId(conversationId)).containsExactlyElementsOf(messages); + + var replacement = List.of(new UserMessage("replacement")); + this.chatMemoryRepository.saveAll(conversationId, replacement); + assertThat(this.chatMemoryRepository.findByConversationId(conversationId)) + .containsExactlyElementsOf(replacement); + } + + @Test + void findConversationIdsAcrossConversations() { + var conversationA = UUID.randomUUID().toString(); + var conversationB = UUID.randomUUID().toString(); + + this.chatMemoryRepository.saveAll(conversationA, List.of(new UserMessage("a1"))); + this.chatMemoryRepository.saveAll(conversationB, List.of(new UserMessage("b1"))); + + assertThat(this.chatMemoryRepository.findConversationIds()).contains(conversationA, conversationB); + } + + @Test + void messagesAreReturnedInChronologicalOrder() { + var conversationId = UUID.randomUUID().toString(); + var messages = List.of(new UserMessage("1-first"), new AssistantMessage("2-second"), + new UserMessage("3-third"), new SystemMessage("4-fourth")); + + this.chatMemoryRepository.saveAll(conversationId, messages); + + assertThat(this.chatMemoryRepository.findByConversationId(conversationId)).containsExactlyElementsOf(messages); + } + + @Test + void messagesAreReturnedInOrderForLargeBatch() { + var conversationId = UUID.randomUUID().toString(); + List messages = new java.util.ArrayList<>(); + for (int i = 0; i < 50; i++) { + messages.add(new UserMessage("Message " + i)); + } + + this.chatMemoryRepository.saveAll(conversationId, messages); + var retrieved = this.chatMemoryRepository.findByConversationId(conversationId); + + assertThat(retrieved).hasSize(50); + for (int i = 0; i < 50; i++) { + assertThat(retrieved.get(i).getText()).isEqualTo("Message " + i); + } + } + + @Test + void deleteConversation() { + var conversationId = UUID.randomUUID().toString(); + this.chatMemoryRepository.saveAll(conversationId, List.of(new UserMessage("hello"))); + + this.chatMemoryRepository.deleteByConversationId(conversationId); + + assertThat(this.chatMemoryRepository.findByConversationId(conversationId)).isEmpty(); + } + + @SpringBootConfiguration + @ImportAutoConfiguration({ DataSourceAutoConfiguration.class, JdbcTemplateAutoConfiguration.class }) + static class TestConfiguration { + + @Bean + ChatMemoryRepository chatMemoryRepository(DataSource dataSource) { + return OracleChatMemoryRepository.builder().dataSource(dataSource).tableName(CHAT_MEMORY_TABLE).build(); + } + + } + +} diff --git a/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/test/java/org/springframework/ai/chat/memory/repository/oracle/OracleRepositoryDialectTests.java b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/test/java/org/springframework/ai/chat/memory/repository/oracle/OracleRepositoryDialectTests.java new file mode 100644 index 0000000000..8986ae19f7 --- /dev/null +++ b/memory/repository/spring-ai-model-chat-memory-repository-oracle/src/test/java/org/springframework/ai/chat/memory/repository/oracle/OracleRepositoryDialectTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023-present 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.repository.oracle; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link OracleRepositoryDialect}. + */ +class OracleRepositoryDialectTests { + + @Test + void sqlUsesConfiguredTableName() { + var dialect = new OracleRepositoryDialect("SPRING_AI_CHAT_MEMORY_CUSTOM"); + + assertThat(dialect.getSelectMessagesSql()).contains("SPRING_AI_CHAT_MEMORY_CUSTOM"); + assertThat(dialect.getInsertMessageSql()).contains("SPRING_AI_CHAT_MEMORY_CUSTOM"); + assertThat(dialect.getSelectConversationIdsSql()).contains("SPRING_AI_CHAT_MEMORY_CUSTOM"); + assertThat(dialect.getDeleteMessagesSql()).contains("SPRING_AI_CHAT_MEMORY_CUSTOM"); + } + + @Test + void acceptsSchemaQualifiedTableName() { + var dialect = new OracleRepositoryDialect("APP.SPRING_AI_CHAT_MEMORY"); + + assertThat(dialect.getSelectMessagesSql()).contains("APP.SPRING_AI_CHAT_MEMORY"); + } + + @Test + void rejectsBlankTableName() { + assertThatIllegalArgumentException().isThrownBy(() -> new OracleRepositoryDialect(" ")); + } + + @Test + void rejectsInvalidTableName() { + assertThatIllegalArgumentException().isThrownBy(() -> new OracleRepositoryDialect("bad-name")); + } + +} diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml index b4a2ae0ea9..a8ee7243d3 100644 --- a/spring-ai-bom/pom.xml +++ b/spring-ai-bom/pom.xml @@ -247,6 +247,12 @@ ${project.version} + + org.springframework.ai + spring-ai-model-chat-memory-repository-oracle + ${project.version} + + org.springframework.ai spring-ai-model-chat-memory-repository-redis