@denizsvnc/ai-rag-engine 2.2.2 → 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 +16 -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/api/server.js +247 -67
- package/dist/core/engine.d.ts +54 -2
- package/dist/core/metrics/rag-evaluator.d.ts +5 -0
- package/dist/core/sml-engine.d.ts +37 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +306 -76
- 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 +153 -50
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
This file follows Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 2.2.3 — 2026-11-10
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Transformers.js Concurrency Lock:** Implemented static Promise lock chain (`acquireInitLock`) in `SMLEngine` to serialize `init()` calls across independent instances and isolate mutable Transformers.js global `env`. Model inference remains fully concurrent.
|
|
10
|
+
- **LLM Adapter Constructor Flexibility:** Dual constructor signatures added to `GeminiAdapter`, `OpenAIAdapter`, and `OpenRouterAdapter` accepting both `{ apiKey, model, temperature }` configuration objects and positional parameters.
|
|
11
|
+
- **Document Chunking Bounds & Streaming UTF-8 Decoder:** Enforced `RangeError` on invalid chunk boundaries (`chunkSize > 0`, `overlap < chunkSize`). Added `StringDecoder('utf-8')` to prevent split multi-byte characters (e.g. Turkish `ş`, `ğ`) across stream chunks.
|
|
12
|
+
- **Native zlib PDF FlateDecode Ingestion:** Integrated Node's native `zlib.inflateSync` & `inflateRawSync` for zero-dependency decompression of vector PDF streams.
|
|
13
|
+
- **RAGEvaluator Heuristics & Polarity:** Ensured empty answers score 0.0; deduplicated retrieved IDs; added English & Turkish negation and polarity contradiction checks.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Bun Runtime Compatibility:** Dedicated Bun test suite (`test/bun/bun-compat.test.ts`) and npm script `npm run test:bun` verifying ONNX embeddings, PII masking, vector math, and RAG execution under Bun v1.4.4+.
|
|
18
|
+
- **CI Workflow Hardening:** Added automated Bun compatibility verification job in GitHub Actions CI workflow.
|
|
19
|
+
- **Engineering & Security Audits:** Comprehensive audit documentation covering Baseline, Architecture, Security (OWASP Top 10 LLM), Invariants, Performance Benchmarks, RAG Evaluation, and Node/Bun Compatibility in `docs/audits/`.
|
|
20
|
+
|
|
5
21
|
## 2.2.2 — 2026-11-10
|
|
6
22
|
|
|
7
23
|
### Fixed
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { ILLMProvider, ILLMRequest, ILLMResponse } from '../../interfaces/llm.interface.js';
|
|
2
|
+
export interface GeminiConfig {
|
|
3
|
+
/** Google Gemini API Key. Defaults to process.env.GEMINI_API_KEY. */
|
|
4
|
+
apiKey?: string | undefined;
|
|
5
|
+
/** Model identifier (e.g. 'gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-1.5-pro'). Defaults to process.env.GEMINI_MODEL || 'gemini-2.0-flash'. */
|
|
6
|
+
model?: string | undefined;
|
|
7
|
+
/** Sampling temperature. Defaults to 0.1 for deterministic RAG answers. */
|
|
8
|
+
temperature?: number | undefined;
|
|
9
|
+
}
|
|
2
10
|
export declare class GeminiAdapter implements ILLMProvider {
|
|
3
11
|
private apiKey;
|
|
4
12
|
private model;
|
|
5
|
-
|
|
13
|
+
private temperature;
|
|
14
|
+
constructor(configOrApiKey?: string | GeminiConfig, model?: string);
|
|
6
15
|
static createFast(apiKey?: string): GeminiAdapter;
|
|
7
16
|
static createSmart(apiKey?: string): GeminiAdapter;
|
|
8
17
|
generate(request: ILLMRequest): Promise<ILLMResponse>;
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import type { ILLMProvider, ILLMRequest, ILLMResponse } from '../../interfaces/llm.interface.js';
|
|
2
|
+
export interface OpenAIConfig {
|
|
3
|
+
/** OpenAI API Key. Defaults to process.env.OPENAI_API_KEY. */
|
|
4
|
+
apiKey?: string | undefined;
|
|
5
|
+
/** Model identifier (e.g. 'gpt-4o-mini', 'gpt-4o'). Defaults to process.env.OPENAI_MODEL || 'gpt-4o-mini'. */
|
|
6
|
+
model?: string | undefined;
|
|
7
|
+
/** Base API endpoint. Defaults to 'https://api.openai.com/v1/chat/completions'. */
|
|
8
|
+
endpoint?: string | undefined;
|
|
9
|
+
/** Sampling temperature. Defaults to 0.1 for deterministic RAG answers. */
|
|
10
|
+
temperature?: number | undefined;
|
|
11
|
+
}
|
|
2
12
|
export declare class OpenAIAdapter implements ILLMProvider {
|
|
3
13
|
protected apiKey: string;
|
|
4
14
|
protected model: string;
|
|
5
15
|
protected endpoint: string;
|
|
6
|
-
|
|
16
|
+
protected temperature: number;
|
|
17
|
+
constructor(configOrApiKey?: string | OpenAIConfig, model?: string, endpoint?: string);
|
|
7
18
|
static createFast(apiKey?: string): OpenAIAdapter;
|
|
8
19
|
static createSmart(apiKey?: string): OpenAIAdapter;
|
|
9
20
|
generate(request: ILLMRequest): Promise<ILLMResponse>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { OpenAIAdapter } from './openai.adapter.js';
|
|
1
|
+
import { OpenAIAdapter, type OpenAIConfig } from './openai.adapter.js';
|
|
2
|
+
export interface OpenRouterConfig extends OpenAIConfig {
|
|
3
|
+
}
|
|
2
4
|
export declare class OpenRouterAdapter extends OpenAIAdapter {
|
|
3
|
-
constructor(
|
|
5
|
+
constructor(configOrApiKey?: string | OpenRouterConfig, model?: string, endpoint?: string);
|
|
4
6
|
static createFast(apiKey?: string): OpenRouterAdapter;
|
|
5
7
|
static createSmart(apiKey?: string): OpenRouterAdapter;
|
|
6
8
|
}
|
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;
|
|
248
|
-
}
|
|
249
|
-
}).then((pipelineInstance) => {
|
|
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);
|
|
296
|
+
releaseLock();
|
|
270
297
|
}
|
|
271
|
-
|
|
272
|
-
});
|
|
298
|
+
})();
|
|
273
299
|
}
|
|
274
300
|
return this.extractorPromise;
|
|
275
301
|
}
|
|
@@ -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;
|
|
@@ -1576,11 +1667,19 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1576
1667
|
* Streaming chunk generator.
|
|
1577
1668
|
* Ingests chunks of text or buffers incrementally with natural backpressure,
|
|
1578
1669
|
* maintaining a constant O(1) memory footprint regardless of file size.
|
|
1670
|
+
* Preserves UTF-8 multibyte character boundaries across stream buffer splits.
|
|
1579
1671
|
*/
|
|
1580
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");
|
|
1581
1680
|
let window = "";
|
|
1582
1681
|
for await (const rawChunk of textStream) {
|
|
1583
|
-
const chunkStr = Buffer.isBuffer(rawChunk) ?
|
|
1682
|
+
const chunkStr = Buffer.isBuffer(rawChunk) ? decoder.write(rawChunk) : String(rawChunk);
|
|
1584
1683
|
window += chunkStr;
|
|
1585
1684
|
while (window.length >= chunkSize) {
|
|
1586
1685
|
let endIndex = chunkSize;
|
|
@@ -1598,9 +1697,13 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1598
1697
|
if (emitted.length > 0) {
|
|
1599
1698
|
yield emitted;
|
|
1600
1699
|
}
|
|
1601
|
-
window = window.slice(Math.max(
|
|
1700
|
+
window = window.slice(Math.max(0, endIndex - overlap));
|
|
1602
1701
|
}
|
|
1603
1702
|
}
|
|
1703
|
+
const flushed = decoder.end();
|
|
1704
|
+
if (flushed) {
|
|
1705
|
+
window += flushed;
|
|
1706
|
+
}
|
|
1604
1707
|
const remaining = window.trim();
|
|
1605
1708
|
if (remaining.length > 0) {
|
|
1606
1709
|
yield remaining;
|
|
@@ -2038,6 +2141,11 @@ var MemoryVectorAdapter = class {
|
|
|
2038
2141
|
|
|
2039
2142
|
// src/core/engine.ts
|
|
2040
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
|
+
*/
|
|
2041
2149
|
constructor(config) {
|
|
2042
2150
|
this.config = config;
|
|
2043
2151
|
if (config.tools?.items.some((tool) => tool.isMutating || tool.requiresConfirmation)) {
|
|
@@ -2053,7 +2161,15 @@ var RAGEngine = class {
|
|
|
2053
2161
|
config.cache?.ttlMs || 1e3 * 60 * 60 * 24
|
|
2054
2162
|
);
|
|
2055
2163
|
}
|
|
2056
|
-
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
|
+
});
|
|
2057
2173
|
const vectorStore = config.inputs?.vectorStore || new MemoryVectorAdapter(this.smlEngine);
|
|
2058
2174
|
if (typeof vectorStore.setSMLEngine === "function") {
|
|
2059
2175
|
vectorStore.setSMLEngine(this.smlEngine);
|
|
@@ -2158,8 +2274,27 @@ var RAGEngine = class {
|
|
|
2158
2274
|
await this.toolRegistry.registerTool(tool);
|
|
2159
2275
|
}
|
|
2160
2276
|
/**
|
|
2161
|
-
* Executes the query lifecycle:
|
|
2162
|
-
* 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
|
+
* ```
|
|
2163
2298
|
*/
|
|
2164
2299
|
async query(userPrompt, context) {
|
|
2165
2300
|
const startTime = Date.now();
|
|
@@ -2553,13 +2688,29 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
2553
2688
|
apiKey;
|
|
2554
2689
|
model;
|
|
2555
2690
|
endpoint;
|
|
2556
|
-
|
|
2557
|
-
|
|
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;
|
|
2558
2708
|
if (!this.apiKey) {
|
|
2559
2709
|
throw new Error("[OpenAIAdapter] API Key not found. Provide it in constructor or set OPENAI_API_KEY in .env");
|
|
2560
2710
|
}
|
|
2561
|
-
this.model =
|
|
2562
|
-
this.endpoint =
|
|
2711
|
+
this.model = resolvedModel;
|
|
2712
|
+
this.endpoint = resolvedEndpoint;
|
|
2713
|
+
this.temperature = temp;
|
|
2563
2714
|
}
|
|
2564
2715
|
static createFast(apiKey) {
|
|
2565
2716
|
return new _OpenAIAdapter(apiKey, process.env.OPENAI_MODEL_FAST || "gpt-4o-mini");
|
|
@@ -2575,7 +2726,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
2575
2726
|
const body = {
|
|
2576
2727
|
model: this.model,
|
|
2577
2728
|
messages,
|
|
2578
|
-
temperature:
|
|
2729
|
+
temperature: this.temperature
|
|
2579
2730
|
// Low temperature to reduce hallucination in RAG pipelines
|
|
2580
2731
|
};
|
|
2581
2732
|
if (request.tools && request.tools.length > 0) {
|
|
@@ -2625,15 +2776,28 @@ var OpenAIAdapter = class _OpenAIAdapter {
|
|
|
2625
2776
|
var GeminiAdapter = class _GeminiAdapter {
|
|
2626
2777
|
apiKey;
|
|
2627
2778
|
model;
|
|
2628
|
-
|
|
2629
|
-
|
|
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;
|
|
2630
2793
|
if (!this.apiKey) {
|
|
2631
2794
|
throw new Error("[GeminiAdapter] API Key not found. Provide it in constructor or set GEMINI_API_KEY in .env");
|
|
2632
2795
|
}
|
|
2633
|
-
this.model =
|
|
2796
|
+
this.model = resolvedModel;
|
|
2797
|
+
this.temperature = temp;
|
|
2634
2798
|
}
|
|
2635
2799
|
static createFast(apiKey) {
|
|
2636
|
-
return new _GeminiAdapter(apiKey, process.env.GEMINI_MODEL_FAST || "gemini-
|
|
2800
|
+
return new _GeminiAdapter(apiKey, process.env.GEMINI_MODEL_FAST || "gemini-2.0-flash");
|
|
2637
2801
|
}
|
|
2638
2802
|
static createSmart(apiKey) {
|
|
2639
2803
|
return new _GeminiAdapter(apiKey, process.env.GEMINI_MODEL_SMART || "gemini-1.5-pro");
|
|
@@ -2648,7 +2812,7 @@ var GeminiAdapter = class _GeminiAdapter {
|
|
|
2648
2812
|
}
|
|
2649
2813
|
],
|
|
2650
2814
|
generationConfig: {
|
|
2651
|
-
temperature:
|
|
2815
|
+
temperature: this.temperature
|
|
2652
2816
|
// Low temperature to reduce hallucination in RAG pipelines
|
|
2653
2817
|
}
|
|
2654
2818
|
};
|
|
@@ -2712,18 +2876,34 @@ var GeminiAdapter = class _GeminiAdapter {
|
|
|
2712
2876
|
|
|
2713
2877
|
// src/adapters/llm/openrouter.adapter.ts
|
|
2714
2878
|
var OpenRouterAdapter = class _OpenRouterAdapter extends OpenAIAdapter {
|
|
2715
|
-
constructor(
|
|
2879
|
+
constructor(configOrApiKey, model, endpoint = "https://openrouter.ai/api/v1/chat/completions") {
|
|
2716
2880
|
if (process.env.ALLOW_PUBLIC_OPENSOURCE_LLM === "false") {
|
|
2717
2881
|
throw new Error(
|
|
2718
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."
|
|
2719
2883
|
);
|
|
2720
2884
|
}
|
|
2721
|
-
|
|
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
|
+
}
|
|
2722
2898
|
if (!key) {
|
|
2723
2899
|
throw new Error("[OpenRouterAdapter] API Key not found. Provide it in constructor or set OPENROUTER_API_KEY in .env");
|
|
2724
2900
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2901
|
+
super({
|
|
2902
|
+
apiKey: key,
|
|
2903
|
+
model: resolvedModel,
|
|
2904
|
+
endpoint: resolvedEndpoint,
|
|
2905
|
+
temperature: temp
|
|
2906
|
+
});
|
|
2727
2907
|
}
|
|
2728
2908
|
static createFast(apiKey) {
|
|
2729
2909
|
return new _OpenRouterAdapter(apiKey, process.env.OPENROUTER_MODEL_FAST || "meta-llama/llama-3.2-3b-instruct:free");
|
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.
|