Compare commits

...
Author SHA1 Message Date
Ben 4f4b844777 feat(omnigent): add Omnigent integration (Hindsight memory as function tools)
Exposes Hindsight retain/recall/reflect as Omnigent `type: function` tools —
plain Python callables an agent declares in its YAML by dotted path. Omnigent
passes function tools no session context, so the bank/connection come from
configure() or HINDSIGHT_* env vars (one bank per agent process).

- hindsight_omnigent: retain/recall/reflect callables, configure()/env config,
  memory_instructions() prompt-seed helper, tools_yaml() YAML emitter
- tests: faithful in-test replica of Omnigent's LocalCallableTool.invoke +
  real-omnigent guard test (skipif-gated) + live-Hindsight E2E bucket
- CI job, release-integration.sh, generate_changelog.py wiring
- docs gallery card + doc page + icon; README + example agent.yaml
2026-06-15 16:59:41 -04:00
19 changed files with 2617 additions and 1 deletions
+50
View File
@@ -65,6 +65,7 @@ jobs:
integrations-google-adk: ${{ steps.filter.outputs.integrations-google-adk }}
integrations-obsidian: ${{ steps.filter.outputs.integrations-obsidian }}
integrations-omo: ${{ steps.filter.outputs.integrations-omo }}
integrations-omnigent: ${{ steps.filter.outputs.integrations-omnigent }}
integrations-haystack: ${{ steps.filter.outputs.integrations-haystack }}
tools-agent-sdk: ${{ steps.filter.outputs.tools-agent-sdk }}
integrations-roo-code: ${{ steps.filter.outputs.integrations-roo-code }}
@@ -196,6 +197,8 @@ jobs:
- 'hindsight-integrations/obsidian/**'
integrations-omo:
- 'hindsight-integrations/omo/**'
integrations-omnigent:
- 'hindsight-integrations/omnigent/**'
tools-agent-sdk:
- 'hindsight-tools/hindsight-agent-sdk/**'
integrations-roo-code:
@@ -508,6 +511,52 @@ jobs:
working-directory: ./hindsight-integrations/omo
run: python -m pytest tests/ -v
test-omnigent-integration:
needs: [detect-changes]
if: >-
(github.event_name == 'workflow_dispatch' ||
needs.detect-changes.outputs.integrations-omnigent == 'true' ||
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
timeout-minutes: 30
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 omnigent integration
working-directory: ./hindsight-integrations/omnigent
run: uv build
- name: Install dependencies
working-directory: ./hindsight-integrations/omnigent
run: uv sync --frozen
- name: Lint
working-directory: ./hindsight-integrations/omnigent
run: uv run ruff check .
- name: Run tests
working-directory: ./hindsight-integrations/omnigent
# PR CI runs only the deterministic bucket; the real-LLM E2E bucket
# (requires_real_llm) needs a live Hindsight server and runs separately.
# The real-omnigent guard tests skip here (omnigent is a 3.12-only alpha,
# not a CI dep); the Omnigent invoke contract is covered by a faithful
# in-test replica plus those guard tests when omnigent is installed.
run: uv run pytest tests -v -m "not requires_real_llm"
test-cline-integration:
needs: [detect-changes]
if: >-
@@ -4588,6 +4637,7 @@ jobs:
- test-ai-sdk-integration-deno
- test-opencode-integration
- test-omo-integration
- test-omnigent-integration
- test-cloudflare-oauth-proxy-integration
- build-chat-integration
- test-paperclip-integration
@@ -75,6 +75,7 @@ INTEGRATIONS: dict[str, IntegrationMeta] = {
"haystack": IntegrationMeta("hindsight-haystack", "Haystack"),
"roo-code": IntegrationMeta("hindsight-roo-code", "Roo Code"),
"omo": IntegrationMeta("hindsight-omo", "OMO"),
"omnigent": IntegrationMeta("hindsight-omnigent", "Omnigent"),
}
VALID_INTEGRATIONS = list(INTEGRATIONS.keys())
@@ -0,0 +1,155 @@
---
sidebar_position: 37
title: "Omnigent Persistent Memory with Hindsight | Integration Guide"
description: "Add long-term memory to Omnigent agents. Hindsight's retain, recall, and reflect register as Omnigent type: function tools your agent declares in its YAML."
---
# Omnigent
Persistent memory for [Omnigent](https://github.com/omnigent-ai/omnigent) agents via Hindsight.
Exposes Hindsight's **retain**, **recall**, and **reflect** operations as Omnigent
`type: function` tools — plain Python callables your agent declares in its YAML and the LLM
can call directly.
Omnigent invokes function tools with **no session context**, so the Hindsight bank and
connection are configured once per agent process (via `configure()` or `HINDSIGHT_*` env
vars) rather than per call. The natural model is one memory bank per agent.
## Features
- **Omnigent function tools** — `retain` / `recall` / `reflect` referenced by dotted path
- **Drop-in YAML** — `tools_yaml()` emits a ready-to-paste `tools:` block with correct schemas
- **Configure once** — set the URL, key, and bank globally or through env vars
- **System-prompt injection** — `memory_instructions()` pre-recalls memories for the agent prompt
## Installation
```bash
pip install hindsight-omnigent
```
## Quick Start
:::tip Recommended: Hindsight Cloud
[Sign up free](https://ui.hindsight.vectorize.io/signup) and grab an API key — no self-hosting required.
:::
**1. Configure Hindsight** in the Python module that hosts your agent's tools:
```python
# my_agent/tools.py
from hindsight_omnigent import configure
# Re-export the callables so Omnigent can resolve them by dotted path.
from hindsight_omnigent.tools import recall, reflect, retain # noqa: F401
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...", # or set HINDSIGHT_API_KEY
bank_id="user-123", # or set HINDSIGHT_BANK_ID
)
```
**2. Declare the tools** in your `agent.yaml`. Generate the block with
`python -c "from hindsight_omnigent import tools_yaml; print(tools_yaml())"`:
```yaml
name: memory_agent
prompt: You are a helpful assistant with long-term memory.
executor:
harness: claude-sdk
tools:
hindsight_retain:
type: function
description: Store information in long-term memory for later retrieval.
callable: hindsight_omnigent.tools.retain
parameters: {"type": "object", "properties": {"content": {"type": "string"}}, "required": ["content"]}
hindsight_recall:
type: function
description: Search long-term memory for relevant information.
callable: hindsight_omnigent.tools.recall
parameters: {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
hindsight_reflect:
type: function
description: Synthesize a reasoned answer from long-term memories.
callable: hindsight_omnigent.tools.reflect
parameters: {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
```
**3. Run it:**
```bash
omnigent run path/to/agent.yaml
```
The `callable` paths must be importable from where Omnigent runs the agent. Importing
`my_agent.tools` triggers the `configure()` call above; alternatively, skip `configure()`
and set `HINDSIGHT_API_KEY` / `HINDSIGHT_BANK_ID` in the agent's `os_env`.
## Configuration via environment variables
`configure()` is optional — the tools read these on first use:
| Variable | Description |
| -------------------- | -------------------------------------------- |
| `HINDSIGHT_API_URL` | Hindsight API URL (default: Hindsight Cloud) |
| `HINDSIGHT_API_KEY` | API key for authentication |
| `HINDSIGHT_BANK_ID` | Memory bank the tools read/write |
### Self-hosting (local development)
If you're running Hindsight locally with `./scripts/dev/start-api.sh`, point at it:
```python
configure(hindsight_api_url="http://localhost:8888", bank_id="user-123")
```
See the [installation guide](/developer/installation) for self-hosting setup.
## Seeding the prompt
Omnigent has no pre-turn context hook, so to start the agent already knowing what Hindsight
remembers, pre-recall and splice it into the prompt:
```python
from hindsight_omnigent import memory_instructions
context = memory_instructions(query="what we know about the user", max_results=5)
prompt = f"You are a helpful assistant.\n\n{context}" if context else "You are a helpful assistant."
```
## Selecting tools
Generate only the tools you need:
```python
from hindsight_omnigent import tools_yaml
print(tools_yaml(enable_retain=True, enable_recall=True, enable_reflect=False))
```
## `configure()` Reference
| Parameter | Default | Description |
| ------------------- | ------------------------------------------------------ | ------------------------------------------ |
| `hindsight_api_url` | `HINDSIGHT_API_URL` env, else Hindsight Cloud | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `bank_id` | `HINDSIGHT_BANK_ID` env | Memory bank the tools read/write |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Max tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode |
| `client` | `None` | Pre-built Hindsight client (overrides URL/key) |
## Requirements
- Python >= 3.10
- hindsight-client >= 0.4.0
- A running Hindsight API server (or Hindsight Cloud)
- [Omnigent](https://github.com/omnigent-ai/omnigent) (Python >= 3.12) to run the agent
:::note
Omnigent is an early-stage (alpha) project and its agent spec is still evolving. This
integration targets the `type: function` tool contract in Omnigent 0.1.x.
:::
+10
View File
@@ -1,5 +1,15 @@
{
"integrations": [
{
"id": "omnigent",
"name": "Omnigent",
"description": "Persistent memory for Omnigent agents. Hindsight's retain, recall, and reflect drop into an agent YAML as type: function tools.",
"type": "official",
"by": "hindsight",
"category": "framework",
"link": "/sdks/integrations/omnigent",
"icon": "/img/icons/omnigent.png"
},
{
"id": "litellm",
"name": "LiteLLM",
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,9 @@
.venv/
__pycache__/
*.pyc
.pytest_cache/
.ruff_cache/
dist/
build/
*.egg-info/
.env
+137
View File
@@ -0,0 +1,137 @@
# hindsight-omnigent
Persistent memory for [Omnigent](https://github.com/omnigent-ai/omnigent) agents via Hindsight.
Exposes Hindsight's **retain**, **recall**, and **reflect** operations as Omnigent
`type: function` tools — plain Python callables your agent declares in its YAML and the LLM
can call directly.
Omnigent invokes function tools with **no session context**, so the Hindsight bank and
connection are configured once per agent process (via `configure()` or `HINDSIGHT_*` env
vars) rather than per call.
## Features
- **Omnigent function tools** — `retain` / `recall` / `reflect` referenced by dotted path
- **Drop-in YAML** — `tools_yaml()` emits a ready-to-paste `tools:` block with correct schemas
- **Configure once** — set the URL, key, and bank globally or through env vars
- **System-prompt injection** — `memory_instructions()` pre-recalls memories for the agent prompt
## Installation
```bash
pip install hindsight-omnigent
```
## Quick start
> **Recommended: [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup)** — free tier, no self-hosting required.
**1. Configure Hindsight** in the Python module that hosts your agent's tools:
```python
# my_agent/tools.py
from hindsight_omnigent import configure
# Re-exports retain / recall / reflect so Omnigent can resolve them by dotted path.
from hindsight_omnigent.tools import recall, reflect, retain # noqa: F401
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...", # or set HINDSIGHT_API_KEY
bank_id="user-123", # or set HINDSIGHT_BANK_ID
)
```
**2. Declare the tools** in your `agent.yaml`. Generate the block with:
```python
from hindsight_omnigent import tools_yaml
print(tools_yaml())
```
```yaml
name: memory_agent
prompt: You are a helpful assistant with long-term memory. Use the Hindsight tools to remember and recall facts about the user.
executor:
harness: claude-sdk
tools:
hindsight_retain:
type: function
description: Store information in long-term memory for later retrieval.
callable: hindsight_omnigent.tools.retain
parameters: {"type": "object", "properties": {"content": {"type": "string", "description": "The information to store in long-term memory."}}, "required": ["content"]}
hindsight_recall:
type: function
description: Search long-term memory for relevant information.
callable: hindsight_omnigent.tools.recall
parameters: {"type": "object", "properties": {"query": {"type": "string", "description": "The search query to find relevant memories."}}, "required": ["query"]}
hindsight_reflect:
type: function
description: Synthesize a reasoned answer from long-term memories.
callable: hindsight_omnigent.tools.reflect
parameters: {"type": "object", "properties": {"query": {"type": "string", "description": "The question to reflect on using stored memories."}}, "required": ["query"]}
```
**3. Run it:**
```bash
omnigent run path/to/agent.yaml
```
> The `callable` paths must be importable from where Omnigent runs the agent. Importing
> `my_agent.tools` triggers the `configure()` call above; alternatively, skip `configure()`
> entirely and set `HINDSIGHT_API_KEY` / `HINDSIGHT_BANK_ID` in the agent's `os_env`.
## Configuration via environment variables
`configure()` is optional — the tools read these on first use:
| Variable | Description |
| -------------------- | -------------------------------------------- |
| `HINDSIGHT_API_URL` | Hindsight API URL (default: Hindsight Cloud) |
| `HINDSIGHT_API_KEY` | API key for authentication |
| `HINDSIGHT_BANK_ID` | Memory bank the tools read/write |
## `configure()` reference
| Parameter | Default | Description |
| ------------------- | ------------------------------------------------------ | ------------------------------------------ |
| `hindsight_api_url` | `HINDSIGHT_API_URL` env, else Hindsight Cloud | Hindsight API URL |
| `api_key` | `HINDSIGHT_API_KEY` env | API key for authentication |
| `bank_id` | `HINDSIGHT_BANK_ID` env | Memory bank the tools read/write |
| `budget` | `"mid"` | Recall/reflect budget level (low/mid/high) |
| `max_tokens` | `4096` | Max tokens for recall results |
| `tags` | `None` | Tags applied when storing memories |
| `recall_tags` | `None` | Tags to filter when searching |
| `recall_tags_match` | `"any"` | Tag matching mode |
| `client` | `None` | Pre-built Hindsight client (overrides URL/key) |
## Seeding the prompt
Omnigent has no pre-turn context hook, so to start the agent already knowing what Hindsight
remembers, pre-recall and splice it into the prompt:
```python
from hindsight_omnigent import memory_instructions
context = memory_instructions(query="what we know about the user", max_results=5)
prompt = f"You are a helpful assistant.\n\n{context}" if context else "You are a helpful assistant."
```
## Selecting tools
Generate only the tools you need:
```python
tools_yaml(enable_retain=True, enable_recall=True, enable_reflect=False)
```
## Requirements
- Python >= 3.10
- hindsight-client >= 0.4.0
- A running Hindsight API server (or Hindsight Cloud)
- [Omnigent](https://github.com/omnigent-ai/omnigent) (Python >= 3.12) to run the agent
> **Note:** Omnigent is an early-stage (alpha) project and its agent spec is still evolving.
> This integration targets the `type: function` tool contract in Omnigent 0.1.x.
@@ -0,0 +1,32 @@
# Example Omnigent agent with Hindsight long-term memory.
#
# The `callable` paths resolve to hindsight_omnigent.tools.{retain,recall,reflect}.
# Configure the Hindsight connection + bank either by importing a module that calls
# hindsight_omnigent.configure(), or by setting HINDSIGHT_API_KEY / HINDSIGHT_BANK_ID
# in the agent's environment.
#
# Run with: omnigent run examples/agent.yaml
name: memory_agent
prompt: |
You are a helpful assistant with long-term memory.
- Call hindsight_recall before answering, to check what you already know about the user.
- Call hindsight_retain whenever the user shares a durable fact, preference, or decision.
- Call hindsight_reflect when asked to summarize or reason about what you know.
executor:
harness: claude-sdk
tools:
hindsight_retain:
type: function
description: Store information in long-term memory for later retrieval.
callable: hindsight_omnigent.tools.retain
parameters: {"type": "object", "properties": {"content": {"type": "string", "description": "The information to store in long-term memory."}}, "required": ["content"]}
hindsight_recall:
type: function
description: Search long-term memory for relevant information.
callable: hindsight_omnigent.tools.recall
parameters: {"type": "object", "properties": {"query": {"type": "string", "description": "The search query to find relevant memories."}}, "required": ["query"]}
hindsight_reflect:
type: function
description: Synthesize a reasoned answer from long-term memories.
callable: hindsight_omnigent.tools.reflect
parameters: {"type": "object", "properties": {"query": {"type": "string", "description": "The question to reflect on using stored memories."}}, "required": ["query"]}
@@ -0,0 +1,54 @@
"""Hindsight-Omnigent: persistent memory tools for Omnigent agents.
Exposes Hindsight's retain, recall, and reflect operations as Omnigent
``type: function`` tools — plain Python callables referenced by dotted path from
an agent YAML. The Hindsight bank and connection come from :func:`configure` (or
``HINDSIGHT_*`` env vars), since Omnigent passes tool callables no session
context.
Basic usage (in the module that hosts your agent's tools)::
from hindsight_omnigent import configure, tools_yaml
configure(
hindsight_api_url="https://api.hindsight.vectorize.io",
api_key="hsk_...", # or HINDSIGHT_API_KEY
bank_id="user-123", # or HINDSIGHT_BANK_ID
)
print(tools_yaml()) # paste into your agent.yaml under `tools:`
"""
from .config import (
HindsightOmnigentConfig,
configure,
get_config,
reset_config,
)
from .errors import HindsightError
from .tools import (
OmnigentToolSpec,
memory_instructions,
recall,
reflect,
retain,
tool_specs,
tools_yaml,
)
__version__ = "0.1.0"
__all__ = [
"configure",
"get_config",
"reset_config",
"HindsightOmnigentConfig",
"HindsightError",
"retain",
"recall",
"reflect",
"memory_instructions",
"tool_specs",
"tools_yaml",
"OmnigentToolSpec",
]
@@ -0,0 +1,128 @@
"""Global configuration for the Hindsight-Omnigent integration.
Omnigent invokes ``type: function`` tools as plain Python callables and passes
them **no session context** (see ``omnigent/tools/local_callable.py`` —
``invoke`` does ``del ctx``). So, unlike session-aware integrations, the
Hindsight bank can't be derived per call; it comes from this module-level
config (or the matching environment variables). The natural model is one bank
per Omnigent agent process: set it once via :func:`configure` or env vars.
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal
if TYPE_CHECKING:
from hindsight_client import Hindsight
DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io"
HINDSIGHT_API_URL_ENV = "HINDSIGHT_API_URL"
HINDSIGHT_API_KEY_ENV = "HINDSIGHT_API_KEY"
HINDSIGHT_BANK_ID_ENV = "HINDSIGHT_BANK_ID"
Budget = Literal["low", "mid", "high"]
TagsMatch = Literal["any", "all", "any_strict", "all_strict"]
@dataclass
class HindsightOmnigentConfig:
"""Connection and default settings for the Omnigent integration.
Attributes:
hindsight_api_url: URL of the Hindsight API server.
api_key: API key for Hindsight authentication.
bank_id: Hindsight memory bank the tool callables read/write.
budget: Default recall/reflect 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).
client: Pre-built Hindsight client (overrides url/key when set).
"""
hindsight_api_url: str = DEFAULT_HINDSIGHT_API_URL
api_key: str | None = None
bank_id: str | None = None
budget: Budget = "mid"
max_tokens: int = 4096
tags: list[str] | None = None
recall_tags: list[str] | None = None
recall_tags_match: TagsMatch = "any"
client: Hindsight | None = None
_global_config: HindsightOmnigentConfig | None = None
def configure(
hindsight_api_url: str | None = None,
api_key: str | None = None,
bank_id: str | None = None,
budget: Budget = "mid",
max_tokens: int = 4096,
tags: list[str] | None = None,
recall_tags: list[str] | None = None,
recall_tags_match: TagsMatch = "any",
client: Hindsight | None = None,
) -> HindsightOmnigentConfig:
"""Configure the Hindsight connection and default settings.
Call this once at import time of the module that hosts your tool callables,
so they resolve the same connection and bank on every invocation.
Args:
hindsight_api_url: Hindsight API URL. Falls back to ``HINDSIGHT_API_URL``
env var, then to Hindsight Cloud.
api_key: API key. Falls back to ``HINDSIGHT_API_KEY`` env var.
bank_id: Memory bank to read/write. Falls back to ``HINDSIGHT_BANK_ID``.
budget: Default recall/reflect 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.
client: Pre-built Hindsight client (overrides url/key when set).
Returns:
The configured HindsightOmnigentConfig.
"""
global _global_config
resolved_url = hindsight_api_url or os.environ.get(HINDSIGHT_API_URL_ENV) or DEFAULT_HINDSIGHT_API_URL
resolved_key = api_key or os.environ.get(HINDSIGHT_API_KEY_ENV)
resolved_bank = bank_id or os.environ.get(HINDSIGHT_BANK_ID_ENV)
_global_config = HindsightOmnigentConfig(
hindsight_api_url=resolved_url,
api_key=resolved_key,
bank_id=resolved_bank,
budget=budget,
max_tokens=max_tokens,
tags=tags,
recall_tags=recall_tags,
recall_tags_match=recall_tags_match,
client=client,
)
return _global_config
def get_config() -> HindsightOmnigentConfig:
"""Return the active config, creating one from env vars on first use.
Tool callables call this with no prior :func:`configure`, so an agent can be
wired up entirely through ``HINDSIGHT_*`` environment variables in its
Omnigent ``os_env`` block.
"""
global _global_config
if _global_config is None:
_global_config = configure()
return _global_config
def reset_config() -> None:
"""Reset global configuration to None."""
global _global_config
_global_config = None
@@ -0,0 +1,7 @@
"""Hindsight-Omnigent error types."""
class HindsightError(Exception):
"""Exception raised when a Hindsight memory operation fails."""
pass
@@ -0,0 +1,327 @@
"""Hindsight memory tools for Omnigent agents.
Omnigent (https://github.com/omnigent-ai/omnigent) runs ``type: function`` tools
as plain, in-process Python callables resolved from a dotted import path. It
invokes them with the LLM's JSON arguments parsed into keyword args and coerces
the return value to a string (see ``omnigent/tools/local_callable.py``).
This module exposes Hindsight's retain / recall / reflect as exactly that shape:
tools:
hindsight_recall:
type: function
callable: hindsight_omnigent.tools.recall
parameters: {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
Because Omnigent passes the callable **no session context**, the Hindsight bank
and connection come from :func:`hindsight_omnigent.configure` (or ``HINDSIGHT_*``
env vars) — one bank per agent process. Use :func:`tools_yaml` to emit a ready
``tools:`` block for an agent YAML.
"""
from __future__ import annotations
import json
import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from .config import get_config
from .errors import HindsightError
if TYPE_CHECKING:
from hindsight_client import Hindsight
logger = logging.getLogger(__name__)
# Banks we've already ensured exist this process, so retain doesn't issue a
# redundant create_bank on every call. Keyed by bank id.
_created_banks: set[str] = set()
# ---------------------------------------------------------------------------
# Connection / bank resolution
# ---------------------------------------------------------------------------
def _client() -> Hindsight:
"""Resolve a Hindsight client from the global config."""
config = get_config()
if config.client is not None:
return config.client
from hindsight_client import Hindsight
kwargs: dict[str, Any] = {"base_url": config.hindsight_api_url, "timeout": 30.0}
if config.api_key:
kwargs["api_key"] = config.api_key
return Hindsight(**kwargs)
def _bank() -> str:
"""Resolve the configured Hindsight bank, or raise if none is set."""
bank = get_config().bank_id
if not bank:
raise HindsightError(
"No Hindsight bank configured. Call configure(bank_id=...) or set "
"the HINDSIGHT_BANK_ID environment variable."
)
return bank
def _ensure_bank(client: Hindsight, bank: str) -> None:
"""Create the bank once per process; tolerate it already existing."""
if bank in _created_banks:
return
try:
client.create_bank(bank_id=bank, name=bank)
except Exception as e:
# Bank likely already exists; treat as created either way. Logged at
# debug so a real auth/network failure is visible here rather than only
# surfacing later on the retain call.
logger.debug(f"create_bank({bank!r}) failed (assuming it exists): {e}")
_created_banks.add(bank)
def _reset_created_banks() -> None:
"""Clear the per-process bank cache (used by tests)."""
_created_banks.clear()
# ---------------------------------------------------------------------------
# Tool callables — referenced by dotted path from an Omnigent agent YAML
# ---------------------------------------------------------------------------
def retain(content: str) -> str:
"""Store information in long-term memory for later retrieval.
Use this to save important facts, user preferences, decisions, or anything
that should be remembered across conversations.
"""
config = get_config()
try:
client = _client()
bank = _bank()
_ensure_bank(client, bank)
kwargs: dict[str, Any] = {"bank_id": bank, "content": content}
if config.tags:
kwargs["tags"] = config.tags
client.retain(**kwargs)
return "Stored to long-term memory."
except HindsightError:
raise
except Exception as e:
logger.error(f"Retain failed: {e}")
raise HindsightError(f"Retain failed: {e}") from e
def recall(query: str) -> str:
"""Search long-term memory for relevant information.
Use this to find previously stored facts, preferences, or context. Returns
the matching memories as a bullet list, or a note that none were found.
"""
config = get_config()
try:
client = _client()
bank = _bank()
kwargs: dict[str, Any] = {
"bank_id": bank,
"query": query,
"budget": config.budget,
"max_tokens": config.max_tokens,
}
if config.recall_tags:
kwargs["tags"] = config.recall_tags
kwargs["tags_match"] = config.recall_tags_match
response = client.recall(**kwargs)
results = response.results or []
memories = [r.text for r in results]
if not memories:
return "No relevant memories found."
return "\n".join(f"- {m}" for m in memories)
except HindsightError:
raise
except Exception as e:
logger.error(f"Recall failed: {e}")
raise HindsightError(f"Recall failed: {e}") from e
def reflect(query: str) -> str:
"""Synthesize a reasoned 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.
"""
config = get_config()
try:
client = _client()
bank = _bank()
response = client.reflect(bank_id=bank, query=query, budget=config.budget)
return response.text or "No relevant memories found."
except HindsightError:
raise
except Exception as e:
logger.error(f"Reflect failed: {e}")
raise HindsightError(f"Reflect failed: {e}") from e
# ---------------------------------------------------------------------------
# System-prompt pre-injection (Omnigent has no pre-turn context hook)
# ---------------------------------------------------------------------------
def memory_instructions(
*,
query: str = "relevant context about the user",
bank_id: str | None = None,
max_results: int = 5,
prefix: str = "Relevant memories:\n",
) -> str:
"""Pre-recall memories for injection into an agent's ``prompt``.
Omnigent agents have a static YAML ``prompt`` and no pre-turn context hook,
so to seed an agent with what Hindsight already knows, call this and splice
the result into the prompt yourself. Returns an empty string if no bank is
configured or nothing is found — never raises.
Args:
query: The recall query used to find relevant memories.
bank_id: Bank to recall from (defaults to the configured bank).
max_results: Maximum number of memories to include.
prefix: Text prepended before the memory list.
"""
config = get_config()
bank = bank_id or config.bank_id
if not bank:
return ""
try:
client = _client()
kwargs: dict[str, Any] = {
"bank_id": bank,
"query": query,
"budget": "low",
"max_tokens": config.max_tokens,
}
if config.recall_tags:
kwargs["tags"] = config.recall_tags
kwargs["tags_match"] = config.recall_tags_match
response = client.recall(**kwargs)
results = response.results[:max_results] if response.results else []
if not results:
return ""
lines = [prefix]
for i, result in enumerate(results, 1):
lines.append(f"{i}. {result.text}")
return "\n".join(lines)
except Exception:
# Silently return empty — instructions failures shouldn't block the agent.
return ""
# ---------------------------------------------------------------------------
# Agent-YAML helpers
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class OmnigentToolSpec:
"""A Hindsight tool declaration for an Omnigent agent YAML.
Attributes:
name: Tool name advertised to the LLM (the YAML key).
callable_path: Dotted import path Omnigent resolves and calls.
description: One-line description shown to the LLM.
parameters: OpenAI/JSON-Schema parameter block for the tool.
"""
name: str
callable_path: str
description: str
parameters: dict[str, Any]
def _string_param(field: str, description: str) -> dict[str, Any]:
return {
"type": "object",
"properties": {field: {"type": "string", "description": description}},
"required": [field],
}
_RETAIN_SPEC = OmnigentToolSpec(
name="hindsight_retain",
callable_path="hindsight_omnigent.tools.retain",
description="Store information in long-term memory for later retrieval.",
parameters=_string_param("content", "The information to store in long-term memory."),
)
_RECALL_SPEC = OmnigentToolSpec(
name="hindsight_recall",
callable_path="hindsight_omnigent.tools.recall",
description="Search long-term memory for relevant information.",
parameters=_string_param("query", "The search query to find relevant memories."),
)
_REFLECT_SPEC = OmnigentToolSpec(
name="hindsight_reflect",
callable_path="hindsight_omnigent.tools.reflect",
description="Synthesize a reasoned answer from long-term memories.",
parameters=_string_param("query", "The question to reflect on using stored memories."),
)
def tool_specs(
*,
enable_retain: bool = True,
enable_recall: bool = True,
enable_reflect: bool = True,
) -> list[OmnigentToolSpec]:
"""Return the Hindsight tool specs to declare on an Omnigent agent.
Args:
enable_retain: Include the retain (store) tool.
enable_recall: Include the recall (search) tool.
enable_reflect: Include the reflect (synthesize) tool.
"""
specs: list[OmnigentToolSpec] = []
if enable_retain:
specs.append(_RETAIN_SPEC)
if enable_recall:
specs.append(_RECALL_SPEC)
if enable_reflect:
specs.append(_REFLECT_SPEC)
return specs
def tools_yaml(
*,
enable_retain: bool = True,
enable_recall: bool = True,
enable_reflect: bool = True,
indent: int = 2,
) -> str:
"""Render a ``tools:`` block for an Omnigent agent YAML.
Each tool's ``parameters`` is emitted as inline JSON (valid YAML flow style),
so the schema round-trips exactly. Drop the result into your ``agent.yaml``.
Args:
enable_retain: Include the retain (store) tool.
enable_recall: Include the recall (search) tool.
enable_reflect: Include the reflect (synthesize) tool.
indent: Spaces per indent level.
"""
specs = tool_specs(
enable_retain=enable_retain,
enable_recall=enable_recall,
enable_reflect=enable_reflect,
)
pad = " " * indent
lines = ["tools:"]
for spec in specs:
lines.append(f"{pad}{spec.name}:")
lines.append(f"{pad * 2}type: function")
lines.append(f"{pad * 2}description: {spec.description}")
lines.append(f"{pad * 2}callable: {spec.callable_path}")
lines.append(f"{pad * 2}parameters: {json.dumps(spec.parameters)}")
return "\n".join(lines) + "\n"
@@ -0,0 +1,55 @@
[project]
name = "hindsight-omnigent"
version = "0.1.0"
description = "Omnigent integration for Hindsight - persistent memory tools for AI agents"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [
{ name = "Vectorize", email = "[email protected]" }
]
keywords = [
"ai",
"memory",
"omnigent",
"agents",
"hindsight",
"tools",
]
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.urls]
Homepage = "https://github.com/vectorize-io/hindsight"
Documentation = "https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/omnigent"
Repository = "https://github.com/vectorize-io/hindsight"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["hindsight_omnigent"]
[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"requires_real_llm: end-to-end test that needs live external services (a running Hindsight server and/or real LLM provider keys). Excluded from the deterministic PR-CI bucket via -m 'not requires_real_llm'; run on its own via -m requires_real_llm.",
]
[dependency-groups]
dev = [
"pytest>=9.0.2",
"ruff>=0.8.0",
]
@@ -0,0 +1,79 @@
"""Unit tests for Hindsight Omnigent configuration."""
import pytest
from hindsight_omnigent import (
HindsightOmnigentConfig,
configure,
get_config,
reset_config,
)
from hindsight_omnigent.config import DEFAULT_HINDSIGHT_API_URL
@pytest.fixture(autouse=True)
def _clean_config():
reset_config()
yield
reset_config()
def test_defaults():
cfg = configure()
assert isinstance(cfg, HindsightOmnigentConfig)
assert cfg.hindsight_api_url == DEFAULT_HINDSIGHT_API_URL
assert cfg.api_key is None
assert cfg.bank_id is None
assert cfg.budget == "mid"
assert cfg.max_tokens == 4096
assert cfg.recall_tags_match == "any"
def test_explicit_values_win():
cfg = configure(
hindsight_api_url="http://localhost:8888",
api_key="hsk_x",
bank_id="b1",
budget="high",
max_tokens=2048,
tags=["t"],
recall_tags=["r"],
recall_tags_match="all_strict",
)
assert cfg.hindsight_api_url == "http://localhost:8888"
assert cfg.api_key == "hsk_x"
assert cfg.bank_id == "b1"
assert cfg.budget == "high"
assert cfg.max_tokens == 2048
assert cfg.tags == ["t"]
assert cfg.recall_tags == ["r"]
assert cfg.recall_tags_match == "all_strict"
def test_env_fallbacks(monkeypatch):
monkeypatch.setenv("HINDSIGHT_API_URL", "http://env:9999")
monkeypatch.setenv("HINDSIGHT_API_KEY", "hsk_env")
monkeypatch.setenv("HINDSIGHT_BANK_ID", "env-bank")
cfg = configure()
assert cfg.hindsight_api_url == "http://env:9999"
assert cfg.api_key == "hsk_env"
assert cfg.bank_id == "env-bank"
def test_explicit_overrides_env(monkeypatch):
monkeypatch.setenv("HINDSIGHT_BANK_ID", "env-bank")
cfg = configure(bank_id="explicit")
assert cfg.bank_id == "explicit"
def test_get_config_auto_creates_from_env(monkeypatch):
monkeypatch.setenv("HINDSIGHT_BANK_ID", "lazy-bank")
# No prior configure() call — get_config() should build one from env.
cfg = get_config()
assert cfg.bank_id == "lazy-bank"
def test_reset_config():
configure(bank_id="b")
reset_config()
# get_config rebuilds from env (none set) -> bank_id None
assert get_config().bank_id is None
@@ -0,0 +1,96 @@
"""End-to-end tests for the Hindsight-Omnigent integration.
Exercises the retain/recall/reflect tool callables against a live Hindsight
server. The callables talk to Hindsight directly (the server's LLM does fact
extraction), so only a running Hindsight instance is required — no provider key.
By default these tests are skipped; point ``HINDSIGHT_API_URL`` at a reachable
server to enable them.
The whole module is the real-LLM bucket (``requires_real_llm``).
"""
from __future__ import annotations
import os
import time
import urllib.request
import uuid
import pytest
from hindsight_client import Hindsight
from hindsight_omnigent import configure, recall, reflect, reset_config, retain
from hindsight_omnigent.tools import _reset_created_banks
HINDSIGHT_API_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
_NO_MEMORIES_RECALL = "No relevant memories found."
def _hindsight_available() -> bool:
try:
with urllib.request.urlopen(f"{HINDSIGHT_API_URL}/health", timeout=3) as r:
return r.status == 200
except Exception:
return False
requires_hindsight = pytest.mark.skipif(
not _hindsight_available(),
reason=f"Hindsight not reachable at {HINDSIGHT_API_URL}",
)
pytestmark = [requires_hindsight, pytest.mark.requires_real_llm]
def _recall_until_nonempty(query, attempts=12, delay=1.0):
for _ in range(attempts):
out = recall(query=query)
if out and out != _NO_MEMORIES_RECALL:
return out
time.sleep(delay)
pytest.fail(
f"recall({query!r}) returned no memories after {attempts * delay:.0f}s — "
"either retain failed to surface or the query no longer matches."
)
@pytest.fixture
def live():
reset_config()
_reset_created_banks()
bank_id = f"omnigent-e2e-{uuid.uuid4().hex[:8]}"
configure(hindsight_api_url=HINDSIGHT_API_URL, bank_id=bank_id)
client = Hindsight(base_url=HINDSIGHT_API_URL)
try:
yield bank_id
finally:
try:
client.delete_bank(bank_id)
except Exception:
pass
reset_config()
_reset_created_banks()
class TestE2ETools:
def test_retain_and_recall_roundtrip(self, live):
assert retain(content="The team uses PostgreSQL 16 and deploys to us-east-1.") == (
"Stored to long-term memory."
)
recalled = _recall_until_nonempty("What technologies does the team use?")
lowered = recalled.lower()
assert "postgresql" in lowered or "us-east-1" in lowered, (
f"recall surfaced results but none referenced the stored content: {recalled}"
)
def test_reflect_synthesizes_from_memory(self, live):
retain(content="The team uses PostgreSQL 16 and deploys to us-east-1.")
_recall_until_nonempty("What technologies does the team use?")
answer = reflect(query="What do I know about the team's tech stack?")
assert answer and answer != _NO_MEMORIES_RECALL, "reflect should synthesise non-empty text"
lowered = answer.lower()
assert "postgresql" in lowered or "us-east" in lowered, (
f"reflect text didn't reference the stored memory: {answer[:300]}"
)
def test_recall_empty_bank(self, live):
assert recall(query="anything at all") == _NO_MEMORIES_RECALL
@@ -0,0 +1,387 @@
"""Unit tests for the Hindsight Omnigent tools.
The bulk of these tests drive the tool callables through ``_invoke_like_omnigent``
— a faithful replica of how Omnigent's ``LocalCallableTool.invoke`` calls a
``type: function`` tool (parse the LLM's JSON args into kwargs, call the
callable, coerce the return to a string). That exercises the real framework
contract without installing the (alpha, 3.12-only) ``omnigent`` package.
``TestRealOmnigentInvocation`` additionally loads the callables through the real
``omnigent`` machinery when it's importable, guarding against contract drift.
"""
import json
import logging
from unittest.mock import MagicMock
import pytest
from hindsight_omnigent import (
OmnigentToolSpec,
configure,
memory_instructions,
recall,
reflect,
retain,
reset_config,
tool_specs,
tools_yaml,
)
from hindsight_omnigent.errors import HindsightError
from hindsight_omnigent.tools import _reset_created_banks
try:
from omnigent.spec.types import LocalToolInfo
from omnigent.tools.local_callable import load_local_callable_tools
_HAS_OMNIGENT = True
except Exception: # pragma: no cover - omnigent is an optional, 3.12-only dep
_HAS_OMNIGENT = False
# ---------------------------------------------------------------------------
# Fakes / helpers
# ---------------------------------------------------------------------------
def _stringify(value):
"""Replica of ``omnigent.tools.local_callable._stringify``."""
if value is None:
return ""
if isinstance(value, str):
return value
try:
return json.dumps(value)
except (TypeError, ValueError):
return repr(value)
def _invoke_like_omnigent(fn, **arguments) -> str:
"""Replicate ``LocalCallableTool.invoke``: JSON args -> kwargs -> stringify."""
kwargs = json.loads(json.dumps(arguments)) # round-trip as the LLM payload would
return _stringify(fn(**kwargs))
def _mock_client():
client = MagicMock()
client.retain = MagicMock()
client.recall = MagicMock()
client.reflect = MagicMock()
client.create_bank = MagicMock()
return client
def _mock_recall_response(texts):
response = MagicMock()
results = []
for t in texts:
r = MagicMock()
r.text = t
results.append(r)
response.results = results
return response
def _mock_reflect_response(text):
response = MagicMock()
response.text = text
return response
class _Base:
def setup_method(self):
reset_config()
_reset_created_banks()
def teardown_method(self):
reset_config()
_reset_created_banks()
# ---------------------------------------------------------------------------
# Bank / connection resolution
# ---------------------------------------------------------------------------
class TestBankResolution(_Base):
def test_retain_uses_configured_bank(self):
client = _mock_client()
configure(client=client, bank_id="alice")
out = _invoke_like_omnigent(retain, content="hi")
assert out == "Stored to long-term memory."
client.retain.assert_called_once_with(bank_id="alice", content="hi")
client.create_bank.assert_called_once_with(bank_id="alice", name="alice")
def test_bank_from_env(self, monkeypatch):
monkeypatch.setenv("HINDSIGHT_BANK_ID", "from-env")
client = _mock_client()
configure(client=client) # reads HINDSIGHT_BANK_ID
_invoke_like_omnigent(retain, content="hi")
assert client.retain.call_args[1]["bank_id"] == "from-env"
def test_missing_bank_raises(self):
configure(client=_mock_client()) # no bank_id, no env
with pytest.raises(HindsightError, match="No Hindsight bank configured"):
retain(content="hi")
def test_bank_created_once(self):
client = _mock_client()
configure(client=client, bank_id="alice")
retain(content="a")
retain(content="b")
client.create_bank.assert_called_once()
# ---------------------------------------------------------------------------
# Retain
# ---------------------------------------------------------------------------
class TestRetain(_Base):
def test_retain_with_tags(self):
client = _mock_client()
configure(client=client, bank_id="alice", tags=["env:test"])
retain(content="c")
assert client.retain.call_args[1]["tags"] == ["env:test"]
def test_retain_bank_already_exists(self):
client = _mock_client()
client.create_bank.side_effect = Exception("already exists")
configure(client=client, bank_id="alice")
assert retain(content="c") == "Stored to long-term memory."
def test_retain_failure_raises_and_logs(self, caplog):
client = _mock_client()
client.retain.side_effect = RuntimeError("network error")
configure(client=client, bank_id="alice")
with caplog.at_level(logging.ERROR), pytest.raises(HindsightError, match="Retain failed"):
retain(content="c")
assert "Retain failed" in caplog.text
# ---------------------------------------------------------------------------
# Recall
# ---------------------------------------------------------------------------
class TestRecall(_Base):
def test_recall_returns_bullet_list(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response(["f1", "f2"])
configure(client=client, bank_id="alice")
out = _invoke_like_omnigent(recall, query="q")
assert out == "- f1\n- f2"
def test_recall_no_results(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response([])
configure(client=client, bank_id="alice")
assert recall(query="q") == "No relevant memories found."
def test_recall_none_results(self):
client = _mock_client()
response = MagicMock()
response.results = None
client.recall.return_value = response
configure(client=client, bank_id="alice")
assert recall(query="q") == "No relevant memories found."
def test_recall_passes_budget_and_max_tokens(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response(["f"])
configure(client=client, bank_id="alice", budget="high", max_tokens=2048)
recall(query="q")
call = client.recall.call_args[1]
assert call["budget"] == "high"
assert call["max_tokens"] == 2048
def test_recall_with_tags(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response(["f"])
configure(client=client, bank_id="alice", recall_tags=["scope:global"], recall_tags_match="all")
recall(query="q")
call = client.recall.call_args[1]
assert call["tags"] == ["scope:global"]
assert call["tags_match"] == "all"
def test_recall_without_tags_omits_tag_kwargs(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response(["f"])
configure(client=client, bank_id="alice")
recall(query="q")
call = client.recall.call_args[1]
assert "tags" not in call
assert "tags_match" not in call
def test_recall_does_not_create_bank(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response(["f"])
configure(client=client, bank_id="alice")
recall(query="q")
client.create_bank.assert_not_called()
def test_recall_failure_raises(self):
client = _mock_client()
client.recall.side_effect = RuntimeError("network error")
configure(client=client, bank_id="alice")
with pytest.raises(HindsightError, match="Recall failed"):
recall(query="q")
# ---------------------------------------------------------------------------
# Reflect
# ---------------------------------------------------------------------------
class TestReflect(_Base):
def test_reflect_returns_answer(self):
client = _mock_client()
client.reflect.return_value = _mock_reflect_response("Synthesized answer")
configure(client=client, bank_id="alice")
assert _invoke_like_omnigent(reflect, query="q") == "Synthesized answer"
def test_reflect_empty_text_returns_fallback(self):
client = _mock_client()
client.reflect.return_value = _mock_reflect_response("")
configure(client=client, bank_id="alice")
assert reflect(query="q") == "No relevant memories found."
def test_reflect_passes_budget(self):
client = _mock_client()
client.reflect.return_value = _mock_reflect_response("a")
configure(client=client, bank_id="alice", budget="high")
reflect(query="q")
assert client.reflect.call_args[1]["budget"] == "high"
def test_reflect_failure_raises(self):
client = _mock_client()
client.reflect.side_effect = RuntimeError("network error")
configure(client=client, bank_id="alice")
with pytest.raises(HindsightError, match="Reflect failed"):
reflect(query="q")
# ---------------------------------------------------------------------------
# memory_instructions
# ---------------------------------------------------------------------------
class TestMemoryInstructions(_Base):
def test_formats_results_with_prefix(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response(["likes tea", "lives in NYC"])
configure(client=client, bank_id="b")
out = memory_instructions()
assert out == "Relevant memories:\n\n1. likes tea\n2. lives in NYC"
def test_caps_at_max_results(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response(["a", "b", "c", "d"])
configure(client=client, bank_id="b")
out = memory_instructions(max_results=2)
assert "1. a" in out and "2. b" in out
assert "c" not in out and "d" not in out
def test_explicit_bank_overrides_config(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response(["x"])
configure(client=client, bank_id="config-bank")
memory_instructions(bank_id="explicit-bank")
assert client.recall.call_args[1]["bank_id"] == "explicit-bank"
def test_returns_empty_without_bank(self):
configure(client=_mock_client()) # no bank
assert memory_instructions() == ""
def test_returns_empty_when_no_results(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response([])
configure(client=client, bank_id="b")
assert memory_instructions() == ""
def test_returns_empty_on_failure(self):
client = _mock_client()
client.recall.side_effect = RuntimeError("boom")
configure(client=client, bank_id="b")
assert memory_instructions() == ""
# ---------------------------------------------------------------------------
# Agent-YAML helpers
# ---------------------------------------------------------------------------
class TestToolSpecs(_Base):
def test_default_three_specs(self):
specs = tool_specs()
assert [s.name for s in specs] == [
"hindsight_retain",
"hindsight_recall",
"hindsight_reflect",
]
assert all(isinstance(s, OmnigentToolSpec) for s in specs)
assert all(s.callable_path.startswith("hindsight_omnigent.tools.") for s in specs)
def test_enable_flags(self):
specs = tool_specs(enable_retain=False, enable_reflect=False)
assert [s.name for s in specs] == ["hindsight_recall"]
def test_tools_yaml_is_parseable_and_complete(self):
text = tools_yaml(enable_reflect=False)
assert text.startswith("tools:\n")
# Each tool's parameters line is inline JSON (valid YAML) — load it back.
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("parameters:"):
payload = stripped[len("parameters:") :].strip()
schema = json.loads(payload)
assert schema["type"] == "object"
assert schema["required"]
assert "hindsight_omnigent.tools.retain" in text
assert "hindsight_omnigent.tools.recall" in text
assert "hindsight_omnigent.tools.reflect" not in text
# ---------------------------------------------------------------------------
# Real Omnigent invocation (guards against the function-tool contract drifting)
# ---------------------------------------------------------------------------
@pytest.mark.skipif(not _HAS_OMNIGENT, reason="omnigent not installed (3.12-only alpha)")
class TestRealOmnigentInvocation(_Base):
def _info(self, name, path, field):
return LocalToolInfo(
name=name,
path=path,
language="omnigent-python-callable",
parameters={
"type": "object",
"properties": {field: {"type": "string"}},
"required": [field],
},
)
def test_recall_through_local_callable_tool(self):
client = _mock_client()
client.recall.return_value = _mock_recall_response(["fact one", "fact two"])
configure(client=client, bank_id="alice")
tools = load_local_callable_tools([self._info("hindsight_recall", "hindsight_omnigent.tools.recall", "query")])
assert len(tools) == 1
tool = tools[0]
assert tool.name() == "hindsight_recall"
assert tool.get_schema()["function"]["name"] == "hindsight_recall"
out = tool.invoke(json.dumps({"query": "what tech?"}), None)
assert "fact one" in out and "fact two" in out
assert client.recall.call_args[1]["bank_id"] == "alice"
def test_retain_through_local_callable_tool(self):
client = _mock_client()
configure(client=client, bank_id="alice")
tools = load_local_callable_tools(
[self._info("hindsight_retain", "hindsight_omnigent.tools.retain", "content")]
)
out = tools[0].invoke(json.dumps({"content": "remember this"}), None)
assert out == "Stored to long-term memory."
client.retain.assert_called_once_with(bank_id="alice", content="remember this")
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,7 +13,7 @@ print_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
VALID_INTEGRATIONS=("ag2" "agent-framework" "agentcore" "agno" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "codex" "crewai" "cursor" "cursor-cli" "dify" "flowise" "gemini-spark" "google-adk" "haystack" "langgraph" "litellm" "llamaindex" "n8n" "nemoclaw" "obsidian" "omo" "openai-agents" "openclaw" "opencode" "paperclip" "pipecat" "pydantic-ai" "roo-code" "smolagents" "strands" "superagent" "vapi")
VALID_INTEGRATIONS=("ag2" "agent-framework" "agentcore" "agno" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "codex" "crewai" "cursor" "cursor-cli" "dify" "flowise" "gemini-spark" "google-adk" "haystack" "langgraph" "litellm" "llamaindex" "n8n" "nemoclaw" "obsidian" "omnigent" "omo" "openai-agents" "openclaw" "opencode" "paperclip" "pipecat" "pydantic-ai" "roo-code" "smolagents" "strands" "superagent" "vapi")
usage() {
print_error "Usage: $0 <integration> <version>"