Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions plugins/memory/hindsight/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Long-term memory with knowledge graph, entity resolution, and multi-strategy
retrieval. Supports cloud (API key) and local modes.

Configurable timeout via HINDSIGHT_TIMEOUT env var or config.json.

Original PR #1811 by benfrank241, adapted to MemoryProvider ABC.

Config via environment variables:
Expand All @@ -11,6 +13,7 @@
HINDSIGHT_BUDGET — recall budget: low/mid/high (default: mid)
HINDSIGHT_API_URL — API endpoint
HINDSIGHT_MODE — cloud or local (default: cloud)
HINDSIGHT_TIMEOUT — API request timeout in seconds (default: 120)

Or via $HERMES_HOME/hindsight/config.json (profile-scoped), falling back to
~/.hindsight/config.json (legacy, shared) for backward compatibility.
Expand All @@ -36,6 +39,7 @@
_DEFAULT_API_URL = "https://api.hindsight.vectorize.io"
_DEFAULT_LOCAL_URL = "http://localhost:8888"
_MIN_CLIENT_VERSION = "0.4.22"
_DEFAULT_TIMEOUT = 120 # seconds — cloud API can take 30-40s per request
_VALID_BUDGETS = {"low", "mid", "high"}
_PROVIDER_DEFAULT_MODELS = {
"openai": "gpt-4o-mini",
Expand Down Expand Up @@ -77,13 +81,18 @@ def _run():
return _loop


def _run_sync(coro, timeout: float = 120.0):
def _run_sync(coro, timeout: float = _DEFAULT_TIMEOUT):
"""Schedule *coro* on the shared loop and block until done."""
loop = _get_loop()
future = asyncio.run_coroutine_threadsafe(coro, loop)
return future.result(timeout=timeout)


# ---------------------------------------------------------------------------
# Backward-compatible alias — instances use self._run_sync() instead.
# ---------------------------------------------------------------------------


# ---------------------------------------------------------------------------
# Tool schemas
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -196,6 +205,7 @@ def __init__(self):
self._memory_mode = "hybrid" # "context", "tools", or "hybrid"
self._prefetch_method = "recall" # "recall" or "reflect"
self._client = None
self._timeout = _DEFAULT_TIMEOUT
self._prefetch_result = ""
self._prefetch_lock = threading.Lock()
self._prefetch_thread = None
Expand Down Expand Up @@ -372,6 +382,11 @@ def post_setup(self, hermes_home: str, config: dict) -> None:
# Step 4: Save everything
provider_config["bank_id"] = "hermes"
provider_config["recall_budget"] = "mid"
# Read existing timeout from config if present, otherwise use default
existing_timeout = self._config.get("timeout") if self._config else None
timeout_val = existing_timeout if existing_timeout else _DEFAULT_TIMEOUT
provider_config["timeout"] = timeout_val
env_writes["HINDSIGHT_TIMEOUT"] = str(timeout_val)
Comment thread
tekgnosis-net marked this conversation as resolved.
bank_id = "hermes"
config["memory"]["provider"] = "hindsight"
save_config(config)
Expand Down Expand Up @@ -434,6 +449,7 @@ def get_config_schema(self):
{"key": "recall_max_tokens", "description": "Maximum tokens for recall results", "default": 4096},
{"key": "recall_max_input_chars", "description": "Maximum input query length for auto-recall", "default": 800},
{"key": "recall_prompt_preamble", "description": "Custom preamble for recalled memories in context"},
{"key": "timeout", "description": "API request timeout in seconds", "default": _DEFAULT_TIMEOUT},
Comment thread
tekgnosis-net marked this conversation as resolved.
]

def _get_client(self):
Expand All @@ -458,14 +474,19 @@ def _get_client(self):
self._client = HindsightEmbedded(**kwargs)
else:
from hindsight_client import Hindsight
kwargs = {"base_url": self._api_url, "timeout": 30.0}
timeout = self._timeout or _DEFAULT_TIMEOUT
kwargs = {"base_url": self._api_url, "timeout": float(timeout)}
if self._api_key:
kwargs["api_key"] = self._api_key
logger.debug("Creating Hindsight cloud client (url=%s, has_key=%s)",
self._api_url, bool(self._api_key))
logger.debug("Creating Hindsight cloud client (url=%s, has_key=%s, timeout=%s)",
self._api_url, bool(self._api_key), kwargs["timeout"])
self._client = Hindsight(**kwargs)
return self._client

def _run_sync(self, coro):
"""Schedule *coro* on the shared loop using the configured timeout."""
return _run_sync(coro, timeout=self._timeout)

def initialize(self, session_id: str, **kwargs) -> None:
self._session_id = session_id

Expand Down Expand Up @@ -497,6 +518,8 @@ def initialize(self, session_id: str, **kwargs) -> None:

self._config = _load_config()
self._mode = self._config.get("mode", "cloud")
# Read timeout from config or env var, fall back to default
self._timeout = self._config.get("timeout") or int(os.environ.get("HINDSIGHT_TIMEOUT", str(_DEFAULT_TIMEOUT)))
Comment thread
tekgnosis-net marked this conversation as resolved.
# "local" is a legacy alias for "local_embedded"
if self._mode == "local":
self._mode = "local_embedded"
Expand Down Expand Up @@ -685,7 +708,7 @@ def _run():
client = self._get_client()
if self._prefetch_method == "reflect":
logger.debug("Prefetch: calling reflect (bank=%s, query_len=%d)", self._bank_id, len(query))
resp = _run_sync(client.areflect(bank_id=self._bank_id, query=query, budget=self._budget))
resp = self._run_sync(client.areflect(bank_id=self._bank_id, query=query, budget=self._budget))
text = resp.text or ""
else:
recall_kwargs: dict = {
Expand All @@ -699,7 +722,7 @@ def _run():
recall_kwargs["types"] = self._recall_types
logger.debug("Prefetch: calling recall (bank=%s, query_len=%d, budget=%s)",
self._bank_id, len(query), self._budget)
resp = _run_sync(client.arecall(**recall_kwargs))
resp = self._run_sync(client.arecall(**recall_kwargs))
num_results = len(resp.results) if resp.results else 0
logger.debug("Prefetch: recall returned %d results", num_results)
text = "\n".join(f"- {r.text}" for r in resp.results if r.text) if resp.results else ""
Expand Down Expand Up @@ -756,7 +779,7 @@ def _sync():
item["tags"] = self._tags
logger.debug("Hindsight retain: bank=%s, doc=%s, async=%s, content_len=%d, num_turns=%d",
self._bank_id, self._session_id, self._retain_async, len(content), len(self._session_turns))
_run_sync(client.aretain_batch(
self._run_sync(client.aretain_batch(
bank_id=self._bank_id,
items=[item],
document_id=self._session_id,
Expand Down Expand Up @@ -796,7 +819,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:
retain_kwargs["tags"] = self._tags
logger.debug("Tool hindsight_retain: bank=%s, content_len=%d, context=%s",
self._bank_id, len(content), context)
_run_sync(client.aretain(**retain_kwargs))
self._run_sync(client.aretain(**retain_kwargs))
logger.debug("Tool hindsight_retain: success")
return json.dumps({"result": "Memory stored successfully."})
except Exception as e:
Expand All @@ -819,7 +842,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:
recall_kwargs["types"] = self._recall_types
logger.debug("Tool hindsight_recall: bank=%s, query_len=%d, budget=%s",
self._bank_id, len(query), self._budget)
resp = _run_sync(client.arecall(**recall_kwargs))
resp = self._run_sync(client.arecall(**recall_kwargs))
num_results = len(resp.results) if resp.results else 0
logger.debug("Tool hindsight_recall: %d results", num_results)
if not resp.results:
Expand All @@ -837,7 +860,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:
try:
logger.debug("Tool hindsight_reflect: bank=%s, query_len=%d, budget=%s",
self._bank_id, len(query), self._budget)
resp = _run_sync(client.areflect(
resp = self._run_sync(client.areflect(
bank_id=self._bank_id, query=query, budget=self._budget
))
logger.debug("Tool hindsight_reflect: response_len=%d", len(resp.text or ""))
Expand Down Expand Up @@ -865,7 +888,7 @@ def shutdown(self) -> None:
except RuntimeError:
pass
else:
_run_sync(self._client.aclose())
self._run_sync(self._client.aclose())
except Exception:
pass
self._client = None
Expand Down