Skip to content

Commit 70964b0

Browse files
committed
Latest round of changes to support a vector store interface
1 parent d704a6e commit 70964b0

File tree

11 files changed

+888
-1022
lines changed

11 files changed

+888
-1022
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ docker-compose down # Stop all services
3939
IMPORTANT: This project uses `pre-commit`. You should run `pre-commit`
4040
before committing:
4141
```bash
42+
uv run pre-commit install # Install the hooks first
4243
uv run pre-commit run --all-files
4344
```
4445

agent_memory_server/api.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,13 +381,12 @@ async def search_long_term_memory(
381381
if not settings.long_term_memory:
382382
raise HTTPException(status_code=400, detail="Long-term memory is disabled")
383383

384-
redis = await get_redis_conn()
384+
await get_redis_conn()
385385

386386
# Extract filter objects from the payload
387387
filters = payload.get_filters()
388388

389389
kwargs = {
390-
"redis": redis,
391390
"distance_threshold": payload.distance_threshold,
392391
"limit": payload.limit,
393392
"offset": payload.offset,
@@ -397,7 +396,7 @@ async def search_long_term_memory(
397396
if payload.text:
398397
kwargs["text"] = payload.text
399398

400-
# Pass text, redis, and filter objects to the search function
399+
# Pass text and filter objects to the search function (no redis needed for vectorstore adapter)
401400
return await long_term_memory.search_long_term_memories(**kwargs)
402401

403402

agent_memory_server/config.py

Lines changed: 98 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from typing import Literal
2+
from typing import Any, Literal
33

44
import yaml
55
from dotenv import load_dotenv
@@ -9,12 +9,42 @@
99
load_dotenv()
1010

1111

12-
def load_yaml_settings():
13-
config_path = os.getenv("APP_CONFIG_FILE", "config.yaml")
14-
if os.path.exists(config_path):
15-
with open(config_path) as f:
16-
return yaml.safe_load(f) or {}
17-
return {}
12+
# Model configuration mapping
13+
MODEL_CONFIGS = {
14+
"gpt-4o": {"provider": "openai", "embedding_dimensions": None},
15+
"gpt-4o-mini": {"provider": "openai", "embedding_dimensions": None},
16+
"gpt-4": {"provider": "openai", "embedding_dimensions": None},
17+
"gpt-3.5-turbo": {"provider": "openai", "embedding_dimensions": None},
18+
"text-embedding-3-small": {"provider": "openai", "embedding_dimensions": 1536},
19+
"text-embedding-3-large": {"provider": "openai", "embedding_dimensions": 3072},
20+
"text-embedding-ada-002": {"provider": "openai", "embedding_dimensions": 1536},
21+
"claude-3-opus-20240229": {"provider": "anthropic", "embedding_dimensions": None},
22+
"claude-3-sonnet-20240229": {"provider": "anthropic", "embedding_dimensions": None},
23+
"claude-3-haiku-20240307": {"provider": "anthropic", "embedding_dimensions": None},
24+
"claude-3-5-sonnet-20240620": {
25+
"provider": "anthropic",
26+
"embedding_dimensions": None,
27+
},
28+
"claude-3-5-sonnet-20241022": {
29+
"provider": "anthropic",
30+
"embedding_dimensions": None,
31+
},
32+
"claude-3-5-haiku-20241022": {
33+
"provider": "anthropic",
34+
"embedding_dimensions": None,
35+
},
36+
"claude-3-7-sonnet-20250219": {
37+
"provider": "anthropic",
38+
"embedding_dimensions": None,
39+
},
40+
"claude-3-7-sonnet-latest": {"provider": "anthropic", "embedding_dimensions": None},
41+
"claude-3-5-sonnet-latest": {"provider": "anthropic", "embedding_dimensions": None},
42+
"claude-3-5-haiku-latest": {"provider": "anthropic", "embedding_dimensions": None},
43+
"claude-3-opus-latest": {"provider": "anthropic", "embedding_dimensions": None},
44+
"o1": {"provider": "openai", "embedding_dimensions": None},
45+
"o1-mini": {"provider": "openai", "embedding_dimensions": None},
46+
"o3-mini": {"provider": "openai", "embedding_dimensions": None},
47+
}
1848

1949

2050
class Settings(BaseSettings):
@@ -28,55 +58,19 @@ class Settings(BaseSettings):
2858
port: int = 8000
2959
mcp_port: int = 9000
3060

31-
# Long-term memory backend configuration
32-
long_term_memory_backend: str = (
33-
"redis" # redis, chroma, pinecone, weaviate, qdrant, etc.
61+
# Vector store factory configuration
62+
# Python dotted path to function that returns VectorStore or VectorStoreAdapter
63+
# Function signature: (embeddings: Embeddings) -> Union[VectorStore, VectorStoreAdapter]
64+
# Examples:
65+
# - "agent_memory_server.vectorstore_factory.create_redis_vectorstore"
66+
# - "my_module.my_vectorstore_factory"
67+
# - "my_package.adapters.create_custom_adapter"
68+
vectorstore_factory: str = (
69+
"agent_memory_server.vectorstore_factory.create_redis_vectorstore"
3470
)
3571

36-
# Redis backend settings (existing)
37-
# redis_url already defined above
38-
39-
# Chroma backend settings
40-
chroma_host: str = "localhost"
41-
chroma_port: int = 8000
42-
chroma_collection_name: str = "agent_memory"
43-
chroma_persist_directory: str | None = None
44-
45-
# Pinecone backend settings
46-
pinecone_api_key: str | None = None
47-
pinecone_environment: str | None = None
48-
pinecone_index_name: str = "agent-memory"
49-
50-
# Weaviate backend settings
51-
weaviate_url: str = "http://localhost:8080"
52-
weaviate_api_key: str | None = None
53-
weaviate_class_name: str = "AgentMemory"
54-
55-
# Qdrant backend settings
56-
qdrant_url: str = "http://localhost:6333"
57-
qdrant_api_key: str | None = None
58-
qdrant_collection_name: str = "agent_memory"
59-
60-
# Milvus backend settings
61-
milvus_host: str = "localhost"
62-
milvus_port: int = 19530
63-
milvus_collection_name: str = "agent_memory"
64-
milvus_user: str | None = None
65-
milvus_password: str | None = None
66-
67-
# PostgreSQL/PGVector backend settings
68-
postgres_url: str | None = None
69-
postgres_table_name: str = "agent_memory"
70-
71-
# LanceDB backend settings
72-
lancedb_uri: str = "./lancedb"
73-
lancedb_table_name: str = "agent_memory"
74-
75-
# OpenSearch backend settings
76-
opensearch_url: str = "http://localhost:9200"
77-
opensearch_username: str | None = None
78-
opensearch_password: str | None = None
79-
opensearch_index_name: str = "agent-memory"
72+
# RedisVL configuration (used by default Redis factory)
73+
redisvl_index_name: str = "memory_records"
8074

8175
# The server indexes messages in long-term memory by default. If this
8276
# setting is enabled, we also extract discrete memories from message text
@@ -95,10 +89,9 @@ class Settings(BaseSettings):
9589
ner_model: str = "dbmdz/bert-large-cased-finetuned-conll03-english"
9690
enable_ner: bool = True
9791

98-
# RedisVL Settings (kept for backwards compatibility)
92+
# RedisVL Settings
9993
redisvl_distance_metric: str = "COSINE"
10094
redisvl_vector_dimensions: str = "1536"
101-
redisvl_index_name: str = "memory_idx"
10295
redisvl_index_prefix: str = "memory_idx"
10396

10497
# Docket settings
@@ -122,8 +115,54 @@ class Settings(BaseSettings):
122115
class Config:
123116
env_file = ".env"
124117
env_file_encoding = "utf-8"
118+
extra = "ignore" # Ignore extra environment variables
119+
120+
@property
121+
def generation_model_config(self) -> dict[str, Any]:
122+
"""Get configuration for the generation model."""
123+
return MODEL_CONFIGS.get(self.generation_model, {})
124+
125+
@property
126+
def embedding_model_config(self) -> dict[str, Any]:
127+
"""Get configuration for the embedding model."""
128+
return MODEL_CONFIGS.get(self.embedding_model, {})
129+
130+
def load_yaml_config(self, config_path: str) -> dict[str, Any]:
131+
"""Load configuration from YAML file."""
132+
if not os.path.exists(config_path):
133+
return {}
134+
with open(config_path) as f:
135+
return yaml.safe_load(f) or {}
136+
137+
138+
settings = Settings()
139+
140+
141+
def get_config():
142+
"""Get configuration from environment and settings files."""
143+
config_data = {}
144+
145+
# If REDIS_MEMORY_CONFIG is set, load config from file
146+
config_file = os.getenv("REDIS_MEMORY_CONFIG")
147+
if config_file:
148+
try:
149+
with open(config_file) as f:
150+
if config_file.endswith((".yaml", ".yml")):
151+
config_data = yaml.safe_load(f) or {}
152+
else:
153+
# Assume JSON
154+
import json
155+
156+
config_data = json.load(f) or {}
157+
except FileNotFoundError:
158+
print(f"Warning: Config file {config_file} not found")
159+
except Exception as e:
160+
print(f"Warning: Error loading config file {config_file}: {e}")
125161

162+
# Environment variables override file config
163+
for key, value in os.environ.items():
164+
if key.startswith("REDIS_MEMORY_"):
165+
config_key = key[13:].lower() # Remove REDIS_MEMORY_ prefix
166+
config_data[config_key] = value
126167

127-
# Load YAML config first, then let env vars override
128-
yaml_settings = load_yaml_settings()
129-
settings = Settings(**yaml_settings)
168+
return config_data

agent_memory_server/filters.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,7 @@ def __init__(self, **data):
238238

239239
class EventDate(DateTimeFilter):
240240
field: str = "event_date"
241+
242+
243+
class MemoryHash(TagFilter):
244+
field: str = "memory_hash"

agent_memory_server/long_term_memory.py

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
Entities,
2020
EventDate,
2121
LastAccessed,
22+
MemoryHash,
2223
MemoryType,
2324
Namespace,
2425
SessionId,
@@ -683,7 +684,6 @@ async def index_long_term_memories(
683684

684685
async def search_long_term_memories(
685686
text: str,
686-
redis: Redis | None = None,
687687
session_id: SessionId | None = None,
688688
user_id: UserId | None = None,
689689
namespace: Namespace | None = None,
@@ -694,6 +694,7 @@ async def search_long_term_memories(
694694
distance_threshold: float | None = None,
695695
memory_type: MemoryType | None = None,
696696
event_date: EventDate | None = None,
697+
memory_hash: MemoryHash | None = None,
697698
limit: int = 10,
698699
offset: int = 0,
699700
) -> MemoryRecordResults:
@@ -713,6 +714,7 @@ async def search_long_term_memories(
713714
distance_threshold: Optional similarity threshold
714715
memory_type: Optional memory type filter
715716
event_date: Optional event date filter
717+
memory_hash: Optional memory hash filter
716718
limit: Maximum number of results
717719
offset: Offset for pagination
718720
@@ -734,6 +736,7 @@ async def search_long_term_memories(
734736
entities=entities,
735737
memory_type=memory_type,
736738
event_date=event_date,
739+
memory_hash=memory_hash,
737740
distance_threshold=distance_threshold,
738741
limit=limit,
739742
offset=offset,
@@ -793,7 +796,6 @@ async def search_memories(
793796
try:
794797
long_term_results = await search_long_term_memories(
795798
text=text,
796-
redis=redis,
797799
session_id=session_id,
798800
user_id=user_id,
799801
namespace=namespace,
@@ -994,49 +996,62 @@ async def deduplicate_by_hash(
994996
}
995997
)
996998

997-
# Build filters for the search
998-
filters = []
999-
if namespace or memory.namespace:
1000-
ns = namespace or memory.namespace
1001-
filters.append(f"@namespace:{{{ns}}}")
1002-
if user_id or memory.user_id:
1003-
uid = user_id or memory.user_id
1004-
filters.append(f"@user_id:{{{uid}}}")
1005-
if session_id or memory.session_id:
1006-
sid = session_id or memory.session_id
1007-
filters.append(f"@session_id:{{{sid}}}")
1008-
1009-
filter_str = " ".join(filters) if filters else ""
999+
# Use vectorstore adapter to search for memories with the same hash
1000+
try:
1001+
# Build filter objects
1002+
namespace_filter = None
1003+
if namespace or memory.namespace:
1004+
namespace_filter = Namespace(eq=namespace or memory.namespace)
1005+
1006+
user_id_filter = None
1007+
if user_id or memory.user_id:
1008+
user_id_filter = UserId(eq=user_id or memory.user_id)
1009+
1010+
session_id_filter = None
1011+
if session_id or memory.session_id:
1012+
session_id_filter = SessionId(eq=session_id or memory.session_id)
1013+
1014+
# Create memory hash filter
1015+
memory_hash_filter = MemoryHash(eq=memory_hash)
1016+
1017+
# Use vectorstore adapter to search for memories with the same hash
1018+
adapter = await get_vectorstore_adapter()
1019+
1020+
# Search for existing memories with the same hash
1021+
# Use a dummy query since we're filtering by hash, not doing semantic search
1022+
results = await adapter.search_memories(
1023+
query="", # Empty query since we're filtering by hash
1024+
session_id=session_id_filter,
1025+
user_id=user_id_filter,
1026+
namespace=namespace_filter,
1027+
memory_hash=memory_hash_filter,
1028+
limit=1, # We only need to know if one exists
1029+
)
10101030

1011-
# Search for existing memories with the same hash
1012-
index_name = Keys.search_index_name()
1031+
if results.memories and len(results.memories) > 0:
1032+
# Found existing memory with the same hash
1033+
logger.info(f"Found existing memory with hash {memory_hash}")
10131034

1014-
# Use FT.SEARCH to find memories with this hash
1015-
# TODO: Use RedisVL
1016-
search_query = (
1017-
f"FT.SEARCH {index_name} "
1018-
f"(@memory_hash:{{{memory_hash}}}) {filter_str} "
1019-
"RETURN 1 id_ "
1020-
"SORTBY last_accessed DESC" # Newest first
1021-
)
1035+
# Update the last_accessed timestamp of the existing memory
1036+
existing_memory = results.memories[0]
1037+
if existing_memory.id:
1038+
# Use the memory key format to update last_accessed
1039+
existing_key = Keys.memory_key(
1040+
existing_memory.id, existing_memory.namespace
1041+
)
1042+
await redis_client.hset(
1043+
existing_key,
1044+
"last_accessed",
1045+
str(int(datetime.now(UTC).timestamp())),
1046+
) # type: ignore
10221047

1023-
search_results = await redis_client.execute_command(search_query)
1048+
# Don't save this memory, it's a duplicate
1049+
return None, True
10241050

1025-
if search_results and search_results[0] > 0:
1026-
# Found existing memory with the same hash
1027-
logger.info(f"Found existing memory with hash {memory_hash}")
1028-
1029-
# Update the last_accessed timestamp of the existing memory
1030-
if search_results[0] >= 1:
1031-
existing_key = search_results[1].decode()
1032-
await redis_client.hset(
1033-
existing_key,
1034-
"last_accessed",
1035-
str(int(datetime.now(UTC).timestamp())),
1036-
) # type: ignore
1037-
1038-
# Don't save this memory, it's a duplicate
1039-
return None, True
1051+
except Exception as e:
1052+
logger.error(f"Error searching for hash duplicates using vectorstore: {e}")
1053+
# If search fails, proceed with the original memory
1054+
pass
10401055

10411056
# No duplicates found, return the original memory
10421057
return memory, False

0 commit comments

Comments
 (0)