feat: improve ai sdk tools (#394)
This commit is contained in:
parent
be8728b313
commit
d06a0259cc
7 changed files with 496 additions and 749 deletions
|
|
@ -68,7 +68,7 @@ Hindsight supports three PostgreSQL vector extensions:
|
||||||
#### **pgvector** (HNSW - default)
|
#### **pgvector** (HNSW - default)
|
||||||
- In-memory index using Hierarchical Navigable Small World algorithm
|
- In-memory index using Hierarchical Navigable Small World algorithm
|
||||||
- Works well for most embeddings and dataset sizes
|
- 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
|
- Higher memory usage for large datasets
|
||||||
- Most widely deployed and supported
|
- Most widely deployed and supported
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ Hindsight supports three PostgreSQL vector extensions:
|
||||||
- When disk I/O is not a bottleneck
|
- When disk I/O is not a bottleneck
|
||||||
|
|
||||||
**When to use pgvector (HNSW):**
|
**When to use pgvector (HNSW):**
|
||||||
- Small-medium datasets (<10M vectors)
|
- Small-medium datasets (<10M vectors)
|
||||||
- Maximum query speed when all data fits in memory
|
- Maximum query speed when all data fits in memory
|
||||||
- Simple nearest-neighbor queries without filters
|
- Simple nearest-neighbor queries without filters
|
||||||
- Standard PostgreSQL deployment preference
|
- Standard PostgreSQL deployment preference
|
||||||
|
|
|
||||||
|
|
@ -1,366 +0,0 @@
|
||||||
---
|
|
||||||
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',
|
|
||||||
});
|
|
||||||
```
|
|
||||||
97
hindsight-docs/docs/sdks/integrations/ai-sdk.mdx
Normal file
97
hindsight-docs/docs/sdks/integrations/ai-sdk.mdx
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
---
|
||||||
|
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 |
|
||||||
122
hindsight-docs/examples/integrations/ai-sdk.ts
Normal file
122
hindsight-docs/examples/integrations/ai-sdk.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
/**
|
||||||
|
* 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]
|
||||||
4
hindsight-integrations/ai-sdk/package-lock.json
generated
4
hindsight-integrations/ai-sdk/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@vectorize-io/hindsight-ai-sdk",
|
"name": "@vectorize-io/hindsight-ai-sdk",
|
||||||
"version": "0.4.8",
|
"version": "0.4.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@vectorize-io/hindsight-ai-sdk",
|
"name": "@vectorize-io/hindsight-ai-sdk",
|
||||||
"version": "0.4.8",
|
"version": "0.4.11",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
|
|
||||||
|
|
@ -13,31 +13,34 @@ describe('createHindsightTools', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('tool creation', () => {
|
describe('tool creation', () => {
|
||||||
it('should create all three tools', () => {
|
it('should create all tools', () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
|
|
||||||
expect(tools).toHaveProperty('retain');
|
expect(tools).toHaveProperty('retain');
|
||||||
expect(tools).toHaveProperty('recall');
|
expect(tools).toHaveProperty('recall');
|
||||||
expect(tools).toHaveProperty('reflect');
|
expect(tools).toHaveProperty('reflect');
|
||||||
|
expect(tools).toHaveProperty('getMentalModel');
|
||||||
|
expect(tools).toHaveProperty('getDocument');
|
||||||
expect(typeof tools.retain.execute).toBe('function');
|
expect(typeof tools.retain.execute).toBe('function');
|
||||||
expect(typeof tools.recall.execute).toBe('function');
|
expect(typeof tools.recall.execute).toBe('function');
|
||||||
expect(typeof tools.reflect.execute).toBe('function');
|
expect(typeof tools.reflect.execute).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use default descriptions when not provided', () => {
|
it('should use default descriptions when not provided', () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
|
|
||||||
expect(tools.retain.description).toContain('Store information in long-term memory');
|
expect(tools.retain.description).toContain('Store information in long-term memory');
|
||||||
expect(tools.recall.description).toContain('Search memory for relevant information');
|
expect(tools.recall.description).toContain('Search memory for relevant information');
|
||||||
expect(tools.reflect.description).toContain('Analyze memories to form insights');
|
expect(tools.reflect.description).toContain('Analyze memories to form insights');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use custom descriptions when provided', () => {
|
it('should use custom descriptions from nested options', () => {
|
||||||
const tools = createHindsightTools({
|
const tools = createHindsightTools({
|
||||||
client: mockClient,
|
client: mockClient,
|
||||||
retainDescription: 'Custom retain description',
|
bankId: 'test-bank',
|
||||||
recallDescription: 'Custom recall description',
|
retain: { description: 'Custom retain description' },
|
||||||
reflectDescription: 'Custom reflect description',
|
recall: { description: 'Custom recall description' },
|
||||||
|
reflect: { description: 'Custom reflect description' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(tools.retain.description).toBe('Custom retain description');
|
expect(tools.retain.description).toBe('Custom retain description');
|
||||||
|
|
@ -47,8 +50,8 @@ describe('createHindsightTools', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('retain tool', () => {
|
describe('retain tool', () => {
|
||||||
it('should call client.retain with correct parameters', async () => {
|
it('should call client.retain with agent inputs and constructor defaults', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
vi.mocked(mockClient.retain).mockResolvedValue({
|
vi.mocked(mockClient.retain).mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
bank_id: 'test-bank',
|
bank_id: 'test-bank',
|
||||||
|
|
@ -56,21 +59,21 @@ describe('createHindsightTools', () => {
|
||||||
async: false,
|
async: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tools.retain.execute({
|
const result = await tools.retain.execute({ content: 'Test content' });
|
||||||
bankId: 'test-bank',
|
|
||||||
content: 'Test content',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockClient.retain).toHaveBeenCalledWith('test-bank', 'Test content', {
|
expect(mockClient.retain).toHaveBeenCalledWith('test-bank', 'Test content', {
|
||||||
documentId: undefined,
|
documentId: undefined,
|
||||||
timestamp: undefined,
|
timestamp: undefined,
|
||||||
context: undefined,
|
context: undefined,
|
||||||
|
tags: undefined,
|
||||||
|
metadata: undefined,
|
||||||
|
async: false,
|
||||||
});
|
});
|
||||||
expect(result).toEqual({ success: true, itemsCount: 5 });
|
expect(result).toEqual({ success: true, itemsCount: 5 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass optional parameters to client.retain', async () => {
|
it('should pass agent-provided optional inputs', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
vi.mocked(mockClient.retain).mockResolvedValue({
|
vi.mocked(mockClient.retain).mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
bank_id: 'test-bank',
|
bank_id: 'test-bank',
|
||||||
|
|
@ -79,7 +82,6 @@ describe('createHindsightTools', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await tools.retain.execute({
|
await tools.retain.execute({
|
||||||
bankId: 'test-bank',
|
|
||||||
content: 'Test content',
|
content: 'Test content',
|
||||||
documentId: 'doc-123',
|
documentId: 'doc-123',
|
||||||
timestamp: '2024-01-01T00:00:00Z',
|
timestamp: '2024-01-01T00:00:00Z',
|
||||||
|
|
@ -90,100 +92,123 @@ describe('createHindsightTools', () => {
|
||||||
documentId: 'doc-123',
|
documentId: 'doc-123',
|
||||||
timestamp: '2024-01-01T00:00:00Z',
|
timestamp: '2024-01-01T00:00:00Z',
|
||||||
context: 'Test context',
|
context: 'Test context',
|
||||||
|
tags: undefined,
|
||||||
|
metadata: undefined,
|
||||||
|
async: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transform response correctly', async () => {
|
it('should apply constructor-level retain options', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({
|
||||||
|
client: mockClient,
|
||||||
|
bankId: 'test-bank',
|
||||||
|
retain: {
|
||||||
|
async: true,
|
||||||
|
tags: ['env:prod', 'app:support'],
|
||||||
|
metadata: { version: '1.0' },
|
||||||
|
},
|
||||||
|
});
|
||||||
vi.mocked(mockClient.retain).mockResolvedValue({
|
vi.mocked(mockClient.retain).mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
bank_id: 'test-bank',
|
bank_id: 'test-bank',
|
||||||
items_count: 10,
|
items_count: 1,
|
||||||
async: false,
|
async: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tools.retain.execute({
|
await tools.retain.execute({ content: 'Test content' });
|
||||||
bankId: 'test-bank',
|
|
||||||
content: 'Test content',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true, itemsCount: 10 });
|
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,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('recall tool', () => {
|
describe('recall tool', () => {
|
||||||
it('should call client.recall with correct parameters', async () => {
|
it('should call client.recall with agent inputs and constructor defaults', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
vi.mocked(mockClient.recall).mockResolvedValue({
|
vi.mocked(mockClient.recall).mockResolvedValue({
|
||||||
results: [
|
results: [{ id: 'fact-1', text: 'Test fact', type: 'preference' }],
|
||||||
{
|
|
||||||
id: 'fact-1',
|
|
||||||
text: 'Test fact',
|
|
||||||
type: 'preference',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tools.recall.execute({
|
const result = await tools.recall.execute({ query: 'Test query' });
|
||||||
bankId: 'test-bank',
|
|
||||||
query: 'Test query',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
|
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
|
||||||
types: undefined,
|
types: undefined,
|
||||||
maxTokens: undefined,
|
maxTokens: undefined,
|
||||||
budget: undefined,
|
budget: 'mid',
|
||||||
queryTimestamp: undefined,
|
queryTimestamp: undefined,
|
||||||
includeEntities: undefined,
|
includeEntities: false,
|
||||||
includeChunks: undefined,
|
includeChunks: false,
|
||||||
});
|
});
|
||||||
expect(result.results).toHaveLength(1);
|
expect(result.results).toHaveLength(1);
|
||||||
expect(result.results[0].id).toBe('fact-1');
|
expect(result.results[0].id).toBe('fact-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass all optional parameters to client.recall', async () => {
|
it('should pass agent-provided queryTimestamp', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
vi.mocked(mockClient.recall).mockResolvedValue({
|
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
|
||||||
results: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
await tools.recall.execute({
|
await tools.recall.execute({
|
||||||
bankId: 'test-bank',
|
|
||||||
query: 'Test query',
|
query: 'Test query',
|
||||||
types: ['preference', 'fact'],
|
|
||||||
maxTokens: 1000,
|
|
||||||
budget: 'high',
|
|
||||||
queryTimestamp: '2024-01-01T00:00:00Z',
|
queryTimestamp: '2024-01-01T00:00:00Z',
|
||||||
includeEntities: true,
|
|
||||||
includeChunks: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
|
||||||
|
|
||||||
|
await tools.recall.execute({ query: 'Test query' });
|
||||||
|
|
||||||
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
|
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', {
|
||||||
types: ['preference', 'fact'],
|
types: ['preference', 'fact'],
|
||||||
maxTokens: 1000,
|
maxTokens: 1000,
|
||||||
budget: 'high',
|
budget: 'high',
|
||||||
queryTimestamp: '2024-01-01T00:00:00Z',
|
queryTimestamp: undefined,
|
||||||
includeEntities: true,
|
includeEntities: true,
|
||||||
includeChunks: true,
|
includeChunks: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty results', async () => {
|
it('should handle empty results', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
vi.mocked(mockClient.recall).mockResolvedValue({
|
vi.mocked(mockClient.recall).mockResolvedValue({ results: undefined as any });
|
||||||
results: undefined as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tools.recall.execute({
|
const result = await tools.recall.execute({ query: 'Test query' });
|
||||||
bankId: 'test-bank',
|
|
||||||
query: 'Test query',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.results).toEqual([]);
|
expect(result.results).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include entities when present', async () => {
|
it('should include entities when present', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({
|
||||||
|
client: mockClient,
|
||||||
|
bankId: 'test-bank',
|
||||||
|
recall: { includeEntities: true },
|
||||||
|
});
|
||||||
const entities = {
|
const entities = {
|
||||||
'entity-1': {
|
'entity-1': {
|
||||||
entity_id: 'entity-1',
|
entity_id: 'entity-1',
|
||||||
|
|
@ -191,59 +216,39 @@ describe('createHindsightTools', () => {
|
||||||
observations: [{ text: 'Alice loves hiking' }],
|
observations: [{ text: 'Alice loves hiking' }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
vi.mocked(mockClient.recall).mockResolvedValue({ results: [], entities });
|
||||||
|
|
||||||
vi.mocked(mockClient.recall).mockResolvedValue({
|
const result = await tools.recall.execute({ query: 'Test query' });
|
||||||
results: [],
|
|
||||||
entities,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tools.recall.execute({
|
|
||||||
bankId: 'test-bank',
|
|
||||||
query: 'Test query',
|
|
||||||
includeEntities: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.entities).toEqual(entities);
|
expect(result.entities).toEqual(entities);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('reflect tool', () => {
|
describe('reflect tool', () => {
|
||||||
it('should call client.reflect with correct parameters', async () => {
|
it('should call client.reflect with agent inputs and constructor defaults', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
vi.mocked(mockClient.reflect).mockResolvedValue({
|
vi.mocked(mockClient.reflect).mockResolvedValue({
|
||||||
text: 'Reflection result',
|
text: 'Reflection result',
|
||||||
based_on: [
|
based_on: [{ id: 'fact-1', text: 'Supporting fact' }],
|
||||||
{
|
|
||||||
id: 'fact-1',
|
|
||||||
text: 'Supporting fact',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tools.reflect.execute({
|
const result = await tools.reflect.execute({ query: 'What are my preferences?' });
|
||||||
bankId: 'test-bank',
|
|
||||||
query: 'What are my preferences?',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', {
|
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', {
|
||||||
context: undefined,
|
context: undefined,
|
||||||
budget: undefined,
|
budget: 'mid',
|
||||||
});
|
});
|
||||||
expect(result.text).toBe('Reflection result');
|
expect(result.text).toBe('Reflection result');
|
||||||
expect(result.basedOn).toHaveLength(1);
|
expect(result.basedOn).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass optional parameters to client.reflect', async () => {
|
it('should pass agent-provided context', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
vi.mocked(mockClient.reflect).mockResolvedValue({
|
vi.mocked(mockClient.reflect).mockResolvedValue({ text: 'Reflection result' });
|
||||||
text: 'Reflection result',
|
|
||||||
});
|
|
||||||
|
|
||||||
await tools.reflect.execute({
|
await tools.reflect.execute({
|
||||||
bankId: 'test-bank',
|
|
||||||
query: 'What are my preferences?',
|
query: 'What are my preferences?',
|
||||||
context: 'User context',
|
context: 'User context',
|
||||||
budget: 'mid',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', {
|
expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', {
|
||||||
|
|
@ -252,44 +257,43 @@ describe('createHindsightTools', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty text response with fallback', async () => {
|
it('should apply constructor-level reflect budget', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({
|
||||||
vi.mocked(mockClient.reflect).mockResolvedValue({
|
client: mockClient,
|
||||||
text: undefined as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tools.reflect.execute({
|
|
||||||
bankId: 'test-bank',
|
bankId: 'test-bank',
|
||||||
query: 'Test query',
|
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 result = await tools.reflect.execute({ query: 'Test query' });
|
||||||
|
|
||||||
expect(result.text).toBe('No insights available yet.');
|
expect(result.text).toBe('No insights available yet.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include basedOn facts when present', async () => {
|
it('should include basedOn facts when present', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
const basedOn = [
|
const basedOn = [
|
||||||
{
|
{ id: 'fact-1', text: 'User prefers spicy food', type: 'preference' },
|
||||||
id: 'fact-1',
|
{ id: 'fact-2', text: 'User is allergic to nuts', type: 'health' },
|
||||||
text: 'User prefers spicy food',
|
|
||||||
type: 'preference',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'fact-2',
|
|
||||||
text: 'User is allergic to nuts',
|
|
||||||
type: 'health',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
vi.mocked(mockClient.reflect).mockResolvedValue({
|
vi.mocked(mockClient.reflect).mockResolvedValue({
|
||||||
text: 'Based on your history, you prefer spicy Asian cuisine',
|
text: 'Based on your history, you prefer spicy Asian cuisine',
|
||||||
based_on: basedOn,
|
based_on: basedOn,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await tools.reflect.execute({
|
const result = await tools.reflect.execute({ query: 'What do I like?' });
|
||||||
bankId: 'test-bank',
|
|
||||||
query: 'What do I like?',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.basedOn).toEqual(basedOn);
|
expect(result.basedOn).toEqual(basedOn);
|
||||||
});
|
});
|
||||||
|
|
@ -297,66 +301,70 @@ describe('createHindsightTools', () => {
|
||||||
|
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
it('should propagate errors from client.retain', async () => {
|
it('should propagate errors from client.retain', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
const error = new Error('Retain failed');
|
vi.mocked(mockClient.retain).mockRejectedValue(new Error('Retain failed'));
|
||||||
vi.mocked(mockClient.retain).mockRejectedValue(error);
|
|
||||||
|
|
||||||
await expect(
|
await expect(tools.retain.execute({ content: 'Test content' })).rejects.toThrow('Retain failed');
|
||||||
tools.retain.execute({
|
|
||||||
bankId: 'test-bank',
|
|
||||||
content: 'Test content',
|
|
||||||
})
|
|
||||||
).rejects.toThrow('Retain failed');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should propagate errors from client.recall', async () => {
|
it('should propagate errors from client.recall', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
const error = new Error('Recall failed');
|
vi.mocked(mockClient.recall).mockRejectedValue(new Error('Recall failed'));
|
||||||
vi.mocked(mockClient.recall).mockRejectedValue(error);
|
|
||||||
|
|
||||||
await expect(
|
await expect(tools.recall.execute({ query: 'Test query' })).rejects.toThrow('Recall failed');
|
||||||
tools.recall.execute({
|
|
||||||
bankId: 'test-bank',
|
|
||||||
query: 'Test query',
|
|
||||||
})
|
|
||||||
).rejects.toThrow('Recall failed');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should propagate errors from client.reflect', async () => {
|
it('should propagate errors from client.reflect', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank' });
|
||||||
const error = new Error('Reflect failed');
|
vi.mocked(mockClient.reflect).mockRejectedValue(new Error('Reflect failed'));
|
||||||
vi.mocked(mockClient.reflect).mockRejectedValue(error);
|
|
||||||
|
|
||||||
await expect(
|
await expect(tools.reflect.execute({ query: 'Test query' })).rejects.toThrow('Reflect failed');
|
||||||
tools.reflect.execute({
|
|
||||||
bankId: 'test-bank',
|
|
||||||
query: 'Test query',
|
|
||||||
})
|
|
||||||
).rejects.toThrow('Reflect failed');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('budget schema', () => {
|
describe('budget defaults', () => {
|
||||||
it('should accept valid budget values', async () => {
|
it('should default recall budget to mid', async () => {
|
||||||
const tools = createHindsightTools({ client: mockClient });
|
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 () => {
|
||||||
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
|
vi.mocked(mockClient.recall).mockResolvedValue({ results: [] });
|
||||||
|
|
||||||
for (const budget of ['low', 'mid', 'high'] as const) {
|
for (const budget of ['low', 'mid', 'high'] as const) {
|
||||||
await tools.recall.execute({
|
const tools = createHindsightTools({ client: mockClient, bankId: 'test-bank', recall: { budget } });
|
||||||
bankId: 'test-bank',
|
await tools.recall.execute({ query: 'Test' });
|
||||||
query: 'Test',
|
expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test', expect.objectContaining({ budget }));
|
||||||
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,6 +7,12 @@ import { z } from 'zod';
|
||||||
export const BudgetSchema = z.enum(['low', 'mid', 'high']);
|
export const BudgetSchema = z.enum(['low', 'mid', 'high']);
|
||||||
export type Budget = z.infer<typeof BudgetSchema>;
|
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
|
* Recall result item from Hindsight
|
||||||
*/
|
*/
|
||||||
|
|
@ -105,15 +111,6 @@ export interface MentalModelResponse {
|
||||||
trigger?: MentalModelTrigger;
|
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
|
* Document response from Hindsight
|
||||||
*/
|
*/
|
||||||
|
|
@ -128,36 +125,6 @@ export interface DocumentResponse {
|
||||||
tags?: string[];
|
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
|
* Hindsight client interface - matches @vectorize-io/hindsight-client
|
||||||
*/
|
*/
|
||||||
|
|
@ -179,7 +146,7 @@ export interface HindsightClient {
|
||||||
bankId: string,
|
bankId: string,
|
||||||
query: string,
|
query: string,
|
||||||
options?: {
|
options?: {
|
||||||
types?: string[];
|
types?: FactType[];
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
budget?: Budget;
|
budget?: Budget;
|
||||||
trace?: boolean;
|
trace?: boolean;
|
||||||
|
|
@ -197,21 +164,10 @@ export interface HindsightClient {
|
||||||
options?: {
|
options?: {
|
||||||
context?: string;
|
context?: string;
|
||||||
budget?: Budget;
|
budget?: Budget;
|
||||||
|
maxTokens?: number;
|
||||||
}
|
}
|
||||||
): Promise<ReflectResponse>;
|
): Promise<ReflectResponse>;
|
||||||
|
|
||||||
createMentalModel(
|
|
||||||
bankId: string,
|
|
||||||
options?: {
|
|
||||||
id?: string;
|
|
||||||
name?: string;
|
|
||||||
sourceQuery?: string;
|
|
||||||
tags?: string[];
|
|
||||||
maxTokens?: number;
|
|
||||||
trigger?: MentalModelTrigger;
|
|
||||||
}
|
|
||||||
): Promise<CreateMentalModelResponse>;
|
|
||||||
|
|
||||||
getMentalModel(
|
getMentalModel(
|
||||||
bankId: string,
|
bankId: string,
|
||||||
mentalModelId: string
|
mentalModelId: string
|
||||||
|
|
@ -221,148 +177,125 @@ export interface HindsightClient {
|
||||||
bankId: string,
|
bankId: string,
|
||||||
documentId: string
|
documentId: string
|
||||||
): Promise<DocumentResponse | null>;
|
): 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 {
|
export interface HindsightToolsOptions {
|
||||||
/** Hindsight client instance */
|
/** Hindsight client instance */
|
||||||
client: HindsightClient;
|
client: HindsightClient;
|
||||||
/**
|
/** Memory bank ID to use for all tool calls (e.g. the user ID) */
|
||||||
* Custom description for the retain tool.
|
bankId: string;
|
||||||
*/
|
|
||||||
retainDescription?: string;
|
/** Options for the retain tool */
|
||||||
/**
|
retain?: {
|
||||||
* Custom description for the recall tool.
|
/** Fire-and-forget retain without waiting for completion (default: false) */
|
||||||
*/
|
async?: boolean;
|
||||||
recallDescription?: string;
|
/** Tags always attached to every retained memory (default: undefined) */
|
||||||
/**
|
tags?: string[];
|
||||||
* Custom description for the reflect tool.
|
/** Metadata always attached to every retained memory (default: undefined) */
|
||||||
*/
|
metadata?: Record<string, string>;
|
||||||
reflectDescription?: string;
|
/** Custom tool description */
|
||||||
/**
|
description?: string;
|
||||||
* Custom description for the createMentalModel tool.
|
};
|
||||||
*/
|
|
||||||
createMentalModelDescription?: string;
|
/** Options for the recall tool */
|
||||||
/**
|
recall?: {
|
||||||
* Custom description for the queryMentalModel tool.
|
/** Restrict results to these fact types: 'world', 'experience', 'observation' (default: undefined = all types) */
|
||||||
*/
|
types?: FactType[];
|
||||||
queryMentalModelDescription?: string;
|
/** Maximum tokens to return (default: undefined = API default) */
|
||||||
/**
|
maxTokens?: number;
|
||||||
* Custom description for the getDocument tool.
|
/** Processing budget controlling latency vs. depth (default: 'mid') */
|
||||||
*/
|
budget?: Budget;
|
||||||
getDocumentDescription?: string;
|
/** 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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates AI SDK tools for Hindsight memory operations.
|
* Creates AI SDK tools for Hindsight memory operations.
|
||||||
*
|
*
|
||||||
* Features:
|
* The bank ID and all infrastructure concerns (budget, tags, async mode, etc.)
|
||||||
* - Dynamic bank ID per call (supports multi-user/multi-bank scenarios)
|
* are fixed at creation time. The agent only controls semantic inputs:
|
||||||
* - Full API parameter support for retain, recall, and reflect
|
* content, queries, names, and timestamps.
|
||||||
* - Ready to use with streamText, generateText, or ToolLoopAgent
|
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* const tools = createHindsightTools({
|
* const tools = createHindsightTools({
|
||||||
* client: hindsightClient,
|
* client: hindsightClient,
|
||||||
|
* bankId: userId,
|
||||||
|
* recall: { budget: 'high', includeEntities: true },
|
||||||
|
* retain: { async: true, tags: ['env:prod'] },
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* // Use with AI SDK
|
|
||||||
* const result = await generateText({
|
* const result = await generateText({
|
||||||
* model: openai('gpt-4'),
|
* model: openai('gpt-4o'),
|
||||||
* tools,
|
* tools,
|
||||||
* prompt: 'Remember that Alice loves hiking',
|
* messages,
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function createHindsightTools({
|
export function createHindsightTools({
|
||||||
client,
|
client,
|
||||||
retainDescription,
|
bankId,
|
||||||
recallDescription,
|
retain: retainOpts = {},
|
||||||
reflectDescription,
|
recall: recallOpts = {},
|
||||||
createMentalModelDescription,
|
reflect: reflectOpts = {},
|
||||||
queryMentalModelDescription,
|
getMentalModel: getMentalModelOpts = {},
|
||||||
getDocumentDescription,
|
getDocument: getDocumentOpts = {},
|
||||||
}: HindsightToolsOptions) {
|
}: HindsightToolsOptions) {
|
||||||
|
// Agent-controlled params only: content, timestamp, documentId, context
|
||||||
const retainParams = z.object({
|
const retainParams = z.object({
|
||||||
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
|
|
||||||
content: z.string().describe('Content to store in memory'),
|
content: z.string().describe('Content to store in memory'),
|
||||||
documentId: z.string().optional().describe('Optional document ID for grouping/upserting content'),
|
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'),
|
timestamp: z.string().optional().describe('Optional ISO timestamp for when the memory occurred'),
|
||||||
context: z.string().optional().describe('Optional context about the memory'),
|
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({
|
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'),
|
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)'),
|
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({
|
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'),
|
query: z.string().describe('Question to reflect on based on memories'),
|
||||||
context: z.string().optional().describe('Additional context for the reflection'),
|
context: z.string().optional().describe('Additional context for the reflection'),
|
||||||
budget: BudgetSchema.optional().describe('Processing budget: low, mid, or high'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const createMentalModelParams = z.object({
|
const getMentalModelParams = z.object({
|
||||||
bankId: z.string().describe('Memory bank ID (usually the user ID)'),
|
mentalModelId: z.string().describe('ID of the mental model to retrieve'),
|
||||||
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({
|
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'),
|
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 RetainInput = z.infer<typeof retainParams>;
|
||||||
type RetainOutput = { success: boolean; itemsCount: number };
|
type RetainOutput = { success: boolean; itemsCount: number };
|
||||||
|
|
||||||
|
|
@ -372,37 +305,31 @@ export function createHindsightTools({
|
||||||
type ReflectInput = z.infer<typeof reflectParams>;
|
type ReflectInput = z.infer<typeof reflectParams>;
|
||||||
type ReflectOutput = { text: string; basedOn?: ReflectFact[] };
|
type ReflectOutput = { text: string; basedOn?: ReflectFact[] };
|
||||||
|
|
||||||
type CreateMentalModelInput = z.infer<typeof createMentalModelParams>;
|
type GetMentalModelInput = z.infer<typeof getMentalModelParams>;
|
||||||
type CreateMentalModelOutput = { mentalModelId: string; createdAt: string };
|
type GetMentalModelOutput = { content: string; name?: string; updatedAt: string };
|
||||||
|
|
||||||
type QueryMentalModelInput = z.infer<typeof queryMentalModelParams>;
|
|
||||||
type QueryMentalModelOutput = { content: string; name?: string; updatedAt: string };
|
|
||||||
|
|
||||||
type GetDocumentInput = z.infer<typeof getDocumentParams>;
|
type GetDocumentInput = z.infer<typeof getDocumentParams>;
|
||||||
type GetDocumentOutput = { originalText: string; id: string; createdAt: string; updatedAt: string } | null;
|
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 {
|
return {
|
||||||
retain: tool<RetainInput, RetainOutput>({
|
retain: tool<RetainInput, RetainOutput>({
|
||||||
description:
|
description:
|
||||||
retainDescription ??
|
retainOpts.description ??
|
||||||
`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.`,
|
`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,
|
inputSchema: retainParams,
|
||||||
execute: async (input) => {
|
execute: async (input) => {
|
||||||
console.log('[AI SDK Tool] Retain input:', {
|
console.log('[AI SDK Tool] Retain input:', {
|
||||||
bankId: input.bankId,
|
bankId,
|
||||||
documentId: input.documentId,
|
documentId: input.documentId,
|
||||||
tags: input.tags,
|
|
||||||
hasContent: !!input.content,
|
hasContent: !!input.content,
|
||||||
});
|
});
|
||||||
const result = await client.retain(input.bankId, input.content, {
|
const result = await client.retain(bankId, input.content, {
|
||||||
documentId: input.documentId,
|
documentId: input.documentId,
|
||||||
timestamp: input.timestamp,
|
timestamp: input.timestamp,
|
||||||
context: input.context,
|
context: input.context,
|
||||||
tags: input.tags,
|
tags: retainOpts.tags,
|
||||||
metadata: input.metadata as Record<string, string> | undefined,
|
metadata: retainOpts.metadata,
|
||||||
|
async: retainOpts.async ?? false,
|
||||||
});
|
});
|
||||||
return { success: result.success, itemsCount: result.items_count };
|
return { success: result.success, itemsCount: result.items_count };
|
||||||
},
|
},
|
||||||
|
|
@ -410,17 +337,17 @@ export function createHindsightTools({
|
||||||
|
|
||||||
recall: tool<RecallInput, RecallOutput>({
|
recall: tool<RecallInput, RecallOutput>({
|
||||||
description:
|
description:
|
||||||
recallDescription ??
|
recallOpts.description ??
|
||||||
`Search memory for relevant information. Use this to find previously stored information that can help personalize responses or provide context.`,
|
`Search memory for relevant information. Use this to find previously stored information that can help personalize responses or provide context.`,
|
||||||
inputSchema: recallParams,
|
inputSchema: recallParams,
|
||||||
execute: async (input) => {
|
execute: async (input) => {
|
||||||
const result = await client.recall(input.bankId, input.query, {
|
const result = await client.recall(bankId, input.query, {
|
||||||
types: input.types,
|
types: recallOpts.types,
|
||||||
maxTokens: input.maxTokens,
|
maxTokens: recallOpts.maxTokens,
|
||||||
budget: input.budget,
|
budget: recallOpts.budget ?? 'mid',
|
||||||
queryTimestamp: input.queryTimestamp,
|
queryTimestamp: input.queryTimestamp,
|
||||||
includeEntities: input.includeEntities,
|
includeEntities: recallOpts.includeEntities ?? false,
|
||||||
includeChunks: input.includeChunks,
|
includeChunks: recallOpts.includeChunks ?? false,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
results: result.results ?? [],
|
results: result.results ?? [],
|
||||||
|
|
@ -431,13 +358,14 @@ export function createHindsightTools({
|
||||||
|
|
||||||
reflect: tool<ReflectInput, ReflectOutput>({
|
reflect: tool<ReflectInput, ReflectOutput>({
|
||||||
description:
|
description:
|
||||||
reflectDescription ??
|
reflectOpts.description ??
|
||||||
`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.`,
|
`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,
|
inputSchema: reflectParams,
|
||||||
execute: async (input) => {
|
execute: async (input) => {
|
||||||
const result = await client.reflect(input.bankId, input.query, {
|
const result = await client.reflect(bankId, input.query, {
|
||||||
context: input.context,
|
context: input.context,
|
||||||
budget: input.budget,
|
budget: reflectOpts.budget ?? 'mid',
|
||||||
|
maxTokens: reflectOpts.maxTokens,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
text: result.text ?? 'No insights available yet.',
|
text: result.text ?? 'No insights available yet.',
|
||||||
|
|
@ -446,34 +374,13 @@ export function createHindsightTools({
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createMentalModel: tool<CreateMentalModelInput, CreateMentalModelOutput>({
|
getMentalModel: tool<GetMentalModelInput, GetMentalModelOutput>({
|
||||||
description:
|
description:
|
||||||
createMentalModelDescription ??
|
getMentalModelOpts.description ??
|
||||||
`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.`,
|
`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: createMentalModelParams,
|
inputSchema: getMentalModelParams,
|
||||||
execute: async (input) => {
|
execute: async (input) => {
|
||||||
const result = await client.createMentalModel(input.bankId, {
|
const result = await client.getMentalModel(bankId, input.mentalModelId);
|
||||||
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 {
|
return {
|
||||||
content: result.content ?? 'No content available yet.',
|
content: result.content ?? 'No content available yet.',
|
||||||
name: result.name,
|
name: result.name,
|
||||||
|
|
@ -484,11 +391,11 @@ export function createHindsightTools({
|
||||||
|
|
||||||
getDocument: tool<GetDocumentInput, GetDocumentOutput>({
|
getDocument: tool<GetDocumentInput, GetDocumentOutput>({
|
||||||
description:
|
description:
|
||||||
getDocumentDescription ??
|
getDocumentOpts.description ??
|
||||||
`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.`,
|
`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,
|
inputSchema: getDocumentParams,
|
||||||
execute: async (input) => {
|
execute: async (input) => {
|
||||||
const result = await client.getDocument(input.bankId, input.documentId);
|
const result = await client.getDocument(bankId, input.documentId);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -501,27 +408,6 @@ 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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue