Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions docs/INTELLIGENT_MEMORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Intelligent Memory Management

## Overview

The `add` method in Mem4j now features intelligent memory management that automatically decides whether to INSERT, UPDATE, DELETE, or SKIP memories based on their similarity to existing memories and LLM-driven analysis.

## How It Works

When adding new memories, the system:

1. **Extracts memories** from conversations using LLM
2. **Searches for similar existing memories** in the vector database (similarity threshold: 0.7)
3. **Makes intelligent decisions** based on similarity scores:
- **Score > 0.95**: SKIP (near-duplicate)
- **Score 0.85-0.95**: Consult LLM to decide UPDATE/DELETE/SKIP/INSERT
- **Score 0.7-0.85**: INSERT as separate memory
- **No similar memories**: INSERT new memory

## Decision Logic

### INSERT
- New memory is sufficiently different from existing memories
- Adds complementary information that should be kept separate
- Default action when no similar memories exist

### UPDATE
- New memory contains more recent or accurate information
- LLM merges the new and existing memory content
- Preserves the memory ID and updates the content
- Example: "User likes coffee" → "User prefers tea over coffee"

### DELETE
- New memory contradicts or makes existing memory obsolete
- Old information is removed completely
- Example: "User is a developer" → "User is no longer a developer"

### SKIP
- New memory is essentially a duplicate
- No new information would be added
- Avoids redundancy in the memory store

## LLM Decision Prompt

The system uses a structured prompt to ask the LLM to analyze memory pairs:

```
You are a memory management system. Compare these two memories and decide what action to take:

EXISTING MEMORY: [existing content]
NEW MEMORY: [new content]

Analyze and decide:
1. UPDATE: [merged content] - if new memory contains updated information
2. DELETE - if new memory makes existing memory obsolete
3. SKIP - if new memory adds no value
4. INSERT - if they are complementary but distinct

Consider:
- Temporal context (newer information may supersede older)
- Specificity (more specific information may update general information)
- Contradictions (direct contradictions should trigger DELETE + INSERT)
- Redundancy (avoid storing the same information twice)
```

## Benefits

1. **Reduces redundancy**: Avoids storing duplicate or near-duplicate information
2. **Keeps information current**: Updates memories when newer information arrives
3. **Maintains coherence**: Removes contradictory or obsolete memories
4. **Saves storage**: Only stores valuable, distinct memories
5. **Improves search quality**: Cleaner memory store leads to better retrieval

## Usage Example

```java
Memory memory = new Memory(config, vectorStore, llmService, embeddingService);

// Initial conversation
List<Message> messages1 = Arrays.asList(
new Message("user", "I love drinking coffee"),
new Message("assistant", "I'll remember that!")
);
memory.add(messages1, userId);
// Result: 1 inserted

// Similar conversation (will skip)
List<Message> messages2 = Arrays.asList(
new Message("user", "I really enjoy coffee"),
new Message("assistant", "Yes, you mentioned that!")
);
memory.add(messages2, userId);
// Result: 1 skipped (duplicate)

// Updated preference (will update)
List<Message> messages3 = Arrays.asList(
new Message("user", "I've switched to tea instead of coffee"),
new Message("assistant", "I'll update that!")
);
memory.add(messages3, userId);
// Result: 1 updated (coffee → tea)
```

## Configuration

The intelligent decision-making uses the standard similarity threshold from the configuration:

```yaml
mem4j:
similarity-threshold: 0.7 # Used for finding similar memories
```

## Performance Considerations

- **Search overhead**: Each new memory triggers a similarity search (5 results max)
- **LLM calls**: Only for memories with similarity 0.85-0.95 (most cases use rule-based decisions)
- **Fallback behavior**: If LLM fails, defaults to INSERT to avoid data loss

## Monitoring

The system logs detailed information about memory operations:

```
INFO: Memory operations for user john: 2 inserted, 1 updated, 0 deleted, 1 skipped (total extracted: 4)
DEBUG: Inserting new memory: 'User lives in Seattle' - Reason: No similar memories found
DEBUG: Updating existing memory 'User likes coffee' -> 'User prefers tea' - Reason: LLM decided to merge
DEBUG: Skipping memory: 'User enjoys coffee' - Reason: Memory is nearly identical (score: 0.96)
```

## Testing

Comprehensive test coverage includes:
- Insert new memory when no similar exists
- Skip near-duplicates (score > 0.95)
- Update memory when LLM decides
- Delete memory when LLM decides
- Insert separate memory for moderate similarity
- Multiple memories with different actions
- LLM error fallback to INSERT

See `IntelligentMemoryTest.java` for full test suite.

## Future Enhancements

Potential improvements for future versions:
- Configurable similarity thresholds for each action
- Batch processing for multiple memories
- User-configurable decision strategies
- Memory merge history tracking
- Async LLM decision-making
180 changes: 155 additions & 25 deletions mem4j-core/src/main/java/io/github/mem4j/memory/Memory.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,49 +68,179 @@ public void add(List<Message> messages, String userId, Map<String, Object> metad
.map(memory -> createMemoryItem(memory, userId, metadata, memoryType))
.collect(Collectors.toList());

// Filter out duplicate memories before adding
List<MemoryItem> newMemories = new ArrayList<>();
int duplicateCount = 0;
// Process each memory intelligently
int insertCount = 0;
int updateCount = 0;
int deleteCount = 0;
int skipCount = 0;

for (MemoryItem item : memoryItems) {
Double[] embedding = embeddingService.embed(item.getContent());
item.setEmbedding(embedding);

// Check for similar existing memories (high similarity threshold for
// deduplication)
// Search for similar existing memories with moderate threshold
List<MemoryItem> similarMemories = vectorStoreService.search(embedding,
buildSearchFilters(userId, null), 5, 0.85);

boolean isDuplicate = false;
for (MemoryItem existing : similarMemories) {
if (existing.getScore() > 0.85) {
logger.debug("Skipping duplicate memory: '{}' (similar to existing: '{}')", item.getContent(),
existing.getContent());
isDuplicate = true;
duplicateCount++;
buildSearchFilters(userId, null), 5, 0.7);

// Make intelligent decision about what to do with this memory
MemoryDecision decision = decideMemoryAction(item, similarMemories);

// Execute the decision
switch (decision.getAction()) {
case INSERT:
vectorStoreService.add(item);
insertCount++;
logger.debug("Inserting new memory: '{}' - Reason: {}", item.getContent(),
decision.getReason());
break;
}
}

if (!isDuplicate) {
newMemories.add(item);
}
}
case UPDATE:
MemoryItem existingItem = decision.getExistingMemory();
existingItem.setContent(decision.getNewContent());
existingItem.setEmbedding(embeddingService.embed(decision.getNewContent()));
existingItem.setUpdatedAt(java.time.Instant.now());
vectorStoreService.update(existingItem);
updateCount++;
logger.debug("Updating existing memory '{}' -> '{}' - Reason: {}", existingItem.getContent(),
decision.getNewContent(), decision.getReason());
break;

// Store only new memories
for (MemoryItem item : newMemories) {
vectorStoreService.add(item);
case DELETE:
vectorStoreService.delete(decision.getExistingMemory().getId());
deleteCount++;
logger.debug("Deleting obsolete memory: '{}' - Reason: {}",
decision.getExistingMemory().getContent(), decision.getReason());
break;

case SKIP:
skipCount++;
logger.debug("Skipping memory: '{}' - Reason: {}", item.getContent(), decision.getReason());
break;
}
}

logger.info("Added {} new memories for user {} (skipped {} duplicates)", newMemories.size(), userId,
duplicateCount);
logger.info(
"Memory operations for user {}: {} inserted, {} updated, {} deleted, {} skipped (total extracted: {})",
userId, insertCount, updateCount, deleteCount, skipCount, memoryItems.size());
}
catch (Exception e) {
logger.error("Error adding memories for user {}", userId, e);
throw new RuntimeException("Failed to add memories", e);
}
}

/**
* Intelligently decide what action to take on a memory by comparing with similar
* existing memories Uses LLM to make nuanced decisions about insert/update/delete
*/
private MemoryDecision decideMemoryAction(MemoryItem newMemory, List<MemoryItem> similarMemories) {

// If no similar memories exist, insert the new memory
if (similarMemories.isEmpty()) {
return new MemoryDecision(MemoryAction.INSERT, newMemory.getContent(), null,
"No similar memories found, inserting new memory");
}

// Find the most similar memory
MemoryItem mostSimilar = similarMemories.get(0);
double highestScore = mostSimilar.getScore();

// If similarity is very high (>0.95), consider it a duplicate and skip
if (highestScore > 0.95) {
return new MemoryDecision(MemoryAction.SKIP, null, mostSimilar,
"Memory is nearly identical to existing memory (score: " + String.format("%.2f", highestScore)
+ ")");
}

// If similarity is high (0.85-0.95), use LLM to decide if update is needed
if (highestScore > 0.85) {
String llmDecision = getLLMMemoryDecision(newMemory.getContent(), mostSimilar.getContent());

if (llmDecision.contains("UPDATE")) {
// Extract merged content from LLM response
String mergedContent = extractMergedContent(llmDecision);
if (mergedContent != null && !mergedContent.isEmpty()) {
return new MemoryDecision(MemoryAction.UPDATE, mergedContent, mostSimilar,
"LLM decided to merge memories with updated information");
}
}
else if (llmDecision.contains("DELETE")) {
return new MemoryDecision(MemoryAction.DELETE, null, mostSimilar,
"LLM decided old memory is obsolete or contradicted");
}
else if (llmDecision.contains("SKIP")) {
return new MemoryDecision(MemoryAction.SKIP, null, mostSimilar, "LLM decided new memory adds no value");
}
}

// If similarity is moderate (0.7-0.85), insert as separate memory
if (highestScore > 0.7) {
return new MemoryDecision(MemoryAction.INSERT, newMemory.getContent(), null,
"Memory is related but sufficiently different to keep separate (score: "
+ String.format("%.2f", highestScore) + ")");
}

// Default: insert as new memory
return new MemoryDecision(MemoryAction.INSERT, newMemory.getContent(), null,
"Memory is distinct enough to insert separately");
}

/**
* Use LLM to decide whether to update, delete, or skip a memory
*/
private String getLLMMemoryDecision(String newMemory, String existingMemory) {

String prompt = String.format(
"""
You are a memory management system. Compare these two memories and decide what action to take:

EXISTING MEMORY: %s
NEW MEMORY: %s

Analyze and decide:
1. If the new memory contains UPDATED information that contradicts or improves the existing memory, respond with:
UPDATE: [merged content combining both memories with the most accurate/recent information]

2. If the new memory makes the existing memory OBSOLETE or CONTRADICTS it completely, respond with:
DELETE

3. If the new memory adds NO NEW value (it's essentially the same), respond with:
SKIP

4. If they are COMPLEMENTARY but distinct enough to keep separate, respond with:
INSERT

Consider:
- Temporal context (newer information may supersede older)
- Specificity (more specific information may update general information)
- Contradictions (direct contradictions should trigger DELETE of old + INSERT of new)
- Redundancy (avoid storing the same information twice)

Respond with ONLY one of: UPDATE: [content], DELETE, SKIP, or INSERT
""",
existingMemory, newMemory);

try {
return llmService.generate(prompt);
}
catch (Exception e) {
logger.warn("Error getting LLM decision for memory action, defaulting to INSERT", e);
return "INSERT";
}
}

/**
* Extract merged content from LLM response
*/
private String extractMergedContent(String llmResponse) {
// Look for content after "UPDATE:"
String[] parts = llmResponse.split("UPDATE:", 2);
if (parts.length > 1) {
return parts[1].trim();
}
return null;
}

/** Search for relevant memories */
public List<MemoryItem> search(String query, String userId) {
return search(query, userId, null, 10, null);
Expand Down
44 changes: 44 additions & 0 deletions mem4j-core/src/main/java/io/github/mem4j/memory/MemoryAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2024-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 io.github.mem4j.memory;

/**
* Enumeration of actions that can be taken on memories
*/
public enum MemoryAction {

/**
* Insert a new memory
*/
INSERT,

/**
* Update an existing memory
*/
UPDATE,

/**
* Delete an existing memory
*/
DELETE,

/**
* Skip - no action needed
*/
SKIP

}
Loading
Loading