import { promises as fs } from 'fs'; import { join, dirname } from 'path'; import { NunjucksRenderer } from '../renderer'; import { TemplateValidator } from '../template-validator'; import { FileInjector } from '../file-injector'; import * as crypto from 'crypto'; export interface KGEntity { id: string; name: string; type: string; properties?: Record; created?: string; source?: string; } export interface KGRelationship { subject: string; predicate: string; object: string; confidence?: number; validFrom?: string; validTo?: string; } export interface KGDataset { domain: string; baseUri?: string; description?: string; version?: string; entities?: KGEntity[]; geospatialEntities?: any[]; temporalEntities?: any[]; relationships?: KGRelationship[]; hierarchicalRelationships?: any[]; networkRelationships?: any[]; qualifiedRelationships?: any[]; classAlignments?: any[]; propertyAlignments?: any[]; vocabularyMappings?: any[]; conceptSchemes?: any[]; crosswalkMappings?: any[]; sourceSystems?: any[]; transformations?: any[]; qualityMetrics?: any; statistics?: any; versions?: any[]; license?: any; usageStats?: any; contact?: any; organization?: any; } export interface KGPipelineOptions { templatesDir: string; outputDir: string; force?: boolean; validate?: boolean; format?: 'turtle' | 'rdfxml' | 'jsonld' | 'ntriples'; shaclValidation?: boolean; } export interface KGValidationResult { valid: boolean; errors: string[]; warnings: string[]; statistics: { totalTriples: number; totalEntities: number; validationTime: number; }; } export class KnowledgeGraphPipeline { private renderer: NunjucksRenderer; private validator: TemplateValidator; private injector: FileInjector; constructor(private options: KGPipelineOptions) { this.renderer = new NunjucksRenderer(); this.validator = new TemplateValidator(); this.injector = new FileInjector(); } /** * Generate knowledge graph from dataset using templates */ async generateKnowledgeGraph(dataset: KGDataset): Promise<{ files: string[]; triples: number; entities: number; validationResult?: KGValidationResult; }> { const startTime = Date.now(); const generatedFiles: string[] = []; try { // Ensure output directory exists await fs.mkdir(this.options.outputDir, { recursive: true }); // Generate entity extraction RDF if (dataset.entities || dataset.geospatialEntities || dataset.temporalEntities) { const entitiesFile = await this.generateEntities(dataset); generatedFiles.push(entitiesFile); } // Generate relationship mappings if (dataset.relationships || dataset.hierarchicalRelationships || dataset.networkRelationships) { const relationshipsFile = await this.generateRelationships(dataset); generatedFiles.push(relationshipsFile); } // Generate schema mappings if (dataset.classAlignments || dataset.propertyAlignments || dataset.vocabularyMappings) { const schemaFile = await this.generateSchemaMappings(dataset); generatedFiles.push(schemaFile); } // Generate metadata and provenance const metadataFile = await this.generateMetadata(dataset); generatedFiles.push(metadataFile); // Generate SPARQL queries const queriesFile = await this.generateSPARQLQueries(dataset); generatedFiles.push(queriesFile); // Calculate statistics const statistics = await this.calculateStatistics(generatedFiles); // Validate if requested let validationResult: KGValidationResult | undefined; if (this.options.validate) { validationResult = await this.validateKnowledgeGraph(generatedFiles); } const processingTime = Date.now() - startTime; return { files: generatedFiles, triples: statistics.totalTriples, entities: statistics.totalEntities, validationResult }; } catch (error) { throw new Error(`Knowledge graph generation failed: ${error.message}`); } } /** * Generate entity extraction RDF */ private async generateEntities(dataset: KGDataset): Promise { const templatePath = join(this.options.templatesDir, 'entity-extraction.ttl.njk'); const outputPath = join(this.options.outputDir, 'entities', `${dataset.domain}-entities.ttl`); // Ensure entities directory exists await fs.mkdir(dirname(outputPath), { recursive: true }); const content = await this.renderer.renderTemplate(templatePath, { ...dataset, outputDir: this.options.outputDir, force: this.options.force, now: () => new Date(), formatDate: (date: Date | string, format?: string) => { const d = typeof date === 'string' ? new Date(date) : date; if (format === 'YYYY-MM-DD') { return d.toISOString().split('T')[0]; } return d.toISOString(); } }); await fs.writeFile(outputPath, content, 'utf8'); return outputPath; } /** * Generate relationship mapping RDF */ private async generateRelationships(dataset: KGDataset): Promise { const templatePath = join(this.options.templatesDir, 'relationship-mapping.ttl.njk'); const outputPath = join(this.options.outputDir, 'relationships', `${dataset.domain}-relationships.ttl`); await fs.mkdir(dirname(outputPath), { recursive: true }); const content = await this.renderer.renderTemplate(templatePath, { ...dataset, outputDir: this.options.outputDir, force: this.options.force, now: () => new Date(), formatDate: (date: Date | string, format?: string) => { const d = typeof date === 'string' ? new Date(date) : date; if (format === 'YYYY-MM-DD') { return d.toISOString().split('T')[0]; } return d.toISOString(); } }); await fs.writeFile(outputPath, content, 'utf8'); return outputPath; } /** * Generate schema mapping RDF */ private async generateSchemaMappings(dataset: KGDataset): Promise { const templatePath = join(this.options.templatesDir, 'schema-mapping.ttl.njk'); const outputPath = join(this.options.outputDir, 'schema', `${dataset.domain}-schema.ttl`); await fs.mkdir(dirname(outputPath), { recursive: true }); const content = await this.renderer.renderTemplate(templatePath, { ...dataset, outputDir: this.options.outputDir, force: this.options.force, now: () => new Date(), formatDate: (date: Date | string, format?: string) => { const d = typeof date === 'string' ? new Date(date) : date; if (format === 'YYYY-MM-DD') { return d.toISOString().split('T')[0]; } return d.toISOString(); }, totalClasses: dataset.statistics?.totalClasses || 0, totalProperties: dataset.statistics?.totalProperties || 0 }); await fs.writeFile(outputPath, content, 'utf8'); return outputPath; } /** * Generate metadata and provenance RDF */ private async generateMetadata(dataset: KGDataset): Promise { const templatePath = join(this.options.templatesDir, 'graph-metadata.ttl.njk'); const outputPath = join(this.options.outputDir, 'metadata', `${dataset.domain}-metadata.ttl`); await fs.mkdir(dirname(outputPath), { recursive: true }); const content = await this.renderer.renderTemplate(templatePath, { ...dataset, outputDir: this.options.outputDir, force: this.options.force, now: () => new Date(), formatDate: (date: Date | string, format?: string) => { const d = typeof date === 'string' ? new Date(date) : date; if (format === 'YYYY-MM-DD') { return d.toISOString().split('T')[0]; } return d.toISOString(); }, startTime: new Date().toISOString(), endTime: new Date().toISOString(), agentName: 'Unjucks Knowledge Graph Pipeline', agentVersion: '1.0.0' }); await fs.writeFile(outputPath, content, 'utf8'); return outputPath; } /** * Generate SPARQL queries */ private async generateSPARQLQueries(dataset: KGDataset): Promise { const templatePath = join(this.options.templatesDir, 'sparql-queries.sparql.njk'); const outputPath = join(this.options.outputDir, 'queries', `${dataset.domain}-queries.sparql`); await fs.mkdir(dirname(outputPath), { recursive: true }); // Generate common queries based on dataset structure const basicQueries = [ { name: 'List All Entities', description: 'Retrieve all entities in the knowledge graph', variables: ['entity', 'name', 'type'], patterns: [ '?entity a ?type', '?entity schema:name ?name' ], filters: ['!isBlank(?entity)'], orderBy: '?name', limit: 100 }, { name: 'Find Entities by Type', description: 'Find all entities of a specific type', variables: ['entity', 'name'], patterns: [ '?entity a schema:{{ entityType || "Thing" }}', '?entity schema:name ?name' ], orderBy: '?name' } ]; const relationshipQueries = [ { name: 'Entity Relationships', description: 'Find all relationships for an entity', variables: ['subject', 'predicate', 'object', 'subjectLabel', 'objectLabel'], patterns: [ '?subject ?predicate ?object', 'FILTER(?subject = kg:{{ entityId || "entity_001" }} || ?object = kg:{{ entityId || "entity_001" }})' ], optional: [ '?subject rdfs:label ?subjectLabel', '?object rdfs:label ?objectLabel' ] } ]; const content = await this.renderer.renderTemplate(templatePath, { ...dataset, outputDir: this.options.outputDir, force: this.options.force, now: () => new Date(), formatDate: (date: Date | string, format?: string) => { const d = typeof date === 'string' ? new Date(date) : date; if (format === 'YYYY-MM-DD') { return d.toISOString().split('T')[0]; } return d.toISOString(); }, basicQueries, relationshipQueries, searchPattern: '.*', relationshipLimit: 100, centralityLimit: 20, performanceLimit: 50, startYear: 2020, endYear: new Date().getFullYear() }); await fs.writeFile(outputPath, content, 'utf8'); return outputPath; } /** * Calculate knowledge graph statistics */ private async calculateStatistics(files: string[]): Promise<{ totalTriples: number; totalEntities: number; totalFiles: number; totalSize: number; }> { let totalTriples = 0; let totalEntities = 0; let totalSize = 0; for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); const stats = await fs.stat(file); totalSize += stats.size; // Rough estimation of triples by counting lines with '.' const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('@') && trimmed.endsWith('.')) { totalTriples++; } } // Rough estimation of entities by counting unique URIs const uriMatches = content.match(/kg:\w+/g) || []; const uniqueUris = new Set(uriMatches); totalEntities += uniqueUris.size; } catch (error) { console.warn(`Could not process file ${file}: ${error.message}`); } } return { totalTriples: Math.max(totalTriples, 1), // Avoid zero totalEntities: Math.max(totalEntities, 1), totalFiles: files.length, totalSize }; } /** * Validate knowledge graph using SHACL */ private async validateKnowledgeGraph(files: string[]): Promise { const startTime = Date.now(); const errors: string[] = []; const warnings: string[] = []; try { // Basic syntax validation for RDF files for (const file of files) { const content = await fs.readFile(file, 'utf8'); // Check for basic RDF syntax issues const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Skip comments and empty lines if (!line || line.startsWith('#') || line.startsWith('@prefix')) { continue; } // Check for incomplete triples if (line.includes('?') && !line.includes('FILTER')) { warnings.push(`Possible incomplete triple at line ${i + 1} in ${file}`); } // Check for invalid URIs if (line.includes('<') && line.includes('>')) { const uriMatch = line.match(/<([^>]+)>/); if (uriMatch) { const uri = uriMatch[1]; if (!this.isValidURI(uri)) { errors.push(`Invalid URI at line ${i + 1} in ${file}: ${uri}`); } } } } } // Calculate statistics const statistics = await this.calculateStatistics(files); const validationTime = Date.now() - startTime; return { valid: errors.length === 0, errors, warnings, statistics: { totalTriples: statistics.totalTriples, totalEntities: statistics.totalEntities, validationTime } }; } catch (error) { errors.push(`Validation failed: ${error.message}`); return { valid: false, errors, warnings, statistics: { totalTriples: 0, totalEntities: 0, validationTime: Date.now() - startTime } }; } } /** * Basic URI validation */ private isValidURI(uri: string): boolean { try { new URL(uri); return true; } catch { // Check for relative URIs or known URI schemes return uri.includes(':') && !uri.includes(' '); } } /** * Convert knowledge graph to different RDF formats */ async convertFormat(inputFile: string, outputFormat: 'turtle' | 'rdfxml' | 'jsonld' | 'ntriples'): Promise { // This is a placeholder for RDF format conversion // In a real implementation, you would use libraries like: // - rdflib.js // - N3.js // - jsonld.js const inputContent = await fs.readFile(inputFile, 'utf8'); const outputFile = inputFile.replace(/\.[^.]+$/, `.${this.getFileExtension(outputFormat)}`); // For now, just copy the content (turtle format) if (outputFormat === 'turtle') { await fs.writeFile(outputFile, inputContent, 'utf8'); } else { // Placeholder conversion const convertedContent = `# Converted to ${outputFormat} format\n${inputContent}`; await fs.writeFile(outputFile, convertedContent, 'utf8'); } return outputFile; } /** * Get file extension for RDF format */ private getFileExtension(format: string): string { const extensions: Record = { turtle: 'ttl', rdfxml: 'rdf', jsonld: 'jsonld', ntriples: 'nt' }; return extensions[format] || 'ttl'; } /** * Generate knowledge graph quality report */ async generateQualityReport(dataset: KGDataset, validationResult: KGValidationResult): Promise { const reportPath = join(this.options.outputDir, 'reports', `${dataset.domain}-quality-report.json`); await fs.mkdir(dirname(reportPath), { recursive: true }); const qualityReport = { dataset: { domain: dataset.domain, version: dataset.version, generatedAt: new Date().toISOString() }, statistics: validationResult.statistics, validation: { valid: validationResult.valid, errorCount: validationResult.errors.length, warningCount: validationResult.warnings.length, errors: validationResult.errors, warnings: validationResult.warnings }, qualityMetrics: dataset.qualityMetrics, recommendations: this.generateQualityRecommendations(dataset, validationResult) }; await fs.writeFile(reportPath, JSON.stringify(qualityReport, null, 2), 'utf8'); return reportPath; } /** * Generate quality recommendations */ private generateQualityRecommendations(dataset: KGDataset, validation: KGValidationResult): string[] { const recommendations: string[] = []; if (validation.errors.length > 0) { recommendations.push('Fix validation errors to improve data quality'); } if (dataset.qualityMetrics) { const { completeness, accuracy, consistency } = dataset.qualityMetrics; if (completeness?.overall < 0.8) { recommendations.push('Improve data completeness by filling missing values'); } if (accuracy?.overall < 0.9) { recommendations.push('Enhance data accuracy through better validation rules'); } if (consistency?.overall < 0.85) { recommendations.push('Address consistency issues in data representation'); } } if (dataset.entities && dataset.entities.length > 0) { const entitiesWithoutTypes = dataset.entities.filter(e => !e.type); if (entitiesWithoutTypes.length > 0) { recommendations.push(`Add type information for ${entitiesWithoutTypes.length} entities`); } } return recommendations; } }