Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
884e056853 | ||
|
|
fbf6b8c0db | ||
|
|
1d56f85705 | ||
|
|
ced852033b | ||
|
|
b9ad8fa8b2 | ||
|
|
e3f5ce7b41 |
@@ -32,12 +32,13 @@ def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedPr
|
||||
if key.startswith("MEM0_"):
|
||||
del env[key]
|
||||
env.pop("FORCE_COLOR", None)
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
timeout=15,
|
||||
)
|
||||
@@ -99,12 +100,13 @@ class TestArgvPreprocessing:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env={
|
||||
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||
"HOME": clean_home,
|
||||
"MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
|
||||
"FORCE_COLOR": "0",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
@@ -133,12 +135,13 @@ class TestJsonEnvelopeParity:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env={
|
||||
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||
"HOME": clean_home,
|
||||
"MEM0_BASE_URL": "http://127.0.0.1:1",
|
||||
"FORCE_COLOR": "0",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
@@ -49,6 +49,7 @@ def _run(
|
||||
if key.startswith("MEM0_"):
|
||||
del env[key]
|
||||
env.pop("FORCE_COLOR", None)
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
if env_override:
|
||||
@@ -56,7 +57,7 @@ def _run(
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
|
||||
@@ -67,7 +67,8 @@ class TestConfig:
|
||||
from mem0_cli.config import CONFIG_FILE
|
||||
|
||||
mode = os.stat(CONFIG_FILE).st_mode & 0o777
|
||||
assert mode == 0o600
|
||||
if os.name != "nt":
|
||||
assert mode == 0o600
|
||||
|
||||
def test_defaults_save_and_load(self, isolate_config):
|
||||
config = Mem0Config()
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Regression tests for per-agent memory isolation helpers and
|
||||
* message filtering logic.
|
||||
*/
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import memoryPlugin, {
|
||||
extractAgentId,
|
||||
effectiveUserId,
|
||||
agentUserId,
|
||||
@@ -16,6 +16,47 @@ import {
|
||||
filterMessagesForExtraction,
|
||||
} from "./index.ts";
|
||||
|
||||
function createPluginApi(registrationMode?: string) {
|
||||
return {
|
||||
pluginConfig: {
|
||||
mode: "platform",
|
||||
apiKey: "test-api-key",
|
||||
userId: "alice",
|
||||
},
|
||||
registrationMode,
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
resolvePath: vi.fn((p: string) => p),
|
||||
registerTool: vi.fn(),
|
||||
on: vi.fn(),
|
||||
registerCli: vi.fn(),
|
||||
registerCommand: vi.fn(),
|
||||
registerService: vi.fn(),
|
||||
registerMemoryCapability: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("plugin registration modes", () => {
|
||||
it("keeps cli-metadata registration free of runtime side effects", () => {
|
||||
const api = createPluginApi("cli-metadata");
|
||||
|
||||
memoryPlugin.register(api as any);
|
||||
|
||||
expect(api.registerCli).toHaveBeenCalledTimes(1);
|
||||
expect(api.registerService).not.toHaveBeenCalled();
|
||||
expect(api.registerMemoryCapability).not.toHaveBeenCalled();
|
||||
expect(api.registerTool).not.toHaveBeenCalled();
|
||||
expect(api.on).not.toHaveBeenCalled();
|
||||
expect(api.logger.info).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("openclaw-mem0: registered"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// extractAgentId
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -107,6 +107,7 @@ const memoryPlugin = definePluginEntry({
|
||||
baseUrl: pluginAuth.baseUrl,
|
||||
};
|
||||
const cfg = mem0ConfigSchema.parse(api.pluginConfig, fileConfig);
|
||||
const isMetadataRegistration = api.registrationMode === "cli-metadata";
|
||||
|
||||
// Telemetry context bound to this plugin instance's config
|
||||
const telemetryCtx = {
|
||||
@@ -122,6 +123,21 @@ const memoryPlugin = definePluginEntry({
|
||||
}
|
||||
};
|
||||
|
||||
if (isMetadataRegistration) {
|
||||
registerCliCommands(
|
||||
api,
|
||||
null as any,
|
||||
null as any,
|
||||
cfg,
|
||||
() => cfg.userId,
|
||||
(id: string) => `${cfg.userId}:agent:${id}`,
|
||||
() => ({ user_id: cfg.userId, top_k: cfg.topK }),
|
||||
() => undefined,
|
||||
(cmd: string) => _captureEvent(`openclaw.cli.${cmd}`, { command: cmd }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cfg.needsSetup) {
|
||||
api.logger.warn(
|
||||
"openclaw-mem0: API key not configured. Memory features are disabled.\n" +
|
||||
|
||||
@@ -26,6 +26,7 @@ declare module "openclaw/plugin-sdk" {
|
||||
|
||||
export interface OpenClawPluginApi {
|
||||
pluginConfig: Record<string, unknown>;
|
||||
registrationMode?: "full" | "cli-metadata" | string;
|
||||
logger: {
|
||||
info(msg: string): void;
|
||||
warn(msg: string): void;
|
||||
|
||||
@@ -75,8 +75,8 @@
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@langchain/core": "^0.3.42",
|
||||
"@langchain/community": "^1.1.18",
|
||||
"@langchain/core": "^1.1.27",
|
||||
"axios": "^1.16.0",
|
||||
"mem0ai": "^2.1.8",
|
||||
"uuid": "^11.1.1",
|
||||
|
||||
+7
-2
@@ -341,6 +341,11 @@ def _build_session_scope(filters):
|
||||
return "&".join(parts)
|
||||
|
||||
|
||||
def _entity_collection_name(provider: str, collection_name: str) -> str:
|
||||
separator = "-" if provider == "s3_vectors" else "_"
|
||||
return f"{collection_name}{separator}entities"
|
||||
|
||||
|
||||
setup_config()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -417,7 +422,7 @@ class Memory(MemoryBase):
|
||||
"""Lazily initialize entity store on first use."""
|
||||
if self._entity_store is None:
|
||||
entity_config = _safe_deepcopy_config(self.config.vector_store.config)
|
||||
entity_collection = f"{self.collection_name}_entities"
|
||||
entity_collection = _entity_collection_name(self.config.vector_store.provider, self.collection_name)
|
||||
# Set collection name on the cloned config
|
||||
if hasattr(entity_config, 'collection_name'):
|
||||
entity_config.collection_name = entity_collection
|
||||
@@ -1889,7 +1894,7 @@ class AsyncMemory(MemoryBase):
|
||||
"""Lazily initialize entity store on first use."""
|
||||
if self._entity_store is None:
|
||||
entity_config = _safe_deepcopy_config(self.config.vector_store.config)
|
||||
entity_collection = f"{self.collection_name}_entities"
|
||||
entity_collection = _entity_collection_name(self.config.vector_store.provider, self.collection_name)
|
||||
if hasattr(entity_config, 'collection_name'):
|
||||
entity_config.collection_name = entity_collection
|
||||
elif isinstance(entity_config, dict):
|
||||
|
||||
Generated
+9
-9
@@ -3494,15 +3494,15 @@ pytest = ["pytest (>=7.0.0)", "rich (>=13.9.4,<14.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.83.7"
|
||||
version = "1.88.1"
|
||||
description = "Library to easily interface with LLM API providers"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.9"
|
||||
python-versions = "<3.14,>=3.10"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"llms\""
|
||||
files = [
|
||||
{file = "litellm-1.83.7-py3-none-any.whl", hash = "sha256:5784a1d9a9a4a8acd6ca1e347003a5e2e1b3c749b4d41e7da4904577adade111"},
|
||||
{file = "litellm-1.83.7.tar.gz", hash = "sha256:e2f2cb99df2e2b2eab63f1354faa45c88dd7c8d40c18eb648afb1b349c689633"},
|
||||
{file = "litellm-1.88.1-py3-none-any.whl", hash = "sha256:369b84e57d9426582ddc35e731956ddb6618cda97cc44e4e4d2dfa75982a6e3a"},
|
||||
{file = "litellm-1.88.1.tar.gz", hash = "sha256:89c6b74cc7912d6365793006ff951c0450fe847625008dfe49de8a7dc4529aa5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -3515,7 +3515,7 @@ jinja2 = "3.1.6"
|
||||
jsonschema = "4.23.0"
|
||||
openai = "2.30.0"
|
||||
pydantic = "2.12.5"
|
||||
python-dotenv = "1.0.1"
|
||||
python-dotenv = "1.2.2"
|
||||
tiktoken = "0.12.0"
|
||||
tokenizers = "0.22.2"
|
||||
|
||||
@@ -6866,15 +6866,15 @@ six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.1"
|
||||
version = "1.2.2"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"vector-stores\" or extra == \"llms\" or extra == \"extras\""
|
||||
files = [
|
||||
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
|
||||
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
|
||||
{file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
|
||||
{file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ vector_stores = [
|
||||
llms = [
|
||||
"groq>=0.3.0",
|
||||
"together>=0.2.10",
|
||||
"litellm>=1.83.7",
|
||||
"litellm>=1.88.1",
|
||||
"openai>=1.90.0",
|
||||
"ollama>=0.3.0",
|
||||
"vertexai>=0.1.0",
|
||||
|
||||
@@ -451,6 +451,10 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
|
||||
if search_req.explain is not None:
|
||||
params["explain"] = search_req.explain
|
||||
return get_memory_instance().search(query=search_req.query, filters=filters, **params)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
raise upstream_error()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
|
||||
from mem0 import Memory
|
||||
from mem0.configs.base import MemoryConfig
|
||||
from mem0.memory.main import _entity_collection_name
|
||||
from mem0.memory.utils import normalize_facts
|
||||
|
||||
|
||||
@@ -37,6 +38,14 @@ def test_create_memory(memory_client):
|
||||
assert result["results"][0]["memory"] == data
|
||||
|
||||
|
||||
def test_entity_collection_name_uses_dash_for_s3_vectors():
|
||||
assert _entity_collection_name("s3_vectors", "test-index") == "test-index-entities"
|
||||
|
||||
|
||||
def test_entity_collection_name_keeps_underscore_for_other_stores():
|
||||
assert _entity_collection_name("qdrant", "mem0") == "mem0_entities"
|
||||
|
||||
|
||||
def test_get_memory(memory_client):
|
||||
data = "Name is John Doe."
|
||||
memory_client.add([{"role": "user", "content": data}], user_id="test_user")
|
||||
|
||||
@@ -682,3 +682,22 @@ class TestSearchEntityIdMapping:
|
||||
_, kwargs = mock_memory.search.call_args
|
||||
assert kwargs["filters"]["user_id"] == "u1"
|
||||
assert kwargs["filters"]["category"] == "food"
|
||||
|
||||
|
||||
class TestSearchValidationErrors:
|
||||
"""Verify that ValueError from Memory.search() returns 400, not 502."""
|
||||
|
||||
def test_empty_filters_returns_400(self, client, mock_memory):
|
||||
mock_memory.search.side_effect = ValueError(
|
||||
"filters must contain at least one of: user_id, agent_id, run_id"
|
||||
)
|
||||
resp = client.post("/search", json={"query": "food", "filters": {}})
|
||||
assert resp.status_code == 400
|
||||
assert "filters must contain" in resp.json()["detail"]
|
||||
|
||||
def test_no_identifiers_returns_400(self, client, mock_memory):
|
||||
mock_memory.search.side_effect = ValueError(
|
||||
"filters must contain at least one of: user_id, agent_id, run_id"
|
||||
)
|
||||
resp = client.post("/search", json={"query": "food"})
|
||||
assert resp.status_code == 400
|
||||
|
||||
@@ -110,6 +110,32 @@ def test_memory_initialization_with_config(mock_boto_client, mock_llm, mock_embe
|
||||
pytest.fail("Memory initialization failed")
|
||||
|
||||
|
||||
def test_memory_entity_store_uses_s3_valid_index_name(mock_boto_client, mock_llm, mock_embedder):
|
||||
mock_boto_client.get_vector_bucket.return_value = {}
|
||||
mock_boto_client.get_index.return_value = {}
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "s3_vectors",
|
||||
"config": {
|
||||
"vector_bucket_name": BUCKET_NAME,
|
||||
"collection_name": INDEX_NAME,
|
||||
"embedding_model_dims": EMBEDDING_DIMS,
|
||||
"distance_metric": "cosine",
|
||||
"region_name": REGION,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
memory = Memory.from_config(config)
|
||||
assert memory.entity_store is not None
|
||||
|
||||
index_names = [call.kwargs["indexName"] for call in mock_boto_client.get_index.call_args_list]
|
||||
assert INDEX_NAME in index_names
|
||||
assert f"{INDEX_NAME}-entities" in index_names
|
||||
assert f"{INDEX_NAME}_entities" not in index_names
|
||||
|
||||
|
||||
def test_insert(mock_boto_client):
|
||||
"""Test inserting vectors."""
|
||||
store = S3Vectors(
|
||||
|
||||
Reference in New Issue
Block a user