voyageai-cli 1.23.1 → 1.26.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/README.md +64 -0
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +1 -1
- package/src/commands/bug.js +1 -1
- package/src/commands/mcp-server.js +74 -0
- package/src/commands/playground.js +31 -0
- package/src/commands/scaffold.js +23 -1
- package/src/commands/workflow.js +336 -0
- package/src/lib/explanations.js +53 -0
- package/src/lib/scaffold-structure.js +8 -9
- package/src/lib/telemetry.js +1 -1
- package/src/lib/template-engine.js +240 -0
- package/src/lib/templates/nextjs/README.md.tpl +78 -55
- package/src/lib/templates/nextjs/favicon.svg.tpl +11 -0
- package/src/lib/templates/nextjs/footer.jsx.tpl +49 -0
- package/src/lib/templates/nextjs/layout.jsx.tpl +16 -10
- package/src/lib/templates/nextjs/lib-mongo.js.tpl +5 -5
- package/src/lib/templates/nextjs/lib-voyage.js.tpl +13 -8
- package/src/lib/templates/nextjs/navbar.jsx.tpl +98 -0
- package/src/lib/templates/nextjs/page-home.jsx.tpl +201 -0
- package/src/lib/templates/nextjs/page-search.jsx.tpl +184 -82
- package/src/lib/templates/nextjs/theme-registry.jsx.tpl +51 -0
- package/src/lib/templates/nextjs/theme.js.tpl +138 -65
- package/src/lib/templates/nextjs/vai-logo-256.png +0 -0
- package/src/lib/workflow-utils.js +65 -0
- package/src/lib/workflow.js +1259 -0
- package/src/mcp/install.js +201 -0
- package/src/mcp/tools/management.js +1 -60
- 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/icons/watermark.png +0 -0
- package/src/playground/index.html +125 -73
- package/src/workflows/consistency-check.json +64 -0
- package/src/workflows/cost-analysis.json +69 -0
- package/src/workflows/multi-collection-search.json +80 -0
- package/src/workflows/research-and-summarize.json +46 -0
- package/src/workflows/smart-ingest.json +63 -0
package/src/lib/explanations.js
CHANGED
|
@@ -1434,6 +1434,50 @@ const concepts = {
|
|
|
1434
1434
|
'vai chat --db myapp --collection knowledge',
|
|
1435
1435
|
],
|
|
1436
1436
|
},
|
|
1437
|
+
|
|
1438
|
+
workflows: {
|
|
1439
|
+
title: 'Agentic Workflows',
|
|
1440
|
+
summary: 'Composable, multi-step RAG pipelines as JSON files',
|
|
1441
|
+
content: [
|
|
1442
|
+
`${pc.cyan('Workflows')} are composable, multi-step RAG pipelines defined as portable`,
|
|
1443
|
+
`JSON files. Think Docker Compose or GitHub Actions, but for search and retrieval`,
|
|
1444
|
+
`pipelines.`,
|
|
1445
|
+
``,
|
|
1446
|
+
`${pc.bold('Why workflows?')} Instead of writing bash scripts to chain vai commands,`,
|
|
1447
|
+
`a workflow file captures the intent declaratively. Workflows are reproducible,`,
|
|
1448
|
+
`shareable (commit them to git), and inspectable.`,
|
|
1449
|
+
``,
|
|
1450
|
+
`${pc.bold('How it works:')}`,
|
|
1451
|
+
`Each workflow defines a DAG (directed acyclic graph) of steps. Each step maps`,
|
|
1452
|
+
`to a vai operation (query, search, rerank, embed, etc.) or a control flow`,
|
|
1453
|
+
`operation (merge, filter, transform, generate). Steps reference outputs from`,
|
|
1454
|
+
`previous steps using ${pc.cyan('{{ template expressions }}')} for data flow.`,
|
|
1455
|
+
``,
|
|
1456
|
+
`${pc.bold('Template expressions:')}`,
|
|
1457
|
+
` ${pc.cyan('{{ inputs.query }}')} workflow input parameter`,
|
|
1458
|
+
` ${pc.cyan('{{ search.output.results }}')} results from a previous step`,
|
|
1459
|
+
` ${pc.cyan('{{ merge.output.results[0] }}')} array indexing`,
|
|
1460
|
+
` ${pc.cyan('{{ defaults.db }}')} workflow default value`,
|
|
1461
|
+
``,
|
|
1462
|
+
`${pc.bold('Parallel execution:')} The engine automatically detects independent steps`,
|
|
1463
|
+
`and runs them in parallel. No configuration needed.`,
|
|
1464
|
+
``,
|
|
1465
|
+
`${pc.bold('Step types:')}`,
|
|
1466
|
+
` ${pc.dim('VAI tools:')} query, search, rerank, embed, similarity, ingest,`,
|
|
1467
|
+
` collections, models, explain, estimate`,
|
|
1468
|
+
` ${pc.dim('Control flow:')} merge, filter, transform, generate`,
|
|
1469
|
+
``,
|
|
1470
|
+
`${pc.bold('Built-in templates:')} Run ${pc.cyan('vai workflow list')} to see available`,
|
|
1471
|
+
`templates like multi-collection-search, smart-ingest, research-and-summarize.`,
|
|
1472
|
+
].join('\n'),
|
|
1473
|
+
links: ['https://github.com/mrlynn/voyageai-cli#workflows'],
|
|
1474
|
+
tryIt: [
|
|
1475
|
+
'vai workflow list',
|
|
1476
|
+
'vai workflow init --name my-pipeline',
|
|
1477
|
+
'vai workflow validate ./my-pipeline.vai-workflow.json',
|
|
1478
|
+
'vai workflow run multi-collection-search --input query="test" --dry-run',
|
|
1479
|
+
],
|
|
1480
|
+
},
|
|
1437
1481
|
};
|
|
1438
1482
|
|
|
1439
1483
|
/**
|
|
@@ -1598,6 +1642,15 @@ const aliases = {
|
|
|
1598
1642
|
conversational: 'chat',
|
|
1599
1643
|
'chat-history': 'chat',
|
|
1600
1644
|
llm: 'chat',
|
|
1645
|
+
// Workflow aliases
|
|
1646
|
+
workflow: 'workflows',
|
|
1647
|
+
workflows: 'workflows',
|
|
1648
|
+
'rag-pipeline': 'workflows',
|
|
1649
|
+
composable: 'workflows',
|
|
1650
|
+
'workflow-engine': 'workflows',
|
|
1651
|
+
'vai-workflow': 'workflows',
|
|
1652
|
+
pipeline: 'workflows',
|
|
1653
|
+
dag: 'workflows',
|
|
1601
1654
|
};
|
|
1602
1655
|
|
|
1603
1656
|
/**
|
|
@@ -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} */
|
package/src/lib/telemetry.js
CHANGED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Template expression engine for vai workflows.
|
|
5
|
+
*
|
|
6
|
+
* Resolves {{ expression }} strings against a context object.
|
|
7
|
+
* Supports dot-path access and array indexing only.
|
|
8
|
+
* No eval(), no function calls, no arithmetic.
|
|
9
|
+
*
|
|
10
|
+
* Grammar:
|
|
11
|
+
* expression = segment ("." segment)*
|
|
12
|
+
* segment = identifier ("[" index "]")?
|
|
13
|
+
* identifier = [a-zA-Z_][a-zA-Z0-9_]*
|
|
14
|
+
* index = [0-9]+
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Matches {{ path.to.value }} including {{ path[0].field }}
|
|
18
|
+
const TEMPLATE_RE = /\{\{\s*(.+?)\s*\}\}/g;
|
|
19
|
+
|
|
20
|
+
// A string that is exactly one template expression with no surrounding text
|
|
21
|
+
// Use [^}] to prevent matching across multiple {{ }} pairs
|
|
22
|
+
const SOLE_TEMPLATE_RE = /^\{\{\s*([^}]+?)\s*\}\}$/;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if a string contains template expressions.
|
|
26
|
+
* @param {string} str
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function isTemplateString(str) {
|
|
30
|
+
if (typeof str !== 'string') return false;
|
|
31
|
+
// Use a fresh regex to avoid global lastIndex state issues
|
|
32
|
+
return /\{\{\s*(.+?)\s*\}\}/.test(str);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a dot-path expression into segments.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} expr - e.g. "search_api.output.results[0].content"
|
|
39
|
+
* @returns {Array<{key: string, index?: number}>}
|
|
40
|
+
* @throws {Error} on invalid syntax
|
|
41
|
+
*/
|
|
42
|
+
function parseExpression(expr) {
|
|
43
|
+
const trimmed = expr.trim();
|
|
44
|
+
if (!trimmed) throw new Error('Empty expression');
|
|
45
|
+
|
|
46
|
+
const segments = [];
|
|
47
|
+
const parts = trimmed.split('.');
|
|
48
|
+
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
// Check for array indexing: identifier[index]
|
|
51
|
+
const bracketMatch = part.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[(\d+)\]$/);
|
|
52
|
+
if (bracketMatch) {
|
|
53
|
+
segments.push({ key: bracketMatch[1], index: parseInt(bracketMatch[2], 10) });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Plain identifier
|
|
58
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(part)) {
|
|
59
|
+
segments.push({ key: part });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error(`Invalid expression segment: "${part}" in "${expr}"`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return segments;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a parsed path against a context object.
|
|
71
|
+
* Returns undefined (no throw) if any segment is missing.
|
|
72
|
+
*
|
|
73
|
+
* @param {Array<{key: string, index?: number}>} segments
|
|
74
|
+
* @param {object} context
|
|
75
|
+
* @returns {*}
|
|
76
|
+
*/
|
|
77
|
+
function resolvePath(segments, context) {
|
|
78
|
+
let current = context;
|
|
79
|
+
|
|
80
|
+
for (const seg of segments) {
|
|
81
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
82
|
+
|
|
83
|
+
current = current[seg.key];
|
|
84
|
+
if (current == null) return current; // null or undefined
|
|
85
|
+
|
|
86
|
+
if (seg.index !== undefined) {
|
|
87
|
+
if (!Array.isArray(current)) return undefined;
|
|
88
|
+
current = current[seg.index];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return current;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve template expressions within a single string.
|
|
97
|
+
*
|
|
98
|
+
* If the entire string is a single template expression, return the resolved
|
|
99
|
+
* value directly (preserving type: array, number, object, etc.).
|
|
100
|
+
* If the string contains templates mixed with text, return a string with
|
|
101
|
+
* substitutions.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} str
|
|
104
|
+
* @param {object} context
|
|
105
|
+
* @returns {*}
|
|
106
|
+
*/
|
|
107
|
+
function resolveString(str, context) {
|
|
108
|
+
// Fast path: no templates
|
|
109
|
+
if (!str.includes('{{')) return str;
|
|
110
|
+
|
|
111
|
+
// Check if the entire string is a single template expression
|
|
112
|
+
const soleMatch = str.match(SOLE_TEMPLATE_RE);
|
|
113
|
+
if (soleMatch) {
|
|
114
|
+
try {
|
|
115
|
+
const segments = parseExpression(soleMatch[1]);
|
|
116
|
+
return resolvePath(segments, context);
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Mixed text + templates: substitute all, coerce to string
|
|
123
|
+
return str.replace(TEMPLATE_RE, (_match, expr) => {
|
|
124
|
+
try {
|
|
125
|
+
const segments = parseExpression(expr);
|
|
126
|
+
const value = resolvePath(segments, context);
|
|
127
|
+
if (value === undefined) return '';
|
|
128
|
+
if (value === null) return 'null';
|
|
129
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
130
|
+
return String(value);
|
|
131
|
+
} catch {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Recursively resolve all template expressions in a value.
|
|
139
|
+
* Handles strings, arrays, and plain objects.
|
|
140
|
+
*
|
|
141
|
+
* @param {*} value
|
|
142
|
+
* @param {object} context
|
|
143
|
+
* @returns {*}
|
|
144
|
+
*/
|
|
145
|
+
function resolveTemplate(value, context) {
|
|
146
|
+
if (typeof value === 'string') {
|
|
147
|
+
return resolveString(value, context);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (Array.isArray(value)) {
|
|
151
|
+
return value.map(item => resolveTemplate(item, context));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (value !== null && typeof value === 'object') {
|
|
155
|
+
const resolved = {};
|
|
156
|
+
for (const [k, v] of Object.entries(value)) {
|
|
157
|
+
resolved[k] = resolveTemplate(v, context);
|
|
158
|
+
}
|
|
159
|
+
return resolved;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// numbers, booleans, null, undefined pass through
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Deep-resolve all template expressions in an object tree.
|
|
168
|
+
* Alias for resolveTemplate when called on an object.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} obj
|
|
171
|
+
* @param {object} context
|
|
172
|
+
* @returns {object}
|
|
173
|
+
*/
|
|
174
|
+
function resolveAllTemplates(obj, context) {
|
|
175
|
+
return resolveTemplate(obj, context);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract step IDs referenced by template expressions in an object tree.
|
|
180
|
+
* Ignores "inputs" and "defaults" prefixes (those are workflow-level, not step refs).
|
|
181
|
+
*
|
|
182
|
+
* @param {*} obj - Step inputs, condition, forEach value
|
|
183
|
+
* @returns {Set<string>} Set of step IDs referenced
|
|
184
|
+
*/
|
|
185
|
+
function extractDependencies(obj) {
|
|
186
|
+
const deps = new Set();
|
|
187
|
+
|
|
188
|
+
function scan(value) {
|
|
189
|
+
if (typeof value === 'string') {
|
|
190
|
+
// Reset regex state
|
|
191
|
+
const re = /\{\{\s*(.+?)\s*\}\}/g;
|
|
192
|
+
let match;
|
|
193
|
+
while ((match = re.exec(value)) !== null) {
|
|
194
|
+
const expr = match[1].trim();
|
|
195
|
+
// Extract the first segment (root identifier)
|
|
196
|
+
const dotIdx = expr.indexOf('.');
|
|
197
|
+
const bracketIdx = expr.indexOf('[');
|
|
198
|
+
let root;
|
|
199
|
+
if (dotIdx === -1 && bracketIdx === -1) {
|
|
200
|
+
root = expr;
|
|
201
|
+
} else if (dotIdx === -1) {
|
|
202
|
+
root = expr.slice(0, bracketIdx);
|
|
203
|
+
} else if (bracketIdx === -1) {
|
|
204
|
+
root = expr.slice(0, dotIdx);
|
|
205
|
+
} else {
|
|
206
|
+
root = expr.slice(0, Math.min(dotIdx, bracketIdx));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Skip workflow-level references
|
|
210
|
+
if (root !== 'inputs' && root !== 'defaults') {
|
|
211
|
+
deps.add(root);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (Array.isArray(value)) {
|
|
218
|
+
value.forEach(scan);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (value !== null && typeof value === 'object') {
|
|
223
|
+
Object.values(value).forEach(scan);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
scan(obj);
|
|
228
|
+
return deps;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
isTemplateString,
|
|
233
|
+
parseExpression,
|
|
234
|
+
resolvePath,
|
|
235
|
+
resolveString,
|
|
236
|
+
resolveTemplate,
|
|
237
|
+
resolveAllTemplates,
|
|
238
|
+
extractDependencies,
|
|
239
|
+
TEMPLATE_RE,
|
|
240
|
+
};
|
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
# {{projectName}}
|
|
1
|
+
# 🔍 {{projectName}}
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Semantic search application powered by **Voyage AI** embeddings, **MongoDB Atlas Vector Search**, and **Next.js** with Material UI.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://github.com/mrlynn/voyageai-cli)
|
|
6
|
+
[](https://nextjs.org)
|
|
7
|
+
[](https://mui.com)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## ⚡ Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# 1. Install
|
|
15
|
+
npm install
|
|
16
|
+
|
|
17
|
+
# 2. Configure
|
|
18
|
+
cp .env.example .env.local
|
|
19
|
+
# → Edit .env.local with your Voyage AI key + MongoDB URI
|
|
20
|
+
|
|
21
|
+
# 3. Create vector index (see below)
|
|
22
|
+
|
|
23
|
+
# 4. Run
|
|
24
|
+
npm run dev
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Open **[http://localhost:3000](http://localhost:3000)** — you'll see a branded landing page with a link to the search UI.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🔧 Configuration
|
|
6
32
|
|
|
7
33
|
| Setting | Value |
|
|
8
34
|
|---------|-------|
|
|
9
35
|
| Embedding Model | `{{model}}` |
|
|
10
|
-
| Dimensions | {{dimensions}} |
|
|
36
|
+
| Dimensions | `{{dimensions}}` |
|
|
11
37
|
| Database | `{{db}}` |
|
|
12
38
|
| Collection | `{{collection}}` |
|
|
13
39
|
| Vector Index | `{{index}}` |
|
|
@@ -15,77 +41,74 @@ A semantic search application powered by Voyage AI embeddings, MongoDB Atlas Vec
|
|
|
15
41
|
| Rerank Model | `{{rerankModel}}` |
|
|
16
42
|
{{/if}}
|
|
17
43
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
### 1. Install dependencies
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### 2. Configure environment
|
|
27
|
-
|
|
28
|
-
Copy `.env.example` to `.env.local` and fill in your credentials:
|
|
44
|
+
### Environment Variables
|
|
29
45
|
|
|
30
|
-
```
|
|
31
|
-
|
|
46
|
+
```env
|
|
47
|
+
VOYAGE_API_KEY= # From dash.voyageai.com
|
|
48
|
+
MONGODB_URI= # Atlas connection string
|
|
32
49
|
```
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
- `VOYAGE_API_KEY` - Your Voyage AI API key from [dash.voyageai.com](https://dash.voyageai.com)
|
|
36
|
-
- `MONGODB_URI` - Your MongoDB Atlas connection string
|
|
51
|
+
### Vector Search Index
|
|
37
52
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
In MongoDB Atlas, create a vector search index on your collection:
|
|
53
|
+
Create this index on your `{{collection}}` collection in Atlas:
|
|
41
54
|
|
|
42
55
|
```json
|
|
43
56
|
{
|
|
44
|
-
"fields": [
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
]
|
|
57
|
+
"fields": [{
|
|
58
|
+
"type": "vector",
|
|
59
|
+
"path": "{{field}}",
|
|
60
|
+
"numDimensions": {{dimensions}},
|
|
61
|
+
"similarity": "cosine"
|
|
62
|
+
}]
|
|
52
63
|
}
|
|
53
64
|
```
|
|
54
65
|
|
|
55
|
-
Name
|
|
56
|
-
|
|
57
|
-
### 4. Start the development server
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
npm run dev
|
|
61
|
-
```
|
|
66
|
+
Name it **`{{index}}`**.
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
---
|
|
64
69
|
|
|
65
|
-
## Project Structure
|
|
70
|
+
## 📁 Project Structure
|
|
66
71
|
|
|
67
72
|
```
|
|
68
73
|
{{projectName}}/
|
|
69
74
|
├── app/
|
|
70
|
-
│ ├── layout.jsx
|
|
71
|
-
│ ├── page.jsx
|
|
72
|
-
│ ├── search/
|
|
73
|
-
│ │ └── page.jsx # Search page (MUI)
|
|
75
|
+
│ ├── layout.jsx # Root layout (Inter font, theme)
|
|
76
|
+
│ ├── page.jsx # Landing page (hero + features)
|
|
77
|
+
│ ├── search/page.jsx # Search UI (score bars, copy, fade-in)
|
|
74
78
|
│ └── api/
|
|
75
|
-
│ ├── search/route.js
|
|
76
|
-
│ └── ingest/route.js
|
|
79
|
+
│ ├── search/route.js # POST /api/search
|
|
80
|
+
│ └── ingest/route.js # POST /api/ingest
|
|
81
|
+
├── components/
|
|
82
|
+
│ ├── ThemeRegistry.jsx # Light/dark mode with persistence
|
|
83
|
+
│ ├── Navbar.jsx # Branded AppBar + dark toggle
|
|
84
|
+
│ └── Footer.jsx # Powered-by footer
|
|
77
85
|
├── lib/
|
|
78
|
-
│ ├── voyage.js
|
|
79
|
-
│ ├── mongodb.js
|
|
80
|
-
│ └── theme.js
|
|
86
|
+
│ ├── voyage.js # Voyage AI client
|
|
87
|
+
│ ├── mongodb.js # MongoDB connection + vector search
|
|
88
|
+
│ └── theme.js # MUI theme (light + dark palettes)
|
|
89
|
+
├── public/favicon.svg
|
|
81
90
|
├── .env.example
|
|
82
|
-
|
|
83
|
-
└── README.md
|
|
91
|
+
└── package.json
|
|
84
92
|
```
|
|
85
93
|
|
|
86
|
-
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 🌗 Features
|
|
97
|
+
|
|
98
|
+
- **Dark mode** — auto-detects system preference, persists choice
|
|
99
|
+
- **Score visualization** — color-coded progress bars for relevance scores
|
|
100
|
+
- **Copy to clipboard** — one-click copy on each result
|
|
101
|
+
- **Responsive** — works on mobile and desktop
|
|
102
|
+
- **Branded** — vai + Voyage AI + MongoDB branding throughout
|
|
103
|
+
{{#if rerank}}
|
|
104
|
+
- **Reranking** — results refined with {{rerankModel}} for better relevance
|
|
105
|
+
{{/if}}
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 🔌 API Endpoints
|
|
87
110
|
|
|
88
|
-
### POST /api/search
|
|
111
|
+
### `POST /api/search`
|
|
89
112
|
|
|
90
113
|
```bash
|
|
91
114
|
curl -X POST http://localhost:3000/api/search \
|
|
@@ -93,7 +116,7 @@ curl -X POST http://localhost:3000/api/search \
|
|
|
93
116
|
-d '{"query": "How does vector search work?", "limit": 5}'
|
|
94
117
|
```
|
|
95
118
|
|
|
96
|
-
### POST /api/ingest
|
|
119
|
+
### `POST /api/ingest`
|
|
97
120
|
|
|
98
121
|
```bash
|
|
99
122
|
curl -X POST http://localhost:3000/api/ingest \
|
|
@@ -103,4 +126,4 @@ curl -X POST http://localhost:3000/api/ingest \
|
|
|
103
126
|
|
|
104
127
|
---
|
|
105
128
|
|
|
106
|
-
Generated by [vai](https://github.com/mrlynn/voyageai-cli) v{{vaiVersion}}
|
|
129
|
+
<sub>Generated by [vai](https://github.com/mrlynn/voyageai-cli) v{{vaiVersion}} on {{generatedAt}}</sub>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="vg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#00ED64"/>
|
|
5
|
+
<stop offset="100%" stop-color="#00D4AA"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="32" height="32" rx="8" fill="#001E2B"/>
|
|
9
|
+
<path d="M8 8 L16 26 L24 8" fill="none" stroke="url(#vg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
10
|
+
<text x="18" y="20" font-family="sans-serif" font-size="7" font-weight="bold" fill="url(#vg)">ai</text>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded footer
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { Box, Typography, Link, Stack, Divider } from '@mui/material';
|
|
9
|
+
|
|
10
|
+
export default function Footer() {
|
|
11
|
+
return (
|
|
12
|
+
<Box component="footer" sx={{ mt: 'auto', pt: 6, pb: 4 }}>
|
|
13
|
+
<Divider sx={{ mb: 3 }} />
|
|
14
|
+
<Stack
|
|
15
|
+
direction={{ xs: 'column', sm: 'row' }}
|
|
16
|
+
justifyContent="space-between"
|
|
17
|
+
alignItems="center"
|
|
18
|
+
spacing={1}
|
|
19
|
+
sx={{ px: 3 }}
|
|
20
|
+
>
|
|
21
|
+
<Typography variant="caption" color="text.secondary">
|
|
22
|
+
Built with{' '}
|
|
23
|
+
<Link
|
|
24
|
+
href="https://github.com/mrlynn/voyageai-cli"
|
|
25
|
+
target="_blank"
|
|
26
|
+
rel="noopener"
|
|
27
|
+
underline="hover"
|
|
28
|
+
color="primary"
|
|
29
|
+
fontWeight={600}
|
|
30
|
+
>
|
|
31
|
+
vai
|
|
32
|
+
</Link>
|
|
33
|
+
{' '}· Powered by{' '}
|
|
34
|
+
<Link href="https://www.voyageai.com" target="_blank" rel="noopener" underline="hover" color="inherit">
|
|
35
|
+
Voyage AI
|
|
36
|
+
</Link>
|
|
37
|
+
{' '}+{' '}
|
|
38
|
+
<Link href="https://www.mongodb.com/atlas/search" target="_blank" rel="noopener" underline="hover" color="inherit">
|
|
39
|
+
MongoDB Atlas Vector Search
|
|
40
|
+
</Link>
|
|
41
|
+
</Typography>
|
|
42
|
+
|
|
43
|
+
<Typography variant="caption" color="text.secondary" sx={{ fontFamily: 'monospace', fontSize: '0.65rem' }}>
|
|
44
|
+
v{{vaiVersion}}
|
|
45
|
+
</Typography>
|
|
46
|
+
</Stack>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Root Layout with
|
|
2
|
+
* Root Layout with vai branding
|
|
3
3
|
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
|
|
7
|
-
import {
|
|
8
|
-
import CssBaseline from '@mui/material/CssBaseline';
|
|
9
|
-
import { theme } from '@/lib/theme';
|
|
7
|
+
import { ThemeRegistry } from '@/components/ThemeRegistry';
|
|
10
8
|
|
|
11
9
|
export const metadata = {
|
|
12
|
-
title: '{{projectName}}',
|
|
13
|
-
description: 'Semantic search powered by Voyage AI',
|
|
10
|
+
title: '{{projectName}} — Semantic Search',
|
|
11
|
+
description: 'Semantic search powered by Voyage AI embeddings and MongoDB Atlas Vector Search.',
|
|
12
|
+
icons: { icon: '/favicon.svg' },
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
export default function RootLayout({ children }) {
|
|
17
16
|
return (
|
|
18
|
-
<html lang="en">
|
|
17
|
+
<html lang="en" suppressHydrationWarning>
|
|
18
|
+
<head>
|
|
19
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
20
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
21
|
+
<link
|
|
22
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
|
|
23
|
+
rel="stylesheet"
|
|
24
|
+
/>
|
|
25
|
+
</head>
|
|
19
26
|
<body>
|
|
20
27
|
<AppRouterCacheProvider>
|
|
21
|
-
<
|
|
22
|
-
<CssBaseline />
|
|
28
|
+
<ThemeRegistry>
|
|
23
29
|
{children}
|
|
24
|
-
</
|
|
30
|
+
</ThemeRegistry>
|
|
25
31
|
</AppRouterCacheProvider>
|
|
26
32
|
</body>
|
|
27
33
|
</html>
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { MongoClient } from 'mongodb';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (!MONGODB_URI)
|
|
13
|
-
|
|
10
|
+
function getUri() {
|
|
11
|
+
const uri = process.env.MONGODB_URI;
|
|
12
|
+
if (!uri) throw new Error('MONGODB_URI environment variable is required');
|
|
13
|
+
return uri;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// Cache the client promise for connection reuse in serverless
|
|
@@ -30,7 +30,7 @@ export async function getMongoClient() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (!cached.promise) {
|
|
33
|
-
cached.promise = MongoClient.connect(
|
|
33
|
+
cached.promise = MongoClient.connect(getUri());
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
cached.conn = await cached.promise;
|