Skip to content

Commit 394d66e

Browse files
DK09876claudeDK09876
authored
feat(integrations): add Haystack integration (#1256)
* feat(integrations): add Haystack integration for persistent agent memory Add hindsight-haystack package providing Haystack Tool instances backed by Hindsight's retain/recall/reflect APIs. Uses async client methods with event-loop-safe sync wrapper to work correctly inside Haystack's agent runtime. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(haystack): use persistent event loop for async client calls aiohttp binds its session to the creating event loop, so asyncio.run() (which creates/destroys a loop per call) breaks on sequential calls. Switch to a persistent daemon-thread event loop with run_coroutine_threadsafe. Also removes unused per-operation timeout constants and adds _run_sync tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(haystack): add HindsightToolset with auto-recall/retain, fix review issues - Add HindsightToolset(Toolset) with auto_recall and auto_retain flags that automatically inject recalled memories into the system prompt before each turn and retain user/assistant messages after each turn - Fix _ensure_bank to retry on transient errors instead of permanently disabling bank creation - Fix reflect_on_memory to return structured_output JSON when response_schema is set - Truncate error messages to avoid dumping raw HTTP responses to agents - Extract _build_backend_kwargs() and _build_tools() as shared helpers - Add 20 new tests (60 -> 80 total) covering toolset, auto-recall, auto-retain, structured output, and bank creation retry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(haystack): address review round 2 — max_recall_results, role metadata, _run_sync cleanup - Add max_recall_results param to HindsightToolset (default 10) to cap auto-recall prompt injection size, matching Pydantic AI pattern - Auto-retain now includes role + source metadata on messages, matching LlamaIndex's metadata pattern for distinguishable conversation turns - _recall_for_prompt now calls the API directly with result cap instead of going through the formatted string from recall_memory - Serialize/deserialize max_recall_results in to_dict/from_dict - Add tests for max_recall_results and role metadata (82 total) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(haystack): default to Cloud without configure(); add gated E2E + bucketing - resolve_client() now falls back to DEFAULT_HINDSIGHT_API_URL and the HINDSIGHT_API_KEY env var when configure() was never called (it previously raised "No Hindsight API URL configured"). Updated the unit test to assert the cloud-default + env-key behavior. Satisfies the "default to Cloud" goal. - Add a gated tests/test_e2e.py (retain/recall/reflect tools against a live Hindsight server), marked requires_real_llm; register the marker in pyproject; the test-haystack-integration CI job now runs the deterministic bucket (-m "not requires_real_llm"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(haystack): close owned clients at exit; run E2E client I/O on the bridge loop The tools run async client calls on a persistent background event loop. aiohttp sessions bound to that loop were never closed, surfacing as "Unclosed client session/connector" warnings. Track module-owned Hindsight clients (those created when the caller didn't pass client=) and close them on the loop via an atexit hook, then stop the loop. The live E2E now performs all client I/O through that same loop (acreate_bank/adelete_bank/aclose via _run_sync) and logs cleanup failures instead of swallowing them — zero unclosed-connector warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(haystack): sync openapi.json with main Branch carried an older snapshot of hindsight-docs/static/openapi.json that pre-dated main's addition of OperationProgress. Re-checkout main's openapi.json so check-openapi-compatibility passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(haystack): strip api_key from to_dict() so it doesn't leak to YAML _build_backend_kwargs was emitting the api_key in the serializable dict that to_dict() returns. Haystack pipelines get dumped to YAML for inspection, checkpointing, and sharing — a serialized key leaks into every dump. Reviewer (benfrank241) flagged this on #1256. Drop the api_key from the serialized backend_kwargs. resolve_client() already reads HINDSIGHT_API_KEY from the env var as a final fallback, so a redeployed pipeline picks the key back up from the host's environment rather than from the YAML. The test_tools_round_trip_serialization_with_client test previously asserted the leak — flipped it to assert the key is NOT present and added a json.dumps probe asserting the literal key value also doesn't appear under any other field name. Pre-fix, the test fails: AssertionError: api_key must not appear in serialized backend_kwargs — would leak to YAML pipeline dumps assert 'api_key' not in {'api_key': 'client-key', ...} 86/86 tests pass post-fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: re-trigger CI A previous push to this branch silently did not trigger a pull_request event in GitHub Actions, leaving the PR without a CI run for the latest HEAD. Push an empty commit to force a new event. * ci: empty commit to attach pull_request CI check to the PR head (Previous pushes did not auto-trigger pull_request workflow events for reasons internal to GitHub Actions; manual workflow_dispatch runs passed green but their checks don't roll up onto the PR. Re-poking the head to surface the green state on the PR.) * ci: trailing newline to force CI retrigger * fix(haystack): register in changelog/gallery + docs page + tidy tools Review follow-ups for the Haystack integration: 1. Add haystack to the INTEGRATIONS map in generate_changelog.py so the release script's changelog step resolves the slug (was missing, which would fail the release). 2. Add the integrations.json gallery entry, a doc page at docs-integrations/haystack.md, and an icon — required by check-integrations.mjs (forward: entry needs a doc page; reverse: a released integration must appear in the gallery). 3. Drop the inaccurate 'Raises: HindsightError' clause from create_hindsight_tools — resolution always succeeds (URL defaults to Cloud) so it never raises; the error type stays exported as the conventional public catch type. 4. Replace the _TOOL_DEFS dict-of-3-tuples with a frozen _ToolDef dataclass and drop the redundant method-name field (it equalled the dict key). * fix(haystack): use official Haystack logo for gallery icon Replace the placeholder glyph with the real deepset Haystack mark (teal #0EAF9C rounded square + white symbol), extracted as vector from deepset's own website source (deepset-ai/haystack-home site-logo partial). --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: DK09876 <dk09876@DK09876s-MacBook-Pro.local>
1 parent 9891f53 commit 394d66e

19 files changed

Lines changed: 4938 additions & 1 deletion

File tree

.github/workflows/test.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ jobs:
5959
integrations-vapi: ${{ steps.filter.outputs.integrations-vapi }}
6060
integrations-flowise: ${{ steps.filter.outputs.integrations-flowise }}
6161
integrations-google-adk: ${{ steps.filter.outputs.integrations-google-adk }}
62+
integrations-haystack: ${{ steps.filter.outputs.integrations-haystack }}
6263
tools-agent-sdk: ${{ steps.filter.outputs.tools-agent-sdk }}
6364
integrations-roo-code: ${{ steps.filter.outputs.integrations-roo-code }}
6465
dev: ${{ steps.filter.outputs.dev }}
@@ -141,6 +142,8 @@ jobs:
141142
- 'hindsight-integrations/langgraph/**'
142143
integrations-llamaindex:
143144
- 'hindsight-integrations/llamaindex/**'
145+
integrations-haystack:
146+
- 'hindsight-integrations/haystack/**'
144147
integrations-paperclip:
145148
- 'hindsight-integrations/paperclip/**'
146149
integrations-opencode:
@@ -3273,6 +3276,45 @@ jobs:
32733276
# (requires_real_llm) needs a live Hindsight server and runs separately.
32743277
run: uv run pytest tests -v -m "not requires_real_llm"
32753278

3279+
test-haystack-integration:
3280+
needs: [detect-changes]
3281+
if: >-
3282+
github.event_name != 'pull_request_review' &&
3283+
(github.event_name == 'workflow_dispatch' ||
3284+
needs.detect-changes.outputs.integrations-haystack == 'true' ||
3285+
needs.detect-changes.outputs.ci == 'true')
3286+
runs-on: ubuntu-latest
3287+
3288+
steps:
3289+
- uses: actions/checkout@v6
3290+
with:
3291+
ref: ${{ github.event.pull_request.head.sha || '' }}
3292+
3293+
- name: Install uv
3294+
uses: astral-sh/setup-uv@v7
3295+
with:
3296+
enable-cache: true
3297+
prune-cache: false
3298+
3299+
- name: Set up Python
3300+
uses: actions/setup-python@v6
3301+
with:
3302+
python-version-file: ".python-version"
3303+
3304+
- name: Build haystack integration
3305+
working-directory: ./hindsight-integrations/haystack
3306+
run: uv build
3307+
3308+
- name: Install dependencies
3309+
working-directory: ./hindsight-integrations/haystack
3310+
run: uv sync --frozen
3311+
3312+
- name: Run tests
3313+
working-directory: ./hindsight-integrations/haystack
3314+
# PR CI runs only the deterministic bucket; the real-LLM E2E bucket
3315+
# (requires_real_llm) needs a live Hindsight server and runs separately.
3316+
run: uv run pytest tests -v -m "not requires_real_llm"
3317+
32763318
test-openai-agents-integration:
32773319
needs: [detect-changes]
32783320
if: >-
@@ -4257,6 +4299,7 @@ jobs:
42574299
- test-llamaindex-integration
42584300
- test-openai-agents-integration
42594301
- test-agentcore-integration
4302+
- test-haystack-integration
42604303
- test-pip-slim
42614304
- test-embed
42624305
- test-embed-windows

hindsight-dev/hindsight_dev/generate_changelog.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class IntegrationMeta:
6868
"flowise": IntegrationMeta("@vectorize-io/flowise-nodes-hindsight", "Flowise"),
6969
"google-adk": IntegrationMeta("hindsight-google-adk", "Google ADK"),
7070
"superagent": IntegrationMeta("hindsight-superagent", "Superagent"),
71+
"haystack": IntegrationMeta("hindsight-haystack", "Haystack"),
7172
"roo-code": IntegrationMeta("hindsight-roo-code", "Roo Code"),
7273
}
7374

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
---
2+
sidebar_position: 34
3+
title: "Haystack Persistent Memory with Hindsight | Integration"
4+
description: "Add persistent long-term memory to Haystack agents with Hindsight. Provides retain/recall/reflect Tools plus a HindsightToolset with optional auto-recall and auto-retain."
5+
---
6+
7+
# Haystack
8+
9+
Persistent long-term memory for [Haystack](https://haystack.deepset.ai/) agents via Hindsight. The `hindsight-haystack` package gives you two complementary patterns:
10+
11+
- **`create_hindsight_tools(...)`** — Returns a list of Haystack `Tool`s (`retain_memory`, `recall_memory`, `reflect_on_memory`) the model can call directly inside a turn.
12+
- **`HindsightToolset`** — A Haystack `Toolset` that bundles the same tools and adds optional **auto-recall** (inject relevant memories into the system prompt before each turn) and **auto-retain** (store user + assistant messages after each turn).
13+
14+
## Installation
15+
16+
```bash
17+
pip install hindsight-haystack
18+
```
19+
20+
## Quick Start
21+
22+
```python
23+
from hindsight_client import Hindsight
24+
from hindsight_haystack import create_hindsight_tools
25+
from haystack.components.agents import Agent
26+
from haystack.components.generators.chat import OpenAIChatGenerator
27+
from haystack.dataclasses import ChatMessage
28+
29+
client = Hindsight(base_url="http://localhost:8888")
30+
31+
tools = create_hindsight_tools(
32+
client=client,
33+
bank_id="user-123",
34+
mission="Track user preferences",
35+
)
36+
37+
agent = Agent(
38+
chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"),
39+
tools=tools,
40+
system_prompt=(
41+
"You are a helpful assistant with long-term memory. "
42+
"Use retain_memory to store important facts. "
43+
"Use recall_memory to search memory before answering."
44+
),
45+
)
46+
47+
result = agent.run(messages=[ChatMessage.from_user("Remember that I prefer dark mode")])
48+
print(result["messages"][-1].text)
49+
```
50+
51+
## Automatic Memory with HindsightToolset
52+
53+
For automatic recall and retain without relying on the agent to call tools:
54+
55+
```python
56+
from hindsight_haystack import HindsightToolset
57+
58+
toolset = HindsightToolset(
59+
client=client,
60+
bank_id="user-123",
61+
mission="Track user preferences",
62+
auto_recall=True, # Inject memories into the system prompt before each turn
63+
auto_retain=True, # Store user + assistant messages after each turn
64+
)
65+
66+
agent = Agent(
67+
chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"),
68+
tools=toolset,
69+
system_prompt="You are a helpful assistant with long-term memory.",
70+
)
71+
72+
# Use toolset.run() for automatic memory behavior
73+
result = toolset.run(agent, messages=[ChatMessage.from_user("I prefer dark mode")])
74+
```
75+
76+
## Selective Tools
77+
78+
```python
79+
# Only retain + recall (no reflect)
80+
tools = create_hindsight_tools(
81+
client=client,
82+
bank_id="user-123",
83+
include_reflect=False,
84+
)
85+
```
86+
87+
## Configuration
88+
89+
Call `configure()` once to set connection defaults so you can omit `client=`/`hindsight_api_url=` on every call:
90+
91+
```python
92+
from hindsight_haystack import configure
93+
94+
configure(
95+
hindsight_api_url="http://localhost:8888",
96+
api_key="your-api-key",
97+
budget="mid",
98+
tags=["source:haystack"],
99+
context="my-app",
100+
mission="Track user preferences",
101+
)
102+
103+
tools = create_hindsight_tools(bank_id="user-123")
104+
```
105+
106+
The API URL defaults to Hindsight Cloud (`https://api.hindsight.vectorize.io`), and the API key falls back to the `HINDSIGHT_API_KEY` environment variable.
107+
108+
## Requirements
109+
110+
- Python 3.10+
111+
- `haystack-ai >= 2.12.0`
112+
- `hindsight-client >= 0.4.0`
113+
114+
## Prerequisites
115+
116+
A running Hindsight instance:
117+
118+
**Hindsight Cloud (recommended):** [Sign up](https://ui.hindsight.vectorize.io/signup) — no self-hosting required.
119+
120+
**Self-hosted:**
121+
122+
```bash
123+
pip install hindsight-all
124+
export HINDSIGHT_API_LLM_API_KEY=your-api-key
125+
hindsight-api # starts on http://localhost:8888
126+
```

hindsight-docs/src/data/integrations.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,16 @@
200200
"link": "/sdks/integrations/google-adk",
201201
"icon": "/img/icons/google-adk.png"
202202
},
203+
{
204+
"id": "haystack",
205+
"name": "Haystack",
206+
"description": "Persistent memory for Haystack agents. Provides retain/recall/reflect Tools plus a HindsightToolset with optional auto-recall and auto-retain.",
207+
"type": "official",
208+
"by": "hindsight",
209+
"category": "framework",
210+
"link": "/sdks/integrations/haystack",
211+
"icon": "/img/icons/haystack.svg"
212+
},
203213
{
204214
"id": "nemoclaw",
205215
"name": "NemoClaw",
Lines changed: 6 additions & 0 deletions
Loading
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# hindsight-haystack
2+
3+
Haystack integration for [Hindsight](https://github.com/vectorize-io/hindsight) — persistent long-term memory for AI agents.
4+
5+
Provides Haystack `Tool` instances that give any Haystack `Agent` persistent memory via Hindsight's retain/recall/reflect APIs.
6+
7+
## Installation
8+
9+
```bash
10+
pip install hindsight-haystack
11+
```
12+
13+
## Quick Start
14+
15+
```python
16+
from hindsight_client import Hindsight
17+
from hindsight_haystack import create_hindsight_tools
18+
from haystack.components.agents import Agent
19+
from haystack.components.generators.chat import OpenAIChatGenerator
20+
from haystack.dataclasses import ChatMessage
21+
22+
client = Hindsight(base_url="http://localhost:8888")
23+
24+
tools = create_hindsight_tools(
25+
client=client,
26+
bank_id="user-123",
27+
mission="Track user preferences",
28+
)
29+
30+
agent = Agent(
31+
chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"),
32+
tools=tools,
33+
system_prompt=(
34+
"You are a helpful assistant with long-term memory. "
35+
"Use retain_memory to store important facts. "
36+
"Use recall_memory to search memory before answering."
37+
),
38+
)
39+
40+
result = agent.run(messages=[ChatMessage.from_user("Remember that I prefer dark mode")])
41+
print(result["messages"][-1].text)
42+
```
43+
44+
## Automatic Memory with HindsightToolset
45+
46+
For automatic recall and retain without relying on the agent to call tools:
47+
48+
```python
49+
from hindsight_haystack import HindsightToolset
50+
51+
toolset = HindsightToolset(
52+
client=client,
53+
bank_id="user-123",
54+
mission="Track user preferences",
55+
auto_recall=True, # Inject memories into system prompt before each turn
56+
auto_retain=True, # Store user + assistant messages after each turn
57+
)
58+
59+
agent = Agent(
60+
chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"),
61+
tools=toolset,
62+
system_prompt="You are a helpful assistant with long-term memory.",
63+
)
64+
65+
# Use toolset.run() for automatic memory behavior
66+
result = toolset.run(agent, messages=[ChatMessage.from_user("I prefer dark mode")])
67+
```
68+
69+
## Selective Tools
70+
71+
```python
72+
# Only retain + recall (no reflect)
73+
tools = create_hindsight_tools(
74+
client=client,
75+
bank_id="user-123",
76+
include_reflect=False,
77+
)
78+
```
79+
80+
## Configuration
81+
82+
```python
83+
from hindsight_haystack import configure
84+
85+
configure(
86+
hindsight_api_url="http://localhost:8888",
87+
api_key="your-api-key",
88+
budget="mid",
89+
tags=["source:haystack"],
90+
context="my-app",
91+
mission="Track user preferences",
92+
)
93+
94+
# Now you can skip client= and url= arguments
95+
tools = create_hindsight_tools(bank_id="user-123")
96+
```
97+
98+
## Requirements
99+
100+
- Python 3.10+
101+
- `haystack-ai >= 2.12.0`
102+
- `hindsight-client >= 0.4.0`
103+
104+
## Documentation
105+
106+
- [Integration docs](https://docs.hindsight.vectorize.io/docs/sdks/integrations/haystack)
107+
- [Hindsight API docs](https://docs.hindsight.vectorize.io)
108+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Hindsight memory integration for Haystack agents.
2+
3+
Provides Haystack-compatible ``Tool`` instances backed by Hindsight's
4+
retain/recall/reflect APIs. Use ``create_hindsight_tools()`` to create
5+
tools for any Haystack ``Agent``.
6+
7+
Usage::
8+
9+
from hindsight_haystack import create_hindsight_tools
10+
from haystack.components.agents import Agent
11+
from haystack.components.generators.chat import OpenAIChatGenerator
12+
13+
tools = create_hindsight_tools(bank_id="user-123", client=client)
14+
agent = Agent(chat_generator=OpenAIChatGenerator(), tools=tools)
15+
"""
16+
17+
from .config import (
18+
HindsightHaystackConfig,
19+
configure,
20+
get_config,
21+
reset_config,
22+
)
23+
from .errors import HindsightError
24+
from .tools import HindsightToolset, create_hindsight_tools
25+
26+
__version__ = "0.1.0"
27+
28+
__all__ = [
29+
"configure",
30+
"get_config",
31+
"reset_config",
32+
"HindsightHaystackConfig",
33+
"HindsightError",
34+
"create_hindsight_tools",
35+
"HindsightToolset",
36+
]

0 commit comments

Comments
 (0)