@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.
@@ -1,41 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import { bootstrap, shutdown } from '@strav/cli'
4
- import { BaseModel } from '@strav/database'
5
- import RagManager from '../rag_manager.ts'
6
-
7
- export function register(program: Command): void {
8
- program
9
- .command('rag:flush <model>')
10
- .description("Flush all vectors from a model's vector collection")
11
- .action(async (modelPath: string) => {
12
- let db
13
- try {
14
- const { db: database, config } = await bootstrap()
15
- db = database
16
-
17
- new BaseModel(db)
18
- new RagManager(config)
19
-
20
- const resolved = require.resolve(`${process.cwd()}/${modelPath}`)
21
- const module = await import(resolved)
22
- const ModelClass = module.default ?? (Object.values(module)[0] as any)
23
-
24
- if (typeof ModelClass?.flushVectors !== 'function') {
25
- console.error(chalk.red(`Model "${modelPath}" does not use the retrievable() mixin.`))
26
- process.exit(1)
27
- }
28
-
29
- const collectionName = ModelClass.retrievableAs()
30
- console.log(chalk.dim(`Flushing "${collectionName}"...`))
31
-
32
- await ModelClass.flushVectors()
33
- console.log(chalk.green(`Flushed all vectors from "${collectionName}".`))
34
- } catch (err) {
35
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
36
- process.exit(1)
37
- } finally {
38
- if (db) await shutdown(db)
39
- }
40
- })
41
- }
@@ -1,45 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import { bootstrap, shutdown } from '@strav/cli'
4
- import { BaseModel } from '@strav/database'
5
- import { BrainManager } from '@strav/brain'
6
- import RagManager from '../rag_manager.ts'
7
-
8
- export function register(program: Command): void {
9
- program
10
- .command('rag:ingest <model>')
11
- .description('Vectorize all records for a model into the vector store')
12
- .option('--chunk <size>', 'Records per batch', '100')
13
- .action(async (modelPath: string, options: { chunk: string }) => {
14
- let db
15
- try {
16
- const { db: database, config } = await bootstrap()
17
- db = database
18
-
19
- new BaseModel(db)
20
- new RagManager(config)
21
- new BrainManager(config)
22
-
23
- const resolved = require.resolve(`${process.cwd()}/${modelPath}`)
24
- const module = await import(resolved)
25
- const ModelClass = module.default ?? (Object.values(module)[0] as any)
26
-
27
- if (typeof ModelClass?.importAll !== 'function') {
28
- console.error(chalk.red(`Model "${modelPath}" does not use the retrievable() mixin.`))
29
- process.exit(1)
30
- }
31
-
32
- const chunkSize = parseInt(options.chunk, 10)
33
- const collectionName = ModelClass.retrievableAs()
34
- console.log(chalk.dim(`Vectorizing ${ModelClass.name} into "${collectionName}"...`))
35
-
36
- const count = await ModelClass.importAll(chunkSize)
37
- console.log(chalk.green(`Vectorized ${count} record(s) into "${collectionName}".`))
38
- } catch (err) {
39
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
40
- process.exit(1)
41
- } finally {
42
- if (db) await shutdown(db)
43
- }
44
- })
45
- }
@@ -1,21 +0,0 @@
1
- import type { VectorStore } from '../vector_store.ts'
2
- import type { VectorDocument, QueryOptions, QueryResult } from '../types.ts'
3
-
4
- export class NullDriver implements VectorStore {
5
- readonly name = 'null'
6
-
7
- async createCollection(_collection: string, _dimension: number): Promise<void> {}
8
- async deleteCollection(_collection: string): Promise<void> {}
9
- async upsert(_collection: string, _documents: VectorDocument[]): Promise<void> {}
10
- async delete(_collection: string, _ids: (string | number)[]): Promise<void> {}
11
- async deleteBySource(_collection: string, _sourceId: string | number): Promise<void> {}
12
- async flush(_collection: string): Promise<void> {}
13
-
14
- async query(
15
- _collection: string,
16
- _vector: number[],
17
- _options?: QueryOptions
18
- ): Promise<QueryResult> {
19
- return { matches: [] }
20
- }
21
- }
package/src/errors.ts DELETED
@@ -1,21 +0,0 @@
1
- import { StravError } from '@strav/kernel'
2
-
3
- export class RagError extends StravError {}
4
-
5
- export class CollectionNotFoundError extends RagError {
6
- constructor(collection: string) {
7
- super(`Vector collection "${collection}" not found.`)
8
- }
9
- }
10
-
11
- export class VectorQueryError extends RagError {
12
- constructor(collection: string, cause?: string) {
13
- super(`Vector query on "${collection}" failed${cause ? `: ${cause}` : ''}.`)
14
- }
15
- }
16
-
17
- export class EmbeddingError extends RagError {
18
- constructor(cause?: string) {
19
- super(`Embedding generation failed${cause ? `: ${cause}` : ''}.`)
20
- }
21
- }
package/src/helpers.ts DELETED
@@ -1,186 +0,0 @@
1
- import { brain } from '@strav/brain'
2
- import RagManager from './rag_manager.ts'
3
- import type { VectorStore } from './vector_store.ts'
4
- import type {
5
- RetrieveOptions,
6
- RetrieveResult,
7
- RetrievedDocument,
8
- VectorDocument,
9
- StoreConfig,
10
- } from './types.ts'
11
- import { createChunker } from './chunking/chunker.ts'
12
- import { EmbeddingError } from './errors.ts'
13
-
14
- export interface IngestOptions {
15
- metadata?: Record<string, unknown>
16
- sourceId?: string | number
17
- chunkSize?: number
18
- overlap?: number
19
- strategy?: string
20
- /**
21
- * Optional per-chunk sanitizer applied AFTER chunking, BEFORE
22
- * embedding. Use to scrub PII, secrets, or prompt-injection markers
23
- * out of untrusted source content before it lands in the vector
24
- * store. Return `null` to drop a chunk; otherwise return the
25
- * (possibly modified) text.
26
- *
27
- * The hook is the caller's escape valve — RAG cannot judge what's
28
- * sensitive in your domain. See `docs/rag/rag.md` "Content trust
29
- * model" for the threat surface (prompt injection at retrieval
30
- * time, indexed PII, accidental secret indexing).
31
- */
32
- sanitize?: (chunk: { content: string; index: number }) => string | null | Promise<string | null>
33
- }
34
-
35
- export const rag = {
36
- store(name?: string): VectorStore {
37
- return RagManager.store(name)
38
- },
39
-
40
- extend(name: string, factory: (config: StoreConfig) => VectorStore): void {
41
- RagManager.extend(name, factory)
42
- },
43
-
44
- async ingest(
45
- collection: string,
46
- content: string,
47
- options: IngestOptions = {}
48
- ): Promise<string[]> {
49
- const config = RagManager.config
50
- const fullCollection = RagManager.collectionName(collection)
51
-
52
- const chunkerConfig = {
53
- strategy: options.strategy ?? config.chunking.strategy,
54
- chunkSize: options.chunkSize ?? config.chunking.chunkSize,
55
- overlap: options.overlap ?? config.chunking.overlap,
56
- separators: config.chunking.separators,
57
- }
58
- const chunker = createChunker(chunkerConfig)
59
- let chunks = chunker.chunk(content)
60
-
61
- if (chunks.length === 0) return []
62
-
63
- // Apply the optional sanitize hook before embedding. Drops chunks
64
- // where the hook returns null (e.g., a chunk that's all PII).
65
- if (options.sanitize) {
66
- const sanitized: typeof chunks = []
67
- for (const chunk of chunks) {
68
- const result = await options.sanitize({ content: chunk.content, index: chunk.index })
69
- if (result === null) continue
70
- sanitized.push({ ...chunk, content: result })
71
- }
72
- chunks = sanitized
73
- if (chunks.length === 0) return []
74
- }
75
-
76
- const chunkTexts = chunks.map(c => c.content)
77
- let embeddings: number[][]
78
- try {
79
- embeddings = await brain.embed(chunkTexts, {
80
- provider: config.embedding.provider,
81
- model: config.embedding.model,
82
- })
83
- } catch (err) {
84
- throw new EmbeddingError(err instanceof Error ? err.message : String(err))
85
- }
86
-
87
- const baseId = crypto.randomUUID()
88
- const documents: VectorDocument[] = chunks.map((chunk, i) => ({
89
- id: `${baseId}_${i}`,
90
- sourceId: options.sourceId,
91
- content: chunk.content,
92
- embedding: embeddings[i]!,
93
- metadata: {
94
- ...options.metadata,
95
- chunkIndex: chunk.index,
96
- startOffset: chunk.startOffset,
97
- endOffset: chunk.endOffset,
98
- },
99
- }))
100
-
101
- await RagManager.store().upsert(fullCollection, documents)
102
- return documents.map(d => String(d.id))
103
- },
104
-
105
- async retrieve(query: string, options: RetrieveOptions = {}): Promise<RetrieveResult> {
106
- const start = performance.now()
107
- const config = RagManager.config
108
- const collection = RagManager.collectionName(options.collection ?? 'default')
109
-
110
- let queryVector: number[]
111
- try {
112
- const vectors = await brain.embed(query, {
113
- provider: config.embedding.provider,
114
- model: config.embedding.model,
115
- })
116
- queryVector = vectors[0]!
117
- } catch (err) {
118
- throw new EmbeddingError(err instanceof Error ? err.message : String(err))
119
- }
120
-
121
- const queryResult = await RagManager.store().query(collection, queryVector, {
122
- topK: options.topK,
123
- threshold: options.threshold,
124
- filter: options.filter,
125
- })
126
-
127
- let matches: RetrievedDocument[] = queryResult.matches.map(m => ({
128
- id: m.id,
129
- content: m.content,
130
- score: m.score,
131
- similarity: m.score,
132
- metadata: m.metadata,
133
- }))
134
-
135
- if (options.rerank) {
136
- const {
137
- similarityWeight = 0.6,
138
- authorityWeight = 0.2,
139
- recencyWeight = 0.2,
140
- } = options.rerank
141
-
142
- matches = matches.map(m => {
143
- const authority =
144
- typeof m.metadata.authority === 'number' ? m.metadata.authority : 0
145
- const createdAt = m.metadata.createdAt
146
- const recencyScore = createdAt
147
- ? 1 / (1 + daysSince(new Date(createdAt as string)) / 30)
148
- : 0.5
149
-
150
- const finalScore =
151
- m.similarity * similarityWeight +
152
- authority * authorityWeight +
153
- recencyScore * recencyWeight
154
-
155
- return { ...m, score: finalScore }
156
- })
157
-
158
- matches.sort((a, b) => b.score - a.score)
159
- }
160
-
161
- return {
162
- matches,
163
- query,
164
- processingTimeMs: performance.now() - start,
165
- }
166
- },
167
-
168
- async delete(collection: string, ids: (string | number)[]): Promise<void> {
169
- const fullCollection = RagManager.collectionName(collection)
170
- await RagManager.store().delete(fullCollection, ids)
171
- },
172
-
173
- async deleteBySource(collection: string, sourceId: string | number): Promise<void> {
174
- const fullCollection = RagManager.collectionName(collection)
175
- await RagManager.store().deleteBySource(fullCollection, sourceId)
176
- },
177
-
178
- async flush(collection: string): Promise<void> {
179
- const fullCollection = RagManager.collectionName(collection)
180
- await RagManager.store().flush(fullCollection)
181
- },
182
- }
183
-
184
- function daysSince(date: Date): number {
185
- return (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24)
186
- }
@@ -1,33 +0,0 @@
1
- import { env } from '@strav/kernel'
2
-
3
- export default {
4
- default: env('RAG_DRIVER', 'pgvector'),
5
-
6
- prefix: env('RAG_PREFIX', ''),
7
-
8
- embedding: {
9
- provider: env('RAG_EMBEDDING_PROVIDER', 'openai'),
10
- model: env('RAG_EMBEDDING_MODEL', 'text-embedding-3-small'),
11
- dimension: 1536,
12
- },
13
-
14
- chunking: {
15
- strategy: 'recursive',
16
- chunkSize: 512,
17
- overlap: 64,
18
- },
19
-
20
- stores: {
21
- pgvector: {
22
- driver: 'pgvector',
23
- },
24
-
25
- memory: {
26
- driver: 'memory',
27
- },
28
-
29
- null: {
30
- driver: 'null',
31
- },
32
- },
33
- }
package/tsconfig.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "include": ["src/**/*.ts"],
4
- "exclude": ["node_modules", "tests"]
5
- }