voyageai-cli 1.24.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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +2 -0
  3. package/src/commands/about.js +1 -1
  4. package/src/commands/bug.js +1 -1
  5. package/src/commands/chat.js +281 -78
  6. package/src/commands/playground.js +73 -19
  7. package/src/commands/scaffold.js +23 -1
  8. package/src/commands/workflow.js +336 -0
  9. package/src/lib/chat.js +170 -4
  10. package/src/lib/explanations.js +53 -0
  11. package/src/lib/llm.js +304 -2
  12. package/src/lib/mongo.js +6 -6
  13. package/src/lib/prompt.js +60 -1
  14. package/src/lib/scaffold-structure.js +8 -9
  15. package/src/lib/telemetry.js +1 -1
  16. package/src/lib/template-engine.js +240 -0
  17. package/src/lib/templates/nextjs/README.md.tpl +78 -55
  18. package/src/lib/templates/nextjs/favicon.svg.tpl +11 -0
  19. package/src/lib/templates/nextjs/footer.jsx.tpl +49 -0
  20. package/src/lib/templates/nextjs/layout.jsx.tpl +16 -10
  21. package/src/lib/templates/nextjs/lib-mongo.js.tpl +5 -5
  22. package/src/lib/templates/nextjs/lib-voyage.js.tpl +13 -8
  23. package/src/lib/templates/nextjs/navbar.jsx.tpl +98 -0
  24. package/src/lib/templates/nextjs/page-home.jsx.tpl +201 -0
  25. package/src/lib/templates/nextjs/page-search.jsx.tpl +184 -82
  26. package/src/lib/templates/nextjs/theme-registry.jsx.tpl +51 -0
  27. package/src/lib/templates/nextjs/theme.js.tpl +138 -65
  28. package/src/lib/templates/nextjs/vai-logo-256.png +0 -0
  29. package/src/lib/tool-registry.js +194 -0
  30. package/src/lib/workflow-utils.js +65 -0
  31. package/src/lib/workflow.js +1259 -0
  32. package/src/mcp/tools/embedding.js +55 -43
  33. package/src/mcp/tools/ingest.js +74 -67
  34. package/src/mcp/tools/management.js +54 -101
  35. package/src/mcp/tools/retrieval.js +181 -163
  36. package/src/mcp/tools/utility.js +171 -153
  37. package/src/playground/icons/dark/128.png +0 -0
  38. package/src/playground/icons/dark/16.png +0 -0
  39. package/src/playground/icons/dark/256.png +0 -0
  40. package/src/playground/icons/dark/32.png +0 -0
  41. package/src/playground/icons/dark/64.png +0 -0
  42. package/src/playground/icons/light/128.png +0 -0
  43. package/src/playground/icons/light/16.png +0 -0
  44. package/src/playground/icons/light/256.png +0 -0
  45. package/src/playground/icons/light/32.png +0 -0
  46. package/src/playground/icons/light/64.png +0 -0
  47. package/src/playground/icons/watermark.png +0 -0
  48. package/src/playground/index.html +633 -83
  49. package/src/workflows/consistency-check.json +64 -0
  50. package/src/workflows/cost-analysis.json +69 -0
  51. package/src/workflows/multi-collection-search.json +80 -0
  52. package/src/workflows/research-and-summarize.json +46 -0
  53. package/src/workflows/smart-ingest.json +63 -0
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 zero new dependencies.
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 exit with a helpful error.
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
- console.error('Error: MONGODB_URI is not set.');
13
- console.error('');
14
- console.error('Option 1: export MONGODB_URI="mongodb+srv://user:pass@cluster.mongodb.net/"');
15
- console.error('Option 2: vai config set mongodb-uri "mongodb+srv://user:pass@cluster.mongodb.net/"');
16
- process.exit(1);
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
  };
@@ -31,23 +31,22 @@ const PROJECT_STRUCTURE = {
31
31
  { template: 'env.example', output: '.env.example' },
32
32
  { template: 'README.md', output: 'README.md' },
33
33
  { template: 'layout.jsx', output: 'app/layout.jsx' },
34
+ { template: 'page-home.jsx', output: 'app/page.jsx' },
34
35
  { template: 'page-search.jsx', output: 'app/search/page.jsx' },
35
36
  { template: 'route-search.js', output: 'app/api/search/route.js' },
36
37
  { template: 'route-ingest.js', output: 'app/api/ingest/route.js' },
37
38
  { template: 'lib-voyage.js', output: 'lib/voyage.js' },
38
39
  { template: 'lib-mongo.js', output: 'lib/mongodb.js' },
39
40
  { template: 'theme.js', output: 'lib/theme.js' },
41
+ { template: 'theme-registry.jsx', output: 'components/ThemeRegistry.jsx' },
42
+ { template: 'navbar.jsx', output: 'components/Navbar.jsx' },
43
+ { template: 'footer.jsx', output: 'components/Footer.jsx' },
44
+ { template: 'favicon.svg', output: 'public/favicon.svg' },
45
+ ],
46
+ binaryFiles: [
47
+ { source: 'vai-logo-256.png', output: 'public/vai-logo.png' },
40
48
  ],
41
49
  extraFiles: [
42
- {
43
- output: 'app/page.jsx',
44
- content: `'use client';
45
- import { redirect } from 'next/navigation';
46
- export default function Home() {
47
- redirect('/search');
48
- }
49
- `,
50
- },
51
50
  {
52
51
  output: 'next.config.js',
53
52
  content: `/** @type {import('next').NextConfig} */
@@ -9,7 +9,7 @@
9
9
  * - command name, version, platform, locale
10
10
  */
11
11
 
12
- const TELEMETRY_URL = 'https://vai.mlynn.org/api/telemetry';
12
+ const TELEMETRY_URL = 'https://vaicli.com/api/telemetry';
13
13
  const TIMEOUT_MS = 3000;
14
14
 
15
15
  /**