voyageai-cli 1.22.1 → 1.23.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 +4 -2
- package/src/cli.js +4 -0
- package/src/commands/chat.js +503 -0
- package/src/commands/demo.js +75 -0
- package/src/commands/embed.js +10 -0
- package/src/commands/index.js +1 -1
- package/src/commands/init.js +34 -97
- package/src/commands/mcp-server.js +49 -0
- package/src/commands/ping.js +54 -2
- package/src/commands/pipeline.js +17 -3
- package/src/commands/playground.js +186 -0
- package/src/commands/purge.js +3 -1
- package/src/commands/refresh.js +3 -1
- package/src/commands/rerank.js +10 -0
- package/src/commands/scaffold.js +1 -2
- package/src/lib/api.js +6 -8
- package/src/lib/chat.js +252 -0
- package/src/lib/codegen.js +5 -4
- package/src/lib/config.js +5 -1
- package/src/lib/cost.js +352 -0
- package/src/lib/explanations.js +74 -0
- package/src/lib/history.js +260 -0
- package/src/lib/llm.js +485 -0
- package/src/lib/preflight.js +281 -0
- package/src/lib/prompt.js +111 -0
- package/src/lib/wizard-cli.js +135 -0
- package/src/lib/wizard-steps-chat.js +171 -0
- package/src/lib/wizard-steps-init.js +174 -0
- package/src/lib/wizard.js +222 -0
- package/src/mcp/schemas/index.js +102 -0
- package/src/mcp/server.js +162 -0
- package/src/mcp/tools/embedding.js +67 -0
- package/src/mcp/tools/ingest.js +89 -0
- package/src/mcp/tools/management.js +132 -0
- package/src/mcp/tools/retrieval.js +209 -0
- package/src/mcp/tools/utility.js +219 -0
- package/src/playground/index.html +1195 -199
package/src/lib/explanations.js
CHANGED
|
@@ -1368,6 +1368,72 @@ const concepts = {
|
|
|
1368
1368
|
'vai eval compare --test-set test.jsonl --configs baseline.json,experiment.json',
|
|
1369
1369
|
],
|
|
1370
1370
|
},
|
|
1371
|
+
chat: {
|
|
1372
|
+
title: 'RAG Chat',
|
|
1373
|
+
summary: 'How vai chat works — retrieval-augmented conversational AI',
|
|
1374
|
+
content: [
|
|
1375
|
+
`${pc.cyan('vai chat')} adds a conversational layer on top of your existing RAG pipeline.`,
|
|
1376
|
+
`It connects your embedded documents to an LLM for grounded Q&A with citations.`,
|
|
1377
|
+
``,
|
|
1378
|
+
`${pc.bold('The Two-Stage Pipeline:')}`,
|
|
1379
|
+
``,
|
|
1380
|
+
` ┌─────────────────────────────────────────────────────┐`,
|
|
1381
|
+
` │ ${pc.cyan('STAGE 1: RETRIEVAL')} (Voyage AI + MongoDB) │`,
|
|
1382
|
+
` │ │`,
|
|
1383
|
+
` │ Your question → Voyage AI creates an embedding │`,
|
|
1384
|
+
` │ → MongoDB Atlas finds similar document chunks │`,
|
|
1385
|
+
` │ → Voyage AI reranks for better relevance │`,
|
|
1386
|
+
` │ │`,
|
|
1387
|
+
` │ Output: Top 5 relevant text chunks │`,
|
|
1388
|
+
` ├─────────────────────────────────────────────────────┤`,
|
|
1389
|
+
` │ ${pc.cyan('STAGE 2: GENERATION')} (Your chosen LLM) │`,
|
|
1390
|
+
` │ │`,
|
|
1391
|
+
` │ Those text chunks + your question → sent to LLM │`,
|
|
1392
|
+
` │ → LLM reads the context and writes an answer │`,
|
|
1393
|
+
` │ → Response streamed back with citations │`,
|
|
1394
|
+
` │ │`,
|
|
1395
|
+
` │ Output: Conversational answer │`,
|
|
1396
|
+
` └─────────────────────────────────────────────────────┘`,
|
|
1397
|
+
``,
|
|
1398
|
+
`${pc.bold('Key insight:')} The LLM ${pc.cyan('never sees embedding vectors')}. It receives`,
|
|
1399
|
+
`plain text — the retrieved document chunks — and produces plain text.`,
|
|
1400
|
+
`Voyage AI finds the right documents; the LLM reads them and writes`,
|
|
1401
|
+
`an answer. They are completely independent systems.`,
|
|
1402
|
+
``,
|
|
1403
|
+
`${pc.bold('What goes where:')}`,
|
|
1404
|
+
``,
|
|
1405
|
+
` ${pc.dim('Voyage AI API')} ← your question text, document chunks for reranking`,
|
|
1406
|
+
` ${pc.dim('Your MongoDB')} ← embedded documents, chat history, session data`,
|
|
1407
|
+
` ${pc.dim('LLM Provider')} ← system prompt, retrieved chunks, question, history`,
|
|
1408
|
+
` ${pc.dim(' (Ollama)')} ← fully local, nothing leaves your machine`,
|
|
1409
|
+
``,
|
|
1410
|
+
`${pc.bold('Supported LLM Providers:')}`,
|
|
1411
|
+
``,
|
|
1412
|
+
` ${pc.cyan('anthropic')} Claude (API key required)`,
|
|
1413
|
+
` ${pc.cyan('openai')} GPT-4o and others (API key required)`,
|
|
1414
|
+
` ${pc.cyan('ollama')} Fully local, free (requires Ollama installed)`,
|
|
1415
|
+
``,
|
|
1416
|
+
`${pc.bold('Conversation History:')}`,
|
|
1417
|
+
`Previous turns are included so follow-up questions work naturally.`,
|
|
1418
|
+
`History is stored in your MongoDB (collection: ${pc.dim('vai_chat_history')}).`,
|
|
1419
|
+
`Sessions can be resumed with: ${pc.cyan('vai chat --session <id>')}`,
|
|
1420
|
+
``,
|
|
1421
|
+
`${pc.bold('Slash Commands (inside chat):')}`,
|
|
1422
|
+
` ${pc.cyan('/sources')} Show sources from last response`,
|
|
1423
|
+
` ${pc.cyan('/context')} Show retrieved document chunks`,
|
|
1424
|
+
` ${pc.cyan('/history')} List recent chat sessions`,
|
|
1425
|
+
` ${pc.cyan('/session')} Show current session ID`,
|
|
1426
|
+
` ${pc.cyan('/export')} Export to Markdown or JSON`,
|
|
1427
|
+
` ${pc.cyan('/clear')} Clear conversation`,
|
|
1428
|
+
` ${pc.cyan('/model')} Show or switch LLM model`,
|
|
1429
|
+
].join('\n'),
|
|
1430
|
+
links: ['https://github.com/mrlynn/voyageai-cli#chat'],
|
|
1431
|
+
tryIt: [
|
|
1432
|
+
'vai config set llm-provider anthropic',
|
|
1433
|
+
'vai config set llm-api-key YOUR_KEY',
|
|
1434
|
+
'vai chat --db myapp --collection knowledge',
|
|
1435
|
+
],
|
|
1436
|
+
},
|
|
1371
1437
|
};
|
|
1372
1438
|
|
|
1373
1439
|
/**
|
|
@@ -1524,6 +1590,14 @@ const aliases = {
|
|
|
1524
1590
|
'vs-anthropic': 'provider-comparison',
|
|
1525
1591
|
competitors: 'provider-comparison',
|
|
1526
1592
|
alternatives: 'provider-comparison',
|
|
1593
|
+
// Chat aliases
|
|
1594
|
+
chat: 'chat',
|
|
1595
|
+
'vai-chat': 'chat',
|
|
1596
|
+
'rag-chat': 'chat',
|
|
1597
|
+
conversation: 'chat',
|
|
1598
|
+
conversational: 'chat',
|
|
1599
|
+
'chat-history': 'chat',
|
|
1600
|
+
llm: 'chat',
|
|
1527
1601
|
};
|
|
1528
1602
|
|
|
1529
1603
|
/**
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Chat History Manager
|
|
7
|
+
*
|
|
8
|
+
* Manages conversation sessions with in-memory storage
|
|
9
|
+
* and optional MongoDB persistence.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a new session ID.
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function generateSessionId() {
|
|
17
|
+
return crypto.randomUUID();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* In-memory history store for a single session.
|
|
22
|
+
*/
|
|
23
|
+
class ChatHistory {
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} [opts]
|
|
26
|
+
* @param {string} [opts.sessionId] - Resume an existing session
|
|
27
|
+
* @param {number} [opts.maxTurns] - Max turns to keep (default 20)
|
|
28
|
+
* @param {object} [opts.mongo] - { client, collection } for persistence
|
|
29
|
+
*/
|
|
30
|
+
constructor(opts = {}) {
|
|
31
|
+
this.sessionId = opts.sessionId || generateSessionId();
|
|
32
|
+
this.maxTurns = opts.maxTurns || 20;
|
|
33
|
+
this.turns = []; // Array of { role, content, context?, metadata?, timestamp }
|
|
34
|
+
this._mongo = opts.mongo || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load existing session from MongoDB.
|
|
39
|
+
* @returns {Promise<boolean>} true if session was found and loaded
|
|
40
|
+
*/
|
|
41
|
+
async load() {
|
|
42
|
+
if (!this._mongo) return false;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const docs = await this._mongo.collection
|
|
46
|
+
.find({ sessionId: this.sessionId })
|
|
47
|
+
.sort({ timestamp: 1 })
|
|
48
|
+
.limit(this.maxTurns * 2) // user + assistant turns
|
|
49
|
+
.toArray();
|
|
50
|
+
|
|
51
|
+
if (docs.length === 0) return false;
|
|
52
|
+
|
|
53
|
+
this.turns = docs.map(d => ({
|
|
54
|
+
role: d.role,
|
|
55
|
+
content: d.content,
|
|
56
|
+
context: d.context || undefined,
|
|
57
|
+
metadata: d.metadata || undefined,
|
|
58
|
+
timestamp: d.timestamp,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
// Persistence failure is non-fatal
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Add a turn to history and optionally persist.
|
|
70
|
+
* @param {object} turn - { role, content, context?, metadata? }
|
|
71
|
+
*/
|
|
72
|
+
async addTurn(turn) {
|
|
73
|
+
const entry = {
|
|
74
|
+
...turn,
|
|
75
|
+
timestamp: new Date(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this.turns.push(entry);
|
|
79
|
+
|
|
80
|
+
// Trim to maxTurns (keep pairs)
|
|
81
|
+
const maxEntries = this.maxTurns * 2;
|
|
82
|
+
if (this.turns.length > maxEntries) {
|
|
83
|
+
this.turns = this.turns.slice(-maxEntries);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Persist to MongoDB if available
|
|
87
|
+
if (this._mongo) {
|
|
88
|
+
try {
|
|
89
|
+
await this._mongo.collection.insertOne({
|
|
90
|
+
sessionId: this.sessionId,
|
|
91
|
+
...entry,
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
// Persistence failure is non-fatal — chat continues in-memory
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get conversation history as message array for the LLM.
|
|
101
|
+
* Returns only role + content (no metadata).
|
|
102
|
+
* @returns {Array<{role: string, content: string}>}
|
|
103
|
+
*/
|
|
104
|
+
getMessages() {
|
|
105
|
+
return this.turns.map(t => ({ role: t.role, content: t.content }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the last assistant turn's context docs.
|
|
110
|
+
* @returns {Array|null}
|
|
111
|
+
*/
|
|
112
|
+
getLastContext() {
|
|
113
|
+
for (let i = this.turns.length - 1; i >= 0; i--) {
|
|
114
|
+
if (this.turns[i].role === 'assistant' && this.turns[i].context) {
|
|
115
|
+
return this.turns[i].context;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the last assistant turn's sources formatted for display.
|
|
123
|
+
* @returns {Array<{source: string, score: number}>|null}
|
|
124
|
+
*/
|
|
125
|
+
getLastSources() {
|
|
126
|
+
const ctx = this.getLastContext();
|
|
127
|
+
if (!ctx) return null;
|
|
128
|
+
return ctx.map(d => ({
|
|
129
|
+
source: d.source || d.metadata?.source || 'unknown',
|
|
130
|
+
score: d.score,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Clear conversation history (keep session ID).
|
|
136
|
+
*/
|
|
137
|
+
clear() {
|
|
138
|
+
this.turns = [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get conversation history trimmed to fit a token budget.
|
|
143
|
+
* Uses ~4 chars per token estimate. Prioritizes recent turns.
|
|
144
|
+
* @param {number} [maxTokens=8000] - Token budget for history
|
|
145
|
+
* @returns {Array<{role: string, content: string}>}
|
|
146
|
+
*/
|
|
147
|
+
getMessagesWithBudget(maxTokens = 8000) {
|
|
148
|
+
const messages = this.getMessages();
|
|
149
|
+
if (messages.length === 0) return [];
|
|
150
|
+
|
|
151
|
+
let totalChars = 0;
|
|
152
|
+
const maxChars = maxTokens * 4;
|
|
153
|
+
const result = [];
|
|
154
|
+
|
|
155
|
+
// Work backwards from most recent
|
|
156
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
157
|
+
const charCount = messages[i].content.length;
|
|
158
|
+
if (totalChars + charCount > maxChars && result.length > 0) break;
|
|
159
|
+
result.unshift(messages[i]);
|
|
160
|
+
totalChars += charCount;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Export conversation to markdown.
|
|
168
|
+
* @returns {string}
|
|
169
|
+
*/
|
|
170
|
+
exportMarkdown() {
|
|
171
|
+
const lines = [
|
|
172
|
+
`# Chat Session: ${this.sessionId}`,
|
|
173
|
+
`_Exported: ${new Date().toISOString()}_`,
|
|
174
|
+
'',
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
for (const turn of this.turns) {
|
|
178
|
+
if (turn.role === 'user') {
|
|
179
|
+
lines.push(`**You:** ${turn.content}`);
|
|
180
|
+
} else if (turn.role === 'assistant') {
|
|
181
|
+
lines.push(`**Assistant:** ${turn.content}`);
|
|
182
|
+
if (turn.context && turn.context.length > 0) {
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push('Sources:');
|
|
185
|
+
for (const doc of turn.context) {
|
|
186
|
+
const src = doc.source || doc.metadata?.source || 'unknown';
|
|
187
|
+
lines.push(`- ${src} (${doc.score?.toFixed(2) || 'N/A'})`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
lines.push('');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return lines.join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Export conversation to JSON.
|
|
199
|
+
* @returns {object}
|
|
200
|
+
*/
|
|
201
|
+
exportJSON() {
|
|
202
|
+
return {
|
|
203
|
+
sessionId: this.sessionId,
|
|
204
|
+
exportedAt: new Date().toISOString(),
|
|
205
|
+
turns: this.turns,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Ensure MongoDB indexes exist for chat history.
|
|
211
|
+
* Called once on first persist.
|
|
212
|
+
* @param {import('mongodb').Collection} collection
|
|
213
|
+
*/
|
|
214
|
+
static async ensureIndexes(collection) {
|
|
215
|
+
try {
|
|
216
|
+
await collection.createIndex(
|
|
217
|
+
{ sessionId: 1, timestamp: 1 },
|
|
218
|
+
{ background: true }
|
|
219
|
+
);
|
|
220
|
+
} catch {
|
|
221
|
+
// Index creation failure is non-fatal
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* List recent chat sessions from MongoDB.
|
|
228
|
+
* @param {import('mongodb').Collection} collection
|
|
229
|
+
* @param {number} [limit=10]
|
|
230
|
+
* @returns {Promise<Array<{sessionId: string, firstMessage: string, lastActivity: Date, turnCount: number}>>}
|
|
231
|
+
*/
|
|
232
|
+
async function listSessions(collection, limit = 10) {
|
|
233
|
+
const pipeline = [
|
|
234
|
+
{
|
|
235
|
+
$group: {
|
|
236
|
+
_id: '$sessionId',
|
|
237
|
+
firstMessage: { $first: '$content' },
|
|
238
|
+
firstRole: { $first: '$role' },
|
|
239
|
+
lastActivity: { $max: '$timestamp' },
|
|
240
|
+
turnCount: { $sum: 1 },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{ $sort: { lastActivity: -1 } },
|
|
244
|
+
{ $limit: limit },
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const sessions = await collection.aggregate(pipeline).toArray();
|
|
248
|
+
return sessions.map(s => ({
|
|
249
|
+
sessionId: s._id,
|
|
250
|
+
firstMessage: s.firstRole === 'user' ? s.firstMessage : '(continued)',
|
|
251
|
+
lastActivity: s.lastActivity,
|
|
252
|
+
turnCount: s.turnCount,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
generateSessionId,
|
|
258
|
+
ChatHistory,
|
|
259
|
+
listSessions,
|
|
260
|
+
};
|