brainy/src/augmentations/intelligentImport/IntelligentImportAugmentation.ts
David Snelling 1874b77896 feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.

**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes

**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()

**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults

**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests

**Breaking Changes:** None - backward compatible

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00

227 lines
6.9 KiB
TypeScript

/**
* Intelligent Import Augmentation
*
* Automatically detects and processes CSV, Excel, and PDF files with:
* - Format detection and routing
* - Lazy-loaded handlers
* - Intelligent entity and relationship extraction
* - Integration with NeuralImport augmentation
*/
import { BaseAugmentation, AugmentationContext } from '../brainyAugmentation.js'
import { FormatHandler, IntelligentImportConfig, ProcessedData } from './types.js'
import { CSVHandler } from './handlers/csvHandler.js'
import { ExcelHandler } from './handlers/excelHandler.js'
import { PDFHandler } from './handlers/pdfHandler.js'
import { ImageHandler } from './handlers/imageHandler.js'
export class IntelligentImportAugmentation extends BaseAugmentation {
readonly name = 'intelligent-import'
readonly timing = 'before' as const
readonly metadata = {
reads: '*' as '*',
writes: ['_intelligentImport', '_processedFormat', '_extractedData'] as string[]
}
readonly operations = ['import', 'importFile', 'importFromFile', 'importFromURL', 'all'] as any[]
readonly priority = 75 // Before NeuralImport (80), after validation
protected config: IntelligentImportConfig
private handlers: Map<string, FormatHandler> = new Map()
private initialized = false
constructor(config: Partial<IntelligentImportConfig> = {}) {
super(config)
this.config = {
enableCSV: true,
enableExcel: true,
enablePDF: true,
enableImage: true, // v5.2.0: Image handler enabled by default
maxFileSize: 100 * 1024 * 1024, // 100MB default
enableCache: true,
cacheTTL: 24 * 60 * 60 * 1000, // 24 hours
...config
}
}
protected async onInitialize(): Promise<void> {
// Initialize handlers based on config
if (this.config.enableCSV) {
this.handlers.set('csv', new CSVHandler())
}
if (this.config.enableExcel) {
this.handlers.set('excel', new ExcelHandler())
}
if (this.config.enablePDF) {
this.handlers.set('pdf', new PDFHandler())
}
if (this.config.enableImage) {
this.handlers.set('image', new ImageHandler())
}
this.initialized = true
this.log(`Initialized with ${this.handlers.size} format handlers (CSV: ${this.config.enableCSV}, Excel: ${this.config.enableExcel}, PDF: ${this.config.enablePDF}, Image: ${this.config.enableImage})`)
}
async execute<T = any>(
operation: string,
params: any,
next: () => Promise<T>
): Promise<T> {
// Only process import operations
if (!this.shouldProcess(operation, params)) {
return next()
}
try {
// Extract file data from params
const fileData = this.extractFileData(params)
if (!fileData) {
return next()
}
// Check file size limit
if (this.config.maxFileSize && fileData.data.length > this.config.maxFileSize) {
this.log(`File too large (${fileData.data.length} bytes), skipping intelligent import`, 'warn')
return next()
}
// Detect format and get appropriate handler
const handler = this.detectHandler(fileData.data, fileData.filename)
if (!handler) {
// Not a supported format, pass through
return next()
}
this.log(`Processing ${fileData.filename || 'file'} with ${handler.format} handler`)
// Process the file
const processed = await handler.process(fileData.data, {
filename: fileData.filename,
ext: fileData.ext,
...this.config.csvDefaults,
...this.config.excelDefaults,
...this.config.pdfDefaults,
...this.config.imageDefaults,
...params.options
})
// Enrich params with processed data
params._intelligentImport = true
params._processedFormat = processed.format
params._extractedData = processed.data
params._metadata = {
...params._metadata,
intelligentImport: processed.metadata
}
// If this is an import operation, transform params to include the structured data
if (processed.data.length > 0) {
// Store processed data for the neural import augmentation to use
params.data = processed.data
params.metadata = params._metadata
}
this.log(`Extracted ${processed.data.length} items from ${processed.format} file`)
return next()
} catch (error) {
this.log(`Intelligent import processing failed: ${error instanceof Error ? error.message : String(error)}`, 'warn')
// Fall through to normal import on error
return next()
}
}
/**
* Check if we should process this operation
*/
private shouldProcess(operation: string, params: any): boolean {
// Only process if we have handlers initialized
if (!this.initialized || this.handlers.size === 0) {
return false
}
// Check operation type
const validOps = ['import', 'importFile', 'importFromFile', 'importFromURL']
if (!validOps.some(op => operation.includes(op))) {
return false
}
// Must have some data
if (!params || (!params.source && !params.data && !params.filePath && !params.url)) {
return false
}
return true
}
/**
* Extract file data from various param formats
*/
private extractFileData(params: any): { data: Buffer, filename?: string, ext?: string } | null {
// From source parameter
if (params.source) {
if (Buffer.isBuffer(params.source)) {
return { data: params.source, filename: params.filename }
}
if (typeof params.source === 'string') {
return { data: Buffer.from(params.source), filename: params.filename }
}
}
// From data parameter
if (params.data) {
if (Buffer.isBuffer(params.data)) {
return { data: params.data, filename: params.filename }
}
if (typeof params.data === 'string') {
return { data: Buffer.from(params.data), filename: params.filename }
}
}
// From file path (would need to read - but that should be handled by UniversalImportAPI)
if (params.filePath && typeof params.filePath === 'string') {
const ext = params.filePath.split('.').pop()
return null // File reading handled elsewhere
}
return null
}
/**
* Detect which handler can process this file
*/
private detectHandler(data: Buffer, filename?: string): FormatHandler | null {
// Try each handler's canHandle method
for (const handler of this.handlers.values()) {
if (handler.canHandle(data) || (filename && handler.canHandle({ filename }))) {
return handler
}
}
return null
}
/**
* Get handler by format name
*/
getHandler(format: string): FormatHandler | undefined {
return this.handlers.get(format.toLowerCase())
}
/**
* Get all registered handlers
*/
getHandlers(): FormatHandler[] {
return Array.from(this.handlers.values())
}
/**
* Get supported formats
*/
getSupportedFormats(): string[] {
return Array.from(this.handlers.keys())
}
}