Skip to content

Add a long-term memory interface #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jul 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ docker-compose down # Stop all services
IMPORTANT: This project uses `pre-commit`. You should run `pre-commit`
before committing:
```bash
uv run pre-commit install # Install the hooks first
uv run pre-commit run --all-files
```

Expand All @@ -68,7 +69,7 @@ Working Memory (Session-scoped) → Long-term Memory (Persistent)
```python
# Correct - Use RedisVL queries
from redisvl.query import VectorQuery, FilterQuery
query = VectorQuery(vector=embedding, vector_field_name="embedding", return_fields=["text"])
query = VectorQuery(vector=embedding, vector_field_name="vector", return_fields=["text"])

# Avoid - Direct redis client searches
# redis.ft().search(...) # Don't do this
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ A Redis-powered memory server built for AI agents and applications. It manages b
- **Long-Term Memory**

- Persistent storage for memories across sessions
- **Pluggable Vector Store Backends** - Support for multiple vector databases through LangChain VectorStore interface:
- **Redis** (default) - RedisStack with RediSearch
- **Chroma** - Open-source vector database
- **Pinecone** - Managed vector database service
- **Weaviate** - Open-source vector search engine
- **Qdrant** - Vector similarity search engine
- **Milvus** - Cloud-native vector database
- **PostgreSQL/PGVector** - PostgreSQL with vector extensions
- **LanceDB** - Embedded vector database
- **OpenSearch** - Open-source search and analytics suite
- Semantic search to retrieve memories with advanced filtering system
- Filter by session, namespace, topics, entities, timestamps, and more
- Supports both exact match and semantic similarity search
Expand Down Expand Up @@ -84,6 +94,8 @@ Configure servers and workers using environment variables. Includes background t

For complete configuration details, see [Configuration Guide](docs/configuration.md).

For vector store backend options and setup, see [Vector Store Backends](docs/vector-store-backends.md).

## License

Apache 2.0 License - see [LICENSE](LICENSE) file for details.
Expand Down
2 changes: 1 addition & 1 deletion agent-memory-client/agent_memory_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
memory management capabilities for AI agents and applications.
"""

__version__ = "0.9.0b4"
__version__ = "0.9.0b5"

from .client import MemoryAPIClient, MemoryClientConfig, create_memory_client
from .exceptions import (
Expand Down
81 changes: 57 additions & 24 deletions agent-memory-client/agent_memory_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

import asyncio
import re
from collections.abc import AsyncIterator
from collections.abc import AsyncIterator, Sequence
from typing import TYPE_CHECKING, Any, Literal, TypedDict

if TYPE_CHECKING:
from typing_extensions import Self

import httpx
import ulid
from pydantic import BaseModel
from ulid import ULID

from .exceptions import MemoryClientError, MemoryServerError, MemoryValidationError
from .filters import (
Expand Down Expand Up @@ -416,7 +416,7 @@ async def set_working_memory_data(
async def add_memories_to_working_memory(
self,
session_id: str,
memories: list[ClientMemoryRecord | MemoryRecord],
memories: Sequence[ClientMemoryRecord | MemoryRecord],
namespace: str | None = None,
replace: bool = False,
) -> WorkingMemoryResponse:
Expand Down Expand Up @@ -459,14 +459,14 @@ async def add_memories_to_working_memory(

# Determine final memories list
if replace or not existing_memory:
final_memories = memories
final_memories = list(memories)
else:
final_memories = existing_memory.memories + memories
final_memories = existing_memory.memories + list(memories)

# Auto-generate IDs for memories that don't have them
for memory in final_memories:
if not memory.id:
memory.id = str(ulid.ULID())
memory.id = str(ULID())

# Create new working memory with the memories
working_memory = WorkingMemory(
Expand All @@ -482,7 +482,7 @@ async def add_memories_to_working_memory(
return await self.put_working_memory(session_id, working_memory)

async def create_long_term_memory(
self, memories: list[ClientMemoryRecord | MemoryRecord]
self, memories: Sequence[ClientMemoryRecord | MemoryRecord]
) -> AckResponse:
"""
Create long-term memories for later retrieval.
Expand Down Expand Up @@ -541,6 +541,29 @@ async def create_long_term_memory(
self._handle_http_error(e.response)
raise

async def delete_long_term_memories(self, memory_ids: Sequence[str]) -> AckResponse:
"""
Delete long-term memories.

Args:
memory_ids: List of memory IDs to delete

Returns:
AckResponse indicating success
"""
params = {"memory_ids": list(memory_ids)}

try:
response = await self._client.delete(
"/v1/long-term-memory",
params=params,
)
response.raise_for_status()
return AckResponse(**response.json())
except httpx.HTTPStatusError as e:
self._handle_http_error(e.response)
raise

async def search_long_term_memory(
self,
text: str,
Expand Down Expand Up @@ -666,8 +689,8 @@ async def search_long_term_memory(
async def search_memory_tool(
self,
query: str,
topics: list[str] | None = None,
entities: list[str] | None = None,
topics: Sequence[str] | None = None,
entities: Sequence[str] | None = None,
memory_type: str | None = None,
max_results: int = 5,
min_relevance: float | None = None,
Expand Down Expand Up @@ -722,8 +745,8 @@ async def search_memory_tool(
from .filters import Entities, MemoryType, Topics

# Convert simple parameters to filter objects
topics_filter = Topics(any=topics) if topics else None
entities_filter = Entities(any=entities) if entities else None
topics_filter = Topics(any=list(topics)) if topics else None
entities_filter = Entities(any=list(entities)) if entities else None
memory_type_filter = MemoryType(eq=memory_type) if memory_type else None
user_id_filter = UserId(eq=user_id) if user_id else None

Expand Down Expand Up @@ -940,8 +963,8 @@ async def add_memory_tool(
session_id: str,
text: str,
memory_type: str,
topics: list[str] | None = None,
entities: list[str] | None = None,
topics: Sequence[str] | None = None,
entities: Sequence[str] | None = None,
namespace: str | None = None,
user_id: str | None = None,
) -> dict[str, Any]:
Expand Down Expand Up @@ -982,8 +1005,8 @@ async def add_memory_tool(
memory = ClientMemoryRecord(
text=text,
memory_type=MemoryTypeEnum(memory_type),
topics=topics,
entities=entities,
topics=list(topics) if topics else None,
entities=list(entities) if entities else None,
namespace=namespace or self.config.default_namespace,
user_id=user_id,
)
Expand Down Expand Up @@ -1172,7 +1195,7 @@ def get_update_memory_data_tool_schema(cls) -> dict[str, Any]:
}

@classmethod
def get_all_memory_tool_schemas(cls) -> list[dict[str, Any]]:
def get_all_memory_tool_schemas(cls) -> Sequence[dict[str, Any]]:
"""
Get all memory-related tool schemas for easy LLM integration.

Expand Down Expand Up @@ -1200,7 +1223,7 @@ def get_all_memory_tool_schemas(cls) -> list[dict[str, Any]]:
]

@classmethod
def get_all_memory_tool_schemas_anthropic(cls) -> list[dict[str, Any]]:
def get_all_memory_tool_schemas_anthropic(cls) -> Sequence[dict[str, Any]]:
"""
Get all memory-related tool schemas in Anthropic format.

Expand Down Expand Up @@ -1470,11 +1493,11 @@ async def resolve_tool_call(

async def resolve_tool_calls(
self,
tool_calls: list[dict[str, Any]],
tool_calls: Sequence[dict[str, Any]],
session_id: str,
namespace: str | None = None,
user_id: str | None = None,
) -> list[ToolCallResolutionResult]:
) -> Sequence[ToolCallResolutionResult]:
"""
Resolve multiple tool calls from any LLM provider format.

Expand Down Expand Up @@ -1713,11 +1736,11 @@ async def _resolve_update_memory_data(

async def resolve_function_calls(
self,
function_calls: list[dict[str, Any]],
function_calls: Sequence[dict[str, Any]],
session_id: str,
namespace: str | None = None,
user_id: str | None = None,
) -> list[ToolCallResolutionResult]:
) -> Sequence[ToolCallResolutionResult]:
"""
Resolve multiple function calls in batch.

Expand Down Expand Up @@ -1765,7 +1788,7 @@ async def resolve_function_calls(
async def promote_working_memories_to_long_term(
self,
session_id: str,
memory_ids: list[str] | None = None,
memory_ids: Sequence[str] | None = None,
namespace: str | None = None,
) -> AckResponse:
"""
Expand Down Expand Up @@ -1805,10 +1828,10 @@ async def promote_working_memories_to_long_term(

async def bulk_create_long_term_memories(
self,
memory_batches: list[list[ClientMemoryRecord | MemoryRecord]],
memory_batches: Sequence[Sequence[ClientMemoryRecord | MemoryRecord]],
batch_size: int = 100,
delay_between_batches: float = 0.1,
) -> list[AckResponse]:
) -> Sequence[AckResponse]:
"""
Create multiple batches of memories with proper rate limiting.

Expand Down Expand Up @@ -2104,6 +2127,8 @@ async def memory_prompt(
"""
Hydrate a user query with memory context and return a prompt ready to send to an LLM.

NOTE: `long_term_search` uses the same filter options as `search_long_term_memories`.

Args:
query: The input text to find relevant context for
session_id: Optional session ID to include session messages
Expand Down Expand Up @@ -2163,9 +2188,17 @@ async def memory_prompt(

# Add long-term search parameters if provided
if long_term_search is not None:
if "namespace" not in long_term_search:
if namespace is not None:
long_term_search["namespace"] = {"eq": namespace}
elif self.config.default_namespace is not None:
long_term_search["namespace"] = {
"eq": self.config.default_namespace
}
payload["long_term_search"] = long_term_search

try:
print("Payload: ", payload)
response = await self._client.post(
"/v1/memory/prompt",
json=payload,
Expand Down
4 changes: 2 additions & 2 deletions agent-memory-client/agent_memory_client/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from enum import Enum
from typing import Any, Literal, TypedDict

import ulid
from pydantic import BaseModel, Field
from ulid import ULID

# Model name literals for model-specific window sizes
ModelNameLiteral = Literal[
Expand Down Expand Up @@ -122,7 +122,7 @@ class ClientMemoryRecord(MemoryRecord):
"""A memory record with a client-provided ID"""

id: str = Field(
default_factory=lambda: str(ulid.ULID()),
default_factory=lambda: str(ULID()),
description="Client-provided ID generated by the client (ULID)",
)

Expand Down
2 changes: 1 addition & 1 deletion agent_memory_server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Redis Agent Memory Server - A memory system for conversational AI."""

__version__ = "0.9.0b4"
__version__ = "0.9.0b5"
Loading