voyageai-cli 1.20.5 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -26
- package/README.md +153 -2
- package/package.json +1 -1
- package/src/cli.js +12 -0
- package/src/commands/about.js +39 -3
- package/src/commands/doctor.js +325 -0
- package/src/commands/eval.js +420 -10
- package/src/commands/generate.js +220 -0
- package/src/commands/playground.js +93 -0
- package/src/commands/purge.js +271 -0
- package/src/commands/quickstart.js +203 -0
- package/src/commands/refresh.js +322 -0
- package/src/commands/scaffold.js +217 -0
- package/src/lib/codegen.js +313 -0
- package/src/lib/explanations.js +163 -1
- package/src/lib/scaffold-structure.js +114 -0
- package/src/lib/templates/nextjs/README.md.tpl +106 -0
- package/src/lib/templates/nextjs/env.example.tpl +8 -0
- package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
- package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
- package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
- package/src/lib/templates/nextjs/package.json.tpl +33 -0
- package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
- package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
- package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
- package/src/lib/templates/nextjs/theme.js.tpl +84 -0
- package/src/lib/templates/python/README.md.tpl +145 -0
- package/src/lib/templates/python/app.py.tpl +221 -0
- package/src/lib/templates/python/chunker.py.tpl +127 -0
- package/src/lib/templates/python/env.example.tpl +12 -0
- package/src/lib/templates/python/mongo_client.py.tpl +125 -0
- package/src/lib/templates/python/requirements.txt.tpl +10 -0
- package/src/lib/templates/python/voyage_client.py.tpl +124 -0
- package/src/lib/templates/vanilla/README.md.tpl +156 -0
- package/src/lib/templates/vanilla/client.js.tpl +103 -0
- package/src/lib/templates/vanilla/connection.js.tpl +126 -0
- package/src/lib/templates/vanilla/env.example.tpl +11 -0
- package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
- package/src/lib/templates/vanilla/package.json.tpl +31 -0
- package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
- package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
- package/src/lib/templates/vanilla/server.js.tpl +81 -0
- package/src/lib/zip.js +130 -0
- package/src/playground/index.html +772 -33
|
@@ -114,6 +114,99 @@ function createPlaygroundServer() {
|
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// API: Generate code
|
|
118
|
+
if (req.method === 'POST' && req.url === '/api/generate') {
|
|
119
|
+
let body = '';
|
|
120
|
+
req.on('data', chunk => { body += chunk; });
|
|
121
|
+
req.on('end', () => {
|
|
122
|
+
try {
|
|
123
|
+
const { target, component, config } = JSON.parse(body);
|
|
124
|
+
const codegen = require('../lib/codegen');
|
|
125
|
+
|
|
126
|
+
const templateMap = {
|
|
127
|
+
vanilla: { client: 'client.js', connection: 'connection.js', retrieval: 'retrieval.js', ingest: 'ingest.js', 'search-api': 'search-api.js' },
|
|
128
|
+
nextjs: { client: 'lib-voyage.js', connection: 'lib-mongo.js', retrieval: 'route-search.js', ingest: 'route-ingest.js', 'search-page': 'page-search.jsx' },
|
|
129
|
+
python: { client: 'voyage_client.py', connection: 'mongo_client.py', retrieval: 'app.py', ingest: 'chunker.py' },
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const templateName = (templateMap[target] || {})[component];
|
|
133
|
+
if (!templateName) {
|
|
134
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
135
|
+
res.end(JSON.stringify({ error: `Unknown component: ${component}` }));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const context = codegen.buildContext(config || {}, { projectName: 'my-app' });
|
|
140
|
+
const code = codegen.renderTemplate(target, templateName.replace(/\.(js|jsx|py)$/, ''), context);
|
|
141
|
+
|
|
142
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
143
|
+
res.end(JSON.stringify({ code, filename: templateName }));
|
|
144
|
+
} catch (err) {
|
|
145
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
146
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// API: Scaffold project (returns ZIP for web mode)
|
|
153
|
+
if (req.method === 'POST' && req.url === '/api/scaffold') {
|
|
154
|
+
let body = '';
|
|
155
|
+
req.on('data', chunk => { body += chunk; });
|
|
156
|
+
req.on('end', () => {
|
|
157
|
+
try {
|
|
158
|
+
const { projectName, target, config } = JSON.parse(body);
|
|
159
|
+
const codegen = require('../lib/codegen');
|
|
160
|
+
const { PROJECT_STRUCTURE } = require('../lib/scaffold-structure');
|
|
161
|
+
const { createZip } = require('../lib/zip');
|
|
162
|
+
|
|
163
|
+
const structure = PROJECT_STRUCTURE[target];
|
|
164
|
+
if (!structure) {
|
|
165
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
166
|
+
res.end(JSON.stringify({ error: `Unknown target: ${target}` }));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const context = codegen.buildContext(config || {}, { projectName: projectName || 'my-app' });
|
|
171
|
+
const files = [];
|
|
172
|
+
|
|
173
|
+
// Render template files
|
|
174
|
+
for (const file of structure.files) {
|
|
175
|
+
const content = codegen.renderTemplate(target, file.template.replace(/\.(js|jsx|py|json|md|txt)$/, ''), context);
|
|
176
|
+
files.push({
|
|
177
|
+
name: `${projectName}/${file.output}`,
|
|
178
|
+
content,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Add extra static files
|
|
183
|
+
if (structure.extraFiles) {
|
|
184
|
+
for (const file of structure.extraFiles) {
|
|
185
|
+
const content = typeof file.content === 'function' ? file.content(context) : file.content;
|
|
186
|
+
files.push({
|
|
187
|
+
name: `${projectName}/${file.output}`,
|
|
188
|
+
content,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Create ZIP
|
|
194
|
+
const zipBuffer = createZip(files);
|
|
195
|
+
|
|
196
|
+
res.writeHead(200, {
|
|
197
|
+
'Content-Type': 'application/zip',
|
|
198
|
+
'Content-Disposition': `attachment; filename="${projectName}.zip"`,
|
|
199
|
+
'Content-Length': zipBuffer.length,
|
|
200
|
+
});
|
|
201
|
+
res.end(zipBuffer);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
204
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
117
210
|
// API: Concepts (from vai explain)
|
|
118
211
|
if (req.method === 'GET' && req.url === '/api/concepts') {
|
|
119
212
|
const { concepts } = require('../lib/explanations');
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const p = require('@clack/prompts');
|
|
6
|
+
const { loadProject } = require('../lib/project');
|
|
7
|
+
const { connect, close } = require('../lib/mongo');
|
|
8
|
+
const ui = require('../lib/ui');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build a MongoDB filter from the provided criteria.
|
|
12
|
+
*/
|
|
13
|
+
function buildFilter(options) {
|
|
14
|
+
const conditions = [];
|
|
15
|
+
|
|
16
|
+
// Filter by source pattern (glob-like)
|
|
17
|
+
if (options.source) {
|
|
18
|
+
// Convert glob pattern to regex
|
|
19
|
+
const pattern = options.source
|
|
20
|
+
.replace(/\./g, '\\.')
|
|
21
|
+
.replace(/\*/g, '.*')
|
|
22
|
+
.replace(/\?/g, '.');
|
|
23
|
+
conditions.push({ 'metadata.source': { $regex: pattern } });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Filter by embedded date
|
|
27
|
+
if (options.before) {
|
|
28
|
+
const date = new Date(options.before);
|
|
29
|
+
if (isNaN(date.getTime())) {
|
|
30
|
+
throw new Error(`Invalid date format: ${options.before}`);
|
|
31
|
+
}
|
|
32
|
+
conditions.push({ _embeddedAt: { $lt: date } });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Filter by model
|
|
36
|
+
if (options.model) {
|
|
37
|
+
conditions.push({ _model: options.model });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Raw MongoDB filter
|
|
41
|
+
if (options.filter) {
|
|
42
|
+
try {
|
|
43
|
+
const rawFilter = JSON.parse(options.filter);
|
|
44
|
+
conditions.push(rawFilter);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw new Error(`Invalid JSON filter: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Combine conditions with $and
|
|
51
|
+
if (conditions.length === 0) {
|
|
52
|
+
return {};
|
|
53
|
+
} else if (conditions.length === 1) {
|
|
54
|
+
return conditions[0];
|
|
55
|
+
} else {
|
|
56
|
+
return { $and: conditions };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check which documents have stale source files (file no longer exists on disk).
|
|
62
|
+
*/
|
|
63
|
+
async function findStaleDocuments(collection, baseDir) {
|
|
64
|
+
const docs = await collection.find({ 'metadata.source': { $exists: true } }).toArray();
|
|
65
|
+
const staleIds = [];
|
|
66
|
+
|
|
67
|
+
for (const doc of docs) {
|
|
68
|
+
const source = doc.metadata?.source;
|
|
69
|
+
if (source) {
|
|
70
|
+
// Resolve relative to baseDir or treat as absolute
|
|
71
|
+
const filePath = path.isAbsolute(source) ? source : path.join(baseDir, source);
|
|
72
|
+
if (!fs.existsSync(filePath)) {
|
|
73
|
+
staleIds.push(doc._id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return staleIds;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Format a sample of documents for display.
|
|
83
|
+
*/
|
|
84
|
+
function formatSample(docs, limit = 5) {
|
|
85
|
+
const sample = docs.slice(0, limit);
|
|
86
|
+
return sample.map(doc => {
|
|
87
|
+
const source = doc.metadata?.source || doc._id?.toString() || 'unknown';
|
|
88
|
+
const model = doc._model || 'unknown';
|
|
89
|
+
const date = doc._embeddedAt ? new Date(doc._embeddedAt).toISOString().split('T')[0] : 'unknown';
|
|
90
|
+
return ` • ${source} (model: ${model}, date: ${date})`;
|
|
91
|
+
}).join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Execute the purge command.
|
|
96
|
+
*/
|
|
97
|
+
async function purge(options = {}) {
|
|
98
|
+
const quiet = options.quiet || options.json;
|
|
99
|
+
|
|
100
|
+
// Load project config
|
|
101
|
+
const project = loadProject();
|
|
102
|
+
const db = options.db || project.db || process.env.VAI_DB || 'vai';
|
|
103
|
+
const collectionName = options.collection || project.collection || process.env.VAI_COLLECTION || 'embeddings';
|
|
104
|
+
|
|
105
|
+
if (!quiet) {
|
|
106
|
+
p.intro(ui.title('vai purge'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate that at least one filter is provided
|
|
110
|
+
if (!options.source && !options.before && !options.model && !options.filter && !options.stale) {
|
|
111
|
+
if (options.json) {
|
|
112
|
+
console.log(JSON.stringify({ error: 'No filter criteria provided. Use --source, --before, --model, --filter, or --stale.' }));
|
|
113
|
+
} else {
|
|
114
|
+
p.log.error('No filter criteria provided.');
|
|
115
|
+
p.log.info('Use --source, --before, --model, --filter, or --stale to specify what to purge.');
|
|
116
|
+
}
|
|
117
|
+
return { success: false, error: 'No filter criteria' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let client;
|
|
121
|
+
try {
|
|
122
|
+
// Connect to MongoDB
|
|
123
|
+
if (!quiet) {
|
|
124
|
+
p.log.step(`Connecting to database: ${db}`);
|
|
125
|
+
}
|
|
126
|
+
client = await connect(db);
|
|
127
|
+
const collection = client.db(db).collection(collectionName);
|
|
128
|
+
|
|
129
|
+
let filter = {};
|
|
130
|
+
let staleIds = [];
|
|
131
|
+
|
|
132
|
+
if (options.stale) {
|
|
133
|
+
// Find documents with stale source files
|
|
134
|
+
if (!quiet) {
|
|
135
|
+
p.log.step('Scanning for stale documents (source files that no longer exist)...');
|
|
136
|
+
}
|
|
137
|
+
const baseDir = project.root || process.cwd();
|
|
138
|
+
staleIds = await findStaleDocuments(collection, baseDir);
|
|
139
|
+
|
|
140
|
+
if (staleIds.length === 0) {
|
|
141
|
+
if (options.json) {
|
|
142
|
+
console.log(JSON.stringify({ success: true, count: 0, message: 'No stale documents found' }));
|
|
143
|
+
} else {
|
|
144
|
+
p.log.success('No stale documents found.');
|
|
145
|
+
p.outro('Nothing to purge.');
|
|
146
|
+
}
|
|
147
|
+
return { success: true, count: 0 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
filter = { _id: { $in: staleIds } };
|
|
151
|
+
} else {
|
|
152
|
+
// Build filter from criteria
|
|
153
|
+
filter = buildFilter(options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Count matching documents
|
|
157
|
+
const count = options.stale ? staleIds.length : await collection.countDocuments(filter);
|
|
158
|
+
|
|
159
|
+
if (count === 0) {
|
|
160
|
+
if (options.json) {
|
|
161
|
+
console.log(JSON.stringify({ success: true, count: 0, message: 'No matching documents found' }));
|
|
162
|
+
} else {
|
|
163
|
+
p.log.success('No matching documents found.');
|
|
164
|
+
p.outro('Nothing to purge.');
|
|
165
|
+
}
|
|
166
|
+
return { success: true, count: 0 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Get sample for display
|
|
170
|
+
const sampleDocs = await collection.find(filter).limit(5).toArray();
|
|
171
|
+
|
|
172
|
+
if (options.json) {
|
|
173
|
+
if (options.dryRun) {
|
|
174
|
+
console.log(JSON.stringify({
|
|
175
|
+
dryRun: true,
|
|
176
|
+
count,
|
|
177
|
+
sample: sampleDocs.map(d => ({
|
|
178
|
+
id: d._id?.toString(),
|
|
179
|
+
source: d.metadata?.source,
|
|
180
|
+
model: d._model,
|
|
181
|
+
embeddedAt: d._embeddedAt,
|
|
182
|
+
})),
|
|
183
|
+
}));
|
|
184
|
+
return { success: true, dryRun: true, count };
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// Show what will be deleted
|
|
188
|
+
p.log.warn(`Found ${count} document${count === 1 ? '' : 's'} matching criteria:`);
|
|
189
|
+
console.log(formatSample(sampleDocs));
|
|
190
|
+
if (count > 5) {
|
|
191
|
+
console.log(` ... and ${count - 5} more`);
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Dry run - stop here
|
|
197
|
+
if (options.dryRun) {
|
|
198
|
+
if (!quiet) {
|
|
199
|
+
p.log.info('Dry run - no documents deleted.');
|
|
200
|
+
p.outro(`Would delete ${count} document${count === 1 ? '' : 's'}.`);
|
|
201
|
+
}
|
|
202
|
+
return { success: true, dryRun: true, count };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Confirm unless --force
|
|
206
|
+
if (!options.force && !options.json) {
|
|
207
|
+
const confirmed = await p.confirm({
|
|
208
|
+
message: `Delete ${count} document${count === 1 ? '' : 's'}? This cannot be undone.`,
|
|
209
|
+
initialValue: false,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
213
|
+
p.log.info('Purge cancelled.');
|
|
214
|
+
p.outro('No documents deleted.');
|
|
215
|
+
return { success: false, cancelled: true };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Delete documents
|
|
220
|
+
if (!quiet) {
|
|
221
|
+
p.log.step('Deleting documents...');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const result = await collection.deleteMany(filter);
|
|
225
|
+
const deleted = result.deletedCount;
|
|
226
|
+
|
|
227
|
+
if (options.json) {
|
|
228
|
+
console.log(JSON.stringify({ success: true, deleted }));
|
|
229
|
+
} else {
|
|
230
|
+
p.log.success(`Deleted ${deleted} document${deleted === 1 ? '' : 's'}.`);
|
|
231
|
+
p.outro('Purge complete.');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { success: true, deleted };
|
|
235
|
+
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (options.json) {
|
|
238
|
+
console.log(JSON.stringify({ error: err.message }));
|
|
239
|
+
} else {
|
|
240
|
+
p.log.error(`Purge failed: ${err.message}`);
|
|
241
|
+
}
|
|
242
|
+
return { success: false, error: err.message };
|
|
243
|
+
} finally {
|
|
244
|
+
if (client) {
|
|
245
|
+
await close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Register the purge command with Commander.
|
|
252
|
+
*/
|
|
253
|
+
function register(program) {
|
|
254
|
+
program
|
|
255
|
+
.command('purge')
|
|
256
|
+
.description('Remove embeddings from MongoDB based on criteria')
|
|
257
|
+
.option('--db <database>', 'Database name')
|
|
258
|
+
.option('--collection <name>', 'Collection name')
|
|
259
|
+
.option('--source <glob>', 'Filter by metadata.source pattern')
|
|
260
|
+
.option('--before <date>', 'Filter by _embeddedAt before date (ISO 8601)')
|
|
261
|
+
.option('-m, --model <model>', 'Filter by _model field')
|
|
262
|
+
.option('--filter <json>', 'Raw MongoDB filter (JSON)')
|
|
263
|
+
.option('--stale', 'Remove docs whose source files no longer exist')
|
|
264
|
+
.option('--force', 'Skip confirmation prompt')
|
|
265
|
+
.option('--dry-run', 'Show what would be deleted without acting')
|
|
266
|
+
.option('--json', 'Machine-readable output')
|
|
267
|
+
.option('-q, --quiet', 'Suppress non-essential output')
|
|
268
|
+
.action(purge);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = { register, purge, buildFilter };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pc = require('picocolors');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const { getConfigValue } = require('../lib/config');
|
|
6
|
+
const { embed } = require('../lib/api');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* vai quickstart — Zero-to-search interactive tutorial
|
|
10
|
+
* Gets developers from nothing to their first semantic search in minutes.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const SAMPLE_DOCS = [
|
|
14
|
+
"MongoDB Atlas Vector Search enables semantic search on your data using machine learning embeddings.",
|
|
15
|
+
"Voyage AI provides state-of-the-art embedding models with the best quality-to-cost ratio.",
|
|
16
|
+
"RAG (Retrieval-Augmented Generation) combines vector search with LLMs for accurate AI responses.",
|
|
17
|
+
"The shared embedding space in Voyage 4 models lets you embed queries and documents with different models.",
|
|
18
|
+
"Reranking improves search precision by re-scoring results with a cross-encoder model.",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function sleep(ms) {
|
|
22
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createPrompt() {
|
|
26
|
+
const rl = readline.createInterface({
|
|
27
|
+
input: process.stdin,
|
|
28
|
+
output: process.stdout,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
ask: (question) => new Promise((resolve) => {
|
|
33
|
+
rl.question(question, (answer) => resolve(answer));
|
|
34
|
+
}),
|
|
35
|
+
close: () => rl.close(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function runQuickstart(options = {}) {
|
|
40
|
+
const { skip } = options;
|
|
41
|
+
|
|
42
|
+
console.log(pc.bold('\n🚀 Voyage AI CLI Quickstart\n'));
|
|
43
|
+
console.log(pc.dim('This tutorial will get you from zero to semantic search in 2 minutes.\n'));
|
|
44
|
+
|
|
45
|
+
// Check for API key
|
|
46
|
+
const apiKey = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
|
|
47
|
+
if (!apiKey) {
|
|
48
|
+
console.log(pc.red('✗ No API key found.\n'));
|
|
49
|
+
console.log(' First, get a free API key:');
|
|
50
|
+
console.log(pc.cyan(' → https://dash.voyageai.com/api-keys\n'));
|
|
51
|
+
console.log(' Then configure it:');
|
|
52
|
+
console.log(pc.cyan(' → vai config set api-key YOUR_KEY\n'));
|
|
53
|
+
console.log(' Or set the environment variable:');
|
|
54
|
+
console.log(pc.cyan(' → export VOYAGE_API_KEY=YOUR_KEY\n'));
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(pc.green('✓') + ' API key configured\n');
|
|
59
|
+
|
|
60
|
+
// Step 1: Explain what we're doing
|
|
61
|
+
console.log(pc.bold('Step 1: Understanding Embeddings'));
|
|
62
|
+
console.log(pc.dim('─'.repeat(40)));
|
|
63
|
+
console.log(`
|
|
64
|
+
Embeddings turn text into ${pc.cyan('vectors')} (arrays of numbers) that capture
|
|
65
|
+
meaning. Similar texts have similar vectors, enabling semantic search.
|
|
66
|
+
|
|
67
|
+
We'll embed these ${SAMPLE_DOCS.length} sample documents:
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
SAMPLE_DOCS.forEach((doc, i) => {
|
|
71
|
+
const preview = doc.length > 70 ? doc.slice(0, 70) + '...' : doc;
|
|
72
|
+
console.log(` ${i + 1}. ${pc.dim(preview)}`);
|
|
73
|
+
});
|
|
74
|
+
console.log('');
|
|
75
|
+
|
|
76
|
+
if (!skip) {
|
|
77
|
+
const prompt = createPrompt();
|
|
78
|
+
await prompt.ask(pc.dim('Press Enter to continue...'));
|
|
79
|
+
prompt.close();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Step 2: Embed the documents
|
|
83
|
+
console.log(pc.bold('\nStep 2: Embedding Documents'));
|
|
84
|
+
console.log(pc.dim('─'.repeat(40)));
|
|
85
|
+
console.log(`
|
|
86
|
+
Running: ${pc.cyan('vai embed --model voyage-3-lite')}
|
|
87
|
+
`);
|
|
88
|
+
|
|
89
|
+
let embeddings;
|
|
90
|
+
try {
|
|
91
|
+
process.stdout.write(' Embedding documents... ');
|
|
92
|
+
const result = await embed({
|
|
93
|
+
texts: SAMPLE_DOCS,
|
|
94
|
+
model: 'voyage-3-lite',
|
|
95
|
+
inputType: 'document',
|
|
96
|
+
});
|
|
97
|
+
embeddings = result.embeddings;
|
|
98
|
+
console.log(pc.green('✓'));
|
|
99
|
+
console.log(`
|
|
100
|
+
${pc.green('✓')} Created ${embeddings.length} embeddings
|
|
101
|
+
${pc.dim(` Dimensions: ${embeddings[0].length}`)}
|
|
102
|
+
${pc.dim(` Model: voyage-3-lite`)}
|
|
103
|
+
`);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.log(pc.red('✗'));
|
|
106
|
+
console.log(pc.red(`\n Error: ${err.message}\n`));
|
|
107
|
+
console.log(' Check your API key with: vai doctor\n');
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Step 3: Search
|
|
112
|
+
console.log(pc.bold('Step 3: Semantic Search'));
|
|
113
|
+
console.log(pc.dim('─'.repeat(40)));
|
|
114
|
+
|
|
115
|
+
const query = 'How do I improve search accuracy?';
|
|
116
|
+
console.log(`
|
|
117
|
+
Now let's search! We'll embed a query and find the most similar documents.
|
|
118
|
+
|
|
119
|
+
Query: "${pc.cyan(query)}"
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
process.stdout.write(' Embedding query... ');
|
|
124
|
+
const queryResult = await embed({
|
|
125
|
+
texts: [query],
|
|
126
|
+
model: 'voyage-3-lite',
|
|
127
|
+
inputType: 'query',
|
|
128
|
+
});
|
|
129
|
+
const queryEmbedding = queryResult.embeddings[0];
|
|
130
|
+
console.log(pc.green('✓'));
|
|
131
|
+
|
|
132
|
+
// Calculate similarities
|
|
133
|
+
const similarities = embeddings.map((docEmb, i) => {
|
|
134
|
+
const dotProduct = docEmb.reduce((sum, val, j) => sum + val * queryEmbedding[j], 0);
|
|
135
|
+
const normA = Math.sqrt(docEmb.reduce((sum, val) => sum + val * val, 0));
|
|
136
|
+
const normB = Math.sqrt(queryEmbedding.reduce((sum, val) => sum + val * val, 0));
|
|
137
|
+
return {
|
|
138
|
+
index: i,
|
|
139
|
+
score: dotProduct / (normA * normB),
|
|
140
|
+
text: SAMPLE_DOCS[i],
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Sort by similarity
|
|
145
|
+
similarities.sort((a, b) => b.score - a.score);
|
|
146
|
+
|
|
147
|
+
console.log(`
|
|
148
|
+
${pc.bold('Results (ranked by similarity):')}
|
|
149
|
+
`);
|
|
150
|
+
|
|
151
|
+
similarities.forEach((item, rank) => {
|
|
152
|
+
const scoreColor = item.score > 0.5 ? pc.green : item.score > 0.3 ? pc.yellow : pc.dim;
|
|
153
|
+
const preview = item.text.length > 60 ? item.text.slice(0, 60) + '...' : item.text;
|
|
154
|
+
console.log(` ${rank + 1}. ${scoreColor(item.score.toFixed(3))} ${preview}`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.log(pc.red('✗'));
|
|
159
|
+
console.log(pc.red(`\n Error: ${err.message}\n`));
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Success!
|
|
164
|
+
console.log(pc.bold('\n✨ Congratulations!'));
|
|
165
|
+
console.log(pc.dim('─'.repeat(40)));
|
|
166
|
+
console.log(`
|
|
167
|
+
You just performed your first semantic search with Voyage AI!
|
|
168
|
+
|
|
169
|
+
The top result about ${pc.cyan('reranking')} is relevant because it discusses
|
|
170
|
+
improving search ${pc.cyan('precision')} — even though it doesn't contain the
|
|
171
|
+
exact words "improve" or "accuracy". That's the power of embeddings!
|
|
172
|
+
|
|
173
|
+
${pc.bold('Why Voyage AI?')}
|
|
174
|
+
• ${pc.cyan('Best quality-to-cost ratio')} — SOTA quality at lower prices
|
|
175
|
+
• ${pc.cyan('Shared embedding space')} — mix models for cost optimization
|
|
176
|
+
• ${pc.cyan('Domain-specific models')} — code, finance, law, multilingual
|
|
177
|
+
• ${pc.cyan('Reranking')} — boost precision with two-stage retrieval
|
|
178
|
+
|
|
179
|
+
${pc.bold('Next Steps:')}
|
|
180
|
+
${pc.cyan('vai explain embeddings')} — Learn more about how embeddings work
|
|
181
|
+
${pc.cyan('vai explain reranking')} — Understand two-stage retrieval
|
|
182
|
+
${pc.cyan('vai demo')} — Full interactive walkthrough
|
|
183
|
+
${pc.cyan('vai pipeline')} — Build a complete RAG pipeline
|
|
184
|
+
${pc.cyan('vai playground')} — Visual exploration in your browser
|
|
185
|
+
|
|
186
|
+
${pc.dim('Docs: https://docs.voyageai.com | Dashboard: https://dash.voyageai.com')}
|
|
187
|
+
`);
|
|
188
|
+
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function register(program) {
|
|
193
|
+
program
|
|
194
|
+
.command('quickstart')
|
|
195
|
+
.description('Zero-to-search tutorial — learn semantic search in 2 minutes')
|
|
196
|
+
.option('--skip', 'Skip interactive prompts')
|
|
197
|
+
.action(async (options) => {
|
|
198
|
+
const exitCode = await runQuickstart(options);
|
|
199
|
+
process.exit(exitCode);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = { register, runQuickstart };
|