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
|
@@ -19,191 +19,209 @@ function resolveDbCollection(input) {
|
|
|
19
19
|
return { db, collection };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Handler for vai_query: full RAG query (embed, vector search, rerank).
|
|
24
|
+
* @param {object} input - Validated input matching querySchema
|
|
25
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
26
|
+
*/
|
|
27
|
+
async function handleVaiQuery(input) {
|
|
28
|
+
const { db, collection: collName } = resolveDbCollection(input);
|
|
29
|
+
const { config: proj } = loadProject();
|
|
30
|
+
const model = input.model || proj.model || getDefaultModel();
|
|
31
|
+
const index = proj.index || 'vector_index';
|
|
32
|
+
const field = proj.field || 'embedding';
|
|
33
|
+
const dimensions = proj.dimensions;
|
|
34
|
+
const limit = input.limit;
|
|
35
|
+
const candidateLimit = Math.min(limit * 4, 20);
|
|
36
|
+
const start = Date.now();
|
|
37
|
+
|
|
38
|
+
// Step 1: Embed query
|
|
39
|
+
const embedOpts = { model, inputType: 'query' };
|
|
40
|
+
if (dimensions) embedOpts.dimensions = dimensions;
|
|
41
|
+
const embedResult = await generateEmbeddings([input.query], embedOpts);
|
|
42
|
+
const queryVector = embedResult.data[0].embedding;
|
|
43
|
+
|
|
44
|
+
// Step 2: Vector search
|
|
45
|
+
const { client, collection: coll } = await getMongoCollection(db, collName);
|
|
46
|
+
try {
|
|
47
|
+
const vectorSearchStage = {
|
|
48
|
+
index,
|
|
49
|
+
path: field,
|
|
50
|
+
queryVector,
|
|
51
|
+
numCandidates: Math.min(candidateLimit * 15, 10000),
|
|
52
|
+
limit: candidateLimit,
|
|
53
|
+
};
|
|
54
|
+
if (input.filter) vectorSearchStage.filter = input.filter;
|
|
55
|
+
|
|
56
|
+
const searchResults = await coll.aggregate([
|
|
57
|
+
{ $vectorSearch: vectorSearchStage },
|
|
58
|
+
{ $addFields: { _vsScore: { $meta: 'vectorSearchScore' } } },
|
|
59
|
+
]).toArray();
|
|
60
|
+
|
|
61
|
+
if (searchResults.length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
structuredContent: { query: input.query, results: [], metadata: { collection: collName, model, reranked: false, retrievalTimeMs: Date.now() - start, resultCount: 0 } },
|
|
64
|
+
content: [{ type: 'text', text: `No results found for "${input.query}" in ${db}.${collName}` }],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 3: Rerank (optional)
|
|
69
|
+
let finalResults;
|
|
70
|
+
let reranked = false;
|
|
71
|
+
|
|
72
|
+
if (input.rerank && searchResults.length > 1) {
|
|
73
|
+
const documents = searchResults.map(doc => doc.text || JSON.stringify(doc));
|
|
74
|
+
const rerankResult = await apiRequest('/rerank', {
|
|
75
|
+
query: input.query,
|
|
76
|
+
documents,
|
|
77
|
+
model: DEFAULT_RERANK_MODEL,
|
|
78
|
+
top_k: limit,
|
|
79
|
+
});
|
|
80
|
+
reranked = true;
|
|
81
|
+
finalResults = (rerankResult.data || []).map(item => {
|
|
82
|
+
const doc = searchResults[item.index];
|
|
83
|
+
return {
|
|
84
|
+
source: doc.metadata?.source || doc.source || 'unknown',
|
|
85
|
+
content: doc.text || '',
|
|
86
|
+
score: doc._vsScore,
|
|
87
|
+
rerankedScore: item.relevance_score,
|
|
88
|
+
metadata: doc.metadata || {},
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
finalResults = searchResults.slice(0, limit).map(doc => ({
|
|
93
|
+
source: doc.metadata?.source || doc.source || 'unknown',
|
|
94
|
+
content: doc.text || '',
|
|
95
|
+
score: doc._vsScore,
|
|
96
|
+
metadata: doc.metadata || {},
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const retrievalTimeMs = Date.now() - start;
|
|
101
|
+
const structured = {
|
|
102
|
+
query: input.query,
|
|
103
|
+
results: finalResults,
|
|
104
|
+
metadata: { collection: collName, model, reranked, retrievalTimeMs, resultCount: finalResults.length },
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const textLines = finalResults.map((r, i) =>
|
|
108
|
+
`[${i + 1}] ${r.source} (score: ${(r.rerankedScore || r.score || 0).toFixed(3)})\n${r.content.slice(0, 500)}`
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
structuredContent: structured,
|
|
113
|
+
content: [{ type: 'text', text: `Found ${finalResults.length} results for "${input.query}" (${retrievalTimeMs}ms):\n\n${textLines.join('\n\n')}` }],
|
|
114
|
+
};
|
|
115
|
+
} finally {
|
|
116
|
+
await client.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handler for vai_search: raw vector similarity search (no reranking).
|
|
122
|
+
* @param {object} input - Validated input matching searchSchema
|
|
123
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
124
|
+
*/
|
|
125
|
+
async function handleVaiSearch(input) {
|
|
126
|
+
const { db, collection: collName } = resolveDbCollection(input);
|
|
127
|
+
const { config: proj } = loadProject();
|
|
128
|
+
const model = input.model || proj.model || getDefaultModel();
|
|
129
|
+
const index = proj.index || 'vector_index';
|
|
130
|
+
const field = proj.field || 'embedding';
|
|
131
|
+
const dimensions = proj.dimensions;
|
|
132
|
+
const start = Date.now();
|
|
133
|
+
|
|
134
|
+
const embedOpts = { model, inputType: 'query' };
|
|
135
|
+
if (dimensions) embedOpts.dimensions = dimensions;
|
|
136
|
+
const embedResult = await generateEmbeddings([input.query], embedOpts);
|
|
137
|
+
const queryVector = embedResult.data[0].embedding;
|
|
138
|
+
|
|
139
|
+
const { client, collection: coll } = await getMongoCollection(db, collName);
|
|
140
|
+
try {
|
|
141
|
+
const vectorSearchStage = {
|
|
142
|
+
index,
|
|
143
|
+
path: field,
|
|
144
|
+
queryVector,
|
|
145
|
+
numCandidates: Math.min(input.limit * 15, 10000),
|
|
146
|
+
limit: input.limit,
|
|
147
|
+
};
|
|
148
|
+
if (input.filter) vectorSearchStage.filter = input.filter;
|
|
149
|
+
|
|
150
|
+
const results = await coll.aggregate([
|
|
151
|
+
{ $vectorSearch: vectorSearchStage },
|
|
152
|
+
{ $addFields: { _vsScore: { $meta: 'vectorSearchScore' } } },
|
|
153
|
+
]).toArray();
|
|
154
|
+
|
|
155
|
+
const mapped = results.map(doc => ({
|
|
156
|
+
source: doc.metadata?.source || doc.source || 'unknown',
|
|
157
|
+
content: doc.text || '',
|
|
158
|
+
score: doc._vsScore,
|
|
159
|
+
metadata: doc.metadata || {},
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
const retrievalTimeMs = Date.now() - start;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
structuredContent: { query: input.query, results: mapped, metadata: { collection: collName, model, retrievalTimeMs, resultCount: mapped.length } },
|
|
166
|
+
content: [{ type: 'text', text: `Found ${mapped.length} results for "${input.query}" (${retrievalTimeMs}ms):\n\n${mapped.map((r, i) => `[${i + 1}] ${r.source} (${(r.score || 0).toFixed(3)})\n${r.content.slice(0, 500)}`).join('\n\n')}` }],
|
|
167
|
+
};
|
|
168
|
+
} finally {
|
|
169
|
+
await client.close();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Handler for vai_rerank: standalone reranking.
|
|
175
|
+
* @param {object} input - Validated input matching rerankSchema
|
|
176
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
177
|
+
*/
|
|
178
|
+
async function handleVaiRerank(input) {
|
|
179
|
+
const start = Date.now();
|
|
180
|
+
const result = await apiRequest('/rerank', {
|
|
181
|
+
query: input.query,
|
|
182
|
+
documents: input.documents,
|
|
183
|
+
model: input.model,
|
|
184
|
+
top_k: input.documents.length,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const ranked = (result.data || []).map(item => ({
|
|
188
|
+
index: item.index,
|
|
189
|
+
relevanceScore: item.relevance_score,
|
|
190
|
+
document: input.documents[item.index].slice(0, 200) + (input.documents[item.index].length > 200 ? '...' : ''),
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
structuredContent: { query: input.query, results: ranked, model: input.model, timeMs: Date.now() - start },
|
|
195
|
+
content: [{ type: 'text', text: `Reranked ${input.documents.length} documents:\n\n${ranked.map((r, i) => `[${i + 1}] Score: ${r.relevanceScore.toFixed(3)} — ${r.document}`).join('\n')}` }],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
22
199
|
/**
|
|
23
200
|
* Register retrieval tools: vai_query, vai_search, vai_rerank
|
|
24
201
|
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
25
202
|
* @param {object} schemas
|
|
26
203
|
*/
|
|
27
204
|
function registerRetrievalTools(server, schemas) {
|
|
28
|
-
// vai_query — full RAG query: embed → vector search → rerank
|
|
29
205
|
server.tool(
|
|
30
206
|
'vai_query',
|
|
31
207
|
'Full RAG query: embeds the question with Voyage AI, runs vector search against MongoDB Atlas, and reranks results. Use this when you need to answer a question using the knowledge base.',
|
|
32
208
|
schemas.querySchema,
|
|
33
|
-
|
|
34
|
-
const { db, collection: collName } = resolveDbCollection(input);
|
|
35
|
-
const { config: proj } = loadProject();
|
|
36
|
-
const model = input.model || proj.model || getDefaultModel();
|
|
37
|
-
const index = proj.index || 'vector_index';
|
|
38
|
-
const field = proj.field || 'embedding';
|
|
39
|
-
const dimensions = proj.dimensions;
|
|
40
|
-
const limit = input.limit;
|
|
41
|
-
const candidateLimit = Math.min(limit * 4, 20);
|
|
42
|
-
const start = Date.now();
|
|
43
|
-
|
|
44
|
-
// Step 1: Embed query
|
|
45
|
-
const embedOpts = { model, inputType: 'query' };
|
|
46
|
-
if (dimensions) embedOpts.dimensions = dimensions;
|
|
47
|
-
const embedResult = await generateEmbeddings([input.query], embedOpts);
|
|
48
|
-
const queryVector = embedResult.data[0].embedding;
|
|
49
|
-
|
|
50
|
-
// Step 2: Vector search
|
|
51
|
-
const { client, collection: coll } = await getMongoCollection(db, collName);
|
|
52
|
-
try {
|
|
53
|
-
const vectorSearchStage = {
|
|
54
|
-
index,
|
|
55
|
-
path: field,
|
|
56
|
-
queryVector,
|
|
57
|
-
numCandidates: Math.min(candidateLimit * 15, 10000),
|
|
58
|
-
limit: candidateLimit,
|
|
59
|
-
};
|
|
60
|
-
if (input.filter) vectorSearchStage.filter = input.filter;
|
|
61
|
-
|
|
62
|
-
const searchResults = await coll.aggregate([
|
|
63
|
-
{ $vectorSearch: vectorSearchStage },
|
|
64
|
-
{ $addFields: { _vsScore: { $meta: 'vectorSearchScore' } } },
|
|
65
|
-
]).toArray();
|
|
66
|
-
|
|
67
|
-
if (searchResults.length === 0) {
|
|
68
|
-
return {
|
|
69
|
-
structuredContent: { query: input.query, results: [], metadata: { collection: collName, model, reranked: false, retrievalTimeMs: Date.now() - start, resultCount: 0 } },
|
|
70
|
-
content: [{ type: 'text', text: `No results found for "${input.query}" in ${db}.${collName}` }],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Step 3: Rerank (optional)
|
|
75
|
-
let finalResults;
|
|
76
|
-
let reranked = false;
|
|
77
|
-
|
|
78
|
-
if (input.rerank && searchResults.length > 1) {
|
|
79
|
-
const documents = searchResults.map(doc => doc.text || JSON.stringify(doc));
|
|
80
|
-
const rerankResult = await apiRequest('/rerank', {
|
|
81
|
-
query: input.query,
|
|
82
|
-
documents,
|
|
83
|
-
model: DEFAULT_RERANK_MODEL,
|
|
84
|
-
top_k: limit,
|
|
85
|
-
});
|
|
86
|
-
reranked = true;
|
|
87
|
-
finalResults = (rerankResult.data || []).map(item => {
|
|
88
|
-
const doc = searchResults[item.index];
|
|
89
|
-
return {
|
|
90
|
-
source: doc.metadata?.source || doc.source || 'unknown',
|
|
91
|
-
content: doc.text || '',
|
|
92
|
-
score: doc._vsScore,
|
|
93
|
-
rerankedScore: item.relevance_score,
|
|
94
|
-
metadata: doc.metadata || {},
|
|
95
|
-
};
|
|
96
|
-
});
|
|
97
|
-
} else {
|
|
98
|
-
finalResults = searchResults.slice(0, limit).map(doc => ({
|
|
99
|
-
source: doc.metadata?.source || doc.source || 'unknown',
|
|
100
|
-
content: doc.text || '',
|
|
101
|
-
score: doc._vsScore,
|
|
102
|
-
metadata: doc.metadata || {},
|
|
103
|
-
}));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const retrievalTimeMs = Date.now() - start;
|
|
107
|
-
const structured = {
|
|
108
|
-
query: input.query,
|
|
109
|
-
results: finalResults,
|
|
110
|
-
metadata: { collection: collName, model, reranked, retrievalTimeMs, resultCount: finalResults.length },
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const textLines = finalResults.map((r, i) =>
|
|
114
|
-
`[${i + 1}] ${r.source} (score: ${(r.rerankedScore || r.score || 0).toFixed(3)})\n${r.content.slice(0, 500)}`
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
structuredContent: structured,
|
|
119
|
-
content: [{ type: 'text', text: `Found ${finalResults.length} results for "${input.query}" (${retrievalTimeMs}ms):\n\n${textLines.join('\n\n')}` }],
|
|
120
|
-
};
|
|
121
|
-
} finally {
|
|
122
|
-
await client.close();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
209
|
+
handleVaiQuery
|
|
125
210
|
);
|
|
126
211
|
|
|
127
|
-
// vai_search — raw vector similarity search (no reranking)
|
|
128
212
|
server.tool(
|
|
129
213
|
'vai_search',
|
|
130
214
|
'Raw vector similarity search without reranking. Faster than vai_query but results are ordered by vector distance only. Use for exploratory searches or when you plan to rerank separately.',
|
|
131
215
|
schemas.searchSchema,
|
|
132
|
-
|
|
133
|
-
const { db, collection: collName } = resolveDbCollection(input);
|
|
134
|
-
const { config: proj } = loadProject();
|
|
135
|
-
const model = input.model || proj.model || getDefaultModel();
|
|
136
|
-
const index = proj.index || 'vector_index';
|
|
137
|
-
const field = proj.field || 'embedding';
|
|
138
|
-
const dimensions = proj.dimensions;
|
|
139
|
-
const start = Date.now();
|
|
140
|
-
|
|
141
|
-
const embedOpts = { model, inputType: 'query' };
|
|
142
|
-
if (dimensions) embedOpts.dimensions = dimensions;
|
|
143
|
-
const embedResult = await generateEmbeddings([input.query], embedOpts);
|
|
144
|
-
const queryVector = embedResult.data[0].embedding;
|
|
145
|
-
|
|
146
|
-
const { client, collection: coll } = await getMongoCollection(db, collName);
|
|
147
|
-
try {
|
|
148
|
-
const vectorSearchStage = {
|
|
149
|
-
index,
|
|
150
|
-
path: field,
|
|
151
|
-
queryVector,
|
|
152
|
-
numCandidates: Math.min(input.limit * 15, 10000),
|
|
153
|
-
limit: input.limit,
|
|
154
|
-
};
|
|
155
|
-
if (input.filter) vectorSearchStage.filter = input.filter;
|
|
156
|
-
|
|
157
|
-
const results = await coll.aggregate([
|
|
158
|
-
{ $vectorSearch: vectorSearchStage },
|
|
159
|
-
{ $addFields: { _vsScore: { $meta: 'vectorSearchScore' } } },
|
|
160
|
-
]).toArray();
|
|
161
|
-
|
|
162
|
-
const mapped = results.map(doc => ({
|
|
163
|
-
source: doc.metadata?.source || doc.source || 'unknown',
|
|
164
|
-
content: doc.text || '',
|
|
165
|
-
score: doc._vsScore,
|
|
166
|
-
metadata: doc.metadata || {},
|
|
167
|
-
}));
|
|
168
|
-
|
|
169
|
-
const retrievalTimeMs = Date.now() - start;
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
structuredContent: { query: input.query, results: mapped, metadata: { collection: collName, model, retrievalTimeMs, resultCount: mapped.length } },
|
|
173
|
-
content: [{ type: 'text', text: `Found ${mapped.length} results for "${input.query}" (${retrievalTimeMs}ms):\n\n${mapped.map((r, i) => `[${i + 1}] ${r.source} (${(r.score || 0).toFixed(3)})\n${r.content.slice(0, 500)}`).join('\n\n')}` }],
|
|
174
|
-
};
|
|
175
|
-
} finally {
|
|
176
|
-
await client.close();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
216
|
+
handleVaiSearch
|
|
179
217
|
);
|
|
180
218
|
|
|
181
|
-
// vai_rerank — standalone reranking
|
|
182
219
|
server.tool(
|
|
183
220
|
'vai_rerank',
|
|
184
221
|
'Rerank documents against a query using Voyage AI reranker. Takes a query and candidate documents, returns them reordered by relevance. Use when you have documents from another source and want to order them by relevance.',
|
|
185
222
|
schemas.rerankSchema,
|
|
186
|
-
|
|
187
|
-
const start = Date.now();
|
|
188
|
-
const result = await apiRequest('/rerank', {
|
|
189
|
-
query: input.query,
|
|
190
|
-
documents: input.documents,
|
|
191
|
-
model: input.model,
|
|
192
|
-
top_k: input.documents.length,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const ranked = (result.data || []).map(item => ({
|
|
196
|
-
index: item.index,
|
|
197
|
-
relevanceScore: item.relevance_score,
|
|
198
|
-
document: input.documents[item.index].slice(0, 200) + (input.documents[item.index].length > 200 ? '...' : ''),
|
|
199
|
-
}));
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
structuredContent: { query: input.query, results: ranked, model: input.model, timeMs: Date.now() - start },
|
|
203
|
-
content: [{ type: 'text', text: `Reranked ${input.documents.length} documents:\n\n${ranked.map((r, i) => `[${i + 1}] Score: ${r.relevanceScore.toFixed(3)} — ${r.document}`).join('\n')}` }],
|
|
204
|
-
};
|
|
205
|
-
}
|
|
223
|
+
handleVaiRerank
|
|
206
224
|
);
|
|
207
225
|
}
|
|
208
226
|
|
|
209
|
-
module.exports = { registerRetrievalTools };
|
|
227
|
+
module.exports = { registerRetrievalTools, handleVaiQuery, handleVaiSearch, handleVaiRerank };
|