feat(mem0): add self-hosted support via MEM0_HOST / host config

The mem0 plugin previously hardcoded api.mem0.ai as the endpoint.
This adds a `host` config key and MEM0_HOST env var so users can
point the plugin at a self-hosted Mem0 instance.

Changes:
- _load_config(): read MEM0_HOST env var
- is_available(): accept host OR api_key (self-hosted may not need a real key)
- get_config_schema(): add host field
- initialize(): read host from config
- _get_client(): pass host kwarg to MemoryClient when set
- system_prompt_block(): show target (cloud vs URL)
- README: document self-hosted setup
This commit is contained in:
buihongduc132 2026-04-21 13:50:45 +07:00 committed by Teknium
parent 012f40c98c
commit b6d2ac176e
2 changed files with 43 additions and 42 deletions

View file

@ -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 |

View file

@ -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())