@devilsdev/rag-pipeline-utils 2.3.0 → 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 -361
  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,235 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+ const { DAG } = require('../dag/dag-engine');
5
+ const { QueryPlanner } = require('./query-planner');
6
+ const { IterativeRetriever } = require('./iterative-retriever');
7
+ const { SelfCritiqueChecker } = require('./self-critique');
8
+
9
+ /**
10
+ * Default configuration for the AgenticPipeline.
11
+ * @type {object}
12
+ */
13
+ const DEFAULT_CONFIG = {
14
+ maxConcurrency: 3,
15
+ timeout: 60000,
16
+ enablePlanning: true,
17
+ enableCritique: true,
18
+ enableIterativeRetrieval: false,
19
+ maxIterations: 3,
20
+ critiqueThreshold: 0.5,
21
+ };
22
+
23
+ /**
24
+ * DAG-based agentic RAG orchestrator with two-phase execution:
25
+ * Phase 1 - Plan: decompose query into sub-queries
26
+ * Phase 2 - Execute: build DAG of retrieve -> merge -> (critique ->) generate
27
+ *
28
+ * @extends EventEmitter
29
+ */
30
+ class AgenticPipeline extends EventEmitter {
31
+ /**
32
+ * @param {object} [options] - Override default configuration
33
+ * @param {number} [options.maxConcurrency=3] - Max concurrent DAG node execution
34
+ * @param {number} [options.timeout=60000] - Global timeout in ms
35
+ * @param {boolean} [options.enablePlanning=true] - Whether to decompose queries
36
+ * @param {boolean} [options.enableCritique=true] - Whether to self-critique answers
37
+ * @param {boolean} [options.enableIterativeRetrieval=false] - Whether to use iterative retrieval
38
+ * @param {number} [options.maxIterations=3] - Max iterations for iterative retrieval
39
+ * @param {number} [options.critiqueThreshold=0.5] - Groundedness threshold for approval
40
+ * @param {object} [options.plannerOptions] - Options forwarded to QueryPlanner
41
+ * @param {object} [options.iterativeOptions] - Options forwarded to IterativeRetriever
42
+ */
43
+ constructor(options = {}) {
44
+ super();
45
+ this.config = { ...DEFAULT_CONFIG, ...options };
46
+ this.planner = new QueryPlanner(options.plannerOptions);
47
+ this.critiqueChecker = new SelfCritiqueChecker({
48
+ approvalThreshold: this.config.critiqueThreshold,
49
+ });
50
+ if (this.config.enableIterativeRetrieval) {
51
+ this.iterativeRetriever = new IterativeRetriever(
52
+ options.iterativeOptions,
53
+ );
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Run the full agentic pipeline: plan -> retrieve -> merge -> (critique ->) generate.
59
+ *
60
+ * @param {object} params
61
+ * @param {string} params.query - The user query
62
+ * @param {object} params.retriever - Retriever instance with .retrieve() method
63
+ * @param {object} params.llm - LLM instance with .generate() method
64
+ * @param {Array<number>} [params.queryVector] - Optional pre-computed query vector
65
+ * @param {object} [params.options={}] - Extra options passed to llm.generate()
66
+ * @returns {Promise<{
67
+ * success: boolean,
68
+ * query: string,
69
+ * answer: string,
70
+ * results: Array<object>,
71
+ * critique: object|null,
72
+ * subQueries: Array<object>,
73
+ * metadata: object
74
+ * }>}
75
+ * @throws {Error} If retriever or llm is missing
76
+ */
77
+ async run({ query, retriever, llm, queryVector, options = {} }) {
78
+ // 1. Validate inputs
79
+ if (!retriever || typeof retriever.retrieve !== 'function') {
80
+ throw new Error(
81
+ 'AgenticPipeline.run(): retriever with .retrieve() method is required',
82
+ );
83
+ }
84
+ if (!llm || typeof llm.generate !== 'function') {
85
+ throw new Error(
86
+ 'AgenticPipeline.run(): llm with .generate() method is required',
87
+ );
88
+ }
89
+ if (!query || typeof query !== 'string' || !query.trim()) {
90
+ throw new Error(
91
+ 'AgenticPipeline.run(): query must be a non-empty string',
92
+ );
93
+ }
94
+
95
+ // Phase 1: Plan
96
+ let subQueries;
97
+ if (this.config.enablePlanning) {
98
+ const plan = this.planner.plan(query);
99
+ subQueries = plan.subQueries;
100
+ this.emit('planned', plan);
101
+ } else {
102
+ subQueries = [{ subQuery: query, strategy: 'general', priority: 0 }];
103
+ }
104
+
105
+ // Phase 2: Build and execute DAG
106
+ const dag = new DAG();
107
+
108
+ // Add retrieve nodes (one per sub-query)
109
+ for (let i = 0; i < subQueries.length; i++) {
110
+ const sq = subQueries[i];
111
+ dag.addNode(
112
+ `retrieve-${i}`,
113
+ async () => {
114
+ if (this.iterativeRetriever) {
115
+ const result = await this.iterativeRetriever.retrieve({
116
+ query: sq.subQuery,
117
+ retriever,
118
+ llm,
119
+ queryVector,
120
+ });
121
+ return result.results;
122
+ }
123
+ return await retriever.retrieve({
124
+ query: sq.subQuery,
125
+ queryVector,
126
+ topK: 10,
127
+ });
128
+ },
129
+ { timeout: this.config.timeout / 2, priority: sq.priority },
130
+ );
131
+ }
132
+
133
+ // Add merge node
134
+ dag.addNode('merge', async (inputs) => {
135
+ const allResults = Array.isArray(inputs)
136
+ ? inputs.flat()
137
+ : [inputs].flat();
138
+ // Deduplicate by id
139
+ const seen = new Map();
140
+ for (const r of allResults) {
141
+ if (r == null) continue;
142
+ const id = r?.id || JSON.stringify(r);
143
+ if (!seen.has(id) || (r.score && r.score > (seen.get(id).score || 0))) {
144
+ seen.set(id, r);
145
+ }
146
+ }
147
+ return Array.from(seen.values());
148
+ });
149
+
150
+ // Connect retrieve nodes to merge
151
+ for (let i = 0; i < subQueries.length; i++) {
152
+ dag.connect(`retrieve-${i}`, 'merge');
153
+ }
154
+
155
+ // Add critique + generate nodes
156
+ if (this.config.enableCritique) {
157
+ dag.addNode('pre-generate', async (mergedResults) => {
158
+ // Generate initial answer for critique
159
+ const answer = await llm.generate(query, mergedResults, options);
160
+ const critique = this.critiqueChecker.check(
161
+ String(answer),
162
+ mergedResults,
163
+ );
164
+ this.emit('critiqued', critique);
165
+ return { answer: String(answer), mergedResults, critique };
166
+ });
167
+ dag.connect('merge', 'pre-generate');
168
+
169
+ dag.addNode('generate', async ({ answer, mergedResults, critique }) => {
170
+ if (critique.approved) {
171
+ return { answer, results: mergedResults, critique };
172
+ }
173
+ // Re-generate with critique feedback
174
+ const issueTexts = critique.issues.map(
175
+ (i) => i.sentence || i.text || String(i),
176
+ );
177
+ const refinedPrompt = `${query}\n\nPrevious answer had issues: ${issueTexts.join('; ')}. Please provide a more grounded answer.`;
178
+ const refinedAnswer = await llm.generate(
179
+ refinedPrompt,
180
+ mergedResults,
181
+ options,
182
+ );
183
+ return {
184
+ answer: String(refinedAnswer),
185
+ results: mergedResults,
186
+ critique,
187
+ };
188
+ });
189
+ dag.connect('pre-generate', 'generate');
190
+ } else {
191
+ dag.addNode('generate', async (mergedResults) => {
192
+ const answer = await llm.generate(query, mergedResults, options);
193
+ return { answer: String(answer), results: mergedResults };
194
+ });
195
+ dag.connect('merge', 'generate');
196
+ }
197
+
198
+ // Execute DAG
199
+ const nodeCount =
200
+ subQueries.length + 2 + (this.config.enableCritique ? 1 : 0);
201
+ this.emit('executing', { nodeCount });
202
+
203
+ const dagResult = await dag.execute(query, {
204
+ concurrency: this.config.maxConcurrency,
205
+ timeout: this.config.timeout,
206
+ continueOnError: true,
207
+ gracefulDegradation: true,
208
+ });
209
+
210
+ // Extract final result from generate node
211
+ const output = dagResult?.answer
212
+ ? dagResult
213
+ : dagResult?.get
214
+ ? dagResult.get('generate')
215
+ : dagResult;
216
+
217
+ this.emit('completed', output);
218
+
219
+ return {
220
+ success: true,
221
+ query,
222
+ answer: output?.answer || '',
223
+ results: output?.results || [],
224
+ critique: output?.critique || null,
225
+ subQueries,
226
+ metadata: {
227
+ subQueryCount: subQueries.length,
228
+ enabledCritique: this.config.enableCritique,
229
+ enabledIterative: this.config.enableIterativeRetrieval,
230
+ },
231
+ };
232
+ }
233
+ }
234
+
235
+ module.exports = { AgenticPipeline, DEFAULT_CONFIG };
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+ const { retry } = require('../utils/retry');
5
+ const { tokenize, computeJaccardSimilarity } = require('../evaluate/scoring');
6
+
7
+ /**
8
+ * Default configuration for the IterativeRetriever.
9
+ * @type {object}
10
+ */
11
+ const DEFAULT_CONFIG = {
12
+ maxIterations: 3,
13
+ coverageThreshold: 0.6,
14
+ retryOptions: { retries: 2, baseDelay: 100 },
15
+ };
16
+
17
+ /**
18
+ * Iterative retriever that loops: retrieve -> check coverage -> refine -> re-retrieve.
19
+ * Continues until coverage threshold is met or maxIterations is reached.
20
+ *
21
+ * @extends EventEmitter
22
+ */
23
+ class IterativeRetriever extends EventEmitter {
24
+ /**
25
+ * @param {object} [options] - Override default configuration
26
+ * @param {number} [options.maxIterations=3] - Maximum retrieval iterations
27
+ * @param {number} [options.coverageThreshold=0.6] - Jaccard similarity threshold for convergence
28
+ * @param {object} [options.retryOptions] - Options passed to retry() on each retrieval call
29
+ */
30
+ constructor(options = {}) {
31
+ super();
32
+ this.config = { ...DEFAULT_CONFIG, ...options };
33
+ }
34
+
35
+ /**
36
+ * Merge new results into accumulated results, deduplicating by id.
37
+ * When duplicates exist, keep the one with the higher score.
38
+ *
39
+ * @param {Array<object>} accumulated - Existing accumulated results
40
+ * @param {Array<object>} newResults - Newly retrieved results
41
+ * @returns {Array<object>}
42
+ * @private
43
+ */
44
+ _mergeResults(accumulated, newResults) {
45
+ const seen = new Map();
46
+
47
+ for (const r of accumulated) {
48
+ const id = r?.id || JSON.stringify(r);
49
+ seen.set(id, r);
50
+ }
51
+
52
+ for (const r of newResults) {
53
+ const id = r?.id || JSON.stringify(r);
54
+ if (!seen.has(id) || (r.score && r.score > (seen.get(id).score || 0))) {
55
+ seen.set(id, r);
56
+ }
57
+ }
58
+
59
+ return Array.from(seen.values());
60
+ }
61
+
62
+ /**
63
+ * Compute query coverage against accumulated result contents using Jaccard similarity.
64
+ *
65
+ * @param {string} query - Original query
66
+ * @param {Array<object>} results - Accumulated results
67
+ * @returns {number} Coverage score between 0 and 1
68
+ * @private
69
+ */
70
+ _computeCoverage(query, results) {
71
+ const queryTokens = tokenize(query);
72
+ if (queryTokens.length === 0) return 0;
73
+
74
+ // Combine all result content into a single token set
75
+ const resultText = results
76
+ .map((r) => r?.content || r?.text || r?.chunk || '')
77
+ .join(' ');
78
+ const resultTokens = tokenize(resultText);
79
+
80
+ return computeJaccardSimilarity(queryTokens, resultTokens);
81
+ }
82
+
83
+ /**
84
+ * Heuristic query refinement: find query tokens missing from results
85
+ * and append them to the original query.
86
+ *
87
+ * @param {string} originalQuery - The original user query
88
+ * @param {Array<object>} results - Accumulated results
89
+ * @returns {string} Refined query
90
+ * @private
91
+ */
92
+ _heuristicRefine(originalQuery, results) {
93
+ const queryTokens = tokenize(originalQuery);
94
+ const resultText = results
95
+ .map((r) => r?.content || r?.text || r?.chunk || '')
96
+ .join(' ');
97
+ const resultTokenSet = new Set(tokenize(resultText));
98
+
99
+ const missing = queryTokens.filter((t) => !resultTokenSet.has(t));
100
+
101
+ if (missing.length === 0) return originalQuery;
102
+ return `${originalQuery} more about ${missing.join(' ')}`;
103
+ }
104
+
105
+ /**
106
+ * Perform iterative retrieval with coverage checking and query refinement.
107
+ *
108
+ * @param {object} params
109
+ * @param {string} params.query - The search query
110
+ * @param {object} params.retriever - Retriever instance with .retrieve() method
111
+ * @param {object} [params.llm] - Optional LLM for query refinement
112
+ * @param {Array<number>} [params.queryVector] - Optional pre-computed query vector
113
+ * @returns {Promise<{results: Array<object>, iterations: number, converged: boolean, coverage: number}>}
114
+ * @throws {Error} If query or retriever is missing
115
+ */
116
+ async retrieve({ query, retriever, llm, queryVector }) {
117
+ // 1. Validate inputs
118
+ if (!query || typeof query !== 'string' || !query.trim()) {
119
+ throw new Error(
120
+ 'IterativeRetriever.retrieve(): query must be a non-empty string',
121
+ );
122
+ }
123
+ if (!retriever || typeof retriever.retrieve !== 'function') {
124
+ throw new Error(
125
+ 'IterativeRetriever.retrieve(): retriever with .retrieve() method is required',
126
+ );
127
+ }
128
+
129
+ // 2. Initialize
130
+ let accumulatedResults = [];
131
+ let currentQuery = query;
132
+ let iterations = 0;
133
+ let coverage = 0;
134
+ let converged = false;
135
+
136
+ // 3. Retrieval loop
137
+ while (iterations < this.config.maxIterations) {
138
+ // a. Retrieve with retry
139
+ const retrieveQuery = currentQuery;
140
+ const newResults = await retry(
141
+ () =>
142
+ retriever.retrieve({ query: retrieveQuery, queryVector, topK: 10 }),
143
+ this.config.retryOptions,
144
+ );
145
+
146
+ // b. Merge and deduplicate
147
+ const resultsArray = Array.isArray(newResults) ? newResults : [];
148
+ accumulatedResults = this._mergeResults(accumulatedResults, resultsArray);
149
+
150
+ // c. Compute coverage
151
+ coverage = this._computeCoverage(query, accumulatedResults);
152
+
153
+ // d. Emit 'iteration' event
154
+ this.emit('iteration', {
155
+ iteration: iterations,
156
+ coverage,
157
+ resultCount: accumulatedResults.length,
158
+ query: currentQuery,
159
+ });
160
+
161
+ iterations += 1;
162
+
163
+ // e. Check convergence
164
+ if (coverage >= this.config.coverageThreshold) {
165
+ converged = true;
166
+ break;
167
+ }
168
+
169
+ // f. Refine query (skip on last iteration since we won't retrieve again)
170
+ if (iterations < this.config.maxIterations) {
171
+ let refined = null;
172
+
173
+ // Try LLM-based refinement first
174
+ if (llm && typeof llm.generate === 'function') {
175
+ try {
176
+ refined = await llm.generate(
177
+ `Given the query "${query}" and that current results don't fully cover the topic, generate a refined search query to find missing information.`,
178
+ accumulatedResults,
179
+ );
180
+ if (refined && typeof refined === 'string' && refined.trim()) {
181
+ refined = refined.trim();
182
+ } else {
183
+ refined = null;
184
+ }
185
+ } catch (_err) {
186
+ // LLM failed -- fall through to heuristic
187
+ refined = null;
188
+ }
189
+ }
190
+
191
+ // Heuristic fallback
192
+ if (!refined) {
193
+ refined = this._heuristicRefine(query, accumulatedResults);
194
+ }
195
+
196
+ currentQuery = refined;
197
+ }
198
+ }
199
+
200
+ // 4. Emit 'completed' event
201
+ this.emit('completed', {
202
+ results: accumulatedResults,
203
+ iterations,
204
+ converged,
205
+ coverage,
206
+ });
207
+
208
+ // 5. Return
209
+ return { results: accumulatedResults, iterations, converged, coverage };
210
+ }
211
+ }
212
+
213
+ module.exports = { IterativeRetriever, DEFAULT_CONFIG };
@@ -0,0 +1,208 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+
5
+ /**
6
+ * Default configuration for the QueryPlanner.
7
+ * @type {object}
8
+ */
9
+ const DEFAULT_CONFIG = {
10
+ minComplexityToSplit: 2,
11
+ maxSubQueries: 5,
12
+ };
13
+
14
+ /** Question words that indicate query complexity */
15
+ const QUESTION_WORDS = ['who', 'what', 'when', 'where', 'why', 'how', 'which'];
16
+
17
+ /** Conjunctions used for splitting compound queries */
18
+ const CONJUNCTIONS = ['and', 'or', 'also', 'as well as'];
19
+
20
+ /** Splitter patterns for decomposing compound queries */
21
+ const SPLIT_PATTERNS = [/ and /i, / or /i, /; ?/];
22
+
23
+ /** Keywords that indicate a comparative strategy */
24
+ const COMPARATIVE_KEYWORDS = [
25
+ 'compare',
26
+ 'contrast',
27
+ 'difference',
28
+ 'versus',
29
+ 'vs',
30
+ ];
31
+
32
+ /**
33
+ * Heuristic query decomposition planner -- no LLM needed.
34
+ * Splits compound queries into sub-queries with assigned strategies
35
+ * based on detected question types.
36
+ *
37
+ * @extends EventEmitter
38
+ */
39
+ class QueryPlanner extends EventEmitter {
40
+ /**
41
+ * @param {object} [options] - Override default configuration
42
+ * @param {number} [options.minComplexityToSplit=2] - Minimum complexity score before splitting
43
+ * @param {number} [options.maxSubQueries=5] - Maximum number of sub-queries to produce
44
+ */
45
+ constructor(options = {}) {
46
+ super();
47
+ this.config = { ...DEFAULT_CONFIG, ...options };
48
+ }
49
+
50
+ /**
51
+ * Compute the complexity score of a query string.
52
+ *
53
+ * Scoring rules:
54
+ * +1 per question word (who, what, when, where, why, how, which)
55
+ * +1 per conjunction (and, or, also, as well as)
56
+ * +1 if length > 100 characters
57
+ * +1 per semicolon
58
+ * +1 per comma-separated clause (commas - 1 if > 0 commas)
59
+ *
60
+ * @param {string} query
61
+ * @returns {number}
62
+ * @private
63
+ */
64
+ _computeComplexity(query) {
65
+ const lower = query.toLowerCase();
66
+ const words = lower.split(/\s+/);
67
+ let score = 0;
68
+
69
+ // +1 per question word
70
+ for (const w of QUESTION_WORDS) {
71
+ if (words.includes(w)) score += 1;
72
+ }
73
+
74
+ // +1 per conjunction
75
+ for (const c of CONJUNCTIONS) {
76
+ // "as well as" is multi-word -- use indexOf on the full string
77
+ if (c.includes(' ')) {
78
+ if (lower.includes(c)) score += 1;
79
+ } else if (words.includes(c)) {
80
+ score += 1;
81
+ }
82
+ }
83
+
84
+ // +1 if length > 100 chars
85
+ if (query.length > 100) score += 1;
86
+
87
+ // +1 per semicolon
88
+ const semicolons = (query.match(/;/g) || []).length;
89
+ score += semicolons;
90
+
91
+ // +1 per comma-separated clause (number of commas)
92
+ const commas = (query.match(/,/g) || []).length;
93
+ if (commas > 0) score += commas;
94
+
95
+ return score;
96
+ }
97
+
98
+ /**
99
+ * Detect the retrieval strategy for a query fragment.
100
+ *
101
+ * @param {string} text
102
+ * @returns {'factual'|'analytical'|'comparative'|'general'}
103
+ * @private
104
+ */
105
+ _detectStrategy(text) {
106
+ const lower = text.toLowerCase();
107
+ const words = lower.split(/\s+/);
108
+
109
+ // Check comparative keywords first (more specific)
110
+ for (const kw of COMPARATIVE_KEYWORDS) {
111
+ if (words.includes(kw) || lower.includes(kw)) return 'comparative';
112
+ }
113
+
114
+ // Check question words
115
+ for (const w of words) {
116
+ if (['who', 'what', 'where', 'when'].includes(w)) return 'factual';
117
+ if (['why', 'how'].includes(w)) return 'analytical';
118
+ }
119
+
120
+ return 'general';
121
+ }
122
+
123
+ /**
124
+ * Plan query decomposition.
125
+ *
126
+ * @param {string} query - The user query to decompose
127
+ * @returns {{
128
+ * subQueries: Array<{subQuery: string, strategy: string, priority: number}>,
129
+ * complexity: number,
130
+ * originalQuery: string
131
+ * }}
132
+ * @throws {Error} If query is not a non-empty string
133
+ * @fires QueryPlanner#planned
134
+ */
135
+ plan(query) {
136
+ // 1. Validate
137
+ if (!query || typeof query !== 'string' || !query.trim()) {
138
+ throw new Error('QueryPlanner.plan(): query must be a non-empty string');
139
+ }
140
+
141
+ const trimmed = query.trim();
142
+
143
+ // 2. Compute complexity
144
+ const complexity = this._computeComplexity(trimmed);
145
+
146
+ // 3. If complexity below threshold, return single sub-query
147
+ if (complexity < this.config.minComplexityToSplit) {
148
+ const result = {
149
+ subQueries: [
150
+ {
151
+ subQuery: trimmed,
152
+ strategy: this._detectStrategy(trimmed),
153
+ priority: 0,
154
+ },
155
+ ],
156
+ complexity,
157
+ originalQuery: trimmed,
158
+ };
159
+ this.emit('planned', result);
160
+ return result;
161
+ }
162
+
163
+ // 4. Split into sub-queries
164
+ let parts = [trimmed];
165
+ for (const pattern of SPLIT_PATTERNS) {
166
+ const newParts = [];
167
+ for (const part of parts) {
168
+ const split = part
169
+ .split(pattern)
170
+ .map((s) => s.trim())
171
+ .filter(Boolean);
172
+ newParts.push(...split);
173
+ }
174
+ parts = newParts;
175
+ }
176
+
177
+ // Build sub-queries with strategy and priority
178
+ let subQueries = parts.map((part, i) => ({
179
+ subQuery: part,
180
+ strategy: this._detectStrategy(part),
181
+ priority: i,
182
+ }));
183
+
184
+ // 5. Deduplicate by subQuery text (case-insensitive)
185
+ const seen = new Set();
186
+ subQueries = subQueries.filter((sq) => {
187
+ const key = sq.subQuery.toLowerCase();
188
+ if (seen.has(key)) return false;
189
+ seen.add(key);
190
+ return true;
191
+ });
192
+
193
+ // Limit to maxSubQueries
194
+ subQueries = subQueries.slice(0, this.config.maxSubQueries);
195
+
196
+ // 6. Emit 'planned' event
197
+ const result = {
198
+ subQueries,
199
+ complexity,
200
+ originalQuery: trimmed,
201
+ };
202
+
203
+ this.emit('planned', result);
204
+ return result;
205
+ }
206
+ }
207
+
208
+ module.exports = { QueryPlanner, DEFAULT_CONFIG };