@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
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+
3
+ const path = require("path");
4
+ const pluginRegistry = require("../core/plugin-registry.js");
5
+
6
+ /**
7
+ * Built-in components a .ragrc.json pipeline entry can name. Each factory
8
+ * receives the entry's `config` object and the directory of the config file.
9
+ * Requires are lazy so naming one provider does not load the others.
10
+ */
11
+ const BUILT_INS = {
12
+ // "auto" picks a loader per file extension (see loader/document-reader).
13
+ loader: {
14
+ auto: () => require("../loader/document-reader"),
15
+ },
16
+ embedder: {
17
+ local: (cfg) =>
18
+ new (require("../connectors/local-embedder").LocalEmbedder)(cfg),
19
+ openai: (cfg) =>
20
+ new (require("../connectors/openai-connector").OpenAIConnector)(cfg),
21
+ ollama: (cfg) =>
22
+ new (require("../connectors/ollama-connector").OllamaConnector)(cfg),
23
+ cohere: (cfg) =>
24
+ new (require("../connectors/cohere-connector").CohereConnector)(cfg),
25
+ },
26
+ retriever: {
27
+ "file-store": (cfg, configDir) =>
28
+ new (require("../retrieval/file-vector-store").FileVectorStore)({
29
+ path: path.resolve(configDir, cfg.path || ".rag/index.json"),
30
+ }),
31
+ memory: (cfg) =>
32
+ new (require("../connectors/memory-retriever").MemoryRetriever)(cfg),
33
+ },
34
+ llm: {
35
+ openai: (cfg) =>
36
+ new (require("../connectors/openai-connector").OpenAIConnector)(cfg),
37
+ anthropic: (cfg) =>
38
+ new (require("../connectors/anthropic-connector").AnthropicConnector)(
39
+ cfg,
40
+ ),
41
+ ollama: (cfg) =>
42
+ new (require("../connectors/ollama-connector").OllamaConnector)(cfg),
43
+ },
44
+ reranker: {
45
+ scoring: (cfg) =>
46
+ new (require("../reranker/scoring-reranker").ScoringReranker)(cfg),
47
+ },
48
+ };
49
+
50
+ const STAGES = ["loader", "embedder", "retriever", "reranker", "llm"];
51
+
52
+ /**
53
+ * Instantiate a custom plugin from `plugins.<stage>.<name>` in the config.
54
+ * The module may export a class (constructed with the entry config), an
55
+ * instance, or either of those as `default`.
56
+ */
57
+ function loadPluginModule(modulePath, entryConfig, configDir) {
58
+ const resolved = path.resolve(configDir, modulePath);
59
+ const mod = require(resolved);
60
+ const exported = mod && mod.default ? mod.default : mod;
61
+ if (typeof exported === "function") {
62
+ return /^class\s/.test(Function.prototype.toString.call(exported))
63
+ ? new exported(entryConfig)
64
+ : exported(entryConfig);
65
+ }
66
+ if (exported && typeof exported === "object") return exported;
67
+ throw new Error(`Plugin module ${modulePath} must export a class or object`);
68
+ }
69
+
70
+ /**
71
+ * Turn a normalized .ragrc.json config into component instances.
72
+ *
73
+ * Each `pipeline` entry `{ stage, name, config }` is resolved in order:
74
+ * 1. a built-in name (see BUILT_INS),
75
+ * 2. a custom module path under `plugins.<stage>.<name>`,
76
+ * 3. a plugin registered on the shared pluginRegistry.
77
+ *
78
+ * @param {object} config - Output of loadRagConfig()/validateRagrc().normalized
79
+ * @param {object} [options]
80
+ * @param {string} [options.configDir=process.cwd()] - Paths resolve relative to this
81
+ * @returns {{embedder?: object, retriever?: object, llm?: object, reranker?: object, loader?: object, names: object}}
82
+ */
83
+ function buildComponents(config, options = {}) {
84
+ const configDir = options.configDir || process.cwd();
85
+ const components = { names: {} };
86
+
87
+ for (const entry of config.pipeline || []) {
88
+ if (!STAGES.includes(entry.stage)) {
89
+ throw new Error(
90
+ `Unknown pipeline stage "${entry.stage}". Expected one of: ${STAGES.join(", ")}`,
91
+ );
92
+ }
93
+ components[entry.stage] = resolveComponent(entry, config, configDir);
94
+ components.names[entry.stage] = entry.name;
95
+ }
96
+ return components;
97
+ }
98
+
99
+ function resolveComponent(entry, config, configDir) {
100
+ const { stage, name } = entry;
101
+ const entryConfig = entry.config || {};
102
+
103
+ const builtIn = BUILT_INS[stage]?.[name];
104
+ if (builtIn) return builtIn(entryConfig, configDir);
105
+
106
+ const modulePath = config.plugins?.[stage]?.[name];
107
+ if (typeof modulePath === "string") {
108
+ return loadPluginModule(modulePath, entryConfig, configDir);
109
+ }
110
+
111
+ try {
112
+ return pluginRegistry.get(stage, name);
113
+ } catch {
114
+ const builtIns = Object.keys(BUILT_INS[stage] || {});
115
+ throw new Error(
116
+ `Unknown ${stage} "${name}". ` +
117
+ (builtIns.length
118
+ ? `Built-in ${stage}s: ${builtIns.join(", ")}. `
119
+ : "") +
120
+ `For a custom plugin add "plugins": { "${stage}": { "${name}": "./path/to/module.js" } }.`,
121
+ );
122
+ }
123
+ }
124
+
125
+ module.exports = { buildComponents, loadPluginModule, BUILT_INS, STAGES };
@@ -3,8 +3,6 @@
3
3
  * Single source of truth for configuration validation
4
4
  */
5
5
 
6
- const Ajv = require("ajv"); // eslint-disable-line global-require
7
- const addFormats = require("ajv-formats"); // eslint-disable-line global-require
8
6
  const { normalizeConfig } = require("./normalize-config");
9
7
 
10
8
  /**
@@ -18,11 +18,18 @@ const CONFIG_FILENAME = ".ragrc.json";
18
18
  * @param {string} [cwd=process.cwd()] - Directory to resolve the config from
19
19
  * @returns {object} Validated configuration object
20
20
  */
21
- function loadRagConfig(cwd = process.cwd()) {
22
- const configPath = path.resolve(cwd, CONFIG_FILENAME);
21
+ /**
22
+ * Load and validate .ragrc.json.
23
+ *
24
+ * @param {string} [location=process.cwd()] - A directory containing
25
+ * .ragrc.json, or the path of a config file (any name ending in .json)
26
+ * @returns {object} Normalized configuration
27
+ */
28
+ function loadRagConfig(location = process.cwd()) {
29
+ const configPath = resolveConfigPath(location);
23
30
 
31
+ // Errors are thrown, not logged: callers (e.g. the CLI) report them once.
24
32
  if (!fs.existsSync(configPath)) {
25
- logger.error(`❌ Config file not found: ${configPath}`);
26
33
  throw new Error(`Config file not found: ${configPath}`);
27
34
  }
28
35
 
@@ -32,23 +39,35 @@ function loadRagConfig(cwd = process.cwd()) {
32
39
  try {
33
40
  config = JSON.parse(raw);
34
41
  } catch (err) {
35
- logger.error("❌ Failed to parse JSON configuration.");
36
- throw new Error("Invalid JSON in config file.");
42
+ throw new Error(
43
+ `Invalid JSON in config file ${configPath}: ${err.message}`,
44
+ );
37
45
  }
38
46
 
39
47
  const { valid, errors, normalized } = validateRagrc(config);
40
48
 
41
49
  if (!valid) {
42
- logger.error(
43
- `❌ Config validation failed:\n${JSON.stringify(errors, null, 2)}`,
44
- );
45
- throw new Error("Config validation failed");
50
+ const details = (errors || []).map((e) => e.message).join("; ");
51
+ throw new Error(`Config validation failed: ${details}`);
46
52
  }
47
53
 
48
- logger.info("✅ Configuration loaded and normalized successfully");
54
+ logger.debug("Configuration loaded", { configPath });
49
55
  return normalized;
50
56
  }
51
57
 
58
+ /**
59
+ * @param {string} [location=process.cwd()] - Directory or config file path
60
+ * @returns {string} Absolute path of the config file
61
+ */
62
+ function resolveConfigPath(location = process.cwd()) {
63
+ const absolute = path.resolve(location);
64
+ const isFile =
65
+ (fs.existsSync(absolute) && fs.statSync(absolute).isFile()) ||
66
+ absolute.toLowerCase().endsWith(".json");
67
+ return isFile ? absolute : path.join(absolute, CONFIG_FILENAME);
68
+ }
69
+
52
70
  module.exports = {
53
71
  loadRagConfig,
72
+ resolveConfigPath,
54
73
  };
@@ -17,6 +17,48 @@ const { validatePluginSchema } = require("./validate-schema.js");
17
17
  const { logger } = require("../utils/logger.js");
18
18
  // eslint-disable-line global-require
19
19
 
20
+ /**
21
+ * Verify a plugin file against its signed manifest BEFORE it is imported
22
+ * (importing runs the plugin's code).
23
+ *
24
+ * The manifest is read from `<plugin file>.manifest.json`. Its
25
+ * `contentDigest` (if any) must cover the plugin file plus any extra files
26
+ * listed in `manifest.files` (paths relative to the plugin file's
27
+ * directory). Nothing is checked when the registry's signature verification
28
+ * is disabled; when it is enabled and the registry was created with
29
+ * `requireSignedPlugins: true`, a missing manifest is an error.
30
+ *
31
+ * @param {string} name - Plugin name
32
+ * @param {string} absPath - Absolute path of the plugin entry file
33
+ * @returns {Promise<void>}
34
+ */
35
+ async function verifyPluginBeforeImport(name, absPath) {
36
+ if (
37
+ typeof registry.isSignatureVerificationEnabled !== "function" ||
38
+ !registry.isSignatureVerificationEnabled()
39
+ ) {
40
+ return;
41
+ }
42
+
43
+ const manifestPath = `${absPath}.manifest.json`;
44
+ if (!fs.existsSync(manifestPath)) {
45
+ if (registry.requiresSignedPlugins()) {
46
+ throw new Error(
47
+ `Plugin ${name} has no signed manifest (${manifestPath}) and requireSignedPlugins is enabled`,
48
+ );
49
+ }
50
+ return;
51
+ }
52
+
53
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
54
+ const baseDir = path.dirname(absPath);
55
+ const extraFiles = Array.isArray(manifest.files) ? manifest.files : [];
56
+ await registry.verifyPluginContent(name, manifest, {
57
+ baseDir,
58
+ files: [absPath, ...extraFiles.map((f) => path.resolve(baseDir, f))],
59
+ });
60
+ }
61
+
20
62
  /**
21
63
  * Loads plugins from validated _config path and registers them.
22
64
  * @param {string} configPath
@@ -43,6 +85,7 @@ async function loadPluginsFromJson(configPath) {
43
85
  const fileUrl = pathToFileURL(absPath).href;
44
86
 
45
87
  try {
88
+ await verifyPluginBeforeImport(name, absPath);
46
89
  const mod = await import(fileUrl);
47
90
  const PluginClass = mod?.default;
48
91
  if (typeof PluginClass !== "function") {
@@ -52,7 +95,8 @@ async function loadPluginsFromJson(configPath) {
52
95
  }
53
96
 
54
97
  const _instance = new PluginClass();
55
- registry.register(_type, name, _instance);
98
+ // register() is async; await it so failures are not unhandled
99
+ await registry.register(_type, name, _instance);
56
100
 
57
101
  logger.info({ _type, name }, `Registered plugin: ${_type}:${name}`);
58
102
  } catch (err) {
@@ -66,4 +110,4 @@ async function loadPluginsFromJson(configPath) {
66
110
  }
67
111
  }
68
112
 
69
- module.exports = { loadPluginsFromJson };
113
+ module.exports = { loadPluginsFromJson, verifyPluginBeforeImport };
@@ -5,7 +5,8 @@
5
5
  * File: /src/_config/validate-schema.js
6
6
  */
7
7
 
8
- const Ajv = require("ajv");
8
+ // ajv loads when a schema is validated, not on import.
9
+ const loadAjv = () => require("ajv"); // eslint-disable-line global-require
9
10
  // eslint-disable-line global-require
10
11
 
11
12
  /**
@@ -89,6 +90,7 @@ const pluginSchema = {
89
90
  * @returns {{ valid: boolean, errors?: any[] }}
90
91
  */
91
92
  function validateRagrcSchema(config) {
93
+ const Ajv = loadAjv();
92
94
  const ajv = new Ajv({ allErrors: true, strict: false });
93
95
  const validate = ajv.compile(ragrcSchema);
94
96
  const valid = validate(config);
@@ -101,6 +103,7 @@ function validateRagrcSchema(config) {
101
103
  * @returns {{ valid: boolean, errors?: any[] }}
102
104
  */
103
105
  function validatePluginSchema(config) {
106
+ const Ajv = loadAjv();
104
107
  const ajv = new Ajv({ allErrors: true, strict: false });
105
108
  const validate = ajv.compile(pluginSchema);
106
109
  const valid = validate(config);
@@ -1,19 +1,38 @@
1
1
  'use strict';
2
2
 
3
- const axios = require('axios');
3
+ // axios loads on first use, so importing the package does not pay for it
4
+ // (about 125 ms) unless this connector is constructed.
5
+ const loadAxios = () => require('axios'); // eslint-disable-line global-require
4
6
  const { BaseConnector } = require('./base-connector');
7
+ const {
8
+ missingApiKeyError,
9
+ raceAbort,
10
+ resolveApiKey,
11
+ signalOptions,
12
+ throwIfAborted,
13
+ } = require('./connector-utils');
14
+ const { buildRagPrompt } = require('./rag-prompt');
15
+ const { componentCacheKey } = require('../cache/component-keys');
16
+
17
+ const API_KEY_ENV = 'ANTHROPIC_API_KEY';
5
18
 
6
19
  /**
7
20
  * Default configuration for the Anthropic connector.
21
+ * `apiKey` is a getter so the environment is read when a connector is
22
+ * constructed, not when this module is first required.
8
23
  * @type {Object}
9
24
  */
10
25
  const DEFAULT_CONFIG = {
11
26
  model: 'claude-3-5-sonnet-20241022',
12
- apiKey: process.env.ANTHROPIC_API_KEY || '',
13
27
  baseURL: 'https://api.anthropic.com',
14
28
  apiVersion: '2023-06-01',
15
29
  maxTokens: 4096,
16
30
  temperature: 0.7,
31
+ maxRetries: 3,
32
+ retryDelay: 1000,
33
+ get apiKey() {
34
+ return process.env[API_KEY_ENV] || '';
35
+ },
17
36
  };
18
37
 
19
38
  /**
@@ -23,19 +42,43 @@ const DEFAULT_CONFIG = {
23
42
  * @extends BaseConnector
24
43
  */
25
44
  class AnthropicConnector extends BaseConnector {
45
+ /**
46
+ * Cache identity for createRagPipeline({ cache }).
47
+ * @param {'llm'} role
48
+ * @returns {string|null}
49
+ */
50
+ getCacheKey(role) {
51
+ if (role !== 'llm') return null;
52
+ const c = this.config;
53
+ return componentCacheKey('anthropic', {
54
+ baseURL: c.baseURL,
55
+ model: c.model,
56
+ temperature: c.temperature,
57
+ maxTokens: c.maxTokens,
58
+ systemPrompt: c.systemPrompt,
59
+ });
60
+ }
61
+
26
62
  /**
27
63
  * @param {Object} [options]
28
64
  * @param {string} [options.model='claude-3-5-sonnet-20241022'] - Claude model to use
29
- * @param {string} [options.apiKey] - Anthropic API key
65
+ * @param {string} [options.apiKey] - Anthropic API key (defaults to ANTHROPIC_API_KEY at construction)
30
66
  * @param {string} [options.baseURL='https://api.anthropic.com'] - API base URL
31
67
  * @param {string} [options.apiVersion='2023-06-01'] - API version header
32
68
  * @param {number} [options.maxTokens=4096] - Max tokens in response
33
69
  * @param {number} [options.temperature=0.7] - Sampling temperature
70
+ * @param {number} [options.maxRetries=3] - Retries for transient failures
71
+ * @param {number} [options.retryDelay=1000] - Base backoff delay in ms
72
+ * @param {string} [options.systemPrompt] - System prompt used when RAG context is supplied
34
73
  */
35
74
  constructor(options = {}) {
36
75
  super({ name: 'anthropic', ...options });
37
- this.config = { ...DEFAULT_CONFIG, ...options };
38
- this.client = axios.create({
76
+ this.config = {
77
+ ...DEFAULT_CONFIG,
78
+ ...options,
79
+ apiKey: resolveApiKey(options, API_KEY_ENV),
80
+ };
81
+ this.client = loadAxios().create({
39
82
  baseURL: this.config.baseURL,
40
83
  headers: {
41
84
  'x-api-key': this.config.apiKey,
@@ -45,51 +88,73 @@ class AnthropicConnector extends BaseConnector {
45
88
  });
46
89
  }
47
90
 
91
+ /**
92
+ * @throws {Error} code MISSING_API_KEY when no key is configured
93
+ * @private
94
+ */
95
+ _requireApiKey() {
96
+ if (!this.config.apiKey) {
97
+ throw missingApiKeyError('Anthropic', API_KEY_ENV);
98
+ }
99
+ }
100
+
48
101
  /**
49
102
  * Connect to Anthropic. Verifies the API key is present.
50
103
  * @returns {Promise<void>}
51
104
  * @throws {Error} If no API key is configured
52
105
  */
53
106
  async connect() {
54
- if (!this.config.apiKey) {
55
- throw new Error(
56
- 'Anthropic API key required. Set ANTHROPIC_API_KEY or pass apiKey option.',
57
- );
58
- }
107
+ this._requireApiKey();
59
108
  await super.connect();
60
109
  }
61
110
 
111
+ /**
112
+ * Build the Messages API request body. Retrieved context goes into the
113
+ * user message as delimited, untrusted reference data; the system prompt
114
+ * only carries instructions.
115
+ * @private
116
+ */
117
+ _buildBody(query, context, options) {
118
+ const prompt = buildRagPrompt(query, context, {
119
+ systemPrompt: this.config.systemPrompt,
120
+ });
121
+ const body = {
122
+ model: options.model || this.config.model,
123
+ max_tokens: options.maxTokens ?? this.config.maxTokens,
124
+ temperature: options.temperature ?? this.config.temperature,
125
+ messages: [{ role: 'user', content: prompt.user }],
126
+ };
127
+ if (prompt.system) body.system = prompt.system;
128
+ return body;
129
+ }
130
+
62
131
  /**
63
132
  * Generate a response from Claude using the provided query and optional RAG context.
64
133
  *
65
134
  * @param {string} query - The user query / prompt
66
135
  * @param {Array<Object|string>} [context=[]] - Retrieved documents for RAG
67
136
  * @param {Object} [options={}] - Override model, temperature, maxTokens per-call
137
+ * @param {AbortSignal} [options.signal] - Cancels the request
68
138
  * @returns {Promise<string>} The generated text
69
139
  */
70
140
  async generate(query, context = [], options = {}) {
71
- return this.withRetry(async () => {
72
- const body = {
73
- model: options.model || this.config.model,
74
- max_tokens: options.maxTokens ?? this.config.maxTokens,
75
- temperature: options.temperature ?? this.config.temperature,
76
- messages: [
77
- { role: 'user', content: this._buildPrompt(query, context) },
78
- ],
79
- };
80
-
81
- // Add system message when context is provided
82
- if (context && context.length > 0) {
83
- const contextText = context
84
- .map((doc) => doc.content || doc.text || String(doc))
85
- .join('\n\n');
86
- body.system = `Use the following context to answer the question:\n\n${contextText}`;
87
- body.messages = [{ role: 'user', content: query }];
88
- }
89
-
90
- const response = await this.client.post('/v1/messages', body);
91
- return response.data.content[0].text;
92
- });
141
+ const { signal } = options;
142
+ throwIfAborted(signal);
143
+ this._requireApiKey();
144
+ const body = this._buildBody(query, context, options);
145
+ return this.withRetry(
146
+ async () => {
147
+ const response = await this.client.post(
148
+ '/v1/messages',
149
+ body,
150
+ signal ? { signal } : undefined,
151
+ );
152
+ return response.data.content[0].text;
153
+ },
154
+ this.config.maxRetries,
155
+ this.config.retryDelay,
156
+ { signal },
157
+ );
93
158
  }
94
159
 
95
160
  /**
@@ -98,32 +163,27 @@ class AnthropicConnector extends BaseConnector {
98
163
  * @param {string} query - The user query / prompt
99
164
  * @param {Array<Object|string>} [context=[]] - Retrieved documents for RAG
100
165
  * @param {Object} [options={}] - Override model, temperature, maxTokens per-call
166
+ * @param {AbortSignal} [options.signal] - Cancels the stream
101
167
  * @yields {{ token: string, done: boolean }}
102
168
  */
103
169
  async *generateStream(query, context = [], options = {}) {
104
- const body = {
105
- model: options.model || this.config.model,
106
- max_tokens: options.maxTokens ?? this.config.maxTokens,
107
- temperature: options.temperature ?? this.config.temperature,
108
- messages: [{ role: 'user', content: this._buildPrompt(query, context) }],
109
- stream: true,
110
- };
111
-
112
- if (context && context.length > 0) {
113
- const contextText = context
114
- .map((doc) => doc.content || doc.text || String(doc))
115
- .join('\n\n');
116
- body.system = `Use the following context to answer the question:\n\n${contextText}`;
117
- body.messages = [{ role: 'user', content: query }];
118
- }
119
-
120
- const response = await this.client.post('/v1/messages', body, {
121
- responseType: 'stream',
122
- });
170
+ const { signal } = options;
171
+ throwIfAborted(signal);
172
+ this._requireApiKey();
173
+ const body = { ...this._buildBody(query, context, options), stream: true };
174
+
175
+ const response = await raceAbort(
176
+ this.client.post('/v1/messages', body, {
177
+ responseType: 'stream',
178
+ ...signalOptions(signal),
179
+ }),
180
+ signal,
181
+ );
123
182
 
124
183
  let buffer = '';
125
184
 
126
185
  for await (const chunk of response.data) {
186
+ throwIfAborted(signal);
127
187
  buffer += chunk.toString();
128
188
  const lines = buffer.split('\n');
129
189
  // Keep the last incomplete line in the buffer
@@ -160,7 +220,9 @@ class AnthropicConnector extends BaseConnector {
160
220
  }
161
221
 
162
222
  /**
163
- * Build a prompt string that includes RAG context when available.
223
+ * Build a single prompt string that includes RAG context when available.
224
+ * Kept for backward compatibility; the context is delimited and labelled
225
+ * as untrusted reference data.
164
226
  *
165
227
  * @param {string} query - User query
166
228
  * @param {Array<Object|string>} context - RAG context documents
@@ -168,11 +230,7 @@ class AnthropicConnector extends BaseConnector {
168
230
  * @private
169
231
  */
170
232
  _buildPrompt(query, context) {
171
- if (!context || context.length === 0) return query;
172
- const contextText = context
173
- .map((doc) => doc.content || doc.text || String(doc))
174
- .join('\n\n');
175
- return `Context:\n${contextText}\n\nQuestion: ${query}`;
233
+ return buildRagPrompt(query, context).user;
176
234
  }
177
235
  }
178
236
 
@@ -1,6 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  const { EventEmitter } = require('events');
4
+ const {
5
+ abortableSleep,
6
+ computeRetryDelay,
7
+ createAbortError,
8
+ getErrorStatus,
9
+ isAbortError,
10
+ isRetryableError,
11
+ raceAbort,
12
+ throwIfAborted,
13
+ } = require('./connector-utils');
4
14
 
5
15
  /**
6
16
  * Abstract base class for all connectors.
@@ -49,33 +59,78 @@ class BaseConnector extends EventEmitter {
49
59
  }
50
60
 
51
61
  /**
52
- * Retry wrapper that attempts a function up to N times with a delay between retries.
53
- * Emits a 'retry' event on each retry attempt.
54
- * @param {Function} fn - Async function to execute
62
+ * Retry wrapper for transient failures.
63
+ *
64
+ * Only retryable errors are retried: network errors and HTTP
65
+ * 408/409/425/429/5xx (see {@link isRetryableError}). Other errors
66
+ * (400, 401, 403, 404, aborts, errors with no status or network code) are
67
+ * thrown immediately. Backoff is exponential with full jitter, starting at
68
+ * `delay` and capped at `opts.maxDelay`; a Retry-After header on the error
69
+ * is honoured (capped at `opts.maxDelay`). Emits a 'retry' event before
70
+ * each retry.
71
+ *
72
+ * @param {Function} fn - Async function to execute; receives the attempt number
55
73
  * @param {number} [retries=3] - Maximum number of retry attempts
56
- * @param {number} [delay=1000] - Delay in ms between retries
74
+ * @param {number} [delay=1000] - Base backoff delay in ms
75
+ * @param {object} [opts]
76
+ * @param {AbortSignal} [opts.signal] - Aborts the call and any pending backoff
77
+ * @param {number} [opts.maxDelay=30000] - Upper bound for a single backoff delay
78
+ * @param {(err: *) => boolean} [opts.shouldRetry] - Override retry classification
57
79
  * @returns {Promise<*>} Result of the function
58
- * @throws {Error} Last error if all retries are exhausted
80
+ * @throws {Error} The first non-retryable error, or the last error once retries are exhausted
59
81
  */
60
- async withRetry(fn, retries = 3, delay = 1000) {
61
- let lastError;
62
- for (let attempt = 0; attempt <= retries; attempt++) {
82
+ async withRetry(fn, retries = 3, delay = 1000, opts = {}) {
83
+ const signal = opts.signal;
84
+ const shouldRetry = opts.shouldRetry || isRetryableError;
85
+ const maxDelay = opts.maxDelay ?? this.options.retryMaxDelay ?? 30000;
86
+
87
+ for (let attempt = 0; ; attempt++) {
88
+ throwIfAborted(signal);
63
89
  try {
64
- return await fn();
90
+ return await raceAbort(() => fn(attempt), signal);
65
91
  } catch (err) {
66
- lastError = err;
67
- if (attempt < retries) {
68
- this.emit('retry', {
69
- name: this.name,
70
- attempt: attempt + 1,
71
- maxRetries: retries,
72
- error: err.message,
73
- });
74
- await new Promise((resolve) => setTimeout(resolve, delay));
92
+ if (isAbortError(err) || (signal && signal.aborted)) {
93
+ throw createAbortError(signal);
94
+ }
95
+ if (attempt >= retries || !shouldRetry(err)) {
96
+ throw err;
75
97
  }
98
+ const delayMs = computeRetryDelay(err, attempt, {
99
+ baseDelay: delay,
100
+ maxDelay,
101
+ random: () => this._random(),
102
+ });
103
+ this.emit('retry', {
104
+ name: this.name,
105
+ attempt: attempt + 1,
106
+ maxRetries: retries,
107
+ error: err.message,
108
+ status: getErrorStatus(err),
109
+ delayMs,
110
+ });
111
+ await this._sleep(delayMs, signal);
76
112
  }
77
113
  }
78
- throw lastError;
114
+ }
115
+
116
+ /**
117
+ * Wait between retries. Overridable in tests.
118
+ * @param {number} ms
119
+ * @param {AbortSignal} [signal]
120
+ * @returns {Promise<void>}
121
+ * @protected
122
+ */
123
+ _sleep(ms, signal) {
124
+ return abortableSleep(ms, signal);
125
+ }
126
+
127
+ /**
128
+ * Jitter source for retry backoff. Overridable in tests.
129
+ * @returns {number} Value in [0, 1)
130
+ * @protected
131
+ */
132
+ _random() {
133
+ return Math.random();
79
134
  }
80
135
 
81
136
  /**