chore: drop hindsight-hermes integration (#931)
* chore: drop hindsight-hermes integration in favor of native Hermes memory provider Hermes Agent now ships with a native Hindsight memory provider (NousResearch/hermes-agent#5094), making our pip-installable hindsight-hermes package redundant. Removes: - hindsight-integrations/hermes/ (source, tests, config) - CI job, release script entry, changelog generator references - Cookbook page and pip package changelog (referenced deleted code) Keeps: - Integration docs (updated by #881 for native provider) - Blog posts (historical, already have deprecation notices) - Sidebar/banner entries (still valid for native integration) * fix(docs): remove broken cookbook link to deleted hermes-memory page
This commit is contained in:
parent
e5944b63e7
commit
cf0537ba7e
19 changed files with 3 additions and 4359 deletions
41
.github/workflows/test.yml
vendored
41
.github/workflows/test.yml
vendored
|
|
@ -43,7 +43,6 @@ jobs:
|
||||||
integrations-litellm: ${{ steps.filter.outputs.integrations-litellm }}
|
integrations-litellm: ${{ steps.filter.outputs.integrations-litellm }}
|
||||||
integrations-pydantic-ai: ${{ steps.filter.outputs.integrations-pydantic-ai }}
|
integrations-pydantic-ai: ${{ steps.filter.outputs.integrations-pydantic-ai }}
|
||||||
integrations-ag2: ${{ steps.filter.outputs.integrations-ag2 }}
|
integrations-ag2: ${{ steps.filter.outputs.integrations-ag2 }}
|
||||||
integrations-hermes: ${{ steps.filter.outputs.integrations-hermes }}
|
|
||||||
integrations-llamaindex: ${{ steps.filter.outputs.integrations-llamaindex }}
|
integrations-llamaindex: ${{ steps.filter.outputs.integrations-llamaindex }}
|
||||||
integrations-paperclip: ${{ steps.filter.outputs.integrations-paperclip }}
|
integrations-paperclip: ${{ steps.filter.outputs.integrations-paperclip }}
|
||||||
integrations-opencode: ${{ steps.filter.outputs.integrations-opencode }}
|
integrations-opencode: ${{ steps.filter.outputs.integrations-opencode }}
|
||||||
|
|
@ -115,8 +114,6 @@ jobs:
|
||||||
- 'hindsight-integrations/pydantic-ai/**'
|
- 'hindsight-integrations/pydantic-ai/**'
|
||||||
integrations-ag2:
|
integrations-ag2:
|
||||||
- 'hindsight-integrations/ag2/**'
|
- 'hindsight-integrations/ag2/**'
|
||||||
integrations-hermes:
|
|
||||||
- 'hindsight-integrations/hermes/**'
|
|
||||||
integrations-llamaindex:
|
integrations-llamaindex:
|
||||||
- 'hindsight-integrations/llamaindex/**'
|
- 'hindsight-integrations/llamaindex/**'
|
||||||
integrations-paperclip:
|
integrations-paperclip:
|
||||||
|
|
@ -1823,43 +1820,6 @@ jobs:
|
||||||
working-directory: ./hindsight-integrations/pydantic-ai
|
working-directory: ./hindsight-integrations/pydantic-ai
|
||||||
run: uv run pytest tests -v
|
run: uv run pytest tests -v
|
||||||
|
|
||||||
test-hermes-integration:
|
|
||||||
needs: [detect-changes]
|
|
||||||
if: >-
|
|
||||||
github.event_name != 'pull_request_review' &&
|
|
||||||
(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
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.pull_request.head.sha || '' }}
|
|
||||||
|
|
||||||
- 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-llamaindex-integration:
|
test-llamaindex-integration:
|
||||||
needs: [detect-changes]
|
needs: [detect-changes]
|
||||||
if: >-
|
if: >-
|
||||||
|
|
@ -2516,7 +2476,6 @@ jobs:
|
||||||
- test-crewai-integration
|
- test-crewai-integration
|
||||||
- test-litellm-integration
|
- test-litellm-integration
|
||||||
- test-pydantic-ai-integration
|
- test-pydantic-ai-integration
|
||||||
- test-hermes-integration
|
|
||||||
- test-llamaindex-integration
|
- test-llamaindex-integration
|
||||||
- test-pip-slim
|
- test-pip-slim
|
||||||
- test-embed
|
- test-embed
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ VALID_INTEGRATIONS = [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
"llamaindex",
|
"llamaindex",
|
||||||
"codex",
|
"codex",
|
||||||
"hermes",
|
|
||||||
"autogen",
|
"autogen",
|
||||||
"paperclip",
|
"paperclip",
|
||||||
"opencode",
|
"opencode",
|
||||||
|
|
@ -529,7 +528,6 @@ def _get_package_name(integration: str) -> str:
|
||||||
"claude-code": "hindsight-memory",
|
"claude-code": "hindsight-memory",
|
||||||
"llamaindex": "hindsight-llamaindex",
|
"llamaindex": "hindsight-llamaindex",
|
||||||
"codex": "hindsight-codex",
|
"codex": "hindsight-codex",
|
||||||
"hermes": "hindsight-hermes",
|
|
||||||
"autogen": "hindsight-autogen",
|
"autogen": "hindsight-autogen",
|
||||||
"paperclip": "@vectorize-io/hindsight-paperclip",
|
"paperclip": "@vectorize-io/hindsight-paperclip",
|
||||||
"opencode": "@vectorize-io/opencode-hindsight",
|
"opencode": "@vectorize-io/opencode-hindsight",
|
||||||
|
|
@ -559,7 +557,6 @@ def _integration_display_name(integration: str) -> str:
|
||||||
"claude-code": "Claude Code",
|
"claude-code": "Claude Code",
|
||||||
"llamaindex": "LlamaIndex",
|
"llamaindex": "LlamaIndex",
|
||||||
"codex": "Codex",
|
"codex": "Codex",
|
||||||
"hermes": "Hermes",
|
|
||||||
"autogen": "AutoGen",
|
"autogen": "AutoGen",
|
||||||
"paperclip": "Paperclip",
|
"paperclip": "Paperclip",
|
||||||
"opencode": "OpenCode",
|
"opencode": "OpenCode",
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,6 @@ function sdkIcon(sdk: string): string | null {
|
||||||
if (sdk.includes('-go') || sdk === 'go') {
|
if (sdk.includes('-go') || sdk === 'go') {
|
||||||
return '/img/icons/golang.png';
|
return '/img/icons/golang.png';
|
||||||
}
|
}
|
||||||
if (sdk.includes('hermes')) {
|
|
||||||
return '/img/icons/hermes.png';
|
|
||||||
}
|
|
||||||
if (sdk.includes('agno')) {
|
if (sdk.includes('agno')) {
|
||||||
return '/img/icons/agno.png';
|
return '/img/icons/agno.png';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,8 @@
|
||||||
"id": "hermes",
|
"id": "hermes",
|
||||||
"name": "Hermes",
|
"name": "Hermes",
|
||||||
"description": "Use Hindsight as the memory backend for the Hermes multi-agent messaging framework.",
|
"description": "Use Hindsight as the memory backend for the Hermes multi-agent messaging framework.",
|
||||||
"type": "official",
|
"type": "native",
|
||||||
"by": "hindsight",
|
"by": "hermes",
|
||||||
"category": "framework",
|
"category": "framework",
|
||||||
"link": "/sdks/integrations/hermes",
|
"link": "/sdks/integrations/hermes",
|
||||||
"icon": "/img/icons/hermes.png"
|
"icon": "/img/icons/hermes.png"
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
hide_table_of_contents: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Hermes Integration Changelog
|
|
||||||
|
|
||||||
Changelog for [`hindsight-hermes`](https://pypi.org/project/hindsight-hermes/).
|
|
||||||
|
|
||||||
For the source code, see [`hindsight-integrations/hermes`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/hermes).
|
|
||||||
|
|
||||||
← [Back to main changelog](/changelog)
|
|
||||||
|
|
||||||
## [0.5.0](https://github.com/vectorize-io/hindsight/tree/integrations/hermes/v0.5.0)
|
|
||||||
|
|
||||||
**Features**
|
|
||||||
|
|
||||||
- Added the Hermes Agent integration for Hindsight. ([`ef90842f`](https://github.com/vectorize-io/hindsight/commit/ef90842f))
|
|
||||||
|
|
||||||
**Improvements**
|
|
||||||
|
|
||||||
- Enabled file-based configuration for the Hermes integration. ([`0ff36548`](https://github.com/vectorize-io/hindsight/commit/0ff36548))
|
|
||||||
|
|
||||||
**Bug Fixes**
|
|
||||||
|
|
||||||
- Fixed potential event loop deadlocks by using asynchronous Hermes client calls. ([`35dfd3aa`](https://github.com/vectorize-io/hindsight/commit/35dfd3aa))
|
|
||||||
- Synchronized lifecycle hook behavior for compatibility with hermes-agent 0.5.0. ([`e7c9a683`](https://github.com/vectorize-io/hindsight/commit/e7c9a683))
|
|
||||||
|
|
@ -1,221 +0,0 @@
|
||||||
---
|
|
||||||
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
|
|
||||||
|
|
@ -129,12 +129,6 @@ import PageHero from '@site/src/components/PageHero';
|
||||||
description: "Delivery agent simulation demonstrating learning through mental models",
|
description: "Delivery agent simulation demonstrating learning through mental models",
|
||||||
tags: { sdk: "hindsight-litellm", topic: "Learning" }
|
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",
|
title: "Go Memory-Augmented API",
|
||||||
href: "/cookbook/applications/go-memory-service",
|
href: "/cookbook/applications/go-memory-service",
|
||||||
|
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
# hindsight-hermes
|
|
||||||
|
|
||||||
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** (via Hermes lifecycle hooks):
|
|
||||||
|
|
||||||
- **`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** (via Hermes plugin system):
|
|
||||||
|
|
||||||
- **`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
|
|
||||||
|
|
||||||
> 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
|
|
||||||
|
|
||||||
All settings live in `~/.hindsight/hermes.json`. Environment variables override file values.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Example config
|
|
||||||
|
|
||||||
```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."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connection
|
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### Memory Bank
|
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### Auto-Recall
|
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### Auto-Retain
|
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|
||||||
### 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
**Plugin not loading** — verify the entry point:
|
|
||||||
```bash
|
|
||||||
python -c "
|
|
||||||
import importlib.metadata
|
|
||||||
eps = importlib.metadata.entry_points(group='hermes_agent.plugins')
|
|
||||||
print(list(eps))
|
|
||||||
"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tools missing from `/tools`** — the plugin skips registration when `hindsightApiUrl` is not configured. Check `~/.hindsight/hermes.json` or env vars.
|
|
||||||
|
|
||||||
**Connection refused** — verify the API is running: `curl http://localhost:9077/health`
|
|
||||||
|
|
||||||
**No memories recalled** — memories need at least one retain cycle. Store a fact, start a new session, then ask about it.
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
"""Hindsight-Hermes: Persistent memory for Hermes agents.
|
|
||||||
|
|
||||||
Provides Hindsight retain/recall/reflect as native Hermes tools via the
|
|
||||||
plugin system or manual ``register_tools()`` call. When running on a
|
|
||||||
Hermes build that supports lifecycle hooks, the plugin also:
|
|
||||||
|
|
||||||
- **pre_llm_call** — recalls relevant memories and injects them into the
|
|
||||||
system prompt so the model has cross-session context on every turn.
|
|
||||||
- **post_llm_call** — retains the user/assistant exchange so it can be
|
|
||||||
recalled in future sessions.
|
|
||||||
|
|
||||||
Plugin usage (auto-discovery)::
|
|
||||||
|
|
||||||
pip install hindsight-hermes
|
|
||||||
# Configure via ~/.hindsight/hermes.json:
|
|
||||||
# {"hindsightApiUrl": "http://localhost:9077", "bankId": "my-agent"}
|
|
||||||
|
|
||||||
Manual usage::
|
|
||||||
|
|
||||||
from hindsight_hermes import register_tools
|
|
||||||
|
|
||||||
register_tools(
|
|
||||||
bank_id="my-agent",
|
|
||||||
hindsight_api_url="http://localhost:9077",
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .config import (
|
|
||||||
USER_CONFIG_PATH,
|
|
||||||
load_config,
|
|
||||||
write_config,
|
|
||||||
)
|
|
||||||
from .errors import HindsightError
|
|
||||||
from .tools import (
|
|
||||||
get_tool_definitions,
|
|
||||||
memory_instructions,
|
|
||||||
register,
|
|
||||||
register_tools,
|
|
||||||
)
|
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"USER_CONFIG_PATH",
|
|
||||||
"load_config",
|
|
||||||
"write_config",
|
|
||||||
"HindsightError",
|
|
||||||
"register_tools",
|
|
||||||
"register",
|
|
||||||
"memory_instructions",
|
|
||||||
"get_tool_definitions",
|
|
||||||
]
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
"""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
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Loading
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
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 _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:
|
|
||||||
config_path: Override the user config path (for testing).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A plain dict with all configuration values.
|
|
||||||
"""
|
|
||||||
config = dict(DEFAULTS)
|
|
||||||
|
|
||||||
# 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})
|
|
||||||
|
|
||||||
# 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",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
"""Hindsight-Hermes error types."""
|
|
||||||
|
|
||||||
|
|
||||||
class HindsightError(Exception):
|
|
||||||
"""Exception raised when a Hindsight memory operation fails."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
@ -1,504 +0,0 @@
|
||||||
"""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 load_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."""
|
|
||||||
if client is not None:
|
|
||||||
return client
|
|
||||||
|
|
||||||
if hindsight_api_url is None:
|
|
||||||
raise HindsightError(
|
|
||||||
"No Hindsight API URL configured. "
|
|
||||||
"Pass client= or hindsight_api_url=, or create ~/.hindsight/hermes.json."
|
|
||||||
)
|
|
||||||
|
|
||||||
kwargs: dict[str, Any] = {"base_url": hindsight_api_url, "timeout": 30.0}
|
|
||||||
if api_key:
|
|
||||||
kwargs["api_key"] = api_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()
|
|
||||||
|
|
||||||
async def _ensure_bank(bid: str) -> None:
|
|
||||||
if bid in created_banks:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
await resolved_client.acreate_bank(bank_id=bid, name=bid)
|
|
||||||
created_banks.add(bid)
|
|
||||||
except Exception:
|
|
||||||
created_banks.add(bid)
|
|
||||||
|
|
||||||
async def handle_retain(args: dict[str, Any], **kwargs: Any) -> str:
|
|
||||||
try:
|
|
||||||
bid = _resolve_bank_id(args, bank_id, bank_resolver)
|
|
||||||
await _ensure_bank(bid)
|
|
||||||
retain_kwargs: dict[str, Any] = {"bank_id": bid, "content": args["content"]}
|
|
||||||
if tags:
|
|
||||||
retain_kwargs["tags"] = tags
|
|
||||||
await resolved_client.aretain(**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)})
|
|
||||||
|
|
||||||
async 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 = await resolved_client.arecall(**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)})
|
|
||||||
|
|
||||||
async 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 = await resolved_client.areflect(**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 ``~/.hindsight/hermes.json`` and environment
|
|
||||||
variables (env vars take priority), then registers tools and hooks.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ctx: Hermes PluginContext.
|
|
||||||
"""
|
|
||||||
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: not configured (need hindsightApiUrl or hindsightApiToken). "
|
|
||||||
"Create ~/.hindsight/hermes.json or set HINDSIGHT_API_URL."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
resolved_client = _resolve_client(None, hindsight_api_url, api_key)
|
|
||||||
created_banks: set[str] = set()
|
|
||||||
|
|
||||||
async def _ensure_bank(bid: str) -> None:
|
|
||||||
if bid in created_banks:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
await resolved_client.acreate_bank(bank_id=bid, name=bid)
|
|
||||||
created_banks.add(bid)
|
|
||||||
except Exception:
|
|
||||||
created_banks.add(bid)
|
|
||||||
|
|
||||||
async def handle_retain(args: dict[str, Any], **kwargs: Any) -> str:
|
|
||||||
try:
|
|
||||||
bid = _resolve_bank_id(args, bank_id, None)
|
|
||||||
await _ensure_bank(bid)
|
|
||||||
await resolved_client.aretain(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)})
|
|
||||||
|
|
||||||
async def handle_recall(args: dict[str, Any], **kwargs: Any) -> str:
|
|
||||||
try:
|
|
||||||
bid = _resolve_bank_id(args, bank_id, None)
|
|
||||||
response = await resolved_client.arecall(
|
|
||||||
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)})
|
|
||||||
|
|
||||||
async def handle_reflect(args: dict[str, Any], **kwargs: Any) -> str:
|
|
||||||
try:
|
|
||||||
bid = _resolve_bank_id(args, bank_id, None)
|
|
||||||
response = await resolved_client.areflect(
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ── Lifecycle hooks ──────────────────────────────────────────────────
|
|
||||||
# These require hermes-agent ≥ 0.5.0 which invokes pre/post_llm_call.
|
|
||||||
# On older hermes-agent the hooks are simply never called, so
|
|
||||||
# registering them is always safe.
|
|
||||||
#
|
|
||||||
# IMPORTANT: hermes-agent calls hooks synchronously via invoke_hook(),
|
|
||||||
# so these must be sync functions. We use the sync client methods
|
|
||||||
# (recall / retain / create_bank) rather than the async variants.
|
|
||||||
|
|
||||||
recall_budget = cfg.get("recallBudget", budget)
|
|
||||||
recall_max_tokens = cfg.get("recallMaxTokens", 4096)
|
|
||||||
retain_enabled = cfg.get("autoRetain", True)
|
|
||||||
recall_preamble = cfg.get("recallPromptPreamble", "")
|
|
||||||
|
|
||||||
created_banks_sync: set[str] = set()
|
|
||||||
|
|
||||||
def _ensure_bank_sync(bid: str) -> None:
|
|
||||||
if bid in created_banks_sync:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
resolved_client.create_bank(bank_id=bid, name=bid)
|
|
||||||
created_banks_sync.add(bid)
|
|
||||||
except Exception:
|
|
||||||
created_banks_sync.add(bid)
|
|
||||||
|
|
||||||
def _on_pre_llm_call(
|
|
||||||
*,
|
|
||||||
session_id: str = "",
|
|
||||||
user_message: str = "",
|
|
||||||
conversation_history: list | None = None,
|
|
||||||
is_first_turn: bool = False,
|
|
||||||
model: str = "",
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> dict[str, str] | None:
|
|
||||||
"""Recall relevant memories and inject them as system prompt context."""
|
|
||||||
if not user_message or not bank_id:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
_ensure_bank_sync(bank_id)
|
|
||||||
response = resolved_client.recall(
|
|
||||||
bank_id=bank_id,
|
|
||||||
query=user_message,
|
|
||||||
budget=recall_budget,
|
|
||||||
max_tokens=recall_max_tokens,
|
|
||||||
)
|
|
||||||
if not response.results:
|
|
||||||
return None
|
|
||||||
lines = [f"- {r.text}" for r in response.results]
|
|
||||||
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."
|
|
||||||
)
|
|
||||||
context = header + "\n\n" + "\n".join(lines)
|
|
||||||
return {"context": context}
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning("Hindsight pre_llm_call recall failed: %s", exc)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _on_post_llm_call(
|
|
||||||
*,
|
|
||||||
session_id: str = "",
|
|
||||||
user_message: str = "",
|
|
||||||
assistant_response: str = "",
|
|
||||||
model: str = "",
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> None:
|
|
||||||
"""Retain the conversation turn so it can be recalled in future sessions."""
|
|
||||||
if not retain_enabled or not bank_id:
|
|
||||||
return
|
|
||||||
if not user_message or not assistant_response:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
_ensure_bank_sync(bank_id)
|
|
||||||
content = f"User: {user_message}\nAssistant: {assistant_response}"
|
|
||||||
resolved_client.retain(bank_id=bank_id, content=content)
|
|
||||||
except Exception as exc:
|
|
||||||
logger.warning("Hindsight post_llm_call retain failed: %s", exc)
|
|
||||||
|
|
||||||
ctx.register_hook("pre_llm_call", _on_pre_llm_call)
|
|
||||||
ctx.register_hook("post_llm_call", _on_post_llm_call)
|
|
||||||
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
[project]
|
|
||||||
name = "hindsight-hermes"
|
|
||||||
version = "0.5.0"
|
|
||||||
description = "Hermes agent integration for Hindsight - persistent memory tools for AI agents"
|
|
||||||
readme = "README.md"
|
|
||||||
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",
|
|
||||||
"pytest-asyncio>=0.23.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.uv.sources]
|
|
||||||
# hermes-agent 0.5.0 is not on PyPI yet; pin to the release tag
|
|
||||||
hermes-agent = { git = "https://github.com/NousResearch/hermes-agent.git", tag = "v2026.3.28" }
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
testpaths = ["tests"]
|
|
||||||
asyncio_mode = "auto"
|
|
||||||
|
|
||||||
[dependency-groups]
|
|
||||||
dev = [
|
|
||||||
"pytest>=9.0.2",
|
|
||||||
"pytest-asyncio>=0.23.0",
|
|
||||||
"hermes-agent>=0.5.0 ; python_version >= '3.11'",
|
|
||||||
]
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
"""Tests for hindsight_hermes.config module."""
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from hindsight_hermes.config import DEFAULTS, load_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 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_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_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_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_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_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_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_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
|
|
||||||
|
|
@ -1,478 +0,0 @@
|
||||||
"""Tests for hindsight_hermes.tools module."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from types import SimpleNamespace
|
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from hindsight_hermes.config import DEFAULTS
|
|
||||||
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_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()
|
|
||||||
def mock_client():
|
|
||||||
client = MagicMock()
|
|
||||||
# Sync methods (used by memory_instructions and register_tools sync path)
|
|
||||||
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"))
|
|
||||||
# Async methods (used by tool handlers and hooks)
|
|
||||||
client.acreate_bank = AsyncMock()
|
|
||||||
client.aretain = AsyncMock()
|
|
||||||
client.arecall = AsyncMock(
|
|
||||||
return_value=SimpleNamespace(
|
|
||||||
results=[
|
|
||||||
SimpleNamespace(text="Memory 1"),
|
|
||||||
SimpleNamespace(text="Memory 2"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
client.areflect = AsyncMock(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_creates_client_from_args(self):
|
|
||||||
with patch("hindsight_hermes.tools.Hindsight") as MockH:
|
|
||||||
_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):
|
|
||||||
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"}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async 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(await handler({"content": "hello"}))
|
|
||||||
assert result["result"] == "Memory stored successfully."
|
|
||||||
mock_client.aretain.assert_called_once_with(bank_id="b", content="hello")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async 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"]
|
|
||||||
await handler({"content": "hello"})
|
|
||||||
mock_client.aretain.assert_called_once_with(bank_id="b", content="hello", tags=["tag1"])
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async 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(await handler({"query": "test"}))
|
|
||||||
assert "Memory 1" in result["result"]
|
|
||||||
assert "Memory 2" in result["result"]
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_recall_handler_no_results(self, mock_client, mock_registry):
|
|
||||||
mock_client.arecall.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(await handler({"query": "test"}))
|
|
||||||
assert result["result"] == "No relevant memories found."
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async 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(await handler({"query": "test"}))
|
|
||||||
assert result["result"] == "Synthesized answer"
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_handler_returns_error_on_exception(self, mock_client, mock_registry):
|
|
||||||
mock_client.aretain.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(await handler({"content": "hello"}))
|
|
||||||
assert "error" in result
|
|
||||||
assert "boom" in result["error"]
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async 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"]
|
|
||||||
await handler({"content": "hello"})
|
|
||||||
mock_client.acreate_bank.assert_called_once_with(bank_id="b", name="b")
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async 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"]
|
|
||||||
await handler({"content": "first"})
|
|
||||||
await handler({"content": "second"})
|
|
||||||
# acreate_bank should only be called once
|
|
||||||
mock_client.acreate_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):
|
|
||||||
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()
|
|
||||||
|
|
||||||
def test_register_hooks(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)
|
|
||||||
hook_names = {call.args[0] for call in ctx.register_hook.call_args_list}
|
|
||||||
assert hook_names == {"pre_llm_call", "post_llm_call"}
|
|
||||||
|
|
||||||
|
|
||||||
# --- lifecycle hook tests ---
|
|
||||||
|
|
||||||
|
|
||||||
class TestLifecycleHooks:
|
|
||||||
"""Tests for pre_llm_call and post_llm_call hook callbacks.
|
|
||||||
|
|
||||||
Hooks are synchronous functions (hermes-agent 0.5.0 calls them via
|
|
||||||
invoke_hook which is sync), so these tests call them directly without await.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _get_hook(self, ctx_mock, hook_name: str):
|
|
||||||
"""Extract the registered hook callback by name from the mock ctx."""
|
|
||||||
for call in ctx_mock.register_hook.call_args_list:
|
|
||||||
if call.args[0] == hook_name:
|
|
||||||
return call.args[1]
|
|
||||||
raise AssertionError(f"Hook {hook_name!r} not registered")
|
|
||||||
|
|
||||||
def _register_with_hooks(self, monkeypatch, mock_client, **env_overrides):
|
|
||||||
monkeypatch.setenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
|
||||||
monkeypatch.setenv("HINDSIGHT_BANK_ID", "test-bank")
|
|
||||||
for k, v in env_overrides.items():
|
|
||||||
monkeypatch.setenv(k, v)
|
|
||||||
ctx = MagicMock()
|
|
||||||
with patch("hindsight_hermes.tools._resolve_client", return_value=mock_client):
|
|
||||||
register(ctx)
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
# -- pre_llm_call --
|
|
||||||
|
|
||||||
def test_pre_llm_call_returns_context(self, monkeypatch, mock_client):
|
|
||||||
ctx = self._register_with_hooks(monkeypatch, mock_client)
|
|
||||||
hook = self._get_hook(ctx, "pre_llm_call")
|
|
||||||
result = hook(
|
|
||||||
session_id="s1",
|
|
||||||
user_message="what color do I like?",
|
|
||||||
conversation_history=[],
|
|
||||||
is_first_turn=True,
|
|
||||||
model="test",
|
|
||||||
)
|
|
||||||
assert result is not None
|
|
||||||
assert "context" in result
|
|
||||||
assert "Memory 1" in result["context"]
|
|
||||||
assert "Memory 2" in result["context"]
|
|
||||||
mock_client.recall.assert_called_once()
|
|
||||||
|
|
||||||
def test_pre_llm_call_returns_none_on_no_results(self, monkeypatch, mock_client):
|
|
||||||
mock_client.recall.return_value = SimpleNamespace(results=[])
|
|
||||||
ctx = self._register_with_hooks(monkeypatch, mock_client)
|
|
||||||
hook = self._get_hook(ctx, "pre_llm_call")
|
|
||||||
result = hook(user_message="hello")
|
|
||||||
assert result is None
|
|
||||||
|
|
||||||
def test_pre_llm_call_returns_none_on_empty_message(self, monkeypatch, mock_client):
|
|
||||||
ctx = self._register_with_hooks(monkeypatch, mock_client)
|
|
||||||
hook = self._get_hook(ctx, "pre_llm_call")
|
|
||||||
result = hook(user_message="")
|
|
||||||
assert result is None
|
|
||||||
mock_client.recall.assert_not_called()
|
|
||||||
|
|
||||||
def test_pre_llm_call_returns_none_on_error(self, monkeypatch, mock_client):
|
|
||||||
mock_client.recall.side_effect = RuntimeError("connection failed")
|
|
||||||
ctx = self._register_with_hooks(monkeypatch, mock_client)
|
|
||||||
hook = self._get_hook(ctx, "pre_llm_call")
|
|
||||||
result = hook(user_message="hello")
|
|
||||||
assert result is None
|
|
||||||
|
|
||||||
# -- post_llm_call --
|
|
||||||
|
|
||||||
def test_post_llm_call_retains_turn(self, monkeypatch, mock_client):
|
|
||||||
ctx = self._register_with_hooks(monkeypatch, mock_client)
|
|
||||||
hook = self._get_hook(ctx, "post_llm_call")
|
|
||||||
hook(
|
|
||||||
session_id="s1",
|
|
||||||
user_message="remember I like green",
|
|
||||||
assistant_response="Got it, you like green!",
|
|
||||||
model="test",
|
|
||||||
)
|
|
||||||
mock_client.retain.assert_called_once()
|
|
||||||
content = mock_client.retain.call_args.kwargs["content"]
|
|
||||||
assert "remember I like green" in content
|
|
||||||
assert "Got it, you like green!" in content
|
|
||||||
|
|
||||||
def test_post_llm_call_skips_empty_messages(self, monkeypatch, mock_client):
|
|
||||||
ctx = self._register_with_hooks(monkeypatch, mock_client)
|
|
||||||
hook = self._get_hook(ctx, "post_llm_call")
|
|
||||||
hook(user_message="", assistant_response="hello")
|
|
||||||
mock_client.retain.assert_not_called()
|
|
||||||
|
|
||||||
def test_post_llm_call_skips_when_disabled(self, monkeypatch, mock_client):
|
|
||||||
ctx = self._register_with_hooks(
|
|
||||||
monkeypatch, mock_client, HINDSIGHT_AUTO_RETAIN="false"
|
|
||||||
)
|
|
||||||
hook = self._get_hook(ctx, "post_llm_call")
|
|
||||||
hook(user_message="hi", assistant_response="hello")
|
|
||||||
mock_client.retain.assert_not_called()
|
|
||||||
|
|
||||||
def test_post_llm_call_does_not_raise_on_error(self, monkeypatch, mock_client):
|
|
||||||
mock_client.retain.side_effect = RuntimeError("boom")
|
|
||||||
ctx = self._register_with_hooks(monkeypatch, mock_client)
|
|
||||||
hook = self._get_hook(ctx, "post_llm_call")
|
|
||||||
# Should not raise
|
|
||||||
hook(user_message="hi", assistant_response="hello")
|
|
||||||
|
|
||||||
|
|
||||||
# --- 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:")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Integration tests with real hermes-agent plugin system ---
|
|
||||||
|
|
||||||
|
|
||||||
class TestHermesPluginIntegration:
|
|
||||||
"""Tests that verify hooks work correctly when invoked through
|
|
||||||
hermes-agent's real PluginManager.invoke_hook (sync dispatch)."""
|
|
||||||
|
|
||||||
def _setup_plugin_manager(self, monkeypatch, mock_client):
|
|
||||||
"""Register our plugin via the real PluginContext and return the PluginManager."""
|
|
||||||
from hermes_cli.plugins import PluginManager, PluginManifest, PluginContext
|
|
||||||
|
|
||||||
monkeypatch.setenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
|
||||||
monkeypatch.setenv("HINDSIGHT_BANK_ID", "test-bank")
|
|
||||||
|
|
||||||
manager = PluginManager()
|
|
||||||
manifest = PluginManifest(name="hindsight", source="test")
|
|
||||||
ctx = PluginContext(manifest, manager)
|
|
||||||
|
|
||||||
with (
|
|
||||||
patch("hindsight_hermes.tools._resolve_client", return_value=mock_client),
|
|
||||||
patch.dict(sys.modules, {"tools": MagicMock(), "tools.registry": MagicMock()}),
|
|
||||||
):
|
|
||||||
register(ctx)
|
|
||||||
|
|
||||||
return manager
|
|
||||||
|
|
||||||
def test_invoke_pre_llm_call_returns_context_dict(self, monkeypatch, mock_client):
|
|
||||||
"""invoke_hook('pre_llm_call') should return a list with a context dict."""
|
|
||||||
manager = self._setup_plugin_manager(monkeypatch, mock_client)
|
|
||||||
|
|
||||||
results = manager.invoke_hook(
|
|
||||||
"pre_llm_call",
|
|
||||||
session_id="s1",
|
|
||||||
user_message="what is my favorite color?",
|
|
||||||
conversation_history=[],
|
|
||||||
is_first_turn=True,
|
|
||||||
model="test",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(results) == 1
|
|
||||||
assert isinstance(results[0], dict)
|
|
||||||
assert "context" in results[0]
|
|
||||||
assert "Memory 1" in results[0]["context"]
|
|
||||||
mock_client.recall.assert_called_once()
|
|
||||||
|
|
||||||
def test_invoke_post_llm_call_retains(self, monkeypatch, mock_client):
|
|
||||||
"""invoke_hook('post_llm_call') should call retain synchronously."""
|
|
||||||
manager = self._setup_plugin_manager(monkeypatch, mock_client)
|
|
||||||
|
|
||||||
results = manager.invoke_hook(
|
|
||||||
"post_llm_call",
|
|
||||||
session_id="s1",
|
|
||||||
user_message="remember I like blue",
|
|
||||||
assistant_response="Noted, you like blue!",
|
|
||||||
model="test",
|
|
||||||
)
|
|
||||||
|
|
||||||
# post_llm_call returns None, so results should be empty
|
|
||||||
assert results == []
|
|
||||||
mock_client.retain.assert_called_once()
|
|
||||||
content = mock_client.retain.call_args.kwargs["content"]
|
|
||||||
assert "remember I like blue" in content
|
|
||||||
|
|
||||||
def test_invoke_pre_llm_call_no_results(self, monkeypatch, mock_client):
|
|
||||||
"""invoke_hook returns empty list when recall finds nothing."""
|
|
||||||
mock_client.recall.return_value = SimpleNamespace(results=[])
|
|
||||||
manager = self._setup_plugin_manager(monkeypatch, mock_client)
|
|
||||||
|
|
||||||
results = manager.invoke_hook(
|
|
||||||
"pre_llm_call",
|
|
||||||
session_id="s1",
|
|
||||||
user_message="hello",
|
|
||||||
conversation_history=[],
|
|
||||||
is_first_turn=True,
|
|
||||||
model="test",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert results == []
|
|
||||||
|
|
||||||
def test_hooks_are_not_coroutines(self, monkeypatch, mock_client):
|
|
||||||
"""Hooks must be plain functions, not async — verify invoke_hook
|
|
||||||
doesn't return coroutine objects."""
|
|
||||||
manager = self._setup_plugin_manager(monkeypatch, mock_client)
|
|
||||||
|
|
||||||
results = manager.invoke_hook(
|
|
||||||
"pre_llm_call",
|
|
||||||
session_id="s1",
|
|
||||||
user_message="test",
|
|
||||||
conversation_history=[],
|
|
||||||
is_first_turn=True,
|
|
||||||
model="test",
|
|
||||||
)
|
|
||||||
|
|
||||||
for r in results:
|
|
||||||
assert not asyncio.iscoroutine(r), (
|
|
||||||
"Hook returned a coroutine — hermes invoke_hook is sync and "
|
|
||||||
"will not await it"
|
|
||||||
)
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -13,7 +13,7 @@ print_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
VALID_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ag2" "ai-sdk" "chat" "openclaw" "langgraph" "llamaindex" "nemoclaw" "strands" "claude-code" "codex" "hermes" "autogen" "paperclip" "opencode")
|
VALID_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ag2" "ai-sdk" "chat" "openclaw" "langgraph" "llamaindex" "nemoclaw" "strands" "claude-code" "codex" "autogen" "paperclip" "opencode")
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
print_error "Usage: $0 <integration> <version>"
|
print_error "Usage: $0 <integration> <version>"
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
hide_table_of_contents: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# Hermes Integration Changelog
|
|
||||||
|
|
||||||
Changelog for [`hindsight-hermes`](https://pypi.org/project/hindsight-hermes/).
|
|
||||||
|
|
||||||
For the source code, see [`hindsight-integrations/hermes`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/hermes).
|
|
||||||
|
|
||||||
← [Back to main changelog](../index.md)
|
|
||||||
|
|
||||||
## [0.5.0](https://github.com/vectorize-io/hindsight/tree/integrations/hermes/v0.5.0)
|
|
||||||
|
|
||||||
**Features**
|
|
||||||
|
|
||||||
- Added the Hermes Agent integration for Hindsight. ([`ef90842f`](https://github.com/vectorize-io/hindsight/commit/ef90842f))
|
|
||||||
|
|
||||||
**Improvements**
|
|
||||||
|
|
||||||
- Enabled file-based configuration for the Hermes integration. ([`0ff36548`](https://github.com/vectorize-io/hindsight/commit/0ff36548))
|
|
||||||
|
|
||||||
**Bug Fixes**
|
|
||||||
|
|
||||||
- Fixed potential event loop deadlocks by using asynchronous Hermes client calls. ([`35dfd3aa`](https://github.com/vectorize-io/hindsight/commit/35dfd3aa))
|
|
||||||
- Synchronized lifecycle hook behavior for compatibility with hermes-agent 0.5.0. ([`e7c9a683`](https://github.com/vectorize-io/hindsight/commit/e7c9a683))
|
|
||||||
Loading…
Reference in a new issue