Skip to content

Commit 056b71a

Browse files
committed
more tweaks to compaction
1 parent 629e6c7 commit 056b71a

File tree

6 files changed

+51
-42
lines changed

6 files changed

+51
-42
lines changed

agent-memory-client/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ mypy agent_memory_client/
301301
- Python 3.10+
302302
- httpx >= 0.25.0
303303
- pydantic >= 2.0.0
304-
- ulid-py >= 1.1.0
304+
- python-ulid >= 3.0.0
305305

306306
## License
307307

agent_memory_server/logging.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ def configure_logging():
2121
handler.setLevel(level)
2222
logging.basicConfig(level=level, handlers=[handler], format="%(message)s")
2323

24+
# Quiet down noisy third-party loggers
25+
logging.getLogger("httpx").setLevel(logging.WARNING)
26+
logging.getLogger("docket.worker").setLevel(logging.WARNING)
27+
logging.getLogger("agent_memory_server.dependencies").setLevel(logging.WARNING)
28+
29+
# Set PyTorch to be less verbose about device selection
30+
logging.getLogger("torch").setLevel(logging.WARNING)
31+
2432
# Configure structlog with processors honoring the log level and structured output
2533
structlog.configure(
2634
processors=[

agent_memory_server/long_term_memory.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,17 @@ def generate_memory_hash(memory: MemoryRecord) -> str:
127127
Returns:
128128
A stable hash string
129129
"""
130-
# Create a deterministic string representation of the key fields
131-
return hashlib.sha256(memory.model_dump_json().encode()).hexdigest()
130+
# Create a deterministic string representation of the key content fields only
131+
# This ensures merged memories with same content have the same hash
132+
content_fields = {
133+
"text": memory.text,
134+
"user_id": memory.user_id,
135+
"session_id": memory.session_id,
136+
"namespace": memory.namespace,
137+
"memory_type": memory.memory_type,
138+
}
139+
content_json = json.dumps(content_fields, sort_keys=True)
140+
return hashlib.sha256(content_json.encode()).hexdigest()
132141

133142

134143
async def merge_memories_with_llm(
@@ -382,14 +391,23 @@ async def compact_long_term_memories(
382391
# and delete the rest
383392
memories_to_delete = []
384393

385-
for j in range(1, len(search_results), 2):
394+
# Each memory result has: key + 6 field-value pairs = 13 elements
395+
# Keys are at positions: 1, 14, 27, ... (1 + n * 13)
396+
elements_per_memory = 1 + 6 * 2 # key + 6 field-value pairs
397+
for n in range(num_duplicates):
398+
key_index = 1 + n * elements_per_memory
386399
# Skip the last item (newest) which we'll keep
387-
if (
388-
j < (int(num_duplicates) - 1) * 2 + 1
389-
and search_results[j] is not None
400+
if n < num_duplicates - 1 and key_index < len(
401+
search_results
390402
):
391-
key = search_results[j].decode()
392-
memories_to_delete.append(key)
403+
key = search_results[key_index]
404+
if key is not None:
405+
key_str = (
406+
key.decode()
407+
if isinstance(key, bytes)
408+
else key
409+
)
410+
memories_to_delete.append(key_str)
393411

394412
# Delete older duplicates
395413
if memories_to_delete:
@@ -501,7 +519,7 @@ async def compact_long_term_memories(
501519
discrete_memory_extracted=memory_result.discrete_memory_extracted, # type: ignore
502520
)
503521

504-
# Add this memory to processed list
522+
# Add this memory to processed list BEFORE processing to prevent cycles
505523
processed_ids.add(memory_id)
506524

507525
# Check for semantic duplicates
@@ -530,6 +548,8 @@ async def compact_long_term_memories(
530548
redis_client=redis_client,
531549
deduplicate=False, # Already deduplicated
532550
)
551+
# Mark the merged memory as processed to prevent cycles
552+
processed_ids.add(merged_memory.id)
533553
logger.info(
534554
f"Completed semantic deduplication. Merged {semantic_memories_merged} memories."
535555
)

agent_memory_server/vectorstore_adapter.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
and LangChain VectorStore implementations, allowing for pluggable backends.
44
"""
55

6-
import hashlib
76
import logging
87
from abc import ABC, abstractmethod
98
from collections.abc import Callable
@@ -47,7 +46,9 @@ def _select_relevance_score_fn(self) -> Callable[[float], float]:
4746
"""Select the relevance score function based on the distance."""
4847

4948
def relevance_score_fn(distance: float) -> float:
50-
return max((2 - distance) / 2, 0)
49+
# Ensure score is between 0 and 1
50+
score = (2 - distance) / 2
51+
return max(min(score, 1.0), 0.0)
5152

5253
return relevance_score_fn
5354

@@ -373,15 +374,10 @@ def generate_memory_hash(self, memory: MemoryRecord) -> str:
373374
Returns:
374375
A stable hash string
375376
"""
376-
text = memory.text
377-
user_id = memory.user_id or ""
378-
session_id = memory.session_id or ""
377+
# Use the same hash logic as long_term_memory.py for consistency
378+
from agent_memory_server.long_term_memory import generate_memory_hash
379379

380-
# Combine the fields in a predictable order
381-
hash_content = f"{text}|{user_id}|{session_id}"
382-
383-
# Create a stable hash
384-
return hashlib.sha256(hash_content.encode()).hexdigest()
380+
return generate_memory_hash(memory)
385381

386382
def _convert_filters_to_backend_format(
387383
self,

agent_memory_server/vectorstore_factory.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,7 @@
2626
from langchain_redis.config import RedisConfig
2727
from pydantic.types import SecretStr
2828

29-
30-
# Monkey patch RedisVL ULID issue before importing anything else
31-
try:
32-
import redisvl.utils.utils
33-
from ulid import ULID
34-
35-
def patched_create_ulid() -> str:
36-
"""Patched ULID creation function that works with python-ulid."""
37-
return str(ULID())
38-
39-
# Replace the broken function with our working one
40-
redisvl.utils.utils.create_ulid = patched_create_ulid
41-
logging.info("Successfully patched RedisVL ULID function")
42-
except Exception as e:
43-
logging.warning(f"Could not patch RedisVL ULID function: {e}")
44-
29+
# RedisVL uses the same python-ulid library as this project, so no patching needed
4530
from agent_memory_server.config import settings
4631
from agent_memory_server.vectorstore_adapter import (
4732
LangChainVectorStoreAdapter,

tests/test_long_term_memory.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,16 +216,16 @@ def test_generate_memory_hash(self):
216216
memory_type=MemoryTypeEnum.SEMANTIC,
217217
)
218218

219-
# MemoryRecord objects with different IDs will produce different hashes
220-
# since model_dump_json() includes all fields including the ID
219+
# MemoryRecord objects with same content produce same hash (content-based hashing)
220+
# IDs and timestamps don't affect the hash
221221
hash1 = generate_memory_hash(memory1)
222222
hash2 = generate_memory_hash(memory2)
223223
hash3 = generate_memory_hash(memory3)
224224

225-
# All hashes should be different because IDs are different
226-
assert hash1 != hash2 # Different IDs
227-
assert hash1 != hash3 # Different text and IDs
228-
assert hash2 != hash3 # Different text and IDs
225+
# Same content should produce same hash
226+
assert hash1 == hash2 # Same content, different IDs
227+
assert hash1 != hash3 # Different text
228+
assert hash2 != hash3 # Different text
229229

230230
# Test with missing user_id field
231231
memory4 = MemoryRecord(

0 commit comments

Comments
 (0)