voyageai-cli 1.26.0 → 1.26.1
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 +1 -1
- package/src/commands/chat.js +281 -78
- package/src/commands/playground.js +42 -19
- package/src/lib/chat.js +170 -4
- package/src/lib/llm.js +304 -2
- package/src/lib/mongo.js +6 -6
- package/src/lib/prompt.js +60 -1
- package/src/lib/tool-registry.js +194 -0
- package/src/mcp/tools/embedding.js +55 -43
- package/src/mcp/tools/ingest.js +74 -67
- package/src/mcp/tools/management.js +60 -48
- package/src/mcp/tools/retrieval.js +181 -163
- package/src/mcp/tools/utility.js +171 -153
- package/src/playground/index.html +508 -10
package/src/mcp/tools/utility.js
CHANGED
|
@@ -40,180 +40,198 @@ function suggestTopics(input, topics) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
* @param {
|
|
45
|
-
* @
|
|
43
|
+
* Handler for vai_topics: list all available educational topics.
|
|
44
|
+
* @param {object} input - Validated input matching topicsSchema
|
|
45
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
46
46
|
*/
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
47
|
+
async function handleVaiTopics(input) {
|
|
48
|
+
const allTopics = listConcepts();
|
|
49
|
+
|
|
50
|
+
let topics;
|
|
51
|
+
if (input.search) {
|
|
52
|
+
// Filter topics by search term
|
|
53
|
+
const suggestions = suggestTopics(input.search, allTopics);
|
|
54
|
+
if (suggestions.length === 0) {
|
|
88
55
|
return {
|
|
89
|
-
structuredContent: {
|
|
90
|
-
|
|
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
|
-
}],
|
|
56
|
+
structuredContent: { search: input.search, results: [], totalTopics: allTopics.length },
|
|
57
|
+
content: [{ type: 'text', text: `No topics matching "${input.search}". Use vai_topics without a search to see all ${allTopics.length} topics.` }],
|
|
99
58
|
};
|
|
100
59
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
}
|
|
60
|
+
topics = suggestions;
|
|
61
|
+
} else {
|
|
62
|
+
// List all topics with summaries
|
|
63
|
+
topics = allTopics.map(key => {
|
|
64
|
+
const concept = getConcept(key);
|
|
65
|
+
return { topic: key, title: concept.title, summary: concept.summary };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Group by category for better browsing
|
|
70
|
+
const categories = {
|
|
71
|
+
'Core Concepts': ['embeddings', 'vector-search', 'rag', 'cosine-similarity', 'input-type', 'two-stage-retrieval'],
|
|
72
|
+
'Models & Pricing': ['models', 'mixture-of-experts', 'voyage-4-nano', 'shared-embedding-space', 'quantization', 'benchmarking', 'rteb-benchmarks', 'provider-comparison'],
|
|
73
|
+
'Multimodal': ['multimodal-embeddings', 'cross-modal-search', 'modality-gap', 'multimodal-rag'],
|
|
74
|
+
'API & Configuration': ['api-keys', 'api-access', 'batch-processing', 'auto-embedding', 'vai-vs-auto-embedding'],
|
|
75
|
+
'Reranking & Evaluation': ['reranking', 'rerank-eval', 'eval-comparison'],
|
|
76
|
+
'Code & Chat': ['code-generation', 'scaffolding', 'chat'],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const textLines = topics.map(t => `• **${t.topic}** — ${t.summary}`);
|
|
80
|
+
const searchNote = input.search ? ` matching "${input.search}"` : '';
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
structuredContent: {
|
|
84
|
+
search: input.search || null,
|
|
85
|
+
topics,
|
|
86
|
+
categories: input.search ? undefined : categories,
|
|
87
|
+
totalTopics: allTopics.length,
|
|
88
|
+
},
|
|
89
|
+
content: [{
|
|
90
|
+
type: 'text',
|
|
91
|
+
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.`,
|
|
92
|
+
}],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
138
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Handler for vai_explain: educational content with fuzzy matching.
|
|
98
|
+
* @param {object} input - Validated input matching explainSchema
|
|
99
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
100
|
+
*/
|
|
101
|
+
async function handleVaiExplain(input) {
|
|
102
|
+
const key = resolveConcept(input.topic);
|
|
103
|
+
if (!key) {
|
|
104
|
+
// Try fuzzy matching before giving up
|
|
105
|
+
const allTopics = listConcepts();
|
|
106
|
+
const suggestions = suggestTopics(input.topic, allTopics);
|
|
107
|
+
|
|
108
|
+
if (suggestions.length > 0) {
|
|
109
|
+
// Auto-resolve if there's a strong match
|
|
110
|
+
const bestMatch = suggestions[0];
|
|
111
|
+
const bestKey = resolveConcept(bestMatch.topic);
|
|
112
|
+
if (bestKey) {
|
|
113
|
+
const concept = getConcept(bestKey);
|
|
139
114
|
return {
|
|
140
|
-
structuredContent: {
|
|
115
|
+
structuredContent: {
|
|
116
|
+
topic: bestKey,
|
|
117
|
+
title: concept.title,
|
|
118
|
+
summary: concept.summary,
|
|
119
|
+
content: concept.content,
|
|
120
|
+
links: concept.links || [],
|
|
121
|
+
matchedFrom: input.topic,
|
|
122
|
+
relatedTopics: suggestions.slice(1).map(s => s.topic),
|
|
123
|
+
},
|
|
141
124
|
content: [{
|
|
142
125
|
type: 'text',
|
|
143
|
-
text: suggestions.length >
|
|
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.`,
|
|
126
|
+
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(', ')}` : ''}`,
|
|
146
127
|
}],
|
|
147
128
|
};
|
|
148
129
|
}
|
|
130
|
+
}
|
|
149
131
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
132
|
+
return {
|
|
133
|
+
structuredContent: { error: 'unknown_topic', topic: input.topic, suggestions, available: allTopics },
|
|
134
|
+
content: [{
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: suggestions.length > 0
|
|
137
|
+
? `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.`
|
|
138
|
+
: `Unknown topic: "${input.topic}"\n\nUse vai_topics to browse all ${allTopics.length} available topics.`,
|
|
139
|
+
}],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const concept = getConcept(key);
|
|
144
|
+
|
|
145
|
+
// Find related topics based on the current topic
|
|
146
|
+
const allTopics = listConcepts().filter(t => t !== key);
|
|
147
|
+
const related = suggestTopics(key, allTopics).slice(0, 3);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
structuredContent: {
|
|
151
|
+
topic: key,
|
|
152
|
+
title: concept.title,
|
|
153
|
+
summary: concept.summary,
|
|
154
|
+
content: concept.content,
|
|
155
|
+
links: concept.links || [],
|
|
156
|
+
tryIt: concept.tryIt || null,
|
|
157
|
+
relatedTopics: related.map(r => r.topic),
|
|
158
|
+
},
|
|
159
|
+
content: [{
|
|
160
|
+
type: 'text',
|
|
161
|
+
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(', ')}` : ''}`,
|
|
162
|
+
}],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
155
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Handler for vai_estimate: cost estimation.
|
|
168
|
+
* @param {object} input - Validated input matching estimateSchema
|
|
169
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
170
|
+
*/
|
|
171
|
+
async function handleVaiEstimate(input) {
|
|
172
|
+
const { docs, queries, months } = input;
|
|
173
|
+
// Average tokens per doc chunk (~250 tokens)
|
|
174
|
+
const avgTokensPerDoc = 250;
|
|
175
|
+
const totalEmbedTokens = docs * avgTokensPerDoc;
|
|
176
|
+
|
|
177
|
+
const embeddingModels = MODEL_CATALOG
|
|
178
|
+
.filter(m => m.type === 'embedding' && !m.legacy && !m.unreleased && m.pricePerMToken)
|
|
179
|
+
.map(m => {
|
|
180
|
+
const embedCost = (totalEmbedTokens / 1_000_000) * m.pricePerMToken;
|
|
181
|
+
const queryCostPerMonth = queries > 0 ? (queries * avgTokensPerDoc / 1_000_000) * m.pricePerMToken : 0;
|
|
182
|
+
const totalCost = embedCost + (queryCostPerMonth * months);
|
|
156
183
|
return {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
}],
|
|
184
|
+
model: m.name,
|
|
185
|
+
pricePerMToken: m.pricePerMToken,
|
|
186
|
+
embeddingCost: Math.round(embedCost * 100) / 100,
|
|
187
|
+
monthlyCost: Math.round(queryCostPerMonth * 100) / 100,
|
|
188
|
+
totalCost: Math.round(totalCost * 100) / 100,
|
|
170
189
|
};
|
|
171
|
-
}
|
|
190
|
+
})
|
|
191
|
+
.sort((a, b) => a.totalCost - b.totalCost);
|
|
192
|
+
|
|
193
|
+
const structured = {
|
|
194
|
+
input: { docs, queries, months },
|
|
195
|
+
estimates: embeddingModels,
|
|
196
|
+
recommendation: embeddingModels[0]?.model || getDefaultModel(),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const lines = embeddingModels.map(e =>
|
|
200
|
+
`• ${e.model}: embed $${e.embeddingCost} + $${e.monthlyCost}/mo queries = $${e.totalCost} total (${months}mo)`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
structuredContent: structured,
|
|
205
|
+
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}` }],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Register utility tools: vai_topics, vai_explain, vai_estimate
|
|
211
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
212
|
+
* @param {object} schemas
|
|
213
|
+
*/
|
|
214
|
+
function registerUtilityTools(server, schemas) {
|
|
215
|
+
server.tool(
|
|
216
|
+
'vai_topics',
|
|
217
|
+
'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.',
|
|
218
|
+
schemas.topicsSchema,
|
|
219
|
+
handleVaiTopics
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
server.tool(
|
|
223
|
+
'vai_explain',
|
|
224
|
+
'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.',
|
|
225
|
+
schemas.explainSchema,
|
|
226
|
+
handleVaiExplain
|
|
172
227
|
);
|
|
173
228
|
|
|
174
|
-
// vai_estimate — cost estimation
|
|
175
229
|
server.tool(
|
|
176
230
|
'vai_estimate',
|
|
177
231
|
'Estimate costs for Voyage AI embedding and query operations at various scales. Use when planning ingestion, budgeting, or comparing model costs.',
|
|
178
232
|
schemas.estimateSchema,
|
|
179
|
-
|
|
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
|
-
}
|
|
233
|
+
handleVaiEstimate
|
|
216
234
|
);
|
|
217
235
|
}
|
|
218
236
|
|
|
219
|
-
module.exports = { registerUtilityTools };
|
|
237
|
+
module.exports = { registerUtilityTools, handleVaiTopics, handleVaiExplain, handleVaiEstimate };
|