feat: hindsight-hermes integration for Hermes Agent (#600)
* feat: add hindsight-hermes integration for Hermes Agent * chore: add Hermes docs page, icon, and release process bindings - Add cookbook page for Hermes integration (synced with README) - Add Hermes icon and map hindsight-hermes SDK tag in CookbookGrid - Add cookbook entry to index.mdx - Add hindsight-hermes 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> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f68e2e2851
commit
ef90842f87
16 changed files with 2473 additions and 2 deletions
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
|
@ -62,6 +62,10 @@ jobs:
|
|||
working-directory: ./hindsight-integrations/pydantic-ai
|
||||
run: uv build --out-dir dist
|
||||
|
||||
- name: Build hindsight-hermes
|
||||
working-directory: ./hindsight-integrations/hermes
|
||||
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
|
||||
|
|
@ -117,6 +121,12 @@ jobs:
|
|||
packages-dir: ./hindsight-integrations/pydantic-ai/dist
|
||||
skip-existing: true
|
||||
|
||||
- name: Publish hindsight-hermes to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: ./hindsight-integrations/hermes/dist
|
||||
skip-existing: true
|
||||
|
||||
# Upload artifacts for GitHub release
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
|
|
@ -132,6 +142,7 @@ jobs:
|
|||
hindsight-embed/dist/*
|
||||
hindsight-integrations/crewai/dist/*
|
||||
hindsight-integrations/pydantic-ai/dist/*
|
||||
hindsight-integrations/hermes/dist/*
|
||||
retention-days: 1
|
||||
|
||||
release-typescript-client:
|
||||
|
|
@ -669,6 +680,7 @@ jobs:
|
|||
cp artifacts/python-packages/hindsight-all-slim/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/hermes/dist/* release-assets/ || true
|
||||
cp artifacts/python-packages/hindsight-embed/dist/* release-assets/ || true
|
||||
# TypeScript client
|
||||
cp artifacts/typescript-client/*.tgz release-assets/ || true
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ function sdkIcon(sdk: string): string | null {
|
|||
if (sdk.includes('-go') || sdk === 'go') {
|
||||
return '/img/icons/golang.png';
|
||||
}
|
||||
if (sdk.includes('hermes')) {
|
||||
return '/img/icons/hermes.png';
|
||||
}
|
||||
if (sdk.includes('hindsight-client') || sdk.includes('hindsight-api') || sdk.includes('litellm') || sdk.includes('pydantic') || sdk.includes('crewai')) {
|
||||
return '/img/icons/python.svg';
|
||||
}
|
||||
|
|
|
|||
221
hindsight-docs/src/pages/cookbook/applications/hermes-memory.md
Normal file
221
hindsight-docs/src/pages/cookbook/applications/hermes-memory.md
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
---
|
||||
sidebar_position: 16
|
||||
---
|
||||
|
||||
# Hermes Agent + Hindsight Memory
|
||||
|
||||
:::info Complete Application
|
||||
This is a complete, runnable application demonstrating Hindsight integration.
|
||||
[**View source on GitHub →**](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/hermes)
|
||||
:::
|
||||
|
||||
Give your [Hermes Agent](https://github.com/NousResearch/hermes-agent) persistent long-term memory. The plugin registers retain, recall, and reflect as native Hermes tools via the `hermes_agent.plugins` entry point.
|
||||
|
||||
## What This Demonstrates
|
||||
|
||||
- **Native plugin registration** — tools appear under `[hindsight]` in Hermes's `/tools` list
|
||||
- **Three memory tools** — `hindsight_retain`, `hindsight_recall`, `hindsight_reflect`
|
||||
- **Environment-based configuration** — set `HINDSIGHT_API_URL` and `HINDSIGHT_BANK_ID`, launch Hermes
|
||||
- **Memory instructions** — pre-recall context for system prompt injection
|
||||
- **Graceful degradation** — plugin silently skips if Hindsight is not configured
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Hermes Session:
|
||||
User: "Remember that my favourite colour is red"
|
||||
│
|
||||
├─ Hermes routes to hindsight_retain ──► stores the fact
|
||||
└─ Response shows ⚡ hindsight confirmation
|
||||
|
||||
User: "What's my favourite colour?"
|
||||
│
|
||||
├─ Hermes routes to hindsight_recall ──► searches stored memories
|
||||
└─ Response: "Your favourite colour is red"
|
||||
|
||||
User: "Suggest a colour scheme for my IDE"
|
||||
│
|
||||
├─ Hermes routes to hindsight_reflect ──► synthesizes from memories
|
||||
└─ Response: personalized recommendation based on stored preferences
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Hindsight running**
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-key
|
||||
|
||||
docker run --rm -it --pull always -p 8888:8888 -p 9999:9999 \
|
||||
-e HINDSIGHT_API_LLM_API_KEY=$OPENAI_API_KEY \
|
||||
-e HINDSIGHT_API_LLM_MODEL=o3-mini \
|
||||
-v $HOME/.hindsight-docker:/home/hindsight/.pg0 \
|
||||
ghcr.io/vectorize-io/hindsight:latest
|
||||
```
|
||||
|
||||
2. **Hermes Agent installed**
|
||||
|
||||
Follow the [Hermes Agent setup guide](https://github.com/NousResearch/hermes-agent).
|
||||
|
||||
3. **Install the plugin** into the Hermes venv
|
||||
|
||||
```bash
|
||||
# Activate the same venv Hermes runs in
|
||||
source /path/to/hermes-agent/.venv/bin/activate
|
||||
pip install hindsight-hermes
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Set Environment Variables
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_URL=http://localhost:8888
|
||||
export HINDSIGHT_BANK_ID=my-agent
|
||||
```
|
||||
|
||||
### 2. Disable Hermes's Built-In Memory
|
||||
|
||||
Hermes has its own `memory` tool that saves to local files. Disable it so the LLM uses Hindsight instead:
|
||||
|
||||
```bash
|
||||
hermes tools disable memory
|
||||
```
|
||||
|
||||
### 3. Launch Hermes
|
||||
|
||||
```bash
|
||||
hermes
|
||||
```
|
||||
|
||||
Verify the plugin loaded by typing `/tools`:
|
||||
|
||||
```
|
||||
[hindsight]
|
||||
* hindsight_recall - Search long-term memory for relevant information.
|
||||
* hindsight_reflect - Synthesize a thoughtful answer from long-term memories.
|
||||
* hindsight_retain - Store information to long-term memory for later retrieval.
|
||||
```
|
||||
|
||||
### 4. Test It
|
||||
|
||||
**Store a memory:**
|
||||
> Remember that my favourite colour is red
|
||||
|
||||
**Recall a memory:**
|
||||
> What's my favourite colour?
|
||||
|
||||
**Reflect on memories:**
|
||||
> Based on what you know about me, suggest a colour scheme for my IDE
|
||||
|
||||
## How It Works
|
||||
|
||||
### Plugin Entry Point
|
||||
|
||||
The package registers via `hermes_agent.plugins` entry point in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[project.entry-points."hermes_agent.plugins"]
|
||||
hindsight = "hindsight_hermes"
|
||||
```
|
||||
|
||||
When Hermes starts, it discovers and loads the plugin automatically.
|
||||
|
||||
### Manual Registration
|
||||
|
||||
For more control, register tools directly in a startup script:
|
||||
|
||||
```python
|
||||
from hindsight_hermes import register_tools
|
||||
|
||||
register_tools(
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
budget="mid",
|
||||
tags=["hermes"],
|
||||
recall_tags=["hermes"],
|
||||
)
|
||||
```
|
||||
|
||||
### Memory Instructions
|
||||
|
||||
Pre-recall memories at startup and inject them into the system prompt:
|
||||
|
||||
```python
|
||||
from hindsight_hermes import memory_instructions
|
||||
|
||||
context = memory_instructions(
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
query="user preferences and important context",
|
||||
budget="low",
|
||||
max_results=5,
|
||||
)
|
||||
# Returns:
|
||||
# Relevant memories:
|
||||
# 1. User's favourite colour is red
|
||||
# 2. User prefers dark mode
|
||||
```
|
||||
|
||||
This never raises — if the API is down or no memories exist, it returns an empty string.
|
||||
|
||||
### Global Configuration
|
||||
|
||||
Configure once instead of passing parameters to every call:
|
||||
|
||||
```python
|
||||
from hindsight_hermes import configure
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
api_key="your-key",
|
||||
budget="mid",
|
||||
tags=["hermes"],
|
||||
)
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
| Parameter | Env Var | Default | Description |
|
||||
|-----------|---------|---------|-------------|
|
||||
| `hindsight_api_url` | `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` | Hindsight API URL |
|
||||
| `api_key` | `HINDSIGHT_API_KEY` | — | API key for authentication |
|
||||
| `bank_id` | `HINDSIGHT_BANK_ID` | — | Memory bank ID |
|
||||
| `budget` | `HINDSIGHT_BUDGET` | `mid` | Recall budget (low/mid/high) |
|
||||
| `max_tokens` | — | `4096` | Max tokens for recall results |
|
||||
| `tags` | — | — | Tags applied when storing memories |
|
||||
| `recall_tags` | — | — | Tags to filter recall results |
|
||||
| `recall_tags_match` | — | `any` | Tag matching mode (any/all/any_strict/all_strict) |
|
||||
| `toolset` | — | `hindsight` | Hermes toolset group name |
|
||||
|
||||
## MCP Alternative
|
||||
|
||||
Hermes also supports MCP servers natively. You can use Hindsight's MCP server directly instead of this plugin:
|
||||
|
||||
```yaml
|
||||
# In your Hermes config
|
||||
mcp_servers:
|
||||
- name: hindsight
|
||||
url: http://localhost:8888/mcp
|
||||
```
|
||||
|
||||
The tradeoff is that MCP tools may have different naming and the LLM needs to discover them, whereas the plugin registers tools with Hermes-native schemas.
|
||||
|
||||
## Common Issues
|
||||
|
||||
**Tools don't appear in `/tools`**
|
||||
- Check the plugin is installed in the correct venv: `python -c "from hindsight_hermes import register; print('OK')"`
|
||||
- Check `HINDSIGHT_API_URL` is set — the plugin skips registration silently if unconfigured
|
||||
|
||||
**Hermes uses built-in memory instead of Hindsight**
|
||||
- Run `hermes tools disable memory` and restart
|
||||
|
||||
**Connection refused**
|
||||
- Make sure Hindsight is running: `curl http://localhost:8888/health`
|
||||
|
||||
---
|
||||
|
||||
**Built with:**
|
||||
- [Hermes Agent](https://github.com/NousResearch/hermes-agent) - Open-source AI agent by Nous Research
|
||||
- [hindsight-hermes](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/hermes) - Hindsight memory plugin for Hermes
|
||||
- [Hindsight](https://github.com/vectorize-io/hindsight) - Long-term memory for AI agents
|
||||
|
|
@ -143,6 +143,12 @@ import CookbookGrid from '@site/src/components/CookbookGrid';
|
|||
description: "Delivery agent simulation demonstrating learning through mental models",
|
||||
tags: { sdk: "hindsight-litellm", topic: "Learning" }
|
||||
},
|
||||
{
|
||||
title: "Hermes Agent + Hindsight Memory",
|
||||
href: "/cookbook/applications/hermes-memory",
|
||||
description: "Hermes Agent plugin with persistent long-term memory via Hindsight",
|
||||
tags: { sdk: "hindsight-hermes", topic: "Agents" }
|
||||
},
|
||||
{
|
||||
title: "Go Memory-Augmented API",
|
||||
href: "/cookbook/applications/go-memory-service",
|
||||
|
|
|
|||
BIN
hindsight-docs/static/img/icons/hermes.png
Normal file
BIN
hindsight-docs/static/img/icons/hermes.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
237
hindsight-integrations/hermes/README.md
Normal file
237
hindsight-integrations/hermes/README.md
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
# hindsight-hermes
|
||||
|
||||
Hindsight memory integration for [Hermes Agent](https://github.com/NousResearch/hermes-agent). Gives your Hermes agent persistent long-term memory via retain, recall, and reflect tools.
|
||||
|
||||
## What it does
|
||||
|
||||
This package registers three tools into Hermes via its plugin system:
|
||||
|
||||
- **`hindsight_retain`** — Stores information to long-term memory. Hermes calls this when the user shares facts, preferences, or anything worth remembering.
|
||||
- **`hindsight_recall`** — Searches long-term memory for relevant information. Returns a numbered list of matching memories.
|
||||
- **`hindsight_reflect`** — Synthesizes a thoughtful answer from stored memories. Use this when you want Hermes to reason over what it knows rather than return raw facts.
|
||||
|
||||
These tools appear under the `[hindsight]` toolset in Hermes's `/tools` list.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install hindsight-hermes into the Hermes venv
|
||||
|
||||
The package must be installed in the **same Python environment** that Hermes runs in, so the entry point is discoverable.
|
||||
|
||||
```bash
|
||||
# If you installed Hermes from source:
|
||||
cd /path/to/hermes-agent
|
||||
source .venv/bin/activate
|
||||
pip install hindsight-hermes
|
||||
|
||||
# Or from a local checkout:
|
||||
pip install -e /path/to/hindsight-integrations/hermes
|
||||
```
|
||||
|
||||
### 2. Set environment variables
|
||||
|
||||
The plugin reads its configuration from environment variables. Set these before launching Hermes:
|
||||
|
||||
```bash
|
||||
# Required — tells the plugin where Hindsight is running
|
||||
export HINDSIGHT_API_URL=http://localhost:8888
|
||||
|
||||
# Required — the memory bank to read/write. Think of this as a "brain" for one user or agent.
|
||||
export HINDSIGHT_BANK_ID=my-agent
|
||||
|
||||
# Optional — only needed if using Hindsight Cloud (https://api.hindsight.vectorize.io)
|
||||
export HINDSIGHT_API_KEY=your-api-key
|
||||
|
||||
# Optional — recall budget: low (fast), mid (default), high (thorough)
|
||||
export HINDSIGHT_BUDGET=mid
|
||||
```
|
||||
|
||||
If neither `HINDSIGHT_API_URL` nor `HINDSIGHT_API_KEY` is set, the plugin silently skips registration — Hermes starts normally without the Hindsight tools.
|
||||
|
||||
### 3. Disable Hermes's built-in memory tool
|
||||
|
||||
Hermes has its own `memory` tool that saves to local files (`~/.hermes/`). If both are active, the LLM tends to prefer the built-in one since it's familiar. Disable it so the LLM uses Hindsight instead:
|
||||
|
||||
```bash
|
||||
hermes tools disable memory
|
||||
```
|
||||
|
||||
This persists across sessions. You can re-enable it later with `hermes tools enable memory`.
|
||||
|
||||
### 4. Start Hindsight API
|
||||
|
||||
In a separate terminal, start the Hindsight API server:
|
||||
|
||||
```bash
|
||||
# From the hindsight repo
|
||||
cd /path/to/hindsight-main
|
||||
.venv/bin/hindsight-api
|
||||
|
||||
# Or if using Hindsight Cloud, skip this — just point HINDSIGHT_API_URL
|
||||
# to https://api.hindsight.vectorize.io
|
||||
```
|
||||
|
||||
Wait for the health check to pass:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8888/health
|
||||
# {"status":"healthy","database":"connected"}
|
||||
```
|
||||
|
||||
### 5. Launch Hermes
|
||||
|
||||
```bash
|
||||
hermes
|
||||
```
|
||||
|
||||
Verify the plugin loaded by typing `/tools` — you should see:
|
||||
|
||||
```
|
||||
[hindsight]
|
||||
* hindsight_recall - Search long-term memory for relevant information.
|
||||
* hindsight_reflect - Synthesize a thoughtful answer from long-term memories.
|
||||
* hindsight_retain - Store information to long-term memory for later retrieval.
|
||||
```
|
||||
|
||||
### 6. Test it
|
||||
|
||||
**Store a memory:**
|
||||
> Remember that my favourite colour is red
|
||||
|
||||
You should see `⚡ hindsight` in the response, confirming it called `hindsight_retain`.
|
||||
|
||||
**Recall a memory:**
|
||||
> What's my favourite colour?
|
||||
|
||||
**Reflect on memories:**
|
||||
> Based on what you know about me, suggest a colour scheme for my IDE
|
||||
|
||||
This calls `hindsight_reflect`, which synthesizes a response from all stored memories.
|
||||
|
||||
**Verify via API:**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8888/v1/default/banks/my-agent/memories/recall \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "favourite colour", "budget": "low"}' | python3 -m json.tool
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tools don't appear in `/tools`
|
||||
|
||||
1. **Check the plugin is installed in the right venv.** Run this from the Hermes venv:
|
||||
```bash
|
||||
python -c "from hindsight_hermes import register; print('OK')"
|
||||
```
|
||||
|
||||
2. **Check the entry point is registered:**
|
||||
```bash
|
||||
python -c "
|
||||
import importlib.metadata
|
||||
eps = importlib.metadata.entry_points(group='hermes_agent.plugins')
|
||||
print(list(eps))
|
||||
"
|
||||
```
|
||||
You should see `EntryPoint(name='hindsight', value='hindsight_hermes', group='hermes_agent.plugins')`.
|
||||
|
||||
3. **Check env vars are set.** The plugin skips registration silently if `HINDSIGHT_API_URL` and `HINDSIGHT_API_KEY` are both unset.
|
||||
|
||||
### Hermes uses built-in memory instead of Hindsight
|
||||
|
||||
Run `hermes tools disable memory` and restart. The built-in `memory` tool and Hindsight tools have overlapping purposes — the LLM will prefer whichever it's more familiar with, which is usually the built-in one.
|
||||
|
||||
### Bank not found errors
|
||||
|
||||
The plugin auto-creates banks on first use. If you see bank errors, check that the Hindsight API is running and `HINDSIGHT_API_URL` is correct.
|
||||
|
||||
### Connection refused
|
||||
|
||||
Make sure the Hindsight API is running and listening on the URL you configured. Test with:
|
||||
```bash
|
||||
curl http://localhost:8888/health
|
||||
```
|
||||
|
||||
## Manual registration (advanced)
|
||||
|
||||
If you don't want to use the plugin system, you can register tools directly in a Hermes startup script or custom agent:
|
||||
|
||||
```python
|
||||
from hindsight_hermes import register_tools
|
||||
|
||||
register_tools(
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
budget="mid",
|
||||
tags=["hermes"], # applied to all retained memories
|
||||
recall_tags=["hermes"], # filter recall to only these tags
|
||||
)
|
||||
```
|
||||
|
||||
This imports `tools.registry` from Hermes at call time and registers the three tools directly. This approach gives you more control over parameters but requires Hermes to be importable.
|
||||
|
||||
## Memory instructions (system prompt injection)
|
||||
|
||||
Pre-recall memories at startup and inject them into the system prompt, so the agent starts every conversation with relevant context:
|
||||
|
||||
```python
|
||||
from hindsight_hermes import memory_instructions
|
||||
|
||||
context = memory_instructions(
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
query="user preferences and important context",
|
||||
budget="low",
|
||||
max_results=5,
|
||||
)
|
||||
# Returns:
|
||||
# Relevant memories:
|
||||
# 1. User's favourite colour is red
|
||||
# 2. User prefers dark mode
|
||||
```
|
||||
|
||||
This never raises — if the API is down or no memories exist, it returns an empty string.
|
||||
|
||||
## Global configuration (advanced)
|
||||
|
||||
Instead of passing parameters to every call, configure once:
|
||||
|
||||
```python
|
||||
from hindsight_hermes import configure
|
||||
|
||||
configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
api_key="your-key",
|
||||
budget="mid",
|
||||
tags=["hermes"],
|
||||
)
|
||||
```
|
||||
|
||||
Subsequent calls to `register_tools()` or `memory_instructions()` will use these defaults if no explicit values are provided.
|
||||
|
||||
## MCP alternative
|
||||
|
||||
Hermes also supports MCP servers natively. You can use Hindsight's MCP server directly instead of this plugin — no `hindsight-hermes` package needed:
|
||||
|
||||
```yaml
|
||||
# In your Hermes config
|
||||
mcp_servers:
|
||||
- name: hindsight
|
||||
url: http://localhost:8888/mcp
|
||||
```
|
||||
|
||||
This exposes the same retain/recall/reflect operations through Hermes's MCP integration. The tradeoff is that MCP tools may have different naming and the LLM needs to discover them, whereas the plugin registers tools with Hermes-native schemas.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
| Parameter | Env Var | Default | Description |
|
||||
|-----------|---------|---------|-------------|
|
||||
| `hindsight_api_url` | `HINDSIGHT_API_URL` | `https://api.hindsight.vectorize.io` | Hindsight API URL |
|
||||
| `api_key` | `HINDSIGHT_API_KEY` | — | API key for authentication |
|
||||
| `bank_id` | `HINDSIGHT_BANK_ID` | — | Memory bank ID |
|
||||
| `budget` | `HINDSIGHT_BUDGET` | `mid` | Recall budget (low/mid/high) |
|
||||
| `max_tokens` | — | `4096` | Max tokens for recall results |
|
||||
| `tags` | — | — | Tags applied when storing memories |
|
||||
| `recall_tags` | — | — | Tags to filter recall results |
|
||||
| `recall_tags_match` | — | `any` | Tag matching mode (any/all/any_strict/all_strict) |
|
||||
| `toolset` | — | `hindsight` | Hermes toolset group name |
|
||||
48
hindsight-integrations/hermes/hindsight_hermes/__init__.py
Normal file
48
hindsight-integrations/hermes/hindsight_hermes/__init__.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"""Hindsight-Hermes: Persistent memory tools for Hermes agents.
|
||||
|
||||
Provides Hindsight retain/recall/reflect as native Hermes tools via the
|
||||
plugin system or manual ``register_tools()`` call.
|
||||
|
||||
Plugin usage (auto-discovery)::
|
||||
|
||||
pip install hindsight-hermes
|
||||
export HINDSIGHT_API_URL=http://localhost:8888
|
||||
export HINDSIGHT_BANK_ID=my-agent
|
||||
|
||||
Manual usage::
|
||||
|
||||
from hindsight_hermes import register_tools
|
||||
|
||||
register_tools(
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
)
|
||||
"""
|
||||
|
||||
from .config import (
|
||||
HindsightHermesConfig,
|
||||
configure,
|
||||
get_config,
|
||||
reset_config,
|
||||
)
|
||||
from .errors import HindsightError
|
||||
from .tools import (
|
||||
get_tool_definitions,
|
||||
memory_instructions,
|
||||
register,
|
||||
register_tools,
|
||||
)
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
"configure",
|
||||
"get_config",
|
||||
"reset_config",
|
||||
"HindsightHermesConfig",
|
||||
"HindsightError",
|
||||
"register_tools",
|
||||
"register",
|
||||
"memory_instructions",
|
||||
"get_tool_definitions",
|
||||
]
|
||||
92
hindsight-integrations/hermes/hindsight_hermes/config.py
Normal file
92
hindsight-integrations/hermes/hindsight_hermes/config.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
"""Global configuration for Hindsight-Hermes 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 HindsightHermesConfig:
|
||||
"""Connection and default settings for the Hermes 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: HindsightHermesConfig | 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,
|
||||
) -> HindsightHermesConfig:
|
||||
"""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 HindsightHermesConfig.
|
||||
"""
|
||||
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 = HindsightHermesConfig(
|
||||
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() -> HindsightHermesConfig | 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/hermes/hindsight_hermes/errors.py
Normal file
7
hindsight-integrations/hermes/hindsight_hermes/errors.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""Hindsight-Hermes error types."""
|
||||
|
||||
|
||||
class HindsightError(Exception):
|
||||
"""Exception raised when a Hindsight memory operation fails."""
|
||||
|
||||
pass
|
||||
423
hindsight-integrations/hermes/hindsight_hermes/tools.py
Normal file
423
hindsight-integrations/hermes/hindsight_hermes/tools.py
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
"""Hermes tool definitions and registration for Hindsight memory operations.
|
||||
|
||||
Provides retain/recall/reflect as native Hermes tools via the plugin system
|
||||
or manual ``register_tools()`` call.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
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 `hindsight_retain` to save important facts, user preferences, decisions, \
|
||||
or any information that should be remembered across conversations.
|
||||
- Use `hindsight_recall` to search for previously stored facts, preferences, or context.
|
||||
- Use `hindsight_reflect` 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.\
|
||||
"""
|
||||
|
||||
RETAIN_SCHEMA = {
|
||||
"name": "hindsight_retain",
|
||||
"description": (
|
||||
"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."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "The information to store in memory.",
|
||||
},
|
||||
},
|
||||
"required": ["content"],
|
||||
},
|
||||
}
|
||||
|
||||
RECALL_SCHEMA = {
|
||||
"name": "hindsight_recall",
|
||||
"description": (
|
||||
"Search long-term memory for relevant information. "
|
||||
"Use this to find previously stored facts, preferences, or context."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query to find relevant memories.",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
}
|
||||
|
||||
REFLECT_SCHEMA = {
|
||||
"name": "hindsight_reflect",
|
||||
"description": (
|
||||
"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."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The question to reflect on using stored memories.",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def _resolve_bank_id(
|
||||
args: dict[str, Any],
|
||||
bank_id: str | None,
|
||||
bank_resolver: Callable[[dict[str, Any]], str] | None,
|
||||
) -> str:
|
||||
"""Resolve the effective bank_id for an operation.
|
||||
|
||||
Resolution order:
|
||||
1. bank_resolver(args) if set
|
||||
2. Static bank_id if set
|
||||
3. HINDSIGHT_BANK_ID env var
|
||||
4. Raise HindsightError
|
||||
"""
|
||||
if bank_resolver is not None:
|
||||
return bank_resolver(args)
|
||||
|
||||
if bank_id is not None:
|
||||
return bank_id
|
||||
|
||||
env_bank = os.environ.get("HINDSIGHT_BANK_ID")
|
||||
if env_bank:
|
||||
return env_bank
|
||||
|
||||
raise HindsightError(
|
||||
"No bank_id available. Provide bank_id=, bank_resolver=, "
|
||||
"or set the HINDSIGHT_BANK_ID environment variable."
|
||||
)
|
||||
|
||||
|
||||
def register_tools(
|
||||
*,
|
||||
bank_id: str | None = None,
|
||||
bank_resolver: Callable[[dict[str, Any]], 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",
|
||||
toolset: str = "hindsight",
|
||||
) -> None:
|
||||
"""Register Hindsight memory tools into the Hermes tool registry.
|
||||
|
||||
This imports ``tools.registry`` lazily so that hermes-agent is not a
|
||||
hard dependency of the package.
|
||||
|
||||
Args:
|
||||
bank_id: Static memory bank ID.
|
||||
bank_resolver: Callable that resolves bank_id from tool args dict.
|
||||
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).
|
||||
toolset: Hermes toolset name for grouping.
|
||||
"""
|
||||
from tools.registry import registry # type: ignore[import-untyped]
|
||||
|
||||
resolved_client = _resolve_client(client, hindsight_api_url, api_key)
|
||||
created_banks: set[str] = set()
|
||||
|
||||
def _ensure_bank(bid: str) -> None:
|
||||
if bid in created_banks:
|
||||
return
|
||||
try:
|
||||
resolved_client.create_bank(bank_id=bid, name=bid)
|
||||
created_banks.add(bid)
|
||||
except Exception:
|
||||
created_banks.add(bid)
|
||||
|
||||
def handle_retain(args: dict[str, Any], **kwargs: Any) -> str:
|
||||
try:
|
||||
bid = _resolve_bank_id(args, bank_id, bank_resolver)
|
||||
_ensure_bank(bid)
|
||||
retain_kwargs: dict[str, Any] = {"bank_id": bid, "content": args["content"]}
|
||||
if tags:
|
||||
retain_kwargs["tags"] = tags
|
||||
resolved_client.retain(**retain_kwargs)
|
||||
return json.dumps({"result": "Memory stored successfully."})
|
||||
except Exception as e:
|
||||
logger.error(f"Retain failed: {e}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
def handle_recall(args: dict[str, Any], **kwargs: Any) -> str:
|
||||
try:
|
||||
bid = _resolve_bank_id(args, bank_id, bank_resolver)
|
||||
recall_kwargs: dict[str, Any] = {
|
||||
"bank_id": bid,
|
||||
"query": args["query"],
|
||||
"budget": budget,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
if recall_tags:
|
||||
recall_kwargs["tags"] = recall_tags
|
||||
recall_kwargs["tags_match"] = recall_tags_match
|
||||
response = resolved_client.recall(**recall_kwargs)
|
||||
if not response.results:
|
||||
return json.dumps({"result": "No relevant memories found."})
|
||||
lines = []
|
||||
for i, result in enumerate(response.results, 1):
|
||||
lines.append(f"{i}. {result.text}")
|
||||
return json.dumps({"result": "\n".join(lines)})
|
||||
except Exception as e:
|
||||
logger.error(f"Recall failed: {e}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
def handle_reflect(args: dict[str, Any], **kwargs: Any) -> str:
|
||||
try:
|
||||
bid = _resolve_bank_id(args, bank_id, bank_resolver)
|
||||
reflect_kwargs: dict[str, Any] = {
|
||||
"bank_id": bid,
|
||||
"query": args["query"],
|
||||
"budget": budget,
|
||||
}
|
||||
response = resolved_client.reflect(**reflect_kwargs)
|
||||
return json.dumps(
|
||||
{"result": response.text or "No relevant memories found."}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Reflect failed: {e}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
registry.register(
|
||||
name="hindsight_retain",
|
||||
toolset=toolset,
|
||||
schema=RETAIN_SCHEMA,
|
||||
handler=handle_retain,
|
||||
)
|
||||
registry.register(
|
||||
name="hindsight_recall",
|
||||
toolset=toolset,
|
||||
schema=RECALL_SCHEMA,
|
||||
handler=handle_recall,
|
||||
)
|
||||
registry.register(
|
||||
name="hindsight_reflect",
|
||||
toolset=toolset,
|
||||
schema=REFLECT_SCHEMA,
|
||||
handler=handle_reflect,
|
||||
)
|
||||
|
||||
|
||||
def register(ctx: Any) -> None:
|
||||
"""Hermes plugin entry point — called via ``hermes_agent.plugins`` entry point.
|
||||
|
||||
Reads configuration from environment variables and registers tools
|
||||
using ``ctx.register_tool()``.
|
||||
|
||||
Args:
|
||||
ctx: Hermes PluginContext.
|
||||
"""
|
||||
hindsight_api_url = os.environ.get("HINDSIGHT_API_URL")
|
||||
api_key = os.environ.get("HINDSIGHT_API_KEY")
|
||||
bank_id = os.environ.get("HINDSIGHT_BANK_ID")
|
||||
budget = os.environ.get("HINDSIGHT_BUDGET", "mid")
|
||||
|
||||
if not hindsight_api_url and not api_key:
|
||||
logger.debug(
|
||||
"Hindsight plugin: no API URL or key configured, skipping registration"
|
||||
)
|
||||
return
|
||||
|
||||
resolved_client = _resolve_client(None, hindsight_api_url, api_key)
|
||||
created_banks: set[str] = set()
|
||||
|
||||
def _ensure_bank(bid: str) -> None:
|
||||
if bid in created_banks:
|
||||
return
|
||||
try:
|
||||
resolved_client.create_bank(bank_id=bid, name=bid)
|
||||
created_banks.add(bid)
|
||||
except Exception:
|
||||
created_banks.add(bid)
|
||||
|
||||
def handle_retain(args: dict[str, Any], **kwargs: Any) -> str:
|
||||
try:
|
||||
bid = _resolve_bank_id(args, bank_id, None)
|
||||
_ensure_bank(bid)
|
||||
resolved_client.retain(bank_id=bid, content=args["content"])
|
||||
return json.dumps({"result": "Memory stored successfully."})
|
||||
except Exception as e:
|
||||
logger.error(f"Retain failed: {e}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
def handle_recall(args: dict[str, Any], **kwargs: Any) -> str:
|
||||
try:
|
||||
bid = _resolve_bank_id(args, bank_id, None)
|
||||
response = resolved_client.recall(
|
||||
bank_id=bid, query=args["query"], budget=budget
|
||||
)
|
||||
if not response.results:
|
||||
return json.dumps({"result": "No relevant memories found."})
|
||||
lines = [f"{i}. {r.text}" for i, r in enumerate(response.results, 1)]
|
||||
return json.dumps({"result": "\n".join(lines)})
|
||||
except Exception as e:
|
||||
logger.error(f"Recall failed: {e}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
def handle_reflect(args: dict[str, Any], **kwargs: Any) -> str:
|
||||
try:
|
||||
bid = _resolve_bank_id(args, bank_id, None)
|
||||
response = resolved_client.reflect(
|
||||
bank_id=bid, query=args["query"], budget=budget
|
||||
)
|
||||
return json.dumps(
|
||||
{"result": response.text or "No relevant memories found."}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Reflect failed: {e}")
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
ctx.register_tool(
|
||||
name="hindsight_retain",
|
||||
toolset="hindsight",
|
||||
schema=RETAIN_SCHEMA,
|
||||
handler=handle_retain,
|
||||
)
|
||||
ctx.register_tool(
|
||||
name="hindsight_recall",
|
||||
toolset="hindsight",
|
||||
schema=RECALL_SCHEMA,
|
||||
handler=handle_recall,
|
||||
)
|
||||
ctx.register_tool(
|
||||
name="hindsight_reflect",
|
||||
toolset="hindsight",
|
||||
schema=REFLECT_SCHEMA,
|
||||
handler=handle_reflect,
|
||||
)
|
||||
|
||||
|
||||
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 system prompt.
|
||||
|
||||
Performs a sync recall and returns a formatted string of memories.
|
||||
Silently returns empty string on failure so it never blocks the agent.
|
||||
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
resolved_client = _resolve_client(client, hindsight_api_url, api_key)
|
||||
except HindsightError:
|
||||
return ""
|
||||
|
||||
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:
|
||||
return ""
|
||||
|
||||
|
||||
def get_tool_definitions() -> list[dict[str, Any]]:
|
||||
"""Return tool schema dicts without registering them.
|
||||
|
||||
Useful for inspection or manual integration without importing Hermes.
|
||||
|
||||
Returns:
|
||||
List of OpenAI function-calling format schema dicts.
|
||||
"""
|
||||
return [RETAIN_SCHEMA, RECALL_SCHEMA, REFLECT_SCHEMA]
|
||||
58
hindsight-integrations/hermes/pyproject.toml
Normal file
58
hindsight-integrations/hermes/pyproject.toml
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
[project]
|
||||
name = "hindsight-hermes"
|
||||
version = "0.1.0"
|
||||
description = "Hermes agent 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",
|
||||
"hermes",
|
||||
"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 = [
|
||||
"hindsight-client>=0.4.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
]
|
||||
|
||||
[project.entry-points."hermes_agent.plugins"]
|
||||
hindsight = "hindsight_hermes"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/vectorize-io/hindsight"
|
||||
Documentation = "https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/hermes"
|
||||
Repository = "https://github.com/vectorize-io/hindsight"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_hermes"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=9.0.2",
|
||||
]
|
||||
0
hindsight-integrations/hermes/tests/__init__.py
Normal file
0
hindsight-integrations/hermes/tests/__init__.py
Normal file
57
hindsight-integrations/hermes/tests/test_config.py
Normal file
57
hindsight-integrations/hermes/tests/test_config.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""Tests for hindsight_hermes.config module."""
|
||||
|
||||
from hindsight_hermes.config import (
|
||||
HindsightHermesConfig,
|
||||
configure,
|
||||
get_config,
|
||||
reset_config,
|
||||
)
|
||||
|
||||
|
||||
class TestConfigure:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
|
||||
def test_configure_returns_config(self):
|
||||
cfg = configure(hindsight_api_url="http://localhost:8888", api_key="test-key")
|
||||
assert isinstance(cfg, HindsightHermesConfig)
|
||||
assert cfg.hindsight_api_url == "http://localhost:8888"
|
||||
assert cfg.api_key == "test-key"
|
||||
|
||||
def test_configure_defaults(self):
|
||||
cfg = configure()
|
||||
assert cfg.hindsight_api_url == "https://api.hindsight.vectorize.io"
|
||||
assert cfg.api_key is None
|
||||
assert cfg.budget == "mid"
|
||||
assert cfg.max_tokens == 4096
|
||||
assert cfg.tags is None
|
||||
assert cfg.recall_tags is None
|
||||
assert cfg.recall_tags_match == "any"
|
||||
assert cfg.verbose is False
|
||||
|
||||
def test_get_config_returns_none_before_configure(self):
|
||||
assert get_config() is None
|
||||
|
||||
def test_get_config_returns_configured(self):
|
||||
configure(api_key="k")
|
||||
cfg = get_config()
|
||||
assert cfg is not None
|
||||
assert cfg.api_key == "k"
|
||||
|
||||
def test_reset_config(self):
|
||||
configure(api_key="k")
|
||||
reset_config()
|
||||
assert get_config() is None
|
||||
|
||||
def test_configure_reads_env_var(self, monkeypatch):
|
||||
monkeypatch.setenv("HINDSIGHT_API_KEY", "env-key")
|
||||
cfg = configure()
|
||||
assert cfg.api_key == "env-key"
|
||||
|
||||
def test_explicit_key_overrides_env(self, monkeypatch):
|
||||
monkeypatch.setenv("HINDSIGHT_API_KEY", "env-key")
|
||||
cfg = configure(api_key="explicit-key")
|
||||
assert cfg.api_key == "explicit-key"
|
||||
243
hindsight-integrations/hermes/tests/test_tools.py
Normal file
243
hindsight-integrations/hermes/tests/test_tools.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
"""Tests for hindsight_hermes.tools module."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_hermes.config import configure, reset_config
|
||||
from hindsight_hermes.errors import HindsightError
|
||||
from hindsight_hermes.tools import (
|
||||
RECALL_SCHEMA,
|
||||
REFLECT_SCHEMA,
|
||||
RETAIN_SCHEMA,
|
||||
_resolve_bank_id,
|
||||
_resolve_client,
|
||||
get_tool_definitions,
|
||||
memory_instructions,
|
||||
register,
|
||||
register_tools,
|
||||
)
|
||||
|
||||
|
||||
# --- Fixtures ---
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_config():
|
||||
reset_config()
|
||||
yield
|
||||
reset_config()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_client():
|
||||
client = MagicMock()
|
||||
client.create_bank = MagicMock()
|
||||
client.retain = MagicMock()
|
||||
client.recall = MagicMock(
|
||||
return_value=SimpleNamespace(
|
||||
results=[
|
||||
SimpleNamespace(text="Memory 1"),
|
||||
SimpleNamespace(text="Memory 2"),
|
||||
]
|
||||
)
|
||||
)
|
||||
client.reflect = MagicMock(return_value=SimpleNamespace(text="Synthesized answer"))
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_registry():
|
||||
"""Patch tools.registry.registry so register_tools() can import it."""
|
||||
mock_reg = MagicMock()
|
||||
mock_module = MagicMock()
|
||||
mock_module.registry = mock_reg
|
||||
with patch.dict(sys.modules, {"tools": MagicMock(), "tools.registry": mock_module}):
|
||||
yield mock_reg
|
||||
|
||||
|
||||
# --- Schema tests ---
|
||||
|
||||
|
||||
class TestSchemas:
|
||||
def test_retain_schema_has_content(self):
|
||||
assert RETAIN_SCHEMA["name"] == "hindsight_retain"
|
||||
assert "content" in RETAIN_SCHEMA["parameters"]["properties"]
|
||||
assert "content" in RETAIN_SCHEMA["parameters"]["required"]
|
||||
|
||||
def test_recall_schema_has_query(self):
|
||||
assert RECALL_SCHEMA["name"] == "hindsight_recall"
|
||||
assert "query" in RECALL_SCHEMA["parameters"]["properties"]
|
||||
assert "query" in RECALL_SCHEMA["parameters"]["required"]
|
||||
|
||||
def test_reflect_schema_has_query(self):
|
||||
assert REFLECT_SCHEMA["name"] == "hindsight_reflect"
|
||||
assert "query" in REFLECT_SCHEMA["parameters"]["properties"]
|
||||
|
||||
def test_get_tool_definitions(self):
|
||||
defs = get_tool_definitions()
|
||||
assert len(defs) == 3
|
||||
names = {d["name"] for d in defs}
|
||||
assert names == {"hindsight_retain", "hindsight_recall", "hindsight_reflect"}
|
||||
|
||||
|
||||
# --- Bank resolution tests ---
|
||||
|
||||
|
||||
class TestResolveBankId:
|
||||
def test_bank_resolver_takes_priority(self):
|
||||
resolver = lambda args: "resolved-bank"
|
||||
assert _resolve_bank_id({}, "static-bank", resolver) == "resolved-bank"
|
||||
|
||||
def test_static_bank_id(self):
|
||||
assert _resolve_bank_id({}, "static-bank", None) == "static-bank"
|
||||
|
||||
def test_env_var_fallback(self, monkeypatch):
|
||||
monkeypatch.setenv("HINDSIGHT_BANK_ID", "env-bank")
|
||||
assert _resolve_bank_id({}, None, None) == "env-bank"
|
||||
|
||||
def test_raises_when_no_bank(self, monkeypatch):
|
||||
monkeypatch.delenv("HINDSIGHT_BANK_ID", raising=False)
|
||||
with pytest.raises(HindsightError, match="No bank_id available"):
|
||||
_resolve_bank_id({}, None, None)
|
||||
|
||||
|
||||
# --- Client resolution tests ---
|
||||
|
||||
|
||||
class TestResolveClient:
|
||||
def test_returns_provided_client(self, mock_client):
|
||||
assert _resolve_client(mock_client, None, None) is mock_client
|
||||
|
||||
def test_uses_global_config(self):
|
||||
configure(hindsight_api_url="http://localhost:9999", api_key="key")
|
||||
with patch("hindsight_hermes.tools.Hindsight") as MockH:
|
||||
_resolve_client(None, None, None)
|
||||
MockH.assert_called_once_with(base_url="http://localhost:9999", timeout=30.0, api_key="key")
|
||||
|
||||
def test_raises_without_url(self):
|
||||
with pytest.raises(HindsightError, match="No Hindsight API URL"):
|
||||
_resolve_client(None, None, None)
|
||||
|
||||
|
||||
# --- register_tools tests ---
|
||||
|
||||
|
||||
class TestRegisterTools:
|
||||
def test_registers_three_tools(self, mock_client, mock_registry):
|
||||
register_tools(bank_id="b", client=mock_client)
|
||||
assert mock_registry.register.call_count == 3
|
||||
names = {call.kwargs["name"] for call in mock_registry.register.call_args_list}
|
||||
assert names == {"hindsight_retain", "hindsight_recall", "hindsight_reflect"}
|
||||
|
||||
def test_retain_handler_success(self, mock_client, mock_registry):
|
||||
register_tools(bank_id="b", client=mock_client)
|
||||
handler = mock_registry.register.call_args_list[0].kwargs["handler"]
|
||||
result = json.loads(handler({"content": "hello"}))
|
||||
assert result["result"] == "Memory stored successfully."
|
||||
mock_client.retain.assert_called_once_with(bank_id="b", content="hello")
|
||||
|
||||
def test_retain_handler_with_tags(self, mock_client, mock_registry):
|
||||
register_tools(bank_id="b", client=mock_client, tags=["tag1"])
|
||||
handler = mock_registry.register.call_args_list[0].kwargs["handler"]
|
||||
handler({"content": "hello"})
|
||||
mock_client.retain.assert_called_once_with(bank_id="b", content="hello", tags=["tag1"])
|
||||
|
||||
def test_recall_handler_success(self, mock_client, mock_registry):
|
||||
register_tools(bank_id="b", client=mock_client)
|
||||
handler = mock_registry.register.call_args_list[1].kwargs["handler"]
|
||||
result = json.loads(handler({"query": "test"}))
|
||||
assert "Memory 1" in result["result"]
|
||||
assert "Memory 2" in result["result"]
|
||||
|
||||
def test_recall_handler_no_results(self, mock_client, mock_registry):
|
||||
mock_client.recall.return_value = SimpleNamespace(results=[])
|
||||
register_tools(bank_id="b", client=mock_client)
|
||||
handler = mock_registry.register.call_args_list[1].kwargs["handler"]
|
||||
result = json.loads(handler({"query": "test"}))
|
||||
assert result["result"] == "No relevant memories found."
|
||||
|
||||
def test_reflect_handler_success(self, mock_client, mock_registry):
|
||||
register_tools(bank_id="b", client=mock_client)
|
||||
handler = mock_registry.register.call_args_list[2].kwargs["handler"]
|
||||
result = json.loads(handler({"query": "test"}))
|
||||
assert result["result"] == "Synthesized answer"
|
||||
|
||||
def test_handler_returns_error_on_exception(self, mock_client, mock_registry):
|
||||
mock_client.retain.side_effect = RuntimeError("boom")
|
||||
register_tools(bank_id="b", client=mock_client)
|
||||
handler = mock_registry.register.call_args_list[0].kwargs["handler"]
|
||||
result = json.loads(handler({"content": "hello"}))
|
||||
assert "error" in result
|
||||
assert "boom" in result["error"]
|
||||
|
||||
def test_ensure_bank_called(self, mock_client, mock_registry):
|
||||
register_tools(bank_id="b", client=mock_client)
|
||||
handler = mock_registry.register.call_args_list[0].kwargs["handler"]
|
||||
handler({"content": "hello"})
|
||||
mock_client.create_bank.assert_called_once_with(bank_id="b", name="b")
|
||||
|
||||
def test_ensure_bank_idempotent(self, mock_client, mock_registry):
|
||||
register_tools(bank_id="b", client=mock_client)
|
||||
handler = mock_registry.register.call_args_list[0].kwargs["handler"]
|
||||
handler({"content": "first"})
|
||||
handler({"content": "second"})
|
||||
# create_bank should only be called once
|
||||
mock_client.create_bank.assert_called_once()
|
||||
|
||||
|
||||
# --- register (plugin entry point) tests ---
|
||||
|
||||
|
||||
class TestRegisterPlugin:
|
||||
def test_register_calls_ctx_register_tool(self, monkeypatch, mock_client):
|
||||
monkeypatch.setenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
monkeypatch.setenv("HINDSIGHT_BANK_ID", "test-bank")
|
||||
ctx = MagicMock()
|
||||
with patch("hindsight_hermes.tools._resolve_client", return_value=mock_client):
|
||||
register(ctx)
|
||||
assert ctx.register_tool.call_count == 3
|
||||
|
||||
def test_register_skips_without_config(self, monkeypatch):
|
||||
monkeypatch.delenv("HINDSIGHT_API_URL", raising=False)
|
||||
monkeypatch.delenv("HINDSIGHT_API_KEY", raising=False)
|
||||
ctx = MagicMock()
|
||||
register(ctx)
|
||||
ctx.register_tool.assert_not_called()
|
||||
|
||||
|
||||
# --- memory_instructions tests ---
|
||||
|
||||
|
||||
class TestMemoryInstructions:
|
||||
def test_returns_formatted_memories(self, mock_client):
|
||||
result = memory_instructions(bank_id="b", client=mock_client)
|
||||
assert "Relevant memories:" in result
|
||||
assert "1. Memory 1" in result
|
||||
assert "2. Memory 2" in result
|
||||
|
||||
def test_returns_empty_on_no_results(self, mock_client):
|
||||
mock_client.recall.return_value = SimpleNamespace(results=[])
|
||||
result = memory_instructions(bank_id="b", client=mock_client)
|
||||
assert result == ""
|
||||
|
||||
def test_returns_empty_on_exception(self, mock_client):
|
||||
mock_client.recall.side_effect = RuntimeError("fail")
|
||||
result = memory_instructions(bank_id="b", client=mock_client)
|
||||
assert result == ""
|
||||
|
||||
def test_returns_empty_on_no_client(self):
|
||||
result = memory_instructions(bank_id="b")
|
||||
assert result == ""
|
||||
|
||||
def test_respects_max_results(self, mock_client):
|
||||
result = memory_instructions(bank_id="b", client=mock_client, max_results=1)
|
||||
assert "1. Memory 1" in result
|
||||
assert "Memory 2" not in result
|
||||
|
||||
def test_custom_prefix(self, mock_client):
|
||||
result = memory_instructions(bank_id="b", client=mock_client, prefix="Context:\n")
|
||||
assert result.startswith("Context:")
|
||||
1064
hindsight-integrations/hermes/uv.lock
Normal file
1064
hindsight-integrations/hermes/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-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-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-embed
|
||||
- Python packages: hindsight-api, hindsight-dev, hindsight-all, hindsight-litellm, hindsight-crewai, hindsight-pydantic-ai, hindsight-hermes, hindsight-embed
|
||||
- Python client: hindsight-clients/python
|
||||
- TypeScript client: hindsight-clients/typescript
|
||||
- Rust CLI: hindsight-cli
|
||||
|
|
|
|||
Loading…
Reference in a new issue