@devilsdev/rag-pipeline-utils 2.6.1 → 2.7.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.
Files changed (90) hide show
  1. package/BENCHMARKS.md +7 -1
  2. package/CHANGELOG.md +135 -0
  3. package/README.md +37 -24
  4. package/dist/index.cjs +4164 -1825
  5. package/dist/index.d.ts +265 -256
  6. package/dist/index.mjs +1 -1
  7. package/dist/types/cache/memory-cache.d.ts +4 -1
  8. package/dist/types/cache/redis-cache.d.ts +4 -1
  9. package/dist/types/cli/services/instrument.d.ts +14 -0
  10. package/dist/types/cli/services/rag-service.d.ts +140 -0
  11. package/dist/types/cli/shared/config-context.d.ts +9 -0
  12. package/dist/types/config/component-factory.d.ts +57 -0
  13. package/dist/types/config/enhanced-ragrc-schema.d.ts +648 -0
  14. package/dist/types/config/load-config.d.ts +18 -0
  15. package/dist/types/config/normalize-config.d.ts +9 -0
  16. package/dist/types/core/observability/event-logger.d.ts +211 -0
  17. package/dist/types/core/observability/metrics.d.ts +585 -0
  18. package/dist/types/core/performance/parallel-processor.d.ts +92 -0
  19. package/dist/types/core/plugin-registry.d.ts +5 -0
  20. package/dist/types/dag/core/execution-scheduler.d.ts +3 -1
  21. package/dist/types/dag/dag-engine.d.ts +15 -2
  22. package/dist/types/dev/dev-server.d.ts +136 -0
  23. package/dist/types/dev/hot-reload.d.ts +226 -0
  24. package/dist/types/ecosystem/plugin-analytics-dashboard.d.ts +141 -0
  25. package/dist/types/ecosystem/plugin-hub.d.ts +161 -0
  26. package/dist/types/ecosystem/plugin-installer.d.ts +40 -0
  27. package/dist/types/ecosystem/plugin-probe.d.ts +21 -0
  28. package/dist/types/ecosystem/plugin-scanner.d.ts +30 -0
  29. package/dist/types/ecosystem/tar-extract.d.ts +24 -0
  30. package/dist/types/enterprise/audit-logging.d.ts +217 -0
  31. package/dist/types/evaluate/compare.d.ts +140 -0
  32. package/dist/types/evaluate/dataset.d.ts +43 -0
  33. package/dist/types/evaluate/evaluator.d.ts +14 -0
  34. package/dist/types/evaluate/report.d.ts +56 -0
  35. package/dist/types/evaluate/synthetic.d.ts +64 -0
  36. package/dist/types/loader/csv-loader.d.ts +10 -0
  37. package/dist/types/loader/document-reader.d.ts +18 -0
  38. package/dist/types/loader/html-loader.d.ts +21 -0
  39. package/dist/types/loader/markdown-loader.d.ts +21 -0
  40. package/dist/types/retrieval/bm25-search.d.ts +4 -0
  41. package/dist/types/retrieval/file-vector-store.d.ts +4 -1
  42. package/dist/types/retrieval/hybrid-retriever.d.ts +4 -0
  43. package/dist/types/retrieval/pgvector-retriever.d.ts +123 -0
  44. package/dist/types/retrieval/qdrant-retriever.d.ts +106 -0
  45. package/dist/types/security/validator.d.ts +11 -1
  46. package/dist/types/testing.d.ts +2 -0
  47. package/dist/types/utils/batch-processor.d.ts +205 -0
  48. package/dist/types/utils/connection-pool.d.ts +158 -0
  49. package/dist/types/utils/error-formatter.d.ts +312 -0
  50. package/dist/types/utils/logger.d.ts +29 -4
  51. package/dist/types/utils/rate-limiter.d.ts +13 -8
  52. package/dist/types/utils/retry.d.ts +10 -0
  53. package/package.json +13 -3
  54. package/src/cache/memory-cache.js +4 -1
  55. package/src/cache/redis-cache.js +4 -1
  56. package/src/cli/enhanced-cli-commands.js +79 -0
  57. package/src/cli/handlers/ai-handler.js +2 -2
  58. package/src/cli/handlers/eval-handler.js +201 -0
  59. package/src/cli/handlers/generate-handler.js +103 -0
  60. package/src/cli/services/benchmark.js +2 -2
  61. package/src/cli/services/rag-service.js +59 -10
  62. package/src/cli/shared/cli-utils.js +40 -1
  63. package/src/config/component-factory.js +32 -0
  64. package/src/connectors/local-embedder.js +20 -13
  65. package/src/core/observability/event-logger.js +135 -1
  66. package/src/core/observability/metrics.js +223 -5
  67. package/src/core/performance/parallel-processor.js +2 -0
  68. package/src/core/plugin-registry.js +5 -1
  69. package/src/dag/core/execution-scheduler.js +2 -0
  70. package/src/dag/dag-engine.js +13 -0
  71. package/src/dev/dev-server.js +6 -0
  72. package/src/ecosystem/plugin-hub.js +4 -0
  73. package/src/enterprise/audit-logging.js +5 -0
  74. package/src/evaluate/compare.js +395 -0
  75. package/src/evaluate/dataset.js +207 -0
  76. package/src/evaluate/report.js +350 -0
  77. package/src/evaluate/synthetic.js +246 -0
  78. package/src/index.js +17 -0
  79. package/src/retrieval/bm25-search.js +4 -0
  80. package/src/retrieval/file-vector-store.js +4 -1
  81. package/src/retrieval/hybrid-retriever.js +4 -0
  82. package/src/retrieval/pgvector-retriever.js +420 -0
  83. package/src/retrieval/qdrant-retriever.js +361 -0
  84. package/src/security/validator.js +8 -0
  85. package/src/testing.js +2 -0
  86. package/src/utils/batch-processor.js +7 -3
  87. package/src/utils/error-formatter.js +48 -20
  88. package/src/utils/logger.js +31 -1
  89. package/src/utils/rate-limiter.js +9 -4
  90. package/src/utils/retry.js +10 -1
@@ -0,0 +1,420 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Retriever on PostgreSQL with the pgvector extension.
5
+ *
6
+ * Pass a `pg` Pool or Client you manage (`pool`), or a `connectionString`
7
+ * and this class creates a Pool with the optional `pg` package. Documents
8
+ * live in one table: id (text, primary key), content, metadata (jsonb) and
9
+ * embedding (vector). Similarity is cosine; `score` is 1 - cosine distance.
10
+ *
11
+ * The database can change without this process knowing, so the retriever
12
+ * has no cache identity unless you give it a `cacheKey`; see getCacheKey.
13
+ */
14
+
15
+ const { throwIfAborted, raceAbort } = require('../connectors/connector-utils');
16
+
17
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/;
18
+ const DEFAULT_TABLE = 'rag_documents';
19
+ // pgvector's default hnsw.ef_search
20
+ const PG_DEFAULT_EF_SEARCH = 40;
21
+ // 4 parameters per row; pg allows 65535 per statement.
22
+ const MAX_BATCH = 1000;
23
+
24
+ /** Quotes a table name, optionally schema-qualified, after validating it. */
25
+ function quoteTable(name) {
26
+ const parts = String(name).split('.');
27
+ if (parts.length > 2 || !parts.every((p) => IDENTIFIER.test(p))) {
28
+ throw new Error(
29
+ `PgVectorRetriever: invalid table name "${name}" (letters, digits and underscores; optionally schema.table)`,
30
+ );
31
+ }
32
+ return parts.map((p) => `"${p}"`).join('.');
33
+ }
34
+
35
+ function vectorLiteral(vector, id) {
36
+ if (!Array.isArray(vector) || vector.length === 0) {
37
+ throw new Error(`PgVectorRetriever: document "${id}" has no vector`);
38
+ }
39
+ for (const x of vector) {
40
+ if (typeof x !== 'number' || !Number.isFinite(x)) {
41
+ throw new Error(
42
+ `PgVectorRetriever: document "${id}" has a vector with a non-finite value`,
43
+ );
44
+ }
45
+ }
46
+ return `[${vector.join(',')}]`;
47
+ }
48
+
49
+ /** Turns a pg error into a message that says what to do. */
50
+ function explain(err, table) {
51
+ const code = err && err.code;
52
+ if (code === '42P01') {
53
+ return new Error(
54
+ `PgVectorRetriever: table ${table} does not exist. Store documents first (rag-pipeline ingest), or create it.`,
55
+ );
56
+ }
57
+ if (
58
+ code === '42501' ||
59
+ /permission denied/i.test(String(err && err.message))
60
+ ) {
61
+ return new Error(
62
+ 'PgVectorRetriever: permission denied. Run `CREATE EXTENSION IF NOT EXISTS vector;` as a database owner, ' +
63
+ `and grant this user access to ${table}. (${err.message})`,
64
+ );
65
+ }
66
+ if (
67
+ /different vector dimensions|expected \d+ dimensions/i.test(
68
+ String(err && err.message),
69
+ )
70
+ ) {
71
+ return new Error(
72
+ `PgVectorRetriever: ${err.message}. The table holds vectors from another embedder; use the same embedder or a new table.`,
73
+ );
74
+ }
75
+ return err;
76
+ }
77
+
78
+ /**
79
+ * Retriever on PostgreSQL with the pgvector extension. Creates the
80
+ * extension, table and index on first use; similarity is cosine.
81
+ */
82
+ class PgVectorRetriever {
83
+ /**
84
+ * @param {Object} options
85
+ * @param {{ query: Function, connect?: Function }} [options.pool] - A pg Pool or Client you manage
86
+ * @param {string} [options.connectionString] - Used to create a Pool when no `pool` is given
87
+ * @param {string} [options.table='rag_documents'] - Table name, optionally `schema.table`
88
+ * @param {number} [options.dimensions] - Vector size for a new table; defaults to the first stored vector's
89
+ * @param {boolean} [options.createTable=true] - Create the extension, table and index on first store
90
+ * @param {'hnsw'|'none'} [options.index='hnsw'] - Index created with the table
91
+ * @param {number} [options.efSearch] - HNSW candidate list size per query (pgvector's default is 40). Higher finds more of the true nearest neighbours, more slowly; a query always uses at least its topK
92
+ * @param {string} [options.cacheKey] - Cache identity; change it when the table changes (see getCacheKey)
93
+ */
94
+ constructor(options = {}) {
95
+ const {
96
+ pool,
97
+ connectionString,
98
+ table = DEFAULT_TABLE,
99
+ dimensions,
100
+ createTable = true,
101
+ index = 'hnsw',
102
+ efSearch,
103
+ cacheKey,
104
+ } = options;
105
+ if (!pool && !connectionString) {
106
+ throw new Error(
107
+ 'PgVectorRetriever needs a pg `pool` (or Client) or a `connectionString`',
108
+ );
109
+ }
110
+ if (pool && typeof pool.query !== 'function') {
111
+ throw new Error('PgVectorRetriever: `pool` must have a query() method');
112
+ }
113
+ if (!['hnsw', 'none'].includes(index)) {
114
+ throw new Error(
115
+ `PgVectorRetriever: unknown index "${index}" (use "hnsw" or "none")`,
116
+ );
117
+ }
118
+ if (
119
+ dimensions !== undefined &&
120
+ !(Number.isInteger(dimensions) && dimensions > 0)
121
+ ) {
122
+ throw new Error(
123
+ 'PgVectorRetriever: `dimensions` must be a positive integer',
124
+ );
125
+ }
126
+ if (
127
+ efSearch !== undefined &&
128
+ !(Number.isInteger(efSearch) && efSearch > 0 && efSearch <= 1000)
129
+ ) {
130
+ throw new Error(
131
+ 'PgVectorRetriever: `efSearch` must be an integer from 1 to 1000',
132
+ );
133
+ }
134
+ this.efSearch = efSearch;
135
+ this.table = quoteTable(table);
136
+ this.tableName = table;
137
+ this.dimensions = dimensions;
138
+ this.createTable = createTable;
139
+ this.index = index;
140
+ this.cacheKey = cacheKey;
141
+ this._pool = pool || null;
142
+ this._connectionString = connectionString;
143
+ this._ownsPool = !pool;
144
+ this._ready = null;
145
+ }
146
+
147
+ /** The pg Pool or Client in use; creates the Pool on first use. */
148
+ _db() {
149
+ if (!this._pool) {
150
+ let pg;
151
+ try {
152
+ pg = require('pg'); // eslint-disable-line global-require
153
+ } catch {
154
+ throw new Error(
155
+ 'PgVectorRetriever with a connectionString needs the "pg" package: npm install pg',
156
+ );
157
+ }
158
+ // allowExitOnIdle: an idle pool must not keep the CLI or a script alive.
159
+ this._pool = new pg.Pool({
160
+ connectionString: this._connectionString,
161
+ allowExitOnIdle: true,
162
+ });
163
+ }
164
+ return this._pool;
165
+ }
166
+
167
+ async _query(text, params, signal) {
168
+ throwIfAborted(signal);
169
+ try {
170
+ return await raceAbort(this._db().query(text, params), signal);
171
+ } catch (err) {
172
+ throw explain(err, this.table);
173
+ }
174
+ }
175
+
176
+ /** Runs one query with `SET LOCAL hnsw.ef_search`, in its own transaction. */
177
+ async _queryWithEfSearch(text, params, ef, signal) {
178
+ throwIfAborted(signal);
179
+ const db = this._db();
180
+ const pooled =
181
+ typeof db.connect === 'function' &&
182
+ typeof db.end === 'function' &&
183
+ db.totalCount !== undefined;
184
+ const client = pooled ? await db.connect() : db;
185
+ try {
186
+ await client.query('BEGIN');
187
+ // ef is an integer checked above; SET takes no bind parameters
188
+ await client.query(`SET LOCAL hnsw.ef_search = ${Math.min(ef, 1000)}`);
189
+ const result = await client.query(text, params);
190
+ await client.query('COMMIT');
191
+ return result;
192
+ } catch (err) {
193
+ await client.query('ROLLBACK').catch(() => {});
194
+ throw explain(err, this.table);
195
+ } finally {
196
+ if (pooled) client.release();
197
+ }
198
+ }
199
+
200
+ /** Creates the extension, table and index once, sized to `dimensions`. */
201
+ _ensureTable(dimensions) {
202
+ if (!this.createTable) return Promise.resolve();
203
+ if (!this._ready) {
204
+ const size = this.dimensions || dimensions;
205
+ const indexName = `"${this.tableName.split('.').pop()}_embedding_hnsw"`;
206
+ this._ready = (async () => {
207
+ await this._query('CREATE EXTENSION IF NOT EXISTS vector');
208
+ await this._query(
209
+ `CREATE TABLE IF NOT EXISTS ${this.table} (` +
210
+ 'id text PRIMARY KEY, ' +
211
+ 'content text NOT NULL DEFAULT \'\', ' +
212
+ 'metadata jsonb NOT NULL DEFAULT \'{}\'::jsonb, ' +
213
+ `embedding vector(${size}) NOT NULL)`,
214
+ );
215
+ if (this.index === 'hnsw') {
216
+ await this._query(
217
+ `CREATE INDEX IF NOT EXISTS ${indexName} ON ${this.table} USING hnsw (embedding vector_cosine_ops)`,
218
+ );
219
+ }
220
+ })().catch((err) => {
221
+ this._ready = null;
222
+ throw err;
223
+ });
224
+ }
225
+ return this._ready;
226
+ }
227
+
228
+ /**
229
+ * Cache identity for createRagPipeline({ cache }). Other processes can
230
+ * write to the table without this one knowing, so there is none unless
231
+ * you set `cacheKey`: then cached retrieval results are reused until you
232
+ * change the key or their time to live ends.
233
+ * @param {'retriever'} role
234
+ * @returns {string|null}
235
+ */
236
+ getCacheKey(role) {
237
+ if (role !== 'retriever' || !this.cacheKey) return null;
238
+ return `pgvector|${this.tableName}|${this.cacheKey}`;
239
+ }
240
+
241
+ /**
242
+ * Inserts or replaces documents, in one transaction: a failed batch
243
+ * changes nothing.
244
+ * @param {Array<{ id: string, vector: number[], content?: string, metadata?: Object }>} docs
245
+ * @returns {Promise<void>}
246
+ */
247
+ async store(docs) {
248
+ if (!Array.isArray(docs))
249
+ throw new Error('PgVectorRetriever.store() takes an array');
250
+ if (docs.length === 0) return;
251
+ let dimensions = null;
252
+ const rows = docs.map((doc) => {
253
+ const id = doc && doc.id;
254
+ if (typeof id !== 'string' || id.length === 0) {
255
+ throw new Error(
256
+ 'PgVectorRetriever.store(): every document needs a string id',
257
+ );
258
+ }
259
+ const literal = vectorLiteral(doc.vector, id);
260
+ if (dimensions === null) dimensions = doc.vector.length;
261
+ else if (doc.vector.length !== dimensions) {
262
+ throw new Error(
263
+ `PgVectorRetriever.store(): document "${id}" has ${doc.vector.length} dimensions, the batch has ${dimensions}`,
264
+ );
265
+ }
266
+ return [
267
+ id,
268
+ doc.content || '',
269
+ JSON.stringify(doc.metadata || {}),
270
+ literal,
271
+ ];
272
+ });
273
+ await this._ensureTable(dimensions);
274
+
275
+ const db = this._db();
276
+ const pooled =
277
+ typeof db.connect === 'function' &&
278
+ typeof db.end === 'function' &&
279
+ db.totalCount !== undefined;
280
+ const client = pooled ? await db.connect() : db;
281
+ try {
282
+ await client.query('BEGIN');
283
+ for (let i = 0; i < rows.length; i += MAX_BATCH) {
284
+ const batch = rows.slice(i, i + MAX_BATCH);
285
+ const values = batch
286
+ .map(
287
+ (_, j) =>
288
+ `($${j * 4 + 1}, $${j * 4 + 2}, $${j * 4 + 3}::jsonb, $${j * 4 + 4}::vector)`,
289
+ )
290
+ .join(', ');
291
+ await client.query(
292
+ `INSERT INTO ${this.table} (id, content, metadata, embedding) VALUES ${values} ` +
293
+ 'ON CONFLICT (id) DO UPDATE SET content = EXCLUDED.content, ' +
294
+ 'metadata = EXCLUDED.metadata, embedding = EXCLUDED.embedding',
295
+ batch.flat(),
296
+ );
297
+ }
298
+ await client.query('COMMIT');
299
+ } catch (err) {
300
+ await client.query('ROLLBACK').catch(() => {});
301
+ throw explain(err, this.table);
302
+ } finally {
303
+ if (pooled) client.release();
304
+ }
305
+ }
306
+
307
+ /**
308
+ * The documents most similar to a query vector.
309
+ * @param {number[]|{ queryVector: number[], topK?: number, signal?: AbortSignal }} request
310
+ * @param {number} [k=10] - Results when `request` is a vector
311
+ * @returns {Promise<Array<{ id: string, score: number, content: string, metadata: Object }>>}
312
+ */
313
+ async retrieve(request, k = 10) {
314
+ const {
315
+ queryVector,
316
+ topK = k,
317
+ signal,
318
+ } = Array.isArray(request) ? { queryVector: request } : request || {};
319
+ if (!Number.isInteger(topK) || topK < 1) {
320
+ throw new Error(
321
+ 'PgVectorRetriever.retrieve(): topK must be a positive integer',
322
+ );
323
+ }
324
+ const literal = vectorLiteral(queryVector, 'query');
325
+ const sql =
326
+ `SELECT id, content, metadata, 1 - (embedding <=> $1::vector) AS score FROM ${this.table} ` +
327
+ 'ORDER BY embedding <=> $1::vector LIMIT $2';
328
+ // An HNSW scan returns at most ef_search rows, so a topK above it
329
+ // would come back short.
330
+ const ef = Math.max(this.efSearch || 0, topK);
331
+ const { rows } =
332
+ ef > PG_DEFAULT_EF_SEARCH || this.efSearch
333
+ ? await this._queryWithEfSearch(sql, [literal, topK], ef, signal)
334
+ : await this._query(sql, [literal, topK], signal);
335
+ throwIfAborted(signal);
336
+ return rows.map((r) => ({
337
+ id: r.id,
338
+ score: Number(r.score),
339
+ content: r.content,
340
+ metadata: r.metadata || {},
341
+ }));
342
+ }
343
+
344
+ /**
345
+ * Deletes the chunks of one source (`metadata.source`), for re-ingesting.
346
+ * @param {string} source
347
+ * @returns {Promise<number>} Rows deleted
348
+ */
349
+ async removeBySource(source) {
350
+ const { rowCount } = await this._query(
351
+ `DELETE FROM ${this.table} WHERE metadata->>'source' = $1`,
352
+ [String(source)],
353
+ ).catch((err) => {
354
+ if (/does not exist/.test(err.message)) return { rowCount: 0 };
355
+ throw err;
356
+ });
357
+ return rowCount || 0;
358
+ }
359
+
360
+ /**
361
+ * Deletes documents by id.
362
+ * @param {string[]} ids
363
+ * @returns {Promise<number>} Rows deleted
364
+ */
365
+ async delete(ids) {
366
+ const { rowCount } = await this._query(
367
+ `DELETE FROM ${this.table} WHERE id = ANY($1::text[])`,
368
+ [ids.map(String)],
369
+ );
370
+ return rowCount || 0;
371
+ }
372
+
373
+ /** Deletes every document; the table stays. */
374
+ async clear() {
375
+ await this._query(`DELETE FROM ${this.table}`).catch((err) => {
376
+ if (!/does not exist/.test(err.message)) throw err;
377
+ });
378
+ }
379
+
380
+ /** @returns {Promise<number>} */
381
+ async size() {
382
+ try {
383
+ const { rows } = await this._query(
384
+ `SELECT count(*)::int AS n FROM ${this.table}`,
385
+ );
386
+ return rows[0].n;
387
+ } catch (err) {
388
+ if (/does not exist/.test(err.message)) return 0;
389
+ throw err;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Every document, without vectors, in id order.
395
+ * @param {{ limit?: number }} [options]
396
+ * @returns {Promise<Array<{ id: string, content: string, metadata: Object }>>}
397
+ */
398
+ async list({ limit } = {}) {
399
+ const { rows } = await this._query(
400
+ `SELECT id, content, metadata FROM ${this.table} ORDER BY id${limit ? ' LIMIT $1' : ''}`,
401
+ limit ? [limit] : [],
402
+ );
403
+ return rows.map((r) => ({
404
+ id: r.id,
405
+ content: r.content,
406
+ metadata: r.metadata || {},
407
+ }));
408
+ }
409
+
410
+ /** Closes the Pool this retriever created; a pool you passed in stays open. */
411
+ async close() {
412
+ if (this._ownsPool && this._pool) {
413
+ const pool = this._pool;
414
+ this._pool = null;
415
+ await pool.end();
416
+ }
417
+ }
418
+ }
419
+
420
+ module.exports = { PgVectorRetriever, quoteTable };