Spring Boot auto-configuration for chat memory functionality in Spring AI applications
Advanced scenarios, error handling patterns, and edge case management.
@Service
public class SafeChatMemoryService {
@Autowired
private ChatMemory chatMemory;
private static final Logger logger = LoggerFactory.getLogger(SafeChatMemoryService.class);
public boolean addMessageSafely(String conversationId, Message message) {
try {
// Validate conversation ID
if (conversationId == null) {
logger.error("Conversation ID cannot be null");
return false;
}
if (conversationId.trim().isEmpty()) {
logger.error("Conversation ID cannot be empty");
return false;
}
// Validate message
if (message == null) {
logger.error("Message cannot be null");
return false;
}
chatMemory.add(conversationId, message);
return true;
} catch (IllegalArgumentException e) {
logger.error("Validation error: {}", e.getMessage());
return false;
} catch (Exception e) {
logger.error("Unexpected error adding message", e);
return false;
}
}
}public void addMessagesBatch(String conversationId, List<Message> messages) {
if (messages == null || messages.isEmpty()) {
logger.warn("No messages to add for conversation: {}", conversationId);
return;
}
// Check for null messages in the list
if (messages.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException(
"Message list cannot contain null elements"
);
}
try {
chatMemory.add(conversationId, messages);
} catch (IllegalArgumentException e) {
logger.error("Failed to add messages batch: {}", e.getMessage());
throw e;
}
}@Service
public class ConcurrentSafeChatService {
@Autowired
private ChatMemory chatMemory;
private final ConcurrentHashMap<String, ReentrantLock> conversationLocks =
new ConcurrentHashMap<>();
public void addMessageWithLock(String conversationId, Message message) {
ReentrantLock lock = conversationLocks.computeIfAbsent(
conversationId,
k -> new ReentrantLock()
);
lock.lock();
try {
chatMemory.add(conversationId, message);
} finally {
lock.unlock();
}
}
public List<Message> getMessagesWithLock(String conversationId) {
ReentrantLock lock = conversationLocks.computeIfAbsent(
conversationId,
k -> new ReentrantLock()
);
lock.lock();
try {
// Return a defensive copy
return new ArrayList<>(chatMemory.get(conversationId));
} finally {
lock.unlock();
}
}
}@Service
public class ParallelMessageProcessor {
@Autowired
private ChatMemory chatMemory;
public void processMessagesInParallel(Map<String, String> conversationMessages) {
// ChatMemory implementations should be thread-safe
conversationMessages.entrySet().parallelStream()
.forEach(entry -> {
try {
chatMemory.add(
entry.getKey(),
new UserMessage(entry.getValue())
);
} catch (Exception e) {
logger.error("Failed to process message for {}: {}",
entry.getKey(), e.getMessage());
}
});
}
}@Service
public class ResilientChatMemoryService {
@Autowired
private ChatMemory chatMemory;
@Autowired
private ChatMemoryRepository repository;
private final RetryTemplate retryTemplate;
public ResilientChatMemoryService() {
this.retryTemplate = RetryTemplate.builder()
.maxAttempts(3)
.fixedBackoff(1000)
.retryOn(DataAccessException.class)
.build();
}
public void addMessageWithRetry(String conversationId, Message message) {
retryTemplate.execute(context -> {
chatMemory.add(conversationId, message);
return null;
});
}
public List<Message> getMessagesWithFallback(String conversationId) {
try {
return chatMemory.get(conversationId);
} catch (DataAccessException e) {
logger.error("Database error retrieving messages, returning empty list", e);
return Collections.emptyList();
}
}
public boolean isRepositoryHealthy() {
try {
repository.findConversationIds();
return true;
} catch (Exception e) {
logger.error("Repository health check failed", e);
return false;
}
}
}@Service
public class TransactionalChatService {
@Autowired
private ChatMemory chatMemory;
@Autowired
private ExternalService externalService;
@Transactional(rollbackFor = Exception.class)
public void addMessageWithExternalCall(String conversationId, Message message) {
try {
// Add message to memory
chatMemory.add(conversationId, message);
// Call external service
externalService.notifyNewMessage(conversationId, message);
} catch (Exception e) {
logger.error("Transaction failed, rolling back", e);
throw e; // Rollback transaction
}
}
@Transactional(rollbackFor = Exception.class)
public void addMessagesAtomic(String conversationId, List<Message> messages) {
// All messages added or none (atomic operation)
chatMemory.add(conversationId, messages);
}
}@Service
public class MessageWindowManager {
@Autowired
private ChatMemory chatMemory;
private final int maxMessages = 20; // Default window size
public void addMessageWithOverflowWarning(String conversationId, Message message) {
List<Message> currentMessages = chatMemory.get(conversationId);
if (currentMessages.size() >= maxMessages) {
logger.warn("Conversation {} at capacity ({} messages), oldest will be evicted",
conversationId, maxMessages);
}
chatMemory.add(conversationId, message);
}
public List<Message> getMessagesWithSystemPreservation(String conversationId) {
List<Message> messages = chatMemory.get(conversationId);
// System messages should be preserved
long systemMessageCount = messages.stream()
.filter(m -> m.getMessageType() == MessageType.SYSTEM)
.count();
if (systemMessageCount == 0) {
logger.warn("No system message found in conversation: {}", conversationId);
}
return messages;
}
}@Service
public class LargeMessageHandler {
@Autowired
private ChatMemory chatMemory;
private static final int MAX_MESSAGE_SIZE = 10000; // characters
public void addLargeMessage(String conversationId, String content) {
if (content.length() > MAX_MESSAGE_SIZE) {
logger.warn("Message exceeds size limit ({} chars), truncating",
content.length());
// Option 1: Truncate
String truncated = content.substring(0, MAX_MESSAGE_SIZE) + "... [truncated]";
chatMemory.add(conversationId, new UserMessage(truncated));
// Option 2: Split into multiple messages
// splitAndAddMessages(conversationId, content);
} else {
chatMemory.add(conversationId, new UserMessage(content));
}
}
private void splitAndAddMessages(String conversationId, String content) {
List<Message> messages = new ArrayList<>();
int start = 0;
int part = 1;
while (start < content.length()) {
int end = Math.min(start + MAX_MESSAGE_SIZE, content.length());
String chunk = content.substring(start, end);
messages.add(new UserMessage(
String.format("[Part %d] %s", part++, chunk)
));
start = end;
}
chatMemory.add(conversationId, messages);
}
}@Service
public class SystemMessageManager {
@Autowired
private ChatMemory chatMemory;
public void updateSystemMessage(String conversationId, String newSystemPrompt) {
// MessageWindowChatMemory replaces old system messages
SystemMessage newSystemMessage = new SystemMessage(newSystemPrompt);
chatMemory.add(conversationId, newSystemMessage);
// Verify only one system message exists
List<Message> messages = chatMemory.get(conversationId);
long systemMessageCount = messages.stream()
.filter(m -> m.getMessageType() == MessageType.SYSTEM)
.count();
if (systemMessageCount > 1) {
logger.warn("Multiple system messages found in conversation: {}",
conversationId);
}
}
public void ensureSystemMessage(String conversationId, String defaultPrompt) {
List<Message> messages = chatMemory.get(conversationId);
boolean hasSystemMessage = messages.stream()
.anyMatch(m -> m.getMessageType() == MessageType.SYSTEM);
if (!hasSystemMessage) {
logger.info("Adding default system message to conversation: {}",
conversationId);
chatMemory.add(conversationId, new SystemMessage(defaultPrompt));
}
}
}@Service
public class ConversationIdValidator {
private static final Pattern VALID_ID_PATTERN =
Pattern.compile("^[a-zA-Z0-9:_-]+$");
public String sanitizeConversationId(String rawId) {
if (rawId == null || rawId.trim().isEmpty()) {
throw new IllegalArgumentException("Conversation ID cannot be null or empty");
}
// Remove invalid characters
String sanitized = rawId.replaceAll("[^a-zA-Z0-9:_-]", "_");
if (!sanitized.equals(rawId)) {
logger.warn("Sanitized conversation ID from '{}' to '{}'", rawId, sanitized);
}
return sanitized;
}
public boolean isValidConversationId(String conversationId) {
return conversationId != null &&
!conversationId.trim().isEmpty() &&
VALID_ID_PATTERN.matcher(conversationId).matches();
}
}@Service
public class UniqueConversationIdGenerator {
private final AtomicLong counter = new AtomicLong(0);
public String generateUniqueId(String prefix) {
return String.format("%s:%d:%d",
prefix,
System.currentTimeMillis(),
counter.incrementAndGet()
);
}
public String generateUserConversationId(String userId) {
// Ensure uniqueness with timestamp
return String.format("user:%s:%d",
userId,
System.currentTimeMillis()
);
}
}@Service
public class MediaMessageHandler {
@Autowired
private ChatMemory chatMemory;
public void addMessageWithMedia(String conversationId, String text, byte[] imageData) {
try {
Media media = Media.builder()
.mimeType(Media.Format.IMAGE_PNG)
.data(imageData)
.name("image.png")
.build();
UserMessage message = UserMessage.builder()
.text(text)
.media(media)
.build();
chatMemory.add(conversationId, message);
} catch (Exception e) {
logger.error("Failed to add media message", e);
// Fallback: add text-only message
chatMemory.add(conversationId, new UserMessage(text));
}
}
public void handleLargeMedia(String conversationId, String text, byte[] mediaData) {
if (mediaData.length > 10_000_000) { // 10MB
logger.warn("Media size exceeds limit: {} bytes", mediaData.length);
// Store media externally and add reference
String mediaUrl = storeMediaExternally(mediaData);
chatMemory.add(conversationId,
new UserMessage(text + "\n[Media: " + mediaUrl + "]"));
} else {
addMessageWithMedia(conversationId, text, mediaData);
}
}
}@Service
public class ToolResponseHandler {
@Autowired
private ChatMemory chatMemory;
public void addToolResponse(String conversationId, String toolCallId,
String toolName, String responseData) {
try {
ToolResponseMessage.ToolResponse response =
new ToolResponseMessage.ToolResponse(toolCallId, toolName, responseData);
ToolResponseMessage message = ToolResponseMessage.builder()
.responses(List.of(response))
.build();
chatMemory.add(conversationId, message);
} catch (Exception e) {
logger.error("Failed to add tool response", e);
}
}
public void addMultipleToolResponses(String conversationId,
List<ToolResponseMessage.ToolResponse> responses) {
if (responses == null || responses.isEmpty()) {
logger.warn("No tool responses to add");
return;
}
try {
ToolResponseMessage message = ToolResponseMessage.builder()
.responses(responses)
.build();
chatMemory.add(conversationId, message);
} catch (IllegalArgumentException e) {
logger.error("Invalid tool responses: {}", e.getMessage());
}
}
}@Service
public class JdbcSchemaValidator {
@Autowired
private DataSource dataSource;
public boolean validateSchema() {
try (Connection conn = dataSource.getConnection()) {
DatabaseMetaData metaData = conn.getMetaData();
// Check if chat memory table exists
try (ResultSet rs = metaData.getTables(null, null, "chat_memory", null)) {
if (!rs.next()) {
logger.error("Chat memory table does not exist");
return false;
}
}
return true;
} catch (SQLException e) {
logger.error("Failed to validate schema", e);
return false;
}
}
public void initializeSchemaManually() {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
String createTable = """
CREATE TABLE IF NOT EXISTS chat_memory (
conversation_id VARCHAR(255) NOT NULL,
message_id VARCHAR(255) NOT NULL,
message_type VARCHAR(50) NOT NULL,
content TEXT,
metadata TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (conversation_id, message_id)
)
""";
stmt.execute(createTable);
logger.info("Schema initialized successfully");
} catch (SQLException e) {
logger.error("Failed to initialize schema", e);
throw new RuntimeException("Schema initialization failed", e);
}
}
}@Service
public class MongoHealthChecker {
@Autowired
private MongoTemplate mongoTemplate;
public boolean isMongoAvailable() {
try {
mongoTemplate.executeCommand("{ ping: 1 }");
return true;
} catch (Exception e) {
logger.error("MongoDB not available", e);
return false;
}
}
public void ensureIndexes() {
try {
IndexOperations indexOps = mongoTemplate.indexOps("chat_memory");
// Create conversation_id index
Index conversationIndex = new Index()
.on("conversationId", Sort.Direction.ASC);
indexOps.ensureIndex(conversationIndex);
logger.info("MongoDB indexes ensured");
} catch (Exception e) {
logger.error("Failed to create indexes", e);
}
}
}@Service
public class ConversationCleanupService {
@Autowired
private ChatMemoryRepository repository;
public void cleanupEmptyConversations() {
List<String> conversationIds = repository.findConversationIds();
conversationIds.forEach(id -> {
List<Message> messages = repository.findByConversationId(id);
if (messages.isEmpty()) {
logger.info("Deleting empty conversation: {}", id);
repository.deleteByConversationId(id);
}
});
}
public void cleanupOldConversations(Duration maxAge) {
List<String> conversationIds = repository.findConversationIds();
Instant cutoff = Instant.now().minus(maxAge);
conversationIds.forEach(id -> {
if (isOlderThan(id, cutoff)) {
logger.info("Deleting old conversation: {}", id);
repository.deleteByConversationId(id);
}
});
}
private boolean isOlderThan(String conversationId, Instant cutoff) {
List<Message> messages = repository.findByConversationId(conversationId);
if (messages.isEmpty()) return true;
// Check last message timestamp
Message lastMessage = messages.get(messages.size() - 1);
Object timestamp = lastMessage.getMetadata().get("timestamp");
return timestamp instanceof Instant &&
((Instant) timestamp).isBefore(cutoff);
}
}@Service
public class CircuitBreakerChatService {
@Autowired
private ChatMemory chatMemory;
private final CircuitBreaker circuitBreaker;
public CircuitBreakerChatService() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(10)
.build();
this.circuitBreaker = CircuitBreaker.of("chatMemory", config);
}
public void addMessageWithCircuitBreaker(String conversationId, Message message) {
Supplier<Void> supplier = () -> {
chatMemory.add(conversationId, message);
return null;
};
Try.ofSupplier(CircuitBreaker.decorateSupplier(circuitBreaker, supplier))
.onFailure(e -> logger.error("Circuit breaker prevented call", e));
}
}tessl i tessl/maven-org-springframework-ai--spring-ai-autoconfigure-model-chat-memory@1.1.0