Skip to content

Commit ceba13f

Browse files
authored
Merge pull request #68 from redis/docs/restructure-memory-documentation
This PR includes a few changes: a new feature, a performance fix, a bug fix, and docs enhancements. New feature: Reconstruct working memory messages from long-term storage If you turn on the index_all_messages_in_long_term_memory setting (off by default), the server will copy all messages found in working memory sessions to long-term storage. Later, if a client requests a working memory session by ID, and the server does not find one (because it's expired or you deleted it), the server will try to reconstruct the session from long-term storage by searching for messages with that session ID. This allows you to set a TTL on working memory and expire it quickly, but retain the ability to restore the session from working memory if a user resumes the session later. NOTE: This setting requires some care, so it's disabled by default. You most likely want to set TTLs on working memory if you use this feature. Also, if you use long-term search without specifying memory types, you may get duplicates of the same information (one from a message, one from episodic or semantic memory). Bug fix: calculating remaining context works better now There was a bug in how we calculated remaining context after updating a working memory session. Defaults change: Turn off query optimization by default Long-term search was optimizing all queries by default. This introduces latency and is mostly useful if you're searching directly with user queries, which most agents won't do (instead, an LLM will search through function calls). Docs: Restructures the documentation to provide a cleaner, more logical organization.
2 parents e57f084 + f18af75 commit ceba13f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3962
-1231
lines changed

.github/workflows/python-tests.yml

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -65,49 +65,3 @@ jobs:
6565
uv run pytest --run-api-tests
6666
env:
6767
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
68-
69-
docker:
70-
needs: test
71-
runs-on: ubuntu-latest
72-
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
73-
steps:
74-
- name: Checkout
75-
uses: actions/checkout@v4
76-
77-
- name: Set up Docker Buildx
78-
uses: docker/setup-buildx-action@v3
79-
80-
- name: Log in to Docker Hub
81-
uses: docker/login-action@v3
82-
with:
83-
username: ${{ secrets.DOCKER_USERNAME }}
84-
password: ${{ secrets.DOCKER_TOKEN }}
85-
86-
- name: Log in to GitHub Container Registry
87-
uses: docker/login-action@v3
88-
with:
89-
registry: ghcr.io
90-
username: ${{ github.actor }}
91-
password: ${{ secrets.GITHUB_TOKEN }}
92-
93-
- name: Extract version from __init__.py
94-
id: version
95-
run: |
96-
VERSION=$(grep '__version__ =' agent_memory_server/__init__.py | sed 's/__version__ = "\(.*\)"/\1/' || echo "latest")
97-
echo "version=$VERSION" >> $GITHUB_OUTPUT
98-
echo "Version: $VERSION"
99-
100-
- name: Build and push Docker image
101-
uses: docker/build-push-action@v5
102-
with:
103-
context: .
104-
file: ./Dockerfile
105-
platforms: linux/amd64,linux/arm64
106-
push: true
107-
tags: |
108-
redislabs/agent-memory-server:latest
109-
redislabs/agent-memory-server:${{ steps.version.outputs.version }}
110-
ghcr.io/${{ github.repository }}:latest
111-
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}
112-
cache-from: type=gha
113-
cache-to: type=gha,mode=max

.github/workflows/release.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Release Docker Images
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: 'Version to release (leave empty to use version from __init__.py)'
8+
required: false
9+
type: string
10+
push_latest:
11+
description: 'Also tag as latest'
12+
required: true
13+
type: boolean
14+
default: true
15+
16+
jobs:
17+
release:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Docker Buildx
24+
uses: docker/setup-buildx-action@v3
25+
26+
- name: Log in to Docker Hub
27+
uses: docker/login-action@v3
28+
with:
29+
username: ${{ secrets.DOCKER_USERNAME }}
30+
password: ${{ secrets.DOCKER_TOKEN }}
31+
32+
- name: Log in to GitHub Container Registry
33+
uses: docker/login-action@v3
34+
with:
35+
registry: ghcr.io
36+
username: ${{ github.actor }}
37+
password: ${{ secrets.GITHUB_TOKEN }}
38+
39+
- name: Determine version
40+
id: version
41+
run: |
42+
if [ -n "${{ inputs.version }}" ]; then
43+
VERSION="${{ inputs.version }}"
44+
else
45+
VERSION=$(grep '__version__ =' agent_memory_server/__init__.py | sed 's/__version__ = "\(.*\)"/\1/' || echo "latest")
46+
fi
47+
echo "version=$VERSION" >> $GITHUB_OUTPUT
48+
echo "Version to release: $VERSION"
49+
50+
- name: Build tags list
51+
id: tags
52+
run: |
53+
TAGS="redislabs/agent-memory-server:${{ steps.version.outputs.version }}"
54+
TAGS="$TAGS,ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}"
55+
56+
if [ "${{ inputs.push_latest }}" = "true" ]; then
57+
TAGS="$TAGS,redislabs/agent-memory-server:latest"
58+
TAGS="$TAGS,ghcr.io/${{ github.repository }}:latest"
59+
fi
60+
61+
echo "tags=$TAGS" >> $GITHUB_OUTPUT
62+
echo "Tags to push: $TAGS"
63+
64+
- name: Build and push Docker image
65+
uses: docker/build-push-action@v5
66+
with:
67+
context: .
68+
file: ./Dockerfile
69+
platforms: linux/amd64,linux/arm64
70+
push: true
71+
tags: ${{ steps.tags.outputs.tags }}
72+
cache-from: type=gha
73+
cache-to: type=gha,mode=max
74+
75+
- name: Create GitHub Release
76+
uses: actions/create-release@v1
77+
env:
78+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
79+
with:
80+
tag_name: v${{ steps.version.outputs.version }}
81+
release_name: Release v${{ steps.version.outputs.version }}
82+
body: |
83+
Docker images published:
84+
- `redislabs/agent-memory-server:${{ steps.version.outputs.version }}`
85+
- `ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }}`
86+
${{ inputs.push_latest && format('- `redislabs/agent-memory-server:latest`{0}- `ghcr.io/{1}:latest`', '\n ', github.repository) || '' }}
87+
draft: false
88+
prerelease: false

agent-memory-client/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ working_memory = WorkingMemory(
100100
messages=[
101101
MemoryMessage(role="user", content="Hello!"),
102102
MemoryMessage(role="assistant", content="Hi there! How can I help?")
103+
# created_at timestamps are automatically set for proper chronological ordering
103104
],
104105
namespace="chat-app"
105106
)

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.1"
8+
__version__ = "0.12.2"
99

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

agent-memory-client/agent_memory_client/client.py

Lines changed: 129 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717
from pydantic import BaseModel
1818
from ulid import ULID
1919

20-
from .exceptions import MemoryClientError, MemoryServerError, MemoryValidationError
20+
from .exceptions import (
21+
MemoryClientError,
22+
MemoryNotFoundError,
23+
MemoryServerError,
24+
MemoryValidationError,
25+
)
2126
from .filters import (
2227
CreatedAt,
2328
Entities,
@@ -364,8 +369,15 @@ async def get_or_create_working_memory(
364369
return (True, created_memory)
365370

366371
return (False, existing_memory)
367-
except httpx.HTTPStatusError as e:
368-
if e.response.status_code == 404:
372+
except (httpx.HTTPStatusError, MemoryNotFoundError) as e:
373+
# Handle both HTTPStatusError and MemoryNotFoundError for 404s
374+
is_404 = False
375+
if isinstance(e, httpx.HTTPStatusError):
376+
is_404 = e.response.status_code == 404
377+
elif isinstance(e, MemoryNotFoundError):
378+
is_404 = True
379+
380+
if is_404:
369381
# Session doesn't exist, create it
370382
empty_memory = WorkingMemory(
371383
session_id=session_id,
@@ -885,14 +897,6 @@ async def search_long_term_memory(
885897
)
886898
response.raise_for_status()
887899
data = response.json()
888-
# Some tests may stub json() as an async function; handle awaitable
889-
try:
890-
import inspect
891-
892-
if inspect.isawaitable(data):
893-
data = await data
894-
except Exception:
895-
pass
896900
return MemoryRecordResults(**data)
897901
except httpx.HTTPStatusError as e:
898902
self._handle_http_error(e.response)
@@ -1477,8 +1481,8 @@ def get_add_memory_tool_schema(cls) -> dict[str, Any]:
14771481
},
14781482
"memory_type": {
14791483
"type": "string",
1480-
"enum": ["episodic", "semantic", "message"],
1481-
"description": "Type of memory: 'episodic' (events/experiences), 'semantic' (facts/preferences), 'message' (conversation snippets)",
1484+
"enum": ["episodic", "semantic"],
1485+
"description": "Type of memory: 'episodic' (events/experiences), 'semantic' (facts/preferences)",
14821486
},
14831487
"topics": {
14841488
"type": "array",
@@ -1595,8 +1599,8 @@ def edit_long_term_memory_tool_schema(cls) -> dict[str, Any]:
15951599
},
15961600
"memory_type": {
15971601
"type": "string",
1598-
"enum": ["episodic", "semantic", "message"],
1599-
"description": "Updated memory type: 'episodic' (events/experiences), 'semantic' (facts/preferences), 'message' (conversation snippets)",
1602+
"enum": ["episodic", "semantic"],
1603+
"description": "Updated memory type: 'episodic' (events/experiences), 'semantic' (facts/preferences)",
16001604
},
16011605
"namespace": {
16021606
"type": "string",
@@ -1620,6 +1624,67 @@ def edit_long_term_memory_tool_schema(cls) -> dict[str, Any]:
16201624
},
16211625
}
16221626

1627+
@classmethod
1628+
def create_long_term_memory_tool_schema(cls) -> dict[str, Any]:
1629+
"""
1630+
Get OpenAI-compatible tool schema for creating long-term memories directly.
1631+
1632+
Returns:
1633+
Tool schema dictionary compatible with OpenAI tool calling format
1634+
"""
1635+
return {
1636+
"type": "function",
1637+
"function": {
1638+
"name": "create_long_term_memory",
1639+
"description": (
1640+
"Create long-term memories directly for immediate storage and retrieval. "
1641+
"Use this for important information that should be permanently stored without going through working memory. "
1642+
"This is the 'eager' approach - memories are created immediately in long-term storage. "
1643+
"Examples: User preferences, important facts, key events that need to be searchable right away. "
1644+
"For episodic memories, include event_date in ISO format."
1645+
),
1646+
"parameters": {
1647+
"type": "object",
1648+
"properties": {
1649+
"memories": {
1650+
"type": "array",
1651+
"items": {
1652+
"type": "object",
1653+
"properties": {
1654+
"text": {
1655+
"type": "string",
1656+
"description": "The memory content to store",
1657+
},
1658+
"memory_type": {
1659+
"type": "string",
1660+
"enum": ["episodic", "semantic"],
1661+
"description": "Type of memory: 'episodic' (events/experiences), 'semantic' (facts/preferences)",
1662+
},
1663+
"topics": {
1664+
"type": "array",
1665+
"items": {"type": "string"},
1666+
"description": "Optional topics for categorization",
1667+
},
1668+
"entities": {
1669+
"type": "array",
1670+
"items": {"type": "string"},
1671+
"description": "Optional entities mentioned in the memory",
1672+
},
1673+
"event_date": {
1674+
"type": "string",
1675+
"description": "Optional event date for episodic memories (ISO 8601 format: '2024-01-15T14:30:00Z')",
1676+
},
1677+
},
1678+
"required": ["text", "memory_type"],
1679+
},
1680+
"description": "List of memories to create",
1681+
},
1682+
},
1683+
"required": ["memories"],
1684+
},
1685+
},
1686+
}
1687+
16231688
@classmethod
16241689
def delete_long_term_memories_tool_schema(cls) -> dict[str, Any]:
16251690
"""
@@ -1674,6 +1739,7 @@ def get_all_memory_tool_schemas(cls) -> Sequence[dict[str, Any]]:
16741739
cls.get_add_memory_tool_schema(),
16751740
cls.get_update_memory_data_tool_schema(),
16761741
cls.get_long_term_memory_tool_schema(),
1742+
cls.create_long_term_memory_tool_schema(),
16771743
cls.edit_long_term_memory_tool_schema(),
16781744
cls.delete_long_term_memories_tool_schema(),
16791745
cls.get_current_datetime_tool_schema(),
@@ -1706,6 +1772,7 @@ def get_all_memory_tool_schemas_anthropic(cls) -> Sequence[dict[str, Any]]:
17061772
cls.get_add_memory_tool_schema_anthropic(),
17071773
cls.get_update_memory_data_tool_schema_anthropic(),
17081774
cls.get_long_term_memory_tool_schema_anthropic(),
1775+
cls.create_long_term_memory_tool_schema_anthropic(),
17091776
cls.edit_long_term_memory_tool_schema_anthropic(),
17101777
cls.delete_long_term_memories_tool_schema_anthropic(),
17111778
cls.get_current_datetime_tool_schema_anthropic(),
@@ -1764,6 +1831,12 @@ def get_long_term_memory_tool_schema_anthropic(cls) -> dict[str, Any]:
17641831
openai_schema = cls.get_long_term_memory_tool_schema()
17651832
return cls._convert_openai_to_anthropic_schema(openai_schema)
17661833

1834+
@classmethod
1835+
def create_long_term_memory_tool_schema_anthropic(cls) -> dict[str, Any]:
1836+
"""Get create long-term memory tool schema in Anthropic format."""
1837+
openai_schema = cls.create_long_term_memory_tool_schema()
1838+
return cls._convert_openai_to_anthropic_schema(openai_schema)
1839+
17671840
@classmethod
17681841
def edit_long_term_memory_tool_schema_anthropic(cls) -> dict[str, Any]:
17691842
"""Get edit long-term memory tool schema in Anthropic format."""
@@ -2143,6 +2216,11 @@ async def resolve_function_call(
21432216
elif function_name == "get_long_term_memory":
21442217
result = await self._resolve_get_long_term_memory(args)
21452218

2219+
elif function_name == "create_long_term_memory":
2220+
result = await self._resolve_create_long_term_memory(
2221+
args, effective_namespace, user_id
2222+
)
2223+
21462224
elif function_name == "edit_long_term_memory":
21472225
result = await self._resolve_edit_long_term_memory(args)
21482226

@@ -2287,6 +2365,40 @@ async def _resolve_get_long_term_memory(
22872365
result = await self.get_long_term_memory(memory_id=memory_id)
22882366
return {"memory": result}
22892367

2368+
async def _resolve_create_long_term_memory(
2369+
self, args: dict[str, Any], namespace: str | None, user_id: str | None = None
2370+
) -> dict[str, Any]:
2371+
"""Resolve create_long_term_memory function call."""
2372+
memories_data = args.get("memories")
2373+
if not memories_data:
2374+
raise ValueError(
2375+
"memories parameter is required for create_long_term_memory"
2376+
)
2377+
2378+
# Convert dict memories to ClientMemoryRecord objects
2379+
from .models import ClientMemoryRecord, MemoryTypeEnum
2380+
2381+
memories = []
2382+
for memory_data in memories_data:
2383+
# Apply defaults
2384+
if namespace and "namespace" not in memory_data:
2385+
memory_data["namespace"] = namespace
2386+
if user_id and "user_id" not in memory_data:
2387+
memory_data["user_id"] = user_id
2388+
2389+
# Convert memory_type string to enum if needed
2390+
if "memory_type" in memory_data:
2391+
memory_data["memory_type"] = MemoryTypeEnum(memory_data["memory_type"])
2392+
2393+
memory = ClientMemoryRecord(**memory_data)
2394+
memories.append(memory)
2395+
2396+
result = await self.create_long_term_memory(memories)
2397+
return {
2398+
"status": result.status,
2399+
"message": f"Created {len(memories)} memories successfully",
2400+
}
2401+
22902402
async def _resolve_edit_long_term_memory(
22912403
self, args: dict[str, Any]
22922404
) -> dict[str, Any]:
@@ -2757,7 +2869,7 @@ async def memory_prompt(
27572869
context_window_max: int | None = None,
27582870
long_term_search: dict[str, Any] | None = None,
27592871
user_id: str | None = None,
2760-
optimize_query: bool = True,
2872+
optimize_query: bool = False,
27612873
) -> dict[str, Any]:
27622874
"""
27632875
Hydrate a user query with memory context and return a prompt ready to send to an LLM.
@@ -2861,7 +2973,7 @@ async def hydrate_memory_prompt(
28612973
memory_type: dict[str, Any] | None = None,
28622974
limit: int = 10,
28632975
offset: int = 0,
2864-
optimize_query: bool = True,
2976+
optimize_query: bool = False,
28652977
) -> dict[str, Any]:
28662978
"""
28672979
Hydrate a user query with long-term memory context using filters.

0 commit comments

Comments
 (0)