@opensaas/stack-rag 0.1.6
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +10 -0
- package/CLAUDE.md +565 -0
- package/LICENSE +21 -0
- package/README.md +406 -0
- package/dist/config/index.d.ts +63 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +94 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/plugin.d.ts +38 -0
- package/dist/config/plugin.d.ts.map +1 -0
- package/dist/config/plugin.js +215 -0
- package/dist/config/plugin.js.map +1 -0
- package/dist/config/plugin.test.d.ts +2 -0
- package/dist/config/plugin.test.d.ts.map +1 -0
- package/dist/config/plugin.test.js +554 -0
- package/dist/config/plugin.test.js.map +1 -0
- package/dist/config/types.d.ts +249 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +5 -0
- package/dist/config/types.js.map +1 -0
- package/dist/fields/embedding.d.ts +85 -0
- package/dist/fields/embedding.d.ts.map +1 -0
- package/dist/fields/embedding.js +81 -0
- package/dist/fields/embedding.js.map +1 -0
- package/dist/fields/embedding.test.d.ts +2 -0
- package/dist/fields/embedding.test.d.ts.map +1 -0
- package/dist/fields/embedding.test.js +323 -0
- package/dist/fields/embedding.test.js.map +1 -0
- package/dist/fields/index.d.ts +6 -0
- package/dist/fields/index.d.ts.map +1 -0
- package/dist/fields/index.js +5 -0
- package/dist/fields/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +19 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +18 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/providers/index.d.ts +38 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +68 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ollama.d.ts +49 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +151 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai.d.ts +41 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +126 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/providers.test.d.ts +2 -0
- package/dist/providers/providers.test.d.ts.map +1 -0
- package/dist/providers/providers.test.js +224 -0
- package/dist/providers/providers.test.js.map +1 -0
- package/dist/providers/types.d.ts +88 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/runtime/batch.d.ts +183 -0
- package/dist/runtime/batch.d.ts.map +1 -0
- package/dist/runtime/batch.js +240 -0
- package/dist/runtime/batch.js.map +1 -0
- package/dist/runtime/batch.test.d.ts +2 -0
- package/dist/runtime/batch.test.d.ts.map +1 -0
- package/dist/runtime/batch.test.js +251 -0
- package/dist/runtime/batch.test.js.map +1 -0
- package/dist/runtime/chunking.d.ts +42 -0
- package/dist/runtime/chunking.d.ts.map +1 -0
- package/dist/runtime/chunking.js +264 -0
- package/dist/runtime/chunking.js.map +1 -0
- package/dist/runtime/chunking.test.d.ts +2 -0
- package/dist/runtime/chunking.test.d.ts.map +1 -0
- package/dist/runtime/chunking.test.js +212 -0
- package/dist/runtime/chunking.test.js.map +1 -0
- package/dist/runtime/embeddings.d.ts +147 -0
- package/dist/runtime/embeddings.d.ts.map +1 -0
- package/dist/runtime/embeddings.js +201 -0
- package/dist/runtime/embeddings.js.map +1 -0
- package/dist/runtime/embeddings.test.d.ts +2 -0
- package/dist/runtime/embeddings.test.d.ts.map +1 -0
- package/dist/runtime/embeddings.test.js +366 -0
- package/dist/runtime/embeddings.test.js.map +1 -0
- package/dist/runtime/index.d.ts +14 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +18 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/search.d.ts +135 -0
- package/dist/runtime/search.d.ts.map +1 -0
- package/dist/runtime/search.js +101 -0
- package/dist/runtime/search.js.map +1 -0
- package/dist/storage/index.d.ts +41 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +73 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/json.d.ts +34 -0
- package/dist/storage/json.d.ts.map +1 -0
- package/dist/storage/json.js +82 -0
- package/dist/storage/json.js.map +1 -0
- package/dist/storage/pgvector.d.ts +53 -0
- package/dist/storage/pgvector.d.ts.map +1 -0
- package/dist/storage/pgvector.js +168 -0
- package/dist/storage/pgvector.js.map +1 -0
- package/dist/storage/sqlite-vss.d.ts +49 -0
- package/dist/storage/sqlite-vss.d.ts.map +1 -0
- package/dist/storage/sqlite-vss.js +148 -0
- package/dist/storage/sqlite-vss.js.map +1 -0
- package/dist/storage/storage.test.d.ts +2 -0
- package/dist/storage/storage.test.d.ts.map +1 -0
- package/dist/storage/storage.test.js +440 -0
- package/dist/storage/storage.test.js.map +1 -0
- package/dist/storage/types.d.ts +79 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +49 -0
- package/dist/storage/types.js.map +1 -0
- package/package.json +82 -0
- package/src/config/index.ts +116 -0
- package/src/config/plugin.test.ts +664 -0
- package/src/config/plugin.ts +257 -0
- package/src/config/types.ts +283 -0
- package/src/fields/embedding.test.ts +408 -0
- package/src/fields/embedding.ts +150 -0
- package/src/fields/index.ts +6 -0
- package/src/index.ts +33 -0
- package/src/mcp/index.ts +21 -0
- package/src/providers/index.ts +81 -0
- package/src/providers/ollama.ts +186 -0
- package/src/providers/openai.ts +161 -0
- package/src/providers/providers.test.ts +275 -0
- package/src/providers/types.ts +100 -0
- package/src/runtime/batch.test.ts +332 -0
- package/src/runtime/batch.ts +424 -0
- package/src/runtime/chunking.test.ts +258 -0
- package/src/runtime/chunking.ts +334 -0
- package/src/runtime/embeddings.test.ts +441 -0
- package/src/runtime/embeddings.ts +380 -0
- package/src/runtime/index.ts +51 -0
- package/src/runtime/search.ts +243 -0
- package/src/storage/index.ts +86 -0
- package/src/storage/json.ts +106 -0
- package/src/storage/pgvector.ts +206 -0
- package/src/storage/sqlite-vss.ts +193 -0
- package/src/storage/storage.test.ts +521 -0
- package/src/storage/types.ts +126 -0
- package/tsconfig.json +13 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +18 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @opensaas/stack-rag
|
|
2
|
+
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 39996ca: Fix missing StoredEmbedding type import in generated types. Fields can now declare TypeScript imports needed for their types via the new `getTypeScriptImports()` method. This resolves the type error where `StoredEmbedding` was referenced but not imported in the generated `.opensaas/types.ts` file.
|
|
8
|
+
- Updated dependencies [39996ca]
|
|
9
|
+
- Updated dependencies [39996ca]
|
|
10
|
+
- @opensaas/stack-core@0.1.6
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
# @opensaas/stack-rag
|
|
2
|
+
|
|
3
|
+
RAG (Retrieval-Augmented Generation) and AI embeddings integration for OpenSaas Stack. Turn your OpenSaas app into a knowledge base with semantic search capabilities.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
Adds vector embeddings and semantic search to OpenSaas Stack apps with minimal configuration. Supports multiple embedding providers (OpenAI, Ollama) and vector storage backends (pgvector, SQLite VSS, JSON).
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
- **Multiple abstraction levels**: From automatic "magic" RAG to low-level primitives
|
|
12
|
+
- **Embedding providers**: OpenAI, Ollama (local), extensible for custom providers
|
|
13
|
+
- **Vector storage**: pgvector, sqlite-vss, JSON-based (for development)
|
|
14
|
+
- **MCP integration**: Automatic semantic search tools for AI assistants
|
|
15
|
+
- **Access control**: All searches respect existing access control rules
|
|
16
|
+
- **Automatic embedding generation**: Via hooks when source fields change
|
|
17
|
+
|
|
18
|
+
## Package Structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
packages/rag/
|
|
22
|
+
├── src/
|
|
23
|
+
│ ├── config/ # withRAG(), ragConfig()
|
|
24
|
+
│ ├── fields/ # embedding() field type
|
|
25
|
+
│ ├── providers/ # OpenAI, Ollama embedding providers
|
|
26
|
+
│ ├── storage/ # pgvector, sqlite-vss, JSON backends
|
|
27
|
+
│ ├── runtime/ # generateEmbeddings(), semanticSearch()
|
|
28
|
+
│ └── mcp/ # MCP tool generators
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Package Exports
|
|
32
|
+
|
|
33
|
+
### Main exports (`@opensaas/stack-rag`)
|
|
34
|
+
|
|
35
|
+
- `withRAG(config, ragConfig)` - Wrap OpenSaas config with RAG integration
|
|
36
|
+
- `ragConfig({ ... })` - RAG configuration builder
|
|
37
|
+
- `openaiEmbeddings({ ... })` - OpenAI provider config helper
|
|
38
|
+
- `ollamaEmbeddings({ ... })` - Ollama provider config helper
|
|
39
|
+
- `pgvectorStorage({ ... })` - pgvector backend config helper
|
|
40
|
+
- `sqliteVssStorage({ ... })` - SQLite VSS backend config helper
|
|
41
|
+
- `jsonStorage()` - JSON-based storage config helper
|
|
42
|
+
|
|
43
|
+
### Fields (`@opensaas/stack-rag/fields`)
|
|
44
|
+
|
|
45
|
+
- `embedding({ ... })` - Vector embedding field type
|
|
46
|
+
|
|
47
|
+
### Providers (`@opensaas/stack-rag/providers`)
|
|
48
|
+
|
|
49
|
+
- `createEmbeddingProvider(config)` - Factory for embedding providers
|
|
50
|
+
- `OpenAIEmbeddingProvider` - OpenAI implementation
|
|
51
|
+
- `OllamaEmbeddingProvider` - Ollama implementation
|
|
52
|
+
- `registerEmbeddingProvider(type, factory)` - Register custom providers
|
|
53
|
+
|
|
54
|
+
### Storage (`@opensaas/stack-rag/storage`)
|
|
55
|
+
|
|
56
|
+
- `createVectorStorage(config)` - Factory for storage backends
|
|
57
|
+
- `JsonVectorStorage` - JSON-based storage (development)
|
|
58
|
+
- `PgVectorStorage` - PostgreSQL pgvector storage
|
|
59
|
+
- `SqliteVssStorage` - SQLite VSS storage
|
|
60
|
+
- `registerVectorStorage(type, factory)` - Register custom backends
|
|
61
|
+
|
|
62
|
+
### Runtime (`@opensaas/stack-rag/runtime`)
|
|
63
|
+
|
|
64
|
+
- `generateEmbeddings(config, text, provider)` - Generate embeddings
|
|
65
|
+
- `semanticSearch(context, query, options)` - Search by natural language
|
|
66
|
+
- `findSimilar(context, itemId, options)` - Find similar items
|
|
67
|
+
- `chunkText(text, strategy)` - Text chunking utilities
|
|
68
|
+
|
|
69
|
+
### MCP (`@opensaas/stack-rag/mcp`)
|
|
70
|
+
|
|
71
|
+
- Auto-generated semantic search tools for MCP server
|
|
72
|
+
|
|
73
|
+
## Usage Patterns
|
|
74
|
+
|
|
75
|
+
### Basic Setup with OpenAI
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// opensaas.config.ts
|
|
79
|
+
import { config, list } from '@opensaas/stack-core'
|
|
80
|
+
import { text } from '@opensaas/stack-core/fields'
|
|
81
|
+
import { withRAG, ragConfig, openaiEmbeddings, pgvectorStorage } from '@opensaas/stack-rag'
|
|
82
|
+
import { embedding } from '@opensaas/stack-rag/fields'
|
|
83
|
+
|
|
84
|
+
export default withRAG(
|
|
85
|
+
config({
|
|
86
|
+
db: {
|
|
87
|
+
provider: 'postgresql',
|
|
88
|
+
url: process.env.DATABASE_URL!,
|
|
89
|
+
},
|
|
90
|
+
lists: {
|
|
91
|
+
Article: list({
|
|
92
|
+
fields: {
|
|
93
|
+
title: text({ validation: { isRequired: true } }),
|
|
94
|
+
content: text({ validation: { isRequired: true } }),
|
|
95
|
+
contentEmbedding: embedding({
|
|
96
|
+
sourceField: 'content',
|
|
97
|
+
provider: 'openai',
|
|
98
|
+
dimensions: 1536,
|
|
99
|
+
autoGenerate: true,
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
ragConfig({
|
|
106
|
+
provider: openaiEmbeddings({
|
|
107
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
108
|
+
model: 'text-embedding-3-small',
|
|
109
|
+
}),
|
|
110
|
+
storage: pgvectorStorage({ distanceFunction: 'cosine' }),
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Local Development with Ollama
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// opensaas.config.ts
|
|
119
|
+
import { withRAG, ragConfig, ollamaEmbeddings, jsonStorage } from '@opensaas/stack-rag'
|
|
120
|
+
|
|
121
|
+
export default withRAG(
|
|
122
|
+
config({
|
|
123
|
+
db: { provider: 'sqlite', url: 'file:./dev.db' },
|
|
124
|
+
lists: {
|
|
125
|
+
Document: list({
|
|
126
|
+
fields: {
|
|
127
|
+
text: text(),
|
|
128
|
+
embedding: embedding({
|
|
129
|
+
sourceField: 'text',
|
|
130
|
+
provider: 'ollama',
|
|
131
|
+
autoGenerate: true,
|
|
132
|
+
}),
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
ragConfig({
|
|
138
|
+
provider: ollamaEmbeddings({
|
|
139
|
+
baseURL: 'http://localhost:11434',
|
|
140
|
+
model: 'nomic-embed-text',
|
|
141
|
+
}),
|
|
142
|
+
storage: jsonStorage(), // Good for development, no DB extensions needed
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Multiple Providers
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
ragConfig({
|
|
151
|
+
providers: {
|
|
152
|
+
openai: openaiEmbeddings({
|
|
153
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
154
|
+
model: 'text-embedding-3-small',
|
|
155
|
+
}),
|
|
156
|
+
ollama: ollamaEmbeddings({
|
|
157
|
+
model: 'nomic-embed-text',
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
storage: pgvectorStorage(),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// In fields
|
|
164
|
+
fields: {
|
|
165
|
+
titleEmbedding: embedding({
|
|
166
|
+
sourceField: 'title',
|
|
167
|
+
provider: 'ollama', // Use local Ollama for titles
|
|
168
|
+
}),
|
|
169
|
+
contentEmbedding: embedding({
|
|
170
|
+
sourceField: 'content',
|
|
171
|
+
provider: 'openai', // Use OpenAI for content
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Manual Embedding Storage (Low-Level)
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { createEmbeddingProvider } from '@opensaas/stack-rag/providers'
|
|
180
|
+
import { getContext } from '@/.opensaas/context'
|
|
181
|
+
|
|
182
|
+
// Generate embeddings manually
|
|
183
|
+
const provider = createEmbeddingProvider({
|
|
184
|
+
type: 'openai',
|
|
185
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const vector = await provider.embed('Hello world')
|
|
189
|
+
|
|
190
|
+
// Store manually
|
|
191
|
+
const context = await getContext()
|
|
192
|
+
await context.db.article.create({
|
|
193
|
+
data: {
|
|
194
|
+
title: 'Hello',
|
|
195
|
+
contentEmbedding: {
|
|
196
|
+
vector,
|
|
197
|
+
metadata: {
|
|
198
|
+
model: provider.model,
|
|
199
|
+
provider: provider.type,
|
|
200
|
+
dimensions: provider.dimensions,
|
|
201
|
+
generatedAt: new Date().toISOString(),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Semantic Search (Runtime)
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { createVectorStorage } from '@opensaas/stack-rag/storage'
|
|
212
|
+
import { createEmbeddingProvider } from '@opensaas/stack-rag/providers'
|
|
213
|
+
import { getContext } from '@/.opensaas/context'
|
|
214
|
+
import config from '@/opensaas.config'
|
|
215
|
+
|
|
216
|
+
// Server action or API route
|
|
217
|
+
export async function searchArticles(query: string) {
|
|
218
|
+
const context = await getContext()
|
|
219
|
+
|
|
220
|
+
// Generate query embedding
|
|
221
|
+
const provider = createEmbeddingProvider({
|
|
222
|
+
type: 'openai',
|
|
223
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
224
|
+
})
|
|
225
|
+
const queryVector = await provider.embed(query)
|
|
226
|
+
|
|
227
|
+
// Search
|
|
228
|
+
const storage = createVectorStorage({ type: 'pgvector' })
|
|
229
|
+
const results = await storage.search('Article', 'contentEmbedding', queryVector, {
|
|
230
|
+
limit: 10,
|
|
231
|
+
minScore: 0.7,
|
|
232
|
+
context,
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
return results.map((r) => ({
|
|
236
|
+
article: r.item,
|
|
237
|
+
similarity: r.score,
|
|
238
|
+
}))
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### MCP Integration (Automatic)
|
|
243
|
+
|
|
244
|
+
When RAG is enabled with MCP, semantic search tools are automatically generated:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// opensaas.config.ts
|
|
248
|
+
import { withAuth, authConfig } from '@opensaas/stack-auth'
|
|
249
|
+
import { withRAG, ragConfig } from '@opensaas/stack-rag'
|
|
250
|
+
|
|
251
|
+
export default withRAG(
|
|
252
|
+
withAuth(
|
|
253
|
+
config({
|
|
254
|
+
db: { provider: 'postgresql', url: process.env.DATABASE_URL! },
|
|
255
|
+
mcp: {
|
|
256
|
+
enabled: true,
|
|
257
|
+
auth: { type: 'better-auth', loginPage: '/sign-in' },
|
|
258
|
+
},
|
|
259
|
+
lists: {
|
|
260
|
+
Article: list({
|
|
261
|
+
fields: {
|
|
262
|
+
content: text(),
|
|
263
|
+
contentEmbedding: embedding({ sourceField: 'content' }),
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
authConfig({ emailAndPassword: { enabled: true } }),
|
|
269
|
+
),
|
|
270
|
+
ragConfig({
|
|
271
|
+
provider: openaiEmbeddings({ apiKey: process.env.OPENAI_API_KEY! }),
|
|
272
|
+
enableMcpTools: true, // Auto-generates semantic_search_article tool
|
|
273
|
+
}),
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
AI assistants can then use:
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"name": "semantic_search_article",
|
|
282
|
+
"arguments": {
|
|
283
|
+
"query": "articles about machine learning",
|
|
284
|
+
"limit": 5
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Architecture Patterns
|
|
290
|
+
|
|
291
|
+
### Automatic Embedding Generation
|
|
292
|
+
|
|
293
|
+
The `withRAG()` wrapper injects hooks into fields with `sourceField` set:
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// User config
|
|
297
|
+
contentEmbedding: embedding({
|
|
298
|
+
sourceField: 'content',
|
|
299
|
+
autoGenerate: true
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
// withRAG() automatically adds:
|
|
303
|
+
contentEmbedding: embedding({
|
|
304
|
+
hooks: {
|
|
305
|
+
afterOperation: async ({ operation, value, item, context }) => {
|
|
306
|
+
if (operation === 'create' || operation === 'update') {
|
|
307
|
+
// Check if source field changed
|
|
308
|
+
const sourceText = item.content
|
|
309
|
+
const currentEmbedding = item.contentEmbedding
|
|
310
|
+
|
|
311
|
+
// Generate embedding if needed
|
|
312
|
+
if (shouldRegenerate(sourceText, currentEmbedding)) {
|
|
313
|
+
const provider = getEmbeddingProvider(ragConfig)
|
|
314
|
+
const vector = await provider.embed(sourceText)
|
|
315
|
+
|
|
316
|
+
await context.db.article.update({
|
|
317
|
+
where: { id: item.id },
|
|
318
|
+
data: {
|
|
319
|
+
contentEmbedding: {
|
|
320
|
+
vector,
|
|
321
|
+
metadata: { ... }
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
})
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Access Control Integration
|
|
333
|
+
|
|
334
|
+
All searches use the access-controlled context:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// Search respects access control
|
|
338
|
+
const context = await getContext({ userId: 'user-123' })
|
|
339
|
+
|
|
340
|
+
const results = await storage.search('Article', 'contentEmbedding', queryVector, {
|
|
341
|
+
context, // Access control applied
|
|
342
|
+
where: { published: true }, // Additional filters
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// Users only see articles they have access to
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Vector Storage Backends
|
|
349
|
+
|
|
350
|
+
Storage backends are pluggable:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// pgvector (best for production PostgreSQL)
|
|
354
|
+
storage: pgvectorStorage({ distanceFunction: 'cosine' })
|
|
355
|
+
// Uses pgvector extension, fast indexed search
|
|
356
|
+
|
|
357
|
+
// sqlite-vss (good for SQLite apps)
|
|
358
|
+
storage: sqliteVssStorage({ distanceFunction: 'cosine' })
|
|
359
|
+
// Uses sqlite-vss extension
|
|
360
|
+
|
|
361
|
+
// JSON (good for development)
|
|
362
|
+
storage: jsonStorage()
|
|
363
|
+
// No extensions needed, JS-based similarity search
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Embedding Providers
|
|
367
|
+
|
|
368
|
+
Providers are pluggable and extensible:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
// Register custom provider
|
|
372
|
+
import { registerEmbeddingProvider } from '@opensaas/stack-rag/providers'
|
|
373
|
+
|
|
374
|
+
registerEmbeddingProvider('custom', (config) => {
|
|
375
|
+
return {
|
|
376
|
+
type: 'custom',
|
|
377
|
+
model: config.model,
|
|
378
|
+
dimensions: config.dimensions,
|
|
379
|
+
async embed(text) {
|
|
380
|
+
// Your implementation
|
|
381
|
+
return [
|
|
382
|
+
/* vector */
|
|
383
|
+
]
|
|
384
|
+
},
|
|
385
|
+
async embedBatch(texts) {
|
|
386
|
+
// Batch implementation
|
|
387
|
+
return [
|
|
388
|
+
[
|
|
389
|
+
/* vectors */
|
|
390
|
+
],
|
|
391
|
+
]
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Database Setup
|
|
398
|
+
|
|
399
|
+
### PostgreSQL with pgvector
|
|
400
|
+
|
|
401
|
+
```sql
|
|
402
|
+
-- Enable pgvector extension
|
|
403
|
+
CREATE EXTENSION vector;
|
|
404
|
+
|
|
405
|
+
-- Prisma will generate schema with Json fields
|
|
406
|
+
-- For optimal performance, create indexes:
|
|
407
|
+
CREATE INDEX article_embedding_vector_idx
|
|
408
|
+
ON "Article" USING ivfflat (("contentEmbedding"->>'vector')::vector(1536))
|
|
409
|
+
WITH (lists = 100);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### SQLite with VSS
|
|
413
|
+
|
|
414
|
+
```sql
|
|
415
|
+
-- Load VSS extension (depends on your SQLite setup)
|
|
416
|
+
-- Embeddings stored as JSON, VSS used for search
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Any Database with JSON Storage
|
|
420
|
+
|
|
421
|
+
No special setup needed. Embeddings stored as JSON, similarity computed in JavaScript.
|
|
422
|
+
|
|
423
|
+
## Type Safety
|
|
424
|
+
|
|
425
|
+
All operations are fully typed:
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import type { SearchResult, StoredEmbedding } from '@opensaas/stack-rag'
|
|
429
|
+
|
|
430
|
+
const results: SearchResult<Article>[] = await storage.search(...)
|
|
431
|
+
|
|
432
|
+
const embedding: StoredEmbedding = {
|
|
433
|
+
vector: [0.1, 0.2, 0.3],
|
|
434
|
+
metadata: {
|
|
435
|
+
model: 'text-embedding-3-small',
|
|
436
|
+
provider: 'openai',
|
|
437
|
+
dimensions: 1536,
|
|
438
|
+
generatedAt: new Date().toISOString()
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Performance Considerations
|
|
444
|
+
|
|
445
|
+
1. **Batch embedding generation**: Use `embedBatch()` for multiple texts
|
|
446
|
+
2. **Index embeddings**: Create vector indexes in production (pgvector)
|
|
447
|
+
3. **Chunking strategy**: Configure chunking for long texts
|
|
448
|
+
4. **Rate limiting**: Configure `rateLimit` in RAG config to avoid API limits
|
|
449
|
+
5. **Caching**: Hash source text to avoid regenerating unchanged embeddings
|
|
450
|
+
|
|
451
|
+
## Common Patterns
|
|
452
|
+
|
|
453
|
+
### Text Chunking for Long Documents
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { chunkText } from '@opensaas/stack-rag/runtime'
|
|
457
|
+
|
|
458
|
+
const chunks = chunkText(longDocument, {
|
|
459
|
+
strategy: 'recursive',
|
|
460
|
+
maxTokens: 500,
|
|
461
|
+
overlap: 50,
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
// Generate embeddings for each chunk
|
|
465
|
+
const vectors = await provider.embedBatch(chunks)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Hybrid Search (Keyword + Semantic)
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
// Combine traditional search with semantic search
|
|
472
|
+
const keywordResults = await context.db.article.findMany({
|
|
473
|
+
where: {
|
|
474
|
+
OR: [
|
|
475
|
+
{ title: { contains: query } },
|
|
476
|
+
{ content: { contains: query } },
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const semanticResults = await storage.search(...)
|
|
482
|
+
|
|
483
|
+
// Merge and deduplicate results
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Find Similar Items
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
// Find articles similar to a given article
|
|
490
|
+
const article = await context.db.article.findUnique({ where: { id } })
|
|
491
|
+
const queryVector = article.contentEmbedding.vector
|
|
492
|
+
|
|
493
|
+
const similar = await storage.search('Article', 'contentEmbedding', queryVector, {
|
|
494
|
+
limit: 5,
|
|
495
|
+
where: { id: { not: id } }, // Exclude the article itself
|
|
496
|
+
context,
|
|
497
|
+
})
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Testing
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
// packages/rag/__tests__/providers.test.ts
|
|
504
|
+
import { describe, it, expect } from 'vitest'
|
|
505
|
+
import { createOpenAIProvider } from '../src/providers/openai'
|
|
506
|
+
|
|
507
|
+
describe('OpenAIEmbeddingProvider', () => {
|
|
508
|
+
it('should generate embeddings', async () => {
|
|
509
|
+
const provider = createOpenAIProvider({
|
|
510
|
+
type: 'openai',
|
|
511
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
const embedding = await provider.embed('Hello world')
|
|
515
|
+
|
|
516
|
+
expect(embedding).toHaveLength(1536)
|
|
517
|
+
expect(embedding[0]).toBeTypeOf('number')
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## Migration Guide
|
|
523
|
+
|
|
524
|
+
### Adding RAG to Existing App
|
|
525
|
+
|
|
526
|
+
1. Install package: `pnpm add @opensaas/stack-rag`
|
|
527
|
+
2. Install provider: `pnpm add openai` (for OpenAI)
|
|
528
|
+
3. Wrap config with `withRAG()`
|
|
529
|
+
4. Add `embedding()` fields to lists
|
|
530
|
+
5. Run `pnpm generate` and `pnpm db:push`
|
|
531
|
+
6. Embeddings will be generated automatically on create/update
|
|
532
|
+
|
|
533
|
+
### Switching Storage Backends
|
|
534
|
+
|
|
535
|
+
Change storage config and regenerate schema:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
// From JSON to pgvector
|
|
539
|
+
storage: pgvectorStorage() // instead of jsonStorage()
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Then:
|
|
543
|
+
|
|
544
|
+
```bash
|
|
545
|
+
pnpm generate
|
|
546
|
+
pnpm db:push
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Existing embeddings in JSON format are compatible.
|
|
550
|
+
|
|
551
|
+
## Limitations
|
|
552
|
+
|
|
553
|
+
- **Dimensions must match**: Provider and field dimensions must match
|
|
554
|
+
- **JSON storage performance**: JSON-based search doesn't scale to millions of vectors
|
|
555
|
+
- **No automatic re-embedding**: Changing provider/model requires manual re-embedding
|
|
556
|
+
- **Access control bypass**: Raw Prisma queries bypass access control (handled in implementation)
|
|
557
|
+
|
|
558
|
+
## Future Enhancements
|
|
559
|
+
|
|
560
|
+
- Automatic re-embedding when provider changes
|
|
561
|
+
- Built-in chunking strategies for fields
|
|
562
|
+
- Hybrid search utilities (keyword + semantic)
|
|
563
|
+
- Pinecone, Qdrant, Weaviate integrations
|
|
564
|
+
- Embedding caching and deduplication
|
|
565
|
+
- Advanced distance metrics
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OpenSaas Stack Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|