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,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}}
@@ -0,0 +1,221 @@
1
+ """
2
+ Voyage AI RAG API (Flask)
3
+ Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+
5
+ Endpoints:
6
+ - POST /api/search - Semantic search with optional reranking
7
+ - POST /api/ingest - Document ingestion
8
+ - GET /api/health - Health check
9
+ """
10
+
11
+ import os
12
+ import time
13
+ from flask import Flask, request, jsonify
14
+ from dotenv import load_dotenv
15
+
16
+ load_dotenv()
17
+
18
+ from voyage_client import embed_query{{#if rerank}}, rerank{{/if}}
19
+ from mongo_client import connect, vector_search, insert_documents, close
20
+ from chunker import chunk_text
21
+
22
+ app = Flask(__name__)
23
+
24
+
25
+ @app.route("/")
26
+ def index():
27
+ """API information."""
28
+ return jsonify({
29
+ "name": "{{projectName}}",
30
+ "description": "Voyage AI RAG API",
31
+ "model": "{{model}}",
32
+ "database": "{{db}}",
33
+ "collection": "{{collection}}",
34
+ "endpoints": {
35
+ "search": "POST /api/search",
36
+ "ingest": "POST /api/ingest",
37
+ "health": "GET /api/health",
38
+ },
39
+ })
40
+
41
+
42
+ @app.route("/api/search", methods=["POST"])
43
+ def search():
44
+ """
45
+ Semantic search endpoint.
46
+
47
+ Request body:
48
+ {
49
+ "query": "What is vector search?",
50
+ "limit": 5,
51
+ "include_context": true
52
+ }
53
+ """
54
+ start = time.time()
55
+ data = request.get_json()
56
+
57
+ query = data.get("query")
58
+ if not query:
59
+ return jsonify({"error": "Query is required"}), 400
60
+
61
+ limit = data.get("limit", 5)
62
+ {{#if rerank}}
63
+ candidates = data.get("candidates", 20)
64
+ {{/if}}
65
+ include_context = data.get("include_context", False)
66
+ filter_query = data.get("filter")
67
+
68
+ # Step 1: Embed the query
69
+ query_embedding = embed_query(query)
70
+
71
+ # Step 2: Vector search
72
+ {{#if rerank}}
73
+ search_results = vector_search(query_embedding, limit=candidates, filter_query=filter_query)
74
+ {{else}}
75
+ search_results = vector_search(query_embedding, limit=limit, filter_query=filter_query)
76
+ {{/if}}
77
+
78
+ if not search_results:
79
+ return jsonify({
80
+ "results": [],
81
+ "meta": {"model": "{{model}}", "took": int((time.time() - start) * 1000)},
82
+ })
83
+
84
+ {{#if rerank}}
85
+ # Step 3: Rerank for better relevance
86
+ documents = [r["document"]["text"] for r in search_results]
87
+ rerank_result = rerank(query, documents, top_k=limit)
88
+
89
+ results = [
90
+ {
91
+ "text": search_results[r["index"]]["document"]["text"],
92
+ "score": r["relevance_score"],
93
+ "metadata": search_results[r["index"]]["document"].get("metadata", {}),
94
+ }
95
+ for r in rerank_result["results"]
96
+ ]
97
+ {{else}}
98
+ results = [
99
+ {
100
+ "text": r["document"]["text"],
101
+ "score": r["score"],
102
+ "metadata": r["document"].get("metadata", {}),
103
+ }
104
+ for r in search_results
105
+ ]
106
+ {{/if}}
107
+
108
+ response = {
109
+ "results": results,
110
+ "meta": {
111
+ "model": "{{model}}",
112
+ {{#if rerank}}
113
+ "rerank_model": "{{rerankModel}}",
114
+ {{/if}}
115
+ "took": int((time.time() - start) * 1000),
116
+ },
117
+ }
118
+
119
+ if include_context:
120
+ response["context"] = "\n\n---\n\n".join(r["text"] for r in results)
121
+
122
+ return jsonify(response)
123
+
124
+
125
+ @app.route("/api/ingest", methods=["POST"])
126
+ def ingest():
127
+ """
128
+ Document ingestion endpoint.
129
+
130
+ Request body:
131
+ {
132
+ "text": "Document content...",
133
+ "metadata": {"source": "api"}
134
+ }
135
+ """
136
+ start = time.time()
137
+ data = request.get_json()
138
+
139
+ text = data.get("text")
140
+ if not text:
141
+ return jsonify({"error": "Text is required"}), 400
142
+
143
+ metadata = data.get("metadata", {})
144
+ metadata["source"] = metadata.get("source", "api")
145
+
146
+ # Chunk the text
147
+ chunks = chunk_text(text)
148
+
149
+ # Embed all chunks
150
+ from voyage_client import embed
151
+ result = embed(chunks, input_type="document")
152
+ embeddings = result["embeddings"]
153
+
154
+ # Prepare documents
155
+ documents = [
156
+ {
157
+ "text": chunk,
158
+ "embedding": embeddings[i],
159
+ "metadata": {
160
+ **metadata,
161
+ "chunk_index": i,
162
+ "total_chunks": len(chunks),
163
+ },
164
+ }
165
+ for i, chunk in enumerate(chunks)
166
+ ]
167
+
168
+ # Insert into MongoDB
169
+ insert_result = insert_documents(documents)
170
+
171
+ return jsonify({
172
+ "success": True,
173
+ "chunks": len(chunks),
174
+ "tokens": result["usage"]["total_tokens"],
175
+ "took": int((time.time() - start) * 1000),
176
+ })
177
+
178
+
179
+ @app.route("/api/health", methods=["GET"])
180
+ def health():
181
+ """Health check endpoint."""
182
+ try:
183
+ connect()
184
+ return jsonify({
185
+ "status": "healthy",
186
+ "model": "{{model}}",
187
+ "database": "{{db}}",
188
+ "collection": "{{collection}}",
189
+ })
190
+ except Exception as e:
191
+ return jsonify({
192
+ "status": "unhealthy",
193
+ "error": str(e),
194
+ }), 503
195
+
196
+
197
+ @app.teardown_appcontext
198
+ def shutdown(exception=None):
199
+ """Clean up on shutdown."""
200
+ pass # Connection pooling handles this
201
+
202
+
203
+ if __name__ == "__main__":
204
+ port = int(os.getenv("PORT", 3000))
205
+ print(f"""
206
+ 🚀 Voyage AI RAG Server (Flask)
207
+
208
+ Model: {{model}}
209
+ Database: {{db}}.{{collection}}
210
+ {{#if rerank}}
211
+ Reranking: {{rerankModel}}
212
+ {{/if}}
213
+
214
+ Endpoints:
215
+ POST /api/search - Semantic search
216
+ POST /api/ingest - Document ingestion
217
+ GET /api/health - Health check
218
+
219
+ Running on http://localhost:{port}
220
+ """)
221
+ app.run(host="0.0.0.0", port=port, debug=os.getenv("FLASK_DEBUG", "0") == "1")
@@ -0,0 +1,127 @@
1
+ """
2
+ Text Chunker
3
+ Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+
5
+ Strategy: {{chunkStrategy}}
6
+ Size: {{chunkSize}} characters
7
+ Overlap: {{chunkOverlap}} characters
8
+ """
9
+
10
+ from typing import List
11
+
12
+
13
+ def chunk_text(
14
+ text: str,
15
+ size: int = {{chunkSize}},
16
+ overlap: int = {{chunkOverlap}},
17
+ ) -> List[str]:
18
+ """
19
+ Chunk text using recursive splitting with smart boundaries.
20
+
21
+ Args:
22
+ text: Text to chunk
23
+ size: Maximum chunk size in characters
24
+ overlap: Overlap between chunks
25
+
26
+ Returns:
27
+ List of text chunks
28
+ """
29
+ separators = ["\n\n", "\n", ". ", " "]
30
+
31
+ def split_recursive(text: str, sep_index: int = 0) -> List[str]:
32
+ if len(text) <= size:
33
+ stripped = text.strip()
34
+ return [stripped] if stripped else []
35
+
36
+ if sep_index >= len(separators):
37
+ # Fall back to fixed-size split
38
+ chunks = []
39
+ start = 0
40
+ while start < len(text):
41
+ chunk = text[start:start + size].strip()
42
+ if chunk:
43
+ chunks.append(chunk)
44
+ start += size - overlap
45
+ return chunks
46
+
47
+ separator = separators[sep_index]
48
+ parts = text.split(separator)
49
+ chunks = []
50
+ current = ""
51
+
52
+ for part in parts:
53
+ potential = f"{current}{separator}{part}" if current else part
54
+
55
+ if len(potential) <= size:
56
+ current = potential
57
+ else:
58
+ if current:
59
+ chunks.extend(split_recursive(current, sep_index + 1))
60
+ current = part
61
+
62
+ if current:
63
+ chunks.extend(split_recursive(current, sep_index + 1))
64
+
65
+ return chunks
66
+
67
+ return split_recursive(text)
68
+
69
+
70
+ def chunk_markdown(text: str, size: int = {{chunkSize}}, overlap: int = {{chunkOverlap}}) -> List[str]:
71
+ """
72
+ Chunk markdown with header awareness.
73
+ Tries to keep sections together when possible.
74
+ """
75
+ import re
76
+
77
+ # Split by headers (## or ###)
78
+ header_pattern = r"(^#{1,3}\s+.+$)"
79
+ parts = re.split(header_pattern, text, flags=re.MULTILINE)
80
+
81
+ chunks = []
82
+ current = ""
83
+
84
+ for part in parts:
85
+ if not part.strip():
86
+ continue
87
+
88
+ potential = f"{current}\n\n{part}" if current else part
89
+
90
+ if len(potential) <= size:
91
+ current = potential
92
+ else:
93
+ if current:
94
+ # Chunk the current section
95
+ chunks.extend(chunk_text(current, size, overlap))
96
+ current = part
97
+
98
+ if current:
99
+ chunks.extend(chunk_text(current, size, overlap))
100
+
101
+ return chunks
102
+
103
+
104
+ if __name__ == "__main__":
105
+ # Test chunking
106
+ sample = """
107
+ # Introduction
108
+
109
+ This is a sample document for testing the chunker.
110
+ It contains multiple paragraphs and sections.
111
+
112
+ ## Section 1
113
+
114
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
115
+ Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
116
+
117
+ ## Section 2
118
+
119
+ Ut enim ad minim veniam, quis nostrud exercitation ullamco.
120
+ Duis aute irure dolor in reprehenderit in voluptate velit.
121
+ """
122
+
123
+ chunks = chunk_text(sample)
124
+ print(f"Created {len(chunks)} chunks:")
125
+ for i, chunk in enumerate(chunks):
126
+ print(f"\n--- Chunk {i + 1} ({len(chunk)} chars) ---")
127
+ print(chunk[:200] + "..." if len(chunk) > 200 else chunk)
@@ -0,0 +1,12 @@
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
+ # Server
8
+ PORT=3000
9
+ FLASK_DEBUG=0
10
+
11
+ # Optional: Override defaults
12
+ # VOYAGE_API_URL=https://api.voyageai.com/v1