mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 16:01:49 +08:00
Adapts PR #1811 (Hindsight by benfrank241) and PR #2933 (Mem0 by kartik-mem0) to the MemoryProvider interface as drop-in plugins. Hindsight plugin (plugins/hindsight-memory/): - 3 tools: hindsight_retain, hindsight_recall, hindsight_reflect - Cloud (API key) or local (embedded PostgreSQL) modes - Background prefetch with thread isolation for aiohttp - Auto-sync turns to knowledge graph Mem0 plugin (plugins/mem0-memory/): - 4 tools: mem0_profile, mem0_search, mem0_context, mem0_conclude - Server-side LLM fact extraction and deduplication - Semantic search with optional reranking - Verbatim fact storage via conclude (infer=False) Both require API keys (HINDSIGHT_API_KEY / MEM0_API_KEY) and the respective SDK packages (hindsight-client / mem0ai). is_available() gates on credentials so installing the plugin without a key is safe.
287 lines
10 KiB
Python
287 lines
10 KiB
Python
"""Mem0 memory plugin — MemoryProvider interface.
|
|
|
|
Server-side LLM fact extraction, semantic search with reranking, and
|
|
automatic deduplication via the Mem0 Platform API.
|
|
|
|
Original PR #2933 by kartik-mem0, adapted to MemoryProvider ABC.
|
|
|
|
Config via environment variables:
|
|
MEM0_API_KEY — Mem0 Platform API key (required)
|
|
MEM0_USER_ID — User identifier (default: hermes-user)
|
|
MEM0_AGENT_ID — Agent identifier (default: hermes)
|
|
|
|
Or via $HERMES_HOME/mem0.json.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List
|
|
|
|
from agent.memory_provider import MemoryProvider
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Config
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _load_config() -> dict:
|
|
"""Load config from $HERMES_HOME/mem0.json or env vars."""
|
|
hermes_home = os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes"))
|
|
config_path = Path(hermes_home) / "mem0.json"
|
|
|
|
if config_path.exists():
|
|
try:
|
|
return json.loads(config_path.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
pass
|
|
|
|
return {
|
|
"api_key": os.environ.get("MEM0_API_KEY", ""),
|
|
"user_id": os.environ.get("MEM0_USER_ID", "hermes-user"),
|
|
"agent_id": os.environ.get("MEM0_AGENT_ID", "hermes"),
|
|
"rerank": True,
|
|
"keyword_search": False,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 (+150ms)."
|
|
),
|
|
"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"],
|
|
},
|
|
}
|
|
|
|
CONTEXT_SCHEMA = {
|
|
"name": "mem0_context",
|
|
"description": (
|
|
"Deep retrieval with forced reranking. Use when you need the most "
|
|
"relevant memories for a specific topic."
|
|
),
|
|
"parameters": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {"type": "string", "description": "What to search for."},
|
|
},
|
|
"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 Mem0MemoryProvider(MemoryProvider):
|
|
"""Mem0 Platform memory with server-side extraction and semantic search."""
|
|
|
|
def __init__(self):
|
|
self._config = None
|
|
self._client = None
|
|
self._api_key = ""
|
|
self._user_id = "hermes-user"
|
|
self._agent_id = "hermes"
|
|
self._rerank = True
|
|
self._prefetch_result = ""
|
|
self._prefetch_lock = threading.Lock()
|
|
self._prefetch_thread = None
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "mem0"
|
|
|
|
def is_available(self) -> bool:
|
|
cfg = _load_config()
|
|
return bool(cfg.get("api_key"))
|
|
|
|
def _get_client(self):
|
|
if self._client is not None:
|
|
return self._client
|
|
try:
|
|
from mem0 import MemoryClient
|
|
self._client = MemoryClient(api_key=self._api_key)
|
|
return self._client
|
|
except ImportError:
|
|
raise RuntimeError("mem0 package not installed. Run: pip install mem0ai")
|
|
|
|
def initialize(self, session_id: str, **kwargs) -> None:
|
|
self._config = _load_config()
|
|
self._api_key = self._config.get("api_key", "")
|
|
self._user_id = self._config.get("user_id", "hermes-user")
|
|
self._agent_id = self._config.get("agent_id", "hermes")
|
|
self._rerank = self._config.get("rerank", True)
|
|
|
|
def system_prompt_block(self) -> str:
|
|
return (
|
|
"# Mem0 Memory\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) -> str:
|
|
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) -> None:
|
|
def _run():
|
|
try:
|
|
client = self._get_client()
|
|
results = client.search(
|
|
query=query,
|
|
user_id=self._user_id,
|
|
rerank=self._rerank,
|
|
top_k=5,
|
|
)
|
|
if results:
|
|
lines = [r.get("memory", "") for r in results if r.get("memory")]
|
|
with self._prefetch_lock:
|
|
self._prefetch_result = "\n".join(f"- {l}" for l in lines)
|
|
except Exception as e:
|
|
logger.debug("Mem0 prefetch failed: %s", e)
|
|
|
|
self._prefetch_thread = threading.Thread(target=_run, daemon=True, name="mem0-prefetch")
|
|
self._prefetch_thread.start()
|
|
|
|
def sync_turn(self, user_content: str, assistant_content: str) -> None:
|
|
"""Send the turn to Mem0 for server-side fact extraction."""
|
|
try:
|
|
client = self._get_client()
|
|
messages = [
|
|
{"role": "user", "content": user_content},
|
|
{"role": "assistant", "content": assistant_content},
|
|
]
|
|
client.add(messages, user_id=self._user_id, agent_id=self._agent_id)
|
|
except Exception as e:
|
|
logger.warning("Mem0 sync failed: %s", e)
|
|
|
|
def get_tool_schemas(self) -> List[Dict[str, Any]]:
|
|
return [PROFILE_SCHEMA, SEARCH_SCHEMA, CONTEXT_SCHEMA, CONCLUDE_SCHEMA]
|
|
|
|
def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:
|
|
try:
|
|
client = self._get_client()
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)})
|
|
|
|
if tool_name == "mem0_profile":
|
|
try:
|
|
memories = client.get_all(user_id=self._user_id)
|
|
if not memories:
|
|
return json.dumps({"result": "No memories stored yet."})
|
|
lines = [m.get("memory", "") for m in memories if m.get("memory")]
|
|
return json.dumps({"result": "\n".join(lines), "count": len(lines)})
|
|
except Exception as e:
|
|
return json.dumps({"error": f"Failed to fetch profile: {e}"})
|
|
|
|
elif tool_name == "mem0_search":
|
|
query = args.get("query", "")
|
|
if not query:
|
|
return json.dumps({"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, user_id=self._user_id,
|
|
rerank=rerank, top_k=top_k,
|
|
)
|
|
if not results:
|
|
return json.dumps({"result": "No relevant memories found."})
|
|
items = [{"memory": r.get("memory", ""), "score": r.get("score", 0)} for r in results]
|
|
return json.dumps({"results": items, "count": len(items)})
|
|
except Exception as e:
|
|
return json.dumps({"error": f"Search failed: {e}"})
|
|
|
|
elif tool_name == "mem0_context":
|
|
query = args.get("query", "")
|
|
if not query:
|
|
return json.dumps({"error": "Missing required parameter: query"})
|
|
try:
|
|
results = client.search(
|
|
query=query, user_id=self._user_id,
|
|
rerank=True, top_k=5,
|
|
)
|
|
if not results:
|
|
return json.dumps({"result": "No relevant memories found."})
|
|
items = [{"memory": r.get("memory", ""), "score": r.get("score", 0)} for r in results]
|
|
return json.dumps({"results": items, "count": len(items)})
|
|
except Exception as e:
|
|
return json.dumps({"error": f"Context retrieval failed: {e}"})
|
|
|
|
elif tool_name == "mem0_conclude":
|
|
conclusion = args.get("conclusion", "")
|
|
if not conclusion:
|
|
return json.dumps({"error": "Missing required parameter: conclusion"})
|
|
try:
|
|
client.add(
|
|
[{"role": "user", "content": conclusion}],
|
|
user_id=self._user_id,
|
|
agent_id=self._agent_id,
|
|
infer=False,
|
|
)
|
|
return json.dumps({"result": "Fact stored."})
|
|
except Exception as e:
|
|
return json.dumps({"error": f"Failed to store: {e}"})
|
|
|
|
return json.dumps({"error": f"Unknown tool: {tool_name}"})
|
|
|
|
def shutdown(self) -> None:
|
|
if self._prefetch_thread and self._prefetch_thread.is_alive():
|
|
self._prefetch_thread.join(timeout=5.0)
|
|
self._client = None
|
|
|
|
|
|
def register(ctx) -> None:
|
|
"""Register Mem0 as a memory provider plugin."""
|
|
ctx.register_memory_provider(Mem0MemoryProvider())
|