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