voyageai-cli 1.20.5 → 1.21.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +142 -26
  2. package/README.md +153 -2
  3. package/package.json +1 -1
  4. package/src/cli.js +12 -0
  5. package/src/commands/about.js +39 -3
  6. package/src/commands/doctor.js +325 -0
  7. package/src/commands/eval.js +420 -10
  8. package/src/commands/generate.js +220 -0
  9. package/src/commands/playground.js +93 -0
  10. package/src/commands/purge.js +271 -0
  11. package/src/commands/quickstart.js +203 -0
  12. package/src/commands/refresh.js +322 -0
  13. package/src/commands/scaffold.js +217 -0
  14. package/src/lib/codegen.js +313 -0
  15. package/src/lib/explanations.js +163 -1
  16. package/src/lib/scaffold-structure.js +114 -0
  17. package/src/lib/templates/nextjs/README.md.tpl +106 -0
  18. package/src/lib/templates/nextjs/env.example.tpl +8 -0
  19. package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
  20. package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
  21. package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
  22. package/src/lib/templates/nextjs/package.json.tpl +33 -0
  23. package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
  24. package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
  25. package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
  26. package/src/lib/templates/nextjs/theme.js.tpl +84 -0
  27. package/src/lib/templates/python/README.md.tpl +145 -0
  28. package/src/lib/templates/python/app.py.tpl +221 -0
  29. package/src/lib/templates/python/chunker.py.tpl +127 -0
  30. package/src/lib/templates/python/env.example.tpl +12 -0
  31. package/src/lib/templates/python/mongo_client.py.tpl +125 -0
  32. package/src/lib/templates/python/requirements.txt.tpl +10 -0
  33. package/src/lib/templates/python/voyage_client.py.tpl +124 -0
  34. package/src/lib/templates/vanilla/README.md.tpl +156 -0
  35. package/src/lib/templates/vanilla/client.js.tpl +103 -0
  36. package/src/lib/templates/vanilla/connection.js.tpl +126 -0
  37. package/src/lib/templates/vanilla/env.example.tpl +11 -0
  38. package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
  39. package/src/lib/templates/vanilla/package.json.tpl +31 -0
  40. package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
  41. package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
  42. package/src/lib/templates/vanilla/server.js.tpl +81 -0
  43. package/src/lib/zip.js +130 -0
  44. package/src/playground/index.html +772 -33
@@ -0,0 +1,106 @@
1
+ # {{projectName}}
2
+
3
+ A semantic search application powered by Voyage AI embeddings, MongoDB Atlas Vector Search, and Next.js with Material UI.
4
+
5
+ ## Configuration
6
+
7
+ | Setting | Value |
8
+ |---------|-------|
9
+ | Embedding Model | `{{model}}` |
10
+ | Dimensions | {{dimensions}} |
11
+ | Database | `{{db}}` |
12
+ | Collection | `{{collection}}` |
13
+ | Vector Index | `{{index}}` |
14
+ {{#if rerank}}
15
+ | Rerank Model | `{{rerankModel}}` |
16
+ {{/if}}
17
+
18
+ ## Setup
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:
29
+
30
+ ```bash
31
+ cp .env.example .env.local
32
+ ```
33
+
34
+ Required variables:
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
37
+
38
+ ### 3. Create vector index
39
+
40
+ In MongoDB Atlas, create a vector search index on your collection:
41
+
42
+ ```json
43
+ {
44
+ "fields": [
45
+ {
46
+ "type": "vector",
47
+ "path": "{{field}}",
48
+ "numDimensions": {{dimensions}},
49
+ "similarity": "cosine"
50
+ }
51
+ ]
52
+ }
53
+ ```
54
+
55
+ Name the index `{{index}}`.
56
+
57
+ ### 4. Start the development server
58
+
59
+ ```bash
60
+ npm run dev
61
+ ```
62
+
63
+ Open [http://localhost:3000](http://localhost:3000) to see the search interface.
64
+
65
+ ## Project Structure
66
+
67
+ ```
68
+ {{projectName}}/
69
+ ├── app/
70
+ │ ├── layout.jsx # Root layout with MUI theme
71
+ │ ├── page.jsx # Home page
72
+ │ ├── search/
73
+ │ │ └── page.jsx # Search page (MUI)
74
+ │ └── api/
75
+ │ ├── search/route.js # Search endpoint
76
+ │ └── ingest/route.js # Ingest endpoint
77
+ ├── lib/
78
+ │ ├── voyage.js # Voyage AI client
79
+ │ ├── mongodb.js # MongoDB connection
80
+ │ └── theme.js # MUI theme
81
+ ├── .env.example
82
+ ├── package.json
83
+ └── README.md
84
+ ```
85
+
86
+ ## API Endpoints
87
+
88
+ ### POST /api/search
89
+
90
+ ```bash
91
+ curl -X POST http://localhost:3000/api/search \
92
+ -H "Content-Type: application/json" \
93
+ -d '{"query": "How does vector search work?", "limit": 5}'
94
+ ```
95
+
96
+ ### POST /api/ingest
97
+
98
+ ```bash
99
+ curl -X POST http://localhost:3000/api/ingest \
100
+ -H "Content-Type: application/json" \
101
+ -d '{"text": "Your document content...", "metadata": {"source": "api"}}'
102
+ ```
103
+
104
+ ---
105
+
106
+ Generated by [vai](https://github.com/mrlynn/voyageai-cli) v{{vaiVersion}}
@@ -0,0 +1,8 @@
1
+ # Voyage AI API
2
+ VOYAGE_API_KEY=your_voyage_api_key_here
3
+
4
+ # MongoDB Atlas
5
+ MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/{{db}}?retryWrites=true&w=majority
6
+
7
+ # Optional: Override defaults
8
+ # VOYAGE_API_URL=https://api.voyageai.com/v1
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Root Layout with MUI Theme
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ */
5
+
6
+ import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
7
+ import { ThemeProvider } from '@mui/material/styles';
8
+ import CssBaseline from '@mui/material/CssBaseline';
9
+ import { theme } from '@/lib/theme';
10
+
11
+ export const metadata = {
12
+ title: '{{projectName}}',
13
+ description: 'Semantic search powered by Voyage AI',
14
+ };
15
+
16
+ export default function RootLayout({ children }) {
17
+ return (
18
+ <html lang="en">
19
+ <body>
20
+ <AppRouterCacheProvider>
21
+ <ThemeProvider theme={theme}>
22
+ <CssBaseline />
23
+ {children}
24
+ </ThemeProvider>
25
+ </AppRouterCacheProvider>
26
+ </body>
27
+ </html>
28
+ );
29
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * MongoDB Connection Helper (Next.js)
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ *
5
+ * Uses connection caching for serverless environments.
6
+ */
7
+
8
+ import { MongoClient } from 'mongodb';
9
+
10
+ const MONGODB_URI = process.env.MONGODB_URI;
11
+
12
+ if (!MONGODB_URI) {
13
+ throw new Error('MONGODB_URI environment variable is required');
14
+ }
15
+
16
+ // Cache the client promise for connection reuse in serverless
17
+ let cached = global.mongo;
18
+
19
+ if (!cached) {
20
+ cached = global.mongo = { conn: null, promise: null };
21
+ }
22
+
23
+ /**
24
+ * Get a cached MongoDB client connection.
25
+ * Reuses existing connection in serverless environment.
26
+ */
27
+ export async function getMongoClient() {
28
+ if (cached.conn) {
29
+ return cached.conn;
30
+ }
31
+
32
+ if (!cached.promise) {
33
+ cached.promise = MongoClient.connect(MONGODB_URI);
34
+ }
35
+
36
+ cached.conn = await cached.promise;
37
+ return cached.conn;
38
+ }
39
+
40
+ /**
41
+ * Get the database instance.
42
+ */
43
+ export async function getDb() {
44
+ const client = await getMongoClient();
45
+ return client.db('{{db}}');
46
+ }
47
+
48
+ /**
49
+ * Get the documents collection.
50
+ */
51
+ export async function getCollection() {
52
+ const db = await getDb();
53
+ return db.collection('{{collection}}');
54
+ }
55
+
56
+ /**
57
+ * Perform a vector search.
58
+ */
59
+ export async function vectorSearch(embedding, options = {}) {
60
+ const collection = await getCollection();
61
+ const limit = options.limit || 10;
62
+ const numCandidates = options.numCandidates || limit * 10;
63
+
64
+ const pipeline = [
65
+ {
66
+ $vectorSearch: {
67
+ index: '{{index}}',
68
+ path: '{{field}}',
69
+ queryVector: embedding,
70
+ numCandidates,
71
+ limit,
72
+ },
73
+ },
74
+ {
75
+ $project: {
76
+ _id: 1,
77
+ text: 1,
78
+ metadata: 1,
79
+ score: { $meta: 'vectorSearchScore' },
80
+ },
81
+ },
82
+ ];
83
+
84
+ if (options.filter) {
85
+ pipeline[0].$vectorSearch.filter = options.filter;
86
+ }
87
+
88
+ const results = await collection.aggregate(pipeline).toArray();
89
+ return results.map(doc => ({
90
+ document: doc,
91
+ score: doc.score,
92
+ }));
93
+ }
94
+
95
+ /**
96
+ * Insert documents with embeddings.
97
+ */
98
+ export async function insertDocuments(docs) {
99
+ const collection = await getCollection();
100
+
101
+ const documents = docs.map(doc => ({
102
+ text: doc.text,
103
+ {{field}}: doc.embedding,
104
+ metadata: doc.metadata || {},
105
+ _embeddedAt: new Date(),
106
+ _model: '{{model}}',
107
+ }));
108
+
109
+ const result = await collection.insertMany(documents);
110
+ return { insertedCount: result.insertedCount };
111
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Voyage AI API Client (Next.js)
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ *
5
+ * Model: {{model}}
6
+ * Dimensions: {{dimensions}}
7
+ */
8
+
9
+ const VOYAGE_API_URL = process.env.VOYAGE_API_URL || 'https://api.voyageai.com/v1';
10
+ const VOYAGE_API_KEY = process.env.VOYAGE_API_KEY;
11
+
12
+ /**
13
+ * Generate embeddings for text(s) using Voyage AI.
14
+ */
15
+ export async function embed(input, options = {}) {
16
+ if (!VOYAGE_API_KEY) {
17
+ throw new Error('VOYAGE_API_KEY environment variable is required');
18
+ }
19
+
20
+ const texts = Array.isArray(input) ? input : [input];
21
+
22
+ const response = await fetch(`${VOYAGE_API_URL}/embeddings`, {
23
+ method: 'POST',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ 'Authorization': `Bearer ${VOYAGE_API_KEY}`,
27
+ },
28
+ body: JSON.stringify({
29
+ model: options.model || '{{model}}',
30
+ input: texts,
31
+ input_type: options.inputType || '{{inputType}}',
32
+ output_dimension: options.outputDimension || {{dimensions}},
33
+ }),
34
+ });
35
+
36
+ if (!response.ok) {
37
+ const error = await response.text();
38
+ throw new Error(`Voyage AI API error: ${response.status} ${error}`);
39
+ }
40
+
41
+ const data = await response.json();
42
+ return {
43
+ embeddings: data.data.map(d => d.embedding),
44
+ usage: data.usage,
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Embed a single query.
50
+ */
51
+ export async function embedQuery(query, options = {}) {
52
+ const result = await embed(query, { ...options, inputType: 'query' });
53
+ return result.embeddings[0];
54
+ }
55
+
56
+ /**
57
+ * Embed multiple documents.
58
+ */
59
+ export async function embedDocuments(documents, options = {}) {
60
+ const result = await embed(documents, { ...options, inputType: 'document' });
61
+ return result.embeddings;
62
+ }
63
+
64
+ {{#if rerank}}
65
+ /**
66
+ * Rerank documents by relevance to a query.
67
+ */
68
+ export async function rerank(query, documents, options = {}) {
69
+ if (!VOYAGE_API_KEY) {
70
+ throw new Error('VOYAGE_API_KEY environment variable is required');
71
+ }
72
+
73
+ const response = await fetch(`${VOYAGE_API_URL}/rerank`, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ 'Authorization': `Bearer ${VOYAGE_API_KEY}`,
78
+ },
79
+ body: JSON.stringify({
80
+ model: options.model || '{{rerankModel}}',
81
+ query,
82
+ documents,
83
+ top_k: options.topK,
84
+ return_documents: true,
85
+ }),
86
+ });
87
+
88
+ if (!response.ok) {
89
+ const error = await response.text();
90
+ throw new Error(`Voyage AI rerank error: ${response.status} ${error}`);
91
+ }
92
+
93
+ const data = await response.json();
94
+ return {
95
+ results: data.data.map(d => ({
96
+ index: d.index,
97
+ relevanceScore: d.relevance_score,
98
+ document: d.document,
99
+ })),
100
+ usage: data.usage,
101
+ };
102
+ }
103
+ {{/if}}
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@emotion/cache": "^11.11.0",
13
+ "@emotion/react": "^11.11.3",
14
+ "@emotion/styled": "^11.11.0",
15
+ "@mui/icons-material": "^5.15.0",
16
+ "@mui/material": "^5.15.0",
17
+ "@mui/material-nextjs": "^5.15.0",
18
+ "mongodb": "^6.3.0",
19
+ "next": "^14.1.0",
20
+ "react": "^18.2.0",
21
+ "react-dom": "^18.2.0"
22
+ },
23
+ "devDependencies": {
24
+ "eslint": "^8.56.0",
25
+ "eslint-config-next": "^14.1.0"
26
+ },
27
+ "generated": {
28
+ "by": "vai",
29
+ "version": "{{vaiVersion}}",
30
+ "model": "{{model}}",
31
+ "at": "{{generatedAt}}"
32
+ }
33
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Search Page (MUI)
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import { useState } from 'react';
9
+ import {
10
+ Box,
11
+ Container,
12
+ TextField,
13
+ Button,
14
+ Card,
15
+ CardContent,
16
+ Typography,
17
+ List,
18
+ ListItem,
19
+ Chip,
20
+ CircularProgress,
21
+ Alert,
22
+ InputAdornment,
23
+ } from '@mui/material';
24
+ import SearchIcon from '@mui/icons-material/Search';
25
+
26
+ export default function SearchPage() {
27
+ const [query, setQuery] = useState('');
28
+ const [results, setResults] = useState([]);
29
+ const [loading, setLoading] = useState(false);
30
+ const [error, setError] = useState(null);
31
+ const [meta, setMeta] = useState(null);
32
+
33
+ const handleSearch = async (e) => {
34
+ e.preventDefault();
35
+ if (!query.trim()) return;
36
+
37
+ setLoading(true);
38
+ setError(null);
39
+
40
+ try {
41
+ const response = await fetch('/api/search', {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({ query, limit: 5 }),
45
+ });
46
+
47
+ if (!response.ok) {
48
+ const data = await response.json();
49
+ throw new Error(data.error || 'Search failed');
50
+ }
51
+
52
+ const data = await response.json();
53
+ setResults(data.results);
54
+ setMeta(data.meta);
55
+ } catch (err) {
56
+ setError(err.message);
57
+ setResults([]);
58
+ } finally {
59
+ setLoading(false);
60
+ }
61
+ };
62
+
63
+ return (
64
+ <Container maxWidth="md" sx={{ py: 4 }}>
65
+ <Typography variant="h4" component="h1" gutterBottom>
66
+ Semantic Search
67
+ </Typography>
68
+
69
+ <Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
70
+ Search your documents using {{model}} embeddings
71
+ {{#if rerank}} with {{rerankModel}} reranking{{/if}}.
72
+ </Typography>
73
+
74
+ <Box component="form" onSubmit={handleSearch} sx={{ mb: 4 }}>
75
+ <TextField
76
+ fullWidth
77
+ value={query}
78
+ onChange={(e) => setQuery(e.target.value)}
79
+ placeholder="Enter your search query..."
80
+ variant="outlined"
81
+ InputProps={{
82
+ startAdornment: (
83
+ <InputAdornment position="start">
84
+ <SearchIcon />
85
+ </InputAdornment>
86
+ ),
87
+ endAdornment: (
88
+ <InputAdornment position="end">
89
+ <Button
90
+ type="submit"
91
+ variant="contained"
92
+ disabled={loading || !query.trim()}
93
+ >
94
+ {loading ? <CircularProgress size={24} /> : 'Search'}
95
+ </Button>
96
+ </InputAdornment>
97
+ ),
98
+ }}
99
+ />
100
+ </Box>
101
+
102
+ {error && (
103
+ <Alert severity="error" sx={{ mb: 3 }}>
104
+ {error}
105
+ </Alert>
106
+ )}
107
+
108
+ {meta && (
109
+ <Box sx={{ mb: 2, display: 'flex', gap: 1 }}>
110
+ <Chip label={`${results.length} results`} size="small" />
111
+ <Chip label={`${meta.took}ms`} size="small" variant="outlined" />
112
+ <Chip label={meta.model} size="small" variant="outlined" />
113
+ </Box>
114
+ )}
115
+
116
+ <List>
117
+ {results.map((result, index) => (
118
+ <ListItem key={index} sx={{ px: 0 }}>
119
+ <Card sx={{ width: '100%' }}>
120
+ <CardContent>
121
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
122
+ <Typography variant="subtitle2" color="text.secondary">
123
+ {result.metadata?.source || 'Document'}
124
+ </Typography>
125
+ <Chip
126
+ label={`Score: ${result.score.toFixed(3)}`}
127
+ size="small"
128
+ color={result.score > 0.8 ? 'success' : 'default'}
129
+ />
130
+ </Box>
131
+ <Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
132
+ {result.text}
133
+ </Typography>
134
+ </CardContent>
135
+ </Card>
136
+ </ListItem>
137
+ ))}
138
+ </List>
139
+
140
+ {results.length === 0 && !loading && query && !error && (
141
+ <Typography color="text.secondary" align="center">
142
+ No results found for "{query}"
143
+ </Typography>
144
+ )}
145
+ </Container>
146
+ );
147
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Ingest API Route Handler
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ *
5
+ * POST /api/ingest
6
+ */
7
+
8
+ import { NextResponse } from 'next/server';
9
+ import { embed } from '@/lib/voyage';
10
+ import { insertDocuments } from '@/lib/mongodb';
11
+
12
+ /**
13
+ * Chunk text using recursive splitting.
14
+ */
15
+ function chunkText(text, size = {{chunkSize}}, overlap = {{chunkOverlap}}) {
16
+ const separators = ['\n\n', '\n', '. ', ' '];
17
+
18
+ function splitRecursive(text, sepIndex = 0) {
19
+ if (text.length <= size) {
20
+ const trimmed = text.trim();
21
+ return trimmed ? [trimmed] : [];
22
+ }
23
+
24
+ if (sepIndex >= separators.length) {
25
+ const chunks = [];
26
+ let start = 0;
27
+ while (start < text.length) {
28
+ const chunk = text.slice(start, start + size).trim();
29
+ if (chunk) chunks.push(chunk);
30
+ start += size - overlap;
31
+ }
32
+ return chunks;
33
+ }
34
+
35
+ const separator = separators[sepIndex];
36
+ const parts = text.split(separator);
37
+ const chunks = [];
38
+ let current = '';
39
+
40
+ for (const part of parts) {
41
+ const potential = current ? current + separator + part : part;
42
+ if (potential.length <= size) {
43
+ current = potential;
44
+ } else {
45
+ if (current) chunks.push(...splitRecursive(current, sepIndex + 1));
46
+ current = part;
47
+ }
48
+ }
49
+
50
+ if (current) chunks.push(...splitRecursive(current, sepIndex + 1));
51
+ return chunks;
52
+ }
53
+
54
+ return splitRecursive(text);
55
+ }
56
+
57
+ export async function POST(request) {
58
+ const start = Date.now();
59
+
60
+ try {
61
+ const body = await request.json();
62
+ const { text, metadata = {} } = body;
63
+
64
+ if (!text || typeof text !== 'string') {
65
+ return NextResponse.json(
66
+ { error: 'Text is required' },
67
+ { status: 400 }
68
+ );
69
+ }
70
+
71
+ // Chunk the text
72
+ const chunks = chunkText(text);
73
+
74
+ if (chunks.length === 0) {
75
+ return NextResponse.json({
76
+ success: true,
77
+ chunks: 0,
78
+ tokens: 0,
79
+ took: Date.now() - start,
80
+ });
81
+ }
82
+
83
+ // Embed all chunks
84
+ const { embeddings, usage } = await embed(chunks, { inputType: 'document' });
85
+
86
+ // Prepare documents
87
+ const documents = chunks.map((chunk, i) => ({
88
+ text: chunk,
89
+ embedding: embeddings[i],
90
+ metadata: {
91
+ ...metadata,
92
+ source: metadata.source || 'api',
93
+ chunkIndex: i,
94
+ totalChunks: chunks.length,
95
+ },
96
+ }));
97
+
98
+ // Insert into MongoDB
99
+ await insertDocuments(documents);
100
+
101
+ return NextResponse.json({
102
+ success: true,
103
+ chunks: chunks.length,
104
+ tokens: usage.total_tokens,
105
+ took: Date.now() - start,
106
+ });
107
+ } catch (error) {
108
+ console.error('Ingest error:', error);
109
+ return NextResponse.json(
110
+ { error: error.message },
111
+ { status: 500 }
112
+ );
113
+ }
114
+ }