This repository has been archived on 2026-09-03. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
open-brainy/src/importers/SmartYAMLImporter.ts
David Snelling 364360d447 fix: exclude __words__ keyword index from corruption detection and getStats()
The __words__ keyword index stores 50-5000 entries per entity (one per
word), which inflated avg entries/entity well above the corruption
threshold of 100. This caused:

1. validateConsistency() to falsely detect corruption on every startup,
   triggering unnecessary clearAllIndexData() + rebuild() cycles
2. getStats() to log false "Metadata index may be corrupted" warnings
   and report inflated totalEntries/totalIds stats

Both methods now skip __words__ when counting, so stats and health
checks reflect metadata fields only (noun, type, createdAt, etc.).
Keyword search is unaffected since the __words__ field index itself
is not modified.
2026-01-27 15:38:21 -08:00

474 lines
13 KiB
TypeScript

/**
* Smart YAML Importer
*
* Extracts entities and relationships from YAML files using:
* - YAML parsing to JSON-like structure
* - Recursive traversal of nested structures
* - NeuralEntityExtractor for entity extraction from text values
* - NaturalLanguageProcessor for relationship inference
* - Hierarchical relationship creation (parent-child, contains, etc.)
*
* New format handler
* NO MOCKS - Production-ready implementation
*/
import { Brainy } from '../brainy.js'
import { NeuralEntityExtractor, ExtractedEntity } from '../neural/entityExtractor.js'
import { NaturalLanguageProcessor } from '../neural/naturalLanguageProcessor.js'
import { SmartRelationshipExtractor } from '../neural/SmartRelationshipExtractor.js'
import { NounType, VerbType } from '../types/graphTypes.js'
import * as yaml from 'js-yaml'
export interface SmartYAMLOptions {
/** Enable neural entity extraction from string values */
enableNeuralExtraction?: boolean
/** Enable hierarchical relationship creation */
enableHierarchicalRelationships?: boolean
/** Enable concept extraction for tagging */
enableConceptExtraction?: boolean
/** Confidence threshold for entities (0-1) */
confidenceThreshold?: number
/** Maximum depth to traverse */
maxDepth?: number
/** Minimum string length to process for entity extraction */
minStringLength?: number
/** Keys that indicate entity names */
nameKeys?: string[]
/** Keys that indicate entity descriptions */
descriptionKeys?: string[]
/** Keys that indicate entity types */
typeKeys?: string[]
/** Progress callback */
onProgress?: (stats: {
processed: number
entities: number
relationships: number
}) => void
}
export interface ExtractedYAMLEntity {
/** Entity ID */
id: string
/** Entity name */
name: string
/** Entity type */
type: NounType
/** Entity description/value */
description: string
/** Confidence score */
confidence: number
/** Weight/importance score */
weight?: number
/** YAML path to this entity */
path: string
/** Parent path in YAML hierarchy */
parentPath: string | null
/** Metadata */
metadata: Record<string, any>
}
export interface ExtractedYAMLRelationship {
from: string
to: string
type: VerbType
confidence: number
weight?: number
evidence: string
}
export interface SmartYAMLResult {
/** Total nodes processed */
nodesProcessed: number
/** Entities extracted */
entitiesExtracted: number
/** Relationships inferred */
relationshipsInferred: number
/** All extracted entities */
entities: ExtractedYAMLEntity[]
/** All relationships */
relationships: ExtractedYAMLRelationship[]
/** Entity ID mapping (path -> ID) */
entityMap: Map<string, string>
/** Processing time in ms */
processingTime: number
/** Extraction statistics */
stats: {
byType: Record<string, number>
byDepth: Record<number, number>
byConfidence: {
high: number // > 0.8
medium: number // 0.6-0.8
low: number // < 0.6
}
}
}
/**
* SmartYAMLImporter - Extracts structured knowledge from YAML files
*/
export class SmartYAMLImporter {
private brain: Brainy
private extractor: NeuralEntityExtractor
private nlp: NaturalLanguageProcessor
private relationshipExtractor: SmartRelationshipExtractor
constructor(brain: Brainy) {
this.brain = brain
this.extractor = new NeuralEntityExtractor(brain)
this.nlp = new NaturalLanguageProcessor(brain)
this.relationshipExtractor = new SmartRelationshipExtractor(brain)
}
/**
* Initialize the importer
*/
async init(): Promise<void> {
await this.nlp.init()
}
/**
* Extract entities and relationships from YAML string or buffer
*/
async extract(
yamlContent: string | Buffer,
options: SmartYAMLOptions = {}
): Promise<SmartYAMLResult> {
const startTime = Date.now()
// Report parsing start
options.onProgress?.({
processed: 0,
entities: 0,
relationships: 0
})
// Parse YAML to JavaScript object
const yamlString = typeof yamlContent === 'string'
? yamlContent
: yamlContent.toString('utf-8')
let data: any
try {
data = yaml.load(yamlString)
} catch (error: any) {
throw new Error(`Failed to parse YAML: ${error.message}`)
}
// Report parsing complete
options.onProgress?.({
processed: 0,
entities: 0,
relationships: 0
})
// Process as JSON-like structure
const result = await this.extractFromData(data, options)
result.processingTime = Date.now() - startTime
return result
}
/**
* Extract entities and relationships from parsed YAML data
*/
private async extractFromData(
data: any,
options: SmartYAMLOptions
): Promise<SmartYAMLResult> {
const opts = {
enableNeuralExtraction: options.enableNeuralExtraction !== false,
enableHierarchicalRelationships: options.enableHierarchicalRelationships !== false,
enableConceptExtraction: options.enableConceptExtraction !== false,
confidenceThreshold: options.confidenceThreshold || 0.6,
maxDepth: options.maxDepth || 10,
minStringLength: options.minStringLength || 3,
nameKeys: options.nameKeys || ['name', 'title', 'label', 'id'],
descriptionKeys: options.descriptionKeys || ['description', 'desc', 'summary', 'value'],
typeKeys: options.typeKeys || ['type', 'kind', 'category'],
onProgress: options.onProgress
}
const entities: ExtractedYAMLEntity[] = []
const relationships: ExtractedYAMLRelationship[] = []
const entityMap = new Map<string, string>()
let nodesProcessed = 0
const stats = {
byType: {} as Record<string, number>,
byDepth: {} as Record<number, number>,
byConfidence: { high: 0, medium: 0, low: 0 }
}
// Traverse YAML structure recursively
const traverse = async (
obj: any,
path: string = '$',
depth: number = 0,
parentPath: string | null = null
): Promise<void> => {
if (depth > opts.maxDepth) return
nodesProcessed++
stats.byDepth[depth] = (stats.byDepth[depth] || 0) + 1
// Report progress
if (options.onProgress && nodesProcessed % 10 === 0) {
options.onProgress({
processed: nodesProcessed,
entities: entities.length,
relationships: relationships.length
})
}
// Handle different value types
if (obj === null || obj === undefined) {
return
}
// Handle arrays
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
await traverse(obj[i], `${path}[${i}]`, depth + 1, path)
}
return
}
// Handle objects
if (typeof obj === 'object') {
// Extract entity from object
const entity = await this.extractEntityFromObject(
obj,
path,
parentPath,
depth,
opts
)
if (entity) {
entities.push(entity)
entityMap.set(path, entity.id)
// Update stats
stats.byType[entity.type] = (stats.byType[entity.type] || 0) + 1
if (entity.confidence > 0.8) stats.byConfidence.high++
else if (entity.confidence >= 0.6) stats.byConfidence.medium++
else stats.byConfidence.low++
// Create hierarchical relationship
if (opts.enableHierarchicalRelationships && parentPath) {
const parentId = entityMap.get(parentPath)
if (parentId) {
// Extract parent name from path for better context
const parentName = parentPath.split('.').pop()?.replace(/\[(\d+)\]/, 'item $1') || 'parent'
const childName = entity.name
// Infer relationship type using SmartRelationshipExtractor
const context = `Hierarchical YAML structure: ${parentName} contains ${childName}. Parent path: ${parentPath}, Child path: ${entity.path}`
const inferredRelationship = await this.relationshipExtractor.infer(
parentName,
childName,
context,
{
objectType: entity.type // Pass child entity type as hint
}
)
relationships.push({
from: parentId,
to: entity.id,
type: inferredRelationship?.type || VerbType.Contains, // Fallback to Contains for hierarchical relationships
confidence: inferredRelationship?.confidence || 0.9,
weight: inferredRelationship?.weight || 1.0,
evidence: inferredRelationship?.evidence || 'Hierarchical parent-child relationship in YAML structure'
})
}
}
}
// Traverse nested objects
for (const [key, value] of Object.entries(obj)) {
await traverse(value, `${path}.${key}`, depth + 1, path)
}
return
}
// Handle primitive values (strings, numbers, booleans)
if (typeof obj === 'string' && obj.length >= opts.minStringLength) {
// Extract entities from string values
if (opts.enableNeuralExtraction) {
const extractedEntities = await this.extractor.extract(obj, {
confidence: opts.confidenceThreshold
})
for (const extracted of extractedEntities) {
const entityId = `${path}:${extracted.text}`
const entity: ExtractedYAMLEntity = {
id: entityId,
name: extracted.text,
type: extracted.type,
description: obj,
confidence: extracted.confidence,
weight: extracted.weight || 1.0,
path,
parentPath,
metadata: {
position: extracted.position,
extractedFrom: 'string-value'
}
}
entities.push(entity)
entityMap.set(entityId, entityId)
// Update stats
stats.byType[entity.type] = (stats.byType[entity.type] || 0) + 1
if (entity.confidence > 0.8) stats.byConfidence.high++
else if (entity.confidence >= 0.6) stats.byConfidence.medium++
else stats.byConfidence.low++
}
}
}
}
// Start traversal
await traverse(data)
// Final progress report
if (options.onProgress) {
options.onProgress({
processed: nodesProcessed,
entities: entities.length,
relationships: relationships.length
})
}
return {
nodesProcessed,
entitiesExtracted: entities.length,
relationshipsInferred: relationships.length,
entities,
relationships,
entityMap,
processingTime: 0, // Will be set by caller
stats
}
}
/**
* Extract an entity from a YAML object node
*/
private async extractEntityFromObject(
obj: any,
path: string,
parentPath: string | null,
depth: number,
opts: {
enableNeuralExtraction: boolean
enableHierarchicalRelationships: boolean
enableConceptExtraction: boolean
confidenceThreshold: number
maxDepth: number
minStringLength: number
nameKeys: string[]
descriptionKeys: string[]
typeKeys: string[]
onProgress?: (stats: { processed: number; entities: number; relationships: number }) => void
}
): Promise<ExtractedYAMLEntity | null> {
// Try to find name
let name: string | null = null
for (const key of opts.nameKeys) {
if (obj[key] && typeof obj[key] === 'string') {
name = obj[key]
break
}
}
// If no explicit name, use path segment
if (!name) {
const segments = path.split('.')
name = segments[segments.length - 1]
if (name === '$') name = 'root'
}
// Try to find description
let description = name
for (const key of opts.descriptionKeys) {
if (obj[key] && typeof obj[key] === 'string') {
description = obj[key]
break
}
}
// Try to find explicit type
let explicitType: string | null = null
for (const key of opts.typeKeys) {
if (obj[key] && typeof obj[key] === 'string') {
explicitType = obj[key]
break
}
}
// Classify entity type using SmartExtractor
const classification = await this.extractor.extract(description, {
confidence: opts.confidenceThreshold
})
const entityType = classification.length > 0
? classification[0].type
: NounType.Thing
const confidence = classification.length > 0
? classification[0].confidence
: 0.5
const weight = classification.length > 0
? classification[0].weight || 1.0
: 1.0
// Create entity
const entity: ExtractedYAMLEntity = {
id: path,
name,
type: entityType,
description,
confidence,
weight,
path,
parentPath,
metadata: {
depth,
explicitType,
yamlKeys: Object.keys(obj)
}
}
return entity
}
}