@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.
- package/README.md +293 -369
- package/contracts/loader-contract.json +11 -6
- package/contracts/reranker-contract.json +18 -26
- package/dist/index.cjs +21190 -47
- package/dist/index.d.ts +298 -68
- package/dist/index.mjs +9 -76
- package/package.json +20 -8
- package/src/agentic/agentic-pipeline.js +235 -0
- package/src/agentic/iterative-retriever.js +213 -0
- package/src/agentic/query-planner.js +208 -0
- package/src/agentic/self-critique.js +102 -0
- package/src/ai/adaptive-retrieval.js +0 -3
- package/src/ai/federated-learning.js +0 -3
- package/src/ai/federation.js +1 -6
- package/src/ai/index.js +9 -20
- package/src/ai/model-training.js +0 -3
- package/src/ai/multimodal-processing.js +0 -3
- package/src/ai/multimodal.js +1 -6
- package/src/ai/orchestrator.js +1 -6
- package/src/ai/retrieval-engine.js +1 -6
- package/src/chunking/chunking-engine.js +142 -0
- package/src/chunking/strategies/fixed-size.js +54 -0
- package/src/chunking/strategies/recursive.js +127 -0
- package/src/chunking/strategies/semantic.js +128 -0
- package/src/chunking/strategies/sentence.js +32 -0
- package/src/chunking/strategies/structure-aware.js +138 -0
- package/src/citation/citation-tracker.js +161 -0
- package/src/citation/hallucination-detector.js +74 -0
- package/src/citation/source-mapper.js +95 -0
- package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
- package/src/cli/commands/ai-ml/federated-commands.js +207 -0
- package/src/cli/commands/ai-ml/index.js +17 -0
- package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
- package/src/cli/commands/ai-ml/shared.js +51 -0
- package/src/cli/commands/ai-ml/training-commands.js +228 -0
- package/src/cli/commands/ai-ml.js +176 -757
- package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
- package/src/cli/commands/plugin-hub/review-commands.js +78 -0
- package/src/cli/commands/plugin-hub.js +106 -782
- package/src/cli/diagnostics/config-checker.js +81 -0
- package/src/cli/diagnostics/dependency-checker.js +69 -0
- package/src/cli/diagnostics/environment-checker.js +45 -0
- package/src/cli/diagnostics/performance-checker.js +51 -0
- package/src/cli/diagnostics/plugin-checker.js +106 -0
- package/src/cli/diagnostics/security-checker.js +61 -0
- package/src/cli/doctor-command.js +70 -369
- package/src/cli/enhanced-cli-commands.js +162 -706
- package/src/cli/handlers/config-handler.js +126 -0
- package/src/cli/handlers/diagnostics-handler.js +113 -0
- package/src/cli/handlers/ingest-handler.js +42 -0
- package/src/cli/handlers/init-handler.js +51 -0
- package/src/cli/handlers/query-handler.js +45 -0
- package/src/cli/handlers/run-handler.js +181 -0
- package/src/cli/interactive-wizard.js +99 -723
- package/src/cli/shared/cli-utils.js +89 -0
- package/src/cli/wizard/configuration.js +148 -0
- package/src/cli/wizard/observability.js +79 -0
- package/src/cli/wizard/performance.js +76 -0
- package/src/cli/wizard/plugin-selection.js +297 -0
- package/src/cli/wizard/preview-save.js +172 -0
- package/src/cli/wizard/project-setup.js +66 -0
- package/src/config/memory-optimization.js +3 -3
- package/src/connectors/anthropic-connector.js +179 -0
- package/src/connectors/base-connector.js +94 -0
- package/src/connectors/cohere-connector.js +143 -0
- package/src/connectors/local-embedder.js +171 -0
- package/src/connectors/memory-retriever.js +150 -0
- package/src/connectors/ollama-connector.js +205 -0
- package/src/connectors/openai-connector.js +173 -0
- package/src/core/create-pipeline.js +21 -1
- package/src/core/observability/tracing.js +2 -1
- package/src/core/pipeline-factory.js +0 -1
- package/src/core/plugin-registry.js +14 -14
- package/src/cost/cost-calculator.js +146 -0
- package/src/cost/cost-tracker.js +260 -0
- package/src/cost/token-budget.js +245 -0
- package/src/dag/core/execution-scheduler.js +2 -1
- package/src/dag/core/topology-validation.js +0 -3
- package/src/dag/dag-engine.js +3 -5
- package/src/dag/dag-node.js +0 -1
- package/src/dag/enhanced-dag-engine.js +171 -99
- package/src/dashboard/dashboard-generator.js +642 -0
- package/src/dashboard/metrics-aggregator.js +409 -0
- package/src/debug/execution-tracer.js +210 -0
- package/src/debug/trace-inspector.js +281 -0
- package/src/dev/dev-server.js +5 -5
- package/src/dev/hot-reload.js +3 -2
- package/src/dx/integration-templates.js +0 -3
- package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
- package/src/ecosystem/plugin-hub.js +2 -2
- package/src/enterprise/audit-logging.js +2 -2
- package/src/enterprise/sso-integration.js +52 -32
- package/src/evaluate/context-metrics.js +127 -0
- package/src/evaluate/evaluator.js +4 -2
- package/src/evaluate/faithfulness.js +117 -0
- package/src/evaluate/groundedness.js +60 -0
- package/src/evaluate/pipeline-evaluator.js +156 -0
- package/src/evaluate/relevance.js +129 -0
- package/src/evaluate/scoring.js +27 -0
- package/src/graph/entity-extractor.js +183 -0
- package/src/graph/graph-index.js +113 -0
- package/src/graph/graph-retriever.js +244 -0
- package/src/graph/knowledge-graph.js +256 -0
- package/src/guardrails/guardrails-pipeline.js +136 -0
- package/src/guardrails/post-generation-guard.js +170 -0
- package/src/guardrails/pre-retrieval-guard.js +161 -0
- package/src/guardrails/retrieval-guard.js +126 -0
- package/src/index.js +255 -9
- package/src/loader/html-loader.js +4 -15
- package/src/loader/markdown-loader.js +4 -15
- package/src/mcp/mcp-server.js +199 -0
- package/src/mcp/mcp-tool-builder.js +121 -0
- package/src/observability/audit-logger.js +0 -2
- package/src/observability/event-logger.js +2 -1
- package/src/observability/security-monitor.js +0 -2
- package/src/observability/telemetry.js +0 -2
- package/src/reranker/cascade-reranker.js +79 -0
- package/src/reranker/embedding-reranker.js +124 -0
- package/src/reranker/llm-reranker.js +6 -3
- package/src/reranker/scoring-reranker.js +123 -0
- package/src/retrieval/bm25-search.js +157 -0
- package/src/retrieval/hybrid-retriever.js +111 -0
- package/src/retrieval/rank-fusion.js +62 -0
- package/src/security/jwt-config.js +4 -3
- package/src/security/jwt-validator.js +2 -1
- package/src/security/plugin-sandbox.js +0 -3
- package/src/security/plugin-signature-verifier.js +4 -6
- package/src/security/validator.js +0 -2
- package/src/streaming/embedding-stream.js +197 -0
- package/src/streaming/sse-adapter.js +168 -0
- package/src/streaming/stream-router.js +210 -0
- package/src/streaming/streaming-embedder.js +231 -0
- package/src/streaming/websocket-adapter.js +171 -0
- package/src/templates/scaffold.js +147 -0
- package/src/templates/template-registry.js +272 -0
- package/src/utils/contract-validator.js +2 -1
- package/src/utils/input-sanitizer.js +80 -99
- package/src/utils/performance-monitor.js +0 -3
- package/src/utils/plugin-scaffolder.js +9 -16
- package/src/utils/rate-limiter.js +0 -1
- package/src/utils/retry-policy.js +0 -3
- package/src/utils/safe-expression-evaluator.js +0 -3
- package/src/utils/secure-logger-config.js +2 -4
- package/src/utils/secure-logging.js +0 -3
- package/src/utils/structured-logger.js +0 -3
- package/src/ai/index-fixed.js +0 -509
- package/src/ai/module-template.js +0 -37
- package/src/ci/workflow-analyzer.js +0 -71
- package/src/cli/plugin-marketplace-commands.js +0 -869
- package/src/dev/README.md +0 -126
- package/src/dev/examples/hot-reload-usage.js +0 -355
- package/src/mocks/openai-embedder.js +0 -119
- package/src/mocks/openai-llm.js +0 -77
- package/src/mocks/pdf-loader.js +0 -21
- package/src/mocks/pinecone-retriever.js +0 -106
- package/src/scripts/script-utilities.js +0 -36
- package/src/utils/ci/diagnostic-reporter.js +0 -57
- 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 };
|