diff --git a/examples/cookbook/vercel-ai-sdk/.env.example b/examples/cookbook/vercel-ai-sdk/.env.example new file mode 100644 index 00000000..c09deee6 --- /dev/null +++ b/examples/cookbook/vercel-ai-sdk/.env.example @@ -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 diff --git a/examples/cookbook/vercel-ai-sdk/README.md b/examples/cookbook/vercel-ai-sdk/README.md new file mode 100644 index 00000000..94ef8257 --- /dev/null +++ b/examples/cookbook/vercel-ai-sdk/README.md @@ -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 +``` diff --git a/examples/cookbook/vercel-ai-sdk/create_index.ts b/examples/cookbook/vercel-ai-sdk/create_index.ts new file mode 100644 index 00000000..4502c276 --- /dev/null +++ b/examples/cookbook/vercel-ai-sdk/create_index.ts @@ -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); +}); diff --git a/examples/cookbook/vercel-ai-sdk/example_usage.ts b/examples/cookbook/vercel-ai-sdk/example_usage.ts new file mode 100644 index 00000000..c7656070 --- /dev/null +++ b/examples/cookbook/vercel-ai-sdk/example_usage.ts @@ -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); +}); diff --git a/examples/cookbook/vercel-ai-sdk/moss_vercel.ts b/examples/cookbook/vercel-ai-sdk/moss_vercel.ts new file mode 100644 index 00000000..38ae9736 --- /dev/null +++ b/examples/cookbook/vercel-ai-sdk/moss_vercel.ts @@ -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 }), + }; +} diff --git a/examples/cookbook/vercel-ai-sdk/package.json b/examples/cookbook/vercel-ai-sdk/package.json new file mode 100644 index 00000000..0addc37c --- /dev/null +++ b/examples/cookbook/vercel-ai-sdk/package.json @@ -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" + } +} diff --git a/examples/cookbook/vercel-ai-sdk/seed_data.ts b/examples/cookbook/vercel-ai-sdk/seed_data.ts new file mode 100644 index 00000000..3e038bcd --- /dev/null +++ b/examples/cookbook/vercel-ai-sdk/seed_data.ts @@ -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.', + }, +]; diff --git a/examples/cookbook/vercel-ai-sdk/test_vercel.ts b/examples/cookbook/vercel-ai-sdk/test_vercel.ts new file mode 100644 index 00000000..de72e233 --- /dev/null +++ b/examples/cookbook/vercel-ai-sdk/test_vercel.ts @@ -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 = {}) { + 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.'); + }); +});