diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/pom.xml b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/pom.xml
new file mode 100644
index 00000000000..4c847b4b8a5
--- /dev/null
+++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai-parent
+ 2.0.0-SNAPSHOT
+ ../../../../../../pom.xml
+
+ spring-ai-autoconfigure-model-chat-memory-repository-s3
+ jar
+ Spring AI S3 Chat Memory Repository Auto Configuration
+ Spring S3 AI Chat Memory Repository Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-model-chat-memory-repository-s3
+ ${project.parent.version}
+
+
+
+ org.springframework.ai
+ spring-ai-autoconfigure-model-chat-memory
+ ${project.parent.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure-processor
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.testcontainers
+ testcontainers-junit-jupiter
+ test
+
+
+
+ org.testcontainers
+ testcontainers-localstack
+ test
+
+
+
+
+
\ No newline at end of file
diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/S3ChatMemoryAutoConfiguration.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/S3ChatMemoryAutoConfiguration.java
new file mode 100644
index 00000000000..cb8c48e17a5
--- /dev/null
+++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/S3ChatMemoryAutoConfiguration.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2023-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.autoconfigure.chat.memory.repository.s3;
+
+import java.net.URI;
+
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3ClientBuilder;
+import software.amazon.awssdk.services.s3.model.StorageClass;
+
+import org.springframework.ai.chat.memory.ChatMemory;
+import org.springframework.ai.chat.memory.ChatMemoryRepository;
+import org.springframework.ai.chat.memory.repository.s3.S3ChatMemoryRepository;
+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.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.util.StringUtils;
+
+/**
+ * Auto-configuration for S3 chat memory repository.
+ *
+ * @author Yuriy Bezsonov
+ * @since 2.0.0
+ */
+@AutoConfiguration
+@ConditionalOnClass({ S3Client.class, ChatMemoryRepository.class })
+@EnableConfigurationProperties(S3ChatMemoryProperties.class)
+@ConditionalOnProperty(prefix = "spring.ai.chat.memory.repository.s3", name = "bucket-name")
+public class S3ChatMemoryAutoConfiguration {
+
+ /**
+ * Creates an S3Client bean if one is not already present.
+ * @param properties the S3 chat memory properties
+ * @return configured S3Client
+ */
+ @Bean
+ @ConditionalOnMissingBean
+ public S3Client s3Client(final S3ChatMemoryProperties properties) {
+ S3ClientBuilder builder = S3Client.builder();
+
+ // Set region
+ if (StringUtils.hasText(properties.getRegion())) {
+ builder.region(Region.of(properties.getRegion()));
+ }
+
+ // Support for custom endpoint (useful for S3-compatible services
+ // like MinIO)
+ String endpoint = System.getProperty("spring.ai.chat.memory.repository.s3.endpoint");
+ if (StringUtils.hasText(endpoint)) {
+ builder.endpointOverride(URI.create(endpoint));
+ }
+
+ return builder.build();
+ }
+
+ /**
+ * Creates an S3ChatMemoryRepository bean if one is not already present.
+ * @param s3Client the S3 client
+ * @param properties the S3 chat memory properties
+ * @return configured S3ChatMemoryRepository
+ */
+ @Bean
+ @ConditionalOnMissingBean({ S3ChatMemoryRepository.class, ChatMemory.class, ChatMemoryRepository.class })
+ public S3ChatMemoryRepository s3ChatMemoryRepository(final S3Client s3Client,
+ final S3ChatMemoryProperties properties) {
+ StorageClass storageClass = StorageClass.fromValue(properties.getStorageClass());
+
+ return S3ChatMemoryRepository.builder()
+ .s3Client(s3Client)
+ .bucketName(properties.getBucketName())
+ .keyPrefix(properties.getKeyPrefix())
+ .initializeBucket(properties.isInitializeBucket())
+ .storageClass(storageClass)
+ .build();
+ }
+
+}
diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/S3ChatMemoryProperties.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/S3ChatMemoryProperties.java
new file mode 100644
index 00000000000..0c6f7a555e9
--- /dev/null
+++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/S3ChatMemoryProperties.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2023-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.autoconfigure.chat.memory.repository.s3;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for S3 chat memory repository.
+ *
+ * @author Yuriy Bezsonov
+ * @since 2.0.0
+ */
+@ConfigurationProperties(prefix = "spring.ai.chat.memory.repository.s3")
+public class S3ChatMemoryProperties {
+
+ /**
+ * The name of the S3 bucket where conversation data will be stored.
+ */
+ private String bucketName;
+
+ /**
+ * The prefix to use for S3 object keys. Defaults to "chat-memory".
+ */
+ private String keyPrefix = "chat-memory";
+
+ /**
+ * The AWS region to use for S3 operations. Defaults to "us-east-1".
+ */
+ private String region = "us-east-1";
+
+ /**
+ * Whether to automatically create the S3 bucket if it doesn't exist. Defaults to
+ * false.
+ */
+ private boolean initializeBucket = false;
+
+ /**
+ * S3 storage class for conversation objects. Defaults to "STANDARD". Supported
+ * values: STANDARD, STANDARD_IA, ONEZONE_IA, REDUCED_REDUNDANCY.
+ */
+ private String storageClass = "STANDARD";
+
+ /**
+ * Gets the S3 bucket name.
+ * @return the bucket name
+ */
+ public String getBucketName() {
+ return this.bucketName;
+ }
+
+ /**
+ * Sets the S3 bucket name.
+ * @param name the bucket name to set
+ */
+ public void setBucketName(final String name) {
+ this.bucketName = name;
+ }
+
+ /**
+ * Gets the S3 key prefix.
+ * @return the key prefix
+ */
+ public String getKeyPrefix() {
+ return this.keyPrefix;
+ }
+
+ /**
+ * Sets the S3 key prefix.
+ * @param prefix the key prefix to set
+ */
+ public void setKeyPrefix(final String prefix) {
+ this.keyPrefix = prefix;
+ }
+
+ /**
+ * Gets the AWS region.
+ * @return the region
+ */
+ public String getRegion() {
+ return this.region;
+ }
+
+ /**
+ * Sets the AWS region.
+ * @param awsRegion the region to set
+ */
+ public void setRegion(final String awsRegion) {
+ this.region = awsRegion;
+ }
+
+ /**
+ * Gets whether to initialize bucket.
+ * @return true if bucket should be initialized
+ */
+ public boolean isInitializeBucket() {
+ return this.initializeBucket;
+ }
+
+ /**
+ * Sets whether to initialize bucket.
+ * @param initialize true to initialize bucket
+ */
+ public void setInitializeBucket(final boolean initialize) {
+ this.initializeBucket = initialize;
+ }
+
+ /**
+ * Gets the storage class.
+ * @return the storage class
+ */
+ public String getStorageClass() {
+ return this.storageClass;
+ }
+
+ /**
+ * Sets the storage class.
+ * @param storage the storage class to set
+ */
+ public void setStorageClass(final String storage) {
+ this.storageClass = storage;
+ }
+
+}
diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/package-info.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/package-info.java
new file mode 100644
index 00000000000..17cd8acb116
--- /dev/null
+++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2023-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.
+ */
+
+/**
+ * Auto-configuration for S3-based Spring AI chat memory repository.
+ *
+ *
+ * This package provides Spring Boot auto-configuration classes for automatically
+ * configuring S3 chat memory repository when the appropriate dependencies are present on
+ * the classpath.
+ *
+ * @author Yuriy Bezsonov
+ * @since 2.0.0
+ */
+package org.springframework.ai.autoconfigure.chat.memory.repository.s3;
diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..7216b324fa2
--- /dev/null
+++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1 @@
+org.springframework.ai.autoconfigure.chat.memory.repository.s3.S3ChatMemoryAutoConfiguration
\ No newline at end of file
diff --git a/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/test/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/S3ChatMemoryAutoConfigurationIT.java b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/test/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/S3ChatMemoryAutoConfigurationIT.java
new file mode 100644
index 00000000000..2b60123ff85
--- /dev/null
+++ b/auto-configurations/models/chat/memory/repository/spring-ai-autoconfigure-model-chat-memory-repository-s3/src/test/java/org/springframework/ai/autoconfigure/chat/memory/repository/s3/S3ChatMemoryAutoConfigurationIT.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2023-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.autoconfigure.chat.memory.repository.s3;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.localstack.LocalStackContainer;
+import org.testcontainers.utility.DockerImageName;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+
+import org.springframework.ai.chat.memory.ChatMemoryRepository;
+import org.springframework.ai.chat.memory.repository.s3.S3ChatMemoryRepository;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for S3ChatMemoryAutoConfiguration.
+ *
+ * @author Yuriy Bezsonov
+ */
+@Testcontainers
+class S3ChatMemoryAutoConfigurationIT {
+
+ @Container
+ static final LocalStackContainer localstack = initializeLocalStack();
+
+ private static LocalStackContainer initializeLocalStack() {
+ LocalStackContainer container = new LocalStackContainer(DockerImageName.parse("localstack/localstack:latest"));
+ container.withServices("s3");
+ return container;
+ }
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(S3ChatMemoryAutoConfiguration.class));
+
+ @Test
+ void autoConfigurationCreatesS3ChatMemoryRepository() {
+ this.contextRunner.withUserConfiguration(TestConfiguration.class)
+ .withPropertyValues("spring.ai.chat.memory.repository.s3.bucket-name=test-bucket")
+ .run(context -> {
+ assertThat(context).hasSingleBean(ChatMemoryRepository.class);
+ assertThat(context).hasSingleBean(S3ChatMemoryRepository.class);
+ assertThat(context.getBean(ChatMemoryRepository.class)).isInstanceOf(S3ChatMemoryRepository.class);
+ });
+ }
+
+ @Test
+ void autoConfigurationBindsProperties() {
+ this.contextRunner.withUserConfiguration(TestConfiguration.class)
+ .withPropertyValues("spring.ai.chat.memory.repository.s3.bucket-name=my-bucket",
+ "spring.ai.chat.memory.repository.s3.key-prefix=my-prefix",
+ "spring.ai.chat.memory.repository.s3.region=us-west-2")
+ .run(context -> {
+ S3ChatMemoryProperties properties = context.getBean(S3ChatMemoryProperties.class);
+ assertThat(properties.getBucketName()).isEqualTo("my-bucket");
+ assertThat(properties.getKeyPrefix()).isEqualTo("my-prefix");
+ assertThat(properties.getRegion()).isEqualTo("us-west-2");
+ });
+ }
+
+ @Test
+ void autoConfigurationUsesCustomS3Client() {
+ this.contextRunner.withUserConfiguration(CustomS3ClientConfiguration.class)
+ .withPropertyValues("spring.ai.chat.memory.repository.s3.bucket-name=test-bucket")
+ .run(context -> {
+ assertThat(context).hasSingleBean(S3Client.class);
+ assertThat(context).hasSingleBean(S3ChatMemoryRepository.class);
+
+ // Verify the repository works with custom S3Client
+ S3ChatMemoryRepository repository = context.getBean(S3ChatMemoryRepository.class);
+ List messages = List.of(UserMessage.builder().text("test").build());
+
+ // This should not throw an exception (though it may fail due to
+ // LocalStack setup)
+ assertThat(repository).isNotNull();
+ });
+ }
+
+ @Test
+ void autoConfigurationDoesNotCreateBeanWhenBucketNameMissing() {
+ this.contextRunner.withUserConfiguration(TestConfiguration.class).run(context -> {
+ assertThat(context).doesNotHaveBean(ChatMemoryRepository.class);
+ assertThat(context).doesNotHaveBean(S3ChatMemoryRepository.class);
+ });
+ }
+
+ @Test
+ void autoConfigurationBindsStorageClassProperty() {
+ this.contextRunner.withUserConfiguration(TestConfiguration.class)
+ .withPropertyValues("spring.ai.chat.memory.repository.s3.bucket-name=test-bucket",
+ "spring.ai.chat.memory.repository.s3.storage-class=STANDARD_IA")
+ .run(context -> {
+ S3ChatMemoryProperties properties = context.getBean(S3ChatMemoryProperties.class);
+ assertThat(properties.getStorageClass()).isEqualTo("STANDARD_IA");
+ });
+ }
+
+ @Test
+ void autoConfigurationUsesDefaultStorageClass() {
+ this.contextRunner.withUserConfiguration(TestConfiguration.class)
+ .withPropertyValues("spring.ai.chat.memory.repository.s3.bucket-name=test-bucket")
+ .run(context -> {
+ S3ChatMemoryProperties properties = context.getBean(S3ChatMemoryProperties.class);
+ assertThat(properties.getStorageClass()).isEqualTo("STANDARD"); // Default
+ // value
+ });
+ }
+
+ @Configuration
+ static class TestConfiguration {
+
+ // Empty configuration for basic tests
+
+ }
+
+ @Configuration
+ static class CustomS3ClientConfiguration {
+
+ @Bean
+ S3Client customS3Client() {
+ return S3Client.builder()
+ .endpointOverride(localstack.getEndpoint())
+ .credentialsProvider(StaticCredentialsProvider
+ .create(AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey())))
+ .region(Region.of(localstack.getRegion()))
+ .build();
+ }
+
+ }
+
+}
diff --git a/memory/repository/spring-ai-model-chat-memory-repository-s3/.jqwik-database b/memory/repository/spring-ai-model-chat-memory-repository-s3/.jqwik-database
new file mode 100644
index 00000000000..711006c3d3b
Binary files /dev/null and b/memory/repository/spring-ai-model-chat-memory-repository-s3/.jqwik-database differ
diff --git a/memory/repository/spring-ai-model-chat-memory-repository-s3/README.md b/memory/repository/spring-ai-model-chat-memory-repository-s3/README.md
new file mode 100644
index 00000000000..24985cd2638
--- /dev/null
+++ b/memory/repository/spring-ai-model-chat-memory-repository-s3/README.md
@@ -0,0 +1,413 @@
+# Spring AI S3 Chat Memory Repository
+
+A Spring AI implementation of `ChatMemoryRepository` that stores conversation history in Amazon S3, providing scalable and cost-effective chat memory persistence for AI applications.
+
+## Features
+
+- **Scalable Storage**: Leverages Amazon S3 for virtually unlimited conversation storage
+- **Cost-Effective**: Multiple S3 storage classes for cost optimization
+- **JSON Serialization**: Rich metadata preservation with Jackson JSON serialization
+- **Automatic Bucket Management**: Optional bucket creation and initialization
+- **Pagination Support**: Efficient handling of large conversation datasets
+- **Spring Boot Integration**: Full auto-configuration support
+- **S3-Compatible Services**: Works with MinIO and other S3-compatible storage
+
+## Quick Start
+
+### Dependencies
+
+Add the S3 Chat Memory Repository dependency to your project:
+
+```xml
+
+
+ org.springframework.ai
+ spring-ai-starter-model-chat-memory-repository-s3
+
+```
+
+### Basic Configuration
+
+Configure your S3 chat memory repository in `application.properties`:
+
+```properties
+# Required: S3 bucket name
+spring.ai.chat.memory.repository.s3.bucket-name=my-chat-memory-bucket
+
+# Optional: AWS region (defaults to us-east-1)
+spring.ai.chat.memory.repository.s3.region=us-west-2
+
+# Optional: S3 key prefix (defaults to "chat-memory")
+spring.ai.chat.memory.repository.s3.key-prefix=conversations
+
+# Optional: Auto-create bucket if it doesn't exist (defaults to false)
+spring.ai.chat.memory.repository.s3.initialize-bucket=true
+
+# Optional: S3 storage class (defaults to STANDARD)
+spring.ai.chat.memory.repository.s3.storage-class=STANDARD_IA
+```
+
+### AWS Credentials
+
+Configure AWS credentials using one of the standard methods:
+
+1. **Environment Variables**:
+ ```bash
+ export AWS_ACCESS_KEY_ID=your-access-key
+ export AWS_SECRET_ACCESS_KEY=your-secret-key
+ ```
+
+2. **AWS Credentials File** (`~/.aws/credentials`):
+ ```ini
+ [default]
+ aws_access_key_id = your-access-key
+ aws_secret_access_key = your-secret-key
+ ```
+
+3. **IAM Roles** (recommended for EC2/ECS/Lambda deployments)
+
+## Usage Examples
+
+### Basic Usage with Auto-Configuration
+
+```java
+@RestController
+public class ChatController {
+
+ private final ChatClient chatClient;
+
+ public ChatController(ChatClient.Builder chatClientBuilder,
+ ChatMemoryRepository chatMemoryRepository) {
+ this.chatClient = chatClientBuilder
+ .defaultAdvisors(new MessageChatMemoryAdvisor(
+ new MessageWindowChatMemory(chatMemoryRepository, 10)))
+ .build();
+ }
+
+ @PostMapping("/chat")
+ public String chat(@RequestParam String message,
+ @RequestParam String conversationId) {
+ return chatClient.prompt()
+ .user(message)
+ .advisors(advisorSpec -> advisorSpec
+ .param(CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId))
+ .call()
+ .content();
+ }
+}
+```
+
+### Manual Configuration
+
+```java
+@Configuration
+public class ChatMemoryConfig {
+
+ @Bean
+ public S3Client s3Client() {
+ return S3Client.builder()
+ .region(Region.US_WEST_2)
+ .build();
+ }
+
+ @Bean
+ public S3ChatMemoryRepository chatMemoryRepository(S3Client s3Client) {
+ return S3ChatMemoryRepository.builder()
+ .s3Client(s3Client)
+ .bucketName("my-chat-conversations")
+ .keyPrefix("chat-memory")
+ .initializeBucket(true)
+ .storageClass(StorageClass.STANDARD_IA)
+ .build();
+ }
+
+ @Bean
+ public ChatMemory chatMemory(S3ChatMemoryRepository repository) {
+ return new MessageWindowChatMemory(repository, 20);
+ }
+}
+```
+
+### Custom S3 Endpoint (MinIO/LocalStack)
+
+```java
+@Bean
+public S3Client s3Client() {
+ return S3Client.builder()
+ .endpointOverride(URI.create("http://localhost:9000")) // MinIO endpoint
+ .region(Region.US_EAST_1)
+ .credentialsProvider(StaticCredentialsProvider.create(
+ AwsBasicCredentials.create("minioadmin", "minioadmin")))
+ .forcePathStyle(true) // Required for MinIO
+ .build();
+}
+```
+
+### Advanced Usage with Custom Configuration
+
+```java
+@Service
+public class ConversationService {
+
+ private final S3ChatMemoryRepository repository;
+
+ public ConversationService(S3ChatMemoryRepository repository) {
+ this.repository = repository;
+ }
+
+ public List getAllConversations() {
+ return repository.findConversationIds();
+ }
+
+ public List getConversationHistory(String conversationId) {
+ return repository.findByConversationId(conversationId);
+ }
+
+ public void archiveConversation(String conversationId) {
+ // Get messages and save to archive location
+ List messages = repository.findByConversationId(conversationId);
+ // ... archive logic ...
+
+ // Delete from active storage
+ repository.deleteByConversationId(conversationId);
+ }
+}
+```
+
+## Configuration Properties
+
+| Property | Description | Default | Required |
+|----------|-------------|---------|----------|
+| `spring.ai.chat.memory.repository.s3.bucket-name` | S3 bucket name for storing conversations | - | ✅ |
+| `spring.ai.chat.memory.repository.s3.region` | AWS region | `us-east-1` | ❌ |
+| `spring.ai.chat.memory.repository.s3.key-prefix` | S3 key prefix for conversation objects | `chat-memory` | ❌ |
+| `spring.ai.chat.memory.repository.s3.initialize-bucket` | Auto-create bucket if it doesn't exist | `false` | ❌ |
+| `spring.ai.chat.memory.repository.s3.storage-class` | S3 storage class | `STANDARD` | ❌ |
+
+**Note**: Message windowing (limiting the number of messages per conversation) is handled by `MessageWindowChatMemory`, not by the repository itself. This follows the standard Spring AI pattern where repositories handle storage and ChatMemory implementations handle business logic like windowing.
+
+### Supported Storage Classes
+
+- `STANDARD` - General purpose storage (default)
+- `STANDARD_IA` - Infrequent access storage (lower cost)
+- `ONEZONE_IA` - Single AZ infrequent access
+- `REDUCED_REDUNDANCY` - Reduced redundancy storage
+
+## S3-Specific Considerations
+
+### Eventual Consistency
+
+Amazon S3 provides **strong read-after-write consistency** for new objects and **strong consistency** for overwrite PUTS and DELETES. However, be aware of these characteristics:
+
+- **New conversations**: Immediately readable after creation
+- **Updated conversations**: Immediately readable after update
+- **Deleted conversations**: Immediately consistent after deletion
+- **Conversation listing**: May have slight delays in very high-throughput scenarios
+
+### Performance Optimization
+
+1. **Key Design**: The repository uses a flat key structure (`{prefix}/{conversationId}.json`) for optimal performance
+2. **Batch Operations**: Each conversation is stored as a single JSON document for atomic updates
+3. **Pagination**: Large conversation lists are automatically paginated using S3's native pagination
+
+### Cost Optimization
+
+```properties
+# Use Standard-IA for conversations older than 30 days
+spring.ai.chat.memory.repository.s3.storage-class=STANDARD_IA
+
+# Consider lifecycle policies for long-term archival
+```
+
+Example S3 Lifecycle Policy:
+```json
+{
+ "Rules": [
+ {
+ "ID": "ChatMemoryLifecycle",
+ "Status": "Enabled",
+ "Filter": {
+ "Prefix": "chat-memory/"
+ },
+ "Transitions": [
+ {
+ "Days": 30,
+ "StorageClass": "STANDARD_IA"
+ },
+ {
+ "Days": 90,
+ "StorageClass": "GLACIER"
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Security Best Practices
+
+1. **IAM Permissions**: Use minimal required permissions
+ ```json
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:GetObject",
+ "s3:PutObject",
+ "s3:DeleteObject",
+ "s3:ListBucket"
+ ],
+ "Resource": [
+ "arn:aws:s3:::your-chat-bucket",
+ "arn:aws:s3:::your-chat-bucket/*"
+ ]
+ }
+ ]
+ }
+ ```
+
+2. **Bucket Encryption**: Enable server-side encryption
+ ```properties
+ # S3 bucket should have default encryption enabled
+ ```
+
+3. **Access Logging**: Enable S3 access logging for audit trails
+
+### Monitoring and Observability
+
+The repository integrates with Spring Boot's observability features:
+
+```properties
+# Enable metrics
+management.metrics.export.cloudwatch.enabled=true
+
+# Enable tracing
+management.tracing.enabled=true
+```
+
+Monitor these key metrics:
+- S3 request latency
+- Error rates (4xx/5xx responses)
+- Storage usage and costs
+- Conversation access patterns
+
+## Error Handling
+
+The repository handles common S3 scenarios gracefully:
+
+- **Bucket doesn't exist**: Creates bucket if `initialize-bucket=true`, otherwise throws `IllegalStateException`
+- **Network issues**: Retries with exponential backoff (AWS SDK default)
+- **Access denied**: Throws `IllegalStateException` with clear error message
+- **Invalid conversation ID**: Normalizes to "default" conversation
+- **Malformed JSON**: Throws `IllegalStateException` during deserialization
+
+## Testing
+
+### Integration Testing with LocalStack
+
+```java
+@Testcontainers
+class S3ChatMemoryRepositoryIT {
+
+ @Container
+ static final LocalStackContainer localstack =
+ new LocalStackContainer(DockerImageName.parse("localstack/localstack:latest"))
+ .withServices("s3");
+
+ @Test
+ void testConversationStorage() {
+ S3Client s3Client = S3Client.builder()
+ .endpointOverride(localstack.getEndpoint())
+ .credentialsProvider(StaticCredentialsProvider.create(
+ AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey())))
+ .region(Region.of(localstack.getRegion()))
+ .forcePathStyle(true)
+ .build();
+
+ S3ChatMemoryRepository repository = S3ChatMemoryRepository.builder()
+ .s3Client(s3Client)
+ .bucketName("test-bucket")
+ .build();
+
+ // Test conversation operations...
+ }
+}
+```
+
+### Property-Based Testing
+
+The repository includes comprehensive property-based tests using jqwik:
+
+```java
+@Property(tries = 100)
+void conversationRoundTrip(@ForAll("conversationIds") String conversationId,
+ @ForAll("messageLists") List messages) {
+ repository.saveAll(conversationId, messages);
+ List retrieved = repository.findByConversationId(conversationId);
+ assertThat(retrieved).isEqualTo(messages);
+}
+```
+
+## Migration from Other Repositories
+
+### From JDBC Chat Memory Repository
+
+```java
+// 1. Export existing conversations
+List conversationIds = jdbcRepository.findConversationIds();
+Map> conversations = new HashMap<>();
+for (String id : conversationIds) {
+ conversations.put(id, jdbcRepository.findByConversationId(id));
+}
+
+// 2. Import to S3 repository
+for (Map.Entry> entry : conversations.entrySet()) {
+ s3Repository.saveAll(entry.getKey(), entry.getValue());
+}
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **"Bucket does not exist" error**
+ - Set `spring.ai.chat.memory.repository.s3.initialize-bucket=true`
+ - Or create the bucket manually in AWS Console
+
+2. **"Access Denied" errors**
+ - Verify AWS credentials are configured
+ - Check IAM permissions for the bucket
+ - Ensure bucket policy allows access
+
+3. **Slow performance**
+ - Check AWS region configuration (use closest region)
+ - Consider using VPC endpoints for EC2 deployments
+ - Monitor S3 request metrics
+
+4. **High costs**
+ - Use appropriate storage class (`STANDARD_IA` for infrequent access)
+ - Implement lifecycle policies for archival
+ - Monitor storage usage patterns
+
+### Debug Logging
+
+Enable debug logging to troubleshoot issues:
+
+```properties
+logging.level.org.springframework.ai.chat.memory.repository.s3=DEBUG
+logging.level.software.amazon.awssdk.services.s3=DEBUG
+```
+
+## Documentation
+
+For more information about Spring AI Chat Memory, see the [official documentation](https://docs.spring.io/spring-ai/reference/api/chatmemory.html).
+
+## Contributing
+
+Contributions are welcome! Please read the [contribution guidelines](../../../../../CONTRIBUTING.adoc) before submitting pull requests.
+
+## License
+
+This project is licensed under the Apache License 2.0 - see the [LICENSE](../../../../../LICENSE.txt) file for details.
\ No newline at end of file
diff --git a/memory/repository/spring-ai-model-chat-memory-repository-s3/pom.xml b/memory/repository/spring-ai-model-chat-memory-repository-s3/pom.xml
new file mode 100644
index 00000000000..ac8a75768cd
--- /dev/null
+++ b/memory/repository/spring-ai-model-chat-memory-repository-s3/pom.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai-parent
+ 2.0.0-SNAPSHOT
+ ../../../pom.xml
+
+
+ spring-ai-model-chat-memory-repository-s3
+ Spring AI S3 Chat Memory
+ Spring AI S3 Chat Memory implementation
+
+ https://github.com/spring-projects/spring-ai
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ -XDaddTypeAnnotationsToSymbol=true
+
+
+
+
+
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-model
+ ${project.version}
+
+
+
+
+ software.amazon.awssdk
+ s3
+ ${awssdk.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+
+ org.testcontainers
+ testcontainers
+ test
+
+
+
+ org.testcontainers
+ testcontainers-localstack
+ test
+
+
+
+ org.testcontainers
+ testcontainers-junit-jupiter
+ test
+
+
+
+
+ net.jqwik
+ jqwik
+ 1.9.1
+ test
+
+
+
diff --git a/memory/repository/spring-ai-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/chat/memory/repository/s3/S3ChatMemoryConfig.java b/memory/repository/spring-ai-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/chat/memory/repository/s3/S3ChatMemoryConfig.java
new file mode 100644
index 00000000000..9c5c1041c14
--- /dev/null
+++ b/memory/repository/spring-ai-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/chat/memory/repository/s3/S3ChatMemoryConfig.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2023-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.repository.s3;
+
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.StorageClass;
+
+import org.springframework.util.Assert;
+
+/**
+ * Configuration class for S3ChatMemoryRepository.
+ *
+ * @author Yuriy Bezsonov
+ * @since 2.0.0
+ */
+public final class S3ChatMemoryConfig {
+
+ /** Default key prefix for S3 objects. */
+ public static final String DEFAULT_KEY_PREFIX = "chat-memory";
+
+ /** Default storage class for S3 objects. */
+ public static final StorageClass DEFAULT_STORAGE_CLASS = StorageClass.STANDARD;
+
+ /** The S3 client for operations. */
+ private final S3Client s3Client;
+
+ /** The S3 bucket name. */
+ private final String bucketName;
+
+ /** The key prefix for S3 objects. */
+ private final String keyPrefix;
+
+ /** Whether to initialize the bucket if it doesn't exist. */
+ private final boolean initializeBucket;
+
+ /** The storage class for S3 objects. */
+ private final StorageClass storageClass;
+
+ private S3ChatMemoryConfig(final Builder builder) {
+ Assert.notNull(builder.s3Client, "s3Client cannot be null");
+ Assert.hasText(builder.bucketName, "bucketName cannot be null or empty");
+
+ this.s3Client = builder.s3Client;
+ this.bucketName = builder.bucketName;
+ this.keyPrefix = builder.keyPrefix != null ? builder.keyPrefix : DEFAULT_KEY_PREFIX;
+ this.initializeBucket = builder.initializeBucket;
+
+ this.storageClass = builder.storageClass != null ? builder.storageClass : DEFAULT_STORAGE_CLASS;
+ }
+
+ /**
+ * Gets the S3 client.
+ * @return the S3 client
+ */
+ public S3Client getS3Client() {
+ return this.s3Client;
+ }
+
+ /**
+ * Gets the bucket name.
+ * @return the bucket name
+ */
+ public String getBucketName() {
+ return this.bucketName;
+ }
+
+ /**
+ * Gets the key prefix.
+ * @return the key prefix
+ */
+ public String getKeyPrefix() {
+ return this.keyPrefix;
+ }
+
+ /**
+ * Checks if bucket initialization is enabled.
+ * @return true if bucket should be initialized
+ */
+ public boolean isInitializeBucket() {
+ return this.initializeBucket;
+ }
+
+ /**
+ * Gets the storage class.
+ * @return the storage class
+ */
+ public StorageClass getStorageClass() {
+ return this.storageClass;
+ }
+
+ /**
+ * Creates a new builder.
+ * @return a new builder instance
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for S3ChatMemoryConfig.
+ */
+ public static final class Builder {
+
+ /** The S3 client. */
+ private S3Client s3Client;
+
+ /** The bucket name. */
+ private String bucketName;
+
+ /** The key prefix. */
+ private String keyPrefix;
+
+ /** Whether to initialize bucket. */
+ private boolean initializeBucket = false;
+
+ /** The storage class. */
+ private StorageClass storageClass;
+
+ /**
+ * Private constructor.
+ */
+ private Builder() {
+ }
+
+ /**
+ * Sets the S3 client.
+ * @param client the S3 client
+ * @return this builder
+ */
+ public Builder s3Client(final S3Client client) {
+ this.s3Client = client;
+ return this;
+ }
+
+ /**
+ * Sets the bucket name.
+ * @param name the bucket name
+ * @return this builder
+ */
+ public Builder bucketName(final String name) {
+ this.bucketName = name;
+ return this;
+ }
+
+ /**
+ * Sets the key prefix.
+ * @param prefix the key prefix
+ * @return this builder
+ */
+ public Builder keyPrefix(final String prefix) {
+ this.keyPrefix = prefix;
+ return this;
+ }
+
+ /**
+ * Sets whether to initialize bucket.
+ * @param initialize true to initialize bucket
+ * @return this builder
+ */
+ public Builder initializeBucket(final boolean initialize) {
+ this.initializeBucket = initialize;
+ return this;
+ }
+
+ /**
+ * Sets the storage class.
+ * @param storage the storage class
+ * @return this builder
+ */
+ public Builder storageClass(final StorageClass storage) {
+ this.storageClass = storage;
+ return this;
+ }
+
+ /**
+ * Builds the configuration.
+ * @return the S3ChatMemoryConfig instance
+ */
+ public S3ChatMemoryConfig build() {
+ return new S3ChatMemoryConfig(this);
+ }
+
+ }
+
+}
diff --git a/memory/repository/spring-ai-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/chat/memory/repository/s3/S3ChatMemoryRepository.java b/memory/repository/spring-ai-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/chat/memory/repository/s3/S3ChatMemoryRepository.java
new file mode 100644
index 00000000000..440d5ccafcc
--- /dev/null
+++ b/memory/repository/spring-ai-model-chat-memory-repository-s3/src/main/java/org/springframework/ai/chat/memory/repository/s3/S3ChatMemoryRepository.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright 2023-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.repository.s3;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.GetObjectResponse;
+import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
+import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
+import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
+import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+import software.amazon.awssdk.services.s3.model.S3Object;
+
+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.ToolResponseMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.util.Assert;
+
+/**
+ * An implementation of {@link ChatMemoryRepository} for Amazon S3 using a simple JSON
+ * format for storing conversation messages.
+ *
+ * @author Yuriy Bezsonov
+ * @since 2.0.0
+ */
+public final class S3ChatMemoryRepository implements ChatMemoryRepository {
+
+ /** JSON file extension for stored conversations. */
+ private static final String JSON_EXTENSION = ".json";
+
+ /** The S3 client for operations. */
+ private final S3Client s3Client;
+
+ /** The S3 configuration. */
+ private final S3ChatMemoryConfig config;
+
+ /** JSON object mapper for serialization. */
+ private final ObjectMapper objectMapper;
+
+ /**
+ * Creates a new S3ChatMemoryRepository.
+ * @param configuration the S3 configuration
+ */
+ public S3ChatMemoryRepository(final S3ChatMemoryConfig configuration) {
+ Assert.notNull(configuration, "config cannot be null");
+
+ this.s3Client = configuration.getS3Client();
+ this.config = configuration;
+ this.objectMapper = new ObjectMapper();
+ }
+
+ /**
+ * Ensures the S3 bucket exists, creating it if necessary.
+ */
+ private void ensureBucketExists() {
+ try {
+ this.s3Client.headBucket(HeadBucketRequest.builder().bucket(this.config.getBucketName()).build());
+ }
+ catch (NoSuchBucketException e) {
+ if (this.config.isInitializeBucket()) {
+ try {
+ this.s3Client
+ .createBucket(CreateBucketRequest.builder().bucket(this.config.getBucketName()).build());
+ }
+ catch (S3Exception createException) {
+ throw new IllegalStateException("Failed to create S3 bucket '" + this.config.getBucketName() + "': "
+ + createException.getMessage(), createException);
+ }
+ }
+ else {
+ throw new IllegalStateException("S3 bucket '" + this.config.getBucketName() + "' does not exist. "
+ + "Create the bucket manually or set " + "spring.ai.chat.memory.repository.s3."
+ + "initialize-bucket=true");
+ }
+ }
+ catch (S3Exception e) {
+ throw new IllegalStateException(
+ "Failed to check S3 bucket '" + this.config.getBucketName() + "': " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Normalizes conversation ID, using default if null or empty.
+ * @param conversationId the conversation ID to normalize
+ * @return the normalized conversation ID
+ */
+ private String normalizeConversationId(final String conversationId) {
+ return (conversationId == null || conversationId.trim().isEmpty()) ? "default" : conversationId.trim();
+ }
+
+ /**
+ * Generates S3 key for conversation. Pattern: {prefix}/{conversationId}.json
+ * @param conversationId the conversation ID
+ * @param prefix the key prefix
+ * @return the S3 key
+ */
+ private String generateKey(final String conversationId, final String prefix) {
+ String normalizedConversationId = normalizeConversationId(conversationId);
+ Assert.hasText(prefix, "prefix cannot be null or empty");
+
+ String normalizedPrefix = prefix.endsWith("/") ? prefix.substring(0, prefix.length() - 1) : prefix;
+
+ return normalizedPrefix + "/" + normalizedConversationId + JSON_EXTENSION;
+ }
+
+ /**
+ * Extracts conversation ID from S3 key.
+ * @param key the S3 key
+ * @param prefix the key prefix
+ * @return the conversation ID or null if invalid
+ */
+ private String extractConversationId(final String key, final String prefix) {
+ Assert.hasText(key, "key cannot be null or empty");
+ Assert.hasText(prefix, "prefix cannot be null or empty");
+
+ String normalizedPrefix = prefix.endsWith("/") ? prefix.substring(0, prefix.length() - 1) : prefix;
+
+ if (!key.startsWith(normalizedPrefix + "/") || !key.endsWith(JSON_EXTENSION)) {
+ return null;
+ }
+
+ int startIndex = normalizedPrefix.length() + 1;
+ int endIndex = key.length() - JSON_EXTENSION.length();
+
+ if (startIndex >= endIndex) {
+ return null;
+ }
+
+ String conversationId = key.substring(startIndex, endIndex);
+
+ if (conversationId.contains("/")) {
+ return null;
+ }
+
+ return conversationId.isEmpty() ? null : conversationId;
+ }
+
+ /**
+ * Serializes conversation messages to JSON.
+ * @param conversationId the conversation ID
+ * @param messages the messages to serialize
+ * @return the JSON string
+ */
+ private String serialize(final String conversationId, final List messages) {
+ try {
+ Map payload = new HashMap<>();
+ payload.put("conversationId", conversationId);
+
+ List