voyageai-cli 1.26.0 → 1.27.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.
@@ -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
- async (input) => {
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
- async (input) => {
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
- async (input) => {
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 };