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.
- package/package.json +1 -1
- package/src/commands/chat.js +281 -78
- package/src/commands/doctor.js +157 -14
- package/src/commands/playground.js +233 -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/icons/dark/128.png +0 -0
- package/src/playground/icons/dark/16.png +0 -0
- package/src/playground/icons/dark/256.png +0 -0
- package/src/playground/icons/dark/32.png +0 -0
- package/src/playground/icons/dark/64.png +0 -0
- package/src/playground/icons/light/128.png +0 -0
- package/src/playground/icons/light/16.png +0 -0
- package/src/playground/icons/light/256.png +0 -0
- package/src/playground/icons/light/32.png +0 -0
- package/src/playground/icons/light/64.png +0 -0
- package/src/playground/index.html +2769 -27
package/src/lib/chat.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Chat Orchestrator
|
|
5
5
|
*
|
|
6
|
-
* Coordinates the retrieval pipeline (embed
|
|
6
|
+
* Coordinates the retrieval pipeline (embed -> search -> rerank)
|
|
7
7
|
* with LLM generation and history management.
|
|
8
|
+
* Supports both pipeline mode (fixed RAG) and agent mode (tool-calling).
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const { generateEmbeddings, apiRequest } = require('./api');
|
|
@@ -32,12 +33,12 @@ function resolveSourceLabel(doc) {
|
|
|
32
33
|
return doc.source || meta.source || doc._id?.toString() || 'unknown';
|
|
33
34
|
}
|
|
34
35
|
const { getMongoCollection } = require('./mongo');
|
|
35
|
-
const { buildMessages } = require('./prompt');
|
|
36
|
+
const { buildMessages, buildAgentMessages } = require('./prompt');
|
|
36
37
|
const { getDefaultModel, DEFAULT_RERANK_MODEL } = require('./catalog');
|
|
37
38
|
const { loadProject } = require('./project');
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
|
-
* Perform retrieval: embed query
|
|
41
|
+
* Perform retrieval: embed query -> vector search -> optional rerank.
|
|
41
42
|
*
|
|
42
43
|
* @param {object} params
|
|
43
44
|
* @param {string} params.query - User's question
|
|
@@ -154,7 +155,7 @@ async function retrieve({ query, db, collection, opts = {} }) {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
/**
|
|
157
|
-
* Execute a single chat turn: retrieve context
|
|
158
|
+
* Execute a single chat turn: retrieve context -> build prompt -> generate response.
|
|
158
159
|
*
|
|
159
160
|
* @param {object} params
|
|
160
161
|
* @param {string} params.query - User's question
|
|
@@ -246,7 +247,172 @@ async function* chatTurn({ query, db, collection, llm, history, opts = {} }) {
|
|
|
246
247
|
};
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Execute a single agent chat turn: LLM decides which tools to call.
|
|
252
|
+
*
|
|
253
|
+
* @param {object} params
|
|
254
|
+
* @param {string} params.query - User's question
|
|
255
|
+
* @param {object} params.llm - LLM provider instance (must have chatWithTools)
|
|
256
|
+
* @param {import('./history').ChatHistory} params.history - Chat history
|
|
257
|
+
* @param {object} [params.opts] - Additional options
|
|
258
|
+
* @param {string} [params.opts.systemPrompt] - Override agent system prompt
|
|
259
|
+
* @param {number} [params.opts.maxIterations] - Max tool-calling iterations (default 10)
|
|
260
|
+
* @param {string} [params.opts.db] - Default database for tool calls
|
|
261
|
+
* @param {string} [params.opts.collection] - Default collection for tool calls
|
|
262
|
+
* @returns {AsyncGenerator<{type: string, data: any}>}
|
|
263
|
+
* Yields: { type: 'tool_call', data: { name, args, result, error, timeMs } }
|
|
264
|
+
* { type: 'chunk', data: string }
|
|
265
|
+
* { type: 'done', data: { fullResponse, toolCalls, metadata } }
|
|
266
|
+
*/
|
|
267
|
+
async function* agentChatTurn({ query, llm, history, opts = {} }) {
|
|
268
|
+
const { getToolDefinitions, executeTool } = require('./tool-registry');
|
|
269
|
+
|
|
270
|
+
const maxIterations = opts.maxIterations || 10;
|
|
271
|
+
const start = Date.now();
|
|
272
|
+
|
|
273
|
+
// 1. Build initial messages
|
|
274
|
+
const initialMessages = buildAgentMessages({
|
|
275
|
+
query,
|
|
276
|
+
history: history.getMessagesWithBudget(8000),
|
|
277
|
+
systemPrompt: opts.systemPrompt,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// 2. Get tool definitions for this provider
|
|
281
|
+
const format = llm.name === 'anthropic' ? 'anthropic' : 'openai';
|
|
282
|
+
const tools = getToolDefinitions(format);
|
|
283
|
+
|
|
284
|
+
// Track messages for the tool-calling loop (mutable copy)
|
|
285
|
+
const messages = [...initialMessages];
|
|
286
|
+
const toolCallLog = [];
|
|
287
|
+
|
|
288
|
+
// 3. Agent loop
|
|
289
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
290
|
+
const response = await llm.chatWithTools(messages, tools);
|
|
291
|
+
|
|
292
|
+
// Text response: done
|
|
293
|
+
if (response.type === 'text') {
|
|
294
|
+
const fullResponse = response.content;
|
|
295
|
+
yield { type: 'chunk', data: fullResponse };
|
|
296
|
+
|
|
297
|
+
const totalTimeMs = Date.now() - start;
|
|
298
|
+
|
|
299
|
+
// Store turns in history
|
|
300
|
+
await history.addTurn({ role: 'user', content: query });
|
|
301
|
+
await history.addTurn({
|
|
302
|
+
role: 'assistant',
|
|
303
|
+
content: fullResponse,
|
|
304
|
+
metadata: {
|
|
305
|
+
mode: 'agent',
|
|
306
|
+
llmProvider: llm.name,
|
|
307
|
+
llmModel: llm.model,
|
|
308
|
+
toolCallCount: toolCallLog.length,
|
|
309
|
+
iterationCount: iteration + 1,
|
|
310
|
+
totalTimeMs,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
yield {
|
|
315
|
+
type: 'done',
|
|
316
|
+
data: {
|
|
317
|
+
fullResponse,
|
|
318
|
+
toolCalls: toolCallLog,
|
|
319
|
+
metadata: {
|
|
320
|
+
mode: 'agent',
|
|
321
|
+
iterationCount: iteration + 1,
|
|
322
|
+
toolCallCount: toolCallLog.length,
|
|
323
|
+
totalTimeMs,
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Tool calls: execute each and continue loop
|
|
331
|
+
if (response.type === 'tool_calls') {
|
|
332
|
+
// Append assistant tool-call message
|
|
333
|
+
messages.push(llm.formatAssistantToolCall(response));
|
|
334
|
+
|
|
335
|
+
for (const call of response.calls) {
|
|
336
|
+
const callStart = Date.now();
|
|
337
|
+
let result;
|
|
338
|
+
let error = null;
|
|
339
|
+
|
|
340
|
+
// Inject default db/collection if not provided
|
|
341
|
+
const args = { ...call.arguments };
|
|
342
|
+
if (opts.db && !args.db) args.db = opts.db;
|
|
343
|
+
if (opts.collection && !args.collection) args.collection = opts.collection;
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
result = await executeTool(call.name, args);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
error = err.message;
|
|
349
|
+
result = { content: [{ type: 'text', text: `Error: ${err.message}` }] };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const callTimeMs = Date.now() - callStart;
|
|
353
|
+
|
|
354
|
+
// Extract text content from result for the LLM
|
|
355
|
+
const resultText = result.content
|
|
356
|
+
? result.content.map(c => c.text || JSON.stringify(c)).join('\n')
|
|
357
|
+
: JSON.stringify(result.structuredContent || {});
|
|
358
|
+
|
|
359
|
+
// Append tool result message
|
|
360
|
+
messages.push(llm.formatToolResult(call.id, resultText, !!error));
|
|
361
|
+
|
|
362
|
+
const logEntry = {
|
|
363
|
+
name: call.name,
|
|
364
|
+
args,
|
|
365
|
+
result: result.structuredContent || null,
|
|
366
|
+
error,
|
|
367
|
+
timeMs: callTimeMs,
|
|
368
|
+
};
|
|
369
|
+
toolCallLog.push(logEntry);
|
|
370
|
+
|
|
371
|
+
yield { type: 'tool_call', data: logEntry };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Continue loop to let LLM see results and decide next action
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Max iterations reached: yield a fallback message
|
|
380
|
+
const fallback = 'I reached the maximum number of tool-calling iterations. Here is what I found so far based on the tool results above.';
|
|
381
|
+
yield { type: 'chunk', data: fallback };
|
|
382
|
+
|
|
383
|
+
await history.addTurn({ role: 'user', content: query });
|
|
384
|
+
await history.addTurn({
|
|
385
|
+
role: 'assistant',
|
|
386
|
+
content: fallback,
|
|
387
|
+
metadata: {
|
|
388
|
+
mode: 'agent',
|
|
389
|
+
llmProvider: llm.name,
|
|
390
|
+
llmModel: llm.model,
|
|
391
|
+
toolCallCount: toolCallLog.length,
|
|
392
|
+
iterationCount: maxIterations,
|
|
393
|
+
totalTimeMs: Date.now() - start,
|
|
394
|
+
maxIterationsReached: true,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
yield {
|
|
399
|
+
type: 'done',
|
|
400
|
+
data: {
|
|
401
|
+
fullResponse: fallback,
|
|
402
|
+
toolCalls: toolCallLog,
|
|
403
|
+
metadata: {
|
|
404
|
+
mode: 'agent',
|
|
405
|
+
iterationCount: maxIterations,
|
|
406
|
+
toolCallCount: toolCallLog.length,
|
|
407
|
+
totalTimeMs: Date.now() - start,
|
|
408
|
+
maxIterationsReached: true,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
249
414
|
module.exports = {
|
|
250
415
|
retrieve,
|
|
251
416
|
chatTurn,
|
|
417
|
+
agentChatTurn,
|
|
252
418
|
};
|
package/src/lib/llm.js
CHANGED
|
@@ -6,8 +6,8 @@ const { loadProject } = require('./project');
|
|
|
6
6
|
/**
|
|
7
7
|
* LLM Provider Adapter
|
|
8
8
|
*
|
|
9
|
-
* Provider-agnostic LLM client with streaming support.
|
|
10
|
-
* Uses native fetch
|
|
9
|
+
* Provider-agnostic LLM client with streaming and tool-calling support.
|
|
10
|
+
* Uses native fetch, zero new dependencies.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// Provider default models
|
|
@@ -107,6 +107,8 @@ class AnthropicProvider {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
get supportsTools() { return true; }
|
|
111
|
+
|
|
110
112
|
async *chat(messages, options = {}) {
|
|
111
113
|
const model = options.model || this.model;
|
|
112
114
|
const maxTokens = options.maxTokens || 4096;
|
|
@@ -156,6 +158,112 @@ class AnthropicProvider {
|
|
|
156
158
|
});
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Non-streaming tool-calling request.
|
|
163
|
+
* @param {Array} messages - Conversation messages
|
|
164
|
+
* @param {Array} tools - Tool definitions in Anthropic format
|
|
165
|
+
* @param {object} [options]
|
|
166
|
+
* @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string}>}
|
|
167
|
+
*/
|
|
168
|
+
async chatWithTools(messages, tools, options = {}) {
|
|
169
|
+
const model = options.model || this.model;
|
|
170
|
+
const maxTokens = options.maxTokens || 4096;
|
|
171
|
+
|
|
172
|
+
const systemMsg = messages.find(m => m.role === 'system');
|
|
173
|
+
const nonSystemMsgs = messages.filter(m => m.role !== 'system');
|
|
174
|
+
|
|
175
|
+
const body = {
|
|
176
|
+
model,
|
|
177
|
+
max_tokens: maxTokens,
|
|
178
|
+
stream: false,
|
|
179
|
+
messages: nonSystemMsgs,
|
|
180
|
+
tools,
|
|
181
|
+
};
|
|
182
|
+
if (systemMsg) {
|
|
183
|
+
body.system = systemMsg.content;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const res = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: {
|
|
189
|
+
'Content-Type': 'application/json',
|
|
190
|
+
'x-api-key': this.apiKey,
|
|
191
|
+
'anthropic-version': '2023-06-01',
|
|
192
|
+
},
|
|
193
|
+
body: JSON.stringify(body),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
const errBody = await res.text();
|
|
198
|
+
throw new Error(`Anthropic API error (${res.status}): ${errBody}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const json = await res.json();
|
|
202
|
+
const stopReason = json.stop_reason || 'end_turn';
|
|
203
|
+
|
|
204
|
+
// Check for tool_use blocks
|
|
205
|
+
const toolBlocks = (json.content || []).filter(b => b.type === 'tool_use');
|
|
206
|
+
if (toolBlocks.length > 0) {
|
|
207
|
+
return {
|
|
208
|
+
type: 'tool_calls',
|
|
209
|
+
calls: toolBlocks.map(b => ({
|
|
210
|
+
id: b.id,
|
|
211
|
+
name: b.name,
|
|
212
|
+
arguments: b.input,
|
|
213
|
+
})),
|
|
214
|
+
stopReason,
|
|
215
|
+
_raw: json.content,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Text response
|
|
220
|
+
const textBlocks = (json.content || []).filter(b => b.type === 'text');
|
|
221
|
+
return {
|
|
222
|
+
type: 'text',
|
|
223
|
+
content: textBlocks.map(b => b.text).join(''),
|
|
224
|
+
stopReason,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Format a tool-calling response as an assistant message.
|
|
230
|
+
* @param {object} response - Response from chatWithTools
|
|
231
|
+
* @returns {{role: string, content: Array}}
|
|
232
|
+
*/
|
|
233
|
+
formatAssistantToolCall(response) {
|
|
234
|
+
if (response._raw) {
|
|
235
|
+
return { role: 'assistant', content: response._raw };
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
role: 'assistant',
|
|
239
|
+
content: response.calls.map(c => ({
|
|
240
|
+
type: 'tool_use',
|
|
241
|
+
id: c.id,
|
|
242
|
+
name: c.name,
|
|
243
|
+
input: c.arguments,
|
|
244
|
+
})),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Format a tool result as a user message.
|
|
250
|
+
* @param {string} callId - Tool call ID
|
|
251
|
+
* @param {string} content - Stringified result
|
|
252
|
+
* @param {boolean} [isError=false]
|
|
253
|
+
* @returns {{role: string, content: Array}}
|
|
254
|
+
*/
|
|
255
|
+
formatToolResult(callId, content, isError = false) {
|
|
256
|
+
return {
|
|
257
|
+
role: 'user',
|
|
258
|
+
content: [{
|
|
259
|
+
type: 'tool_result',
|
|
260
|
+
tool_use_id: callId,
|
|
261
|
+
content,
|
|
262
|
+
...(isError && { is_error: true }),
|
|
263
|
+
}],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
159
267
|
async ping() {
|
|
160
268
|
try {
|
|
161
269
|
const res = await fetch(`${this.baseUrl}/v1/messages`, {
|
|
@@ -202,6 +310,8 @@ class OpenAIProvider {
|
|
|
202
310
|
}
|
|
203
311
|
}
|
|
204
312
|
|
|
313
|
+
get supportsTools() { return true; }
|
|
314
|
+
|
|
205
315
|
async *chat(messages, options = {}) {
|
|
206
316
|
const model = options.model || this.model;
|
|
207
317
|
const maxTokens = options.maxTokens || 4096;
|
|
@@ -242,6 +352,103 @@ class OpenAIProvider {
|
|
|
242
352
|
});
|
|
243
353
|
}
|
|
244
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Non-streaming tool-calling request (OpenAI format).
|
|
357
|
+
* @param {Array} messages - Conversation messages
|
|
358
|
+
* @param {Array} tools - Tool definitions in OpenAI format
|
|
359
|
+
* @param {object} [options]
|
|
360
|
+
* @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string}>}
|
|
361
|
+
*/
|
|
362
|
+
async chatWithTools(messages, tools, options = {}) {
|
|
363
|
+
const model = options.model || this.model;
|
|
364
|
+
const maxTokens = options.maxTokens || 4096;
|
|
365
|
+
|
|
366
|
+
const body = {
|
|
367
|
+
model,
|
|
368
|
+
max_tokens: maxTokens,
|
|
369
|
+
stream: false,
|
|
370
|
+
messages,
|
|
371
|
+
tools,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
headers: {
|
|
377
|
+
'Content-Type': 'application/json',
|
|
378
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify(body),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
if (!res.ok) {
|
|
384
|
+
const errBody = await res.text();
|
|
385
|
+
throw new Error(`OpenAI API error (${res.status}): ${errBody}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const json = await res.json();
|
|
389
|
+
const choice = json.choices?.[0] || {};
|
|
390
|
+
const msg = choice.message || {};
|
|
391
|
+
const stopReason = choice.finish_reason || 'stop';
|
|
392
|
+
|
|
393
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
394
|
+
return {
|
|
395
|
+
type: 'tool_calls',
|
|
396
|
+
calls: msg.tool_calls.map(tc => ({
|
|
397
|
+
id: tc.id,
|
|
398
|
+
name: tc.function.name,
|
|
399
|
+
arguments: typeof tc.function.arguments === 'string'
|
|
400
|
+
? JSON.parse(tc.function.arguments)
|
|
401
|
+
: tc.function.arguments,
|
|
402
|
+
})),
|
|
403
|
+
stopReason,
|
|
404
|
+
_raw: msg,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
type: 'text',
|
|
410
|
+
content: msg.content || '',
|
|
411
|
+
stopReason,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Format a tool-calling response as an assistant message.
|
|
417
|
+
* @param {object} response - Response from chatWithTools
|
|
418
|
+
* @returns {{role: string, content: string|null, tool_calls: Array}}
|
|
419
|
+
*/
|
|
420
|
+
formatAssistantToolCall(response) {
|
|
421
|
+
if (response._raw) {
|
|
422
|
+
return response._raw;
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
role: 'assistant',
|
|
426
|
+
content: null,
|
|
427
|
+
tool_calls: response.calls.map(c => ({
|
|
428
|
+
id: c.id,
|
|
429
|
+
type: 'function',
|
|
430
|
+
function: {
|
|
431
|
+
name: c.name,
|
|
432
|
+
arguments: JSON.stringify(c.arguments),
|
|
433
|
+
},
|
|
434
|
+
})),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Format a tool result as a tool message.
|
|
440
|
+
* @param {string} callId - Tool call ID
|
|
441
|
+
* @param {string} content - Stringified result
|
|
442
|
+
* @returns {{role: string, tool_call_id: string, content: string}}
|
|
443
|
+
*/
|
|
444
|
+
formatToolResult(callId, content) {
|
|
445
|
+
return {
|
|
446
|
+
role: 'tool',
|
|
447
|
+
tool_call_id: callId,
|
|
448
|
+
content,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
245
452
|
async ping() {
|
|
246
453
|
try {
|
|
247
454
|
const res = await fetch(`${this.baseUrl}/v1/models`, {
|
|
@@ -268,6 +475,8 @@ class OllamaProvider {
|
|
|
268
475
|
this.baseUrl = config.baseUrl || PROVIDER_BASE_URLS.ollama;
|
|
269
476
|
}
|
|
270
477
|
|
|
478
|
+
get supportsTools() { return true; }
|
|
479
|
+
|
|
271
480
|
async *chat(messages, options = {}) {
|
|
272
481
|
const model = options.model || this.model;
|
|
273
482
|
const stream = options.stream !== false;
|
|
@@ -303,6 +512,99 @@ class OllamaProvider {
|
|
|
303
512
|
});
|
|
304
513
|
}
|
|
305
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Non-streaming tool-calling request (OpenAI-compatible format).
|
|
517
|
+
* @param {Array} messages - Conversation messages
|
|
518
|
+
* @param {Array} tools - Tool definitions in OpenAI format
|
|
519
|
+
* @param {object} [options]
|
|
520
|
+
* @returns {Promise<{type: 'text'|'tool_calls', content?: string, calls?: Array, stopReason: string}>}
|
|
521
|
+
*/
|
|
522
|
+
async chatWithTools(messages, tools, options = {}) {
|
|
523
|
+
const model = options.model || this.model;
|
|
524
|
+
|
|
525
|
+
const body = {
|
|
526
|
+
model,
|
|
527
|
+
stream: false,
|
|
528
|
+
messages,
|
|
529
|
+
tools,
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
533
|
+
method: 'POST',
|
|
534
|
+
headers: { 'Content-Type': 'application/json' },
|
|
535
|
+
body: JSON.stringify(body),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (!res.ok) {
|
|
539
|
+
const errBody = await res.text();
|
|
540
|
+
throw new Error(`Ollama API error (${res.status}): ${errBody}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const json = await res.json();
|
|
544
|
+
const choice = json.choices?.[0] || {};
|
|
545
|
+
const msg = choice.message || {};
|
|
546
|
+
const stopReason = choice.finish_reason || 'stop';
|
|
547
|
+
|
|
548
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
549
|
+
return {
|
|
550
|
+
type: 'tool_calls',
|
|
551
|
+
calls: msg.tool_calls.map(tc => ({
|
|
552
|
+
id: tc.id || `call_${Date.now()}`,
|
|
553
|
+
name: tc.function.name,
|
|
554
|
+
arguments: typeof tc.function.arguments === 'string'
|
|
555
|
+
? JSON.parse(tc.function.arguments)
|
|
556
|
+
: tc.function.arguments,
|
|
557
|
+
})),
|
|
558
|
+
stopReason,
|
|
559
|
+
_raw: msg,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return {
|
|
564
|
+
type: 'text',
|
|
565
|
+
content: msg.content || '',
|
|
566
|
+
stopReason,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Format a tool-calling response as an assistant message.
|
|
572
|
+
* (Same as OpenAI format since Ollama uses OpenAI-compatible API)
|
|
573
|
+
* @param {object} response - Response from chatWithTools
|
|
574
|
+
* @returns {{role: string, content: string|null, tool_calls: Array}}
|
|
575
|
+
*/
|
|
576
|
+
formatAssistantToolCall(response) {
|
|
577
|
+
if (response._raw) {
|
|
578
|
+
return response._raw;
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
role: 'assistant',
|
|
582
|
+
content: null,
|
|
583
|
+
tool_calls: response.calls.map(c => ({
|
|
584
|
+
id: c.id,
|
|
585
|
+
type: 'function',
|
|
586
|
+
function: {
|
|
587
|
+
name: c.name,
|
|
588
|
+
arguments: JSON.stringify(c.arguments),
|
|
589
|
+
},
|
|
590
|
+
})),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Format a tool result as a tool message.
|
|
596
|
+
* @param {string} callId - Tool call ID
|
|
597
|
+
* @param {string} content - Stringified result
|
|
598
|
+
* @returns {{role: string, tool_call_id: string, content: string}}
|
|
599
|
+
*/
|
|
600
|
+
formatToolResult(callId, content) {
|
|
601
|
+
return {
|
|
602
|
+
role: 'tool',
|
|
603
|
+
tool_call_id: callId,
|
|
604
|
+
content,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
306
608
|
async ping() {
|
|
307
609
|
try {
|
|
308
610
|
const res = await fetch(`${this.baseUrl}/v1/models`);
|
package/src/lib/mongo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Get MongoDB URI or
|
|
4
|
+
* Get MongoDB URI or throw with a helpful error.
|
|
5
5
|
* Checks: env var → config file.
|
|
6
6
|
* @returns {string}
|
|
7
7
|
*/
|
|
@@ -9,11 +9,11 @@ function requireMongoUri() {
|
|
|
9
9
|
const { getConfigValue } = require('./config');
|
|
10
10
|
const uri = process.env.MONGODB_URI || getConfigValue('mongodbUri');
|
|
11
11
|
if (!uri) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
throw new Error(
|
|
13
|
+
'MONGODB_URI is not set.\n' +
|
|
14
|
+
'Option 1: export MONGODB_URI="mongodb+srv://user:pass@cluster.mongodb.net/"\n' +
|
|
15
|
+
'Option 2: vai config set mongodb-uri "mongodb+srv://user:pass@cluster.mongodb.net/"'
|
|
16
|
+
);
|
|
17
17
|
}
|
|
18
18
|
return uri;
|
|
19
19
|
}
|
package/src/lib/prompt.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Constructs the message array sent to the LLM from
|
|
7
7
|
* retrieved documents, conversation history, and user query.
|
|
8
|
+
* Supports both pipeline mode (fixed RAG) and agent mode (tool-calling).
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const DEFAULT_SYSTEM_PROMPT = `You are an assistant powered by a retrieval-augmented generation (RAG) pipeline built with Voyage AI embeddings and MongoDB Atlas Vector Search. Your answers are grounded in documents retrieved from the user's knowledge base.
|
|
@@ -23,6 +24,32 @@ const DEFAULT_SYSTEM_PROMPT = `You are an assistant powered by a retrieval-augme
|
|
|
23
24
|
4. Be concise. Prefer short, direct answers. Use lists or structure when it aids clarity.
|
|
24
25
|
5. For follow-up questions, rely on the newly retrieved context for that turn. Prior context may be stale.`;
|
|
25
26
|
|
|
27
|
+
const AGENT_SYSTEM_PROMPT = `You are an AI assistant with access to a suite of Voyage AI and MongoDB Atlas tools. You can search knowledge bases, embed text, compare documents, explore collections, and more. Use your tools to answer the user's questions accurately.
|
|
28
|
+
|
|
29
|
+
## Available tools
|
|
30
|
+
|
|
31
|
+
- **vai_query**: Full RAG pipeline (embed, vector search, rerank). Use this as your primary tool for answering questions from the knowledge base.
|
|
32
|
+
- **vai_search**: Raw vector search without reranking. Faster, useful for exploratory queries.
|
|
33
|
+
- **vai_rerank**: Rerank candidate documents against a query. Use when you have documents from another source.
|
|
34
|
+
- **vai_embed**: Get the raw embedding vector for a text. Use for debugging or custom logic.
|
|
35
|
+
- **vai_similarity**: Compare two texts semantically. Returns a cosine similarity score.
|
|
36
|
+
- **vai_collections**: List available collections with document counts and vector index info. Call this first if you need to discover which knowledge bases exist.
|
|
37
|
+
- **vai_models**: List available Voyage AI models with pricing. Use when the user asks about model options.
|
|
38
|
+
- **vai_topics**: List educational topics that vai can explain.
|
|
39
|
+
- **vai_explain**: Get a detailed explanation of a topic (embeddings, RAG, vector search, etc).
|
|
40
|
+
- **vai_estimate**: Estimate costs for embedding and query operations.
|
|
41
|
+
- **vai_ingest**: Add new content to a collection (chunk, embed, store).
|
|
42
|
+
|
|
43
|
+
## Answering rules
|
|
44
|
+
|
|
45
|
+
1. Always use tools to retrieve information before answering. Do not guess or make up facts.
|
|
46
|
+
2. Cite sources from tool results using [Source: <label>] format.
|
|
47
|
+
3. You may call multiple tools in sequence. For example: vai_collections to discover collections, then vai_query to search one.
|
|
48
|
+
4. If a tool returns no results or errors, explain what happened and suggest alternatives.
|
|
49
|
+
5. Be concise. Prefer short, direct answers. Use lists or structure when it aids clarity.
|
|
50
|
+
6. For questions about Voyage AI concepts, use vai_explain rather than answering from memory.
|
|
51
|
+
7. If the user asks you to ingest content, use vai_ingest. Confirm what was stored.`;
|
|
52
|
+
|
|
26
53
|
/**
|
|
27
54
|
* Format retrieved documents into a context block.
|
|
28
55
|
* @param {Array<{source: string, text: string, score: number}>} docs
|
|
@@ -66,7 +93,7 @@ ${customPrompt}`;
|
|
|
66
93
|
}
|
|
67
94
|
|
|
68
95
|
/**
|
|
69
|
-
* Build the message array for the LLM.
|
|
96
|
+
* Build the message array for the LLM (pipeline mode).
|
|
70
97
|
*
|
|
71
98
|
* @param {object} params
|
|
72
99
|
* @param {string} params.query - Current user question
|
|
@@ -103,9 +130,41 @@ function buildMessages({ query, contextDocs = [], history = [], systemPrompt })
|
|
|
103
130
|
return messages;
|
|
104
131
|
}
|
|
105
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Build the message array for agent mode (no context injection).
|
|
135
|
+
* The agent fetches its own context via tool calls.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} params
|
|
138
|
+
* @param {string} params.query - Current user question
|
|
139
|
+
* @param {Array} [params.history] - Previous conversation turns [{role, content}]
|
|
140
|
+
* @param {string} [params.systemPrompt] - Override the agent system prompt
|
|
141
|
+
* @returns {Array<{role: string, content: string}>}
|
|
142
|
+
*/
|
|
143
|
+
function buildAgentMessages({ query, history = [], systemPrompt }) {
|
|
144
|
+
const messages = [];
|
|
145
|
+
|
|
146
|
+
// 1. Agent system prompt
|
|
147
|
+
messages.push({
|
|
148
|
+
role: 'system',
|
|
149
|
+
content: systemPrompt || AGENT_SYSTEM_PROMPT,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 2. Conversation history
|
|
153
|
+
for (const turn of history) {
|
|
154
|
+
messages.push({ role: turn.role, content: turn.content });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. Current user message (no context injection, agent decides what to fetch)
|
|
158
|
+
messages.push({ role: 'user', content: query });
|
|
159
|
+
|
|
160
|
+
return messages;
|
|
161
|
+
}
|
|
162
|
+
|
|
106
163
|
module.exports = {
|
|
107
164
|
DEFAULT_SYSTEM_PROMPT,
|
|
165
|
+
AGENT_SYSTEM_PROMPT,
|
|
108
166
|
buildSystemPrompt,
|
|
109
167
|
formatContextBlock,
|
|
110
168
|
buildMessages,
|
|
169
|
+
buildAgentMessages,
|
|
111
170
|
};
|