@devilsdev/rag-pipeline-utils 2.4.5 → 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/.ragrc.schema.json +83 -192
- package/CHANGELOG.md +431 -0
- package/README.md +418 -117
- package/SECURITY.md +5 -5
- package/dist/experimental.cjs +2 -0
- package/dist/experimental.d.ts +1 -0
- package/dist/experimental.mjs +5 -0
- package/dist/index.cjs +6442 -1355
- package/dist/index.d.ts +129 -2
- 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/agentic/agentic-pipeline.d.ts +103 -0
- package/dist/types/agentic/iterative-retriever.d.ts +85 -0
- package/dist/types/agentic/query-planner.d.ts +145 -0
- package/dist/types/agentic/self-critique.d.ts +45 -0
- package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
- package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
- package/dist/types/ai/multimodal-processing.d.ts +32 -0
- package/dist/types/ai/multimodal.d.ts +39 -0
- package/dist/types/ai/retrieval-engine.d.ts +37 -0
- package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -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/chunking/chunking-engine.d.ts +70 -0
- package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
- package/dist/types/chunking/strategies/recursive.d.ts +20 -0
- package/dist/types/chunking/strategies/semantic.d.ts +19 -0
- package/dist/types/chunking/strategies/sentence.d.ts +13 -0
- package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
- package/dist/types/citation/citation-tracker.d.ts +64 -0
- package/dist/types/citation/hallucination-detector.d.ts +40 -0
- package/dist/types/citation/source-mapper.d.ts +37 -0
- package/dist/types/connectors/anthropic-connector.d.ts +97 -0
- package/dist/types/connectors/base-connector.d.ts +87 -0
- package/dist/types/connectors/cohere-connector.d.ts +94 -0
- package/dist/types/connectors/connector-utils.d.ts +110 -0
- package/dist/types/connectors/local-embedder.d.ts +95 -0
- package/dist/types/connectors/memory-retriever.d.ts +99 -0
- package/dist/types/connectors/ollama-connector.d.ts +115 -0
- package/dist/types/connectors/openai-connector.d.ts +129 -0
- package/dist/types/connectors/rag-prompt.d.ts +40 -0
- 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/cost/cost-calculator.d.ts +152 -0
- package/dist/types/cost/cost-tracker.d.ts +126 -0
- package/dist/types/cost/token-budget.d.ts +110 -0
- package/dist/types/dag/core/error-context.d.ts +239 -0
- package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
- package/dist/types/dag/core/topology-validation.d.ts +226 -0
- package/dist/types/dag/dag-engine.d.ts +156 -0
- package/dist/types/dag/dag-node.d.ts +37 -0
- package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
- package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
- package/dist/types/debug/execution-tracer.d.ts +57 -0
- package/dist/types/debug/trace-inspector.d.ts +31 -0
- package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
- package/dist/types/enterprise/data-governance.d.ts +203 -0
- package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
- package/dist/types/enterprise/sso-integration.d.ts +420 -0
- package/dist/types/evaluate/context-metrics.d.ts +55 -0
- package/dist/types/evaluate/faithfulness.d.ts +41 -0
- package/dist/types/evaluate/groundedness.d.ts +60 -0
- package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
- package/dist/types/evaluate/relevance.d.ts +21 -0
- package/dist/types/evaluate/scoring.d.ts +123 -0
- package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
- package/dist/types/experimental.d.ts +15 -0
- package/dist/types/graph/entity-extractor.d.ts +84 -0
- package/dist/types/graph/graph-index.d.ts +49 -0
- package/dist/types/graph/graph-retriever.d.ts +81 -0
- package/dist/types/graph/knowledge-graph.d.ts +129 -0
- package/dist/types/grounding/check-answer.d.ts +101 -0
- package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
- package/dist/types/guardrails/injection-detector.d.ts +47 -0
- package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
- package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
- package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
- package/dist/types/mcp/mcp-server.d.ts +85 -0
- package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
- package/dist/types/reranker/cascade-reranker.d.ts +40 -0
- package/dist/types/reranker/embedding-reranker.d.ts +77 -0
- package/dist/types/reranker/scoring-reranker.d.ts +41 -0
- package/dist/types/retrieval/bm25-search.d.ts +43 -0
- package/dist/types/retrieval/file-vector-store.d.ts +99 -0
- package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
- package/dist/types/retrieval/rank-fusion.d.ts +10 -0
- package/dist/types/security/jwt-config.d.ts +80 -0
- package/dist/types/security/jwt-validator.d.ts +195 -0
- package/dist/types/security/plugin-sandbox.d.ts +158 -0
- package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
- package/dist/types/security/validator.d.ts +5 -0
- package/dist/types/streaming/embedding-stream.d.ts +65 -0
- package/dist/types/streaming/sse-adapter.d.ts +59 -0
- package/dist/types/streaming/stream-router.d.ts +88 -0
- package/dist/types/streaming/streaming-embedder.d.ts +99 -0
- package/dist/types/streaming/websocket-adapter.d.ts +71 -0
- package/dist/types/templates/scaffold.d.ts +55 -0
- package/dist/types/templates/template-registry.d.ts +63 -0
- package/dist/types/testing.d.ts +168 -0
- package/dist/types/utils/embed-query.d.ts +20 -0
- package/dist/types/utils/logger.d.ts +50 -0
- package/dist/types/utils/rate-limiter.d.ts +66 -0
- package/dist/types/utils/retry.d.ts +27 -0
- package/dist/types/utils/run-abort.d.ts +34 -0
- package/dist/types/utils/secure-logging.d.ts +51 -0
- package/dist/types/utils/structured-logger.d.ts +126 -0
- package/package.json +72 -33
- package/src/adapters/ai-sdk.js +215 -0
- package/src/agentic/agentic-pipeline.js +75 -7
- package/src/agentic/iterative-retriever.js +47 -10
- package/src/agentic/query-planner.js +329 -78
- 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/citation/citation-tracker.js +6 -4
- package/src/citation/hallucination-detector.js +2 -2
- package/src/citation/source-mapper.js +15 -28
- package/src/cli/commands/ai.js +106 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
- package/src/cli/commands/plugin-hub/local-commands.js +143 -0
- package/src/cli/commands/plugin-hub.js +114 -49
- package/src/cli/diagnostics/config-checker.js +76 -63
- package/src/cli/diagnostics/environment-checker.js +62 -31
- package/src/cli/diagnostics/plugin-checker.js +36 -89
- package/src/cli/doctor-command.js +39 -5
- package/src/cli/enhanced-cli-commands.js +104 -233
- package/src/cli/handlers/ai-handler.js +364 -0
- package/src/cli/handlers/diagnostics-handler.js +78 -31
- package/src/cli/handlers/ingest-handler.js +28 -22
- package/src/cli/handlers/init-handler.js +148 -25
- package/src/cli/handlers/query-handler.js +203 -25
- package/src/cli/handlers/run-handler.js +16 -167
- package/src/cli/handlers/serve-handler.js +60 -0
- package/src/cli/services/bench-stats.js +54 -0
- package/src/cli/services/benchmark.js +161 -0
- package/src/cli/services/feedback.js +155 -0
- package/src/cli/services/instrument.js +83 -0
- package/src/cli/services/rag-service.js +361 -0
- package/src/cli/shared/cli-utils.js +24 -7
- package/src/cli/shared/config-context.js +21 -0
- package/src/cli/wizard/config-builder.js +81 -0
- package/src/cli/wizard/init-wizard.js +602 -0
- package/src/config/component-factory.js +125 -0
- package/src/config/enhanced-ragrc-schema.js +0 -2
- package/src/config/load-config.js +29 -10
- package/src/config/load-plugin-config.js +46 -2
- package/src/config/validate-schema.js +4 -1
- package/src/connectors/anthropic-connector.js +115 -57
- package/src/connectors/base-connector.js +74 -19
- package/src/connectors/cohere-connector.js +108 -34
- package/src/connectors/connector-utils.js +302 -0
- package/src/connectors/local-embedder.js +63 -5
- package/src/connectors/memory-retriever.js +125 -14
- package/src/connectors/ollama-connector.js +112 -51
- package/src/connectors/openai-connector.js +153 -44
- package/src/connectors/rag-prompt.js +88 -0
- package/src/core/create-pipeline.js +307 -61
- package/src/core/performance/parallel-processor.js +62 -1
- package/src/core/plugin-registry.js +125 -26
- package/src/core/stage-contracts.js +36 -0
- package/src/core/validate-plugin.js +85 -0
- package/src/cost/cost-calculator.js +205 -32
- package/src/debug/execution-tracer.js +14 -4
- package/src/dev/dev-server.js +173 -11
- package/src/dev/hot-reload.js +44 -4
- package/src/ecosystem/plugin-checks.js +181 -0
- package/src/ecosystem/plugin-hub.js +50 -125
- package/src/ecosystem/plugin-installer.js +298 -0
- package/src/ecosystem/plugin-probe.js +127 -0
- package/src/ecosystem/plugin-scanner.js +185 -0
- package/src/ecosystem/tar-extract.js +114 -0
- package/src/enterprise/audit-logging.js +158 -29
- package/src/enterprise/sso-integration.js +716 -228
- package/src/evaluate/context-metrics.js +28 -16
- package/src/evaluate/evaluator.js +3 -3
- package/src/evaluate/faithfulness.js +18 -14
- package/src/evaluate/groundedness.js +43 -12
- package/src/evaluate/pipeline-evaluator.js +35 -8
- package/src/evaluate/scoring.js +251 -3
- package/src/evaluate/semantic-similarity.js +79 -0
- package/src/experimental.js +71 -0
- package/src/grounding/check-answer.js +173 -0
- package/src/guardrails/guardrails-pipeline.js +69 -7
- package/src/guardrails/injection-detector.js +283 -0
- package/src/guardrails/post-generation-guard.js +19 -0
- package/src/guardrails/pre-retrieval-guard.js +36 -36
- package/src/guardrails/retrieval-guard.js +58 -8
- package/src/index.js +58 -1
- package/src/loader/csv-loader.js +3 -2
- package/src/loader/document-reader.js +91 -0
- package/src/loader/html-loader.js +3 -1
- package/src/loader/markdown-loader.js +3 -2
- package/src/reranker/cascade-reranker.js +3 -0
- package/src/reranker/embedding-reranker.js +136 -21
- package/src/reranker/llm-reranker.js +79 -12
- package/src/reranker/scoring-reranker.js +3 -0
- package/src/retrieval/bm25-search.js +12 -6
- package/src/retrieval/file-vector-store.js +241 -0
- package/src/retrieval/hybrid-retriever.js +22 -19
- package/src/security/jwt-config.js +13 -2
- package/src/security/jwt-validator.js +139 -34
- package/src/security/plugin-sandbox.js +18 -1
- package/src/security/plugin-signature-verifier.js +245 -70
- package/src/testing.js +254 -0
- package/src/utils/deprecate.js +69 -0
- package/src/utils/embed-query.js +33 -0
- package/src/utils/input-sanitizer.js +19 -4
- package/src/utils/logger.js +34 -4
- package/src/utils/run-abort.js +102 -0
- package/src/ai/federated-learning.js +0 -211
- package/src/ai/federation.js +0 -71
- package/src/ai/index.js +0 -22
- package/src/ai/model-trainer.js +0 -217
- package/src/ai/model-training.js +0 -202
- package/src/ai/multimodal/multi-modal-processor.js +0 -345
- package/src/ai/orchestrator.js +0 -65
- package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
- package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
- package/src/cli/commands/ai-ml/federated-commands.js +0 -207
- package/src/cli/commands/ai-ml/index.js +0 -17
- package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
- package/src/cli/commands/ai-ml/shared.js +0 -51
- package/src/cli/commands/ai-ml/training-commands.js +0 -228
- package/src/cli/commands/ai-ml.js +0 -386
- package/src/cli/commands/docs.js +0 -228
- package/src/cli/commands/dx.js +0 -538
- package/src/cli/commands/wizard.js +0 -627
- package/src/cli/input-sanitization-middleware.js +0 -356
- package/src/cli/interactive-wizard.js +0 -373
- package/src/cli/templates/development.template.json +0 -64
- package/src/cli/templates/minimal.template.json +0 -33
- package/src/cli/templates/production.template.json +0 -104
- package/src/cli/templates/testing.template.json +0 -45
- package/src/cli/wizard/configuration.js +0 -148
- package/src/cli/wizard/observability.js +0 -79
- package/src/cli/wizard/performance.js +0 -76
- package/src/cli/wizard/plugin-selection.js +0 -297
- package/src/cli/wizard/preview-save.js +0 -172
- package/src/cli/wizard/project-setup.js +0 -66
- package/src/core/pipeline-factory.js +0 -59
- package/src/dag/enhanced-dag-engine.js +0 -894
- package/src/dx/index.js +0 -40
- package/src/dx/integration-templates.js +0 -662
- package/src/dx/performance-profiler.js +0 -968
- package/src/dx/realtime-debugger.js +0 -755
- package/src/dx/visual-pipeline-builder.js +0 -578
- package/src/ingest.js +0 -59
- package/src/pipeline/pipeline.js +0 -18
- package/src/query.js +0 -117
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve command handler
|
|
3
|
+
* Runs DevServer: a local HTTP API (GET /health, POST /query) over the
|
|
4
|
+
* pipeline in .ragrc.json, reloading custom plugin modules on change.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { loadCliConfig } = require("../shared/config-context.js");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} globalOptions - Global CLI options from program.opts()
|
|
12
|
+
* @param {object} options - { port, host }
|
|
13
|
+
* @returns {Promise<object|undefined>} The started server (for tests)
|
|
14
|
+
*/
|
|
15
|
+
async function handleServe(globalOptions, options = {}) {
|
|
16
|
+
try {
|
|
17
|
+
// Validate the config up front so a broken file fails at startup.
|
|
18
|
+
const { config, configPath, configDir } = loadCliConfig(globalOptions);
|
|
19
|
+
const pluginFiles = Object.values(config.plugins || {})
|
|
20
|
+
.flatMap((group) => Object.values(group || {}))
|
|
21
|
+
.filter((p) => typeof p === "string")
|
|
22
|
+
.map((p) => path.resolve(configDir, p));
|
|
23
|
+
|
|
24
|
+
const { DevServer } = require("../../dev/dev-server.js");
|
|
25
|
+
const server = new DevServer({
|
|
26
|
+
port: options.port,
|
|
27
|
+
host: options.host,
|
|
28
|
+
configPath,
|
|
29
|
+
watchPaths: pluginFiles,
|
|
30
|
+
verbose: false,
|
|
31
|
+
});
|
|
32
|
+
await server.start();
|
|
33
|
+
|
|
34
|
+
console.log(`🚀 Serving ${path.basename(configPath)} at ${server.url}`);
|
|
35
|
+
console.log(` GET ${server.url}/health`);
|
|
36
|
+
console.log(` POST ${server.url}/query {"question": "...", "topK": 3}`);
|
|
37
|
+
if (
|
|
38
|
+
!["127.0.0.1", "localhost", "::1"].includes(options.host || "127.0.0.1")
|
|
39
|
+
) {
|
|
40
|
+
console.warn(
|
|
41
|
+
`⚠️ Listening on ${options.host}: the API has no authentication and anyone who can reach this address can query your index.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
console.log("Press Ctrl+C to stop.");
|
|
45
|
+
|
|
46
|
+
const shutdown = async () => {
|
|
47
|
+
await server.stop();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
};
|
|
50
|
+
process.once("SIGINT", shutdown);
|
|
51
|
+
process.once("SIGTERM", shutdown);
|
|
52
|
+
return server;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`❌ Server failed to start: ${error.message}`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { handleServe };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Summary statistics for latency samples measured by `rag-pipeline ai
|
|
5
|
+
* benchmark`. Pure functions so the math can be unit-tested.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Nearest-rank percentile: the smallest sample such that at least p% of the
|
|
10
|
+
* samples are less than or equal to it. Always returns an observed value.
|
|
11
|
+
*
|
|
12
|
+
* @param {number[]} samples - Unsorted samples (not modified)
|
|
13
|
+
* @param {number} p - Percentile in (0, 100]
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
function percentile(samples, p) {
|
|
17
|
+
if (!Array.isArray(samples) || samples.length === 0) {
|
|
18
|
+
throw new Error("percentile() needs at least one sample");
|
|
19
|
+
}
|
|
20
|
+
if (!(p > 0 && p <= 100)) {
|
|
21
|
+
throw new Error(`Percentile must be in (0, 100], got ${p}`);
|
|
22
|
+
}
|
|
23
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
24
|
+
const rank = Math.ceil((p / 100) * sorted.length);
|
|
25
|
+
return sorted[rank - 1];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {number[]} samples
|
|
30
|
+
* @returns {number} Arithmetic mean
|
|
31
|
+
*/
|
|
32
|
+
function mean(samples) {
|
|
33
|
+
if (!Array.isArray(samples) || samples.length === 0) {
|
|
34
|
+
throw new Error("mean() needs at least one sample");
|
|
35
|
+
}
|
|
36
|
+
return samples.reduce((sum, x) => sum + x, 0) / samples.length;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {number[]} samples - Latencies in milliseconds
|
|
41
|
+
* @returns {{count: number, mean: number, p50: number, p95: number, min: number, max: number}}
|
|
42
|
+
*/
|
|
43
|
+
function summarize(samples) {
|
|
44
|
+
return {
|
|
45
|
+
count: samples.length,
|
|
46
|
+
mean: mean(samples),
|
|
47
|
+
p50: percentile(samples, 50),
|
|
48
|
+
p95: percentile(samples, 95),
|
|
49
|
+
min: Math.min(...samples),
|
|
50
|
+
max: Math.max(...samples),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { percentile, mean, summarize };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `rag-pipeline ai benchmark`: time real queries against the configured
|
|
5
|
+
* pipeline and index. Every number reported is measured here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const { embedQuery } = require("../../utils/embed-query");
|
|
10
|
+
const { loadComponents, query } = require("./rag-service");
|
|
11
|
+
const { summarize } = require("./bench-stats");
|
|
12
|
+
|
|
13
|
+
const SAMPLE_QUERIES = 5;
|
|
14
|
+
const SAMPLE_WORDS = 8;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read queries from a file: a JSON array of strings, or one query per line.
|
|
18
|
+
* @param {string} file
|
|
19
|
+
* @returns {string[]}
|
|
20
|
+
*/
|
|
21
|
+
function readQueryFile(file) {
|
|
22
|
+
if (!fs.existsSync(file)) throw new Error(`Queries file not found: ${file}`);
|
|
23
|
+
const text = fs.readFileSync(file, "utf8");
|
|
24
|
+
const trimmed = text.trim();
|
|
25
|
+
let queries;
|
|
26
|
+
if (trimmed.startsWith("[")) {
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(trimmed);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Queries file ${file} is not valid JSON: ${error.message}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!parsed.every((q) => typeof q === "string")) {
|
|
36
|
+
throw new Error(`Queries file ${file} must be a JSON array of strings`);
|
|
37
|
+
}
|
|
38
|
+
queries = parsed;
|
|
39
|
+
} else {
|
|
40
|
+
queries = text.split(/\r?\n/);
|
|
41
|
+
}
|
|
42
|
+
queries = queries.map((q) => q.trim()).filter(Boolean);
|
|
43
|
+
if (queries.length === 0) throw new Error(`No queries found in ${file}`);
|
|
44
|
+
return queries;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Deterministic sample: the first words of the first few indexed chunks.
|
|
49
|
+
* @param {Array<{content: string}>} chunks
|
|
50
|
+
* @returns {string[]}
|
|
51
|
+
*/
|
|
52
|
+
function sampleQueries(chunks, count = SAMPLE_QUERIES) {
|
|
53
|
+
return chunks
|
|
54
|
+
.map((c) => String(c.content).split(/\s+/).filter(Boolean))
|
|
55
|
+
.filter((words) => words.length > 0)
|
|
56
|
+
.slice(0, count)
|
|
57
|
+
.map((words) => words.slice(0, SAMPLE_WORDS).join(" "));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const elapsedMs = (start) => Number(process.hrtime.bigint() - start) / 1e6;
|
|
61
|
+
|
|
62
|
+
async function timed(fn) {
|
|
63
|
+
const start = process.hrtime.bigint();
|
|
64
|
+
const value = await fn();
|
|
65
|
+
return { value, ms: elapsedMs(start) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {object} params
|
|
70
|
+
* @param {object} params.config
|
|
71
|
+
* @param {string} params.configDir
|
|
72
|
+
* @param {string} [params.queriesFile]
|
|
73
|
+
* @param {number} [params.runs=3] - Passes over the query set
|
|
74
|
+
* @param {number} [params.topK=3]
|
|
75
|
+
* @returns {Promise<object>} Measured report
|
|
76
|
+
*/
|
|
77
|
+
async function runBenchmark({
|
|
78
|
+
config,
|
|
79
|
+
configDir,
|
|
80
|
+
queriesFile,
|
|
81
|
+
runs = 3,
|
|
82
|
+
topK = 3,
|
|
83
|
+
}) {
|
|
84
|
+
const setup = await timed(() => loadComponents({ config, configDir }));
|
|
85
|
+
const components = setup.value;
|
|
86
|
+
const { embedder, retriever, reranker, llm } = components;
|
|
87
|
+
|
|
88
|
+
const queries = queriesFile
|
|
89
|
+
? readQueryFile(queriesFile)
|
|
90
|
+
: sampleQueries(retriever.list ? retriever.list() : []);
|
|
91
|
+
if (queries.length === 0) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"No queries: pass --queries <file> (the index has no text to sample from).",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// First query pays one-time costs (lazy init, JIT); reported, not counted.
|
|
98
|
+
const warmup = await timed(() =>
|
|
99
|
+
query({ components, question: queries[0], topK }),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const samples = { embed: [], retrieve: [], rerank: [], endToEnd: [] };
|
|
103
|
+
const resultCounts = [];
|
|
104
|
+
for (let run = 0; run < runs; run++) {
|
|
105
|
+
for (const question of queries) {
|
|
106
|
+
const embedded = await timed(() => embedQuery(embedder, question));
|
|
107
|
+
samples.embed.push(embedded.ms);
|
|
108
|
+
const retrieved = await timed(() =>
|
|
109
|
+
retriever.retrieve({
|
|
110
|
+
query: question,
|
|
111
|
+
queryVector: embedded.value,
|
|
112
|
+
topK,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
samples.retrieve.push(retrieved.ms);
|
|
116
|
+
if (reranker) {
|
|
117
|
+
const reranked = await timed(() =>
|
|
118
|
+
reranker.rerank(question, retrieved.value, { topK }),
|
|
119
|
+
);
|
|
120
|
+
samples.rerank.push(reranked.ms);
|
|
121
|
+
}
|
|
122
|
+
const e2e = await timed(() => query({ components, question, topK }));
|
|
123
|
+
samples.endToEnd.push(e2e.ms);
|
|
124
|
+
resultCounts.push(e2e.value.results.length);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const stages = {
|
|
129
|
+
embed: summarize(samples.embed),
|
|
130
|
+
retrieve: summarize(samples.retrieve),
|
|
131
|
+
...(reranker && { rerank: summarize(samples.rerank) }),
|
|
132
|
+
endToEnd: summarize(samples.endToEnd),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
queries,
|
|
137
|
+
queriesSource: queriesFile || "sampled from indexed chunks",
|
|
138
|
+
runs,
|
|
139
|
+
topK,
|
|
140
|
+
setupMs: setup.ms,
|
|
141
|
+
warmupMs: warmup.ms,
|
|
142
|
+
stages,
|
|
143
|
+
includesGeneration: Boolean(llm),
|
|
144
|
+
results: {
|
|
145
|
+
min: Math.min(...resultCounts),
|
|
146
|
+
max: Math.max(...resultCounts),
|
|
147
|
+
},
|
|
148
|
+
environment: {
|
|
149
|
+
node: process.version,
|
|
150
|
+
platform: `${process.platform} ${process.arch}`,
|
|
151
|
+
chunks: retriever.size ? retriever.size() : null,
|
|
152
|
+
embedder: components.names.embedder,
|
|
153
|
+
dimensions: retriever.getEmbedderInfo?.()?.dimensions ?? null,
|
|
154
|
+
retriever: components.names.retriever,
|
|
155
|
+
reranker: components.names.reranker || null,
|
|
156
|
+
llm: components.names.llm || null,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { runBenchmark, readQueryFile, sampleQueries };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Relevance feedback for `rag-pipeline ai feedback` / `ai search`.
|
|
5
|
+
*
|
|
6
|
+
* Feedback is stored as JSON next to the index. Scoring is deterministic and
|
|
7
|
+
* explainable: every stored vote for a result's docId contributes
|
|
8
|
+
*
|
|
9
|
+
* sign(vote) * VOTE_WEIGHT * jaccard(terms(question), terms(votedQuestion))
|
|
10
|
+
*
|
|
11
|
+
* and the summed adjustment is capped to +/- MAX_ADJUSTMENT before being added
|
|
12
|
+
* to the retrieval score. Votes on unrelated questions (no shared terms)
|
|
13
|
+
* contribute nothing.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const path = require("path");
|
|
18
|
+
const { tokenize } = require("../../evaluate/scoring");
|
|
19
|
+
|
|
20
|
+
const FORMAT_VERSION = 1;
|
|
21
|
+
const VOTE_WEIGHT = 0.1;
|
|
22
|
+
const MAX_ADJUSTMENT = 0.3;
|
|
23
|
+
const RATINGS = ["up", "down"];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Where feedback lives: next to a file-store index, else <configDir>/.rag.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} retriever - Built retriever (FileVectorStore has `path`)
|
|
29
|
+
* @param {string} configDir
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function feedbackPath(retriever, configDir) {
|
|
33
|
+
const dir =
|
|
34
|
+
retriever && typeof retriever.path === "string"
|
|
35
|
+
? path.dirname(retriever.path)
|
|
36
|
+
: path.join(configDir, ".rag");
|
|
37
|
+
return path.join(dir, "feedback.json");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} file
|
|
42
|
+
* @returns {{version: number, entries: Array<{question: string, docId: string, rating: string, at: string}>}}
|
|
43
|
+
*/
|
|
44
|
+
function loadFeedback(file) {
|
|
45
|
+
if (!fs.existsSync(file)) return { version: FORMAT_VERSION, entries: [] };
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Feedback file ${file} is not valid JSON: ${error.message}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (parsed.version !== FORMAT_VERSION || !Array.isArray(parsed.entries)) {
|
|
55
|
+
throw new Error(`Unsupported feedback file format in ${file}`);
|
|
56
|
+
}
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Append one vote and write the file atomically (temp file, then rename).
|
|
62
|
+
*
|
|
63
|
+
* @param {string} file
|
|
64
|
+
* @param {{question: string, docId: string, rating: string}} vote
|
|
65
|
+
* @returns {{entry: object, total: number}}
|
|
66
|
+
*/
|
|
67
|
+
function addFeedback(file, { question, docId, rating }) {
|
|
68
|
+
if (!RATINGS.includes(rating)) {
|
|
69
|
+
throw new Error(`Rating must be one of: ${RATINGS.join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
if (!String(question || "").trim()) throw new Error("Question is empty");
|
|
72
|
+
const data = loadFeedback(file);
|
|
73
|
+
const entry = { question, docId, rating, at: new Date().toISOString() };
|
|
74
|
+
data.entries.push(entry);
|
|
75
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
76
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
77
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
78
|
+
fs.renameSync(tmp, file);
|
|
79
|
+
return { entry, total: data.entries.length };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Jaccard similarity of the two texts' token sets (0..1).
|
|
84
|
+
* @param {string} a
|
|
85
|
+
* @param {string} b
|
|
86
|
+
* @returns {number}
|
|
87
|
+
*/
|
|
88
|
+
function termSimilarity(a, b) {
|
|
89
|
+
const sa = new Set(tokenize(a));
|
|
90
|
+
const sb = new Set(tokenize(b));
|
|
91
|
+
if (sa.size === 0 || sb.size === 0) return 0;
|
|
92
|
+
let shared = 0;
|
|
93
|
+
for (const t of sa) if (sb.has(t)) shared++;
|
|
94
|
+
return shared / (sa.size + sb.size - shared);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Re-score retrieval results with stored feedback.
|
|
99
|
+
*
|
|
100
|
+
* @param {Array<{id: string, score: number}>} results - Retrieval results
|
|
101
|
+
* @param {Array<{question: string, docId: string, rating: string}>} entries - Stored votes
|
|
102
|
+
* @param {string} question - Current question
|
|
103
|
+
* @param {object} [options]
|
|
104
|
+
* @param {number} [options.voteWeight=VOTE_WEIGHT]
|
|
105
|
+
* @param {number} [options.maxAdjustment=MAX_ADJUSTMENT]
|
|
106
|
+
* @returns {Array<object>} Results sorted by finalScore (ties keep retrieval
|
|
107
|
+
* order), each with baseScore, adjustment, finalScore and `reasons`.
|
|
108
|
+
*/
|
|
109
|
+
function applyFeedback(results, entries, question, options = {}) {
|
|
110
|
+
const voteWeight = options.voteWeight ?? VOTE_WEIGHT;
|
|
111
|
+
const maxAdjustment = options.maxAdjustment ?? MAX_ADJUSTMENT;
|
|
112
|
+
|
|
113
|
+
const scored = results.map((result, rank) => {
|
|
114
|
+
const reasons = [];
|
|
115
|
+
let raw = 0;
|
|
116
|
+
for (const vote of entries) {
|
|
117
|
+
if (vote.docId !== result.id) continue;
|
|
118
|
+
const similarity = termSimilarity(question, vote.question);
|
|
119
|
+
if (similarity === 0) continue;
|
|
120
|
+
const contribution =
|
|
121
|
+
(vote.rating === "up" ? 1 : -1) * voteWeight * similarity;
|
|
122
|
+
raw += contribution;
|
|
123
|
+
reasons.push({
|
|
124
|
+
rating: vote.rating,
|
|
125
|
+
question: vote.question,
|
|
126
|
+
similarity,
|
|
127
|
+
contribution,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const adjustment = Math.max(-maxAdjustment, Math.min(maxAdjustment, raw));
|
|
131
|
+
const baseScore = typeof result.score === "number" ? result.score : 0;
|
|
132
|
+
return {
|
|
133
|
+
...result,
|
|
134
|
+
baseScore,
|
|
135
|
+
adjustment,
|
|
136
|
+
finalScore: baseScore + adjustment,
|
|
137
|
+
reasons,
|
|
138
|
+
rank,
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
scored.sort((a, b) => b.finalScore - a.finalScore || a.rank - b.rank);
|
|
143
|
+
return scored.map(({ rank, ...rest }) => rest);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = {
|
|
147
|
+
feedbackPath,
|
|
148
|
+
loadFeedback,
|
|
149
|
+
addFeedback,
|
|
150
|
+
termSimilarity,
|
|
151
|
+
applyFeedback,
|
|
152
|
+
RATINGS,
|
|
153
|
+
VOTE_WEIGHT,
|
|
154
|
+
MAX_ADJUSTMENT,
|
|
155
|
+
};
|
|
@@ -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 };
|