@devilsdev/rag-pipeline-utils 2.3.0 → 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 -361
- 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,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
splitSentences,
|
|
5
|
+
tokenize,
|
|
6
|
+
computeJaccardSimilarity,
|
|
7
|
+
} = require('./scoring');
|
|
8
|
+
const { mapSentenceToSources } = require('../citation/source-mapper');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute faithfulness score for an answer against retrieved documents.
|
|
12
|
+
* Measures how well each sentence in the answer is supported by the retrieved context.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} answer - The generated answer text
|
|
15
|
+
* @param {Array<{content: string}>} retrievedDocs - Array of retrieved documents with content
|
|
16
|
+
* @param {object} [options={}] - Configuration options
|
|
17
|
+
* @param {number} [options.threshold=0.3] - Minimum similarity score to consider a sentence faithful
|
|
18
|
+
* @returns {{ score: number, faithfulSentences: number, totalSentences: number, details: Array<{ sentence: string, score: number, isFaithful: boolean, bestSource: number }> }}
|
|
19
|
+
*/
|
|
20
|
+
function computeFaithfulness(answer, retrievedDocs, options = {}) {
|
|
21
|
+
const { threshold = 0.3 } = options;
|
|
22
|
+
|
|
23
|
+
if (!answer || typeof answer !== 'string' || answer.trim().length === 0) {
|
|
24
|
+
return { score: 0, faithfulSentences: 0, totalSentences: 0, details: [] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!Array.isArray(retrievedDocs) || retrievedDocs.length === 0) {
|
|
28
|
+
const sentences = splitSentences(answer);
|
|
29
|
+
const details = sentences.map((sentence) => ({
|
|
30
|
+
sentence,
|
|
31
|
+
score: 0,
|
|
32
|
+
isFaithful: false,
|
|
33
|
+
bestSource: -1,
|
|
34
|
+
}));
|
|
35
|
+
return {
|
|
36
|
+
score: 0,
|
|
37
|
+
faithfulSentences: 0,
|
|
38
|
+
totalSentences: sentences.length,
|
|
39
|
+
details,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sentences = splitSentences(answer);
|
|
44
|
+
|
|
45
|
+
if (sentences.length === 0) {
|
|
46
|
+
return { score: 0, faithfulSentences: 0, totalSentences: 0, details: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const details = sentences.map((sentence) => {
|
|
50
|
+
const sentenceTokens = tokenize(sentence);
|
|
51
|
+
let bestScore = 0;
|
|
52
|
+
let bestSource = -1;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < retrievedDocs.length; i++) {
|
|
55
|
+
const doc = retrievedDocs[i];
|
|
56
|
+
const docContent = doc && (doc.content || doc.text || '');
|
|
57
|
+
|
|
58
|
+
if (!docContent) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const docTokens = tokenize(docContent);
|
|
63
|
+
const similarity = computeJaccardSimilarity(sentenceTokens, docTokens);
|
|
64
|
+
|
|
65
|
+
if (similarity > bestScore) {
|
|
66
|
+
bestScore = similarity;
|
|
67
|
+
bestSource = i;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
sentence,
|
|
73
|
+
score: bestScore,
|
|
74
|
+
isFaithful: bestScore >= threshold,
|
|
75
|
+
bestSource,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const faithfulSentences = details.filter((d) => d.isFaithful).length;
|
|
80
|
+
const totalSentences = details.length;
|
|
81
|
+
const score = totalSentences > 0 ? faithfulSentences / totalSentences : 0;
|
|
82
|
+
|
|
83
|
+
return { score, faithfulSentences, totalSentences, details };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Derive faithfulness score from an existing citation result.
|
|
88
|
+
* Shortcut that reuses citation analysis data rather than recomputing.
|
|
89
|
+
*
|
|
90
|
+
* @param {object} citationResult - Result from citation analysis
|
|
91
|
+
* @param {number} citationResult.groundednessScore - Pre-computed groundedness score
|
|
92
|
+
* @param {Array} [citationResult.citations] - Array of citation details
|
|
93
|
+
* @returns {{ score: number, details: Array }}
|
|
94
|
+
*/
|
|
95
|
+
function computeFaithfulnessFromCitations(citationResult) {
|
|
96
|
+
if (!citationResult || typeof citationResult !== 'object') {
|
|
97
|
+
return { score: 0, details: [] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const score =
|
|
101
|
+
typeof citationResult.groundednessScore === 'number'
|
|
102
|
+
? citationResult.groundednessScore
|
|
103
|
+
: 0;
|
|
104
|
+
|
|
105
|
+
const details = Array.isArray(citationResult.citations)
|
|
106
|
+
? citationResult.citations.map((c) => ({
|
|
107
|
+
sentence: c.sentence || c.text || '',
|
|
108
|
+
score: c.score || c.similarity || 0,
|
|
109
|
+
isFaithful: (c.score || c.similarity || 0) > 0,
|
|
110
|
+
bestSource: c.sourceIndex != null ? c.sourceIndex : -1,
|
|
111
|
+
}))
|
|
112
|
+
: [];
|
|
113
|
+
|
|
114
|
+
return { score, details };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { computeFaithfulness, computeFaithfulnessFromCitations };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { computeFaithfulness } = require('./faithfulness');
|
|
4
|
+
const {
|
|
5
|
+
computeContextPrecision,
|
|
6
|
+
computeContextRecall,
|
|
7
|
+
} = require('./context-metrics');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default weights for the groundedness composite score.
|
|
11
|
+
* @type {{ faithfulness: number, contextPrecision: number, contextRecall: number }}
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_WEIGHTS = {
|
|
14
|
+
faithfulness: 0.4,
|
|
15
|
+
contextPrecision: 0.3,
|
|
16
|
+
contextRecall: 0.3,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Compute a composite groundedness score combining faithfulness, context precision, and context recall.
|
|
21
|
+
* Uses a weighted average of the three component metrics.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} query - The user query
|
|
24
|
+
* @param {string} answer - The generated answer text
|
|
25
|
+
* @param {Array<{content: string}>} retrievedDocs - Array of retrieved documents
|
|
26
|
+
* @param {object} [options={}] - Configuration options
|
|
27
|
+
* @param {{ faithfulness: number, contextPrecision: number, contextRecall: number }} [options.weights] - Custom weights for each component
|
|
28
|
+
* @param {number} [options.threshold=0.3] - Similarity threshold passed to component metrics
|
|
29
|
+
* @returns {{ score: number, components: { faithfulness: object, contextPrecision: object, contextRecall: object }, weights: { faithfulness: number, contextPrecision: number, contextRecall: number } }}
|
|
30
|
+
*/
|
|
31
|
+
function computeGroundedness(query, answer, retrievedDocs, options = {}) {
|
|
32
|
+
const { weights = DEFAULT_WEIGHTS, threshold = 0.3 } = options;
|
|
33
|
+
|
|
34
|
+
const faithfulness = computeFaithfulness(answer, retrievedDocs, {
|
|
35
|
+
threshold,
|
|
36
|
+
});
|
|
37
|
+
const contextPrecision = computeContextPrecision(query, retrievedDocs, {
|
|
38
|
+
threshold: threshold * 0.67,
|
|
39
|
+
});
|
|
40
|
+
const contextRecall = computeContextRecall(answer, retrievedDocs, {
|
|
41
|
+
threshold,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const score =
|
|
45
|
+
weights.faithfulness * faithfulness.score +
|
|
46
|
+
weights.contextPrecision * contextPrecision.score +
|
|
47
|
+
weights.contextRecall * contextRecall.score;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
score,
|
|
51
|
+
components: {
|
|
52
|
+
faithfulness,
|
|
53
|
+
contextPrecision,
|
|
54
|
+
contextRecall,
|
|
55
|
+
},
|
|
56
|
+
weights,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { computeGroundedness, DEFAULT_WEIGHTS };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
const {
|
|
5
|
+
computeFaithfulness,
|
|
6
|
+
computeFaithfulnessFromCitations,
|
|
7
|
+
} = require('./faithfulness');
|
|
8
|
+
const { computeAnswerRelevance } = require('./relevance');
|
|
9
|
+
const {
|
|
10
|
+
computeContextPrecision,
|
|
11
|
+
computeContextRecall,
|
|
12
|
+
} = require('./context-metrics');
|
|
13
|
+
const { computeGroundedness } = require('./groundedness');
|
|
14
|
+
const { scoreAnswer } = require('./scoring');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default configuration for PipelineEvaluator.
|
|
18
|
+
* @type {{ metrics: string[], threshold: number, includeBLEU: boolean, includeROUGE: boolean }}
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_CONFIG = {
|
|
21
|
+
metrics: [
|
|
22
|
+
'faithfulness',
|
|
23
|
+
'relevance',
|
|
24
|
+
'contextPrecision',
|
|
25
|
+
'contextRecall',
|
|
26
|
+
'groundedness',
|
|
27
|
+
],
|
|
28
|
+
threshold: 0.3,
|
|
29
|
+
includeBLEU: false,
|
|
30
|
+
includeROUGE: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Evaluates RAG pipeline outputs using multiple quality metrics.
|
|
35
|
+
* Emits an 'evaluated' event after each evaluation with the full result.
|
|
36
|
+
*
|
|
37
|
+
* @extends EventEmitter
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const evaluator = new PipelineEvaluator({ metrics: ['faithfulness', 'relevance'] });
|
|
41
|
+
* evaluator.on('evaluated', (result) => console.log(result.scores));
|
|
42
|
+
* const result = evaluator.evaluate({ query: 'What is RAG?', answer: '...', results: [...] });
|
|
43
|
+
*/
|
|
44
|
+
class PipelineEvaluator extends EventEmitter {
|
|
45
|
+
/**
|
|
46
|
+
* Create a new PipelineEvaluator.
|
|
47
|
+
* @param {object} [options={}] - Configuration options
|
|
48
|
+
* @param {string[]} [options.metrics] - List of metrics to compute
|
|
49
|
+
* @param {number} [options.threshold] - Similarity threshold for metrics
|
|
50
|
+
* @param {boolean} [options.includeBLEU] - Whether to include BLEU score when reference answer is provided
|
|
51
|
+
* @param {boolean} [options.includeROUGE] - Whether to include ROUGE score when reference answer is provided
|
|
52
|
+
*/
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
super();
|
|
55
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Evaluate a pipeline result across all configured metrics.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} pipelineResult - The pipeline output to evaluate
|
|
62
|
+
* @param {string} pipelineResult.query - The original user query
|
|
63
|
+
* @param {string} pipelineResult.answer - The generated answer
|
|
64
|
+
* @param {Array<{content: string}>} pipelineResult.results - Retrieved documents
|
|
65
|
+
* @param {object} [options={}] - Additional evaluation options
|
|
66
|
+
* @param {object} [options.citationResult] - Pre-computed citation result for faithfulness shortcut
|
|
67
|
+
* @param {string} [options.referenceAnswer] - Reference answer for BLEU/ROUGE scoring
|
|
68
|
+
* @returns {{ scores: Object<string, number>, details: Object<string, object>, metadata: { metricsComputed: string[], threshold: number, timestamp: string } }}
|
|
69
|
+
*/
|
|
70
|
+
evaluate(pipelineResult, options = {}) {
|
|
71
|
+
const { citationResult, referenceAnswer } = options;
|
|
72
|
+
const query = (pipelineResult && pipelineResult.query) || '';
|
|
73
|
+
const answer = (pipelineResult && pipelineResult.answer) || '';
|
|
74
|
+
const results = (pipelineResult && pipelineResult.results) || [];
|
|
75
|
+
|
|
76
|
+
const scores = {};
|
|
77
|
+
const details = {};
|
|
78
|
+
const metricsComputed = [];
|
|
79
|
+
const { metrics, threshold } = this.config;
|
|
80
|
+
|
|
81
|
+
if (metrics.includes('faithfulness')) {
|
|
82
|
+
let faithResult;
|
|
83
|
+
if (citationResult) {
|
|
84
|
+
faithResult = computeFaithfulnessFromCitations(citationResult);
|
|
85
|
+
} else {
|
|
86
|
+
faithResult = computeFaithfulness(answer, results, { threshold });
|
|
87
|
+
}
|
|
88
|
+
scores.faithfulness = faithResult.score;
|
|
89
|
+
details.faithfulness = faithResult;
|
|
90
|
+
metricsComputed.push('faithfulness');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (metrics.includes('relevance')) {
|
|
94
|
+
const relevanceResult = computeAnswerRelevance(query, answer);
|
|
95
|
+
scores.relevance = relevanceResult.score;
|
|
96
|
+
details.relevance = relevanceResult;
|
|
97
|
+
metricsComputed.push('relevance');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (metrics.includes('contextPrecision')) {
|
|
101
|
+
const precisionResult = computeContextPrecision(query, results, {
|
|
102
|
+
threshold: threshold * 0.67,
|
|
103
|
+
});
|
|
104
|
+
scores.contextPrecision = precisionResult.score;
|
|
105
|
+
details.contextPrecision = precisionResult;
|
|
106
|
+
metricsComputed.push('contextPrecision');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (metrics.includes('contextRecall')) {
|
|
110
|
+
const recallResult = computeContextRecall(answer, results, { threshold });
|
|
111
|
+
scores.contextRecall = recallResult.score;
|
|
112
|
+
details.contextRecall = recallResult;
|
|
113
|
+
metricsComputed.push('contextRecall');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (metrics.includes('groundedness')) {
|
|
117
|
+
const groundednessResult = computeGroundedness(query, answer, results, {
|
|
118
|
+
threshold,
|
|
119
|
+
});
|
|
120
|
+
scores.groundedness = groundednessResult.score;
|
|
121
|
+
details.groundedness = groundednessResult;
|
|
122
|
+
metricsComputed.push('groundedness');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Optional BLEU/ROUGE when reference answer is provided
|
|
126
|
+
if (referenceAnswer && typeof referenceAnswer === 'string') {
|
|
127
|
+
if (this.config.includeBLEU || this.config.includeROUGE) {
|
|
128
|
+
const refScores = scoreAnswer(answer, referenceAnswer);
|
|
129
|
+
if (this.config.includeBLEU) {
|
|
130
|
+
scores.bleu = refScores.bleu;
|
|
131
|
+
metricsComputed.push('bleu');
|
|
132
|
+
}
|
|
133
|
+
if (this.config.includeROUGE) {
|
|
134
|
+
scores.rouge = refScores.rouge;
|
|
135
|
+
metricsComputed.push('rouge');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const result = {
|
|
141
|
+
scores,
|
|
142
|
+
details,
|
|
143
|
+
metadata: {
|
|
144
|
+
metricsComputed,
|
|
145
|
+
threshold,
|
|
146
|
+
timestamp: new Date().toISOString(),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
this.emit('evaluated', result);
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { PipelineEvaluator, DEFAULT_CONFIG };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { tokenize, computeJaccardSimilarity } = require('./scoring');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Common English stopwords to skip when measuring query token coverage.
|
|
7
|
+
* Only tokens longer than 3 characters are considered, but these are
|
|
8
|
+
* additionally excluded to avoid inflating coverage scores.
|
|
9
|
+
* @type {Set<string>}
|
|
10
|
+
*/
|
|
11
|
+
const STOPWORDS = new Set([
|
|
12
|
+
'the',
|
|
13
|
+
'that',
|
|
14
|
+
'this',
|
|
15
|
+
'with',
|
|
16
|
+
'from',
|
|
17
|
+
'have',
|
|
18
|
+
'been',
|
|
19
|
+
'will',
|
|
20
|
+
'would',
|
|
21
|
+
'could',
|
|
22
|
+
'should',
|
|
23
|
+
'does',
|
|
24
|
+
'what',
|
|
25
|
+
'when',
|
|
26
|
+
'where',
|
|
27
|
+
'which',
|
|
28
|
+
'their',
|
|
29
|
+
'there',
|
|
30
|
+
'they',
|
|
31
|
+
'them',
|
|
32
|
+
'then',
|
|
33
|
+
'than',
|
|
34
|
+
'these',
|
|
35
|
+
'those',
|
|
36
|
+
'your',
|
|
37
|
+
'about',
|
|
38
|
+
'into',
|
|
39
|
+
'each',
|
|
40
|
+
'also',
|
|
41
|
+
'more',
|
|
42
|
+
'some',
|
|
43
|
+
'such',
|
|
44
|
+
'only',
|
|
45
|
+
'over',
|
|
46
|
+
'very',
|
|
47
|
+
'just',
|
|
48
|
+
'being',
|
|
49
|
+
'were',
|
|
50
|
+
'here',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compute answer relevance score measuring how well the answer addresses the query.
|
|
55
|
+
* Combines Jaccard similarity with query token coverage for a balanced metric.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} query - The user query
|
|
58
|
+
* @param {string} answer - The generated answer text
|
|
59
|
+
* @param {object} [options={}] - Configuration options
|
|
60
|
+
* @param {number} [options.jaccardWeight=0.5] - Weight for Jaccard similarity component
|
|
61
|
+
* @param {number} [options.coverageWeight=0.5] - Weight for query token coverage component
|
|
62
|
+
* @returns {{ score: number, jaccardSimilarity: number, queryTokensCovered: number, totalQueryTokens: number, missingQueryTerms: string[] }}
|
|
63
|
+
*/
|
|
64
|
+
function computeAnswerRelevance(query, answer, options = {}) {
|
|
65
|
+
const { jaccardWeight = 0.5, coverageWeight = 0.5 } = options;
|
|
66
|
+
|
|
67
|
+
if (!query || typeof query !== 'string' || query.trim().length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
score: 0,
|
|
70
|
+
jaccardSimilarity: 0,
|
|
71
|
+
queryTokensCovered: 0,
|
|
72
|
+
totalQueryTokens: 0,
|
|
73
|
+
missingQueryTerms: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!answer || typeof answer !== 'string' || answer.trim().length === 0) {
|
|
78
|
+
const significantQueryTokens = tokenize(query).filter(
|
|
79
|
+
(t) => t.length > 3 && !STOPWORDS.has(t),
|
|
80
|
+
);
|
|
81
|
+
return {
|
|
82
|
+
score: 0,
|
|
83
|
+
jaccardSimilarity: 0,
|
|
84
|
+
queryTokensCovered: 0,
|
|
85
|
+
totalQueryTokens: significantQueryTokens.length,
|
|
86
|
+
missingQueryTerms: [...new Set(significantQueryTokens)],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const queryTokens = tokenize(query);
|
|
91
|
+
const answerTokens = tokenize(answer);
|
|
92
|
+
|
|
93
|
+
// Jaccard similarity between full token sets
|
|
94
|
+
const jaccardSimilarity = computeJaccardSimilarity(queryTokens, answerTokens);
|
|
95
|
+
|
|
96
|
+
// Query token coverage: filter to significant query tokens (length > 3, not stopwords)
|
|
97
|
+
const significantQueryTokens = queryTokens.filter(
|
|
98
|
+
(t) => t.length > 3 && !STOPWORDS.has(t),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const answerTokenSet = new Set(answerTokens);
|
|
102
|
+
const coveredTokens = significantQueryTokens.filter((t) =>
|
|
103
|
+
answerTokenSet.has(t),
|
|
104
|
+
);
|
|
105
|
+
const totalQueryTokens = significantQueryTokens.length;
|
|
106
|
+
const queryTokensCovered = coveredTokens.length;
|
|
107
|
+
|
|
108
|
+
const coverage =
|
|
109
|
+
totalQueryTokens > 0 ? queryTokensCovered / totalQueryTokens : 0;
|
|
110
|
+
|
|
111
|
+
// Combined score
|
|
112
|
+
const score = jaccardWeight * jaccardSimilarity + coverageWeight * coverage;
|
|
113
|
+
|
|
114
|
+
// Missing query terms (unique)
|
|
115
|
+
const coveredSet = new Set(coveredTokens);
|
|
116
|
+
const missingQueryTerms = [
|
|
117
|
+
...new Set(significantQueryTokens.filter((t) => !coveredSet.has(t))),
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
score,
|
|
122
|
+
jaccardSimilarity,
|
|
123
|
+
queryTokensCovered,
|
|
124
|
+
totalQueryTokens,
|
|
125
|
+
missingQueryTerms,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { computeAnswerRelevance };
|
package/src/evaluate/scoring.js
CHANGED
|
@@ -83,9 +83,36 @@ function scoreAnswer(_actual, _expected) {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Split text into sentences on sentence-ending punctuation
|
|
88
|
+
* @param {string} text
|
|
89
|
+
* @returns {string[]}
|
|
90
|
+
*/
|
|
91
|
+
function splitSentences(text) {
|
|
92
|
+
if (!text || typeof text !== 'string') return [];
|
|
93
|
+
return text.split(/(?<=[.!?])\s+/).filter((s) => s.trim().length > 0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Compute Jaccard similarity between two token arrays
|
|
98
|
+
* @param {string[]} tokensA
|
|
99
|
+
* @param {string[]} tokensB
|
|
100
|
+
* @returns {number} Similarity score between 0 and 1
|
|
101
|
+
*/
|
|
102
|
+
function computeJaccardSimilarity(tokensA, tokensB) {
|
|
103
|
+
if (!tokensA.length || !tokensB.length) return 0;
|
|
104
|
+
const setA = new Set(tokensA);
|
|
105
|
+
const setB = new Set(tokensB);
|
|
106
|
+
const intersection = new Set([...setA].filter((x) => setB.has(x)));
|
|
107
|
+
const union = new Set([...setA, ...setB]);
|
|
108
|
+
return intersection.size / union.size;
|
|
109
|
+
}
|
|
110
|
+
|
|
86
111
|
module.exports = {
|
|
87
112
|
scoreAnswer,
|
|
88
113
|
tokenize,
|
|
89
114
|
computeBLEU,
|
|
90
115
|
computeROUGE,
|
|
116
|
+
splitSentences,
|
|
117
|
+
computeJaccardSimilarity,
|
|
91
118
|
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
const { tokenize } = require('../evaluate/scoring');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default regex patterns for heuristic entity extraction.
|
|
8
|
+
* @type {Object<string, RegExp>}
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_PATTERNS = {
|
|
11
|
+
/** Capitalized multi-word sequences (proper nouns / names) */
|
|
12
|
+
properNoun: /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b/g,
|
|
13
|
+
/** Dates in YYYY-MM-DD or YYYY/MM/DD format */
|
|
14
|
+
date: /\b\d{4}[-/]\d{1,2}[-/]\d{1,2}\b/g,
|
|
15
|
+
/** Numbers with units */
|
|
16
|
+
measurement:
|
|
17
|
+
/\b\d+(?:\.\d+)?\s*(?:GB|MB|KB|ms|seconds?|minutes?|hours?|days?|%)\b/gi,
|
|
18
|
+
/** URLs */
|
|
19
|
+
url: /https?:\/\/[^\s]+/g,
|
|
20
|
+
/** Email addresses */
|
|
21
|
+
email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
22
|
+
/** Technical terms (CamelCase or ALLCAPS) */
|
|
23
|
+
technical: /\b[A-Z][a-z]+[A-Z]\w+\b/g,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Heuristic entity extraction from text — no LLM required.
|
|
28
|
+
*
|
|
29
|
+
* Uses configurable regex patterns to identify named entities,
|
|
30
|
+
* dates, measurements, URLs, emails, and technical terms.
|
|
31
|
+
*/
|
|
32
|
+
class EntityExtractor extends EventEmitter {
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} [options={}] - Extractor options
|
|
35
|
+
* @param {Object<string, RegExp>} [options.patterns] - Custom patterns to merge with defaults
|
|
36
|
+
*/
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
super();
|
|
39
|
+
this.patterns = { ...DEFAULT_PATTERNS, ...(options.patterns || {}) };
|
|
40
|
+
this.config = options;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extract entities from a single text string.
|
|
45
|
+
*
|
|
46
|
+
* For each configured pattern type, finds all matches, deduplicates
|
|
47
|
+
* by normalized name, and returns an array of entity descriptors.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} text - Input text to extract entities from
|
|
50
|
+
* @returns {Array<{name: string, type: string, positions: Array<{start: number, end: number}>, frequency: number}>}
|
|
51
|
+
*/
|
|
52
|
+
extract(text) {
|
|
53
|
+
if (!text || typeof text !== 'string') {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @type {Map<string, {name: string, type: string, positions: Array<{start: number, end: number}>, frequency: number}>} */
|
|
58
|
+
const entityMap = new Map();
|
|
59
|
+
|
|
60
|
+
for (const [type, pattern] of Object.entries(this.patterns)) {
|
|
61
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
62
|
+
let match;
|
|
63
|
+
|
|
64
|
+
while ((match = regex.exec(text)) !== null) {
|
|
65
|
+
const name = match[1] || match[0];
|
|
66
|
+
const normalized = name.toLowerCase().trim();
|
|
67
|
+
const position = {
|
|
68
|
+
start: match.index,
|
|
69
|
+
end: match.index + match[0].length,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (entityMap.has(normalized)) {
|
|
73
|
+
const existing = entityMap.get(normalized);
|
|
74
|
+
existing.positions.push(position);
|
|
75
|
+
existing.frequency += 1;
|
|
76
|
+
} else {
|
|
77
|
+
entityMap.set(normalized, {
|
|
78
|
+
name,
|
|
79
|
+
type,
|
|
80
|
+
positions: [position],
|
|
81
|
+
frequency: 1,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const entities = [...entityMap.values()];
|
|
88
|
+
this.emit('extracted', { count: entities.length, entities });
|
|
89
|
+
return entities;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract relationships between entities that co-occur in the same sentence.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} text - Input text
|
|
96
|
+
* @param {Array<{name: string, type: string}>} entities - Previously extracted entities
|
|
97
|
+
* @returns {Array<{from: string, to: string, type: string, context: string}>}
|
|
98
|
+
*/
|
|
99
|
+
extractRelationships(text, entities) {
|
|
100
|
+
if (!text || !entities || entities.length === 0) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const sentences = text
|
|
105
|
+
.split(/[.!?]+/)
|
|
106
|
+
.map((s) => s.trim())
|
|
107
|
+
.filter(Boolean);
|
|
108
|
+
const relationships = [];
|
|
109
|
+
|
|
110
|
+
for (const sentence of sentences) {
|
|
111
|
+
const sentenceLower = sentence.toLowerCase();
|
|
112
|
+
const present = entities.filter((e) =>
|
|
113
|
+
sentenceLower.includes(e.name.toLowerCase()),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < present.length; i++) {
|
|
117
|
+
for (let j = i + 1; j < present.length; j++) {
|
|
118
|
+
relationships.push({
|
|
119
|
+
from: present[i].name,
|
|
120
|
+
to: present[j].name,
|
|
121
|
+
type: 'co-occurs',
|
|
122
|
+
context: sentence,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return relationships;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Extract entities and relationships from multiple documents.
|
|
133
|
+
*
|
|
134
|
+
* Combines entities across documents and tracks which source
|
|
135
|
+
* document(s) each entity originates from.
|
|
136
|
+
*
|
|
137
|
+
* @param {Array<{id: string, content: string, metadata?: Object}>} documents
|
|
138
|
+
* @returns {{entities: Array, relationships: Array}}
|
|
139
|
+
*/
|
|
140
|
+
extractFromDocuments(documents) {
|
|
141
|
+
if (!Array.isArray(documents)) {
|
|
142
|
+
return { entities: [], relationships: [] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @type {Map<string, {name: string, type: string, positions: Array, frequency: number, sources: string[]}>} */
|
|
146
|
+
const combinedEntities = new Map();
|
|
147
|
+
const allRelationships = [];
|
|
148
|
+
|
|
149
|
+
for (const doc of documents) {
|
|
150
|
+
const text = doc.content || '';
|
|
151
|
+
const entities = this.extract(text);
|
|
152
|
+
|
|
153
|
+
for (const entity of entities) {
|
|
154
|
+
const normalized = entity.name.toLowerCase().trim();
|
|
155
|
+
if (combinedEntities.has(normalized)) {
|
|
156
|
+
const existing = combinedEntities.get(normalized);
|
|
157
|
+
existing.frequency += entity.frequency;
|
|
158
|
+
existing.positions.push(...entity.positions);
|
|
159
|
+
if (!existing.sources.includes(doc.id)) {
|
|
160
|
+
existing.sources.push(doc.id);
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
combinedEntities.set(normalized, {
|
|
164
|
+
...entity,
|
|
165
|
+
sources: [doc.id],
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const relationships = this.extractRelationships(text, entities);
|
|
171
|
+
for (const rel of relationships) {
|
|
172
|
+
allRelationships.push({ ...rel, source: doc.id });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
entities: [...combinedEntities.values()],
|
|
178
|
+
relationships: allRelationships,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { EntityExtractor, DEFAULT_PATTERNS };
|