Skip to content

Commit f66c07d

Browse files
authored
Merge pull request #28 from redis/feature/memory-duplication-fix
Fix deduplication for "message" type long-term memories
2 parents 09ce62a + d7a0ddb commit f66c07d

27 files changed

+714
-650
lines changed

.github/workflows/python-tests.yml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,12 @@ jobs:
8181
- name: Set up Docker Buildx
8282
uses: docker/setup-buildx-action@v3
8383

84-
- name: Log in to Docker Hub
84+
- name: Log in to GitHub Container Registry
8585
uses: docker/login-action@v3
8686
with:
87-
username: ${{ secrets.DOCKER_USERNAME }}
88-
password: ${{ secrets.DOCKER_TOKEN }}
87+
registry: ghcr.io
88+
username: ${{ github.actor }}
89+
password: ${{ secrets.GITHUB_TOKEN }}
8990

9091
- name: Extract version from __init__.py
9192
id: version
@@ -102,7 +103,7 @@ jobs:
102103
platforms: linux/amd64,linux/arm64
103104
push: true
104105
tags: |
105-
andrewbrookins510/agent-memory-server:latest
106-
andrewbrookins510/agent-memory-server:${{ steps.version.outputs.version }}
106+
ghcr.io/${{ github.repository }}:latest
107+
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
107108
cache-from: type=gha
108109
cache-to: type=gha,mode=max

CLAUDE.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ uv install --all-extras # Install dependencies
1414
uv sync --all-extras # Sync latest dependencies
1515
uv run ruff check # Run linting
1616
uv run ruff format # Format code
17-
uv run pytest # Run tests
18-
uv run pytest tests/ # Run specific test directory
19-
uv run pytest --run-api-tests # Run all tests, including API tests
17+
uv run pytest --run-api-tests # Run all tests
2018
uv add <dependency> # Add a dependency to pyproject.toml and update lock file
2119
uv remove <dependency> # Remove a dependency from pyproject.toml and update lock file
2220

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-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.0b6"
8+
__version__ = "0.9.0b7"
99

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

agent-memory-client/agent_memory_client/client.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
AckResponse,
3232
ClientMemoryRecord,
3333
HealthCheckResponse,
34+
MemoryMessage,
3435
MemoryRecord,
3536
MemoryRecordResults,
3637
MemoryTypeEnum,
@@ -675,7 +676,7 @@ async def search_long_term_memory(
675676

676677
try:
677678
response = await self._client.post(
678-
"/v1/memory/search",
679+
"/v1/long-term-memory/search",
679680
json=payload,
680681
)
681682
response.raise_for_status()
@@ -2055,7 +2056,7 @@ async def update_working_memory_data(
20552056
async def append_messages_to_working_memory(
20562057
self,
20572058
session_id: str,
2058-
messages: list[dict[str, Any]], # Expect proper message dicts
2059+
messages: list[dict[str, Any] | MemoryMessage],
20592060
namespace: str | None = None,
20602061
model_name: str | None = None,
20612062
context_window_max: int | None = None,
@@ -2068,7 +2069,7 @@ async def append_messages_to_working_memory(
20682069
20692070
Args:
20702071
session_id: Target session
2071-
messages: List of message dictionaries with 'role' and 'content' keys
2072+
messages: List of message dictionaries or MemoryMessage objects
20722073
namespace: Optional namespace
20732074
model_name: Optional model name for token-based summarization
20742075
context_window_max: Optional direct specification of context window max tokens
@@ -2081,29 +2082,49 @@ async def append_messages_to_working_memory(
20812082
session_id=session_id, namespace=namespace, user_id=user_id
20822083
)
20832084

2084-
# Validate new messages have required structure
2085+
# Convert messages to MemoryMessage objects
2086+
converted_messages = []
20852087
for msg in messages:
2086-
if not isinstance(msg, dict) or "role" not in msg or "content" not in msg:
2088+
if isinstance(msg, MemoryMessage):
2089+
converted_messages.append(msg)
2090+
elif isinstance(msg, dict):
2091+
if "role" not in msg or "content" not in msg:
2092+
raise ValueError("All messages must have 'role' and 'content' keys")
2093+
# Build message kwargs, only including non-None values
2094+
message_kwargs = {
2095+
"role": msg["role"],
2096+
"content": msg["content"],
2097+
}
2098+
if msg.get("id") is not None:
2099+
message_kwargs["id"] = msg["id"]
2100+
if msg.get("persisted_at") is not None:
2101+
message_kwargs["persisted_at"] = msg["persisted_at"]
2102+
2103+
converted_messages.append(MemoryMessage(**message_kwargs))
2104+
else:
20872105
raise ValueError(
2088-
"All messages must be dictionaries with 'role' and 'content' keys"
2106+
"All messages must be dictionaries or MemoryMessage objects"
20892107
)
20902108

2091-
# Get existing messages (already in proper dict format from get_working_memory)
2109+
# Get existing messages
20922110
existing_messages = []
20932111
if existing_memory and existing_memory.messages:
20942112
existing_messages = existing_memory.messages
20952113

2096-
final_messages = existing_messages + messages
2114+
final_messages = existing_messages + converted_messages
20972115

20982116
# Create updated working memory
2099-
working_memory = WorkingMemory(
2100-
session_id=session_id,
2101-
namespace=namespace or self.config.default_namespace,
2102-
messages=final_messages,
2103-
memories=existing_memory.memories if existing_memory else [],
2104-
data=existing_memory.data if existing_memory else {},
2105-
context=existing_memory.context if existing_memory else None,
2106-
user_id=existing_memory.user_id if existing_memory else None,
2117+
working_memory = (
2118+
existing_memory.model_copy(
2119+
update={"messages": final_messages},
2120+
)
2121+
if existing_memory
2122+
else WorkingMemory(
2123+
session_id=session_id,
2124+
namespace=namespace or self.config.default_namespace,
2125+
messages=final_messages,
2126+
user_id=user_id or None,
2127+
)
21072128
)
21082129

21092130
return await self.put_working_memory(
@@ -2198,7 +2219,6 @@ async def memory_prompt(
21982219
payload["long_term_search"] = long_term_search
21992220

22002221
try:
2201-
print("Payload: ", payload)
22022222
response = await self._client.post(
22032223
"/v1/memory/prompt",
22042224
json=payload,

agent-memory-client/agent_memory_client/models.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from datetime import datetime, timezone
99
from enum import Enum
10-
from typing import Any, Literal, TypedDict
10+
from typing import Any, Literal
1111

1212
from pydantic import BaseModel, Field
1313
from ulid import ULID
@@ -48,11 +48,23 @@ class MemoryTypeEnum(str, Enum):
4848
MESSAGE = "message"
4949

5050

51-
class MemoryMessage(TypedDict):
51+
class MemoryMessage(BaseModel):
5252
"""A message in the memory system"""
5353

5454
role: str
5555
content: str
56+
id: str = Field(
57+
default_factory=lambda: str(ULID()),
58+
description="Unique identifier for the message (auto-generated)",
59+
)
60+
persisted_at: datetime | None = Field(
61+
default=None,
62+
description="Server-assigned timestamp when message was persisted to long-term storage",
63+
)
64+
discrete_memory_extracted: Literal["t", "f"] = Field(
65+
default="f",
66+
description="Whether memory extraction has run for this message",
67+
)
5668

5769

5870
class MemoryRecord(BaseModel):
@@ -134,9 +146,9 @@ class WorkingMemory(BaseModel):
134146
"""Working memory for a session - contains both messages and structured memory records"""
135147

136148
# Support both message-based memory (conversation) and structured memory records
137-
messages: list[dict[str, Any]] = Field(
149+
messages: list[MemoryMessage] = Field(
138150
default_factory=list,
139-
description="Conversation messages (role/content pairs)",
151+
description="Conversation messages with tracking fields",
140152
)
141153
memories: list[MemoryRecord | ClientMemoryRecord] = Field(
142154
default_factory=list,

agent-memory-client/tests/test_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -546,9 +546,9 @@ async def test_append_messages_to_working_memory(self, enhanced_test_client):
546546
# Check that messages were appended
547547
working_memory_arg = mock_put.call_args[0][1]
548548
assert len(working_memory_arg.messages) == 3
549-
assert working_memory_arg.messages[0]["content"] == "First message"
550-
assert working_memory_arg.messages[1]["content"] == "Second message"
551-
assert working_memory_arg.messages[2]["content"] == "Third message"
549+
assert working_memory_arg.messages[0].content == "First message"
550+
assert working_memory_arg.messages[1].content == "Second message"
551+
assert working_memory_arg.messages[2].content == "Third message"
552552

553553
def test_deep_merge_dicts(self, enhanced_test_client):
554554
"""Test the deep merge dictionary utility method."""

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

0 commit comments

Comments
 (0)