mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-03 07:21:54 +00:00
Add Honcho runtime peer mapping
This commit is contained in:
parent
519657aa98
commit
864cdb3d2e
10 changed files with 376 additions and 33 deletions
|
|
@ -360,6 +360,7 @@ class HonchoMemoryProvider(MemoryProvider):
|
|||
config=cfg,
|
||||
context_tokens=cfg.context_tokens,
|
||||
runtime_user_peer_name=kwargs.get("user_id") or None,
|
||||
runtime_user_peer_name_alt=kwargs.get("user_id_alt") or None,
|
||||
)
|
||||
|
||||
# ----- B3: resolve_session_name -----
|
||||
|
|
|
|||
|
|
@ -122,6 +122,34 @@ def _parse_int_config(host_val, root_val, default: int) -> int:
|
|||
return default
|
||||
|
||||
|
||||
def _parse_string_map(host_obj: dict, root_obj: dict, key: str) -> dict[str, str]:
|
||||
"""Parse a string-to-string map with host-level whole-map override."""
|
||||
source = host_obj[key] if key in host_obj else root_obj.get(key)
|
||||
if not isinstance(source, dict):
|
||||
return {}
|
||||
|
||||
result: dict[str, str] = {}
|
||||
for raw_key, raw_value in source.items():
|
||||
alias_key = str(raw_key).strip()
|
||||
alias_value = str(raw_value).strip() if raw_value is not None else ""
|
||||
if alias_key and alias_value:
|
||||
result[alias_key] = alias_value
|
||||
return result
|
||||
|
||||
|
||||
def _parse_optional_string(
|
||||
host_obj: dict, root_obj: dict, key: str, default: str = ""
|
||||
) -> str:
|
||||
"""Parse a string field where host-level empty string can override root."""
|
||||
if key in host_obj:
|
||||
value = host_obj.get(key)
|
||||
else:
|
||||
value = root_obj.get(key, default)
|
||||
if value is None:
|
||||
return default
|
||||
return str(value).strip()
|
||||
|
||||
|
||||
def _parse_dialectic_depth(host_val, root_val) -> int:
|
||||
"""Parse dialecticDepth: host wins, then root, then 1. Clamped to 1-3."""
|
||||
for val in (host_val, root_val):
|
||||
|
|
@ -259,6 +287,12 @@ class HonchoClientConfig:
|
|||
# each platform would fork memory into its own peer (#14984). Default
|
||||
# ``False`` preserves existing multi-user behaviour.
|
||||
pin_peer_name: bool = False
|
||||
# Map gateway runtime user IDs to stable Honcho user peers. Host-level
|
||||
# config replaces the root map as a whole so profiles can intentionally
|
||||
# own their identity mappings.
|
||||
user_peer_aliases: dict[str, str] = field(default_factory=dict)
|
||||
# Optional prefix for unknown gateway runtime user IDs, e.g. "telegram_".
|
||||
runtime_peer_prefix: str = ""
|
||||
# Toggles
|
||||
enabled: bool = False
|
||||
save_messages: bool = True
|
||||
|
|
@ -458,6 +492,16 @@ class HonchoClientConfig:
|
|||
raw.get("pinPeerName"),
|
||||
default=False,
|
||||
),
|
||||
user_peer_aliases=_parse_string_map(
|
||||
host_block,
|
||||
raw,
|
||||
"userPeerAliases",
|
||||
),
|
||||
runtime_peer_prefix=_parse_optional_string(
|
||||
host_block,
|
||||
raw,
|
||||
"runtimePeerPrefix",
|
||||
),
|
||||
enabled=enabled,
|
||||
save_messages=save_messages,
|
||||
write_frequency=write_frequency,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ class HonchoSessionManager:
|
|||
context_tokens: int | None = None,
|
||||
config: Any | None = None,
|
||||
runtime_user_peer_name: str | None = None,
|
||||
runtime_user_peer_name_alt: str | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize the session manager.
|
||||
|
|
@ -89,11 +90,13 @@ class HonchoSessionManager:
|
|||
config: HonchoClientConfig from global config (provides peer_name, ai_peer,
|
||||
write_frequency, observation, etc.).
|
||||
runtime_user_peer_name: Gateway user identity for per-user memory scoping.
|
||||
runtime_user_peer_name_alt: Optional stable alternate gateway identity.
|
||||
"""
|
||||
self._honcho = honcho
|
||||
self._context_tokens = context_tokens
|
||||
self._config = config
|
||||
self._runtime_user_peer_name = runtime_user_peer_name
|
||||
self._runtime_user_peer_name_alt = runtime_user_peer_name_alt
|
||||
self._cache: dict[str, HonchoSession] = {}
|
||||
self._cache_lock = threading.RLock()
|
||||
self._peers_cache: dict[str, Any] = {}
|
||||
|
|
@ -267,6 +270,55 @@ class HonchoSessionManager:
|
|||
"""Sanitize an ID to match Honcho's pattern: ^[a-zA-Z0-9_-]+"""
|
||||
return re.sub(r'[^a-zA-Z0-9_-]', '-', id_str)
|
||||
|
||||
def _runtime_user_ids(self) -> list[str]:
|
||||
"""Return runtime identity candidates in lookup order."""
|
||||
candidates: list[str] = []
|
||||
for value in (self._runtime_user_peer_name, self._runtime_user_peer_name_alt):
|
||||
if value is None:
|
||||
continue
|
||||
candidate = str(value).strip()
|
||||
if candidate and candidate not in candidates:
|
||||
candidates.append(candidate)
|
||||
return candidates
|
||||
|
||||
def _session_key_fallback_peer_id(self, key: str) -> str:
|
||||
parts = key.split(":", 1)
|
||||
channel = parts[0] if len(parts) > 1 else "default"
|
||||
chat_id = parts[1] if len(parts) > 1 else key
|
||||
return self._sanitize_id(f"user-{channel}-{chat_id}")
|
||||
|
||||
def _resolve_user_peer_id(self, key: str) -> str:
|
||||
"""Resolve the Honcho user peer ID for this manager/session."""
|
||||
pin_peer_name = (
|
||||
self._config is not None
|
||||
and bool(getattr(self._config, "peer_name", None))
|
||||
and getattr(self._config, "pin_peer_name", False) is True
|
||||
)
|
||||
if pin_peer_name:
|
||||
return self._sanitize_id(self._config.peer_name)
|
||||
|
||||
runtime_ids = self._runtime_user_ids()
|
||||
if runtime_ids:
|
||||
aliases = getattr(self._config, "user_peer_aliases", {}) if self._config else {}
|
||||
if not isinstance(aliases, dict):
|
||||
aliases = {}
|
||||
for runtime_id in runtime_ids:
|
||||
alias = aliases.get(runtime_id)
|
||||
if isinstance(alias, str) and alias.strip():
|
||||
return self._sanitize_id(alias.strip())
|
||||
|
||||
primary_runtime_id = runtime_ids[0]
|
||||
prefix = getattr(self._config, "runtime_peer_prefix", "") if self._config else ""
|
||||
prefix = prefix.strip() if isinstance(prefix, str) else ""
|
||||
if prefix:
|
||||
return self._sanitize_id(f"{prefix}{primary_runtime_id}")
|
||||
return self._sanitize_id(primary_runtime_id)
|
||||
|
||||
if self._config and self._config.peer_name:
|
||||
return self._sanitize_id(self._config.peer_name)
|
||||
|
||||
return self._session_key_fallback_peer_id(key)
|
||||
|
||||
def get_or_create(self, key: str) -> HonchoSession:
|
||||
"""
|
||||
Get an existing session or create a new one.
|
||||
|
|
@ -285,31 +337,11 @@ class HonchoSessionManager:
|
|||
# Determine peer IDs — no lock needed (read-only, no shared state mutation).
|
||||
# Gateway sessions normally use the runtime user identity (the
|
||||
# platform-native ID: Telegram UID, Discord snowflake, Slack user,
|
||||
# etc.) so multi-user bots scope memory per user. For a single-user
|
||||
# deployment the config-supplied ``peer_name`` is an unambiguous
|
||||
# identity and we should keep it unified across platforms — see
|
||||
# #14984. Opt into that with ``hosts.<host>.pinPeerName: true`` in
|
||||
# ``honcho.json`` (or root-level ``pinPeerName: true``).
|
||||
# `is True` (not `bool(...)`) is deliberate: several multi-user tests
|
||||
# pass a ``MagicMock`` for ``config`` where ``mock.pin_peer_name``
|
||||
# silently returns another MagicMock — truthy by default. Requiring
|
||||
# strict ``True`` keeps pinning as opt-in even for callers that
|
||||
# haven't updated their mocks yet; real configs built via
|
||||
# ``from_global_config`` always produce a proper boolean.
|
||||
pin_peer_name = (
|
||||
self._config is not None
|
||||
and bool(getattr(self._config, "peer_name", None))
|
||||
and getattr(self._config, "pin_peer_name", False) is True
|
||||
)
|
||||
if self._runtime_user_peer_name and not pin_peer_name:
|
||||
user_peer_id = self._sanitize_id(self._runtime_user_peer_name)
|
||||
elif self._config and self._config.peer_name:
|
||||
user_peer_id = self._sanitize_id(self._config.peer_name)
|
||||
else:
|
||||
parts = key.split(":", 1)
|
||||
channel = parts[0] if len(parts) > 1 else "default"
|
||||
chat_id = parts[1] if len(parts) > 1 else key
|
||||
user_peer_id = self._sanitize_id(f"user-{channel}-{chat_id}")
|
||||
# etc.) so multi-user bots scope memory per user. Config can alias
|
||||
# known runtime IDs or prefix unknown IDs. For a single-user
|
||||
# deployment, ``pinPeerName`` still pins all runtime identities to
|
||||
# ``peerName`` (see #14984).
|
||||
user_peer_id = self._resolve_user_peer_id(key)
|
||||
|
||||
assistant_peer_id = self._sanitize_id(
|
||||
self._config.ai_peer if self._config else "hermes-assistant"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue