@devilsdev/rag-pipeline-utils 2.3.1 → 2.4.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 (160) hide show
  1. package/README.md +293 -369
  2. package/contracts/loader-contract.json +11 -6
  3. package/contracts/reranker-contract.json +18 -26
  4. package/dist/index.cjs +21190 -47
  5. package/dist/index.d.ts +298 -68
  6. package/dist/index.mjs +9 -76
  7. package/package.json +20 -8
  8. package/src/agentic/agentic-pipeline.js +235 -0
  9. package/src/agentic/iterative-retriever.js +213 -0
  10. package/src/agentic/query-planner.js +208 -0
  11. package/src/agentic/self-critique.js +102 -0
  12. package/src/ai/adaptive-retrieval.js +0 -3
  13. package/src/ai/federated-learning.js +0 -3
  14. package/src/ai/federation.js +1 -6
  15. package/src/ai/index.js +9 -20
  16. package/src/ai/model-training.js +0 -3
  17. package/src/ai/multimodal-processing.js +0 -3
  18. package/src/ai/multimodal.js +1 -6
  19. package/src/ai/orchestrator.js +1 -6
  20. package/src/ai/retrieval-engine.js +1 -6
  21. package/src/chunking/chunking-engine.js +142 -0
  22. package/src/chunking/strategies/fixed-size.js +54 -0
  23. package/src/chunking/strategies/recursive.js +127 -0
  24. package/src/chunking/strategies/semantic.js +128 -0
  25. package/src/chunking/strategies/sentence.js +32 -0
  26. package/src/chunking/strategies/structure-aware.js +138 -0
  27. package/src/citation/citation-tracker.js +161 -0
  28. package/src/citation/hallucination-detector.js +74 -0
  29. package/src/citation/source-mapper.js +95 -0
  30. package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
  31. package/src/cli/commands/ai-ml/federated-commands.js +207 -0
  32. package/src/cli/commands/ai-ml/index.js +17 -0
  33. package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
  34. package/src/cli/commands/ai-ml/shared.js +51 -0
  35. package/src/cli/commands/ai-ml/training-commands.js +228 -0
  36. package/src/cli/commands/ai-ml.js +176 -757
  37. package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
  38. package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
  39. package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
  40. package/src/cli/commands/plugin-hub/review-commands.js +78 -0
  41. package/src/cli/commands/plugin-hub.js +106 -782
  42. package/src/cli/diagnostics/config-checker.js +81 -0
  43. package/src/cli/diagnostics/dependency-checker.js +69 -0
  44. package/src/cli/diagnostics/environment-checker.js +45 -0
  45. package/src/cli/diagnostics/performance-checker.js +51 -0
  46. package/src/cli/diagnostics/plugin-checker.js +106 -0
  47. package/src/cli/diagnostics/security-checker.js +61 -0
  48. package/src/cli/doctor-command.js +70 -369
  49. package/src/cli/enhanced-cli-commands.js +162 -706
  50. package/src/cli/handlers/config-handler.js +126 -0
  51. package/src/cli/handlers/diagnostics-handler.js +113 -0
  52. package/src/cli/handlers/ingest-handler.js +42 -0
  53. package/src/cli/handlers/init-handler.js +51 -0
  54. package/src/cli/handlers/query-handler.js +45 -0
  55. package/src/cli/handlers/run-handler.js +181 -0
  56. package/src/cli/interactive-wizard.js +99 -723
  57. package/src/cli/shared/cli-utils.js +89 -0
  58. package/src/cli/wizard/configuration.js +148 -0
  59. package/src/cli/wizard/observability.js +79 -0
  60. package/src/cli/wizard/performance.js +76 -0
  61. package/src/cli/wizard/plugin-selection.js +297 -0
  62. package/src/cli/wizard/preview-save.js +172 -0
  63. package/src/cli/wizard/project-setup.js +66 -0
  64. package/src/config/memory-optimization.js +3 -3
  65. package/src/connectors/anthropic-connector.js +179 -0
  66. package/src/connectors/base-connector.js +94 -0
  67. package/src/connectors/cohere-connector.js +143 -0
  68. package/src/connectors/local-embedder.js +171 -0
  69. package/src/connectors/memory-retriever.js +150 -0
  70. package/src/connectors/ollama-connector.js +205 -0
  71. package/src/connectors/openai-connector.js +173 -0
  72. package/src/core/create-pipeline.js +21 -1
  73. package/src/core/observability/tracing.js +2 -1
  74. package/src/core/pipeline-factory.js +0 -1
  75. package/src/core/plugin-registry.js +14 -14
  76. package/src/cost/cost-calculator.js +146 -0
  77. package/src/cost/cost-tracker.js +260 -0
  78. package/src/cost/token-budget.js +245 -0
  79. package/src/dag/core/execution-scheduler.js +2 -1
  80. package/src/dag/core/topology-validation.js +0 -3
  81. package/src/dag/dag-engine.js +3 -5
  82. package/src/dag/dag-node.js +0 -1
  83. package/src/dag/enhanced-dag-engine.js +171 -99
  84. package/src/dashboard/dashboard-generator.js +642 -0
  85. package/src/dashboard/metrics-aggregator.js +409 -0
  86. package/src/debug/execution-tracer.js +210 -0
  87. package/src/debug/trace-inspector.js +281 -0
  88. package/src/dev/dev-server.js +5 -5
  89. package/src/dev/hot-reload.js +3 -2
  90. package/src/dx/integration-templates.js +0 -3
  91. package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
  92. package/src/ecosystem/plugin-hub.js +2 -2
  93. package/src/enterprise/audit-logging.js +2 -2
  94. package/src/enterprise/sso-integration.js +52 -32
  95. package/src/evaluate/context-metrics.js +127 -0
  96. package/src/evaluate/evaluator.js +4 -2
  97. package/src/evaluate/faithfulness.js +117 -0
  98. package/src/evaluate/groundedness.js +60 -0
  99. package/src/evaluate/pipeline-evaluator.js +156 -0
  100. package/src/evaluate/relevance.js +129 -0
  101. package/src/evaluate/scoring.js +27 -0
  102. package/src/graph/entity-extractor.js +183 -0
  103. package/src/graph/graph-index.js +113 -0
  104. package/src/graph/graph-retriever.js +244 -0
  105. package/src/graph/knowledge-graph.js +256 -0
  106. package/src/guardrails/guardrails-pipeline.js +136 -0
  107. package/src/guardrails/post-generation-guard.js +170 -0
  108. package/src/guardrails/pre-retrieval-guard.js +161 -0
  109. package/src/guardrails/retrieval-guard.js +126 -0
  110. package/src/index.js +255 -9
  111. package/src/loader/html-loader.js +4 -15
  112. package/src/loader/markdown-loader.js +4 -15
  113. package/src/mcp/mcp-server.js +199 -0
  114. package/src/mcp/mcp-tool-builder.js +121 -0
  115. package/src/observability/audit-logger.js +0 -2
  116. package/src/observability/event-logger.js +2 -1
  117. package/src/observability/security-monitor.js +0 -2
  118. package/src/observability/telemetry.js +0 -2
  119. package/src/reranker/cascade-reranker.js +79 -0
  120. package/src/reranker/embedding-reranker.js +124 -0
  121. package/src/reranker/llm-reranker.js +6 -3
  122. package/src/reranker/scoring-reranker.js +123 -0
  123. package/src/retrieval/bm25-search.js +157 -0
  124. package/src/retrieval/hybrid-retriever.js +111 -0
  125. package/src/retrieval/rank-fusion.js +62 -0
  126. package/src/security/jwt-config.js +4 -3
  127. package/src/security/jwt-validator.js +2 -1
  128. package/src/security/plugin-sandbox.js +0 -3
  129. package/src/security/plugin-signature-verifier.js +4 -6
  130. package/src/security/validator.js +0 -2
  131. package/src/streaming/embedding-stream.js +197 -0
  132. package/src/streaming/sse-adapter.js +168 -0
  133. package/src/streaming/stream-router.js +210 -0
  134. package/src/streaming/streaming-embedder.js +231 -0
  135. package/src/streaming/websocket-adapter.js +171 -0
  136. package/src/templates/scaffold.js +147 -0
  137. package/src/templates/template-registry.js +272 -0
  138. package/src/utils/contract-validator.js +2 -1
  139. package/src/utils/input-sanitizer.js +80 -99
  140. package/src/utils/performance-monitor.js +0 -3
  141. package/src/utils/plugin-scaffolder.js +9 -16
  142. package/src/utils/rate-limiter.js +0 -1
  143. package/src/utils/retry-policy.js +0 -3
  144. package/src/utils/safe-expression-evaluator.js +0 -3
  145. package/src/utils/secure-logger-config.js +2 -4
  146. package/src/utils/secure-logging.js +0 -3
  147. package/src/utils/structured-logger.js +0 -3
  148. package/src/ai/index-fixed.js +0 -509
  149. package/src/ai/module-template.js +0 -37
  150. package/src/ci/workflow-analyzer.js +0 -71
  151. package/src/cli/plugin-marketplace-commands.js +0 -869
  152. package/src/dev/README.md +0 -126
  153. package/src/dev/examples/hot-reload-usage.js +0 -355
  154. package/src/mocks/openai-embedder.js +0 -119
  155. package/src/mocks/openai-llm.js +0 -77
  156. package/src/mocks/pdf-loader.js +0 -21
  157. package/src/mocks/pinecone-retriever.js +0 -106
  158. package/src/scripts/script-utilities.js +0 -36
  159. package/src/utils/ci/diagnostic-reporter.js +0 -57
  160. package/src/utils/examples/error-formatter-usage.js +0 -351
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ const { BaseConnector } = require('./base-connector');
4
+ const { tokenize } = require('../evaluate/scoring');
5
+
6
+ /**
7
+ * Default configuration for the local TF-IDF embedder.
8
+ * @type {Object}
9
+ */
10
+ const DEFAULT_CONFIG = {
11
+ maxVocabularySize: 10000,
12
+ minDocFrequency: 1,
13
+ };
14
+
15
+ /**
16
+ * TF-IDF based embedder for fully offline use.
17
+ * No external API required -- builds a vocabulary from training documents
18
+ * and produces sparse TF-IDF vectors.
19
+ * @extends BaseConnector
20
+ */
21
+ class LocalEmbedder extends BaseConnector {
22
+ /**
23
+ * @param {Object} options
24
+ * @param {number} [options.maxVocabularySize=10000] - Maximum vocabulary size
25
+ * @param {number} [options.minDocFrequency=1] - Minimum document frequency for a token to be included
26
+ */
27
+ constructor(options = {}) {
28
+ super({ name: 'local-embedder', ...options });
29
+ this.config = { ...DEFAULT_CONFIG, ...options };
30
+ /** @type {Map<string, number>} token -> vocabulary index */
31
+ this.vocabulary = new Map();
32
+ /** @type {Map<string, number>} token -> IDF weight */
33
+ this.idf = new Map();
34
+ /** @type {number} */
35
+ this.docCount = 0;
36
+ this.connected = true; // always available
37
+ }
38
+
39
+ /**
40
+ * Build vocabulary and IDF weights from a corpus of documents.
41
+ * @param {Array<string|{ content?: string, text?: string }>} documents - Training documents
42
+ */
43
+ train(documents) {
44
+ const docs = documents.map((doc) => {
45
+ if (typeof doc === 'string') return doc;
46
+ return doc.content || doc.text || String(doc);
47
+ });
48
+
49
+ this.docCount = docs.length;
50
+
51
+ // Count document frequency for each token
52
+ const docFreq = new Map();
53
+ for (const doc of docs) {
54
+ const tokens = new Set(tokenize(doc));
55
+ for (const token of tokens) {
56
+ docFreq.set(token, (docFreq.get(token) || 0) + 1);
57
+ }
58
+ }
59
+
60
+ // Compute IDF for tokens that meet the minimum document frequency
61
+ const idfEntries = [];
62
+ for (const [token, freq] of docFreq) {
63
+ if (freq < this.config.minDocFrequency) continue;
64
+ const idfValue = Math.log(this.docCount / (freq + 1));
65
+ idfEntries.push([token, idfValue]);
66
+ }
67
+
68
+ // Sort by IDF descending and take top maxVocabularySize tokens
69
+ idfEntries.sort((a, b) => b[1] - a[1]);
70
+ const topEntries = idfEntries.slice(0, this.config.maxVocabularySize);
71
+
72
+ // Build vocabulary map and IDF map
73
+ this.vocabulary.clear();
74
+ this.idf.clear();
75
+ for (let i = 0; i < topEntries.length; i++) {
76
+ const [token, idfValue] = topEntries[i];
77
+ this.vocabulary.set(token, i);
78
+ this.idf.set(token, idfValue);
79
+ }
80
+
81
+ this.emit('trained', {
82
+ vocabSize: this.vocabulary.size,
83
+ docCount: this.docCount,
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Compute a TF-IDF vector for the given text.
89
+ * The vector length equals the vocabulary size.
90
+ * @param {string} text - Text to embed
91
+ * @returns {number[]} L2-normalized TF-IDF vector
92
+ */
93
+ embed(text) {
94
+ const vocabSize = this.vocabulary.size;
95
+
96
+ if (vocabSize === 0) {
97
+ this.emit('warning', {
98
+ message: 'LocalEmbedder has not been trained yet',
99
+ });
100
+ return new Array(1).fill(0);
101
+ }
102
+
103
+ const tokens = tokenize(text);
104
+
105
+ // Count term frequency
106
+ const tf = new Map();
107
+ for (const token of tokens) {
108
+ tf.set(token, (tf.get(token) || 0) + 1);
109
+ }
110
+
111
+ // Build TF-IDF vector
112
+ const vector = new Array(vocabSize).fill(0);
113
+ for (const [token, freq] of tf) {
114
+ const idx = this.vocabulary.get(token);
115
+ if (idx !== undefined) {
116
+ vector[idx] = freq * (this.idf.get(token) || 0);
117
+ }
118
+ }
119
+
120
+ // L2 normalize
121
+ let magnitude = 0;
122
+ for (let i = 0; i < vector.length; i++) {
123
+ magnitude += vector[i] * vector[i];
124
+ }
125
+ magnitude = Math.sqrt(magnitude);
126
+
127
+ if (magnitude > 0) {
128
+ for (let i = 0; i < vector.length; i++) {
129
+ vector[i] /= magnitude;
130
+ }
131
+ }
132
+
133
+ return vector;
134
+ }
135
+
136
+ /**
137
+ * Compute a TF-IDF vector for a query string.
138
+ * Alias for {@link embed}.
139
+ * @param {string} query - Query text to embed
140
+ * @returns {number[]} L2-normalized TF-IDF vector
141
+ */
142
+ embedQuery(query) {
143
+ return this.embed(query);
144
+ }
145
+
146
+ /**
147
+ * Connect the embedder (always succeeds since it is local).
148
+ * @returns {Promise<void>}
149
+ */
150
+ async connect() {
151
+ this.connected = true;
152
+ }
153
+
154
+ /**
155
+ * Check whether the embedder is healthy (vocabulary has been trained).
156
+ * @returns {Promise<boolean>}
157
+ */
158
+ async isHealthy() {
159
+ return this.vocabulary.size > 0;
160
+ }
161
+
162
+ /**
163
+ * Get the current vocabulary size.
164
+ * @returns {number}
165
+ */
166
+ getVocabularySize() {
167
+ return this.vocabulary.size;
168
+ }
169
+ }
170
+
171
+ module.exports = { LocalEmbedder, DEFAULT_CONFIG };
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ const { BaseConnector } = require('./base-connector');
4
+
5
+ /**
6
+ * Default configuration for the in-memory retriever.
7
+ * @type {Object}
8
+ */
9
+ const DEFAULT_CONFIG = {
10
+ maxDocuments: 100000,
11
+ };
12
+
13
+ /**
14
+ * In-memory vector store with cosine similarity retrieval.
15
+ * Stores document vectors and retrieves the most similar ones to a query vector.
16
+ * @extends BaseConnector
17
+ */
18
+ class MemoryRetriever extends BaseConnector {
19
+ /**
20
+ * @param {Object} options
21
+ * @param {number} [options.maxDocuments=100000] - Maximum number of documents to store
22
+ */
23
+ constructor(options = {}) {
24
+ super({ name: 'memory-retriever', ...options });
25
+ this.config = { ...DEFAULT_CONFIG, ...options };
26
+ /** @type {Map<string, { vector: number[], metadata?: Object, content?: string }>} */
27
+ this.documents = new Map();
28
+ this.connected = true;
29
+ }
30
+
31
+ /**
32
+ * Store one or more vector documents.
33
+ * @param {Array<{ id: string, vector: number[], metadata?: Object, content?: string }>} vectors
34
+ * @returns {Promise<void>}
35
+ */
36
+ async store(vectors) {
37
+ for (const doc of vectors) {
38
+ this.documents.set(doc.id, {
39
+ vector: doc.vector,
40
+ metadata: doc.metadata || {},
41
+ content: doc.content || '',
42
+ });
43
+ }
44
+
45
+ if (this.documents.size > this.config.maxDocuments) {
46
+ this.emit('warning', {
47
+ message: `Document count (${this.documents.size}) exceeds maxDocuments (${this.config.maxDocuments})`,
48
+ });
49
+ }
50
+
51
+ this.emit('stored', { count: vectors.length, total: this.documents.size });
52
+ }
53
+
54
+ /**
55
+ * Retrieve the top-k most similar documents to a query vector.
56
+ * @param {number[]|{ query?: string, queryVector: number[], topK?: number }} queryOrObj
57
+ * Either a raw vector array or an object with queryVector and optional topK.
58
+ * @param {number} [k=10] - Number of results to return (used when queryOrObj is an array)
59
+ * @returns {Promise<Array<{ id: string, score: number, metadata: Object, content: string }>>}
60
+ */
61
+ async retrieve(queryOrObj, k = 10) {
62
+ let queryVector;
63
+ let topK = k;
64
+
65
+ if (Array.isArray(queryOrObj)) {
66
+ queryVector = queryOrObj;
67
+ } else {
68
+ queryVector = queryOrObj.queryVector;
69
+ topK = queryOrObj.topK || k;
70
+ }
71
+
72
+ const results = [];
73
+
74
+ for (const [id, doc] of this.documents) {
75
+ const score = this._cosineSimilarity(queryVector, doc.vector);
76
+ results.push({
77
+ id,
78
+ score,
79
+ metadata: doc.metadata,
80
+ content: doc.content,
81
+ });
82
+ }
83
+
84
+ results.sort((a, b) => b.score - a.score);
85
+
86
+ return results.slice(0, topK);
87
+ }
88
+
89
+ /**
90
+ * Compute cosine similarity between two vectors.
91
+ * @param {number[]} a - First vector
92
+ * @param {number[]} b - Second vector
93
+ * @returns {number} Cosine similarity in the range [-1, 1], or 0 for zero vectors
94
+ * @private
95
+ */
96
+ _cosineSimilarity(a, b) {
97
+ const len = Math.min(a.length, b.length);
98
+ let dot = 0;
99
+ let magA = 0;
100
+ let magB = 0;
101
+
102
+ for (let i = 0; i < len; i++) {
103
+ dot += a[i] * b[i];
104
+ magA += a[i] * a[i];
105
+ magB += b[i] * b[i];
106
+ }
107
+
108
+ const magnitude = Math.sqrt(magA) * Math.sqrt(magB);
109
+ if (magnitude === 0) return 0;
110
+
111
+ return dot / magnitude;
112
+ }
113
+
114
+ /**
115
+ * Remove all stored documents.
116
+ * @returns {Promise<void>}
117
+ */
118
+ async clear() {
119
+ this.documents.clear();
120
+ }
121
+
122
+ /**
123
+ * Get statistics about the store.
124
+ * @returns {{ documentCount: number, maxDocuments: number }}
125
+ */
126
+ getStats() {
127
+ return {
128
+ documentCount: this.documents.size,
129
+ maxDocuments: this.config.maxDocuments,
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Connect the retriever (always succeeds since it is in-memory).
135
+ * @returns {Promise<void>}
136
+ */
137
+ async connect() {
138
+ this.connected = true;
139
+ }
140
+
141
+ /**
142
+ * Check whether the retriever is healthy (always true).
143
+ * @returns {Promise<boolean>}
144
+ */
145
+ async isHealthy() {
146
+ return true;
147
+ }
148
+ }
149
+
150
+ module.exports = { MemoryRetriever, DEFAULT_CONFIG };
@@ -0,0 +1,205 @@
1
+ 'use strict';
2
+
3
+ const axios = require('axios');
4
+ const { BaseConnector } = require('./base-connector');
5
+
6
+ /**
7
+ * Default configuration for the Ollama connector.
8
+ * @type {Object}
9
+ */
10
+ const DEFAULT_CONFIG = {
11
+ baseURL: 'http://localhost:11434',
12
+ model: 'llama3',
13
+ embeddingModel: 'nomic-embed-text',
14
+ timeout: 60000,
15
+ temperature: 0.7,
16
+ };
17
+
18
+ /**
19
+ * Connector for Ollama local LLM server.
20
+ * Supports text generation (sync and streaming) and embeddings.
21
+ * @extends BaseConnector
22
+ */
23
+ class OllamaConnector extends BaseConnector {
24
+ /**
25
+ * @param {Object} options
26
+ * @param {string} [options.baseURL='http://localhost:11434'] - Ollama server URL
27
+ * @param {string} [options.model='llama3'] - Default generation model
28
+ * @param {string} [options.embeddingModel='nomic-embed-text'] - Default embedding model
29
+ * @param {number} [options.timeout=60000] - Request timeout in ms
30
+ * @param {number} [options.temperature=0.7] - Generation temperature
31
+ */
32
+ constructor(options = {}) {
33
+ super({ name: 'ollama', ...options });
34
+ this.config = { ...DEFAULT_CONFIG, ...options };
35
+ this.client = axios.create({
36
+ baseURL: this.config.baseURL,
37
+ timeout: this.config.timeout,
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Connect to the Ollama server by verifying it is reachable.
43
+ * @returns {Promise<void>}
44
+ * @throws {Error} If the Ollama server is not reachable
45
+ */
46
+ async connect() {
47
+ try {
48
+ await this.client.get('/api/tags');
49
+ await super.connect();
50
+ } catch (err) {
51
+ throw new Error(
52
+ `Cannot connect to Ollama at ${this.config.baseURL}. Is Ollama running? (ollama serve)`,
53
+ );
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Check whether the Ollama server is reachable.
59
+ * @returns {Promise<boolean>}
60
+ */
61
+ async isHealthy() {
62
+ try {
63
+ await this.client.get('/api/tags');
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Generate a response for the given query with optional context documents.
72
+ * @param {string} query - The user query
73
+ * @param {Array<Object|string>} [context=[]] - Context documents for RAG
74
+ * @param {Object} [options={}] - Override options
75
+ * @param {string} [options.model] - Override the generation model
76
+ * @param {number} [options.temperature] - Override temperature
77
+ * @returns {Promise<string>} Generated response text
78
+ */
79
+ async generate(query, context = [], options = {}) {
80
+ const model = options.model || this.config.model;
81
+ const temperature = options.temperature ?? this.config.temperature;
82
+ const prompt = this._buildPrompt(query, context);
83
+
84
+ const result = await this.withRetry(async () => {
85
+ const response = await this.client.post('/api/generate', {
86
+ model,
87
+ prompt,
88
+ stream: false,
89
+ options: { temperature },
90
+ });
91
+ return response.data.response;
92
+ });
93
+
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * Stream a response token-by-token for the given query.
99
+ * @param {string} query - The user query
100
+ * @param {Array<Object|string>} [context=[]] - Context documents for RAG
101
+ * @param {Object} [options={}] - Override options
102
+ * @param {string} [options.model] - Override the generation model
103
+ * @param {number} [options.temperature] - Override temperature
104
+ * @yields {{ token: string, done: boolean }}
105
+ */
106
+ async *generateStream(query, context = [], options = {}) {
107
+ const model = options.model || this.config.model;
108
+ const temperature = options.temperature ?? this.config.temperature;
109
+ const prompt = this._buildPrompt(query, context);
110
+
111
+ let response;
112
+ try {
113
+ response = await this.client.post(
114
+ '/api/generate',
115
+ {
116
+ model,
117
+ prompt,
118
+ stream: true,
119
+ options: { temperature },
120
+ },
121
+ { responseType: 'stream' },
122
+ );
123
+ } catch (err) {
124
+ throw new Error(`Ollama streaming request failed: ${err.message}`);
125
+ }
126
+
127
+ let buffer = '';
128
+ for await (const chunk of response.data) {
129
+ buffer += chunk.toString();
130
+ const lines = buffer.split('\n');
131
+ // Keep the last potentially incomplete line in the buffer
132
+ buffer = lines.pop() || '';
133
+
134
+ for (const line of lines) {
135
+ const trimmed = line.trim();
136
+ if (!trimmed) continue;
137
+ try {
138
+ const parsed = JSON.parse(trimmed);
139
+ yield { token: parsed.response || '', done: !!parsed.done };
140
+ } catch {
141
+ // Skip malformed lines
142
+ }
143
+ }
144
+ }
145
+
146
+ // Process any remaining data in the buffer
147
+ if (buffer.trim()) {
148
+ try {
149
+ const parsed = JSON.parse(buffer.trim());
150
+ yield { token: parsed.response || '', done: !!parsed.done };
151
+ } catch {
152
+ // Skip malformed trailing data
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Generate an embedding vector for the given text.
159
+ * @param {string} text - Text to embed
160
+ * @returns {Promise<number[]>} Embedding vector
161
+ */
162
+ async embed(text) {
163
+ const result = await this.withRetry(async () => {
164
+ const response = await this.client.post('/api/embeddings', {
165
+ model: this.config.embeddingModel,
166
+ prompt: text,
167
+ });
168
+ return response.data.embedding;
169
+ });
170
+
171
+ return result;
172
+ }
173
+
174
+ /**
175
+ * Generate an embedding vector for a query string.
176
+ * Alias for {@link embed}.
177
+ * @param {string} query - Query text to embed
178
+ * @returns {Promise<number[]>} Embedding vector
179
+ */
180
+ async embedQuery(query) {
181
+ return this.embed(query);
182
+ }
183
+
184
+ /**
185
+ * Build a prompt string incorporating context documents.
186
+ * @param {string} query - The user query
187
+ * @param {Array<Object|string>} context - Context documents
188
+ * @returns {string} Formatted prompt
189
+ * @private
190
+ */
191
+ _buildPrompt(query, context) {
192
+ if (!context || context.length === 0) return query;
193
+
194
+ const contextText = context
195
+ .map((doc) => {
196
+ const text = doc.content || doc.text || String(doc);
197
+ return text;
198
+ })
199
+ .join('\n\n');
200
+
201
+ return `Context:\n${contextText}\n\nQuestion: ${query}\n\nAnswer:`;
202
+ }
203
+ }
204
+
205
+ module.exports = { OllamaConnector, DEFAULT_CONFIG };
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const { BaseConnector } = require('./base-connector');
4
+
5
+ /**
6
+ * Default configuration for the OpenAI connector.
7
+ * @type {Object}
8
+ */
9
+ const DEFAULT_CONFIG = {
10
+ model: 'gpt-4o-mini',
11
+ embeddingModel: 'text-embedding-3-small',
12
+ temperature: 0.7,
13
+ maxTokens: 4096,
14
+ apiKey: process.env.OPENAI_API_KEY || '',
15
+ };
16
+
17
+ /**
18
+ * Full LLM + embedder connector for the OpenAI API.
19
+ * Supports chat completions (with streaming) and text embeddings.
20
+ *
21
+ * @extends BaseConnector
22
+ */
23
+ class OpenAIConnector extends BaseConnector {
24
+ /**
25
+ * @param {Object} [options]
26
+ * @param {string} [options.model='gpt-4o-mini'] - Chat model to use
27
+ * @param {string} [options.embeddingModel='text-embedding-3-small'] - Embedding model
28
+ * @param {number} [options.temperature=0.7] - Sampling temperature
29
+ * @param {number} [options.maxTokens=4096] - Max tokens in completion
30
+ * @param {string} [options.apiKey] - OpenAI API key
31
+ */
32
+ constructor(options = {}) {
33
+ super({ name: 'openai', ...options });
34
+ this.config = { ...DEFAULT_CONFIG, ...options };
35
+ this._client = null; // lazy-loaded
36
+ }
37
+
38
+ /**
39
+ * Lazily initialise and return the OpenAI client.
40
+ * @returns {Object} OpenAI client instance
41
+ * @private
42
+ */
43
+ _getClient() {
44
+ if (!this._client) {
45
+ const OpenAI = require('openai');
46
+ this._client = new OpenAI({ apiKey: this.config.apiKey });
47
+ }
48
+ return this._client;
49
+ }
50
+
51
+ /**
52
+ * Connect to OpenAI. Verifies the API key is present.
53
+ * @returns {Promise<void>}
54
+ * @throws {Error} If no API key is configured
55
+ */
56
+ async connect() {
57
+ if (!this.config.apiKey) {
58
+ throw new Error(
59
+ 'OpenAI API key required. Set OPENAI_API_KEY or pass apiKey option.',
60
+ );
61
+ }
62
+ await super.connect();
63
+ }
64
+
65
+ /**
66
+ * Generate a chat completion using the provided query and optional RAG context.
67
+ *
68
+ * @param {string} query - The user query / prompt
69
+ * @param {Array<Object|string>} [context=[]] - Retrieved documents for RAG
70
+ * @param {Object} [options={}] - Override model, temperature, maxTokens per-call
71
+ * @returns {Promise<string>} The generated text
72
+ */
73
+ async generate(query, context = [], options = {}) {
74
+ return this.withRetry(async () => {
75
+ const client = this._getClient();
76
+ const messages = this._buildMessages(query, context);
77
+
78
+ const response = await client.chat.completions.create({
79
+ model: options.model || this.config.model,
80
+ messages,
81
+ temperature: options.temperature ?? this.config.temperature,
82
+ max_tokens: options.maxTokens ?? this.config.maxTokens,
83
+ });
84
+
85
+ return response.choices[0].message.content;
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Stream a chat completion token-by-token.
91
+ *
92
+ * @param {string} query - The user query / prompt
93
+ * @param {Array<Object|string>} [context=[]] - Retrieved documents for RAG
94
+ * @param {Object} [options={}] - Override model, temperature, maxTokens per-call
95
+ * @yields {{ token: string, done: boolean }}
96
+ */
97
+ async *generateStream(query, context = [], options = {}) {
98
+ const client = this._getClient();
99
+ const messages = this._buildMessages(query, context);
100
+
101
+ const stream = await client.chat.completions.create({
102
+ model: options.model || this.config.model,
103
+ messages,
104
+ temperature: options.temperature ?? this.config.temperature,
105
+ max_tokens: options.maxTokens ?? this.config.maxTokens,
106
+ stream: true,
107
+ });
108
+
109
+ for await (const chunk of stream) {
110
+ const token = chunk.choices[0]?.delta?.content || '';
111
+ yield { token, done: false };
112
+ }
113
+
114
+ yield { token: '', done: true };
115
+ }
116
+
117
+ /**
118
+ * Generate an embedding vector for the given text.
119
+ *
120
+ * @param {string} text - Input text to embed
121
+ * @returns {Promise<number[]>} Embedding vector
122
+ */
123
+ async embed(text) {
124
+ return this.withRetry(async () => {
125
+ const client = this._getClient();
126
+
127
+ const response = await client.embeddings.create({
128
+ model: this.config.embeddingModel,
129
+ input: text,
130
+ });
131
+
132
+ return response.data[0].embedding;
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Generate an embedding vector for a search query.
138
+ * Alias for {@link OpenAIConnector#embed}.
139
+ *
140
+ * @param {string} query - Query text to embed
141
+ * @returns {Promise<number[]>} Embedding vector
142
+ */
143
+ async embedQuery(query) {
144
+ return this.embed(query);
145
+ }
146
+
147
+ /**
148
+ * Build the messages array for a chat completion request.
149
+ *
150
+ * @param {string} query - User query
151
+ * @param {Array<Object|string>} context - RAG context documents
152
+ * @returns {Array<{ role: string, content: string }>}
153
+ * @private
154
+ */
155
+ _buildMessages(query, context) {
156
+ const messages = [];
157
+
158
+ if (context && context.length > 0) {
159
+ const contextText = context
160
+ .map((doc) => doc.content || doc.text || String(doc))
161
+ .join('\n\n');
162
+ messages.push({
163
+ role: 'system',
164
+ content: `Use the following context to answer the question:\n\n${contextText}`,
165
+ });
166
+ }
167
+
168
+ messages.push({ role: 'user', content: query });
169
+ return messages;
170
+ }
171
+ }
172
+
173
+ module.exports = { OpenAIConnector, DEFAULT_CONFIG };