@strav/rag 0.4.31 → 1.0.0-alpha.19

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.
@@ -1,157 +1,222 @@
1
- import { Database } from '@strav/database'
1
+ /**
2
+ * `PgvectorDriver` — `VectorStore` backed by Postgres + the
3
+ * `pgvector` extension. Single table per app (`rag_vector` by
4
+ * default), `collection` is a column inside it.
5
+ *
6
+ * Multitenancy: every query relies on RLS scoping by
7
+ * `current_setting('app.tenant_id')`. Apps wrap calls in
8
+ * `tenants.withTenant(tenantId, async () => { ... })` — the
9
+ * driver itself has no tenant awareness.
10
+ *
11
+ * Why one table instead of one-per-collection:
12
+ *
13
+ * - `defineSchema` doesn't support runtime table creation.
14
+ * - HNSW indexes work fine with `collection` as a leading
15
+ * column; if a collection grows past tens of millions and
16
+ * wants its own partial HNSW, that's a one-line follow-up
17
+ * migration.
18
+ * - One RLS policy, one set of grants, fewer surprises.
19
+ *
20
+ * Why this driver doesn't extend `Repository`:
21
+ *
22
+ * - The framework repository hydrates rows into a `Model`, but
23
+ * `embedding vector(N)` isn't expressible in the framework's
24
+ * type system. The driver uses raw `db.query` / `db.execute`
25
+ * on the table and returns plain objects.
26
+ * - All vector ops (`<=>`, `vector_cosine_ops`) are
27
+ * pgvector-specific; the framework's query builder can't
28
+ * model them.
29
+ */
30
+
31
+ import type { PostgresDatabase } from '@strav/database'
32
+ import { VectorQueryError } from '../rag_error.ts'
33
+ import { ragVectorSchema } from '../rag_vector_schema.ts'
34
+ import type {
35
+ QueryOptions,
36
+ QueryResult,
37
+ StoreConfig,
38
+ VectorDocument,
39
+ VectorMatch,
40
+ } from '../types.ts'
2
41
  import type { VectorStore } from '../vector_store.ts'
3
- import type { VectorDocument, QueryOptions, QueryResult, VectorMatch, StoreConfig } from '../types.ts'
4
- import { VectorQueryError } from '../errors.ts'
42
+
43
+ export interface PgvectorDriverOptions {
44
+ /** PostgresDatabase instance — typically resolved from the container. */
45
+ db: PostgresDatabase
46
+ /** Override table name. Defaults to `rag_vector`. */
47
+ table?: string
48
+ }
5
49
 
6
50
  export class PgvectorDriver implements VectorStore {
7
51
  readonly name = 'pgvector'
8
- private initialized = false
9
52
 
10
- constructor(_config: StoreConfig) {}
53
+ private readonly db: PostgresDatabase
54
+ private readonly table: string
11
55
 
12
- async createCollection(collection: string, dimension: number): Promise<void> {
13
- await this.ensureTable(dimension)
56
+ constructor(options: PgvectorDriverOptions) {
57
+ this.db = options.db
58
+ this.table = options.table ?? ragVectorSchema.name
59
+ }
14
60
 
15
- const indexName = `idx_strav_vectors_hnsw_${collection.replace(/[^a-z0-9_]/gi, '_')}`
16
- try {
17
- await Database.raw.unsafe(
18
- `CREATE INDEX IF NOT EXISTS "${indexName}"
19
- ON _strav_vectors USING hnsw (embedding vector_cosine_ops)
20
- WHERE collection = '${collection}'`
21
- )
22
- } catch {
23
- // Index may already exist
24
- }
61
+ /**
62
+ * Factory used by `RagManager.createStore` — accepts the raw
63
+ * `StoreConfig` from `config.rag.stores[<name>]` and resolves
64
+ * the `db` from the container. Apps that want explicit control
65
+ * `new PgvectorDriver({ db, table })` directly.
66
+ */
67
+ static fromConfig(db: PostgresDatabase, config: StoreConfig): PgvectorDriver {
68
+ return new PgvectorDriver({
69
+ db,
70
+ ...(typeof config.table === 'string' ? { table: config.table } : {}),
71
+ })
72
+ }
73
+
74
+ // ─── Collections ──────────────────────────────────────────────────────
75
+
76
+ async createCollection(_collection: string, _dimension: number): Promise<void> {
77
+ // No-op: every collection lives in the same table. The
78
+ // `applyRagVectorMigration` helper attached the
79
+ // `vector(<dimension>)` column at migration time, so the
80
+ // dimension is fixed per table and enforced at INSERT.
25
81
  }
26
82
 
27
83
  async deleteCollection(collection: string): Promise<void> {
28
- await Database.raw.unsafe(
29
- `DELETE FROM _strav_vectors WHERE collection = $1`,
30
- [collection]
84
+ await this.db.execute(
85
+ `DELETE FROM "${this.table}" WHERE "collection" = $1`,
86
+ [collection],
31
87
  )
32
88
  }
33
89
 
34
- async upsert(collection: string, documents: VectorDocument[]): Promise<void> {
35
- const sql = Database.raw
90
+ // ─── Mutations ────────────────────────────────────────────────────────
36
91
 
92
+ async upsert(
93
+ collection: string,
94
+ documents: readonly VectorDocument[],
95
+ ): Promise<void> {
96
+ if (documents.length === 0) return
97
+ // pgvector accepts the vector as a stringified array literal —
98
+ // `[0.12,0.34,...]` — cast with `::vector` at the boundary.
37
99
  for (const doc of documents) {
38
- const embeddingStr = `[${doc.embedding.join(',')}]`
39
- const metadata = JSON.stringify(doc.metadata ?? {})
40
- const id = doc.id != null ? String(doc.id) : crypto.randomUUID()
41
- const sourceId = doc.sourceId != null ? String(doc.sourceId) : null
42
-
43
- await sql.unsafe(
44
- `INSERT INTO _strav_vectors (collection, source_id, content, metadata, embedding)
45
- VALUES ($1, $2, $3, $4::jsonb, $5::vector)`,
46
- [collection, sourceId, doc.content, metadata, embeddingStr]
100
+ const id = doc.id ?? crypto.randomUUID()
101
+ const embeddingLiteral = `[${doc.embedding.join(',')}]`
102
+ await this.db.execute(
103
+ `INSERT INTO "${this.table}"
104
+ ("id", "collection", "source_id", "content", "metadata", "embedding", "created_at")
105
+ VALUES ($1, $2, $3, $4, $5::jsonb, $6::vector, NOW())
106
+ ON CONFLICT ("id") DO UPDATE SET
107
+ "collection" = EXCLUDED."collection",
108
+ "source_id" = EXCLUDED."source_id",
109
+ "content" = EXCLUDED."content",
110
+ "metadata" = EXCLUDED."metadata",
111
+ "embedding" = EXCLUDED."embedding"`,
112
+ [
113
+ id,
114
+ collection,
115
+ doc.sourceId ?? null,
116
+ doc.content,
117
+ JSON.stringify(doc.metadata ?? {}),
118
+ embeddingLiteral,
119
+ ],
47
120
  )
48
121
  }
49
122
  }
50
123
 
51
- async delete(collection: string, ids: (string | number)[]): Promise<void> {
124
+ async delete(collection: string, ids: readonly string[]): Promise<void> {
52
125
  if (ids.length === 0) return
53
126
  const placeholders = ids.map((_, i) => `$${i + 2}`).join(', ')
54
- await Database.raw.unsafe(
55
- `DELETE FROM _strav_vectors WHERE collection = $1 AND id IN (${placeholders})`,
56
- [collection, ...ids]
127
+ await this.db.execute(
128
+ `DELETE FROM "${this.table}" WHERE "collection" = $1 AND "id" IN (${placeholders})`,
129
+ [collection, ...ids],
57
130
  )
58
131
  }
59
132
 
60
- async deleteBySource(collection: string, sourceId: string | number): Promise<void> {
61
- await Database.raw.unsafe(
62
- `DELETE FROM _strav_vectors WHERE collection = $1 AND source_id = $2`,
63
- [collection, String(sourceId)]
133
+ async deleteBySource(collection: string, sourceId: string): Promise<void> {
134
+ await this.db.execute(
135
+ `DELETE FROM "${this.table}" WHERE "collection" = $1 AND "source_id" = $2`,
136
+ [collection, sourceId],
64
137
  )
65
138
  }
66
139
 
67
140
  async flush(collection: string): Promise<void> {
68
- await Database.raw.unsafe(
69
- `DELETE FROM _strav_vectors WHERE collection = $1`,
70
- [collection]
141
+ await this.db.execute(
142
+ `DELETE FROM "${this.table}" WHERE "collection" = $1`,
143
+ [collection],
71
144
  )
72
145
  }
73
146
 
147
+ // ─── Query ────────────────────────────────────────────────────────────
148
+
74
149
  async query(
75
150
  collection: string,
76
- vector: number[],
77
- options?: QueryOptions
151
+ vector: readonly number[],
152
+ options: QueryOptions = {},
78
153
  ): Promise<QueryResult> {
79
154
  const start = performance.now()
80
- const topK = options?.topK ?? 5
81
- const threshold = options?.threshold ?? 0
82
- const embeddingStr = `[${vector.join(',')}]`
155
+ const topK = options.topK ?? 5
156
+ const threshold = options.threshold
83
157
 
84
- let whereClause = 'collection = $1'
85
- const params: unknown[] = [collection]
86
- let paramIndex = 2
158
+ // pgvector's `<=>` is cosine distance in [0, 2]; `1 - (a <=> b)`
159
+ // is cosine similarity. We further map cos similarity in
160
+ // [-1, 1] → [0, 1] via `(s + 1) / 2` to match MemoryDriver so
161
+ // scores are comparable across drivers.
162
+ const params: unknown[] = [collection, `[${vector.join(',')}]`]
163
+ const where: string[] = [`"collection" = $1`]
87
164
 
88
- if (options?.filter) {
165
+ if (options.filter) {
89
166
  for (const [key, value] of Object.entries(options.filter)) {
90
- whereClause += ` AND metadata->>'${key}' = $${paramIndex}`
91
- params.push(String(value))
92
- paramIndex++
167
+ params.push(JSON.stringify(value))
168
+ where.push(`"metadata" @> jsonb_build_object('${escapeJsonbKey(key)}', $${params.length}::jsonb)`)
93
169
  }
94
170
  }
95
171
 
96
- if (threshold > 0) {
97
- whereClause += ` AND (embedding <=> $${paramIndex}::vector) <= $${paramIndex + 1}`
98
- params.push(embeddingStr, 1 - threshold)
99
- paramIndex += 2
172
+ let sql = `
173
+ SELECT "id", "source_id", "content", "metadata",
174
+ ((1 - ("embedding" <=> $2::vector)) + 1) / 2 AS score
175
+ FROM "${this.table}"
176
+ WHERE ${where.join(' AND ')}
177
+ `
178
+ if (threshold !== undefined) {
179
+ params.push(threshold)
180
+ sql += ` AND ((1 - ("embedding" <=> $2::vector)) + 1) / 2 >= $${params.length}`
100
181
  }
101
-
182
+ params.push(topK)
183
+ sql += ` ORDER BY "embedding" <=> $2::vector LIMIT $${params.length}`
184
+
185
+ let rows: Array<{
186
+ id: string
187
+ source_id: string | null
188
+ content: string
189
+ metadata: Record<string, unknown> | string
190
+ score: number | string
191
+ }>
102
192
  try {
103
- const rows = (await Database.raw.unsafe(
104
- `SELECT id, source_id, content, metadata,
105
- 1 - (embedding <=> $${paramIndex}::vector) AS score
106
- FROM _strav_vectors
107
- WHERE ${whereClause}
108
- ORDER BY embedding <=> $${paramIndex}::vector
109
- LIMIT $${paramIndex + 1}`,
110
- [...params, embeddingStr, topK]
111
- )) as any[]
112
-
113
- const matches: VectorMatch[] = rows.map((row: any) => ({
114
- id: row.source_id ?? row.id,
115
- content: row.content,
116
- score: parseFloat(row.score),
117
- metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata ?? {},
118
- }))
119
-
120
- return {
121
- matches,
122
- processingTimeMs: performance.now() - start,
123
- }
124
- } catch (err) {
125
- throw new VectorQueryError(collection, err instanceof Error ? err.message : String(err))
126
- }
127
- }
128
-
129
- private async ensureTable(dimension: number): Promise<void> {
130
- if (this.initialized) return
131
-
132
- const sql = Database.raw
133
-
134
- await sql.unsafe(`CREATE EXTENSION IF NOT EXISTS vector`)
135
-
136
- await sql.unsafe(`
137
- CREATE TABLE IF NOT EXISTS _strav_vectors (
138
- id BIGSERIAL PRIMARY KEY,
139
- collection VARCHAR(255) NOT NULL,
140
- source_id VARCHAR(255),
141
- content TEXT NOT NULL,
142
- metadata JSONB DEFAULT '{}',
143
- embedding vector(${dimension}),
144
- created_at TIMESTAMPTZ DEFAULT NOW()
193
+ rows = await this.db.query(sql, params)
194
+ } catch (cause) {
195
+ throw new VectorQueryError(
196
+ `pgvector query failed for collection "${collection}".`,
197
+ { context: { collection, table: this.table }, cause },
145
198
  )
146
- `)
199
+ }
147
200
 
148
- await sql.unsafe(
149
- `CREATE INDEX IF NOT EXISTS idx_strav_vectors_collection ON _strav_vectors(collection)`
150
- )
151
- await sql.unsafe(
152
- `CREATE INDEX IF NOT EXISTS idx_strav_vectors_source ON _strav_vectors(collection, source_id)`
153
- )
201
+ const matches: VectorMatch[] = rows.map((r) => ({
202
+ id: r.id,
203
+ content: r.content,
204
+ score: typeof r.score === 'string' ? Number.parseFloat(r.score) : r.score,
205
+ metadata: typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata,
206
+ sourceId: r.source_id,
207
+ }))
208
+ return { matches, processingTimeMs: performance.now() - start }
209
+ }
210
+ }
154
211
 
155
- this.initialized = true
212
+ /**
213
+ * Escape a JSONB object key for embedding in an SQL string. Keys
214
+ * are app-supplied so we sanitize defensively — backslash-escape
215
+ * single quotes; refuse keys with NUL bytes.
216
+ */
217
+ function escapeJsonbKey(key: string): string {
218
+ if (key.includes('\0')) {
219
+ throw new VectorQueryError(`pgvector filter key contains NUL byte: ${JSON.stringify(key)}`)
156
220
  }
221
+ return key.replace(/'/g, "''")
157
222
  }
package/src/index.ts CHANGED
@@ -1,48 +1,52 @@
1
- // Manager
2
- export { default, default as RagManager } from './rag_manager.ts'
1
+ // Public API of `@strav/rag`.
2
+ //
3
+ // V1: vector store abstraction + memory & pgvector drivers +
4
+ // fixed-size & recursive chunkers + RagManager + RagProvider.
5
+ // Composes with `@strav/brain` for embeddings and `@strav/database`
6
+ // for pgvector persistence + multitenancy.
7
+ //
8
+ // Deferred to follow-up slices: `retrievable()` repository mixin,
9
+ // CLI commands (`rag:reindex`, `rag:flush`), re-ranking strategies.
3
10
 
4
- // Provider
5
- export { default as RagProvider } from './rag_provider.ts'
6
-
7
- // Store interface
8
- export type { VectorStore } from './vector_store.ts'
9
-
10
- // Drivers
11
- export { NullDriver } from './drivers/null_driver.ts'
12
- export { MemoryDriver } from './drivers/memory_driver.ts'
13
- export { PgvectorDriver } from './drivers/pgvector_driver.ts'
14
-
15
- // Mixin
16
- export { retrievable } from './retrievable.ts'
17
- export type { RetrievableInstance, RetrievableModel } from './retrievable.ts'
18
-
19
- // Helper
20
- export { rag } from './helpers.ts'
21
-
22
- // Chunking
23
11
  export { createChunker } from './chunking/chunker.ts'
24
12
  export { FixedSizeChunker } from './chunking/fixed_size_chunker.ts'
25
13
  export { RecursiveChunker } from './chunking/recursive_chunker.ts'
26
-
27
- // Errors
28
- export { RagError, CollectionNotFoundError, VectorQueryError, EmbeddingError } from './errors.ts'
29
-
30
- // Types
14
+ export { MemoryDriver } from './drivers/memory_driver.ts'
15
+ export {
16
+ PgvectorDriver,
17
+ type PgvectorDriverOptions,
18
+ } from './drivers/pgvector_driver.ts'
19
+ export {
20
+ applyRagVectorMigration,
21
+ type ApplyRagVectorMigrationOptions,
22
+ } from './migrations.ts'
23
+ export {
24
+ CollectionNotFoundError,
25
+ EmbeddingError,
26
+ RagError,
27
+ VectorQueryError,
28
+ } from './rag_error.ts'
29
+ export {
30
+ type IngestOptions,
31
+ RagManager,
32
+ type RagManagerOptions,
33
+ type StoreFactory,
34
+ } from './rag_manager.ts'
35
+ export { RagProvider } from './rag_provider.ts'
36
+ export { ragVectorSchema } from './rag_vector_schema.ts'
31
37
  export type {
32
- RagConfig,
33
- StoreConfig,
34
- EmbeddingConfig,
38
+ Chunk,
39
+ Chunker,
35
40
  ChunkingConfig,
36
- VectorDocument,
41
+ EmbeddingConfig,
37
42
  QueryOptions,
38
43
  QueryResult,
39
- VectorMatch,
44
+ RagConfig,
40
45
  RetrieveOptions,
41
- RerankOptions,
42
46
  RetrieveResult,
43
47
  RetrievedDocument,
44
- Chunk,
45
- Chunker,
48
+ StoreConfig,
49
+ VectorDocument,
50
+ VectorMatch,
46
51
  } from './types.ts'
47
-
48
- export type { IngestOptions } from './helpers.ts'
52
+ export type { VectorStore } from './vector_store.ts'
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Migration helpers — emit the DDL apps need to put `rag_vector`
3
+ * into a working state. The framework's `emitCreateTable` handles
4
+ * everything except the pgvector-specific bits (the `vector(N)`
5
+ * column type and the HNSW index). This module fills the gap.
6
+ *
7
+ * Apps drop one call into their migration:
8
+ *
9
+ * ```ts
10
+ * import { SchemaRegistry, emitDropTable, type Migration } from '@strav/database'
11
+ * import { applyRagVectorMigration, ragVectorSchema } from '@strav/rag'
12
+ *
13
+ * export const migration: Migration = {
14
+ * name: '20260601000000_create_rag_vector',
15
+ * async up(db) {
16
+ * await applyRagVectorMigration(db, {
17
+ * dimension: 1536, // match the embedding model
18
+ * registry,
19
+ * })
20
+ * },
21
+ * async down(db) {
22
+ * await db.execute(emitDropTable(ragVectorSchema.name).sql)
23
+ * },
24
+ * }
25
+ * ```
26
+ *
27
+ * The helper is idempotent against `IF NOT EXISTS` clauses where
28
+ * Postgres supports them, but apps should still rely on the
29
+ * migration runner's tracking table for re-run safety rather than
30
+ * the helper itself.
31
+ */
32
+
33
+ import {
34
+ emitCreateTable,
35
+ type DatabaseExecutor,
36
+ type SchemaRegistry,
37
+ } from '@strav/database'
38
+ import { ragVectorSchema } from './rag_vector_schema.ts'
39
+
40
+ export interface ApplyRagVectorMigrationOptions {
41
+ /**
42
+ * Vector dimension. Must match the configured embedding model
43
+ * (OpenAI's `text-embedding-3-small` → 1536,
44
+ * `text-embedding-3-large` → 3072, Gemini's
45
+ * `text-embedding-004` → 768, etc.). Mismatched dimensions
46
+ * cause `vector` casts at INSERT to throw.
47
+ */
48
+ dimension: number
49
+ /**
50
+ * Schema registry — required for `emitCreateTable` to resolve
51
+ * foreign-key references (the tenant registry, in this case).
52
+ */
53
+ registry: SchemaRegistry
54
+ /**
55
+ * Optional override table name. Defaults to `rag_vector` (the
56
+ * `ragVectorSchema.name`). Apps that need multiple vector
57
+ * tables (e.g., one per dimension) override this here AND
58
+ * register their own schema variant under the override name.
59
+ */
60
+ table?: string
61
+ /**
62
+ * HNSW construction parameter `m`. Default Postgres-level
63
+ * default (16). Higher = better recall, slower builds.
64
+ */
65
+ hnswM?: number
66
+ /**
67
+ * HNSW construction parameter `ef_construction`. Default 64.
68
+ * Higher = better recall, slower builds.
69
+ */
70
+ hnswEfConstruction?: number
71
+ }
72
+
73
+ export async function applyRagVectorMigration(
74
+ db: DatabaseExecutor,
75
+ options: ApplyRagVectorMigrationOptions,
76
+ ): Promise<void> {
77
+ const table = options.table ?? ragVectorSchema.name
78
+ const { dimension, registry } = options
79
+
80
+ await db.execute(`CREATE EXTENSION IF NOT EXISTS vector`)
81
+
82
+ // Framework table + RLS + tenant_id column come from emitCreateTable.
83
+ await db.execute(emitCreateTable(ragVectorSchema, { registry }).sql)
84
+
85
+ // Vector column — pgvector-specific. NOT NULL because every
86
+ // ingested chunk has an embedding by construction.
87
+ await db.execute(
88
+ `ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "embedding" vector(${dimension}) NOT NULL`,
89
+ )
90
+
91
+ // HNSW index on cosine ops — pgvector's default for similarity
92
+ // search. Partial index per collection isn't possible at
93
+ // CREATE INDEX time without a literal value; apps that have
94
+ // very large per-collection corpora add `WHERE collection = '...'`
95
+ // partial indexes in a separate migration.
96
+ const hnswOpts: string[] = []
97
+ if (options.hnswM !== undefined) hnswOpts.push(`m = ${options.hnswM}`)
98
+ if (options.hnswEfConstruction !== undefined) {
99
+ hnswOpts.push(`ef_construction = ${options.hnswEfConstruction}`)
100
+ }
101
+ const withClause = hnswOpts.length > 0 ? ` WITH (${hnswOpts.join(', ')})` : ''
102
+ await db.execute(
103
+ `CREATE INDEX IF NOT EXISTS "idx_${table}_embedding_hnsw"
104
+ ON "${table}" USING hnsw ("embedding" vector_cosine_ops)${withClause}`,
105
+ )
106
+
107
+ // Helpful secondary indexes for the standard access patterns.
108
+ await db.execute(
109
+ `CREATE INDEX IF NOT EXISTS "idx_${table}_collection"
110
+ ON "${table}" ("collection")`,
111
+ )
112
+ await db.execute(
113
+ `CREATE INDEX IF NOT EXISTS "idx_${table}_source_id"
114
+ ON "${table}" ("source_id") WHERE "source_id" IS NOT NULL`,
115
+ )
116
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * `RagError` hierarchy — typed wrappers for failures in the RAG
3
+ * stack. Each subclass carries a specific error code so apps can
4
+ * branch on the failure mode at the call site instead of parsing
5
+ * error messages.
6
+ *
7
+ * Three concrete subclasses ship in V1:
8
+ *
9
+ * - `CollectionNotFoundError` — `rag.retrieve` against a
10
+ * collection that doesn't exist on the active store. Apps
11
+ * create the collection via `rag.createCollection(...)`
12
+ * before the first ingest.
13
+ *
14
+ * - `VectorQueryError` — the underlying store rejected the
15
+ * query (bad dimension, malformed filter, etc.). Cause
16
+ * carries the driver-native error.
17
+ *
18
+ * - `EmbeddingError` — the brain provider rejected the
19
+ * embedding call. Wraps the brain-side error so apps can
20
+ * `error.cause instanceof BrainError` for retry logic.
21
+ */
22
+
23
+ import { StravError } from '@strav/kernel'
24
+
25
+ export class RagError extends StravError {
26
+ constructor(
27
+ message: string,
28
+ options: {
29
+ code?: string
30
+ status?: number
31
+ context?: Record<string, unknown>
32
+ cause?: unknown
33
+ } = {},
34
+ ) {
35
+ super(
36
+ message,
37
+ { code: options.code ?? 'rag.error', status: options.status ?? 500 },
38
+ { ...(options.context ? { context: options.context } : {}), ...(options.cause !== undefined ? { cause: options.cause } : {}) },
39
+ )
40
+ }
41
+ }
42
+
43
+ export class CollectionNotFoundError extends RagError {
44
+ constructor(collection: string, store: string) {
45
+ super(
46
+ `RAG collection "${collection}" does not exist on store "${store}". Call \`rag.createCollection("${collection}", dim)\` before the first ingest.`,
47
+ {
48
+ code: 'rag.collection_not_found',
49
+ status: 404,
50
+ context: { collection, store },
51
+ },
52
+ )
53
+ }
54
+ }
55
+
56
+ export class VectorQueryError extends RagError {
57
+ constructor(message: string, options: { context?: Record<string, unknown>; cause?: unknown } = {}) {
58
+ super(message, {
59
+ code: 'rag.vector_query',
60
+ status: 500,
61
+ ...(options.context ? { context: options.context } : {}),
62
+ ...(options.cause !== undefined ? { cause: options.cause } : {}),
63
+ })
64
+ }
65
+ }
66
+
67
+ export class EmbeddingError extends RagError {
68
+ constructor(message: string, options: { context?: Record<string, unknown>; cause?: unknown } = {}) {
69
+ super(message, {
70
+ code: 'rag.embedding',
71
+ status: 500,
72
+ ...(options.context ? { context: options.context } : {}),
73
+ ...(options.cause !== undefined ? { cause: options.cause } : {}),
74
+ })
75
+ }
76
+ }