Skip to content

Commit d1bffa0

Browse files
authored
Merge pull request #26 from redis-developer/fix/include-user-id-everywhere-optionally
Support an optional user ID everywhere
2 parents 74f6b2c + f33457f commit d1bffa0

23 files changed

+288
-148
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ uv run ruff check # Run linting
1616
uv run ruff format # Format code
1717
uv run pytest # Run tests
1818
uv run pytest tests/ # Run specific test directory
19+
uv run pytest --run-api-tests # Run all tests, including API tests
1920
uv add <dependency> # Add a dependency to pyproject.toml and update lock file
2021
uv remove <dependency> # Remove a dependency from pyproject.toml and update lock file
2122

agent-memory-client/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ await client.update_working_memory_data(
206206

207207
# Append messages
208208
new_messages = [
209-
MemoryMessage(role="user", content="What's the weather?"),
210-
MemoryMessage(role="assistant", content="It's sunny today!")
209+
{"role": "user", "content": "What's the weather?"},
210+
{"role": "assistant", "content": "It's sunny today!"}
211211
]
212212

213213
await client.append_messages_to_working_memory(

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.9.0b3"
8+
__version__ = "0.9.0b4"
99

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

agent-memory-client/agent_memory_client/client.py

Lines changed: 76 additions & 68 deletions
Large diffs are not rendered by default.

agent-memory-client/tests/test_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,8 @@ async def test_append_messages_to_working_memory(self, enhanced_test_client):
527527
)
528528

529529
new_messages = [
530-
MemoryMessage(role="assistant", content="Second message"),
531-
MemoryMessage(role="user", content="Third message"),
530+
{"role": "assistant", "content": "Second message"},
531+
{"role": "user", "content": "Third message"},
532532
]
533533

534534
with (

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.0b3"
3+
__version__ = "0.9.0b4"

agent_memory_server/api.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ async def list_sessions(
188188
Get a list of session IDs, with optional pagination.
189189
190190
Args:
191-
options: Query parameters (page, size, namespace)
191+
options: Query parameters (limit, offset, namespace, user_id)
192192
193193
Returns:
194194
List of session IDs
@@ -200,6 +200,7 @@ async def list_sessions(
200200
limit=options.limit,
201201
offset=options.offset,
202202
namespace=options.namespace,
203+
user_id=options.user_id,
203204
)
204205

205206
return SessionListResponse(
@@ -211,8 +212,8 @@ async def list_sessions(
211212
@router.get("/v1/working-memory/{session_id}", response_model=WorkingMemoryResponse)
212213
async def get_working_memory(
213214
session_id: str,
215+
user_id: str | None = None,
214216
namespace: str | None = None,
215-
window_size: int = settings.window_size, # Deprecated: kept for backward compatibility
216217
model_name: ModelNameLiteral | None = None,
217218
context_window_max: int | None = None,
218219
current_user: UserInfo = Depends(get_current_user),
@@ -225,8 +226,8 @@ async def get_working_memory(
225226
226227
Args:
227228
session_id: The session ID
229+
user_id: The user ID to retrieve working memory for
228230
namespace: The namespace to use for the session
229-
window_size: DEPRECATED - The number of messages to include (kept for backward compatibility)
230231
model_name: The client's LLM model name (will determine context window size if provided)
231232
context_window_max: Direct specification of the context window max tokens (overrides model_name)
232233
@@ -240,6 +241,7 @@ async def get_working_memory(
240241
session_id=session_id,
241242
namespace=namespace,
242243
redis_client=redis,
244+
user_id=user_id,
243245
)
244246

245247
if not working_mem:
@@ -249,6 +251,7 @@ async def get_working_memory(
249251
memories=[],
250252
session_id=session_id,
251253
namespace=namespace,
254+
user_id=user_id,
252255
)
253256

254257
# Apply token-based truncation if we have messages and model info
@@ -266,17 +269,14 @@ async def get_working_memory(
266269
break
267270
working_mem.messages = truncated_messages
268271

269-
# Fallback to message-count truncation for backward compatibility
270-
elif len(working_mem.messages) > window_size:
271-
working_mem.messages = working_mem.messages[-window_size:]
272-
273272
return working_mem
274273

275274

276275
@router.put("/v1/working-memory/{session_id}", response_model=WorkingMemoryResponse)
277276
async def put_working_memory(
278277
session_id: str,
279278
memory: WorkingMemory,
279+
user_id: str | None = None,
280280
model_name: ModelNameLiteral | None = None,
281281
context_window_max: int | None = None,
282282
background_tasks=Depends(get_background_tasks),
@@ -291,6 +291,7 @@ async def put_working_memory(
291291
Args:
292292
session_id: The session ID
293293
memory: Working memory to save
294+
user_id: Optional user ID for the session (overrides user_id in memory object)
294295
model_name: The client's LLM model name for context window determination
295296
context_window_max: Direct specification of context window max tokens
296297
background_tasks: DocketBackgroundTasks instance (injected automatically)
@@ -303,6 +304,10 @@ async def put_working_memory(
303304
# Ensure session_id matches
304305
memory.session_id = session_id
305306

307+
# Override user_id if provided as query parameter
308+
if user_id is not None:
309+
memory.user_id = user_id
310+
306311
# Validate that all structured memories have id (if any)
307312
for mem in memory.memories:
308313
if not mem.id:
@@ -359,6 +364,7 @@ async def put_working_memory(
359364
@router.delete("/v1/working-memory/{session_id}", response_model=AckResponse)
360365
async def delete_working_memory(
361366
session_id: str,
367+
user_id: str | None = None,
362368
namespace: str | None = None,
363369
current_user: UserInfo = Depends(get_current_user),
364370
):
@@ -369,6 +375,7 @@ async def delete_working_memory(
369375
370376
Args:
371377
session_id: The session ID
378+
user_id: Optional user ID for the session
372379
namespace: Optional namespace for the session
373380
374381
Returns:
@@ -379,6 +386,7 @@ async def delete_working_memory(
379386
# Delete unified working memory
380387
await working_memory.delete_working_memory(
381388
session_id=session_id,
389+
user_id=user_id,
382390
namespace=namespace,
383391
redis_client=redis,
384392
)
@@ -558,6 +566,7 @@ async def memory_prompt(
558566
working_mem = await working_memory.get_working_memory(
559567
session_id=params.session.session_id,
560568
namespace=params.session.namespace,
569+
user_id=params.session.user_id,
561570
redis_client=redis,
562571
)
563572

agent_memory_server/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ def load_yaml_settings():
2020
class Settings(BaseSettings):
2121
redis_url: str = "redis://localhost:6379"
2222
long_term_memory: bool = True
23-
window_size: int = 20
2423
openai_api_key: str | None = None
2524
anthropic_api_key: str | None = None
2625
generation_model: str = "gpt-4o-mini"
@@ -66,6 +65,9 @@ class Settings(BaseSettings):
6665
auth0_client_id: str | None = None
6766
auth0_client_secret: str | None = None
6867

68+
# Working memory settings
69+
window_size: int = 20 # Default number of recent messages to return
70+
6971
# Other Application settings
7072
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
7173

agent_memory_server/dependencies.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,19 @@ async def add_task(
2727
logger.info("Scheduling task through Docket")
2828
# Get the Redis connection that's already configured (will use testcontainer in tests)
2929
redis_conn = await get_redis_conn()
30-
# Use the connection's URL instead of settings.redis_url directly
31-
redis_url = redis_conn.connection_pool.connection_kwargs.get(
32-
"url", settings.redis_url
33-
)
30+
31+
# Extract Redis URL from the connection pool
32+
connection_kwargs = redis_conn.connection_pool.connection_kwargs
33+
if "host" in connection_kwargs and "port" in connection_kwargs:
34+
redis_url = (
35+
f"redis://{connection_kwargs['host']}:{connection_kwargs['port']}"
36+
)
37+
if "db" in connection_kwargs:
38+
redis_url += f"/{connection_kwargs['db']}"
39+
else:
40+
# Fallback to settings if we can't extract from connection
41+
redis_url = settings.redis_url
42+
3443
logger.info("redis_url: %s", redis_url)
3544
logger.info("docket_name: %s", settings.docket_name)
3645
async with Docket(

agent_memory_server/long_term_memory.py

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@
4646
)
4747

4848

49-
DEFAULT_MEMORY_LIMIT = 1000
50-
MEMORY_INDEX = "memory_idx"
51-
5249
# Prompt for extracting memories from messages in working memory context
5350
WORKING_MEMORY_EXTRACTION_PROMPT = """
5451
You are a memory extraction assistant. Your job is to analyze conversation
@@ -354,15 +351,27 @@ async def compact_long_term_memories(
354351
# Find all memories with this hash
355352
# Use FT.SEARCH to find the actual memories with this hash
356353
# TODO: Use RedisVL index
357-
search_query = (
358-
f"FT.SEARCH {index_name} "
359-
f"(@memory_hash:{{{memory_hash}}}) {' '.join(filters)} "
360-
"RETURN 6 id_ text last_accessed created_at user_id session_id "
361-
"SORTBY last_accessed ASC" # Oldest first
362-
)
354+
if filters:
355+
# Combine hash query with filters using boolean AND
356+
query_expr = f"(@memory_hash:{{{memory_hash}}}) ({' '.join(filters)})"
357+
else:
358+
query_expr = f"@memory_hash:{{{memory_hash}}}"
363359

364360
search_results = await redis_client.execute_command(
365-
search_query
361+
"FT.SEARCH",
362+
index_name,
363+
f"'{query_expr}'",
364+
"RETURN",
365+
"6",
366+
"id_",
367+
"text",
368+
"last_accessed",
369+
"created_at",
370+
"user_id",
371+
"session_id",
372+
"SORTBY",
373+
"last_accessed",
374+
"ASC",
366375
)
367376

368377
if search_results and search_results[0] > 1:
@@ -1209,15 +1218,24 @@ async def deduplicate_by_hash(
12091218

12101219
# Use FT.SEARCH to find memories with this hash
12111220
# TODO: Use RedisVL
1212-
search_query = (
1213-
f"FT.SEARCH {index_name} "
1214-
f"(@memory_hash:{{{memory_hash}}}) {filter_str} "
1215-
"RETURN 1 id_ "
1216-
"SORTBY last_accessed DESC" # Newest first
1221+
if filter_str:
1222+
# Combine hash query with filters using boolean AND
1223+
query_expr = f"(@memory_hash:{{{memory_hash}}}) ({filter_str})"
1224+
else:
1225+
query_expr = f"@memory_hash:{{{memory_hash}}}"
1226+
1227+
search_results = await redis_client.execute_command(
1228+
"FT.SEARCH",
1229+
index_name,
1230+
f"'{query_expr}'",
1231+
"RETURN",
1232+
"1",
1233+
"id_",
1234+
"SORTBY",
1235+
"last_accessed",
1236+
"DESC",
12171237
)
12181238

1219-
search_results = await redis_client.execute_command(search_query)
1220-
12211239
if search_results and search_results[0] > 0:
12221240
# Found existing memory with the same hash
12231241
logger.info(f"Found existing memory with hash {memory_hash}")
@@ -1285,15 +1303,25 @@ async def deduplicate_by_id(
12851303

12861304
# Use FT.SEARCH to find memories with this id
12871305
# TODO: Use RedisVL
1288-
search_query = (
1289-
f"FT.SEARCH {index_name} "
1290-
f"(@id:{{{memory.id}}}) {filter_str} "
1291-
"RETURN 2 id_ persisted_at "
1292-
"SORTBY last_accessed DESC" # Newest first
1306+
if filter_str:
1307+
# Combine the id query with filters - Redis FT.SEARCH uses implicit AND between terms
1308+
query_expr = f"@id:{{{memory.id}}} {filter_str}"
1309+
else:
1310+
query_expr = f"@id:{{{memory.id}}}"
1311+
1312+
search_results = await redis_client.execute_command(
1313+
"FT.SEARCH",
1314+
index_name,
1315+
f"'{query_expr}'",
1316+
"RETURN",
1317+
"2",
1318+
"id_",
1319+
"persisted_at",
1320+
"SORTBY",
1321+
"last_accessed",
1322+
"DESC",
12931323
)
12941324

1295-
search_results = await redis_client.execute_command(search_query)
1296-
12971325
if search_results and search_results[0] > 0:
12981326
# Found existing memory with the same id
12991327
logger.info(f"Found existing memory with id {memory.id}, will overwrite")

agent_memory_server/mcp.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ async def memory_prompt(
562562
session = WorkingMemoryRequest(
563563
session_id=_session_id,
564564
namespace=namespace.eq if namespace and namespace.eq else None,
565+
user_id=user_id.eq if user_id and user_id.eq else None,
565566
window_size=window_size,
566567
model_name=model_name,
567568
context_window_max=context_window_max,

agent_memory_server/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ class WorkingMemoryRequest(BaseModel):
216216

217217
session_id: str
218218
namespace: str | None = None
219+
user_id: str | None = None
219220
window_size: int = settings.window_size
220221
model_name: ModelNameLiteral | None = None
221222
context_window_max: int | None = None
@@ -257,6 +258,7 @@ class GetSessionsQuery(BaseModel):
257258
limit: int = Field(default=20, ge=1, le=100)
258259
offset: int = Field(default=0, ge=0)
259260
namespace: str | None = None
261+
user_id: str | None = None
260262

261263

262264
class HealthCheckResponse(BaseModel):

agent_memory_server/utils/keys.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,22 @@ def metadata_key(session_id: str, namespace: str | None = None) -> str:
5656
)
5757

5858
@staticmethod
59-
def working_memory_key(session_id: str, namespace: str | None = None) -> str:
59+
def working_memory_key(
60+
session_id: str, user_id: str | None = None, namespace: str | None = None
61+
) -> str:
6062
"""Get the working memory key for a session."""
61-
return (
62-
f"working_memory:{namespace}:{session_id}"
63-
if namespace
64-
else f"working_memory:{session_id}"
65-
)
63+
# Build key components, filtering out None values
64+
key_parts = ["working_memory"]
65+
66+
if namespace:
67+
key_parts.append(namespace)
68+
69+
if user_id:
70+
key_parts.append(user_id)
71+
72+
key_parts.append(session_id)
73+
74+
return ":".join(key_parts)
6675

6776
@staticmethod
6877
def search_index_name() -> str:

0 commit comments

Comments
 (0)