voyageai-cli 1.22.0 → 1.23.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/package.json +4 -2
- package/src/cli.js +4 -0
- package/src/commands/chat.js +503 -0
- package/src/commands/demo.js +75 -0
- package/src/commands/embed.js +10 -0
- package/src/commands/index.js +1 -1
- package/src/commands/init.js +34 -97
- package/src/commands/mcp-server.js +49 -0
- package/src/commands/ping.js +52 -0
- package/src/commands/pipeline.js +17 -3
- package/src/commands/playground.js +186 -0
- package/src/commands/purge.js +3 -1
- package/src/commands/refresh.js +3 -1
- package/src/commands/rerank.js +10 -0
- package/src/commands/scaffold.js +1 -2
- package/src/lib/chat.js +252 -0
- package/src/lib/codegen.js +5 -4
- package/src/lib/config.js +5 -1
- package/src/lib/cost.js +352 -0
- package/src/lib/explanations.js +260 -0
- package/src/lib/history.js +260 -0
- package/src/lib/llm.js +485 -0
- package/src/lib/preflight.js +281 -0
- package/src/lib/prompt.js +111 -0
- package/src/lib/wizard-cli.js +135 -0
- package/src/lib/wizard-steps-chat.js +171 -0
- package/src/lib/wizard-steps-init.js +174 -0
- package/src/lib/wizard.js +222 -0
- package/src/mcp/schemas/index.js +102 -0
- package/src/mcp/server.js +162 -0
- package/src/mcp/tools/embedding.js +67 -0
- package/src/mcp/tools/ingest.js +89 -0
- package/src/mcp/tools/management.js +132 -0
- package/src/mcp/tools/retrieval.js +209 -0
- package/src/mcp/tools/utility.js +219 -0
- package/src/playground/index.html +1195 -199
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveConcept, listConcepts, getConcept } = require('../../lib/explanations');
|
|
4
|
+
const { MODEL_CATALOG, getDefaultModel } = require('../../lib/catalog');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Simple substring/word-overlap matching for topic suggestions.
|
|
8
|
+
* Returns topics sorted by relevance (best match first).
|
|
9
|
+
* @param {string} input - User's query
|
|
10
|
+
* @param {string[]} topics - Available topic keys
|
|
11
|
+
* @returns {Array<{ topic: string, summary: string }>}
|
|
12
|
+
*/
|
|
13
|
+
function suggestTopics(input, topics) {
|
|
14
|
+
const normalized = input.toLowerCase().trim();
|
|
15
|
+
const words = normalized.split(/[\s\-_]+/).filter(w => w.length > 2);
|
|
16
|
+
|
|
17
|
+
const scored = topics.map(key => {
|
|
18
|
+
const concept = getConcept(key);
|
|
19
|
+
const haystack = `${key} ${concept.title} ${concept.summary}`.toLowerCase();
|
|
20
|
+
let score = 0;
|
|
21
|
+
|
|
22
|
+
// Substring match on topic key
|
|
23
|
+
if (key.includes(normalized)) score += 10;
|
|
24
|
+
if (haystack.includes(normalized)) score += 5;
|
|
25
|
+
|
|
26
|
+
// Word overlap
|
|
27
|
+
for (const word of words) {
|
|
28
|
+
if (key.includes(word)) score += 3;
|
|
29
|
+
if (haystack.includes(word)) score += 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { topic: key, summary: concept.summary, title: concept.title, score };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return scored
|
|
36
|
+
.filter(s => s.score > 0)
|
|
37
|
+
.sort((a, b) => b.score - a.score)
|
|
38
|
+
.slice(0, 5)
|
|
39
|
+
.map(({ topic, summary, title }) => ({ topic, title, summary }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register utility tools: vai_topics, vai_explain, vai_estimate
|
|
44
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
45
|
+
* @param {object} schemas
|
|
46
|
+
*/
|
|
47
|
+
function registerUtilityTools(server, schemas) {
|
|
48
|
+
// vai_topics — list all available educational topics
|
|
49
|
+
server.tool(
|
|
50
|
+
'vai_topics',
|
|
51
|
+
'List all available educational topics with summaries. Call this FIRST to discover what topics vai can explain — covers embeddings, vector search, RAG, reranking, model selection, multimodal, code generation, and more. Then use vai_explain to dive deep into any topic.',
|
|
52
|
+
schemas.topicsSchema,
|
|
53
|
+
async (input) => {
|
|
54
|
+
const allTopics = listConcepts();
|
|
55
|
+
|
|
56
|
+
let topics;
|
|
57
|
+
if (input.search) {
|
|
58
|
+
// Filter topics by search term
|
|
59
|
+
const suggestions = suggestTopics(input.search, allTopics);
|
|
60
|
+
if (suggestions.length === 0) {
|
|
61
|
+
return {
|
|
62
|
+
structuredContent: { search: input.search, results: [], totalTopics: allTopics.length },
|
|
63
|
+
content: [{ type: 'text', text: `No topics matching "${input.search}". Use vai_topics without a search to see all ${allTopics.length} topics.` }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
topics = suggestions;
|
|
67
|
+
} else {
|
|
68
|
+
// List all topics with summaries
|
|
69
|
+
topics = allTopics.map(key => {
|
|
70
|
+
const concept = getConcept(key);
|
|
71
|
+
return { topic: key, title: concept.title, summary: concept.summary };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Group by category for better browsing
|
|
76
|
+
const categories = {
|
|
77
|
+
'Core Concepts': ['embeddings', 'vector-search', 'rag', 'cosine-similarity', 'input-type', 'two-stage-retrieval'],
|
|
78
|
+
'Models & Pricing': ['models', 'mixture-of-experts', 'voyage-4-nano', 'shared-embedding-space', 'quantization', 'benchmarking', 'rteb-benchmarks', 'provider-comparison'],
|
|
79
|
+
'Multimodal': ['multimodal-embeddings', 'cross-modal-search', 'modality-gap', 'multimodal-rag'],
|
|
80
|
+
'API & Configuration': ['api-keys', 'api-access', 'batch-processing', 'auto-embedding', 'vai-vs-auto-embedding'],
|
|
81
|
+
'Reranking & Evaluation': ['reranking', 'rerank-eval', 'eval-comparison'],
|
|
82
|
+
'Code & Chat': ['code-generation', 'scaffolding', 'chat'],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const textLines = topics.map(t => `• **${t.topic}** — ${t.summary}`);
|
|
86
|
+
const searchNote = input.search ? ` matching "${input.search}"` : '';
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
structuredContent: {
|
|
90
|
+
search: input.search || null,
|
|
91
|
+
topics,
|
|
92
|
+
categories: input.search ? undefined : categories,
|
|
93
|
+
totalTopics: allTopics.length,
|
|
94
|
+
},
|
|
95
|
+
content: [{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: `${topics.length} topic${topics.length === 1 ? '' : 's'}${searchNote} available:\n\n${textLines.join('\n')}\n\nUse vai_explain with any topic name to get the full explanation.`,
|
|
98
|
+
}],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// vai_explain — educational content with fuzzy matching
|
|
104
|
+
server.tool(
|
|
105
|
+
'vai_explain',
|
|
106
|
+
'Get a detailed explanation of a topic. Covers embeddings, vector search, RAG, MoE architecture, shared space, quantization, multimodal, reranking, and more. If the exact topic isn\'t found, suggests similar topics. Use vai_topics first to browse available topics.',
|
|
107
|
+
schemas.explainSchema,
|
|
108
|
+
async (input) => {
|
|
109
|
+
const key = resolveConcept(input.topic);
|
|
110
|
+
if (!key) {
|
|
111
|
+
// Try fuzzy matching before giving up
|
|
112
|
+
const allTopics = listConcepts();
|
|
113
|
+
const suggestions = suggestTopics(input.topic, allTopics);
|
|
114
|
+
|
|
115
|
+
if (suggestions.length > 0) {
|
|
116
|
+
// Auto-resolve if there's a strong match
|
|
117
|
+
const bestMatch = suggestions[0];
|
|
118
|
+
const bestKey = resolveConcept(bestMatch.topic);
|
|
119
|
+
if (bestKey) {
|
|
120
|
+
const concept = getConcept(bestKey);
|
|
121
|
+
return {
|
|
122
|
+
structuredContent: {
|
|
123
|
+
topic: bestKey,
|
|
124
|
+
title: concept.title,
|
|
125
|
+
summary: concept.summary,
|
|
126
|
+
content: concept.content,
|
|
127
|
+
links: concept.links || [],
|
|
128
|
+
matchedFrom: input.topic,
|
|
129
|
+
relatedTopics: suggestions.slice(1).map(s => s.topic),
|
|
130
|
+
},
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: `# ${concept.title}\n\n${concept.summary}\n\n${concept.content}${suggestions.length > 1 ? `\n\n---\nRelated topics: ${suggestions.slice(1).map(s => s.topic).join(', ')}` : ''}`,
|
|
134
|
+
}],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
structuredContent: { error: 'unknown_topic', topic: input.topic, suggestions, available: allTopics },
|
|
141
|
+
content: [{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: suggestions.length > 0
|
|
144
|
+
? `No exact match for "${input.topic}". Did you mean:\n\n${suggestions.map(s => `• **${s.topic}** — ${s.summary}`).join('\n')}\n\nUse vai_topics to see all ${allTopics.length} available topics.`
|
|
145
|
+
: `Unknown topic: "${input.topic}"\n\nUse vai_topics to browse all ${allTopics.length} available topics.`,
|
|
146
|
+
}],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const concept = getConcept(key);
|
|
151
|
+
|
|
152
|
+
// Find related topics based on the current topic
|
|
153
|
+
const allTopics = listConcepts().filter(t => t !== key);
|
|
154
|
+
const related = suggestTopics(key, allTopics).slice(0, 3);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
structuredContent: {
|
|
158
|
+
topic: key,
|
|
159
|
+
title: concept.title,
|
|
160
|
+
summary: concept.summary,
|
|
161
|
+
content: concept.content,
|
|
162
|
+
links: concept.links || [],
|
|
163
|
+
tryIt: concept.tryIt || null,
|
|
164
|
+
relatedTopics: related.map(r => r.topic),
|
|
165
|
+
},
|
|
166
|
+
content: [{
|
|
167
|
+
type: 'text',
|
|
168
|
+
text: `# ${concept.title}\n\n${concept.summary}\n\n${concept.content}${concept.links?.length ? `\n\n**Learn more:** ${concept.links.join(', ')}` : ''}${related.length ? `\n\n**Related:** ${related.map(r => r.topic).join(', ')}` : ''}`,
|
|
169
|
+
}],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// vai_estimate — cost estimation
|
|
175
|
+
server.tool(
|
|
176
|
+
'vai_estimate',
|
|
177
|
+
'Estimate costs for Voyage AI embedding and query operations at various scales. Use when planning ingestion, budgeting, or comparing model costs.',
|
|
178
|
+
schemas.estimateSchema,
|
|
179
|
+
async (input) => {
|
|
180
|
+
const { docs, queries, months } = input;
|
|
181
|
+
// Average tokens per doc chunk (~250 tokens)
|
|
182
|
+
const avgTokensPerDoc = 250;
|
|
183
|
+
const totalEmbedTokens = docs * avgTokensPerDoc;
|
|
184
|
+
|
|
185
|
+
const embeddingModels = MODEL_CATALOG
|
|
186
|
+
.filter(m => m.type === 'embedding' && !m.legacy && !m.unreleased && m.pricePerMToken)
|
|
187
|
+
.map(m => {
|
|
188
|
+
const embedCost = (totalEmbedTokens / 1_000_000) * m.pricePerMToken;
|
|
189
|
+
const queryCostPerMonth = queries > 0 ? (queries * avgTokensPerDoc / 1_000_000) * m.pricePerMToken : 0;
|
|
190
|
+
const totalCost = embedCost + (queryCostPerMonth * months);
|
|
191
|
+
return {
|
|
192
|
+
model: m.name,
|
|
193
|
+
pricePerMToken: m.pricePerMToken,
|
|
194
|
+
embeddingCost: Math.round(embedCost * 100) / 100,
|
|
195
|
+
monthlyCost: Math.round(queryCostPerMonth * 100) / 100,
|
|
196
|
+
totalCost: Math.round(totalCost * 100) / 100,
|
|
197
|
+
};
|
|
198
|
+
})
|
|
199
|
+
.sort((a, b) => a.totalCost - b.totalCost);
|
|
200
|
+
|
|
201
|
+
const structured = {
|
|
202
|
+
input: { docs, queries, months },
|
|
203
|
+
estimates: embeddingModels,
|
|
204
|
+
recommendation: embeddingModels[0]?.model || getDefaultModel(),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const lines = embeddingModels.map(e =>
|
|
208
|
+
`• ${e.model}: embed $${e.embeddingCost} + $${e.monthlyCost}/mo queries = $${e.totalCost} total (${months}mo)`
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
structuredContent: structured,
|
|
213
|
+
content: [{ type: 'text', text: `Cost estimate for ${docs.toLocaleString()} docs, ${queries.toLocaleString()} queries/mo over ${months} months:\n\n${lines.join('\n')}\n\nRecommended: ${structured.recommendation}` }],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = { registerUtilityTools };
|