UNPKG

semem

Version:

Semantic Memory for Intelligent Agents

523 lines (440 loc) 19.3 kB
/** * algorithms/index.js - Ragno Graph Algorithms Suite * * This module provides a comprehensive suite of graph algorithms optimized * for RDF-based knowledge graphs following the ragno ontology. It integrates * all the core algorithms needed for advanced graph analysis and semantic search. * * Available Algorithms: * - K-core decomposition for node importance ranking * - Betweenness centrality for identifying bridge nodes * - Leiden clustering for community detection * - Personalized PageRank for semantic search traversal * - Graph connectivity and statistical analysis * * Usage: * ```javascript * import RagnoAlgorithms from './algorithms/index.js' * * const algorithms = new RagnoAlgorithms() * const results = await algorithms.runFullAnalysis(rdfDataset) * ``` */ import rdf from 'rdf-ext' import namespace from '@rdfjs/namespace' import GraphAnalytics from './GraphAnalytics.js' import CommunityDetection from './CommunityDetection.js' import PersonalizedPageRank from './PersonalizedPageRank.js' import Hyde from './Hyde.js' import VSOM from './VSOM.js' import { logger } from '../../Utils.js' export default class RagnoAlgorithms { constructor(options = {}) { this.options = { // Graph analytics options maxIterations: options.maxIterations || 1000, convergenceThreshold: options.convergenceThreshold || 1e-6, // Community detection options resolution: options.resolution || 1.0, minCommunitySize: options.minCommunitySize || 3, // PPR options alpha: options.alpha || 0.15, topKPerType: options.topKPerType || 5, shallowIterations: options.shallowIterations || 2, deepIterations: options.deepIterations || 10, // General options logProgress: options.logProgress || false, exportToRDF: options.exportToRDF || false, ...options } // Initialize algorithm modules this.graphAnalytics = new GraphAnalytics(this.options) this.communityDetection = new CommunityDetection(this.options) this.personalizedPageRank = new PersonalizedPageRank(this.options) this.hyde = new Hyde(this.options) this.vsom = new VSOM(this.options) this.stats = { lastFullAnalysis: null, analysisCount: 0, totalProcessingTime: 0 } logger.info('RagnoAlgorithms suite initialized') } /** * Run complete graph analysis pipeline * @param {Dataset} dataset - RDF-Ext dataset * @param {Object} [options] - Analysis options * @returns {Object} Complete analysis results */ async runFullAnalysis(dataset, options = {}) { const startTime = Date.now() logger.info('Starting full Ragno graph analysis pipeline...') const opts = { ...this.options, ...options } const results = { timestamp: new Date(), options: opts, graph: null, statistics: null, kCore: null, centrality: null, communities: null, components: null, processingTime: 0 } try { // Phase 1: Build graph representation from RDF logger.info('Phase 1: Building graph from RDF dataset...') const graph = this.graphAnalytics.buildGraphFromRDF(dataset, { undirected: true }) results.graph = { nodeCount: graph.nodes.size, edgeCount: graph.edges.size, metadata: 'Graph built from RDF dataset' } if (graph.nodes.size === 0) { logger.warn('Empty graph - skipping analysis') return results } // Phase 2: Basic graph statistics logger.info('Phase 2: Computing graph statistics...') results.statistics = this.graphAnalytics.computeGraphStatistics(graph) // Phase 3: Structural analysis logger.info('Phase 3: Running structural analysis...') // K-core decomposition if (graph.nodes.size > 1) { results.kCore = this.graphAnalytics.computeKCore(graph) } // Betweenness centrality (skip for very large graphs) if (graph.nodes.size <= 1000) { results.centrality = this.graphAnalytics.computeBetweennessCentrality(graph) } else { logger.info('Skipping betweenness centrality for large graph (>1000 nodes)') } // Connected components results.components = this.graphAnalytics.findConnectedComponents(graph) // Phase 4: Community detection logger.info('Phase 4: Detecting communities...') if (graph.nodes.size > 2) { results.communities = this.communityDetection.detectCommunities(graph, opts) } // Phase 5: Export to RDF if requested if (opts.exportToRDF && opts.targetDataset) { logger.info('Phase 5: Exporting results to RDF...') this.exportAllResultsToRDF(results, opts.targetDataset) } const endTime = Date.now() results.processingTime = endTime - startTime // Update statistics this.stats.lastFullAnalysis = new Date() this.stats.analysisCount++ this.stats.totalProcessingTime += results.processingTime logger.info(`Full analysis completed in ${results.processingTime}ms`) return results } catch (error) { logger.error('Error during full analysis:', error) throw error } } /** * Run semantic search using PPR * @param {Dataset} dataset - RDF-Ext dataset * @param {Array} queryEntities - Entity URIs to start search from * @param {Object} [options] - Search options * @returns {Object} Search results with ranked nodes */ async runSemanticSearch(dataset, queryEntities, options = {}) { logger.info(`Running semantic search from ${queryEntities.length} entities...`) const opts = { ...this.options, ...options } // Build graph const graph = this.graphAnalytics.buildGraphFromRDF(dataset, { undirected: true }) if (graph.nodes.size === 0) { logger.warn('Empty graph for semantic search') return { results: [], entryPoints: queryEntities } } // Run appropriate PPR based on options let pprResults if (opts.shallow) { pprResults = this.personalizedPageRank.runShallowPPR(graph, queryEntities, opts) } else if (opts.deep) { pprResults = this.personalizedPageRank.runDeepPPR(graph, queryEntities, opts) } else { pprResults = this.personalizedPageRank.runPPR(graph, queryEntities, opts) } // Enhance results with node metadata const enhancedResults = this.enhanceSearchResults(pprResults, graph, dataset) return enhancedResults } /** * Enhance search results with additional metadata * @param {Object} pprResults - PPR results * @param {Object} graph - Graph representation * @param {Dataset} dataset - Original RDF dataset * @returns {Object} Enhanced results */ enhanceSearchResults(pprResults, graph, dataset) { const enhancedNodes = [] for (const node of pprResults.rankedNodes) { const nodeData = graph.nodes.get(node.nodeUri) const enhanced = { ...node, metadata: { type: nodeData?.type || 'unknown', connections: graph.adjacency.get(node.nodeUri)?.size || 0 } } // Add additional RDF properties if available const nodeTriples = [...dataset.match(nodeData ? rdf.namedNode(nodeData.uri) : null)] enhanced.metadata.tripleCount = nodeTriples.length enhancedNodes.push(enhanced) } return { ...pprResults, rankedNodes: enhancedNodes, enhanced: true } } /** * Run targeted analysis for specific algorithms * @param {Dataset} dataset - RDF-Ext dataset * @param {Array} algorithms - Array of algorithm names * @param {Object} [options] - Analysis options * @returns {Object} Targeted analysis results */ async runTargetedAnalysis(dataset, algorithms, options = {}) { logger.info(`Running targeted analysis: ${algorithms.join(', ')}`) const graph = this.graphAnalytics.buildGraphFromRDF(dataset, { undirected: true }) const results = { timestamp: new Date(), algorithms: algorithms, graph: { nodeCount: graph.nodes.size, edgeCount: graph.edges.size } } for (const algorithm of algorithms) { switch (algorithm.toLowerCase()) { case 'k-core': case 'kcore': results.kCore = this.graphAnalytics.computeKCore(graph) break case 'centrality': case 'betweenness': if (graph.nodes.size <= 1000) { results.centrality = this.graphAnalytics.computeBetweennessCentrality(graph) } break case 'communities': case 'leiden': if (graph.nodes.size > 2) { results.communities = this.communityDetection.detectCommunities(graph, options) } break case 'components': results.components = this.graphAnalytics.findConnectedComponents(graph) break case 'statistics': case 'stats': results.statistics = this.graphAnalytics.computeGraphStatistics(graph) break case 'hyde': case 'hypothetical': // Hyde requires different parameters - would need LLM handler logger.info('Hyde algorithm requires LLM handler - use runHydeGeneration method') break case 'vsom': case 'clustering': // VSOM requires different parameters - would need entity data and embeddings logger.info('VSOM algorithm requires entity data and embeddings - use runEntityClustering method') break default: logger.warn(`Unknown algorithm: ${algorithm}`) } } return results } /** * Get top-k important nodes across all metrics * @param {Object} analysisResults - Results from runFullAnalysis * @param {number} [k=10] - Number of top nodes to return * @returns {Object} Top-k nodes with scores from different algorithms */ getTopKNodes(analysisResults, k = 10) { const nodeScores = new Map() // Collect scores from different algorithms if (analysisResults.kCore?.coreNumbers) { for (const [nodeUri, coreNumber] of analysisResults.kCore.coreNumbers) { if (!nodeScores.has(nodeUri)) { nodeScores.set(nodeUri, {}) } nodeScores.get(nodeUri).coreNumber = coreNumber } } if (analysisResults.centrality?.centrality) { for (const [nodeUri, centrality] of analysisResults.centrality.centrality) { if (!nodeScores.has(nodeUri)) { nodeScores.set(nodeUri, {}) } nodeScores.get(nodeUri).centrality = centrality } } // Calculate composite score const scoredNodes = [] for (const [nodeUri, scores] of nodeScores) { const coreScore = scores.coreNumber || 0 const centralityScore = scores.centrality || 0 // Weighted composite score const compositeScore = coreScore * 0.6 + centralityScore * 0.4 scoredNodes.push({ nodeUri, compositeScore, coreNumber: coreScore, centrality: centralityScore }) } // Sort by composite score and return top-k return scoredNodes .sort((a, b) => b.compositeScore - a.compositeScore) .slice(0, k) } /** * Export all analysis results to RDF * @param {Object} results - Analysis results * @param {Dataset} targetDataset - Target RDF dataset */ exportAllResultsToRDF(results, targetDataset) { logger.info('Exporting all analysis results to RDF...') // Export individual algorithm results if (results.kCore) { this.graphAnalytics.exportResultsToRDF(results.kCore, targetDataset) } if (results.centrality) { this.graphAnalytics.exportResultsToRDF(results.centrality, targetDataset) } if (results.communities) { this.communityDetection.exportCommunitiesToRDF(results.communities, targetDataset) } logger.info('All results exported to RDF') } /** * Run HyDE hypothesis generation * @param {Array|string} inputs - Query strings or entity URIs * @param {Object} llmHandler - LLM handler instance * @param {Dataset} targetDataset - RDF dataset to augment * @param {Object} [options] - Hyde options * @returns {Object} Hyde generation results */ async runHydeGeneration(inputs, llmHandler, targetDataset, options = {}) { logger.info('Running HyDE hypothesis generation...') const opts = { ...this.options, ...options } return await this.hyde.generateHypotheses(inputs, llmHandler, targetDataset, opts) } /** * Query hypothetical content from dataset * @param {Dataset} dataset - RDF dataset to query * @param {Object} [filters] - Query filters * @returns {Array} Hypothetical content matching filters */ queryHypotheticalContent(dataset, filters = {}) { return this.hyde.queryHypotheticalContent(dataset, filters) } /** * Run entity clustering using VSOM * @param {Array} entities - Array of entities to cluster * @param {Object} embeddingHandler - Embedding handler for vector generation * @param {Object} [options] - VSOM options * @returns {Promise<Object>} Clustering results */ async runEntityClustering(entities, embeddingHandler, options = {}) { logger.info('Running VSOM entity clustering...') const opts = { ...this.options, ...options } // Load entities into VSOM const loadResults = await this.vsom.loadFromEntities(entities, embeddingHandler, opts) // Train the VSOM const trainingResults = await this.vsom.train(opts) // Generate clusters const clusters = this.vsom.getClusters(opts.clusterThreshold) return { loadResults, trainingResults, clusters, nodeMappings: this.vsom.getNodeMappings(), topology: this.vsom.getTopology() } } /** * Run VSOM analysis on dataset * @param {Dataset} dataset - RDF dataset containing entities * @param {Object} embeddingHandler - Embedding handler for vector generation * @param {Object} [options] - Analysis options * @returns {Promise<Object>} VSOM analysis results */ async runVSOMAnalysis(dataset, embeddingHandler, options = {}) { logger.info('Running VSOM analysis on RDF dataset...') // Extract entities from dataset const entities = this.extractEntitiesFromDataset(dataset) // Run clustering return await this.runEntityClustering(entities, embeddingHandler, options) } /** * Get comprehensive statistics from all algorithm modules * @returns {Object} Combined statistics */ getAllStatistics() { return { suite: this.stats, graphAnalytics: this.graphAnalytics.getStatistics(), communityDetection: this.communityDetection.getStatistics(), personalizedPageRank: this.personalizedPageRank.getStatistics(), hyde: this.hyde.getStatistics(), vsom: this.vsom.getStatistics() } } /** * Reset all statistics */ resetStatistics() { this.stats = { lastFullAnalysis: null, analysisCount: 0, totalProcessingTime: 0 } logger.info('Algorithm statistics reset') } /** * Extract entities from RDF dataset (helper method) * @param {Dataset} dataset - RDF dataset * @returns {Array} Array of entities */ extractEntitiesFromDataset(dataset) { const entities = [] // Find all ragno:Entity triples const ragnoNS = this.namespaces?.ragno || namespace('http://purl.org/stuff/ragno/') const rdfNS = this.namespaces?.rdf || namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') const rdfsNS = this.namespaces?.rdfs || namespace('http://www.w3.org/2000/01/rdf-schema#') const entityTriples = [...dataset.match(null, rdfNS('type'), ragnoNS('Entity'))] for (const triple of entityTriples) { const entityUri = triple.subject // Get entity properties const labelTriples = [...dataset.match(entityUri, rdfsNS('label'), null)] const contentTriples = [...dataset.match(entityUri, ragnoNS('content'), null)] const label = labelTriples[0]?.object.value || '' const content = contentTriples[0]?.object.value || label if (content) { entities.push({ uri: entityUri.value, content: content, type: 'entity', metadata: { fromDataset: true } }) } } logger.debug(`Extracted ${entities.length} entities from dataset`) return entities } } // Export individual algorithm classes for direct use export { GraphAnalytics, CommunityDetection, PersonalizedPageRank, Hyde, VSOM }