voyageai-cli 1.20.6 → 1.22.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 (42) hide show
  1. package/CHANGELOG.md +142 -26
  2. package/README.md +130 -2
  3. package/package.json +3 -2
  4. package/src/cli.js +10 -0
  5. package/src/commands/bug.js +249 -0
  6. package/src/commands/eval.js +420 -10
  7. package/src/commands/generate.js +220 -0
  8. package/src/commands/playground.js +93 -0
  9. package/src/commands/purge.js +271 -0
  10. package/src/commands/refresh.js +322 -0
  11. package/src/commands/scaffold.js +217 -0
  12. package/src/lib/codegen.js +339 -0
  13. package/src/lib/explanations.js +155 -0
  14. package/src/lib/scaffold-structure.js +114 -0
  15. package/src/lib/templates/nextjs/README.md.tpl +106 -0
  16. package/src/lib/templates/nextjs/env.example.tpl +8 -0
  17. package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
  18. package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
  19. package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
  20. package/src/lib/templates/nextjs/package.json.tpl +33 -0
  21. package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
  22. package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
  23. package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
  24. package/src/lib/templates/nextjs/theme.js.tpl +84 -0
  25. package/src/lib/templates/python/README.md.tpl +145 -0
  26. package/src/lib/templates/python/app.py.tpl +221 -0
  27. package/src/lib/templates/python/chunker.py.tpl +127 -0
  28. package/src/lib/templates/python/env.example.tpl +12 -0
  29. package/src/lib/templates/python/mongo_client.py.tpl +125 -0
  30. package/src/lib/templates/python/requirements.txt.tpl +10 -0
  31. package/src/lib/templates/python/voyage_client.py.tpl +124 -0
  32. package/src/lib/templates/vanilla/README.md.tpl +156 -0
  33. package/src/lib/templates/vanilla/client.js.tpl +103 -0
  34. package/src/lib/templates/vanilla/connection.js.tpl +126 -0
  35. package/src/lib/templates/vanilla/env.example.tpl +11 -0
  36. package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
  37. package/src/lib/templates/vanilla/package.json.tpl +31 -0
  38. package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
  39. package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
  40. package/src/lib/templates/vanilla/server.js.tpl +81 -0
  41. package/src/lib/zip.js +130 -0
  42. package/src/playground/index.html +708 -3
@@ -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
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Search API Route Handler
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ *
5
+ * POST /api/search
6
+ */
7
+
8
+ import { NextResponse } from 'next/server';
9
+ import { embedQuery{{#if rerank}}, rerank{{/if}} } from '@/lib/voyage';
10
+ import { vectorSearch } from '@/lib/mongodb';
11
+
12
+ export async function POST(request) {
13
+ const start = Date.now();
14
+
15
+ try {
16
+ const body = await request.json();
17
+ const { query, limit = 5, includeContext = false, filter } = body;
18
+
19
+ if (!query || typeof query !== 'string') {
20
+ return NextResponse.json(
21
+ { error: 'Query is required' },
22
+ { status: 400 }
23
+ );
24
+ }
25
+
26
+ {{#if rerank}}
27
+ const candidates = body.candidates || 20;
28
+ {{/if}}
29
+
30
+ // Step 1: Embed the query
31
+ const queryEmbedding = await embedQuery(query);
32
+
33
+ // Step 2: Vector search
34
+ {{#if rerank}}
35
+ const searchResults = await vectorSearch(queryEmbedding, {
36
+ limit: candidates,
37
+ filter,
38
+ });
39
+ {{else}}
40
+ const searchResults = await vectorSearch(queryEmbedding, {
41
+ limit,
42
+ filter,
43
+ });
44
+ {{/if}}
45
+
46
+ if (searchResults.length === 0) {
47
+ return NextResponse.json({
48
+ results: [],
49
+ meta: {
50
+ model: '{{model}}',
51
+ took: Date.now() - start,
52
+ },
53
+ });
54
+ }
55
+
56
+ {{#if rerank}}
57
+ // Step 3: Rerank for better relevance
58
+ const documents = searchResults.map(r => r.document.text);
59
+ const { results: reranked } = await rerank(query, documents, { topK: limit });
60
+
61
+ const results = reranked.map(r => ({
62
+ text: searchResults[r.index].document.text,
63
+ score: r.relevanceScore,
64
+ metadata: searchResults[r.index].document.metadata,
65
+ }));
66
+ {{else}}
67
+ const results = searchResults.map(r => ({
68
+ text: r.document.text,
69
+ score: r.score,
70
+ metadata: r.document.metadata,
71
+ }));
72
+ {{/if}}
73
+
74
+ const response = {
75
+ results,
76
+ meta: {
77
+ model: '{{model}}',
78
+ {{#if rerank}}
79
+ rerankModel: '{{rerankModel}}',
80
+ {{/if}}
81
+ took: Date.now() - start,
82
+ },
83
+ };
84
+
85
+ if (includeContext) {
86
+ response.context = results.map(r => r.text).join('\n\n---\n\n');
87
+ }
88
+
89
+ return NextResponse.json(response);
90
+ } catch (error) {
91
+ console.error('Search error:', error);
92
+ return NextResponse.json(
93
+ { error: error.message },
94
+ { status: 500 }
95
+ );
96
+ }
97
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * MUI Theme Configuration
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import { createTheme } from '@mui/material/styles';
9
+
10
+ // MongoDB-inspired color palette
11
+ const palette = {
12
+ primary: {
13
+ main: '#00ED64', // MongoDB Green
14
+ light: '#4DFF94',
15
+ dark: '#00B84A',
16
+ contrastText: '#000',
17
+ },
18
+ secondary: {
19
+ main: '#001E2B', // MongoDB Dark
20
+ light: '#1C3A4B',
21
+ dark: '#000F17',
22
+ contrastText: '#fff',
23
+ },
24
+ background: {
25
+ default: '#FAFAFA',
26
+ paper: '#FFFFFF',
27
+ },
28
+ text: {
29
+ primary: '#001E2B',
30
+ secondary: '#5C6C75',
31
+ },
32
+ };
33
+
34
+ export const theme = createTheme({
35
+ palette,
36
+ typography: {
37
+ fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
38
+ h1: {
39
+ fontWeight: 700,
40
+ },
41
+ h2: {
42
+ fontWeight: 600,
43
+ },
44
+ h3: {
45
+ fontWeight: 600,
46
+ },
47
+ h4: {
48
+ fontWeight: 600,
49
+ },
50
+ },
51
+ shape: {
52
+ borderRadius: 8,
53
+ },
54
+ components: {
55
+ MuiButton: {
56
+ styleOverrides: {
57
+ root: {
58
+ textTransform: 'none',
59
+ fontWeight: 600,
60
+ },
61
+ contained: {
62
+ boxShadow: 'none',
63
+ '&:hover': {
64
+ boxShadow: 'none',
65
+ },
66
+ },
67
+ },
68
+ },
69
+ MuiCard: {
70
+ styleOverrides: {
71
+ root: {
72
+ boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.06)',
73
+ },
74
+ },
75
+ },
76
+ MuiChip: {
77
+ styleOverrides: {
78
+ root: {
79
+ fontWeight: 500,
80
+ },
81
+ },
82
+ },
83
+ },
84
+ });
@@ -0,0 +1,145 @@
1
+ # {{projectName}}
2
+
3
+ A semantic search API powered by Voyage AI embeddings and MongoDB Atlas Vector Search.
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. Create virtual environment
21
+
22
+ ```bash
23
+ python -m venv venv
24
+ source venv/bin/activate # On Windows: venv\Scripts\activate
25
+ ```
26
+
27
+ ### 2. Install dependencies
28
+
29
+ ```bash
30
+ pip install -r requirements.txt
31
+ ```
32
+
33
+ ### 3. Configure environment
34
+
35
+ Copy `.env.example` to `.env` and fill in your credentials:
36
+
37
+ ```bash
38
+ cp .env.example .env
39
+ ```
40
+
41
+ Required variables:
42
+ - `VOYAGE_API_KEY` - Your Voyage AI API key from [dash.voyageai.com](https://dash.voyageai.com)
43
+ - `MONGODB_URI` - Your MongoDB Atlas connection string
44
+
45
+ ### 4. Create vector index
46
+
47
+ In MongoDB Atlas, create a vector search index on your collection:
48
+
49
+ ```json
50
+ {
51
+ "fields": [
52
+ {
53
+ "type": "vector",
54
+ "path": "{{field}}",
55
+ "numDimensions": {{dimensions}},
56
+ "similarity": "cosine"
57
+ }
58
+ ]
59
+ }
60
+ ```
61
+
62
+ Name the index `{{index}}`.
63
+
64
+ ### 5. Start the server
65
+
66
+ ```bash
67
+ python app.py
68
+
69
+ # For production
70
+ gunicorn -w 4 -b 0.0.0.0:3000 app:app
71
+ ```
72
+
73
+ ## API Endpoints
74
+
75
+ ### POST /api/search
76
+
77
+ Search for relevant documents.
78
+
79
+ ```bash
80
+ curl -X POST http://localhost:3000/api/search \
81
+ -H "Content-Type: application/json" \
82
+ -d '{"query": "How does vector search work?", "limit": 5}'
83
+ ```
84
+
85
+ Response:
86
+ ```json
87
+ {
88
+ "results": [
89
+ {
90
+ "text": "Vector search uses...",
91
+ "score": 0.95,
92
+ "metadata": {"source": "docs/guide.md"}
93
+ }
94
+ ],
95
+ "meta": {
96
+ "model": "{{model}}",
97
+ "took": 123
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### POST /api/ingest
103
+
104
+ Ingest text documents.
105
+
106
+ ```bash
107
+ curl -X POST http://localhost:3000/api/ingest \
108
+ -H "Content-Type: application/json" \
109
+ -d '{"text": "Your document content...", "metadata": {"source": "api"}}'
110
+ ```
111
+
112
+ ### GET /api/health
113
+
114
+ Check API health and database connection.
115
+
116
+ ```bash
117
+ curl http://localhost:3000/api/health
118
+ ```
119
+
120
+ ## Project Structure
121
+
122
+ ```
123
+ {{projectName}}/
124
+ ├── app.py # Flask application
125
+ ├── voyage_client.py # Voyage AI API client
126
+ ├── mongo_client.py # MongoDB connection helper
127
+ ├── chunker.py # Text chunking utilities
128
+ ├── requirements.txt
129
+ ├── .env.example
130
+ └── README.md
131
+ ```
132
+
133
+ ## Chunking Configuration
134
+
135
+ Documents are chunked with the following settings:
136
+
137
+ | Setting | Value |
138
+ |---------|-------|
139
+ | Strategy | `{{chunkStrategy}}` |
140
+ | Chunk Size | {{chunkSize}} characters |
141
+ | Overlap | {{chunkOverlap}} characters |
142
+
143
+ ---
144
+
145
+ Generated by [vai](https://github.com/mrlynn/voyageai-cli) v{{vaiVersion}}