@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.
@@ -9,65 +9,170 @@ import type { IAgentTool } from './tool.interface.js';
9
9
  import type { ILLMProvider } from './llm.interface.js';
10
10
  import type { SecurityConfig } from '../core/security/security.types.js';
11
11
  import type { ResponseMetrics, TokenTrackerOptions } from '../core/metrics/token-tracker.js';
12
+ import type { SMLModelPreset } from '../core/sml-engine.js';
13
+ /**
14
+ * Enterprise configuration options for initializing the `RAGEngine`.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const engine = new RAGEngine({
19
+ * llmProvider: new GeminiAdapter({ apiKey: process.env.GEMINI_API_KEY, model: 'gemini-1.5-pro' }),
20
+ * inputs: {
21
+ * vectorStore: new PostgresVectorAdapter({ connectionString: process.env.DATABASE_URL })
22
+ * },
23
+ * smlConfig: {
24
+ * enabled: true,
25
+ * modelName: SMLModelPresets.MULTILINGUAL, // 'Xenova/paraphrase-multilingual-MiniLM-L12-v2'
26
+ * matchThreshold: 0.65
27
+ * }
28
+ * });
29
+ * ```
30
+ */
12
31
  export interface EngineConfig {
32
+ /**
33
+ * Primary LLM Provider used for synthesizing final RAG answers.
34
+ * Can be `GeminiAdapter`, `OpenAIAdapter`, `OllamaAdapter`, `TieredLLMProvider`, or `FallbackLLMProvider`.
35
+ */
13
36
  llmProvider: ILLMProvider;
37
+ /**
38
+ * Cryptographic mutation confirmation settings for sensitive/destructive tool executions.
39
+ */
14
40
  confirmation?: {
41
+ /** HMAC secret used to sign confirmation tickets. */
15
42
  secret?: string | undefined;
43
+ /** Single-use nonce store preventing replay attacks. */
16
44
  nonceStore?: ConfirmationNonceStore | undefined;
17
45
  } | undefined;
46
+ /**
47
+ * Enterprise Zero-Trust security guardrails: prompt injection detection, PII redaction (TCKN, IBAN, Credit Card), and boundary isolation.
48
+ */
18
49
  security?: SecurityConfig | undefined;
50
+ /**
51
+ * Local Small Machine Learning (SML) engine configuration for sub-millisecond semantic routing, FAQ answering, and tool dispatching.
52
+ * Runs locally in-process with zero external API calls.
53
+ */
19
54
  smlConfig?: {
55
+ /** Whether local semantic routing and embedding computation is enabled. Defaults to true. */
20
56
  enabled: boolean;
21
- modelName?: string | undefined;
57
+ /**
58
+ * Hugging Face ONNX embedding model or preset.
59
+ * Presets: `SMLModelPresets.ENGLISH_FAST`, `SMLModelPresets.MULTILINGUAL`, or `SMLModelPresets.MULTILINGUAL_ACCURATE`.
60
+ * Can also be any custom HuggingFace model string (e.g. `'BAAI/bge-m3'`, `'intfloat/multilingual-e5-small'`).
61
+ * Defaults to `'Xenova/all-MiniLM-L6-v2'`.
62
+ */
63
+ modelName?: string | SMLModelPreset | undefined;
64
+ /** Minimum cosine similarity threshold (0.0 - 1.0) required to trigger FAQ matches or tool routing. Defaults to 0.65. */
22
65
  matchThreshold?: number | undefined;
66
+ /** Model tensor quantization: 'q8' (default), 'fp32', 'fp16', or 'q4'. */
67
+ dtype?: 'q8' | 'fp32' | 'fp16' | 'q4' | undefined;
68
+ /** Directory where downloaded ONNX models are stored. Defaults to process.env.RAG_MODEL_CACHE_DIR or './data/models'. */
69
+ cacheDir?: string | undefined;
70
+ /** When true, only loads models already downloaded to disk. Defaults to false. */
71
+ localFilesOnly?: boolean | undefined;
72
+ /** Maximum number of embeddings held in RAM LRU cache. Defaults to 1000. */
73
+ cacheSize?: number | undefined;
74
+ /** Consecutive failures before tripping the circuit breaker OPEN. Defaults to 3. */
75
+ circuitBreakerThreshold?: number | undefined;
76
+ /** Cooldown period in ms before attempting recovery from OPEN circuit. Defaults to 15000 (15s). */
77
+ circuitBreakerCooldownMs?: number | undefined;
23
78
  } | undefined;
79
+ /**
80
+ * Knowledge layers: vector store for unstructured documents, static business rules, and high-frequency FAQ entries.
81
+ */
24
82
  inputs: {
83
+ /** Vector store adapter (`MemoryVectorAdapter` or `PostgresVectorAdapter`). */
25
84
  vectorStore?: IVectorStoreAdapter | undefined;
85
+ /** Business rules adapter (`StaticRulesAdapter`). */
26
86
  rules?: IRulesAdapter | undefined;
87
+ /** Fast semantic FAQ adapter (`LocalFaqAdapter` or `UrlFaqAdapter`). */
27
88
  faq?: IFAQAdapter | undefined;
28
89
  };
90
+ /**
91
+ * Governed agent tools for live tool execution and dynamic function calling.
92
+ */
29
93
  tools?: {
94
+ /** Path to persist computed tool embeddings (e.g. './data/tool-registry.json'). */
30
95
  registryFilePath: string;
96
+ /** Array of governed agent tools conforming to `IAgentTool`. */
31
97
  items: IAgentTool[];
32
98
  } | undefined;
99
+ /**
100
+ * Multi-tiered distributed caching configuration to serve repeated queries with sub-millisecond response time.
101
+ */
33
102
  cache?: {
103
+ /** Enable or disable semantic query caching. */
34
104
  enabled: boolean;
105
+ /** Distributed cache adapter (`RedisCacheAdapter`). If omitted, uses fast in-memory LRU cache. */
35
106
  adapter?: ICacheAdapter | undefined;
107
+ /** File path for persisting cache to disk across server restarts. */
36
108
  filePath?: string | undefined;
109
+ /** Minimum frequency threshold before a response is persisted to disk. */
37
110
  minHitsToPersist?: number | undefined;
111
+ /** Time-to-live for cached queries in milliseconds. Defaults to 24 hours. */
38
112
  ttlMs?: number | undefined;
113
+ /** Time-to-live for cached tool execution results in milliseconds. */
39
114
  toolResultTtlMs?: number | undefined;
115
+ /** Whether to cache tool execution results. */
40
116
  cacheToolResults?: boolean | undefined;
41
117
  } | undefined;
118
+ /**
119
+ * Hybrid search and document retrieval parameters.
120
+ */
42
121
  retrieval?: {
122
+ /** Search strategy: 'dense' (embeddings only), 'sparse' (BM25 keyword only), or 'hybrid' (RRF fusion). Defaults to 'hybrid'. */
43
123
  strategy?: 'dense' | 'sparse' | 'hybrid' | undefined;
124
+ /** Maximum number of top relevant document chunks passed to the LLM prompt. Defaults to 5. */
44
125
  topK?: number | undefined;
126
+ /** Minimum relevance score filter. */
45
127
  minRelevanceScore?: number | undefined;
46
128
  } | undefined;
129
+ /**
130
+ * FinOps telemetry options for token usage estimation and latency tracking.
131
+ */
47
132
  metrics?: TokenTrackerOptions | undefined;
133
+ /**
134
+ * Durable background ingestion pipeline settings for PDF, Markdown, and text streams.
135
+ */
48
136
  ingestion?: {
137
+ /** Storage backend for uploaded documents (`PostgresIngestionStore`). */
49
138
  backend?: IIngestionBackend | undefined;
139
+ /** Target text chunk length in characters. Defaults to 500. */
50
140
  chunkSize?: number | undefined;
141
+ /** Sliding window overlap between chunks in characters. Defaults to 50. */
51
142
  chunkOverlap?: number | undefined;
143
+ /** Background queue engine (`BullMQQueue` or `InMemoryJobQueue`). */
52
144
  jobQueue?: IJobQueue | undefined;
53
145
  } | undefined;
54
146
  }
147
+ /**
148
+ * Standardized response object returned by `engine.query()`.
149
+ */
55
150
  export interface RAGResponse {
151
+ /** The final synthesized answer from LLM, FAQ match, or cached response. */
56
152
  answer: string;
153
+ /** Resolved query intent: 'CACHE' | 'FAQ' | 'TOOL_CALL' | 'RAG_QUERY' | 'SECURITY_BLOCKED'. */
57
154
  intent: string;
155
+ /** The component that resolved the request (e.g. 'InMemoryCache', 'LocalFaqAdapter', 'GeminiAdapter'). */
58
156
  resolvedBy: string;
157
+ /** Document IDs and titles retrieved from the vector store and cited in the answer. */
59
158
  sources?: string[] | undefined;
159
+ /** Active business rules enforced during answer synthesis. */
60
160
  appliedRules?: string[] | undefined;
161
+ /** Tools that were executed during the query lifecycle. */
61
162
  executedTools?: Array<{
62
163
  name: string;
63
164
  args: any;
64
165
  }> | undefined;
166
+ /** Pending cryptographic confirmation tickets for mutating tool operations. */
65
167
  confirmationRequests?: Array<{
66
168
  toolName: string;
67
169
  proposedArgs: unknown;
68
170
  confirmationTicket: string;
69
171
  }> | undefined;
172
+ /** Security alerts (e.g. prompt injection warnings, masked PII types). */
70
173
  securityWarning?: string | undefined;
174
+ /** Total round-trip execution latency in milliseconds. */
71
175
  latencyMs: number;
176
+ /** Token consumption and FinOps cost metrics. */
72
177
  metrics?: ResponseMetrics | undefined;
73
178
  }
@@ -29,7 +29,19 @@ var CircuitBreakerOpenError = class extends Error {
29
29
  this.name = "CircuitBreakerOpenError";
30
30
  }
31
31
  };
32
- var SMLEngine = class {
32
+ var SMLEngine = class _SMLEngine {
33
+ static initLockChain = Promise.resolve();
34
+ static async acquireInitLock() {
35
+ let releaseLock = () => {
36
+ };
37
+ const nextLock = new Promise((resolve) => {
38
+ releaseLock = resolve;
39
+ });
40
+ const currentLock = _SMLEngine.initLockChain;
41
+ _SMLEngine.initLockChain = currentLock.then(() => nextLock);
42
+ await currentLock;
43
+ return releaseLock;
44
+ }
33
45
  modelName;
34
46
  cacheDir;
35
47
  localFilesOnly;
@@ -89,48 +101,62 @@ var SMLEngine = class {
89
101
  console.log(`[SML CircuitBreaker] Cooldown expired. Transitioned to HALF_OPEN for probe.`);
90
102
  }
91
103
  if (!this.extractorPromise) {
92
- console.log(`[SML] Loading ${this.modelName} (dtype: ${this.dtype})...`);
93
- this.extractorPromise = import("@huggingface/transformers").then(async ({ pipeline, env }) => {
94
- const previousCacheDir = env.cacheDir;
95
- const previousAllowRemote = env.allowRemoteModels;
104
+ this.extractorPromise = (async () => {
105
+ const releaseLock = await _SMLEngine.acquireInitLock();
96
106
  try {
97
- if (this.cacheDir) {
98
- env.cacheDir = this.cacheDir;
107
+ console.log(`[SML] Loading ${this.modelName} (dtype: ${this.dtype})...`);
108
+ const { pipeline, env } = await import("@huggingface/transformers");
109
+ const previousCacheDir = env.cacheDir;
110
+ const previousAllowRemote = env.allowRemoteModels;
111
+ const previousLocalModelPath = env.localModelPath;
112
+ try {
113
+ if (this.cacheDir) {
114
+ env.cacheDir = this.cacheDir;
115
+ }
116
+ if (this.localFilesOnly !== void 0) {
117
+ env.allowRemoteModels = !this.localFilesOnly;
118
+ }
119
+ const pipelineOptions = {
120
+ dtype: this.dtype
121
+ };
122
+ if (this.cacheDir) {
123
+ pipelineOptions.cache_dir = this.cacheDir;
124
+ }
125
+ if (this.localFilesOnly !== void 0) {
126
+ pipelineOptions.local_files_only = this.localFilesOnly;
127
+ }
128
+ const pipelineInstance = await pipeline("feature-extraction", this.modelName, pipelineOptions);
129
+ this.circuitState = "CLOSED";
130
+ this.failureCount = 0;
131
+ const extract = async (text, options) => {
132
+ const output = await pipelineInstance(text, options);
133
+ const values = output.data;
134
+ const data = output.data instanceof Float32Array ? output.data : Float32Array.from({ length: values.length }, (_, index) => Number(values[index]));
135
+ return { data, dims: output.dims };
136
+ };
137
+ return extract;
138
+ } finally {
139
+ env.cacheDir = previousCacheDir;
140
+ env.allowRemoteModels = previousAllowRemote;
141
+ env.localModelPath = previousLocalModelPath;
99
142
  }
100
- if (this.localFilesOnly) {
101
- env.allowRemoteModels = false;
143
+ } catch (err) {
144
+ this.failureCount++;
145
+ this.lastFailureTime = Date.now();
146
+ this.extractorPromise = null;
147
+ if (this.failureCount >= this.failureThreshold) {
148
+ this.circuitState = "OPEN";
149
+ console.error(
150
+ `[SML CircuitBreaker] \u26A0\uFE0F Tripped to OPEN state! Model failed ${this.failureCount} times. Pausing requests for ${this.cooldownMs / 1e3}s.`
151
+ );
152
+ } else {
153
+ console.error(`[SML] Model loading error (attempt ${this.failureCount}/${this.failureThreshold}):`, err);
102
154
  }
103
- return await pipeline("feature-extraction", this.modelName, {
104
- dtype: this.dtype
105
- });
155
+ throw err;
106
156
  } finally {
107
- env.cacheDir = previousCacheDir;
108
- env.allowRemoteModels = previousAllowRemote;
157
+ releaseLock();
109
158
  }
110
- }).then((pipelineInstance) => {
111
- this.circuitState = "CLOSED";
112
- this.failureCount = 0;
113
- const extract = async (text, options) => {
114
- const output = await pipelineInstance(text, options);
115
- const values = output.data;
116
- const data = output.data instanceof Float32Array ? output.data : Float32Array.from({ length: values.length }, (_, index) => Number(values[index]));
117
- return { data, dims: output.dims };
118
- };
119
- return extract;
120
- }).catch((err) => {
121
- this.failureCount++;
122
- this.lastFailureTime = Date.now();
123
- this.extractorPromise = null;
124
- if (this.failureCount >= this.failureThreshold) {
125
- this.circuitState = "OPEN";
126
- console.error(
127
- `[SML CircuitBreaker] \u26A0\uFE0F Tripped to OPEN state! Model failed ${this.failureCount} times. Pausing requests for ${this.cooldownMs / 1e3}s.`
128
- );
129
- } else {
130
- console.error(`[SML] Model loading error (attempt ${this.failureCount}/${this.failureThreshold}):`, err);
131
- }
132
- throw err;
133
- });
159
+ })();
134
160
  }
135
161
  return this.extractorPromise;
136
162
  }
@@ -74,15 +74,22 @@ export declare class DocumentIngestionService {
74
74
  */
75
75
  static extractText(filename: string, buffer: Buffer): string;
76
76
  /**
77
- * Enterprise High-Efficiency PDF Lexical Extractor.
78
- * Parses PDF content streams without heavy external C++/WASM dependencies.
77
+ * Enterprise High-Efficiency PDF Lexical Extractor with native zlib.inflate decompression.
78
+ * Decodes and parses PDF content streams without heavy external C++/WASM dependencies.
79
+ *
80
+ * Supports:
81
+ * - Decompression of compressed stream objects (/Filter /FlateDecode) via Node.js native zlib.
82
+ * - BT (Begin Text) ... ET (End Text) matrices.
83
+ * - TJ array operators with kerning spacing preservation.
84
+ * - Literal (Tj) and hexadecimal (<hex> Tj) text operators with escape and octal decoding.
79
85
  */
80
- static extractPdfText(rawContent: string): string;
86
+ static extractPdfText(rawContent: Buffer | string): string;
81
87
  static chunkText(text: string, chunkSize?: number, overlap?: number): string[];
82
88
  /**
83
89
  * Streaming chunk generator.
84
90
  * Ingests chunks of text or buffers incrementally with natural backpressure,
85
91
  * maintaining a constant O(1) memory footprint regardless of file size.
92
+ * Preserves UTF-8 multibyte character boundaries across stream buffer splits.
86
93
  */
87
94
  static chunkStream(textStream: AsyncIterable<string | Buffer>, chunkSize?: number, overlap?: number): AsyncGenerator<string, void, unknown>;
88
95
  /**
package/dist/worker.js CHANGED
@@ -31,7 +31,19 @@ var CircuitBreakerOpenError = class extends Error {
31
31
  this.name = "CircuitBreakerOpenError";
32
32
  }
33
33
  };
34
- var SMLEngine = class {
34
+ var SMLEngine = class _SMLEngine {
35
+ static initLockChain = Promise.resolve();
36
+ static async acquireInitLock() {
37
+ let releaseLock = () => {
38
+ };
39
+ const nextLock = new Promise((resolve) => {
40
+ releaseLock = resolve;
41
+ });
42
+ const currentLock = _SMLEngine.initLockChain;
43
+ _SMLEngine.initLockChain = currentLock.then(() => nextLock);
44
+ await currentLock;
45
+ return releaseLock;
46
+ }
35
47
  modelName;
36
48
  cacheDir;
37
49
  localFilesOnly;
@@ -91,48 +103,62 @@ var SMLEngine = class {
91
103
  console.log(`[SML CircuitBreaker] Cooldown expired. Transitioned to HALF_OPEN for probe.`);
92
104
  }
93
105
  if (!this.extractorPromise) {
94
- console.log(`[SML] Loading ${this.modelName} (dtype: ${this.dtype})...`);
95
- this.extractorPromise = import("@huggingface/transformers").then(async ({ pipeline: pipeline2, env }) => {
96
- const previousCacheDir = env.cacheDir;
97
- const previousAllowRemote = env.allowRemoteModels;
106
+ this.extractorPromise = (async () => {
107
+ const releaseLock = await _SMLEngine.acquireInitLock();
98
108
  try {
99
- if (this.cacheDir) {
100
- env.cacheDir = this.cacheDir;
109
+ console.log(`[SML] Loading ${this.modelName} (dtype: ${this.dtype})...`);
110
+ const { pipeline: pipeline2, env } = await import("@huggingface/transformers");
111
+ const previousCacheDir = env.cacheDir;
112
+ const previousAllowRemote = env.allowRemoteModels;
113
+ const previousLocalModelPath = env.localModelPath;
114
+ try {
115
+ if (this.cacheDir) {
116
+ env.cacheDir = this.cacheDir;
117
+ }
118
+ if (this.localFilesOnly !== void 0) {
119
+ env.allowRemoteModels = !this.localFilesOnly;
120
+ }
121
+ const pipelineOptions = {
122
+ dtype: this.dtype
123
+ };
124
+ if (this.cacheDir) {
125
+ pipelineOptions.cache_dir = this.cacheDir;
126
+ }
127
+ if (this.localFilesOnly !== void 0) {
128
+ pipelineOptions.local_files_only = this.localFilesOnly;
129
+ }
130
+ const pipelineInstance = await pipeline2("feature-extraction", this.modelName, pipelineOptions);
131
+ this.circuitState = "CLOSED";
132
+ this.failureCount = 0;
133
+ const extract = async (text, options) => {
134
+ const output = await pipelineInstance(text, options);
135
+ const values = output.data;
136
+ const data = output.data instanceof Float32Array ? output.data : Float32Array.from({ length: values.length }, (_, index) => Number(values[index]));
137
+ return { data, dims: output.dims };
138
+ };
139
+ return extract;
140
+ } finally {
141
+ env.cacheDir = previousCacheDir;
142
+ env.allowRemoteModels = previousAllowRemote;
143
+ env.localModelPath = previousLocalModelPath;
101
144
  }
102
- if (this.localFilesOnly) {
103
- env.allowRemoteModels = false;
145
+ } catch (err) {
146
+ this.failureCount++;
147
+ this.lastFailureTime = Date.now();
148
+ this.extractorPromise = null;
149
+ if (this.failureCount >= this.failureThreshold) {
150
+ this.circuitState = "OPEN";
151
+ console.error(
152
+ `[SML CircuitBreaker] \u26A0\uFE0F Tripped to OPEN state! Model failed ${this.failureCount} times. Pausing requests for ${this.cooldownMs / 1e3}s.`
153
+ );
154
+ } else {
155
+ console.error(`[SML] Model loading error (attempt ${this.failureCount}/${this.failureThreshold}):`, err);
104
156
  }
105
- return await pipeline2("feature-extraction", this.modelName, {
106
- dtype: this.dtype
107
- });
157
+ throw err;
108
158
  } finally {
109
- env.cacheDir = previousCacheDir;
110
- env.allowRemoteModels = previousAllowRemote;
111
- }
112
- }).then((pipelineInstance) => {
113
- this.circuitState = "CLOSED";
114
- this.failureCount = 0;
115
- const extract = async (text, options) => {
116
- const output = await pipelineInstance(text, options);
117
- const values = output.data;
118
- const data = output.data instanceof Float32Array ? output.data : Float32Array.from({ length: values.length }, (_, index) => Number(values[index]));
119
- return { data, dims: output.dims };
120
- };
121
- return extract;
122
- }).catch((err) => {
123
- this.failureCount++;
124
- this.lastFailureTime = Date.now();
125
- this.extractorPromise = null;
126
- if (this.failureCount >= this.failureThreshold) {
127
- this.circuitState = "OPEN";
128
- console.error(
129
- `[SML CircuitBreaker] \u26A0\uFE0F Tripped to OPEN state! Model failed ${this.failureCount} times. Pausing requests for ${this.cooldownMs / 1e3}s.`
130
- );
131
- } else {
132
- console.error(`[SML] Model loading error (attempt ${this.failureCount}/${this.failureThreshold}):`, err);
159
+ releaseLock();
133
160
  }
134
- throw err;
135
- });
161
+ })();
136
162
  }
137
163
  return this.extractorPromise;
138
164
  }
@@ -708,6 +734,8 @@ import fs from "node:fs";
708
734
  import fsp from "node:fs/promises";
709
735
  import path from "node:path";
710
736
  import os from "node:os";
737
+ import zlib from "node:zlib";
738
+ import { StringDecoder } from "node:string_decoder";
711
739
 
712
740
  // src/core/security/pii-guard.ts
713
741
  var PIIGuard = class {
@@ -1134,17 +1162,71 @@ var DocumentIngestionService = class _DocumentIngestionService {
1134
1162
  * - Performs binary artifact suppression and multi-lingual UTF-8 normalization (Turkish, European, and CJK diacritics).
1135
1163
  */
1136
1164
  static extractText(filename, buffer) {
1137
- const rawString = buffer.toString("utf-8");
1138
- if (buffer.length >= 4 && buffer.slice(0, 4).toString() === "%PDF") {
1139
- return _DocumentIngestionService.extractPdfText(rawString);
1165
+ if (buffer.length >= 4 && buffer.subarray(0, 4).toString() === "%PDF") {
1166
+ return _DocumentIngestionService.extractPdfText(buffer);
1140
1167
  }
1141
- return rawString.trim();
1168
+ return buffer.toString("utf-8").trim();
1142
1169
  }
1143
1170
  /**
1144
- * Enterprise High-Efficiency PDF Lexical Extractor.
1145
- * Parses PDF content streams without heavy external C++/WASM dependencies.
1171
+ * Enterprise High-Efficiency PDF Lexical Extractor with native zlib.inflate decompression.
1172
+ * Decodes and parses PDF content streams without heavy external C++/WASM dependencies.
1173
+ *
1174
+ * Supports:
1175
+ * - Decompression of compressed stream objects (/Filter /FlateDecode) via Node.js native zlib.
1176
+ * - BT (Begin Text) ... ET (End Text) matrices.
1177
+ * - TJ array operators with kerning spacing preservation.
1178
+ * - Literal (Tj) and hexadecimal (<hex> Tj) text operators with escape and octal decoding.
1146
1179
  */
1147
1180
  static extractPdfText(rawContent) {
1181
+ const buffer = Buffer.isBuffer(rawContent) ? rawContent : Buffer.from(rawContent, "latin1");
1182
+ const decompressedStreams = [];
1183
+ const streamMarker = Buffer.from("stream");
1184
+ const endStreamMarker = Buffer.from("endstream");
1185
+ let pos = 0;
1186
+ while (pos < buffer.length) {
1187
+ const streamIdx = buffer.indexOf(streamMarker, pos);
1188
+ if (streamIdx === -1) break;
1189
+ const prevChar = streamIdx > 0 ? buffer[streamIdx - 1] : 32;
1190
+ if (prevChar !== 10 && prevChar !== 13 && prevChar !== 32 && prevChar !== 62) {
1191
+ pos = streamIdx + 6;
1192
+ continue;
1193
+ }
1194
+ let dataStart = streamIdx + 6;
1195
+ if (buffer[dataStart] === 13 && buffer[dataStart + 1] === 10) {
1196
+ dataStart += 2;
1197
+ } else if (buffer[dataStart] === 10 || buffer[dataStart] === 13) {
1198
+ dataStart += 1;
1199
+ }
1200
+ const endIdx = buffer.indexOf(endStreamMarker, dataStart);
1201
+ if (endIdx === -1) break;
1202
+ let dataEnd = endIdx;
1203
+ while (dataEnd > dataStart && (buffer[dataEnd - 1] === 10 || buffer[dataEnd - 1] === 13 || buffer[dataEnd - 1] === 32)) {
1204
+ dataEnd--;
1205
+ }
1206
+ const streamBytes = buffer.subarray(dataStart, dataEnd);
1207
+ const dictStart = Math.max(0, streamIdx - 500);
1208
+ const dictSnippet = buffer.subarray(dictStart, streamIdx).toString("latin1");
1209
+ const isFlate = /\/Filter\s*(?:\[\s*)?\/(?:FlateDecode|Fl)\b/.test(dictSnippet) || !/\/Filter\b/.test(dictSnippet);
1210
+ let decoded = "";
1211
+ if (isFlate && streamBytes.length > 0) {
1212
+ try {
1213
+ decoded = zlib.inflateSync(streamBytes).toString("utf-8");
1214
+ } catch {
1215
+ try {
1216
+ decoded = zlib.inflateRawSync(streamBytes).toString("utf-8");
1217
+ } catch {
1218
+ decoded = streamBytes.toString("utf-8");
1219
+ }
1220
+ }
1221
+ } else {
1222
+ decoded = streamBytes.toString("utf-8");
1223
+ }
1224
+ if (decoded && decoded.trim().length > 0) {
1225
+ decompressedStreams.push(decoded);
1226
+ }
1227
+ pos = endIdx + 9;
1228
+ }
1229
+ const contentToScan = [buffer.toString("latin1"), ...decompressedStreams].join("\n");
1148
1230
  const extractedSegments = [];
1149
1231
  const decodePdfLiteral = (encoded) => {
1150
1232
  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, "\\");
@@ -1159,7 +1241,7 @@ var DocumentIngestionService = class _DocumentIngestionService {
1159
1241
  };
1160
1242
  const btEtRegex = /BT[\s\S]*?ET/g;
1161
1243
  let btMatch;
1162
- while ((btMatch = btEtRegex.exec(rawContent)) !== null) {
1244
+ while ((btMatch = btEtRegex.exec(contentToScan)) !== null) {
1163
1245
  const blockContent = btMatch[0];
1164
1246
  const tjArrayRegex = /\[([\s\S]*?)\]\s*TJ/g;
1165
1247
  let tjArrMatch;
@@ -1204,7 +1286,7 @@ var DocumentIngestionService = class _DocumentIngestionService {
1204
1286
  if (extractedSegments.length > 0) {
1205
1287
  return extractedSegments.join(" ").replace(/\s+/g, " ").trim();
1206
1288
  }
1207
- const cleaned = rawContent.replace(/%PDF-[^\n]+/g, "").replace(/stream[\s\S]*?endstream/g, (streamObj) => {
1289
+ const cleaned = contentToScan.replace(/%PDF-[^\n]+/g, "").replace(/stream[\s\S]*?endstream/g, (streamObj) => {
1208
1290
  const innerMatch = streamObj.match(/\((.*?)\)/g);
1209
1291
  return innerMatch ? innerMatch.map((s) => decodePdfLiteral(s.slice(1, -1))).join(" ") : "";
1210
1292
  }).replace(/obj[\s\S]*?endobj/g, (obj) => {
@@ -1214,8 +1296,17 @@ var DocumentIngestionService = class _DocumentIngestionService {
1214
1296
  return cleaned || "PDF document with no extractable plain text stream.";
1215
1297
  }
1216
1298
  static chunkText(text, chunkSize = 500, overlap = 50) {
1217
- if (!text || text.length <= chunkSize) {
1218
- return text ? [text] : [];
1299
+ if (!Number.isFinite(chunkSize) || chunkSize <= 0) {
1300
+ throw new RangeError(`[DocumentIngestionService] chunkSize must be a positive integer greater than 0. Received: ${chunkSize}`);
1301
+ }
1302
+ if (!Number.isFinite(overlap) || overlap < 0 || overlap >= chunkSize) {
1303
+ throw new RangeError(`[DocumentIngestionService] overlap must be non-negative and strictly less than chunkSize (${chunkSize}). Received: ${overlap}`);
1304
+ }
1305
+ if (!text || text.trim().length === 0) {
1306
+ return [];
1307
+ }
1308
+ if (text.length <= chunkSize) {
1309
+ return [text.trim()];
1219
1310
  }
1220
1311
  const chunks = [];
1221
1312
  let startIndex = 0;
@@ -1237,6 +1328,9 @@ var DocumentIngestionService = class _DocumentIngestionService {
1237
1328
  if (chunk.length > 0) {
1238
1329
  chunks.push(chunk);
1239
1330
  }
1331
+ if (endIndex >= text.length) {
1332
+ break;
1333
+ }
1240
1334
  startIndex = Math.max(startIndex + 1, endIndex - overlap);
1241
1335
  }
1242
1336
  return chunks;
@@ -1245,11 +1339,19 @@ var DocumentIngestionService = class _DocumentIngestionService {
1245
1339
  * Streaming chunk generator.
1246
1340
  * Ingests chunks of text or buffers incrementally with natural backpressure,
1247
1341
  * maintaining a constant O(1) memory footprint regardless of file size.
1342
+ * Preserves UTF-8 multibyte character boundaries across stream buffer splits.
1248
1343
  */
1249
1344
  static async *chunkStream(textStream, chunkSize = 500, overlap = 50) {
1345
+ if (!Number.isFinite(chunkSize) || chunkSize <= 0) {
1346
+ throw new RangeError(`[DocumentIngestionService] chunkSize must be a positive integer greater than 0. Received: ${chunkSize}`);
1347
+ }
1348
+ if (!Number.isFinite(overlap) || overlap < 0 || overlap >= chunkSize) {
1349
+ throw new RangeError(`[DocumentIngestionService] overlap must be non-negative and strictly less than chunkSize (${chunkSize}). Received: ${overlap}`);
1350
+ }
1351
+ const decoder = new StringDecoder("utf-8");
1250
1352
  let window = "";
1251
1353
  for await (const rawChunk of textStream) {
1252
- const chunkStr = Buffer.isBuffer(rawChunk) ? rawChunk.toString("utf-8") : String(rawChunk);
1354
+ const chunkStr = Buffer.isBuffer(rawChunk) ? decoder.write(rawChunk) : String(rawChunk);
1253
1355
  window += chunkStr;
1254
1356
  while (window.length >= chunkSize) {
1255
1357
  let endIndex = chunkSize;
@@ -1267,9 +1369,13 @@ var DocumentIngestionService = class _DocumentIngestionService {
1267
1369
  if (emitted.length > 0) {
1268
1370
  yield emitted;
1269
1371
  }
1270
- window = window.slice(Math.max(1, endIndex - overlap));
1372
+ window = window.slice(Math.max(0, endIndex - overlap));
1271
1373
  }
1272
1374
  }
1375
+ const flushed = decoder.end();
1376
+ if (flushed) {
1377
+ window += flushed;
1378
+ }
1273
1379
  const remaining = window.trim();
1274
1380
  if (remaining.length > 0) {
1275
1381
  yield remaining;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@denizsvnc/ai-rag-engine",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "TypeScript RAG engine with pluggable providers, local semantic routing, tenant-aware retrieval, and durable document ingestion.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -38,6 +38,7 @@
38
38
  "test:consumer": "node test/package/run.mjs",
39
39
  "clean": "node tools/clean.mjs",
40
40
  "test:consumer:integration": "node test/consumer/run.mjs",
41
+ "test:bun": "bun test test/bun/bun-compat.test.ts",
41
42
  "test:package": "npm run build && node tools/verify-package.mjs"
42
43
  },
43
44
  "keywords": [