@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.
Files changed (160) hide show
  1. package/README.md +94 -480
  2. package/contracts/loader-contract.json +11 -6
  3. package/contracts/reranker-contract.json +18 -26
  4. package/dist/index.cjs +21190 -47
  5. package/dist/index.d.ts +298 -68
  6. package/dist/index.mjs +9 -76
  7. package/package.json +37 -11
  8. package/src/agentic/agentic-pipeline.js +235 -0
  9. package/src/agentic/iterative-retriever.js +213 -0
  10. package/src/agentic/query-planner.js +208 -0
  11. package/src/agentic/self-critique.js +102 -0
  12. package/src/ai/adaptive-retrieval.js +0 -3
  13. package/src/ai/federated-learning.js +0 -3
  14. package/src/ai/federation.js +1 -6
  15. package/src/ai/index.js +9 -20
  16. package/src/ai/model-training.js +0 -3
  17. package/src/ai/multimodal-processing.js +0 -3
  18. package/src/ai/multimodal.js +1 -6
  19. package/src/ai/orchestrator.js +1 -6
  20. package/src/ai/retrieval-engine.js +1 -6
  21. package/src/chunking/chunking-engine.js +142 -0
  22. package/src/chunking/strategies/fixed-size.js +54 -0
  23. package/src/chunking/strategies/recursive.js +127 -0
  24. package/src/chunking/strategies/semantic.js +128 -0
  25. package/src/chunking/strategies/sentence.js +32 -0
  26. package/src/chunking/strategies/structure-aware.js +138 -0
  27. package/src/citation/citation-tracker.js +161 -0
  28. package/src/citation/hallucination-detector.js +74 -0
  29. package/src/citation/source-mapper.js +95 -0
  30. package/src/cli/commands/ai-ml/adaptive-commands.js +178 -0
  31. package/src/cli/commands/ai-ml/federated-commands.js +207 -0
  32. package/src/cli/commands/ai-ml/index.js +17 -0
  33. package/src/cli/commands/ai-ml/multimodal-commands.js +187 -0
  34. package/src/cli/commands/ai-ml/shared.js +51 -0
  35. package/src/cli/commands/ai-ml/training-commands.js +228 -0
  36. package/src/cli/commands/ai-ml.js +176 -757
  37. package/src/cli/commands/plugin-hub/certification-commands.js +221 -0
  38. package/src/cli/commands/plugin-hub/event-handlers.js +64 -0
  39. package/src/cli/commands/plugin-hub/hub-commands.js +290 -0
  40. package/src/cli/commands/plugin-hub/review-commands.js +78 -0
  41. package/src/cli/commands/plugin-hub.js +106 -782
  42. package/src/cli/diagnostics/config-checker.js +81 -0
  43. package/src/cli/diagnostics/dependency-checker.js +69 -0
  44. package/src/cli/diagnostics/environment-checker.js +45 -0
  45. package/src/cli/diagnostics/performance-checker.js +51 -0
  46. package/src/cli/diagnostics/plugin-checker.js +106 -0
  47. package/src/cli/diagnostics/security-checker.js +61 -0
  48. package/src/cli/doctor-command.js +70 -369
  49. package/src/cli/enhanced-cli-commands.js +162 -706
  50. package/src/cli/handlers/config-handler.js +126 -0
  51. package/src/cli/handlers/diagnostics-handler.js +113 -0
  52. package/src/cli/handlers/ingest-handler.js +42 -0
  53. package/src/cli/handlers/init-handler.js +51 -0
  54. package/src/cli/handlers/query-handler.js +45 -0
  55. package/src/cli/handlers/run-handler.js +181 -0
  56. package/src/cli/interactive-wizard.js +99 -723
  57. package/src/cli/shared/cli-utils.js +89 -0
  58. package/src/cli/wizard/configuration.js +148 -0
  59. package/src/cli/wizard/observability.js +79 -0
  60. package/src/cli/wizard/performance.js +76 -0
  61. package/src/cli/wizard/plugin-selection.js +297 -0
  62. package/src/cli/wizard/preview-save.js +172 -0
  63. package/src/cli/wizard/project-setup.js +66 -0
  64. package/src/config/memory-optimization.js +3 -3
  65. package/src/connectors/anthropic-connector.js +179 -0
  66. package/src/connectors/base-connector.js +94 -0
  67. package/src/connectors/cohere-connector.js +143 -0
  68. package/src/connectors/local-embedder.js +171 -0
  69. package/src/connectors/memory-retriever.js +150 -0
  70. package/src/connectors/ollama-connector.js +205 -0
  71. package/src/connectors/openai-connector.js +173 -0
  72. package/src/core/create-pipeline.js +21 -1
  73. package/src/core/observability/tracing.js +2 -1
  74. package/src/core/pipeline-factory.js +0 -1
  75. package/src/core/plugin-registry.js +14 -14
  76. package/src/cost/cost-calculator.js +146 -0
  77. package/src/cost/cost-tracker.js +260 -0
  78. package/src/cost/token-budget.js +245 -0
  79. package/src/dag/core/execution-scheduler.js +2 -1
  80. package/src/dag/core/topology-validation.js +0 -3
  81. package/src/dag/dag-engine.js +3 -5
  82. package/src/dag/dag-node.js +0 -1
  83. package/src/dag/enhanced-dag-engine.js +171 -99
  84. package/src/dashboard/dashboard-generator.js +642 -0
  85. package/src/dashboard/metrics-aggregator.js +409 -0
  86. package/src/debug/execution-tracer.js +210 -0
  87. package/src/debug/trace-inspector.js +281 -0
  88. package/src/dev/dev-server.js +5 -5
  89. package/src/dev/hot-reload.js +3 -2
  90. package/src/dx/integration-templates.js +0 -3
  91. package/src/ecosystem/plugin-analytics-dashboard.js +2 -2
  92. package/src/ecosystem/plugin-hub.js +2 -2
  93. package/src/enterprise/audit-logging.js +2 -2
  94. package/src/enterprise/sso-integration.js +52 -32
  95. package/src/evaluate/context-metrics.js +127 -0
  96. package/src/evaluate/evaluator.js +4 -2
  97. package/src/evaluate/faithfulness.js +117 -0
  98. package/src/evaluate/groundedness.js +60 -0
  99. package/src/evaluate/pipeline-evaluator.js +156 -0
  100. package/src/evaluate/relevance.js +129 -0
  101. package/src/evaluate/scoring.js +27 -0
  102. package/src/graph/entity-extractor.js +183 -0
  103. package/src/graph/graph-index.js +113 -0
  104. package/src/graph/graph-retriever.js +244 -0
  105. package/src/graph/knowledge-graph.js +256 -0
  106. package/src/guardrails/guardrails-pipeline.js +136 -0
  107. package/src/guardrails/post-generation-guard.js +170 -0
  108. package/src/guardrails/pre-retrieval-guard.js +161 -0
  109. package/src/guardrails/retrieval-guard.js +126 -0
  110. package/src/index.js +255 -9
  111. package/src/loader/html-loader.js +4 -15
  112. package/src/loader/markdown-loader.js +4 -15
  113. package/src/mcp/mcp-server.js +199 -0
  114. package/src/mcp/mcp-tool-builder.js +121 -0
  115. package/src/observability/audit-logger.js +0 -2
  116. package/src/observability/event-logger.js +2 -1
  117. package/src/observability/security-monitor.js +0 -2
  118. package/src/observability/telemetry.js +0 -2
  119. package/src/reranker/cascade-reranker.js +79 -0
  120. package/src/reranker/embedding-reranker.js +124 -0
  121. package/src/reranker/llm-reranker.js +6 -3
  122. package/src/reranker/scoring-reranker.js +123 -0
  123. package/src/retrieval/bm25-search.js +157 -0
  124. package/src/retrieval/hybrid-retriever.js +111 -0
  125. package/src/retrieval/rank-fusion.js +62 -0
  126. package/src/security/jwt-config.js +4 -3
  127. package/src/security/jwt-validator.js +2 -1
  128. package/src/security/plugin-sandbox.js +0 -3
  129. package/src/security/plugin-signature-verifier.js +4 -6
  130. package/src/security/validator.js +0 -2
  131. package/src/streaming/embedding-stream.js +197 -0
  132. package/src/streaming/sse-adapter.js +168 -0
  133. package/src/streaming/stream-router.js +210 -0
  134. package/src/streaming/streaming-embedder.js +231 -0
  135. package/src/streaming/websocket-adapter.js +171 -0
  136. package/src/templates/scaffold.js +147 -0
  137. package/src/templates/template-registry.js +272 -0
  138. package/src/utils/contract-validator.js +2 -1
  139. package/src/utils/input-sanitizer.js +80 -99
  140. package/src/utils/performance-monitor.js +0 -3
  141. package/src/utils/plugin-scaffolder.js +9 -16
  142. package/src/utils/rate-limiter.js +0 -1
  143. package/src/utils/retry-policy.js +0 -3
  144. package/src/utils/safe-expression-evaluator.js +0 -3
  145. package/src/utils/secure-logger-config.js +2 -4
  146. package/src/utils/secure-logging.js +0 -3
  147. package/src/utils/structured-logger.js +0 -3
  148. package/src/ai/index-fixed.js +0 -509
  149. package/src/ai/module-template.js +0 -37
  150. package/src/ci/workflow-analyzer.js +0 -71
  151. package/src/cli/plugin-marketplace-commands.js +0 -869
  152. package/src/dev/README.md +0 -126
  153. package/src/dev/examples/hot-reload-usage.js +0 -355
  154. package/src/mocks/openai-embedder.js +0 -119
  155. package/src/mocks/openai-llm.js +0 -77
  156. package/src/mocks/pdf-loader.js +0 -21
  157. package/src/mocks/pinecone-retriever.js +0 -106
  158. package/src/scripts/script-utilities.js +0 -36
  159. package/src/utils/ci/diagnostic-reporter.js +0 -57
  160. 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
+ };