feat(hermes): file-based config + updated docs (#686)
* feat(hermes): file-based config + updated docs Replace the old dataclass/configure() singleton with a plain dict config loaded from ~/.hindsight/hermes.json — same field names and conventions as the openclaw and claude-code integrations. Loading order: defaults → config file → env var overrides. - config.py: rewritten with load_config() returning a plain dict, DEFAULTS matching openclaw/claude-code fields, ENV_OVERRIDES with typed casting - tools.py: register() uses load_config() instead of raw env vars - __init__.py: clean exports (removed configure/get_config/reset_config) - README.md: full rewrite with config file examples, tables by category - docs/hermes.md: full rewrite with quick start, architecture, all config tables, gateway section, troubleshooting - tests: updated for new config pattern, 46 tests pass * ci: add test job for hermes integration * chore: regenerate docs skill for hermes integration
This commit is contained in:
parent
d344ef26da
commit
0ff36548e0
9 changed files with 700 additions and 748 deletions
37
.github/workflows/test.yml
vendored
37
.github/workflows/test.yml
vendored
|
|
@ -35,6 +35,7 @@ jobs:
|
|||
integrations-crewai: ${{ steps.filter.outputs.integrations-crewai }}
|
||||
integrations-litellm: ${{ steps.filter.outputs.integrations-litellm }}
|
||||
integrations-pydantic-ai: ${{ steps.filter.outputs.integrations-pydantic-ai }}
|
||||
integrations-hermes: ${{ steps.filter.outputs.integrations-hermes }}
|
||||
dev: ${{ steps.filter.outputs.dev }}
|
||||
ci: ${{ steps.filter.outputs.ci }}
|
||||
steps:
|
||||
|
|
@ -93,6 +94,8 @@ jobs:
|
|||
- 'hindsight-integrations/litellm/**'
|
||||
integrations-pydantic-ai:
|
||||
- 'hindsight-integrations/pydantic-ai/**'
|
||||
integrations-hermes:
|
||||
- 'hindsight-integrations/hermes/**'
|
||||
dev:
|
||||
- 'hindsight-dev/**'
|
||||
ci:
|
||||
|
|
@ -1586,6 +1589,40 @@ jobs:
|
|||
working-directory: ./hindsight-integrations/pydantic-ai
|
||||
run: uv run pytest tests -v
|
||||
|
||||
test-hermes-integration:
|
||||
needs: [detect-changes]
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
needs.detect-changes.outputs.integrations-hermes == 'true' ||
|
||||
needs.detect-changes.outputs.ci == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Build hermes integration
|
||||
working-directory: ./hindsight-integrations/hermes
|
||||
run: uv build
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./hindsight-integrations/hermes
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./hindsight-integrations/hermes
|
||||
run: uv run pytest tests -v
|
||||
|
||||
test-pip-slim:
|
||||
needs: [detect-changes]
|
||||
if: >-
|
||||
|
|
|
|||
|
|
@ -4,216 +4,172 @@ sidebar_position: 10
|
|||
|
||||
# Hermes Agent
|
||||
|
||||
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.
|
||||
Persistent long-term memory for [Hermes Agent](https://github.com/NousResearch/hermes-agent) using [Hindsight](https://vectorize.io/hindsight). Automatically recalls relevant context before every LLM call and retains conversations for future sessions — plus explicit retain/recall/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.
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Install the plugin into Hermes's Python environment
|
||||
uv pip install hindsight-hermes --python $HOME/.hermes/hermes-agent/venv/bin/python
|
||||
|
||||
# 2. Configure (choose one)
|
||||
# Option A: Config file (recommended)
|
||||
mkdir -p ~/.hindsight
|
||||
cat > ~/.hindsight/hermes.json << 'EOF'
|
||||
{
|
||||
"hindsightApiUrl": "http://localhost:9077",
|
||||
"bankId": "hermes"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Option B: Environment variables
|
||||
export HINDSIGHT_API_URL=http://localhost:9077
|
||||
export HINDSIGHT_BANK_ID=hermes
|
||||
|
||||
# 3. Start Hermes — the plugin activates automatically
|
||||
hermes
|
||||
```
|
||||
|
||||
### 2. Set environment variables
|
||||
## Features
|
||||
|
||||
The plugin reads its configuration from environment variables. Set these before launching Hermes:
|
||||
- **Auto-recall** — on every turn, queries Hindsight for relevant memories and injects them into the system prompt (via `pre_llm_call` hook)
|
||||
- **Auto-retain** — after every response, retains the user/assistant exchange to Hindsight (via `post_llm_call` hook)
|
||||
- **Explicit tools** — `hindsight_retain`, `hindsight_recall`, `hindsight_reflect` for direct model control
|
||||
- **Config file** — `~/.hindsight/hermes.json` with the same field names as openclaw and claude-code integrations
|
||||
- **Zero config overhead** — env vars still work as overrides for CI/automation
|
||||
|
||||
```bash
|
||||
# Required — tells the plugin where Hindsight is running
|
||||
export HINDSIGHT_API_URL=http://localhost:8888
|
||||
:::note
|
||||
The lifecycle hooks (`pre_llm_call`/`post_llm_call`) require hermes-agent with [PR #2823](https://github.com/NousResearch/hermes-agent/pull/2823) or later. On older versions, only the three tools are registered — hooks are silently skipped.
|
||||
:::
|
||||
|
||||
# Required — the memory bank to read/write. Think of this as a "brain" for one user or agent.
|
||||
export HINDSIGHT_BANK_ID=my-agent
|
||||
## Architecture
|
||||
|
||||
# Optional — only needed if using Hindsight Cloud (https://api.hindsight.vectorize.io)
|
||||
export HINDSIGHT_API_KEY=your-api-key
|
||||
The plugin registers via Hermes's `hermes_agent.plugins` entry point system:
|
||||
|
||||
# Optional — recall budget: low (fast), mid (default), high (thorough)
|
||||
export HINDSIGHT_BUDGET=mid
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `pre_llm_call` hook | **Auto-recall** — query memories, inject as ephemeral system prompt context |
|
||||
| `post_llm_call` hook | **Auto-retain** — store user/assistant exchange to Hindsight |
|
||||
| `hindsight_retain` tool | Explicit memory storage (model-initiated) |
|
||||
| `hindsight_recall` tool | Explicit memory search (model-initiated) |
|
||||
| `hindsight_reflect` tool | LLM-synthesized answer from stored memories |
|
||||
|
||||
## Connection Modes
|
||||
|
||||
### 1. External API (recommended for production)
|
||||
|
||||
Connect to a running Hindsight server (cloud or self-hosted). No local LLM needed — the server handles fact extraction.
|
||||
|
||||
```json
|
||||
{
|
||||
"hindsightApiUrl": "https://your-hindsight-server.com",
|
||||
"hindsightApiToken": "your-token",
|
||||
"bankId": "hermes"
|
||||
}
|
||||
```
|
||||
|
||||
If neither `HINDSIGHT_API_URL` nor `HINDSIGHT_API_KEY` is set, the plugin silently skips registration — Hermes starts normally without the Hindsight tools.
|
||||
### 2. Local Daemon
|
||||
|
||||
### 3. Disable Hermes's built-in memory tool
|
||||
If you're running `hindsight-embed` locally, point to it:
|
||||
|
||||
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:
|
||||
```json
|
||||
{
|
||||
"hindsightApiUrl": "http://localhost:9077",
|
||||
"bankId": "hermes"
|
||||
}
|
||||
```
|
||||
|
||||
Follow the [Quick Start](/developer/api/quickstart) guide to get the Hindsight API running.
|
||||
|
||||
## Configuration
|
||||
|
||||
All settings are in `~/.hindsight/hermes.json`. Every setting can also be overridden via environment variables (env vars take priority).
|
||||
|
||||
### Connection & Daemon
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `hindsightApiUrl` | — | `HINDSIGHT_API_URL` | Hindsight API URL |
|
||||
| `hindsightApiToken` | `null` | `HINDSIGHT_API_TOKEN` / `HINDSIGHT_API_KEY` | Auth token for API |
|
||||
| `apiPort` | `9077` | `HINDSIGHT_API_PORT` | Port for local Hindsight daemon |
|
||||
| `daemonIdleTimeout` | `0` | `HINDSIGHT_DAEMON_IDLE_TIMEOUT` | Seconds before idle daemon shuts down (0 = never) |
|
||||
| `embedVersion` | `"latest"` | `HINDSIGHT_EMBED_VERSION` | `hindsight-embed` version for `uvx` |
|
||||
|
||||
### LLM Provider (daemon mode only)
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `llmProvider` | auto-detect | `HINDSIGHT_LLM_PROVIDER` | LLM provider: `openai`, `anthropic`, `gemini`, `groq`, `ollama` |
|
||||
| `llmModel` | provider default | `HINDSIGHT_LLM_MODEL` | Model override |
|
||||
|
||||
### Memory Bank
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `bankId` | — | `HINDSIGHT_BANK_ID` | Memory bank ID |
|
||||
| `bankMission` | `""` | `HINDSIGHT_BANK_MISSION` | Agent identity/purpose for the memory bank |
|
||||
| `retainMission` | `null` | — | Custom retain mission (what to extract from conversations) |
|
||||
| `bankIdPrefix` | `""` | — | Prefix for all bank IDs |
|
||||
|
||||
### Auto-Recall
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `autoRecall` | `true` | `HINDSIGHT_AUTO_RECALL` | Enable automatic memory recall via `pre_llm_call` hook |
|
||||
| `recallBudget` | `"mid"` | `HINDSIGHT_RECALL_BUDGET` | Recall effort: `low`, `mid`, `high` |
|
||||
| `recallMaxTokens` | `4096` | `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens in recall response |
|
||||
| `recallMaxQueryChars` | `800` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | Max chars of user message used as query |
|
||||
| `recallPromptPreamble` | see below | — | Header text injected before recalled memories |
|
||||
|
||||
Default preamble:
|
||||
> Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:
|
||||
|
||||
### Auto-Retain
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `autoRetain` | `true` | `HINDSIGHT_AUTO_RETAIN` | Enable automatic retention via `post_llm_call` hook |
|
||||
| `retainEveryNTurns` | `1` | — | Retain every Nth turn |
|
||||
| `retainOverlapTurns` | `2` | — | Extra overlap turns for continuity |
|
||||
| `retainRoles` | `["user", "assistant"]` | — | Which message roles to retain |
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `debug` | `false` | `HINDSIGHT_DEBUG` | Enable debug logging to stderr |
|
||||
|
||||
## Hermes Gateway (Telegram, Discord, Slack)
|
||||
|
||||
When using Hermes in gateway mode (multi-platform messaging), the plugin works across all platforms. Hermes creates a fresh `AIAgent` per message, and the plugin's `pre_llm_call` hook ensures relevant memories are recalled for each turn regardless of platform.
|
||||
|
||||
## Disabling Hermes's Built-in Memory
|
||||
|
||||
Hermes has a built-in `memory` tool that saves to local markdown files. If both are active, the LLM may prefer the built-in one. Disable it:
|
||||
|
||||
```bash
|
||||
hermes tools disable memory
|
||||
```
|
||||
|
||||
This persists across sessions. You can re-enable it later with `hermes tools enable memory`.
|
||||
|
||||
### 4. Start Hindsight API
|
||||
|
||||
Follow the [Quick Start](/developer/api/quickstart) guide to get the Hindsight API running, then come back here.
|
||||
|
||||
### 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
|
||||
```
|
||||
Re-enable later with `hermes tools enable memory`.
|
||||
|
||||
## 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:
|
||||
**Plugin not loading**: Verify the entry point is registered:
|
||||
```bash
|
||||
curl http://localhost:8888/health
|
||||
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', ...)`.
|
||||
|
||||
**Tools don't appear in `/tools`**: Check that `hindsightApiUrl` (or `HINDSIGHT_API_URL`) is set. The plugin silently skips registration when unconfigured.
|
||||
|
||||
**Connection refused**: Verify the Hindsight API is running:
|
||||
```bash
|
||||
curl http://localhost:9077/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 |
|
||||
**Recall returning no memories**: Memories need at least one retain cycle. Try storing a fact first, then asking about it in a new session.
|
||||
|
|
|
|||
|
|
@ -1,247 +1,133 @@
|
|||
# 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.
|
||||
Persistent long-term memory for [Hermes Agent](https://github.com/NousResearch/hermes-agent) using [Hindsight](https://vectorize.io/hindsight). Automatically recalls relevant context before every LLM call and retains conversations for future sessions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Install into Hermes's Python environment
|
||||
uv pip install hindsight-hermes --python $HOME/.hermes/hermes-agent/venv/bin/python
|
||||
|
||||
# 2. Configure
|
||||
mkdir -p ~/.hindsight
|
||||
cat > ~/.hindsight/hermes.json << 'EOF'
|
||||
{
|
||||
"hindsightApiUrl": "http://localhost:9077",
|
||||
"bankId": "hermes"
|
||||
}
|
||||
EOF
|
||||
|
||||
# 3. Start Hermes — the plugin activates automatically
|
||||
hermes
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
**Automatic memory on every turn** — no tool calls required:
|
||||
**Automatic memory on every turn** (via Hermes lifecycle hooks):
|
||||
|
||||
- **`pre_llm_call` hook** — Before each LLM call, recalls relevant memories and injects them into the system prompt. The model sees cross-session context automatically.
|
||||
- **`post_llm_call` hook** — After each turn, retains the user/assistant exchange so it can be recalled in future sessions.
|
||||
- **`pre_llm_call`** — Recalls relevant memories and injects them into the system prompt. The model sees cross-session context automatically, no tool call needed.
|
||||
- **`post_llm_call`** — Retains the user/assistant exchange so it can be recalled in future sessions.
|
||||
|
||||
**Three explicit tools** for when the model wants direct control:
|
||||
**Three explicit tools** (via Hermes 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.
|
||||
- **`hindsight_retain`** — Store information to long-term memory
|
||||
- **`hindsight_recall`** — Search long-term memory for relevant information
|
||||
- **`hindsight_reflect`** — Synthesize a reasoned answer from stored memories
|
||||
|
||||
These tools appear under the `[hindsight]` toolset in Hermes's `/tools` list.
|
||||
> The lifecycle hooks require hermes-agent with [PR #2823](https://github.com/NousResearch/hermes-agent/pull/2823) or later. On older versions, only the tools are registered — hooks are silently skipped.
|
||||
|
||||
> **Note:** The lifecycle hooks require hermes-agent with [PR #2823](https://github.com/NousResearch/hermes-agent/pull/2823) or later. On older versions, only the tools are registered — hooks are silently skipped.
|
||||
## Configuration
|
||||
|
||||
## Setup
|
||||
All settings live in `~/.hindsight/hermes.json`. Environment variables override file values.
|
||||
|
||||
### 1. Install hindsight-hermes into the Hermes venv
|
||||
Same field names as the [openclaw](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/openclaw) and [claude-code](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/claude-code) integrations.
|
||||
|
||||
The package must be installed in the **same Python environment** that Hermes runs in, so the entry point is discoverable.
|
||||
### Example config
|
||||
|
||||
```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
|
||||
```json
|
||||
{
|
||||
"hindsightApiUrl": "http://localhost:9077",
|
||||
"bankId": "hermes",
|
||||
"autoRecall": true,
|
||||
"autoRetain": true,
|
||||
"recallBudget": "mid",
|
||||
"recallMaxTokens": 4096,
|
||||
"bankMission": "Focus on user preferences, project context, and technical decisions."
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Set environment variables
|
||||
### Connection
|
||||
|
||||
The plugin reads its configuration from environment variables. Set these before launching Hermes:
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `hindsightApiUrl` | `HINDSIGHT_API_URL` | — | Hindsight API URL |
|
||||
| `hindsightApiToken` | `HINDSIGHT_API_TOKEN` / `HINDSIGHT_API_KEY` | — | Auth token |
|
||||
| `apiPort` | `HINDSIGHT_API_PORT` | `9077` | Local daemon port |
|
||||
| `daemonIdleTimeout` | `HINDSIGHT_DAEMON_IDLE_TIMEOUT` | `0` | Idle shutdown (seconds, 0 = never) |
|
||||
| `embedVersion` | `HINDSIGHT_EMBED_VERSION` | `"latest"` | `hindsight-embed` version |
|
||||
|
||||
```bash
|
||||
# Required — tells the plugin where Hindsight is running
|
||||
export HINDSIGHT_API_URL=http://localhost:8888
|
||||
### Memory Bank
|
||||
|
||||
# Required — the memory bank to read/write. Think of this as a "brain" for one user or agent.
|
||||
export HINDSIGHT_BANK_ID=my-agent
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `bankId` | `HINDSIGHT_BANK_ID` | — | Memory bank ID |
|
||||
| `bankMission` | `HINDSIGHT_BANK_MISSION` | `""` | Agent purpose for the bank |
|
||||
| `retainMission` | — | — | Custom extraction prompt |
|
||||
| `bankIdPrefix` | — | `""` | Prefix for bank IDs |
|
||||
|
||||
# Optional — only needed if using Hindsight Cloud (https://api.hindsight.vectorize.io)
|
||||
export HINDSIGHT_API_KEY=your-api-key
|
||||
### Auto-Recall
|
||||
|
||||
# Optional — recall budget: low (fast), mid (default), high (thorough)
|
||||
export HINDSIGHT_BUDGET=mid
|
||||
```
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `autoRecall` | `HINDSIGHT_AUTO_RECALL` | `true` | Enable `pre_llm_call` recall |
|
||||
| `recallBudget` | `HINDSIGHT_RECALL_BUDGET` | `"mid"` | Effort: `low`/`mid`/`high` |
|
||||
| `recallMaxTokens` | `HINDSIGHT_RECALL_MAX_TOKENS` | `4096` | Max tokens in response |
|
||||
| `recallMaxQueryChars` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | `800` | Max query chars |
|
||||
| `recallPromptPreamble` | — | see below | Header before recalled memories |
|
||||
|
||||
If neither `HINDSIGHT_API_URL` nor `HINDSIGHT_API_KEY` is set, the plugin silently skips registration — Hermes starts normally without the Hindsight tools.
|
||||
### Auto-Retain
|
||||
|
||||
### 3. Disable Hermes's built-in memory tool
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `autoRetain` | `HINDSIGHT_AUTO_RETAIN` | `true` | Enable `post_llm_call` retain |
|
||||
| `retainEveryNTurns` | — | `1` | Retain every Nth turn |
|
||||
| `retainOverlapTurns` | — | `2` | Overlap turns for continuity |
|
||||
| `retainRoles` | — | `["user", "assistant"]` | Roles to retain |
|
||||
|
||||
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:
|
||||
### LLM (daemon mode)
|
||||
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `llmProvider` | `HINDSIGHT_LLM_PROVIDER` | auto-detect | `openai`/`anthropic`/`gemini`/`groq`/`ollama` |
|
||||
| `llmModel` | `HINDSIGHT_LLM_MODEL` | provider default | Model override |
|
||||
|
||||
### Misc
|
||||
|
||||
| Setting | Env Var | Default | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `debug` | `HINDSIGHT_DEBUG` | `false` | Debug logging |
|
||||
|
||||
## Disabling Hermes's built-in memory
|
||||
|
||||
Hermes has a built-in `memory` tool that saves to local files. 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:
|
||||
**Plugin not loading** — verify the entry point:
|
||||
```bash
|
||||
curl http://localhost:8888/health
|
||||
python -c "
|
||||
import importlib.metadata
|
||||
eps = importlib.metadata.entry_points(group='hermes_agent.plugins')
|
||||
print(list(eps))
|
||||
"
|
||||
```
|
||||
|
||||
## Manual registration (advanced)
|
||||
**Tools missing from `/tools`** — the plugin skips registration when `hindsightApiUrl` is not configured. Check `~/.hindsight/hermes.json` or env vars.
|
||||
|
||||
If you don't want to use the plugin system, you can register tools directly in a Hermes startup script or custom agent:
|
||||
**Connection refused** — verify the API is running: `curl http://localhost:9077/health`
|
||||
|
||||
```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) |
|
||||
| — | `HINDSIGHT_AUTO_RETAIN` | `true` | Auto-retain conversation turns via `post_llm_call` hook |
|
||||
| — | `HINDSIGHT_RECALL_BUDGET` | same as `budget` | Budget for the `pre_llm_call` recall hook |
|
||||
| — | `HINDSIGHT_RECALL_MAX_TOKENS` | `4096` | Max tokens for the `pre_llm_call` recall hook |
|
||||
| `max_tokens` | — | `4096` | Max tokens for recall results (tools) |
|
||||
| `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 |
|
||||
**No memories recalled** — memories need at least one retain cycle. Store a fact, start a new session, then ask about it.
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ Hermes build that supports lifecycle hooks, the plugin also:
|
|||
Plugin usage (auto-discovery)::
|
||||
|
||||
pip install hindsight-hermes
|
||||
export HINDSIGHT_API_URL=http://localhost:8888
|
||||
export HINDSIGHT_BANK_ID=my-agent
|
||||
# Configure via ~/.hindsight/hermes.json:
|
||||
# {"hindsightApiUrl": "http://localhost:9077", "bankId": "my-agent"}
|
||||
|
||||
Manual usage::
|
||||
|
||||
|
|
@ -21,15 +21,14 @@ Manual usage::
|
|||
|
||||
register_tools(
|
||||
bank_id="my-agent",
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
hindsight_api_url="http://localhost:9077",
|
||||
)
|
||||
"""
|
||||
|
||||
from .config import (
|
||||
HindsightHermesConfig,
|
||||
configure,
|
||||
get_config,
|
||||
reset_config,
|
||||
USER_CONFIG_PATH,
|
||||
load_config,
|
||||
write_config,
|
||||
)
|
||||
from .errors import HindsightError
|
||||
from .tools import (
|
||||
|
|
@ -42,10 +41,9 @@ from .tools import (
|
|||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
"configure",
|
||||
"get_config",
|
||||
"reset_config",
|
||||
"HindsightHermesConfig",
|
||||
"USER_CONFIG_PATH",
|
||||
"load_config",
|
||||
"write_config",
|
||||
"HindsightError",
|
||||
"register_tools",
|
||||
"register",
|
||||
|
|
|
|||
|
|
@ -1,92 +1,170 @@
|
|||
"""Global configuration for Hindsight-Hermes integration."""
|
||||
"""Configuration management for Hindsight-Hermes plugin.
|
||||
|
||||
Loads settings from ``~/.hindsight/hermes.json`` merged with environment
|
||||
variable overrides. Follows the same conventions as the openclaw and
|
||||
claude-code integrations.
|
||||
|
||||
Loading order (later entries win):
|
||||
1. Built-in defaults
|
||||
2. User config (``~/.hindsight/hermes.json``)
|
||||
3. Environment variable overrides
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json as _json
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io"
|
||||
HINDSIGHT_API_KEY_ENV = "HINDSIGHT_API_KEY"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
USER_CONFIG_PATH = Path.home() / ".hindsight" / "hermes.json"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defaults — same field names as openclaw / claude-code settings.json
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFAULTS: dict[str, Any] = {
|
||||
# Connection
|
||||
"hindsightApiUrl": None,
|
||||
"hindsightApiToken": None,
|
||||
"apiPort": 9077,
|
||||
"daemonIdleTimeout": 0,
|
||||
"embedVersion": "latest",
|
||||
"embedPackagePath": None,
|
||||
# Bank
|
||||
"bankId": None,
|
||||
"bankIdPrefix": "",
|
||||
"bankMission": "",
|
||||
"retainMission": None,
|
||||
# Recall
|
||||
"autoRecall": True,
|
||||
"recallBudget": "mid",
|
||||
"recallMaxTokens": 4096,
|
||||
"recallTypes": ["world", "experience"],
|
||||
"recallContextTurns": 1,
|
||||
"recallMaxQueryChars": 800,
|
||||
"recallRoles": ["user", "assistant"],
|
||||
"recallPromptPreamble": (
|
||||
"Relevant memories from past conversations (prioritize recent when "
|
||||
"conflicting). Only use memories that are directly useful to continue "
|
||||
"this conversation; ignore the rest:"
|
||||
),
|
||||
"recallTopK": None,
|
||||
# Retain
|
||||
"autoRetain": True,
|
||||
"retainRoles": ["user", "assistant"],
|
||||
"retainEveryNTurns": 1,
|
||||
"retainOverlapTurns": 2,
|
||||
"retainContext": "hermes",
|
||||
# LLM (for daemon mode)
|
||||
"llmProvider": None,
|
||||
"llmModel": None,
|
||||
"llmApiKeyEnv": None,
|
||||
# Misc
|
||||
"debug": False,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Env var → config key mapping (same convention as claude-code)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ENV_OVERRIDES: dict[str, tuple[str, type]] = {
|
||||
"HINDSIGHT_API_URL": ("hindsightApiUrl", str),
|
||||
"HINDSIGHT_API_TOKEN": ("hindsightApiToken", str),
|
||||
"HINDSIGHT_API_KEY": ("hindsightApiToken", str), # alias
|
||||
"HINDSIGHT_BANK_ID": ("bankId", str),
|
||||
"HINDSIGHT_AUTO_RECALL": ("autoRecall", bool),
|
||||
"HINDSIGHT_AUTO_RETAIN": ("autoRetain", bool),
|
||||
"HINDSIGHT_RECALL_BUDGET": ("recallBudget", str),
|
||||
"HINDSIGHT_RECALL_MAX_TOKENS": ("recallMaxTokens", int),
|
||||
"HINDSIGHT_RECALL_MAX_QUERY_CHARS": ("recallMaxQueryChars", int),
|
||||
"HINDSIGHT_API_PORT": ("apiPort", int),
|
||||
"HINDSIGHT_DAEMON_IDLE_TIMEOUT": ("daemonIdleTimeout", int),
|
||||
"HINDSIGHT_EMBED_VERSION": ("embedVersion", str),
|
||||
"HINDSIGHT_EMBED_PACKAGE_PATH": ("embedPackagePath", str),
|
||||
"HINDSIGHT_BANK_MISSION": ("bankMission", str),
|
||||
"HINDSIGHT_LLM_PROVIDER": ("llmProvider", str),
|
||||
"HINDSIGHT_LLM_MODEL": ("llmModel", str),
|
||||
"HINDSIGHT_DEBUG": ("debug", bool),
|
||||
}
|
||||
|
||||
|
||||
@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
|
||||
# ---------------------------------------------------------------------------
|
||||
# Loading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_global_config: HindsightHermesConfig | None = None
|
||||
def _cast_env(value: str, typ: type) -> Any:
|
||||
"""Cast environment variable string to target type. Returns None on failure."""
|
||||
try:
|
||||
if typ is bool:
|
||||
return value.lower() in ("true", "1", "yes")
|
||||
if typ is int:
|
||||
return int(value)
|
||||
return value
|
||||
except (ValueError, AttributeError):
|
||||
return 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.
|
||||
def _load_json_file(path: Path | str) -> dict[str, Any]:
|
||||
"""Read a JSON file, returning {} on any error."""
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
return {}
|
||||
try:
|
||||
return _json.loads(p.read_text(encoding="utf-8")) or {}
|
||||
except Exception as exc:
|
||||
_debug_log(None, f"Failed to read {p}: {exc}")
|
||||
return {}
|
||||
|
||||
|
||||
def load_config(config_path: Path | str | None = None) -> dict[str, Any]:
|
||||
"""Load plugin configuration.
|
||||
|
||||
Loading order (later entries win):
|
||||
1. Built-in defaults
|
||||
2. User config (``~/.hindsight/hermes.json``)
|
||||
3. Environment variable overrides
|
||||
|
||||
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.
|
||||
config_path: Override the user config path (for testing).
|
||||
|
||||
Returns:
|
||||
The configured HindsightHermesConfig.
|
||||
A plain dict with all configuration values.
|
||||
"""
|
||||
global _global_config
|
||||
config = dict(DEFAULTS)
|
||||
|
||||
resolved_url = hindsight_api_url or DEFAULT_HINDSIGHT_API_URL
|
||||
resolved_key = api_key or os.environ.get(HINDSIGHT_API_KEY_ENV)
|
||||
# User config — stable, version-independent
|
||||
user_path = Path(config_path) if config_path else USER_CONFIG_PATH
|
||||
file_cfg = _load_json_file(user_path)
|
||||
config.update({k: v for k, v in file_cfg.items() if v is not None})
|
||||
|
||||
_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,
|
||||
# Environment variable overrides (highest priority)
|
||||
for env_name, (key, typ) in ENV_OVERRIDES.items():
|
||||
val = os.environ.get(env_name)
|
||||
if val is not None:
|
||||
cast_val = _cast_env(val, typ)
|
||||
if cast_val is not None:
|
||||
config[key] = cast_val
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def write_config(data: dict[str, Any], config_path: Path | str | None = None) -> None:
|
||||
"""Write configuration to the user config file."""
|
||||
p = Path(config_path) if config_path else USER_CONFIG_PATH
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(
|
||||
_json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
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
|
||||
def _debug_log(config: dict | None, *args: Any) -> None:
|
||||
"""Log to stderr if debug mode is enabled."""
|
||||
if config and config.get("debug"):
|
||||
print("[Hindsight]", *args, file=sys.stderr)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from typing import Any
|
|||
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
from .config import get_config
|
||||
from .config import load_config
|
||||
from .errors import HindsightError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -94,23 +94,19 @@ def _resolve_client(
|
|||
hindsight_api_url: str | None,
|
||||
api_key: str | None,
|
||||
) -> Hindsight:
|
||||
"""Resolve a Hindsight client from explicit args or global config."""
|
||||
"""Resolve a Hindsight client from explicit args."""
|
||||
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:
|
||||
if hindsight_api_url is None:
|
||||
raise HindsightError(
|
||||
"No Hindsight API URL configured. "
|
||||
"Pass client= or hindsight_api_url=, or call configure() first."
|
||||
"Pass client= or hindsight_api_url=, or create ~/.hindsight/hermes.json."
|
||||
)
|
||||
|
||||
kwargs: dict[str, Any] = {"base_url": url, "timeout": 30.0}
|
||||
if key:
|
||||
kwargs["api_key"] = key
|
||||
kwargs: dict[str, Any] = {"base_url": hindsight_api_url, "timeout": 30.0}
|
||||
if api_key:
|
||||
kwargs["api_key"] = api_key
|
||||
return Hindsight(**kwargs)
|
||||
|
||||
|
||||
|
|
@ -264,20 +260,23 @@ def register_tools(
|
|||
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()``.
|
||||
Reads configuration from ``~/.hindsight/hermes.json`` and environment
|
||||
variables (env vars take priority), then registers tools and hooks.
|
||||
|
||||
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")
|
||||
cfg = load_config()
|
||||
|
||||
hindsight_api_url = cfg.get("hindsightApiUrl")
|
||||
api_key = cfg.get("hindsightApiToken")
|
||||
bank_id = cfg.get("bankId")
|
||||
budget = cfg.get("recallBudget", "mid")
|
||||
|
||||
if not hindsight_api_url and not api_key:
|
||||
logger.debug(
|
||||
"Hindsight plugin: no API URL or key configured, skipping registration"
|
||||
"Hindsight plugin: not configured (need hindsightApiUrl or hindsightApiToken). "
|
||||
"Create ~/.hindsight/hermes.json or set HINDSIGHT_API_URL."
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -354,9 +353,10 @@ def register(ctx: Any) -> None:
|
|||
# When running on an older hermes-agent the hooks are simply never called,
|
||||
# so registering them is always safe.
|
||||
|
||||
recall_budget = os.environ.get("HINDSIGHT_RECALL_BUDGET", budget)
|
||||
recall_max_tokens = int(os.environ.get("HINDSIGHT_RECALL_MAX_TOKENS", "4096"))
|
||||
retain_enabled = os.environ.get("HINDSIGHT_AUTO_RETAIN", "true").lower() in {"1", "true", "yes", "on"}
|
||||
recall_budget = cfg.get("recallBudget", budget)
|
||||
recall_max_tokens = cfg.get("recallMaxTokens", 4096)
|
||||
retain_enabled = cfg.get("autoRetain", True)
|
||||
recall_preamble = cfg.get("recallPromptPreamble", "")
|
||||
|
||||
async def _on_pre_llm_call(
|
||||
*,
|
||||
|
|
@ -381,12 +381,12 @@ def register(ctx: Any) -> None:
|
|||
if not response.results:
|
||||
return None
|
||||
lines = [f"- {r.text}" for r in response.results]
|
||||
context = (
|
||||
header = recall_preamble or (
|
||||
"# Hindsight Memory (persistent cross-session context)\n"
|
||||
"Use this to answer questions about the user and prior sessions. "
|
||||
"Do not call tools to look up information that is already present here.\n\n"
|
||||
+ "\n".join(lines)
|
||||
"Do not call tools to look up information that is already present here."
|
||||
)
|
||||
context = header + "\n\n" + "\n".join(lines)
|
||||
return {"context": context}
|
||||
except Exception as exc:
|
||||
logger.warning("Hindsight pre_llm_call recall failed: %s", exc)
|
||||
|
|
|
|||
|
|
@ -1,57 +1,98 @@
|
|||
"""Tests for hindsight_hermes.config module."""
|
||||
|
||||
from hindsight_hermes.config import (
|
||||
HindsightHermesConfig,
|
||||
configure,
|
||||
get_config,
|
||||
reset_config,
|
||||
)
|
||||
import json
|
||||
|
||||
from hindsight_hermes.config import DEFAULTS, load_config
|
||||
|
||||
|
||||
class TestConfigure:
|
||||
def setup_method(self):
|
||||
reset_config()
|
||||
class TestLoadConfig:
|
||||
def test_defaults_when_no_file(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("HINDSIGHT_API_URL", raising=False)
|
||||
monkeypatch.delenv("HINDSIGHT_API_KEY", raising=False)
|
||||
monkeypatch.delenv("HINDSIGHT_BANK_ID", raising=False)
|
||||
cfg = load_config(config_path=tmp_path / "nope.json")
|
||||
assert cfg["hindsightApiUrl"] is None
|
||||
assert cfg["bankId"] is None
|
||||
assert cfg["recallBudget"] == "mid"
|
||||
assert cfg["autoRecall"] is True
|
||||
assert cfg["autoRetain"] is True
|
||||
|
||||
def teardown_method(self):
|
||||
reset_config()
|
||||
def test_reads_from_file(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("HINDSIGHT_API_URL", raising=False)
|
||||
monkeypatch.delenv("HINDSIGHT_API_KEY", raising=False)
|
||||
monkeypatch.delenv("HINDSIGHT_BANK_ID", raising=False)
|
||||
monkeypatch.delenv("HINDSIGHT_AUTO_RETAIN", raising=False)
|
||||
f = tmp_path / "hermes.json"
|
||||
f.write_text(json.dumps({
|
||||
"hindsightApiUrl": "http://localhost:9077",
|
||||
"hindsightApiToken": "file-token",
|
||||
"bankId": "my-bank",
|
||||
"recallBudget": "high",
|
||||
"autoRetain": False,
|
||||
"recallMaxTokens": 2048,
|
||||
}))
|
||||
cfg = load_config(config_path=f)
|
||||
assert cfg["hindsightApiUrl"] == "http://localhost:9077"
|
||||
assert cfg["hindsightApiToken"] == "file-token"
|
||||
assert cfg["bankId"] == "my-bank"
|
||||
assert cfg["recallBudget"] == "high"
|
||||
assert cfg["autoRetain"] is False
|
||||
assert cfg["recallMaxTokens"] == 2048
|
||||
|
||||
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_env_overrides_file(self, tmp_path, monkeypatch):
|
||||
f = tmp_path / "hermes.json"
|
||||
f.write_text(json.dumps({
|
||||
"hindsightApiUrl": "http://from-file:9077",
|
||||
"bankId": "file-bank",
|
||||
"recallBudget": "low",
|
||||
}))
|
||||
monkeypatch.setenv("HINDSIGHT_API_URL", "http://from-env:9077")
|
||||
monkeypatch.setenv("HINDSIGHT_BANK_ID", "env-bank")
|
||||
monkeypatch.delenv("HINDSIGHT_RECALL_BUDGET", raising=False)
|
||||
cfg = load_config(config_path=f)
|
||||
assert cfg["hindsightApiUrl"] == "http://from-env:9077"
|
||||
assert cfg["bankId"] == "env-bank"
|
||||
assert cfg["recallBudget"] == "low" # from file (env not set)
|
||||
|
||||
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_api_key_env_maps_to_token(self, tmp_path, monkeypatch):
|
||||
"""HINDSIGHT_API_KEY is an alias for hindsightApiToken."""
|
||||
monkeypatch.setenv("HINDSIGHT_API_KEY", "my-key")
|
||||
cfg = load_config(config_path=tmp_path / "nope.json")
|
||||
assert cfg["hindsightApiToken"] == "my-key"
|
||||
|
||||
def test_get_config_returns_none_before_configure(self):
|
||||
assert get_config() is None
|
||||
def test_bool_env_casting(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HINDSIGHT_AUTO_RETAIN", "false")
|
||||
monkeypatch.setenv("HINDSIGHT_AUTO_RECALL", "1")
|
||||
cfg = load_config(config_path=tmp_path / "nope.json")
|
||||
assert cfg["autoRetain"] is False
|
||||
assert cfg["autoRecall"] is True
|
||||
|
||||
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_int_env_casting(self, tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HINDSIGHT_RECALL_MAX_TOKENS", "2048")
|
||||
cfg = load_config(config_path=tmp_path / "nope.json")
|
||||
assert cfg["recallMaxTokens"] == 2048
|
||||
|
||||
def test_reset_config(self):
|
||||
configure(api_key="k")
|
||||
reset_config()
|
||||
assert get_config() is None
|
||||
def test_malformed_file_returns_defaults(self, tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("HINDSIGHT_API_URL", raising=False)
|
||||
monkeypatch.delenv("HINDSIGHT_BANK_ID", raising=False)
|
||||
f = tmp_path / "hermes.json"
|
||||
f.write_text("not json {{{")
|
||||
cfg = load_config(config_path=f)
|
||||
assert cfg["recallBudget"] == "mid"
|
||||
|
||||
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_null_values_in_file_ignored(self, tmp_path, monkeypatch):
|
||||
"""null values in JSON should not override defaults."""
|
||||
monkeypatch.delenv("HINDSIGHT_API_URL", raising=False)
|
||||
monkeypatch.delenv("HINDSIGHT_BANK_ID", raising=False)
|
||||
f = tmp_path / "hermes.json"
|
||||
f.write_text(json.dumps({"bankId": None, "recallBudget": "high"}))
|
||||
cfg = load_config(config_path=f)
|
||||
assert cfg["bankId"] is None # stays default (None)
|
||||
assert cfg["recallBudget"] == "high"
|
||||
|
||||
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"
|
||||
def test_all_defaults_present(self):
|
||||
"""Every key in DEFAULTS should exist in a freshly loaded config."""
|
||||
# Use a path that doesn't exist and clean env
|
||||
cfg = DEFAULTS.copy()
|
||||
for key in DEFAULTS:
|
||||
assert key in cfg
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
from hindsight_hermes.config import configure, reset_config
|
||||
from hindsight_hermes.config import DEFAULTS
|
||||
from hindsight_hermes.errors import HindsightError
|
||||
from hindsight_hermes.tools import (
|
||||
RECALL_SCHEMA,
|
||||
|
|
@ -26,10 +26,11 @@ from hindsight_hermes.tools import (
|
|||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_config():
|
||||
reset_config()
|
||||
yield
|
||||
reset_config()
|
||||
def _clean_env(monkeypatch):
|
||||
"""Ensure no stale env vars leak between tests."""
|
||||
for key in ("HINDSIGHT_API_URL", "HINDSIGHT_API_KEY", "HINDSIGHT_API_TOKEN",
|
||||
"HINDSIGHT_BANK_ID", "HINDSIGHT_AUTO_RETAIN", "HINDSIGHT_RECALL_BUDGET"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
|
@ -125,10 +126,9 @@ 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")
|
||||
def test_creates_client_from_args(self):
|
||||
with patch("hindsight_hermes.tools.Hindsight") as MockH:
|
||||
_resolve_client(None, None, None)
|
||||
_resolve_client(None, "http://localhost:9999", "key")
|
||||
MockH.assert_called_once_with(base_url="http://localhost:9999", timeout=30.0, api_key="key")
|
||||
|
||||
def test_raises_without_url(self):
|
||||
|
|
@ -222,10 +222,10 @@ class TestRegisterPlugin:
|
|||
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)
|
||||
def test_register_skips_without_config(self):
|
||||
empty_cfg = dict(DEFAULTS) # no apiUrl, no apiToken
|
||||
ctx = MagicMock()
|
||||
with patch("hindsight_hermes.tools.load_config", return_value=empty_cfg):
|
||||
register(ctx)
|
||||
ctx.register_tool.assert_not_called()
|
||||
|
||||
|
|
|
|||
|
|
@ -4,216 +4,172 @@ sidebar_position: 10
|
|||
|
||||
# Hermes Agent
|
||||
|
||||
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.
|
||||
Persistent long-term memory for [Hermes Agent](https://github.com/NousResearch/hermes-agent) using [Hindsight](https://vectorize.io/hindsight). Automatically recalls relevant context before every LLM call and retains conversations for future sessions — plus explicit retain/recall/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.
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Install the plugin into Hermes's Python environment
|
||||
uv pip install hindsight-hermes --python $HOME/.hermes/hermes-agent/venv/bin/python
|
||||
|
||||
# 2. Configure (choose one)
|
||||
# Option A: Config file (recommended)
|
||||
mkdir -p ~/.hindsight
|
||||
cat > ~/.hindsight/hermes.json << 'EOF'
|
||||
{
|
||||
"hindsightApiUrl": "http://localhost:9077",
|
||||
"bankId": "hermes"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Option B: Environment variables
|
||||
export HINDSIGHT_API_URL=http://localhost:9077
|
||||
export HINDSIGHT_BANK_ID=hermes
|
||||
|
||||
# 3. Start Hermes — the plugin activates automatically
|
||||
hermes
|
||||
```
|
||||
|
||||
### 2. Set environment variables
|
||||
## Features
|
||||
|
||||
The plugin reads its configuration from environment variables. Set these before launching Hermes:
|
||||
- **Auto-recall** — on every turn, queries Hindsight for relevant memories and injects them into the system prompt (via `pre_llm_call` hook)
|
||||
- **Auto-retain** — after every response, retains the user/assistant exchange to Hindsight (via `post_llm_call` hook)
|
||||
- **Explicit tools** — `hindsight_retain`, `hindsight_recall`, `hindsight_reflect` for direct model control
|
||||
- **Config file** — `~/.hindsight/hermes.json` with the same field names as openclaw and claude-code integrations
|
||||
- **Zero config overhead** — env vars still work as overrides for CI/automation
|
||||
|
||||
```bash
|
||||
# Required — tells the plugin where Hindsight is running
|
||||
export HINDSIGHT_API_URL=http://localhost:8888
|
||||
:::note
|
||||
The lifecycle hooks (`pre_llm_call`/`post_llm_call`) require hermes-agent with [PR #2823](https://github.com/NousResearch/hermes-agent/pull/2823) or later. On older versions, only the three tools are registered — hooks are silently skipped.
|
||||
:::
|
||||
|
||||
# Required — the memory bank to read/write. Think of this as a "brain" for one user or agent.
|
||||
export HINDSIGHT_BANK_ID=my-agent
|
||||
## Architecture
|
||||
|
||||
# Optional — only needed if using Hindsight Cloud (https://api.hindsight.vectorize.io)
|
||||
export HINDSIGHT_API_KEY=your-api-key
|
||||
The plugin registers via Hermes's `hermes_agent.plugins` entry point system:
|
||||
|
||||
# Optional — recall budget: low (fast), mid (default), high (thorough)
|
||||
export HINDSIGHT_BUDGET=mid
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `pre_llm_call` hook | **Auto-recall** — query memories, inject as ephemeral system prompt context |
|
||||
| `post_llm_call` hook | **Auto-retain** — store user/assistant exchange to Hindsight |
|
||||
| `hindsight_retain` tool | Explicit memory storage (model-initiated) |
|
||||
| `hindsight_recall` tool | Explicit memory search (model-initiated) |
|
||||
| `hindsight_reflect` tool | LLM-synthesized answer from stored memories |
|
||||
|
||||
## Connection Modes
|
||||
|
||||
### 1. External API (recommended for production)
|
||||
|
||||
Connect to a running Hindsight server (cloud or self-hosted). No local LLM needed — the server handles fact extraction.
|
||||
|
||||
```json
|
||||
{
|
||||
"hindsightApiUrl": "https://your-hindsight-server.com",
|
||||
"hindsightApiToken": "your-token",
|
||||
"bankId": "hermes"
|
||||
}
|
||||
```
|
||||
|
||||
If neither `HINDSIGHT_API_URL` nor `HINDSIGHT_API_KEY` is set, the plugin silently skips registration — Hermes starts normally without the Hindsight tools.
|
||||
### 2. Local Daemon
|
||||
|
||||
### 3. Disable Hermes's built-in memory tool
|
||||
If you're running `hindsight-embed` locally, point to it:
|
||||
|
||||
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:
|
||||
```json
|
||||
{
|
||||
"hindsightApiUrl": "http://localhost:9077",
|
||||
"bankId": "hermes"
|
||||
}
|
||||
```
|
||||
|
||||
Follow the [Quick Start](../../developer/api/quickstart.md) guide to get the Hindsight API running.
|
||||
|
||||
## Configuration
|
||||
|
||||
All settings are in `~/.hindsight/hermes.json`. Every setting can also be overridden via environment variables (env vars take priority).
|
||||
|
||||
### Connection & Daemon
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `hindsightApiUrl` | — | `HINDSIGHT_API_URL` | Hindsight API URL |
|
||||
| `hindsightApiToken` | `null` | `HINDSIGHT_API_TOKEN` / `HINDSIGHT_API_KEY` | Auth token for API |
|
||||
| `apiPort` | `9077` | `HINDSIGHT_API_PORT` | Port for local Hindsight daemon |
|
||||
| `daemonIdleTimeout` | `0` | `HINDSIGHT_DAEMON_IDLE_TIMEOUT` | Seconds before idle daemon shuts down (0 = never) |
|
||||
| `embedVersion` | `"latest"` | `HINDSIGHT_EMBED_VERSION` | `hindsight-embed` version for `uvx` |
|
||||
|
||||
### LLM Provider (daemon mode only)
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `llmProvider` | auto-detect | `HINDSIGHT_LLM_PROVIDER` | LLM provider: `openai`, `anthropic`, `gemini`, `groq`, `ollama` |
|
||||
| `llmModel` | provider default | `HINDSIGHT_LLM_MODEL` | Model override |
|
||||
|
||||
### Memory Bank
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `bankId` | — | `HINDSIGHT_BANK_ID` | Memory bank ID |
|
||||
| `bankMission` | `""` | `HINDSIGHT_BANK_MISSION` | Agent identity/purpose for the memory bank |
|
||||
| `retainMission` | `null` | — | Custom retain mission (what to extract from conversations) |
|
||||
| `bankIdPrefix` | `""` | — | Prefix for all bank IDs |
|
||||
|
||||
### Auto-Recall
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `autoRecall` | `true` | `HINDSIGHT_AUTO_RECALL` | Enable automatic memory recall via `pre_llm_call` hook |
|
||||
| `recallBudget` | `"mid"` | `HINDSIGHT_RECALL_BUDGET` | Recall effort: `low`, `mid`, `high` |
|
||||
| `recallMaxTokens` | `4096` | `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens in recall response |
|
||||
| `recallMaxQueryChars` | `800` | `HINDSIGHT_RECALL_MAX_QUERY_CHARS` | Max chars of user message used as query |
|
||||
| `recallPromptPreamble` | see below | — | Header text injected before recalled memories |
|
||||
|
||||
Default preamble:
|
||||
> Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:
|
||||
|
||||
### Auto-Retain
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `autoRetain` | `true` | `HINDSIGHT_AUTO_RETAIN` | Enable automatic retention via `post_llm_call` hook |
|
||||
| `retainEveryNTurns` | `1` | — | Retain every Nth turn |
|
||||
| `retainOverlapTurns` | `2` | — | Extra overlap turns for continuity |
|
||||
| `retainRoles` | `["user", "assistant"]` | — | Which message roles to retain |
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
| Setting | Default | Env Var | Description |
|
||||
|---------|---------|---------|-------------|
|
||||
| `debug` | `false` | `HINDSIGHT_DEBUG` | Enable debug logging to stderr |
|
||||
|
||||
## Hermes Gateway (Telegram, Discord, Slack)
|
||||
|
||||
When using Hermes in gateway mode (multi-platform messaging), the plugin works across all platforms. Hermes creates a fresh `AIAgent` per message, and the plugin's `pre_llm_call` hook ensures relevant memories are recalled for each turn regardless of platform.
|
||||
|
||||
## Disabling Hermes's Built-in Memory
|
||||
|
||||
Hermes has a built-in `memory` tool that saves to local markdown files. If both are active, the LLM may prefer the built-in one. Disable it:
|
||||
|
||||
```bash
|
||||
hermes tools disable memory
|
||||
```
|
||||
|
||||
This persists across sessions. You can re-enable it later with `hermes tools enable memory`.
|
||||
|
||||
### 4. Start Hindsight API
|
||||
|
||||
Follow the [Quick Start](../../developer/api/quickstart.md) guide to get the Hindsight API running, then come back here.
|
||||
|
||||
### 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
|
||||
```
|
||||
Re-enable later with `hermes tools enable memory`.
|
||||
|
||||
## 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:
|
||||
**Plugin not loading**: Verify the entry point is registered:
|
||||
```bash
|
||||
curl http://localhost:8888/health
|
||||
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', ...)`.
|
||||
|
||||
**Tools don't appear in `/tools`**: Check that `hindsightApiUrl` (or `HINDSIGHT_API_URL`) is set. The plugin silently skips registration when unconfigured.
|
||||
|
||||
**Connection refused**: Verify the Hindsight API is running:
|
||||
```bash
|
||||
curl http://localhost:9077/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 |
|
||||
**Recall returning no memories**: Memories need at least one retain cycle. Try storing a fact first, then asking about it in a new session.
|
||||
|
|
|
|||
Loading…
Reference in a new issue