diff --git a/dashboard/plugin_api.py b/dashboard/plugin_api.py index 2fd3c54..e0bf527 100644 --- a/dashboard/plugin_api.py +++ b/dashboard/plugin_api.py @@ -604,10 +604,30 @@ def pick(key: str, env_key: str, default: Any = None) -> Any: rerank = pick("rerank", "MEM0_RERANK", True) if isinstance(rerank, str): rerank = rerank.strip().lower() not in {"0", "false", "no", "off"} + + # Detect OSS mode: mem0.json has "mode": "oss" or contains an "oss" block. + oss_mode = ( + str(file_cfg.get("mode") or "").strip().lower() == "oss" + or isinstance(file_cfg.get("oss"), dict) + ) + # Issue #3: if mode=oss but no "oss" sub-block, treat the whole file_cfg + # (minus meta-keys) as the OSS config rather than silently falling back to + # Memory() defaults which would ignore all user configuration. + _oss_block = file_cfg.get("oss") + if isinstance(_oss_block, dict): + oss_config: Optional[Dict[str, Any]] = _oss_block + elif oss_mode: + # Flat layout: entire file is the config (minus the "mode" key). + oss_config = {k: v for k, v in file_cfg.items() if k != "mode"} + else: + oss_config = None + return { "config_path": str(config_path), "config_exists": config_path.exists(), "api_key_present": bool(api_key), + "mem0_mode": "oss" if oss_mode else "cloud", + "oss_config": oss_config, "user_id": pick("user_id", "MEM0_USER_ID", "hermes-user"), "agent_id": pick("agent_id", "MEM0_AGENT_ID", "hermes"), "rerank": rerank, @@ -671,6 +691,7 @@ def _mem0_payload( "label": "Mem0 memory", "provider_configured": provider == "mem0", "mode": "read-only", + "mem0_mode": mem0_cfg["mem0_mode"], "config_path": mem0_cfg["config_path"], "config_exists": mem0_cfg["config_exists"], "api_key_present": mem0_cfg["api_key_present"], @@ -686,22 +707,71 @@ def _mem0_payload( } try: - if not mem0_cfg["api_key_present"]: - base["error"] = "Mem0 API key not configured. Set MEM0_API_KEY in $HERMES_HOME/.env or the process environment." - return base + if mem0_cfg["mem0_mode"] == "oss": + # OSS / self-hosted path: use mem0.Memory with the full config from mem0.json. + try: + from mem0 import Memory # type: ignore + except ImportError: + base["error"] = "mem0 package not installed in the dashboard environment. Install mem0ai." + return base + + oss_cfg = mem0_cfg["oss_config"] + if oss_cfg: + # Issues #1 & #2: hoist import, catch ImportError explicitly, + # give a clean user-facing error instead of silently falling + # through to Memory(config=raw_dict) which always crashes. + try: + from mem0.configs.base import MemoryConfig as _MemoryConfig # type: ignore + except ImportError: + base["error"] = "Could not import MemoryConfig from mem0. Ensure mem0ai is installed and up to date." + return base + try: + mem0_config: Any = _MemoryConfig.model_validate(oss_cfg) + except Exception: + try: + mem0_config = _MemoryConfig(**oss_cfg) + except Exception as cfg_exc: + base["error"] = f"Invalid OSS config in mem0.json: {_safe_error(cfg_exc)}" + return base + client = Memory(config=mem0_config) + else: + client = Memory() + # Issue #4: apply agent_id filter consistently only when explicitly set + # (not the default "hermes") so OSS and cloud return the same scope. + oss_filters: Dict[str, Any] = {"user_id": mem0_cfg["user_id"]} + if mem0_cfg["agent_id"] and mem0_cfg["agent_id"] != "hermes": + oss_filters["agent_id"] = mem0_cfg["agent_id"] + if search: + response = client.search( + query=search, + filters=oss_filters, + top_k=limit, + rerank=mem0_cfg["rerank"], # Issue #5: forward rerank to OSS search + ) + else: + response = client.get_all( + filters=oss_filters, + top_k=limit, # Issue #7: pass limit server-side, not just client-side + ) + else: + # Cloud path: use MemoryClient with an API key. + if not mem0_cfg["api_key_present"]: + base["error"] = "Mem0 API key not configured. Set MEM0_API_KEY in $HERMES_HOME/.env or the process environment." + return base - try: - from mem0 import MemoryClient # type: ignore - except ImportError: - base["error"] = "mem0 package not installed in the dashboard environment. Install mem0ai." - return base + try: + from mem0 import MemoryClient # type: ignore + except ImportError: + base["error"] = "mem0 package not installed in the dashboard environment. Install mem0ai." + return base + + client = MemoryClient(api_key=mem0_cfg["_api_key"]) + filters = {"user_id": mem0_cfg["user_id"]} + if search: + response = client.search(query=search, filters=filters, rerank=mem0_cfg["rerank"], top_k=limit) + else: + response = client.get_all(filters=filters) - client = MemoryClient(api_key=mem0_cfg["_api_key"]) - filters = {"user_id": mem0_cfg["user_id"]} - if search: - response = client.search(query=search, filters=filters, rerank=mem0_cfg["rerank"], top_k=limit) - else: - response = client.get_all(filters=filters) all_memories = [_normalize_mem0_memory(item, index) for index, item in enumerate(_unwrap_mem0_results(response))] base["total_memories"] = len(all_memories) base["memories"] = _filter_mem0_memories(all_memories, None, limit) @@ -2374,6 +2444,7 @@ async def status() -> Dict[str, Any]: "config_path": mem0_cfg["config_path"], "config_exists": mem0_cfg["config_exists"], "api_key_present": mem0_cfg["api_key_present"], + "mem0_mode": mem0_cfg["mem0_mode"], "user_id": mem0_cfg["user_id"], "agent_id": mem0_cfg["agent_id"], "provider_configured": _dig(config, "memory", "provider", default=None) == "mem0", diff --git a/tests/test_plugin_api.py b/tests/test_plugin_api.py index 573da11..0f785e7 100644 --- a/tests/test_plugin_api.py +++ b/tests/test_plugin_api.py @@ -21,6 +21,42 @@ def load_plugin_api(monkeypatch, tmp_path): return module +def test_load_mem0_config_uses_mem0_mode_oss_for_flat_oss_config(monkeypatch, tmp_path): + (tmp_path / "mem0.json").write_text( + json.dumps({"mode": "oss", "vector_store": {"provider": "qdrant"}, "agent_id": "agent-x"}), + encoding="utf-8", + ) + + module = load_plugin_api(monkeypatch, tmp_path) + cfg = module._load_mem0_config({}) + + assert cfg["mem0_mode"] == "oss" + assert cfg["oss_config"] == {"vector_store": {"provider": "qdrant"}, "agent_id": "agent-x"} + + +def test_load_mem0_config_uses_mem0_mode_oss_for_nested_oss_config(monkeypatch, tmp_path): + (tmp_path / "mem0.json").write_text( + json.dumps({"mode": "cloud", "oss": {"vector_store": {"provider": "chroma"}}}), + encoding="utf-8", + ) + + module = load_plugin_api(monkeypatch, tmp_path) + cfg = module._load_mem0_config({}) + + assert cfg["mem0_mode"] == "oss" + assert cfg["oss_config"] == {"vector_store": {"provider": "chroma"}} + + +def test_load_mem0_config_defaults_to_cloud_mem0_mode(monkeypatch, tmp_path): + (tmp_path / "mem0.json").write_text(json.dumps({"user_id": "u1"}), encoding="utf-8") + + module = load_plugin_api(monkeypatch, tmp_path) + cfg = module._load_mem0_config({}) + + assert cfg["mem0_mode"] == "cloud" + assert cfg["oss_config"] is None + + def test_session_search_payload_uses_hermes_session_search_tool_without_source(monkeypatch, tmp_path): calls = []