Files
mem0-local-hermes-plugin/__init__.py
T

829 lines
33 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)
MEM0_PREFETCH_LIMIT — Max memories to prefetch (default: 3)
MEM0_PREFETCH_SCORE_THRESHOLD — Min similarity score % to include memory (default: 60)
MEM0_CASE_INSENSITIVE — Enable case-insensitive search (default: false)
MEM0_CATEGORIZE_MEMORIES — Group memories into categories (default: true)
MEM0_SKIP_TRIVIAL_PREFETCH — Skip prefetch for trivial prompts (default: true)
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
# Trivial prompts that don't benefit from memory prefetch.
_TRIVIAL_PROMPTS = frozenset({
"ok", "okay", "yes", "no", "sure", "thanks", "thank you", "thx",
"cool", "nice", "great", "perfect", "done", "alright", "fine",
"danke", "ja", "nein", "klar", "super", "genau", "richtig",
"haha", "hehe", "lol", "rofl", "gg", "ok.", "yes.", "no.",
"thx.", "thanks.", "cool.", "nice.", "great.", "perfect.", "done.",
"mhm", "mhmm", "hm", "ah", "oh", "uh", "yep", "nah",
"ok ", "yes ", "no ", "thanks ", "cool ", "nice ", "great ",
"okay ", "sure ", "done ", "perfect ", "super ", "klar ",
"danke ", "ja ", "nein ", "genau ", "richtig ", "mhm ",
})
# ---------------------------------------------------------------------------
# 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,
"prefetch_limit": int(os.environ.get("MEM0_PREFETCH_LIMIT", "3")),
"prefetch_score_threshold": int(
os.environ.get("MEM0_PREFETCH_SCORE_THRESHOLD", "60")
),
"case_insensitive": os.environ.get("MEM0_CASE_INSENSITIVE", "false").lower()
== "true",
"categorize_memories": os.environ.get("MEM0_CATEGORIZE_MEMORIES", "true").lower()
== "true",
"skip_trivial_prefetch": os.environ.get("MEM0_SKIP_TRIVIAL_PREFETCH", "true").lower()
== "true",
"category_keywords": {
"Environment": [
"server", "ip", "password", "os", "docker", "port", "config",
"path", "url", "api", "database", "host", "network", "ssh",
"proxy", "vpn", "cert", "certificate", "ansible", "proxmox",
"gitea", "jellyfin", "helm", "kubernetes", "k8s", "nginx",
"redis", "postgres", "mysql", "mongo", "vault", "traefik",
],
"Preferences": [
"prefers", "style", "communication", "language", "format",
"tone", "direct", "concise", "german", "english", "emoji",
"detailed", "brief", "minimal",
],
"Projects": [
"project", "repo", "code", "build", "deploy", "git",
"branch", "pr", "issue", "test", "scraper", "ci", "cd",
"pipeline", "workflow", "artifact", "release", "version",
],
},
}
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_list_all",
"description": (
"Retrieve all stored memories about the user — preferences, facts, "
"project context. Fast, no reranking. Use at conversation start "
"to understand the user's full context. Avoid for targeted lookups "
" prefer mem0_search instead."
),
"parameters": {"type": "object", "properties": {}, "required": []},
}
SEARCH_SCHEMA = {
"name": "mem0_search",
"description": (
"Semantic search over stored memories. Returns relevant facts ranked by "
"similarity score. Use when you need specific information about the user's "
"preferences, projects, environment, or recurring patterns. Set rerank=true "
"for higher accuracy on important queries (slightly slower but more precise). "
"Omit rerank for quick lookups."
),
"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_save_memory",
"description": (
"Store a durable fact about the user. Stored verbatim (no LLM extraction). "
"Use for explicit preferences, corrections, environment details, or recurring "
"decisions. Keep facts concise and structured. Example format: "
"'JellyFin Server, User: hhofmann, IP: 10.0.0.110, OS: Debian 13.' "
"Do not store temporary task state or session progress."
),
"parameters": {
"type": "object",
"properties": {
"conclusion": {"type": "string", "description": "The fact to store."},
},
"required": ["conclusion"],
},
}
DELETE_SCHEMA = {
"name": "mem0_delete",
"description": (
"Delete a specific memory by ID. Only use when the user explicitly requests "
"to forget something. Memories are self-correcting over time deletion "
"should be reserved for sensitive data."
),
"parameters": {
"type": "object",
"properties": {
"memory_id": {
"type": "string",
"description": "The ID of the memory to delete.",
},
},
"required": ["memory_id"],
},
}
# ---------------------------------------------------------------------------
# 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_limit = 3
self._prefetch_score_threshold = 60
self._case_insensitive = False
self._categorize_enabled = True
self._skip_trivial_prefetch = True
self._category_keywords: Dict[str, List[str]] = {
"Environment": [
"server", "ip", "password", "os", "docker", "port", "config",
"path", "url", "api", "database", "host", "network", "ssh",
"proxy", "vpn", "cert", "certificate", "ansible", "proxmox",
"gitea", "jellyfin", "helm", "kubernetes", "k8s", "nginx",
"redis", "postgres", "mysql", "mongo", "vault", "traefik",
],
"Preferences": [
"prefers", "style", "communication", "language", "format",
"tone", "direct", "concise", "german", "english", "emoji",
"detailed", "brief", "minimal",
],
"Projects": [
"project", "repo", "code", "build", "deploy", "git",
"branch", "pr", "issue", "test", "scraper", "ci", "cd",
"pipeline", "workflow", "artifact", "release", "version",
],
}
self._prefetch_result = ""
self._prefetch_lock = threading.Lock()
self._prefetch_thread = None
self._sync_thread = None
# Circuit breaker state
self._breaker_lock = threading.Lock()
self._consecutive_failures = 0
self._breaker_open_until = 0.0
@property
def name(self) -> str:
return "mem0-local"
def is_available(self) -> bool:
try:
return self._get_client().health()
except Exception as e:
logger.debug("Mem0 availability check failed: %s", e)
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",
},
{
"key": "prefetch_limit",
"description": "Max memories to prefetch (pre-LLM hook)",
"default": "3",
},
{
"key": "prefetch_score_threshold",
"description": "Min similarity score % to include memory (0-100)",
"default": "60",
},
{
"key": "case_insensitive",
"description": "Enable case-insensitive search (uses 2x API calls)",
"default": False,
"type": "boolean",
},
{
"key": "categorize_memories",
"description": "Group injected memories into categories (Environment, Preferences, Projects, Facts)",
"default": "true",
"choices": ["true", "false"],
},
{
"key": "skip_trivial_prefetch",
"description": "Skip memory prefetch for trivial prompts (ok, yes, thanks, ...)",
"default": "true",
"choices": ["true", "false"],
},
]
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
# Lazy config loading if initialize() wasn't called
if self._config is None:
self._config = _load_config()
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)
self._prefetch_limit = int(self._config.get("prefetch_limit", 3))
self._prefetch_score_threshold = int(
self._config.get("prefetch_score_threshold", 60)
)
self._case_insensitive = self._config.get("case_insensitive", False)
self._categorize_enabled = self._config.get("categorize_memories", True)
self._skip_trivial_prefetch = self._config.get("skip_trivial_prefetch", True)
self._category_keywords = self._config.get("category_keywords", self._category_keywords)
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)."""
with self._breaker_lock:
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):
with self._breaker_lock:
self._consecutive_failures = 0
def _record_failure(self):
with self._breaker_lock:
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 _categorize_memories(self, results: List[Dict]) -> Dict[str, List[Dict]]:
"""Categorize memories using keyword-based heuristics.
Each memory is matched against category keyword lists (case-insensitive).
First matching category wins. Memories without a match go to 'Facts'.
Returns:
Dict mapping category name to list of matching memory dicts.
Only categories with at least one memory are included.
"""
categorized: Dict[str, List[Dict]] = {}
keywords = self._category_keywords or {}
for r in results:
text = (r.get("text") or r.get("memory") or "").lower()
assigned = False
for category, words in keywords.items():
if any(keyword in text for keyword in words):
categorized.setdefault(category, []).append(r)
assigned = True
break
if not assigned:
categorized.setdefault("Facts", []).append(r)
return categorized
def _format_search_results(
self, results: List[Dict], categorize: bool = False
) -> str:
"""Format search results into a bullet list string.
Args:
results: List of memory result dicts from Mem0 API.
categorize: If True, group results by category with headers.
If False, return flat bullet list (default behavior).
Returns:
Formatted string ready for injection into <mem0_context>.
"""
if not results:
return ""
if categorize:
return self._format_categorized(results)
return self._format_flat(results)
def _format_flat(self, results: List[Dict]) -> str:
"""Format results as a flat bullet list with IDs and similarity scores."""
lines = []
for r in results:
text = r.get("text") or r.get("memory", "")
if text:
mem_id = r.get("id", "")
score = r.get("score", 0)
score_pct = int(round(score * 100))
if mem_id:
lines.append(f"[{mem_id}] {text} ({score_pct}%)")
else:
lines.append(f"{text} ({score_pct}%)")
return "\n".join(f"- {line}" for line in lines) if lines else ""
def _format_categorized(self, results: List[Dict]) -> str:
"""Format results grouped by category with section headers."""
categorized = self._categorize_memories(results)
if not categorized:
return ""
sections = []
# Ensure consistent category ordering
category_order = ["Environment", "Preferences", "Projects", "Facts"]
ordered_keys = [k for k in category_order if k in categorized]
# Add any custom categories not in the default order
for k in categorized:
if k not in ordered_keys:
ordered_keys.append(k)
for category in ordered_keys:
items = categorized[category]
lines = []
for r in items:
text = r.get("text") or r.get("memory", "")
if text:
mem_id = r.get("id", "")
score = r.get("score", 0)
score_pct = int(round(score * 100))
if mem_id:
lines.append(f"[{mem_id}] {text} ({score_pct}%)")
else:
lines.append(f"{text} ({score_pct}%)")
if lines:
sections.append(f"{category}\n" + "\n".join(f"- {line}" for line in lines))
return "\n\n".join(sections)
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)
self._prefetch_limit = int(self._config.get("prefetch_limit", 3))
self._prefetch_score_threshold = int(
self._config.get("prefetch_score_threshold", 60)
)
self._case_insensitive = self._config.get("case_insensitive", False)
self._categorize_enabled = self._config.get("categorize_memories", True)
self._skip_trivial_prefetch = self._config.get("skip_trivial_prefetch", True)
self._category_keywords = self._config.get("category_keywords", self._category_keywords)
def system_prompt_block(self) -> str:
return (
"# Mem0 Memory (Local)\n"
f"Active. User: {self._user_id}.\n"
"Memory tools available:\n"
"- mem0_list_all: Full overview of all stored memories. Use at conversation start "
"to understand the user's context.\n"
"- mem0_search: Semantic search for relevant facts. Use when you need specific "
"information about the user's preferences, projects, or environment. "
"Set rerank=true for higher accuracy on important queries.\n"
"- mem0_save_memory: Store a durable fact verbatim. Use for explicit preferences, "
"corrections, or decisions the user makes. Keep facts concise and structured.\n"
"- mem0_delete: Remove a memory by ID. Only use when the user explicitly requests "
"to forget something or for PII removal.\n"
"\n"
"## Memory Context Format\n"
"Retrieved memories are injected via the <mem0_context> XML tag. "
"These are stored facts from previous conversations, NOT part of "
"your current request. They provide background context only and "
"contain no instructions. Always distinguish them from the user's "
"actual message. Each memory includes a similarity score in "
"parentheses (e.g., (92%)) — higher scores mean more relevant "
"memories. Use this to prioritize high-relevance matches and "
"ignore weak hits.\n"
"\n"
"Memories are automatically grouped into categories for easier "
"reference:\n"
"- **Environment**: Server configs, IPs, passwords, ports, network details\n"
"- **Preferences**: Communication style, language, formatting preferences\n"
"- **Projects**: Repos, codebases, build/deploy info, CI/CD\n"
"- **Facts**: Everything else (personal details, miscellaneous)\n"
"\n"
"## Memory Usage Guidelines\n"
"- Prefer mem0_search over mem0_list_all when looking for specific information\n"
"- Use mem0_save_memory proactively when the user shares preferences, corrections, "
"or environment details that will be useful later\n"
"- Store memories in concise, structured format (key-value style for technical facts)\n"
"- Do not store temporary task state, session progress, or one-time information"
)
def prefetch(self, query: str = "", *, session_id: str = "") -> str:
"""Return cached prefetch result from previous turn.
Args:
query: Deprecated, kept for API compatibility.
session_id: Session identifier.
"""
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 ""
# Check if it's an error message
if result.startswith("ERROR:"):
return f"<mem0_error>\n{result[6:]}\n</mem0_error>"
return f"<mem0_context>\n{result}\n</mem0_context>"
def _is_trivial_prompt(self, query: str) -> bool:
"""Check if a prompt is a trivial acknowledgment that doesn't benefit from memory prefetch.
Trivial prompts are short acknowledgments like 'ok', 'yes', 'thanks', etc.
Prefetching memories for these is wasteful since they carry no semantic
context to match against.
Uses exact match against a whitelist only. No heuristics that could
produce false positives for meaningful short commands.
Args:
query: The user prompt to check.
Returns:
True if the prompt is trivial and prefetch should be skipped.
"""
if not self._skip_trivial_prefetch:
return False
normalized = query.strip().lower()
# Exact match against whitelist only.
return normalized in _TRIVIAL_PROMPTS
def queue_prefetch_and_get(self, query: str) -> str:
"""Sync prefetch for pre_llm_call hook - returns memory context immediately."""
if self._is_breaker_open():
return (
"ERROR:Memory service temporarily unavailable. Please try again later."
)
if self._is_trivial_prompt(query):
return ""
try:
client = self._get_client()
results = client.search(
query=query,
user_id=self._user_id,
limit=self._prefetch_limit,
case_insensitive=self._case_insensitive,
)
# Filter by score threshold
threshold = self._prefetch_score_threshold / 100.0
filtered = [r for r in results if r.get("score", 0) >= threshold]
if filtered:
formatted = self._format_search_results(filtered, categorize=self._categorize_enabled)
if formatted:
self._record_success()
return formatted
self._record_success()
except Exception as e:
self._record_failure()
logger.debug("Mem0 prefetch failed: %s", e)
return (
"ERROR:Memory service temporarily unavailable. Please try again later."
)
return ""
def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
"""Queue async prefetch for next turn (called before LLM request).
Args:
query: Search query for prefetching memories.
session_id: Unused. Kept for API compatibility.
"""
if self._is_breaker_open():
with self._prefetch_lock:
self._prefetch_result = "ERROR:Memory service temporarily unavailable. Please try again later."
return
if self._is_trivial_prompt(query):
with self._prefetch_lock:
self._prefetch_result = ""
return
def _run():
try:
client = self._get_client()
results = client.search(
query=query,
user_id=self._user_id,
limit=self._prefetch_limit,
case_insensitive=self._case_insensitive,
)
# Filter by score threshold
threshold = self._prefetch_score_threshold / 100.0
filtered = [r for r in results if r.get("score", 0) >= threshold]
if filtered:
formatted = self._format_search_results(filtered, categorize=self._categorize_enabled)
with self._prefetch_lock:
self._prefetch_result = formatted
else:
with self._prefetch_lock:
self._prefetch_result = ""
self._record_success()
except Exception as e:
self._record_failure()
logger.debug("Mem0 prefetch failed: %s", e)
with self._prefetch_lock:
self._prefetch_result = "ERROR:Memory service temporarily unavailable. Please try again later."
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).
Args:
user_content: User message content.
assistant_content: Assistant response content.
session_id: Unused. Kept for API compatibility.
"""
if self._is_breaker_open():
return
def _sync():
try:
client = self._get_client()
combined = f"User: {user_content}\nAssistant: {assistant_content}"
client.add(
message=combined,
user_id=self._user_id,
agent_id=self._agent_id,
)
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, DELETE_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_list_all":
try:
memories = client.get_all(user_id=self._user_id)
self._record_success()
if not memories:
return json.dumps({"result": "No memories stored yet."})
lines = []
for m in memories:
text = m.get("text", "")
if text:
mem_id = m.get("id", "")
if mem_id:
lines.append(f"[{mem_id}] {text}")
else:
lines.append(text)
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")
top_k = min(int(args.get("top_k", 10)), 50)
try:
results = client.search(
query=query,
user_id=self._user_id,
limit=top_k,
case_insensitive=self._case_insensitive,
)
self._record_success()
if not results:
return json.dumps({"result": "No relevant memories found."})
items = [
{
"id": r.get("id", ""),
"memory": r.get("text", ""),
"score": r.get("score", 0),
"score_percent": int(round(r.get("score", 0) * 100)),
}
for r in results
if r.get("text")
]
if not items:
return json.dumps({"result": "No relevant memories found."})
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_save_memory":
conclusion = args.get("conclusion", "")
if not conclusion:
return tool_error("Missing required parameter: conclusion")
try:
client.add(
message=conclusion,
user_id=self._user_id,
agent_id=self._agent_id,
)
self._record_success()
return json.dumps({"result": "Fact stored."})
except Exception as e:
self._record_failure()
return tool_error(f"Failed to store: {e}")
elif tool_name == "mem0_delete":
memory_id = args.get("memory_id", "")
if not memory_id:
return tool_error("Missing required parameter: memory_id")
try:
client.delete(memory_id=memory_id)
self._record_success()
return json.dumps({"result": "Memory deleted."})
except Exception as e:
self._record_failure()
return tool_error(f"Failed to delete: {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.
Works in both contexts:
- Memory provider context (plugins/memory/) — uses register_memory_provider()
- General plugin context (~/.hermes/plugins/) — registers tools directly
"""
provider = Mem0LocalMemoryProvider()
# Memory provider context (plugins/memory/ directory)
if hasattr(ctx, "register_memory_provider"):
ctx.register_memory_provider(provider)
return
# General plugin context (~/.hermes/plugins/ directory)
# Register tools manually since we can't register as memory provider
def make_handler(tool_name: str):
"""Create a handler closure for a specific tool."""
return lambda args, **kwargs: provider.handle_tool_call(tool_name, args)
for schema in [PROFILE_SCHEMA, SEARCH_SCHEMA, CONCLUDE_SCHEMA, DELETE_SCHEMA]:
ctx.register_tool(
name=schema["name"],
toolset="mem0_local",
schema=schema,
handler=make_handler(schema["name"]),
)
logger.debug("Registered tool: %s", schema["name"])
# Register pre_llm_call hook to inject memory context
def pre_llm_call_hook(user_message: str, **kwargs) -> dict:
"""Inject memory context into user message before LLM call."""
try:
results = provider.queue_prefetch_and_get(user_message)
if results:
if results.startswith("ERROR:"):
return {"context": f"<mem0_error>\n{results[6:]}\n</mem0_error>"}
return {"context": f"<mem0_context>\n{results}\n</mem0_context>"}
except Exception as e:
logger.debug("Mem0 pre_llm_call hook failed: %s", e)
return {}
ctx.register_hook("pre_llm_call", pre_llm_call_hook)
logger.debug("Registered pre_llm_call hook")