Compare commits

..
2 Commits
Author SHA1 Message Date
Nicolò Boschi 79f683cdf9 fix 2026-02-18 13:44:37 +01:00
Nicolò Boschi ce74b1fc56 feat: add iris as file parser 2026-02-18 12:06:04 +01:00
29 changed files with 1044 additions and 586 deletions
+9 -1
View File
@@ -272,6 +272,8 @@ ENV_FILE_STORAGE_AZURE_CONTAINER = "HINDSIGHT_API_FILE_STORAGE_AZURE_CONTAINER"
ENV_FILE_STORAGE_AZURE_ACCOUNT_NAME = "HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_NAME"
ENV_FILE_STORAGE_AZURE_ACCOUNT_KEY = "HINDSIGHT_API_FILE_STORAGE_AZURE_ACCOUNT_KEY"
ENV_FILE_PARSER = "HINDSIGHT_API_FILE_PARSER"
ENV_FILE_PARSER_IRIS_TOKEN = "HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN"
ENV_FILE_PARSER_IRIS_ORG_ID = "HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID"
ENV_FILE_CONVERSION_MAX_BATCH_SIZE_MB = "HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE_MB"
ENV_FILE_CONVERSION_MAX_BATCH_SIZE = "HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE"
ENV_ENABLE_FILE_UPLOAD_API = "HINDSIGHT_API_ENABLE_FILE_UPLOAD_API"
@@ -645,7 +647,9 @@ class HindsightConfig:
file_storage_azure_container: str | None # Azure container name (required for azure storage)
file_storage_azure_account_name: str | None # Azure storage account name
file_storage_azure_account_key: str | None # Azure storage account key
file_parser: str # File parser to use (e.g., "markitdown")
file_parser: str # File parser to use (e.g., "markitdown", "iris")
file_parser_iris_token: str | None # Vectorize API token for iris parser (VECTORIZE_TOKEN)
file_parser_iris_org_id: str | None # Vectorize org ID for iris parser (VECTORIZE_ORG_ID)
file_conversion_max_batch_size_mb: int # Max total batch size in MB (all files combined)
file_conversion_max_batch_size: int # Max files per request
enable_file_upload_api: bool
@@ -712,6 +716,8 @@ class HindsightConfig:
"file_storage_s3_secret_access_key",
"file_storage_gcs_service_account_key",
"file_storage_azure_account_key",
# File parser credentials
"file_parser_iris_token",
}
# CONFIGURABLE_FIELDS: Safe behavioral settings that can be customized per-tenant/bank
@@ -1030,6 +1036,8 @@ class HindsightConfig:
file_storage_azure_account_name=os.getenv(ENV_FILE_STORAGE_AZURE_ACCOUNT_NAME) or None,
file_storage_azure_account_key=os.getenv(ENV_FILE_STORAGE_AZURE_ACCOUNT_KEY) or None,
file_parser=os.getenv(ENV_FILE_PARSER, DEFAULT_FILE_PARSER),
file_parser_iris_token=os.getenv(ENV_FILE_PARSER_IRIS_TOKEN) or None,
file_parser_iris_org_id=os.getenv(ENV_FILE_PARSER_IRIS_ORG_ID) or None,
file_conversion_max_batch_size_mb=int(
os.getenv(ENV_FILE_CONVERSION_MAX_BATCH_SIZE_MB, str(DEFAULT_FILE_CONVERSION_MAX_BATCH_SIZE_MB))
),
@@ -1374,7 +1374,7 @@ class MemoryEngine(MemoryEngineInterface):
logger.debug(f"File storage initialized ({config.file_storage_type})")
# Initialize parser registry
from .parsers import FileParserRegistry, MarkitdownParser
from .parsers import FileParserRegistry, IrisParser, MarkitdownParser
self._parser_registry = FileParserRegistry()
try:
@@ -1382,6 +1382,13 @@ class MemoryEngine(MemoryEngineInterface):
logger.debug("Registered markitdown parser")
except ImportError:
logger.warning("markitdown not available - file parsing disabled")
iris_token = config.file_parser_iris_token
iris_org_id = config.file_parser_iris_org_id
if iris_token and iris_org_id:
self._parser_registry.register(IrisParser(token=iris_token, org_id=iris_org_id))
logger.debug("Registered iris parser")
else:
logger.debug("Iris parser not registered (VECTORIZE_TOKEN or VECTORIZE_ORG_ID not set)")
# Set executor for task backend and initialize
self._task_backend.set_executor(self.execute_task)
@@ -2871,44 +2878,15 @@ class MemoryEngine(MemoryEngineInterface):
)
top_results_dicts.append(result_dict)
# Get entities for each fact if include_entities is requested
fact_entity_map = {} # unit_id -> list of (entity_id, entity_name)
if include_entities and top_scored:
unit_ids = [uuid.UUID(sr.id) for sr in top_scored]
if unit_ids:
async with acquire_with_retry(pool) as entity_conn:
entity_rows = await entity_conn.fetch(
f"""
SELECT ue.unit_id, e.id as entity_id, e.canonical_name
FROM {fq_table("unit_entities")} ue
JOIN {fq_table("entities")} e ON ue.entity_id = e.id
WHERE ue.unit_id = ANY($1::uuid[])
""",
unit_ids,
)
for row in entity_rows:
unit_id = str(row["unit_id"])
if unit_id not in fact_entity_map:
fact_entity_map[unit_id] = []
fact_entity_map[unit_id].append(
{"entity_id": str(row["entity_id"]), "canonical_name": row["canonical_name"]}
)
# Convert results to MemoryFact objects
memory_facts = []
for result_dict in top_results_dicts:
result_id = str(result_dict.get("id"))
# Get entity names for this fact
entity_names = None
if include_entities and result_id in fact_entity_map:
entity_names = [e["canonical_name"] for e in fact_entity_map[result_id]]
memory_facts.append(
MemoryFact(
id=result_id,
id=str(result_dict.get("id")),
text=result_dict.get("text"),
fact_type=result_dict.get("fact_type", "world"),
entities=entity_names,
entities=None, # Entity observations removed
context=result_dict.get("context"),
occurred_start=result_dict.get("occurred_start"),
occurred_end=result_dict.get("occurred_end"),
@@ -2919,32 +2897,8 @@ class MemoryEngine(MemoryEngineInterface):
)
)
# Fetch entity observations if requested
# Entity observations removed - always set to None
entities_dict = None
total_entity_tokens = 0
if include_entities and fact_entity_map:
# Collect unique entities in order of fact relevance (preserving order from top_scored)
entities_ordered = [] # list of (entity_id, entity_name) tuples
seen_entity_ids = set()
for sr in top_scored:
unit_id = sr.id
if unit_id in fact_entity_map:
for entity in fact_entity_map[unit_id]:
entity_id = entity["entity_id"]
entity_name = entity["canonical_name"]
if entity_id not in seen_entity_ids:
entities_ordered.append((entity_id, entity_name))
seen_entity_ids.add(entity_id)
# Return entities with empty observations (summaries now live in mental models)
entities_dict = {}
for entity_id, entity_name in entities_ordered:
entities_dict[entity_name] = EntityState(
entity_id=entity_id,
canonical_name=entity_name,
observations=[], # Mental models provide this now
)
# Finalize trace if enabled
trace_dict = None
@@ -2955,7 +2909,6 @@ class MemoryEngine(MemoryEngineInterface):
# Log final recall stats
total_time = time.time() - recall_start
num_chunks = len(chunks_dict) if chunks_dict else 0
num_entities = len(entities_dict) if entities_dict else 0
# Include wait times in log if significant
wait_parts = []
if semaphore_wait > 0.01:
@@ -2964,7 +2917,7 @@ class MemoryEngine(MemoryEngineInterface):
wait_parts.append(f"conn={max_conn_wait:.3f}s")
wait_info = f" | waits: {', '.join(wait_parts)}" if wait_parts else ""
log_buffer.append(
f"[RECALL {recall_id}] Complete: {len(top_scored)} facts ({total_tokens} tok), {num_chunks} chunks ({total_chunk_tokens} tok), {num_entities} entities ({total_entity_tokens} tok) | {fact_type_summary} | {total_time:.3f}s{wait_info}"
f"[RECALL {recall_id}] Complete: {len(top_scored)} facts ({total_tokens} tok), {num_chunks} chunks ({total_chunk_tokens} tok) | {fact_type_summary} | {total_time:.3f}s{wait_info}"
)
if not quiet:
logger.info("\n" + "\n".join(log_buffer))
@@ -1,9 +1,10 @@
"""File parser implementations."""
from .base import FileParser
from .base import FileParser, UnsupportedFileTypeError
from .iris import IrisParser
from .markitdown import MarkitdownParser
__all__ = ["FileParser", "MarkitdownParser", "FileParserRegistry"]
__all__ = ["FileParser", "UnsupportedFileTypeError", "IrisParser", "MarkitdownParser", "FileParserRegistry"]
class FileParserRegistry:
@@ -43,7 +44,8 @@ class FileParserRegistry:
ValueError: If no suitable parser found
"""
if name:
# Explicit parser requested
# Explicit parser requested — return it directly, let the parser
# raise UnsupportedFileTypeError from convert() if needed
if name not in self._parsers:
raise ValueError(f"Parser '{name}' not found. Available: {list(self._parsers.keys())}")
return self._parsers[name]
@@ -3,6 +3,12 @@
from abc import ABC, abstractmethod
class UnsupportedFileTypeError(Exception):
"""Raised by a parser when it does not support the given file type."""
pass
class FileParser(ABC):
"""Abstract base for file to markdown parsers."""
@@ -19,24 +25,27 @@ class FileParser(ABC):
Markdown content as string
Raises:
ValueError: If file format is not supported
RuntimeError: If parsing fails
UnsupportedFileTypeError: If the file type is not supported by this parser
RuntimeError: If parsing fails for another reason
"""
pass
@abstractmethod
def supports(self, filename: str, content_type: str | None = None) -> bool:
"""
Check if parser supports this file type.
Override this for local/static extension-based filtering.
Parsers that delegate to a remote service should leave this as True
and raise UnsupportedFileTypeError from convert() instead.
Args:
filename: File name (used for extension check)
content_type: MIME type (optional)
Returns:
True if this parser can handle the file
True if this parser can handle the file (default: True)
"""
pass
return True
@abstractmethod
def name(self) -> str:
@@ -0,0 +1,137 @@
"""Iris parser implementation using the Vectorize Iris HTTP API."""
import asyncio
import logging
import mimetypes
import time
import httpx
from .base import FileParser, UnsupportedFileTypeError
logger = logging.getLogger(__name__)
_IRIS_BASE_URL = "https://api.vectorize.io/v1"
_DEFAULT_POLL_INTERVAL = 2.0 # seconds
_DEFAULT_TIMEOUT = 300.0 # seconds
class IrisParser(FileParser):
"""
Iris file parser using the Vectorize Iris cloud extraction service.
Uploads files to the Vectorize Iris API, starts an extraction job,
and polls until the text is ready. The API determines which file types
are supported — UnsupportedFileTypeError is raised if the file is rejected.
Authentication:
Requires HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN and
HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID environment variables,
or pass them explicitly via the constructor.
"""
def __init__(
self,
token: str,
org_id: str,
poll_interval: float = _DEFAULT_POLL_INTERVAL,
timeout: float = _DEFAULT_TIMEOUT,
):
"""
Initialize iris parser.
Args:
token: Vectorize API token
org_id: Vectorize organization ID
poll_interval: Seconds between status poll requests (default: 2)
timeout: Maximum seconds to wait for extraction (default: 300)
"""
self._token = token
self._org_id = org_id
self._poll_interval = poll_interval
self._timeout = timeout
self._auth_headers = {"Authorization": f"Bearer {token}"}
async def convert(self, file_data: bytes, filename: str) -> str:
"""
Parse file to text using the Vectorize Iris API.
Raises:
UnsupportedFileTypeError: If the Iris API rejects the file type (4xx)
RuntimeError: If extraction fails for another reason
"""
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
async with httpx.AsyncClient() as client:
# Step 1: Request a presigned upload URL
init_resp = await client.post(
f"{_IRIS_BASE_URL}/org/{self._org_id}/files",
headers=self._auth_headers,
json={"name": filename, "contentType": content_type},
)
_raise_for_status(init_resp, filename, "file upload init")
init_data = init_resp.json()
file_id: str = init_data["fileId"]
upload_url: str = init_data["uploadUrl"]
# Step 2: Upload the file bytes to the presigned URL (no auth header)
upload_resp = await client.put(
upload_url,
content=file_data,
headers={"Content-Type": content_type},
)
_raise_for_status(upload_resp, filename, "file upload")
# Step 3: Start extraction
extract_resp = await client.post(
f"{_IRIS_BASE_URL}/org/{self._org_id}/extraction",
headers=self._auth_headers,
json={"fileId": file_id},
)
_raise_for_status(extract_resp, filename, "start extraction")
extraction_id: str = extract_resp.json()["extractionId"]
# Step 4: Poll until ready or timeout
deadline = time.monotonic() + self._timeout
while True:
status_resp = await client.get(
f"{_IRIS_BASE_URL}/org/{self._org_id}/extraction/{extraction_id}",
headers=self._auth_headers,
)
_raise_for_status(status_resp, filename, "poll extraction status")
status_data = status_resp.json()
if status_data.get("ready"):
data = status_data.get("data", {})
if not data.get("success"):
error = data.get("error", "unknown error")
raise RuntimeError(f"Iris extraction failed for '{filename}': {error}")
text = data.get("text")
if not text:
raise RuntimeError(f"No content extracted from '{filename}'")
return text
if time.monotonic() >= deadline:
raise RuntimeError(f"Iris extraction timed out after {self._timeout}s for '{filename}'")
await asyncio.sleep(self._poll_interval)
def name(self) -> str:
"""Get parser name."""
return "iris"
def _raise_for_status(response: httpx.Response, filename: str, step: str) -> None:
"""
Raise an appropriate error including the response body on HTTP errors.
Raises UnsupportedFileTypeError for 4xx responses (file rejected by the API),
RuntimeError for other HTTP errors.
"""
if not response.is_error:
return
body = response.text or "<empty>"
msg = f"Iris API error during {step} for '{filename}': {response.status_code} {response.reason_phrase}{body}"
if response.is_client_error:
raise UnsupportedFileTypeError(msg)
raise RuntimeError(msg)
+2
View File
@@ -262,6 +262,8 @@ def main():
file_storage_azure_account_name=config.file_storage_azure_account_name,
file_storage_azure_account_key=config.file_storage_azure_account_key,
file_parser=config.file_parser,
file_parser_iris_token=config.file_parser_iris_token,
file_parser_iris_org_id=config.file_parser_iris_org_id,
file_conversion_max_batch_size_mb=config.file_conversion_max_batch_size_mb,
file_conversion_max_batch_size=config.file_conversion_max_batch_size,
enable_file_upload_api=config.enable_file_upload_api,
+72
View File
@@ -0,0 +1,72 @@
"""
Integration tests for the Iris file parser.
Tests are skipped automatically if HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN
and HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID are not set in the environment.
"""
import os
import pytest
from hindsight_api.config import ENV_FILE_PARSER_IRIS_ORG_ID, ENV_FILE_PARSER_IRIS_TOKEN
from hindsight_api.engine.parsers.iris import IrisParser
_token = os.getenv(ENV_FILE_PARSER_IRIS_TOKEN)
_org_id = os.getenv(ENV_FILE_PARSER_IRIS_ORG_ID)
pytestmark = pytest.mark.skipif(
not (_token and _org_id),
reason="HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN and HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID not set",
)
# Minimal valid PDF with the text "Hello from Hindsight"
_SAMPLE_PDF = b"""%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]
/Contents 4 0 R /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> >>
endobj
4 0 obj
<< /Length 44 >>
stream
BT /F1 12 Tf 100 700 Td (Hello from Hindsight) Tj ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000274 00000 n
trailer << /Size 5 /Root 1 0 R >>
startxref
369
%%EOF"""
@pytest.fixture
def iris_parser() -> IrisParser:
return IrisParser(token=_token, org_id=_org_id)
@pytest.mark.asyncio
async def test_iris_parser_converts_pdf(iris_parser: IrisParser):
"""IrisParser should extract text from a valid PDF."""
result = await iris_parser.convert(_SAMPLE_PDF, "sample.pdf")
assert isinstance(result, str)
assert len(result) > 0
@pytest.mark.asyncio
async def test_iris_parser_name(iris_parser: IrisParser):
"""IrisParser.name() should return 'iris'."""
assert iris_parser.name() == "iris"
+1 -1
View File
@@ -22,7 +22,7 @@ go get golang.org/x/net/context
Put the package under your project folder and add the following in import:
```go
import hindsight "github.com/vectorize-io/hindsight/hindsight-clients/go"
import hindsight "github.com/vectorize-io/hindsight-client-go"
```
To use a proxy, set the environment variable `HTTP_PROXY`:
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/vectorize-io/hindsight/hindsight-clients/go
module github.com/vectorize-io/hindsight-client-go
go 1.18
@@ -3,8 +3,8 @@ outputDir: ./
inputSpec: ../../hindsight-docs/static/openapi.json
packageName: hindsight
gitUserId: vectorize-io
gitRepoId: hindsight/hindsight-clients/go
isGoSubmodule: true
gitRepoId: hindsight-client-go
isGoSubmodule: false
enumClassPrefix: true
structPrefix: true
generateInterfaces: true
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
openapiclient "github.com/vectorize-io/hindsight/hindsight-clients/go"
openapiclient "github.com/vectorize-io/hindsight-client-go"
)
func Test_hindsight_BanksAPIService(t *testing.T) {
@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
openapiclient "github.com/vectorize-io/hindsight/hindsight-clients/go"
openapiclient "github.com/vectorize-io/hindsight-client-go"
)
func Test_hindsight_DirectivesAPIService(t *testing.T) {
@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
openapiclient "github.com/vectorize-io/hindsight/hindsight-clients/go"
openapiclient "github.com/vectorize-io/hindsight-client-go"
)
func Test_hindsight_DocumentsAPIService(t *testing.T) {
@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
openapiclient "github.com/vectorize-io/hindsight/hindsight-clients/go"
openapiclient "github.com/vectorize-io/hindsight-client-go"
)
func Test_hindsight_EntitiesAPIService(t *testing.T) {
+1 -1
View File
@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
openapiclient "github.com/vectorize-io/hindsight/hindsight-clients/go"
openapiclient "github.com/vectorize-io/hindsight-client-go"
)
func Test_hindsight_MemoryAPIService(t *testing.T) {
@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
openapiclient "github.com/vectorize-io/hindsight/hindsight-clients/go"
openapiclient "github.com/vectorize-io/hindsight-client-go"
)
func Test_hindsight_MentalModelsAPIService(t *testing.T) {
@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
openapiclient "github.com/vectorize-io/hindsight/hindsight-clients/go"
openapiclient "github.com/vectorize-io/hindsight-client-go"
)
func Test_hindsight_MonitoringAPIService(t *testing.T) {
@@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
openapiclient "github.com/vectorize-io/hindsight/hindsight-clients/go"
openapiclient "github.com/vectorize-io/hindsight-client-go"
)
func Test_hindsight_OperationsAPIService(t *testing.T) {
+26 -4
View File
@@ -68,7 +68,7 @@ Hindsight supports three PostgreSQL vector extensions:
#### **pgvector** (HNSW - default)
- In-memory index using Hierarchical Navigable Small World algorithm
- Works well for most embeddings and dataset sizes
- Fast for small-medium datasets (&lt;10M vectors)
- Fast for small-medium datasets (<10M vectors)
- Higher memory usage for large datasets
- Most widely deployed and supported
@@ -97,7 +97,7 @@ Hindsight supports three PostgreSQL vector extensions:
- When disk I/O is not a bottleneck
**When to use pgvector (HNSW):**
- Small-medium datasets (&lt;10M vectors)
- Small-medium datasets (<10M vectors)
- Maximum query speed when all data fits in memory
- Simple nearest-neighbor queries without filters
- Standard PostgreSQL deployment preference
@@ -636,12 +636,34 @@ Configuration for the file upload and conversion pipeline (used by `POST /v1/def
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_ENABLE_FILE_UPLOAD_API` | Enable the file upload API endpoint | `true` |
| `HINDSIGHT_API_FILE_PARSER` | File parser to use (`markitdown`) | `markitdown` |
| `HINDSIGHT_API_FILE_PARSER` | File parser to use (`markitdown`, `iris`) | `markitdown` |
| `HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE` | Max files per upload request | `10` |
| `HINDSIGHT_API_FILE_CONVERSION_MAX_BATCH_SIZE_MB` | Max total upload size per request (MB) | `100` |
| `HINDSIGHT_API_FILE_DELETE_AFTER_RETAIN` | Delete stored files after memory extraction completes | `true` |
**Supported formats (via markitdown):** PDF, DOCX, DOC, PPTX, PPT, XLSX, XLS, images (JPG, PNG, GIF — OCR), audio (MP3, WAV — transcription), HTML, TXT, MD, CSV, and more.
#### Parser: markitdown (default)
Local file-to-markdown conversion using [Microsoft's markitdown](https://github.com/microsoft/markitdown). No external service required.
**Supported formats:** PDF, DOCX, DOC, PPTX, PPT, XLSX, XLS, images (JPG, PNG — OCR), audio (MP3, WAV — transcription), HTML, TXT, MD, CSV.
#### Parser: iris
Cloud-based extraction via [Vectorize Iris](https://docs.vectorize.io/build-deploy/extract-information/understanding-iris/). Higher quality extraction for complex documents, powered by a remote AI service.
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN` | Vectorize API token | — |
| `HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID` | Vectorize organization ID | — |
**Supported formats:** PDF, DOCX, DOC, PPTX, PPT, XLSX, XLS, images (JPG, JPEG, PNG, GIF, BMP, TIFF, WEBP), HTML, TXT, MD, CSV.
```bash
# Use iris parser (requires Vectorize account)
export HINDSIGHT_API_FILE_PARSER=iris
export HINDSIGHT_API_FILE_PARSER_IRIS_TOKEN=your-vectorize-token
export HINDSIGHT_API_FILE_PARSER_IRIS_ORG_ID=your-org-id
```
```bash
# Increase batch limits for large file imports
+1 -1
View File
@@ -12,7 +12,7 @@ import quickstartGo from '!!raw-loader!@site/examples/api/quickstart.go';
## Installation
```bash
go get github.com/vectorize-io/hindsight/hindsight-clients/go
go get github.com/vectorize-io/hindsight-client-go
```
Requires Go 1.23+.
@@ -0,0 +1,366 @@
---
sidebar_position: 4
---
# Vercel AI SDK
Official Hindsight integration for the [Vercel AI SDK](https://ai-sdk.dev).
## Features
- **7 Memory Tools**: Core memory operations (retain, recall, reflect), mental models (create, query), documents (get), and directives (create)
- **AI SDK 6 Native**: Works seamlessly with `generateText`, `streamText`, and `ToolLoopAgent`
- **Multi-User Support**: Dynamic bank IDs per tool call for multi-user/multi-tenant scenarios
- **Full Parameter Support**: Complete access to all Hindsight API parameters
- **Type-Safe**: Full TypeScript support with Zod schemas for validation
## Installation
```bash
npm install @vectorize-io/hindsight-ai-sdk @vectorize-io/hindsight-client ai zod
```
## Quick Start
### 1. Set up your Hindsight client
```typescript
import { HindsightClient } from '@vectorize-io/hindsight-client';
const hindsightClient = new HindsightClient({
apiUrl: process.env.HINDSIGHT_API_URL || 'http://localhost:8000',
});
```
### 2. Create Hindsight tools
```typescript
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const tools = createHindsightTools({
client: hindsightClient,
});
```
### 3. Use with AI SDK
```typescript
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
tools,
prompt: 'Remember that Alice loves hiking and prefers spicy food',
});
console.log(result.text);
```
## Memory Tools
The integration provides seven tools that the AI model can use to manage memory:
### `retain` - Store Information
The model calls this tool to store information for future recall.
**Parameters:**
- `bankId` (required): Memory bank ID (usually the user ID)
- `content` (required): Content to store
- `documentId` (optional): Document ID for grouping/upserting related memories
- `timestamp` (optional): ISO timestamp for when the memory occurred
- `context` (optional): Additional context about the memory
- `metadata` (optional): Key-value metadata for filtering
**Example tool call:**
```typescript
{
bankId: "user-123",
content: "Alice loves hiking and goes to Yosemite every summer",
context: "User preferences",
timestamp: "2024-01-15T10:30:00Z"
}
```
**Returns:**
```typescript
{
success: true,
itemsCount: 1
}
```
### `recall` - Search Memories
The model calls this tool to search for relevant information in memory.
**Parameters:**
- `bankId` (required): Memory bank ID
- `query` (required): What to search for
- `types` (optional): Filter by fact types (`['world', 'experience', 'opinion']`)
- `maxTokens` (optional): Maximum tokens to return (default: 4096)
- `budget` (optional): Processing budget - `'low'`, `'mid'`, or `'high'`
- `queryTimestamp` (optional): Query from a specific time (ISO format)
- `includeEntities` (optional): Include entity observations
- `includeChunks` (optional): Include raw document chunks
**Example tool call:**
```typescript
{
bankId: "user-123",
query: "What does Alice like to do outdoors?",
types: ["world", "experience"],
maxTokens: 2048,
budget: "mid"
}
```
**Returns:**
```typescript
{
results: [
{
id: "mem-123",
text: "Alice loves hiking",
type: "world",
entities: ["Alice"],
context: "User preferences",
occurred_start: "2024-01-15T10:30:00Z",
document_id: "doc-456",
metadata: { source: "chat" }
}
],
entities: {
"Alice": {
canonical_name: "Alice",
mention_count: 15,
observations: [...]
}
}
}
```
### `reflect` - Synthesize Insights
The model calls this tool to analyze memories and generate contextual insights.
**Parameters:**
- `bankId` (required): Memory bank ID
- `query` (required): Question to reflect on
- `context` (optional): Additional context for reflection
- `budget` (optional): Processing budget - `'low'`, `'mid'`, or `'high'`
**Example tool call:**
```typescript
{
bankId: "user-123",
query: "What outdoor activities does Alice enjoy?",
context: "Planning a weekend trip",
budget: "mid"
}
```
**Returns:**
```typescript
{
text: "Alice is an avid hiker who particularly enjoys visiting Yosemite National Park during summer months. She has expressed strong preferences for mountain trails over beach activities.",
basedOn: [
{
id: "mem-123",
text: "Alice loves hiking",
type: "world",
context: "User preferences",
occurred_start: "2024-01-15T10:30:00Z"
}
]
}
```
### `createMentalModel` - Create Knowledge Consolidation
The model calls this tool to create a mental model that automatically consolidates memories into structured knowledge.
**Parameters:**
- `bankId` (required): Memory bank ID
- `mentalModelId` (optional): Custom ID for the mental model (auto-generated if not provided)
- `name` (optional): Name for the mental model
- `sourceQuery` (optional): Query defining which memories to consolidate
- `tags` (optional): Tags for organizing mental models
- `maxTokens` (optional): Maximum tokens for the content
- `autoRefresh` (optional): Auto-refresh after new consolidations (default: false)
**Example tool call:**
```typescript
{
bankId: "user-123",
name: "User Preferences",
sourceQuery: "What are the user's preferences?",
tags: ["preferences"],
autoRefresh: true
}
```
**Returns:**
```typescript
{
mentalModelId: "mm-456",
createdAt: "2024-01-15T10:30:00Z"
}
```
### `queryMentalModel` - Retrieve Consolidated Knowledge
The model calls this tool to retrieve synthesized insights from an existing mental model.
**Parameters:**
- `bankId` (required): Memory bank ID
- `mentalModelId` (required): ID of the mental model to query
**Example tool call:**
```typescript
{
bankId: "user-123",
mentalModelId: "mm-456"
}
```
**Returns:**
```typescript
{
content: "The user prefers outdoor activities, particularly hiking. They enjoy mountain trails and visit Yosemite regularly during summer.",
name: "User Preferences",
updatedAt: "2024-01-20T15:45:00Z"
}
```
### `getDocument` - Retrieve Stored Document
The model calls this tool to retrieve a stored document by its ID.
**Parameters:**
- `bankId` (required): Memory bank ID
- `documentId` (required): ID of the document to retrieve
**Example tool call:**
```typescript
{
bankId: "user-123",
documentId: "doc-789"
}
```
**Returns:**
```typescript
{
originalText: "User profile: Alice, Software Engineer, loves hiking...",
id: "doc-789",
createdAt: "2024-01-10T09:00:00Z",
updatedAt: "2024-01-15T14:30:00Z"
}
```
### `createDirective` - Create Behavioral Rule
The model calls this tool to create a directive—a hard rule injected into prompts during reflect operations.
**Parameters:**
- `bankId` (required): Memory bank ID
- `name` (required): Human-readable name for the directive
- `content` (required): The directive text to inject
- `priority` (optional): Higher priority directives are injected first (default: 0)
- `isActive` (optional): Whether this directive is active (default: true)
- `tags` (optional): Tags for filtering (e.g., user-specific directives)
**Example tool call:**
```typescript
{
bankId: "user-123",
name: "Response Format",
content: "Always provide responses in bullet-point format",
priority: 10,
tags: ["formatting"]
}
```
**Returns:**
```typescript
{
id: "dir-321",
name: "Response Format",
content: "Always provide responses in bullet-point format",
tags: ["formatting"],
createdAt: "2024-01-15T10:30:00Z"
}
```
## Usage Examples
### Using with `generateText`
```typescript
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const hindsightClient = new HindsightClient({
apiUrl: 'http://localhost:8000',
});
const tools = createHindsightTools({ client: hindsightClient });
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
tools,
system: `You are a helpful assistant with long-term memory. Use the recall tool to check for relevant memories before responding.`,
prompt: 'Remember that Alice loves hiking and prefers spicy food',
});
console.log(result.text);
```
### Using with `streamText`
```typescript
import { streamText } from 'ai';
const result = streamText({
model: anthropic('claude-sonnet-4-20250514'),
tools,
system: `You have persistent memory. Use retain to store important information and recall to retrieve it.`,
prompt: 'What do you know about Alice?',
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
```
### Using with `ToolLoopAgent`
```typescript
import { ToolLoopAgent, stopWhen, stepCountIs } from 'ai';
const agent = new ToolLoopAgent({
model: anthropic('claude-sonnet-4-20250514'),
tools,
instructions: `You are a personal assistant with long-term memory. Always check recall before responding and use retain to store important information.`,
stopWhen: stepCountIs(10),
});
const result = await agent.generate({
prompt: 'What did I say I wanted to work on this week?',
});
```
### Multi-User Support
```typescript
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
tools,
system: `You are a helpful assistant. The user's ID is: ${userId}. Always pass this as the bankId parameter to memory tools.`,
prompt: 'Remember that I prefer dark mode',
});
```
@@ -1,97 +0,0 @@
---
sidebar_position: 4
---
# Vercel AI SDK
The `@vectorize-io/hindsight-ai-sdk` package integrates [Hindsight](https://hindsight.vectorize.io) memory with the [Vercel AI SDK](https://ai-sdk.dev). It provides five ready-to-use tools for retaining, recalling, and reflecting on long-term memories.
import CodeSnippet from '@site/src/components/CodeSnippet';
import aiSdkTs from '!!raw-loader!@site/examples/integrations/ai-sdk.ts';
## Installation
```bash
npm install @vectorize-io/hindsight-ai-sdk @vectorize-io/hindsight-client ai
```
## Setup
Create a Hindsight client and pass it to `createHindsightTools` along with a `bankId`. The `bankId` identifies the memory store for this session—typically a user ID.
<CodeSnippet code={aiSdkTs} section="setup" language="typescript" />
:::tip Per-request bank IDs
In multi-user applications, create `tools` inside your request handler so each request closes over the correct `bankId`. See the [Next.js example](#in-a-nextjs-route-handler) below.
:::
## Usage
### With `generateText`
<CodeSnippet code={aiSdkTs} section="generate-text" language="typescript" />
### With `streamText`
<CodeSnippet code={aiSdkTs} section="stream-text" language="typescript" />
### With `ToolLoopAgent`
<CodeSnippet code={aiSdkTs} section="tool-loop-agent" language="typescript" />
### In a Next.js Route Handler
<CodeSnippet code={aiSdkTs} section="next-api-route" language="typescript" />
---
## Tools Reference
Five tools are registered. The `bankId` is fixed at creation time—the agent cannot change it.
| Tool | What the agent provides | What the constructor controls |
|------|------------------------|-------------------------------|
| `retain` | `content`, `documentId`, `timestamp`, `context` | `async`, `tags`, `metadata` |
| `recall` | `query`, `queryTimestamp` | `budget`, `types`, `maxTokens`, `includeEntities`, `includeChunks` |
| `reflect` | `query`, `context` | `budget` |
| `getMentalModel` | `mentalModelId` | — |
| `getDocument` | `documentId` | — |
**Why this split?** Semantic inputs (what to remember, what to search for) belong to the agent. Infrastructure concerns (cost budget, tagging strategy, async mode) belong to the application.
---
## Constructor Options
All options except `client` and `bankId` are optional. Each tool's options are grouped under the tool name.
<CodeSnippet code={aiSdkTs} section="constructor-options" language="typescript" />
### `retain`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `async` | `boolean` | `false` | Fire-and-forget — do not wait for ingestion to complete |
| `tags` | `string[]` | — | Tags attached to every retained memory |
| `metadata` | `Record<string, string>` | — | Metadata attached to every retained memory |
| `description` | `string` | built-in | Override the tool description shown to the model |
### `recall`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `budget` | `'low' \| 'mid' \| 'high'` | `'mid'` | Controls retrieval depth and latency |
| `types` | `('world' \| 'experience' \| 'observation')[]` | all | Restrict results to these fact types |
| `maxTokens` | `number` | API default | Cap the total tokens returned |
| `includeEntities` | `boolean` | `false` | Include entity observations in results |
| `includeChunks` | `boolean` | `false` | Include raw source chunks in results |
| `description` | `string` | built-in | Override the tool description shown to the model |
### `reflect`
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `budget` | `'low' \| 'mid' \| 'high'` | `'mid'` | Controls synthesis depth and latency |
| `maxTokens` | `number` | API default | Maximum tokens for the response |
| `description` | `string` | built-in | Override the tool description shown to the model |
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"os"
"time"
hindsight "github.com/vectorize-io/hindsight/hindsight-clients/go"
hindsight "github.com/vectorize-io/hindsight-client-go"
)
func main() {
@@ -1,122 +0,0 @@
/**
* Hindsight AI SDK integration examples
* These snippets are embedded in the documentation via CodeSnippet.
*/
// [docs:setup]
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
const tools = createHindsightTools({
client,
bankId: 'user-123',
});
// [/docs:setup]
// [docs:generate-text]
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const { text } = await generateText({
model: openai('gpt-4o'),
tools,
maxSteps: 5,
system: 'You are a helpful assistant with long-term memory.',
prompt: 'Remember that I prefer dark mode and large fonts.',
});
// [/docs:generate-text]
// [docs:stream-text]
import { streamText } from 'ai';
const result = streamText({
model: openai('gpt-4o'),
tools,
maxSteps: 5,
system: 'You are a helpful assistant with long-term memory.',
prompt: 'What are my display preferences?',
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
// [/docs:stream-text]
// [docs:tool-loop-agent]
import { generateText, ToolLoopAgent, stepCountIs } from 'ai';
import { openai } from '@ai-sdk/openai';
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const client = new HindsightClient({ baseUrl: process.env.HINDSIGHT_API_URL! });
const agent = new ToolLoopAgent({
model: openai('gpt-4o'),
tools: createHindsightTools({ client, bankId: 'user-123' }),
stopWhen: stepCountIs(10),
system: 'You are a helpful assistant with long-term memory.',
});
const result = await agent.generate({
prompt: 'Remember that my favorite editor is Neovim',
});
// [/docs:tool-loop-agent]
// [docs:next-api-route]
// app/api/chat/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { HindsightClient } from '@vectorize-io/hindsight-client';
import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk';
const hindsightClient = new HindsightClient({
baseUrl: process.env.HINDSIGHT_API_URL!,
});
export async function POST(req: Request) {
const { messages, userId } = await req.json();
// Tools are created per-request, closing over the current user's bankId
const tools = createHindsightTools({
client: hindsightClient,
bankId: userId,
});
return streamText({
model: openai('gpt-4o'),
tools,
maxSteps: 5,
system: 'You are a helpful assistant with long-term memory.',
messages,
}).toDataStreamResponse();
}
// [/docs:next-api-route]
// [docs:constructor-options]
const tools = createHindsightTools({
client,
bankId: userId,
retain: {
async: true, // fire-and-forget (default: false)
tags: ['env:prod', 'app:support'], // always attached to every retained memory
metadata: { version: '2.0' }, // always attached to every retained memory
},
recall: {
budget: 'high', // processing depth: low | mid | high (default: 'mid')
types: ['experience', 'world'], // restrict to these fact types (default: all)
maxTokens: 2048, // cap token budget (default: API default)
includeEntities: true, // include entity observations (default: false)
includeChunks: true, // include raw source chunks (default: false)
},
reflect: {
budget: 'mid', // processing depth (default: 'mid')
},
});
// [/docs:constructor-options]
@@ -9,7 +9,7 @@ Official Go client for the Hindsight API, built on [ogen](https://github.com/oge
## Installation
```bash
go get github.com/vectorize-io/hindsight/hindsight-clients/go
go get github.com/vectorize-io/hindsight-client-go
```
Requires Go 1.25+.
@@ -24,7 +24,7 @@ import (
"fmt"
"log"
hindsight "github.com/vectorize-io/hindsight/hindsight-clients/go"
hindsight "github.com/vectorize-io/hindsight-client-go"
)
func main() {
@@ -53,7 +53,7 @@ func main() {
## Client Initialization
```go
import hindsight "github.com/vectorize-io/hindsight/hindsight-clients/go"
import hindsight "github.com/vectorize-io/hindsight-client-go"
// Default client
client, err := hindsight.New("http://localhost:8888")
@@ -231,7 +231,7 @@ resp, _ := client.Reflect(ctx, "my-bank", "Summarize project X",
For operations not covered by the high-level wrapper (documents, entities, mental models, directives, operations), access the generated ogen client directly:
```go
import "github.com/vectorize-io/hindsight/hindsight-clients/go/internal/ogenapi"
import "github.com/vectorize-io/hindsight-client-go/internal/ogenapi"
ogen := client.OgenClient()
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@vectorize-io/hindsight-ai-sdk",
"version": "0.4.11",
"version": "0.4.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@vectorize-io/hindsight-ai-sdk",
"version": "0.4.11",
"version": "0.4.8",
"license": "MIT",
"devDependencies": {
"@types/node": "^22.0.0",
@@ -13,34 +13,31 @@ describe('createHindsightTools', () => {
});
describe('tool creation', () => {
it('should create all tools', () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
it('should create all three tools', () => {
const tools = createHindsightTools({ client: mockClient });
expect(tools).toHaveProperty('retain');
expect(tools).toHaveProperty('recall');
expect(tools).toHaveProperty('reflect');
expect(tools).toHaveProperty('getMentalModel');
expect(tools).toHaveProperty('getDocument');
expect(typeof tools.retain.execute).toBe('function');
expect(typeof tools.recall.execute).toBe('function');
expect(typeof tools.reflect.execute).toBe('function');
});
it('should use default descriptions when not provided', () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
const tools = createHindsightTools({ client: mockClient });
expect(tools.retain.description).toContain('Store information in long-term memory');
expect(tools.recall.description).toContain('Search memory for relevant information');
expect(tools.reflect.description).toContain('Analyze memories to form insights');
});
it('should use custom descriptions from nested options', () => {
it('should use custom descriptions when provided', () => {
const tools = createHindsightTools({
client: mockClient,
bankId: 'test-bank',
retain: { description: 'Custom retain description' },
recall: { description: 'Custom recall description' },
reflect: { description: 'Custom reflect description' },
retainDescription: 'Custom retain description',
recallDescription: 'Custom recall description',
reflectDescription: 'Custom reflect description',
});
expect(tools.retain.description).toBe('Custom retain description');
@@ -50,8 +47,8 @@ describe('createHindsightTools', () => {
});
describe('retain tool', () => {
it('should call client.retain with agent inputs and constructor defaults', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
it('should call client.retain with correct parameters', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.retain).mockResolvedValue({
success: true,
bank_id: 'test-bank',
@@ -59,21 +56,21 @@ describe('createHindsightTools', () => {
async: false,
});
const result = await tools.retain.execute({ content: 'Test content' });
const result = await tools.retain.execute({
bankId: 'test-bank',
content: 'Test content',
});
expect(mockClient.retain).toHaveBeenCalledWith('test-bank', 'Test content', {
documentId: undefined,
timestamp: undefined,
context: undefined,
tags: undefined,
metadata: undefined,
async: false,
});
expect(result).toEqual({ success: true, itemsCount: 5 });
});
it('should pass agent-provided optional inputs', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
it('should pass optional parameters to client.retain', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.retain).mockResolvedValue({
success: true,
bank_id: 'test-bank',
@@ -82,6 +79,7 @@ describe('createHindsightTools', () => {
});
await tools.retain.execute({
bankId: 'test-bank',
content: 'Test content',
documentId: 'doc-123',
timestamp: '2024-01-01T00:00:00Z',
@@ -92,123 +90,100 @@ describe('createHindsightTools', () => {
documentId: 'doc-123',
timestamp: '2024-01-01T00:00:00Z',
context: 'Test context',
tags: undefined,
metadata: undefined,
async: false,
});
});
it('should apply constructor-level retain options', async () => {
const tools = createHindsightTools({
client: mockClient,
bankId: 'test-bank',
retain: {
async: true,
tags: ['env:prod', 'app:support'],
metadata: { version: '1.0' },
},
});
it('should transform response correctly', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.retain).mockResolvedValue({
success: true,
bank_id: 'test-bank',
items_count: 1,
async: true,
items_count: 10,
async: false,
});
await tools.retain.execute({ content: 'Test content' });
expect(mockClient.retain).toHaveBeenCalledWith('test-bank', 'Test content', {
documentId: undefined,
timestamp: undefined,
context: undefined,
tags: ['env:prod', 'app:support'],
metadata: { version: '1.0' },
async: true,
const result = await tools.retain.execute({
bankId: 'test-bank',
content: 'Test content',
});
expect(result).toEqual({ success: true, itemsCount: 10 });
});
});
describe('recall tool', () => {
it('should call client.recall with agent inputs and constructor defaults', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
it('should call client.recall with correct parameters', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.recall).mockResolvedValue({
results: [{ id: 'fact-1', text: 'Test fact', type: 'preference' }],
results: [
{
id: 'fact-1',
text: 'Test fact',
type: 'preference',
},
],
});
const result = await tools.recall.execute({ query: 'Test query' });
const result = await tools.recall.execute({
bankId: 'test-bank',
query: 'Test query',
});
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
types: undefined,
maxTokens: undefined,
budget: 'mid',
budget: undefined,
queryTimestamp: undefined,
includeEntities: false,
includeChunks: false,
includeEntities: undefined,
includeChunks: undefined,
});
expect(result.results).toHaveLength(1);
expect(result.results[0].id).toBe('fact-1');
});
it('should pass agent-provided queryTimestamp', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
it('should pass all optional parameters to client.recall', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.recall).mockResolvedValue({
results: [],
});
await tools.recall.execute({
query: 'Test query',
queryTimestamp: '2024-01-01T00:00:00Z',
});
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
types: undefined,
maxTokens: undefined,
budget: 'mid',
queryTimestamp: '2024-01-01T00:00:00Z',
includeEntities: false,
includeChunks: false,
});
});
it('should apply constructor-level recall options', async () => {
const tools = createHindsightTools({
client: mockClient,
bankId: 'test-bank',
recall: {
types: ['preference', 'fact'],
maxTokens: 1000,
budget: 'high',
includeEntities: true,
includeChunks: true,
},
query: 'Test query',
types: ['preference', 'fact'],
maxTokens: 1000,
budget: 'high',
queryTimestamp: '2024-01-01T00:00:00Z',
includeEntities: true,
includeChunks: true,
});
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
await tools.recall.execute({ query: 'Test query' });
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
types: ['preference', 'fact'],
maxTokens: 1000,
budget: 'high',
queryTimestamp: undefined,
queryTimestamp: '2024-01-01T00:00:00Z',
includeEntities: true,
includeChunks: true,
});
});
it('should handle empty results', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.recall).mockResolvedValue({ results: undefined as any });
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.recall).mockResolvedValue({
results: undefined as any,
});
const result = await tools.recall.execute({ query: 'Test query' });
const result = await tools.recall.execute({
bankId: 'test-bank',
query: 'Test query',
});
expect(result.results).toEqual([]);
});
it('should include entities when present', async () => {
const tools = createHindsightTools({
client: mockClient,
bankId: 'test-bank',
recall: { includeEntities: true },
});
const tools = createHindsightTools({ client: mockClient });
const entities = {
'entity-1': {
entity_id: 'entity-1',
@@ -216,39 +191,59 @@ describe('createHindsightTools', () => {
observations: [{ text: 'Alice loves hiking' }],
},
};
vi.mocked(mockClient.recall).mockResolvedValue({ results: [], entities });
const result = await tools.recall.execute({ query: 'Test query' });
vi.mocked(mockClient.recall).mockResolvedValue({
results: [],
entities,
});
const result = await tools.recall.execute({
bankId: 'test-bank',
query: 'Test query',
includeEntities: true,
});
expect(result.entities).toEqual(entities);
});
});
describe('reflect tool', () => {
it('should call client.reflect with agent inputs and constructor defaults', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
it('should call client.reflect with correct parameters', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.reflect).mockResolvedValue({
text: 'Reflection result',
based_on: [{ id: 'fact-1', text: 'Supporting fact' }],
based_on: [
{
id: 'fact-1',
text: 'Supporting fact',
},
],
});
const result = await tools.reflect.execute({ query: 'What are my preferences?' });
const result = await tools.reflect.execute({
bankId: 'test-bank',
query: 'What are my preferences?',
});
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', {
context: undefined,
budget: 'mid',
budget: undefined,
});
expect(result.text).toBe('Reflection result');
expect(result.basedOn).toHaveLength(1);
});
it('should pass agent-provided context', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.reflect).mockResolvedValue({ text: 'Reflection result' });
it('should pass optional parameters to client.reflect', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.reflect).mockResolvedValue({
text: 'Reflection result',
});
await tools.reflect.execute({
bankId: 'test-bank',
query: 'What are my preferences?',
context: 'User context',
budget: 'mid',
});
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', {
@@ -257,43 +252,44 @@ describe('createHindsightTools', () => {
});
});
it('should apply constructor-level reflect budget', async () => {
const tools = createHindsightTools({
client: mockClient,
bankId: 'test-bank',
reflect: { budget: 'low' },
});
vi.mocked(mockClient.reflect).mockResolvedValue({ text: 'Reflection result' });
await tools.reflect.execute({ query: 'Test query' });
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'Test query', {
context: undefined,
budget: 'low',
});
});
it('should handle empty text response with fallback', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.reflect).mockResolvedValue({ text: undefined as any });
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.reflect).mockResolvedValue({
text: undefined as any,
});
const result = await tools.reflect.execute({ query: 'Test query' });
const result = await tools.reflect.execute({
bankId: 'test-bank',
query: 'Test query',
});
expect(result.text).toBe('No insights available yet.');
});
it('should include basedOn facts when present', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
const tools = createHindsightTools({ client: mockClient });
const basedOn = [
{ id: 'fact-1', text: 'User prefers spicy food', type: 'preference' },
{ id: 'fact-2', text: 'User is allergic to nuts', type: 'health' },
{
id: 'fact-1',
text: 'User prefers spicy food',
type: 'preference',
},
{
id: 'fact-2',
text: 'User is allergic to nuts',
type: 'health',
},
];
vi.mocked(mockClient.reflect).mockResolvedValue({
text: 'Based on your history, you prefer spicy Asian cuisine',
based_on: basedOn,
});
const result = await tools.reflect.execute({ query: 'What do I like?' });
const result = await tools.reflect.execute({
bankId: 'test-bank',
query: 'What do I like?',
});
expect(result.basedOn).toEqual(basedOn);
});
@@ -301,70 +297,66 @@ describe('createHindsightTools', () => {
describe('error handling', () => {
it('should propagate errors from client.retain', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.retain).mockRejectedValue(new Error('Retain failed'));
const tools = createHindsightTools({ client: mockClient });
const error = new Error('Retain failed');
vi.mocked(mockClient.retain).mockRejectedValue(error);
await expect(tools.retain.execute({ content: 'Test content' })).rejects.toThrow('Retain failed');
await expect(
tools.retain.execute({
bankId: 'test-bank',
content: 'Test content',
})
).rejects.toThrow('Retain failed');
});
it('should propagate errors from client.recall', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.recall).mockRejectedValue(new Error('Recall failed'));
const tools = createHindsightTools({ client: mockClient });
const error = new Error('Recall failed');
vi.mocked(mockClient.recall).mockRejectedValue(error);
await expect(tools.recall.execute({ query: 'Test query' })).rejects.toThrow('Recall failed');
await expect(
tools.recall.execute({
bankId: 'test-bank',
query: 'Test query',
})
).rejects.toThrow('Recall failed');
});
it('should propagate errors from client.reflect', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.reflect).mockRejectedValue(new Error('Reflect failed'));
const tools = createHindsightTools({ client: mockClient });
const error = new Error('Reflect failed');
vi.mocked(mockClient.reflect).mockRejectedValue(error);
await expect(tools.reflect.execute({ query: 'Test query' })).rejects.toThrow('Reflect failed');
await expect(
tools.reflect.execute({
bankId: 'test-bank',
query: 'Test query',
})
).rejects.toThrow('Reflect failed');
});
});
describe('budget defaults', () => {
it('should default recall budget to mid', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
await tools.recall.execute({ query: 'Test' });
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test', expect.objectContaining({ budget: 'mid' }));
});
it('should default reflect budget to mid', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
vi.mocked(mockClient.reflect).mockResolvedValue({ text: 'ok' });
await tools.reflect.execute({ query: 'Test' });
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'Test', expect.objectContaining({ budget: 'mid' }));
});
it('should accept low/mid/high budget values', async () => {
describe('budget schema', () => {
it('should accept valid budget values', async () => {
const tools = createHindsightTools({ client: mockClient });
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
for (const budget of ['low', 'mid', 'high'] as const) {
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank', recall: { budget } });
await tools.recall.execute({ query: 'Test' });
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test', expect.objectContaining({ budget }));
await tools.recall.execute({
bankId: 'test-bank',
query: 'Test',
budget,
});
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test', {
types: undefined,
maxTokens: undefined,
budget,
queryTimestamp: undefined,
includeEntities: undefined,
includeChunks: undefined,
});
}
});
});
describe('bankId enforcement', () => {
it('should always use the bankId from constructor options', async () => {
const tools = createHindsightTools({ client: mockClient, bankId: 'forced-bank' });
vi.mocked(mockClient.retain).mockResolvedValue({
success: true,
bank_id: 'forced-bank',
items_count: 1,
async: false,
});
await tools.retain.execute({ content: 'Test' });
expect(mockClient.retain).toHaveBeenCalledWith('forced-bank', 'Test', expect.anything());
});
});
});
+219 -105
View File
@@ -7,12 +7,6 @@ import { z } from 'zod';
export const BudgetSchema = z.enum(['low', 'mid', 'high']);
export type Budget = z.infer<typeof BudgetSchema>;
/**
* Fact types for filtering recall results.
*/
export const FactTypeSchema = z.enum(['world', 'experience', 'observation']);
export type FactType = z.infer<typeof FactTypeSchema>;
/**
* Recall result item from Hindsight
*/
@@ -111,6 +105,15 @@ export interface MentalModelResponse {
trigger?: MentalModelTrigger;
}
/**
* Create mental model response from Hindsight
*/
export interface CreateMentalModelResponse {
mental_model_id: string;
bank_id: string;
created_at: string;
}
/**
* Document response from Hindsight
*/
@@ -125,6 +128,36 @@ export interface DocumentResponse {
tags?: string[];
}
/**
* Directive response from Hindsight
*/
export interface DirectiveResponse {
id: string;
bank_id: string;
name: string;
content: string;
priority: number;
is_active: boolean;
tags: string[];
created_at: string;
updated_at: string;
}
/**
* Create directive response from Hindsight
*/
export interface CreateDirectiveResponse {
id: string;
bank_id: string;
name: string;
content: string;
priority: number;
is_active: boolean;
tags: string[];
created_at: string;
updated_at: string;
}
/**
* Hindsight client interface - matches @vectorize-io/hindsight-client
*/
@@ -146,7 +179,7 @@ export interface HindsightClient {
bankId: string,
query: string,
options?: {
types?: FactType[];
types?: string[];
maxTokens?: number;
budget?: Budget;
trace?: boolean;
@@ -164,10 +197,21 @@ export interface HindsightClient {
options?: {
context?: string;
budget?: Budget;
maxTokens?: number;
}
): Promise<ReflectResponse>;
createMentalModel(
bankId: string,
options?: {
id?: string;
name?: string;
sourceQuery?: string;
tags?: string[];
maxTokens?: number;
trigger?: MentalModelTrigger;
}
): Promise<CreateMentalModelResponse>;
getMentalModel(
bankId: string,
mentalModelId: string
@@ -177,125 +221,148 @@ export interface HindsightClient {
bankId: string,
documentId: string
): Promise<DocumentResponse | null>;
createDirective(
bankId: string,
options: {
name: string;
content: string;
priority?: number;
isActive?: boolean;
tags?: string[];
}
): Promise<CreateDirectiveResponse>;
listDirectives(
bankId: string,
options?: {
tags?: string[];
tagsMatch?: 'any' | 'all' | 'exact';
activeOnly?: boolean;
limit?: number;
offset?: number;
}
): Promise<{ directives: DirectiveResponse[]; total: number }>;
}
export interface HindsightToolsOptions {
/** Hindsight client instance */
client: HindsightClient;
/** Memory bank ID to use for all tool calls (e.g. the user ID) */
bankId: string;
/** Options for the retain tool */
retain?: {
/** Fire-and-forget retain without waiting for completion (default: false) */
async?: boolean;
/** Tags always attached to every retained memory (default: undefined) */
tags?: string[];
/** Metadata always attached to every retained memory (default: undefined) */
metadata?: Record<string, string>;
/** Custom tool description */
description?: string;
};
/** Options for the recall tool */
recall?: {
/** Restrict results to these fact types: 'world', 'experience', 'observation' (default: undefined = all types) */
types?: FactType[];
/** Maximum tokens to return (default: undefined = API default) */
maxTokens?: number;
/** Processing budget controlling latency vs. depth (default: 'mid') */
budget?: Budget;
/** Include entity observations in results (default: false) */
includeEntities?: boolean;
/** Include raw source chunks in results (default: false) */
includeChunks?: boolean;
/** Custom tool description */
description?: string;
};
/** Options for the reflect tool */
reflect?: {
/** Processing budget controlling latency vs. depth (default: 'mid') */
budget?: Budget;
/** Maximum tokens for the response (default: undefined = API default) */
maxTokens?: number;
/** Custom tool description */
description?: string;
};
/** Options for the getMentalModel tool */
getMentalModel?: {
/** Custom tool description */
description?: string;
};
/** Options for the getDocument tool */
getDocument?: {
/** Custom tool description */
description?: string;
};
/**
* Custom description for the retain tool.
*/
retainDescription?: string;
/**
* Custom description for the recall tool.
*/
recallDescription?: string;
/**
* Custom description for the reflect tool.
*/
reflectDescription?: string;
/**
* Custom description for the createMentalModel tool.
*/
createMentalModelDescription?: string;
/**
* Custom description for the queryMentalModel tool.
*/
queryMentalModelDescription?: string;
/**
* Custom description for the getDocument tool.
*/
getDocumentDescription?: string;
}
/**
* Creates AI SDK tools for Hindsight memory operations.
*
* The bank ID and all infrastructure concerns (budget, tags, async mode, etc.)
* are fixed at creation time. The agent only controls semantic inputs:
* content, queries, names, and timestamps.
* Features:
* - Dynamic bank ID per call (supports multi-user/multi-bank scenarios)
* - Full API parameter support for retain, recall, and reflect
* - Ready to use with streamText, generateText, or ToolLoopAgent
*
* @example
* ```ts
* const tools = createHindsightTools({
* client: hindsightClient,
* bankId: userId,
* recall: { budget: 'high', includeEntities: true },
* retain: { async: true, tags: ['env:prod'] },
* });
*
* // Use with AI SDK
* const result = await generateText({
* model: openai('gpt-4o'),
* model: openai('gpt-4'),
* tools,
* messages,
* prompt: 'Remember that Alice loves hiking',
* });
* ```
*/
export function createHindsightTools({
client,
bankId,
retain: retainOpts = {},
recall: recallOpts = {},
reflect: reflectOpts = {},
getMentalModel: getMentalModelOpts = {},
getDocument: getDocumentOpts = {},
retainDescription,
recallDescription,
reflectDescription,
createMentalModelDescription,
queryMentalModelDescription,
getDocumentDescription,
}: HindsightToolsOptions) {
// Agent-controlled params only: content, timestamp, documentId, context
const retainParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
content: z.string().describe('Content to store in memory'),
documentId: z.string().optional().describe('Optional document ID for grouping/upserting content'),
timestamp: z.string().optional().describe('Optional ISO timestamp for when the memory occurred'),
context: z.string().optional().describe('Optional context about the memory'),
tags: z.array(z.string()).optional().describe('Optional tags for visibility scoping'),
metadata: z.record(z.string(), z.string()).optional().describe('Optional user-defined metadata'),
});
// Agent-controlled params only: query, queryTimestamp
const recallParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
query: z.string().describe('What to search for in memory'),
types: z.array(z.string()).optional().describe('Filter by fact types'),
maxTokens: z.number().optional().describe('Maximum tokens to return'),
budget: BudgetSchema.optional().describe('Processing budget: low, mid, or high'),
queryTimestamp: z.string().optional().describe('Query from a specific point in time (ISO format)'),
includeEntities: z.boolean().optional().describe('Include entity observations in results'),
includeChunks: z.boolean().optional().describe('Include raw chunks in results'),
});
// Agent-controlled params only: query, context
const reflectParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
query: z.string().describe('Question to reflect on based on memories'),
context: z.string().optional().describe('Additional context for the reflection'),
budget: BudgetSchema.optional().describe('Processing budget: low, mid, or high'),
});
const getMentalModelParams = z.object({
mentalModelId: z.string().describe('ID of the mental model to retrieve'),
const createMentalModelParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
mentalModelId: z.string().optional().describe('Optional custom ID for the mental model (auto-generated if not provided)'),
name: z.string().optional().describe('Optional name for the mental model'),
sourceQuery: z.string().optional().describe('Query to define what memories to consolidate'),
tags: z.array(z.string()).optional().describe('Optional tags for organizing mental models'),
maxTokens: z.number().optional().describe('Maximum tokens for the mental model content'),
autoRefresh: z.boolean().optional().describe('Auto-refresh mental model after new consolidations (default: false)'),
});
const queryMentalModelParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
mentalModelId: z.string().describe('ID of the mental model to query'),
});
const getDocumentParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
documentId: z.string().describe('ID of the document to retrieve'),
});
const createDirectiveParams = z.object({
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
name: z.string().describe('Human-readable name for the directive'),
content: z.string().describe('The directive text to inject into prompts'),
priority: z.number().optional().describe('Higher priority directives are injected first (default 0)'),
isActive: z.boolean().optional().describe('Whether this directive is active (default true)'),
tags: z.array(z.string()).optional().describe('Tags for filtering'),
});
type RetainInput = z.infer<typeof retainParams>;
type RetainOutput = { success: boolean; itemsCount: number };
@@ -305,31 +372,37 @@ export function createHindsightTools({
type ReflectInput = z.infer<typeof reflectParams>;
type ReflectOutput = { text: string; basedOn?: ReflectFact[] };
type GetMentalModelInput = z.infer<typeof getMentalModelParams>;
type GetMentalModelOutput = { content: string; name?: string; updatedAt: string };
type CreateMentalModelInput = z.infer<typeof createMentalModelParams>;
type CreateMentalModelOutput = { mentalModelId: string; createdAt: string };
type QueryMentalModelInput = z.infer<typeof queryMentalModelParams>;
type QueryMentalModelOutput = { content: string; name?: string; updatedAt: string };
type GetDocumentInput = z.infer<typeof getDocumentParams>;
type GetDocumentOutput = { originalText: string; id: string; createdAt: string; updatedAt: string } | null;
type CreateDirectiveInput = z.infer<typeof createDirectiveParams>;
type CreateDirectiveOutput = { id: string; name: string; content: string; tags: string[]; createdAt: string };
return {
retain: tool<RetainInput, RetainOutput>({
description:
retainOpts.description ??
retainDescription ??
`Store information in long-term memory. Use this when information should be remembered for future interactions, such as user preferences, facts, experiences, or important context.`,
inputSchema: retainParams,
execute: async (input) => {
console.log('[AI SDK Tool] Retain input:', {
bankId,
bankId: input.bankId,
documentId: input.documentId,
tags: input.tags,
hasContent: !!input.content,
});
const result = await client.retain(bankId, input.content, {
const result = await client.retain(input.bankId, input.content, {
documentId: input.documentId,
timestamp: input.timestamp,
context: input.context,
tags: retainOpts.tags,
metadata: retainOpts.metadata,
async: retainOpts.async ?? false,
tags: input.tags,
metadata: input.metadata as Record<string, string> | undefined,
});
return { success: result.success, itemsCount: result.items_count };
},
@@ -337,17 +410,17 @@ export function createHindsightTools({
recall: tool<RecallInput, RecallOutput>({
description:
recallOpts.description ??
recallDescription ??
`Search memory for relevant information. Use this to find previously stored information that can help personalize responses or provide context.`,
inputSchema: recallParams,
execute: async (input) => {
const result = await client.recall(bankId, input.query, {
types: recallOpts.types,
maxTokens: recallOpts.maxTokens,
budget: recallOpts.budget ?? 'mid',
const result = await client.recall(input.bankId, input.query, {
types: input.types,
maxTokens: input.maxTokens,
budget: input.budget,
queryTimestamp: input.queryTimestamp,
includeEntities: recallOpts.includeEntities ?? false,
includeChunks: recallOpts.includeChunks ?? false,
includeEntities: input.includeEntities,
includeChunks: input.includeChunks,
});
return {
results: result.results ?? [],
@@ -358,14 +431,13 @@ export function createHindsightTools({
reflect: tool<ReflectInput, ReflectOutput>({
description:
reflectOpts.description ??
reflectDescription ??
`Analyze memories to form insights and generate contextual answers. Use this to understand patterns, synthesize information, or answer questions that require reasoning over stored memories.`,
inputSchema: reflectParams,
execute: async (input) => {
const result = await client.reflect(bankId, input.query, {
const result = await client.reflect(input.bankId, input.query, {
context: input.context,
budget: reflectOpts.budget ?? 'mid',
maxTokens: reflectOpts.maxTokens,
budget: input.budget,
});
return {
text: result.text ?? 'No insights available yet.',
@@ -374,13 +446,34 @@ export function createHindsightTools({
},
}),
getMentalModel: tool<GetMentalModelInput, GetMentalModelOutput>({
createMentalModel: tool<CreateMentalModelInput, CreateMentalModelOutput>({
description:
getMentalModelOpts.description ??
`Retrieve a mental model to get consolidated knowledge synthesized from memories. Mental models provide synthesized insights that are faster and more efficient to retrieve than searching through raw memories.`,
inputSchema: getMentalModelParams,
createMentalModelDescription ??
`Create a mental model that automatically consolidates memories into structured knowledge. Mental models are continuously updated as new memories are added, making them ideal for maintaining up-to-date user preferences, behavioral patterns, and accumulated wisdom.`,
inputSchema: createMentalModelParams,
execute: async (input) => {
const result = await client.getMentalModel(bankId, input.mentalModelId);
const result = await client.createMentalModel(input.bankId, {
id: input.mentalModelId,
name: input.name,
sourceQuery: input.sourceQuery,
tags: input.tags,
maxTokens: input.maxTokens,
trigger: input.autoRefresh !== undefined ? { refresh_after_consolidation: input.autoRefresh } : undefined,
});
return {
mentalModelId: result.mental_model_id,
createdAt: result.created_at,
};
},
}),
queryMentalModel: tool<QueryMentalModelInput, QueryMentalModelOutput>({
description:
queryMentalModelDescription ??
`Query an existing mental model to retrieve consolidated knowledge. Mental models provide synthesized insights from memories, making them faster and more efficient than searching through raw memories.`,
inputSchema: queryMentalModelParams,
execute: async (input) => {
const result = await client.getMentalModel(input.bankId, input.mentalModelId);
return {
content: result.content ?? 'No content available yet.',
name: result.name,
@@ -391,11 +484,11 @@ export function createHindsightTools({
getDocument: tool<GetDocumentInput, GetDocumentOutput>({
description:
getDocumentOpts.description ??
getDocumentDescription ??
`Retrieve a stored document by its ID. Documents are used to store structured data like application state, user profiles, or any data that needs exact retrieval.`,
inputSchema: getDocumentParams,
execute: async (input) => {
const result = await client.getDocument(bankId, input.documentId);
const result = await client.getDocument(input.bankId, input.documentId);
if (!result) {
return null;
}
@@ -408,6 +501,27 @@ export function createHindsightTools({
},
}),
createDirective: tool<CreateDirectiveInput, CreateDirectiveOutput>({
description:
`Create a directive - a hard rule that is injected into prompts during reflect operations. Directives are explicit instructions that guide agent behavior. Use tags to control when directives are applied (e.g., user-specific directives with 'user:username' tags).`,
inputSchema: createDirectiveParams,
execute: async (input) => {
const result = await client.createDirective(input.bankId, {
name: input.name,
content: input.content,
priority: input.priority,
isActive: input.isActive,
tags: input.tags,
});
return {
id: result.id,
name: result.name,
content: result.content,
tags: result.tags,
createdAt: result.created_at,
};
},
}),
};
}
+1 -1
View File
@@ -381,7 +381,7 @@ else
-o . \
--package-name hindsight \
--git-user-id vectorize-io \
--git-repo-id hindsight/hindsight-clients/go \
--git-repo-id hindsight-client-go \
--global-property apiDocs=false,apiTests=false,modelDocs=false,modelTests=false
# Remove OpenAPI Generator boilerplate files