Skip to content

Commit eb87d44

Browse files
authored
Merge pull request #82 from redis/fix-long-term-memory-strategy-support
Add long_term_memory_strategy support to client and MCP
2 parents 9be6751 + a08eebf commit eb87d44

File tree

7 files changed

+365
-11
lines changed

7 files changed

+365
-11
lines changed

agent-memory-client/agent_memory_client/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
memory management capabilities for AI agents and applications.
66
"""
77

8-
__version__ = "0.12.7"
8+
__version__ = "0.13.0"
99

1010
from .client import MemoryAPIClient, MemoryClientConfig, create_memory_client
1111
from .exceptions import (

agent-memory-client/agent_memory_client/client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
MemoryMessage,
4141
MemoryRecord,
4242
MemoryRecordResults,
43+
MemoryStrategyConfig,
4344
MemoryTypeEnum,
4445
ModelNameLiteral,
4546
RecencyConfig,
@@ -304,6 +305,7 @@ async def get_or_create_working_memory(
304305
namespace: str | None = None,
305306
model_name: ModelNameLiteral | None = None,
306307
context_window_max: int | None = None,
308+
long_term_memory_strategy: MemoryStrategyConfig | None = None,
307309
) -> tuple[bool, WorkingMemory]:
308310
"""
309311
Get working memory for a session, creating it if it doesn't exist.
@@ -318,6 +320,8 @@ async def get_or_create_working_memory(
318320
namespace: Optional namespace for the session
319321
model_name: Optional model name to determine context window size
320322
context_window_max: Optional direct specification of context window tokens
323+
long_term_memory_strategy: Optional strategy configuration for memory extraction
324+
when promoting to long-term memory
321325
322326
Returns:
323327
Tuple of (created: bool, memory: WorkingMemory)
@@ -338,6 +342,16 @@ async def get_or_create_working_memory(
338342
logging.info("Found existing session")
339343
340344
logging.info(f"Session has {len(memory.messages)} messages")
345+
346+
# With memory extraction strategy options
347+
created, memory = await client.get_or_create_working_memory(
348+
session_id="chat_session_456",
349+
user_id="user_789",
350+
long_term_memory_strategy=MemoryStrategyConfig(
351+
strategy="summary",
352+
config={"max_summary_length": 500}
353+
)
354+
)
341355
```
342356
"""
343357
try:
@@ -390,6 +404,8 @@ async def get_or_create_working_memory(
390404
memories=[],
391405
data={},
392406
user_id=user_id,
407+
long_term_memory_strategy=long_term_memory_strategy
408+
or MemoryStrategyConfig(),
393409
)
394410

395411
created_memory = await self.put_working_memory(
@@ -1211,6 +1227,7 @@ async def get_or_create_working_memory_tool(
12111227
session_id: str,
12121228
namespace: str | None = None,
12131229
user_id: str | None = None,
1230+
long_term_memory_strategy: MemoryStrategyConfig | None = None,
12141231
) -> dict[str, Any]:
12151232
"""
12161233
Get or create working memory state formatted for LLM consumption.
@@ -1223,6 +1240,8 @@ async def get_or_create_working_memory_tool(
12231240
session_id: The session ID to get or create memory for
12241241
namespace: Optional namespace for the session
12251242
user_id: Optional user ID for the session
1243+
long_term_memory_strategy: Optional strategy configuration for memory extraction
1244+
when promoting to long-term memory
12261245
12271246
Returns:
12281247
Dict with formatted working memory information and creation status
@@ -1242,13 +1261,22 @@ async def get_or_create_working_memory_tool(
12421261
logging.info(memory_state["summary"]) # Human-readable summary
12431262
logging.info(f"Messages: {memory_state['message_count']}")
12441263
logging.info(f"Memories: {len(memory_state['memories'])}")
1264+
1265+
# With memory extraction strategy options
1266+
memory_state = await client.get_or_create_working_memory_tool(
1267+
session_id="current_session",
1268+
long_term_memory_strategy=MemoryStrategyConfig(
1269+
strategy="preferences",
1270+
)
1271+
)
12451272
```
12461273
"""
12471274
try:
12481275
created, result = await self.get_or_create_working_memory(
12491276
session_id=session_id,
12501277
namespace=namespace or self.config.default_namespace,
12511278
user_id=user_id,
1279+
long_term_memory_strategy=long_term_memory_strategy,
12521280
)
12531281

12541282
# Format for LLM consumption

agent-memory-client/agent_memory_client/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ class MemoryTypeEnum(str, Enum):
4848
MESSAGE = "message"
4949

5050

51+
class MemoryStrategyConfig(BaseModel):
52+
"""Configuration for memory extraction strategy."""
53+
54+
strategy: Literal["discrete", "summary", "preferences", "custom"] = Field(
55+
default="discrete", description="Type of memory extraction strategy to use"
56+
)
57+
config: dict[str, Any] = Field(
58+
default_factory=dict, description="Strategy-specific configuration options"
59+
)
60+
61+
5162
class MemoryMessage(BaseModel):
5263
"""A message in the memory system"""
5364

@@ -198,6 +209,10 @@ class WorkingMemory(BaseModel):
198209
default=None,
199210
description="Optional namespace for the working memory",
200211
)
212+
long_term_memory_strategy: MemoryStrategyConfig = Field(
213+
default_factory=MemoryStrategyConfig,
214+
description="Configuration for memory extraction strategy when promoting to long-term memory",
215+
)
201216

202217
# TTL and timestamps
203218
ttl_seconds: int | None = Field(

agent_memory_server/mcp.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
MemoryPromptResponse,
3838
MemoryRecord,
3939
MemoryRecordResults,
40+
MemoryStrategyConfig,
4041
MemoryTypeEnum,
4142
ModelNameLiteral,
4243
SearchRequest,
@@ -703,6 +704,7 @@ async def set_working_memory(
703704
namespace: str | None = settings.default_mcp_namespace,
704705
user_id: str | None = settings.default_mcp_user_id,
705706
ttl_seconds: int = 3600,
707+
long_term_memory_strategy: MemoryStrategyConfig | None = None,
706708
) -> WorkingMemoryResponse:
707709
"""
708710
Set working memory for a session. This works like the PUT /sessions/{id}/memory API endpoint.
@@ -795,6 +797,12 @@ async def set_working_memory(
795797
namespace: Optional namespace for scoping
796798
user_id: Optional user ID
797799
ttl_seconds: TTL for the working memory (default 1 hour)
800+
long_term_memory_strategy: Optional strategy configuration for memory extraction
801+
when promoting to long-term memory. Examples:
802+
- MemoryStrategyConfig(strategy="discrete", config={}) # Default
803+
- MemoryStrategyConfig(strategy="summary", config={"max_summary_length": 500})
804+
- MemoryStrategyConfig(strategy="preferences", config={})
805+
- MemoryStrategyConfig(strategy="custom", config={"custom_prompt": "..."})
798806
799807
Returns:
800808
Updated working memory response (may include summarization if window exceeded)
@@ -847,22 +855,24 @@ async def set_working_memory(
847855

848856
processed_messages.append(processed_message)
849857

850-
# Create the working memory object
851-
working_memory_obj = WorkingMemory(
852-
session_id=session_id,
858+
# Create the UpdateWorkingMemory object (without session_id, which comes from URL path)
859+
from agent_memory_server.models import UpdateWorkingMemory
860+
861+
update_memory_obj = UpdateWorkingMemory(
853862
namespace=namespace,
854863
memories=processed_memories,
855864
messages=processed_messages,
856865
context=context,
857866
data=data or {},
858867
user_id=user_id,
859868
ttl_seconds=ttl_seconds,
869+
long_term_memory_strategy=long_term_memory_strategy or MemoryStrategyConfig(),
860870
)
861871

862872
# Update working memory via the API - this handles summarization and background promotion
863873
result = await core_put_working_memory(
864874
session_id=session_id,
865-
memory=working_memory_obj,
875+
memory=update_memory_obj,
866876
background_tasks=get_background_tasks(),
867877
)
868878

docs/working-memory.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,12 @@ Working memory can automatically extract and promote memories to long-term stora
118118
By default, the memory server automatically analyzes working memory content and extracts meaningful memories in the background. This is ideal when you want the memory server to handle all LLM operations internally.
119119

120120
```python
121-
# Configure automatic extraction strategy
121+
# Configure automatic extraction strategy - use "summary" to create conversation summaries
122122
working_memory = WorkingMemory(
123123
session_id="chat_123",
124124
long_term_memory_strategy=MemoryStrategyConfig(
125-
extraction_strategy="thread_aware", # Analyzes conversation threads
126-
custom_prompt="Extract key facts about user preferences and important events",
127-
enable_topic_extraction=True,
128-
enable_entity_extraction=True
125+
strategy="summary", # Creates conversation summaries
126+
config={"max_summary_length": 500}
129127
),
130128
messages=[
131129
MemoryMessage(role="user", content="I'm a software engineer at TechCorp"),
@@ -134,9 +132,32 @@ working_memory = WorkingMemory(
134132
]
135133
)
136134

137-
# The server will automatically extract memories like:
135+
# The server will automatically extract a summary like:
136+
# - "User is a software engineer at TechCorp who works with Python and React for web applications"
137+
138+
# Or use "discrete" strategy (default) to extract individual facts:
139+
working_memory = WorkingMemory(
140+
session_id="chat_123",
141+
long_term_memory_strategy=MemoryStrategyConfig(
142+
strategy="discrete", # Extracts individual semantic and episodic facts
143+
config={}
144+
),
145+
messages=[...]
146+
)
147+
148+
# The discrete strategy will extract separate memories like:
138149
# - "User is a software engineer at TechCorp"
139150
# - "User works with Python and React for web applications"
151+
152+
# Or use "custom" strategy with your own extraction prompt:
153+
working_memory = WorkingMemory(
154+
session_id="chat_123",
155+
long_term_memory_strategy=MemoryStrategyConfig(
156+
strategy="custom",
157+
config={"custom_prompt": "Extract key facts about user preferences and important events"}
158+
),
159+
messages=[...]
160+
)
140161
```
141162

142163
### Your LLM Extracts (Client-Side)
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Tests for client long_term_memory_strategy parameter support."""
2+
3+
import pytest
4+
from agent_memory_client import MemoryAPIClient, MemoryClientConfig
5+
from agent_memory_client.models import MemoryStrategyConfig
6+
7+
8+
@pytest.fixture
9+
async def memory_client(use_test_redis_connection):
10+
"""Create a memory client for testing."""
11+
from agent_memory_client import __version__
12+
13+
config = MemoryClientConfig(
14+
base_url="http://test",
15+
disable_auth=True,
16+
)
17+
18+
# Import here to avoid circular imports
19+
from httpx import ASGITransport, AsyncClient
20+
21+
from agent_memory_server.main import app
22+
23+
async with AsyncClient(
24+
transport=ASGITransport(app=app),
25+
base_url="http://test",
26+
headers={
27+
"User-Agent": f"agent-memory-client/{__version__}",
28+
"X-Client-Version": __version__,
29+
},
30+
) as http_client:
31+
client = MemoryAPIClient(config=config)
32+
client._client = http_client
33+
yield client
34+
35+
36+
@pytest.mark.asyncio
37+
async def test_get_or_create_working_memory_with_strategy(
38+
memory_client: MemoryAPIClient,
39+
):
40+
"""Test get_or_create_working_memory with long_term_memory_strategy parameter."""
41+
session_id = "test-strategy-session-1"
42+
43+
# Create with custom strategy
44+
strategy = MemoryStrategyConfig(
45+
strategy="summary", config={"max_summary_length": 500}
46+
)
47+
48+
created, memory = await memory_client.get_or_create_working_memory(
49+
session_id=session_id,
50+
long_term_memory_strategy=strategy,
51+
)
52+
53+
assert created is True
54+
assert memory.session_id == session_id
55+
assert memory.long_term_memory_strategy.strategy == "summary"
56+
assert memory.long_term_memory_strategy.config == {"max_summary_length": 500}
57+
58+
# Get existing session - strategy should be preserved
59+
created2, memory2 = await memory_client.get_or_create_working_memory(
60+
session_id=session_id,
61+
)
62+
63+
assert created2 is False
64+
assert memory2.session_id == session_id
65+
assert memory2.long_term_memory_strategy.strategy == "summary"
66+
assert memory2.long_term_memory_strategy.config == {"max_summary_length": 500}
67+
68+
69+
@pytest.mark.asyncio
70+
async def test_get_or_create_working_memory_tool_with_strategy(
71+
memory_client: MemoryAPIClient,
72+
):
73+
"""Test get_or_create_working_memory_tool with long_term_memory_strategy parameter."""
74+
session_id = "test-strategy-session-2"
75+
76+
# Create with preferences strategy
77+
strategy = MemoryStrategyConfig(strategy="preferences", config={})
78+
79+
result = await memory_client.get_or_create_working_memory_tool(
80+
session_id=session_id,
81+
long_term_memory_strategy=strategy,
82+
)
83+
84+
assert result["created"] is True
85+
assert result["session_id"] == session_id
86+
87+
# Verify the strategy was applied by getting the session
88+
created, memory = await memory_client.get_or_create_working_memory(
89+
session_id=session_id,
90+
)
91+
92+
assert created is False
93+
assert memory.long_term_memory_strategy.strategy == "preferences"
94+
95+
96+
@pytest.mark.asyncio
97+
async def test_get_or_create_working_memory_with_custom_strategy(
98+
memory_client: MemoryAPIClient,
99+
):
100+
"""Test get_or_create_working_memory with custom strategy."""
101+
session_id = "test-strategy-session-3"
102+
103+
# Create with custom strategy
104+
strategy = MemoryStrategyConfig(
105+
strategy="custom",
106+
config={
107+
"custom_prompt": "Extract technical decisions from: {message}\nReturn JSON."
108+
},
109+
)
110+
111+
created, memory = await memory_client.get_or_create_working_memory(
112+
session_id=session_id,
113+
long_term_memory_strategy=strategy,
114+
)
115+
116+
assert created is True
117+
assert memory.session_id == session_id
118+
assert memory.long_term_memory_strategy.strategy == "custom"
119+
assert "custom_prompt" in memory.long_term_memory_strategy.config
120+
121+
122+
@pytest.mark.asyncio
123+
async def test_get_or_create_working_memory_default_strategy(
124+
memory_client: MemoryAPIClient,
125+
):
126+
"""Test get_or_create_working_memory uses default strategy when not specified."""
127+
session_id = "test-strategy-session-4"
128+
129+
# Create without specifying strategy
130+
created, memory = await memory_client.get_or_create_working_memory(
131+
session_id=session_id,
132+
)
133+
134+
assert created is True
135+
assert memory.session_id == session_id
136+
# Should default to discrete strategy
137+
assert memory.long_term_memory_strategy.strategy == "discrete"
138+
assert memory.long_term_memory_strategy.config == {}

0 commit comments

Comments
 (0)