@denizsvnc/ai-rag-engine 2.2.1 → 2.2.3
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/CHANGELOG.md +31 -0
- package/dist/adapters/faq/local-faq.adapter.d.ts +2 -0
- package/dist/adapters/llm/fallback.adapter.d.ts +1 -0
- package/dist/adapters/llm/gemini.adapter.d.ts +10 -1
- package/dist/adapters/llm/openai.adapter.d.ts +12 -1
- package/dist/adapters/llm/openrouter.adapter.d.ts +4 -2
- package/dist/adapters/rules/static-rules.adapter.d.ts +6 -1
- package/dist/adapters/vector/memory-vector.adapter.d.ts +2 -0
- package/dist/api/server.js +291 -73
- package/dist/core/engine.d.ts +54 -2
- package/dist/core/metrics/rag-evaluator.d.ts +5 -0
- package/dist/core/queue/job-queue.interface.d.ts +7 -0
- package/dist/core/sml-engine.d.ts +37 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +360 -82
- package/dist/interfaces/engine-config.interface.d.ts +106 -1
- package/dist/scripts/migrate-vector.js +64 -38
- package/dist/services/document-ingestion.service.d.ts +10 -3
- package/dist/worker.js +156 -50
- package/package.json +2 -1
package/dist/api/server.js
CHANGED
|
@@ -168,7 +168,19 @@ var CircuitBreakerOpenError = class extends Error {
|
|
|
168
168
|
this.name = "CircuitBreakerOpenError";
|
|
169
169
|
}
|
|
170
170
|
};
|
|
171
|
-
var SMLEngine = class {
|
|
171
|
+
var SMLEngine = class _SMLEngine {
|
|
172
|
+
static initLockChain = Promise.resolve();
|
|
173
|
+
static async acquireInitLock() {
|
|
174
|
+
let releaseLock = () => {
|
|
175
|
+
};
|
|
176
|
+
const nextLock = new Promise((resolve) => {
|
|
177
|
+
releaseLock = resolve;
|
|
178
|
+
});
|
|
179
|
+
const currentLock = _SMLEngine.initLockChain;
|
|
180
|
+
_SMLEngine.initLockChain = currentLock.then(() => nextLock);
|
|
181
|
+
await currentLock;
|
|
182
|
+
return releaseLock;
|
|
183
|
+
}
|
|
172
184
|
modelName;
|
|
173
185
|
cacheDir;
|
|
174
186
|
localFilesOnly;
|
|
@@ -228,48 +240,62 @@ var SMLEngine = class {
|
|
|
228
240
|
console.log(`[SML CircuitBreaker] Cooldown expired. Transitioned to HALF_OPEN for probe.`);
|
|
229
241
|
}
|
|
230
242
|
if (!this.extractorPromise) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const previousCacheDir = env.cacheDir;
|
|
234
|
-
const previousAllowRemote = env.allowRemoteModels;
|
|
243
|
+
this.extractorPromise = (async () => {
|
|
244
|
+
const releaseLock = await _SMLEngine.acquireInitLock();
|
|
235
245
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
246
|
+
console.log(`[SML] Loading ${this.modelName} (dtype: ${this.dtype})...`);
|
|
247
|
+
const { pipeline: pipeline2, env } = await import("@huggingface/transformers");
|
|
248
|
+
const previousCacheDir = env.cacheDir;
|
|
249
|
+
const previousAllowRemote = env.allowRemoteModels;
|
|
250
|
+
const previousLocalModelPath = env.localModelPath;
|
|
251
|
+
try {
|
|
252
|
+
if (this.cacheDir) {
|
|
253
|
+
env.cacheDir = this.cacheDir;
|
|
254
|
+
}
|
|
255
|
+
if (this.localFilesOnly !== void 0) {
|
|
256
|
+
env.allowRemoteModels = !this.localFilesOnly;
|
|
257
|
+
}
|
|
258
|
+
const pipelineOptions = {
|
|
259
|
+
dtype: this.dtype
|
|
260
|
+
};
|
|
261
|
+
if (this.cacheDir) {
|
|
262
|
+
pipelineOptions.cache_dir = this.cacheDir;
|
|
263
|
+
}
|
|
264
|
+
if (this.localFilesOnly !== void 0) {
|
|
265
|
+
pipelineOptions.local_files_only = this.localFilesOnly;
|
|
266
|
+
}
|
|
267
|
+
const pipelineInstance = await pipeline2("feature-extraction", this.modelName, pipelineOptions);
|
|
268
|
+
this.circuitState = "CLOSED";
|
|
269
|
+
this.failureCount = 0;
|
|
270
|
+
const extract = async (text, options) => {
|
|
271
|
+
const output = await pipelineInstance(text, options);
|
|
272
|
+
const values = output.data;
|
|
273
|
+
const data = output.data instanceof Float32Array ? output.data : Float32Array.from({ length: values.length }, (_, index) => Number(values[index]));
|
|
274
|
+
return { data, dims: output.dims };
|
|
275
|
+
};
|
|
276
|
+
return extract;
|
|
277
|
+
} finally {
|
|
278
|
+
env.cacheDir = previousCacheDir;
|
|
279
|
+
env.allowRemoteModels = previousAllowRemote;
|
|
280
|
+
env.localModelPath = previousLocalModelPath;
|
|
238
281
|
}
|
|
239
|
-
|
|
240
|
-
|
|
282
|
+
} catch (err) {
|
|
283
|
+
this.failureCount++;
|
|
284
|
+
this.lastFailureTime = Date.now();
|
|
285
|
+
this.extractorPromise = null;
|
|
286
|
+
if (this.failureCount >= this.failureThreshold) {
|
|
287
|
+
this.circuitState = "OPEN";
|
|
288
|
+
console.error(
|
|
289
|
+
`[SML CircuitBreaker] \u26A0\uFE0F Tripped to OPEN state! Model failed ${this.failureCount} times. Pausing requests for ${this.cooldownMs / 1e3}s.`
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
console.error(`[SML] Model loading error (attempt ${this.failureCount}/${this.failureThreshold}):`, err);
|
|
241
293
|
}
|
|
242
|
-
|
|
243
|
-
dtype: this.dtype
|
|
244
|
-
});
|
|
294
|
+
throw err;
|
|
245
295
|
} finally {
|
|
246
|
-
|
|
247
|
-
env.allowRemoteModels = previousAllowRemote;
|
|
296
|
+
releaseLock();
|
|
248
297
|
}
|
|
249
|
-
})
|
|
250
|
-
this.circuitState = "CLOSED";
|
|
251
|
-
this.failureCount = 0;
|
|
252
|
-
const extract = async (text, options) => {
|
|
253
|
-
const output = await pipelineInstance(text, options);
|
|
254
|
-
const values = output.data;
|
|
255
|
-
const data = output.data instanceof Float32Array ? output.data : Float32Array.from({ length: values.length }, (_, index) => Number(values[index]));
|
|
256
|
-
return { data, dims: output.dims };
|
|
257
|
-
};
|
|
258
|
-
return extract;
|
|
259
|
-
}).catch((err) => {
|
|
260
|
-
this.failureCount++;
|
|
261
|
-
this.lastFailureTime = Date.now();
|
|
262
|
-
this.extractorPromise = null;
|
|
263
|
-
if (this.failureCount >= this.failureThreshold) {
|
|
264
|
-
this.circuitState = "OPEN";
|
|
265
|
-
console.error(
|
|
266
|
-
`[SML CircuitBreaker] \u26A0\uFE0F Tripped to OPEN state! Model failed ${this.failureCount} times. Pausing requests for ${this.cooldownMs / 1e3}s.`
|
|
267
|
-
);
|
|
268
|
-
} else {
|
|
269
|
-
console.error(`[SML] Model loading error (attempt ${this.failureCount}/${this.failureThreshold}):`, err);
|
|
270
|
-
}
|
|
271
|
-
throw err;
|
|
272
|
-
});
|
|
298
|
+
})();
|
|
273
299
|
}
|
|
274
300
|
return this.extractorPromise;
|
|
275
301
|
}
|
|
@@ -535,11 +561,11 @@ var InjectionDetector = class {
|
|
|
535
561
|
// 2. Jailbreaks, DAN modes, role hijacking
|
|
536
562
|
{
|
|
537
563
|
id: "JAILBREAK_MODE_EN",
|
|
538
|
-
regex: /\b(you\s+are\s+now|act\s+as|switch\s+to|enter)\s+(an?\s+)?(developer|dan|jailbreak|unrestricted|unfiltered|god)\s*(mode|persona|ai)?\b/i
|
|
564
|
+
regex: /\b(you\s+are\s+now|act\s+as|switch\s+to|enter)\s+(in\s+(an?\s+)?|an?\s+)?(developer|dan|jailbreak|unrestricted|unfiltered|god)\s*(mode|persona|ai)?\b/i
|
|
539
565
|
},
|
|
540
566
|
{
|
|
541
567
|
id: "JAILBREAK_MODE_TR",
|
|
542
|
-
regex:
|
|
568
|
+
regex: /(?:^|\s|[.,!?])(artık|şimdi)\s+(bir\s+)?(geliştirici|dan|kısıtlamasız|admin|jailbreak)\s+(modundasın|moduna\s+geç|olarak\s+davran)(?:$|\s|[.,!?])/i
|
|
543
569
|
},
|
|
544
570
|
{
|
|
545
571
|
id: "DISABLE_SAFETY_EN",
|
|
@@ -1211,6 +1237,8 @@ import fs3 from "node:fs";
|
|
|
1211
1237
|
import fsp from "node:fs/promises";
|
|
1212
1238
|
import path3 from "node:path";
|
|
1213
1239
|
import os from "node:os";
|
|
1240
|
+
import zlib from "node:zlib";
|
|
1241
|
+
import { StringDecoder } from "node:string_decoder";
|
|
1214
1242
|
|
|
1215
1243
|
// src/core/queue/in-memory-queue.ts
|
|
1216
1244
|
import { randomUUID } from "node:crypto";
|
|
@@ -1462,17 +1490,71 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1462
1490
|
* - Performs binary artifact suppression and multi-lingual UTF-8 normalization (Turkish, European, and CJK diacritics).
|
|
1463
1491
|
*/
|
|
1464
1492
|
static extractText(filename, buffer) {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
return _DocumentIngestionService.extractPdfText(rawString);
|
|
1493
|
+
if (buffer.length >= 4 && buffer.subarray(0, 4).toString() === "%PDF") {
|
|
1494
|
+
return _DocumentIngestionService.extractPdfText(buffer);
|
|
1468
1495
|
}
|
|
1469
|
-
return
|
|
1496
|
+
return buffer.toString("utf-8").trim();
|
|
1470
1497
|
}
|
|
1471
1498
|
/**
|
|
1472
|
-
* Enterprise High-Efficiency PDF Lexical Extractor.
|
|
1473
|
-
*
|
|
1499
|
+
* Enterprise High-Efficiency PDF Lexical Extractor with native zlib.inflate decompression.
|
|
1500
|
+
* Decodes and parses PDF content streams without heavy external C++/WASM dependencies.
|
|
1501
|
+
*
|
|
1502
|
+
* Supports:
|
|
1503
|
+
* - Decompression of compressed stream objects (/Filter /FlateDecode) via Node.js native zlib.
|
|
1504
|
+
* - BT (Begin Text) ... ET (End Text) matrices.
|
|
1505
|
+
* - TJ array operators with kerning spacing preservation.
|
|
1506
|
+
* - Literal (Tj) and hexadecimal (<hex> Tj) text operators with escape and octal decoding.
|
|
1474
1507
|
*/
|
|
1475
1508
|
static extractPdfText(rawContent) {
|
|
1509
|
+
const buffer = Buffer.isBuffer(rawContent) ? rawContent : Buffer.from(rawContent, "latin1");
|
|
1510
|
+
const decompressedStreams = [];
|
|
1511
|
+
const streamMarker = Buffer.from("stream");
|
|
1512
|
+
const endStreamMarker = Buffer.from("endstream");
|
|
1513
|
+
let pos = 0;
|
|
1514
|
+
while (pos < buffer.length) {
|
|
1515
|
+
const streamIdx = buffer.indexOf(streamMarker, pos);
|
|
1516
|
+
if (streamIdx === -1) break;
|
|
1517
|
+
const prevChar = streamIdx > 0 ? buffer[streamIdx - 1] : 32;
|
|
1518
|
+
if (prevChar !== 10 && prevChar !== 13 && prevChar !== 32 && prevChar !== 62) {
|
|
1519
|
+
pos = streamIdx + 6;
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
let dataStart = streamIdx + 6;
|
|
1523
|
+
if (buffer[dataStart] === 13 && buffer[dataStart + 1] === 10) {
|
|
1524
|
+
dataStart += 2;
|
|
1525
|
+
} else if (buffer[dataStart] === 10 || buffer[dataStart] === 13) {
|
|
1526
|
+
dataStart += 1;
|
|
1527
|
+
}
|
|
1528
|
+
const endIdx = buffer.indexOf(endStreamMarker, dataStart);
|
|
1529
|
+
if (endIdx === -1) break;
|
|
1530
|
+
let dataEnd = endIdx;
|
|
1531
|
+
while (dataEnd > dataStart && (buffer[dataEnd - 1] === 10 || buffer[dataEnd - 1] === 13 || buffer[dataEnd - 1] === 32)) {
|
|
1532
|
+
dataEnd--;
|
|
1533
|
+
}
|
|
1534
|
+
const streamBytes = buffer.subarray(dataStart, dataEnd);
|
|
1535
|
+
const dictStart = Math.max(0, streamIdx - 500);
|
|
1536
|
+
const dictSnippet = buffer.subarray(dictStart, streamIdx).toString("latin1");
|
|
1537
|
+
const isFlate = /\/Filter\s*(?:\[\s*)?\/(?:FlateDecode|Fl)\b/.test(dictSnippet) || !/\/Filter\b/.test(dictSnippet);
|
|
1538
|
+
let decoded = "";
|
|
1539
|
+
if (isFlate && streamBytes.length > 0) {
|
|
1540
|
+
try {
|
|
1541
|
+
decoded = zlib.inflateSync(streamBytes).toString("utf-8");
|
|
1542
|
+
} catch {
|
|
1543
|
+
try {
|
|
1544
|
+
decoded = zlib.inflateRawSync(streamBytes).toString("utf-8");
|
|
1545
|
+
} catch {
|
|
1546
|
+
decoded = streamBytes.toString("utf-8");
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
} else {
|
|
1550
|
+
decoded = streamBytes.toString("utf-8");
|
|
1551
|
+
}
|
|
1552
|
+
if (decoded && decoded.trim().length > 0) {
|
|
1553
|
+
decompressedStreams.push(decoded);
|
|
1554
|
+
}
|
|
1555
|
+
pos = endIdx + 9;
|
|
1556
|
+
}
|
|
1557
|
+
const contentToScan = [buffer.toString("latin1"), ...decompressedStreams].join("\n");
|
|
1476
1558
|
const extractedSegments = [];
|
|
1477
1559
|
const decodePdfLiteral = (encoded) => {
|
|
1478
1560
|
return encoded.replace(/\\([0-7]{1,3})/g, (_, oct) => String.fromCharCode(parseInt(oct, 8))).replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\b/g, "\b").replace(/\\f/g, "\f").replace(/\\\(/g, "(").replace(/\\\)/g, ")").replace(/\\\\/g, "\\");
|
|
@@ -1487,7 +1569,7 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1487
1569
|
};
|
|
1488
1570
|
const btEtRegex = /BT[\s\S]*?ET/g;
|
|
1489
1571
|
let btMatch;
|
|
1490
|
-
while ((btMatch = btEtRegex.exec(
|
|
1572
|
+
while ((btMatch = btEtRegex.exec(contentToScan)) !== null) {
|
|
1491
1573
|
const blockContent = btMatch[0];
|
|
1492
1574
|
const tjArrayRegex = /\[([\s\S]*?)\]\s*TJ/g;
|
|
1493
1575
|
let tjArrMatch;
|
|
@@ -1532,7 +1614,7 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1532
1614
|
if (extractedSegments.length > 0) {
|
|
1533
1615
|
return extractedSegments.join(" ").replace(/\s+/g, " ").trim();
|
|
1534
1616
|
}
|
|
1535
|
-
const cleaned =
|
|
1617
|
+
const cleaned = contentToScan.replace(/%PDF-[^\n]+/g, "").replace(/stream[\s\S]*?endstream/g, (streamObj) => {
|
|
1536
1618
|
const innerMatch = streamObj.match(/\((.*?)\)/g);
|
|
1537
1619
|
return innerMatch ? innerMatch.map((s) => decodePdfLiteral(s.slice(1, -1))).join(" ") : "";
|
|
1538
1620
|
}).replace(/obj[\s\S]*?endobj/g, (obj) => {
|
|
@@ -1542,8 +1624,17 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1542
1624
|
return cleaned || "PDF document with no extractable plain text stream.";
|
|
1543
1625
|
}
|
|
1544
1626
|
static chunkText(text, chunkSize = 500, overlap = 50) {
|
|
1545
|
-
if (!
|
|
1546
|
-
|
|
1627
|
+
if (!Number.isFinite(chunkSize) || chunkSize <= 0) {
|
|
1628
|
+
throw new RangeError(`[DocumentIngestionService] chunkSize must be a positive integer greater than 0. Received: ${chunkSize}`);
|
|
1629
|
+
}
|
|
1630
|
+
if (!Number.isFinite(overlap) || overlap < 0 || overlap >= chunkSize) {
|
|
1631
|
+
throw new RangeError(`[DocumentIngestionService] overlap must be non-negative and strictly less than chunkSize (${chunkSize}). Received: ${overlap}`);
|
|
1632
|
+
}
|
|
1633
|
+
if (!text || text.trim().length === 0) {
|
|
1634
|
+
return [];
|
|
1635
|
+
}
|
|
1636
|
+
if (text.length <= chunkSize) {
|
|
1637
|
+
return [text.trim()];
|
|
1547
1638
|
}
|
|
1548
1639
|
const chunks = [];
|
|
1549
1640
|
let startIndex = 0;
|
|
@@ -1565,6 +1656,9 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1565
1656
|
if (chunk.length > 0) {
|
|
1566
1657
|
chunks.push(chunk);
|
|
1567
1658
|
}
|
|
1659
|
+
if (endIndex >= text.length) {
|
|
1660
|
+
break;
|
|
1661
|
+
}
|
|
1568
1662
|
startIndex = Math.max(startIndex + 1, endIndex - overlap);
|
|
1569
1663
|
}
|
|
1570
1664
|
return chunks;
|
|
@@ -1573,11 +1667,19 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1573
1667
|
* Streaming chunk generator.
|
|
1574
1668
|
* Ingests chunks of text or buffers incrementally with natural backpressure,
|
|
1575
1669
|
* maintaining a constant O(1) memory footprint regardless of file size.
|
|
1670
|
+
* Preserves UTF-8 multibyte character boundaries across stream buffer splits.
|
|
1576
1671
|
*/
|
|
1577
1672
|
static async *chunkStream(textStream, chunkSize = 500, overlap = 50) {
|
|
1673
|
+
if (!Number.isFinite(chunkSize) || chunkSize <= 0) {
|
|
1674
|
+
throw new RangeError(`[DocumentIngestionService] chunkSize must be a positive integer greater than 0. Received: ${chunkSize}`);
|
|
1675
|
+
}
|
|
1676
|
+
if (!Number.isFinite(overlap) || overlap < 0 || overlap >= chunkSize) {
|
|
1677
|
+
throw new RangeError(`[DocumentIngestionService] overlap must be non-negative and strictly less than chunkSize (${chunkSize}). Received: ${overlap}`);
|
|
1678
|
+
}
|
|
1679
|
+
const decoder = new StringDecoder("utf-8");
|
|
1578
1680
|
let window = "";
|
|
1579
1681
|
for await (const rawChunk of textStream) {
|
|
1580
|
-
const chunkStr = Buffer.isBuffer(rawChunk) ?
|
|
1682
|
+
const chunkStr = Buffer.isBuffer(rawChunk) ? decoder.write(rawChunk) : String(rawChunk);
|
|
1581
1683
|
window += chunkStr;
|
|
1582
1684
|
while (window.length >= chunkSize) {
|
|
1583
1685
|
let endIndex = chunkSize;
|
|
@@ -1595,9 +1697,13 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1595
1697
|
if (emitted.length > 0) {
|
|
1596
1698
|
yield emitted;
|
|
1597
1699
|
}
|
|
1598
|
-
window = window.slice(Math.max(
|
|
1700
|
+
window = window.slice(Math.max(0, endIndex - overlap));
|
|
1599
1701
|
}
|
|
1600
1702
|
}
|
|
1703
|
+
const flushed = decoder.end();
|
|
1704
|
+
if (flushed) {
|
|
1705
|
+
window += flushed;
|
|
1706
|
+
}
|
|
1601
1707
|
const remaining = window.trim();
|
|
1602
1708
|
if (remaining.length > 0) {
|
|
1603
1709
|
yield remaining;
|
|
@@ -1945,6 +2051,12 @@ var MemoryVectorAdapter = class {
|
|
|
1945
2051
|
smlEngine;
|
|
1946
2052
|
documents = [];
|
|
1947
2053
|
bm25Index = new BM25Index();
|
|
2054
|
+
setSMLEngine(engine) {
|
|
2055
|
+
this.smlEngine = engine;
|
|
2056
|
+
}
|
|
2057
|
+
getSMLEngine() {
|
|
2058
|
+
return this.smlEngine;
|
|
2059
|
+
}
|
|
1948
2060
|
/**
|
|
1949
2061
|
* Adds documents to memory, computes their embeddings, and indexes them in BM25.
|
|
1950
2062
|
*/
|
|
@@ -2029,6 +2141,11 @@ var MemoryVectorAdapter = class {
|
|
|
2029
2141
|
|
|
2030
2142
|
// src/core/engine.ts
|
|
2031
2143
|
var RAGEngine = class {
|
|
2144
|
+
/**
|
|
2145
|
+
* Initializes a new instance of `RAGEngine` with the provided configuration.
|
|
2146
|
+
*
|
|
2147
|
+
* @param config - The engine configuration options (`EngineConfig`).
|
|
2148
|
+
*/
|
|
2032
2149
|
constructor(config) {
|
|
2033
2150
|
this.config = config;
|
|
2034
2151
|
if (config.tools?.items.some((tool) => tool.isMutating || tool.requiresConfirmation)) {
|
|
@@ -2044,8 +2161,22 @@ var RAGEngine = class {
|
|
|
2044
2161
|
config.cache?.ttlMs || 1e3 * 60 * 60 * 24
|
|
2045
2162
|
);
|
|
2046
2163
|
}
|
|
2047
|
-
this.smlEngine = new SMLEngine({
|
|
2164
|
+
this.smlEngine = new SMLEngine({
|
|
2165
|
+
modelName: config.smlConfig?.modelName,
|
|
2166
|
+
dtype: config.smlConfig?.dtype,
|
|
2167
|
+
cacheDir: config.smlConfig?.cacheDir,
|
|
2168
|
+
localFilesOnly: config.smlConfig?.localFilesOnly,
|
|
2169
|
+
cacheSize: config.smlConfig?.cacheSize,
|
|
2170
|
+
circuitBreakerThreshold: config.smlConfig?.circuitBreakerThreshold,
|
|
2171
|
+
circuitBreakerCooldownMs: config.smlConfig?.circuitBreakerCooldownMs
|
|
2172
|
+
});
|
|
2048
2173
|
const vectorStore = config.inputs?.vectorStore || new MemoryVectorAdapter(this.smlEngine);
|
|
2174
|
+
if (typeof vectorStore.setSMLEngine === "function") {
|
|
2175
|
+
vectorStore.setSMLEngine(this.smlEngine);
|
|
2176
|
+
}
|
|
2177
|
+
if (config.inputs?.faq && typeof config.inputs.faq.setSMLEngine === "function") {
|
|
2178
|
+
config.inputs.faq.setSMLEngine(this.smlEngine);
|
|
2179
|
+
}
|
|
2049
2180
|
this.config = {
|
|
2050
2181
|
...config,
|
|
2051
2182
|
inputs: {
|
|
@@ -2143,8 +2274,27 @@ var RAGEngine = class {
|
|
|
2143
2274
|
await this.toolRegistry.registerTool(tool);
|
|
2144
2275
|
}
|
|
2145
2276
|
/**
|
|
2146
|
-
* Executes the query lifecycle:
|
|
2147
|
-
* Security
|
|
2277
|
+
* Executes the complete RAG query lifecycle:
|
|
2278
|
+
* 1. Security Inspection (Prompt Injection Defense, PII Masking)
|
|
2279
|
+
* 2. Cache Lookup (Role & Tenant isolated caching)
|
|
2280
|
+
* 3. SML Semantic Routing (FAQ matching and Tool execution)
|
|
2281
|
+
* 4. Hybrid Dense/Sparse Retrieval (Vector search + BM25 keyword search)
|
|
2282
|
+
* 5. Untrusted Boundary Framing (Anti-indirect injection prompt wrapping)
|
|
2283
|
+
* 6. LLM Answer Synthesis & Citation Tagging
|
|
2284
|
+
*
|
|
2285
|
+
* @param userPrompt - The natural language query or question from the user.
|
|
2286
|
+
* @param context - Optional multi-tenant security context including `tenantId`, `userId`, and `roles`.
|
|
2287
|
+
* @returns Promise resolving to `RAGResponse` containing synthesized answer, citations, intent, and metrics.
|
|
2288
|
+
*
|
|
2289
|
+
* @example
|
|
2290
|
+
* ```typescript
|
|
2291
|
+
* const response = await engine.query('What is our return policy?', {
|
|
2292
|
+
* tenantId: 'tenant_123',
|
|
2293
|
+
* userId: 'usr_456',
|
|
2294
|
+
* roles: ['customer']
|
|
2295
|
+
* });
|
|
2296
|
+
* console.log(response.answer);
|
|
2297
|
+
* ```
|
|
2148
2298
|
*/
|
|
2149
2299
|
async query(userPrompt, context) {
|
|
2150
2300
|
const startTime = Date.now();
|
|
@@ -2538,13 +2688,29 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
2538
2688
|
apiKey;
|
|
2539
2689
|
model;
|
|
2540
2690
|
endpoint;
|
|
2541
|
-
|
|
2542
|
-
|
|
2691
|
+
temperature;
|
|
2692
|
+
constructor(configOrApiKey, model, endpoint) {
|
|
2693
|
+
let key = "";
|
|
2694
|
+
let resolvedModel = "";
|
|
2695
|
+
let resolvedEndpoint = "";
|
|
2696
|
+
let temp = 0.1;
|
|
2697
|
+
if (typeof configOrApiKey === "object" && configOrApiKey !== null) {
|
|
2698
|
+
key = configOrApiKey.apiKey || process.env.OPENAI_API_KEY || "";
|
|
2699
|
+
resolvedModel = configOrApiKey.model || process.env.OPENAI_MODEL || process.env.OPENAI_MODEL_FAST || "gpt-4o-mini";
|
|
2700
|
+
resolvedEndpoint = configOrApiKey.endpoint || process.env.OPENAI_ENDPOINT || "https://api.openai.com/v1/chat/completions";
|
|
2701
|
+
if (configOrApiKey.temperature !== void 0) temp = configOrApiKey.temperature;
|
|
2702
|
+
} else {
|
|
2703
|
+
key = configOrApiKey || process.env.OPENAI_API_KEY || "";
|
|
2704
|
+
resolvedModel = model || process.env.OPENAI_MODEL || process.env.OPENAI_MODEL_FAST || "gpt-4o-mini";
|
|
2705
|
+
resolvedEndpoint = endpoint || process.env.OPENAI_ENDPOINT || "https://api.openai.com/v1/chat/completions";
|
|
2706
|
+
}
|
|
2707
|
+
this.apiKey = key;
|
|
2543
2708
|
if (!this.apiKey) {
|
|
2544
2709
|
throw new Error("[OpenAIAdapter] API Key not found. Provide it in constructor or set OPENAI_API_KEY in .env");
|
|
2545
2710
|
}
|
|
2546
|
-
this.model =
|
|
2547
|
-
this.endpoint =
|
|
2711
|
+
this.model = resolvedModel;
|
|
2712
|
+
this.endpoint = resolvedEndpoint;
|
|
2713
|
+
this.temperature = temp;
|
|
2548
2714
|
}
|
|
2549
2715
|
static createFast(apiKey) {
|
|
2550
2716
|
return new _OpenAIAdapter(apiKey, process.env.OPENAI_MODEL_FAST || "gpt-4o-mini");
|
|
@@ -2560,7 +2726,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
2560
2726
|
const body = {
|
|
2561
2727
|
model: this.model,
|
|
2562
2728
|
messages,
|
|
2563
|
-
temperature:
|
|
2729
|
+
temperature: this.temperature
|
|
2564
2730
|
// Low temperature to reduce hallucination in RAG pipelines
|
|
2565
2731
|
};
|
|
2566
2732
|
if (request.tools && request.tools.length > 0) {
|
|
@@ -2610,15 +2776,28 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
2610
2776
|
var GeminiAdapter = class _GeminiAdapter {
|
|
2611
2777
|
apiKey;
|
|
2612
2778
|
model;
|
|
2613
|
-
|
|
2614
|
-
|
|
2779
|
+
temperature;
|
|
2780
|
+
constructor(configOrApiKey, model) {
|
|
2781
|
+
let key = "";
|
|
2782
|
+
let resolvedModel = "";
|
|
2783
|
+
let temp = 0.1;
|
|
2784
|
+
if (typeof configOrApiKey === "object" && configOrApiKey !== null) {
|
|
2785
|
+
key = configOrApiKey.apiKey || process.env.GEMINI_API_KEY || "";
|
|
2786
|
+
resolvedModel = configOrApiKey.model || process.env.GEMINI_MODEL || process.env.GEMINI_MODEL_FAST || "gemini-2.0-flash";
|
|
2787
|
+
if (configOrApiKey.temperature !== void 0) temp = configOrApiKey.temperature;
|
|
2788
|
+
} else {
|
|
2789
|
+
key = configOrApiKey || process.env.GEMINI_API_KEY || "";
|
|
2790
|
+
resolvedModel = model || process.env.GEMINI_MODEL || process.env.GEMINI_MODEL_FAST || "gemini-2.0-flash";
|
|
2791
|
+
}
|
|
2792
|
+
this.apiKey = key;
|
|
2615
2793
|
if (!this.apiKey) {
|
|
2616
2794
|
throw new Error("[GeminiAdapter] API Key not found. Provide it in constructor or set GEMINI_API_KEY in .env");
|
|
2617
2795
|
}
|
|
2618
|
-
this.model =
|
|
2796
|
+
this.model = resolvedModel;
|
|
2797
|
+
this.temperature = temp;
|
|
2619
2798
|
}
|
|
2620
2799
|
static createFast(apiKey) {
|
|
2621
|
-
return new _GeminiAdapter(apiKey, process.env.GEMINI_MODEL_FAST || "gemini-
|
|
2800
|
+
return new _GeminiAdapter(apiKey, process.env.GEMINI_MODEL_FAST || "gemini-2.0-flash");
|
|
2622
2801
|
}
|
|
2623
2802
|
static createSmart(apiKey) {
|
|
2624
2803
|
return new _GeminiAdapter(apiKey, process.env.GEMINI_MODEL_SMART || "gemini-1.5-pro");
|
|
@@ -2633,7 +2812,7 @@ var GeminiAdapter = class _GeminiAdapter {
|
|
|
2633
2812
|
}
|
|
2634
2813
|
],
|
|
2635
2814
|
generationConfig: {
|
|
2636
|
-
temperature:
|
|
2815
|
+
temperature: this.temperature
|
|
2637
2816
|
// Low temperature to reduce hallucination in RAG pipelines
|
|
2638
2817
|
}
|
|
2639
2818
|
};
|
|
@@ -2697,18 +2876,34 @@ var GeminiAdapter = class _GeminiAdapter {
|
|
|
2697
2876
|
|
|
2698
2877
|
// src/adapters/llm/openrouter.adapter.ts
|
|
2699
2878
|
var OpenRouterAdapter = class _OpenRouterAdapter extends OpenAIAdapter {
|
|
2700
|
-
constructor(
|
|
2879
|
+
constructor(configOrApiKey, model, endpoint = "https://openrouter.ai/api/v1/chat/completions") {
|
|
2701
2880
|
if (process.env.ALLOW_PUBLIC_OPENSOURCE_LLM === "false") {
|
|
2702
2881
|
throw new Error(
|
|
2703
2882
|
"[OpenRouterAdapter] Blocked by security policy: ALLOW_PUBLIC_OPENSOURCE_LLM is set to false in .env. Corporate prompts cannot be routed to public external open-source models."
|
|
2704
2883
|
);
|
|
2705
2884
|
}
|
|
2706
|
-
|
|
2885
|
+
let key = "";
|
|
2886
|
+
let resolvedModel = "";
|
|
2887
|
+
let resolvedEndpoint = endpoint;
|
|
2888
|
+
let temp = 0.1;
|
|
2889
|
+
if (typeof configOrApiKey === "object" && configOrApiKey !== null) {
|
|
2890
|
+
key = configOrApiKey.apiKey || process.env.OPENROUTER_API_KEY || "";
|
|
2891
|
+
resolvedModel = configOrApiKey.model || process.env.OPENROUTER_MODEL || process.env.OPENROUTER_MODEL_FAST || "meta-llama/llama-3.2-3b-instruct:free";
|
|
2892
|
+
if (configOrApiKey.endpoint) resolvedEndpoint = configOrApiKey.endpoint;
|
|
2893
|
+
if (configOrApiKey.temperature !== void 0) temp = configOrApiKey.temperature;
|
|
2894
|
+
} else {
|
|
2895
|
+
key = configOrApiKey || process.env.OPENROUTER_API_KEY || "";
|
|
2896
|
+
resolvedModel = model || process.env.OPENROUTER_MODEL || process.env.OPENROUTER_MODEL_FAST || "meta-llama/llama-3.2-3b-instruct:free";
|
|
2897
|
+
}
|
|
2707
2898
|
if (!key) {
|
|
2708
2899
|
throw new Error("[OpenRouterAdapter] API Key not found. Provide it in constructor or set OPENROUTER_API_KEY in .env");
|
|
2709
2900
|
}
|
|
2710
|
-
|
|
2711
|
-
|
|
2901
|
+
super({
|
|
2902
|
+
apiKey: key,
|
|
2903
|
+
model: resolvedModel,
|
|
2904
|
+
endpoint: resolvedEndpoint,
|
|
2905
|
+
temperature: temp
|
|
2906
|
+
});
|
|
2712
2907
|
}
|
|
2713
2908
|
static createFast(apiKey) {
|
|
2714
2909
|
return new _OpenRouterAdapter(apiKey, process.env.OPENROUTER_MODEL_FAST || "meta-llama/llama-3.2-3b-instruct:free");
|
|
@@ -2865,11 +3060,24 @@ var FallbackLLMProvider = class {
|
|
|
2865
3060
|
this.providers = providers;
|
|
2866
3061
|
this.logFailover = options?.logFailover ?? true;
|
|
2867
3062
|
}
|
|
3063
|
+
getProviderName(provider, index) {
|
|
3064
|
+
if (provider.name && typeof provider.name === "string") {
|
|
3065
|
+
return provider.name;
|
|
3066
|
+
}
|
|
3067
|
+
if (provider.model && typeof provider.model === "string") {
|
|
3068
|
+
const ctor = provider.constructor?.name && provider.constructor.name !== "Object" ? provider.constructor.name : "LLMProvider";
|
|
3069
|
+
return `${ctor}(${provider.model})`;
|
|
3070
|
+
}
|
|
3071
|
+
if (provider.constructor?.name && provider.constructor.name !== "Object") {
|
|
3072
|
+
return provider.constructor.name;
|
|
3073
|
+
}
|
|
3074
|
+
return `Provider#${index + 1}`;
|
|
3075
|
+
}
|
|
2868
3076
|
async generate(request) {
|
|
2869
3077
|
const failureErrors = [];
|
|
2870
3078
|
for (let i = 0; i < this.providers.length; i++) {
|
|
2871
3079
|
const provider = this.providers[i];
|
|
2872
|
-
const providerName = provider
|
|
3080
|
+
const providerName = this.getProviderName(provider, i);
|
|
2873
3081
|
try {
|
|
2874
3082
|
return await provider.generate(request);
|
|
2875
3083
|
} catch (error) {
|
|
@@ -2877,7 +3085,7 @@ var FallbackLLMProvider = class {
|
|
|
2877
3085
|
failureErrors.push({ providerName, error: errorMessage });
|
|
2878
3086
|
if (this.logFailover) {
|
|
2879
3087
|
const nextProvider = this.providers[i + 1];
|
|
2880
|
-
const nextName = nextProvider ? nextProvider
|
|
3088
|
+
const nextName = nextProvider ? this.getProviderName(nextProvider, i + 1) : "None";
|
|
2881
3089
|
console.warn(
|
|
2882
3090
|
`[FallbackLLMProvider] ${providerName} failed: "${errorMessage}". Failing over to next provider: ${nextName}`
|
|
2883
3091
|
);
|
|
@@ -3006,8 +3214,12 @@ var TieredLLMProvider = class _TieredLLMProvider {
|
|
|
3006
3214
|
// src/adapters/rules/static-rules.adapter.ts
|
|
3007
3215
|
var StaticRulesAdapter = class {
|
|
3008
3216
|
rules;
|
|
3009
|
-
constructor(rules) {
|
|
3010
|
-
this.rules = rules
|
|
3217
|
+
constructor(rules = []) {
|
|
3218
|
+
this.rules = rules.map((r) => {
|
|
3219
|
+
if (typeof r === "string") return r;
|
|
3220
|
+
if (r && typeof r === "object" && typeof r.rule === "string") return r.rule;
|
|
3221
|
+
return String(r);
|
|
3222
|
+
});
|
|
3011
3223
|
}
|
|
3012
3224
|
// Optional tenantId can be used for tenant-specific rules
|
|
3013
3225
|
async getActiveRules(tenantId) {
|
|
@@ -3029,6 +3241,12 @@ var LocalFaqAdapter = class {
|
|
|
3029
3241
|
this.smlEngine = options?.smlEngine || new SMLEngine();
|
|
3030
3242
|
this.defaultThreshold = options?.defaultMatchThreshold ?? 0.6;
|
|
3031
3243
|
}
|
|
3244
|
+
setSMLEngine(engine) {
|
|
3245
|
+
this.smlEngine = engine;
|
|
3246
|
+
}
|
|
3247
|
+
getSMLEngine() {
|
|
3248
|
+
return this.smlEngine;
|
|
3249
|
+
}
|
|
3032
3250
|
/**
|
|
3033
3251
|
* Initializes the adapter by reading the JSON file and caching embeddings in RAM.
|
|
3034
3252
|
*/
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -5,6 +5,34 @@ import type { Job } from './queue/job-queue.interface.js';
|
|
|
5
5
|
import type { QueryContext } from '../interfaces/query-context.interface.js';
|
|
6
6
|
import type { ToolAuditLog } from '../interfaces/tool.interface.js';
|
|
7
7
|
import type { ICacheAdapter } from '../interfaces/cache.interface.js';
|
|
8
|
+
/**
|
|
9
|
+
* Enterprise Production-Grade Retrieval-Augmented Generation (RAG) Engine.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Multi-tenant isolation & zero-trust security guardrails (prompt injection detection, PII redaction, boundary framing).
|
|
13
|
+
* - Pluggable LLM providers (Gemini, OpenAI, Ollama, Tiered, Fallback).
|
|
14
|
+
* - In-process Small Machine Learning (SML) engine for sub-millisecond semantic routing and multilingual embeddings.
|
|
15
|
+
* - Hybrid dense/sparse vector retrieval with Reciprocal Rank Fusion (RRF).
|
|
16
|
+
* - Governed tool execution with role-based access control (RBAC) and cryptographic mutation tickets.
|
|
17
|
+
* - High-throughput document ingestion supporting PDF (/FlateDecode decompressed), Markdown, and text streams.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const engine = new RAGEngine({
|
|
22
|
+
* llmProvider: new GeminiAdapter({ apiKey: process.env.GEMINI_API_KEY }),
|
|
23
|
+
* inputs: {
|
|
24
|
+
* vectorStore: new PostgresVectorAdapter({ connectionString: process.env.DATABASE_URL })
|
|
25
|
+
* }
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* const response = await engine.query('Summarize our Q3 compliance audit report.', {
|
|
29
|
+
* tenantId: 'enterprise-client-42',
|
|
30
|
+
* userId: 'usr_8472',
|
|
31
|
+
* roles: ['compliance_officer']
|
|
32
|
+
* });
|
|
33
|
+
* console.log(response.answer, response.sources);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
8
36
|
export declare class RAGEngine {
|
|
9
37
|
private config;
|
|
10
38
|
private confirmationService;
|
|
@@ -17,6 +45,11 @@ export declare class RAGEngine {
|
|
|
17
45
|
private initPromise;
|
|
18
46
|
private auditLogs;
|
|
19
47
|
private readonly maxAuditLogs;
|
|
48
|
+
/**
|
|
49
|
+
* Initializes a new instance of `RAGEngine` with the provided configuration.
|
|
50
|
+
*
|
|
51
|
+
* @param config - The engine configuration options (`EngineConfig`).
|
|
52
|
+
*/
|
|
20
53
|
constructor(config: EngineConfig);
|
|
21
54
|
/**
|
|
22
55
|
* Records a tool audit log entry into a bounded ring buffer (max 1000 items)
|
|
@@ -31,8 +64,27 @@ export declare class RAGEngine {
|
|
|
31
64
|
init(): Promise<void>;
|
|
32
65
|
registerTool(tool: any): Promise<void>;
|
|
33
66
|
/**
|
|
34
|
-
* Executes the query lifecycle:
|
|
35
|
-
* Security
|
|
67
|
+
* Executes the complete RAG query lifecycle:
|
|
68
|
+
* 1. Security Inspection (Prompt Injection Defense, PII Masking)
|
|
69
|
+
* 2. Cache Lookup (Role & Tenant isolated caching)
|
|
70
|
+
* 3. SML Semantic Routing (FAQ matching and Tool execution)
|
|
71
|
+
* 4. Hybrid Dense/Sparse Retrieval (Vector search + BM25 keyword search)
|
|
72
|
+
* 5. Untrusted Boundary Framing (Anti-indirect injection prompt wrapping)
|
|
73
|
+
* 6. LLM Answer Synthesis & Citation Tagging
|
|
74
|
+
*
|
|
75
|
+
* @param userPrompt - The natural language query or question from the user.
|
|
76
|
+
* @param context - Optional multi-tenant security context including `tenantId`, `userId`, and `roles`.
|
|
77
|
+
* @returns Promise resolving to `RAGResponse` containing synthesized answer, citations, intent, and metrics.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const response = await engine.query('What is our return policy?', {
|
|
82
|
+
* tenantId: 'tenant_123',
|
|
83
|
+
* userId: 'usr_456',
|
|
84
|
+
* roles: ['customer']
|
|
85
|
+
* });
|
|
86
|
+
* console.log(response.answer);
|
|
87
|
+
* ```
|
|
36
88
|
*/
|
|
37
89
|
query(userPrompt: string, context?: QueryContext): Promise<RAGResponse>;
|
|
38
90
|
/**
|
|
@@ -49,6 +49,11 @@ export declare class RAGEvaluator {
|
|
|
49
49
|
grounded: boolean;
|
|
50
50
|
groundingScore: number;
|
|
51
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Evaluates Answer Faithfulness / Groundedness using deterministic heuristics.
|
|
54
|
+
* Note: This measures lexical consistency, numerical overlap, and negation polarity.
|
|
55
|
+
* It serves as a fast in-process heuristic filter and is not an external semantic LLM judge.
|
|
56
|
+
*/
|
|
52
57
|
static evaluateFaithfulness(answer: string, contextTexts: string[]): FaithfulnessEvalResult;
|
|
53
58
|
/**
|
|
54
59
|
* Conducts a full RAG evaluation given a query, generated answer, retrieved docs, and ground truth IDs.
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export type JobStatus = 'pending' | 'queued' | 'processing' | 'completed' | 'failed';
|
|
2
|
+
export declare const JobStatusValues: {
|
|
3
|
+
readonly PENDING: 'pending';
|
|
4
|
+
readonly QUEUED: 'queued';
|
|
5
|
+
readonly PROCESSING: 'processing';
|
|
6
|
+
readonly COMPLETED: 'completed';
|
|
7
|
+
readonly FAILED: 'failed';
|
|
8
|
+
};
|
|
2
9
|
export interface Job<TData = any, TResult = any> {
|
|
3
10
|
attemptsMade?: number | undefined;
|
|
4
11
|
maxAttempts?: number | undefined;
|