@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,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fast entity lookup index for the knowledge graph.
|
|
5
|
+
*
|
|
6
|
+
* Maintains secondary indexes by type, name, and arbitrary properties
|
|
7
|
+
* for efficient querying without full graph traversal.
|
|
8
|
+
*/
|
|
9
|
+
class GraphIndex {
|
|
10
|
+
constructor() {
|
|
11
|
+
/** @type {Map<string, Set<string>>} type -> Set of entity IDs */
|
|
12
|
+
this.byType = new Map();
|
|
13
|
+
/** @type {Map<string, string>} normalized name -> entity ID */
|
|
14
|
+
this.byName = new Map();
|
|
15
|
+
/** @type {Map<string, Map<*, Set<string>>>} propertyKey -> Map of value -> Set of entity IDs */
|
|
16
|
+
this.byProperty = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Index an entity by type, name, and each of its properties.
|
|
21
|
+
* @param {{id: string, type: string, properties: Object}} entity
|
|
22
|
+
*/
|
|
23
|
+
indexEntity(entity) {
|
|
24
|
+
const { id, type, properties } = entity;
|
|
25
|
+
|
|
26
|
+
// Index by type
|
|
27
|
+
if (!this.byType.has(type)) {
|
|
28
|
+
this.byType.set(type, new Set());
|
|
29
|
+
}
|
|
30
|
+
this.byType.get(type).add(id);
|
|
31
|
+
|
|
32
|
+
// Index by normalized name
|
|
33
|
+
const name = (properties && properties.name) || entity.name || id;
|
|
34
|
+
const normalized = String(name).toLowerCase();
|
|
35
|
+
this.byName.set(normalized, id);
|
|
36
|
+
|
|
37
|
+
// Index by each property
|
|
38
|
+
if (properties) {
|
|
39
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
40
|
+
if (!this.byProperty.has(key)) {
|
|
41
|
+
this.byProperty.set(key, new Map());
|
|
42
|
+
}
|
|
43
|
+
const valueMap = this.byProperty.get(key);
|
|
44
|
+
if (!valueMap.has(value)) {
|
|
45
|
+
valueMap.set(value, new Set());
|
|
46
|
+
}
|
|
47
|
+
valueMap.get(value).add(id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fuzzy search for entities whose name contains the query (case-insensitive).
|
|
54
|
+
*
|
|
55
|
+
* Results are sorted by relevance: exact matches first, then partial matches.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} query - Search query
|
|
58
|
+
* @returns {string[]} Array of matching entity IDs
|
|
59
|
+
*/
|
|
60
|
+
searchByName(query) {
|
|
61
|
+
if (!query || typeof query !== 'string') {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const normalized = query.toLowerCase();
|
|
66
|
+
const exact = [];
|
|
67
|
+
const partial = [];
|
|
68
|
+
|
|
69
|
+
for (const [name, entityId] of this.byName.entries()) {
|
|
70
|
+
if (name === normalized) {
|
|
71
|
+
exact.push(entityId);
|
|
72
|
+
} else if (name.includes(normalized)) {
|
|
73
|
+
partial.push(entityId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [...exact, ...partial];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return all entity IDs of a given type.
|
|
82
|
+
* @param {string} type - Entity type
|
|
83
|
+
* @returns {string[]} Array of entity IDs
|
|
84
|
+
*/
|
|
85
|
+
searchByType(type) {
|
|
86
|
+
const ids = this.byType.get(type);
|
|
87
|
+
return ids ? [...ids] : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Return entity IDs matching a property key-value pair.
|
|
92
|
+
* @param {string} key - Property key
|
|
93
|
+
* @param {*} value - Property value
|
|
94
|
+
* @returns {string[]} Array of entity IDs
|
|
95
|
+
*/
|
|
96
|
+
searchByProperty(key, value) {
|
|
97
|
+
const valueMap = this.byProperty.get(key);
|
|
98
|
+
if (!valueMap) return [];
|
|
99
|
+
const ids = valueMap.get(value);
|
|
100
|
+
return ids ? [...ids] : [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Clear all indexes.
|
|
105
|
+
*/
|
|
106
|
+
clear() {
|
|
107
|
+
this.byType.clear();
|
|
108
|
+
this.byName.clear();
|
|
109
|
+
this.byProperty.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { GraphIndex };
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
const { KnowledgeGraph } = require('./knowledge-graph');
|
|
5
|
+
const { EntityExtractor } = require('./entity-extractor');
|
|
6
|
+
const { GraphIndex } = require('./graph-index');
|
|
7
|
+
const { tokenize } = require('../evaluate/scoring');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default configuration for the GraphRetriever.
|
|
11
|
+
* @type {Object}
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
traversalDepth: 2,
|
|
15
|
+
maxResults: 10,
|
|
16
|
+
entityWeight: 0.6,
|
|
17
|
+
textWeight: 0.4,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Retriever that uses a knowledge graph for document retrieval.
|
|
22
|
+
*
|
|
23
|
+
* Combines entity-based graph traversal with token-overlap text scoring
|
|
24
|
+
* to rank and retrieve relevant documents from a corpus.
|
|
25
|
+
*/
|
|
26
|
+
class GraphRetriever extends EventEmitter {
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} [options={}] - Retriever options
|
|
29
|
+
* @param {number} [options.traversalDepth=2] - Graph traversal depth
|
|
30
|
+
* @param {number} [options.maxResults=10] - Maximum results to return
|
|
31
|
+
* @param {number} [options.entityWeight=0.6] - Weight for entity-graph score
|
|
32
|
+
* @param {number} [options.textWeight=0.4] - Weight for text-overlap score
|
|
33
|
+
* @param {Object} [options.extractorOptions] - Options for the EntityExtractor
|
|
34
|
+
*/
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
super();
|
|
37
|
+
this.config = { ...DEFAULT_CONFIG, ...options };
|
|
38
|
+
this.graph = new KnowledgeGraph();
|
|
39
|
+
this.extractor = new EntityExtractor(options.extractorOptions);
|
|
40
|
+
this.index = new GraphIndex();
|
|
41
|
+
/** @type {Map<string, {id: string, content: string, metadata: Object}>} */
|
|
42
|
+
this.documents = new Map();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Store documents, extract entities and relationships, and build the graph.
|
|
47
|
+
*
|
|
48
|
+
* @param {Array<{id: string, content: string, metadata?: Object}>} documents
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async store(documents) {
|
|
52
|
+
if (!Array.isArray(documents)) return;
|
|
53
|
+
|
|
54
|
+
for (const doc of documents) {
|
|
55
|
+
const id = doc.id || `doc_${this.documents.size + 1}`;
|
|
56
|
+
this.documents.set(id, {
|
|
57
|
+
id,
|
|
58
|
+
content: doc.content || '',
|
|
59
|
+
metadata: doc.metadata || {},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { entities, relationships } = this.extractor.extractFromDocuments(
|
|
64
|
+
documents.map((doc) => ({
|
|
65
|
+
id: doc.id || `doc_${this.documents.size}`,
|
|
66
|
+
content: doc.content || '',
|
|
67
|
+
metadata: doc.metadata || {},
|
|
68
|
+
})),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
for (const entity of entities) {
|
|
72
|
+
const entityId = entity.name.toLowerCase().trim().replace(/\s+/g, '_');
|
|
73
|
+
this.graph.addEntity(entityId, entity.type, {
|
|
74
|
+
name: entity.name,
|
|
75
|
+
frequency: entity.frequency,
|
|
76
|
+
sources: entity.sources || [],
|
|
77
|
+
});
|
|
78
|
+
this.index.indexEntity({
|
|
79
|
+
id: entityId,
|
|
80
|
+
type: entity.type,
|
|
81
|
+
name: entity.name,
|
|
82
|
+
properties: {
|
|
83
|
+
name: entity.name,
|
|
84
|
+
frequency: entity.frequency,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const rel of relationships) {
|
|
90
|
+
const fromId = rel.from.toLowerCase().trim().replace(/\s+/g, '_');
|
|
91
|
+
const toId = rel.to.toLowerCase().trim().replace(/\s+/g, '_');
|
|
92
|
+
|
|
93
|
+
if (this.graph.getEntity(fromId) && this.graph.getEntity(toId)) {
|
|
94
|
+
this.graph.addRelationship(fromId, toId, rel.type, {
|
|
95
|
+
context: rel.context,
|
|
96
|
+
source: rel.source,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.emit('stored', {
|
|
102
|
+
documentCount: documents.length,
|
|
103
|
+
stats: this.graph.getStats(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Retrieve documents relevant to a query using graph + text scoring.
|
|
109
|
+
*
|
|
110
|
+
* @param {string|{query: string, topK?: number}} queryOrObj - Query string or options object
|
|
111
|
+
* @param {number} [k] - Number of results (overrides config.maxResults)
|
|
112
|
+
* @returns {Promise<Array<{id: string, score: number, content: string, metadata: Object}>>}
|
|
113
|
+
*/
|
|
114
|
+
async retrieve(queryOrObj, k) {
|
|
115
|
+
const query =
|
|
116
|
+
typeof queryOrObj === 'string' ? queryOrObj : queryOrObj.query;
|
|
117
|
+
const topK =
|
|
118
|
+
k ||
|
|
119
|
+
(typeof queryOrObj === 'object' && queryOrObj.topK) ||
|
|
120
|
+
this.config.maxResults;
|
|
121
|
+
|
|
122
|
+
// Extract entities from query
|
|
123
|
+
const queryEntities = this.extractor.extract(query);
|
|
124
|
+
const queryTokens = tokenize(query);
|
|
125
|
+
|
|
126
|
+
// Score documents
|
|
127
|
+
/** @type {Map<string, {graphScore: number, textScore: number, entities: string[], relationships: Array}>} */
|
|
128
|
+
const docScores = new Map();
|
|
129
|
+
|
|
130
|
+
// Initialize all documents with zero scores
|
|
131
|
+
for (const [docId] of this.documents) {
|
|
132
|
+
docScores.set(docId, {
|
|
133
|
+
graphScore: 0,
|
|
134
|
+
textScore: 0,
|
|
135
|
+
entities: [],
|
|
136
|
+
relationships: [],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Graph-based scoring: find matching entities and traverse
|
|
141
|
+
for (const qEntity of queryEntities) {
|
|
142
|
+
const normalized = qEntity.name.toLowerCase().trim();
|
|
143
|
+
const matchingIds = this.index.searchByName(normalized);
|
|
144
|
+
|
|
145
|
+
for (const entityId of matchingIds) {
|
|
146
|
+
const traversal = this.graph.traverse(
|
|
147
|
+
entityId,
|
|
148
|
+
this.config.traversalDepth,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
for (const entity of traversal.entities) {
|
|
152
|
+
const sources =
|
|
153
|
+
(entity.properties && entity.properties.sources) || [];
|
|
154
|
+
for (const sourceDocId of sources) {
|
|
155
|
+
if (docScores.has(sourceDocId)) {
|
|
156
|
+
const scores = docScores.get(sourceDocId);
|
|
157
|
+
scores.graphScore += 1;
|
|
158
|
+
if (!scores.entities.includes(entity.id)) {
|
|
159
|
+
scores.entities.push(entity.id);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const rel of traversal.relationships) {
|
|
166
|
+
for (const [docId, scores] of docScores) {
|
|
167
|
+
const docEntities = scores.entities;
|
|
168
|
+
if (
|
|
169
|
+
docEntities.includes(rel.from) ||
|
|
170
|
+
docEntities.includes(rel.to)
|
|
171
|
+
) {
|
|
172
|
+
scores.relationships.push(rel);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Text-based scoring: token overlap
|
|
180
|
+
if (queryTokens.length > 0) {
|
|
181
|
+
for (const [docId, doc] of this.documents) {
|
|
182
|
+
const docTokens = tokenize(doc.content);
|
|
183
|
+
if (docTokens.length === 0) continue;
|
|
184
|
+
|
|
185
|
+
const querySet = new Set(queryTokens);
|
|
186
|
+
let overlap = 0;
|
|
187
|
+
for (const token of docTokens) {
|
|
188
|
+
if (querySet.has(token)) {
|
|
189
|
+
overlap += 1;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const textScore = overlap / Math.max(queryTokens.length, 1);
|
|
193
|
+
|
|
194
|
+
if (docScores.has(docId)) {
|
|
195
|
+
docScores.get(docId).textScore = textScore;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Combine scores and sort
|
|
201
|
+
const results = [];
|
|
202
|
+
for (const [docId, scores] of docScores) {
|
|
203
|
+
const doc = this.documents.get(docId);
|
|
204
|
+
const combinedScore =
|
|
205
|
+
this.config.entityWeight * scores.graphScore +
|
|
206
|
+
this.config.textWeight * scores.textScore;
|
|
207
|
+
|
|
208
|
+
if (combinedScore > 0) {
|
|
209
|
+
results.push({
|
|
210
|
+
id: docId,
|
|
211
|
+
score: combinedScore,
|
|
212
|
+
content: doc.content,
|
|
213
|
+
metadata: {
|
|
214
|
+
...doc.metadata,
|
|
215
|
+
entities: scores.entities,
|
|
216
|
+
relationships: scores.relationships,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
results.sort((a, b) => b.score - a.score);
|
|
223
|
+
return results.slice(0, topK);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Return basic graph statistics.
|
|
228
|
+
* @returns {{entityCount: number, relationshipCount: number}}
|
|
229
|
+
*/
|
|
230
|
+
getGraphStats() {
|
|
231
|
+
return this.graph.getStats();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Clear all stored data, the graph, and indexes.
|
|
236
|
+
*/
|
|
237
|
+
clear() {
|
|
238
|
+
this.graph.clear();
|
|
239
|
+
this.index.clear();
|
|
240
|
+
this.documents.clear();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = { GraphRetriever, DEFAULT_CONFIG };
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In-memory knowledge graph with entities and relationships.
|
|
7
|
+
*
|
|
8
|
+
* Stores typed entities and directed relationships between them,
|
|
9
|
+
* supporting traversal, pattern-based querying, and adjacency lookups.
|
|
10
|
+
*/
|
|
11
|
+
class KnowledgeGraph extends EventEmitter {
|
|
12
|
+
/**
|
|
13
|
+
* @param {Object} [options={}] - Graph configuration options
|
|
14
|
+
*/
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
super();
|
|
17
|
+
/** @type {Map<string, {id: string, type: string, properties: Object, labels: string[]}>} */
|
|
18
|
+
this.entities = new Map();
|
|
19
|
+
/** @type {Map<string, {id: string, from: string, to: string, type: string, properties: Object}>} */
|
|
20
|
+
this.relationships = new Map();
|
|
21
|
+
/** @type {Map<string, Set<string>>} */
|
|
22
|
+
this.adjacency = new Map();
|
|
23
|
+
this.options = options;
|
|
24
|
+
this._relationshipCounter = 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Add an entity to the graph.
|
|
29
|
+
* @param {string} id - Unique entity identifier
|
|
30
|
+
* @param {string} type - Entity type (e.g. 'person', 'date', 'url')
|
|
31
|
+
* @param {Object} [properties={}] - Arbitrary key-value properties
|
|
32
|
+
* @returns {{id: string, type: string, properties: Object, labels: string[]}}
|
|
33
|
+
*/
|
|
34
|
+
addEntity(id, type, properties = {}) {
|
|
35
|
+
const entity = {
|
|
36
|
+
id,
|
|
37
|
+
type,
|
|
38
|
+
properties: { ...properties },
|
|
39
|
+
labels: properties.labels || [type],
|
|
40
|
+
};
|
|
41
|
+
this.entities.set(id, entity);
|
|
42
|
+
|
|
43
|
+
if (!this.adjacency.has(id)) {
|
|
44
|
+
this.adjacency.set(id, new Set());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.emit('entityAdded', entity);
|
|
48
|
+
return entity;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Add a directed relationship between two entities.
|
|
53
|
+
* @param {string} fromId - Source entity ID
|
|
54
|
+
* @param {string} toId - Target entity ID
|
|
55
|
+
* @param {string} type - Relationship type (e.g. 'co-occurs', 'references')
|
|
56
|
+
* @param {Object} [properties={}] - Arbitrary key-value properties
|
|
57
|
+
* @returns {{id: string, from: string, to: string, type: string, properties: Object}}
|
|
58
|
+
* @throws {Error} If either entity does not exist
|
|
59
|
+
*/
|
|
60
|
+
addRelationship(fromId, toId, type, properties = {}) {
|
|
61
|
+
if (!this.entities.has(fromId)) {
|
|
62
|
+
throw new Error(`Entity not found: ${fromId}`);
|
|
63
|
+
}
|
|
64
|
+
if (!this.entities.has(toId)) {
|
|
65
|
+
throw new Error(`Entity not found: ${toId}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this._relationshipCounter += 1;
|
|
69
|
+
const id = `rel_${this._relationshipCounter}`;
|
|
70
|
+
|
|
71
|
+
const relationship = {
|
|
72
|
+
id,
|
|
73
|
+
from: fromId,
|
|
74
|
+
to: toId,
|
|
75
|
+
type,
|
|
76
|
+
properties: { ...properties },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
this.relationships.set(id, relationship);
|
|
80
|
+
|
|
81
|
+
if (!this.adjacency.has(fromId)) {
|
|
82
|
+
this.adjacency.set(fromId, new Set());
|
|
83
|
+
}
|
|
84
|
+
if (!this.adjacency.has(toId)) {
|
|
85
|
+
this.adjacency.set(toId, new Set());
|
|
86
|
+
}
|
|
87
|
+
this.adjacency.get(fromId).add(id);
|
|
88
|
+
this.adjacency.get(toId).add(id);
|
|
89
|
+
|
|
90
|
+
this.emit('relationshipAdded', relationship);
|
|
91
|
+
return relationship;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Retrieve an entity by ID.
|
|
96
|
+
* @param {string} id - Entity ID
|
|
97
|
+
* @returns {{id: string, type: string, properties: Object, labels: string[]}|null}
|
|
98
|
+
*/
|
|
99
|
+
getEntity(id) {
|
|
100
|
+
return this.entities.get(id) || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get relationships for an entity, optionally filtered.
|
|
105
|
+
* @param {string} entityId - Entity ID
|
|
106
|
+
* @param {Object} [options={}] - Filter options
|
|
107
|
+
* @param {string} [options.type] - Filter by relationship type
|
|
108
|
+
* @param {'outgoing'|'incoming'|'both'} [options.direction='both'] - Direction filter
|
|
109
|
+
* @returns {Array<{id: string, from: string, to: string, type: string, properties: Object}>}
|
|
110
|
+
*/
|
|
111
|
+
getRelationships(entityId, options = {}) {
|
|
112
|
+
const { type, direction = 'both' } = options;
|
|
113
|
+
const relIds = this.adjacency.get(entityId);
|
|
114
|
+
if (!relIds) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const results = [];
|
|
119
|
+
for (const relId of relIds) {
|
|
120
|
+
const rel = this.relationships.get(relId);
|
|
121
|
+
if (!rel) continue;
|
|
122
|
+
|
|
123
|
+
if (type && rel.type !== type) continue;
|
|
124
|
+
|
|
125
|
+
if (direction === 'outgoing' && rel.from !== entityId) continue;
|
|
126
|
+
if (direction === 'incoming' && rel.to !== entityId) continue;
|
|
127
|
+
|
|
128
|
+
results.push(rel);
|
|
129
|
+
}
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* BFS traversal from startId up to a given depth.
|
|
135
|
+
* @param {string} startId - Starting entity ID
|
|
136
|
+
* @param {number} [depth=2] - Maximum traversal depth
|
|
137
|
+
* @returns {{entities: Array, relationships: Array, paths: Array<string[]>}}
|
|
138
|
+
*/
|
|
139
|
+
traverse(startId, depth = 2) {
|
|
140
|
+
const visited = new Set();
|
|
141
|
+
const resultEntities = [];
|
|
142
|
+
const resultRelationships = [];
|
|
143
|
+
const paths = [];
|
|
144
|
+
|
|
145
|
+
/** @type {Array<{id: string, depth: number, path: string[]}>} */
|
|
146
|
+
const queue = [{ id: startId, depth: 0, path: [startId] }];
|
|
147
|
+
visited.add(startId);
|
|
148
|
+
|
|
149
|
+
const startEntity = this.entities.get(startId);
|
|
150
|
+
if (startEntity) {
|
|
151
|
+
resultEntities.push(startEntity);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
while (queue.length > 0) {
|
|
155
|
+
const { id, depth: currentDepth, path } = queue.shift();
|
|
156
|
+
|
|
157
|
+
if (currentDepth >= depth) {
|
|
158
|
+
paths.push(path);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const relIds = this.adjacency.get(id);
|
|
163
|
+
if (!relIds || relIds.size === 0) {
|
|
164
|
+
paths.push(path);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let expanded = false;
|
|
169
|
+
for (const relId of relIds) {
|
|
170
|
+
const rel = this.relationships.get(relId);
|
|
171
|
+
if (!rel) continue;
|
|
172
|
+
|
|
173
|
+
const neighborId = rel.from === id ? rel.to : rel.from;
|
|
174
|
+
if (visited.has(neighborId)) continue;
|
|
175
|
+
|
|
176
|
+
visited.add(neighborId);
|
|
177
|
+
expanded = true;
|
|
178
|
+
|
|
179
|
+
const neighborEntity = this.entities.get(neighborId);
|
|
180
|
+
if (neighborEntity) {
|
|
181
|
+
resultEntities.push(neighborEntity);
|
|
182
|
+
}
|
|
183
|
+
resultRelationships.push(rel);
|
|
184
|
+
|
|
185
|
+
const newPath = [...path, neighborId];
|
|
186
|
+
queue.push({ id: neighborId, depth: currentDepth + 1, path: newPath });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!expanded) {
|
|
190
|
+
paths.push(path);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
entities: resultEntities,
|
|
196
|
+
relationships: resultRelationships,
|
|
197
|
+
paths,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Query the graph by pattern matching.
|
|
203
|
+
* @param {Object} pattern - Query pattern
|
|
204
|
+
* @param {string} [pattern.entityType] - Filter entities by type
|
|
205
|
+
* @param {string} [pattern.relationshipType] - Filter relationships by type
|
|
206
|
+
* @param {Object} [pattern.properties] - Filter by property key-value pairs
|
|
207
|
+
* @returns {{entities: Array, relationships: Array}}
|
|
208
|
+
*/
|
|
209
|
+
query(pattern) {
|
|
210
|
+
const { entityType, relationshipType, properties } = pattern || {};
|
|
211
|
+
|
|
212
|
+
let matchedEntities = [...this.entities.values()];
|
|
213
|
+
if (entityType) {
|
|
214
|
+
matchedEntities = matchedEntities.filter((e) => e.type === entityType);
|
|
215
|
+
}
|
|
216
|
+
if (properties) {
|
|
217
|
+
matchedEntities = matchedEntities.filter((e) =>
|
|
218
|
+
Object.entries(properties).every(
|
|
219
|
+
([key, value]) => e.properties[key] === value,
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let matchedRelationships = [...this.relationships.values()];
|
|
225
|
+
if (relationshipType) {
|
|
226
|
+
matchedRelationships = matchedRelationships.filter(
|
|
227
|
+
(r) => r.type === relationshipType,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { entities: matchedEntities, relationships: matchedRelationships };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Return basic graph statistics.
|
|
236
|
+
* @returns {{entityCount: number, relationshipCount: number}}
|
|
237
|
+
*/
|
|
238
|
+
getStats() {
|
|
239
|
+
return {
|
|
240
|
+
entityCount: this.entities.size,
|
|
241
|
+
relationshipCount: this.relationships.size,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Clear all graph data.
|
|
247
|
+
*/
|
|
248
|
+
clear() {
|
|
249
|
+
this.entities.clear();
|
|
250
|
+
this.relationships.clear();
|
|
251
|
+
this.adjacency.clear();
|
|
252
|
+
this._relationshipCounter = 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = { KnowledgeGraph };
|