fix(llamaindex): give HindsightMemory.from_defaults a real cloud-default ctor

Audit finding (2026-06-02): HindsightMemory's create paths are asymmetric
with what create_hindsight_tools offers. The tools factory uses
resolve_client() so callers get the standard cloud-default + env-var
fallback for free; the memory adapter required either an explicit client
(from_client) or an explicit URL (from_url) and its from_defaults() raised
NotImplementedError. Callers wanting the same "no-config → Cloud" path
had to wire it themselves.

Fix: from_defaults(bank_id, ...) now calls resolve_client() exactly the
way the tools factory does. Falls back to DEFAULT_HINDSIGHT_API_URL when
no URL is supplied; reads HINDSIGHT_API_KEY from the environment if no
api_key is supplied; explicit `client=` still wins.

Tests pinning the new behaviour:
- from_defaults with nothing supplied → Hindsight constructed with
  DEFAULT_HINDSIGHT_API_URL.
- from_defaults with api_key → constructed with the configured key.
- from_defaults with explicit client → no new Hindsight constructed.

Replaces the previous test_from_defaults_raises (which pinned the
NotImplementedError that we're removing).

Verification:
- Deterministic bucket: 86 pass / 4 deselected (84 prior + 2 new
  cloud-default tests; one prior raises-test rewritten).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
DK09876
2026-06-02 15:14:17 -07:00
co-authored by Claude Opus 4.7
parent e7c0593844
commit 92926e2cae
2 changed files with 91 additions and 6 deletions
@@ -17,6 +17,8 @@ from llama_index.core.bridge.pydantic import Field, PrivateAttr
from llama_index.core.llms import ChatMessage, MessageRole
from llama_index.core.memory.types import BaseMemory
from ._client import resolve_client
logger = logging.getLogger(__name__)
DEFAULT_SYSTEM_PROMPT = (
@@ -99,9 +101,59 @@ class HindsightMemory(BaseMemory):
return "HindsightMemory"
@classmethod
def from_defaults(cls, **kwargs: Any) -> "HindsightMemory":
"""Create from defaults. Prefer ``from_client()`` instead."""
raise NotImplementedError("Use HindsightMemory.from_client() or HindsightMemory.from_url() instead.")
def from_defaults(
cls,
bank_id: str,
*,
client: Optional[Hindsight] = None,
hindsight_api_url: Optional[str] = None,
api_key: Optional[str] = None,
mission: Optional[str] = None,
context: str = "llamaindex",
budget: str = "mid",
max_tokens: int = 4096,
tags: Optional[list[str]] = None,
recall_tags: Optional[list[str]] = None,
recall_tags_match: str = "any",
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
chat_history_limit: int = 100,
**kwargs: Any,
) -> "HindsightMemory":
"""Create a HindsightMemory using the shared client-resolution path.
Mirrors what ``create_hindsight_tools`` does for the tools factory:
when neither ``client`` nor ``hindsight_api_url`` is supplied, falls
back to ``DEFAULT_HINDSIGHT_API_URL`` (Hindsight Cloud) and reads
``HINDSIGHT_API_KEY`` from the environment. Equivalent to
``from_client(resolve_client(...), bank_id, ...)`` but spells out the
common cloud-default and env-var paths so callers don't have to wire
them themselves.
Args:
bank_id: Memory bank ID (required).
client: Pre-configured Hindsight client. Wins over URL/key.
hindsight_api_url: API URL. Defaults to the configured value or
``DEFAULT_HINDSIGHT_API_URL``.
api_key: API key. Defaults to the configured value or
``HINDSIGHT_API_KEY`` env var.
mission, context, budget, max_tokens, tags, recall_tags,
recall_tags_match, system_prompt, chat_history_limit:
Passed to ``from_client``.
"""
resolved = resolve_client(client, hindsight_api_url, api_key)
return cls.from_client(
client=resolved,
bank_id=bank_id,
mission=mission,
context=context,
budget=budget,
max_tokens=max_tokens,
tags=tags,
recall_tags=recall_tags,
recall_tags_match=recall_tags_match,
system_prompt=system_prompt,
chat_history_limit=chat_history_limit,
)
@classmethod
def from_client(
@@ -68,9 +68,42 @@ class TestHindsightMemoryCreation:
base_url="http://localhost:8888", timeout=30.0
)
def test_from_defaults_raises(self):
with pytest.raises(NotImplementedError):
HindsightMemory.from_defaults()
def test_from_defaults_uses_cloud_default_when_nothing_supplied(self):
"""Parity with create_hindsight_tools: no URL/key → DEFAULT_HINDSIGHT_API_URL.
Pins the cloud-default constructor that the 2026-06-02 audit asked for
— HindsightMemory used to require an explicit client or explicit URL,
leaving callers to wire the cloud-default themselves while the tools
factory did it for them.
"""
from hindsight_llamaindex.config import DEFAULT_HINDSIGHT_API_URL
with patch("hindsight_llamaindex._client.Hindsight") as mock_cls:
mock_cls.return_value = _mock_client()
memory = HindsightMemory.from_defaults(bank_id="test-bank")
assert memory.bank_id == "test-bank"
kwargs = mock_cls.call_args.kwargs
assert kwargs.get("base_url") == DEFAULT_HINDSIGHT_API_URL
def test_from_defaults_threads_api_key_into_constructed_client(self):
"""When api_key is provided, resolve_client must include it."""
with patch("hindsight_llamaindex._client.Hindsight") as mock_cls:
mock_cls.return_value = _mock_client()
HindsightMemory.from_defaults(
bank_id="test-bank",
api_key="hsk_test_key_42",
)
kwargs = mock_cls.call_args.kwargs
assert kwargs.get("api_key") == "hsk_test_key_42"
def test_from_defaults_explicit_client_wins(self):
"""When client is given, no new Hindsight is constructed."""
client = _mock_client()
with patch("hindsight_llamaindex._client.Hindsight") as mock_cls:
memory = HindsightMemory.from_defaults(
bank_id="test-bank", client=client
)
assert memory.bank_id == "test-bank"
mock_cls.assert_not_called()
def test_class_name(self):
assert HindsightMemory.class_name() == "HindsightMemory"