@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 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
- constructor(apiKey?: string, model?: string);
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
- constructor(apiKey?: string, model?: string, endpoint?: string);
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(apiKey?: string, model?: string, endpoint?: string);
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
  }
@@ -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
- console.log(`[SML] Loading ${this.modelName} (dtype: ${this.dtype})...`);
232
- this.extractorPromise = import("@huggingface/transformers").then(async ({ pipeline: pipeline2, env }) => {
233
- const previousCacheDir = env.cacheDir;
234
- const previousAllowRemote = env.allowRemoteModels;
243
+ this.extractorPromise = (async () => {
244
+ const releaseLock = await _SMLEngine.acquireInitLock();
235
245
  try {
236
- if (this.cacheDir) {
237
- env.cacheDir = this.cacheDir;
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
- if (this.localFilesOnly) {
240
- env.allowRemoteModels = false;
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
- return await pipeline2("feature-extraction", this.modelName, {
243
- dtype: this.dtype
244
- });
294
+ throw err;
245
295
  } finally {
246
- env.cacheDir = previousCacheDir;
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
- throw err;
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
- const rawString = buffer.toString("utf-8");
1466
- if (buffer.length >= 4 && buffer.slice(0, 4).toString() === "%PDF") {
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 rawString.trim();
1496
+ return buffer.toString("utf-8").trim();
1470
1497
  }
1471
1498
  /**
1472
- * Enterprise High-Efficiency PDF Lexical Extractor.
1473
- * Parses PDF content streams without heavy external C++/WASM dependencies.
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(rawContent)) !== null) {
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 = rawContent.replace(/%PDF-[^\n]+/g, "").replace(/stream[\s\S]*?endstream/g, (streamObj) => {
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 (!text || text.length <= chunkSize) {
1546
- return text ? [text] : [];
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) ? rawChunk.toString("utf-8") : String(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(1, endIndex - overlap));
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({ modelName: config.smlConfig?.modelName });
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 Guardrail -> Cache -> SML Routing -> Retrieval -> Untrusted Boundary Framing -> LLM Generation.
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
- constructor(apiKey, model, endpoint) {
2557
- this.apiKey = apiKey || process.env.OPENAI_API_KEY || "";
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 = model || process.env.OPENAI_MODEL || process.env.OPENAI_MODEL_FAST || "gpt-4o-mini";
2562
- this.endpoint = endpoint || process.env.OPENAI_ENDPOINT || "https://api.openai.com/v1/chat/completions";
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: 0.1
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
- constructor(apiKey, model) {
2629
- this.apiKey = apiKey || process.env.GEMINI_API_KEY || "";
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 = model || process.env.GEMINI_MODEL || process.env.GEMINI_MODEL_FAST || "gemini-1.5-flash";
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-1.5-flash");
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: 0.1
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(apiKey, model, endpoint = "https://openrouter.ai/api/v1/chat/completions") {
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
- const key = apiKey || process.env.OPENROUTER_API_KEY || "";
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
- const resolvedModel = model || process.env.OPENROUTER_MODEL || process.env.OPENROUTER_MODEL_FAST || "meta-llama/llama-3.2-3b-instruct:free";
2726
- super(key, resolvedModel, endpoint);
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");
@@ -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 Guardrail -> Cache -> SML Routing -> Retrieval -> Untrusted Boundary Framing -> LLM Generation.
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.