@devilsdev/rag-pipeline-utils 2.3.1 → 2.4.1
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 +94 -480
- 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 +37 -11
- 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,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sentence-based chunking strategy.
|
|
5
|
+
* Extracted from MarkdownLoader._chunkText for backward compatibility.
|
|
6
|
+
* Splits text on sentence boundaries and merges into chunks up to maxLen.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} text - The input text to chunk.
|
|
9
|
+
* @param {object} [options={}] - Chunking options.
|
|
10
|
+
* @param {number} [options.maxLen=500] - Maximum character length per chunk.
|
|
11
|
+
* @returns {string[]} Array of text chunks.
|
|
12
|
+
*/
|
|
13
|
+
function sentenceChunk(text, options = {}) {
|
|
14
|
+
const { maxLen = 500 } = options;
|
|
15
|
+
|
|
16
|
+
const sentences = text.split(/(?<=[.!?])\s+/);
|
|
17
|
+
const chunks = [];
|
|
18
|
+
let buffer = '';
|
|
19
|
+
|
|
20
|
+
for (const sentence of sentences) {
|
|
21
|
+
if ((buffer + sentence).length <= maxLen) {
|
|
22
|
+
buffer += sentence + ' ';
|
|
23
|
+
} else {
|
|
24
|
+
chunks.push(buffer.trim());
|
|
25
|
+
buffer = sentence + ' ';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (buffer.trim()) chunks.push(buffer.trim());
|
|
29
|
+
return chunks;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { sentenceChunk };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { recursiveChunk } = require('./recursive');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect document format from content.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} text - The text to sniff.
|
|
9
|
+
* @returns {'markdown'|'html'|'plain'} Detected format.
|
|
10
|
+
* @private
|
|
11
|
+
*/
|
|
12
|
+
function _detectFormat(text) {
|
|
13
|
+
// Check for Markdown headers
|
|
14
|
+
if (/^#{1,6}\s/m.test(text)) {
|
|
15
|
+
return 'markdown';
|
|
16
|
+
}
|
|
17
|
+
// Check for HTML heading or section tags
|
|
18
|
+
if (/<h[1-6][^>]*>/i.test(text) || /<section[^>]*>/i.test(text)) {
|
|
19
|
+
return 'html';
|
|
20
|
+
}
|
|
21
|
+
return 'plain';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Split Markdown text into sections by header boundaries.
|
|
26
|
+
* Keeps headers as chunk prefixes and preserves code blocks as atomic units.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} text - Markdown text.
|
|
29
|
+
* @returns {string[]} Array of sections with their header prefixes.
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
32
|
+
function _splitMarkdown(text) {
|
|
33
|
+
// Protect code blocks by replacing them with placeholders
|
|
34
|
+
const codeBlocks = [];
|
|
35
|
+
const withPlaceholders = text.replace(/```[\s\S]*?```/g, (match) => {
|
|
36
|
+
const index = codeBlocks.length;
|
|
37
|
+
codeBlocks.push(match);
|
|
38
|
+
return `__CODE_BLOCK_${index}__`;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Split on header lines
|
|
42
|
+
const sections = [];
|
|
43
|
+
const lines = withPlaceholders.split('\n');
|
|
44
|
+
let currentSection = '';
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (/^#{1,6}\s/.test(line) && currentSection.trim()) {
|
|
48
|
+
sections.push(currentSection.trim());
|
|
49
|
+
currentSection = line + '\n';
|
|
50
|
+
} else {
|
|
51
|
+
currentSection += line + '\n';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (currentSection.trim()) {
|
|
56
|
+
sections.push(currentSection.trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Restore code blocks
|
|
60
|
+
return sections.map((section) =>
|
|
61
|
+
section.replace(
|
|
62
|
+
/__CODE_BLOCK_(\d+)__/g,
|
|
63
|
+
(_, idx) => codeBlocks[Number(idx)],
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Split HTML text into sections by heading and section tag boundaries.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} text - HTML text.
|
|
72
|
+
* @returns {string[]} Array of sections.
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
function _splitHtml(text) {
|
|
76
|
+
// Split on heading tags or section tags
|
|
77
|
+
const parts = text.split(/(?=<(?:h[1-6]|section)[^>]*>)/i);
|
|
78
|
+
return parts.map((p) => p.trim()).filter((p) => p.length > 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Structure-aware chunking strategy.
|
|
83
|
+
* Detects document format (Markdown, HTML, or plain) and respects
|
|
84
|
+
* structural boundaries. For Markdown, splits on headers and preserves
|
|
85
|
+
* code blocks. For HTML, splits on heading/section tags. Oversized
|
|
86
|
+
* sections are further split via the recursive strategy.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} text - The input text to chunk.
|
|
89
|
+
* @param {object} [options={}] - Chunking options.
|
|
90
|
+
* @param {number} [options.chunkSize=512] - Maximum chunk size in characters.
|
|
91
|
+
* @param {number} [options.chunkOverlap=50] - Number of overlapping characters between chunks.
|
|
92
|
+
* @param {string} [options.format] - Document format: 'markdown', 'html', 'plain', or auto-detect.
|
|
93
|
+
* @returns {string[]} Array of text chunks.
|
|
94
|
+
*/
|
|
95
|
+
function structureAwareChunk(text, options = {}) {
|
|
96
|
+
const { chunkSize = 512, chunkOverlap = 50, format } = options;
|
|
97
|
+
|
|
98
|
+
if (!text || typeof text !== 'string') {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const detectedFormat = format || _detectFormat(text);
|
|
103
|
+
|
|
104
|
+
let sections;
|
|
105
|
+
|
|
106
|
+
switch (detectedFormat) {
|
|
107
|
+
case 'markdown':
|
|
108
|
+
sections = _splitMarkdown(text);
|
|
109
|
+
break;
|
|
110
|
+
case 'html':
|
|
111
|
+
sections = _splitHtml(text);
|
|
112
|
+
break;
|
|
113
|
+
case 'plain':
|
|
114
|
+
default:
|
|
115
|
+
sections = [text];
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Apply chunkSize limit: oversized sections get further split via recursive strategy
|
|
120
|
+
const chunks = [];
|
|
121
|
+
|
|
122
|
+
for (const section of sections) {
|
|
123
|
+
const trimmed = section.trim();
|
|
124
|
+
if (!trimmed) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (trimmed.length <= chunkSize) {
|
|
128
|
+
chunks.push(trimmed);
|
|
129
|
+
} else {
|
|
130
|
+
const subChunks = recursiveChunk(trimmed, { chunkSize, chunkOverlap });
|
|
131
|
+
chunks.push(...subChunks);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return chunks.filter((c) => c.length > 0);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { structureAwareChunk };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
const { splitSentences, tokenize } = require('../evaluate/scoring');
|
|
5
|
+
const { mapSentenceToSources, buildIDFWeights } = require('./source-mapper');
|
|
6
|
+
const { detectHallucinations } = require('./hallucination-detector');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default configuration for the CitationTracker.
|
|
10
|
+
* @type {object}
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
similarityThreshold: 0.3,
|
|
14
|
+
maxCitationsPerSentence: 3,
|
|
15
|
+
useIDFWeighting: true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tracks citations between an LLM-generated answer and retrieved source documents.
|
|
20
|
+
* Emits a 'tracked' event when citation analysis completes.
|
|
21
|
+
*
|
|
22
|
+
* @extends EventEmitter
|
|
23
|
+
*/
|
|
24
|
+
class CitationTracker extends EventEmitter {
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} [options] - Override default configuration
|
|
27
|
+
* @param {number} [options.similarityThreshold=0.3] - Minimum similarity for a citation match
|
|
28
|
+
* @param {number} [options.maxCitationsPerSentence=3] - Max citations per sentence
|
|
29
|
+
* @param {boolean} [options.useIDFWeighting=true] - Whether to apply IDF weighting
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
super();
|
|
33
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a document object to ensure it has a usable text content string.
|
|
38
|
+
* Looks for .content, .text, or .chunk fields. If .chunk is a function, calls it and joins the result.
|
|
39
|
+
* @param {object} doc
|
|
40
|
+
* @returns {object} Normalized doc with .content field
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
_normalizeDoc(doc) {
|
|
44
|
+
if (doc.content && typeof doc.content === 'string') return doc;
|
|
45
|
+
|
|
46
|
+
let content = '';
|
|
47
|
+
if (typeof doc.text === 'string') {
|
|
48
|
+
content = doc.text;
|
|
49
|
+
} else if (typeof doc.chunk === 'function') {
|
|
50
|
+
const chunks = doc.chunk();
|
|
51
|
+
content = Array.isArray(chunks) ? chunks.join(' ') : String(chunks);
|
|
52
|
+
} else if (typeof doc.chunk === 'string') {
|
|
53
|
+
content = doc.chunk;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { ...doc, content };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Track citations for an answer against retrieved documents.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} answer - The LLM-generated answer text
|
|
63
|
+
* @param {Array<object>} retrievedDocs - Array of source documents (each should have .content, .text, or .chunk)
|
|
64
|
+
* @returns {{
|
|
65
|
+
* citations: Array<{sentence: string, sources: Array<{docId: string|undefined, docIndex: number, score: number}>}>,
|
|
66
|
+
* groundednessScore: number,
|
|
67
|
+
* hallucinationReport: object,
|
|
68
|
+
* metadata: {sentenceCount: number, docCount: number, config: object}
|
|
69
|
+
* }}
|
|
70
|
+
* @fires CitationTracker#tracked
|
|
71
|
+
*/
|
|
72
|
+
track(answer, retrievedDocs) {
|
|
73
|
+
// 1. Validate inputs
|
|
74
|
+
if (!answer || typeof answer !== 'string') {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'CitationTracker.track(): answer must be a non-empty string',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(retrievedDocs) || retrievedDocs.length === 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'CitationTracker.track(): retrievedDocs must be a non-empty array',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Normalize docs
|
|
86
|
+
const docs = retrievedDocs.map((doc) => this._normalizeDoc(doc));
|
|
87
|
+
|
|
88
|
+
// 3. Split answer into sentences
|
|
89
|
+
const sentences = splitSentences(answer);
|
|
90
|
+
if (sentences.length === 0) {
|
|
91
|
+
const result = {
|
|
92
|
+
citations: [],
|
|
93
|
+
groundednessScore: 0,
|
|
94
|
+
hallucinationReport: detectHallucinations([]),
|
|
95
|
+
metadata: {
|
|
96
|
+
sentenceCount: 0,
|
|
97
|
+
docCount: docs.length,
|
|
98
|
+
config: this.config,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
this.emit('tracked', result);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. Build IDF weights if enabled
|
|
106
|
+
const idfWeights = this.config.useIDFWeighting
|
|
107
|
+
? buildIDFWeights(docs)
|
|
108
|
+
: null;
|
|
109
|
+
|
|
110
|
+
// 5 & 6. Map each sentence to sources and build citations array
|
|
111
|
+
const citations = sentences.map((sentence) => {
|
|
112
|
+
const sources = mapSentenceToSources(sentence, docs, {
|
|
113
|
+
maxCitations: this.config.maxCitationsPerSentence,
|
|
114
|
+
threshold: this.config.similarityThreshold,
|
|
115
|
+
idfWeights,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
sentence,
|
|
120
|
+
sources: sources.map((s) => ({
|
|
121
|
+
docId: s.docId,
|
|
122
|
+
docIndex: s.docIndex,
|
|
123
|
+
score: s.score,
|
|
124
|
+
})),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 7. Detect hallucinations
|
|
129
|
+
const hallucinationReport = detectHallucinations(citations, {
|
|
130
|
+
threshold: this.config.similarityThreshold,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// 8. Compute groundedness score
|
|
134
|
+
const groundedCount = hallucinationReport.summary.grounded;
|
|
135
|
+
const groundednessScore =
|
|
136
|
+
sentences.length > 0 ? groundedCount / sentences.length : 0;
|
|
137
|
+
|
|
138
|
+
// 9. Emit 'tracked' event
|
|
139
|
+
const result = {
|
|
140
|
+
citations,
|
|
141
|
+
groundednessScore,
|
|
142
|
+
hallucinationReport,
|
|
143
|
+
metadata: {
|
|
144
|
+
sentenceCount: sentences.length,
|
|
145
|
+
docCount: docs.length,
|
|
146
|
+
config: this.config,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @event CitationTracker#tracked
|
|
152
|
+
* @type {object}
|
|
153
|
+
*/
|
|
154
|
+
this.emit('tracked', result);
|
|
155
|
+
|
|
156
|
+
// 10. Return result
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { CitationTracker, DEFAULT_CONFIG };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect hallucinations by analyzing citation results and classifying each sentence.
|
|
5
|
+
*
|
|
6
|
+
* Classification:
|
|
7
|
+
* - 'definite_hallucination': bestScore === 0 (no overlap with any source)
|
|
8
|
+
* - 'likely_hallucination': bestScore > 0 but below threshold
|
|
9
|
+
* - 'grounded': bestScore >= threshold
|
|
10
|
+
*
|
|
11
|
+
* @param {Array<{sentence: string, sources: Array<{docId: string|undefined, docIndex: number, score: number}>}>} citationResults
|
|
12
|
+
* @param {object} [options]
|
|
13
|
+
* @param {number} [options.threshold=0.3] - Score threshold for grounded classification
|
|
14
|
+
* @returns {{
|
|
15
|
+
* hallucinationRate: number,
|
|
16
|
+
* sentences: Array<{text: string, classification: string, bestScore: number, bestSource: object|null}>,
|
|
17
|
+
* summary: {grounded: number, likely_hallucination: number, definite_hallucination: number}
|
|
18
|
+
* }}
|
|
19
|
+
*/
|
|
20
|
+
function detectHallucinations(citationResults, options = {}) {
|
|
21
|
+
const { threshold = 0.3 } = options;
|
|
22
|
+
|
|
23
|
+
if (!Array.isArray(citationResults) || citationResults.length === 0) {
|
|
24
|
+
return {
|
|
25
|
+
hallucinationRate: 0,
|
|
26
|
+
sentences: [],
|
|
27
|
+
summary: {
|
|
28
|
+
grounded: 0,
|
|
29
|
+
likely_hallucination: 0,
|
|
30
|
+
definite_hallucination: 0,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const summary = {
|
|
36
|
+
grounded: 0,
|
|
37
|
+
likely_hallucination: 0,
|
|
38
|
+
definite_hallucination: 0,
|
|
39
|
+
};
|
|
40
|
+
const sentences = [];
|
|
41
|
+
|
|
42
|
+
for (const entry of citationResults) {
|
|
43
|
+
const sources = entry.sources || [];
|
|
44
|
+
const bestSource = sources.length > 0 ? sources[0] : null;
|
|
45
|
+
const bestScore = bestSource ? bestSource.score : 0;
|
|
46
|
+
|
|
47
|
+
let classification;
|
|
48
|
+
if (bestScore === 0) {
|
|
49
|
+
classification = 'definite_hallucination';
|
|
50
|
+
} else if (bestScore < threshold) {
|
|
51
|
+
classification = 'likely_hallucination';
|
|
52
|
+
} else {
|
|
53
|
+
classification = 'grounded';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
summary[classification]++;
|
|
57
|
+
sentences.push({
|
|
58
|
+
text: entry.sentence,
|
|
59
|
+
classification,
|
|
60
|
+
bestScore,
|
|
61
|
+
bestSource,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const total = sentences.length;
|
|
66
|
+
const hallucinationRate =
|
|
67
|
+
total > 0
|
|
68
|
+
? (summary.definite_hallucination + summary.likely_hallucination) / total
|
|
69
|
+
: 0;
|
|
70
|
+
|
|
71
|
+
return { hallucinationRate, sentences, summary };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { detectHallucinations };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { tokenize, computeJaccardSimilarity } = require('../evaluate/scoring');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build IDF (Inverse Document Frequency) weights from a collection of documents.
|
|
7
|
+
* IDF = log(totalDocs / docsContainingToken)
|
|
8
|
+
* @param {Array<{content?: string, text?: string}>} docs - Array of document objects
|
|
9
|
+
* @returns {Map<string, number>} Map of token to IDF weight
|
|
10
|
+
*/
|
|
11
|
+
function buildIDFWeights(docs) {
|
|
12
|
+
const totalDocs = docs.length;
|
|
13
|
+
if (totalDocs === 0) return new Map();
|
|
14
|
+
|
|
15
|
+
const docFrequency = new Map();
|
|
16
|
+
|
|
17
|
+
for (const doc of docs) {
|
|
18
|
+
const text = doc.content || doc.text || '';
|
|
19
|
+
const tokens = new Set(tokenize(text));
|
|
20
|
+
for (const token of tokens) {
|
|
21
|
+
docFrequency.set(token, (docFrequency.get(token) || 0) + 1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const idfWeights = new Map();
|
|
26
|
+
for (const [token, df] of docFrequency) {
|
|
27
|
+
idfWeights.set(token, Math.log(totalDocs / df));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return idfWeights;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Map a sentence to its most relevant source documents using token similarity.
|
|
35
|
+
* @param {string} sentence - The sentence to map
|
|
36
|
+
* @param {Array<{content?: string, text?: string, id?: string}>} docs - Source documents
|
|
37
|
+
* @param {object} [options] - Configuration options
|
|
38
|
+
* @param {number} [options.maxCitations=3] - Maximum number of citations to return
|
|
39
|
+
* @param {number} [options.threshold=0.3] - Minimum similarity threshold
|
|
40
|
+
* @param {Map<string, number>|null} [options.idfWeights=null] - Optional IDF weights for weighted similarity
|
|
41
|
+
* @returns {Array<{docIndex: number, docId: string|undefined, score: number, matchedTokens: string[]}>}
|
|
42
|
+
*/
|
|
43
|
+
function mapSentenceToSources(sentence, docs, options = {}) {
|
|
44
|
+
const { maxCitations = 3, threshold = 0.3, idfWeights = null } = options;
|
|
45
|
+
|
|
46
|
+
const sentenceTokens = tokenize(sentence);
|
|
47
|
+
if (!sentenceTokens.length) return [];
|
|
48
|
+
|
|
49
|
+
const results = [];
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < docs.length; i++) {
|
|
52
|
+
const doc = docs[i];
|
|
53
|
+
const docText = doc.content || doc.text || '';
|
|
54
|
+
const docTokens = tokenize(docText);
|
|
55
|
+
if (!docTokens.length) continue;
|
|
56
|
+
|
|
57
|
+
let score;
|
|
58
|
+
const sentenceSet = new Set(sentenceTokens);
|
|
59
|
+
const docSet = new Set(docTokens);
|
|
60
|
+
const matched = [...sentenceSet].filter((t) => docSet.has(t));
|
|
61
|
+
|
|
62
|
+
if (idfWeights) {
|
|
63
|
+
// IDF-weighted Jaccard: sum IDF weights of intersection / sum IDF weights of union
|
|
64
|
+
const union = new Set([...sentenceSet, ...docSet]);
|
|
65
|
+
let intersectionWeight = 0;
|
|
66
|
+
let unionWeight = 0;
|
|
67
|
+
|
|
68
|
+
for (const token of union) {
|
|
69
|
+
const weight = idfWeights.get(token) || 1;
|
|
70
|
+
if (sentenceSet.has(token) && docSet.has(token)) {
|
|
71
|
+
intersectionWeight += weight;
|
|
72
|
+
}
|
|
73
|
+
unionWeight += weight;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
score = unionWeight > 0 ? intersectionWeight / unionWeight : 0;
|
|
77
|
+
} else {
|
|
78
|
+
score = computeJaccardSimilarity(sentenceTokens, docTokens);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (score >= threshold) {
|
|
82
|
+
results.push({
|
|
83
|
+
docIndex: i,
|
|
84
|
+
docId: doc.id,
|
|
85
|
+
score,
|
|
86
|
+
matchedTokens: matched,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
results.sort((a, b) => b.score - a.score);
|
|
92
|
+
return results.slice(0, maxCitations);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { mapSentenceToSources, buildIDFWeights };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive retrieval CLI commands
|
|
3
|
+
* Handles user profiles, adaptive search, and feedback processing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require("chalk");
|
|
7
|
+
const ora = require("ora");
|
|
8
|
+
const inquirer = require("inquirer");
|
|
9
|
+
const Table = require("cli-table3");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Manage user profile (view or create)
|
|
13
|
+
* @param {AdaptiveRetrievalEngine} adaptiveRetrieval - The adaptive retrieval engine instance
|
|
14
|
+
* @param {string} userId - User ID
|
|
15
|
+
* @param {object} _options - Command options
|
|
16
|
+
*/
|
|
17
|
+
async function manageUserProfile(adaptiveRetrieval, userId, _options) {
|
|
18
|
+
const spinner = ora("Managing user profile...").start();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
let profile;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
profile = await adaptiveRetrieval.getUserProfile(userId);
|
|
25
|
+
spinner.succeed("User profile found");
|
|
26
|
+
} catch {
|
|
27
|
+
// Profile doesn't exist, create new one
|
|
28
|
+
const profileData = {};
|
|
29
|
+
|
|
30
|
+
if (_options.interests) {
|
|
31
|
+
profileData.interests = _options.interests
|
|
32
|
+
.split(",")
|
|
33
|
+
.map((i) => i.trim());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (_options.expertise) {
|
|
37
|
+
profileData.expertise = _options.expertise;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
profile = await adaptiveRetrieval.initializeUserProfile(
|
|
41
|
+
userId,
|
|
42
|
+
profileData,
|
|
43
|
+
);
|
|
44
|
+
spinner.succeed("User profile created");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const table = new Table({
|
|
48
|
+
head: ["Property", "Value"],
|
|
49
|
+
colWidths: [20, 50],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
table.push(
|
|
53
|
+
["User ID", profile.userId],
|
|
54
|
+
["Interests", profile.preferences?.interests?.join(", ") || "None"],
|
|
55
|
+
["Expertise", profile.preferences?.expertise || "Not set"],
|
|
56
|
+
[
|
|
57
|
+
"Learning History",
|
|
58
|
+
`${profile.learningHistory?.length || 0} interactions`,
|
|
59
|
+
],
|
|
60
|
+
["Created", profile.createdAt || "Unknown"],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
console.log(table.toString());
|
|
64
|
+
// eslint-disable-line no-console
|
|
65
|
+
} catch (error) {
|
|
66
|
+
spinner.fail(`Profile management failed: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Perform adaptive search
|
|
72
|
+
* @param {AdaptiveRetrievalEngine} adaptiveRetrieval - The adaptive retrieval engine instance
|
|
73
|
+
* @param {string} userId - User ID
|
|
74
|
+
* @param {string} query - Search query
|
|
75
|
+
* @param {object} _options - Command options
|
|
76
|
+
*/
|
|
77
|
+
async function adaptiveSearch(adaptiveRetrieval, userId, query, _options) {
|
|
78
|
+
const spinner = ora("Performing adaptive search...").start();
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const searchOptions = {
|
|
82
|
+
maxResults: parseInt(_options.maxResults),
|
|
83
|
+
explain: !!_options.explain,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const results = await adaptiveRetrieval.adaptiveRetrieve(
|
|
87
|
+
userId,
|
|
88
|
+
query,
|
|
89
|
+
searchOptions,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
spinner.succeed(`Found ${results.documents.length} results`);
|
|
93
|
+
|
|
94
|
+
// Display results
|
|
95
|
+
console.log(chalk.blue(`\nSearch Results for: "${query}"`));
|
|
96
|
+
// eslint-disable-line no-console
|
|
97
|
+
console.log(chalk.gray("=".repeat(50)));
|
|
98
|
+
// eslint-disable-line no-console
|
|
99
|
+
|
|
100
|
+
results.documents.forEach((doc, index) => {
|
|
101
|
+
console.log(chalk.green(`\n${index + 1}. ${doc.title || doc.id}`));
|
|
102
|
+
// eslint-disable-line no-console
|
|
103
|
+
console.log(chalk.gray(` Score: ${doc.score?.toFixed(4) || "N/A"}`));
|
|
104
|
+
// eslint-disable-line no-console
|
|
105
|
+
console.log(
|
|
106
|
+
chalk.gray(
|
|
107
|
+
` Personalized Score: ${doc.personalizedScore?.toFixed(4) || "N/A"}`,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
// eslint-disable-line no-console
|
|
111
|
+
console.log(` ${doc.content?.substring(0, 200)}...`);
|
|
112
|
+
// eslint-disable-line no-console
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (_options.explain && results.adaptationMetadata) {
|
|
116
|
+
console.log(chalk.blue("\nAdaptation Explanation:"));
|
|
117
|
+
// eslint-disable-line no-console
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.gray(JSON.stringify(results.adaptationMetadata, null, 2)),
|
|
120
|
+
);
|
|
121
|
+
// eslint-disable-line no-console
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
spinner.fail(`Adaptive search failed: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Provide feedback for learning
|
|
130
|
+
* @param {AdaptiveRetrievalEngine} adaptiveRetrieval - The adaptive retrieval engine instance
|
|
131
|
+
* @param {string} userId - User ID
|
|
132
|
+
* @param {object} _options - Command options
|
|
133
|
+
*/
|
|
134
|
+
async function provideFeedback(adaptiveRetrieval, userId, _options) {
|
|
135
|
+
if (!_options.query) {
|
|
136
|
+
const answers = await inquirer.prompt([
|
|
137
|
+
{
|
|
138
|
+
_type: "input",
|
|
139
|
+
name: "query",
|
|
140
|
+
message: "Enter the original query:",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
_type: "input",
|
|
144
|
+
name: "ratings",
|
|
145
|
+
message: "Enter ratings for results (comma-separated, 1-5):",
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
_options.query = answers.query;
|
|
149
|
+
_options.ratings = answers.ratings;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const spinner = ora("Processing feedback...").start();
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const ratings = _options.ratings.split(",").map((r) => parseInt(r.trim()));
|
|
156
|
+
|
|
157
|
+
const feedback = {
|
|
158
|
+
query: _options.query,
|
|
159
|
+
ratings,
|
|
160
|
+
clickedResults: [0], // Assume first result was clicked
|
|
161
|
+
dwellTime: [120], // Assume 2 minutes dwell time
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
await adaptiveRetrieval.processFeedback(userId, feedback);
|
|
165
|
+
|
|
166
|
+
spinner.succeed("Feedback processed successfully");
|
|
167
|
+
console.log(chalk.green("\n✓ User profile updated with feedback"));
|
|
168
|
+
// eslint-disable-line no-console
|
|
169
|
+
} catch (error) {
|
|
170
|
+
spinner.fail(`Feedback processing failed: ${error.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
manageUserProfile,
|
|
176
|
+
adaptiveSearch,
|
|
177
|
+
provideFeedback,
|
|
178
|
+
};
|