feat(integrations): add Hermes provider with shared memory utilities

This commit is contained in:
kartik-mem0
2026-09-18 13:20:57 +05:30
parent 84bf468176
commit 4284ef0d2a
40 changed files with 4168 additions and 954 deletions
+8 -133
View File
@@ -11,7 +11,6 @@ from __future__ import annotations
import functools
import hashlib
import json
import math
import os
import re
import sqlite3
@@ -27,6 +26,14 @@ from pathlib import Path
from typing import Any, Iterable
import telemetry
from message_utils import MAX_EXTRACTION_INPUT_TOKENS as MAX_EXTRACTION_INPUT_TOKENS
from message_utils import SECRET_PATTERNS as SECRET_PATTERNS
from message_utils import _estimated_tokens as _estimated_tokens
from message_utils import _is_agent_assignment as _is_agent_assignment
from message_utils import _is_agent_response as _is_agent_response
from message_utils import _message_tokens as _message_tokens
from message_utils import extraction_message_batches as extraction_message_batches
from message_utils import redact as redact
DEFAULT_API_URL = "https://api.mem0.ai"
PLUGIN_VERSION = "0.3.1"
@@ -66,7 +73,6 @@ CHECKPOINT_EXCHANGES = 5
CHECKPOINT_MESSAGES = 10
CHECKPOINT_SOURCE_CHARS = 40000
DEFAULT_MAX_CONTEXT_CHARS = 4000
MAX_EXTRACTION_INPUT_TOKENS = 24000
MAX_FLUSH_ATTEMPTS = 5
FORGET_PAGE_SIZE = 100
FORGET_MAX_PAGES = 50
@@ -139,48 +145,11 @@ BUILD_COMMAND_RE = re.compile(
re.IGNORECASE,
)
SECRET_PATTERNS = [
re.compile(r"(?i)(authorization\s*[:=]\s*(?:bearer|token)\s+)[^\s\"']+"),
re.compile(
r"(?i)((?:api[_-]?key|secret[_-]?access[_-]?key|session[_-]?token)\s*[:=]\s*)[^\s\"']+"
),
re.compile(
r"(?i)((?:access[_-]?token|refresh[_-]?token|password|credential)"
r"\s*[:=]\s*)[^\s&\"']+"
),
re.compile(r"\b(?:sk|m0|mem0_sk|psk)-[A-Za-z0-9_\-]{12,}\b"),
re.compile(r"\b(?:ASIA|AKIA)[A-Z0-9]{12,}\b"),
re.compile(r"\b(?:ghp_|github_pat_|xox[baprs]-)[A-Za-z0-9_\-]{12,}\b"),
re.compile(
r"-----BEGIN [^-]*PRIVATE KEY-----.*?-----END [^-]*PRIVATE KEY-----",
re.DOTALL,
),
re.compile(
r'(?i)("(?:api[_-]?key|password|secret(?:[_-]?access[_-]?key)?'
r'|(?:access|refresh|session)[_-]?token|token|authorization|credential'
r')"\s*:\s*")(?:\\.|[^"\\])*'
),
]
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
def redact(value: Any) -> str:
text = (
value
if isinstance(value, str)
else json.dumps(value, ensure_ascii=False, default=str)
)
for pattern in SECRET_PATTERNS:
if pattern.groups:
text = pattern.sub(r"\1[REDACTED]", text)
else:
text = pattern.sub("[REDACTED]", text)
return text
def bounded(value: Any, limit: int) -> str:
text = redact(value).strip()
if len(text) <= limit:
@@ -1706,100 +1675,6 @@ def build_extraction_messages(structured: dict[str, Any]) -> list[dict[str, str]
return messages
def _estimated_tokens(value: str) -> int:
"""Conservatively estimate tokens without adding a tokenizer dependency."""
ascii_chars = sum(ord(char) < 128 for char in value)
return math.ceil((ascii_chars * 0.4) + (len(value) - ascii_chars))
def _message_tokens(messages: list[dict[str, str]]) -> int:
return _estimated_tokens(json.dumps(messages, ensure_ascii=False))
def _is_agent_assignment(message: dict[str, str]) -> bool:
return message.get("role") == "assistant" and message.get(
"content", ""
).startswith("Subagent assignment (")
def _is_agent_response(message: dict[str, str]) -> bool:
return message.get("role") == "assistant" and message.get(
"content", ""
).startswith("Subagent response (")
def extraction_message_batches(
messages: list[dict[str, str]],
*,
max_tokens: int = MAX_EXTRACTION_INPUT_TOKENS,
) -> list[list[dict[str, str]]]:
"""Keep exchanges together when possible; split oversized messages to enforce the request budget."""
if not messages or _message_tokens(messages) <= max_tokens:
return [messages]
exchanges: list[list[dict[str, str]]] = []
exchange: list[dict[str, str]] = []
for message in messages:
if message.get("role") == "user" and exchange:
exchanges.append(exchange)
exchange = []
exchange.append(message)
if exchange:
exchanges.append(exchange)
units: list[list[dict[str, str]]] = []
for exchange in exchanges:
if _message_tokens(exchange) <= max_tokens:
units.append(exchange)
continue
index = 0
while index < len(exchange):
message = exchange[index]
if (
_is_agent_assignment(message)
and index + 1 < len(exchange)
and _is_agent_response(exchange[index + 1])
):
units.append(exchange[index : index + 2])
index += 2
else:
units.append([message])
index += 1
bounded_units: list[list[dict[str, str]]] = []
for unit in units:
if _message_tokens(unit) <= max_tokens:
bounded_units.append(unit)
continue
for message in unit:
remaining = message["content"]
while remaining:
low, high = 0, len(remaining)
while low < high:
middle = (low + high + 1) // 2
if _message_tokens([{**message, "content": remaining[:middle]}]) <= max_tokens:
low = middle
else:
high = middle - 1
if low == 0:
raise ValueError("Extraction token budget cannot fit a message")
bounded_units.append([{**message, "content": remaining[:low]}])
remaining = remaining[low:]
batches: list[list[dict[str, str]]] = []
batch: list[dict[str, str]] = []
for unit in bounded_units:
candidate = [*batch, *unit]
if batch and _message_tokens(candidate) > max_tokens:
batches.append(batch)
batch = list(unit)
else:
batch = candidate
if batch:
batches.append(batch)
return batches
def _request_json(
url: str, key: str, payload: dict[str, Any], timeout: float
) -> tuple[dict[str, Any] | list[Any], int, int]:
@@ -0,0 +1,127 @@
"""Shared, host-independent redaction and lossless extraction batching."""
from __future__ import annotations
import json
import math
import re
from typing import Any
MAX_EXTRACTION_INPUT_TOKENS = 24000
SECRET_PATTERNS = [
re.compile(r"(?i)(authorization\s*[:=]\s*(?:bearer|token)\s+)[^\s\"']+"),
re.compile(r"(?i)((?:api[_-]?key|secret[_-]?access[_-]?key|session[_-]?token)\s*[:=]\s*)[^\s\"']+"),
re.compile(
r"(?i)((?:access[_-]?token|refresh[_-]?token|password|credential)"
r"\s*[:=]\s*)[^\s&\"']+"
),
re.compile(r"\b(?:sk|m0|mem0_sk|psk)-[A-Za-z0-9_\-]{12,}\b"),
re.compile(r"\b(?:ASIA|AKIA)[A-Z0-9]{12,}\b"),
re.compile(r"\b(?:ghp_|github_pat_|xox[baprs]-)[A-Za-z0-9_\-]{12,}\b"),
re.compile(
r"-----BEGIN [^-]*PRIVATE KEY-----.*?-----END [^-]*PRIVATE KEY-----",
re.DOTALL,
),
re.compile(
r'(?i)("(?:api[_-]?key|password|secret(?:[_-]?access[_-]?key)?'
r"|(?:access|refresh|session)[_-]?token|token|authorization|credential"
r')"\s*:\s*")(?:\\.|[^"\\])*'
),
]
def redact(value: Any) -> str:
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, default=str)
for pattern in SECRET_PATTERNS:
if pattern.groups:
text = pattern.sub(r"\1[REDACTED]", text)
else:
text = pattern.sub("[REDACTED]", text)
return text
def _estimated_tokens(value: str) -> int:
"""Conservatively estimate tokens without adding a tokenizer dependency."""
ascii_chars = sum(ord(char) < 128 for char in value)
return math.ceil((ascii_chars * 0.4) + (len(value) - ascii_chars))
def _message_tokens(messages: list[dict[str, str]]) -> int:
return _estimated_tokens(json.dumps(messages, ensure_ascii=False))
def _is_agent_assignment(message: dict[str, str]) -> bool:
return message.get("role") == "assistant" and message.get("content", "").startswith("Subagent assignment (")
def _is_agent_response(message: dict[str, str]) -> bool:
return message.get("role") == "assistant" and message.get("content", "").startswith("Subagent response (")
def extraction_message_batches(
messages: list[dict[str, str]],
*,
max_tokens: int = MAX_EXTRACTION_INPUT_TOKENS,
) -> list[list[dict[str, str]]]:
"""Keep exchanges together when possible; split oversized messages to enforce the request budget."""
if not messages or _message_tokens(messages) <= max_tokens:
return [messages]
exchanges: list[list[dict[str, str]]] = []
exchange: list[dict[str, str]] = []
for message in messages:
if message.get("role") == "user" and exchange:
exchanges.append(exchange)
exchange = []
exchange.append(message)
if exchange:
exchanges.append(exchange)
units: list[list[dict[str, str]]] = []
for exchange in exchanges:
if _message_tokens(exchange) <= max_tokens:
units.append(exchange)
continue
index = 0
while index < len(exchange):
message = exchange[index]
if _is_agent_assignment(message) and index + 1 < len(exchange) and _is_agent_response(exchange[index + 1]):
units.append(exchange[index : index + 2])
index += 2
else:
units.append([message])
index += 1
bounded_units: list[list[dict[str, str]]] = []
for unit in units:
if _message_tokens(unit) <= max_tokens:
bounded_units.append(unit)
continue
for message in unit:
remaining = message["content"]
while remaining:
low, high = 0, len(remaining)
while low < high:
middle = (low + high + 1) // 2
if _message_tokens([{**message, "content": remaining[:middle]}]) <= max_tokens:
low = middle
else:
high = middle - 1
if low == 0:
raise ValueError("Extraction token budget cannot fit a message")
bounded_units.append([{**message, "content": remaining[:low]}])
remaining = remaining[low:]
batches: list[list[dict[str, str]]] = []
batch: list[dict[str, str]] = []
for unit in bounded_units:
candidate = [*batch, *unit]
if batch and _message_tokens(candidate) > max_tokens:
batches.append(batch)
batch = list(unit)
else:
batch = candidate
if batch:
batches.append(batch)
return batches