Competing installers and checkout-local venv assumptions bypassed PM selection, install consent, and generation lifetimes. Route consumers through PM and installation-bound launchers. Refresh source launchers before obsolete Python entries can be collected. Remove Node, browser, and CUA acquisition engines, obsolete venv-holder handling, detached sync, and unused PM APIs. Keep historical updater exports inert and preserve external tool ownership and native integration. Share product freshness and prepared inputs across builders. Align plugin admission, Docker provisioning, setup instructions, and behavioral tests. Verified targeted Python and JavaScript tests, desktop and web typechecks, scoped lint, real product builds, and the Docker frontend smoke test. The missed post-setup test cleanup is included and verified. Native Windows/macOS execution, full Rust compilation, and the complete repository suite remain unverified. Historical compatibility requirements were preserved and extended, not fully rescanned.
128 lines
5.2 KiB
Python
128 lines
5.2 KiB
Python
"""Hindsight plugin constants and pure config normalizers (no I/O, no origin imports)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import logging
|
|
import re
|
|
from typing import Any, List
|
|
|
|
# Log under the plugin package's own logger name (loader-path independent).
|
|
logger = logging.getLogger(__name__.rpartition(".")[0])
|
|
|
|
_DEFAULT_API_URL = "https://api.hindsight.vectorize.io"
|
|
_DEFAULT_LOCAL_URL = "http://localhost:8888"
|
|
|
|
_DEFAULT_TIMEOUT = 120 # seconds — cloud API can take 30-40s per request
|
|
_DEFAULT_IDLE_TIMEOUT = 300 # seconds — Hindsight embedded daemon default
|
|
# ``metadata.source`` on retained memories is OPT-IN (AGENTS.md forbids
|
|
# on-by-default attribution tags): ``retain_source`` / HINDSIGHT_RETAIN_SOURCE.
|
|
_DEFAULT_RETAIN_SOURCE = ""
|
|
# Hindsight brand mark (eye ringed by graph nodes) for the recall/retain indicators.
|
|
_HINDSIGHT_GLYPH = "👁️"
|
|
# Hindsight 0.5.0 added ``update_mode='append'``; older APIs would silently
|
|
# overwrite prior turns under a stable document_id, so they keep the per-process id.
|
|
# Mirrors hindsight-integrations/openclaw — Hindsight 0.5.0 added `update_mode='append'` semantics on retain
|
|
# (vectorize-io/hindsight#932).
|
|
_MIN_VERSION_FOR_UPDATE_MODE_APPEND = "0.5.0"
|
|
_VALID_BUDGETS = {"low", "mid", "high"}
|
|
_PROVIDER_DEFAULT_MODELS = {
|
|
"openai": "gpt-4o-mini",
|
|
"anthropic": "claude-haiku-4-5",
|
|
"gemini": "gemini-3.6-flash",
|
|
"groq": "openai/gpt-oss-120b",
|
|
"openrouter": "qwen/qwen3.5-9b",
|
|
"minimax": "MiniMax-M2.7",
|
|
"ollama": "gemma3:12b",
|
|
"lmstudio": "local-model",
|
|
"openai_compatible": "your-model-name",
|
|
}
|
|
# The embedded daemon speaks OpenAI wire format for these providers.
|
|
_OPENAI_WIRE_PROVIDERS = {"openai_compatible", "openrouter"}
|
|
_OBSERVATION_SCOPE_KEYWORDS = {"per_tag", "combined", "all_combinations"}
|
|
|
|
|
|
def _parse_int_setting(value: Any, default: int) -> int:
|
|
"""Parse an integer config/env value, falling back on invalid input."""
|
|
if value is None or value == "":
|
|
return default
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
logger.warning("Invalid integer Hindsight setting %r; using default %s", value, default)
|
|
return default
|
|
|
|
|
|
def _daemon_llm_provider(provider: str) -> str:
|
|
return "openai" if provider in _OPENAI_WIRE_PROVIDERS else provider
|
|
|
|
|
|
def _normalize_retain_tags(value: Any) -> List[str]:
|
|
"""Normalize tag config/tool values to a deduplicated list of strings."""
|
|
if value is None:
|
|
return []
|
|
raw_items = value if isinstance(value, list) else [value]
|
|
if isinstance(value, str):
|
|
text = value.strip()
|
|
parsed = None
|
|
if text.startswith("["):
|
|
with contextlib.suppress(Exception):
|
|
parsed = json.loads(text)
|
|
raw_items = parsed if isinstance(parsed, list) else text.split(",")
|
|
normalized: list[str] = []
|
|
for item in raw_items:
|
|
tag = str(item).strip()
|
|
if tag and tag not in normalized:
|
|
normalized.append(tag)
|
|
return normalized
|
|
|
|
|
|
def _normalize_observation_scopes(value: Any) -> Any:
|
|
"""Normalize observation_scopes to a keyword string, ``list[list[str]]`` (one inner
|
|
list per consolidation pass), or ``None`` (Hindsight's ``combined`` default).
|
|
Accepts a keyword, a JSON-encoded list, a flat tag list (one scope) or a list of
|
|
tag-lists; anything unrecognized -> ``None`` so we never send an invalid payload."""
|
|
if isinstance(value, str):
|
|
text = value.strip()
|
|
if text in _OBSERVATION_SCOPE_KEYWORDS:
|
|
return text
|
|
if text.startswith("["):
|
|
try:
|
|
return _normalize_observation_scopes(json.loads(text))
|
|
except Exception:
|
|
return None
|
|
return None
|
|
if not isinstance(value, (list, tuple)):
|
|
return None
|
|
if all(isinstance(entry, str) for entry in value): # flat tag list -> one scope
|
|
value = [value]
|
|
scopes = [
|
|
[str(tag).strip() for tag in entry if str(tag).strip()] if isinstance(entry, (list, tuple))
|
|
else [entry.strip()] if isinstance(entry, str) and entry.strip() else []
|
|
for entry in value
|
|
]
|
|
return [s for s in scopes if s] or None
|
|
|
|
|
|
def _sanitize_bank_segment(value: str) -> str:
|
|
"""URL/filesystem-safe bank_id placeholder: runs outside ``[A-Za-z0-9_-]`` (per
|
|
``str.isalnum``) become one dash; leading/trailing ``-``/``_`` are stripped."""
|
|
# \w == str.isalnum() + "_" for str patterns, so this matches the per-char rule.
|
|
return re.sub(r"[^\w-]+", "-", str(value)).strip("-_") if value else ""
|
|
|
|
|
|
def _resolve_bank_id_template(template: str, fallback: str, **placeholders: str) -> str:
|
|
"""Render a bank_id template ({profile}, {workspace}, {platform}, {user}, {session}),
|
|
sanitizing each placeholder; the ``-``/``_`` runs empty placeholders leave are
|
|
collapsed (``hermes-{user}`` -> ``hermes``). Empty/invalid template -> *fallback*."""
|
|
if not template:
|
|
return fallback
|
|
try:
|
|
rendered = template.format(**{k: _sanitize_bank_segment(v) for k, v in placeholders.items()})
|
|
except (KeyError, IndexError) as exc:
|
|
logger.warning("Invalid bank_id_template %r: %s — using fallback %r",
|
|
template, exc, fallback)
|
|
return fallback
|
|
return re.sub(r"([-_])\1+", r"\1", rendered).strip("-_") or fallback
|