@devilsdev/rag-pipeline-utils 2.4.4 → 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
@@ -2,18 +2,31 @@
2
2
 
3
3
  const axios = require('axios');
4
4
  const { BaseConnector } = require('./base-connector');
5
+ const {
6
+ missingApiKeyError,
7
+ resolveApiKey,
8
+ throwIfAborted,
9
+ } = require('./connector-utils');
10
+
11
+ const API_KEY_ENV = 'COHERE_API_KEY';
5
12
 
6
13
  /**
7
14
  * Default configuration for the Cohere connector.
15
+ * `apiKey` is a getter so the environment is read when a connector is
16
+ * constructed, not when this module is first required.
8
17
  * @type {Object}
9
18
  */
10
19
  const DEFAULT_CONFIG = {
11
- apiKey: process.env.COHERE_API_KEY || '',
20
+ get apiKey() {
21
+ return process.env[API_KEY_ENV] || '';
22
+ },
12
23
  baseURL: 'https://api.cohere.ai/v1',
13
24
  embeddingModel: 'embed-english-v3.0',
14
25
  rerankModel: 'rerank-english-v3.0',
15
26
  inputType: 'search_document',
16
27
  queryInputType: 'search_query',
28
+ maxRetries: 3,
29
+ retryDelay: 1000,
17
30
  };
18
31
 
19
32
  /**
@@ -35,7 +48,11 @@ class CohereConnector extends BaseConnector {
35
48
  */
36
49
  constructor(options = {}) {
37
50
  super({ name: 'cohere', ...options });
38
- this.config = { ...DEFAULT_CONFIG, ...options };
51
+ this.config = {
52
+ ...DEFAULT_CONFIG,
53
+ ...options,
54
+ apiKey: resolveApiKey(options, API_KEY_ENV),
55
+ };
39
56
  this.client = axios.create({
40
57
  baseURL: this.config.baseURL,
41
58
  headers: {
@@ -45,37 +62,72 @@ class CohereConnector extends BaseConnector {
45
62
  });
46
63
  }
47
64
 
65
+ /**
66
+ * @throws {Error} code MISSING_API_KEY when no key is configured
67
+ * @private
68
+ */
69
+ _requireApiKey() {
70
+ if (!this.config.apiKey) {
71
+ throw missingApiKeyError('Cohere', API_KEY_ENV);
72
+ }
73
+ }
74
+
48
75
  /**
49
76
  * Connect to Cohere. Verifies the API key is present.
50
77
  * @returns {Promise<void>}
51
78
  * @throws {Error} If no API key is configured
52
79
  */
53
80
  async connect() {
54
- if (!this.config.apiKey) {
55
- throw new Error(
56
- 'Cohere API key required. Set COHERE_API_KEY or pass apiKey option.',
57
- );
58
- }
81
+ this._requireApiKey();
59
82
  await super.connect();
60
83
  }
61
84
 
85
+ /**
86
+ * POST with the connector's retry policy and cancellation.
87
+ * @param {string} path
88
+ * @param {object} body
89
+ * @param {AbortSignal} [signal]
90
+ * @returns {Promise<object>} Response data
91
+ * @private
92
+ */
93
+ async _post(path, body, signal) {
94
+ throwIfAborted(signal);
95
+ this._requireApiKey();
96
+ return this.withRetry(
97
+ async () => {
98
+ const response = await this.client.post(
99
+ path,
100
+ body,
101
+ signal ? { signal } : undefined,
102
+ );
103
+ return response.data;
104
+ },
105
+ this.config.maxRetries,
106
+ this.config.retryDelay,
107
+ { signal },
108
+ );
109
+ }
110
+
62
111
  /**
63
112
  * Generate an embedding vector for document text.
64
113
  * Uses the document input type by default.
65
114
  *
66
115
  * @param {string} text - Input text to embed
116
+ * @param {Object} [options={}]
117
+ * @param {AbortSignal} [options.signal] - Cancels the request
67
118
  * @returns {Promise<number[]>} Embedding vector
68
119
  */
69
- async embed(text) {
70
- return this.withRetry(async () => {
71
- const response = await this.client.post('/embed', {
120
+ async embed(text, options = {}) {
121
+ const data = await this._post(
122
+ '/embed',
123
+ {
72
124
  texts: [text],
73
125
  model: this.config.embeddingModel,
74
126
  input_type: this.config.inputType,
75
- });
76
-
77
- return response.data.embeddings[0];
78
- });
127
+ },
128
+ options.signal,
129
+ );
130
+ return data.embeddings[0];
79
131
  }
80
132
 
81
133
  /**
@@ -83,18 +135,21 @@ class CohereConnector extends BaseConnector {
83
135
  * Uses the query input type for better retrieval accuracy.
84
136
  *
85
137
  * @param {string} query - Query text to embed
138
+ * @param {Object} [options={}]
139
+ * @param {AbortSignal} [options.signal] - Cancels the request
86
140
  * @returns {Promise<number[]>} Embedding vector
87
141
  */
88
- async embedQuery(query) {
89
- return this.withRetry(async () => {
90
- const response = await this.client.post('/embed', {
142
+ async embedQuery(query, options = {}) {
143
+ const data = await this._post(
144
+ '/embed',
145
+ {
91
146
  texts: [query],
92
147
  model: this.config.embeddingModel,
93
148
  input_type: this.config.queryInputType,
94
- });
95
-
96
- return response.data.embeddings[0];
97
- });
149
+ },
150
+ options.signal,
151
+ );
152
+ return data.embeddings[0];
98
153
  }
99
154
 
100
155
  /**
@@ -104,26 +159,27 @@ class CohereConnector extends BaseConnector {
104
159
  * @param {Array<Object|string>} documents - Documents to rerank
105
160
  * @param {Object} [options={}]
106
161
  * @param {number} [options.topK] - Number of top results to return (defaults to all)
162
+ * @param {AbortSignal} [options.signal] - Cancels the request
107
163
  * @returns {Promise<Array<Object>>} Reranked documents with relevance scores
108
164
  */
109
165
  async rerank(query, documents, options = {}) {
110
- return this.withRetry(async () => {
111
- const docs = documents.map(
112
- (doc) => doc.content || doc.text || String(doc),
113
- );
166
+ const docs = documents.map((doc) => doc.content || doc.text || String(doc));
114
167
 
115
- const response = await this.client.post('/rerank', {
168
+ const data = await this._post(
169
+ '/rerank',
170
+ {
116
171
  model: this.config.rerankModel,
117
172
  query,
118
173
  documents: docs,
119
174
  top_n: options.topK || documents.length,
120
- });
175
+ },
176
+ options.signal,
177
+ );
121
178
 
122
- return response.data.results.map((result) => ({
123
- ...documents[result.index],
124
- score: result.relevance_score,
125
- }));
126
- });
179
+ return data.results.map((result) => ({
180
+ ...documents[result.index],
181
+ score: result.relevance_score,
182
+ }));
127
183
  }
128
184
 
129
185
  /**
@@ -0,0 +1,302 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared helpers for the built-in connectors: AbortSignal handling, retry
5
+ * classification and backoff, and API key resolution.
6
+ *
7
+ * Everything here must run on Node 18 (no AbortSignal.any, no crypto.hash).
8
+ */
9
+
10
+ /** HTTP status codes that are worth retrying. */
11
+ const RETRYABLE_STATUS = new Set([408, 409, 425, 429]);
12
+
13
+ /** Error codes produced by Node, axios and undici for transient network failures. */
14
+ const RETRYABLE_CODES = new Set([
15
+ 'ECONNRESET',
16
+ 'ECONNREFUSED',
17
+ 'ETIMEDOUT',
18
+ 'ECONNABORTED',
19
+ 'EPIPE',
20
+ 'EAI_AGAIN',
21
+ 'ENETUNREACH',
22
+ 'ENETDOWN',
23
+ 'EHOSTUNREACH',
24
+ 'ERR_NETWORK',
25
+ ]);
26
+
27
+ /** Error names (openai SDK) that signal a connection-level failure. */
28
+ const RETRYABLE_NAMES = new Set([
29
+ 'APIConnectionError',
30
+ 'APIConnectionTimeoutError',
31
+ ]);
32
+
33
+ /**
34
+ * Create an AbortError for the given signal.
35
+ * Reuses signal.reason when it already is an AbortError.
36
+ *
37
+ * @param {AbortSignal} [signal]
38
+ * @returns {Error} Error with name 'AbortError' and code 'ABORT_ERR'
39
+ */
40
+ function createAbortError(signal) {
41
+ const reason = signal ? signal.reason : undefined;
42
+ if (reason instanceof Error && reason.name === 'AbortError') {
43
+ return reason;
44
+ }
45
+ const err = new Error('The operation was aborted');
46
+ err.name = 'AbortError';
47
+ err.code = 'ABORT_ERR';
48
+ if (reason !== undefined) err.cause = reason;
49
+ return err;
50
+ }
51
+
52
+ /**
53
+ * @param {*} err
54
+ * @returns {boolean} True when err represents a cancellation.
55
+ */
56
+ function isAbortError(err) {
57
+ if (!err) return false;
58
+ return (
59
+ err.name === 'AbortError' ||
60
+ err.code === 'ABORT_ERR' ||
61
+ err.name === 'CanceledError' ||
62
+ err.code === 'ERR_CANCELED' ||
63
+ err.name === 'APIUserAbortError'
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Throw an AbortError when the signal is already aborted.
69
+ * @param {AbortSignal} [signal]
70
+ */
71
+ function throwIfAborted(signal) {
72
+ if (signal && signal.aborted) {
73
+ throw createAbortError(signal);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Race a promise against an AbortSignal so callers reject promptly even when
79
+ * the underlying SDK or HTTP client ignores the signal. The abort listener is
80
+ * removed once the promise settles.
81
+ *
82
+ * @template T
83
+ * @param {Promise<T>|(() => Promise<T>)} work - Promise or function returning one
84
+ * @param {AbortSignal} [signal]
85
+ * @returns {Promise<T>}
86
+ */
87
+ function raceAbort(work, signal) {
88
+ if (!signal) {
89
+ return typeof work === 'function' ? Promise.resolve().then(work) : work;
90
+ }
91
+ if (signal.aborted) {
92
+ return Promise.reject(createAbortError(signal));
93
+ }
94
+ const promise =
95
+ typeof work === 'function' ? Promise.resolve().then(work) : work;
96
+ return new Promise((resolve, reject) => {
97
+ const onAbort = () => reject(createAbortError(signal));
98
+ signal.addEventListener('abort', onAbort, { once: true });
99
+ promise.then(
100
+ (value) => {
101
+ signal.removeEventListener('abort', onAbort);
102
+ resolve(value);
103
+ },
104
+ (err) => {
105
+ signal.removeEventListener('abort', onAbort);
106
+ // Normalise SDK/axios cancellation errors to AbortError.
107
+ reject(
108
+ signal.aborted || isAbortError(err) ? createAbortError(signal) : err,
109
+ );
110
+ },
111
+ );
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Sleep that rejects with AbortError as soon as the signal aborts.
117
+ * @param {number} ms
118
+ * @param {AbortSignal} [signal]
119
+ * @returns {Promise<void>}
120
+ */
121
+ function abortableSleep(ms, signal) {
122
+ if (signal && signal.aborted) {
123
+ return Promise.reject(createAbortError(signal));
124
+ }
125
+ return new Promise((resolve, reject) => {
126
+ let onAbort = null;
127
+ const timer = setTimeout(() => {
128
+ if (signal && onAbort) signal.removeEventListener('abort', onAbort);
129
+ resolve();
130
+ }, ms);
131
+ if (signal) {
132
+ onAbort = () => {
133
+ clearTimeout(timer);
134
+ reject(createAbortError(signal));
135
+ };
136
+ signal.addEventListener('abort', onAbort, { once: true });
137
+ }
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Extract the HTTP status from an openai SDK error or an axios error.
143
+ * @param {*} err
144
+ * @returns {number|undefined}
145
+ */
146
+ function getErrorStatus(err) {
147
+ if (!err) return undefined;
148
+ const status =
149
+ (err.response && err.response.status) ?? err.status ?? err.statusCode;
150
+ return typeof status === 'number' ? status : undefined;
151
+ }
152
+
153
+ /**
154
+ * Read a header from a plain object, an AxiosHeaders instance or a fetch Headers.
155
+ * @param {*} headers
156
+ * @param {string} name - Lower-case header name
157
+ * @returns {string|undefined}
158
+ */
159
+ function getHeader(headers, name) {
160
+ if (!headers) return undefined;
161
+ if (typeof headers.get === 'function') {
162
+ const value = headers.get(name);
163
+ if (value != null) return String(value);
164
+ }
165
+ for (const key of Object.keys(headers)) {
166
+ if (key.toLowerCase() === name && headers[key] != null) {
167
+ return String(headers[key]);
168
+ }
169
+ }
170
+ return undefined;
171
+ }
172
+
173
+ /**
174
+ * Parse Retry-After / retry-after-ms from an error's response headers.
175
+ *
176
+ * @param {*} err
177
+ * @param {number} [now=Date.now()]
178
+ * @returns {number|undefined} Delay in milliseconds, or undefined when absent
179
+ */
180
+ function getRetryAfterMs(err, now = Date.now()) {
181
+ if (!err) return undefined;
182
+ const headers = (err.response && err.response.headers) || err.headers;
183
+ const ms = getHeader(headers, 'retry-after-ms');
184
+ if (ms !== undefined && /^\d+(\.\d+)?$/.test(ms.trim())) {
185
+ return Math.max(0, Number(ms));
186
+ }
187
+ const value = getHeader(headers, 'retry-after');
188
+ if (value === undefined) return undefined;
189
+ const trimmed = value.trim();
190
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
191
+ return Math.max(0, Number(trimmed) * 1000);
192
+ }
193
+ const date = Date.parse(trimmed);
194
+ if (!Number.isNaN(date)) return Math.max(0, date - now);
195
+ return undefined;
196
+ }
197
+
198
+ /**
199
+ * Default retry classification: network failures and HTTP
200
+ * 408/409/425/429/5xx are retried; aborts, other 4xx (400, 401, 403, 404,
201
+ * 422, ...) and errors without a status or network code are not.
202
+ *
203
+ * @param {*} err
204
+ * @returns {boolean}
205
+ */
206
+ function isRetryableError(err) {
207
+ if (!err || isAbortError(err)) return false;
208
+ if (err.code === 'MISSING_API_KEY') return false;
209
+ const status = getErrorStatus(err);
210
+ if (status !== undefined) {
211
+ return RETRYABLE_STATUS.has(status) || (status >= 500 && status <= 599);
212
+ }
213
+ const code = err.code || (err.cause && err.cause.code);
214
+ if (typeof code === 'string') {
215
+ if (RETRYABLE_CODES.has(code) || code.startsWith('UND_ERR_')) return true;
216
+ }
217
+ if (RETRYABLE_NAMES.has(err.name)) return true;
218
+ // fetch() network failures surface as TypeError('fetch failed').
219
+ if (err.name === 'TypeError' && /fetch failed/i.test(err.message || '')) {
220
+ return true;
221
+ }
222
+ return false;
223
+ }
224
+
225
+ /**
226
+ * Compute the delay before the next retry attempt.
227
+ * Honours Retry-After (capped at maxDelay); otherwise uses exponential backoff
228
+ * with full jitter: random() * min(maxDelay, baseDelay * 2^attempt).
229
+ *
230
+ * @param {*} err - The error that triggered the retry
231
+ * @param {number} attempt - Zero-based retry attempt
232
+ * @param {object} [opts]
233
+ * @param {number} [opts.baseDelay=1000]
234
+ * @param {number} [opts.maxDelay=30000]
235
+ * @param {() => number} [opts.random=Math.random]
236
+ * @returns {number} Delay in milliseconds
237
+ */
238
+ function computeRetryDelay(err, attempt, opts = {}) {
239
+ const baseDelay = opts.baseDelay ?? 1000;
240
+ const maxDelay = opts.maxDelay ?? 30000;
241
+ const random = opts.random || Math.random;
242
+ const retryAfter = getRetryAfterMs(err);
243
+ if (retryAfter !== undefined) {
244
+ return Math.min(retryAfter, maxDelay);
245
+ }
246
+ const exp = Math.min(maxDelay, baseDelay * 2 ** attempt);
247
+ return Math.floor(random() * exp);
248
+ }
249
+
250
+ /**
251
+ * Resolve an API key at construction time. An explicit option (even '')
252
+ * wins over the environment variable.
253
+ *
254
+ * @param {object} options - Connector options
255
+ * @param {string} envVar - Environment variable name
256
+ * @returns {string}
257
+ */
258
+ function resolveApiKey(options, envVar) {
259
+ if (options && options.apiKey !== undefined) return options.apiKey || '';
260
+ return process.env[envVar] || '';
261
+ }
262
+
263
+ /**
264
+ * Error thrown when a provider call is attempted without an API key.
265
+ * @param {string} provider - Display name, e.g. 'OpenAI'
266
+ * @param {string} envVar - Environment variable name
267
+ * @returns {Error}
268
+ */
269
+ function missingApiKeyError(provider, envVar) {
270
+ const err = new Error(
271
+ `${provider} API key required. Set ${envVar} or pass the apiKey option.`,
272
+ );
273
+ err.code = 'MISSING_API_KEY';
274
+ return err;
275
+ }
276
+
277
+ /**
278
+ * Build request options carrying the signal only when one was supplied, so
279
+ * calls without a signal look exactly as before.
280
+ * @param {AbortSignal} [signal]
281
+ * @returns {object}
282
+ */
283
+ function signalOptions(signal) {
284
+ return signal ? { signal } : {};
285
+ }
286
+
287
+ module.exports = {
288
+ createAbortError,
289
+ isAbortError,
290
+ throwIfAborted,
291
+ raceAbort,
292
+ abortableSleep,
293
+ getErrorStatus,
294
+ getRetryAfterMs,
295
+ isRetryableError,
296
+ computeRetryDelay,
297
+ resolveApiKey,
298
+ missingApiKeyError,
299
+ signalOptions,
300
+ RETRYABLE_STATUS,
301
+ RETRYABLE_CODES,
302
+ };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { BaseConnector } = require('./base-connector');
4
4
  const { tokenize } = require('../evaluate/scoring');
5
+ const { throwIfAborted } = require('./connector-utils');
5
6
 
6
7
  /**
7
8
  * Default configuration for the local TF-IDF embedder.
@@ -61,7 +62,9 @@ class LocalEmbedder extends BaseConnector {
61
62
  const idfEntries = [];
62
63
  for (const [token, freq] of docFreq) {
63
64
  if (freq < this.config.minDocFrequency) continue;
64
- const idfValue = Math.log(this.docCount / (freq + 1));
65
+ // Smoothed IDF stays positive; log(N / (df + 1)) is <= 0 for every
66
+ // token in small corpora (N <= 2), which produced all-zero vectors.
67
+ const idfValue = Math.log(1 + this.docCount / freq);
65
68
  idfEntries.push([token, idfValue]);
66
69
  }
67
70
 
@@ -86,11 +89,16 @@ class LocalEmbedder extends BaseConnector {
86
89
 
87
90
  /**
88
91
  * Compute a TF-IDF vector for the given text.
89
- * The vector length equals the vocabulary size.
92
+ * The vector length equals the vocabulary size. Synchronous; when
93
+ * options.signal is already aborted it throws an AbortError (which an
94
+ * awaiting caller sees as a rejection).
90
95
  * @param {string} text - Text to embed
96
+ * @param {Object} [options={}]
97
+ * @param {AbortSignal} [options.signal] - Cancellation signal
91
98
  * @returns {number[]} L2-normalized TF-IDF vector
92
99
  */
93
- embed(text) {
100
+ embed(text, options = {}) {
101
+ throwIfAborted(options && options.signal);
94
102
  const vocabSize = this.vocabulary.size;
95
103
 
96
104
  if (vocabSize === 0) {
@@ -137,10 +145,40 @@ class LocalEmbedder extends BaseConnector {
137
145
  * Compute a TF-IDF vector for a query string.
138
146
  * Alias for {@link embed}.
139
147
  * @param {string} query - Query text to embed
148
+ * @param {Object} [options={}] - Same as embed()
140
149
  * @returns {number[]} L2-normalized TF-IDF vector
141
150
  */
142
- embedQuery(query) {
143
- return this.embed(query);
151
+ embedQuery(query, options = {}) {
152
+ return this.embed(query, options);
153
+ }
154
+
155
+ /**
156
+ * Serializable vocabulary and IDF weights, so a trained embedder can be
157
+ * restored in another process (e.g. by FileVectorStore-backed CLI queries).
158
+ *
159
+ * @returns {{docCount: number, vocabulary: Array<[string, number]>, idf: Array<[string, number]>}}
160
+ */
161
+ getState() {
162
+ return {
163
+ docCount: this.docCount,
164
+ vocabulary: [...this.vocabulary],
165
+ idf: [...this.idf],
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Restore state produced by getState().
171
+ * @param {{docCount: number, vocabulary: Array<[string, number]>, idf: Array<[string, number]>}} state
172
+ */
173
+ setState(state) {
174
+ this.docCount = state.docCount || 0;
175
+ this.vocabulary = new Map(state.vocabulary || []);
176
+ this.idf = new Map(state.idf || []);
177
+ }
178
+
179
+ /** @returns {number} Vector length produced by embed() once trained */
180
+ get dimensions() {
181
+ return this.vocabulary.size;
144
182
  }
145
183
 
146
184
  /**