@devilsdev/rag-pipeline-utils 2.3.1 → 2.4.0

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.
Files changed (160) hide show
  1. package/README.md +293 -369
  2. package/contracts/loader-contract.json +11 -6
  3. package/contracts/reranker-contract.json +18 -26
  4. package/dist/index.cjs +21190 -47
  5. package/dist/index.d.ts +298 -68
  6. package/dist/index.mjs +9 -76
  7. package/package.json +20 -8
  8. package/src/agentic/agentic-pipeline.js +235 -0
  9. package/src/agentic/iterative-retriever.js +213 -0
  10. package/src/agentic/query-planner.js +208 -0
  11. package/src/agentic/self-critique.js +102 -0
  12. package/src/ai/adaptive-retrieval.js +0 -3
  13. package/src/ai/federated-learning.js +0 -3
  14. package/src/ai/federation.js +1 -6
  15. package/src/ai/index.js +9 -20
  16. package/src/ai/model-training.js +0 -3
  17. package/src/ai/multimodal-processing.js +0 -3
  18. package/src/ai/multimodal.js +1 -6
  19. package/src/ai/orchestrator.js +1 -6
  20. package/src/ai/retrieval-engine.js +1 -6
  21. package/src/chunking/chunking-engine.js +142 -0
  22. package/src/chunking/strategies/fixed-size.js +54 -0
  23. package/src/chunking/strategies/recursive.js +127 -0
  24. package/src/chunking/strategies/semantic.js +128 -0
  25. package/src/chunking/strategies/sentence.js +32 -0
  26. package/src/chunking/strategies/structure-aware.js +138 -0
  27. package/src/citation/citation-tracker.js +161 -0
  28. package/src/citation/hallucination-detector.js +74 -0
  29. package/src/citation/source-mapper.js +95 -0
  30. package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
  31. package/src/cli/commands/ai-ml/federated-commands.js +207 -0
  32. package/src/cli/commands/ai-ml/index.js +17 -0
  33. package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
  34. package/src/cli/commands/ai-ml/shared.js +51 -0
  35. package/src/cli/commands/ai-ml/training-commands.js +228 -0
  36. package/src/cli/commands/ai-ml.js +176 -757
  37. package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
  38. package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
  39. package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
  40. package/src/cli/commands/plugin-hub/review-commands.js +78 -0
  41. package/src/cli/commands/plugin-hub.js +106 -782
  42. package/src/cli/diagnostics/config-checker.js +81 -0
  43. package/src/cli/diagnostics/dependency-checker.js +69 -0
  44. package/src/cli/diagnostics/environment-checker.js +45 -0
  45. package/src/cli/diagnostics/performance-checker.js +51 -0
  46. package/src/cli/diagnostics/plugin-checker.js +106 -0
  47. package/src/cli/diagnostics/security-checker.js +61 -0
  48. package/src/cli/doctor-command.js +70 -369
  49. package/src/cli/enhanced-cli-commands.js +162 -706
  50. package/src/cli/handlers/config-handler.js +126 -0
  51. package/src/cli/handlers/diagnostics-handler.js +113 -0
  52. package/src/cli/handlers/ingest-handler.js +42 -0
  53. package/src/cli/handlers/init-handler.js +51 -0
  54. package/src/cli/handlers/query-handler.js +45 -0
  55. package/src/cli/handlers/run-handler.js +181 -0
  56. package/src/cli/interactive-wizard.js +99 -723
  57. package/src/cli/shared/cli-utils.js +89 -0
  58. package/src/cli/wizard/configuration.js +148 -0
  59. package/src/cli/wizard/observability.js +79 -0
  60. package/src/cli/wizard/performance.js +76 -0
  61. package/src/cli/wizard/plugin-selection.js +297 -0
  62. package/src/cli/wizard/preview-save.js +172 -0
  63. package/src/cli/wizard/project-setup.js +66 -0
  64. package/src/config/memory-optimization.js +3 -3
  65. package/src/connectors/anthropic-connector.js +179 -0
  66. package/src/connectors/base-connector.js +94 -0
  67. package/src/connectors/cohere-connector.js +143 -0
  68. package/src/connectors/local-embedder.js +171 -0
  69. package/src/connectors/memory-retriever.js +150 -0
  70. package/src/connectors/ollama-connector.js +205 -0
  71. package/src/connectors/openai-connector.js +173 -0
  72. package/src/core/create-pipeline.js +21 -1
  73. package/src/core/observability/tracing.js +2 -1
  74. package/src/core/pipeline-factory.js +0 -1
  75. package/src/core/plugin-registry.js +14 -14
  76. package/src/cost/cost-calculator.js +146 -0
  77. package/src/cost/cost-tracker.js +260 -0
  78. package/src/cost/token-budget.js +245 -0
  79. package/src/dag/core/execution-scheduler.js +2 -1
  80. package/src/dag/core/topology-validation.js +0 -3
  81. package/src/dag/dag-engine.js +3 -5
  82. package/src/dag/dag-node.js +0 -1
  83. package/src/dag/enhanced-dag-engine.js +171 -99
  84. package/src/dashboard/dashboard-generator.js +642 -0
  85. package/src/dashboard/metrics-aggregator.js +409 -0
  86. package/src/debug/execution-tracer.js +210 -0
  87. package/src/debug/trace-inspector.js +281 -0
  88. package/src/dev/dev-server.js +5 -5
  89. package/src/dev/hot-reload.js +3 -2
  90. package/src/dx/integration-templates.js +0 -3
  91. package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
  92. package/src/ecosystem/plugin-hub.js +2 -2
  93. package/src/enterprise/audit-logging.js +2 -2
  94. package/src/enterprise/sso-integration.js +52 -32
  95. package/src/evaluate/context-metrics.js +127 -0
  96. package/src/evaluate/evaluator.js +4 -2
  97. package/src/evaluate/faithfulness.js +117 -0
  98. package/src/evaluate/groundedness.js +60 -0
  99. package/src/evaluate/pipeline-evaluator.js +156 -0
  100. package/src/evaluate/relevance.js +129 -0
  101. package/src/evaluate/scoring.js +27 -0
  102. package/src/graph/entity-extractor.js +183 -0
  103. package/src/graph/graph-index.js +113 -0
  104. package/src/graph/graph-retriever.js +244 -0
  105. package/src/graph/knowledge-graph.js +256 -0
  106. package/src/guardrails/guardrails-pipeline.js +136 -0
  107. package/src/guardrails/post-generation-guard.js +170 -0
  108. package/src/guardrails/pre-retrieval-guard.js +161 -0
  109. package/src/guardrails/retrieval-guard.js +126 -0
  110. package/src/index.js +255 -9
  111. package/src/loader/html-loader.js +4 -15
  112. package/src/loader/markdown-loader.js +4 -15
  113. package/src/mcp/mcp-server.js +199 -0
  114. package/src/mcp/mcp-tool-builder.js +121 -0
  115. package/src/observability/audit-logger.js +0 -2
  116. package/src/observability/event-logger.js +2 -1
  117. package/src/observability/security-monitor.js +0 -2
  118. package/src/observability/telemetry.js +0 -2
  119. package/src/reranker/cascade-reranker.js +79 -0
  120. package/src/reranker/embedding-reranker.js +124 -0
  121. package/src/reranker/llm-reranker.js +6 -3
  122. package/src/reranker/scoring-reranker.js +123 -0
  123. package/src/retrieval/bm25-search.js +157 -0
  124. package/src/retrieval/hybrid-retriever.js +111 -0
  125. package/src/retrieval/rank-fusion.js +62 -0
  126. package/src/security/jwt-config.js +4 -3
  127. package/src/security/jwt-validator.js +2 -1
  128. package/src/security/plugin-sandbox.js +0 -3
  129. package/src/security/plugin-signature-verifier.js +4 -6
  130. package/src/security/validator.js +0 -2
  131. package/src/streaming/embedding-stream.js +197 -0
  132. package/src/streaming/sse-adapter.js +168 -0
  133. package/src/streaming/stream-router.js +210 -0
  134. package/src/streaming/streaming-embedder.js +231 -0
  135. package/src/streaming/websocket-adapter.js +171 -0
  136. package/src/templates/scaffold.js +147 -0
  137. package/src/templates/template-registry.js +272 -0
  138. package/src/utils/contract-validator.js +2 -1
  139. package/src/utils/input-sanitizer.js +80 -99
  140. package/src/utils/performance-monitor.js +0 -3
  141. package/src/utils/plugin-scaffolder.js +9 -16
  142. package/src/utils/rate-limiter.js +0 -1
  143. package/src/utils/retry-policy.js +0 -3
  144. package/src/utils/safe-expression-evaluator.js +0 -3
  145. package/src/utils/secure-logger-config.js +2 -4
  146. package/src/utils/secure-logging.js +0 -3
  147. package/src/utils/structured-logger.js +0 -3
  148. package/src/ai/index-fixed.js +0 -509
  149. package/src/ai/module-template.js +0 -37
  150. package/src/ci/workflow-analyzer.js +0 -71
  151. package/src/cli/plugin-marketplace-commands.js +0 -869
  152. package/src/dev/README.md +0 -126
  153. package/src/dev/examples/hot-reload-usage.js +0 -355
  154. package/src/mocks/openai-embedder.js +0 -119
  155. package/src/mocks/openai-llm.js +0 -77
  156. package/src/mocks/pdf-loader.js +0 -21
  157. package/src/mocks/pinecone-retriever.js +0 -106
  158. package/src/scripts/script-utilities.js +0 -36
  159. package/src/utils/ci/diagnostic-reporter.js +0 -57
  160. package/src/utils/examples/error-formatter-usage.js +0 -351
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+ const { PreRetrievalGuard } = require('./pre-retrieval-guard');
5
+ const { RetrievalGuard } = require('./retrieval-guard');
6
+ const { PostGenerationGuard } = require('./post-generation-guard');
7
+
8
+ /**
9
+ * Default configuration for the GuardrailsPipeline.
10
+ * @type {object}
11
+ */
12
+ const DEFAULT_CONFIG = {
13
+ preRetrieval: {}, // PreRetrievalGuard options, or false to disable
14
+ retrieval: {}, // RetrievalGuard options, or false to disable
15
+ postGeneration: {}, // PostGenerationGuard options, or false to disable
16
+ strict: false, // If true, block on any guard failure; if false, warn and continue
17
+ };
18
+
19
+ /**
20
+ * Pipeline wrapper that applies all three guardrail layers (pre-retrieval,
21
+ * retrieval, and post-generation) around an existing RAG pipeline. In strict
22
+ * mode, any guard failure aborts the pipeline; otherwise issues are collected
23
+ * as warnings and execution continues.
24
+ *
25
+ * @extends EventEmitter
26
+ */
27
+ class GuardrailsPipeline extends EventEmitter {
28
+ /**
29
+ * @param {object} pipeline - The underlying RAG pipeline (must have a .run() method)
30
+ * @param {object} [options] - Override default configuration
31
+ * @param {object|false} [options.preRetrieval={}] - PreRetrievalGuard options, or false to disable
32
+ * @param {object|false} [options.retrieval={}] - RetrievalGuard options, or false to disable
33
+ * @param {object|false} [options.postGeneration={}] - PostGenerationGuard options, or false to disable
34
+ * @param {boolean} [options.strict=false] - Whether to block on any guard failure
35
+ */
36
+ constructor(pipeline, options = {}) {
37
+ super();
38
+
39
+ if (!pipeline || typeof pipeline.run !== 'function') {
40
+ throw new Error(
41
+ 'GuardrailsPipeline requires a pipeline with a .run() method',
42
+ );
43
+ }
44
+
45
+ this.pipeline = pipeline;
46
+ this.config = { ...DEFAULT_CONFIG, ...options };
47
+
48
+ this.preGuard =
49
+ this.config.preRetrieval !== false
50
+ ? new PreRetrievalGuard(this.config.preRetrieval || {})
51
+ : null;
52
+ this.retrievalGuard =
53
+ this.config.retrieval !== false
54
+ ? new RetrievalGuard(this.config.retrieval || {})
55
+ : null;
56
+ this.postGuard =
57
+ this.config.postGeneration !== false
58
+ ? new PostGenerationGuard(this.config.postGeneration || {})
59
+ : null;
60
+ }
61
+
62
+ /**
63
+ * Run the full guarded pipeline.
64
+ *
65
+ * @param {object} input - Pipeline input
66
+ * @param {string} input.query - The user query
67
+ * @param {Float32Array} [input.queryVector] - Optional precomputed query vector
68
+ * @param {object} [input.options] - Additional pipeline options
69
+ * @param {object} [input.context] - Request context (e.g. userPermissions for ACL)
70
+ * @returns {Promise<object>} Pipeline result with guardrails metadata attached
71
+ */
72
+ async run(input) {
73
+ const guardrails = {
74
+ preRetrieval: null,
75
+ retrieval: null,
76
+ postGeneration: null,
77
+ };
78
+
79
+ // 1. Pre-retrieval guard
80
+ let query = input.query;
81
+ if (this.preGuard) {
82
+ guardrails.preRetrieval = this.preGuard.check(query);
83
+ this.emit('preRetrieval', guardrails.preRetrieval);
84
+
85
+ if (!guardrails.preRetrieval.safe && this.config.strict) {
86
+ return {
87
+ success: false,
88
+ error: 'Pre-retrieval guard blocked query',
89
+ guardrails,
90
+ };
91
+ }
92
+ query = guardrails.preRetrieval.sanitizedQuery;
93
+ }
94
+
95
+ // 2. Run the underlying pipeline with (possibly sanitized) query
96
+ const result = await this.pipeline.run({
97
+ ...input,
98
+ query,
99
+ options: { ...input.options, citations: true },
100
+ });
101
+
102
+ // 3. Retrieval guard on results
103
+ if (this.retrievalGuard && result.results) {
104
+ guardrails.retrieval = this.retrievalGuard.filter(
105
+ result.results,
106
+ input.context,
107
+ );
108
+ this.emit('retrieval', guardrails.retrieval);
109
+ result.results = guardrails.retrieval.results;
110
+ }
111
+
112
+ // 4. Post-generation guard on answer
113
+ if (this.postGuard && result.answer) {
114
+ guardrails.postGeneration = this.postGuard.check(
115
+ result.answer,
116
+ result.results,
117
+ );
118
+ this.emit('postGeneration', guardrails.postGeneration);
119
+
120
+ if (!guardrails.postGeneration.safe && this.config.strict) {
121
+ return {
122
+ ...result,
123
+ success: false,
124
+ error: 'Post-generation guard flagged output',
125
+ guardrails,
126
+ };
127
+ }
128
+ result.answer = guardrails.postGeneration.sanitizedOutput;
129
+ }
130
+
131
+ result.guardrails = guardrails;
132
+ return result;
133
+ }
134
+ }
135
+
136
+ module.exports = { GuardrailsPipeline, DEFAULT_CONFIG };
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+
5
+ /**
6
+ * Default configuration for the PostGenerationGuard.
7
+ * @type {object}
8
+ */
9
+ const DEFAULT_CONFIG = {
10
+ enablePIIDetection: true,
11
+ enableGroundednessCheck: true,
12
+ groundednessThreshold: 0.3,
13
+ maxResponseLength: 50000,
14
+ minResponseLength: 0,
15
+ piiPatterns: {
16
+ email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
17
+ phone: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
18
+ ssn: /\b\d{3}-\d{2}-\d{4}\b/g,
19
+ creditCard: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g,
20
+ },
21
+ };
22
+
23
+ /**
24
+ * Post-generation guard that checks LLM output for PII leakage,
25
+ * groundedness against source documents, and length constraints.
26
+ * Sanitizes output by redacting detected PII.
27
+ *
28
+ * @extends EventEmitter
29
+ */
30
+ class PostGenerationGuard extends EventEmitter {
31
+ /**
32
+ * @param {object} [options] - Override default configuration
33
+ * @param {boolean} [options.enablePIIDetection=true] - Whether to detect PII in output
34
+ * @param {boolean} [options.enableGroundednessCheck=true] - Whether to check groundedness
35
+ * @param {number} [options.groundednessThreshold=0.3] - Minimum groundedness score
36
+ * @param {number} [options.maxResponseLength=50000] - Maximum allowed response length
37
+ * @param {number} [options.minResponseLength=0] - Minimum response length
38
+ * @param {object} [options.piiPatterns] - Custom PII regex patterns (merged with defaults)
39
+ */
40
+ constructor(options = {}) {
41
+ super();
42
+ this.config = { ...DEFAULT_CONFIG, ...options };
43
+ if (options.piiPatterns) {
44
+ this.config.piiPatterns = {
45
+ ...DEFAULT_CONFIG.piiPatterns,
46
+ ...options.piiPatterns,
47
+ };
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Check generated output for safety issues, PII, and groundedness.
53
+ *
54
+ * @param {string} output - The LLM-generated output text
55
+ * @param {Array<object>} [retrievedDocs=[]] - Source documents for groundedness checking
56
+ * @returns {{
57
+ * safe: boolean,
58
+ * issues: string[],
59
+ * sanitizedOutput: string,
60
+ * groundednessScore?: number
61
+ * }}
62
+ * @fires PostGenerationGuard#checked
63
+ */
64
+ check(output, retrievedDocs = []) {
65
+ const issues = [];
66
+
67
+ // 1. Validate output is a string
68
+ if (output == null || typeof output !== 'string') {
69
+ issues.push('Output must be a string');
70
+ const result = { safe: false, issues, sanitizedOutput: '' };
71
+ this.emit('checked', result);
72
+ return result;
73
+ }
74
+
75
+ // 2. Check length constraints
76
+ if (
77
+ this.config.maxResponseLength > 0 &&
78
+ output.length > this.config.maxResponseLength
79
+ ) {
80
+ issues.push(
81
+ `Output exceeds maximum length of ${this.config.maxResponseLength} characters`,
82
+ );
83
+ }
84
+ if (
85
+ this.config.minResponseLength > 0 &&
86
+ output.length < this.config.minResponseLength
87
+ ) {
88
+ issues.push(
89
+ `Output is shorter than minimum length of ${this.config.minResponseLength} characters`,
90
+ );
91
+ }
92
+
93
+ // 3. PII detection
94
+ const piiMatches = {};
95
+ if (this.config.enablePIIDetection) {
96
+ for (const [name, pattern] of Object.entries(this.config.piiPatterns)) {
97
+ // Reset lastIndex for global regex patterns
98
+ pattern.lastIndex = 0;
99
+ const matches = output.match(pattern);
100
+ if (matches && matches.length > 0) {
101
+ piiMatches[name] = matches;
102
+ issues.push(
103
+ `PII detected (${name}): ${matches.length} occurrence(s)`,
104
+ );
105
+ }
106
+ }
107
+ }
108
+
109
+ // 4. Groundedness check
110
+ let groundednessScore;
111
+ if (
112
+ this.config.enableGroundednessCheck &&
113
+ Array.isArray(retrievedDocs) &&
114
+ retrievedDocs.length > 0
115
+ ) {
116
+ try {
117
+ // Lazy-require CitationTracker to avoid circular dependencies
118
+ const { CitationTracker } = require('../citation/citation-tracker');
119
+ const tracker = new CitationTracker();
120
+ const tracking = tracker.track(output, retrievedDocs);
121
+ groundednessScore = tracking.groundednessScore;
122
+
123
+ if (groundednessScore < this.config.groundednessThreshold) {
124
+ issues.push(
125
+ `Groundedness score (${groundednessScore.toFixed(2)}) is below threshold (${this.config.groundednessThreshold})`,
126
+ );
127
+ }
128
+ } catch (err) {
129
+ // If citation tracking fails, log as issue but don't block
130
+ issues.push(`Groundedness check failed: ${err.message}`);
131
+ }
132
+ }
133
+
134
+ // 5. Sanitize: replace PII matches with [REDACTED]
135
+ let sanitizedOutput = output;
136
+ if (Object.keys(piiMatches).length > 0) {
137
+ for (const [, matches] of Object.entries(piiMatches)) {
138
+ for (const match of matches) {
139
+ sanitizedOutput = sanitizedOutput.split(match).join('[REDACTED]');
140
+ }
141
+ }
142
+ }
143
+
144
+ // Truncate if over max length
145
+ if (
146
+ this.config.maxResponseLength > 0 &&
147
+ sanitizedOutput.length > this.config.maxResponseLength
148
+ ) {
149
+ sanitizedOutput = sanitizedOutput.substring(
150
+ 0,
151
+ this.config.maxResponseLength,
152
+ );
153
+ }
154
+
155
+ const result = {
156
+ safe: issues.length === 0,
157
+ issues,
158
+ sanitizedOutput,
159
+ };
160
+
161
+ if (groundednessScore !== undefined) {
162
+ result.groundednessScore = groundednessScore;
163
+ }
164
+
165
+ this.emit('checked', result);
166
+ return result;
167
+ }
168
+ }
169
+
170
+ module.exports = { PostGenerationGuard, DEFAULT_CONFIG };
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+ const { validateInput } = require('../security/validator');
5
+ const { tokenize } = require('../evaluate/scoring');
6
+
7
+ /**
8
+ * Default configuration for the PreRetrievalGuard.
9
+ * @type {object}
10
+ */
11
+ const DEFAULT_CONFIG = {
12
+ maxQueryLength: 10000,
13
+ enableInjectionDetection: true,
14
+ enableTopicFiltering: false,
15
+ blockedTopics: [],
16
+ allowedTopics: [],
17
+ injectionPatterns: [
18
+ /ignore\s+(all\s+)?previous\s+instructions/i,
19
+ /you\s+are\s+now/i,
20
+ /forget\s+everything/i,
21
+ /act\s+as\s+(a|an)?\s*/i,
22
+ /pretend\s+(you\s+are|to\s+be)/i,
23
+ /system\s*:/i,
24
+ /\bDAN\b/,
25
+ /bypass\s+(all\s+)?restrictions/i,
26
+ /override\s+(safety|content|system)/i,
27
+ /jailbreak/i,
28
+ ],
29
+ };
30
+
31
+ /**
32
+ * Pre-retrieval guard that validates and sanitizes user queries before
33
+ * they reach the retrieval layer. Checks for injection attacks, topic
34
+ * violations, and input length constraints.
35
+ *
36
+ * @extends EventEmitter
37
+ */
38
+ class PreRetrievalGuard extends EventEmitter {
39
+ /**
40
+ * @param {object} [options] - Override default configuration
41
+ * @param {number} [options.maxQueryLength=10000] - Maximum allowed query length
42
+ * @param {boolean} [options.enableInjectionDetection=true] - Whether to check for prompt injection
43
+ * @param {boolean} [options.enableTopicFiltering=false] - Whether to enforce topic constraints
44
+ * @param {string[]} [options.blockedTopics=[]] - Topics to reject
45
+ * @param {string[]} [options.allowedTopics=[]] - If non-empty, only these topics are permitted
46
+ * @param {RegExp[]} [options.injectionPatterns] - Additional injection patterns (merged with defaults)
47
+ */
48
+ constructor(options = {}) {
49
+ super();
50
+ this.config = { ...DEFAULT_CONFIG, ...options };
51
+ if (options.injectionPatterns) {
52
+ this.config.injectionPatterns = [
53
+ ...DEFAULT_CONFIG.injectionPatterns,
54
+ ...options.injectionPatterns,
55
+ ];
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Check a query for safety issues, injection attempts, and topic violations.
61
+ *
62
+ * @param {string} query - The user query to validate
63
+ * @returns {{
64
+ * safe: boolean,
65
+ * issues: string[],
66
+ * sanitizedQuery: string
67
+ * }}
68
+ * @fires PreRetrievalGuard#checked
69
+ */
70
+ check(query) {
71
+ const issues = [];
72
+
73
+ // 1. Validate query is a non-empty string
74
+ if (
75
+ query == null ||
76
+ typeof query !== 'string' ||
77
+ query.trim().length === 0
78
+ ) {
79
+ issues.push('Query must be a non-empty string');
80
+ const result = { safe: false, issues, sanitizedQuery: '' };
81
+ this.emit('checked', result);
82
+ return result;
83
+ }
84
+
85
+ // 2. Run basic input validation (length + disallowed tokens)
86
+ const validation = validateInput(query, {
87
+ maxLength: this.config.maxQueryLength,
88
+ });
89
+ if (!validation.valid) {
90
+ for (const reason of validation.reasons) {
91
+ issues.push(reason);
92
+ }
93
+ }
94
+
95
+ let sanitizedQuery = validation.sanitized;
96
+
97
+ // 3. Injection detection
98
+ const matchedPatterns = [];
99
+ if (this.config.enableInjectionDetection) {
100
+ for (const pattern of this.config.injectionPatterns) {
101
+ if (pattern.test(sanitizedQuery)) {
102
+ matchedPatterns.push(pattern);
103
+ issues.push(`Potential prompt injection detected: ${pattern}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ // 4. Topic filtering
109
+ if (this.config.enableTopicFiltering) {
110
+ const tokens = tokenize(sanitizedQuery);
111
+
112
+ // Check blocked topics
113
+ if (this.config.blockedTopics.length > 0) {
114
+ for (const topic of this.config.blockedTopics) {
115
+ const topicLower = topic.toLowerCase();
116
+ if (tokens.includes(topicLower)) {
117
+ issues.push(`Query contains blocked topic: ${topic}`);
118
+ }
119
+ }
120
+ }
121
+
122
+ // Check allowed topics (whitelist mode)
123
+ if (this.config.allowedTopics.length > 0) {
124
+ const allowedLower = this.config.allowedTopics.map((t) =>
125
+ t.toLowerCase(),
126
+ );
127
+ const hasAllowedTopic = tokens.some((token) =>
128
+ allowedLower.includes(token),
129
+ );
130
+ if (!hasAllowedTopic) {
131
+ issues.push('Query does not match any allowed topics');
132
+ }
133
+ }
134
+ }
135
+
136
+ // 5. Sanitize: strip matched injection patterns from query
137
+ if (matchedPatterns.length > 0) {
138
+ for (const pattern of matchedPatterns) {
139
+ // Create a global version of the pattern for replacement
140
+ const flags = pattern.flags.includes('g')
141
+ ? pattern.flags
142
+ : pattern.flags + 'g';
143
+ const globalPattern = new RegExp(pattern.source, flags);
144
+ sanitizedQuery = sanitizedQuery.replace(globalPattern, '').trim();
145
+ }
146
+ // Collapse multiple spaces left after stripping
147
+ sanitizedQuery = sanitizedQuery.replace(/\s{2,}/g, ' ').trim();
148
+ }
149
+
150
+ const result = {
151
+ safe: issues.length === 0,
152
+ issues,
153
+ sanitizedQuery,
154
+ };
155
+
156
+ this.emit('checked', result);
157
+ return result;
158
+ }
159
+ }
160
+
161
+ module.exports = { PreRetrievalGuard, DEFAULT_CONFIG };
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+
5
+ /**
6
+ * Default configuration for the RetrievalGuard.
7
+ * @type {object}
8
+ */
9
+ const DEFAULT_CONFIG = {
10
+ minRelevanceScore: 0,
11
+ maxAgeDays: 0, // 0 = disabled
12
+ enforceACL: false,
13
+ };
14
+
15
+ /**
16
+ * Retrieval guard that filters search results based on relevance score,
17
+ * document freshness, and access control lists. Applied between the
18
+ * retrieval and generation stages of the pipeline.
19
+ *
20
+ * @extends EventEmitter
21
+ */
22
+ class RetrievalGuard extends EventEmitter {
23
+ /**
24
+ * @param {object} [options] - Override default configuration
25
+ * @param {number} [options.minRelevanceScore=0] - Minimum relevance score threshold
26
+ * @param {number} [options.maxAgeDays=0] - Maximum document age in days (0 = disabled)
27
+ * @param {boolean} [options.enforceACL=false] - Whether to enforce access control
28
+ */
29
+ constructor(options = {}) {
30
+ super();
31
+ this.config = { ...DEFAULT_CONFIG, ...options };
32
+ }
33
+
34
+ /**
35
+ * Filter retrieval results based on configured guards.
36
+ *
37
+ * @param {Array<object>} results - Array of retrieval results, each with .score and optional .metadata
38
+ * @param {object} [context={}] - Request context
39
+ * @param {string[]} [context.userPermissions] - User's permission roles for ACL filtering
40
+ * @returns {{
41
+ * results: Array<object>,
42
+ * removed: Array<object>,
43
+ * metadata: { reasons: Object<string, number> }
44
+ * }}
45
+ * @fires RetrievalGuard#filtered
46
+ */
47
+ filter(results, context = {}) {
48
+ if (!Array.isArray(results)) {
49
+ const output = { results: [], removed: [], metadata: { reasons: {} } };
50
+ this.emit('filtered', output);
51
+ return output;
52
+ }
53
+
54
+ const kept = [];
55
+ const removed = [];
56
+ const reasons = {};
57
+
58
+ const now = Date.now();
59
+ const maxAgeMs =
60
+ this.config.maxAgeDays > 0
61
+ ? this.config.maxAgeDays * 24 * 60 * 60 * 1000
62
+ : 0;
63
+
64
+ for (const result of results) {
65
+ let dominated = false;
66
+ const doc = result.metadata || result.doc || result;
67
+
68
+ // 1. Relevance score filter
69
+ if (this.config.minRelevanceScore > 0) {
70
+ const score = result.score != null ? result.score : 0;
71
+ if (score < this.config.minRelevanceScore) {
72
+ dominated = true;
73
+ reasons.relevance = (reasons.relevance || 0) + 1;
74
+ }
75
+ }
76
+
77
+ // 2. Freshness filter
78
+ if (!dominated && maxAgeMs > 0) {
79
+ const timestamp = doc.updatedAt || doc.timestamp;
80
+ if (timestamp) {
81
+ const docTime = new Date(timestamp).getTime();
82
+ if (!isNaN(docTime) && now - docTime > maxAgeMs) {
83
+ dominated = true;
84
+ reasons.freshness = (reasons.freshness || 0) + 1;
85
+ }
86
+ }
87
+ }
88
+
89
+ // 3. ACL filter
90
+ if (!dominated && this.config.enforceACL && context.userPermissions) {
91
+ const requiredRoles = doc.requiredRole
92
+ ? [].concat(doc.requiredRole)
93
+ : doc.acl
94
+ ? [].concat(doc.acl)
95
+ : null;
96
+
97
+ if (requiredRoles && requiredRoles.length > 0) {
98
+ const hasAccess = requiredRoles.some((role) =>
99
+ context.userPermissions.includes(role),
100
+ );
101
+ if (!hasAccess) {
102
+ dominated = true;
103
+ reasons.acl = (reasons.acl || 0) + 1;
104
+ }
105
+ }
106
+ }
107
+
108
+ if (dominated) {
109
+ removed.push(result);
110
+ } else {
111
+ kept.push(result);
112
+ }
113
+ }
114
+
115
+ const output = {
116
+ results: kept,
117
+ removed,
118
+ metadata: { reasons },
119
+ };
120
+
121
+ this.emit('filtered', { original: results, filtered: kept, removed });
122
+ return output;
123
+ }
124
+ }
125
+
126
+ module.exports = { RetrievalGuard, DEFAULT_CONFIG };