Compare commits

..
Author SHA1 Message Date
kartik-mem0andClaude Opus 4.5 e3e73ffca4 fix(docs): update expiration-date redirect to proper destination
Update the redirect for /platform/features/expiration-date to point to
/cookbooks/essentials/memory-expiration-short-and-long-term instead of
the homepage, matching the link updates in this PR.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-13 20:50:10 +05:30
mintlify[bot] 313d7b1c1b Fix broken link to expiration-date page in delete.mdx
Generated-By: mintlify-agent
2026-04-12 00:09:19 +00:00
20 changed files with 126 additions and 308 deletions
-21
View File
@@ -50,25 +50,4 @@ Here are the parameters available for configuring Valkey:
| `hnsw_m` | Number of bi-directional links for HNSW | `16` |
| `hnsw_ef_construction` | Size of dynamic candidate list for HNSW | `200` |
| `hnsw_ef_runtime` | Size of dynamic candidate list for search | `10` |
| `cluster_mode` | Enable cluster mode for Valkey cluster (CME) deployments | `false` |
| `distance_metric` | Distance metric for vector similarity | `cosine` |
## Cluster Mode
To use Valkey with cluster mode enabled (CME), set `cluster_mode` to `true`:
```python
config = {
"vector_store": {
"provider": "valkey",
"config": {
"collection_name": "memories",
"valkey_url": "valkey://cluster-endpoint:6379",
"embedding_model_dims": 1536,
"cluster_mode": True
}
}
}
```
When cluster mode is enabled, the connector uses `ValkeyCluster` instead of the standalone client, which handles `MOVED`/`ASK` redirections automatically. Search queries are coordinated across all shards by the valkey-search module's built-in coordinator. See the [valkey-search documentation](https://github.com/valkey-io/valkey-search) for details on cluster mode behavior.
@@ -0,0 +1,70 @@
---
title: Browser Extension Memory
description: "Add Mem0's universal memory layer to Chrome chat surfaces."
---
Enhance your AI interactions with Mem0, a Chrome extension that introduces a universal memory layer across platforms like ChatGPT, Claude, and Perplexity. Mem0 ensures seamless context sharing, making your AI experiences more personalized and efficient.
<Note>
We now support Grok! The Mem0 Chrome Extension has been updated to work with Grok, bringing the same powerful memory capabilities to your Grok conversations.
</Note>
## Features
- **Universal Memory Layer**: Share context seamlessly across ChatGPT, Claude, Perplexity, and Grok.
- **Smart Context Detection**: Automatically captures relevant information from your conversations.
- **Intelligent Memory Retrieval**: Surfaces pertinent memories at the right time.
- **One-Click Sync**: Easily synchronize with existing ChatGPT memories.
- **Memory Dashboard**: Manage all your memories in one centralized location.
## Installation
You can install the Mem0 Chrome Extension using one of the following methods:
### Method 1: Chrome Web Store Installation
1. **Download the Extension**: Open Google Chrome and navigate to the [Mem0 Chrome Extension page](https://chromewebstore.google.com/detail/mem0/onihkkbipkfeijkadecaafbgagkhglop?hl=en).
2. **Add to Chrome**: Click on the "Add to Chrome" button.
3. **Confirm Installation**: In the pop-up dialog, click "Add extension" to confirm. The Mem0 icon should now appear in your Chrome toolbar.
### Method 2: Manual Installation
1. **Download the Extension**: Clone or download the extension files from the [Mem0 Chrome Extension GitHub repository](https://github.com/mem0ai/mem0-chrome-extension).
2. **Access Chrome Extensions**: Open Google Chrome and navigate to `chrome://extensions`.
3. **Enable Developer Mode**: Toggle the "Developer mode" switch in the top right corner.
4. **Load Unpacked Extension**: Click "Load unpacked" and select the directory containing the extension files.
5. **Confirm Installation**: The Mem0 Chrome Extension should now appear in your Chrome toolbar.
## Usage
1. **Locate the Mem0 Icon**: After installation, find the Mem0 icon in your Chrome toolbar.
2. **Sign In**: Click the icon and sign in with your Google account.
3. **Interact with AI Assistants**:
- **ChatGPT and Perplexity**: Continue your conversations as usual; Mem0 operates seamlessly in the background.
- **Claude**: Click the Mem0 button or use the shortcut `Ctrl + M` to activate memory functions.
## Configuration
- **API Key**: Obtain your API key from the Mem0 Dashboard to connect the extension to the Mem0 API.
- **User ID**: This is your unique identifier in the Mem0 system. If not provided, it defaults to `chrome-extension-user`.
## Demo Video
<iframe width="700" height="400" src="https://www.youtube.com/embed/dqenCMMlfwQ?si=zhGVrkq6IS_0Jwyj" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## Privacy and Data Security
Your messages are sent to the Mem0 API for extracting and retrieving memories. Mem0 is committed to ensuring your data's privacy and security.
---
<CardGroup cols={2}>
<Card title="Build a Mem0 Companion" icon="users" href="/cookbooks/essentials/building-ai-companion">
Learn the foundations of memory-powered assistants that work across platforms.
</Card>
<Card title="Multimodal Support" icon="image" href="/platform/features/multimodal-support">
Extend your browser interactions with vision and audio memory.
</Card>
</CardGroup>
+4 -1
View File
@@ -201,7 +201,10 @@ Here are some examples of how Mem0 can be integrated into various applications:
>
Persistent personality for Eliza agents.
</Card>
</CardGroup>
<Card title="Browser Extension Memory" icon="globe" href="/cookbooks/frameworks/chrome-extension">
Universal memory layer for Chrome.
</Card>
</CardGroup>
---
+4 -7
View File
@@ -373,6 +373,7 @@
"cookbooks/frameworks/llamaindex-multiagent",
"cookbooks/frameworks/multimodal-retrieval",
"cookbooks/frameworks/eliza-os-character",
"cookbooks/frameworks/chrome-extension",
"cookbooks/frameworks/gemini-3-with-mem0-mcp",
"cookbooks/frameworks/mirofish-swarm-memory"
]
@@ -621,7 +622,7 @@
},
{
"source": "/platform/features/expiration-date",
"destination": "/"
"destination": "/cookbooks/essentials/memory-expiration-short-and-long-term"
},
{
"source": "/platform/features/async-mode-default-change",
@@ -797,11 +798,7 @@
},
{
"source": "/examples/chrome-extension",
"destination": "/cookbooks/overview"
},
{
"source": "/cookbooks/frameworks/chrome-extension",
"destination": "/cookbooks/overview"
"destination": "/cookbooks/frameworks/chrome-extension"
},
{
"source": "/examples",
@@ -841,7 +838,7 @@
},
{
"source": "/v0x/examples/chrome-extension",
"destination": "/cookbooks/overview"
"destination": "/cookbooks/frameworks/chrome-extension"
},
{
"source": "/v0x/examples/youtube-assistant",
+1
View File
@@ -245,6 +245,7 @@ Key differentiators:
- [LlamaIndex Multiagent](https://docs.mem0.ai/cookbooks/frameworks/llamaindex-multiagent): Multi-agent systems with shared memory
- [Multimodal Retrieval](https://docs.mem0.ai/cookbooks/frameworks/multimodal-retrieval): Memory systems handling text, images, and documents
- [Eliza OS Character](https://docs.mem0.ai/cookbooks/frameworks/eliza-os-character): Character-based AI with persistent personality
- [Chrome Extension](https://docs.mem0.ai/cookbooks/frameworks/chrome-extension): Browser extensions that remember user interactions
- [Gemini with Mem0 MCP](https://docs.mem0.ai/cookbooks/frameworks/gemini-3-with-mem0-mcp): Google Gemini integration using MCP server
- [Mirofish Swarm Memory](https://docs.mem0.ai/cookbooks/frameworks/mirofish-swarm-memory): Swarm-based multi-agent memory patterns
+2 -2
View File
@@ -8,6 +8,6 @@
},
"homepage": "https://mem0.ai",
"repository": "https://github.com/mem0ai/mem0",
"license": "Apache-2.0",
"keywords": ["memory", "personalization", "mcp", "semantic-search"]
"logo": "logo.svg",
"license": "Apache-2.0"
}
+1
View File
@@ -1,4 +1,5 @@
{
"description": "Mem0 memory capture hooks — automatic memory extraction at key lifecycle points",
"hooks": {
"SessionStart": [
{
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "3.0.0-beta.0",
"version": "2.4.6",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
-1
View File
@@ -110,7 +110,6 @@ export class ConfigManager {
((userConf as Record<string, unknown>)?.lmstudio_base_url as
| string
| undefined) ??
userConf?.url ??
defaultConf.baseURL;
return {
@@ -127,20 +127,6 @@ describe("ConfigManager", () => {
expect(config.llm.config.url).toBe("http://fallback:11434");
});
it("should use url as baseURL fallback when no baseURL provided (issue #4715)", () => {
const config = ConfigManager.mergeConfig({
embedder: baseEmbedder,
vectorStore: baseVectorStore,
llm: {
provider: "ollama",
config: { model: "llama3.1:8b", url: "http://my-ollama-host:11434" },
},
});
expect(config.llm.config.baseURL).toBe("http://my-ollama-host:11434");
expect(config.llm.config.url).toBe("http://my-ollama-host:11434");
});
it("should use default baseURL when no url or baseURL provided", () => {
const config = ConfigManager.mergeConfig({
embedder: baseEmbedder,
+1 -6
View File
@@ -29,7 +29,7 @@ class OpenAIConfig(BaseLlmConfig):
openrouter_base_url: Optional[str] = None,
site_url: Optional[str] = None,
app_name: Optional[str] = None,
store: Optional[bool] = None,
store: bool = False,
# Response monitoring callback
response_callback: Optional[Callable[[Any, dict, dict], None]] = None,
):
@@ -53,11 +53,6 @@ class OpenAIConfig(BaseLlmConfig):
openrouter_base_url: OpenRouter base URL, defaults to None
site_url: Site URL for OpenRouter, defaults to None
app_name: Application name for OpenRouter, defaults to None
store: Whether to store the conversation on OpenAI's server. Opt-in;
defaults to None (not sent). Set to True or False only if you
want the value forwarded to the OpenAI API. Leaving it None
avoids leaking the field into OpenAI-compatible backends that
reject unknown fields (Gemini, Groq, vLLM, etc.).
response_callback: Optional callback for monitoring LLM responses.
"""
# Initialize base parameters
-1
View File
@@ -14,7 +14,6 @@ class ValkeyConfig(BaseModel):
hnsw_m: int = Field(16, description="HNSW: number of connections per layer")
hnsw_ef_construction: int = Field(200, description="HNSW: search width during index construction")
hnsw_ef_runtime: int = Field(10, description="HNSW: search width during queries")
cluster_mode: bool = Field(False, description="Enable cluster mode for Valkey cluster (CME) deployments")
@model_validator(mode="before")
@classmethod
+5 -6
View File
@@ -126,12 +126,11 @@ class OpenAILLM(LLMBase):
params.update(**openrouter_params)
else:
# Only send OpenAI-specific parameters when the user has explicitly
# configured them. OpenAI-compatible backends (Gemini, Groq, vLLM, etc.)
# reject unknown fields, so `store` must be opt-in, not opt-out.
if self.config.store is not None:
params["store"] = self.config.store
openai_specific_generation_params = ["store"]
for param in openai_specific_generation_params:
if hasattr(self.config, param):
params[param] = getattr(self.config, param)
if response_format:
params["response_format"] = response_format
if tools: # TODO: Remove tools if no issues found with new memory addition logic
@@ -6,7 +6,7 @@ from mem0.configs.rerankers.base import BaseRerankerConfig
from mem0.configs.rerankers.sentence_transformer import SentenceTransformerRerankerConfig
try:
from sentence_transformers import CrossEncoder
from sentence_transformers import SentenceTransformer
SENTENCE_TRANSFORMERS_AVAILABLE = True
except ImportError:
SENTENCE_TRANSFORMERS_AVAILABLE = False
@@ -41,7 +41,7 @@ class SentenceTransformerReranker(BaseReranker):
)
self.config = config
self.model = CrossEncoder(self.config.model, device=self.config.device)
self.model = SentenceTransformer(self.config.model, device=self.config.device)
def rerank(self, query: str, documents: List[Dict[str, Any]], top_k: int = None) -> List[Dict[str, Any]]:
"""
@@ -74,11 +74,8 @@ class SentenceTransformerReranker(BaseReranker):
# Create query-document pairs
pairs = [[query, doc_text] for doc_text in doc_texts]
scores = self.model.predict(
pairs,
batch_size=self.config.batch_size,
show_progress_bar=self.config.show_progress_bar,
)
# Get similarity scores
scores = self.model.predict(pairs)
if isinstance(scores, np.ndarray):
scores = scores.tolist()
+1 -31
View File
@@ -122,37 +122,7 @@ class S3Vectors(VectorStoreBase):
)
def update(self, vector_id, vector=None, payload=None):
# S3 Vectors uses put_vectors for updates (overwrite).
# When vector=None (e.g. metadata-only update triggered by event=NONE),
# fetch the existing vector data first to avoid passing None to boto3
# which causes a parameter validation error:
# "Invalid type for parameter vectors[0].data.float32, value: None"
if vector is None:
existing = self.get(vector_id)
if existing is None:
logger.warning(f"update called with vector=None but {vector_id} not found; skipping")
return
try:
response = self.client.get_vectors(
vectorBucketName=self.vector_bucket_name,
indexName=self.collection_name,
keys=[vector_id],
returnData=True,
returnMetadata=True,
)
vectors = response.get("vectors", [])
if not vectors:
logger.warning(f"update: no vector data found for {vector_id}; skipping")
return
vector = vectors[0].get("data", {}).get("float32")
if vector is None:
logger.warning(f"update: float32 data is None for {vector_id}; skipping")
return
if payload is None:
payload = existing.payload
except Exception as e:
logger.error(f"update: failed to fetch existing vector for {vector_id}: {e}")
return
# S3 Vectors uses put_vectors for updates (overwrite)
self.insert(vectors=[vector], payloads=[payload], ids=[vector_id])
def get(self, vector_id) -> Optional[OutputData]:
+3 -13
View File
@@ -52,7 +52,6 @@ class ValkeyDB(VectorStoreBase):
hnsw_m: int = 16,
hnsw_ef_construction: int = 200,
hnsw_ef_runtime: int = 10,
cluster_mode: bool = False,
):
"""
Initialize the Valkey vector store.
@@ -66,7 +65,6 @@ class ValkeyDB(VectorStoreBase):
hnsw_m (int, optional): HNSW M parameter (connections per node). Defaults to 16.
hnsw_ef_construction (int, optional): HNSW ef_construction parameter. Defaults to 200.
hnsw_ef_runtime (int, optional): HNSW ef_runtime parameter. Defaults to 10.
cluster_mode (bool, optional): Enable cluster mode for Valkey cluster (CME) deployments. Defaults to False.
"""
self.embedding_model_dims = embedding_model_dims
self.collection_name = collection_name
@@ -76,7 +74,6 @@ class ValkeyDB(VectorStoreBase):
self.hnsw_m = hnsw_m
self.hnsw_ef_construction = hnsw_ef_construction
self.hnsw_ef_runtime = hnsw_ef_runtime
self.cluster_mode = cluster_mode
# Validate index type
if self.index_type not in ["hnsw", "flat"]:
@@ -84,13 +81,8 @@ class ValkeyDB(VectorStoreBase):
# Connect to Valkey
try:
if self.cluster_mode:
from valkey.cluster import ValkeyCluster
self.client = ValkeyCluster.from_url(valkey_url)
else:
self.client = valkey.from_url(valkey_url)
logger.debug(f"Successfully connected to Valkey at {valkey_url} (cluster_mode={cluster_mode})")
self.client = valkey.from_url(valkey_url)
logger.debug(f"Successfully connected to Valkey at {valkey_url}")
except Exception as e:
logger.exception(f"Failed to connect to Valkey at {valkey_url}: {e}")
raise
@@ -193,6 +185,7 @@ class ValkeyDB(VectorStoreBase):
"""
# Check if the search module is available
try:
# Try to execute a search command
self.client.execute_command("FT._LIST")
except ResponseError as e:
if "unknown command" in str(e).lower():
@@ -360,9 +353,6 @@ class ValkeyDB(VectorStoreBase):
"""
Execute a search query.
In cluster mode, the valkey-search module's built-in coordinator handles
fan-out across all shards and aggregates results server-side.
Args:
query (str): The search query to execute.
params (dict): The query parameters.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0ai"
version = "2.0.0b0"
version = "1.0.11"
description = "Long-term memory for AI Agents"
authors = [
{ name = "Mem0", email = "support@mem0.ai" }
+2 -56
View File
@@ -55,7 +55,7 @@ def test_generate_response_without_tools(mock_openai_client):
response = llm.generate_response(messages)
mock_openai_client.chat.completions.create.assert_called_once_with(
model="gpt-4.1-nano-2025-04-14", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0
model="gpt-4.1-nano-2025-04-14", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0, store=False
)
assert response == "I'm doing well, thank you for asking!"
@@ -97,7 +97,7 @@ def test_generate_response_with_tools(mock_openai_client):
response = llm.generate_response(messages, tools=tools)
mock_openai_client.chat.completions.create.assert_called_once_with(
model="gpt-4.1-nano-2025-04-14", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0, tools=tools, tool_choice="auto"
model="gpt-4.1-nano-2025-04-14", messages=messages, temperature=0.7, max_tokens=100, top_p=1.0, tools=tools, tool_choice="auto", store=False
)
assert response["content"] == "I've added the memory for you."
@@ -231,60 +231,6 @@ def test_reasoning_effort_config_values():
assert config.reasoning_effort is None
def test_store_not_sent_by_default(mock_openai_client):
"""`store` must NOT be injected into requests when the user has not
explicitly configured it. Regression test for issue #4709, where
`store=False` was unconditionally sent and rejected by OpenAI-compatible
backends such as Google Gemini."""
config = OpenAIConfig(model="gpt-4.1-nano-2025-04-14", temperature=0.1)
assert config.store is None # new opt-in default
llm = OpenAILLM(config)
messages = [{"role": "user", "content": "Hello"}]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Response"))]
mock_openai_client.chat.completions.create.return_value = mock_response
llm.generate_response(messages)
call_kwargs = mock_openai_client.chat.completions.create.call_args.kwargs
assert "store" not in call_kwargs
def test_store_sent_when_explicitly_true(mock_openai_client):
"""When the user explicitly sets `store=True`, the field must be forwarded."""
config = OpenAIConfig(model="gpt-4.1-nano-2025-04-14", store=True)
llm = OpenAILLM(config)
messages = [{"role": "user", "content": "Hello"}]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Response"))]
mock_openai_client.chat.completions.create.return_value = mock_response
llm.generate_response(messages)
call_kwargs = mock_openai_client.chat.completions.create.call_args.kwargs
assert call_kwargs["store"] is True
def test_store_sent_when_explicitly_false(mock_openai_client):
"""When the user explicitly sets `store=False`, the field must still be
forwarded — explicit opt-out is a valid configuration for users who rely on
it for OpenAI's zero-data-retention behavior."""
config = OpenAIConfig(model="gpt-4.1-nano-2025-04-14", store=False)
llm = OpenAILLM(config)
messages = [{"role": "user", "content": "Hello"}]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Response"))]
mock_openai_client.chat.completions.create.return_value = mock_response
llm.generate_response(messages)
call_kwargs = mock_openai_client.chat.completions.create.call_args.kwargs
assert call_kwargs["store"] is False
def test_callback_with_tools(mock_openai_client):
mock_callback = Mock()
config = OpenAIConfig(model="gpt-4.1-nano-2025-04-14", response_callback=mock_callback)
+20 -20
View File
@@ -2,7 +2,7 @@
Verifies that the Pydantic request models in server/main.py correctly accept
and forward all parameters supported by the underlying Memory class methods,
including top_k, threshold, infer, memory_type, and prompt — which were
including limit, threshold, infer, memory_type, and prompt — which were
previously silently dropped by Pydantic v2's default extra='ignore' behavior.
"""
@@ -55,31 +55,31 @@ def mock_memory(_mock_memory):
# ===========================================================================
# SearchRequest: top_k parameter
# SearchRequest: limit parameter
# ===========================================================================
class TestSearchLimit:
"""Verify that the top_k parameter is accepted and forwarded to Memory.search()."""
"""Verify that the limit parameter is accepted and forwarded to Memory.search()."""
def test_limit_forwarded(self, client, mock_memory):
resp = client.post("/search", json={"query": "food", "user_id": "u1", "top_k": 5})
resp = client.post("/search", json={"query": "food", "user_id": "u1", "limit": 5})
assert resp.status_code == 200
_, kwargs = mock_memory.search.call_args
assert kwargs["top_k"] == 5
assert kwargs["limit"] == 5
def test_limit_one(self, client, mock_memory):
resp = client.post("/search", json={"query": "food", "user_id": "u1", "top_k": 1})
resp = client.post("/search", json={"query": "food", "user_id": "u1", "limit": 1})
assert resp.status_code == 200
_, kwargs = mock_memory.search.call_args
assert kwargs["top_k"] == 1
assert kwargs["limit"] == 1
def test_limit_omitted_uses_memory_default(self, client, mock_memory):
"""When top_k is not sent, it should not appear in the kwargs,
"""When limit is not sent, it should not appear in the kwargs,
allowing Memory.search() to use its own default (100)."""
resp = client.post("/search", json={"query": "food", "user_id": "u1"})
assert resp.status_code == 200
_, kwargs = mock_memory.search.call_args
assert "top_k" not in kwargs
assert "limit" not in kwargs
# ===========================================================================
@@ -110,18 +110,18 @@ class TestSearchThreshold:
# ===========================================================================
# SearchRequest: top_k + threshold together
# SearchRequest: limit + threshold together
# ===========================================================================
class TestSearchLimitAndThreshold:
def test_both_forwarded(self, client, mock_memory):
resp = client.post("/search", json={
"query": "food", "user_id": "u1", "top_k": 10, "threshold": 0.5
"query": "food", "user_id": "u1", "limit": 10, "threshold": 0.5
})
assert resp.status_code == 200
_, kwargs = mock_memory.search.call_args
assert kwargs["top_k"] == 10
assert kwargs["limit"] == 10
assert kwargs["threshold"] == 0.5
@@ -334,8 +334,8 @@ class TestOpenAPISchema:
def test_search_schema_includes_limit(self, client):
schema = client.get("/openapi.json").json()
search_props = schema["components"]["schemas"]["SearchRequest"]["properties"]
assert "top_k" in search_props
assert search_props["top_k"]["description"] == "Maximum number of results to return."
assert "limit" in search_props
assert search_props["limit"]["description"] == "Maximum number of results to return."
def test_search_schema_includes_threshold(self, client):
schema = client.get("/openapi.json").json()
@@ -367,7 +367,7 @@ class TestTypeValidation:
def test_limit_string_rejected(self, client):
resp = client.post("/search", json={
"query": "food", "user_id": "u1", "top_k": "not_a_number",
"query": "food", "user_id": "u1", "limit": "not_a_number",
})
assert resp.status_code == 422
@@ -399,7 +399,7 @@ class TestTypeValidation:
def test_limit_float_rejected(self, client):
resp = client.post("/search", json={
"query": "food", "user_id": "u1", "top_k": 5.7,
"query": "food", "user_id": "u1", "limit": 5.7,
})
assert resp.status_code == 422
@@ -422,11 +422,11 @@ class TestExplicitNull:
def test_limit_null_uses_memory_default(self, client, mock_memory):
resp = client.post("/search", json={
"query": "food", "user_id": "u1", "top_k": None,
"query": "food", "user_id": "u1", "limit": None,
})
assert resp.status_code == 200
_, kwargs = mock_memory.search.call_args
assert "top_k" not in kwargs
assert "limit" not in kwargs
def test_infer_null_uses_memory_default(self, client, mock_memory):
resp = client.post("/memories", json={
@@ -462,12 +462,12 @@ class TestCallSignatureMatch:
resp = client.post("/search", json={
"query": "food", "user_id": "u1", "agent_id": "a1",
"run_id": "r1", "filters": {"k": "v"},
"top_k": 10, "threshold": 0.5,
"limit": 10, "threshold": 0.5,
})
assert resp.status_code == 200
# The handler passes query= as a keyword arg, so it appears in kwargs too
_, kwargs = mock_memory.search.call_args
valid_params = {"query", "user_id", "agent_id", "run_id", "top_k", "filters", "threshold", "rerank"}
valid_params = {"query", "user_id", "agent_id", "run_id", "limit", "filters", "threshold", "rerank"}
for key in kwargs:
assert key in valid_params, f"Unexpected kwarg '{key}' forwarded to Memory.search()"
+6 -120
View File
@@ -222,7 +222,9 @@ def test_update_with_none_vector_preserves_embedding(valkey_db, mock_valkey_clie
mock_valkey_client.hset.assert_called_once()
args, kwargs = mock_valkey_client.hset.call_args
assert "embedding" not in kwargs["mapping"], "embedding should not be in hash_data when vector is None"
assert "embedding" not in kwargs["mapping"], (
"embedding should not be in hash_data when vector is None"
)
assert kwargs["mapping"]["memory_id"] == "test_id"
assert kwargs["mapping"]["memory"] == "updated_data"
@@ -241,7 +243,9 @@ def test_update_with_vector_includes_embedding(valkey_db, mock_valkey_client):
mock_valkey_client.hset.assert_called_once()
args, kwargs = mock_valkey_client.hset.call_args
assert "embedding" in kwargs["mapping"], "embedding should be in hash_data when vector is provided"
assert "embedding" in kwargs["mapping"], (
"embedding should be in hash_data when vector is provided"
)
expected_bytes = np.array(vector, dtype=np.float32).tobytes()
assert kwargs["mapping"]["embedding"] == expected_bytes
@@ -902,121 +906,3 @@ def test_list_with_missing_fields_and_defaults(valkey_db, mock_valkey_client):
assert result.id == "fallback_id"
assert "hash" in result.payload
assert "data" in result.payload # memory is renamed to data
# Cluster mode tests
@pytest.fixture
def mock_valkey_cluster_client():
"""Create a mock ValkeyCluster client."""
with patch("valkey.cluster.ValkeyCluster.from_url") as mock_from_url:
mock_client = MagicMock()
mock_ft = MagicMock()
mock_client.ft = MagicMock(return_value=mock_ft)
mock_client.execute_command = MagicMock()
mock_client.hset = MagicMock()
mock_client.hgetall = MagicMock()
mock_client.delete = MagicMock()
mock_from_url.return_value = mock_client
yield mock_client
@pytest.fixture
def valkey_db_cluster(mock_valkey_cluster_client):
"""Create a ValkeyDB instance in cluster mode with a mock client."""
valkey_db = ValkeyDB(
valkey_url="valkey://localhost:7000",
collection_name="test_cluster",
embedding_model_dims=1536,
cluster_mode=True,
)
valkey_db.client = mock_valkey_cluster_client
return valkey_db
def test_cluster_mode_init(mock_valkey_cluster_client):
"""Test that cluster_mode=True uses ValkeyCluster client."""
db = ValkeyDB(
valkey_url="valkey://localhost:7000",
collection_name="test_cluster",
embedding_model_dims=1536,
cluster_mode=True,
)
assert db.cluster_mode is True
def test_cluster_mode_create_index(valkey_db_cluster, mock_valkey_cluster_client):
"""Test that index creation works in cluster mode (server handles propagation)."""
mock_valkey_cluster_client.execute_command.reset_mock()
mock_valkey_cluster_client.ft.return_value.info.side_effect = ResponseError("not found")
valkey_db_cluster._create_index(1536)
call_args = mock_valkey_cluster_client.execute_command.call_args
assert call_args is not None
assert "FT.CREATE" in call_args.args
def test_cluster_mode_drop_index(valkey_db_cluster, mock_valkey_cluster_client):
"""Test that dropping index works in cluster mode."""
mock_valkey_cluster_client.execute_command.reset_mock()
valkey_db_cluster._drop_index("test_cluster")
call_args = mock_valkey_cluster_client.execute_command.call_args
assert "FT.DROPINDEX" in call_args.args
def test_cluster_mode_search(valkey_db_cluster, mock_valkey_cluster_client):
"""Test that search in cluster mode uses ft().search() (server handles cross-shard fan-out)."""
ts = str(int(datetime.now().timestamp()))
mock_doc = MagicMock()
mock_doc.memory_id = "id1"
mock_doc.hash = "h1"
mock_doc.memory = "data1"
mock_doc.created_at = ts
mock_doc.metadata = "{}"
mock_doc.vector_score = "0.1"
mock_results = MagicMock()
mock_results.docs = [mock_doc]
mock_valkey_cluster_client.ft.return_value.search.return_value = mock_results
results = valkey_db_cluster.search("test", np.random.rand(1536).tolist(), top_k=5)
assert len(results) == 1
assert results[0].id == "id1"
mock_valkey_cluster_client.ft.return_value.search.assert_called_once()
def test_cluster_mode_insert(valkey_db_cluster, mock_valkey_cluster_client):
"""Test that insert works in cluster mode (ValkeyCluster handles routing)."""
vectors = [np.random.rand(1536).tolist()]
payloads = [{"hash": "h1", "data": "test", "user_id": "u1"}]
ids = ["id1"]
valkey_db_cluster.insert(vectors=vectors, payloads=payloads, ids=ids)
mock_valkey_cluster_client.hset.assert_called_once()
def test_cluster_mode_get(valkey_db_cluster, mock_valkey_cluster_client):
"""Test that get works in cluster mode."""
mock_valkey_cluster_client.hgetall.return_value = {
"memory_id": "id1",
"hash": "h1",
"memory": "test_data",
"created_at": str(int(datetime.now().timestamp())),
"metadata": "{}",
}
result = valkey_db_cluster.get("id1")
assert result.id == "id1"
assert result.payload["data"] == "test_data"
def test_cluster_mode_delete(valkey_db_cluster, mock_valkey_cluster_client):
"""Test that delete works in cluster mode."""
valkey_db_cluster.delete("id1")
mock_valkey_cluster_client.delete.assert_called_once_with("mem0:test_cluster:id1")