@devilsdev/rag-pipeline-utils 2.4.5 → 2.6.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 (258) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +431 -0
  3. package/README.md +418 -117
  4. package/SECURITY.md +5 -5
  5. package/dist/experimental.cjs +2 -0
  6. package/dist/experimental.d.ts +1 -0
  7. package/dist/experimental.mjs +5 -0
  8. package/dist/index.cjs +6442 -1355
  9. package/dist/index.d.ts +129 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/testing.cjs +2 -0
  12. package/dist/testing.d.ts +1 -0
  13. package/dist/testing.mjs +5 -0
  14. package/dist/types/adapters/ai-sdk.d.ts +53 -0
  15. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  16. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  17. package/dist/types/agentic/query-planner.d.ts +145 -0
  18. package/dist/types/agentic/self-critique.d.ts +45 -0
  19. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  20. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  21. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  22. package/dist/types/ai/multimodal.d.ts +39 -0
  23. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  24. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  25. package/dist/types/cache/component-keys.d.ts +7 -0
  26. package/dist/types/cache/memory-cache.d.ts +54 -0
  27. package/dist/types/cache/pipeline-cache.d.ts +66 -0
  28. package/dist/types/cache/redis-cache.d.ts +46 -0
  29. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  30. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  31. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  32. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  33. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  34. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  35. package/dist/types/citation/citation-tracker.d.ts +64 -0
  36. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  37. package/dist/types/citation/source-mapper.d.ts +37 -0
  38. package/dist/types/connectors/anthropic-connector.d.ts +97 -0
  39. package/dist/types/connectors/base-connector.d.ts +87 -0
  40. package/dist/types/connectors/cohere-connector.d.ts +94 -0
  41. package/dist/types/connectors/connector-utils.d.ts +110 -0
  42. package/dist/types/connectors/local-embedder.d.ts +95 -0
  43. package/dist/types/connectors/memory-retriever.d.ts +99 -0
  44. package/dist/types/connectors/ollama-connector.d.ts +115 -0
  45. package/dist/types/connectors/openai-connector.d.ts +129 -0
  46. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  47. package/dist/types/core/create-pipeline.d.ts +8 -0
  48. package/dist/types/core/plugin-registry.d.ts +136 -0
  49. package/dist/types/core/stage-contracts.d.ts +36 -0
  50. package/dist/types/core/validate-plugin.d.ts +45 -0
  51. package/dist/types/cost/cost-calculator.d.ts +152 -0
  52. package/dist/types/cost/cost-tracker.d.ts +126 -0
  53. package/dist/types/cost/token-budget.d.ts +110 -0
  54. package/dist/types/dag/core/error-context.d.ts +239 -0
  55. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  56. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  57. package/dist/types/dag/dag-engine.d.ts +156 -0
  58. package/dist/types/dag/dag-node.d.ts +37 -0
  59. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  60. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  61. package/dist/types/debug/execution-tracer.d.ts +57 -0
  62. package/dist/types/debug/trace-inspector.d.ts +31 -0
  63. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  64. package/dist/types/enterprise/data-governance.d.ts +203 -0
  65. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  66. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  67. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  68. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  69. package/dist/types/evaluate/groundedness.d.ts +60 -0
  70. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  71. package/dist/types/evaluate/relevance.d.ts +21 -0
  72. package/dist/types/evaluate/scoring.d.ts +123 -0
  73. package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
  74. package/dist/types/experimental.d.ts +15 -0
  75. package/dist/types/graph/entity-extractor.d.ts +84 -0
  76. package/dist/types/graph/graph-index.d.ts +49 -0
  77. package/dist/types/graph/graph-retriever.d.ts +81 -0
  78. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  79. package/dist/types/grounding/check-answer.d.ts +101 -0
  80. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  81. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  82. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  83. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  84. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  85. package/dist/types/mcp/mcp-server.d.ts +85 -0
  86. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  87. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  88. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  89. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  90. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  91. package/dist/types/retrieval/file-vector-store.d.ts +99 -0
  92. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  93. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  94. package/dist/types/security/jwt-config.d.ts +80 -0
  95. package/dist/types/security/jwt-validator.d.ts +195 -0
  96. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  97. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  98. package/dist/types/security/validator.d.ts +5 -0
  99. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  100. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  101. package/dist/types/streaming/stream-router.d.ts +88 -0
  102. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  103. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  104. package/dist/types/templates/scaffold.d.ts +55 -0
  105. package/dist/types/templates/template-registry.d.ts +63 -0
  106. package/dist/types/testing.d.ts +168 -0
  107. package/dist/types/utils/embed-query.d.ts +20 -0
  108. package/dist/types/utils/logger.d.ts +50 -0
  109. package/dist/types/utils/rate-limiter.d.ts +66 -0
  110. package/dist/types/utils/retry.d.ts +27 -0
  111. package/dist/types/utils/run-abort.d.ts +34 -0
  112. package/dist/types/utils/secure-logging.d.ts +51 -0
  113. package/dist/types/utils/structured-logger.d.ts +126 -0
  114. package/package.json +72 -33
  115. package/src/adapters/ai-sdk.js +215 -0
  116. package/src/agentic/agentic-pipeline.js +75 -7
  117. package/src/agentic/iterative-retriever.js +47 -10
  118. package/src/agentic/query-planner.js +329 -78
  119. package/src/cache/component-keys.js +38 -0
  120. package/src/cache/memory-cache.js +87 -0
  121. package/src/cache/pipeline-cache.js +260 -0
  122. package/src/cache/redis-cache.js +83 -0
  123. package/src/citation/citation-tracker.js +6 -4
  124. package/src/citation/hallucination-detector.js +2 -2
  125. package/src/citation/source-mapper.js +15 -28
  126. package/src/cli/commands/ai.js +106 -0
  127. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  128. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  129. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  130. package/src/cli/commands/plugin-hub.js +114 -49
  131. package/src/cli/diagnostics/config-checker.js +76 -63
  132. package/src/cli/diagnostics/environment-checker.js +62 -31
  133. package/src/cli/diagnostics/plugin-checker.js +36 -89
  134. package/src/cli/doctor-command.js +39 -5
  135. package/src/cli/enhanced-cli-commands.js +104 -233
  136. package/src/cli/handlers/ai-handler.js +364 -0
  137. package/src/cli/handlers/diagnostics-handler.js +78 -31
  138. package/src/cli/handlers/ingest-handler.js +28 -22
  139. package/src/cli/handlers/init-handler.js +148 -25
  140. package/src/cli/handlers/query-handler.js +203 -25
  141. package/src/cli/handlers/run-handler.js +16 -167
  142. package/src/cli/handlers/serve-handler.js +60 -0
  143. package/src/cli/services/bench-stats.js +54 -0
  144. package/src/cli/services/benchmark.js +161 -0
  145. package/src/cli/services/feedback.js +155 -0
  146. package/src/cli/services/instrument.js +83 -0
  147. package/src/cli/services/rag-service.js +361 -0
  148. package/src/cli/shared/cli-utils.js +24 -7
  149. package/src/cli/shared/config-context.js +21 -0
  150. package/src/cli/wizard/config-builder.js +81 -0
  151. package/src/cli/wizard/init-wizard.js +602 -0
  152. package/src/config/component-factory.js +125 -0
  153. package/src/config/enhanced-ragrc-schema.js +0 -2
  154. package/src/config/load-config.js +29 -10
  155. package/src/config/load-plugin-config.js +46 -2
  156. package/src/config/validate-schema.js +4 -1
  157. package/src/connectors/anthropic-connector.js +115 -57
  158. package/src/connectors/base-connector.js +74 -19
  159. package/src/connectors/cohere-connector.js +108 -34
  160. package/src/connectors/connector-utils.js +302 -0
  161. package/src/connectors/local-embedder.js +63 -5
  162. package/src/connectors/memory-retriever.js +125 -14
  163. package/src/connectors/ollama-connector.js +112 -51
  164. package/src/connectors/openai-connector.js +153 -44
  165. package/src/connectors/rag-prompt.js +88 -0
  166. package/src/core/create-pipeline.js +307 -61
  167. package/src/core/performance/parallel-processor.js +62 -1
  168. package/src/core/plugin-registry.js +125 -26
  169. package/src/core/stage-contracts.js +36 -0
  170. package/src/core/validate-plugin.js +85 -0
  171. package/src/cost/cost-calculator.js +205 -32
  172. package/src/debug/execution-tracer.js +14 -4
  173. package/src/dev/dev-server.js +173 -11
  174. package/src/dev/hot-reload.js +44 -4
  175. package/src/ecosystem/plugin-checks.js +181 -0
  176. package/src/ecosystem/plugin-hub.js +50 -125
  177. package/src/ecosystem/plugin-installer.js +298 -0
  178. package/src/ecosystem/plugin-probe.js +127 -0
  179. package/src/ecosystem/plugin-scanner.js +185 -0
  180. package/src/ecosystem/tar-extract.js +114 -0
  181. package/src/enterprise/audit-logging.js +158 -29
  182. package/src/enterprise/sso-integration.js +716 -228
  183. package/src/evaluate/context-metrics.js +28 -16
  184. package/src/evaluate/evaluator.js +3 -3
  185. package/src/evaluate/faithfulness.js +18 -14
  186. package/src/evaluate/groundedness.js +43 -12
  187. package/src/evaluate/pipeline-evaluator.js +35 -8
  188. package/src/evaluate/scoring.js +251 -3
  189. package/src/evaluate/semantic-similarity.js +79 -0
  190. package/src/experimental.js +71 -0
  191. package/src/grounding/check-answer.js +173 -0
  192. package/src/guardrails/guardrails-pipeline.js +69 -7
  193. package/src/guardrails/injection-detector.js +283 -0
  194. package/src/guardrails/post-generation-guard.js +19 -0
  195. package/src/guardrails/pre-retrieval-guard.js +36 -36
  196. package/src/guardrails/retrieval-guard.js +58 -8
  197. package/src/index.js +58 -1
  198. package/src/loader/csv-loader.js +3 -2
  199. package/src/loader/document-reader.js +91 -0
  200. package/src/loader/html-loader.js +3 -1
  201. package/src/loader/markdown-loader.js +3 -2
  202. package/src/reranker/cascade-reranker.js +3 -0
  203. package/src/reranker/embedding-reranker.js +136 -21
  204. package/src/reranker/llm-reranker.js +79 -12
  205. package/src/reranker/scoring-reranker.js +3 -0
  206. package/src/retrieval/bm25-search.js +12 -6
  207. package/src/retrieval/file-vector-store.js +241 -0
  208. package/src/retrieval/hybrid-retriever.js +22 -19
  209. package/src/security/jwt-config.js +13 -2
  210. package/src/security/jwt-validator.js +139 -34
  211. package/src/security/plugin-sandbox.js +18 -1
  212. package/src/security/plugin-signature-verifier.js +245 -70
  213. package/src/testing.js +254 -0
  214. package/src/utils/deprecate.js +69 -0
  215. package/src/utils/embed-query.js +33 -0
  216. package/src/utils/input-sanitizer.js +19 -4
  217. package/src/utils/logger.js +34 -4
  218. package/src/utils/run-abort.js +102 -0
  219. package/src/ai/federated-learning.js +0 -211
  220. package/src/ai/federation.js +0 -71
  221. package/src/ai/index.js +0 -22
  222. package/src/ai/model-trainer.js +0 -217
  223. package/src/ai/model-training.js +0 -202
  224. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  225. package/src/ai/orchestrator.js +0 -65
  226. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  227. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  228. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  229. package/src/cli/commands/ai-ml/index.js +0 -17
  230. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  231. package/src/cli/commands/ai-ml/shared.js +0 -51
  232. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  233. package/src/cli/commands/ai-ml.js +0 -386
  234. package/src/cli/commands/docs.js +0 -228
  235. package/src/cli/commands/dx.js +0 -538
  236. package/src/cli/commands/wizard.js +0 -627
  237. package/src/cli/input-sanitization-middleware.js +0 -356
  238. package/src/cli/interactive-wizard.js +0 -373
  239. package/src/cli/templates/development.template.json +0 -64
  240. package/src/cli/templates/minimal.template.json +0 -33
  241. package/src/cli/templates/production.template.json +0 -104
  242. package/src/cli/templates/testing.template.json +0 -45
  243. package/src/cli/wizard/configuration.js +0 -148
  244. package/src/cli/wizard/observability.js +0 -79
  245. package/src/cli/wizard/performance.js +0 -76
  246. package/src/cli/wizard/plugin-selection.js +0 -297
  247. package/src/cli/wizard/preview-save.js +0 -172
  248. package/src/cli/wizard/project-setup.js +0 -66
  249. package/src/core/pipeline-factory.js +0 -59
  250. package/src/dag/enhanced-dag-engine.js +0 -894
  251. package/src/dx/index.js +0 -40
  252. package/src/dx/integration-templates.js +0 -662
  253. package/src/dx/performance-profiler.js +0 -968
  254. package/src/dx/realtime-debugger.js +0 -755
  255. package/src/dx/visual-pipeline-builder.js +0 -578
  256. package/src/ingest.js +0 -59
  257. package/src/pipeline/pipeline.js +0 -18
  258. 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 splitting compound queries */
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
- * Heuristic query decomposition planner -- no LLM needed.
34
- * Splits compound queries into sub-queries with assigned strategies
35
- * based on detected question types.
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 score before splitting
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(/\s+/);
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) || lower.includes(kw)) return 'comparative';
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
- * Plan query decomposition.
211
+ * Split a clause describing a comparison into its entities.
125
212
  *
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
213
+ * @param {string} clause
214
+ * @returns {{ entities: string[], aspect: string|null }|null}
215
+ * @private
134
216
  */
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');
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
- const trimmed = query.trim();
142
-
143
- // 2. Compute complexity
144
- const complexity = this._computeComplexity(trimmed);
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
- // 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);
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
- parts = newParts;
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
- // 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
- }));
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
- // 5. Deduplicate by subQuery text (case-insensitive)
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
- subQueries = subQueries.filter((sq) => {
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
- // Limit to maxSubQueries
194
- subQueries = subQueries.slice(0, this.config.maxSubQueries);
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
- // 6. Emit 'planned' event
197
- const result = {
198
- subQueries,
199
- complexity,
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
  }
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Builds the cache identity a built-in component reports through
5
+ * getCacheKey(role): every setting that changes its output, so two
6
+ * configurations never share cached results.
7
+ */
8
+
9
+ const crypto = require('crypto');
10
+
11
+ const digest = (value) =>
12
+ crypto
13
+ .createHash('sha256')
14
+ .update(String(value ?? ''))
15
+ .digest('hex')
16
+ .slice(0, 16);
17
+
18
+ /**
19
+ * @param {string} kind - e.g. 'openai'
20
+ * @param {Object} fields - Settings that change the output
21
+ * @returns {string}
22
+ */
23
+ function componentCacheKey(kind, fields) {
24
+ const parts = Object.keys(fields)
25
+ .sort()
26
+ .map((name) => {
27
+ const value = fields[name];
28
+ // Long free text (system prompts) is hashed to keep keys short.
29
+ const text =
30
+ typeof value === 'string' && value.length > 64
31
+ ? `#${digest(value)}`
32
+ : value;
33
+ return `${name}=${text === undefined ? '' : text}`;
34
+ });
35
+ return `${kind}|${parts.join('|')}`;
36
+ }
37
+
38
+ module.exports = { componentCacheKey, digest };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * In-process cache with least-recently-used eviction and per-entry time to
5
+ * live. Implements the CacheBackend contract:
6
+ *
7
+ * get(key) -> Promise<value | undefined>
8
+ * set(key, value, { ttlSeconds }) -> Promise<void>
9
+ * delete(key) -> Promise<void>
10
+ * stats() -> Promise<{ hits, misses, size }>
11
+ *
12
+ * Values are stored as JSON copies, so later changes to a returned value do
13
+ * not change the cache (the same behaviour as RedisCache).
14
+ */
15
+ class MemoryCache {
16
+ /**
17
+ * @param {Object} [options]
18
+ * @param {number} [options.maxEntries=1000] - Entries kept before the least
19
+ * recently used is evicted
20
+ * @param {number} [options.ttlSeconds] - Default time to live; none means
21
+ * entries never expire on their own
22
+ */
23
+ constructor({ maxEntries = 1000, ttlSeconds } = {}) {
24
+ if (!Number.isInteger(maxEntries) || maxEntries < 1) {
25
+ throw new Error('maxEntries must be a positive integer');
26
+ }
27
+ this.maxEntries = maxEntries;
28
+ this.ttlSeconds = ttlSeconds;
29
+ this._entries = new Map(); // insertion order = recency order
30
+ this._hits = 0;
31
+ this._misses = 0;
32
+ }
33
+
34
+ /**
35
+ * @param {string} key
36
+ * @returns {Promise<unknown|undefined>}
37
+ */
38
+ async get(key) {
39
+ const entry = this._entries.get(key);
40
+ if (!entry || (entry.expiresAt !== null && entry.expiresAt <= Date.now())) {
41
+ if (entry) this._entries.delete(key);
42
+ this._misses += 1;
43
+ return undefined;
44
+ }
45
+ // Move to the most recently used position.
46
+ this._entries.delete(key);
47
+ this._entries.set(key, entry);
48
+ this._hits += 1;
49
+ return JSON.parse(entry.json);
50
+ }
51
+
52
+ /**
53
+ * @param {string} key
54
+ * @param {unknown} value - JSON-serialisable value
55
+ * @param {{ ttlSeconds?: number }} [options]
56
+ * @returns {Promise<void>}
57
+ */
58
+ async set(key, value, { ttlSeconds = this.ttlSeconds } = {}) {
59
+ const json = JSON.stringify(value);
60
+ if (json === undefined) return;
61
+ this._entries.delete(key);
62
+ this._entries.set(key, {
63
+ json,
64
+ expiresAt: ttlSeconds > 0 ? Date.now() + ttlSeconds * 1000 : null,
65
+ });
66
+ while (this._entries.size > this.maxEntries) {
67
+ this._entries.delete(this._entries.keys().next().value);
68
+ }
69
+ }
70
+
71
+ /** @param {string} key */
72
+ async delete(key) {
73
+ this._entries.delete(key);
74
+ }
75
+
76
+ /** Remove every entry. */
77
+ async clear() {
78
+ this._entries.clear();
79
+ }
80
+
81
+ /** @returns {Promise<{ hits: number, misses: number, size: number }>} */
82
+ async stats() {
83
+ return { hits: this._hits, misses: this._misses, size: this._entries.size };
84
+ }
85
+ }
86
+
87
+ module.exports = { MemoryCache };