Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions examples/cookbook/vercel-ai-sdk/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
MOSS_PROJECT_ID=your_moss_project_id
MOSS_PROJECT_KEY=your_moss_project_key
MOSS_INDEX_NAME=support-docs
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o-mini
63 changes: 63 additions & 0 deletions examples/cookbook/vercel-ai-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Vercel AI SDK + Moss Cookbook

This cookbook demonstrates how to integrate [Moss](https://moss.dev/) fast semantic search with the [Vercel AI SDK](https://sdk.vercel.ai/) using the `@moss-tools/vercel-sdk` package.

By passing Moss tools into `generateText` and `streamText`, AI agents can automatically retrieve sub-10ms semantic search results from a Moss index before generating a response.

## Prerequisites

1. Moss Account (Create one at [moss.dev](https://moss.dev/))
2. OpenAI Account (for the LLM powering the agent)
3. Node.js

## Setup

Navigate to this directory and install dependencies:

```bash
npm install
```

Configure your environment variables. Copy `.env.example` to `.env` either here or in the root of the moss repository:

```bash
MOSS_PROJECT_ID=your_moss_project_id
MOSS_PROJECT_KEY=your_moss_project_key
MOSS_INDEX_NAME=support-docs
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o-mini
```

## Run the Example

### 1. Create the Index

Seed your Moss index with sample FAQ data:

```bash
npm run create-index
```

### 2. Run the Agent

```bash
npm start
```

### What happens?

1. `MossVercelToolkit` loads the index into memory for fast local queries (~1-10ms).
2. `mossSearchTool` and `mossLoadIndexTool` are passed to the Vercel AI SDK.
3. **Scenario 1** — `generateText`: the LLM calls `mossSearchTool` automatically to retrieve context, then returns a grounded response.
4. **Scenario 2** — `streamText`: the same flow, streamed token-by-token to stdout.

## Project Structure

```text
.
├── moss_vercel.ts # MossVercelToolkit — pure integration, no model references
├── example_usage.ts # Runnable demo: generateText + streamText with Moss tools
├── create_index.ts # One-time script to seed the Moss index with sample data
├── seed_data.ts # Sample FAQ documents
└── .env.example # Environment variable template
```
38 changes: 38 additions & 0 deletions examples/cookbook/vercel-ai-sdk/create_index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MossClient } from '@moss-dev/moss';
import { sampleDocs } from './seed_data.js';
import { config } from 'dotenv';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
config({ path: path.join(__dirname, '../../../.env') });
config();

async function createIndex() {
const projectId = process.env.MOSS_PROJECT_ID;
const projectKey = process.env.MOSS_PROJECT_KEY;
const indexName = process.env.MOSS_INDEX_NAME;

if (!projectId || !projectKey || !indexName) {
console.error('Error: Missing MOSS_PROJECT_ID, MOSS_PROJECT_KEY, or MOSS_INDEX_NAME');
process.exit(1);
}

const client = new MossClient(projectId, projectKey);

const deleted = await client.deleteIndex(indexName);
if (deleted) {
console.log(`Deleted existing index "${indexName}".`);
} else {
console.log(`Index "${indexName}" did not exist, skipping delete.`);
}

console.log(`Creating index "${indexName}" with ${sampleDocs.length} documents...`);
await client.createIndex(indexName, sampleDocs);
console.log('Index created successfully!');
}

createIndex().catch((err) => {
console.error(err);
process.exit(1);
});
73 changes: 73 additions & 0 deletions examples/cookbook/vercel-ai-sdk/example_usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { MossClient } from '@moss-dev/moss';
import { generateText, streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { config } from 'dotenv';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createMossTools } from './moss_vercel.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
config({ path: path.join(__dirname, '../../../.env') });
config();

function requireEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}

async function run() {
const client = new MossClient(
requireEnv('MOSS_PROJECT_ID'),
requireEnv('MOSS_PROJECT_KEY'),
);
const indexName = requireEnv('MOSS_INDEX_NAME');
const model = process.env.OPENAI_MODEL ?? 'gpt-4o-mini';

console.log(`Loading index "${indexName}" into memory for fast local queries...`);
const tools = await createMossTools(client, indexName);
console.log('Index loaded.\n');
const systemPrompt =
'You are a helpful customer support assistant. Use the search tool to find ' +
'relevant information before answering. Always cite the retrieved context in your response.';

// --- Scenario 1: generateText (single-turn Q&A) ---
console.log('--- Scenario 1: generateText ---');
const question = 'How long does a refund take and what is the return policy?';
console.log(`User: ${question}`);

const { text, steps } = await generateText({
model: openai(model),
tools,
maxSteps: 3,
system: systemPrompt,
prompt: question,
});

console.log(`\nAssistant: ${text}`);
console.log(`Tool calls made: ${steps.flatMap((s) => s.toolCalls).length}\n`);

// --- Scenario 2: streamText (streaming response) ---
console.log('--- Scenario 2: streamText ---');
const streamQuestion = 'What payment methods do you accept and how do I cancel my subscription?';
console.log(`User: ${streamQuestion}`);
process.stdout.write('\nAssistant: ');

const stream = streamText({
model: openai(model),
tools,
maxSteps: 3,
system: systemPrompt,
prompt: streamQuestion,
});

for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}
console.log('\n');
}

run().catch((err) => {
console.error(err);
process.exit(1);
});
10 changes: 10 additions & 0 deletions examples/cookbook/vercel-ai-sdk/moss_vercel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { MossClient } from '@moss-dev/moss';
import { mossSearchTool, mossLoadIndexTool } from '@moss-tools/vercel-sdk';

export async function createMossTools(client: MossClient, indexName: string) {
await client.loadIndex(indexName);
return {
search: mossSearchTool({ client, indexName }),
loadIndex: mossLoadIndexTool({ client, indexName }),
};
}
25 changes: 25 additions & 0 deletions examples/cookbook/vercel-ai-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "moss-vercel-ai-sdk-cookbook",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Vercel AI SDK integration for Moss semantic search",
"scripts": {
"create-index": "tsx create_index.ts",
"start": "tsx example_usage.ts",
"test": "tsx --test test_vercel.ts"
},
"dependencies": {
"@ai-sdk/openai": "^1.0.0",
"@moss-dev/moss": "latest",
"@moss-tools/vercel-sdk": "latest",
"ai": "^6.0.0",
"dotenv": "^16.4.5",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/node": "^20.12.11",
"tsx": "^4.19.1",
"typescript": "^5.6.3"
}
}
34 changes: 34 additions & 0 deletions examples/cookbook/vercel-ai-sdk/seed_data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export const sampleDocs = [
{
id: 'faq_1',
text: 'Refunds for standard items are processed within 3-5 business days after the item is received at our warehouse.',
},
{
id: 'faq_2',
text: 'You can track your order in real-time by visiting the "My Orders" section of your dashboard and clicking "Track Shipment".',
},
{
id: 'faq_3',
text: 'We offer 24/7 live chat support for all technical queries. For billing issues, our team is available 9 AM - 5 PM EST.',
},
{
id: 'faq_4',
text: 'International shipping typically takes 7-14 business days. Customs delays are not included in this estimate.',
},
{
id: 'faq_5',
text: 'Our return policy allows for returns within 30 days of purchase, provided the item is in its original packaging.',
},
{
id: 'faq_6',
text: 'To reset your password, go to the login page and click "Forgot Password". A reset link will be sent to your registered email.',
},
{
id: 'faq_7',
text: 'We accept Visa, Mastercard, American Express, PayPal, and Apple Pay. All transactions are secured with 256-bit encryption.',
},
{
id: 'faq_8',
text: 'Subscription plans can be cancelled anytime from the Billing section in your account settings. Cancellations take effect at the end of the current billing cycle.',
},
];
91 changes: 91 additions & 0 deletions examples/cookbook/vercel-ai-sdk/test_vercel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* Unit tests for Moss + Vercel AI SDK integration.
* Runs with: tsx --test test_vercel.ts
* No real API credentials required.
*/

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { createMossTools } from './moss_vercel.js';

function makeMockClient(overrides: Record<string, unknown> = {}) {
return {
loadIndex: async (_name: string) => {},
query: async () => ({
docs: [],
query: '',
indexName: 'test-index',
timeTakenInMs: 0,
}),
...overrides,
} as any;
}

describe('createMossTools', () => {
it('calls loadIndex with the correct index name', async () => {
const calls: string[] = [];
const client = makeMockClient({
loadIndex: async (name: string) => { calls.push(name); },
});

await createMossTools(client, 'my-index');

assert.equal(calls.length, 1, 'loadIndex should be called exactly once');
assert.equal(calls[0], 'my-index');
});

it('returns an object with search and loadIndex tools', async () => {
const client = makeMockClient();
const tools = await createMossTools(client, 'my-index');

assert.ok('search' in tools, 'tools should have a search key');
assert.ok('loadIndex' in tools, 'tools should have a loadIndex key');
assert.equal(typeof tools.search.execute, 'function');
assert.equal(typeof tools.loadIndex.execute, 'function');
});

it('search tool forwards query and topK to client.query', async () => {
const calls: Array<{ index: string; query: string; opts: { topK: number } }> = [];
const client = makeMockClient({
query: async (index: string, query: string, opts: { topK: number }) => {
calls.push({ index, query, opts });
return { docs: [], query, indexName: index, timeTakenInMs: 1 };
},
});

const tools = await createMossTools(client, 'test-index');
await tools.search.execute(
{ query: 'what is the refund policy?', topK: 3 },
{ toolCallId: 'call-1', messages: [], abortSignal: new AbortController().signal },
);

assert.equal(calls.length, 1, 'query should be called exactly once');
assert.equal(calls[0].index, 'test-index');
assert.equal(calls[0].query, 'what is the refund policy?');
assert.equal(calls[0].opts.topK, 3);
});

it('search tool returns docs from client.query', async () => {
const client = makeMockClient({
query: async () => ({
docs: [
{ id: 'doc1', text: 'Refunds take 3-5 business days.', score: 0.95, metadata: {} },
{ id: 'doc2', text: 'Returns are accepted within 30 days.', score: 0.88, metadata: {} },
],
query: 'refund',
indexName: 'test-index',
timeTakenInMs: 4,
}),
});

const tools = await createMossTools(client, 'test-index');
const result = await tools.search.execute(
{ query: 'refund', topK: 5 },
{ toolCallId: 'call-2', messages: [], abortSignal: new AbortController().signal },
);

assert.equal(result.docs.length, 2);
assert.equal(result.docs[0].text, 'Refunds take 3-5 business days.');
assert.equal(result.docs[1].text, 'Returns are accepted within 30 days.');
});
});
Loading