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.
- package/CHANGELOG.md +142 -26
- package/README.md +130 -2
- package/package.json +3 -2
- package/src/cli.js +10 -0
- package/src/commands/bug.js +249 -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/refresh.js +322 -0
- package/src/commands/scaffold.js +217 -0
- package/src/lib/codegen.js +339 -0
- package/src/lib/explanations.js +155 -0
- 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 +708 -3
|
@@ -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
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB Connection Helper
|
|
3
|
+
Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
|
|
5
|
+
Database: {{db}}
|
|
6
|
+
Collection: {{collection}}
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import List, Dict, Any, Optional
|
|
12
|
+
from pymongo import MongoClient
|
|
13
|
+
|
|
14
|
+
MONGODB_URI = os.getenv("MONGODB_URI")
|
|
15
|
+
|
|
16
|
+
if not MONGODB_URI:
|
|
17
|
+
raise ValueError("MONGODB_URI environment variable is required")
|
|
18
|
+
|
|
19
|
+
# Module-level connection (singleton pattern)
|
|
20
|
+
_client: Optional[MongoClient] = None
|
|
21
|
+
_db = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def connect():
|
|
25
|
+
"""Connect to MongoDB and return the database instance."""
|
|
26
|
+
global _client, _db
|
|
27
|
+
|
|
28
|
+
if _db is not None:
|
|
29
|
+
return _db
|
|
30
|
+
|
|
31
|
+
_client = MongoClient(MONGODB_URI)
|
|
32
|
+
_db = _client["{{db}}"]
|
|
33
|
+
print(f"Connected to MongoDB: {{db}}")
|
|
34
|
+
return _db
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_collection():
|
|
38
|
+
"""Get the documents collection."""
|
|
39
|
+
db = connect()
|
|
40
|
+
return db["{{collection}}"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def close():
|
|
44
|
+
"""Close the MongoDB connection."""
|
|
45
|
+
global _client, _db
|
|
46
|
+
if _client:
|
|
47
|
+
_client.close()
|
|
48
|
+
_client = None
|
|
49
|
+
_db = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def vector_search(
|
|
53
|
+
embedding: List[float],
|
|
54
|
+
limit: int = 10,
|
|
55
|
+
num_candidates: Optional[int] = None,
|
|
56
|
+
filter_query: Optional[Dict] = None,
|
|
57
|
+
) -> List[Dict[str, Any]]:
|
|
58
|
+
"""
|
|
59
|
+
Perform a vector search on the collection.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
embedding: Query embedding vector
|
|
63
|
+
limit: Number of results to return
|
|
64
|
+
num_candidates: Candidates to consider (default: limit * 10)
|
|
65
|
+
filter_query: Optional MongoDB filter
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of documents with scores
|
|
69
|
+
"""
|
|
70
|
+
collection = get_collection()
|
|
71
|
+
num_candidates = num_candidates or limit * 10
|
|
72
|
+
|
|
73
|
+
pipeline = [
|
|
74
|
+
{
|
|
75
|
+
"$vectorSearch": {
|
|
76
|
+
"index": "{{index}}",
|
|
77
|
+
"path": "{{field}}",
|
|
78
|
+
"queryVector": embedding,
|
|
79
|
+
"numCandidates": num_candidates,
|
|
80
|
+
"limit": limit,
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"$project": {
|
|
85
|
+
"_id": 1,
|
|
86
|
+
"text": 1,
|
|
87
|
+
"metadata": 1,
|
|
88
|
+
"score": {"$meta": "vectorSearchScore"},
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# Add filter if provided
|
|
94
|
+
if filter_query:
|
|
95
|
+
pipeline[0]["$vectorSearch"]["filter"] = filter_query
|
|
96
|
+
|
|
97
|
+
results = list(collection.aggregate(pipeline))
|
|
98
|
+
return [{"document": doc, "score": doc.pop("score")} for doc in results]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def insert_documents(docs: List[Dict[str, Any]]) -> Dict[str, int]:
|
|
102
|
+
"""
|
|
103
|
+
Insert documents with embeddings.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
docs: List of dicts with 'text', 'embedding', and optional 'metadata'
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Dict with 'inserted_count'
|
|
110
|
+
"""
|
|
111
|
+
collection = get_collection()
|
|
112
|
+
|
|
113
|
+
documents = [
|
|
114
|
+
{
|
|
115
|
+
"text": doc["text"],
|
|
116
|
+
"{{field}}": doc["embedding"],
|
|
117
|
+
"metadata": doc.get("metadata", {}),
|
|
118
|
+
"_embeddedAt": datetime.utcnow(),
|
|
119
|
+
"_model": "{{model}}",
|
|
120
|
+
}
|
|
121
|
+
for doc in docs
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
result = collection.insert_many(documents)
|
|
125
|
+
return {"inserted_count": len(result.inserted_ids)}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Voyage AI API Client
|
|
3
|
+
Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
|
|
5
|
+
Model: {{model}}
|
|
6
|
+
Dimensions: {{dimensions}}
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import requests
|
|
11
|
+
from typing import List, Optional, Dict, Any
|
|
12
|
+
|
|
13
|
+
VOYAGE_API_URL = os.getenv("VOYAGE_API_URL", "https://api.voyageai.com/v1")
|
|
14
|
+
VOYAGE_API_KEY = os.getenv("VOYAGE_API_KEY")
|
|
15
|
+
|
|
16
|
+
if not VOYAGE_API_KEY:
|
|
17
|
+
raise ValueError("VOYAGE_API_KEY environment variable is required")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def embed(
|
|
21
|
+
texts: List[str],
|
|
22
|
+
model: str = "{{model}}",
|
|
23
|
+
input_type: str = "{{inputType}}",
|
|
24
|
+
output_dimension: int = {{dimensions}},
|
|
25
|
+
) -> Dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Generate embeddings for a list of texts using Voyage AI.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
texts: List of texts to embed
|
|
31
|
+
model: Embedding model name
|
|
32
|
+
input_type: 'document' or 'query'
|
|
33
|
+
output_dimension: Output vector dimensions
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Dict with 'embeddings' (list of vectors) and 'usage' (token counts)
|
|
37
|
+
"""
|
|
38
|
+
response = requests.post(
|
|
39
|
+
f"{VOYAGE_API_URL}/embeddings",
|
|
40
|
+
headers={
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
"Authorization": f"Bearer {VOYAGE_API_KEY}",
|
|
43
|
+
},
|
|
44
|
+
json={
|
|
45
|
+
"model": model,
|
|
46
|
+
"input": texts,
|
|
47
|
+
"input_type": input_type,
|
|
48
|
+
"output_dimension": output_dimension,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
response.raise_for_status()
|
|
53
|
+
data = response.json()
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"embeddings": [d["embedding"] for d in data["data"]],
|
|
57
|
+
"usage": data["usage"],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def embed_query(query: str, **kwargs) -> List[float]:
|
|
62
|
+
"""Embed a single query, returning the embedding vector."""
|
|
63
|
+
result = embed([query], input_type="query", **kwargs)
|
|
64
|
+
return result["embeddings"][0]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def embed_documents(documents: List[str], **kwargs) -> List[List[float]]:
|
|
68
|
+
"""Embed multiple documents, returning list of embedding vectors."""
|
|
69
|
+
result = embed(documents, input_type="document", **kwargs)
|
|
70
|
+
return result["embeddings"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
{{#if rerank}}
|
|
74
|
+
def rerank(
|
|
75
|
+
query: str,
|
|
76
|
+
documents: List[str],
|
|
77
|
+
model: str = "{{rerankModel}}",
|
|
78
|
+
top_k: Optional[int] = None,
|
|
79
|
+
) -> Dict[str, Any]:
|
|
80
|
+
"""
|
|
81
|
+
Rerank documents by relevance to a query.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
query: The query to rank against
|
|
85
|
+
documents: List of documents to rerank
|
|
86
|
+
model: Rerank model name
|
|
87
|
+
top_k: Number of top results to return
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Dict with 'results' containing index, relevance_score, and document
|
|
91
|
+
"""
|
|
92
|
+
payload = {
|
|
93
|
+
"model": model,
|
|
94
|
+
"query": query,
|
|
95
|
+
"documents": documents,
|
|
96
|
+
"return_documents": True,
|
|
97
|
+
}
|
|
98
|
+
if top_k:
|
|
99
|
+
payload["top_k"] = top_k
|
|
100
|
+
|
|
101
|
+
response = requests.post(
|
|
102
|
+
f"{VOYAGE_API_URL}/rerank",
|
|
103
|
+
headers={
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
"Authorization": f"Bearer {VOYAGE_API_KEY}",
|
|
106
|
+
},
|
|
107
|
+
json=payload,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
data = response.json()
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"results": [
|
|
115
|
+
{
|
|
116
|
+
"index": d["index"],
|
|
117
|
+
"relevance_score": d["relevance_score"],
|
|
118
|
+
"document": d.get("document"),
|
|
119
|
+
}
|
|
120
|
+
for d in data["data"]
|
|
121
|
+
],
|
|
122
|
+
"usage": data.get("usage"),
|
|
123
|
+
}
|
|
124
|
+
{{/if}}
|