voyageai-cli
Version:
CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search
1,125 lines (967 loc) • 40.9 kB
JavaScript
;
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const pc = require('picocolors');
const { getConfigValue } = require('../lib/config');
const SAMPLE_DATA_DIR = path.join(__dirname, '..', 'demo', 'sample-data');
const SAMPLE_CODE_DIR = path.join(__dirname, '..', 'demo', 'sample-code');
// ── Verbose helpers ──────────────────────────────────────────────────
function theory(verbose, ...lines) {
if (!verbose) return;
console.log('');
for (const line of lines) {
console.log(` ${pc.dim('ℹ ' + line)}`);
}
console.log('');
}
function step(verbose, description) {
if (!verbose) return;
console.log(` ${pc.dim('→ ' + description)}`);
}
// ── Retry helper ─────────────────────────────────────────────────────
/**
* Retry an async function with delay, showing status on index-not-ready errors.
* @param {function} fn - async function to execute
* @param {object} [opts]
* @param {number} [opts.maxRetries=3]
* @param {number} [opts.delayMs=4000]
* @returns {Promise<any>} result of fn, or throws on final failure
*/
async function retryQuery(fn, opts = {}) {
const maxRetries = opts.maxRetries || 3;
const delayMs = opts.delayMs || 4000;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
const isIndexError = err.message?.includes('index') ||
err.codeName === 'InvalidPipelineOperator' ||
err.message?.includes('PlanExecutor');
if (isIndexError && attempt < maxRetries) {
console.log(` ${pc.dim(` Index warming up, retrying in ${delayMs / 1000}s... (${attempt + 1}/${maxRetries})`)}`);
await new Promise(resolve => setTimeout(resolve, delayMs));
continue;
}
throw err;
}
}
}
// ── Prerequisites ────────────────────────────────────────────────────
function checkPrerequisites(required) {
const errors = [];
if (required.includes('api-key')) {
const apiKey = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
if (!apiKey) {
errors.push('VOYAGE_API_KEY not configured. Run: vai config set api-key "your-key"');
}
}
if (required.includes('mongodb')) {
const mongoUri = process.env.MONGODB_URI || getConfigValue('mongodbUri');
if (!mongoUri) {
errors.push('MONGODB_URI not configured. Run: vai config set mongodb-uri "mongodb+srv://..."');
}
}
if (required.includes('llm')) {
const { resolveLLMConfig } = require('../lib/llm');
const llmConfig = resolveLLMConfig();
if (!llmConfig.provider) {
errors.push('No LLM provider configured. Set one: vai config set llm-provider openai');
}
}
return { ok: errors.length === 0, errors };
}
function printPrereqErrors(errors) {
console.error('');
console.error(pc.red(' Prerequisites not met:'));
for (const err of errors) {
console.error(` ${pc.red('✗')} ${err}`);
}
console.error('');
}
// ── Registration ─────────────────────────────────────────────────────
function registerDemo(program) {
const cmd = program
.command('demo [subcommand]')
.description('Guided demonstrations of Voyage AI features')
.option('--no-pause', 'Skip Enter prompts and interactive REPL (for CI/recording)')
.option('-v, --verbose', 'Show theory and behind-the-scenes detail')
.option('--local', 'Use local nano embeddings for chat demo')
.action(async (subcommand, opts) => {
if (!subcommand) {
await showDemoMenu(opts);
return;
}
switch (subcommand) {
case 'cost-optimizer':
await runCostOptimizerDemo(opts);
break;
case 'code-search':
await runCodeSearchDemo(opts);
break;
case 'chat':
await runChatDemo(opts);
break;
case 'nano': {
const { runNanoDemo } = require('../demos/nano');
await runNanoDemo(opts);
break;
}
case 'cleanup':
await runCleanup(opts);
break;
default:
console.error(pc.red(` Unknown demo: ${subcommand}`));
process.exit(1);
}
});
return cmd;
}
// ── Menu ─────────────────────────────────────────────────────────────
async function showDemoMenu(opts) {
console.log('');
console.log(pc.bold(' Welcome to vai demos!'));
console.log('');
console.log(' Choose a demonstration:');
console.log('');
console.log(' ' + pc.cyan('1. 💰 Cost Optimizer'));
console.log(' Prove the shared embedding space saves money — on your data.');
console.log('');
console.log(' ' + pc.cyan('2. 🔍 Code Search in 5 Minutes'));
console.log(' Index and search a codebase with semantic AI.');
console.log('');
console.log(' ' + pc.cyan('3. 💬 Chat With Your Docs'));
console.log(' Turn documents into conversational AI with RAG.');
console.log('');
console.log(' ' + pc.cyan('4. Local Embeddings (Nano)'));
console.log(' Experience embedding inference locally -- no API key needed.');
console.log('');
console.log(' ' + pc.dim('Tip: add --verbose for behind-the-scenes theory'));
console.log('');
if (opts.noPause) {
console.log(pc.dim(' (--no-pause: selecting demo 1)'));
await runCostOptimizerDemo(opts);
return;
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(' Select (1-4): ', (answer) => {
rl.close();
switch (answer.trim()) {
case '1':
runCostOptimizerDemo(opts).then(resolve);
break;
case '2':
runCodeSearchDemo(opts).then(resolve);
break;
case '3':
runChatDemo(opts).then(resolve);
break;
case '4': {
const { runNanoDemo } = require('../demos/nano');
runNanoDemo(opts).then(resolve);
break;
}
default:
console.log(pc.red('\n Invalid selection'));
resolve();
}
});
});
}
// ── Demo 1: Cost Optimizer ───────────────────────────────────────────
async function runCostOptimizerDemo(opts) {
const telemetry = require('../lib/telemetry');
const verbose = opts.verbose || false;
const prereq = checkPrerequisites(['api-key', 'mongodb']);
if (!prereq.ok) {
printPrereqErrors(prereq.errors);
process.exit(1);
}
console.log('');
console.log(pc.bold(' 💰 Cost Optimizer Demo'));
console.log(pc.dim(' ━━━━━━━━━━━━━━━━━━━━━━'));
console.log('');
console.log(' This demo proves that the Voyage AI shared embedding space works:');
console.log(' documents embedded with voyage-4-large can be queried with voyage-4-lite');
console.log(' — with identical retrieval results and dramatic cost savings.');
console.log('');
theory(verbose,
'Voyage AI models share a common 1024-dimensional embedding space.',
'This means vectors from voyage-4-large and voyage-4-lite live in the same',
'geometric space — cosine similarity works across models.',
'',
'The cost optimization strategy: embed documents once with the higher-quality',
'voyage-4-large model, but query at runtime with the cheaper voyage-4-lite.',
'You get large-quality document representations with lite-cost queries.',
);
const demoStart = Date.now();
try {
// Step 1: Ingest sample data
console.log(pc.bold(' Step 1: Preparing knowledge base...'));
console.log('');
step(verbose, 'Reading 65 sample markdown files from src/demo/sample-data/');
step(verbose, 'Embedding each file with voyage-4-large (1024 dims, ~$0.05/1M tokens)');
const { ingestSampleData } = require('../lib/demo-ingest');
const { docCount, collectionName } = await ingestSampleData(SAMPLE_DATA_DIR, {
db: opts.db || 'vai_demo',
collection: opts.collection || 'cost_optimizer_demo',
});
console.log(` ✓ Ingested ${docCount} sample documents`);
console.log(` ✓ Collection: ${collectionName}`);
console.log('');
// Step 2: Run cost analysis
console.log(pc.bold(' Step 2: Analyzing cost savings...'));
console.log('');
theory(verbose,
'The optimizer generates sample queries from your corpus, then runs each',
'query through both voyage-4-large and voyage-4-lite to compare results.',
'It measures "overlap" — how many of the same documents both models retrieve.',
'High overlap means lite queries are just as accurate as large queries.',
);
const { Optimizer } = require('../lib/optimizer');
const optimizer = new Optimizer({
db: opts.db || 'vai_demo',
collection: opts.collection || 'cost_optimizer_demo',
});
const queries = await optimizer.generateSampleQueries(5);
step(verbose, `Generated ${queries.length} sample queries from the corpus`);
step(verbose, 'Running vector search with voyage-4-large (baseline)');
step(verbose, 'Running vector search with voyage-4-lite (cost-optimized)');
step(verbose, 'Comparing top-5 retrieved documents for each query');
const result = await optimizer.analyze({
queries,
models: ['voyage-4-large', 'voyage-4-lite'],
scale: {
docs: 1_000_000,
queriesPerMonth: 50_000_000,
months: 12,
},
});
// Step 3: Display results
console.log(pc.bold(' Step 3: Results'));
console.log('');
console.log(pc.cyan(' ── Retrieval Quality ──'));
console.log('');
console.log(' Comparing voyage-4-large (baseline) vs voyage-4-lite:');
console.log('');
let totalOverlap = 0;
for (let i = 0; i < result.queries.length; i++) {
const q = result.queries[i];
const shortQuery = q.query.length > 60 ? q.query.slice(0, 57) + '...' : q.query;
console.log(` Query ${i + 1}: "${shortQuery}"`);
console.log(` Overlap: ${q.overlap}/5 documents (${Math.round(q.overlapPercent)}%)`);
totalOverlap += q.overlapPercent;
}
const avgOverlap = (totalOverlap / result.queries.length).toFixed(1);
console.log('');
console.log(` Average overlap: ${avgOverlap}%`);
console.log(pc.green(' ✓ voyage-4-lite retrieves nearly identical results from the same documents'));
console.log('');
theory(verbose,
`With ${avgOverlap}% overlap, the lite model finds almost exactly the same`,
'documents as the large model. This is the shared embedding space in action.',
'The vectors are geometrically close enough that cosine similarity rankings',
'are preserved even across different model sizes.',
);
// Print cost projection
console.log(pc.cyan(' ── Cost Projection (1M docs, 50M queries/month, 12 months) ──'));
console.log('');
const symmetric = result.costs.symmetric;
const asymmetric = result.costs.asymmetric;
const savings = symmetric - asymmetric;
const savingsPercent = ((savings / symmetric) * 100).toFixed(1);
console.log(` Symmetric (large everywhere): $${symmetric.toLocaleString('en-US', {
minimumFractionDigits: 2, maximumFractionDigits: 2,
})}`);
console.log(` Asymmetric (large→docs, lite→queries): $${asymmetric.toLocaleString('en-US', {
minimumFractionDigits: 2, maximumFractionDigits: 2,
})}`);
console.log('');
console.log(pc.green(` 💰 Annual savings: $${savings.toLocaleString('en-US', {
minimumFractionDigits: 2, maximumFractionDigits: 2,
})} (${savingsPercent}%)`));
console.log('');
theory(verbose,
'Savings come from the pricing difference:',
' voyage-4-large: ~$0.05 per 1M tokens (used once at ingest)',
' voyage-4-lite: ~$0.02 per 1M tokens (used for every query)',
'At 50M queries/month, that difference compounds significantly.',
);
// Step 4: Next steps
console.log(pc.cyan(' ── Next Steps ──'));
console.log('');
console.log(' Run `vai optimize` with your real data:');
console.log('');
console.log(` ${pc.dim('vai pipeline ./my-docs/ --db myapp --collection knowledge --create-index')}`);
console.log(` ${pc.dim('vai optimize --db myapp --collection knowledge --export report.md')}`);
console.log('');
console.log(' Or visualize the analysis in the Playground:');
console.log(` ${pc.dim('vai playground')}`);
console.log('');
if (telemetry && telemetry.send) {
telemetry.send('demo_cost_optimizer_completed', {
duration: Date.now() - demoStart,
docCount,
queries: queries.length,
});
}
} catch (err) {
console.error('');
console.error(pc.red(' Demo failed:'), err.message);
if (process.env.DEBUG) console.error(err);
process.exit(1);
}
}
// ── Demo 2: Code Search ──────────────────────────────────────────────
async function runCodeSearchDemo(opts) {
const { generateEmbeddings } = require('../lib/api');
const { getMongoCollection } = require('../lib/mongo');
const { smartChunkCode, extractSymbols, findCodeFiles } = require('../lib/code-search');
const { ensureVectorIndex, waitForIndex } = require('../lib/demo-ingest');
const telemetry = require('../lib/telemetry');
const verbose = opts.verbose || false;
const interactive = opts.pause !== false; // --no-pause disables interactive
const prereq = checkPrerequisites(['api-key', 'mongodb']);
if (!prereq.ok) {
printPrereqErrors(prereq.errors);
process.exit(1);
}
console.log('');
console.log(pc.bold(' 🔍 Code Search Demo'));
console.log(pc.dim(' ━━━━━━━━━━━━━━━━━━━━'));
console.log('');
console.log(' This demo indexes a sample Node.js API project and performs semantic');
console.log(' code search — finding relevant code using natural language queries.');
console.log('');
theory(verbose,
'Semantic code search uses voyage-code-3, an embedding model trained',
'specifically on source code. Unlike text search (grep, ripgrep), it',
'understands intent — "error handling" finds try/catch blocks, retry',
'logic, and error formatting even if those words don\'t appear literally.',
'',
'The process:',
' 1. Scan files → smart-chunk by function/class boundaries',
' 2. Embed each chunk with voyage-code-3 (1024 dims)',
' 3. Store in MongoDB Atlas with vector search index',
' 4. Query: embed question → cosine search → rerank',
);
const demoStart = Date.now();
const dbName = opts.db || 'vai_demo';
const collName = opts.collection || 'code_search_demo';
let client, collection;
try {
// Step 1: Index the sample "TaskFlow API" project
console.log(pc.bold(' Step 1: Indexing sample TaskFlow API...'));
console.log('');
const srcDir = SAMPLE_CODE_DIR;
step(verbose, `Scanning sample project (${path.basename(srcDir)}/) for code files`);
const files = await findCodeFiles(srcDir, { maxFiles: 50, maxFileSize: 50000 });
console.log(` ✓ Found ${files.length} code files`);
step(verbose, 'Smart-chunking files by function/class boundaries');
step(verbose, 'Extracting symbols (function names, exports) per chunk');
const allDocs = [];
for (const filePath of files) {
const content = require('fs').readFileSync(filePath, 'utf-8');
const relativePath = path.relative(srcDir, filePath);
const ext = path.extname(filePath).toLowerCase();
const symbols = extractSymbols(content, filePath);
const chunks = smartChunkCode(content, filePath, { chunkSize: 512, chunkOverlap: 50 });
for (const c of chunks) {
allDocs.push({
text: c.text,
metadata: {
source: relativePath,
language: ext.slice(1),
startLine: c.startLine,
endLine: c.endLine,
chunkType: c.type,
symbols: symbols.filter(s => c.text.includes(s)),
},
});
}
}
console.log(` ✓ Created ${allDocs.length} code chunks`);
console.log('');
theory(verbose,
'Smart chunking splits code at natural boundaries (function declarations,',
'class definitions, module.exports) rather than at fixed character counts.',
'This means each chunk is a coherent unit of code — not a random slice.',
'Symbols extracted: function names, class names, exported identifiers.',
);
// Step 2: Embed and store
console.log(pc.bold(' Step 2: Embedding with voyage-code-3...'));
console.log('');
step(verbose, 'voyage-code-3 is specifically trained on source code');
step(verbose, 'It understands syntax, patterns, and programming concepts');
const batchSize = 10; // Small batches for resilience
let totalTokens = 0;
const mongoResult = await getMongoCollection(dbName, collName);
client = mongoResult.client;
collection = mongoResult.collection;
try { await collection.drop(); } catch { /* doesn't exist */ }
for (let i = 0; i < allDocs.length; i += batchSize) {
const batch = allDocs.slice(i, i + batchSize);
const texts = batch.map(d => d.text);
// Retry embedding with backoff on transient network errors
let embedResult;
for (let attempt = 0; attempt < 3; attempt++) {
try {
embedResult = await generateEmbeddings(texts, {
model: 'voyage-code-3',
inputType: 'document',
});
break;
} catch (err) {
const isTransient = err.code === 'EPIPE' || err.code === 'ECONNRESET' ||
err.message?.includes('EPIPE') || err.message?.includes('socket') ||
err.message?.includes('ECONNRESET') || err.message?.includes('timeout');
if (isTransient && attempt < 2) {
const delay = (attempt + 1) * 2000;
process.stdout.write(`\r Embedding... ${pc.dim(`network error, retrying in ${delay / 1000}s...`)} `);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw err;
}
}
totalTokens += embedResult.usage?.total_tokens || 0;
const docsToInsert = batch.map((doc, idx) => ({
text: doc.text,
embedding: embedResult.data[idx].embedding,
metadata: doc.metadata,
}));
await collection.insertMany(docsToInsert);
process.stdout.write(`\r Embedding... ${Math.min(i + batchSize, allDocs.length)}/${allDocs.length} chunks`);
}
console.log(pc.green(' done'));
console.log(` ✓ Used ${totalTokens.toLocaleString()} tokens`);
console.log('');
// Create and wait for index
process.stdout.write(' Creating vector search index... ');
await ensureVectorIndex(collection, 'code_search_index');
console.log(pc.green('done'));
process.stdout.write(' Waiting for index to become queryable... ');
const ready = await waitForIndex(collection, 'code_search_index', 120000, {
probeDimensions: 1024,
onStatus: (status, elapsed) => {
const secs = Math.round(elapsed / 1000);
if (status === 'WARMING') {
process.stdout.write(`\r Waiting for index to become queryable... ${pc.dim(`${secs}s (warming)`)} `);
} else if (status === 'READY_PROBING') {
process.stdout.write(`\r Waiting for index to become queryable... ${pc.dim(`${secs}s (probing)`)} `);
}
},
});
if (ready) {
console.log(`\r Waiting for index to become queryable... ${pc.green('ready')} `);
} else {
console.log(`\r Waiting for index to become queryable... ${pc.yellow('timeout')} `);
}
console.log('');
// Step 3: Run demo queries
console.log(pc.bold(' Step 3: Semantic code search'));
console.log('');
const demoQueries = [
'How does authentication and JWT token verification work?',
'error handling middleware and custom error classes',
'rate limiting implementation',
'database connection with retry logic',
];
theory(verbose,
'Each query is embedded with voyage-code-3 as a "query" input type.',
'The query vector is compared against all code chunk vectors using',
'cosine similarity via MongoDB Atlas $vectorSearch.',
'Top candidates are then reranked with Voyage AI\'s reranker for',
'precision — the reranker considers the full text, not just vectors.',
);
/**
* Run a single code search query against the collection.
* @returns {Array} search results
*/
async function executeCodeSearch(query) {
const embedResult = await generateEmbeddings([query], {
model: 'voyage-code-3',
inputType: 'query',
});
const queryVector = embedResult.data[0].embedding;
return collection.aggregate([
{
$vectorSearch: {
index: 'code_search_index',
path: 'embedding',
queryVector,
numCandidates: 50,
limit: 3,
},
},
{ $addFields: { _vsScore: { $meta: 'vectorSearchScore' } } },
]).toArray();
}
/** Print code search results */
function printCodeResults(searchResults) {
if (searchResults.length === 0) {
console.log(` ${pc.dim(' No results')}`);
return;
}
for (let i = 0; i < Math.min(searchResults.length, 3); i++) {
const r = searchResults[i];
const meta = r.metadata || {};
const lineRange = meta.startLine ? `:${meta.startLine}-${meta.endLine}` : '';
const score = r._vsScore ? r._vsScore.toFixed(3) : '—';
const symbols = (meta.symbols || []).slice(0, 3).join(', ');
const snippet = (r.text || '').split('\n').slice(0, 2).join(' ').slice(0, 80);
console.log(` ${pc.bold(`#${i + 1}`)} ${pc.dim(meta.source + lineRange)} ${pc.dim(`score:${score}`)}`);
if (symbols) console.log(` ${pc.dim('symbols:')} ${symbols}`);
console.log(` ${pc.dim(snippet + '...')}`);
}
}
// Run canned queries with retry
for (const query of demoQueries) {
console.log(` ${pc.cyan('Q:')} ${query}`);
try {
const searchResults = await retryQuery(() => executeCodeSearch(query), {
maxRetries: 3,
delayMs: 4000,
});
printCodeResults(searchResults);
} catch (err) {
console.log(` ${pc.yellow(' ⚠')} Search failed: ${err.message}`);
}
console.log('');
}
const elapsed = ((Date.now() - demoStart) / 1000).toFixed(1);
console.log(` ${pc.dim(`Canned queries completed in ${elapsed}s`)}`);
console.log('');
// Interactive REPL
if (interactive) {
console.log(pc.cyan(' ── Try it yourself ──'));
console.log('');
console.log(' Type a natural language query to search the sample TaskFlow API.');
console.log(` ${pc.dim('Type /quit to exit.')}`);
console.log('');
await new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: pc.cyan(' code-search> '),
});
rl.prompt();
rl.on('line', async (line) => {
const input = line.trim();
if (!input) { rl.prompt(); return; }
if (input === '/quit' || input === '/exit' || input === '/q') {
rl.close();
return;
}
try {
const results = await executeCodeSearch(input);
printCodeResults(results);
} catch (err) {
console.log(` ${pc.yellow(' ⚠')} ${err.message}`);
}
console.log('');
rl.prompt();
});
rl.on('close', resolve);
rl.on('SIGINT', () => { console.log(''); rl.close(); });
});
}
// Next steps
console.log('');
console.log(pc.cyan(' ── Next Steps ──'));
console.log('');
console.log(' Index your own codebase:');
console.log('');
console.log(` ${pc.dim('vai code-search init ./my-project')}`);
console.log(` ${pc.dim('vai code-search "how does authentication work?"')}`);
console.log('');
if (telemetry && telemetry.send) {
telemetry.send('demo_code_search_completed', {
duration: Date.now() - demoStart,
files: files.length,
chunks: allDocs.length,
tokens: totalTokens,
});
}
} catch (err) {
console.error('');
console.error(pc.red(' Demo failed:'), err.message);
if (process.env.DEBUG) console.error(err);
process.exit(1);
} finally {
if (client) await client.close();
}
}
// ── Demo 3: Chat With Your Docs ──────────────────────────────────────
async function runChatDemo(opts) {
const { createLLMProvider, resolveLLMConfig } = require('../lib/llm');
const { ChatHistory } = require('../lib/history');
const { chatTurn } = require('../lib/chat');
const { ingestChunkedData, waitForIndex } = require('../lib/demo-ingest');
const { getMongoCollection } = require('../lib/mongo');
const chatUI = require('../lib/chat-ui');
const telemetry = require('../lib/telemetry');
const verbose = opts.verbose || false;
const interactive = opts.pause !== false;
const isLocal = opts.local || false;
// Nano prerequisite check (before standard prerequisites)
let generateLocalEmbeddings;
if (isLocal) {
const { checkVenv, checkModel } = require('../nano/nano-health');
const venv = checkVenv();
const model = checkModel();
if (!venv.ok || !model.ok) {
console.log('');
console.log(pc.red(' voyage-4-nano is not set up.'));
console.log(` Run ${pc.cyan('vai nano setup')} to install voyage-4-nano`);
console.log('');
process.exit(1);
}
generateLocalEmbeddings = require('../nano/nano-local').generateLocalEmbeddings;
}
const requiredChecks = isLocal ? ['mongodb', 'llm'] : ['api-key', 'mongodb', 'llm'];
const prereq = checkPrerequisites(requiredChecks);
if (!prereq.ok) {
printPrereqErrors(prereq.errors);
process.exit(1);
}
console.log('');
console.log(pc.bold(isLocal ? ' 💬 Chat With Your Docs Demo (local)' : ' 💬 Chat With Your Docs Demo'));
console.log(pc.dim(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
console.log('');
console.log(' This demo ingests sample documentation, then runs a RAG-powered');
console.log(' chat session — asking questions answered by the knowledge base.');
console.log('');
if (isLocal) {
theory(verbose,
'RAG (Retrieval-Augmented Generation) combines search with LLM generation:',
'',
' 1. CHUNK: Split documents into overlapping sections (~800 chars)',
' 2. EMBED: Convert each chunk to a 1024-dim vector with voyage-4-nano (local inference)',
' 3. INDEX: Store vectors in MongoDB Atlas with a vector search index',
' 4. QUERY: Embed the user\'s question, find nearest chunk vectors',
' 5. RERANK: Skipped in local mode (requires API key)',
' 6. GENERATE: Send top chunks + question to an LLM for a grounded answer',
'',
'The result: the LLM answers using your documents as evidence,',
'reducing hallucination and providing traceable sources.',
);
} else {
theory(verbose,
'RAG (Retrieval-Augmented Generation) combines search with LLM generation:',
'',
' 1. CHUNK: Split documents into overlapping sections (~800 chars)',
' 2. EMBED: Convert each chunk to a 1024-dim vector with voyage-4-large',
' 3. INDEX: Store vectors in MongoDB Atlas with a vector search index',
' 4. QUERY: Embed the user\'s question, find nearest chunk vectors',
' 5. RERANK: Re-score candidates with a cross-encoder for precision',
' 6. GENERATE: Send top chunks + question to an LLM for a grounded answer',
'',
'The result: the LLM answers using your documents as evidence,',
'reducing hallucination and providing traceable sources.',
);
}
const demoStart = Date.now();
const dbName = opts.db || 'vai_demo';
const collName = opts.collection || 'chat_demo';
try {
// Step 1: Ingest with chunking
console.log(pc.bold(' Step 1: Ingesting and chunking documents...'));
console.log('');
step(verbose, 'Reading markdown files from src/demo/sample-data/');
step(verbose, 'Splitting by ## heading sections (preserving context)');
step(verbose, 'Stamping source metadata for human-readable attribution');
const ingestOpts = {
db: dbName,
collection: collName,
onProgress: (event, data) => {
switch (event) {
case 'scan':
console.log(` ✓ Found ${data.fileCount} sample documents`);
break;
case 'chunks':
console.log(` ✓ Created ${data.chunkCount} chunks`);
break;
case 'embed':
process.stdout.write(`\r Embedding... ${data.done}/${data.total} chunks`);
if (data.done >= data.total) console.log(pc.green(' done'));
break;
}
},
};
if (isLocal) {
ingestOpts.embedFn = generateLocalEmbeddings;
ingestOpts.model = 'voyage-4-nano';
ingestOpts.dimensions = 1024;
}
const ingestResult = await ingestChunkedData(SAMPLE_DATA_DIR, ingestOpts);
console.log(` ✓ Stored in ${ingestResult.collectionName}`);
console.log('');
theory(verbose,
`${ingestResult.fileCount} files → ${ingestResult.chunkCount} chunks.`,
'Each chunk carries metadata: title, filename, chunk index.',
'When the chat retrieves chunks, resolveSourceLabel() extracts the',
'human-readable title. deduplicateSources() groups chunks from the',
'same document, showing the best score and chunk count.',
);
// Wait for vector index
console.log(pc.bold(' Step 2: Waiting for vector index...'));
console.log('');
step(verbose, 'MongoDB Atlas builds the vector search index asynchronously');
step(verbose, 'Probing with $vectorSearch to confirm queryability');
const { client: waitClient, collection: waitColl } = await getMongoCollection(dbName, collName);
process.stdout.write(' Waiting for index... ');
const ready = await waitForIndex(waitColl, 'vector_index', 120000, {
probeDimensions: 1024,
onStatus: (status, elapsed) => {
const secs = Math.round(elapsed / 1000);
if (status === 'WARMING' || status === 'READY_PROBING') {
process.stdout.write(`\r Waiting for index... ${pc.dim(`${secs}s (${status.toLowerCase()})`)} `);
}
},
});
await waitClient.close();
if (ready) {
console.log(`\r Waiting for index... ${pc.green('ready')} `);
} else {
console.log(`\r Waiting for index... ${pc.yellow('timeout — will retry queries')} `);
}
console.log('');
// Step 3: Run demo chat queries
console.log(pc.bold(' Step 3: RAG chat session'));
console.log('');
const llmConfig = resolveLLMConfig(opts);
const llm = createLLMProvider(llmConfig);
const history = new ChatHistory({ maxTurns: 10 });
console.log(` ${pc.dim(`LLM: ${llm.name}/${llm.model}`)}`);
console.log('');
/** Run a single chat turn with retry and rendering */
async function executeChatTurn(query) {
let sources = [];
let genSpinner = null;
const streamRenderer = chatUI.createStreamRenderer();
const spinner = chatUI.createTimedSpinner('Searching');
const chatOpts = { maxDocs: 3, stream: true, textField: 'text' };
if (isLocal) {
chatOpts.embedFn = generateLocalEmbeddings;
chatOpts.model = 'voyage-4-nano';
chatOpts.dimensions = 1024;
chatOpts.rerank = false;
}
try {
await retryQuery(async () => {
for await (const event of chatTurn({
query,
db: dbName,
collection: collName,
llm,
history,
opts: chatOpts,
})) {
switch (event.type) {
case 'retrieval':
spinner.stop();
if (verbose) {
const docs = event.data.docs || [];
const rerankedNote = isLocal ? ', reranking skipped' : '';
console.log(` ${pc.dim(` Retrieved ${docs.length} chunks in ${event.data.timeMs}ms${rerankedNote}`)}`);
for (const d of docs.slice(0, 3)) {
console.log(` ${pc.dim(` • ${d.source} (score: ${(d.score || 0).toFixed(3)})`)}`);
}
}
genSpinner = chatUI.createTimedSpinner('Generating response');
break;
case 'chunk':
if (genSpinner) {
genSpinner.stop();
genSpinner = null;
process.stdout.write(`\n ${pc.green('vai:')} `);
}
streamRenderer.write(event.data);
break;
case 'done':
streamRenderer.flush();
sources = event.data.sources || [];
break;
}
}
}, { maxRetries: 2, delayMs: 5000 });
} catch (err) {
spinner.stop();
if (genSpinner) genSpinner.stop();
throw err;
}
console.log('');
if (sources.length > 0) {
console.log(chatUI.renderSources(sources));
}
return sources;
}
const demoQueries = [
'How should I handle API key rotation and what are the security best practices?',
'What retry strategies are recommended for handling transient errors?',
'Explain the database sharding approach and when to use it.',
];
for (let qi = 0; qi < demoQueries.length; qi++) {
const query = demoQueries[qi];
console.log(` ${pc.cyan('You:')} ${query}`);
console.log('');
if (isLocal) {
theory(verbose,
'Step A: Embedding query with voyage-4-nano (local)',
`Step B: $vectorSearch against ${ingestResult.chunkCount} chunks (cosine similarity)`,
'Step C: Reranking: skipped (local mode)',
'Step D: Building prompt with retrieved context + history',
`Step E: Streaming response from ${llm.name}/${llm.model}`,
);
} else {
theory(verbose,
'Step A: Embedding query with voyage-4-large',
`Step B: $vectorSearch against ${ingestResult.chunkCount} chunks (cosine similarity)`,
'Step C: Reranking top candidates with cross-encoder',
'Step D: Building prompt with retrieved context + history',
`Step E: Streaming response from ${llm.name}/${llm.model}`,
);
}
try {
await executeChatTurn(query);
} catch (err) {
console.log('');
console.log(` ${pc.yellow('⚠')} Query failed: ${err.message}`);
console.log('');
}
if (verbose && qi < demoQueries.length - 1) {
console.log(pc.dim(' ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─'));
console.log('');
}
}
const elapsed = ((Date.now() - demoStart) / 1000).toFixed(1);
console.log(` ${pc.dim(`Canned queries completed in ${elapsed}s`)}`);
console.log('');
// Show knowledge base topics
const topicDirs = fs.readdirSync(SAMPLE_DATA_DIR, { withFileTypes: true })
.filter(d => d.isDirectory())
.map(d => {
const dirPath = path.join(SAMPLE_DATA_DIR, d.name);
const mdFiles = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'));
const titles = mdFiles.map(f => {
const first = fs.readFileSync(path.join(dirPath, f), 'utf8').split('\n')[0];
return first.replace(/^#\s+/, '').trim();
});
return { category: d.name, count: mdFiles.length, titles };
});
console.log(pc.cyan(' ── Knowledge Base ──'));
console.log('');
for (const topic of topicDirs) {
console.log(` ${pc.bold(topic.category)} ${pc.dim(`(${topic.count} docs)`)}`);
for (const title of topic.titles) {
console.log(` ${pc.dim('•')} ${title}`);
}
}
console.log('');
// Interactive REPL
if (interactive) {
console.log(pc.cyan(' ── Try it yourself ──'));
console.log('');
console.log(' Ask a question about the sample documentation.');
console.log(' Conversation history carries over — try follow-up questions!');
console.log(` ${pc.dim('Type /quit to exit.')}`);
console.log('');
await new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: pc.cyan(' chat> '),
});
rl.prompt();
rl.on('line', async (line) => {
const input = line.trim();
if (!input) { rl.prompt(); return; }
if (input === '/quit' || input === '/exit' || input === '/q') {
rl.close();
return;
}
console.log('');
try {
await executeChatTurn(input);
} catch (err) {
console.log(` ${pc.yellow('⚠')} ${err.message}`);
console.log('');
}
rl.prompt();
});
rl.on('close', resolve);
rl.on('SIGINT', () => { console.log(''); rl.close(); });
});
}
// Next steps
console.log('');
console.log(pc.cyan(' ── Next Steps ──'));
console.log('');
console.log(' Chat with your own documents:');
console.log('');
console.log(` ${pc.dim('vai ingest ./my-docs/ --db myapp --collection knowledge')}`);
console.log(` ${pc.dim('vai chat --db myapp --collection knowledge')}`);
console.log('');
console.log(' Or try agent mode (LLM picks its own tools):');
console.log(` ${pc.dim('vai chat --mode agent --db myapp --collection knowledge')}`);
console.log('');
if (telemetry && telemetry.send) {
telemetry.send('demo_chat_completed', {
duration: Date.now() - demoStart,
fileCount: ingestResult.fileCount,
chunkCount: ingestResult.chunkCount,
queries: demoQueries.length,
llmProvider: llm.name,
llmModel: llm.model,
});
}
} catch (err) {
console.error('');
console.error(pc.red(' Demo failed:'), err.message);
if (process.env.DEBUG) console.error(err);
process.exit(1);
}
}
// ── Cleanup ──────────────────────────────────────────────────────────
async function runCleanup(opts) {
const { getConnection } = require('../lib/mongo');
const telemetry = require('../lib/telemetry');
const prereq = checkPrerequisites(['mongodb']);
if (!prereq.ok) {
printPrereqErrors(prereq.errors);
process.exit(1);
}
console.log('');
console.log(pc.yellow(' Cleaning up demo data...'));
try {
const client = await getConnection();
const db = client.db('vai_demo');
const collectionNames = ['cost_optimizer_demo', 'code_search_demo', 'chat_demo'];
let dropped = 0;
for (const collName of collectionNames) {
try {
await db.collection(collName).drop();
console.log(pc.dim(` ✓ Dropped vai_demo.${collName}`));
dropped++;
} catch {
// Collection may not exist
}
}
console.log('');
if (dropped > 0) {
console.log(pc.green(` ✓ Cleaned up ${dropped} collection(s)`));
} else {
console.log(pc.dim(' No demo data to clean.'));
}
telemetry.send('demo_cleanup', { collectionsDropped: dropped });
} catch (err) {
console.error(pc.red(' Cleanup failed:'), err.message);
process.exit(1);
}
}
module.exports = { registerDemo };