feat: add Agno integration with Hindsight memory toolkit (#596)

* feat: add Agno integration with Hindsight memory toolkit

Add hindsight-agno package providing Hindsight memory tools (retain,
recall, reflect) as an Agno Toolkit, following the same pattern as
Agno's Mem0Tools. Includes per-user bank isolation, global config,
bank auto-creation, and memory_instructions() for system prompt
injection.

Also adds cookbook documentation page with architecture diagrams,
quick start examples, and configuration reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove n8n blog post, add Agno icon, bind to release process

- Remove n8n blog post from the agno integration branch
- Add Agno logo icon and map hindsight-agno SDK tag in CookbookGrid
- Add hindsight-agno to release.sh PYTHON_PACKAGES array
- Add build, publish, artifact upload, and release asset steps in release.yml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove cookbook page (moved to hindsight-cookbook repo)

The Agno cookbook application now lives in
vectorize-io/hindsight-cookbook/applications/agno-memory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ben 2026-03-18 06:17:23 -04:00 committed by GitHub
parent e2b19d3b38
commit 8c378b981a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 4175 additions and 282 deletions

View file

@ -66,6 +66,10 @@ jobs:
working-directory: ./hindsight-integrations/hermes
run: uv build --out-dir dist
- name: Build hindsight-agno
working-directory: ./hindsight-integrations/agno
run: uv build --out-dir dist
# Publish in order (client and api-slim first, then api/all wrappers which depend on them)
- name: Publish hindsight-client to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
@ -127,6 +131,12 @@ jobs:
packages-dir: ./hindsight-integrations/hermes/dist
skip-existing: true
- name: Publish hindsight-agno to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: ./hindsight-integrations/agno/dist
skip-existing: true
# Upload artifacts for GitHub release
- name: Upload artifacts
uses: actions/upload-artifact@v7
@ -143,6 +153,7 @@ jobs:
hindsight-integrations/crewai/dist/*
hindsight-integrations/pydantic-ai/dist/*
hindsight-integrations/hermes/dist/*
hindsight-integrations/agno/dist/*
retention-days: 1
release-typescript-client:
@ -681,6 +692,7 @@ jobs:
cp artifacts/python-packages/hindsight-integrations/litellm/dist/* release-assets/ || true
cp artifacts/python-packages/hindsight-integrations/pydantic-ai/dist/* release-assets/ || true
cp artifacts/python-packages/hindsight-integrations/hermes/dist/* release-assets/ || true
cp artifacts/python-packages/hindsight-integrations/agno/dist/* release-assets/ || true
cp artifacts/python-packages/hindsight-embed/dist/* release-assets/ || true
# TypeScript client
cp artifacts/typescript-client/*.tgz release-assets/ || true

View file

@ -1,280 +0,0 @@
---
title: "3 Nodes. Zero Code. Persistent Memory for n8n Workflows"
authors: [benfrank241]
date: 2026-03-16
tags: [n8n, tutorial, workflow, memory, no-code]
image: /img/blog/n8n-memory-workflows.png
---
![How to Add Persistent Memory to n8n Workflows](/img/blog/n8n-memory-workflows.png)
n8n workflows are stateless — every execution starts from zero. [Hindsight](https://ui.hindsight.vectorize.io/signup) adds persistent n8n memory via three HTTP Request nodes. No custom nodes, no vector database, no code.
<!-- truncate -->
**TL;DR:**
- n8n workflows are stateless — every execution starts from zero
- Hindsight adds persistent memory via three HTTP Request nodes
- No custom nodes, no vector database, no code
- Works with Hindsight Cloud (zero setup) or self-hosted
- Retain customer interactions, recall relevant context, reflect for synthesis
- Works with any [n8n](https://n8n.io/) workflow: support bots, lead enrichment, onboarding sequences
## The problem: n8n workflows without memory
You build an n8n workflow that handles customer support tickets. It triages, responds, escalates. It works.
But every execution is isolated.
Ticket comes in from Alice. Your workflow doesn't know Alice called last week about the same issue. Doesn't know she's on the Enterprise plan. Doesn't know she prefers email.
You could store this in a database. But then you need:
- A schema for every fact type
- Queries for every retrieval pattern
- Logic to decide what's relevant
That's not a workflow anymore. That's a backend project.
What you actually need: store facts as they happen, retrieve what's relevant, and synthesize when asked. Without leaving n8n.
## Architecture: three nodes for n8n persistent memory
```
Trigger (webhook, schedule, etc.)
Retain — POST to Hindsight, store the interaction
Your workflow logic
Recall — POST to Hindsight, get relevant past context
AI node / response — use memory to personalize
```
Three [HTTP Request nodes](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/). Same workflow structure you already use.
Under the hood, Hindsight automatically extracts entities and relationships from your content, builds a [knowledge graph with semantic search](/blog/2026/03/12/spreading-activation-memory-graphs), and returns relevant facts when you query. You don't manage any of that — it's handled by the three API calls below.
## Setting up Hindsight and n8n
### Start Hindsight
You have two options: Hindsight Cloud (no setup) or self-hosted (run it yourself).
**Option A: Hindsight Cloud**
1. [Sign up at Hindsight Cloud](https://ui.hindsight.vectorize.io/signup)
2. Create a memory bank in the dashboard and copy your API key
3. Your base URL is `https://api.hindsight.vectorize.io` and all requests need an `Authorization: Bearer hsk_your-key-here` header.
This is the easiest path if you're using n8n Cloud, since n8n Cloud can't reach localhost. Hindsight Cloud gives you a public API endpoint with no infrastructure to manage.
**Option B: Self-hosted**
Install and start the memory server:
```bash
pip install hindsight-all
export HINDSIGHT_API_LLM_API_KEY=YOUR_OPENAI_KEY
hindsight-api
```
It runs at `http://localhost:8888`. Embedded Postgres, fact extraction, semantic search, knowledge graph — all included.
### Create a memory bank
If you're using Hindsight Cloud, create a bank in the dashboard. For self-hosted, create one via the API:
```bash
curl -X PUT http://localhost:8888/v1/default/banks/n8n-workflow \
-H "Content-Type: application/json" \
-d '{
"name": "n8n Workflow Memory",
"mission": "Remember customer interactions and workflow context."
}'
```
This is idempotent — safe to run multiple times.
### Start n8n
```bash
npx n8n
```
Open `http://localhost:5678` and create a new workflow.
## The three n8n memory operations: retain, recall, reflect
### Retain — store interactions as they happen
Add an [HTTP Request node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/) after your trigger:
- **Method**: POST
- **URL**:
- Cloud: `https://api.hindsight.vectorize.io/v1/default/banks/n8n-workflow/memories`
- Self-hosted: `http://YOUR_IP:8888/v1/default/banks/n8n-workflow/memories`
- **Send Body**: on
- **Body Content Type**: JSON
- **Specify Body**: Using JSON
- **JSON**:
```json
{"items": [{"content": "Customer Bob prefers email communication and is on the Enterprise plan."}]}
```
If you're using Hindsight Cloud, add an Authorization header: `Bearer hsk_your-key-here`. In the HTTP Request node, go to **Options → Headers** and add it.
> **Self-hosted gotcha**: Use your machine's IP address (e.g., `192.168.x.x`), not `localhost`. n8n resolves `localhost` to its own process. Find your IP with `ipconfig getifaddr en0` (macOS) or `hostname -I` (Linux). This doesn't apply if you're using Hindsight Cloud or n8n Cloud.
Click **Execute step**. You should get a success response:
```json
{
"success": true,
"bank_id": "n8n-workflow",
"items_count": 1,
"async": false
}
```
Hindsight extracts facts from the content automatically — entities, relationships, timestamps. You don't manage any of that.
### Recall — retrieve relevant context
Add another HTTP Request node:
- **Method**: POST
- **URL**:
- Cloud: `https://api.hindsight.vectorize.io/v1/default/banks/n8n-workflow/memories/recall`
- Self-hosted: `http://YOUR_IP:8888/v1/default/banks/n8n-workflow/memories/recall`
- **JSON**:
```json
{"query": "What do we know about Bob?", "budget": "low"}
```
Click **Execute step**. You'll see extracted facts come back:
```json
{
"results": [
{
"text": "Bob prefers email communication and is subscribed to the Enterprise plan.",
"type": "world",
"entities": ["Bob"]
}
]
}
```
This is what you inject into your AI node's system prompt to personalize responses.
### Reflect — synthesize across all memories
For synthesis questions — "summarize this customer" or "what patterns do we see" — use reflect:
- **Method**: POST
- **URL**:
- Cloud: `https://api.hindsight.vectorize.io/v1/default/banks/n8n-workflow/reflect`
- Self-hosted: `http://YOUR_IP:8888/v1/default/banks/n8n-workflow/reflect`
- **JSON**:
```json
{"query": "Summarize what we know about our customers"}
```
Reflect traverses the knowledge graph and reasons across all stored memories. It's slower than recall but produces synthesized analysis, not just raw facts.
## Making n8n memory dynamic
The examples above use hardcoded JSON. In a real workflow, you'd use [n8n expressions](https://docs.n8n.io/code/expressions/) to inject dynamic data.
For **retain**, wire in data from your trigger:
```json
{"items": [{"content": "{{ $json.customer_name }} submitted a {{ $json.ticket_type }} ticket: {{ $json.message }}"}]}
```
For **recall**, query based on the current customer:
```json
{"query": "What do we know about {{ $json.customer_name }}?", "budget": "low"}
```
For **reflect**, ask for a synthesis:
```json
{"query": "Summarize all interactions with {{ $json.customer_name }}"}
```
n8n expressions make the memory layer dynamic without writing code.
## Example: support bot with n8n memory
Here's a practical workflow:
1. **Webhook trigger** — receives incoming support message
2. **Recall node** — retrieves relevant past context for this customer
3. **AI node** (OpenAI, Anthropic, etc.) — generates response with memory in the system prompt
4. **Retain node** — stores the interaction for future reference
5. **Respond to Webhook** — sends the reply
The AI node gets a system prompt like:
```
You are a support agent. Here is what you know about this customer:
{{ $('Recall').item.json.results[0].text }}
```
Now your support bot remembers past conversations, knows customer preferences, and doesn't ask the same questions twice. You can further customize how the agent reasons about that context using [disposition traits](/blog/2026/03/13/disposition-aware-agents) — for example, making it more empathetic for support or more skeptical for fraud detection.
## Pitfalls and edge cases
1. **Use your machine IP, not localhost** (self-hosted only). n8n can't reach `localhost:8888` because it resolves to itself. Use `ipconfig getifaddr en0` (macOS) or `hostname -I` (Linux) to find your LAN IP. If you're using Hindsight Cloud, this isn't an issue — just use `https://api.hindsight.vectorize.io`.
2. **Retain is asynchronous.** Fact extraction happens in the background after the API returns. If you recall immediately after retaining, the new facts may not be available yet. Add a short delay or design your workflow so recall happens on subsequent executions.
3. **The retain endpoint is `/memories`, not `/memories/retain`.** The URL path is `POST /v1/default/banks/{bank_id}/memories` with an `items` array in the body. The Python client method is called `retain()` but the HTTP endpoint is different.
4. **Bank creation uses PUT, not POST.** `PUT /v1/default/banks/{bank_id}` — the bank ID is in the URL path, not the request body.
5. **Set Content-Type explicitly.** n8n's HTTP Request node handles this when you select JSON body content type, but if you switch modes or use expressions, make sure the header is set.
## Tradeoffs: n8n memory with Hindsight vs. a database
| | **Hindsight + n8n** | **Database + custom queries** |
|---|---|---|
| **Setup** | Three HTTP nodes | Schema design, migration, query logic |
| **What it stores** | Natural language facts | Structured records |
| **Retrieval** | Semantic search | Exact match / SQL |
| **Synthesis** | Built-in (reflect) | Build it yourself |
| **Maintenance** | Zero | Schema evolution, query tuning |
**Use Hindsight when**: you want natural language memory without building a backend — customer context, conversation history, learned preferences.
**Use a database when**: you need structured records with exact lookups — order IDs, account balances, inventory counts.
They complement each other. Use Hindsight for the fuzzy, contextual knowledge. Use your database for the structured data.
## Recap
- Three HTTP Request nodes give your n8n workflows persistent memory
- **Retain** (`POST /memories`) — store interactions as they happen
- **Recall** (`POST /memories/recall`) — retrieve relevant context before responding
- **Reflect** (`POST /reflect`) — synthesize across all stored memories
- Works with both [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup) and self-hosted
- Use n8n expressions to make it dynamic
## Next steps
- **Add per-customer banks** — use a different `bank_id` per customer for full isolation
- **Use tags for scoped memory** — add `"tags": ["support"]` on retain, filter with `"tags": ["support"]` on recall
- **Wire recall into AI nodes** — inject memory context into system prompts for personalized responses
- **Try the hosted version** — [use Hindsight Cloud](https://ui.hindsight.vectorize.io/signup) instead of self-hosting
- **Explore the MCP server** — Hindsight also exposes an MCP endpoint at `/mcp/{bank_id}/` for tools that support it
- **Read the docs** — see the [full Hindsight API reference](https://docs.hindsight.vectorize.io/recall) for advanced features
Your workflows just got a long-term memory. No code required.

View file

@ -27,6 +27,9 @@ function sdkIcon(sdk: string): string | null {
if (sdk.includes('hermes')) {
return '/img/icons/hermes.png';
}
if (sdk.includes('agno')) {
return '/img/icons/agno.png';
}
if (sdk.includes('hindsight-client') || sdk.includes('hindsight-api') || sdk.includes('litellm') || sdk.includes('pydantic') || sdk.includes('crewai')) {
return '/img/icons/python.svg';
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

@ -0,0 +1,186 @@
# hindsight-agno
Persistent memory tools for Agno agents via Hindsight. Give your agents long-term memory with retain, recall, and reflect — using Agno's native Toolkit pattern.
## Features
- **Native Toolkit** - Extends Agno's `Toolkit` base class, just like `Mem0Tools`
- **Memory Instructions** - Pre-recall memories for injection into `Agent(instructions=[...])`
- **Three Memory Tools** - Retain (store), Recall (search), Reflect (synthesize) — include any combination
- **Flexible Bank Resolution** - Static bank ID, `RunContext.user_id`, or custom resolver
- **Simple Configuration** - Configure once globally, or pass a client directly
## Installation
```bash
pip install hindsight-agno
```
## Quick Start
```python
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from hindsight_agno import HindsightTools
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
)],
)
agent.print_response("Remember that I prefer dark mode")
agent.print_response("What are my preferences?")
```
The agent now has three tools it can call:
- **`retain_memory`** — Store information to long-term memory
- **`recall_memory`** — Search long-term memory for relevant facts
- **`reflect_on_memory`** — Synthesize a reasoned answer from memories
## With Memory Instructions
Pre-recall relevant memories and inject them into the system prompt:
```python
from hindsight_agno import HindsightTools, memory_instructions
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
)],
instructions=[memory_instructions(
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
)],
)
```
## Selecting Tools
Include only the tools you need:
```python
tools = [HindsightTools(
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
enable_retain=True,
enable_recall=True,
enable_reflect=False, # Omit reflect
)]
```
## Bank Resolution
The bank ID is resolved in order:
1. **`bank_resolver`** — Custom callable `(RunContext) -> str`
2. **`bank_id`** — Static bank ID passed to constructor
3. **`run_context.user_id`** — Automatic per-user banks
```python
# Per-user banks from RunContext
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(hindsight_api_url="http://localhost:8888")],
user_id="user-123", # Used as bank_id
)
# Custom resolver
def resolve_bank(ctx):
return f"team-{ctx.user_id}"
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(
bank_resolver=resolve_bank,
hindsight_api_url="http://localhost:8888",
)],
)
```
## Global Configuration
Instead of passing connection details to every toolkit, configure once:
```python
from hindsight_agno import configure, HindsightTools
configure(
hindsight_api_url="http://localhost:8888",
api_key="your-api-key", # Or set HINDSIGHT_API_KEY env var
budget="mid", # Recall budget: low/mid/high
max_tokens=4096, # Max tokens for recall results
tags=["env:prod"], # Tags for stored memories
recall_tags=["scope:global"], # Tags to filter recall
recall_tags_match="any", # Tag match mode: any/all/any_strict/all_strict
)
# Now create toolkit without passing connection details
tools = [HindsightTools(bank_id="user-123")]
```
## Configuration Reference
### `HindsightTools()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | `None` | Static Hindsight memory bank ID |
| `bank_resolver` | `None` | Callable `(RunContext) -> str` for dynamic bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode |
| `enable_retain` | `True` | Include the retain (store) tool |
| `enable_recall` | `True` | Include the recall (search) tool |
| `enable_reflect` | `True` | Include the reflect (synthesize) tool |
### `memory_instructions()`
| Parameter | Default | Description |
|---|---|---|
| `bank_id` | *required* | Hindsight memory bank ID |
| `client` | `None` | Pre-configured Hindsight client |
| `hindsight_api_url` | `None` | API URL (used if no client provided) |
| `api_key` | `None` | API key (used if no client provided) |
| `query` | `"relevant context about the user"` | Recall query for memory injection |
| `budget` | `"low"` | Recall budget level |
| `max_results` | `5` | Maximum memories to inject |
| `max_tokens` | `4096` | Maximum tokens for recall results |
| `prefix` | `"Relevant memories:\n"` | Text prepended before memory list |
| `tags` | `None` | Tags to filter recall results |
| `tags_match` | `"any"` | Tag matching mode |
### `configure()`
| Parameter | Default | Description |
|---|---|---|
| `hindsight_api_url` | Production API | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `budget` | `"mid"` | Default recall budget level |
| `max_tokens` | `4096` | Default max tokens for recall |
| `tags` | `None` | Default tags for retain operations |
| `recall_tags` | `None` | Default tags to filter recall |
| `recall_tags_match` | `"any"` | Default tag matching mode |
| `verbose` | `False` | Enable verbose logging |
## Requirements
- Python >= 3.10
- agno
- hindsight-client >= 0.4.0
- A running Hindsight API server
## License
MIT

View file

@ -0,0 +1,46 @@
"""Hindsight-Agno: Persistent memory tools for AI agents.
Provides a Hindsight-backed Toolkit for Agno agents,
giving them long-term memory via retain, recall, and reflect tools.
Basic usage::
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from hindsight_agno import HindsightTools, memory_instructions
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
)],
instructions=[memory_instructions(
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
)],
)
agent.print_response("What do you remember about my preferences?")
"""
from .config import (
HindsightAgnoConfig,
configure,
get_config,
reset_config,
)
from .errors import HindsightError
from .tools import HindsightTools, memory_instructions
__version__ = "0.1.0"
__all__ = [
"configure",
"get_config",
"reset_config",
"HindsightAgnoConfig",
"HindsightError",
"HindsightTools",
"memory_instructions",
]

View file

@ -0,0 +1,92 @@
"""Global configuration for Hindsight-Agno integration."""
from __future__ import annotations
import os
from dataclasses import dataclass
DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io"
HINDSIGHT_API_KEY_ENV = "HINDSIGHT_API_KEY"
@dataclass
class HindsightAgnoConfig:
"""Connection and default settings for the Agno integration.
Attributes:
hindsight_api_url: URL of the Hindsight API server.
api_key: API key for Hindsight authentication.
budget: Default recall budget level (low/mid/high).
max_tokens: Default maximum tokens for recall results.
tags: Default tags applied when storing memories.
recall_tags: Default tags to filter when searching memories.
recall_tags_match: Tag matching mode (any/all/any_strict/all_strict).
verbose: Enable verbose logging.
"""
hindsight_api_url: str = DEFAULT_HINDSIGHT_API_URL
api_key: str | None = None
budget: str = "mid"
max_tokens: int = 4096
tags: list[str] | None = None
recall_tags: list[str] | None = None
recall_tags_match: str = "any"
verbose: bool = False
_global_config: HindsightAgnoConfig | None = None
def configure(
hindsight_api_url: str | None = None,
api_key: str | None = None,
budget: str = "mid",
max_tokens: int = 4096,
tags: list[str] | None = None,
recall_tags: list[str] | None = None,
recall_tags_match: str = "any",
verbose: bool = False,
) -> HindsightAgnoConfig:
"""Configure Hindsight connection and default settings.
Args:
hindsight_api_url: Hindsight API URL (default: production).
api_key: API key. Falls back to HINDSIGHT_API_KEY env var.
budget: Default recall budget (low/mid/high).
max_tokens: Default max tokens for recall.
tags: Default tags for retain operations.
recall_tags: Default tags to filter recall/search.
recall_tags_match: Tag matching mode.
verbose: Enable verbose logging.
Returns:
The configured HindsightAgnoConfig.
"""
global _global_config
resolved_url = hindsight_api_url or DEFAULT_HINDSIGHT_API_URL
resolved_key = api_key or os.environ.get(HINDSIGHT_API_KEY_ENV)
_global_config = HindsightAgnoConfig(
hindsight_api_url=resolved_url,
api_key=resolved_key,
budget=budget,
max_tokens=max_tokens,
tags=tags,
recall_tags=recall_tags,
recall_tags_match=recall_tags_match,
verbose=verbose,
)
return _global_config
def get_config() -> HindsightAgnoConfig | None:
"""Get the current global configuration."""
return _global_config
def reset_config() -> None:
"""Reset global configuration to None."""
global _global_config
_global_config = None

View file

@ -0,0 +1,7 @@
"""Hindsight-Agno error types."""
class HindsightError(Exception):
"""Exception raised when a Hindsight memory operation fails."""
pass

View file

@ -0,0 +1,345 @@
"""Agno Toolkit for Hindsight memory operations.
Provides a ``Toolkit`` subclass that registers retain/recall/reflect
as agent-callable tools, following the same pattern as Agno's ``Mem0Tools``.
"""
from __future__ import annotations
import logging
from collections.abc import Callable
from typing import Any
from agno.run.base import RunContext
from agno.tools.toolkit import Toolkit
from hindsight_client import Hindsight
from .config import get_config
from .errors import HindsightError
logger = logging.getLogger(__name__)
_TOOL_INSTRUCTIONS = """\
You have access to long-term memory via Hindsight tools.
- Use `retain_memory` to save important facts, user preferences, decisions, \
or any information that should be remembered across conversations.
- Use `recall_memory` to search for previously stored facts, preferences, or context.
- Use `reflect_on_memory` to synthesize a thoughtful, reasoned answer from \
what you know, rather than raw memory facts.
Proactively store information the user shares that may be useful later. \
When answering questions, check memory first for relevant context.\
"""
def _resolve_client(
client: Hindsight | None,
hindsight_api_url: str | None,
api_key: str | None,
) -> Hindsight:
"""Resolve a Hindsight client from explicit args or global config."""
if client is not None:
return client
config = get_config()
url = hindsight_api_url or (config.hindsight_api_url if config else None)
key = api_key or (config.api_key if config else None)
if url is None:
raise HindsightError(
"No Hindsight API URL configured. "
"Pass client= or hindsight_api_url=, or call configure() first."
)
kwargs: dict[str, Any] = {"base_url": url, "timeout": 30.0}
if key:
kwargs["api_key"] = key
return Hindsight(**kwargs)
class HindsightTools(Toolkit):
"""Agno Toolkit providing Hindsight memory tools.
Registers retain, recall, and reflect as agent-callable tools
following the same pattern as Agno's ``Mem0Tools``.
Args:
bank_id: Static memory bank ID.
bank_resolver: Callable that resolves bank_id from RunContext.
client: Pre-configured Hindsight client.
hindsight_api_url: API URL (used if no client provided).
api_key: API key (used if no client provided).
budget: Recall/reflect budget level (low/mid/high).
max_tokens: Maximum tokens for recall results.
tags: Tags applied when storing memories via retain.
recall_tags: Tags to filter when searching memories.
recall_tags_match: Tag matching mode (any/all/any_strict/all_strict).
enable_retain: Include the retain (store) tool.
enable_recall: Include the recall (search) tool.
enable_reflect: Include the reflect (synthesize) tool.
**kwargs: Passed through to Toolkit (e.g. include_tools, exclude_tools).
Example::
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from hindsight_agno import HindsightTools
agent = Agent(
model=OpenAIChat(id="gpt-4o-mini"),
tools=[HindsightTools(
bank_id="user-123",
hindsight_api_url="http://localhost:8888",
)],
)
agent.print_response("Remember that I prefer dark mode")
"""
def __init__(
self,
*,
bank_id: str | None = None,
bank_resolver: Callable[[RunContext], str] | None = None,
client: Hindsight | None = None,
hindsight_api_url: str | None = None,
api_key: str | None = None,
budget: str = "mid",
max_tokens: int = 4096,
tags: list[str] | None = None,
recall_tags: list[str] | None = None,
recall_tags_match: str = "any",
enable_retain: bool = True,
enable_recall: bool = True,
enable_reflect: bool = True,
**kwargs: Any,
):
self._bank_id = bank_id
self._bank_resolver = bank_resolver
self._client = _resolve_client(client, hindsight_api_url, api_key)
self._created_banks: set[str] = set()
# Resolve defaults from global config
config = get_config()
self._budget = budget or (config.budget if config else "mid")
self._max_tokens = max_tokens or (config.max_tokens if config else 4096)
self._tags = tags if tags is not None else (config.tags if config else None)
self._recall_tags = (
recall_tags
if recall_tags is not None
else (config.recall_tags if config else None)
)
self._recall_tags_match = recall_tags_match or (
config.recall_tags_match if config else "any"
)
# Build list of tools to register based on enable flags
tools: list[Callable[..., Any]] = []
if enable_retain:
tools.append(self.retain_memory)
if enable_recall:
tools.append(self.recall_memory)
if enable_reflect:
tools.append(self.reflect_on_memory)
super().__init__(
name="hindsight_tools",
tools=tools,
instructions=_TOOL_INSTRUCTIONS,
**kwargs,
)
def _resolve_bank_id(self, run_context: RunContext) -> str:
"""Resolve the effective bank_id for this operation.
Resolution order:
1. bank_resolver(run_context) if set
2. Static bank_id if set
3. run_context.user_id if available
4. Raise HindsightError
"""
if self._bank_resolver is not None:
return self._bank_resolver(run_context)
if self._bank_id is not None:
return self._bank_id
user_id = getattr(run_context, "user_id", None)
if user_id:
return user_id
raise HindsightError(
"No bank_id available. Provide bank_id=, bank_resolver=, "
"or ensure run_context.user_id is set."
)
def _ensure_bank(self, bank_id: str) -> None:
"""Create bank if not already created in this session."""
if bank_id in self._created_banks:
return
try:
self._client.create_bank(bank_id=bank_id, name=bank_id)
self._created_banks.add(bank_id)
except Exception:
# Bank may already exist — that's fine
self._created_banks.add(bank_id)
def retain_memory(self, run_context: RunContext, content: str) -> str:
"""Store information to long-term memory for later retrieval.
Use this to save important facts, user preferences, decisions,
or any information that should be remembered across conversations.
Args:
run_context: Agno run context.
content: The information to store in memory.
Returns:
A success message string.
"""
try:
bank_id = self._resolve_bank_id(run_context)
self._ensure_bank(bank_id)
retain_kwargs: dict[str, Any] = {"bank_id": bank_id, "content": content}
if self._tags:
retain_kwargs["tags"] = self._tags
self._client.retain(**retain_kwargs)
return "Memory stored successfully."
except HindsightError:
raise
except Exception as e:
logger.error(f"Retain failed: {e}")
raise HindsightError(f"Retain failed: {e}") from e
def recall_memory(self, run_context: RunContext, query: str) -> str:
"""Search long-term memory for relevant information.
Use this to find previously stored facts, preferences, or context.
Returns a numbered list of matching memories.
Args:
run_context: Agno run context.
query: The search query to find relevant memories.
Returns:
A numbered list of matching memories, or a message if none found.
"""
try:
bank_id = self._resolve_bank_id(run_context)
recall_kwargs: dict[str, Any] = {
"bank_id": bank_id,
"query": query,
"budget": self._budget,
"max_tokens": self._max_tokens,
}
if self._recall_tags:
recall_kwargs["tags"] = self._recall_tags
recall_kwargs["tags_match"] = self._recall_tags_match
response = self._client.recall(**recall_kwargs)
if not response.results:
return "No relevant memories found."
lines = []
for i, result in enumerate(response.results, 1):
lines.append(f"{i}. {result.text}")
return "\n".join(lines)
except HindsightError:
raise
except Exception as e:
logger.error(f"Recall failed: {e}")
raise HindsightError(f"Recall failed: {e}") from e
def reflect_on_memory(self, run_context: RunContext, query: str) -> str:
"""Synthesize a thoughtful answer from long-term memories.
Use this when you need a coherent summary or reasoned response
about what you know, rather than raw memory facts.
Args:
run_context: Agno run context.
query: The question to reflect on using stored memories.
Returns:
A synthesized response based on stored memories.
"""
try:
bank_id = self._resolve_bank_id(run_context)
reflect_kwargs: dict[str, Any] = {
"bank_id": bank_id,
"query": query,
"budget": self._budget,
}
response = self._client.reflect(**reflect_kwargs)
return response.text or "No relevant memories found."
except HindsightError:
raise
except Exception as e:
logger.error(f"Reflect failed: {e}")
raise HindsightError(f"Reflect failed: {e}") from e
def memory_instructions(
*,
bank_id: str,
client: Hindsight | None = None,
hindsight_api_url: str | None = None,
api_key: str | None = None,
query: str = "relevant context about the user",
budget: str = "low",
max_results: int = 5,
max_tokens: int = 4096,
prefix: str = "Relevant memories:\n",
tags: list[str] | None = None,
tags_match: str = "any",
) -> str:
"""Pre-recall memories for injection into Agent instructions.
Performs a sync recall at construction time and returns a formatted
string of memories. Use with ``Agent(instructions=[...])`` to inject
relevant context into every run.
Args:
bank_id: The Hindsight memory bank to recall from.
client: Pre-configured Hindsight client (preferred).
hindsight_api_url: API URL (used if no client provided).
api_key: API key (used if no client provided).
query: The recall query to find relevant memories.
budget: Recall budget level (low/mid/high).
max_results: Maximum number of memories to include.
max_tokens: Maximum tokens for recall results.
prefix: Text prepended before the memory list.
tags: Tags to filter recall results.
tags_match: Tag matching mode (any/all/any_strict/all_strict).
Returns:
A formatted string of memories, or empty string if none found.
Raises:
HindsightError: If no client or API URL can be resolved.
"""
resolved_client = _resolve_client(client, hindsight_api_url, api_key)
try:
recall_kwargs: dict[str, Any] = {
"bank_id": bank_id,
"query": query,
"budget": budget,
"max_tokens": max_tokens,
}
if tags:
recall_kwargs["tags"] = tags
recall_kwargs["tags_match"] = tags_match
response = resolved_client.recall(**recall_kwargs)
results = response.results[:max_results] if response.results else []
if not results:
return ""
lines = [prefix]
for i, result in enumerate(results, 1):
lines.append(f"{i}. {result.text}")
return "\n".join(lines)
except Exception:
# Silently return empty — instructions failures shouldn't block the agent
return ""

View file

@ -0,0 +1,52 @@
[project]
name = "hindsight-agno"
version = "0.1.0"
description = "Agno integration for Hindsight - persistent memory tools for AI agents"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [
{ name = "Vectorize", email = "support@vectorize.io" }
]
keywords = [
"ai",
"memory",
"agno",
"agents",
"hindsight",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
]
dependencies = [
"agno",
"hindsight-client>=0.4.0",
]
[project.urls]
Homepage = "https://github.com/vectorize-io/hindsight"
Documentation = "https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/agno"
Repository = "https://github.com/vectorize-io/hindsight"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["hindsight_agno"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[dependency-groups]
dev = [
"pytest>=9.0.2",
"ruff>=0.8.0",
]

View file

@ -0,0 +1,169 @@
"""Unit tests for hindsight_agno configuration."""
import os
from unittest.mock import patch
from hindsight_agno import configure, get_config, reset_config
from hindsight_agno.config import (
DEFAULT_HINDSIGHT_API_URL,
HINDSIGHT_API_KEY_ENV,
HindsightAgnoConfig,
)
class TestDefaults:
def test_default_api_url(self):
assert DEFAULT_HINDSIGHT_API_URL == "https://api.hindsight.vectorize.io"
def test_env_var_name(self):
assert HINDSIGHT_API_KEY_ENV == "HINDSIGHT_API_KEY"
class TestHindsightAgnoConfigDataclass:
def test_default_values(self):
config = HindsightAgnoConfig()
assert config.hindsight_api_url == DEFAULT_HINDSIGHT_API_URL
assert config.api_key is None
assert config.budget == "mid"
assert config.max_tokens == 4096
assert config.tags is None
assert config.recall_tags is None
assert config.recall_tags_match == "any"
assert config.verbose is False
def test_custom_values(self):
config = HindsightAgnoConfig(
hindsight_api_url="http://custom:9999",
api_key="key-123",
budget="high",
max_tokens=2048,
tags=["t1"],
recall_tags=["r1"],
recall_tags_match="all",
verbose=True,
)
assert config.hindsight_api_url == "http://custom:9999"
assert config.api_key == "key-123"
assert config.budget == "high"
assert config.max_tokens == 2048
assert config.tags == ["t1"]
assert config.recall_tags == ["r1"]
assert config.recall_tags_match == "all"
assert config.verbose is True
def test_is_mutable_dataclass(self):
config = HindsightAgnoConfig()
config.budget = "low"
assert config.budget == "low"
class TestConfigure:
def setup_method(self):
reset_config()
def teardown_method(self):
reset_config()
def test_configure_with_no_arguments(self):
config = configure()
assert config.hindsight_api_url == DEFAULT_HINDSIGHT_API_URL
assert config.api_key is None
assert config.budget == "mid"
assert config.max_tokens == 4096
assert config.tags is None
assert config.recall_tags is None
assert config.recall_tags_match == "any"
assert config.verbose is False
def test_configure_reads_api_key_from_env(self):
with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "test-key"}):
config = configure()
assert config.api_key == "test-key"
def test_configure_explicit_overrides_env(self):
with patch.dict(os.environ, {HINDSIGHT_API_KEY_ENV: "env-key"}):
config = configure(api_key="explicit-key")
assert config.api_key == "explicit-key"
def test_configure_api_key_none_without_env(self):
with patch.dict(os.environ, {}, clear=True):
config = configure()
assert config.api_key is None
def test_configure_all_options(self):
config = configure(
hindsight_api_url="http://custom:8888",
api_key="my-key",
budget="high",
max_tokens=2048,
tags=["env:test"],
recall_tags=["scope:global"],
recall_tags_match="all",
verbose=True,
)
assert config.hindsight_api_url == "http://custom:8888"
assert config.api_key == "my-key"
assert config.budget == "high"
assert config.max_tokens == 2048
assert config.tags == ["env:test"]
assert config.recall_tags == ["scope:global"]
assert config.recall_tags_match == "all"
assert config.verbose is True
def test_configure_returns_config_instance(self):
config = configure()
assert isinstance(config, HindsightAgnoConfig)
def test_configure_replaces_previous_config(self):
configure(budget="low")
config1 = get_config()
assert config1.budget == "low"
configure(budget="high")
config2 = get_config()
assert config2.budget == "high"
assert config1 is not config2
def test_configure_url_defaults_when_none(self):
config = configure(hindsight_api_url=None)
assert config.hindsight_api_url == DEFAULT_HINDSIGHT_API_URL
class TestGetConfig:
def setup_method(self):
reset_config()
def teardown_method(self):
reset_config()
def test_returns_none_without_configure(self):
assert get_config() is None
def test_returns_config_after_configure(self):
configure()
config = get_config()
assert config is not None
assert isinstance(config, HindsightAgnoConfig)
def test_returns_same_instance(self):
configure()
assert get_config() is get_config()
class TestResetConfig:
def setup_method(self):
reset_config()
def teardown_method(self):
reset_config()
def test_reset_config(self):
configure()
assert get_config() is not None
reset_config()
assert get_config() is None
def test_reset_is_idempotent(self):
reset_config()
reset_config()
assert get_config() is None

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -65,7 +65,7 @@ fi
print_info "Updating version in all components..."
# Update Python packages
PYTHON_PACKAGES=("hindsight-api" "hindsight-api-slim" "hindsight-all-slim" "hindsight-dev" "hindsight-all" "hindsight-integrations/litellm" "hindsight-integrations/crewai" "hindsight-integrations/pydantic-ai" "hindsight-integrations/hermes" "hindsight-embed")
PYTHON_PACKAGES=("hindsight-api" "hindsight-api-slim" "hindsight-all-slim" "hindsight-dev" "hindsight-all" "hindsight-integrations/litellm" "hindsight-integrations/crewai" "hindsight-integrations/pydantic-ai" "hindsight-integrations/hermes" "hindsight-integrations/agno" "hindsight-embed")
for package in "${PYTHON_PACKAGES[@]}"; do
PYPROJECT_FILE="$package/pyproject.toml"
if [ -f "$PYPROJECT_FILE" ]; then
@ -216,7 +216,7 @@ COMMIT_MSG="Release v$VERSION
- Update version to $VERSION in all components
- Regenerate OpenAPI spec and client SDKs
- Python packages: hindsight-api, hindsight-dev, hindsight-all, hindsight-litellm, hindsight-crewai, hindsight-pydantic-ai, hindsight-hermes, hindsight-embed
- Python packages: hindsight-api, hindsight-dev, hindsight-all, hindsight-litellm, hindsight-crewai, hindsight-pydantic-ai, hindsight-hermes, hindsight-agno, hindsight-embed
- Python client: hindsight-clients/python
- TypeScript client: hindsight-clients/typescript
- Rust CLI: hindsight-cli