@devilsdev/rag-pipeline-utils 2.4.5 → 2.5.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/.ragrc.schema.json +83 -192
- package/CHANGELOG.md +345 -0
- package/README.md +418 -117
- package/SECURITY.md +5 -5
- package/dist/experimental.cjs +2 -0
- package/dist/experimental.d.ts +1 -0
- package/dist/experimental.mjs +5 -0
- package/dist/index.cjs +4992 -1242
- package/dist/index.d.ts +80 -2
- package/dist/index.mjs +1 -1
- package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
- package/dist/types/agentic/iterative-retriever.d.ts +85 -0
- package/dist/types/agentic/query-planner.d.ts +145 -0
- package/dist/types/agentic/self-critique.d.ts +45 -0
- package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
- package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
- package/dist/types/ai/multimodal-processing.d.ts +32 -0
- package/dist/types/ai/multimodal.d.ts +39 -0
- package/dist/types/ai/retrieval-engine.d.ts +37 -0
- package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
- package/dist/types/chunking/chunking-engine.d.ts +70 -0
- package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
- package/dist/types/chunking/strategies/recursive.d.ts +20 -0
- package/dist/types/chunking/strategies/semantic.d.ts +19 -0
- package/dist/types/chunking/strategies/sentence.d.ts +13 -0
- package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
- package/dist/types/citation/citation-tracker.d.ts +64 -0
- package/dist/types/citation/hallucination-detector.d.ts +40 -0
- package/dist/types/citation/source-mapper.d.ts +37 -0
- package/dist/types/connectors/anthropic-connector.d.ts +92 -0
- package/dist/types/connectors/base-connector.d.ts +87 -0
- package/dist/types/connectors/cohere-connector.d.ts +89 -0
- package/dist/types/connectors/connector-utils.d.ts +110 -0
- package/dist/types/connectors/local-embedder.d.ts +86 -0
- package/dist/types/connectors/memory-retriever.d.ts +91 -0
- package/dist/types/connectors/ollama-connector.d.ts +110 -0
- package/dist/types/connectors/openai-connector.d.ts +114 -0
- package/dist/types/connectors/rag-prompt.d.ts +40 -0
- package/dist/types/cost/cost-calculator.d.ts +152 -0
- package/dist/types/cost/cost-tracker.d.ts +126 -0
- package/dist/types/cost/token-budget.d.ts +110 -0
- package/dist/types/dag/core/error-context.d.ts +239 -0
- package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
- package/dist/types/dag/core/topology-validation.d.ts +226 -0
- package/dist/types/dag/dag-engine.d.ts +156 -0
- package/dist/types/dag/dag-node.d.ts +37 -0
- package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
- package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
- package/dist/types/debug/execution-tracer.d.ts +57 -0
- package/dist/types/debug/trace-inspector.d.ts +31 -0
- package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
- package/dist/types/enterprise/data-governance.d.ts +203 -0
- package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
- package/dist/types/enterprise/sso-integration.d.ts +420 -0
- package/dist/types/evaluate/context-metrics.d.ts +55 -0
- package/dist/types/evaluate/faithfulness.d.ts +41 -0
- package/dist/types/evaluate/groundedness.d.ts +60 -0
- package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
- package/dist/types/evaluate/relevance.d.ts +21 -0
- package/dist/types/evaluate/scoring.d.ts +123 -0
- package/dist/types/experimental.d.ts +15 -0
- package/dist/types/graph/entity-extractor.d.ts +84 -0
- package/dist/types/graph/graph-index.d.ts +49 -0
- package/dist/types/graph/graph-retriever.d.ts +81 -0
- package/dist/types/graph/knowledge-graph.d.ts +129 -0
- package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
- package/dist/types/guardrails/injection-detector.d.ts +47 -0
- package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
- package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
- package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
- package/dist/types/mcp/mcp-server.d.ts +85 -0
- package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
- package/dist/types/reranker/cascade-reranker.d.ts +40 -0
- package/dist/types/reranker/embedding-reranker.d.ts +77 -0
- package/dist/types/reranker/scoring-reranker.d.ts +41 -0
- package/dist/types/retrieval/bm25-search.d.ts +43 -0
- package/dist/types/retrieval/file-vector-store.d.ts +87 -0
- package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
- package/dist/types/retrieval/rank-fusion.d.ts +10 -0
- package/dist/types/security/jwt-config.d.ts +80 -0
- package/dist/types/security/jwt-validator.d.ts +195 -0
- package/dist/types/security/plugin-sandbox.d.ts +158 -0
- package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
- package/dist/types/security/validator.d.ts +5 -0
- package/dist/types/streaming/embedding-stream.d.ts +65 -0
- package/dist/types/streaming/sse-adapter.d.ts +59 -0
- package/dist/types/streaming/stream-router.d.ts +88 -0
- package/dist/types/streaming/streaming-embedder.d.ts +99 -0
- package/dist/types/streaming/websocket-adapter.d.ts +71 -0
- package/dist/types/templates/scaffold.d.ts +55 -0
- package/dist/types/templates/template-registry.d.ts +63 -0
- package/dist/types/utils/embed-query.d.ts +20 -0
- package/dist/types/utils/logger.d.ts +50 -0
- package/dist/types/utils/rate-limiter.d.ts +66 -0
- package/dist/types/utils/retry.d.ts +27 -0
- package/dist/types/utils/run-abort.d.ts +34 -0
- package/dist/types/utils/secure-logging.d.ts +51 -0
- package/dist/types/utils/structured-logger.d.ts +126 -0
- package/package.json +53 -29
- package/src/agentic/agentic-pipeline.js +75 -7
- package/src/agentic/iterative-retriever.js +47 -10
- package/src/agentic/query-planner.js +329 -78
- package/src/citation/citation-tracker.js +6 -4
- package/src/citation/hallucination-detector.js +2 -2
- package/src/citation/source-mapper.js +15 -28
- package/src/cli/commands/ai.js +101 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
- package/src/cli/commands/plugin-hub/local-commands.js +143 -0
- package/src/cli/commands/plugin-hub.js +96 -36
- package/src/cli/diagnostics/config-checker.js +76 -63
- package/src/cli/diagnostics/environment-checker.js +62 -31
- package/src/cli/diagnostics/plugin-checker.js +36 -89
- package/src/cli/doctor-command.js +39 -5
- package/src/cli/enhanced-cli-commands.js +72 -217
- package/src/cli/handlers/ai-handler.js +364 -0
- package/src/cli/handlers/diagnostics-handler.js +78 -31
- package/src/cli/handlers/ingest-handler.js +28 -22
- package/src/cli/handlers/init-handler.js +148 -25
- package/src/cli/handlers/query-handler.js +41 -26
- package/src/cli/handlers/run-handler.js +16 -167
- package/src/cli/handlers/serve-handler.js +60 -0
- package/src/cli/services/bench-stats.js +54 -0
- package/src/cli/services/benchmark.js +161 -0
- package/src/cli/services/feedback.js +155 -0
- package/src/cli/services/rag-service.js +322 -0
- package/src/cli/shared/cli-utils.js +24 -7
- package/src/cli/shared/config-context.js +21 -0
- package/src/cli/wizard/config-builder.js +81 -0
- package/src/cli/wizard/init-wizard.js +617 -0
- package/src/config/component-factory.js +125 -0
- package/src/config/load-config.js +29 -10
- package/src/config/load-plugin-config.js +46 -2
- package/src/connectors/anthropic-connector.js +93 -55
- package/src/connectors/base-connector.js +74 -19
- package/src/connectors/cohere-connector.js +88 -32
- package/src/connectors/connector-utils.js +302 -0
- package/src/connectors/local-embedder.js +43 -5
- package/src/connectors/memory-retriever.js +108 -15
- package/src/connectors/ollama-connector.js +83 -49
- package/src/connectors/openai-connector.js +108 -44
- package/src/connectors/rag-prompt.js +88 -0
- package/src/core/create-pipeline.js +209 -60
- package/src/core/plugin-registry.js +87 -11
- package/src/cost/cost-calculator.js +205 -32
- package/src/dev/dev-server.js +166 -11
- package/src/dev/hot-reload.js +40 -1
- package/src/ecosystem/plugin-checks.js +181 -0
- package/src/ecosystem/plugin-hub.js +50 -125
- package/src/ecosystem/plugin-installer.js +298 -0
- package/src/ecosystem/plugin-probe.js +127 -0
- package/src/ecosystem/plugin-scanner.js +185 -0
- package/src/ecosystem/tar-extract.js +114 -0
- package/src/enterprise/audit-logging.js +158 -29
- package/src/enterprise/sso-integration.js +715 -228
- package/src/evaluate/context-metrics.js +28 -16
- package/src/evaluate/evaluator.js +3 -3
- package/src/evaluate/faithfulness.js +18 -14
- package/src/evaluate/groundedness.js +43 -12
- package/src/evaluate/pipeline-evaluator.js +35 -8
- package/src/evaluate/scoring.js +251 -3
- package/src/experimental.js +71 -0
- package/src/guardrails/guardrails-pipeline.js +69 -7
- package/src/guardrails/injection-detector.js +283 -0
- package/src/guardrails/post-generation-guard.js +19 -0
- package/src/guardrails/pre-retrieval-guard.js +36 -36
- package/src/guardrails/retrieval-guard.js +58 -8
- package/src/index.js +36 -1
- package/src/loader/document-reader.js +91 -0
- package/src/reranker/cascade-reranker.js +3 -0
- package/src/reranker/embedding-reranker.js +136 -21
- package/src/reranker/llm-reranker.js +79 -12
- package/src/reranker/scoring-reranker.js +3 -0
- package/src/retrieval/bm25-search.js +12 -6
- package/src/retrieval/file-vector-store.js +212 -0
- package/src/retrieval/hybrid-retriever.js +22 -19
- package/src/security/jwt-config.js +13 -2
- package/src/security/jwt-validator.js +134 -30
- package/src/security/plugin-sandbox.js +18 -1
- package/src/security/plugin-signature-verifier.js +245 -70
- package/src/utils/deprecate.js +69 -0
- package/src/utils/embed-query.js +33 -0
- package/src/utils/input-sanitizer.js +19 -4
- package/src/utils/logger.js +34 -4
- package/src/utils/run-abort.js +102 -0
- package/src/ai/federated-learning.js +0 -211
- package/src/ai/federation.js +0 -71
- package/src/ai/index.js +0 -22
- package/src/ai/model-trainer.js +0 -217
- package/src/ai/model-training.js +0 -202
- package/src/ai/multimodal/multi-modal-processor.js +0 -345
- package/src/ai/orchestrator.js +0 -65
- package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
- package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
- package/src/cli/commands/ai-ml/federated-commands.js +0 -207
- package/src/cli/commands/ai-ml/index.js +0 -17
- package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
- package/src/cli/commands/ai-ml/shared.js +0 -51
- package/src/cli/commands/ai-ml/training-commands.js +0 -228
- package/src/cli/commands/ai-ml.js +0 -386
- package/src/cli/commands/docs.js +0 -228
- package/src/cli/commands/dx.js +0 -538
- package/src/cli/commands/wizard.js +0 -627
- package/src/cli/input-sanitization-middleware.js +0 -356
- package/src/cli/interactive-wizard.js +0 -373
- package/src/cli/templates/development.template.json +0 -64
- package/src/cli/templates/minimal.template.json +0 -33
- package/src/cli/templates/production.template.json +0 -104
- package/src/cli/templates/testing.template.json +0 -45
- package/src/cli/wizard/configuration.js +0 -148
- package/src/cli/wizard/observability.js +0 -79
- package/src/cli/wizard/performance.js +0 -76
- package/src/cli/wizard/plugin-selection.js +0 -297
- package/src/cli/wizard/preview-save.js +0 -172
- package/src/cli/wizard/project-setup.js +0 -66
- package/src/core/pipeline-factory.js +0 -59
- package/src/dag/enhanced-dag-engine.js +0 -894
- package/src/dx/index.js +0 -40
- package/src/dx/integration-templates.js +0 -662
- package/src/dx/performance-profiler.js +0 -968
- package/src/dx/realtime-debugger.js +0 -755
- package/src/dx/visual-pipeline-builder.js +0 -578
- package/src/ingest.js +0 -59
- package/src/pipeline/pipeline.js +0 -18
- package/src/query.js +0 -117
|
@@ -14,12 +14,9 @@ const DEFAULT_CONFIG = {
|
|
|
14
14
|
/** Question words that indicate query complexity */
|
|
15
15
|
const QUESTION_WORDS = ['who', 'what', 'when', 'where', 'why', 'how', 'which'];
|
|
16
16
|
|
|
17
|
-
/** Conjunctions used for
|
|
17
|
+
/** Conjunctions used for scoring compound queries */
|
|
18
18
|
const CONJUNCTIONS = ['and', 'or', 'also', 'as well as'];
|
|
19
19
|
|
|
20
|
-
/** Splitter patterns for decomposing compound queries */
|
|
21
|
-
const SPLIT_PATTERNS = [/ and /i, / or /i, /; ?/];
|
|
22
|
-
|
|
23
20
|
/** Keywords that indicate a comparative strategy */
|
|
24
21
|
const COMPARATIVE_KEYWORDS = [
|
|
25
22
|
'compare',
|
|
@@ -27,19 +24,111 @@ const COMPARATIVE_KEYWORDS = [
|
|
|
27
24
|
'difference',
|
|
28
25
|
'versus',
|
|
29
26
|
'vs',
|
|
27
|
+
'compared',
|
|
28
|
+
'comparing',
|
|
29
|
+
'comparison',
|
|
30
|
+
'contrasting',
|
|
31
|
+
'differ',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Words that start a new question or instruction. A conjunction ("and",
|
|
36
|
+
* "or", "as well as") splits a query only when one of these follows it, so
|
|
37
|
+
* "What is RAG and how does BM25 work" splits while "salt and pepper" and
|
|
38
|
+
* "Should I use A or B" do not.
|
|
39
|
+
*/
|
|
40
|
+
const CLAUSE_STARTERS = [
|
|
41
|
+
// question words
|
|
42
|
+
'who',
|
|
43
|
+
'whom',
|
|
44
|
+
'whose',
|
|
45
|
+
'what',
|
|
46
|
+
'when',
|
|
47
|
+
'where',
|
|
48
|
+
'why',
|
|
49
|
+
'how',
|
|
50
|
+
'which',
|
|
51
|
+
// auxiliaries opening a yes/no question
|
|
52
|
+
'is',
|
|
53
|
+
'are',
|
|
54
|
+
'was',
|
|
55
|
+
'were',
|
|
56
|
+
'do',
|
|
57
|
+
'does',
|
|
58
|
+
'did',
|
|
59
|
+
'can',
|
|
60
|
+
'could',
|
|
61
|
+
'should',
|
|
62
|
+
'would',
|
|
63
|
+
'will',
|
|
64
|
+
'has',
|
|
65
|
+
'have',
|
|
66
|
+
// imperatives opening an instruction
|
|
67
|
+
'explain',
|
|
68
|
+
'describe',
|
|
69
|
+
'list',
|
|
70
|
+
'compare',
|
|
71
|
+
'contrast',
|
|
72
|
+
'summarize',
|
|
73
|
+
'summarise',
|
|
74
|
+
'tell',
|
|
75
|
+
'show',
|
|
76
|
+
'give',
|
|
77
|
+
'define',
|
|
78
|
+
'find',
|
|
30
79
|
];
|
|
31
80
|
|
|
81
|
+
const STARTER_ALT = CLAUSE_STARTERS.join('|');
|
|
82
|
+
|
|
83
|
+
/** ", and how ...", " and what ...", " or is ...", " as well as why ..." */
|
|
84
|
+
const CONJUNCTION_BOUNDARY = new RegExp(
|
|
85
|
+
`\\s*,?\\s+(?:and|or|as well as|and also|but)\\s+(?=(?:${STARTER_ALT})\\b)`,
|
|
86
|
+
'i',
|
|
87
|
+
);
|
|
88
|
+
|
|
32
89
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
90
|
+
* Explicit boundaries: "?" / "!" followed by more text, "." followed by a
|
|
91
|
+
* capitalised word (so "e.g. salt" stays whole), and ";".
|
|
92
|
+
*/
|
|
93
|
+
const PUNCTUATION_BOUNDARY = /(?<=[?!])\s+(?=\S)|(?<=\.)\s+(?=[A-Z])|\s*;\s*/;
|
|
94
|
+
/** Abbreviations whose period does not end a sentence. */
|
|
95
|
+
const ABBREVIATION_END =
|
|
96
|
+
/\b(?:e\.g|i\.e|etc|vs|cf|approx|mr|mrs|ms|dr|st|no)\.$/i;
|
|
97
|
+
|
|
98
|
+
/** "compare|contrast X and|with|to|vs Y [for|in terms of|... aspect]" */
|
|
99
|
+
const COMPARE_RE =
|
|
100
|
+
/\b(?:compare|contrast)\s+(.+?)\s+(?:and|with|to|against|vs\.?|versus)\s+(.+?)(?:\s+(?:for|in terms of|regarding|when|on)\s+(.+?))?\s*[?.!]?$/i;
|
|
101
|
+
/** "difference(s) between X and Y [for|in|... aspect]" */
|
|
102
|
+
const DIFFERENCE_RE =
|
|
103
|
+
/\bdifferences?\s+between\s+(.+?)\s+and\s+(.+?)(?:\s+(?:for|in terms of|regarding|when|in|on)\s+(.+?))?\s*[?.!]?$/i;
|
|
104
|
+
/** "X vs Y [for|... aspect]" */
|
|
105
|
+
const VERSUS_RE =
|
|
106
|
+
/^(.*?)\s+(?:vs\.?|versus)\s+(.+?)(?:\s+(?:for|in terms of|regarding|when|on)\s+(.+?))?\s*[?.!]?$/i;
|
|
107
|
+
|
|
108
|
+
/** Upper bound on one LLM-proposed sub-query, in characters. */
|
|
109
|
+
const MAX_LLM_SUBQUERY_LENGTH = 500;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Query decomposition planner. The default heuristic needs no LLM:
|
|
113
|
+
*
|
|
114
|
+
* - splits only at clause/question boundaries: "?" / "." followed by more
|
|
115
|
+
* text, ";", and "and" / "or" / "as well as" (optionally after a comma)
|
|
116
|
+
* when a question word, auxiliary verb or imperative follows;
|
|
117
|
+
* - keeps noun-phrase conjunctions intact ("salt and pepper", "A or B");
|
|
118
|
+
* - expands comparisons ("X vs Y", "compare X and Y", "difference between X
|
|
119
|
+
* and Y") into one sub-query per entity plus the comparison itself.
|
|
120
|
+
*
|
|
121
|
+
* `plan(query, { llm })` can instead ask an LLM for a JSON array of
|
|
122
|
+
* sub-queries (strictly validated, heuristic fallback).
|
|
36
123
|
*
|
|
37
124
|
* @extends EventEmitter
|
|
38
125
|
*/
|
|
39
126
|
class QueryPlanner extends EventEmitter {
|
|
40
127
|
/**
|
|
41
128
|
* @param {object} [options] - Override default configuration
|
|
42
|
-
* @param {number} [options.minComplexityToSplit=2] - Minimum complexity
|
|
129
|
+
* @param {number} [options.minComplexityToSplit=2] - Minimum complexity
|
|
130
|
+
* score before splitting at conjunctions; punctuation boundaries and
|
|
131
|
+
* comparisons are always split
|
|
43
132
|
* @param {number} [options.maxSubQueries=5] - Maximum number of sub-queries to produce
|
|
44
133
|
*/
|
|
45
134
|
constructor(options = {}) {
|
|
@@ -56,6 +145,8 @@ class QueryPlanner extends EventEmitter {
|
|
|
56
145
|
* +1 if length > 100 characters
|
|
57
146
|
* +1 per semicolon
|
|
58
147
|
* +1 per comma-separated clause (commas - 1 if > 0 commas)
|
|
148
|
+
* +1 per conjunction followed by a question word, auxiliary or
|
|
149
|
+
* imperative ("... and how ...", "... and explain ...")
|
|
59
150
|
*
|
|
60
151
|
* @param {string} query
|
|
61
152
|
* @returns {number}
|
|
@@ -66,12 +157,10 @@ class QueryPlanner extends EventEmitter {
|
|
|
66
157
|
const words = lower.split(/\s+/);
|
|
67
158
|
let score = 0;
|
|
68
159
|
|
|
69
|
-
// +1 per question word
|
|
70
160
|
for (const w of QUESTION_WORDS) {
|
|
71
161
|
if (words.includes(w)) score += 1;
|
|
72
162
|
}
|
|
73
163
|
|
|
74
|
-
// +1 per conjunction
|
|
75
164
|
for (const c of CONJUNCTIONS) {
|
|
76
165
|
// "as well as" is multi-word -- use indexOf on the full string
|
|
77
166
|
if (c.includes(' ')) {
|
|
@@ -81,17 +170,17 @@ class QueryPlanner extends EventEmitter {
|
|
|
81
170
|
}
|
|
82
171
|
}
|
|
83
172
|
|
|
84
|
-
// +1 if length > 100 chars
|
|
85
173
|
if (query.length > 100) score += 1;
|
|
86
174
|
|
|
87
|
-
// +1 per semicolon
|
|
88
175
|
const semicolons = (query.match(/;/g) || []).length;
|
|
89
176
|
score += semicolons;
|
|
90
177
|
|
|
91
|
-
// +1 per comma-separated clause (number of commas)
|
|
92
178
|
const commas = (query.match(/,/g) || []).length;
|
|
93
179
|
if (commas > 0) score += commas;
|
|
94
180
|
|
|
181
|
+
// +1 per conjunction that opens a new question or instruction
|
|
182
|
+
score += query.split(CONJUNCTION_BOUNDARY).length - 1;
|
|
183
|
+
|
|
95
184
|
return score;
|
|
96
185
|
}
|
|
97
186
|
|
|
@@ -104,14 +193,12 @@ class QueryPlanner extends EventEmitter {
|
|
|
104
193
|
*/
|
|
105
194
|
_detectStrategy(text) {
|
|
106
195
|
const lower = text.toLowerCase();
|
|
107
|
-
const words = lower.split(
|
|
196
|
+
const words = lower.split(/[^a-z]+/);
|
|
108
197
|
|
|
109
|
-
// Check comparative keywords first (more specific)
|
|
110
198
|
for (const kw of COMPARATIVE_KEYWORDS) {
|
|
111
|
-
if (words.includes(kw) ||
|
|
199
|
+
if (words.includes(kw) || words.includes(`${kw}s`)) return 'comparative';
|
|
112
200
|
}
|
|
113
201
|
|
|
114
|
-
// Check question words
|
|
115
202
|
for (const w of words) {
|
|
116
203
|
if (['who', 'what', 'where', 'when'].includes(w)) return 'factual';
|
|
117
204
|
if (['why', 'how'].includes(w)) return 'analytical';
|
|
@@ -121,85 +208,249 @@ class QueryPlanner extends EventEmitter {
|
|
|
121
208
|
}
|
|
122
209
|
|
|
123
210
|
/**
|
|
124
|
-
*
|
|
211
|
+
* Split a clause describing a comparison into its entities.
|
|
125
212
|
*
|
|
126
|
-
* @param {string}
|
|
127
|
-
* @returns {{
|
|
128
|
-
*
|
|
129
|
-
* complexity: number,
|
|
130
|
-
* originalQuery: string
|
|
131
|
-
* }}
|
|
132
|
-
* @throws {Error} If query is not a non-empty string
|
|
133
|
-
* @fires QueryPlanner#planned
|
|
213
|
+
* @param {string} clause
|
|
214
|
+
* @returns {{ entities: string[], aspect: string|null }|null}
|
|
215
|
+
* @private
|
|
134
216
|
*/
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
217
|
+
_parseComparison(clause) {
|
|
218
|
+
const text = clause.trim();
|
|
219
|
+
let match = text.match(COMPARE_RE) || text.match(DIFFERENCE_RE);
|
|
220
|
+
let left;
|
|
221
|
+
if (match) {
|
|
222
|
+
left = match[1];
|
|
223
|
+
} else {
|
|
224
|
+
match = text.match(VERSUS_RE);
|
|
225
|
+
if (!match) return null;
|
|
226
|
+
// "Which is faster, Postgres vs MySQL" -> "Postgres"
|
|
227
|
+
left = match[1].split(/[,:]/).pop();
|
|
228
|
+
left = left.replace(
|
|
229
|
+
/^\s*(?:(?:what|which|who)\s+(?:is|are)\s+\w+(?:\s+\w+)?\s*[:,-]?\s*)/i,
|
|
230
|
+
'',
|
|
231
|
+
);
|
|
139
232
|
}
|
|
233
|
+
const clean = (s) =>
|
|
234
|
+
String(s || '')
|
|
235
|
+
.trim()
|
|
236
|
+
.replace(/^(?:the|a|an)\s+/i, '')
|
|
237
|
+
.replace(/[?.!,;:]+$/, '')
|
|
238
|
+
.trim();
|
|
239
|
+
const entities = [...left.split(/\s*,\s*(?:and\s+)?|\s+and\s+/i), match[2]]
|
|
240
|
+
.map(clean)
|
|
241
|
+
.filter(Boolean);
|
|
242
|
+
if (entities.length < 2) return null;
|
|
243
|
+
const aspect = match[3] ? clean(match[3]) : null;
|
|
244
|
+
return { entities, aspect };
|
|
245
|
+
}
|
|
140
246
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Heuristic decomposition into sub-query strings.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} query - Trimmed query
|
|
251
|
+
* @param {number} complexity
|
|
252
|
+
* @returns {Array<{subQuery: string, strategy: string}>}
|
|
253
|
+
* @private
|
|
254
|
+
*/
|
|
255
|
+
_decompose(query, complexity) {
|
|
256
|
+
const tidy = (s) =>
|
|
257
|
+
s
|
|
258
|
+
.trim()
|
|
259
|
+
.replace(/^[,;]+\s*|[,;]+$/g, '')
|
|
260
|
+
.trim();
|
|
145
261
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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);
|
|
262
|
+
let clauses = [];
|
|
263
|
+
for (const piece of query.split(PUNCTUATION_BOUNDARY)) {
|
|
264
|
+
const prev = clauses[clauses.length - 1];
|
|
265
|
+
// "e.g. Google" / "Dr. Smith": an abbreviation, not a sentence end.
|
|
266
|
+
if (prev && ABBREVIATION_END.test(prev)) {
|
|
267
|
+
clauses[clauses.length - 1] = `${prev} ${piece}`;
|
|
268
|
+
} else {
|
|
269
|
+
clauses.push(piece);
|
|
173
270
|
}
|
|
174
|
-
|
|
271
|
+
}
|
|
272
|
+
clauses = clauses.map(tidy).filter(Boolean);
|
|
273
|
+
if (complexity >= this.config.minComplexityToSplit) {
|
|
274
|
+
clauses = clauses.flatMap((c) =>
|
|
275
|
+
c.split(CONJUNCTION_BOUNDARY).map(tidy).filter(Boolean),
|
|
276
|
+
);
|
|
175
277
|
}
|
|
176
278
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
279
|
+
const out = [];
|
|
280
|
+
for (const clause of clauses) {
|
|
281
|
+
const comparison = this._parseComparison(clause);
|
|
282
|
+
if (comparison) {
|
|
283
|
+
for (const entity of comparison.entities) {
|
|
284
|
+
out.push({
|
|
285
|
+
subQuery: comparison.aspect
|
|
286
|
+
? `${entity} ${comparison.aspect}`
|
|
287
|
+
: entity,
|
|
288
|
+
strategy: 'factual',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
out.push({ subQuery: clause, strategy: 'comparative' });
|
|
292
|
+
} else {
|
|
293
|
+
out.push({ subQuery: clause, strategy: this._detectStrategy(clause) });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
183
298
|
|
|
184
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Turn sub-query texts into the plan result (dedupe, limit, priorities).
|
|
301
|
+
*
|
|
302
|
+
* @param {Array<{subQuery: string, strategy: string}>} items
|
|
303
|
+
* @param {number} complexity
|
|
304
|
+
* @param {string} originalQuery
|
|
305
|
+
* @param {'heuristic'|'llm'} source
|
|
306
|
+
* @returns {object}
|
|
307
|
+
* @private
|
|
308
|
+
*/
|
|
309
|
+
_finalize(items, complexity, originalQuery, source) {
|
|
185
310
|
const seen = new Set();
|
|
186
|
-
|
|
311
|
+
const unique = items.filter((sq) => {
|
|
187
312
|
const key = sq.subQuery.toLowerCase();
|
|
188
313
|
if (seen.has(key)) return false;
|
|
189
314
|
seen.add(key);
|
|
190
315
|
return true;
|
|
191
316
|
});
|
|
317
|
+
const subQueries = unique
|
|
318
|
+
.slice(0, this.config.maxSubQueries)
|
|
319
|
+
.map((sq, i) => ({
|
|
320
|
+
subQuery: sq.subQuery,
|
|
321
|
+
strategy: sq.strategy,
|
|
322
|
+
priority: i,
|
|
323
|
+
}));
|
|
324
|
+
return { subQueries, complexity, originalQuery, source };
|
|
325
|
+
}
|
|
192
326
|
|
|
193
|
-
|
|
194
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Heuristic plan for a trimmed query.
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
_heuristicPlan(trimmed) {
|
|
332
|
+
const complexity = this._computeComplexity(trimmed);
|
|
333
|
+
const items = this._decompose(trimmed, complexity);
|
|
334
|
+
if (items.length === 0) {
|
|
335
|
+
items.push({
|
|
336
|
+
subQuery: trimmed,
|
|
337
|
+
strategy: this._detectStrategy(trimmed),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return this._finalize(items, complexity, trimmed, 'heuristic');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Strictly validate an LLM's sub-query proposal: a JSON array (optionally
|
|
345
|
+
* in a ```json fence) of 1..maxSubQueries non-empty, single-line strings of
|
|
346
|
+
* at most 500 characters. Anything else is rejected.
|
|
347
|
+
*
|
|
348
|
+
* @param {*} raw - Value returned by llm.generate()
|
|
349
|
+
* @returns {string[]|null}
|
|
350
|
+
* @private
|
|
351
|
+
*/
|
|
352
|
+
_parseLlmSubQueries(raw) {
|
|
353
|
+
let text = raw;
|
|
354
|
+
if (text && typeof text === 'object') {
|
|
355
|
+
text = text.text ?? text.content ?? text.answer;
|
|
356
|
+
}
|
|
357
|
+
if (typeof text !== 'string') return null;
|
|
358
|
+
text = text.trim();
|
|
359
|
+
const fenced = text.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
360
|
+
if (fenced) text = fenced[1].trim();
|
|
361
|
+
if (!text.startsWith('[') || !text.endsWith(']')) return null;
|
|
362
|
+
|
|
363
|
+
let parsed;
|
|
364
|
+
try {
|
|
365
|
+
parsed = JSON.parse(text);
|
|
366
|
+
} catch (_err) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (!Array.isArray(parsed)) return null;
|
|
370
|
+
if (parsed.length === 0 || parsed.length > this.config.maxSubQueries) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const out = [];
|
|
374
|
+
for (const item of parsed) {
|
|
375
|
+
if (typeof item !== 'string') return null;
|
|
376
|
+
const s = item.trim();
|
|
377
|
+
if (!s || s.length > MAX_LLM_SUBQUERY_LENGTH || /[\r\n]/.test(s)) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
out.push(s);
|
|
381
|
+
}
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Ask the LLM for sub-queries; fall back to the heuristic plan.
|
|
387
|
+
* @private
|
|
388
|
+
*/
|
|
389
|
+
async _planWithLlm(trimmed, { llm, signal }) {
|
|
390
|
+
const heuristic = this._heuristicPlan(trimmed);
|
|
391
|
+
let result = heuristic;
|
|
392
|
+
try {
|
|
393
|
+
const prompt =
|
|
394
|
+
'Split the user question into independent search queries for a ' +
|
|
395
|
+
'retrieval system. Keep phrases such as "salt and pepper" intact; ' +
|
|
396
|
+
'for a comparison, give one query per compared item plus the ' +
|
|
397
|
+
`comparison. Reply with ONLY a JSON array of 1 to ${this.config.maxSubQueries} ` +
|
|
398
|
+
`strings.\n\nQuestion: ${trimmed}`;
|
|
399
|
+
const raw = await llm.generate(prompt, [], signal ? { signal } : {});
|
|
400
|
+
const proposed = this._parseLlmSubQueries(raw);
|
|
401
|
+
if (proposed) {
|
|
402
|
+
result = this._finalize(
|
|
403
|
+
proposed.map((s) => ({
|
|
404
|
+
subQuery: s,
|
|
405
|
+
strategy: this._detectStrategy(s),
|
|
406
|
+
})),
|
|
407
|
+
heuristic.complexity,
|
|
408
|
+
trimmed,
|
|
409
|
+
'llm',
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
} catch (err) {
|
|
413
|
+
// A cancelled run must not continue with the heuristic plan.
|
|
414
|
+
if (signal && signal.aborted) throw err;
|
|
415
|
+
}
|
|
416
|
+
this.emit('planned', result);
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Plan query decomposition.
|
|
422
|
+
*
|
|
423
|
+
* Without `options.llm` this is synchronous and returns the plan. With
|
|
424
|
+
* `options.llm` (an object with `generate()`), it returns a Promise of the
|
|
425
|
+
* plan: the LLM is asked for a JSON array of sub-queries, which is strictly
|
|
426
|
+
* validated (see _parseLlmSubQueries); on any failure the heuristic plan is
|
|
427
|
+
* used. `result.source` says which one was used.
|
|
428
|
+
*
|
|
429
|
+
* @param {string} query - The user query to decompose
|
|
430
|
+
* @param {object} [options]
|
|
431
|
+
* @param {{ generate: Function }} [options.llm] - Optional LLM planner
|
|
432
|
+
* @param {AbortSignal} [options.signal] - Passed to llm.generate()
|
|
433
|
+
* @returns {{
|
|
434
|
+
* subQueries: Array<{subQuery: string, strategy: string, priority: number}>,
|
|
435
|
+
* complexity: number,
|
|
436
|
+
* originalQuery: string,
|
|
437
|
+
* source: 'heuristic'|'llm'
|
|
438
|
+
* }|Promise<object>}
|
|
439
|
+
* @throws {Error} If query is not a non-empty string
|
|
440
|
+
* @fires QueryPlanner#planned
|
|
441
|
+
*/
|
|
442
|
+
plan(query, options = {}) {
|
|
443
|
+
if (!query || typeof query !== 'string' || !query.trim()) {
|
|
444
|
+
throw new Error('QueryPlanner.plan(): query must be a non-empty string');
|
|
445
|
+
}
|
|
446
|
+
const trimmed = query.trim();
|
|
195
447
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
originalQuery: trimmed,
|
|
201
|
-
};
|
|
448
|
+
const llm = options && options.llm;
|
|
449
|
+
if (llm && typeof llm.generate === 'function') {
|
|
450
|
+
return this._planWithLlm(trimmed, options);
|
|
451
|
+
}
|
|
202
452
|
|
|
453
|
+
const result = this._heuristicPlan(trimmed);
|
|
203
454
|
this.emit('planned', result);
|
|
204
455
|
return result;
|
|
205
456
|
}
|
|
@@ -10,7 +10,7 @@ const { detectHallucinations } = require('./hallucination-detector');
|
|
|
10
10
|
* @type {object}
|
|
11
11
|
*/
|
|
12
12
|
const DEFAULT_CONFIG = {
|
|
13
|
-
similarityThreshold: 0.
|
|
13
|
+
similarityThreshold: 0.5,
|
|
14
14
|
maxCitationsPerSentence: 3,
|
|
15
15
|
useIDFWeighting: true,
|
|
16
16
|
};
|
|
@@ -24,7 +24,7 @@ const DEFAULT_CONFIG = {
|
|
|
24
24
|
class CitationTracker extends EventEmitter {
|
|
25
25
|
/**
|
|
26
26
|
* @param {object} [options] - Override default configuration
|
|
27
|
-
* @param {number} [options.similarityThreshold=0.
|
|
27
|
+
* @param {number} [options.similarityThreshold=0.5] - Minimum similarity for a citation match
|
|
28
28
|
* @param {number} [options.maxCitationsPerSentence=3] - Max citations per sentence
|
|
29
29
|
* @param {boolean} [options.useIDFWeighting=true] - Whether to apply IDF weighting
|
|
30
30
|
*/
|
|
@@ -76,9 +76,11 @@ class CitationTracker extends EventEmitter {
|
|
|
76
76
|
'CitationTracker.track(): answer must be a non-empty string',
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
// An empty array is valid (retrieval found nothing): every sentence is
|
|
80
|
+
// then reported as unsupported rather than failing the whole run.
|
|
81
|
+
if (!Array.isArray(retrievedDocs)) {
|
|
80
82
|
throw new Error(
|
|
81
|
-
'CitationTracker.track(): retrievedDocs must be
|
|
83
|
+
'CitationTracker.track(): retrievedDocs must be an array',
|
|
82
84
|
);
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @param {Array<{sentence: string, sources: Array<{docId: string|undefined, docIndex: number, score: number}>}>} citationResults
|
|
12
12
|
* @param {object} [options]
|
|
13
|
-
* @param {number} [options.threshold=0.
|
|
13
|
+
* @param {number} [options.threshold=0.5] - Score threshold for grounded classification
|
|
14
14
|
* @returns {{
|
|
15
15
|
* hallucinationRate: number,
|
|
16
16
|
* sentences: Array<{text: string, classification: string, bestScore: number, bestSource: object|null}>,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* }}
|
|
19
19
|
*/
|
|
20
20
|
function detectHallucinations(citationResults, options = {}) {
|
|
21
|
-
const { threshold = 0.
|
|
21
|
+
const { threshold = 0.5 } = options;
|
|
22
22
|
|
|
23
23
|
if (!Array.isArray(citationResults) || citationResults.length === 0) {
|
|
24
24
|
return {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { tokenize,
|
|
3
|
+
const { tokenize, computeSupportScore } = require('../evaluate/scoring');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Build IDF (Inverse Document Frequency) weights from a collection of documents.
|
|
7
|
-
* IDF = log(totalDocs / docsContainingToken)
|
|
7
|
+
* IDF = log(1 + totalDocs / docsContainingToken). The +1 keeps weights
|
|
8
|
+
* positive, so tokens found in every document (and every token when only one
|
|
9
|
+
* document was retrieved) still count instead of weighing zero.
|
|
8
10
|
* @param {Array<{content?: string, text?: string}>} docs - Array of document objects
|
|
9
11
|
* @returns {Map<string, number>} Map of token to IDF weight
|
|
10
12
|
*/
|
|
@@ -24,24 +26,25 @@ function buildIDFWeights(docs) {
|
|
|
24
26
|
|
|
25
27
|
const idfWeights = new Map();
|
|
26
28
|
for (const [token, df] of docFrequency) {
|
|
27
|
-
idfWeights.set(token, Math.log(totalDocs / df));
|
|
29
|
+
idfWeights.set(token, Math.log(1 + totalDocs / df));
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
return idfWeights;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
|
-
* Map a sentence to its most relevant source documents
|
|
36
|
+
* Map a sentence to its most relevant source documents by how much of the
|
|
37
|
+
* sentence each document supports (see computeSupportScore).
|
|
35
38
|
* @param {string} sentence - The sentence to map
|
|
36
39
|
* @param {Array<{content?: string, text?: string, id?: string}>} docs - Source documents
|
|
37
40
|
* @param {object} [options] - Configuration options
|
|
38
41
|
* @param {number} [options.maxCitations=3] - Maximum number of citations to return
|
|
39
|
-
* @param {number} [options.threshold=0.
|
|
42
|
+
* @param {number} [options.threshold=0.5] - Minimum similarity threshold
|
|
40
43
|
* @param {Map<string, number>|null} [options.idfWeights=null] - Optional IDF weights for weighted similarity
|
|
41
44
|
* @returns {Array<{docIndex: number, docId: string|undefined, score: number, matchedTokens: string[]}>}
|
|
42
45
|
*/
|
|
43
46
|
function mapSentenceToSources(sentence, docs, options = {}) {
|
|
44
|
-
const { maxCitations = 3, threshold = 0.
|
|
47
|
+
const { maxCitations = 3, threshold = 0.5, idfWeights = null } = options;
|
|
45
48
|
|
|
46
49
|
const sentenceTokens = tokenize(sentence);
|
|
47
50
|
if (!sentenceTokens.length) return [];
|
|
@@ -54,29 +57,13 @@ function mapSentenceToSources(sentence, docs, options = {}) {
|
|
|
54
57
|
const docTokens = tokenize(docText);
|
|
55
58
|
if (!docTokens.length) continue;
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
const sentenceSet = new Set(sentenceTokens);
|
|
60
|
+
// Share of the sentence's content tokens found in this document.
|
|
59
61
|
const docSet = new Set(docTokens);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let intersectionWeight = 0;
|
|
66
|
-
let unionWeight = 0;
|
|
67
|
-
|
|
68
|
-
for (const token of union) {
|
|
69
|
-
const weight = idfWeights.get(token) || 1;
|
|
70
|
-
if (sentenceSet.has(token) && docSet.has(token)) {
|
|
71
|
-
intersectionWeight += weight;
|
|
72
|
-
}
|
|
73
|
-
unionWeight += weight;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
score = unionWeight > 0 ? intersectionWeight / unionWeight : 0;
|
|
77
|
-
} else {
|
|
78
|
-
score = computeJaccardSimilarity(sentenceTokens, docTokens);
|
|
79
|
-
}
|
|
62
|
+
const score = computeSupportScore(sentenceTokens, docSet, idfWeights, {
|
|
63
|
+
claimText: sentence,
|
|
64
|
+
docText,
|
|
65
|
+
});
|
|
66
|
+
const matched = [...new Set(sentenceTokens)].filter((t) => docSet.has(t));
|
|
80
67
|
|
|
81
68
|
if (score >= threshold) {
|
|
82
69
|
results.push({
|