@strav/rag 0.4.31 → 1.0.0-alpha.20
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/package.json +21 -23
- package/src/chunking/chunker.ts +7 -2
- package/src/chunking/fixed_size_chunker.ts +24 -8
- package/src/chunking/recursive_chunker.ts +89 -28
- package/src/console/index.ts +3 -0
- package/src/console/rag_console_provider.ts +17 -0
- package/src/console/rag_flush.ts +51 -0
- package/src/console/rag_list.ts +48 -0
- package/src/drivers/memory_driver.ts +110 -85
- package/src/drivers/pgvector_driver.ts +203 -109
- package/src/index.ts +46 -36
- package/src/migrations.ts +116 -0
- package/src/rag_error.ts +76 -0
- package/src/rag_manager.ts +289 -66
- package/src/rag_provider.ts +85 -7
- package/src/rag_vector_schema.ts +56 -0
- package/src/retrievable.ts +236 -145
- package/src/types.ts +80 -22
- package/src/vector_store.ts +45 -5
- package/src/commands/rag_flush.ts +0 -41
- package/src/commands/rag_ingest.ts +0 -45
- package/src/drivers/null_driver.ts +0 -21
- package/src/errors.ts +0 -21
- package/src/helpers.ts +0 -186
- package/stubs/config/rag.ts +0 -33
- package/tsconfig.json +0 -5
package/src/rag_manager.ts
CHANGED
|
@@ -1,98 +1,321 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `RagManager` — the facade apps use for RAG workflows.
|
|
3
|
+
*
|
|
4
|
+
* Three concept clusters:
|
|
5
|
+
*
|
|
6
|
+
* - **Stores.** Apps register vector stores in
|
|
7
|
+
* `config.rag.stores`; the manager constructs them lazily on
|
|
8
|
+
* first `store(name)` call. Custom drivers register via
|
|
9
|
+
* `manager.extend(name, factory)`.
|
|
10
|
+
*
|
|
11
|
+
* - **Ingest.** `manager.ingest(collection, content, opts?)`
|
|
12
|
+
* chunks → embeds → upserts. Returns the vector ids it
|
|
13
|
+
* wrote. Apps that already have chunks bypass the chunker
|
|
14
|
+
* by passing `IngestOptions.chunks`.
|
|
15
|
+
*
|
|
16
|
+
* - **Retrieve.** `manager.retrieve(query, opts?)` embeds the
|
|
17
|
+
* query, runs the store's similarity search, and returns
|
|
18
|
+
* ranked matches. `topK` / `threshold` / `filter` pass
|
|
19
|
+
* through.
|
|
20
|
+
*
|
|
21
|
+
* Multitenancy: invisible. The pgvector driver relies on RLS via
|
|
22
|
+
* `app.tenant_id` session settings, so apps that wrap calls in
|
|
23
|
+
* `tenants.withTenant(...)` get per-tenant isolation for free.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// biome-ignore lint/style/useImportType: PostgresDatabase value import for the container path that wires PgvectorDriver.
|
|
27
|
+
import { PostgresDatabase } from '@strav/database'
|
|
28
|
+
// biome-ignore lint/style/useImportType: BrainManager value import for @inject() param-type metadata.
|
|
29
|
+
import { BrainManager } from '@strav/brain'
|
|
30
|
+
// biome-ignore lint/style/useImportType: Application value import for the container handle.
|
|
31
|
+
import { Application, inject, ulid } from '@strav/kernel'
|
|
32
|
+
import { createChunker } from './chunking/chunker.ts'
|
|
5
33
|
import { MemoryDriver } from './drivers/memory_driver.ts'
|
|
6
34
|
import { PgvectorDriver } from './drivers/pgvector_driver.ts'
|
|
35
|
+
import { EmbeddingError, RagError } from './rag_error.ts'
|
|
36
|
+
import type {
|
|
37
|
+
ChunkingConfig,
|
|
38
|
+
Chunk,
|
|
39
|
+
Chunker,
|
|
40
|
+
RagConfig,
|
|
41
|
+
RetrieveOptions,
|
|
42
|
+
RetrieveResult,
|
|
43
|
+
RetrievedDocument,
|
|
44
|
+
StoreConfig,
|
|
45
|
+
VectorDocument,
|
|
46
|
+
} from './types.ts'
|
|
47
|
+
import type { VectorStore } from './vector_store.ts'
|
|
7
48
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
export interface IngestOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Override the store. Defaults to the manager's default store.
|
|
52
|
+
*/
|
|
53
|
+
store?: string
|
|
54
|
+
/** App-defined pointer back to the source row this content came from. */
|
|
55
|
+
sourceId?: string
|
|
56
|
+
/** Metadata attached to every chunk. Combined with per-chunk metadata if `chunks` is supplied. */
|
|
57
|
+
metadata?: Record<string, unknown>
|
|
58
|
+
/** Override chunking strategy + size for this call. */
|
|
59
|
+
chunking?: Partial<ChunkingConfig>
|
|
60
|
+
/**
|
|
61
|
+
* Pre-chunked content — skips the chunker entirely. The chunker
|
|
62
|
+
* helpers (`startOffset`, `endOffset`, `index`) carry through
|
|
63
|
+
* as metadata.
|
|
64
|
+
*/
|
|
65
|
+
chunks?: readonly Chunk[]
|
|
66
|
+
/**
|
|
67
|
+
* Optional per-chunk sanitizer applied AFTER chunking, BEFORE
|
|
68
|
+
* embedding. Return `null` to drop the chunk; otherwise return
|
|
69
|
+
* the (possibly modified) text. Use to scrub PII / secrets /
|
|
70
|
+
* prompt-injection markers from untrusted source content.
|
|
71
|
+
*/
|
|
72
|
+
sanitize?(chunk: { content: string; index: number }): string | null | Promise<string | null>
|
|
73
|
+
/** Override the brain provider used for the embedding call. */
|
|
74
|
+
embedProvider?: string
|
|
75
|
+
/** Override the embedding model. */
|
|
76
|
+
embedModel?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface RagManagerOptions {
|
|
80
|
+
config: RagConfig
|
|
81
|
+
brain: BrainManager
|
|
82
|
+
/** Optional — required only if pgvector stores are configured. */
|
|
83
|
+
db?: PostgresDatabase
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Factory for custom drivers — apps register via `manager.extend(name, factory)`. */
|
|
87
|
+
export type StoreFactory = (config: StoreConfig) => VectorStore
|
|
88
|
+
|
|
89
|
+
@inject()
|
|
90
|
+
export class RagManager {
|
|
91
|
+
readonly config: RagConfig
|
|
92
|
+
private readonly brain: BrainManager
|
|
93
|
+
private readonly db: PostgresDatabase | undefined
|
|
94
|
+
private readonly stores = new Map<string, VectorStore>()
|
|
95
|
+
private readonly extensions = new Map<string, StoreFactory>()
|
|
31
96
|
|
|
32
|
-
|
|
33
|
-
if (!
|
|
34
|
-
throw new
|
|
35
|
-
|
|
97
|
+
constructor(options: RagManagerOptions) {
|
|
98
|
+
if (!options.config.stores[options.config.default]) {
|
|
99
|
+
throw new RagError(
|
|
100
|
+
`RagManager: default store "${options.config.default}" is not configured.`,
|
|
101
|
+
{
|
|
102
|
+
context: {
|
|
103
|
+
default: options.config.default,
|
|
104
|
+
available: Object.keys(options.config.stores),
|
|
105
|
+
},
|
|
106
|
+
},
|
|
36
107
|
)
|
|
37
108
|
}
|
|
38
|
-
|
|
109
|
+
this.config = options.config
|
|
110
|
+
this.brain = options.brain
|
|
111
|
+
this.db = options.db
|
|
39
112
|
}
|
|
40
113
|
|
|
41
|
-
|
|
42
|
-
const key = name ?? RagManager.config.default
|
|
114
|
+
// ─── Store management ─────────────────────────────────────────────────
|
|
43
115
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a vector store by name (or the default when omitted).
|
|
118
|
+
* Stores are constructed lazily on first use + memoized.
|
|
119
|
+
*/
|
|
120
|
+
store(name?: string): VectorStore {
|
|
121
|
+
const key = name ?? this.config.default
|
|
122
|
+
const cached = this.stores.get(key)
|
|
123
|
+
if (cached) return cached
|
|
124
|
+
const cfg = this.config.stores[key]
|
|
125
|
+
if (!cfg) {
|
|
126
|
+
throw new RagError(`RagManager: store "${key}" is not configured.`, {
|
|
127
|
+
context: { requested: key, available: Object.keys(this.config.stores) },
|
|
128
|
+
})
|
|
50
129
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
RagManager._stores.set(key, store)
|
|
130
|
+
const store = this.createStore(cfg)
|
|
131
|
+
this.stores.set(key, store)
|
|
54
132
|
return store
|
|
55
133
|
}
|
|
56
134
|
|
|
57
|
-
|
|
58
|
-
|
|
135
|
+
/** Register a custom driver. Subsequent `store(...)` calls can resolve `driver: <name>`. */
|
|
136
|
+
extend(name: string, factory: StoreFactory): void {
|
|
137
|
+
this.extensions.set(name, factory)
|
|
59
138
|
}
|
|
60
139
|
|
|
61
|
-
|
|
62
|
-
|
|
140
|
+
/** Hand-wire a store instance under a name (tests / one-off drivers). */
|
|
141
|
+
useStore(name: string, store: VectorStore): void {
|
|
142
|
+
this.stores.set(name, store)
|
|
63
143
|
}
|
|
64
144
|
|
|
65
|
-
|
|
66
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Compose the configured prefix with `name` — apps that want to
|
|
147
|
+
* namespace collections (per-tenant, per-app) set `config.rag.prefix`
|
|
148
|
+
* and call `manager.collectionName(...)` to resolve at runtime.
|
|
149
|
+
*/
|
|
150
|
+
collectionName(name: string): string {
|
|
151
|
+
return this.config.prefix ? `${this.config.prefix}${name}` : name
|
|
67
152
|
}
|
|
68
153
|
|
|
69
|
-
|
|
70
|
-
|
|
154
|
+
// ─── Ingest ───────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Chunk → embed → upsert. Returns the vector ids it wrote. The
|
|
158
|
+
* caller-supplied `collection` is composed with the configured
|
|
159
|
+
* `prefix` before hitting the store.
|
|
160
|
+
*/
|
|
161
|
+
async ingest(
|
|
162
|
+
collection: string,
|
|
163
|
+
content: string,
|
|
164
|
+
options: IngestOptions = {},
|
|
165
|
+
): Promise<string[]> {
|
|
166
|
+
const fullCollection = this.collectionName(collection)
|
|
167
|
+
const chunkerConfig: ChunkingConfig = {
|
|
168
|
+
strategy: options.chunking?.strategy ?? this.config.chunking.strategy,
|
|
169
|
+
chunkSize: options.chunking?.chunkSize ?? this.config.chunking.chunkSize,
|
|
170
|
+
overlap: options.chunking?.overlap ?? this.config.chunking.overlap,
|
|
171
|
+
...(options.chunking?.separators ?? this.config.chunking.separators
|
|
172
|
+
? { separators: options.chunking?.separators ?? this.config.chunking.separators }
|
|
173
|
+
: {}),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let chunks: Chunk[] = options.chunks
|
|
177
|
+
? [...options.chunks]
|
|
178
|
+
: this.chunker(chunkerConfig).chunk(content)
|
|
179
|
+
if (chunks.length === 0) return []
|
|
180
|
+
|
|
181
|
+
if (options.sanitize) {
|
|
182
|
+
const filtered: Chunk[] = []
|
|
183
|
+
for (const chunk of chunks) {
|
|
184
|
+
const next = await options.sanitize({ content: chunk.content, index: chunk.index })
|
|
185
|
+
if (next === null) continue
|
|
186
|
+
filtered.push({ ...chunk, content: next })
|
|
187
|
+
}
|
|
188
|
+
chunks = filtered
|
|
189
|
+
if (chunks.length === 0) return []
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const texts = chunks.map((c) => c.content)
|
|
193
|
+
let embeddings: number[][]
|
|
194
|
+
try {
|
|
195
|
+
const result = await this.brain.embed(texts, {
|
|
196
|
+
provider: options.embedProvider ?? this.config.embedding.provider,
|
|
197
|
+
model: options.embedModel ?? this.config.embedding.model,
|
|
198
|
+
})
|
|
199
|
+
embeddings = result.embeddings as number[][]
|
|
200
|
+
} catch (cause) {
|
|
201
|
+
throw new EmbeddingError(
|
|
202
|
+
`RagManager.ingest: embedding ${texts.length} chunks failed.`,
|
|
203
|
+
{ context: { collection: fullCollection }, cause },
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const documents: VectorDocument[] = chunks.map((chunk, i) => ({
|
|
208
|
+
id: ulid(),
|
|
209
|
+
...(options.sourceId !== undefined ? { sourceId: options.sourceId } : {}),
|
|
210
|
+
content: chunk.content,
|
|
211
|
+
embedding: embeddings[i]!,
|
|
212
|
+
metadata: {
|
|
213
|
+
...(options.metadata ?? {}),
|
|
214
|
+
chunkIndex: chunk.index,
|
|
215
|
+
startOffset: chunk.startOffset,
|
|
216
|
+
endOffset: chunk.endOffset,
|
|
217
|
+
},
|
|
218
|
+
}))
|
|
219
|
+
|
|
220
|
+
await this.store(options.store).upsert(fullCollection, documents)
|
|
221
|
+
return documents.map((d) => d.id as string)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Retrieve ─────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
async retrieve(
|
|
227
|
+
query: string,
|
|
228
|
+
options: RetrieveOptions = {},
|
|
229
|
+
): Promise<RetrieveResult> {
|
|
230
|
+
const fullCollection = this.collectionName(
|
|
231
|
+
options.collection ?? this.config.default,
|
|
232
|
+
)
|
|
233
|
+
const start = performance.now()
|
|
234
|
+
|
|
235
|
+
let embedding: number[]
|
|
236
|
+
try {
|
|
237
|
+
const result = await this.brain.embed([query], {
|
|
238
|
+
provider: options.embedProvider ?? this.config.embedding.provider,
|
|
239
|
+
model: options.embedModel ?? this.config.embedding.model,
|
|
240
|
+
})
|
|
241
|
+
embedding = result.embeddings[0] as number[]
|
|
242
|
+
} catch (cause) {
|
|
243
|
+
throw new EmbeddingError(
|
|
244
|
+
`RagManager.retrieve: embedding query failed.`,
|
|
245
|
+
{ context: { collection: fullCollection }, cause },
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const queryOpts: { topK?: number; threshold?: number; filter?: Record<string, unknown> } = {}
|
|
250
|
+
if (options.topK !== undefined) queryOpts.topK = options.topK
|
|
251
|
+
if (options.threshold !== undefined) queryOpts.threshold = options.threshold
|
|
252
|
+
if (options.filter !== undefined) queryOpts.filter = options.filter
|
|
253
|
+
|
|
254
|
+
const result = await this.store(options.store).query(
|
|
255
|
+
fullCollection,
|
|
256
|
+
embedding,
|
|
257
|
+
queryOpts,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
const matches: RetrievedDocument[] = result.matches.map((m) => ({
|
|
261
|
+
id: m.id,
|
|
262
|
+
content: m.content,
|
|
263
|
+
score: m.score,
|
|
264
|
+
similarity: m.score,
|
|
265
|
+
metadata: m.metadata,
|
|
266
|
+
...(m.sourceId !== undefined ? { sourceId: m.sourceId } : {}),
|
|
267
|
+
}))
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
query,
|
|
271
|
+
matches,
|
|
272
|
+
processingTimeMs: performance.now() - start,
|
|
273
|
+
}
|
|
71
274
|
}
|
|
72
275
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
276
|
+
/**
|
|
277
|
+
* Create a collection on the active (or named) store. For
|
|
278
|
+
* pgvector this is a no-op — every collection lives in the
|
|
279
|
+
* same table. For MemoryDriver this allocates the bucket.
|
|
280
|
+
* Apps call it once at boot or before the first ingest of a
|
|
281
|
+
* new collection name.
|
|
282
|
+
*/
|
|
283
|
+
async createCollection(
|
|
284
|
+
collection: string,
|
|
285
|
+
options: { store?: string; dimension?: number } = {},
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
const fullCollection = this.collectionName(collection)
|
|
288
|
+
const dimension = options.dimension ?? this.config.embedding.dimension
|
|
289
|
+
await this.store(options.store).createCollection(fullCollection, dimension)
|
|
77
290
|
}
|
|
78
291
|
|
|
79
|
-
|
|
80
|
-
const driverName = config.driver ?? name
|
|
292
|
+
// ─── Internals ────────────────────────────────────────────────────────
|
|
81
293
|
|
|
82
|
-
|
|
83
|
-
|
|
294
|
+
private chunker(config: ChunkingConfig): Chunker {
|
|
295
|
+
return createChunker(config)
|
|
296
|
+
}
|
|
84
297
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
298
|
+
private createStore(config: StoreConfig): VectorStore {
|
|
299
|
+
const ext = this.extensions.get(config.driver)
|
|
300
|
+
if (ext) return ext(config)
|
|
301
|
+
switch (config.driver) {
|
|
88
302
|
case 'memory':
|
|
89
303
|
return new MemoryDriver()
|
|
90
|
-
case '
|
|
91
|
-
|
|
304
|
+
case 'pgvector':
|
|
305
|
+
if (!this.db) {
|
|
306
|
+
throw new RagError(
|
|
307
|
+
'RagManager: pgvector driver requires a PostgresDatabase. Register DatabaseProvider before RagProvider, or pass `db` to the manager constructor.',
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
return PgvectorDriver.fromConfig(this.db, config)
|
|
92
311
|
default:
|
|
93
|
-
throw new
|
|
94
|
-
`
|
|
312
|
+
throw new RagError(
|
|
313
|
+
`RagManager: unknown driver "${config.driver}". Register it via \`manager.extend(...)\`.`,
|
|
314
|
+
{ context: { driver: config.driver } },
|
|
95
315
|
)
|
|
96
316
|
}
|
|
97
317
|
}
|
|
98
318
|
}
|
|
319
|
+
|
|
320
|
+
/** Public alias for the container-resolution helper apps occasionally pass around. */
|
|
321
|
+
export type RagManagerResolver = (app: Application) => RagManager
|
package/src/rag_provider.ts
CHANGED
|
@@ -1,16 +1,94 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* `RagProvider` — `ServiceProvider` that wires `RagManager` into
|
|
3
|
+
* the container from `config.rag`.
|
|
4
|
+
*
|
|
5
|
+
* Eager construction at boot — a malformed config or a missing
|
|
6
|
+
* pgvector dependency should fail before the first call hits.
|
|
7
|
+
* Apps register `BrainProvider` and `DatabaseProvider` (when
|
|
8
|
+
* pgvector is in the store list) before this one; the
|
|
9
|
+
* `dependencies` array makes the order explicit.
|
|
10
|
+
*
|
|
11
|
+
* Config defaults: if `config.rag` is absent entirely, the
|
|
12
|
+
* provider boots a sensible in-memory setup so apps can try
|
|
13
|
+
* `rag.ingest()` / `rag.retrieve()` in dev without configuration
|
|
14
|
+
* — the memory driver is registered as the default store and a
|
|
15
|
+
* `recursive` chunker is configured. Production apps override
|
|
16
|
+
* via a real `config/rag.ts`.
|
|
17
|
+
*/
|
|
4
18
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
19
|
+
// biome-ignore lint/style/useImportType: PostgresDatabase value import — required when any pgvector store is configured. Loaded conditionally below.
|
|
20
|
+
import { PostgresDatabase } from '@strav/database'
|
|
21
|
+
// biome-ignore lint/style/useImportType: BrainManager value import for c.resolve.
|
|
22
|
+
import { BrainManager } from '@strav/brain'
|
|
23
|
+
import {
|
|
24
|
+
type Application,
|
|
25
|
+
ConfigError,
|
|
26
|
+
ConfigRepository,
|
|
27
|
+
ServiceProvider,
|
|
28
|
+
} from '@strav/kernel'
|
|
29
|
+
import { RagManager, type RagManagerOptions } from './rag_manager.ts'
|
|
30
|
+
import type { RagConfig } from './types.ts'
|
|
31
|
+
|
|
32
|
+
export class RagProvider extends ServiceProvider {
|
|
33
|
+
override readonly name = 'rag'
|
|
34
|
+
override readonly dependencies = ['config', 'brain']
|
|
8
35
|
|
|
9
36
|
override register(app: Application): void {
|
|
10
|
-
app.singleton(RagManager)
|
|
37
|
+
app.singleton(RagManager, (c) => {
|
|
38
|
+
const raw = c.resolve(ConfigRepository).get('rag') as Partial<RagConfig> | undefined
|
|
39
|
+
const config = applyDefaults(raw)
|
|
40
|
+
|
|
41
|
+
const brain = c.resolve(BrainManager)
|
|
42
|
+
const opts: RagManagerOptions = { config, brain }
|
|
43
|
+
|
|
44
|
+
// Only resolve the database when at least one store needs it.
|
|
45
|
+
const needsDb = Object.values(config.stores).some((s) => s.driver === 'pgvector')
|
|
46
|
+
if (needsDb) {
|
|
47
|
+
try {
|
|
48
|
+
opts.db = c.resolve(PostgresDatabase)
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
throw new ConfigError(
|
|
51
|
+
'RagProvider: at least one store uses `driver: "pgvector"` but PostgresDatabase is not registered. Register DatabaseProvider before RagProvider.',
|
|
52
|
+
{ cause },
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return new RagManager(opts)
|
|
57
|
+
})
|
|
11
58
|
}
|
|
12
59
|
|
|
13
60
|
override boot(app: Application): void {
|
|
61
|
+
// Force-resolve so config errors surface at boot, not on first call.
|
|
14
62
|
app.resolve(RagManager)
|
|
15
63
|
}
|
|
16
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fill in defaults for omitted config fields. Apps with no
|
|
68
|
+
* `config/rag.ts` at all get a working in-memory setup.
|
|
69
|
+
*/
|
|
70
|
+
function applyDefaults(raw: Partial<RagConfig> | undefined): RagConfig {
|
|
71
|
+
const config: Partial<RagConfig> = raw ?? {}
|
|
72
|
+
const stores = config.stores ?? { memory: { driver: 'memory' } }
|
|
73
|
+
const def = config.default ?? Object.keys(stores)[0] ?? 'memory'
|
|
74
|
+
if (!stores[def]) {
|
|
75
|
+
throw new ConfigError(
|
|
76
|
+
`RagProvider: default store "${def}" is not declared in config.rag.stores.`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
default: def,
|
|
81
|
+
...(config.prefix !== undefined ? { prefix: config.prefix } : {}),
|
|
82
|
+
embedding: config.embedding ?? {
|
|
83
|
+
provider: 'openai',
|
|
84
|
+
model: 'text-embedding-3-small',
|
|
85
|
+
dimension: 1536,
|
|
86
|
+
},
|
|
87
|
+
chunking: config.chunking ?? {
|
|
88
|
+
strategy: 'recursive',
|
|
89
|
+
chunkSize: 512,
|
|
90
|
+
overlap: 64,
|
|
91
|
+
},
|
|
92
|
+
stores,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ragVectorSchema` — the framework-known shape of the vectors
|
|
3
|
+
* table. Carries every column EXCEPT `embedding`, which lives
|
|
4
|
+
* outside the framework schema system because pgvector's
|
|
5
|
+
* `vector(N)` type isn't expressible via `defineSchema`. The
|
|
6
|
+
* `applyRagVectorMigration` helper attaches the embedding column
|
|
7
|
+
* + HNSW index in the migration step.
|
|
8
|
+
*
|
|
9
|
+
* Columns the framework manages:
|
|
10
|
+
*
|
|
11
|
+
* - `id` ULID primary key.
|
|
12
|
+
* - `tenant_id` Auto-injected by `tenanted: true`. RLS policies
|
|
13
|
+
* scope reads + writes by
|
|
14
|
+
* `current_setting('app.tenant_id')` so apps that
|
|
15
|
+
* wrap calls in `tenants.withTenant(...)` get
|
|
16
|
+
* per-tenant isolation for free.
|
|
17
|
+
* - `collection` Logical bucket — apps create one collection per
|
|
18
|
+
* conceptual corpus (`articles`, `support_docs`,
|
|
19
|
+
* per-user notebooks, ...).
|
|
20
|
+
* - `source_id` Optional pointer back to the source row a chunk
|
|
21
|
+
* came from. `deleteBySource(collection, id)`
|
|
22
|
+
* drops every chunk for one source in a single
|
|
23
|
+
* DELETE — handy when re-indexing on update.
|
|
24
|
+
* - `content` The chunk text. Plain `text` for full storage.
|
|
25
|
+
* - `metadata` Free-form JSONB. Indexable in the recommended
|
|
26
|
+
* migration via a GIN index when apps query by
|
|
27
|
+
* metadata filters.
|
|
28
|
+
* - `created_at` Insert timestamp. Useful for audit + soft
|
|
29
|
+
* recency filtering.
|
|
30
|
+
*
|
|
31
|
+
* Columns attached by `applyRagVectorMigration`:
|
|
32
|
+
*
|
|
33
|
+
* - `embedding vector(<dimension>) NOT NULL` — the vector itself.
|
|
34
|
+
* - `HNSW idx_<table>_embedding` on `(embedding vector_cosine_ops)`.
|
|
35
|
+
*
|
|
36
|
+
* Apps register the schema with `SchemaRegistry` at boot (mirrors
|
|
37
|
+
* every other framework schema), then call
|
|
38
|
+
* `applyRagVectorMigration(db, { dimension, registry })` inside
|
|
39
|
+
* the migration's `up()`.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { Archetype, defineSchema } from '@strav/database'
|
|
43
|
+
|
|
44
|
+
export const ragVectorSchema = defineSchema(
|
|
45
|
+
'rag_vector',
|
|
46
|
+
Archetype.Entity,
|
|
47
|
+
(t) => {
|
|
48
|
+
t.id()
|
|
49
|
+
t.string('collection').max(128).notNull()
|
|
50
|
+
t.string('source_id').max(128).nullable()
|
|
51
|
+
t.text('content').notNull()
|
|
52
|
+
t.json('metadata').notNull().default({})
|
|
53
|
+
t.timestamp('created_at').notNull()
|
|
54
|
+
},
|
|
55
|
+
{ tenanted: true },
|
|
56
|
+
)
|