mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-18 14:52:04 +00:00
feat(memory): add Supermemory setup connection summary
Add post_setup() and get_status_config() to the Supermemory memory provider so `hermes memory setup` and `hermes memory status` print a one-line connection summary (container, profile fact count, auto_recall/auto_capture). Point API-key onboarding at the Hermes connect URL (app.supermemory.ai/integrations?connect=hermes). Salvage of #52988. Two fixes folded in: - Test isolation: the new probe/status tests mocked _SupermemoryClient but not the __import__("supermemory") guard inside _probe_supermemory_connection, so they passed only where the optional supermemory package was installed and failed on a clean checkout / CI (the PR shipped with red CI). Added _stub_supermemory_importable() mirroring the existing test_is_available_false_when_import_missing pattern; the suite now passes with supermemory absent. - post_setup: `if api_key and api_key not in os.environ` checked whether the key's *value* named an env var (always false in practice). Fixed to compare the value: `os.environ.get("SUPERMEMORY_API_KEY") != api_key`. Verified: 38/38 in test_supermemory_provider.py and the full tests/plugins/memory/ suite green with supermemory not installed. Closes #52988
This commit is contained in:
parent
8827300267
commit
1b75b3fd90
5 changed files with 268 additions and 4 deletions
|
|
@ -5,7 +5,7 @@ Semantic long-term memory with profile recall, semantic search, explicit memory
|
|||
## Requirements
|
||||
|
||||
- `pip install supermemory`
|
||||
- Supermemory API key from [supermemory.ai](https://supermemory.ai)
|
||||
- Supermemory API key from [app.supermemory.ai/integrations?connect=hermes](http://app.supermemory.ai/integrations?connect=hermes)
|
||||
|
||||
## Setup
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ _DEFAULT_API_TIMEOUT = 5.0
|
|||
_MIN_CAPTURE_LENGTH = 10
|
||||
_MAX_ENTITY_CONTEXT_LENGTH = 1500
|
||||
_CONVERSATIONS_URL = "https://api.supermemory.ai/v4/conversations"
|
||||
_API_KEY_URL = "http://app.supermemory.ai/integrations?connect=hermes"
|
||||
_TRIVIAL_RE = re.compile(
|
||||
r"^(ok|okay|thanks|thank you|got it|sure|yes|no|yep|nope|k|ty|thx|np)\.?$",
|
||||
re.IGNORECASE,
|
||||
|
|
@ -387,6 +388,65 @@ class _SupermemoryClient:
|
|||
return
|
||||
|
||||
|
||||
def _resolve_container_tag_for_setup(hermes_home: str, *, identity: str = "default") -> str:
|
||||
config = _load_supermemory_config(hermes_home)
|
||||
env_tag = os.environ.get("SUPERMEMORY_CONTAINER_TAG", "").strip()
|
||||
raw_tag = env_tag or config["container_tag"]
|
||||
return _sanitize_tag(raw_tag.replace("{identity}", identity))
|
||||
|
||||
|
||||
def _probe_supermemory_connection(api_key: str, hermes_home: str, *, identity: str = "default") -> dict:
|
||||
config = _load_supermemory_config(hermes_home)
|
||||
status = {
|
||||
"ok": False,
|
||||
"error": "",
|
||||
"container_tag": _resolve_container_tag_for_setup(hermes_home, identity=identity),
|
||||
"profile_facts": 0,
|
||||
"auto_recall": bool(config["auto_recall"]),
|
||||
"auto_capture": bool(config["auto_capture"]),
|
||||
}
|
||||
if not (api_key or "").strip():
|
||||
status["error"] = "SUPERMEMORY_API_KEY not set"
|
||||
return status
|
||||
try:
|
||||
__import__("supermemory")
|
||||
except ImportError:
|
||||
status["error"] = "supermemory package not installed"
|
||||
return status
|
||||
try:
|
||||
client = _SupermemoryClient(
|
||||
api_key=api_key.strip(),
|
||||
timeout=config["api_timeout"],
|
||||
container_tag=status["container_tag"],
|
||||
search_mode=config["search_mode"],
|
||||
)
|
||||
profile = client.get_profile()
|
||||
facts = [
|
||||
fact for fact in (profile.get("static") or []) + (profile.get("dynamic") or [])
|
||||
if fact and str(fact).strip()
|
||||
]
|
||||
status["profile_facts"] = len(facts)
|
||||
status["ok"] = True
|
||||
except Exception as exc:
|
||||
status["error"] = str(exc).strip()[:160] or "connection failed"
|
||||
return status
|
||||
|
||||
|
||||
def _format_connection_summary(status: dict) -> str:
|
||||
recall = "on" if status.get("auto_recall") else "off"
|
||||
capture = "on" if status.get("auto_capture") else "off"
|
||||
container = status.get("container_tag") or _DEFAULT_CONTAINER_TAG
|
||||
if status.get("ok"):
|
||||
facts = int(status.get("profile_facts") or 0)
|
||||
fact_label = "fact" if facts == 1 else "facts"
|
||||
return (
|
||||
f"✓ Connected · container: {container} · {facts} profile {fact_label} · "
|
||||
f"auto_recall {recall} · auto_capture {capture}"
|
||||
)
|
||||
err = status.get("error") or "connection failed"
|
||||
return f"✗ {err} · container: {container} · auto_recall {recall} · auto_capture {capture}"
|
||||
|
||||
|
||||
STORE_SCHEMA = {
|
||||
"name": "supermemory_store",
|
||||
"description": "Store an explicit memory for future recall.",
|
||||
|
|
@ -487,7 +547,7 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
|||
# All other options are documented for $HERMES_HOME/supermemory.json
|
||||
# or the SUPERMEMORY_CONTAINER_TAG env var.
|
||||
return [
|
||||
{"key": "api_key", "description": "Supermemory API key", "secret": True, "required": True, "env_var": "SUPERMEMORY_API_KEY", "url": "https://supermemory.ai"},
|
||||
{"key": "api_key", "description": "Supermemory API key", "secret": True, "required": True, "env_var": "SUPERMEMORY_API_KEY", "url": _API_KEY_URL},
|
||||
]
|
||||
|
||||
def save_config(self, values, hermes_home):
|
||||
|
|
@ -498,6 +558,57 @@ class SupermemoryMemoryProvider(MemoryProvider):
|
|||
sanitized["entity_context"] = _clamp_entity_context(str(sanitized["entity_context"]))
|
||||
_save_supermemory_config(sanitized, hermes_home)
|
||||
|
||||
def get_status_config(self, provider_config: dict) -> dict:
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
del provider_config
|
||||
hermes_home = str(get_hermes_home())
|
||||
api_key = os.environ.get("SUPERMEMORY_API_KEY", "")
|
||||
status = _probe_supermemory_connection(api_key, hermes_home)
|
||||
return {"summary": _format_connection_summary(status)}
|
||||
|
||||
def post_setup(self, hermes_home: str, config: dict) -> None:
|
||||
from pathlib import Path
|
||||
|
||||
from hermes_cli.config import save_config
|
||||
from hermes_cli.memory_setup import _prompt, _write_env_vars
|
||||
|
||||
print("\n Configuring supermemory:\n")
|
||||
print(f" Get your API key at {_API_KEY_URL}\n")
|
||||
|
||||
env_writes: dict[str, str] = {}
|
||||
existing = os.environ.get("SUPERMEMORY_API_KEY", "")
|
||||
if existing:
|
||||
masked = f"...{existing[-4:]}" if len(existing) > 4 else "set"
|
||||
val = _prompt(f"Supermemory API key (current: {masked}, blank to keep)", secret=True)
|
||||
else:
|
||||
val = _prompt("Supermemory API key", secret=True)
|
||||
if val:
|
||||
env_writes["SUPERMEMORY_API_KEY"] = val
|
||||
|
||||
if not isinstance(config.get("memory"), dict):
|
||||
config["memory"] = {}
|
||||
config["memory"]["provider"] = self.name
|
||||
save_config(config)
|
||||
|
||||
if env_writes:
|
||||
_write_env_vars(Path(hermes_home) / ".env", env_writes)
|
||||
|
||||
api_key = env_writes.get("SUPERMEMORY_API_KEY") or existing
|
||||
# Make the freshly-entered key visible to the connection probe below.
|
||||
# (Checks the VALUE of SUPERMEMORY_API_KEY, not whether the key string
|
||||
# happens to name some unrelated env var.)
|
||||
if api_key and os.environ.get("SUPERMEMORY_API_KEY") != api_key:
|
||||
os.environ["SUPERMEMORY_API_KEY"] = api_key
|
||||
|
||||
status = _probe_supermemory_connection(api_key, hermes_home)
|
||||
print(f"\n {_format_connection_summary(status)}")
|
||||
print("\n Memory provider: supermemory")
|
||||
print(" Activation saved to config.yaml")
|
||||
if env_writes:
|
||||
print(" API keys saved to .env")
|
||||
print("\n Start a new session to activate.\n")
|
||||
|
||||
def initialize(self, session_id: str, **kwargs) -> None:
|
||||
from hermes_constants import get_hermes_home
|
||||
self._hermes_home = kwargs.get("hermes_home") or str(get_hermes_home())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue