Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79f683cdf9 | ||
|
|
ce74b1fc56 |
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (<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 (<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
|
||||
|
||||
@@ -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 |
|
||||
@@ -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
@@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user