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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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')) {
|
||||
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 |
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..."
|
||||
|
||||
# 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue