brainy/src/utils/intelligentTypeMapper.ts

393 lines
10 KiB
TypeScript
Raw Normal View History

/**
* Intelligent Type Mapper
* Maps generic/invalid type names to specific semantic types based on data analysis
* Prevents semantic degradation from overuse of generic types
*/
import { NounType, VerbType } from '../types/graphTypes.js'
/**
* Common aliases that users might use
*/
const GENERIC_ALIASES = new Set([
'entity',
'item',
'object',
'node',
'record',
'entry',
'data',
'resource'
])
/**
* Field signatures for type inference
*/
const TYPE_SIGNATURES = {
// Person indicators
person: {
required: [],
indicators: ['email', 'firstName', 'lastName', 'name', 'phone', 'username', 'userId'],
patterns: [/@/, /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i],
weight: 10
},
// User account indicators
user: {
required: [],
indicators: ['username', 'password', 'accountId', 'loginTime', 'permissions', 'role'],
patterns: [],
weight: 9
},
// Organization indicators
organization: {
required: [],
indicators: ['companyName', 'orgName', 'ein', 'vatNumber', 'employees', 'headquarters'],
patterns: [],
weight: 8
},
// Product indicators
product: {
required: [],
indicators: ['price', 'sku', 'barcode', 'inventory', 'cost', 'productId', 'inStock'],
patterns: [/^\$?\d+\.?\d*$/, /^[A-Z0-9-]+$/],
weight: 8
},
// Document indicators
document: {
required: [],
indicators: ['content', 'text', 'body', 'title', 'author', 'markdown', 'html'],
patterns: [],
weight: 7
},
// Message indicators
message: {
required: [],
indicators: ['from', 'to', 'subject', 'body', 'sentAt', 'messageId', 'threadId'],
patterns: [],
weight: 7
},
// Task indicators
task: {
required: [],
indicators: ['dueDate', 'assignee', 'status', 'priority', 'completed', 'taskId'],
patterns: [],
weight: 7
},
// Event indicators
event: {
required: [],
indicators: ['startTime', 'endTime', 'date', 'location', 'attendees', 'eventType'],
patterns: [],
weight: 6
},
// Location indicators
location: {
required: [],
indicators: ['latitude', 'longitude', 'address', 'city', 'country', 'zipCode', 'coordinates'],
patterns: [/^-?\d+\.\d+$/, /^\d{5}(-\d{4})?$/],
weight: 6
},
// File indicators
file: {
required: [],
indicators: ['filename', 'filepath', 'extension', 'mimeType', 'fileSize', 'checksum'],
patterns: [/\.[a-z0-9]+$/i],
weight: 5
},
// Dataset indicators
dataset: {
required: [],
indicators: ['schema', 'rows', 'columns', 'records', 'dataType', 'format'],
patterns: [],
weight: 5
},
// Media indicators
media: {
required: [],
indicators: ['url', 'thumbnail', 'duration', 'resolution', 'codec', 'bitrate'],
patterns: [/\.(jpg|jpeg|png|gif|mp4|mp3|wav|avi)$/i],
weight: 5
},
// Project indicators
project: {
required: [],
indicators: ['deadline', 'budget', 'team', 'milestones', 'deliverables', 'projectId'],
patterns: [],
weight: 5
},
// Service indicators
service: {
required: [],
indicators: ['endpoint', 'apiKey', 'serviceUrl', 'port', 'protocol', 'healthCheck'],
patterns: [/^https?:\/\//, /:\d+$/],
weight: 4
}
}
/**
* Intelligent Type Mapper
*/
export class IntelligentTypeMapper {
private typeCache: Map<string, string> = new Map()
private inferenceStats = {
total: 0,
inferred: 0,
defaulted: 0,
cached: 0
}
/**
* Map a noun type, with intelligent inference for generic types
*/
mapNounType(inputType: string, data?: any): string {
// Check if it's already a valid type
if (this.isValidNounType(inputType)) {
return inputType
}
// Check cache for this exact input
const cacheKey = `${inputType}-${JSON.stringify(data || {}).substring(0, 100)}`
if (this.typeCache.has(cacheKey)) {
this.inferenceStats.cached++
return this.typeCache.get(cacheKey)!
}
this.inferenceStats.total++
// If it's a generic alias and we have data, try to infer
if (GENERIC_ALIASES.has(inputType.toLowerCase()) && data) {
const inferred = this.inferTypeFromData(data)
if (inferred) {
this.inferenceStats.inferred++
this.typeCache.set(cacheKey, inferred)
return inferred
}
}
// Handle specific common mappings
const directMapping = this.getDirectMapping(inputType)
if (directMapping) {
this.typeCache.set(cacheKey, directMapping)
return directMapping
}
// Default to 'thing' for truly unknown types
this.inferenceStats.defaulted++
const defaultType = NounType.Thing
this.typeCache.set(cacheKey, defaultType)
return defaultType
}
/**
* Map a verb type
*/
mapVerbType(inputType: string): string {
// Check if it's already valid
if (this.isValidVerbType(inputType)) {
return inputType
}
// Common verb mappings
const verbMappings: Record<string, string> = {
'related': VerbType.RelatedTo,
'relates': VerbType.RelatedTo,
'has': VerbType.Contains,
'includes': VerbType.Contains,
'belongsTo': VerbType.PartOf,
'in': VerbType.LocatedAt,
'at': VerbType.LocatedAt,
'references': VerbType.References,
'cites': VerbType.References,
'before': VerbType.Precedes,
'after': VerbType.Precedes,
'causes': VerbType.Causes,
'needs': VerbType.Requires,
'requires': VerbType.Requires,
'makes': VerbType.Creates,
'produces': VerbType.Creates,
'changes': VerbType.Modifies,
'updates': VerbType.Modifies,
'owns': VerbType.Owns,
'ownedBy': VerbType.Owns, // Use BelongsTo for reverse ownership
'uses': VerbType.Uses,
'usedBy': VerbType.Uses // Same relationship, just interpret direction
}
const normalized = inputType.toLowerCase()
return verbMappings[normalized] || VerbType.RelatedTo
}
/**
* Infer type from data structure
*/
private inferTypeFromData(data: any): string | null {
if (!data || typeof data !== 'object') return null
const scores: Map<string, number> = new Map()
const fields = Object.keys(data)
const values = Object.values(data)
// Calculate scores for each type based on field matches
for (const [type, signature] of Object.entries(TYPE_SIGNATURES)) {
let score = 0
// Check required fields
if (signature.required.length > 0) {
const hasRequired = signature.required.every(field => fields.includes(field))
if (!hasRequired) continue
score += signature.weight * 2
}
// Check indicator fields
for (const field of fields) {
if (signature.indicators.some(indicator =>
field.toLowerCase().includes(indicator.toLowerCase())
)) {
score += signature.weight
}
}
// Check value patterns
for (const value of values) {
if (typeof value === 'string' && signature.patterns.length > 0) {
for (const pattern of signature.patterns) {
if (pattern.test(value)) {
score += signature.weight / 2
break
}
}
}
}
if (score > 0) {
scores.set(type, score)
}
}
// Return the type with highest score
if (scores.size > 0) {
const sorted = Array.from(scores.entries()).sort((a, b) => b[1] - a[1])
return sorted[0][0]
}
// Fallback inference based on data structure
if (fields.includes('url') || fields.includes('href')) {
return NounType.Document
}
if (Array.isArray(data) || fields.includes('items') || fields.includes('elements')) {
return NounType.Collection
}
// Check if it looks like a process/workflow
if (fields.includes('steps') || fields.includes('stages')) {
return NounType.Process
}
return null
}
/**
* Get direct mapping for common aliases
*/
private getDirectMapping(inputType: string): string | null {
const mappings: Record<string, string> = {
// Specific mappings that aren't generic
'company': NounType.Organization,
'corp': NounType.Organization,
'business': NounType.Organization,
'employee': NounType.Person,
'staff': NounType.Person,
'customer': NounType.Person,
'client': NounType.Person,
'article': NounType.Document,
'post': NounType.Document,
'page': NounType.Document,
'image': NounType.Media,
'video': NounType.Media,
'audio': NounType.Media,
'photo': NounType.Media,
'place': NounType.Location,
'address': NounType.Location,
'country': NounType.Location,
'city': NounType.Location,
'todo': NounType.Task,
'job': NounType.Task,
'work': NounType.Task,
'meeting': NounType.Event,
'appointment': NounType.Event,
'conference': NounType.Event,
'folder': NounType.Collection,
'group': NounType.Collection,
'list': NounType.Collection,
'category': NounType.Collection
}
const normalized = inputType.toLowerCase()
return mappings[normalized] || null
}
/**
* Check if a type is valid
*/
private isValidNounType(type: string): boolean {
return Object.values(NounType).includes(type as any)
}
/**
* Check if a verb type is valid
*/
private isValidVerbType(type: string): boolean {
return Object.values(VerbType).includes(type as any)
}
/**
* Get inference statistics
*/
getStats() {
return {
...this.inferenceStats,
cacheSize: this.typeCache.size,
inferenceRate: this.inferenceStats.total > 0
? (this.inferenceStats.inferred / this.inferenceStats.total)
: 0
}
}
/**
* Clear the type cache
*/
clearCache(): void {
this.typeCache.clear()
}
}
// Singleton instance
export const typeMapper = new IntelligentTypeMapper()
/**
* Helper function for easy type mapping
*/
export function mapNounType(inputType: string, data?: any): string {
return typeMapper.mapNounType(inputType, data)
}
/**
* Helper function for verb mapping
*/
export function mapVerbType(inputType: string): string {
return typeMapper.mapVerbType(inputType)
}