@devilsdev/rag-pipeline-utils 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +1 -1
- package/dist/index.cjs +1484 -147
- package/dist/index.d.ts +49 -0
- package/dist/index.mjs +1 -1
- package/dist/testing.cjs +2 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.mjs +5 -0
- package/dist/types/adapters/ai-sdk.d.ts +53 -0
- package/dist/types/cache/component-keys.d.ts +7 -0
- package/dist/types/cache/memory-cache.d.ts +54 -0
- package/dist/types/cache/pipeline-cache.d.ts +66 -0
- package/dist/types/cache/redis-cache.d.ts +46 -0
- package/dist/types/connectors/anthropic-connector.d.ts +7 -2
- package/dist/types/connectors/cohere-connector.d.ts +7 -2
- package/dist/types/connectors/local-embedder.d.ts +9 -0
- package/dist/types/connectors/memory-retriever.d.ts +8 -0
- package/dist/types/connectors/ollama-connector.d.ts +7 -2
- package/dist/types/connectors/openai-connector.d.ts +16 -1
- package/dist/types/core/create-pipeline.d.ts +8 -0
- package/dist/types/core/plugin-registry.d.ts +136 -0
- package/dist/types/core/stage-contracts.d.ts +36 -0
- package/dist/types/core/validate-plugin.d.ts +45 -0
- package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
- package/dist/types/grounding/check-answer.d.ts +101 -0
- package/dist/types/retrieval/file-vector-store.d.ts +12 -0
- package/dist/types/testing.d.ts +168 -0
- package/package.json +22 -7
- package/src/adapters/ai-sdk.js +215 -0
- package/src/cache/component-keys.js +38 -0
- package/src/cache/memory-cache.js +87 -0
- package/src/cache/pipeline-cache.js +260 -0
- package/src/cache/redis-cache.js +83 -0
- package/src/cli/commands/ai.js +17 -12
- package/src/cli/commands/plugin-hub.js +18 -13
- package/src/cli/enhanced-cli-commands.js +45 -29
- package/src/cli/handlers/query-handler.js +174 -11
- package/src/cli/services/instrument.js +83 -0
- package/src/cli/services/rag-service.js +51 -12
- package/src/cli/wizard/init-wizard.js +2 -17
- package/src/config/enhanced-ragrc-schema.js +0 -2
- package/src/config/validate-schema.js +4 -1
- package/src/connectors/anthropic-connector.js +22 -2
- package/src/connectors/cohere-connector.js +20 -2
- package/src/connectors/local-embedder.js +20 -0
- package/src/connectors/memory-retriever.js +18 -0
- package/src/connectors/ollama-connector.js +29 -2
- package/src/connectors/openai-connector.js +49 -4
- package/src/core/create-pipeline.js +112 -15
- package/src/core/performance/parallel-processor.js +62 -1
- package/src/core/plugin-registry.js +42 -19
- package/src/core/stage-contracts.js +36 -0
- package/src/core/validate-plugin.js +85 -0
- package/src/debug/execution-tracer.js +14 -4
- package/src/dev/dev-server.js +8 -1
- package/src/dev/hot-reload.js +4 -3
- package/src/enterprise/sso-integration.js +4 -3
- package/src/evaluate/semantic-similarity.js +79 -0
- package/src/grounding/check-answer.js +173 -0
- package/src/index.js +22 -0
- package/src/loader/csv-loader.js +3 -2
- package/src/loader/html-loader.js +3 -1
- package/src/loader/markdown-loader.js +3 -2
- package/src/retrieval/file-vector-store.js +30 -1
- package/src/security/jwt-validator.js +5 -4
- package/src/testing.js +254 -0
|
@@ -17,24 +17,19 @@ const {
|
|
|
17
17
|
getExtendedHelp,
|
|
18
18
|
} = require("./shared/cli-utils.js");
|
|
19
19
|
|
|
20
|
-
// Handler modules
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
handleConfigSet,
|
|
34
|
-
handleConfigGet,
|
|
35
|
-
} = require("./handlers/config-handler.js");
|
|
36
|
-
const { handleIngest } = require("./handlers/ingest-handler.js");
|
|
37
|
-
const { handleQuery } = require("./handlers/query-handler.js");
|
|
20
|
+
// Handler modules load when their command runs, so `--help` or one command
|
|
21
|
+
// does not pay for every other command's dependencies (inquirer, jsdom, ora,
|
|
22
|
+
// marked, csv-parse...).
|
|
23
|
+
/* eslint-disable global-require */
|
|
24
|
+
const handlers = {
|
|
25
|
+
run: () => require("./handlers/run-handler.js"),
|
|
26
|
+
init: () => require("./handlers/init-handler.js"),
|
|
27
|
+
diagnostics: () => require("./handlers/diagnostics-handler.js"),
|
|
28
|
+
config: () => require("./handlers/config-handler.js"),
|
|
29
|
+
ingest: () => require("./handlers/ingest-handler.js"),
|
|
30
|
+
query: () => require("./handlers/query-handler.js"),
|
|
31
|
+
};
|
|
32
|
+
/* eslint-enable global-require */
|
|
38
33
|
|
|
39
34
|
function parseNonNegativeInt(value) {
|
|
40
35
|
const n = Number(value);
|
|
@@ -64,6 +59,7 @@ class EnhancedCLI {
|
|
|
64
59
|
.version(getVersion(), "-v, --version", "Display version information")
|
|
65
60
|
.option("--config <path>", "Configuration file path", ".ragrc.json")
|
|
66
61
|
.option("--verbose", "Enable verbose output")
|
|
62
|
+
.option("--quiet", "Print only results, no status messages")
|
|
67
63
|
.option("--no-color", "Disable colored output")
|
|
68
64
|
.option("--dry-run", "Show what would be done without executing")
|
|
69
65
|
.helpOption("-h, --help", "Display help information")
|
|
@@ -87,10 +83,12 @@ class EnhancedCLI {
|
|
|
87
83
|
.option("--output <path>", "Output configuration file", ".ragrc.json")
|
|
88
84
|
.option("--force", "Overwrite existing configuration")
|
|
89
85
|
.action(async (options) => {
|
|
90
|
-
const result = await
|
|
86
|
+
const result = await handlers
|
|
87
|
+
.init()
|
|
88
|
+
.handleInit(this.program.opts(), options);
|
|
91
89
|
// After a connection test, exit even if a provider call left a
|
|
92
90
|
// socket or timer open (see exitAfterConnectionTest).
|
|
93
|
-
exitAfterConnectionTest(result);
|
|
91
|
+
handlers.init().exitAfterConnectionTest(result);
|
|
94
92
|
});
|
|
95
93
|
|
|
96
94
|
this.program
|
|
@@ -102,7 +100,7 @@ class EnhancedCLI {
|
|
|
102
100
|
.option("--top-k <n>", "Passages to retrieve", parsePositiveInteger, 3)
|
|
103
101
|
.option("--json", "Print the query result as JSON")
|
|
104
102
|
.action(async (options) => {
|
|
105
|
-
await handleRun(this.program.opts(), options);
|
|
103
|
+
await handlers.run().handleRun(this.program.opts(), options);
|
|
106
104
|
});
|
|
107
105
|
|
|
108
106
|
this.program
|
|
@@ -118,7 +116,7 @@ class EnhancedCLI {
|
|
|
118
116
|
)
|
|
119
117
|
.option("--report <path>", "Save the diagnostic report as JSON")
|
|
120
118
|
.action(async (options) => {
|
|
121
|
-
await handleDoctor(this.program.opts(), options);
|
|
119
|
+
await handlers.diagnostics().handleDoctor(this.program.opts(), options);
|
|
122
120
|
});
|
|
123
121
|
|
|
124
122
|
this.program
|
|
@@ -127,7 +125,9 @@ class EnhancedCLI {
|
|
|
127
125
|
.argument("<path>", "File or directory (.md, .html, .csv, .txt)")
|
|
128
126
|
.option("--rebuild", "Discard the existing index before ingesting")
|
|
129
127
|
.action(async (inputPath, options) => {
|
|
130
|
-
await
|
|
128
|
+
await handlers
|
|
129
|
+
.ingest()
|
|
130
|
+
.handleIngest(this.program.opts(), inputPath, options);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
this.program
|
|
@@ -136,15 +136,29 @@ class EnhancedCLI {
|
|
|
136
136
|
.argument("<question>", "Question text")
|
|
137
137
|
.option("--top-k <n>", "Passages to retrieve", parsePositiveInteger, 3)
|
|
138
138
|
.option("--json", "Print the result as JSON")
|
|
139
|
+
.option("--streaming", "Print the answer as it is generated")
|
|
140
|
+
.option(
|
|
141
|
+
"--timeout <ms>",
|
|
142
|
+
"Abort the query after this many milliseconds",
|
|
143
|
+
parsePositiveInteger,
|
|
144
|
+
)
|
|
145
|
+
.option("--trace", "Show time spent in each stage")
|
|
146
|
+
.option(
|
|
147
|
+
"--explain",
|
|
148
|
+
"Show passage scores and which passages support each answer sentence",
|
|
149
|
+
)
|
|
150
|
+
.option("--stats", "Show latency and estimated tokens and cost")
|
|
139
151
|
.action(async (question, options) => {
|
|
140
|
-
await
|
|
152
|
+
await handlers
|
|
153
|
+
.query()
|
|
154
|
+
.handleQuery(this.program.opts(), question, options);
|
|
141
155
|
});
|
|
142
156
|
|
|
143
157
|
this.program
|
|
144
158
|
.command("validate")
|
|
145
159
|
.description("Validate the configuration and resolve its components")
|
|
146
160
|
.action(async () => {
|
|
147
|
-
await handleValidate(this.program.opts());
|
|
161
|
+
await handlers.diagnostics().handleValidate(this.program.opts());
|
|
148
162
|
});
|
|
149
163
|
|
|
150
164
|
this.program
|
|
@@ -177,7 +191,7 @@ class EnhancedCLI {
|
|
|
177
191
|
.description("Show current configuration")
|
|
178
192
|
.option("--section <section>", "Show specific configuration section")
|
|
179
193
|
.action(async (options) => {
|
|
180
|
-
await handleConfigShow(this.program.opts(), options);
|
|
194
|
+
await handlers.config().handleConfigShow(this.program.opts(), options);
|
|
181
195
|
});
|
|
182
196
|
|
|
183
197
|
configCmd
|
|
@@ -186,7 +200,9 @@ class EnhancedCLI {
|
|
|
186
200
|
.argument("<key>", "Configuration key (dot notation supported)")
|
|
187
201
|
.argument("<value>", "Configuration value")
|
|
188
202
|
.action(async (key, value, options) => {
|
|
189
|
-
await
|
|
203
|
+
await handlers
|
|
204
|
+
.config()
|
|
205
|
+
.handleConfigSet(this.program.opts(), key, value);
|
|
190
206
|
});
|
|
191
207
|
|
|
192
208
|
configCmd
|
|
@@ -194,7 +210,7 @@ class EnhancedCLI {
|
|
|
194
210
|
.description("Get configuration value")
|
|
195
211
|
.argument("<key>", "Configuration key (dot notation supported)")
|
|
196
212
|
.action(async (key, options) => {
|
|
197
|
-
await handleConfigGet(this.program.opts(), key);
|
|
213
|
+
await handlers.config().handleConfigGet(this.program.opts(), key);
|
|
198
214
|
});
|
|
199
215
|
|
|
200
216
|
this.program.addCommand(configCmd);
|
|
@@ -207,7 +223,7 @@ class EnhancedCLI {
|
|
|
207
223
|
.option("--system", "Show system information")
|
|
208
224
|
.option("--config", "Show configuration summary")
|
|
209
225
|
.action(async (options) => {
|
|
210
|
-
await handleInfo(this.program.opts(), options);
|
|
226
|
+
await handlers.diagnostics().handleInfo(this.program.opts(), options);
|
|
211
227
|
});
|
|
212
228
|
|
|
213
229
|
// Completion command for shell autocompletion
|
|
@@ -2,26 +2,146 @@
|
|
|
2
2
|
* Query command handler
|
|
3
3
|
* Answers a question from the ingested index. Without an llm stage it prints
|
|
4
4
|
* the retrieved passages instead of an answer.
|
|
5
|
+
*
|
|
6
|
+
* Options: --top-k, --json, --streaming, --timeout, --trace (per-stage
|
|
7
|
+
* timings), --explain (passage scores and the passages supporting each
|
|
8
|
+
* answer sentence), --stats (latency, estimated tokens and cost). The global
|
|
9
|
+
* --quiet prints only the answer or the passages.
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
12
|
const { loadCliConfig } = require("../shared/config-context.js");
|
|
8
13
|
const { query } = require("../services/rag-service.js");
|
|
14
|
+
const { CostCalculator } = require("../../cost/cost-calculator.js");
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
const STAGES = ["embed", "retrieve", "rerank", "generate"];
|
|
17
|
+
|
|
18
|
+
function sourceLabel(doc) {
|
|
19
|
+
return doc.metadata?.source || doc.id;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printPassages(results, { quiet } = {}) {
|
|
11
23
|
// A score of 0 means no overlap with the question at all.
|
|
12
24
|
const matches = results.filter((doc) => !(doc.score <= 0));
|
|
13
25
|
if (matches.length === 0) {
|
|
14
|
-
console.log("No matching passages found.");
|
|
26
|
+
if (!quiet) console.log("No matching passages found.");
|
|
15
27
|
return;
|
|
16
28
|
}
|
|
17
29
|
matches.forEach((doc, i) => {
|
|
18
|
-
const source = doc.metadata?.source || doc.id;
|
|
19
30
|
const score = typeof doc.score === "number" ? doc.score.toFixed(3) : "-";
|
|
20
|
-
console.log(`\n[${i + 1}] ${
|
|
31
|
+
console.log(`\n[${i + 1}] ${sourceLabel(doc)} (score ${score})`);
|
|
21
32
|
console.log(doc.content);
|
|
22
33
|
});
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
/** Sum the recorded timings per stage. */
|
|
37
|
+
function stageTimings(timings) {
|
|
38
|
+
const totals = {};
|
|
39
|
+
for (const { stage, ms } of timings)
|
|
40
|
+
totals[stage] = (totals[stage] || 0) + ms;
|
|
41
|
+
return totals;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printTrace(result) {
|
|
45
|
+
const totals = stageTimings(result.timings);
|
|
46
|
+
console.log("\nTrace");
|
|
47
|
+
for (const stage of STAGES) {
|
|
48
|
+
const ms = totals[stage];
|
|
49
|
+
console.log(
|
|
50
|
+
` ${stage.padEnd(9)} ${ms === undefined ? "-" : `${ms.toFixed(1)} ms`}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
console.log(` ${"total".padEnd(9)} ${result.durationMs.toFixed(1)} ms`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Rough token count: about 4 characters per token for English text. */
|
|
57
|
+
const estimateTokens = (text) => Math.ceil(String(text || "").length / 4);
|
|
58
|
+
|
|
59
|
+
function computeStats(result, question, llmModel) {
|
|
60
|
+
const context = result.results.map((d) => d.content || "").join("\n");
|
|
61
|
+
const stats = {
|
|
62
|
+
durationMs: Math.round(result.durationMs),
|
|
63
|
+
passages: result.results.length,
|
|
64
|
+
answerChars: result.answer ? result.answer.length : 0,
|
|
65
|
+
};
|
|
66
|
+
if (result.answer !== null) {
|
|
67
|
+
stats.estimatedInputTokens = estimateTokens(`${question}\n${context}`);
|
|
68
|
+
stats.estimatedOutputTokens = estimateTokens(result.answer);
|
|
69
|
+
const calculator = new CostCalculator();
|
|
70
|
+
// Only price models in the table; unknown models would print a warning.
|
|
71
|
+
if (llmModel && calculator.getModelPricing(llmModel)) {
|
|
72
|
+
const cost = calculator.estimate(
|
|
73
|
+
llmModel,
|
|
74
|
+
stats.estimatedInputTokens,
|
|
75
|
+
stats.estimatedOutputTokens,
|
|
76
|
+
);
|
|
77
|
+
stats.estimatedCostUsd = Number(cost.totalCost.toFixed(6));
|
|
78
|
+
stats.model = cost.model;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return stats;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function printStats(stats) {
|
|
85
|
+
console.log("\nStats");
|
|
86
|
+
console.log(` latency ${stats.durationMs} ms`);
|
|
87
|
+
console.log(` passages ${stats.passages}`);
|
|
88
|
+
if (stats.estimatedInputTokens !== undefined) {
|
|
89
|
+
console.log(
|
|
90
|
+
` tokens ~${stats.estimatedInputTokens} in, ~${stats.estimatedOutputTokens} out (estimated)`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (stats.estimatedCostUsd !== undefined) {
|
|
94
|
+
console.log(
|
|
95
|
+
` cost ~$${stats.estimatedCostUsd} (${stats.model}, estimated)`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Passage rank and score, and the passages that support each sentence. */
|
|
101
|
+
function explanation(result) {
|
|
102
|
+
const passages = result.results.map((doc, i) => ({
|
|
103
|
+
rank: i + 1,
|
|
104
|
+
source: sourceLabel(doc),
|
|
105
|
+
score: typeof doc.score === "number" ? doc.score : null,
|
|
106
|
+
}));
|
|
107
|
+
if (!result.citations) return { passages };
|
|
108
|
+
const rankOf = new Map(result.results.map((d, i) => [String(d.id), i + 1]));
|
|
109
|
+
const sentences = (result.citations.citations || []).map((c) => ({
|
|
110
|
+
sentence: c.sentence,
|
|
111
|
+
supportedBy: (c.sources || []).map((s) => ({
|
|
112
|
+
rank: rankOf.get(String(s.docId)) ?? s.docIndex + 1,
|
|
113
|
+
source: sourceLabel(result.results[s.docIndex] || { id: s.docId }),
|
|
114
|
+
support: s.score,
|
|
115
|
+
})),
|
|
116
|
+
}));
|
|
117
|
+
return {
|
|
118
|
+
passages,
|
|
119
|
+
sentences,
|
|
120
|
+
groundedness: result.citations.groundednessScore,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function printExplanation(explained) {
|
|
125
|
+
console.log("\nExplanation");
|
|
126
|
+
console.log(" Passages, in ranked order:");
|
|
127
|
+
for (const p of explained.passages) {
|
|
128
|
+
const score = p.score === null ? "-" : p.score.toFixed(3);
|
|
129
|
+
console.log(` [${p.rank}] ${p.source} score ${score}`);
|
|
130
|
+
}
|
|
131
|
+
if (!explained.sentences) return;
|
|
132
|
+
console.log(" Answer sentences:");
|
|
133
|
+
for (const s of explained.sentences) {
|
|
134
|
+
const support = s.supportedBy.length
|
|
135
|
+
? s.supportedBy
|
|
136
|
+
.map((p) => `[${p.rank}] ${p.source} (${p.support.toFixed(2)})`)
|
|
137
|
+
.join(", ")
|
|
138
|
+
: "not supported by any passage";
|
|
139
|
+
console.log(` "${s.sentence}"`);
|
|
140
|
+
console.log(` -> ${support}`);
|
|
141
|
+
}
|
|
142
|
+
console.log(` Groundedness: ${explained.groundedness.toFixed(2)}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
25
145
|
/**
|
|
26
146
|
* @param {object} globalOptions - Global CLI options from program.opts()
|
|
27
147
|
* @param {string} question - Query text
|
|
@@ -31,30 +151,73 @@ async function handleQuery(globalOptions, question, options = {}) {
|
|
|
31
151
|
try {
|
|
32
152
|
const { config, configDir } = loadCliConfig(globalOptions);
|
|
33
153
|
const topK = options.topK || 3;
|
|
154
|
+
const quiet = Boolean(globalOptions.quiet);
|
|
34
155
|
|
|
35
156
|
if (globalOptions.dryRun) {
|
|
36
157
|
console.log(`🧪 Dry run: would query the index for: ${question}`);
|
|
37
158
|
return;
|
|
38
159
|
}
|
|
39
160
|
|
|
40
|
-
const
|
|
161
|
+
const hasLlm = (config.pipeline || []).some((s) => s.stage === "llm");
|
|
162
|
+
const streaming = Boolean(options.streaming && hasLlm && !options.json);
|
|
163
|
+
if (options.streaming && !hasLlm && !quiet && !options.json) {
|
|
164
|
+
console.log("--streaming needs an llm stage; showing passages instead.");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const result = await query({
|
|
168
|
+
config,
|
|
169
|
+
configDir,
|
|
170
|
+
question,
|
|
171
|
+
topK,
|
|
172
|
+
timeout: options.timeout,
|
|
173
|
+
...(streaming && { onToken: (t) => process.stdout.write(t) }),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const llmStage = (config.pipeline || []).find((s) => s.stage === "llm");
|
|
177
|
+
const stats = options.stats
|
|
178
|
+
? computeStats(result, question, llmStage?.config?.model)
|
|
179
|
+
: undefined;
|
|
180
|
+
const explained = options.explain ? explanation(result) : undefined;
|
|
41
181
|
|
|
42
182
|
if (options.json) {
|
|
43
|
-
|
|
183
|
+
const { timings, durationMs, ...rest } = result;
|
|
184
|
+
console.log(
|
|
185
|
+
JSON.stringify(
|
|
186
|
+
{
|
|
187
|
+
...rest,
|
|
188
|
+
...(options.trace && {
|
|
189
|
+
trace: { stages: stageTimings(timings), totalMs: durationMs },
|
|
190
|
+
}),
|
|
191
|
+
...(stats && { stats }),
|
|
192
|
+
...(explained && { explanation: explained }),
|
|
193
|
+
},
|
|
194
|
+
null,
|
|
195
|
+
2,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
44
198
|
return;
|
|
45
199
|
}
|
|
46
200
|
|
|
47
201
|
if (result.answer === null) {
|
|
48
|
-
|
|
202
|
+
if (!quiet)
|
|
203
|
+
console.log("No llm stage configured; showing retrieved passages.");
|
|
204
|
+
printPassages(result.results, { quiet });
|
|
49
205
|
} else {
|
|
50
|
-
|
|
51
|
-
console.log(
|
|
206
|
+
if (streaming) process.stdout.write("\n");
|
|
207
|
+
else console.log(result.answer);
|
|
208
|
+
if (!quiet) {
|
|
209
|
+
console.log("\nSources:");
|
|
210
|
+
printPassages(result.results);
|
|
211
|
+
}
|
|
52
212
|
}
|
|
53
|
-
|
|
213
|
+
|
|
214
|
+
if (options.explain) printExplanation(explained);
|
|
215
|
+
if (options.trace) printTrace(result);
|
|
216
|
+
if (options.stats) printStats(stats);
|
|
54
217
|
} catch (error) {
|
|
55
218
|
console.error(`❌ Query failed: ${error.message}`);
|
|
56
219
|
process.exitCode = 1;
|
|
57
220
|
}
|
|
58
221
|
}
|
|
59
222
|
|
|
60
|
-
module.exports = { handleQuery };
|
|
223
|
+
module.exports = { handleQuery, explanation, computeStats };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wraps pipeline components so the CLI can report per-stage timings
|
|
5
|
+
* (`query --trace`) and see the retrieved passages when streaming (the
|
|
6
|
+
* stream's final chunk carries citations, not results).
|
|
7
|
+
*
|
|
8
|
+
* Each wrapper forwards to the real component with `this` bound to it; only
|
|
9
|
+
* the stage methods are timed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { performance } = require("perf_hooks");
|
|
13
|
+
|
|
14
|
+
const STAGE_METHODS = {
|
|
15
|
+
embedder: { embedQuery: "embed", embed: "embed" },
|
|
16
|
+
retriever: { retrieve: "retrieve" },
|
|
17
|
+
reranker: { rerank: "rerank" },
|
|
18
|
+
llm: { generate: "generate", generateStream: "generate" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
async function* timedStream(source, record) {
|
|
22
|
+
const start = performance.now();
|
|
23
|
+
try {
|
|
24
|
+
yield* await source;
|
|
25
|
+
} finally {
|
|
26
|
+
record(performance.now() - start);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {Object} components - { embedder, retriever, reranker, llm }
|
|
32
|
+
* @returns {{ components: Object, timings: Array<{stage: string, ms: number}>, captured: { results: Array|null } }}
|
|
33
|
+
*/
|
|
34
|
+
function instrumentComponents(components) {
|
|
35
|
+
const timings = [];
|
|
36
|
+
const captured = { results: null };
|
|
37
|
+
|
|
38
|
+
const wrap = (kind, component) => {
|
|
39
|
+
if (!component) return component;
|
|
40
|
+
const methods = STAGE_METHODS[kind];
|
|
41
|
+
return new Proxy(component, {
|
|
42
|
+
get(target, prop, receiver) {
|
|
43
|
+
const value = Reflect.get(target, prop, receiver);
|
|
44
|
+
const stage = methods[prop];
|
|
45
|
+
if (typeof value !== "function" || !stage) return value;
|
|
46
|
+
|
|
47
|
+
if (prop === "generateStream") {
|
|
48
|
+
return (...args) =>
|
|
49
|
+
timedStream(value.apply(target, args), (ms) =>
|
|
50
|
+
timings.push({ stage, ms }),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return async (...args) => {
|
|
54
|
+
const start = performance.now();
|
|
55
|
+
try {
|
|
56
|
+
const out = await value.apply(target, args);
|
|
57
|
+
if (kind === "retriever" || kind === "reranker") {
|
|
58
|
+
// The reranker runs after the retriever, so its order wins.
|
|
59
|
+
captured.results = out;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
} finally {
|
|
63
|
+
timings.push({ stage, ms: performance.now() - start });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
components: {
|
|
72
|
+
...components,
|
|
73
|
+
embedder: wrap("embedder", components.embedder),
|
|
74
|
+
retriever: wrap("retriever", components.retriever),
|
|
75
|
+
reranker: wrap("reranker", components.reranker),
|
|
76
|
+
llm: wrap("llm", components.llm),
|
|
77
|
+
},
|
|
78
|
+
timings,
|
|
79
|
+
captured,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { instrumentComponents };
|
|
@@ -11,6 +11,8 @@ const { buildComponents } = require("../../config/component-factory");
|
|
|
11
11
|
const { readDocuments } = require("../../loader/document-reader");
|
|
12
12
|
const { embedQuery } = require("../../utils/embed-query");
|
|
13
13
|
const { createRagPipeline } = require("../../core/create-pipeline");
|
|
14
|
+
const { performance } = require("perf_hooks");
|
|
15
|
+
const { instrumentComponents } = require("./instrument");
|
|
14
16
|
|
|
15
17
|
function requireStage(components, stage) {
|
|
16
18
|
if (!components[stage]) {
|
|
@@ -284,15 +286,24 @@ async function retrieve({ components, config, configDir, question, topK = 3 }) {
|
|
|
284
286
|
* @param {string} params.question
|
|
285
287
|
* @param {number} [params.topK=3]
|
|
286
288
|
* @param {object} [params.components] - Prebuilt components (loadComponents)
|
|
287
|
-
* @
|
|
289
|
+
* @param {number} [params.timeout] - Abort the run after this many ms
|
|
290
|
+
* @param {function(string): void} [params.onToken] - Stream the answer: called
|
|
291
|
+
* with each token (only with an llm stage)
|
|
292
|
+
* @returns {Promise<{answer: string|null, results: Array, citations?: object,
|
|
293
|
+
* timings: Array<{stage: string, ms: number}>, durationMs: number}>}
|
|
288
294
|
*/
|
|
289
|
-
async function query({
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
295
|
+
async function query({
|
|
296
|
+
config,
|
|
297
|
+
configDir,
|
|
298
|
+
question,
|
|
299
|
+
topK = 3,
|
|
300
|
+
components,
|
|
301
|
+
timeout,
|
|
302
|
+
onToken,
|
|
303
|
+
}) {
|
|
304
|
+
const loaded = components || loadComponents({ config, configDir });
|
|
305
|
+
const { components: c, timings, captured } = instrumentComponents(loaded);
|
|
306
|
+
const started = performance.now();
|
|
296
307
|
|
|
297
308
|
const pipeline = createRagPipeline({
|
|
298
309
|
embedder: c.embedder,
|
|
@@ -300,15 +311,43 @@ async function query({ config, configDir, question, topK = 3, components }) {
|
|
|
300
311
|
llm: c.llm,
|
|
301
312
|
reranker: c.reranker,
|
|
302
313
|
});
|
|
314
|
+
const stream = Boolean(onToken && c.llm);
|
|
303
315
|
const result = await pipeline.run({
|
|
304
316
|
query: question,
|
|
305
|
-
options: {
|
|
317
|
+
options: {
|
|
318
|
+
topK,
|
|
319
|
+
citations: Boolean(c.llm),
|
|
320
|
+
...(timeout && { timeout }),
|
|
321
|
+
...(stream && { stream: true }),
|
|
322
|
+
},
|
|
306
323
|
});
|
|
307
|
-
if (
|
|
324
|
+
if (result && result.success === false) throw new Error(result.error);
|
|
325
|
+
|
|
326
|
+
if (stream) {
|
|
327
|
+
let answer = "";
|
|
328
|
+
let citations;
|
|
329
|
+
for await (const chunk of result) {
|
|
330
|
+
if (chunk.token) {
|
|
331
|
+
answer += chunk.token;
|
|
332
|
+
onToken(chunk.token);
|
|
333
|
+
}
|
|
334
|
+
if (chunk.done) citations = chunk.citations;
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
answer,
|
|
338
|
+
results: captured.results || [],
|
|
339
|
+
citations,
|
|
340
|
+
timings,
|
|
341
|
+
durationMs: performance.now() - started,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
308
345
|
return {
|
|
309
|
-
answer: result.answer,
|
|
346
|
+
answer: c.llm ? result.answer : null,
|
|
310
347
|
results: result.results,
|
|
311
|
-
citations: result.citations,
|
|
348
|
+
...(result.citations && { citations: result.citations }),
|
|
349
|
+
timings,
|
|
350
|
+
durationMs: performance.now() - started,
|
|
312
351
|
};
|
|
313
352
|
}
|
|
314
353
|
|
|
@@ -19,6 +19,8 @@ const {
|
|
|
19
19
|
} = require("../../config/component-factory");
|
|
20
20
|
const { embedQuery } = require("../../utils/embed-query");
|
|
21
21
|
const { buildInitConfig, DEFAULT_INDEX_PATH } = require("./config-builder");
|
|
22
|
+
// Methods a custom plugin must expose; shared with validatePlugin().
|
|
23
|
+
const { STAGE_CONTRACTS } = require("../../core/stage-contracts");
|
|
22
24
|
|
|
23
25
|
const CUSTOM = "custom";
|
|
24
26
|
const NONE = "none";
|
|
@@ -58,23 +60,6 @@ const OPTION_LABELS = {
|
|
|
58
60
|
baseURL: "server URL",
|
|
59
61
|
};
|
|
60
62
|
|
|
61
|
-
/**
|
|
62
|
-
* Methods a custom plugin must expose (`anyOf`), and methods the CLI needs
|
|
63
|
-
* for some commands (`recommended`, a warning when missing).
|
|
64
|
-
*/
|
|
65
|
-
const STAGE_CONTRACTS = {
|
|
66
|
-
embedder: {
|
|
67
|
-
anyOf: ["embed", "embedQuery"],
|
|
68
|
-
recommended: { embed: "`ingest` calls embed() to embed documents" },
|
|
69
|
-
},
|
|
70
|
-
llm: { anyOf: ["generate"], recommended: {} },
|
|
71
|
-
retriever: {
|
|
72
|
-
anyOf: ["retrieve"],
|
|
73
|
-
recommended: { store: "`ingest` calls store() to save vectors" },
|
|
74
|
-
},
|
|
75
|
-
reranker: { anyOf: ["rerank"], recommended: {} },
|
|
76
|
-
};
|
|
77
|
-
|
|
78
63
|
const CUSTOM_CHOICE = {
|
|
79
64
|
name: "custom plugin (path to a .js module)",
|
|
80
65
|
value: CUSTOM,
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
* Single source of truth for configuration validation
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const Ajv = require("ajv"); // eslint-disable-line global-require
|
|
7
|
-
const addFormats = require("ajv-formats"); // eslint-disable-line global-require
|
|
8
6
|
const { normalizeConfig } = require("./normalize-config");
|
|
9
7
|
|
|
10
8
|
/**
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* File: /src/_config/validate-schema.js
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// ajv loads when a schema is validated, not on import.
|
|
9
|
+
const loadAjv = () => require("ajv"); // eslint-disable-line global-require
|
|
9
10
|
// eslint-disable-line global-require
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -89,6 +90,7 @@ const pluginSchema = {
|
|
|
89
90
|
* @returns {{ valid: boolean, errors?: any[] }}
|
|
90
91
|
*/
|
|
91
92
|
function validateRagrcSchema(config) {
|
|
93
|
+
const Ajv = loadAjv();
|
|
92
94
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
93
95
|
const validate = ajv.compile(ragrcSchema);
|
|
94
96
|
const valid = validate(config);
|
|
@@ -101,6 +103,7 @@ function validateRagrcSchema(config) {
|
|
|
101
103
|
* @returns {{ valid: boolean, errors?: any[] }}
|
|
102
104
|
*/
|
|
103
105
|
function validatePluginSchema(config) {
|
|
106
|
+
const Ajv = loadAjv();
|
|
104
107
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
105
108
|
const validate = ajv.compile(pluginSchema);
|
|
106
109
|
const valid = validate(config);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// axios loads on first use, so importing the package does not pay for it
|
|
4
|
+
// (about 125 ms) unless this connector is constructed.
|
|
5
|
+
const loadAxios = () => require('axios'); // eslint-disable-line global-require
|
|
4
6
|
const { BaseConnector } = require('./base-connector');
|
|
5
7
|
const {
|
|
6
8
|
missingApiKeyError,
|
|
@@ -10,6 +12,7 @@ const {
|
|
|
10
12
|
throwIfAborted,
|
|
11
13
|
} = require('./connector-utils');
|
|
12
14
|
const { buildRagPrompt } = require('./rag-prompt');
|
|
15
|
+
const { componentCacheKey } = require('../cache/component-keys');
|
|
13
16
|
|
|
14
17
|
const API_KEY_ENV = 'ANTHROPIC_API_KEY';
|
|
15
18
|
|
|
@@ -39,6 +42,23 @@ const DEFAULT_CONFIG = {
|
|
|
39
42
|
* @extends BaseConnector
|
|
40
43
|
*/
|
|
41
44
|
class AnthropicConnector extends BaseConnector {
|
|
45
|
+
/**
|
|
46
|
+
* Cache identity for createRagPipeline({ cache }).
|
|
47
|
+
* @param {'llm'} role
|
|
48
|
+
* @returns {string|null}
|
|
49
|
+
*/
|
|
50
|
+
getCacheKey(role) {
|
|
51
|
+
if (role !== 'llm') return null;
|
|
52
|
+
const c = this.config;
|
|
53
|
+
return componentCacheKey('anthropic', {
|
|
54
|
+
baseURL: c.baseURL,
|
|
55
|
+
model: c.model,
|
|
56
|
+
temperature: c.temperature,
|
|
57
|
+
maxTokens: c.maxTokens,
|
|
58
|
+
systemPrompt: c.systemPrompt,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
/**
|
|
43
63
|
* @param {Object} [options]
|
|
44
64
|
* @param {string} [options.model='claude-3-5-sonnet-20241022'] - Claude model to use
|
|
@@ -58,7 +78,7 @@ class AnthropicConnector extends BaseConnector {
|
|
|
58
78
|
...options,
|
|
59
79
|
apiKey: resolveApiKey(options, API_KEY_ENV),
|
|
60
80
|
};
|
|
61
|
-
this.client =
|
|
81
|
+
this.client = loadAxios().create({
|
|
62
82
|
baseURL: this.config.baseURL,
|
|
63
83
|
headers: {
|
|
64
84
|
'x-api-key': this.config.apiKey,
|