diff --git a/hindsight-api-slim/tests/test_codex_tool_choice.py b/hindsight-api-slim/tests/test_codex_tool_choice.py index e35ced8d5d..408d7f1328 100644 --- a/hindsight-api-slim/tests/test_codex_tool_choice.py +++ b/hindsight-api-slim/tests/test_codex_tool_choice.py @@ -1,35 +1,22 @@ -import asyncio -import sys -import types -import unittest -from unittest.mock import AsyncMock, MagicMock, patch -from pathlib import Path - +""" +Regression tests for Codex provider tool_choice normalization. -ROOT = Path(__file__).resolve().parents[1] -PACKAGE_ROOT = ROOT / "hindsight_api" -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) +The reflect agent forces tool selection via named tool_choice dicts on early iterations: + {"type": "function", "function": {"name": "recall"}} +The Codex Responses API expects the function name at the top level instead: + {"type": "function", "name": "recall"} -def ensure_package(name: str, path: Path) -> None: - module = sys.modules.get(name) - if module is None: - module = types.ModuleType(name) - module.__path__ = [str(path)] - sys.modules[name] = module +Without normalization, Codex rejects the request with: + 400 Unknown parameter: 'tool_choice.function' +""" +from unittest.mock import AsyncMock, MagicMock, patch -ensure_package("hindsight_api", PACKAGE_ROOT) -ensure_package("hindsight_api.engine", PACKAGE_ROOT / "engine") -ensure_package("hindsight_api.engine.providers", PACKAGE_ROOT / "engine" / "providers") -fake_metrics = types.ModuleType("hindsight_api.metrics") -fake_metrics.get_metrics_collector = lambda: types.SimpleNamespace(record_llm_call=lambda **kwargs: None) -sys.modules["hindsight_api.metrics"] = fake_metrics +import pytest from hindsight_api.engine.providers.codex_llm import CodexLLM - TOOLS = [ { "type": "function", @@ -56,52 +43,46 @@ def build_llm() -> CodexLLM: ) -class CodexToolChoiceTests(unittest.TestCase): - def test_codex_normalizes_legacy_named_tool_choice_shape(self) -> None: - async def scenario() -> dict: - llm = build_llm() - response = MagicMock() - response.status_code = 200 - response.raise_for_status.return_value = None - with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post: - mock_post.return_value = response - with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse: - mock_parse.return_value = (None, []) - await llm.call_with_tools( - messages=[{"role": "user", "content": "recall the memory"}], - tools=TOOLS, - tool_choice={"type": "function", "function": {"name": "recall"}}, - max_retries=0, - ) - return mock_post.call_args.kwargs["json"] - - sent_payload = asyncio.run(scenario()) - self.assertEqual(sent_payload["tool_choice"], {"type": "function", "name": "recall"}) - - def test_codex_forced_tool_choice_still_yields_tool_calls(self) -> None: - async def scenario() -> tuple[object, dict]: - llm = build_llm() - response = MagicMock() - response.status_code = 200 - response.raise_for_status.return_value = None - tool_call = {"id": "call-1", "name": "recall", "arguments": {"query": "memory"}} - with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post: - mock_post.return_value = response - with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse: - mock_parse.return_value = (None, [tool_call]) - result = await llm.call_with_tools( - messages=[{"role": "user", "content": "recall the memory"}], - tools=TOOLS, - tool_choice={"type": "function", "function": {"name": "recall"}}, - max_retries=0, - ) - return result, mock_post.call_args.kwargs["json"] - - result, sent_payload = asyncio.run(scenario()) - self.assertEqual(len(result.tool_calls), 1) - self.assertEqual(result.tool_calls[0].name, "recall") - self.assertEqual(sent_payload["tool_choice"], {"type": "function", "name": "recall"}) - - -if __name__ == "__main__": - unittest.main() +@pytest.mark.asyncio +async def test_codex_normalizes_legacy_named_tool_choice_shape(): + llm = build_llm() + response = MagicMock() + response.status_code = 200 + response.raise_for_status.return_value = None + with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post: + mock_post.return_value = response + with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse: + mock_parse.return_value = (None, []) + await llm.call_with_tools( + messages=[{"role": "user", "content": "recall the memory"}], + tools=TOOLS, + tool_choice={"type": "function", "function": {"name": "recall"}}, + max_retries=0, + ) + sent_payload = mock_post.call_args.kwargs["json"] + + assert sent_payload["tool_choice"] == {"type": "function", "name": "recall"} + + +@pytest.mark.asyncio +async def test_codex_forced_tool_choice_still_yields_tool_calls(): + llm = build_llm() + response = MagicMock() + response.status_code = 200 + response.raise_for_status.return_value = None + tool_call = {"id": "call-1", "name": "recall", "arguments": {"query": "memory"}} + with patch.object(llm._client, "post", new_callable=AsyncMock) as mock_post: + mock_post.return_value = response + with patch.object(llm, "_parse_sse_tool_stream", new_callable=AsyncMock) as mock_parse: + mock_parse.return_value = (None, [tool_call]) + result = await llm.call_with_tools( + messages=[{"role": "user", "content": "recall the memory"}], + tools=TOOLS, + tool_choice={"type": "function", "function": {"name": "recall"}}, + max_retries=0, + ) + sent_payload = mock_post.call_args.kwargs["json"] + + assert len(result.tool_calls) == 1 + assert result.tool_calls[0].name == "recall" + assert sent_payload["tool_choice"] == {"type": "function", "name": "recall"} diff --git a/hindsight-api-slim/tests/test_hierarchical_config.py b/hindsight-api-slim/tests/test_hierarchical_config.py index 203ea6ab85..2bad365264 100644 --- a/hindsight-api-slim/tests/test_hierarchical_config.py +++ b/hindsight-api-slim/tests/test_hierarchical_config.py @@ -88,8 +88,16 @@ async def test_hierarchical_fields_categorization(): assert "entities_allow_free_form" in configurable assert "entity_labels" in configurable + # Verify other configurable fields + assert "retain_default_strategy" in configurable + assert "retain_strategies" in configurable + assert "max_observations_per_scope" in configurable + assert "reflect_source_facts_max_tokens" in configurable + assert "llm_gemini_safety_settings" in configurable + assert "mcp_enabled_tools" in configurable + # Verify count is correct - assert len(configurable) == 20 + assert len(configurable) == 21 # Verify credential fields (NEVER exposed) assert "llm_api_key" in credentials diff --git a/hindsight-docs/sidebars.ts b/hindsight-docs/sidebars.ts index 2423c8240a..aad36fa994 100644 --- a/hindsight-docs/sidebars.ts +++ b/hindsight-docs/sidebars.ts @@ -262,6 +262,12 @@ const sidebars: SidebarsConfig = { label: 'AG2', customProps: { icon: '/img/icons/ag2.svg' }, }, + { + type: 'doc', + id: 'sdks/integrations/llamaindex', + label: 'LlamaIndex', + customProps: { icon: '/img/icons/llamaindex.png' }, + }, { type: 'doc', id: 'sdks/integrations/skills', diff --git a/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/codex.md b/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/codex.md new file mode 100644 index 0000000000..edb3b69794 --- /dev/null +++ b/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/codex.md @@ -0,0 +1,189 @@ +--- +sidebar_position: 6 +--- + +# OpenAI Codex CLI + +Persistent memory for [OpenAI Codex CLI](https://github.com/openai/codex) using [Hindsight](https://vectorize.io/hindsight). Three Python hook scripts automatically recall relevant context before each prompt and retain conversations after each turn — no changes to your Codex workflow required. + +## Quick Start + +```bash +# 1. Clone the Hindsight repo and install the plugin +git clone https://github.com/vectorize-io/hindsight.git +cd hindsight/hindsight-integrations/codex +./install.sh + +# 2. Configure your Hindsight connection +cat > ~/.hindsight/codex.json << 'EOF' +{ + "hindsightApiUrl": "https://api.hindsight.vectorize.io", + "hindsightApiToken": "hsk_your_token_here", + "bankId": "codex" +} +EOF + +# 3. Start Codex — memory is live +codex +``` + +For a local Hindsight instance, set `hindsightApiUrl` to `http://localhost:9077` and omit `hindsightApiToken`. + +## Features + +- **Auto-recall** — on every user prompt, queries Hindsight for relevant memories and injects them as `additionalContext` (invisible to the transcript, visible to Codex) +- **Auto-retain** — after each Codex response, stores the conversation transcript to Hindsight for future recall +- **Dynamic bank IDs** — supports per-project memory isolation based on the working directory +- **Session-level upsert** — uses the session ID as the document ID so re-running the same session updates rather than duplicates stored content +- **Zero dependencies** — pure Python stdlib, no pip install required + +## Architecture + +The plugin uses three Codex hook events: + +| Hook | Event | Purpose | +|------|-------|---------| +| `session_start.py` | `SessionStart` | Warm up — verify Hindsight is reachable | +| `recall.py` | `UserPromptSubmit` | **Auto-recall** — query memories, inject as `additionalContext` | +| `retain.py` | `Stop` | **Auto-retain** — extract transcript, POST to Hindsight (async) | + +On `UserPromptSubmit`, the hook reads the prompt, queries Hindsight for the most relevant memories, and outputs a `hookSpecificOutput.additionalContext` block. Codex prepends this to the conversation before sending it to the model: + +``` + +Relevant memories from past conversations... +Current time - 2026-03-27 09:14 + +- Project uses FastAPI with asyncpg — not SQLAlchemy [world] (2026-03-26) +- Preferred testing framework: pytest with pytest-asyncio [experience] (2026-03-26) + +``` + +On `Stop`, the hook reads the session transcript, strips previously injected memory tags (to prevent feedback loops), and POSTs the conversation to Hindsight asynchronously. + +## Connection Modes + +### 1. External API (recommended) + +Connect to a running Hindsight server (cloud or self-hosted): + +```json +{ + "hindsightApiUrl": "https://api.hindsight.vectorize.io", + "hindsightApiToken": "hsk_your_token" +} +``` + +### 2. Local Daemon + +Run `hindsight-embed` locally. The `session_start.py` hook will detect it on `apiPort` (default `9077`). The daemon is not auto-started by the Codex plugin — start it separately: + +```bash +uvx hindsight-embed +``` + +Then leave `hindsightApiUrl` empty in your config and the plugin will connect to `http://localhost:9077`. + +## Configuration + +Settings are loaded from `~/.hindsight/codex.json`. Every setting can also be overridden via environment variable. + +**Loading order** (later entries win): + +1. Built-in defaults +2. Plugin `settings.json` (at `~/.hindsight/codex/settings.json`) +3. User config (`~/.hindsight/codex.json`) +4. Environment variables + +--- + +### Connection + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` | URL of the Hindsight API server. Required. | +| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | API token for authentication. Required for Hindsight Cloud. | +| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port for the local `hindsight-embed` daemon. | + +--- + +### Memory Bank + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `bankId` | `HINDSIGHT_BANK_ID` | `"codex"` | The bank to read from and write to. All sessions share this bank unless `dynamicBankId` is enabled. | +| `bankMission` | `HINDSIGHT_BANK_MISSION` | coding assistant prompt | Describes the agent's purpose. Sent when creating or updating the bank. | +| `retainMission` | — | extraction prompt | Instructions for Hindsight's fact extraction — what to extract from coding conversations. | +| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, derives a unique bank ID from `dynamicBankGranularity` fields — useful for per-project isolation. | +| `dynamicBankGranularity` | — | `["agent", "project"]` | Which fields to combine for dynamic bank IDs. `"project"` = working directory, `"agent"` = agent name. | +| `bankIdPrefix` | — | `""` | Prefix prepended to all bank IDs. | +| `agentName` | `HINDSIGHT_AGENT_NAME` | `"codex"` | Agent name used in dynamic bank ID derivation. | + +--- + +### Auto-Recall + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall. | +| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Search depth: `"low"` (fast), `"mid"` (balanced), `"high"` (thorough). | +| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Max tokens in the recalled memory block. | +| `recallTypes` | — | `["world", "experience"]` | Memory types to retrieve. | +| `recallContextTurns` | `HINDSIGHT_RECALL_CONTEXT_TURNS` | `1` | Prior turns to include when building the recall query. `1` = latest prompt only. | +| `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Max characters in the query sent to Hindsight. | +| `recallRoles` | — | `["user", "assistant"]` | Which roles to include when building a multi-turn query. | +| `recallPromptPreamble` | — | built-in | Text placed above the recalled memories in the injected context block. | + +--- + +### Auto-Retain + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. | +| `retainMode` | `HINDSIGHT_RETAIN_MODE` | `"full-session"` | `"full-session"` sends the full transcript per session (upserted by session ID). `"chunked"` sends sliding windows every N turns. | +| `retainEveryNTurns` | — | `10` | Retain fires every N turns. `1` = every turn. Higher values reduce API calls. | +| `retainOverlapTurns` | — | `2` | Extra turns included from the previous chunk (chunked mode only). | +| `retainRoles` | — | `["user", "assistant"]` | Which roles to include in the retained transcript. | +| `retainTags` | — | `["{session_id}"]` | Tags attached to the stored document. `{session_id}` is replaced at runtime. | +| `retainMetadata` | — | `{}` | Arbitrary key-value metadata attached to the stored document. | +| `retainContext` | — | `"codex"` | Label identifying the source integration. Useful when multiple integrations write to the same bank. | + +--- + +### Debug + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `debug` | `HINDSIGHT_DEBUG` | `false` | Enable verbose logging to stderr. All log lines are prefixed with `[Hindsight]`. | + +## Per-Project Memory + +To give each project its own isolated memory bank, enable dynamic bank IDs: + +```json +{ + "dynamicBankId": true, + "dynamicBankGranularity": ["agent", "project"] +} +``` + +With this config, running Codex in `~/projects/api` and `~/projects/frontend` stores and recalls memories separately. Bank IDs are derived from the working directory path. + +## Troubleshooting + +**Hooks not firing**: Check that `~/.codex/config.toml` contains `codex_hooks = true` under `[features]`. Re-run `install.sh` to write this automatically. + +**No memories recalled**: Recall returns results only after something has been retained. Either complete one Codex session first, or seed your bank manually using the [cookbook example](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/codex-memory). + +**Memory not being stored**: `retainEveryNTurns` defaults to `10` — retain only fires every 10 turns. While testing, add `"retainEveryNTurns": 1` to `~/.hindsight/codex.json`. + +**Debug mode**: Add `"debug": true` to `~/.hindsight/codex.json` to see what Hindsight is doing on each turn: + +``` +[Hindsight] Recalling from bank 'codex', query length: 42 +[Hindsight] Injecting 3 memories +[Hindsight] Retaining to bank 'codex', doc 'sess-abc123', 2 messages, 847 chars +``` + +**High latency on recall**: Use `"recallBudget": "low"` or reduce `recallMaxTokens` to speed up recall queries. diff --git a/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/llamaindex.md b/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/llamaindex.md new file mode 100644 index 0000000000..4a3823715c --- /dev/null +++ b/hindsight-docs/versioned_docs/version-0.4/sdks/integrations/llamaindex.md @@ -0,0 +1,256 @@ +--- +sidebar_position: 8 +--- + +# LlamaIndex + +Persistent long-term memory for [LlamaIndex](https://docs.llamaindex.ai/) agents via Hindsight. Two packages are available: + +- **`llama-index-tools-hindsight`** — Agent-driven memory tools (retain/recall/reflect) +- **`llama-index-memory-hindsight`** — Automatic memory via LlamaIndex's `BaseMemory` interface + +Both follow the LlamaIndex namespace package convention (`from llama_index.tools.hindsight import ...`). + +## Installation + +```bash +# Tools only (agent-driven) +pip install llama-index-tools-hindsight + +# Memory only (automatic) +pip install llama-index-memory-hindsight + +# Both +pip install llama-index-tools-hindsight llama-index-memory-hindsight +``` + +--- + +## Automatic Memory (BaseMemory) + +The simplest way to add Hindsight memory to a LlamaIndex agent. Messages are automatically stored on each turn, and relevant memories are recalled and injected as context. + +```python +import asyncio +from hindsight_client import Hindsight +from llama_index.memory.hindsight import HindsightMemory +from llama_index.core.agent import ReActAgent +from llama_index.llms.openai import OpenAI + +async def main(): + client = Hindsight(base_url="http://localhost:8888") + + memory = HindsightMemory.from_client( + client=client, + bank_id="user-123", + mission="Track user preferences and project context", + ) + + agent = ReActAgent(tools=tools, llm=OpenAI(model="gpt-4o"), memory=memory) + response = await agent.run("Remember that I prefer dark mode") + print(response) + +asyncio.run(main()) +``` + +### How It Works + +| Event | What Happens | +|-------|-------------| +| Agent receives input | `get(input)` recalls relevant memories from Hindsight, prepends as system message | +| Agent produces output | `put(message)` retains the message to Hindsight for future recall | +| New session starts | Previous memories are available via recall; local chat buffer starts empty | + +### `HindsightMemory.from_client()` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `client` | `Hindsight` | *required* | Hindsight client instance | +| `bank_id` | `str` | *required* | Memory bank ID | +| `mission` | `str` | `None` | Bank mission — auto-creates bank on first use | +| `context` | `str` | `"llamaindex"` | Source label for retain operations | +| `budget` | `str` | `"mid"` | Recall budget level | +| `max_tokens` | `int` | `4096` | Max recall tokens | +| `tags` | `list[str]` | `None` | Tags for retain operations | +| `recall_tags` | `list[str]` | `None` | Tags to filter recall | +| `recall_tags_match` | `str` | `"any"` | Tag matching mode | +| `system_prompt` | `str` | *(built-in)* | Template for memory system message. Must contain `{memories}` | +| `chat_history_limit` | `int` | `100` | Max messages in local buffer | + +Also available: `HindsightMemory.from_url(hindsight_api_url, bank_id, ...)` for creating without a pre-built client. + +--- + +## Agent-Driven Tools (BaseToolSpec) + +For explicit control, expose retain/recall/reflect as tools the agent can choose to call. + +### Quick Start: Tool Spec + +```python +import asyncio +from hindsight_client import Hindsight +from llama_index.tools.hindsight import HindsightToolSpec +from llama_index.llms.openai import OpenAI +from llama_index.core.agent import ReActAgent + +async def main(): + client = Hindsight(base_url="http://localhost:8888") + + spec = HindsightToolSpec( + client=client, + bank_id="user-123", + mission="Track user preferences", + ) + tools = spec.to_tool_list() + + agent = ReActAgent(tools=tools, llm=OpenAI(model="gpt-4o")) + response = await agent.run("Remember that I prefer dark mode") + print(response) + +asyncio.run(main()) +``` + +### Quick Start: Factory Function + +```python +from llama_index.tools.hindsight import create_hindsight_tools + +tools = create_hindsight_tools( + client=client, + bank_id="user-123", + mission="Track user preferences", +) +``` + +### Selecting Tools + +```python +# Via to_tool_list() +tools = spec.to_tool_list(spec_functions=["recall_memory", "reflect_on_memory"]) + +# Via factory flags +tools = create_hindsight_tools( + client=client, + bank_id="user-123", + include_retain=True, + include_recall=True, + include_reflect=False, +) +``` + +### Configuration + +Set defaults via `configure()`, override per-call: + +```python +from llama_index.tools.hindsight import configure + +configure( + hindsight_api_url="http://localhost:8888", + api_key="your-api-key", # or set HINDSIGHT_API_KEY env var + budget="mid", + tags=["source:llamaindex"], + context="my-app", + mission="Track user preferences", +) + +# Now create tools without passing client/url +tools = create_hindsight_tools(bank_id="user-123") +``` + +### `HindsightToolSpec()` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `bank_id` | `str` | *required* | Hindsight memory bank to operate on | +| `client` | `Hindsight` | `None` | Pre-configured Hindsight client | +| `hindsight_api_url` | `str` | `None` | API URL (used if no client provided) | +| `api_key` | `str` | `None` | API key (used if no client provided) | +| `budget` | `str` | `None` → `"mid"` | Recall/reflect budget: `low`, `mid`, `high` | +| `max_tokens` | `int` | `None` → `4096` | Max tokens for recall results | +| `tags` | `list[str]` | `None` | Tags applied when storing memories | +| `recall_tags` | `list[str]` | `None` | Tags to filter recall results | +| `recall_tags_match` | `str` | `None` → `"any"` | Tag matching: `any`, `all`, `any_strict`, `all_strict` | +| `retain_metadata` | `dict[str, str]` | `None` | Default metadata for retain operations | +| `retain_document_id` | `str` | `None` | Document ID for retain. Auto-generates `{session}-{timestamp}` if not set | +| `retain_context` | `str` | `"llamaindex"` | Source label for retain operations | +| `recall_types` | `list[str]` | `None` | Fact types: `world`, `experience`, `opinion`, `observation` | +| `recall_include_entities` | `bool` | `False` | Include entity info in recall results | +| `reflect_context` | `str` | `None` | Additional context for reflect | +| `reflect_max_tokens` | `int` | `None` | Max tokens for reflect (defaults to `max_tokens`) | +| `reflect_response_schema` | `dict` | `None` | JSON schema to constrain reflect output | +| `reflect_tags` | `list[str]` | `None` | Tags for reflect (defaults to `recall_tags`) | +| `reflect_tags_match` | `str` | `None` | Tag matching for reflect (defaults to `recall_tags_match`) | +| `mission` | `str` | `None` | Bank mission — auto-creates bank on first use | + +--- + +## Production Patterns + +### Bank Mission + +Set a mission to give the memory engine context for fact extraction: + +```python +# Tools +spec = HindsightToolSpec( + client=client, + bank_id="user-123", + mission="Track user coding preferences, project context, and technical decisions", +) + +# Memory +memory = HindsightMemory.from_client( + client=client, + bank_id="user-123", + mission="Track user coding preferences, project context, and technical decisions", +) +``` + +The bank is created automatically on first use. If it already exists, creation is silently skipped. + +### Memory Scoping with Tags + +```python +spec = HindsightToolSpec( + client=client, + bank_id="user-123", + tags=["source:chat", "session:abc"], # applied to all retains + recall_tags=["source:chat"], # filter recalls to chat memories + recall_tags_match="any", +) +``` + +### Error Handling + +Both packages handle errors gracefully — operations are logged and return friendly messages instead of raising exceptions. Agents continue functioning even if memory is unavailable. + +### Combining Tools + Memory + +Use both packages together for maximum flexibility: + +```python +from llama_index.tools.hindsight import create_hindsight_tools +from llama_index.memory.hindsight import HindsightMemory + +# Automatic memory for context enrichment +memory = HindsightMemory.from_client(client=client, bank_id="user-123") + +# Explicit tools for agent-driven reflect +tools = create_hindsight_tools( + client=client, + bank_id="user-123", + include_retain=False, # memory handles retain automatically + include_recall=False, # memory handles recall automatically + include_reflect=True, # agent can still explicitly reflect +) + +agent = ReActAgent(tools=tools, llm=llm, memory=memory) +``` + +## Requirements + +- Python 3.10+ +- `llama-index-core >= 0.11.0` +- `hindsight-client >= 0.4.0` diff --git a/hindsight-docs/versioned_sidebars/version-0.4-sidebars.json b/hindsight-docs/versioned_sidebars/version-0.4-sidebars.json index 256f8fab10..6a3a009325 100644 --- a/hindsight-docs/versioned_sidebars/version-0.4-sidebars.json +++ b/hindsight-docs/versioned_sidebars/version-0.4-sidebars.json @@ -331,6 +331,22 @@ "icon": "/img/icons/ag2.svg" } }, + { + "type": "doc", + "id": "sdks/integrations/llamaindex", + "label": "LlamaIndex", + "customProps": { + "icon": "/img/icons/llamaindex.png" + } + }, + { + "type": "doc", + "id": "sdks/integrations/codex", + "label": "OpenAI Codex CLI", + "customProps": { + "icon": "/img/icons/openai.png" + } + }, { "type": "doc", "id": "sdks/integrations/skills", diff --git a/skills/hindsight-docs/references/developer/configuration.md b/skills/hindsight-docs/references/developer/configuration.md index c0702ebbf9..0f3c2e9755 100644 --- a/skills/hindsight-docs/references/developer/configuration.md +++ b/skills/hindsight-docs/references/developer/configuration.md @@ -878,6 +878,7 @@ Observations are consolidated knowledge synthesized from facts. | `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS` | Total token budget for source facts included with observations in the consolidation prompt. `-1` = unlimited. Configurable per bank. | `-1` | | `HINDSIGHT_API_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION` | Per-observation token cap for source facts in the consolidation prompt. Each observation independently gets at most this many tokens of source facts. `-1` = unlimited. Configurable per bank. | `256` | | `HINDSIGHT_API_OBSERVATIONS_MISSION` | What this bank should synthesise into durable observations. Replaces the built-in consolidation rules — leave unset to use the server default. | - | +| `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE` | Maximum number of observations allowed per tag scope. When the limit is reached, consolidation will only update or delete existing observations — no new ones are created. Applies per tag scope (e.g., per-tag when using `per_tag` observation scopes). Observations with no tags are not subject to this limit. `-1` = unlimited. Configurable per bank. | `-1` | #### Customizing observations: when to use what @@ -1154,7 +1155,7 @@ Configuration fields are categorized for security: 1. **Configurable Fields** - Safe behavioral settings that can be customized per-bank: - Retention: `retain_chunk_size`, `retain_extraction_mode`, `retain_mission`, `retain_custom_instructions` - - Observations: `enable_observations`, `observations_mission` + - Observations: `enable_observations`, `observations_mission`, `max_observations_per_scope` - MCP access control: `mcp_enabled_tools` 2. **Credential Fields** - NEVER exposed or configurable via API: diff --git a/skills/hindsight-docs/references/sdks/integrations/codex.md b/skills/hindsight-docs/references/sdks/integrations/codex.md new file mode 100644 index 0000000000..edb3b69794 --- /dev/null +++ b/skills/hindsight-docs/references/sdks/integrations/codex.md @@ -0,0 +1,189 @@ +--- +sidebar_position: 6 +--- + +# OpenAI Codex CLI + +Persistent memory for [OpenAI Codex CLI](https://github.com/openai/codex) using [Hindsight](https://vectorize.io/hindsight). Three Python hook scripts automatically recall relevant context before each prompt and retain conversations after each turn — no changes to your Codex workflow required. + +## Quick Start + +```bash +# 1. Clone the Hindsight repo and install the plugin +git clone https://github.com/vectorize-io/hindsight.git +cd hindsight/hindsight-integrations/codex +./install.sh + +# 2. Configure your Hindsight connection +cat > ~/.hindsight/codex.json << 'EOF' +{ + "hindsightApiUrl": "https://api.hindsight.vectorize.io", + "hindsightApiToken": "hsk_your_token_here", + "bankId": "codex" +} +EOF + +# 3. Start Codex — memory is live +codex +``` + +For a local Hindsight instance, set `hindsightApiUrl` to `http://localhost:9077` and omit `hindsightApiToken`. + +## Features + +- **Auto-recall** — on every user prompt, queries Hindsight for relevant memories and injects them as `additionalContext` (invisible to the transcript, visible to Codex) +- **Auto-retain** — after each Codex response, stores the conversation transcript to Hindsight for future recall +- **Dynamic bank IDs** — supports per-project memory isolation based on the working directory +- **Session-level upsert** — uses the session ID as the document ID so re-running the same session updates rather than duplicates stored content +- **Zero dependencies** — pure Python stdlib, no pip install required + +## Architecture + +The plugin uses three Codex hook events: + +| Hook | Event | Purpose | +|------|-------|---------| +| `session_start.py` | `SessionStart` | Warm up — verify Hindsight is reachable | +| `recall.py` | `UserPromptSubmit` | **Auto-recall** — query memories, inject as `additionalContext` | +| `retain.py` | `Stop` | **Auto-retain** — extract transcript, POST to Hindsight (async) | + +On `UserPromptSubmit`, the hook reads the prompt, queries Hindsight for the most relevant memories, and outputs a `hookSpecificOutput.additionalContext` block. Codex prepends this to the conversation before sending it to the model: + +``` + +Relevant memories from past conversations... +Current time - 2026-03-27 09:14 + +- Project uses FastAPI with asyncpg — not SQLAlchemy [world] (2026-03-26) +- Preferred testing framework: pytest with pytest-asyncio [experience] (2026-03-26) + +``` + +On `Stop`, the hook reads the session transcript, strips previously injected memory tags (to prevent feedback loops), and POSTs the conversation to Hindsight asynchronously. + +## Connection Modes + +### 1. External API (recommended) + +Connect to a running Hindsight server (cloud or self-hosted): + +```json +{ + "hindsightApiUrl": "https://api.hindsight.vectorize.io", + "hindsightApiToken": "hsk_your_token" +} +``` + +### 2. Local Daemon + +Run `hindsight-embed` locally. The `session_start.py` hook will detect it on `apiPort` (default `9077`). The daemon is not auto-started by the Codex plugin — start it separately: + +```bash +uvx hindsight-embed +``` + +Then leave `hindsightApiUrl` empty in your config and the plugin will connect to `http://localhost:9077`. + +## Configuration + +Settings are loaded from `~/.hindsight/codex.json`. Every setting can also be overridden via environment variable. + +**Loading order** (later entries win): + +1. Built-in defaults +2. Plugin `settings.json` (at `~/.hindsight/codex/settings.json`) +3. User config (`~/.hindsight/codex.json`) +4. Environment variables + +--- + +### Connection + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `hindsightApiUrl` | `HINDSIGHT_API_URL` | `""` | URL of the Hindsight API server. Required. | +| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` | `null` | API token for authentication. Required for Hindsight Cloud. | +| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Port for the local `hindsight-embed` daemon. | + +--- + +### Memory Bank + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `bankId` | `HINDSIGHT_BANK_ID` | `"codex"` | The bank to read from and write to. All sessions share this bank unless `dynamicBankId` is enabled. | +| `bankMission` | `HINDSIGHT_BANK_MISSION` | coding assistant prompt | Describes the agent's purpose. Sent when creating or updating the bank. | +| `retainMission` | — | extraction prompt | Instructions for Hindsight's fact extraction — what to extract from coding conversations. | +| `dynamicBankId` | `HINDSIGHT_DYNAMIC_BANK_ID` | `false` | When `true`, derives a unique bank ID from `dynamicBankGranularity` fields — useful for per-project isolation. | +| `dynamicBankGranularity` | — | `["agent", "project"]` | Which fields to combine for dynamic bank IDs. `"project"` = working directory, `"agent"` = agent name. | +| `bankIdPrefix` | — | `""` | Prefix prepended to all bank IDs. | +| `agentName` | `HINDSIGHT_AGENT_NAME` | `"codex"` | Agent name used in dynamic bank ID derivation. | + +--- + +### Auto-Recall + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Master switch for auto-recall. | +| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Search depth: `"low"` (fast), `"mid"` (balanced), `"high"` (thorough). | +| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `1024` | Max tokens in the recalled memory block. | +| `recallTypes` | — | `["world", "experience"]` | Memory types to retrieve. | +| `recallContextTurns` | `HINDSIGHT_RECALL_CONTEXT_TURNS` | `1` | Prior turns to include when building the recall query. `1` = latest prompt only. | +| `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Max characters in the query sent to Hindsight. | +| `recallRoles` | — | `["user", "assistant"]` | Which roles to include when building a multi-turn query. | +| `recallPromptPreamble` | — | built-in | Text placed above the recalled memories in the injected context block. | + +--- + +### Auto-Retain + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Master switch for auto-retain. | +| `retainMode` | `HINDSIGHT_RETAIN_MODE` | `"full-session"` | `"full-session"` sends the full transcript per session (upserted by session ID). `"chunked"` sends sliding windows every N turns. | +| `retainEveryNTurns` | — | `10` | Retain fires every N turns. `1` = every turn. Higher values reduce API calls. | +| `retainOverlapTurns` | — | `2` | Extra turns included from the previous chunk (chunked mode only). | +| `retainRoles` | — | `["user", "assistant"]` | Which roles to include in the retained transcript. | +| `retainTags` | — | `["{session_id}"]` | Tags attached to the stored document. `{session_id}` is replaced at runtime. | +| `retainMetadata` | — | `{}` | Arbitrary key-value metadata attached to the stored document. | +| `retainContext` | — | `"codex"` | Label identifying the source integration. Useful when multiple integrations write to the same bank. | + +--- + +### Debug + +| Setting | Env Var | Default | Description | +|---------|---------|---------|-------------| +| `debug` | `HINDSIGHT_DEBUG` | `false` | Enable verbose logging to stderr. All log lines are prefixed with `[Hindsight]`. | + +## Per-Project Memory + +To give each project its own isolated memory bank, enable dynamic bank IDs: + +```json +{ + "dynamicBankId": true, + "dynamicBankGranularity": ["agent", "project"] +} +``` + +With this config, running Codex in `~/projects/api` and `~/projects/frontend` stores and recalls memories separately. Bank IDs are derived from the working directory path. + +## Troubleshooting + +**Hooks not firing**: Check that `~/.codex/config.toml` contains `codex_hooks = true` under `[features]`. Re-run `install.sh` to write this automatically. + +**No memories recalled**: Recall returns results only after something has been retained. Either complete one Codex session first, or seed your bank manually using the [cookbook example](https://github.com/vectorize-io/hindsight-cookbook/tree/main/applications/codex-memory). + +**Memory not being stored**: `retainEveryNTurns` defaults to `10` — retain only fires every 10 turns. While testing, add `"retainEveryNTurns": 1` to `~/.hindsight/codex.json`. + +**Debug mode**: Add `"debug": true` to `~/.hindsight/codex.json` to see what Hindsight is doing on each turn: + +``` +[Hindsight] Recalling from bank 'codex', query length: 42 +[Hindsight] Injecting 3 memories +[Hindsight] Retaining to bank 'codex', doc 'sess-abc123', 2 messages, 847 chars +``` + +**High latency on recall**: Use `"recallBudget": "low"` or reduce `recallMaxTokens` to speed up recall queries. diff --git a/skills/hindsight-docs/references/sdks/integrations/llamaindex.md b/skills/hindsight-docs/references/sdks/integrations/llamaindex.md new file mode 100644 index 0000000000..4a3823715c --- /dev/null +++ b/skills/hindsight-docs/references/sdks/integrations/llamaindex.md @@ -0,0 +1,256 @@ +--- +sidebar_position: 8 +--- + +# LlamaIndex + +Persistent long-term memory for [LlamaIndex](https://docs.llamaindex.ai/) agents via Hindsight. Two packages are available: + +- **`llama-index-tools-hindsight`** — Agent-driven memory tools (retain/recall/reflect) +- **`llama-index-memory-hindsight`** — Automatic memory via LlamaIndex's `BaseMemory` interface + +Both follow the LlamaIndex namespace package convention (`from llama_index.tools.hindsight import ...`). + +## Installation + +```bash +# Tools only (agent-driven) +pip install llama-index-tools-hindsight + +# Memory only (automatic) +pip install llama-index-memory-hindsight + +# Both +pip install llama-index-tools-hindsight llama-index-memory-hindsight +``` + +--- + +## Automatic Memory (BaseMemory) + +The simplest way to add Hindsight memory to a LlamaIndex agent. Messages are automatically stored on each turn, and relevant memories are recalled and injected as context. + +```python +import asyncio +from hindsight_client import Hindsight +from llama_index.memory.hindsight import HindsightMemory +from llama_index.core.agent import ReActAgent +from llama_index.llms.openai import OpenAI + +async def main(): + client = Hindsight(base_url="http://localhost:8888") + + memory = HindsightMemory.from_client( + client=client, + bank_id="user-123", + mission="Track user preferences and project context", + ) + + agent = ReActAgent(tools=tools, llm=OpenAI(model="gpt-4o"), memory=memory) + response = await agent.run("Remember that I prefer dark mode") + print(response) + +asyncio.run(main()) +``` + +### How It Works + +| Event | What Happens | +|-------|-------------| +| Agent receives input | `get(input)` recalls relevant memories from Hindsight, prepends as system message | +| Agent produces output | `put(message)` retains the message to Hindsight for future recall | +| New session starts | Previous memories are available via recall; local chat buffer starts empty | + +### `HindsightMemory.from_client()` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `client` | `Hindsight` | *required* | Hindsight client instance | +| `bank_id` | `str` | *required* | Memory bank ID | +| `mission` | `str` | `None` | Bank mission — auto-creates bank on first use | +| `context` | `str` | `"llamaindex"` | Source label for retain operations | +| `budget` | `str` | `"mid"` | Recall budget level | +| `max_tokens` | `int` | `4096` | Max recall tokens | +| `tags` | `list[str]` | `None` | Tags for retain operations | +| `recall_tags` | `list[str]` | `None` | Tags to filter recall | +| `recall_tags_match` | `str` | `"any"` | Tag matching mode | +| `system_prompt` | `str` | *(built-in)* | Template for memory system message. Must contain `{memories}` | +| `chat_history_limit` | `int` | `100` | Max messages in local buffer | + +Also available: `HindsightMemory.from_url(hindsight_api_url, bank_id, ...)` for creating without a pre-built client. + +--- + +## Agent-Driven Tools (BaseToolSpec) + +For explicit control, expose retain/recall/reflect as tools the agent can choose to call. + +### Quick Start: Tool Spec + +```python +import asyncio +from hindsight_client import Hindsight +from llama_index.tools.hindsight import HindsightToolSpec +from llama_index.llms.openai import OpenAI +from llama_index.core.agent import ReActAgent + +async def main(): + client = Hindsight(base_url="http://localhost:8888") + + spec = HindsightToolSpec( + client=client, + bank_id="user-123", + mission="Track user preferences", + ) + tools = spec.to_tool_list() + + agent = ReActAgent(tools=tools, llm=OpenAI(model="gpt-4o")) + response = await agent.run("Remember that I prefer dark mode") + print(response) + +asyncio.run(main()) +``` + +### Quick Start: Factory Function + +```python +from llama_index.tools.hindsight import create_hindsight_tools + +tools = create_hindsight_tools( + client=client, + bank_id="user-123", + mission="Track user preferences", +) +``` + +### Selecting Tools + +```python +# Via to_tool_list() +tools = spec.to_tool_list(spec_functions=["recall_memory", "reflect_on_memory"]) + +# Via factory flags +tools = create_hindsight_tools( + client=client, + bank_id="user-123", + include_retain=True, + include_recall=True, + include_reflect=False, +) +``` + +### Configuration + +Set defaults via `configure()`, override per-call: + +```python +from llama_index.tools.hindsight import configure + +configure( + hindsight_api_url="http://localhost:8888", + api_key="your-api-key", # or set HINDSIGHT_API_KEY env var + budget="mid", + tags=["source:llamaindex"], + context="my-app", + mission="Track user preferences", +) + +# Now create tools without passing client/url +tools = create_hindsight_tools(bank_id="user-123") +``` + +### `HindsightToolSpec()` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `bank_id` | `str` | *required* | Hindsight memory bank to operate on | +| `client` | `Hindsight` | `None` | Pre-configured Hindsight client | +| `hindsight_api_url` | `str` | `None` | API URL (used if no client provided) | +| `api_key` | `str` | `None` | API key (used if no client provided) | +| `budget` | `str` | `None` → `"mid"` | Recall/reflect budget: `low`, `mid`, `high` | +| `max_tokens` | `int` | `None` → `4096` | Max tokens for recall results | +| `tags` | `list[str]` | `None` | Tags applied when storing memories | +| `recall_tags` | `list[str]` | `None` | Tags to filter recall results | +| `recall_tags_match` | `str` | `None` → `"any"` | Tag matching: `any`, `all`, `any_strict`, `all_strict` | +| `retain_metadata` | `dict[str, str]` | `None` | Default metadata for retain operations | +| `retain_document_id` | `str` | `None` | Document ID for retain. Auto-generates `{session}-{timestamp}` if not set | +| `retain_context` | `str` | `"llamaindex"` | Source label for retain operations | +| `recall_types` | `list[str]` | `None` | Fact types: `world`, `experience`, `opinion`, `observation` | +| `recall_include_entities` | `bool` | `False` | Include entity info in recall results | +| `reflect_context` | `str` | `None` | Additional context for reflect | +| `reflect_max_tokens` | `int` | `None` | Max tokens for reflect (defaults to `max_tokens`) | +| `reflect_response_schema` | `dict` | `None` | JSON schema to constrain reflect output | +| `reflect_tags` | `list[str]` | `None` | Tags for reflect (defaults to `recall_tags`) | +| `reflect_tags_match` | `str` | `None` | Tag matching for reflect (defaults to `recall_tags_match`) | +| `mission` | `str` | `None` | Bank mission — auto-creates bank on first use | + +--- + +## Production Patterns + +### Bank Mission + +Set a mission to give the memory engine context for fact extraction: + +```python +# Tools +spec = HindsightToolSpec( + client=client, + bank_id="user-123", + mission="Track user coding preferences, project context, and technical decisions", +) + +# Memory +memory = HindsightMemory.from_client( + client=client, + bank_id="user-123", + mission="Track user coding preferences, project context, and technical decisions", +) +``` + +The bank is created automatically on first use. If it already exists, creation is silently skipped. + +### Memory Scoping with Tags + +```python +spec = HindsightToolSpec( + client=client, + bank_id="user-123", + tags=["source:chat", "session:abc"], # applied to all retains + recall_tags=["source:chat"], # filter recalls to chat memories + recall_tags_match="any", +) +``` + +### Error Handling + +Both packages handle errors gracefully — operations are logged and return friendly messages instead of raising exceptions. Agents continue functioning even if memory is unavailable. + +### Combining Tools + Memory + +Use both packages together for maximum flexibility: + +```python +from llama_index.tools.hindsight import create_hindsight_tools +from llama_index.memory.hindsight import HindsightMemory + +# Automatic memory for context enrichment +memory = HindsightMemory.from_client(client=client, bank_id="user-123") + +# Explicit tools for agent-driven reflect +tools = create_hindsight_tools( + client=client, + bank_id="user-123", + include_retain=False, # memory handles retain automatically + include_recall=False, # memory handles recall automatically + include_reflect=True, # agent can still explicitly reflect +) + +agent = ReActAgent(tools=tools, llm=llm, memory=memory) +``` + +## Requirements + +- Python 3.10+ +- `llama-index-core >= 0.11.0` +- `hindsight-client >= 0.4.0`