@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.
Files changed (225) hide show
  1. package/.ragrc.schema.json +83 -192
  2. package/CHANGELOG.md +345 -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 +4992 -1242
  9. package/dist/index.d.ts +80 -2
  10. package/dist/index.mjs +1 -1
  11. package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
  12. package/dist/types/agentic/iterative-retriever.d.ts +85 -0
  13. package/dist/types/agentic/query-planner.d.ts +145 -0
  14. package/dist/types/agentic/self-critique.d.ts +45 -0
  15. package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
  16. package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
  17. package/dist/types/ai/multimodal-processing.d.ts +32 -0
  18. package/dist/types/ai/multimodal.d.ts +39 -0
  19. package/dist/types/ai/retrieval-engine.d.ts +37 -0
  20. package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
  21. package/dist/types/chunking/chunking-engine.d.ts +70 -0
  22. package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
  23. package/dist/types/chunking/strategies/recursive.d.ts +20 -0
  24. package/dist/types/chunking/strategies/semantic.d.ts +19 -0
  25. package/dist/types/chunking/strategies/sentence.d.ts +13 -0
  26. package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
  27. package/dist/types/citation/citation-tracker.d.ts +64 -0
  28. package/dist/types/citation/hallucination-detector.d.ts +40 -0
  29. package/dist/types/citation/source-mapper.d.ts +37 -0
  30. package/dist/types/connectors/anthropic-connector.d.ts +92 -0
  31. package/dist/types/connectors/base-connector.d.ts +87 -0
  32. package/dist/types/connectors/cohere-connector.d.ts +89 -0
  33. package/dist/types/connectors/connector-utils.d.ts +110 -0
  34. package/dist/types/connectors/local-embedder.d.ts +86 -0
  35. package/dist/types/connectors/memory-retriever.d.ts +91 -0
  36. package/dist/types/connectors/ollama-connector.d.ts +110 -0
  37. package/dist/types/connectors/openai-connector.d.ts +114 -0
  38. package/dist/types/connectors/rag-prompt.d.ts +40 -0
  39. package/dist/types/cost/cost-calculator.d.ts +152 -0
  40. package/dist/types/cost/cost-tracker.d.ts +126 -0
  41. package/dist/types/cost/token-budget.d.ts +110 -0
  42. package/dist/types/dag/core/error-context.d.ts +239 -0
  43. package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
  44. package/dist/types/dag/core/topology-validation.d.ts +226 -0
  45. package/dist/types/dag/dag-engine.d.ts +156 -0
  46. package/dist/types/dag/dag-node.d.ts +37 -0
  47. package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
  48. package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
  49. package/dist/types/debug/execution-tracer.d.ts +57 -0
  50. package/dist/types/debug/trace-inspector.d.ts +31 -0
  51. package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
  52. package/dist/types/enterprise/data-governance.d.ts +203 -0
  53. package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
  54. package/dist/types/enterprise/sso-integration.d.ts +420 -0
  55. package/dist/types/evaluate/context-metrics.d.ts +55 -0
  56. package/dist/types/evaluate/faithfulness.d.ts +41 -0
  57. package/dist/types/evaluate/groundedness.d.ts +60 -0
  58. package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
  59. package/dist/types/evaluate/relevance.d.ts +21 -0
  60. package/dist/types/evaluate/scoring.d.ts +123 -0
  61. package/dist/types/experimental.d.ts +15 -0
  62. package/dist/types/graph/entity-extractor.d.ts +84 -0
  63. package/dist/types/graph/graph-index.d.ts +49 -0
  64. package/dist/types/graph/graph-retriever.d.ts +81 -0
  65. package/dist/types/graph/knowledge-graph.d.ts +129 -0
  66. package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
  67. package/dist/types/guardrails/injection-detector.d.ts +47 -0
  68. package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
  69. package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
  70. package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
  71. package/dist/types/mcp/mcp-server.d.ts +85 -0
  72. package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
  73. package/dist/types/reranker/cascade-reranker.d.ts +40 -0
  74. package/dist/types/reranker/embedding-reranker.d.ts +77 -0
  75. package/dist/types/reranker/scoring-reranker.d.ts +41 -0
  76. package/dist/types/retrieval/bm25-search.d.ts +43 -0
  77. package/dist/types/retrieval/file-vector-store.d.ts +87 -0
  78. package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
  79. package/dist/types/retrieval/rank-fusion.d.ts +10 -0
  80. package/dist/types/security/jwt-config.d.ts +80 -0
  81. package/dist/types/security/jwt-validator.d.ts +195 -0
  82. package/dist/types/security/plugin-sandbox.d.ts +158 -0
  83. package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
  84. package/dist/types/security/validator.d.ts +5 -0
  85. package/dist/types/streaming/embedding-stream.d.ts +65 -0
  86. package/dist/types/streaming/sse-adapter.d.ts +59 -0
  87. package/dist/types/streaming/stream-router.d.ts +88 -0
  88. package/dist/types/streaming/streaming-embedder.d.ts +99 -0
  89. package/dist/types/streaming/websocket-adapter.d.ts +71 -0
  90. package/dist/types/templates/scaffold.d.ts +55 -0
  91. package/dist/types/templates/template-registry.d.ts +63 -0
  92. package/dist/types/utils/embed-query.d.ts +20 -0
  93. package/dist/types/utils/logger.d.ts +50 -0
  94. package/dist/types/utils/rate-limiter.d.ts +66 -0
  95. package/dist/types/utils/retry.d.ts +27 -0
  96. package/dist/types/utils/run-abort.d.ts +34 -0
  97. package/dist/types/utils/secure-logging.d.ts +51 -0
  98. package/dist/types/utils/structured-logger.d.ts +126 -0
  99. package/package.json +53 -29
  100. package/src/agentic/agentic-pipeline.js +75 -7
  101. package/src/agentic/iterative-retriever.js +47 -10
  102. package/src/agentic/query-planner.js +329 -78
  103. package/src/citation/citation-tracker.js +6 -4
  104. package/src/citation/hallucination-detector.js +2 -2
  105. package/src/citation/source-mapper.js +15 -28
  106. package/src/cli/commands/ai.js +101 -0
  107. package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
  108. package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
  109. package/src/cli/commands/plugin-hub/local-commands.js +143 -0
  110. package/src/cli/commands/plugin-hub.js +96 -36
  111. package/src/cli/diagnostics/config-checker.js +76 -63
  112. package/src/cli/diagnostics/environment-checker.js +62 -31
  113. package/src/cli/diagnostics/plugin-checker.js +36 -89
  114. package/src/cli/doctor-command.js +39 -5
  115. package/src/cli/enhanced-cli-commands.js +72 -217
  116. package/src/cli/handlers/ai-handler.js +364 -0
  117. package/src/cli/handlers/diagnostics-handler.js +78 -31
  118. package/src/cli/handlers/ingest-handler.js +28 -22
  119. package/src/cli/handlers/init-handler.js +148 -25
  120. package/src/cli/handlers/query-handler.js +41 -26
  121. package/src/cli/handlers/run-handler.js +16 -167
  122. package/src/cli/handlers/serve-handler.js +60 -0
  123. package/src/cli/services/bench-stats.js +54 -0
  124. package/src/cli/services/benchmark.js +161 -0
  125. package/src/cli/services/feedback.js +155 -0
  126. package/src/cli/services/rag-service.js +322 -0
  127. package/src/cli/shared/cli-utils.js +24 -7
  128. package/src/cli/shared/config-context.js +21 -0
  129. package/src/cli/wizard/config-builder.js +81 -0
  130. package/src/cli/wizard/init-wizard.js +617 -0
  131. package/src/config/component-factory.js +125 -0
  132. package/src/config/load-config.js +29 -10
  133. package/src/config/load-plugin-config.js +46 -2
  134. package/src/connectors/anthropic-connector.js +93 -55
  135. package/src/connectors/base-connector.js +74 -19
  136. package/src/connectors/cohere-connector.js +88 -32
  137. package/src/connectors/connector-utils.js +302 -0
  138. package/src/connectors/local-embedder.js +43 -5
  139. package/src/connectors/memory-retriever.js +108 -15
  140. package/src/connectors/ollama-connector.js +83 -49
  141. package/src/connectors/openai-connector.js +108 -44
  142. package/src/connectors/rag-prompt.js +88 -0
  143. package/src/core/create-pipeline.js +209 -60
  144. package/src/core/plugin-registry.js +87 -11
  145. package/src/cost/cost-calculator.js +205 -32
  146. package/src/dev/dev-server.js +166 -11
  147. package/src/dev/hot-reload.js +40 -1
  148. package/src/ecosystem/plugin-checks.js +181 -0
  149. package/src/ecosystem/plugin-hub.js +50 -125
  150. package/src/ecosystem/plugin-installer.js +298 -0
  151. package/src/ecosystem/plugin-probe.js +127 -0
  152. package/src/ecosystem/plugin-scanner.js +185 -0
  153. package/src/ecosystem/tar-extract.js +114 -0
  154. package/src/enterprise/audit-logging.js +158 -29
  155. package/src/enterprise/sso-integration.js +715 -228
  156. package/src/evaluate/context-metrics.js +28 -16
  157. package/src/evaluate/evaluator.js +3 -3
  158. package/src/evaluate/faithfulness.js +18 -14
  159. package/src/evaluate/groundedness.js +43 -12
  160. package/src/evaluate/pipeline-evaluator.js +35 -8
  161. package/src/evaluate/scoring.js +251 -3
  162. package/src/experimental.js +71 -0
  163. package/src/guardrails/guardrails-pipeline.js +69 -7
  164. package/src/guardrails/injection-detector.js +283 -0
  165. package/src/guardrails/post-generation-guard.js +19 -0
  166. package/src/guardrails/pre-retrieval-guard.js +36 -36
  167. package/src/guardrails/retrieval-guard.js +58 -8
  168. package/src/index.js +36 -1
  169. package/src/loader/document-reader.js +91 -0
  170. package/src/reranker/cascade-reranker.js +3 -0
  171. package/src/reranker/embedding-reranker.js +136 -21
  172. package/src/reranker/llm-reranker.js +79 -12
  173. package/src/reranker/scoring-reranker.js +3 -0
  174. package/src/retrieval/bm25-search.js +12 -6
  175. package/src/retrieval/file-vector-store.js +212 -0
  176. package/src/retrieval/hybrid-retriever.js +22 -19
  177. package/src/security/jwt-config.js +13 -2
  178. package/src/security/jwt-validator.js +134 -30
  179. package/src/security/plugin-sandbox.js +18 -1
  180. package/src/security/plugin-signature-verifier.js +245 -70
  181. package/src/utils/deprecate.js +69 -0
  182. package/src/utils/embed-query.js +33 -0
  183. package/src/utils/input-sanitizer.js +19 -4
  184. package/src/utils/logger.js +34 -4
  185. package/src/utils/run-abort.js +102 -0
  186. package/src/ai/federated-learning.js +0 -211
  187. package/src/ai/federation.js +0 -71
  188. package/src/ai/index.js +0 -22
  189. package/src/ai/model-trainer.js +0 -217
  190. package/src/ai/model-training.js +0 -202
  191. package/src/ai/multimodal/multi-modal-processor.js +0 -345
  192. package/src/ai/orchestrator.js +0 -65
  193. package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
  194. package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
  195. package/src/cli/commands/ai-ml/federated-commands.js +0 -207
  196. package/src/cli/commands/ai-ml/index.js +0 -17
  197. package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
  198. package/src/cli/commands/ai-ml/shared.js +0 -51
  199. package/src/cli/commands/ai-ml/training-commands.js +0 -228
  200. package/src/cli/commands/ai-ml.js +0 -386
  201. package/src/cli/commands/docs.js +0 -228
  202. package/src/cli/commands/dx.js +0 -538
  203. package/src/cli/commands/wizard.js +0 -627
  204. package/src/cli/input-sanitization-middleware.js +0 -356
  205. package/src/cli/interactive-wizard.js +0 -373
  206. package/src/cli/templates/development.template.json +0 -64
  207. package/src/cli/templates/minimal.template.json +0 -33
  208. package/src/cli/templates/production.template.json +0 -104
  209. package/src/cli/templates/testing.template.json +0 -45
  210. package/src/cli/wizard/configuration.js +0 -148
  211. package/src/cli/wizard/observability.js +0 -79
  212. package/src/cli/wizard/performance.js +0 -76
  213. package/src/cli/wizard/plugin-selection.js +0 -297
  214. package/src/cli/wizard/preview-save.js +0 -172
  215. package/src/cli/wizard/project-setup.js +0 -66
  216. package/src/core/pipeline-factory.js +0 -59
  217. package/src/dag/enhanced-dag-engine.js +0 -894
  218. package/src/dx/index.js +0 -40
  219. package/src/dx/integration-templates.js +0 -662
  220. package/src/dx/performance-profiler.js +0 -968
  221. package/src/dx/realtime-debugger.js +0 -755
  222. package/src/dx/visual-pipeline-builder.js +0 -578
  223. package/src/ingest.js +0 -59
  224. package/src/pipeline/pipeline.js +0 -18
  225. 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 };
@@ -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 };
@@ -2,18 +2,34 @@
2
2
 
3
3
  const axios = require('axios');
4
4
  const { BaseConnector } = require('./base-connector');
5
+ const {
6
+ missingApiKeyError,
7
+ raceAbort,
8
+ resolveApiKey,
9
+ signalOptions,
10
+ throwIfAborted,
11
+ } = require('./connector-utils');
12
+ const { buildRagPrompt } = require('./rag-prompt');
13
+
14
+ const API_KEY_ENV = 'ANTHROPIC_API_KEY';
5
15
 
6
16
  /**
7
17
  * Default configuration for the Anthropic connector.
18
+ * `apiKey` is a getter so the environment is read when a connector is
19
+ * constructed, not when this module is first required.
8
20
  * @type {Object}
9
21
  */
10
22
  const DEFAULT_CONFIG = {
11
23
  model: 'claude-3-5-sonnet-20241022',
12
- apiKey: process.env.ANTHROPIC_API_KEY || '',
13
24
  baseURL: 'https://api.anthropic.com',
14
25
  apiVersion: '2023-06-01',
15
26
  maxTokens: 4096,
16
27
  temperature: 0.7,
28
+ maxRetries: 3,
29
+ retryDelay: 1000,
30
+ get apiKey() {
31
+ return process.env[API_KEY_ENV] || '';
32
+ },
17
33
  };
18
34
 
19
35
  /**
@@ -26,15 +42,22 @@ class AnthropicConnector extends BaseConnector {
26
42
  /**
27
43
  * @param {Object} [options]
28
44
  * @param {string} [options.model='claude-3-5-sonnet-20241022'] - Claude model to use
29
- * @param {string} [options.apiKey] - Anthropic API key
45
+ * @param {string} [options.apiKey] - Anthropic API key (defaults to ANTHROPIC_API_KEY at construction)
30
46
  * @param {string} [options.baseURL='https://api.anthropic.com'] - API base URL
31
47
  * @param {string} [options.apiVersion='2023-06-01'] - API version header
32
48
  * @param {number} [options.maxTokens=4096] - Max tokens in response
33
49
  * @param {number} [options.temperature=0.7] - Sampling temperature
50
+ * @param {number} [options.maxRetries=3] - Retries for transient failures
51
+ * @param {number} [options.retryDelay=1000] - Base backoff delay in ms
52
+ * @param {string} [options.systemPrompt] - System prompt used when RAG context is supplied
34
53
  */
35
54
  constructor(options = {}) {
36
55
  super({ name: 'anthropic', ...options });
37
- this.config = { ...DEFAULT_CONFIG, ...options };
56
+ this.config = {
57
+ ...DEFAULT_CONFIG,
58
+ ...options,
59
+ apiKey: resolveApiKey(options, API_KEY_ENV),
60
+ };
38
61
  this.client = axios.create({
39
62
  baseURL: this.config.baseURL,
40
63
  headers: {
@@ -45,51 +68,73 @@ class AnthropicConnector extends BaseConnector {
45
68
  });
46
69
  }
47
70
 
71
+ /**
72
+ * @throws {Error} code MISSING_API_KEY when no key is configured
73
+ * @private
74
+ */
75
+ _requireApiKey() {
76
+ if (!this.config.apiKey) {
77
+ throw missingApiKeyError('Anthropic', API_KEY_ENV);
78
+ }
79
+ }
80
+
48
81
  /**
49
82
  * Connect to Anthropic. Verifies the API key is present.
50
83
  * @returns {Promise<void>}
51
84
  * @throws {Error} If no API key is configured
52
85
  */
53
86
  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
- }
87
+ this._requireApiKey();
59
88
  await super.connect();
60
89
  }
61
90
 
91
+ /**
92
+ * Build the Messages API request body. Retrieved context goes into the
93
+ * user message as delimited, untrusted reference data; the system prompt
94
+ * only carries instructions.
95
+ * @private
96
+ */
97
+ _buildBody(query, context, options) {
98
+ const prompt = buildRagPrompt(query, context, {
99
+ systemPrompt: this.config.systemPrompt,
100
+ });
101
+ const body = {
102
+ model: options.model || this.config.model,
103
+ max_tokens: options.maxTokens ?? this.config.maxTokens,
104
+ temperature: options.temperature ?? this.config.temperature,
105
+ messages: [{ role: 'user', content: prompt.user }],
106
+ };
107
+ if (prompt.system) body.system = prompt.system;
108
+ return body;
109
+ }
110
+
62
111
  /**
63
112
  * Generate a response from Claude using the provided query and optional RAG context.
64
113
  *
65
114
  * @param {string} query - The user query / prompt
66
115
  * @param {Array<Object|string>} [context=[]] - Retrieved documents for RAG
67
116
  * @param {Object} [options={}] - Override model, temperature, maxTokens per-call
117
+ * @param {AbortSignal} [options.signal] - Cancels the request
68
118
  * @returns {Promise<string>} The generated text
69
119
  */
70
120
  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
- });
121
+ const { signal } = options;
122
+ throwIfAborted(signal);
123
+ this._requireApiKey();
124
+ const body = this._buildBody(query, context, options);
125
+ return this.withRetry(
126
+ async () => {
127
+ const response = await this.client.post(
128
+ '/v1/messages',
129
+ body,
130
+ signal ? { signal } : undefined,
131
+ );
132
+ return response.data.content[0].text;
133
+ },
134
+ this.config.maxRetries,
135
+ this.config.retryDelay,
136
+ { signal },
137
+ );
93
138
  }
94
139
 
95
140
  /**
@@ -98,32 +143,27 @@ class AnthropicConnector extends BaseConnector {
98
143
  * @param {string} query - The user query / prompt
99
144
  * @param {Array<Object|string>} [context=[]] - Retrieved documents for RAG
100
145
  * @param {Object} [options={}] - Override model, temperature, maxTokens per-call
146
+ * @param {AbortSignal} [options.signal] - Cancels the stream
101
147
  * @yields {{ token: string, done: boolean }}
102
148
  */
103
149
  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
- });
150
+ const { signal } = options;
151
+ throwIfAborted(signal);
152
+ this._requireApiKey();
153
+ const body = { ...this._buildBody(query, context, options), stream: true };
154
+
155
+ const response = await raceAbort(
156
+ this.client.post('/v1/messages', body, {
157
+ responseType: 'stream',
158
+ ...signalOptions(signal),
159
+ }),
160
+ signal,
161
+ );
123
162
 
124
163
  let buffer = '';
125
164
 
126
165
  for await (const chunk of response.data) {
166
+ throwIfAborted(signal);
127
167
  buffer += chunk.toString();
128
168
  const lines = buffer.split('\n');
129
169
  // Keep the last incomplete line in the buffer
@@ -160,7 +200,9 @@ class AnthropicConnector extends BaseConnector {
160
200
  }
161
201
 
162
202
  /**
163
- * Build a prompt string that includes RAG context when available.
203
+ * Build a single prompt string that includes RAG context when available.
204
+ * Kept for backward compatibility; the context is delimited and labelled
205
+ * as untrusted reference data.
164
206
  *
165
207
  * @param {string} query - User query
166
208
  * @param {Array<Object|string>} context - RAG context documents
@@ -168,11 +210,7 @@ class AnthropicConnector extends BaseConnector {
168
210
  * @private
169
211
  */
170
212
  _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}`;
213
+ return buildRagPrompt(query, context).user;
176
214
  }
177
215
  }
178
216
 
@@ -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
  /**