@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/dist/index.js CHANGED
@@ -164,7 +164,36 @@ var CircuitBreakerOpenError = class extends Error {
164
164
  this.name = "CircuitBreakerOpenError";
165
165
  }
166
166
  };
167
- var SMLEngine = class {
167
+ var SMLModelPresets = {
168
+ /**
169
+ * Ultra-fast English embedding model (~30MB q8 ONNX).
170
+ * Ideal for English-only workloads, low-latency semantic routing, and minimal RAM footprint.
171
+ */
172
+ ENGLISH_FAST: "Xenova/all-MiniLM-L6-v2",
173
+ /**
174
+ * Highly recommended balanced multilingual model (~117MB q8 ONNX).
175
+ * Provides exceptional semantic similarity across 50+ languages including Turkish, German, English, French, and Spanish.
176
+ */
177
+ MULTILINGUAL: "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
178
+ /**
179
+ * Heavyweight high-accuracy multilingual foundation model (1024-dim, long context, 100+ languages).
180
+ * Highest precision across mixed-language corpora, but requires higher memory and CPU allocation.
181
+ */
182
+ MULTILINGUAL_ACCURATE: "Xenova/bge-m3"
183
+ };
184
+ var SMLEngine = class _SMLEngine {
185
+ static initLockChain = Promise.resolve();
186
+ static async acquireInitLock() {
187
+ let releaseLock = () => {
188
+ };
189
+ const nextLock = new Promise((resolve) => {
190
+ releaseLock = resolve;
191
+ });
192
+ const currentLock = _SMLEngine.initLockChain;
193
+ _SMLEngine.initLockChain = currentLock.then(() => nextLock);
194
+ await currentLock;
195
+ return releaseLock;
196
+ }
168
197
  modelName;
169
198
  cacheDir;
170
199
  localFilesOnly;
@@ -224,48 +253,62 @@ var SMLEngine = class {
224
253
  console.log(`[SML CircuitBreaker] Cooldown expired. Transitioned to HALF_OPEN for probe.`);
225
254
  }
226
255
  if (!this.extractorPromise) {
227
- console.log(`[SML] Loading ${this.modelName} (dtype: ${this.dtype})...`);
228
- this.extractorPromise = import("@huggingface/transformers").then(async ({ pipeline: pipeline2, env }) => {
229
- const previousCacheDir = env.cacheDir;
230
- const previousAllowRemote = env.allowRemoteModels;
256
+ this.extractorPromise = (async () => {
257
+ const releaseLock = await _SMLEngine.acquireInitLock();
231
258
  try {
232
- if (this.cacheDir) {
233
- env.cacheDir = this.cacheDir;
259
+ console.log(`[SML] Loading ${this.modelName} (dtype: ${this.dtype})...`);
260
+ const { pipeline: pipeline2, env } = await import("@huggingface/transformers");
261
+ const previousCacheDir = env.cacheDir;
262
+ const previousAllowRemote = env.allowRemoteModels;
263
+ const previousLocalModelPath = env.localModelPath;
264
+ try {
265
+ if (this.cacheDir) {
266
+ env.cacheDir = this.cacheDir;
267
+ }
268
+ if (this.localFilesOnly !== void 0) {
269
+ env.allowRemoteModels = !this.localFilesOnly;
270
+ }
271
+ const pipelineOptions = {
272
+ dtype: this.dtype
273
+ };
274
+ if (this.cacheDir) {
275
+ pipelineOptions.cache_dir = this.cacheDir;
276
+ }
277
+ if (this.localFilesOnly !== void 0) {
278
+ pipelineOptions.local_files_only = this.localFilesOnly;
279
+ }
280
+ const pipelineInstance = await pipeline2("feature-extraction", this.modelName, pipelineOptions);
281
+ this.circuitState = "CLOSED";
282
+ this.failureCount = 0;
283
+ const extract = async (text, options) => {
284
+ const output = await pipelineInstance(text, options);
285
+ const values = output.data;
286
+ const data = output.data instanceof Float32Array ? output.data : Float32Array.from({ length: values.length }, (_, index) => Number(values[index]));
287
+ return { data, dims: output.dims };
288
+ };
289
+ return extract;
290
+ } finally {
291
+ env.cacheDir = previousCacheDir;
292
+ env.allowRemoteModels = previousAllowRemote;
293
+ env.localModelPath = previousLocalModelPath;
234
294
  }
235
- if (this.localFilesOnly) {
236
- env.allowRemoteModels = false;
295
+ } catch (err) {
296
+ this.failureCount++;
297
+ this.lastFailureTime = Date.now();
298
+ this.extractorPromise = null;
299
+ if (this.failureCount >= this.failureThreshold) {
300
+ this.circuitState = "OPEN";
301
+ console.error(
302
+ `[SML CircuitBreaker] \u26A0\uFE0F Tripped to OPEN state! Model failed ${this.failureCount} times. Pausing requests for ${this.cooldownMs / 1e3}s.`
303
+ );
304
+ } else {
305
+ console.error(`[SML] Model loading error (attempt ${this.failureCount}/${this.failureThreshold}):`, err);
237
306
  }
238
- return await pipeline2("feature-extraction", this.modelName, {
239
- dtype: this.dtype
240
- });
307
+ throw err;
241
308
  } finally {
242
- env.cacheDir = previousCacheDir;
243
- env.allowRemoteModels = previousAllowRemote;
244
- }
245
- }).then((pipelineInstance) => {
246
- this.circuitState = "CLOSED";
247
- this.failureCount = 0;
248
- const extract = async (text, options) => {
249
- const output = await pipelineInstance(text, options);
250
- const values = output.data;
251
- const data = output.data instanceof Float32Array ? output.data : Float32Array.from({ length: values.length }, (_, index) => Number(values[index]));
252
- return { data, dims: output.dims };
253
- };
254
- return extract;
255
- }).catch((err) => {
256
- this.failureCount++;
257
- this.lastFailureTime = Date.now();
258
- this.extractorPromise = null;
259
- if (this.failureCount >= this.failureThreshold) {
260
- this.circuitState = "OPEN";
261
- console.error(
262
- `[SML CircuitBreaker] \u26A0\uFE0F Tripped to OPEN state! Model failed ${this.failureCount} times. Pausing requests for ${this.cooldownMs / 1e3}s.`
263
- );
264
- } else {
265
- console.error(`[SML] Model loading error (attempt ${this.failureCount}/${this.failureThreshold}):`, err);
309
+ releaseLock();
266
310
  }
267
- throw err;
268
- });
311
+ })();
269
312
  }
270
313
  return this.extractorPromise;
271
314
  }
@@ -531,11 +574,11 @@ var InjectionDetector = class {
531
574
  // 2. Jailbreaks, DAN modes, role hijacking
532
575
  {
533
576
  id: "JAILBREAK_MODE_EN",
534
- 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
577
+ 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
535
578
  },
536
579
  {
537
580
  id: "JAILBREAK_MODE_TR",
538
- regex: /\bartık\s+(geliştirici|dan|kısıtlamasız|admin|jailbreak)\s+(modundasın|moduna\s+geç|olarak\s+davran)\b/i
581
+ 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
539
582
  },
540
583
  {
541
584
  id: "DISABLE_SAFETY_EN",
@@ -1207,6 +1250,8 @@ import fs3 from "node:fs";
1207
1250
  import fsp from "node:fs/promises";
1208
1251
  import path3 from "node:path";
1209
1252
  import os from "node:os";
1253
+ import zlib from "node:zlib";
1254
+ import { StringDecoder } from "node:string_decoder";
1210
1255
 
1211
1256
  // src/core/queue/in-memory-queue.ts
1212
1257
  import { randomUUID } from "node:crypto";
@@ -1458,17 +1503,71 @@ var DocumentIngestionService = class _DocumentIngestionService {
1458
1503
  * - Performs binary artifact suppression and multi-lingual UTF-8 normalization (Turkish, European, and CJK diacritics).
1459
1504
  */
1460
1505
  static extractText(filename, buffer) {
1461
- const rawString = buffer.toString("utf-8");
1462
- if (buffer.length >= 4 && buffer.slice(0, 4).toString() === "%PDF") {
1463
- return _DocumentIngestionService.extractPdfText(rawString);
1506
+ if (buffer.length >= 4 && buffer.subarray(0, 4).toString() === "%PDF") {
1507
+ return _DocumentIngestionService.extractPdfText(buffer);
1464
1508
  }
1465
- return rawString.trim();
1509
+ return buffer.toString("utf-8").trim();
1466
1510
  }
1467
1511
  /**
1468
- * Enterprise High-Efficiency PDF Lexical Extractor.
1469
- * Parses PDF content streams without heavy external C++/WASM dependencies.
1512
+ * Enterprise High-Efficiency PDF Lexical Extractor with native zlib.inflate decompression.
1513
+ * Decodes and parses PDF content streams without heavy external C++/WASM dependencies.
1514
+ *
1515
+ * Supports:
1516
+ * - Decompression of compressed stream objects (/Filter /FlateDecode) via Node.js native zlib.
1517
+ * - BT (Begin Text) ... ET (End Text) matrices.
1518
+ * - TJ array operators with kerning spacing preservation.
1519
+ * - Literal (Tj) and hexadecimal (<hex> Tj) text operators with escape and octal decoding.
1470
1520
  */
1471
1521
  static extractPdfText(rawContent) {
1522
+ const buffer = Buffer.isBuffer(rawContent) ? rawContent : Buffer.from(rawContent, "latin1");
1523
+ const decompressedStreams = [];
1524
+ const streamMarker = Buffer.from("stream");
1525
+ const endStreamMarker = Buffer.from("endstream");
1526
+ let pos = 0;
1527
+ while (pos < buffer.length) {
1528
+ const streamIdx = buffer.indexOf(streamMarker, pos);
1529
+ if (streamIdx === -1) break;
1530
+ const prevChar = streamIdx > 0 ? buffer[streamIdx - 1] : 32;
1531
+ if (prevChar !== 10 && prevChar !== 13 && prevChar !== 32 && prevChar !== 62) {
1532
+ pos = streamIdx + 6;
1533
+ continue;
1534
+ }
1535
+ let dataStart = streamIdx + 6;
1536
+ if (buffer[dataStart] === 13 && buffer[dataStart + 1] === 10) {
1537
+ dataStart += 2;
1538
+ } else if (buffer[dataStart] === 10 || buffer[dataStart] === 13) {
1539
+ dataStart += 1;
1540
+ }
1541
+ const endIdx = buffer.indexOf(endStreamMarker, dataStart);
1542
+ if (endIdx === -1) break;
1543
+ let dataEnd = endIdx;
1544
+ while (dataEnd > dataStart && (buffer[dataEnd - 1] === 10 || buffer[dataEnd - 1] === 13 || buffer[dataEnd - 1] === 32)) {
1545
+ dataEnd--;
1546
+ }
1547
+ const streamBytes = buffer.subarray(dataStart, dataEnd);
1548
+ const dictStart = Math.max(0, streamIdx - 500);
1549
+ const dictSnippet = buffer.subarray(dictStart, streamIdx).toString("latin1");
1550
+ const isFlate = /\/Filter\s*(?:\[\s*)?\/(?:FlateDecode|Fl)\b/.test(dictSnippet) || !/\/Filter\b/.test(dictSnippet);
1551
+ let decoded = "";
1552
+ if (isFlate && streamBytes.length > 0) {
1553
+ try {
1554
+ decoded = zlib.inflateSync(streamBytes).toString("utf-8");
1555
+ } catch {
1556
+ try {
1557
+ decoded = zlib.inflateRawSync(streamBytes).toString("utf-8");
1558
+ } catch {
1559
+ decoded = streamBytes.toString("utf-8");
1560
+ }
1561
+ }
1562
+ } else {
1563
+ decoded = streamBytes.toString("utf-8");
1564
+ }
1565
+ if (decoded && decoded.trim().length > 0) {
1566
+ decompressedStreams.push(decoded);
1567
+ }
1568
+ pos = endIdx + 9;
1569
+ }
1570
+ const contentToScan = [buffer.toString("latin1"), ...decompressedStreams].join("\n");
1472
1571
  const extractedSegments = [];
1473
1572
  const decodePdfLiteral = (encoded) => {
1474
1573
  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, "\\");
@@ -1483,7 +1582,7 @@ var DocumentIngestionService = class _DocumentIngestionService {
1483
1582
  };
1484
1583
  const btEtRegex = /BT[\s\S]*?ET/g;
1485
1584
  let btMatch;
1486
- while ((btMatch = btEtRegex.exec(rawContent)) !== null) {
1585
+ while ((btMatch = btEtRegex.exec(contentToScan)) !== null) {
1487
1586
  const blockContent = btMatch[0];
1488
1587
  const tjArrayRegex = /\[([\s\S]*?)\]\s*TJ/g;
1489
1588
  let tjArrMatch;
@@ -1528,7 +1627,7 @@ var DocumentIngestionService = class _DocumentIngestionService {
1528
1627
  if (extractedSegments.length > 0) {
1529
1628
  return extractedSegments.join(" ").replace(/\s+/g, " ").trim();
1530
1629
  }
1531
- const cleaned = rawContent.replace(/%PDF-[^\n]+/g, "").replace(/stream[\s\S]*?endstream/g, (streamObj) => {
1630
+ const cleaned = contentToScan.replace(/%PDF-[^\n]+/g, "").replace(/stream[\s\S]*?endstream/g, (streamObj) => {
1532
1631
  const innerMatch = streamObj.match(/\((.*?)\)/g);
1533
1632
  return innerMatch ? innerMatch.map((s) => decodePdfLiteral(s.slice(1, -1))).join(" ") : "";
1534
1633
  }).replace(/obj[\s\S]*?endobj/g, (obj) => {
@@ -1538,8 +1637,17 @@ var DocumentIngestionService = class _DocumentIngestionService {
1538
1637
  return cleaned || "PDF document with no extractable plain text stream.";
1539
1638
  }
1540
1639
  static chunkText(text, chunkSize = 500, overlap = 50) {
1541
- if (!text || text.length <= chunkSize) {
1542
- return text ? [text] : [];
1640
+ if (!Number.isFinite(chunkSize) || chunkSize <= 0) {
1641
+ throw new RangeError(`[DocumentIngestionService] chunkSize must be a positive integer greater than 0. Received: ${chunkSize}`);
1642
+ }
1643
+ if (!Number.isFinite(overlap) || overlap < 0 || overlap >= chunkSize) {
1644
+ throw new RangeError(`[DocumentIngestionService] overlap must be non-negative and strictly less than chunkSize (${chunkSize}). Received: ${overlap}`);
1645
+ }
1646
+ if (!text || text.trim().length === 0) {
1647
+ return [];
1648
+ }
1649
+ if (text.length <= chunkSize) {
1650
+ return [text.trim()];
1543
1651
  }
1544
1652
  const chunks = [];
1545
1653
  let startIndex = 0;
@@ -1561,6 +1669,9 @@ var DocumentIngestionService = class _DocumentIngestionService {
1561
1669
  if (chunk.length > 0) {
1562
1670
  chunks.push(chunk);
1563
1671
  }
1672
+ if (endIndex >= text.length) {
1673
+ break;
1674
+ }
1564
1675
  startIndex = Math.max(startIndex + 1, endIndex - overlap);
1565
1676
  }
1566
1677
  return chunks;
@@ -1569,11 +1680,19 @@ var DocumentIngestionService = class _DocumentIngestionService {
1569
1680
  * Streaming chunk generator.
1570
1681
  * Ingests chunks of text or buffers incrementally with natural backpressure,
1571
1682
  * maintaining a constant O(1) memory footprint regardless of file size.
1683
+ * Preserves UTF-8 multibyte character boundaries across stream buffer splits.
1572
1684
  */
1573
1685
  static async *chunkStream(textStream, chunkSize = 500, overlap = 50) {
1686
+ if (!Number.isFinite(chunkSize) || chunkSize <= 0) {
1687
+ throw new RangeError(`[DocumentIngestionService] chunkSize must be a positive integer greater than 0. Received: ${chunkSize}`);
1688
+ }
1689
+ if (!Number.isFinite(overlap) || overlap < 0 || overlap >= chunkSize) {
1690
+ throw new RangeError(`[DocumentIngestionService] overlap must be non-negative and strictly less than chunkSize (${chunkSize}). Received: ${overlap}`);
1691
+ }
1692
+ const decoder = new StringDecoder("utf-8");
1574
1693
  let window = "";
1575
1694
  for await (const rawChunk of textStream) {
1576
- const chunkStr = Buffer.isBuffer(rawChunk) ? rawChunk.toString("utf-8") : String(rawChunk);
1695
+ const chunkStr = Buffer.isBuffer(rawChunk) ? decoder.write(rawChunk) : String(rawChunk);
1577
1696
  window += chunkStr;
1578
1697
  while (window.length >= chunkSize) {
1579
1698
  let endIndex = chunkSize;
@@ -1591,9 +1710,13 @@ var DocumentIngestionService = class _DocumentIngestionService {
1591
1710
  if (emitted.length > 0) {
1592
1711
  yield emitted;
1593
1712
  }
1594
- window = window.slice(Math.max(1, endIndex - overlap));
1713
+ window = window.slice(Math.max(0, endIndex - overlap));
1595
1714
  }
1596
1715
  }
1716
+ const flushed = decoder.end();
1717
+ if (flushed) {
1718
+ window += flushed;
1719
+ }
1597
1720
  const remaining = window.trim();
1598
1721
  if (remaining.length > 0) {
1599
1722
  yield remaining;
@@ -1941,6 +2064,12 @@ var MemoryVectorAdapter = class {
1941
2064
  smlEngine;
1942
2065
  documents = [];
1943
2066
  bm25Index = new BM25Index();
2067
+ setSMLEngine(engine) {
2068
+ this.smlEngine = engine;
2069
+ }
2070
+ getSMLEngine() {
2071
+ return this.smlEngine;
2072
+ }
1944
2073
  /**
1945
2074
  * Adds documents to memory, computes their embeddings, and indexes them in BM25.
1946
2075
  */
@@ -2025,6 +2154,11 @@ var MemoryVectorAdapter = class {
2025
2154
 
2026
2155
  // src/core/engine.ts
2027
2156
  var RAGEngine = class {
2157
+ /**
2158
+ * Initializes a new instance of `RAGEngine` with the provided configuration.
2159
+ *
2160
+ * @param config - The engine configuration options (`EngineConfig`).
2161
+ */
2028
2162
  constructor(config) {
2029
2163
  this.config = config;
2030
2164
  if (config.tools?.items.some((tool) => tool.isMutating || tool.requiresConfirmation)) {
@@ -2040,8 +2174,22 @@ var RAGEngine = class {
2040
2174
  config.cache?.ttlMs || 1e3 * 60 * 60 * 24
2041
2175
  );
2042
2176
  }
2043
- this.smlEngine = new SMLEngine({ modelName: config.smlConfig?.modelName });
2177
+ this.smlEngine = new SMLEngine({
2178
+ modelName: config.smlConfig?.modelName,
2179
+ dtype: config.smlConfig?.dtype,
2180
+ cacheDir: config.smlConfig?.cacheDir,
2181
+ localFilesOnly: config.smlConfig?.localFilesOnly,
2182
+ cacheSize: config.smlConfig?.cacheSize,
2183
+ circuitBreakerThreshold: config.smlConfig?.circuitBreakerThreshold,
2184
+ circuitBreakerCooldownMs: config.smlConfig?.circuitBreakerCooldownMs
2185
+ });
2044
2186
  const vectorStore = config.inputs?.vectorStore || new MemoryVectorAdapter(this.smlEngine);
2187
+ if (typeof vectorStore.setSMLEngine === "function") {
2188
+ vectorStore.setSMLEngine(this.smlEngine);
2189
+ }
2190
+ if (config.inputs?.faq && typeof config.inputs.faq.setSMLEngine === "function") {
2191
+ config.inputs.faq.setSMLEngine(this.smlEngine);
2192
+ }
2045
2193
  this.config = {
2046
2194
  ...config,
2047
2195
  inputs: {
@@ -2139,8 +2287,27 @@ var RAGEngine = class {
2139
2287
  await this.toolRegistry.registerTool(tool);
2140
2288
  }
2141
2289
  /**
2142
- * Executes the query lifecycle:
2143
- * Security Guardrail -> Cache -> SML Routing -> Retrieval -> Untrusted Boundary Framing -> LLM Generation.
2290
+ * Executes the complete RAG query lifecycle:
2291
+ * 1. Security Inspection (Prompt Injection Defense, PII Masking)
2292
+ * 2. Cache Lookup (Role & Tenant isolated caching)
2293
+ * 3. SML Semantic Routing (FAQ matching and Tool execution)
2294
+ * 4. Hybrid Dense/Sparse Retrieval (Vector search + BM25 keyword search)
2295
+ * 5. Untrusted Boundary Framing (Anti-indirect injection prompt wrapping)
2296
+ * 6. LLM Answer Synthesis & Citation Tagging
2297
+ *
2298
+ * @param userPrompt - The natural language query or question from the user.
2299
+ * @param context - Optional multi-tenant security context including `tenantId`, `userId`, and `roles`.
2300
+ * @returns Promise resolving to `RAGResponse` containing synthesized answer, citations, intent, and metrics.
2301
+ *
2302
+ * @example
2303
+ * ```typescript
2304
+ * const response = await engine.query('What is our return policy?', {
2305
+ * tenantId: 'tenant_123',
2306
+ * userId: 'usr_456',
2307
+ * roles: ['customer']
2308
+ * });
2309
+ * console.log(response.answer);
2310
+ * ```
2144
2311
  */
2145
2312
  async query(userPrompt, context) {
2146
2313
  const startTime = Date.now();
@@ -2577,7 +2744,9 @@ var RAGEvaluator = class {
2577
2744
  * Proportion of retrieved documents in the top-K that are genuinely relevant.
2578
2745
  */
2579
2746
  static precisionAtK(retrievedIds, relevantIds, k = 3) {
2580
- const topK = retrievedIds.slice(0, k);
2747
+ if (!Number.isFinite(k) || k <= 0) return 0;
2748
+ const uniqueRetrieved = Array.from(new Set(retrievedIds));
2749
+ const topK = uniqueRetrieved.slice(0, k);
2581
2750
  if (topK.length === 0) return 0;
2582
2751
  const relevantSet = new Set(relevantIds);
2583
2752
  const hits = topK.filter((id) => relevantSet.has(id)).length;
@@ -2589,7 +2758,9 @@ var RAGEvaluator = class {
2589
2758
  */
2590
2759
  static recallAtK(retrievedIds, relevantIds, k = 3) {
2591
2760
  if (relevantIds.length === 0) return 1;
2592
- const topK = retrievedIds.slice(0, k);
2761
+ if (!Number.isFinite(k) || k <= 0) return 0;
2762
+ const uniqueRetrieved = Array.from(new Set(retrievedIds));
2763
+ const topK = uniqueRetrieved.slice(0, k);
2593
2764
  const relevantSet = new Set(relevantIds);
2594
2765
  const hits = topK.filter((id) => relevantSet.has(id)).length;
2595
2766
  return Number((hits / relevantIds.length).toFixed(4));
@@ -2623,28 +2794,44 @@ var RAGEvaluator = class {
2623
2794
  groundingScore: res.score
2624
2795
  };
2625
2796
  }
2797
+ /**
2798
+ * Evaluates Answer Faithfulness / Groundedness using deterministic heuristics.
2799
+ * Note: This measures lexical consistency, numerical overlap, and negation polarity.
2800
+ * It serves as a fast in-process heuristic filter and is not an external semantic LLM judge.
2801
+ */
2626
2802
  static evaluateFaithfulness(answer, contextTexts) {
2803
+ if (!answer || answer.trim().length === 0) {
2804
+ return {
2805
+ score: 0,
2806
+ lexicalGroundingScore: 0,
2807
+ totalSentences: 0,
2808
+ groundedSentences: 0,
2809
+ contradictionsDetected: [],
2810
+ ungroundedClaims: ["Empty answer provided"]
2811
+ };
2812
+ }
2627
2813
  const combinedContext = contextTexts.join(" ").toLowerCase();
2628
2814
  const contextClean = combinedContext.replace(/[^\p{L}\p{N}\s]/gu, " ");
2629
2815
  const contextWordTokens = new Set(contextClean.split(/\s+/).filter((w) => w.length > 2));
2630
2816
  const contextNumbers = new Set(
2631
2817
  (combinedContext.match(/\b\d+(?:[\.,]\d+)?\b/g) || []).map((n) => n.replace(",", "."))
2632
2818
  );
2633
- const sentences = answer.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 10);
2819
+ const sentences = answer.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
2634
2820
  if (sentences.length === 0) {
2635
2821
  return {
2636
- score: 1,
2637
- lexicalGroundingScore: 1,
2822
+ score: 0,
2823
+ lexicalGroundingScore: 0,
2638
2824
  totalSentences: 0,
2639
2825
  groundedSentences: 0,
2640
2826
  contradictionsDetected: [],
2641
- ungroundedClaims: []
2827
+ ungroundedClaims: ["No meaningful sentences detected"]
2642
2828
  };
2643
2829
  }
2644
2830
  const ungroundedClaims = [];
2645
2831
  const contradictionsDetected = [];
2646
2832
  let groundedCount = 0;
2647
2833
  let totalLexicalOverlapSum = 0;
2834
+ const negationTokens = /* @__PURE__ */ new Set(["not", "never", "cannot", "de\u011Fil", "yok", "olumsuz", "asla", "hay\u0131r"]);
2648
2835
  for (const sentence of sentences) {
2649
2836
  const lowerSentence = sentence.toLowerCase();
2650
2837
  const sentenceNumbers = (lowerSentence.match(/\b\d+(?:[\.,]\d+)?\b/g) || []).map((n) => n.replace(",", "."));
@@ -2658,16 +2845,28 @@ var RAGEvaluator = class {
2658
2845
  break;
2659
2846
  }
2660
2847
  }
2661
- const words = lowerSentence.replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 2);
2848
+ const rawWords = lowerSentence.replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 0);
2849
+ const sentenceNegations = rawWords.filter((w) => negationTokens.has(w));
2850
+ let hasNegationMismatch = false;
2851
+ if (sentenceNegations.length > 0) {
2852
+ const contextHasNegation = sentenceNegations.some((neg) => contextWordTokens.has(neg) || combinedContext.includes(neg));
2853
+ if (!contextHasNegation) {
2854
+ hasNegationMismatch = true;
2855
+ contradictionsDetected.push(
2856
+ `Polarity Warning: Negation '${sentenceNegations.join(", ")}' in "${sentence}" absent from retrieved context.`
2857
+ );
2858
+ }
2859
+ }
2860
+ const words = rawWords.filter((w) => w.length > 2);
2662
2861
  if (words.length === 0) {
2663
- if (!hasNumberContradiction) groundedCount++;
2862
+ if (!hasNumberContradiction && !hasNegationMismatch) groundedCount++;
2664
2863
  totalLexicalOverlapSum += 1;
2665
2864
  continue;
2666
2865
  }
2667
2866
  const groundedWords = words.filter((word) => contextWordTokens.has(word) || combinedContext.includes(word));
2668
2867
  const overlapRatio = groundedWords.length / words.length;
2669
2868
  totalLexicalOverlapSum += overlapRatio;
2670
- if (overlapRatio >= 0.4 && !hasNumberContradiction) {
2869
+ if (overlapRatio >= 0.4 && !hasNumberContradiction && !hasNegationMismatch) {
2671
2870
  groundedCount++;
2672
2871
  } else {
2673
2872
  ungroundedClaims.push(sentence);
@@ -2708,6 +2907,15 @@ var RAGEvaluator = class {
2708
2907
  }
2709
2908
  };
2710
2909
 
2910
+ // src/core/queue/job-queue.interface.ts
2911
+ var JobStatusValues = {
2912
+ PENDING: "pending",
2913
+ QUEUED: "queued",
2914
+ PROCESSING: "processing",
2915
+ COMPLETED: "completed",
2916
+ FAILED: "failed"
2917
+ };
2918
+
2711
2919
  // src/core/queue/bullmq-queue.ts
2712
2920
  import { Queue, Worker, UnrecoverableError } from "bullmq";
2713
2921
  import { Redis } from "ioredis";
@@ -3389,13 +3597,29 @@ var OpenAIAdapter = class _OpenAIAdapter {
3389
3597
  apiKey;
3390
3598
  model;
3391
3599
  endpoint;
3392
- constructor(apiKey, model, endpoint) {
3393
- this.apiKey = apiKey || process.env.OPENAI_API_KEY || "";
3600
+ temperature;
3601
+ constructor(configOrApiKey, model, endpoint) {
3602
+ let key = "";
3603
+ let resolvedModel = "";
3604
+ let resolvedEndpoint = "";
3605
+ let temp = 0.1;
3606
+ if (typeof configOrApiKey === "object" && configOrApiKey !== null) {
3607
+ key = configOrApiKey.apiKey || process.env.OPENAI_API_KEY || "";
3608
+ resolvedModel = configOrApiKey.model || process.env.OPENAI_MODEL || process.env.OPENAI_MODEL_FAST || "gpt-4o-mini";
3609
+ resolvedEndpoint = configOrApiKey.endpoint || process.env.OPENAI_ENDPOINT || "https://api.openai.com/v1/chat/completions";
3610
+ if (configOrApiKey.temperature !== void 0) temp = configOrApiKey.temperature;
3611
+ } else {
3612
+ key = configOrApiKey || process.env.OPENAI_API_KEY || "";
3613
+ resolvedModel = model || process.env.OPENAI_MODEL || process.env.OPENAI_MODEL_FAST || "gpt-4o-mini";
3614
+ resolvedEndpoint = endpoint || process.env.OPENAI_ENDPOINT || "https://api.openai.com/v1/chat/completions";
3615
+ }
3616
+ this.apiKey = key;
3394
3617
  if (!this.apiKey) {
3395
3618
  throw new Error("[OpenAIAdapter] API Key not found. Provide it in constructor or set OPENAI_API_KEY in .env");
3396
3619
  }
3397
- this.model = model || process.env.OPENAI_MODEL || process.env.OPENAI_MODEL_FAST || "gpt-4o-mini";
3398
- this.endpoint = endpoint || process.env.OPENAI_ENDPOINT || "https://api.openai.com/v1/chat/completions";
3620
+ this.model = resolvedModel;
3621
+ this.endpoint = resolvedEndpoint;
3622
+ this.temperature = temp;
3399
3623
  }
3400
3624
  static createFast(apiKey) {
3401
3625
  return new _OpenAIAdapter(apiKey, process.env.OPENAI_MODEL_FAST || "gpt-4o-mini");
@@ -3411,7 +3635,7 @@ var OpenAIAdapter = class _OpenAIAdapter {
3411
3635
  const body = {
3412
3636
  model: this.model,
3413
3637
  messages,
3414
- temperature: 0.1
3638
+ temperature: this.temperature
3415
3639
  // Low temperature to reduce hallucination in RAG pipelines
3416
3640
  };
3417
3641
  if (request.tools && request.tools.length > 0) {
@@ -3461,15 +3685,28 @@ var OpenAIAdapter = class _OpenAIAdapter {
3461
3685
  var GeminiAdapter = class _GeminiAdapter {
3462
3686
  apiKey;
3463
3687
  model;
3464
- constructor(apiKey, model) {
3465
- this.apiKey = apiKey || process.env.GEMINI_API_KEY || "";
3688
+ temperature;
3689
+ constructor(configOrApiKey, model) {
3690
+ let key = "";
3691
+ let resolvedModel = "";
3692
+ let temp = 0.1;
3693
+ if (typeof configOrApiKey === "object" && configOrApiKey !== null) {
3694
+ key = configOrApiKey.apiKey || process.env.GEMINI_API_KEY || "";
3695
+ resolvedModel = configOrApiKey.model || process.env.GEMINI_MODEL || process.env.GEMINI_MODEL_FAST || "gemini-2.0-flash";
3696
+ if (configOrApiKey.temperature !== void 0) temp = configOrApiKey.temperature;
3697
+ } else {
3698
+ key = configOrApiKey || process.env.GEMINI_API_KEY || "";
3699
+ resolvedModel = model || process.env.GEMINI_MODEL || process.env.GEMINI_MODEL_FAST || "gemini-2.0-flash";
3700
+ }
3701
+ this.apiKey = key;
3466
3702
  if (!this.apiKey) {
3467
3703
  throw new Error("[GeminiAdapter] API Key not found. Provide it in constructor or set GEMINI_API_KEY in .env");
3468
3704
  }
3469
- this.model = model || process.env.GEMINI_MODEL || process.env.GEMINI_MODEL_FAST || "gemini-1.5-flash";
3705
+ this.model = resolvedModel;
3706
+ this.temperature = temp;
3470
3707
  }
3471
3708
  static createFast(apiKey) {
3472
- return new _GeminiAdapter(apiKey, process.env.GEMINI_MODEL_FAST || "gemini-1.5-flash");
3709
+ return new _GeminiAdapter(apiKey, process.env.GEMINI_MODEL_FAST || "gemini-2.0-flash");
3473
3710
  }
3474
3711
  static createSmart(apiKey) {
3475
3712
  return new _GeminiAdapter(apiKey, process.env.GEMINI_MODEL_SMART || "gemini-1.5-pro");
@@ -3484,7 +3721,7 @@ var GeminiAdapter = class _GeminiAdapter {
3484
3721
  }
3485
3722
  ],
3486
3723
  generationConfig: {
3487
- temperature: 0.1
3724
+ temperature: this.temperature
3488
3725
  // Low temperature to reduce hallucination in RAG pipelines
3489
3726
  }
3490
3727
  };
@@ -3548,18 +3785,34 @@ var GeminiAdapter = class _GeminiAdapter {
3548
3785
 
3549
3786
  // src/adapters/llm/openrouter.adapter.ts
3550
3787
  var OpenRouterAdapter = class _OpenRouterAdapter extends OpenAIAdapter {
3551
- constructor(apiKey, model, endpoint = "https://openrouter.ai/api/v1/chat/completions") {
3788
+ constructor(configOrApiKey, model, endpoint = "https://openrouter.ai/api/v1/chat/completions") {
3552
3789
  if (process.env.ALLOW_PUBLIC_OPENSOURCE_LLM === "false") {
3553
3790
  throw new Error(
3554
3791
  "[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."
3555
3792
  );
3556
3793
  }
3557
- const key = apiKey || process.env.OPENROUTER_API_KEY || "";
3794
+ let key = "";
3795
+ let resolvedModel = "";
3796
+ let resolvedEndpoint = endpoint;
3797
+ let temp = 0.1;
3798
+ if (typeof configOrApiKey === "object" && configOrApiKey !== null) {
3799
+ key = configOrApiKey.apiKey || process.env.OPENROUTER_API_KEY || "";
3800
+ resolvedModel = configOrApiKey.model || process.env.OPENROUTER_MODEL || process.env.OPENROUTER_MODEL_FAST || "meta-llama/llama-3.2-3b-instruct:free";
3801
+ if (configOrApiKey.endpoint) resolvedEndpoint = configOrApiKey.endpoint;
3802
+ if (configOrApiKey.temperature !== void 0) temp = configOrApiKey.temperature;
3803
+ } else {
3804
+ key = configOrApiKey || process.env.OPENROUTER_API_KEY || "";
3805
+ resolvedModel = model || process.env.OPENROUTER_MODEL || process.env.OPENROUTER_MODEL_FAST || "meta-llama/llama-3.2-3b-instruct:free";
3806
+ }
3558
3807
  if (!key) {
3559
3808
  throw new Error("[OpenRouterAdapter] API Key not found. Provide it in constructor or set OPENROUTER_API_KEY in .env");
3560
3809
  }
3561
- const resolvedModel = model || process.env.OPENROUTER_MODEL || process.env.OPENROUTER_MODEL_FAST || "meta-llama/llama-3.2-3b-instruct:free";
3562
- super(key, resolvedModel, endpoint);
3810
+ super({
3811
+ apiKey: key,
3812
+ model: resolvedModel,
3813
+ endpoint: resolvedEndpoint,
3814
+ temperature: temp
3815
+ });
3563
3816
  }
3564
3817
  static createFast(apiKey) {
3565
3818
  return new _OpenRouterAdapter(apiKey, process.env.OPENROUTER_MODEL_FAST || "meta-llama/llama-3.2-3b-instruct:free");
@@ -3716,11 +3969,24 @@ var FallbackLLMProvider = class {
3716
3969
  this.providers = providers;
3717
3970
  this.logFailover = options?.logFailover ?? true;
3718
3971
  }
3972
+ getProviderName(provider, index) {
3973
+ if (provider.name && typeof provider.name === "string") {
3974
+ return provider.name;
3975
+ }
3976
+ if (provider.model && typeof provider.model === "string") {
3977
+ const ctor = provider.constructor?.name && provider.constructor.name !== "Object" ? provider.constructor.name : "LLMProvider";
3978
+ return `${ctor}(${provider.model})`;
3979
+ }
3980
+ if (provider.constructor?.name && provider.constructor.name !== "Object") {
3981
+ return provider.constructor.name;
3982
+ }
3983
+ return `Provider#${index + 1}`;
3984
+ }
3719
3985
  async generate(request) {
3720
3986
  const failureErrors = [];
3721
3987
  for (let i = 0; i < this.providers.length; i++) {
3722
3988
  const provider = this.providers[i];
3723
- const providerName = provider.constructor.name || `Provider#${i + 1}`;
3989
+ const providerName = this.getProviderName(provider, i);
3724
3990
  try {
3725
3991
  return await provider.generate(request);
3726
3992
  } catch (error) {
@@ -3728,7 +3994,7 @@ var FallbackLLMProvider = class {
3728
3994
  failureErrors.push({ providerName, error: errorMessage });
3729
3995
  if (this.logFailover) {
3730
3996
  const nextProvider = this.providers[i + 1];
3731
- const nextName = nextProvider ? nextProvider.constructor.name || `Provider#${i + 2}` : "None";
3997
+ const nextName = nextProvider ? this.getProviderName(nextProvider, i + 1) : "None";
3732
3998
  console.warn(
3733
3999
  `[FallbackLLMProvider] ${providerName} failed: "${errorMessage}". Failing over to next provider: ${nextName}`
3734
4000
  );
@@ -3857,8 +4123,12 @@ var TieredLLMProvider = class _TieredLLMProvider {
3857
4123
  // src/adapters/rules/static-rules.adapter.ts
3858
4124
  var StaticRulesAdapter = class {
3859
4125
  rules;
3860
- constructor(rules) {
3861
- this.rules = rules;
4126
+ constructor(rules = []) {
4127
+ this.rules = rules.map((r) => {
4128
+ if (typeof r === "string") return r;
4129
+ if (r && typeof r === "object" && typeof r.rule === "string") return r.rule;
4130
+ return String(r);
4131
+ });
3862
4132
  }
3863
4133
  // Optional tenantId can be used for tenant-specific rules
3864
4134
  async getActiveRules(tenantId) {
@@ -3915,6 +4185,12 @@ var LocalFaqAdapter = class {
3915
4185
  this.smlEngine = options?.smlEngine || new SMLEngine();
3916
4186
  this.defaultThreshold = options?.defaultMatchThreshold ?? 0.6;
3917
4187
  }
4188
+ setSMLEngine(engine) {
4189
+ this.smlEngine = engine;
4190
+ }
4191
+ getSMLEngine() {
4192
+ return this.smlEngine;
4193
+ }
3918
4194
  /**
3919
4195
  * Initializes the adapter by reading the JSON file and caching embeddings in RAM.
3920
4196
  */
@@ -4474,6 +4750,7 @@ export {
4474
4750
  IngestionWorker,
4475
4751
  InjectionDetector,
4476
4752
  InputSanitizer,
4753
+ JobStatusValues,
4477
4754
  LocalFaqAdapter,
4478
4755
  MemoryConfirmationNonceStore,
4479
4756
  MemoryVectorAdapter,
@@ -4491,6 +4768,7 @@ export {
4491
4768
  RedisCacheAdapter,
4492
4769
  RedisConfirmationNonceStore,
4493
4770
  SMLEngine,
4771
+ SMLModelPresets,
4494
4772
  SecurityGuard,
4495
4773
  StaticRulesAdapter,
4496
4774
  TieredLLMProvider,