@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.
- package/README.md +293 -361
- package/contracts/loader-contract.json +11 -6
- package/contracts/reranker-contract.json +18 -26
- package/dist/index.cjs +21190 -47
- package/dist/index.d.ts +298 -68
- package/dist/index.mjs +9 -76
- package/package.json +20 -8
- package/src/agentic/agentic-pipeline.js +235 -0
- package/src/agentic/iterative-retriever.js +213 -0
- package/src/agentic/query-planner.js +208 -0
- package/src/agentic/self-critique.js +102 -0
- package/src/ai/adaptive-retrieval.js +0 -3
- package/src/ai/federated-learning.js +0 -3
- package/src/ai/federation.js +1 -6
- package/src/ai/index.js +9 -20
- package/src/ai/model-training.js +0 -3
- package/src/ai/multimodal-processing.js +0 -3
- package/src/ai/multimodal.js +1 -6
- package/src/ai/orchestrator.js +1 -6
- package/src/ai/retrieval-engine.js +1 -6
- package/src/chunking/chunking-engine.js +142 -0
- package/src/chunking/strategies/fixed-size.js +54 -0
- package/src/chunking/strategies/recursive.js +127 -0
- package/src/chunking/strategies/semantic.js +128 -0
- package/src/chunking/strategies/sentence.js +32 -0
- package/src/chunking/strategies/structure-aware.js +138 -0
- package/src/citation/citation-tracker.js +161 -0
- package/src/citation/hallucination-detector.js +74 -0
- package/src/citation/source-mapper.js +95 -0
- package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
- package/src/cli/commands/ai-ml/federated-commands.js +207 -0
- package/src/cli/commands/ai-ml/index.js +17 -0
- package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
- package/src/cli/commands/ai-ml/shared.js +51 -0
- package/src/cli/commands/ai-ml/training-commands.js +228 -0
- package/src/cli/commands/ai-ml.js +176 -757
- package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
- package/src/cli/commands/plugin-hub/review-commands.js +78 -0
- package/src/cli/commands/plugin-hub.js +106 -782
- package/src/cli/diagnostics/config-checker.js +81 -0
- package/src/cli/diagnostics/dependency-checker.js +69 -0
- package/src/cli/diagnostics/environment-checker.js +45 -0
- package/src/cli/diagnostics/performance-checker.js +51 -0
- package/src/cli/diagnostics/plugin-checker.js +106 -0
- package/src/cli/diagnostics/security-checker.js +61 -0
- package/src/cli/doctor-command.js +70 -369
- package/src/cli/enhanced-cli-commands.js +162 -706
- package/src/cli/handlers/config-handler.js +126 -0
- package/src/cli/handlers/diagnostics-handler.js +113 -0
- package/src/cli/handlers/ingest-handler.js +42 -0
- package/src/cli/handlers/init-handler.js +51 -0
- package/src/cli/handlers/query-handler.js +45 -0
- package/src/cli/handlers/run-handler.js +181 -0
- package/src/cli/interactive-wizard.js +99 -723
- package/src/cli/shared/cli-utils.js +89 -0
- package/src/cli/wizard/configuration.js +148 -0
- package/src/cli/wizard/observability.js +79 -0
- package/src/cli/wizard/performance.js +76 -0
- package/src/cli/wizard/plugin-selection.js +297 -0
- package/src/cli/wizard/preview-save.js +172 -0
- package/src/cli/wizard/project-setup.js +66 -0
- package/src/config/memory-optimization.js +3 -3
- package/src/connectors/anthropic-connector.js +179 -0
- package/src/connectors/base-connector.js +94 -0
- package/src/connectors/cohere-connector.js +143 -0
- package/src/connectors/local-embedder.js +171 -0
- package/src/connectors/memory-retriever.js +150 -0
- package/src/connectors/ollama-connector.js +205 -0
- package/src/connectors/openai-connector.js +173 -0
- package/src/core/create-pipeline.js +21 -1
- package/src/core/observability/tracing.js +2 -1
- package/src/core/pipeline-factory.js +0 -1
- package/src/core/plugin-registry.js +14 -14
- package/src/cost/cost-calculator.js +146 -0
- package/src/cost/cost-tracker.js +260 -0
- package/src/cost/token-budget.js +245 -0
- package/src/dag/core/execution-scheduler.js +2 -1
- package/src/dag/core/topology-validation.js +0 -3
- package/src/dag/dag-engine.js +3 -5
- package/src/dag/dag-node.js +0 -1
- package/src/dag/enhanced-dag-engine.js +171 -99
- package/src/dashboard/dashboard-generator.js +642 -0
- package/src/dashboard/metrics-aggregator.js +409 -0
- package/src/debug/execution-tracer.js +210 -0
- package/src/debug/trace-inspector.js +281 -0
- package/src/dev/dev-server.js +5 -5
- package/src/dev/hot-reload.js +3 -2
- package/src/dx/integration-templates.js +0 -3
- package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
- package/src/ecosystem/plugin-hub.js +2 -2
- package/src/enterprise/audit-logging.js +2 -2
- package/src/enterprise/sso-integration.js +52 -32
- package/src/evaluate/context-metrics.js +127 -0
- package/src/evaluate/evaluator.js +4 -2
- package/src/evaluate/faithfulness.js +117 -0
- package/src/evaluate/groundedness.js +60 -0
- package/src/evaluate/pipeline-evaluator.js +156 -0
- package/src/evaluate/relevance.js +129 -0
- package/src/evaluate/scoring.js +27 -0
- package/src/graph/entity-extractor.js +183 -0
- package/src/graph/graph-index.js +113 -0
- package/src/graph/graph-retriever.js +244 -0
- package/src/graph/knowledge-graph.js +256 -0
- package/src/guardrails/guardrails-pipeline.js +136 -0
- package/src/guardrails/post-generation-guard.js +170 -0
- package/src/guardrails/pre-retrieval-guard.js +161 -0
- package/src/guardrails/retrieval-guard.js +126 -0
- package/src/index.js +255 -9
- package/src/loader/html-loader.js +4 -15
- package/src/loader/markdown-loader.js +4 -15
- package/src/mcp/mcp-server.js +199 -0
- package/src/mcp/mcp-tool-builder.js +121 -0
- package/src/observability/audit-logger.js +0 -2
- package/src/observability/event-logger.js +2 -1
- package/src/observability/security-monitor.js +0 -2
- package/src/observability/telemetry.js +0 -2
- package/src/reranker/cascade-reranker.js +79 -0
- package/src/reranker/embedding-reranker.js +124 -0
- package/src/reranker/llm-reranker.js +6 -3
- package/src/reranker/scoring-reranker.js +123 -0
- package/src/retrieval/bm25-search.js +157 -0
- package/src/retrieval/hybrid-retriever.js +111 -0
- package/src/retrieval/rank-fusion.js +62 -0
- package/src/security/jwt-config.js +4 -3
- package/src/security/jwt-validator.js +2 -1
- package/src/security/plugin-sandbox.js +0 -3
- package/src/security/plugin-signature-verifier.js +4 -6
- package/src/security/validator.js +0 -2
- package/src/streaming/embedding-stream.js +197 -0
- package/src/streaming/sse-adapter.js +168 -0
- package/src/streaming/stream-router.js +210 -0
- package/src/streaming/streaming-embedder.js +231 -0
- package/src/streaming/websocket-adapter.js +171 -0
- package/src/templates/scaffold.js +147 -0
- package/src/templates/template-registry.js +272 -0
- package/src/utils/contract-validator.js +2 -1
- package/src/utils/input-sanitizer.js +80 -99
- package/src/utils/performance-monitor.js +0 -3
- package/src/utils/plugin-scaffolder.js +9 -16
- package/src/utils/rate-limiter.js +0 -1
- package/src/utils/retry-policy.js +0 -3
- package/src/utils/safe-expression-evaluator.js +0 -3
- package/src/utils/secure-logger-config.js +2 -4
- package/src/utils/secure-logging.js +0 -3
- package/src/utils/structured-logger.js +0 -3
- package/src/ai/index-fixed.js +0 -509
- package/src/ai/module-template.js +0 -37
- package/src/ci/workflow-analyzer.js +0 -71
- package/src/cli/plugin-marketplace-commands.js +0 -869
- package/src/dev/README.md +0 -126
- package/src/dev/examples/hot-reload-usage.js +0 -355
- package/src/mocks/openai-embedder.js +0 -119
- package/src/mocks/openai-llm.js +0 -77
- package/src/mocks/pdf-loader.js +0 -21
- package/src/mocks/pinecone-retriever.js +0 -106
- package/src/scripts/script-utilities.js +0 -36
- package/src/utils/ci/diagnostic-reporter.js +0 -57
- 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 };
|