Initial release: Mem0 local server memory provider for Hermes-Agent

- Self-hosted Mem0 integration (no cloud dependency)
- Async prefetch with ~40ms latency
- Automatic context injection via pre_llm_call hook
- Circuit breaker for server resilience
- Full tool support: profile, search, conclude
This commit is contained in:
2026-04-10 12:53:15 +02:00
commit 3a141d9180
7 changed files with 1057 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Environment
.env
.env.local
.env.*.local
# OS
.DS_Store
Thumbs.db
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Henry Hofmann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+239
View File
@@ -0,0 +1,239 @@
# Mem0 Local Hermes Plugin
Self-hosted Mem0 memory provider for Hermes-Agent. Provides semantic memory search, automatic fact extraction, and context injection without tool calls.
## Features
- **Local Mem0 server** — No cloud dependency, full data privacy
- **Async prefetch** — Memory retrieval happens in background (~40ms)
- **Context injection** — Relevant memories injected directly into LLM prompt
- **Automatic fact extraction** — Server-side LLM extracts facts from conversations
- **Semantic search** — Find memories by meaning, not keywords
- **Circuit breaker** — Automatic failover on server unavailability
## Prerequisites
1. **Mem0 server running locally**:
Using Docker:
```bash
docker run -d -p 8000:8000 mem0ai/mem0:latest
```
Or via Docker Compose with custom config:
```yaml
version: "3.8"
services:
mem0:
image: mem0ai/mem0:latest
ports:
- "8000:8000"
environment:
- MEM0_CONFIG_PATH=/app/config.yaml
volumes:
- ./mem0-config.yaml:/app/config.yaml
- mem0_data:/app/data
volumes:
mem0_data:
```
2. **Verify server is reachable**:
```bash
curl http://localhost:8000/health
```
Your setup uses port 8889:
```bash
curl http://10.0.0.150:8889/health
```
## Installation
From GitHub repository:
```bash
hermes plugins install https://github.com/yourusername/mem0-local-hermes-plugin.git
```
Or with shorthand:
```bash
hermes plugins install yourusername/mem0-local-hermes-plugin
```
From local directory (during development):
```bash
hermes plugins install /path/to/mem0-local-hermes-plugin
```
The installer will prompt for:
- `MEM0_BASE_URL` — Your local Mem0 server URL (default: `http://localhost:8000`)
- `MEM0_USER_ID` — User identifier for memory scoping (default: `hermes-user`)
## Configuration
The plugin supports two configuration methods that work together:
1. **Environment variables** (`~/.hermes/.env`) - Primary configuration
2. **Config file** (`~/.hermes/mem0-local.json`) - Optional overrides
**Precedence**: Config file values override environment variables. This allows you to set defaults in `.env` and override specific values in the JSON file.
### Method 1: Environment Variables (Recommended)
Set in `~/.hermes/.env`:
| Variable | Description | Default |
|----------|-------------|---------|
| `MEM0_BASE_URL` | Local Mem0 server URL | `http://localhost:8000` |
| `MEM0_USER_ID` | User identifier | `hermes-user` |
| `MEM0_AGENT_ID` | Agent identifier | `hermes` |
Example:
```env
MEM0_BASE_URL=http://10.0.0.150:8889
MEM0_USER_ID=henry_hofmann
MEM0_AGENT_ID=hermes
```
### Method 2: Config File (Optional Overrides)
Create `~/.hermes/mem0-local.json` to override specific settings:
```json
{
"base_url": "http://10.0.0.150:8889",
"user_id": "henry_hofmann",
"agent_id": "hermes",
"rerank": true,
"timeout": 10.0
}
```
Example for your setup:
```json
{
"base_url": "http://10.0.0.150:8889",
"user_id": "henry_hofmann",
"agent_id": "hermes",
"rerank": true,
"timeout": 10.0
}
```
## Usage
### Activate the Memory Provider
```bash
hermes memory mem0-local
```
### Restart Gateway
```bash
hermes gateway restart
```
### How It Works
1. **User message received** → `queue_prefetch()` spawns background thread
2. **Mem0 search** → Semantic search for relevant memories (~40ms)
3. **Context injection** → Results injected via `pre_llm_call` hook
4. **LLM receives** → User message + memory context (no tool call needed!)
**Example**:
```
User: "Hey, is a new episode out from my favorite anime?"
↓ [Background: mem0.prefetch() searches for "favorite anime"]
LLM receives:
"""
Hey, is a new episode out from my favorite anime?
## Mem0 Memory
- My favorite animes are Naruto, One Piece, and Demon Slayer (score: 0.87)
"""
Assistant: "Let me check for new episodes of Naruto, One Piece, and Demon Slayer..."
```
### Available Tools
The plugin also provides explicit memory tools:
| Tool | Description |
|------|-------------|
| `mem0_profile` | Retrieve all stored memories about the user |
| `mem0_search` | Search memories by semantic similarity |
| `mem0_conclude` | Store a fact verbatim (no LLM extraction) |
**Tool usage examples**:
```
# Get all memories
mem0_profile()
# Search with reranking
mem0_search(query="project deadlines", rerank=true, top_k=5)
# Store a fact explicitly
mem0_conclude(conclusion="I prefer Python over JavaScript for backend development")
```
## Circuit Breaker
After 5 consecutive API failures, the plugin pauses requests for 120 seconds to avoid hammering a down server. The breaker resets automatically.
## Troubleshooting
### "Mem0 server temporarily unavailable"
- Check server is running: `curl http://your-server:port/health`
- Verify `MEM0_BASE_URL` is correct
- Wait 2 minutes for circuit breaker to reset
### "No memories stored yet"
- Mem0 extracts facts automatically from conversations
- Or use `mem0_conclude` to store facts explicitly
### Memory not injected
- Check `is_available()` returns `True` in logs
- Verify `prefetch()` is being called (debug logs)
- Ensure Mem0 server has indexed memories
- Check network connectivity to your local server
### Connection issues
If your Mem0 server is on a different machine (like your 10.0.0.150):
- Ensure firewall allows connections on port 8889
- Verify the server binds to 0.0.0.0, not just localhost
- Check network routing between Hermes-Agent and Mem0 server
## Differences from Cloud Version
| Aspect | Cloud Version | Local Version |
|--------|--------------|---------------|
| **Client** | `mem0.MemoryClient(api_key=...)` | HTTP requests to local server |
| **Auth** | API key | None (local network) |
| **Config** | `MEM0_API_KEY` | `MEM0_BASE_URL` |
| **Latency** | Network-dependent | ~40ms (local) |
| **Privacy** | Cloud processing | Full local control |
| **Cost** | Pay-per-use | Free (self-hosted) |
## Development
For development, install from local path:
```bash
hermes plugins install /path/to/mem0-local-hermes-plugin
```
Watch for changes:
```bash
# In plugin directory
hermes gateway restart # After each change
```
## License
MIT
+408
View File
@@ -0,0 +1,408 @@
"""Mem0 local server memory plugin — MemoryProvider interface.
Self-hosted Mem0 server with semantic search and automatic fact extraction.
Config via environment variables:
MEM0_BASE_URL — Local Mem0 server URL (required, e.g., http://localhost:8000)
MEM0_USER_ID — User identifier for memory scoping (default: hermes-user)
MEM0_AGENT_ID — Agent identifier (default: hermes)
Or via $HERMES_HOME/mem0-local.json.
"""
from __future__ import annotations
import json
import logging
import os
import threading
import time
from typing import Any, Dict, List, Optional
from agent.memory_provider import MemoryProvider
from tools.registry import tool_error
from .client import LocalMem0Client
logger = logging.getLogger(__name__)
# Circuit breaker: after this many consecutive failures, pause API calls
_BREAKER_THRESHOLD = 5
_BREAKER_COOLDOWN_SECS = 120
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
def _load_config() -> dict:
"""Load config from env vars, with $HERMES_HOME/mem0-local.json overrides."""
from hermes_constants import get_hermes_home
config = {
"base_url": os.environ.get("MEM0_BASE_URL", "http://localhost:8000"),
"user_id": os.environ.get("MEM0_USER_ID", "hermes-user"),
"agent_id": os.environ.get("MEM0_AGENT_ID", "hermes"),
"rerank": True,
"timeout": 10.0,
}
config_path = get_hermes_home() / "mem0-local.json"
if config_path.exists():
try:
file_cfg = json.loads(config_path.read_text(encoding="utf-8"))
config.update(
{k: v for k, v in file_cfg.items() if v is not None and v != ""}
)
except Exception as e:
logger.warning("Failed to load mem0-local.json: %s", e)
return config
# ---------------------------------------------------------------------------
# Tool schemas
# ---------------------------------------------------------------------------
PROFILE_SCHEMA = {
"name": "mem0_profile",
"description": (
"Retrieve all stored memories about the user — preferences, facts, "
"project context. Fast, no reranking. Use at conversation start."
),
"parameters": {"type": "object", "properties": {}, "required": []},
}
SEARCH_SCHEMA = {
"name": "mem0_search",
"description": (
"Search memories by meaning. Returns relevant facts ranked by similarity. "
"Set rerank=true for higher accuracy on important queries."
),
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "What to search for."},
"rerank": {
"type": "boolean",
"description": "Enable reranking for precision (default: false).",
},
"top_k": {
"type": "integer",
"description": "Max results (default: 10, max: 50).",
},
},
"required": ["query"],
},
}
CONCLUDE_SCHEMA = {
"name": "mem0_conclude",
"description": (
"Store a durable fact about the user. Stored verbatim (no LLM extraction). "
"Use for explicit preferences, corrections, or decisions."
),
"parameters": {
"type": "object",
"properties": {
"conclusion": {"type": "string", "description": "The fact to store."},
},
"required": ["conclusion"],
},
}
# ---------------------------------------------------------------------------
# MemoryProvider implementation
# ---------------------------------------------------------------------------
class Mem0LocalMemoryProvider(MemoryProvider):
"""Self-hosted Mem0 memory with semantic search and fact extraction."""
def __init__(self):
self._config = None
self._client: Optional[LocalMem0Client] = None
self._client_lock = threading.Lock()
self._user_id = "hermes-user"
self._agent_id = "hermes"
self._rerank = True
self._prefetch_result = ""
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
@property
def name(self) -> str:
return "mem0-local"
def is_available(self) -> bool:
cfg = _load_config()
base_url = cfg.get("base_url", "")
if not base_url:
return False
# Try to reach the server
try:
client = LocalMem0Client(base_url)
return client.health()
except Exception:
return False
def save_config(self, values: dict, hermes_home):
"""Write config to $HERMES_HOME/mem0-local.json."""
from pathlib import Path
config_path = Path(hermes_home) / "mem0-local.json"
existing = {}
if config_path.exists():
try:
existing = json.loads(config_path.read_text())
except Exception:
pass
existing.update(values)
config_path.write_text(json.dumps(existing, indent=2))
def get_config_schema(self):
return [
{
"key": "base_url",
"description": "Local Mem0 server URL",
"required": True,
"env_var": "MEM0_BASE_URL",
"url": "https://github.com/mem0ai/mem0",
},
{
"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"],
},
{
"key": "timeout",
"description": "Request timeout in seconds",
"default": "10.0",
},
]
def _get_client(self) -> LocalMem0Client:
"""Thread-safe client accessor with lazy initialization."""
with self._client_lock:
if self._client is not None:
return self._client
base_url = self._config.get("base_url", "http://localhost:8000")
timeout = float(self._config.get("timeout", 10.0))
self._client = LocalMem0Client(base_url, timeout=timeout)
return self._client
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:
self._consecutive_failures = 0
return False
return True
def _record_success(self):
self._consecutive_failures = 0
def _record_failure(self):
self._consecutive_failures += 1
if self._consecutive_failures >= _BREAKER_THRESHOLD:
self._breaker_open_until = time.monotonic() + _BREAKER_COOLDOWN_SECS
logger.warning(
"Mem0 circuit breaker tripped after %d consecutive failures. "
"Pausing API calls for %ds.",
self._consecutive_failures,
_BREAKER_COOLDOWN_SECS,
)
def initialize(self, session_id: str, **kwargs) -> None:
self._config = _load_config()
# Prefer gateway-provided user_id for per-user memory scoping
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."""
return {"user_id": self._user_id}
def _write_filters(self) -> Dict[str, Any]:
"""Filters for add — scoped to user + agent."""
return {"user_id": self._user_id, "agent_id": self._agent_id}
def system_prompt_block(self) -> str:
return (
"# Mem0 Memory (Local)\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."
)
def prefetch(self, query: str, *, session_id: str = "") -> str:
"""Return cached prefetch result from previous turn."""
if self._prefetch_thread and self._prefetch_thread.is_alive():
self._prefetch_thread.join(timeout=3.0)
with self._prefetch_lock:
result = self._prefetch_result
self._prefetch_result = ""
if not result:
return ""
return f"## Mem0 Memory\n{result}"
def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
"""Queue async prefetch for next turn (called before LLM request)."""
if self._is_breaker_open():
return
def _run():
try:
client = self._get_client()
results = client.search(
query=query,
filters=self._read_filters(),
rerank=self._rerank,
top_k=5,
)
if results:
lines = [
r.get("text") or r.get("memory", "")
for r in results
if r.get("text") or r.get("memory")
]
with self._prefetch_lock:
self._prefetch_result = "\n".join(f"- {l}" for l in lines)
self._record_success()
except Exception as e:
self._record_failure()
logger.debug("Mem0 prefetch failed: %s", e)
self._prefetch_thread = threading.Thread(
target=_run, daemon=True, name="mem0-local-prefetch"
)
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
def _sync():
try:
client = self._get_client()
messages = [
{"role": "user", "content": user_content},
{"role": "assistant", "content": assistant_content},
]
client.add(messages, filters=self._write_filters(), infer=True)
self._record_success()
except Exception as e:
self._record_failure()
logger.warning("Mem0 sync failed: %s", e)
if self._sync_thread and self._sync_thread.is_alive():
self._sync_thread.join(timeout=5.0)
self._sync_thread = threading.Thread(
target=_sync, daemon=True, name="mem0-local-sync"
)
self._sync_thread.start()
def get_tool_schemas(self) -> List[Dict[str, Any]]:
return [PROFILE_SCHEMA, SEARCH_SCHEMA, CONCLUDE_SCHEMA]
def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:
if self._is_breaker_open():
return json.dumps(
{
"error": "Mem0 server temporarily unavailable (multiple consecutive failures). Will retry automatically."
}
)
try:
client = self._get_client()
except Exception as e:
return tool_error(str(e))
if tool_name == "mem0_profile":
try:
memories = client.get_all(filters=self._read_filters())
self._record_success()
if not memories:
return json.dumps({"result": "No memories stored yet."})
lines = [
m.get("text") or m.get("memory", "")
for m in memories
if m.get("text") or m.get("memory")
]
return json.dumps({"result": "\n".join(lines), "count": len(lines)})
except Exception as e:
self._record_failure()
return tool_error(f"Failed to fetch profile: {e}")
elif tool_name == "mem0_search":
query = args.get("query", "")
if not query:
return tool_error("Missing required parameter: query")
rerank = args.get("rerank", False)
top_k = min(int(args.get("top_k", 10)), 50)
try:
results = client.search(
query=query,
filters=self._read_filters(),
rerank=rerank,
top_k=top_k,
)
self._record_success()
if not results:
return json.dumps({"result": "No relevant memories found."})
items = [{"memory": r.get("text") or r.get("memory", ""), "score": r.get("score", 0)} for r in results]
return json.dumps({"results": items, "count": len(items)})
except Exception as e:
self._record_failure()
return tool_error(f"Search failed: {e}")
elif tool_name == "mem0_conclude":
conclusion = args.get("conclusion", "")
if not conclusion:
return tool_error("Missing required parameter: conclusion")
try:
client.add(
[{"role": "user", "content": conclusion}],
filters=self._write_filters(),
infer=False, # Store verbatim
)
self._record_success()
return json.dumps({"result": "Fact stored."})
except Exception as e:
self._record_failure()
return tool_error(f"Failed to store: {e}")
return tool_error(f"Unknown tool: {tool_name}")
def shutdown(self) -> None:
for t in (self._prefetch_thread, self._sync_thread):
if t and t.is_alive():
t.join(timeout=5.0)
with self._client_lock:
self._client = None
def register(ctx) -> None:
"""Register Mem0 local as a memory provider plugin."""
ctx.register_memory_provider(Mem0LocalMemoryProvider())
+194
View File
@@ -0,0 +1,194 @@
# Mem0 Local Plugin Installed ✓
## Quick Start
### 1. Verify Mem0 Server is Running
```bash
curl http://localhost:8000/health
```
If not running:
```bash
docker run -d -p 8000:8000 mem0ai/mem0:latest
```
For your setup on 10.0.0.150:8889:
```bash
curl http://10.0.0.150:8889/health
```
### 2. Configure the Plugin
The installer should have prompted you for configuration. To verify or modify:
```bash
nano ~/.hermes/.env
```
Add or update:
```env
MEM0_BASE_URL=http://10.0.0.150:8889
MEM0_USER_ID=henry_hofmann
MEM0_AGENT_ID=hermes
```
Or create a config file:
```bash
cat > ~/.hermes/mem0-local.json << 'EOF'
{
"base_url": "http://10.0.0.150:8889",
"user_id": "henry_hofmann",
"agent_id": "hermes",
"rerank": true,
"timeout": 10.0
}
EOF
```
### 3. Restart Hermes Gateway
```bash
hermes gateway restart
```
### 4. Activate Memory Provider
```bash
hermes memory mem0-local
```
Verify it's active:
```bash
hermes memory status
```
## Configuration
The plugin supports two configuration methods that work together:
### Method 1: Environment Variables (Primary)
Edit `~/.hermes/.env`:
```env
MEM0_BASE_URL=http://10.0.0.150:8889
MEM0_USER_ID=henry_hofmann
MEM0_AGENT_ID=hermes
```
### Method 2: Config File (Overrides)
Create `~/.hermes/mem0-local.json` to override specific settings:
```json
{
"base_url": "http://10.0.0.150:8889",
"user_id": "henry_hofmann",
"agent_id": "hermes",
"rerank": true,
"timeout": 10.0
}
```
**Note**: Config file values override environment variables. Use `.env` for defaults and JSON for overrides.
Key variables:
- `MEM0_BASE_URL` — Local server URL (your setup: `http://10.0.0.150:8889`)
- `MEM0_USER_ID` — User identifier for memory scoping (your setup: `henry_hofmann`)
- `MEM0_AGENT_ID` — Agent identifier (default: `hermes`)
- `rerank` — Enable reranking for higher precision (default: `true`)
- `timeout` — Request timeout in seconds (default: `10.0`)
## How It Works
Memory injection happens **automatically** without tool calls:
1. You send a message
2. Plugin searches Mem0 in background (~40ms)
3. Relevant memories injected into LLM prompt
4. LLM responds with full context
**Example**: If you stored "My favorite anime is Naruto", then ask "Is there a new episode of my favorite anime?", the LLM will receive:
```
Is there a new episode of my favorite anime?
## Mem0 Memory
- My favorite anime is Naruto
```
No tool call needed — instant context!
## Migration from Hardcoded Config
Your previous hardcoded configuration:
```yaml
mem0:
enabled: true
api_url: http://localhost:8889
user_id: henry_hofmann
collection_name: hermes_memory
mode: local
transparent:
enabled: true
search_threshold: 0.6
max_results: 3
include_match_score: true
injection_format: system_context
```
Is now replaced by the plugin with:
- Same functionality via `MEM0_BASE_URL`, `MEM0_USER_ID`
- Transparent memory injection via `queue_prefetch()` + `prefetch()`
- Configurable via `~/.hermes/mem0-local.json`
**Note**: The plugin uses a slightly different approach - memories are injected into the user message (not system prompt) to preserve prompt caching efficiency.
## Next Steps
- Start a conversation and mention personal facts
- Mem0 will automatically extract and store them
- Try asking questions that require remembering past conversations
- Use `mem0_profile` tool to see all stored memories
## Troubleshooting
If memory doesn't work:
1. **Check server connectivity**:
```bash
curl http://10.0.0.150:8889/health
```
2. **Check gateway logs**:
```bash
hermes gateway logs
```
3. **Verify provider is active**:
```bash
hermes memory status
```
4. **Check plugin is loaded**:
```bash
hermes plugins list
```
5. **Test manual memory operations**:
```bash
# In a conversation, ask Hermes to:
- "Store that my favorite color is blue"
- "What's my favorite color?"
```
## Available Tools
| Tool | Description |
|------|-------------|
| `mem0_profile` | Retrieve all stored memories |
| `mem0_search` | Search memories semantically |
| `mem0_conclude` | Store a fact explicitly |
---
**Enjoy your private, self-hosted memory!** 🚀
+139
View File
@@ -0,0 +1,139 @@
"""Local Mem0 server HTTP client."""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
import requests
logger = logging.getLogger(__name__)
class LocalMem0Client:
"""HTTP client for self-hosted Mem0 server.
Expects Mem0 server at MEM0_BASE_URL with endpoints:
- POST /search
- GET /memories
- POST /memories
"""
def __init__(self, base_url: str, timeout: float = 10.0):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self.session = requests.Session()
self.session.headers.update(
{
"Content-Type": "application/json",
"User-Agent": "hermes-agent-mem0-local-plugin/1.0.0",
}
)
def _request(self, method: str, endpoint: str, json: Optional[Dict] = None, params: Optional[Dict] = None) -> Dict:
"""Make HTTP request with error handling."""
url = f"{self.base_url}{endpoint}"
try:
resp = self.session.request(method, url, json=json, params=params, timeout=self.timeout)
resp.raise_for_status()
return resp.json()
except requests.exceptions.Timeout:
logger.error("Mem0 request timed out after %ss", self.timeout)
raise
except requests.exceptions.ConnectionError as e:
logger.error("Failed to connect to Mem0 server at %s: %s", self.base_url, e)
raise
except requests.exceptions.HTTPError as e:
logger.error(
"Mem0 API error: %s - %s", e.response.status_code, e.response.text
)
raise
def search(
self,
query: str,
filters: Dict[str, Any],
rerank: bool = False,
top_k: int = 10,
) -> List[Dict]:
"""Search memories by semantic similarity.
Args:
query: Search query string
filters: Filters dict (e.g., {"user_id": "hermes-user"})
rerank: Enable reranking for higher precision
top_k: Maximum results to return
Returns:
List of memory dicts with "text", "score", etc.
"""
payload = {
"query": query,
"user_id": filters.get("user_id"),
"agent_id": filters.get("agent_id"),
"top_k": top_k,
}
if rerank is not None:
payload["rerank"] = rerank
result = self._request("POST", "/search", json=payload)
return self._unwrap_results(result)
def get_all(self, filters: Dict[str, Any]) -> List[Dict]:
"""Get all memories matching filters.
Args:
filters: Filters dict (e.g., {"user_id": "hermes-user"})
Returns:
List of all matching memory dicts.
"""
params = filters
result = self._request("GET", "/memories", params=params)
return self._unwrap_results(result)
def add(
self,
messages: List[Dict[str, str]],
filters: Dict[str, Any],
infer: bool = True,
) -> Dict:
"""Add conversation messages for fact extraction.
Args:
messages: List of {"role": "user|assistant", "content": "..."}
filters: Filters dict for scoping (user_id, agent_id)
infer: Whether to extract facts via LLM (True) or store verbatim (False)
Returns:
Response dict with added memory IDs.
"""
payload = {
"messages": messages,
"user_id": filters.get("user_id"),
"agent_id": filters.get("agent_id"),
}
if not infer:
payload["messages"] = [{"role": "user", "content": messages[0].get("content", "") if isinstance(messages[0], dict) else messages[0]}]
return self._request("POST", "/memories", json=payload)
@staticmethod
def _unwrap_results(response: Any) -> List[Dict]:
"""Normalize Mem0 API response.
OSS server returns {"memories": [...]} or {"results": [...]}
Cloud API returns {"results": [...]}
"""
if isinstance(response, dict):
# Try "memories" first (OSS server), then "results" (cloud/API v2)
return response.get("memories", response.get("results", []))
if isinstance(response, list):
return response
return []
def health(self) -> bool:
"""Check if server is reachable."""
try:
resp = self.session.get(f"{self.base_url}/health", timeout=5.0)
return resp.status_code == 200
except requests.exceptions.RequestException:
return False
+20
View File
@@ -0,0 +1,20 @@
name: mem0-local
version: "1.0.0"
description: "Mem0 local server memory provider (self-hosted)"
author: "Henry Hofmann"
manifest_version: 1
requires_env:
- name: MEM0_BASE_URL
description: "Local Mem0 server URL (e.g., http://localhost:8000)"
url: "https://github.com/mem0ai/mem0"
- name: MEM0_USER_ID
description: "User identifier for memory scoping (default: hermes-user)"
provides_tools:
- mem0_profile
- mem0_search
- mem0_conclude
pip_dependencies:
- requests