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:
parent
e2b19d3b38
commit
8c378b981a
16 changed files with 4175 additions and 282 deletions
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
|
@ -66,6 +66,10 @@ jobs:
|
||||||
working-directory: ./hindsight-integrations/hermes
|
working-directory: ./hindsight-integrations/hermes
|
||||||
run: uv build --out-dir dist
|
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)
|
# Publish in order (client and api-slim first, then api/all wrappers which depend on them)
|
||||||
- name: Publish hindsight-client to PyPI
|
- name: Publish hindsight-client to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
@ -127,6 +131,12 @@ jobs:
|
||||||
packages-dir: ./hindsight-integrations/hermes/dist
|
packages-dir: ./hindsight-integrations/hermes/dist
|
||||||
skip-existing: true
|
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
|
# Upload artifacts for GitHub release
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
|
|
@ -143,6 +153,7 @@ jobs:
|
||||||
hindsight-integrations/crewai/dist/*
|
hindsight-integrations/crewai/dist/*
|
||||||
hindsight-integrations/pydantic-ai/dist/*
|
hindsight-integrations/pydantic-ai/dist/*
|
||||||
hindsight-integrations/hermes/dist/*
|
hindsight-integrations/hermes/dist/*
|
||||||
|
hindsight-integrations/agno/dist/*
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
release-typescript-client:
|
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/litellm/dist/* release-assets/ || true
|
||||||
cp artifacts/python-packages/hindsight-integrations/pydantic-ai/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/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
|
cp artifacts/python-packages/hindsight-embed/dist/* release-assets/ || true
|
||||||
# TypeScript client
|
# TypeScript client
|
||||||
cp artifacts/typescript-client/*.tgz release-assets/ || true
|
cp artifacts/typescript-client/*.tgz release-assets/ || true
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
---
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
@ -27,6 +27,9 @@ function sdkIcon(sdk: string): string | null {
|
||||||
if (sdk.includes('hermes')) {
|
if (sdk.includes('hermes')) {
|
||||||
return '/img/icons/hermes.png';
|
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')) {
|
if (sdk.includes('hindsight-client') || sdk.includes('hindsight-api') || sdk.includes('litellm') || sdk.includes('pydantic') || sdk.includes('crewai')) {
|
||||||
return '/img/icons/python.svg';
|
return '/img/icons/python.svg';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 180 KiB |
BIN
hindsight-docs/static/img/icons/agno.png
Normal file
BIN
hindsight-docs/static/img/icons/agno.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
186
hindsight-integrations/agno/README.md
Normal file
186
hindsight-integrations/agno/README.md
Normal 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
|
||||||
46
hindsight-integrations/agno/hindsight_agno/__init__.py
Normal file
46
hindsight-integrations/agno/hindsight_agno/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
92
hindsight-integrations/agno/hindsight_agno/config.py
Normal file
92
hindsight-integrations/agno/hindsight_agno/config.py
Normal 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
|
||||||
7
hindsight-integrations/agno/hindsight_agno/errors.py
Normal file
7
hindsight-integrations/agno/hindsight_agno/errors.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""Hindsight-Agno error types."""
|
||||||
|
|
||||||
|
|
||||||
|
class HindsightError(Exception):
|
||||||
|
"""Exception raised when a Hindsight memory operation fails."""
|
||||||
|
|
||||||
|
pass
|
||||||
345
hindsight-integrations/agno/hindsight_agno/tools.py
Normal file
345
hindsight-integrations/agno/hindsight_agno/tools.py
Normal 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 ""
|
||||||
52
hindsight-integrations/agno/pyproject.toml
Normal file
52
hindsight-integrations/agno/pyproject.toml
Normal 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",
|
||||||
|
]
|
||||||
0
hindsight-integrations/agno/tests/__init__.py
Normal file
0
hindsight-integrations/agno/tests/__init__.py
Normal file
169
hindsight-integrations/agno/tests/test_config.py
Normal file
169
hindsight-integrations/agno/tests/test_config.py
Normal 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
|
||||||
1836
hindsight-integrations/agno/tests/test_tools.py
Normal file
1836
hindsight-integrations/agno/tests/test_tools.py
Normal file
File diff suppressed because it is too large
Load diff
1425
hindsight-integrations/agno/uv.lock
Normal file
1425
hindsight-integrations/agno/uv.lock
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -65,7 +65,7 @@ fi
|
||||||
print_info "Updating version in all components..."
|
print_info "Updating version in all components..."
|
||||||
|
|
||||||
# Update Python packages
|
# 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
|
for package in "${PYTHON_PACKAGES[@]}"; do
|
||||||
PYPROJECT_FILE="$package/pyproject.toml"
|
PYPROJECT_FILE="$package/pyproject.toml"
|
||||||
if [ -f "$PYPROJECT_FILE" ]; then
|
if [ -f "$PYPROJECT_FILE" ]; then
|
||||||
|
|
@ -216,7 +216,7 @@ COMMIT_MSG="Release v$VERSION
|
||||||
|
|
||||||
- Update version to $VERSION in all components
|
- Update version to $VERSION in all components
|
||||||
- Regenerate OpenAPI spec and client SDKs
|
- 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
|
- Python client: hindsight-clients/python
|
||||||
- TypeScript client: hindsight-clients/typescript
|
- TypeScript client: hindsight-clients/typescript
|
||||||
- Rust CLI: hindsight-cli
|
- Rust CLI: hindsight-cli
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue