Skip to content

Commit f6d82cc

Browse files
committed
Merge branch 'main' into claude/issue-42-20250801-1705
2 parents 3df5d15 + 7ab7205 commit f6d82cc

File tree

9 files changed

+177
-36
lines changed

9 files changed

+177
-36
lines changed

agent-memory-client/agent_memory_client/models.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class MemoryRecord(BaseModel):
110110
)
111111
discrete_memory_extracted: Literal["t", "f"] = Field(
112112
default="f",
113-
description="Whether memory extraction has run for this memory (only messages)",
113+
description="Whether memory extraction has run for this memory",
114114
)
115115
memory_type: MemoryTypeEnum = Field(
116116
default=MemoryTypeEnum.MESSAGE,
@@ -130,6 +130,19 @@ class MemoryRecord(BaseModel):
130130
)
131131

132132

133+
class ExtractedMemoryRecord(MemoryRecord):
134+
"""A memory record that has already been extracted (e.g., explicit memories from API/MCP)"""
135+
136+
discrete_memory_extracted: Literal["t", "f"] = Field(
137+
default="t",
138+
description="Whether memory extraction has run for this memory",
139+
)
140+
memory_type: MemoryTypeEnum = Field(
141+
default=MemoryTypeEnum.SEMANTIC,
142+
description="Type of memory",
143+
)
144+
145+
133146
class ClientMemoryRecord(MemoryRecord):
134147
"""A memory record with a client-provided ID"""
135148

agent_memory_server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Redis Agent Memory Server - A memory system for conversational AI."""
22

3-
__version__ = "0.9.3"
3+
__version__ = "0.9.4"

agent_memory_server/cli.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,14 @@ async def setup_and_run():
128128
logger.info(f"Starting MCP server on port {port}\n")
129129
await mcp_app.run_sse_async()
130130
elif mode == "stdio":
131-
# Logging already configured above
131+
# Don't run a task worker in stdio mode.
132+
# TODO: Make configurable with a CLI flag?
133+
settings.use_docket = False
132134
await mcp_app.run_stdio_async()
133135
else:
134136
raise ValueError(f"Invalid mode: {mode}")
135137

138+
# TODO: Do we really need to update the port again?
136139
# Update the port in settings
137140
settings.mcp_port = port
138141

agent_memory_server/long_term_memory.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
get_model_client,
3131
)
3232
from agent_memory_server.models import (
33+
ExtractedMemoryRecord,
3334
MemoryMessage,
3435
MemoryRecord,
3536
MemoryRecordResults,
@@ -593,7 +594,7 @@ async def compact_long_term_memories(
593594

594595

595596
async def index_long_term_memories(
596-
memories: list[MemoryRecord],
597+
memories: list[MemoryRecord | ExtractedMemoryRecord],
597598
redis_client: Redis | None = None,
598599
deduplicate: bool = False,
599600
vector_distance_threshold: float = 0.12,
@@ -1204,7 +1205,6 @@ async def promote_working_memory_to_long_term(
12041205
text=f"{msg.role}: {msg.content}",
12051206
namespace=namespace,
12061207
user_id=current_working_memory.user_id,
1207-
memory_type=MemoryTypeEnum.MESSAGE,
12081208
persisted_at=None,
12091209
)
12101210

agent_memory_server/mcp.py

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,7 @@ async def create_long_term_memories(
310310
if mem.user_id is None and settings.default_mcp_user_id:
311311
mem.user_id = settings.default_mcp_user_id
312312

313-
payload = CreateMemoryRecordRequest(
314-
memories=[MemoryRecord(**mem.model_dump()) for mem in memories]
315-
)
313+
payload = CreateMemoryRecordRequest(memories=memories)
316314
return await core_create_long_term_memory(
317315
payload, background_tasks=get_background_tasks()
318316
)
@@ -360,14 +358,23 @@ async def search_long_term_memory(
360358
search_long_term_memory(text="user's favorite color")
361359
```
362360
363-
2. Search with simple session filter:
361+
2. Get ALL memories for a user (e.g., "what do you remember about me?"):
362+
```python
363+
search_long_term_memory(
364+
text="", # Empty string returns all memories for the user
365+
user_id={"eq": "user_123"},
366+
limit=50 # Adjust based on how many memories you want
367+
)
368+
```
369+
370+
3. Search with simple session filter:
364371
```python
365372
search_long_term_memory(text="user's favorite color", session_id={
366373
"eq": "session_12345"
367374
})
368375
```
369376
370-
3. Search with complex filters:
377+
4. Search with complex filters:
371378
```python
372379
search_long_term_memory(
373380
text="user preferences",
@@ -381,7 +388,7 @@ async def search_long_term_memory(
381388
)
382389
```
383390
384-
4. Search with datetime range filters:
391+
5. Search with datetime range filters:
385392
```python
386393
search_long_term_memory(
387394
text="recent conversations",
@@ -395,7 +402,7 @@ async def search_long_term_memory(
395402
)
396403
```
397404
398-
5. Search with between datetime filter:
405+
6. Search with between datetime filter:
399406
```python
400407
search_long_term_memory(
401408
text="holiday discussions",
@@ -406,7 +413,7 @@ async def search_long_term_memory(
406413
```
407414
408415
Args:
409-
text: The semantic search query text (required)
416+
text: The semantic search query text (required). Use empty string "" to get all memories for a user.
410417
session_id: Filter by session ID
411418
namespace: Filter by namespace
412419
topics: Filter by topics
@@ -482,20 +489,14 @@ async def memory_prompt(
482489
"""
483490
Hydrate a user query with relevant session history and long-term memories.
484491
485-
CRITICAL: Use this tool for EVERY question that might benefit from memory context,
486-
especially when you don't have sufficient information to answer confidently.
487-
488492
This tool enriches the user's query by retrieving:
489493
1. Context from the current conversation session
490494
2. Relevant long-term memories related to the query
491495
492-
ALWAYS use this tool when:
493-
- The user references past conversations
494-
- The question is about user preferences or personal information
495-
- You need additional context to provide a complete answer
496-
- The question seems to assume information you don't have in current context
496+
The tool returns both the relevant memories AND the user's query in a format ready for
497+
generating comprehensive responses.
497498
498-
The function uses the text field from the payload as the user's query,
499+
The function uses the query field from the payload as the user's query,
499500
and any filters to retrieve relevant memories.
500501
501502
DATETIME INPUT FORMAT:
@@ -512,12 +513,20 @@ async def memory_prompt(
512513
COMMON USAGE PATTERNS:
513514
```python
514515
1. Hydrate a user prompt with long-term memory search:
515-
hydrate_memory_prompt(text="What was my favorite color?")
516+
memory_prompt(query="What was my favorite color?")
517+
```
518+
519+
2. Answer "what do you remember about me?" type questions:
520+
memory_prompt(
521+
query="What do you remember about me?",
522+
user_id={"eq": "user_123"},
523+
limit=50
524+
)
516525
```
517526
518-
2. Hydrate a user prompt with long-term memory search and session filter:
519-
hydrate_memory_prompt(
520-
text="What is my favorite color?",
527+
3. Hydrate a user prompt with long-term memory search and session filter:
528+
memory_prompt(
529+
query="What is my favorite color?",
521530
session_id={
522531
"eq": "session_12345"
523532
},
@@ -526,9 +535,9 @@ async def memory_prompt(
526535
}
527536
)
528537
529-
3. Hydrate a user prompt with long-term memory search and complex filters:
530-
hydrate_memory_prompt(
531-
text="What was my favorite color?",
538+
4. Hydrate a user prompt with long-term memory search and complex filters:
539+
memory_prompt(
540+
query="What was my favorite color?",
532541
topics={
533542
"any": ["preferences", "settings"]
534543
},
@@ -538,9 +547,9 @@ async def memory_prompt(
538547
limit=5
539548
)
540549
541-
4. Search with datetime range filters:
542-
hydrate_memory_prompt(
543-
text="What did we discuss recently?",
550+
5. Search with datetime range filters:
551+
memory_prompt(
552+
query="What did we discuss recently?",
544553
created_at={
545554
"gte": "2024-01-01T00:00:00Z",
546555
"lt": "2024-02-01T00:00:00Z"
@@ -552,7 +561,7 @@ async def memory_prompt(
552561
```
553562
554563
Args:
555-
- text: The user's query
564+
- query: The user's query
556565
- session_id: Add conversation history from a working memory session
557566
- namespace: Filter session and long-term memory namespace
558567
- topics: Search for long-term memories matching topics

agent_memory_server/models.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ class MemoryRecord(BaseModel):
130130
)
131131
discrete_memory_extracted: Literal["t", "f"] = Field(
132132
default="f",
133-
description="Whether memory extraction has run for this memory (only messages)",
133+
description="Whether memory extraction has run for this memory",
134134
)
135135
memory_type: MemoryTypeEnum = Field(
136136
default=MemoryTypeEnum.MESSAGE,
@@ -150,6 +150,19 @@ class MemoryRecord(BaseModel):
150150
)
151151

152152

153+
class ExtractedMemoryRecord(MemoryRecord):
154+
"""A memory record that has already been extracted (e.g., explicit memories from API/MCP)"""
155+
156+
discrete_memory_extracted: Literal["t", "f"] = Field(
157+
default="t",
158+
description="Whether memory extraction has run for this memory",
159+
)
160+
memory_type: MemoryTypeEnum = Field(
161+
default=MemoryTypeEnum.SEMANTIC,
162+
description="Type of memory",
163+
)
164+
165+
153166
class ClientMemoryRecord(MemoryRecord):
154167
"""A memory record with a client-provided ID"""
155168

@@ -268,7 +281,7 @@ class MemoryRecordResultsResponse(MemoryRecordResults):
268281
class CreateMemoryRecordRequest(BaseModel):
269282
"""Payload for creating memory records"""
270283

271-
memories: list[MemoryRecord]
284+
memories: list[ExtractedMemoryRecord]
272285

273286

274287
class GetSessionsQuery(BaseModel):
@@ -401,7 +414,7 @@ class MemoryPromptResponse(BaseModel):
401414
messages: list[base.Message | SystemMessage]
402415

403416

404-
class LenientMemoryRecord(MemoryRecord):
417+
class LenientMemoryRecord(ExtractedMemoryRecord):
405418
"""A memory record that can be created without an ID"""
406419

407420
id: str | None = Field(default_factory=lambda: str(ULID()))

agent_memory_server/vectorstore_adapter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,9 @@ def parse_timestamp_to_datetime(timestamp_val):
874874
topics=self._parse_list_field(doc.metadata.get("topics")),
875875
entities=self._parse_list_field(doc.metadata.get("entities")),
876876
memory_hash=doc.metadata.get("memory_hash", ""),
877+
discrete_memory_extracted=doc.metadata.get(
878+
"discrete_memory_extracted", "f"
879+
),
877880
memory_type=doc.metadata.get("memory_type", "message"),
878881
persisted_at=doc.metadata.get("persisted_at"),
879882
extracted_from=self._parse_list_field(

tests/test_mcp.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,3 +437,34 @@ async def test_set_working_memory_auto_id_generation(self, mcp_test_setup):
437437
memory = working_memory.memories[0]
438438
assert memory.id is not None
439439
assert len(memory.id) > 0 # ULID generates non-empty strings
440+
441+
@pytest.mark.asyncio
442+
async def test_mcp_lenient_memory_record_defaults(self, session, mcp_test_setup):
443+
"""Test that LenientMemoryRecord used by MCP has correct defaults for discrete_memory_extracted."""
444+
from agent_memory_server.models import (
445+
ExtractedMemoryRecord,
446+
LenientMemoryRecord,
447+
)
448+
449+
# Test 1: LenientMemoryRecord should default to discrete_memory_extracted='t'
450+
lenient_memory = LenientMemoryRecord(
451+
text="User likes green tea",
452+
memory_type="semantic",
453+
namespace="user_preferences",
454+
)
455+
456+
assert (
457+
lenient_memory.discrete_memory_extracted == "t"
458+
), f"LenientMemoryRecord should default to 't', got '{lenient_memory.discrete_memory_extracted}'"
459+
assert lenient_memory.memory_type.value == "semantic"
460+
assert lenient_memory.id is not None
461+
462+
# Test 2: ExtractedMemoryRecord should also default to discrete_memory_extracted='t'
463+
extracted_memory = ExtractedMemoryRecord(
464+
id="test_001", text="User prefers coffee", memory_type="semantic"
465+
)
466+
467+
assert (
468+
extracted_memory.discrete_memory_extracted == "t"
469+
), f"ExtractedMemoryRecord should default to 't', got '{extracted_memory.discrete_memory_extracted}'"
470+
assert extracted_memory.memory_type.value == "semantic"

tests/test_vectorstore_adapter.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Tests for the VectorStore adapter functionality."""
22

3+
import asyncio
34
from unittest.mock import AsyncMock, MagicMock, patch
45

56
import pytest
67

8+
from agent_memory_server.filters import Namespace
79
from agent_memory_server.models import MemoryRecord, MemoryTypeEnum
810
from agent_memory_server.vectorstore_adapter import (
911
LangChainVectorStoreAdapter,
@@ -544,3 +546,70 @@ async def asimilarity_search_with_relevance_scores(
544546
)
545547

546548
assert len(unprocessed_results_after.memories) == 0
549+
550+
def test_redis_adapter_preserves_discrete_memory_extracted_flag(self):
551+
"""Regression test: Ensure Redis adapter preserves discrete_memory_extracted='t' during search.
552+
553+
This test catches the bug where MCP-created memories with discrete_memory_extracted='t'
554+
were being returned as 'f' because the Redis vector store adapter wasn't populating
555+
the field during document-to-memory conversion.
556+
"""
557+
from datetime import UTC, datetime
558+
from unittest.mock import MagicMock
559+
560+
# Create mock vectorstore and embeddings
561+
mock_vectorstore = MagicMock()
562+
mock_embeddings = MagicMock()
563+
564+
# Create Redis adapter
565+
adapter = RedisVectorStoreAdapter(mock_vectorstore, mock_embeddings)
566+
567+
# Mock document that simulates what Redis returns for an MCP-created memory
568+
mock_doc = MagicMock()
569+
mock_doc.page_content = "User likes green tea"
570+
mock_doc.metadata = {
571+
"id_": "memory_001",
572+
"session_id": None,
573+
"user_id": None,
574+
"namespace": "user_preferences",
575+
"created_at": datetime.now(UTC).timestamp(),
576+
"updated_at": datetime.now(UTC).timestamp(),
577+
"last_accessed": datetime.now(UTC).timestamp(),
578+
"topics": "preferences,beverages",
579+
"entities": "",
580+
"memory_hash": "abc123",
581+
"discrete_memory_extracted": "t", # This should be preserved!
582+
"memory_type": "semantic",
583+
"persisted_at": None,
584+
"extracted_from": "",
585+
"event_date": None,
586+
}
587+
588+
# Mock the search to return our test document
589+
mock_vectorstore.asimilarity_search_with_relevance_scores = AsyncMock(
590+
return_value=[(mock_doc, 0.9)]
591+
)
592+
593+
# Perform search
594+
result = asyncio.run(
595+
adapter.search_memories(
596+
query="green tea",
597+
namespace=Namespace(field="namespace", eq="user_preferences"),
598+
limit=10,
599+
)
600+
)
601+
602+
# Verify we got the memory back
603+
assert len(result.memories) == 1
604+
memory = result.memories[0]
605+
606+
# REGRESSION TEST: This should be 't', not 'f'
607+
assert memory.discrete_memory_extracted == "t", (
608+
f"Regression: Expected discrete_memory_extracted='t', got '{memory.discrete_memory_extracted}'. "
609+
f"This indicates the Redis adapter is not preserving the flag during search."
610+
)
611+
612+
# Also verify other expected properties
613+
assert memory.memory_type.value == "semantic"
614+
assert memory.namespace == "user_preferences"
615+
assert memory.text == "User likes green tea"

0 commit comments

Comments
 (0)