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:
co-authored by
Claude Opus 4.7
parent
e7c0593844
commit
92926e2cae
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user