@denizsvnc/ai-rag-engine 2.2.2 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/adapters/llm/gemini.adapter.d.ts +10 -1
- package/dist/adapters/llm/openai.adapter.d.ts +12 -1
- package/dist/adapters/llm/openrouter.adapter.d.ts +4 -2
- package/dist/api/server.js +247 -67
- package/dist/core/engine.d.ts +54 -2
- package/dist/core/metrics/rag-evaluator.d.ts +5 -0
- package/dist/core/sml-engine.d.ts +37 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +306 -76
- package/dist/interfaces/engine-config.interface.d.ts +106 -1
- package/dist/scripts/migrate-vector.js +64 -38
- package/dist/services/document-ingestion.service.d.ts +10 -3
- package/dist/worker.js +153 -50
- package/package.json +2 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
const previousCacheDir = env.cacheDir;
|
|
95
|
-
const previousAllowRemote = env.allowRemoteModels;
|
|
104
|
+
this.extractorPromise = (async () => {
|
|
105
|
+
const releaseLock = await _SMLEngine.acquireInitLock();
|
|
96
106
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
dtype: this.dtype
|
|
105
|
-
});
|
|
155
|
+
throw err;
|
|
106
156
|
} finally {
|
|
107
|
-
|
|
108
|
-
env.allowRemoteModels = previousAllowRemote;
|
|
157
|
+
releaseLock();
|
|
109
158
|
}
|
|
110
|
-
})
|
|
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
|
-
*
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
const previousCacheDir = env.cacheDir;
|
|
97
|
-
const previousAllowRemote = env.allowRemoteModels;
|
|
106
|
+
this.extractorPromise = (async () => {
|
|
107
|
+
const releaseLock = await _SMLEngine.acquireInitLock();
|
|
98
108
|
try {
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
dtype: this.dtype
|
|
107
|
-
});
|
|
157
|
+
throw err;
|
|
108
158
|
} finally {
|
|
109
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1138
|
-
|
|
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
|
|
1168
|
+
return buffer.toString("utf-8").trim();
|
|
1142
1169
|
}
|
|
1143
1170
|
/**
|
|
1144
|
-
* Enterprise High-Efficiency PDF Lexical Extractor.
|
|
1145
|
-
*
|
|
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(
|
|
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 =
|
|
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 (!
|
|
1218
|
-
|
|
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;
|
|
@@ -1248,11 +1339,19 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1248
1339
|
* Streaming chunk generator.
|
|
1249
1340
|
* Ingests chunks of text or buffers incrementally with natural backpressure,
|
|
1250
1341
|
* maintaining a constant O(1) memory footprint regardless of file size.
|
|
1342
|
+
* Preserves UTF-8 multibyte character boundaries across stream buffer splits.
|
|
1251
1343
|
*/
|
|
1252
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");
|
|
1253
1352
|
let window = "";
|
|
1254
1353
|
for await (const rawChunk of textStream) {
|
|
1255
|
-
const chunkStr = Buffer.isBuffer(rawChunk) ?
|
|
1354
|
+
const chunkStr = Buffer.isBuffer(rawChunk) ? decoder.write(rawChunk) : String(rawChunk);
|
|
1256
1355
|
window += chunkStr;
|
|
1257
1356
|
while (window.length >= chunkSize) {
|
|
1258
1357
|
let endIndex = chunkSize;
|
|
@@ -1270,9 +1369,13 @@ var DocumentIngestionService = class _DocumentIngestionService {
|
|
|
1270
1369
|
if (emitted.length > 0) {
|
|
1271
1370
|
yield emitted;
|
|
1272
1371
|
}
|
|
1273
|
-
window = window.slice(Math.max(
|
|
1372
|
+
window = window.slice(Math.max(0, endIndex - overlap));
|
|
1274
1373
|
}
|
|
1275
1374
|
}
|
|
1375
|
+
const flushed = decoder.end();
|
|
1376
|
+
if (flushed) {
|
|
1377
|
+
window += flushed;
|
|
1378
|
+
}
|
|
1276
1379
|
const remaining = window.trim();
|
|
1277
1380
|
if (remaining.length > 0) {
|
|
1278
1381
|
yield remaining;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@denizsvnc/ai-rag-engine",
|
|
3
|
-
"version": "2.2.
|
|
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": [
|