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.
- package/CHANGELOG.md +142 -26
- package/README.md +153 -2
- package/package.json +1 -1
- package/src/cli.js +12 -0
- package/src/commands/about.js +39 -3
- package/src/commands/doctor.js +325 -0
- package/src/commands/eval.js +420 -10
- package/src/commands/generate.js +220 -0
- package/src/commands/playground.js +93 -0
- package/src/commands/purge.js +271 -0
- package/src/commands/quickstart.js +203 -0
- package/src/commands/refresh.js +322 -0
- package/src/commands/scaffold.js +217 -0
- package/src/lib/codegen.js +313 -0
- package/src/lib/explanations.js +163 -1
- package/src/lib/scaffold-structure.js +114 -0
- package/src/lib/templates/nextjs/README.md.tpl +106 -0
- package/src/lib/templates/nextjs/env.example.tpl +8 -0
- package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
- package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
- package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
- package/src/lib/templates/nextjs/package.json.tpl +33 -0
- package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
- package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
- package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
- package/src/lib/templates/nextjs/theme.js.tpl +84 -0
- package/src/lib/templates/python/README.md.tpl +145 -0
- package/src/lib/templates/python/app.py.tpl +221 -0
- package/src/lib/templates/python/chunker.py.tpl +127 -0
- package/src/lib/templates/python/env.example.tpl +12 -0
- package/src/lib/templates/python/mongo_client.py.tpl +125 -0
- package/src/lib/templates/python/requirements.txt.tpl +10 -0
- package/src/lib/templates/python/voyage_client.py.tpl +124 -0
- package/src/lib/templates/vanilla/README.md.tpl +156 -0
- package/src/lib/templates/vanilla/client.js.tpl +103 -0
- package/src/lib/templates/vanilla/connection.js.tpl +126 -0
- package/src/lib/templates/vanilla/env.example.tpl +11 -0
- package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
- package/src/lib/templates/vanilla/package.json.tpl +31 -0
- package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
- package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
- package/src/lib/templates/vanilla/server.js.tpl +81 -0
- package/src/lib/zip.js +130 -0
- 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
|