diff --git a/plugins/memory/mem0/README.md b/plugins/memory/mem0/README.md index 760f6321971..62c7494af77 100644 --- a/plugins/memory/mem0/README.md +++ b/plugins/memory/mem0/README.md @@ -2,30 +2,45 @@ Server-side LLM fact extraction with semantic search, reranking, and automatic deduplication. +Supports both [Mem0 Cloud](https://app.mem0.ai) and self-hosted instances. + ## Requirements - `pip install mem0ai` -- Mem0 API key from [app.mem0.ai](https://app.mem0.ai) +- Mem0 Cloud API key **or** a self-hosted Mem0 server ## Setup +### Cloud + ```bash hermes memory setup # select "mem0" ``` Or manually: + ```bash hermes config set memory.provider mem0 echo "MEM0_API_KEY=your-key" >> ~/.hermes/.env ``` +### Self-Hosted + +```bash +hermes config set memory.provider mem0 +echo "MEM0_HOST=http://your-mem0-server:24220" >> ~/.hermes/.env +echo "MEM0_API_KEY=your-api-key" >> ~/.hermes/.env # if auth is enabled +``` + ## Config Config file: `$HERMES_HOME/mem0.json` | Key | Default | Description | |-----|---------|-------------| -| `user_id` | `hermes-user` | User identifier on Mem0 | +| `api_key` | — | API key (required for cloud; optional for self-hosted without auth) | +| `host` | `https://api.mem0.ai` | Self-hosted Mem0 URL. When set, overrides the cloud endpoint. | +| `user_id` | `hermes-user` | User identifier | | `agent_id` | `hermes` | Agent identifier | | `rerank` | `true` | Enable reranking for recall | diff --git a/plugins/memory/mem0/__init__.py b/plugins/memory/mem0/__init__.py index 332b3ac9412..9138235a71f 100644 --- a/plugins/memory/mem0/__init__.py +++ b/plugins/memory/mem0/__init__.py @@ -1,12 +1,13 @@ """Mem0 memory plugin — MemoryProvider interface. Server-side LLM fact extraction, semantic search with reranking, and -automatic deduplication via the Mem0 Platform API. +automatic deduplication via the Mem0 Platform API or self-hosted instance. Original PR #2933 by kartik-mem0, adapted to MemoryProvider ABC. Config via environment variables: - MEM0_API_KEY — Mem0 Platform API key (required) + MEM0_API_KEY — Mem0 API key (required for cloud, optional for self-hosted) + MEM0_HOST — Self-hosted Mem0 URL (default: https://api.mem0.ai) MEM0_USER_ID — User identifier (default: hermes-user) MEM0_AGENT_ID — Agent identifier (default: hermes) @@ -27,27 +28,16 @@ from tools.registry import tool_error logger = logging.getLogger(__name__) -# Circuit breaker: after this many consecutive failures, pause API calls -# for _BREAKER_COOLDOWN_SECS to avoid hammering a down server. _BREAKER_THRESHOLD = 5 _BREAKER_COOLDOWN_SECS = 120 -# --------------------------------------------------------------------------- -# Config -# --------------------------------------------------------------------------- - def _load_config() -> dict: - """Load config from env vars, with $HERMES_HOME/mem0.json overrides. - - Environment variables provide defaults; mem0.json (if present) overrides - individual keys. This avoids a silent failure when the JSON file exists - but is missing fields like ``api_key`` that the user set in ``.env``. - """ from hermes_constants import get_hermes_home config = { "api_key": os.environ.get("MEM0_API_KEY", ""), + "host": os.environ.get("MEM0_HOST", ""), "user_id": os.environ.get("MEM0_USER_ID", "hermes-user"), "agent_id": os.environ.get("MEM0_AGENT_ID", "hermes"), "rerank": True, @@ -66,10 +56,6 @@ def _load_config() -> dict: return config -# --------------------------------------------------------------------------- -# Tool schemas -# --------------------------------------------------------------------------- - PROFILE_SCHEMA = { "name": "mem0_profile", "description": ( @@ -112,18 +98,19 @@ CONCLUDE_SCHEMA = { } -# --------------------------------------------------------------------------- -# MemoryProvider implementation -# --------------------------------------------------------------------------- - class Mem0MemoryProvider(MemoryProvider): - """Mem0 Platform memory with server-side extraction and semantic search.""" + """Mem0 memory with server-side extraction and semantic search. + + Supports both Mem0 Cloud (api.mem0.ai) and self-hosted instances + via the ``host`` config key or ``MEM0_HOST`` env var. + """ def __init__(self): self._config = None self._client = None self._client_lock = threading.Lock() self._api_key = "" + self._host = "" self._user_id = "hermes-user" self._agent_id = "hermes" self._rerank = True @@ -131,7 +118,6 @@ class Mem0MemoryProvider(MemoryProvider): self._prefetch_lock = threading.Lock() self._prefetch_thread = None self._sync_thread = None - # Circuit breaker state self._consecutive_failures = 0 self._breaker_open_until = 0.0 @@ -141,10 +127,11 @@ class Mem0MemoryProvider(MemoryProvider): def is_available(self) -> bool: cfg = _load_config() - return bool(cfg.get("api_key")) + host = cfg.get("host", "") + api_key = cfg.get("api_key", "") + return bool(host) or bool(api_key) def save_config(self, values, hermes_home): - """Write config to $HERMES_HOME/mem0.json.""" import json from pathlib import Path config_path = Path(hermes_home) / "mem0.json" @@ -160,30 +147,35 @@ class Mem0MemoryProvider(MemoryProvider): def get_config_schema(self): return [ - {"key": "api_key", "description": "Mem0 Platform API key", "secret": True, "required": True, "env_var": "MEM0_API_KEY", "url": "https://app.mem0.ai"}, + {"key": "api_key", "description": "Mem0 API key (cloud or self-hosted)", "secret": True, "required": False, "env_var": "MEM0_API_KEY", "url": "https://app.mem0.ai"}, + {"key": "host", "description": "Self-hosted Mem0 URL (e.g. http://localhost:24220)", "default": "", "env_var": "MEM0_HOST"}, {"key": "user_id", "description": "User identifier", "default": "hermes-user"}, {"key": "agent_id", "description": "Agent identifier", "default": "hermes"}, {"key": "rerank", "description": "Enable reranking for recall", "default": "true", "choices": ["true", "false"]}, ] def _get_client(self): - """Thread-safe client accessor with lazy initialization.""" with self._client_lock: if self._client is not None: return self._client try: from mem0 import MemoryClient - self._client = MemoryClient(api_key=self._api_key) + kwargs = {} + if self._host: + kwargs["host"] = self._host + if self._api_key: + kwargs["api_key"] = self._api_key + elif not self._host: + raise ValueError("Mem0: either api_key or host is required") + self._client = MemoryClient(**kwargs) return self._client except ImportError: raise RuntimeError("mem0 package not installed. Run: pip install mem0ai") def _is_breaker_open(self) -> bool: - """Return True if the circuit breaker is tripped (too many failures).""" if self._consecutive_failures < _BREAKER_THRESHOLD: return False if time.monotonic() >= self._breaker_open_until: - # Cooldown expired — reset and allow a retry self._consecutive_failures = 0 return False return True @@ -204,23 +196,19 @@ class Mem0MemoryProvider(MemoryProvider): def initialize(self, session_id: str, **kwargs) -> None: self._config = _load_config() self._api_key = self._config.get("api_key", "") - # Prefer gateway-provided user_id for per-user memory scoping; - # fall back to config/env default for CLI (single-user) sessions. + self._host = self._config.get("host", "") self._user_id = kwargs.get("user_id") or self._config.get("user_id", "hermes-user") self._agent_id = self._config.get("agent_id", "hermes") self._rerank = self._config.get("rerank", True) def _read_filters(self) -> Dict[str, Any]: - """Filters for search/get_all — scoped to user only for cross-session recall.""" return {"user_id": self._user_id} def _write_filters(self) -> Dict[str, Any]: - """Filters for add — scoped to user + agent for attribution.""" return {"user_id": self._user_id, "agent_id": self._agent_id} @staticmethod def _unwrap_results(response: Any) -> list: - """Normalize Mem0 API response — v2 wraps results in {"results": [...]}.""" if isinstance(response, dict): return response.get("results", []) if isinstance(response, list): @@ -228,8 +216,9 @@ class Mem0MemoryProvider(MemoryProvider): return [] def system_prompt_block(self) -> str: + target = self._host or "cloud" return ( - "# Mem0 Memory\n" + f"# Mem0 Memory ({target})\n" f"Active. User: {self._user_id}.\n" "Use mem0_search to find memories, mem0_conclude to store facts, " "mem0_profile for a full overview." @@ -271,7 +260,6 @@ class Mem0MemoryProvider(MemoryProvider): self._prefetch_thread.start() def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None: - """Send the turn to Mem0 for server-side fact extraction (non-blocking).""" if self._is_breaker_open(): return @@ -288,7 +276,6 @@ class Mem0MemoryProvider(MemoryProvider): self._record_failure() logger.warning("Mem0 sync failed: %s", e) - # Wait for any previous sync before starting a new one if self._sync_thread and self._sync_thread.is_alive(): self._sync_thread.join(timeout=5.0) @@ -370,5 +357,4 @@ class Mem0MemoryProvider(MemoryProvider): def register(ctx) -> None: - """Register Mem0 as a memory provider plugin.""" ctx.register_memory_provider(Mem0MemoryProvider())